Compare commits

...

34 Commits

Author SHA1 Message Date
Chris Wilson
9f5fceba2d Merge branch 'main' into player-tracker 2022-09-24 18:35:59 -04:00
CaitSith2
813ee5ee3b Factorio: Add explicit support for factory-levels mod. (#1050)
* Factorio: Add explicit support for factory-levels mod.

* Fix inconsistent space/tabs
2022-09-24 02:43:00 -07:00
Fabian Dill
be1158ad78 Windows: update VC Redistributable to 14.32.31332 from 14.29.30037 2022-09-22 08:46:48 +02:00
black-sliver
6d5ddf3cad MultiServer: allow using IDs for hints 2022-09-20 18:38:31 +02:00
black-sliver
809bda02d1 Test: item/location name must not be numeric 2022-09-20 18:38:16 +02:00
black-sliver
2d5ec6ce22 Doc: item/location name must not be numeric 2022-09-20 18:38:16 +02:00
black-sliver
a95d0ce9ef Doc: clarify requirements.txt in world api.md 2022-09-20 09:48:30 +02:00
alwaysintreble
267d9234e5 core: fix options with "random" as default value not generating (#1033)
* core: fix options with "random" as default value not generating

when option is missing from the player yaml,

Using this in #893 and tested there.

* remove if

* OptionSets default to frozenset so handle that

* range had some specific instances of assuming default as a valid value so change this here to call the from_any

* isinstance instead of type

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-09-19 22:40:15 +02:00
black-sliver
4686881566 WebHost: CustomServer: use defaultdicts
also change non_hintable to defaultdict in MultiServer and add some typing
2022-09-19 01:20:36 +02:00
SoldierofOrder
101dab0ea4 SC2: Add helpful feedback when failing to locate SC2 (#1032)
* SC2: The client now throws a descriptive error when ExecuteInfo.txt exists but is empty, and offers more helpful suggestions when the file doesn't exist.

* SC2: Replaced the new RuntimeError with a warning in the logger to keep things consistent.

* Removed communism

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-09-18 16:48:36 +02:00
Fabian Dill
c2d69cb05e Core: add generic interface to add ER data to hints (#1014) 2022-09-18 14:30:43 +02:00
black-sliver
58f66e0f42 autoworld: don't load files/folders starting with '.' (#1030)
* autoworld: don't load files/folders starting with '.'

The imports fail if the folder has a '.' in the name, with a somewhat obscure error, and adding a '.' in front of it is what a linux user might expect to use when disabling a world temporarily.

* autoworld: use tuple to filter .* and _*
2022-09-18 13:02:05 +02:00
Fabian Dill
0215e1fa28 SC2: always show uncollected locations (#1007) 2022-09-18 12:40:35 +02:00
black-sliver
1c0a93acad doc: update use of relative/absolute imports
it matters for apworlds to function
2022-09-18 10:22:17 +02:00
NewSoupVi
4fcde135e5 The Witness: Renaming, Options, Logic Fixes (#1000)
Fixes to postgame detection for "shuffle_postgame"
Renamed many locations to be symbol-independent ("Outside Tutorial Dots Introduction" becomes "Outside Tutorial Shed Row"). This is to set up future alternate modes, like Sigma Expert, which use completely different symbols.
Renamed most door items to be shorter, more consistent, and less... stupid. ("Bunker Bunker Entry Door" -> "Bunker Entry")
Removed "shuffle_uncommon"
Many logic fixes
2022-09-18 04:20:59 +02:00
alwaysintreble
332dde154f core: new freetext and textchoice options (#728)
* add freetext and freetextchoice options

* fix textchoice. create plando_bosses bool so worlds can check if boss plando is enabled

* remove strange unneccessary \ escapes

* lttp: rip boss plando out of core

* fix broken text methods so they read the data correctly

* revert `None` key in boss_shuffle_options. fix failing tests

* lttp: rewrite boss plando

* lttp: rewrite boss shuffle

* add generic verification step and allow options to set a plando module

* add default typing to plando_options set

* use PlandoSettings intflag for lttp boss plando

* fix plandosettings boss flag check

* minor lttp init cleanup

* make suggested changes. account for "random" existing within plando boss options

* override eq operator

* Please document me!

* Forgot to mention it supports plando

* remove auto_display_name

* Throw warning alerting user to which shuffle is being used if plando is off. Set the remaining boss shuffle in init and boss placement cleanup

* move the convoluted string matching to `from_text`

* remove unneccessary text lowering and actually turn off plando option when it's disabled

* typing

* strong typing for verify method and reorder

* typing is your friend

* log warning correctly

* 3.8 support :(

* also list apparently

* rip out old boss shuffle spoiler code

* verification step for plando bosses and locations

* update plando guide to reference new supported behavior

* empty string is not `None`. remove unneccessary error throw

* Fix bad ordering

* validate boss_shuffle only contains a normal boss option at the end

* get random choice from a list dummy

* >:(

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* minor textchoice cleanup

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-09-17 02:55:33 +02:00
black-sliver
8d51205e8f Alttp: only check item.type for own items with retro_cave 2022-09-17 02:25:09 +02:00
black-sliver
ff05e9d7d5 MultiServer: produce nicer output ...
... for headless and when cancelling the file open dialog
2022-09-17 02:24:51 +02:00
N00byKing
516a52c041 sm64ex: Fix WDW 1Up Block Logic 2022-09-17 02:13:32 +02:00
Alchav
9daa64741b New, smarter fast_fill function (#646)
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2022-09-17 02:06:25 +02:00
Fabian Dill
af11fa5150 Core: auto alias (#1022)
* Test: check that default templates can be parsed into Option objects
2022-09-16 00:32:30 +02:00
Chris Wilson
e9e5511583 Merge branch 'main' into player-tracker 2022-08-17 21:48:40 -04:00
Chris Wilson
c546dcd5ff Fix merge conflicts into player-tracker 2022-08-15 21:37:44 -04:00
alwaysintreble
053fb14495 rename variables to fix invalid int loading (#858) 2022-08-03 21:36:26 -04:00
Chris Wilson
ed77d14618 PEP8 Fix 2022-08-03 21:34:59 -04:00
Chris Wilson
3fb287e82b Fix a bug causing the stylized tracker link to point to the wrong player 2022-08-03 19:54:25 -04:00
Chris Wilson
32431cfe04 Merge branch 'main' into player-tracker 2022-08-03 19:15:10 -04:00
Chris Wilson
ca8f4c38ec Merge branch 'main' into player-tracker 2022-07-31 11:17:59 -04:00
Chris Wilson
eb52454ccc Merge branch 'main' into player-tracker 2022-07-31 11:13:14 -04:00
Chris Wilson
14e5f54f59 Merge branch 'main' into player-tracker 2022-07-26 17:19:00 -04:00
Chris Wilson
2052cc55af Merge branch 'main' into player-tracker 2022-07-18 20:06:04 -04:00
Chris Wilson
63a8436240 Merge branch 'main' into player-tracker 2022-07-12 20:03:32 -04:00
Chris Wilson
e60719a20a Merge branch 'main' into player-tracker 2022-06-27 19:19:27 -04:00
alwaysintreble
8742aadc72 Player tracker (#710)
* Player tracker: implement a stylized tracker (#447)

* Move generic tracker to a WebWorld method

* render both a generic tracker at generic_tracker and the specific tracker at /tracker

* create a base template for generic specific tracker and instantiate some information before callng it

* some baseline for the playerTracker.html. update information fed from tracker.py

* playerTracker: finish implementing icons and generic locations rendering. hide any unacquired progression items when not using icons. Place the name of the progression item under its icon.

* player tracker: starting work on regions table

* player tracker: change method calls

* Move generic tracker to a WebWorld method

* render both a generic tracker at generic_tracker and the specific tracker at /tracker

* create a base template for generic specific tracker and instantiate some information before callng it

* some baseline for the playerTracker.html. update information fed from tracker.py

* playerTracker: finish implementing icons and generic locations rendering. hide any unacquired progression items when not using icons. Place the name of the progression item under its icon.

* player tracker: starting work on regions table

* player tracker: change method calls

* Move generic tracker to a WebWorld method

* create a base template for generic specific tracker and instantiate some information before callng it

* some baseline for the playerTracker.html. update information fed from tracker.py

* playerTracker: finish implementing icons and generic locations rendering. hide any unacquired progression items when not using icons. Place the name of the progression item under its icon.

* player tracker: starting work on regions table

* player tracker: switch item, icon and location tables to flex views. Some styling based on theme

* Player Tracker: Finish building html template for all blocks. Set groundwork for theme styling

* Player Tracker: Implement tracker class. Document tracker usage.

* Player Tracker: Add button to switch between trackers. Some styling for styled tracker.

* Player Tracker: reword some text. Attempt to fix page refreshing.

* Player Tracker: reremove the TODOs that got merged back in accidentally.

* player tracker: move render_template import to webworld so it isn't required outside of webhost

* Player Tracker: code cleanup, typing. Add inventory with names to PlayerTracker class in case custom trackers want to use it to change their prog_items attribute.

* Player Tracker: delete a line I forgot about. Add typing to theme.

* Player Tracker: Generate checks_done automatically so worlds don't have to do it

* Player Tracker: Add typing to PlayerTracker class in webworld method. Update documentation

* Player Tracker: code cleanup

* Player Tracker: Sort of implement fetch (works but could be better). Make playerTracker.html more readable.

* specific trackers: significant html cleanup. DOM Endpoint auto updating page every 30 seconds

* Changes by Kono

* specific trackers: cache and only load the data once every minute

* specific tracker: allow for one icon placement to be used for multiple items.

* Player tracker fixes/updates (#635)

* Move generic tracker to a WebWorld method

* render both a generic tracker at generic_tracker and the specific tracker at /tracker

* create a base template for generic specific tracker and instantiate some information before callng it

* some baseline for the playerTracker.html. update information fed from tracker.py

* playerTracker: finish implementing icons and generic locations rendering. hide any unacquired progression items when not using icons. Place the name of the progression item under its icon.

* player tracker: starting work on regions table

* player tracker: change method calls

* Move generic tracker to a WebWorld method

* render both a generic tracker at generic_tracker and the specific tracker at /tracker

* create a base template for generic specific tracker and instantiate some information before callng it

* some baseline for the playerTracker.html. update information fed from tracker.py

* playerTracker: finish implementing icons and generic locations rendering. hide any unacquired progression items when not using icons. Place the name of the progression item under its icon.

* player tracker: starting work on regions table

* player tracker: change method calls

* Move generic tracker to a WebWorld method

* create a base template for generic specific tracker and instantiate some information before callng it

* some baseline for the playerTracker.html. update information fed from tracker.py

* playerTracker: finish implementing icons and generic locations rendering. hide any unacquired progression items when not using icons. Place the name of the progression item under its icon.

* player tracker: starting work on regions table

* player tracker: switch item, icon and location tables to flex views. Some styling based on theme

* Player Tracker: Finish building html template for all blocks. Set groundwork for theme styling

* Player Tracker: Implement tracker class. Document tracker usage.

* Player Tracker: Add button to switch between trackers. Some styling for styled tracker.

* Player Tracker: reword some text. Attempt to fix page refreshing.

* Player Tracker: reremove the TODOs that got merged back in accidentally.

* player tracker: move render_template import to webworld so it isn't required outside of webhost

* Player Tracker: code cleanup, typing. Add inventory with names to PlayerTracker class in case custom trackers want to use it to change their prog_items attribute.

* Player Tracker: delete a line I forgot about. Add typing to theme.

* Player Tracker: Generate checks_done automatically so worlds don't have to do it

* Player Tracker: Add typing to PlayerTracker class in webworld method. Update documentation

* Player Tracker: code cleanup

* Player Tracker: Sort of implement fetch (works but could be better). Make playerTracker.html more readable.

* specific trackers: significant html cleanup. DOM Endpoint auto updating page every 30 seconds

* Changes by Kono

* specific trackers: cache and only load the data once every minute

* specific tracker: allow for one icon placement to be used for multiple items.

* lttp: move tracker to new format. will need more modification to generic solution to handle region keys tracking. likely a new html template that inherits the current

* lttp: fix broken icons rendering, add in progressive mail that i forgor. reorder some icons

* tracker: fix non edited trackers being broken from changes.

* tracker: move theme application before modify method so trackers can use a different theme than the world if desired.

* tracker: starting work on key tracking.

* tracker: styling and cleanup by Farrak

* tracker: styling and cleanup by Farrak

* tracker: styling and cleanup of playerTracker.html

* Revert playerTracker.html

* trackers: rename some files for clarity. move trackers into their own subdirectory

* small tracker.py cleanup

* move minecraft tracker to new system

* add item link attributing from upstream

* change getPlayerTracker to get_player_tracker. refactor broken linkings

* refactor styling files to trackers folders

* fix broken image in minecraft tracker. move oot tracker to new system

* clean up my oot nightmare

* rename lttpKeysTracker to zeldaKeysTracker. Move oot to keys tracker

* implement zeldaKeysTracker.js. fix table locations hiding/showing
2022-06-25 17:01:42 -04:00
79 changed files with 3265 additions and 2227 deletions

View File

@@ -955,6 +955,13 @@ class Region:
return True return True
return False 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): def __repr__(self):
return self.__str__() return self.__str__()
@@ -1422,7 +1429,6 @@ class Spoiler():
"f" in self.world.shop_shuffle[player])) "f" in self.world.shop_shuffle[player]))
outfile.write('Custom Potion Shop: %s\n' % outfile.write('Custom Potion Shop: %s\n' %
bool_to_text("w" in self.world.shop_shuffle[player])) 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 health: %s\n' % self.world.enemy_health[player])
outfile.write('Enemy damage: %s\n' % self.world.enemy_damage[player]) outfile.write('Enemy damage: %s\n' % self.world.enemy_damage[player])
outfile.write('Prize shuffle %s\n' % outfile.write('Prize shuffle %s\n' %

150
Fill.py
View File

@@ -136,33 +136,98 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
itempool.extend(unplaced_items) 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: def distribute_items_restrictive(world: MultiWorld) -> None:
fill_locations = sorted(world.get_unfilled_locations()) fill_locations = sorted(world.get_unfilled_locations())
world.random.shuffle(fill_locations) world.random.shuffle(fill_locations)
# get items to distribute # get items to distribute
itempool = sorted(world.itempool) itempool = sorted(world.itempool)
world.random.shuffle(itempool) world.random.shuffle(itempool)
progitempool: typing.List[Item] = [] progitempool: typing.List[Item] = []
nonexcludeditempool: typing.List[Item] = [] usefulitempool: typing.List[Item] = []
localrestitempool: typing.Dict[int, typing.List[Item]] = {player: [] for player in range(1, world.players + 1)} filleritempool: typing.List[Item] = []
nonlocalrestitempool: typing.List[Item] = []
restitempool: typing.List[Item] = []
for item in itempool: for item in itempool:
if item.advancement: if item.advancement:
progitempool.append(item) progitempool.append(item)
elif item.useful: # this only gets nonprogression items which should not appear in excluded locations elif item.useful:
nonexcludeditempool.append(item) usefulitempool.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)
else: else:
restitempool.append(item) filleritempool.append(item)
call_all(world, "fill_hook", progitempool, nonexcludeditempool, call_all(world, "fill_hook", progitempool, usefulitempool, filleritempool, fill_locations)
localrestitempool, nonlocalrestitempool, restitempool, fill_locations)
locations: typing.Dict[LocationProgressType, typing.List[Location]] = { locations: typing.Dict[LocationProgressType, typing.List[Location]] = {
loc_type: [] for loc_type in LocationProgressType} loc_type: [] for loc_type in LocationProgressType}
@@ -184,50 +249,16 @@ def distribute_items_restrictive(world: MultiWorld) -> None:
raise FillError( raise FillError(
f'Not enough locations for progress items. There are {len(progitempool)} more items than locations') f'Not enough locations for progress items. There are {len(progitempool)} more items than locations')
if nonexcludeditempool: remaining_fill(world, excludedlocations, filleritempool)
world.random.shuffle(defaultlocations) if excludedlocations:
# needs logical fill to not conflict with local items raise FillError(
fill_restrictive( f"Not enough filler items for excluded locations. There are {len(excludedlocations)} more locations than items")
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')
defaultlocations = defaultlocations + excludedlocations restitempool = usefulitempool + filleritempool
world.random.shuffle(defaultlocations)
if any(localrestitempool.values()): # we need to make sure some fills are limited to certain worlds remaining_fill(world, defaultlocations, restitempool)
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)
for player, items in localrestitempool.items(): # items already shuffled unplaced = restitempool
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
unfilled = defaultlocations unfilled = defaultlocations
if unplaced or unfilled: if unplaced or unfilled:
@@ -241,15 +272,6 @@ def distribute_items_restrictive(world: MultiWorld) -> None:
logging.info(f'Per-Player counts: {print_data})') 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: def flood_items(world: MultiWorld) -> None:
# get items to distribute # get items to distribute
world.random.shuffle(world.itempool) world.random.shuffle(world.itempool)

View File

@@ -23,7 +23,6 @@ from worlds.alttp.EntranceRandomizer import parse_arguments
from Main import main as ERmain from Main import main as ERmain
from BaseClasses import seeddigits, get_seed from BaseClasses import seeddigits, get_seed
import Options import Options
from worlds.alttp import Bosses
from worlds.alttp.Text import TextTable from worlds.alttp.Text import TextTable
from worlds.AutoWorld import AutoWorldRegister from worlds.AutoWorld import AutoWorldRegister
import copy import copy
@@ -337,19 +336,6 @@ def prefer_int(input_data: str) -> Union[str, int]:
return input_data 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 = { goals = {
'ganon': 'ganon', 'ganon': 'ganon',
'crystals': 'crystals', 'crystals': 'crystals',
@@ -456,42 +442,7 @@ def roll_triggers(weights: dict, triggers: list) -> dict:
return weights return weights
def get_plando_bosses(boss_shuffle: str, plando_options: Set[str]) -> str: def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, option: type(Options.Option), plando_options: PlandoSettings):
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)):
if option_key in game_weights: if option_key in game_weights:
try: try:
if not option.supports_weighting: 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: except Exception as e:
raise Exception(f"Error generating option {option_key} in {ret.game}") from e raise Exception(f"Error generating option {option_key} in {ret.game}") from e
else: else:
if hasattr(player_option, "verify"): player_option.verify(AutoWorldRegister.world_types[ret.game], ret.name, plando_options)
player_option.verify(AutoWorldRegister.world_types[ret.game])
else: 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): 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: if ret.game in AutoWorldRegister.world_types:
for option_key, option in world_type.option_definitions.items(): 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(): 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 # 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): 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: if PlandoSettings.items in plando_options:
ret.plando_items = game_weights.get("plando_items", []) ret.plando_items = game_weights.get("plando_items", [])
if ret.game == "Minecraft" or ret.game == "Ocarina of Time": 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) 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', ret.enemy_damage = {None: 'default',
'default': 'default', 'default': 'default',

53
Main.py
View File

@@ -12,7 +12,7 @@ from typing import Dict, Tuple, Optional, Set
from BaseClasses import MultiWorld, CollectionState, Region, RegionType, LocationProgressType, Location from BaseClasses import MultiWorld, CollectionState, Region, RegionType, LocationProgressType, Location
from worlds.alttp.Items import item_name_groups 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 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 worlds.alttp.Shops import SHOP_ID_START, total_shop_slots, FillDisabledShopSlots
from Utils import output_path, get_options, __version__, version_tuple 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( output_file_futures.append(
pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir)) 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 # collect ER hint info
er_hint_data = {player: {} for player in world.get_game_players("A Link to the Past") if er_hint_data: Dict[int, Dict[int, str]] = {}
world.shuffle[player] != "vanilla" or world.retro_caves[player]} AutoWorld.call_all(world, 'extend_hint_information', er_hint_data)
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
checks_in_area = {player: {area: list() for area in ordered_areas} checks_in_area = {player: {area: list() for area in ordered_areas}
for player in range(1, world.players + 1)} 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(): for location in world.get_filled_locations():
if type(location.address) is int: if type(location.address) is int:
main_entrance = get_entrance_to_region(location.parent_region)
if location.game != "A Link to the Past": if location.game != "A Link to the Past":
checks_in_area[location.player]["Light World"].append(location.address) checks_in_area[location.player]["Light World"].append(location.address)
elif location.parent_region.dungeon: else:
dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower', main_entrance = location.parent_region.get_connecting_entrance(is_main_entrance)
'Inverted Ganons Tower': 'Ganons Tower'} \ if location.parent_region.dungeon:
.get(location.parent_region.dungeon.name, location.parent_region.dungeon.name) dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower',
checks_in_area[location.player][dungeonname].append(location.address) 'Inverted Ganons Tower': 'Ganons Tower'} \
elif location.parent_region.type == RegionType.LightWorld: .get(location.parent_region.dungeon.name, location.parent_region.dungeon.name)
checks_in_area[location.player]["Light World"].append(location.address) checks_in_area[location.player][dungeonname].append(location.address)
elif location.parent_region.type == RegionType.DarkWorld: elif location.parent_region.type == RegionType.LightWorld:
checks_in_area[location.player]["Dark World"].append(location.address) checks_in_area[location.player]["Light World"].append(location.address)
elif main_entrance.parent_region.type == RegionType.LightWorld: elif location.parent_region.type == RegionType.DarkWorld:
checks_in_area[location.player]["Light World"].append(location.address) checks_in_area[location.player]["Dark World"].append(location.address)
elif main_entrance.parent_region.type == RegionType.DarkWorld: elif main_entrance.parent_region.type == RegionType.LightWorld:
checks_in_area[location.player]["Dark World"].append(location.address) 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 checks_in_area[location.player]["Total"] += 1
oldmancaves = [] oldmancaves = []
@@ -305,7 +291,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
player = region.player player = region.player
location_id = SHOP_ID_START + total_shop_slots + index 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: if main_entrance.parent_region.type == RegionType.LightWorld:
checks_in_area[player]["Light World"].append(location_id) checks_in_area[player]["Light World"].append(location_id)
else: 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()} for player, world_precollected in world.precollected_items.items()}
precollected_hints = {player: set() for player in range(1, world.players + 1 + len(world.groups))} precollected_hints = {player: set() for player in range(1, world.players + 1 + len(world.groups))}
for slot in world.player_ids: for slot in world.player_ids:
slot_data[slot] = world.worlds[slot].fill_slot_data() slot_data[slot] = world.worlds[slot].fill_slot_data()

View File

@@ -126,6 +126,7 @@ class Context:
location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})') 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]] all_item_and_group_names: typing.Dict[str, typing.Set[str]]
forced_auto_forfeits: typing.Dict[str, bool] 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, 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", hint_cost: int, item_cheat: bool, forfeit_mode: str = "disabled", collect_mode="disabled",
@@ -196,7 +197,7 @@ class Context:
self.item_name_groups = {} self.item_name_groups = {}
self.all_item_and_group_names = {} self.all_item_and_group_names = {}
self.forced_auto_forfeits = collections.defaultdict(lambda: False) self.forced_auto_forfeits = collections.defaultdict(lambda: False)
self.non_hintable_names = {} self.non_hintable_names = collections.defaultdict(frozenset)
self._load_game_data() self._load_game_data()
self._init_game_data() self._init_game_data()
@@ -221,11 +222,11 @@ class Context:
self.all_item_and_group_names[game_name] = \ self.all_item_and_group_names[game_name] = \
set(game_package["item_name_to_id"]) | set(self.item_name_groups[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]: def item_names_for_game(self, game: str) -> typing.Optional[typing.Dict[str, int]]:
return self.gamespackage[game]["item_name_to_id"] 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]: def location_names_for_game(self, game: str) -> typing.Optional[typing.Dict[str, int]]:
return self.gamespackage[game]["location_name_to_id"] return self.gamespackage[game]["location_name_to_id"] if game in self.gamespackage else None
# General networking # General networking
async def send_msgs(self, endpoint: Endpoint, msgs: typing.Iterable[dict]) -> bool: 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() 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 = [] hints = []
slots: typing.Set[int] = {slot} slots: typing.Set[int] = {slot}
for group_id, group in ctx.groups.items(): for group_id, group in ctx.groups.items():
if slot in group: if slot in group:
slots.add(group_id) 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 finding_player, check_data in ctx.locations.items():
for location_id, (item_id, receiving_player, item_flags) in check_data.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: 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. " self.output(f"A hint costs {self.ctx.get_hint_cost(self.client.slot)} points. "
f"You have {points_available} points.") f"You have {points_available} points.")
return True 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: else:
game = self.ctx.games[self.client.slot] 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) \ names = self.ctx.location_names_for_game(game) \
if for_location else \ if for_location else \
self.ctx.all_item_and_group_names[game] self.ctx.all_item_and_group_names[game]
hint_name, usable, response = get_intended_text(input_text, hint_name, usable, response = get_intended_text(input_text, names)
names)
if usable: if usable:
if hint_name in self.ctx.non_hintable_names[game]: if hint_name in self.ctx.non_hintable_names[game]:
self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.") 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) hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name)
else: # location name else: # location name
hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_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: else:
self.output(response) self.output(response)
return False 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 @mark_raw
def _cmd_hint(self, item_name: str = "") -> bool: def _cmd_hint(self, item_name: str = "") -> bool:
"""Use !hint {item_name}, """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()) seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
if usable: if usable:
team, slot = self.ctx.player_name_lookup[seeked_player] team, slot = self.ctx.player_name_lookup[seeked_player]
item_name = " ".join(item_name)
game = self.ctx.games[slot] 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 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 = [] 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 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)) hints.extend(collect_hints(self.ctx, team, slot, item_name_from_group))
else: # item name else: # item name or id
hints = collect_hints(self.ctx, team, slot, item_name) hints = collect_hints(self.ctx, team, slot, item)
if hints: if hints:
notify_hints(self.ctx, team, 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()) seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
if usable: if usable:
team, slot = self.ctx.player_name_lookup[seeked_player] team, slot = self.ctx.player_name_lookup[seeked_player]
location_name = " ".join(location_name) game = self.ctx.games[slot]
location_name, usable, response = get_intended_text(location_name, full_name = " ".join(location_name)
self.ctx.location_names_for_game(self.ctx.games[slot]))
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: 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: if hints:
notify_hints(self.ctx, team, hints) notify_hints(self.ctx, team, hints)
else: else:
@@ -2044,15 +2086,28 @@ async def main(args: argparse.Namespace):
args.auto_shutdown, args.compatibility, args.log_network) args.auto_shutdown, args.compatibility, args.log_network)
data_filename = args.multidata data_filename = args.multidata
try: if not data_filename:
if not data_filename: try:
filetypes = (("Multiworld data", (".archipelago", ".zip")),) filetypes = (("Multiworld data", (".archipelago", ".zip")),)
data_filename = Utils.open_filename("Select multiworld data", filetypes) 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) ctx.load(data_filename, args.use_embedded_options)
except Exception as e: except Exception as e:
logging.exception('Failed to read multiworld data (%s)' % e) logging.exception(f"Failed to read multiworld data ({e})")
raise raise
ctx.init_save(not args.disable_save) ctx.init_save(not args.disable_save)

View File

@@ -26,15 +26,31 @@ class AssembleOptions(abc.ABCMeta):
attrs["name_lookup"].update({option_id: name for name, option_id in new_options.items()}) attrs["name_lookup"].update({option_id: name for name, option_id in new_options.items()})
options.update(new_options) options.update(new_options)
# apply aliases, without name_lookup # apply aliases, without name_lookup
aliases = {name[6:].lower(): option_id for name, option_id in attrs.items() if aliases = {name[6:].lower(): option_id for name, option_id in attrs.items() if
name.startswith("alias_")} name.startswith("alias_")}
assert "random" not in aliases, "Choice option 'random' cannot be manually assigned." 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) 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__ # auto-validate schema on __init__
if "schema" in attrs.keys(): 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]: def from_any(cls, data: typing.Any) -> Option[T]:
raise NotImplementedError 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): class NumericOption(Option[int], numbers.Integral):
# note: some of the `typing.Any`` here is a result of unresolved issue in python standards # 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__ __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): class Range(NumericOption):
range_start = 0 range_start = 0
range_end = 1 range_end = 1
@@ -385,7 +483,7 @@ class Range(NumericOption):
if text.startswith("random"): if text.startswith("random"):
return cls.weighted_range(text) return cls.weighted_range(text)
elif text == "default" and hasattr(cls, "default"): elif text == "default" and hasattr(cls, "default"):
return cls(cls.default) return cls.from_any(cls.default)
elif text == "high": elif text == "high":
return cls(cls.range_end) return cls(cls.range_end)
elif text == "low": elif text == "low":
@@ -396,7 +494,7 @@ class Range(NumericOption):
and text in ("true", "false"): and text in ("true", "false"):
# these are the conditions where "true" and "false" make sense # these are the conditions where "true" and "false" make sense
if text == "true": if text == "true":
return cls(cls.default) return cls.from_any(cls.default)
else: # "false" else: # "false"
return cls(0) return cls(0)
return cls(int(text)) return cls(int(text))
@@ -507,7 +605,7 @@ class VerifyKeys:
raise Exception(f"Found unexpected key {', '.join(extra)} in {cls}. " raise Exception(f"Found unexpected key {', '.join(extra)} in {cls}. "
f"Allowed keys: {cls.valid_keys}.") 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: if self.convert_name_groups and self.verify_item_name:
new_value = type(self.value)() # empty container of whatever value is new_value = type(self.value)() # empty container of whatever value is
for item_name in self.value: for item_name in self.value:
@@ -600,10 +698,7 @@ class OptionSet(Option[typing.Set[str]], VerifyKeys):
@classmethod @classmethod
def from_any(cls, data: typing.Any): def from_any(cls, data: typing.Any):
if type(data) == list: if isinstance(data, (list, set, frozenset)):
cls.verify_keys(data)
return cls(data)
elif type(data) == set:
cls.verify_keys(data) cls.verify_keys(data)
return cls(data) return cls(data)
return cls.from_text(str(data)) return cls.from_text(str(data))
@@ -732,8 +827,8 @@ class ItemLinks(OptionList):
pool |= {item_name} pool |= {item_name}
return pool return pool
def verify(self, world): def verify(self, world, player_name: str, plando_options) -> None:
super(ItemLinks, self).verify(world) super(ItemLinks, self).verify(world, player_name, plando_options)
existing_links = set() existing_links = set()
for link in self.value: for link in self.value:
if link["name"] in existing_links: if link["name"] in existing_links:

View File

@@ -295,34 +295,37 @@ class SC2Context(CommonContext):
category_panel.add_widget( category_panel.add_widget(
Label(text=category, size_hint_y=None, height=50, outline_width=1)) Label(text=category, size_hint_y=None, height=50, outline_width=1))
# Map is completed
for mission in categories[category]: for mission in categories[category]:
text = mission text: str = mission
tooltip = "" tooltip: str = ""
# Map has uncollected locations # Map has uncollected locations
if mission in unfinished_missions: if mission in unfinished_missions:
text = f"[color=6495ED]{text}[/color]" 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: elif mission in available_missions:
text = f"[color=FFFFFF]{text}[/color]" text = f"[color=FFFFFF]{text}[/color]"
# Map requirements not met # Map requirements not met
else: else:
text = f"[color=a9a9a9]{text}[/color]" text = f"[color=a9a9a9]{text}[/color]"
tooltip = f"Requires: " 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 tooltip += ", ".join(list(self.ctx.mission_req_table)[req_mission - 1] for
req_mission in req_mission in
self.ctx.mission_req_table[mission].required_world) 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 " 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" 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 = MissionButton(text=text, size_hint_y=None, height=50)
mission_button.tooltip_text = tooltip mission_button.tooltip_text = tooltip
@@ -790,7 +793,12 @@ def check_game_install_path() -> bool:
with open(einfo) as f: with open(einfo) as f:
content = f.read() content = f.read()
if content: 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): if os.path.exists(base):
executable = sc2.paths.latest_executeble(Path(base).expanduser() / "Versions") executable = sc2.paths.latest_executeble(Path(base).expanduser() / "Versions")
@@ -807,7 +815,8 @@ def check_game_install_path() -> bool:
else: else:
sc2_logger.warning(f"{einfo} pointed to {base}, but we could not find an SC2 install there.") sc2_logger.warning(f"{einfo} pointed to {base}, but we could not find an SC2 install there.")
else: 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 return False

View File

@@ -46,4 +46,4 @@ def get_datapackage_versions():
return version_package return version_package
from . import generate, user # trigger registration from . import generate, user, tracker # trigger registration

50
WebHostLib/api/tracker.py Normal file
View File

@@ -0,0 +1,50 @@
import collections
from flask import jsonify
from typing import Optional, Dict, Any, Tuple, List
from Utils import restricted_loads
from uuid import UUID
from ..models import Room
from . import api_endpoints
from ..tracker import fill_tracker_data, get_static_room_data
from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name
from WebHostLib import cache
@api_endpoints.route('/tracker/<suuid:tracker>/<int:tracked_team>/<int:tracked_player>')
@cache.memoize(timeout=60)
def update_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int):
room: Optional[Room] = Room.get(tracker=tracker)
locations = get_static_room_data(room)[0]
items_counter: Dict[int, collections.Counter] = get_item_names_counter(locations)
player_tracker, multisave, inventory, seed_checks_in_area, lttp_checks_done, \
slot_data, games, player_name, display_icons = fill_tracker_data(room, tracked_team, tracked_player)
# convert numbers to string
for item in player_tracker.items_received:
if items_counter[tracked_player][item] == 1:
player_tracker.items_received[item] = ''
else:
player_tracker.items_received[item] = str(player_tracker.items_received[item])
return jsonify({
"items_received": player_tracker.items_received,
"checked_locations": list(sorted(player_tracker.checked_locations)),
"icons": display_icons,
"progressive_names": player_tracker.progressive_names
})
@cache.cached()
def get_item_names_counter(locations: Dict[int, Dict[int, Tuple[int, int, int]]]):
# create and fill dictionary of all progression items for players
items_counters: Dict[int, collections.Counter] = {}
for player in locations:
for location in locations[player]:
item, recipient, flags = locations[player][location]
item_name = lookup_any_item_id_to_name[item]
items_counters.setdefault(recipient, collections.Counter())[item_name] += 1
return items_counters

View File

@@ -1,15 +1,16 @@
from __future__ import annotations from __future__ import annotations
import functools
import websockets
import asyncio import asyncio
import collections
import datetime
import functools
import logging
import pickle
import random
import socket import socket
import threading import threading
import time import time
import random import websockets
import pickle
import logging
import datetime
import Utils import Utils
from .models import db_session, Room, select, commit, Command, db from .models import db_session, Room, select, commit, Command, db
@@ -49,6 +50,8 @@ class DBCommandProcessor(ServerCommandProcessor):
class WebHostContext(Context): class WebHostContext(Context):
room_id: int
def __init__(self, static_server_data: dict): def __init__(self, static_server_data: dict):
# static server data is used during _load_game_data to load required data, # 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 # 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): def _load_game_data(self):
for key, value in self.static_server_data.items(): for key, value in self.static_server_data.items():
setattr(self, key, value) 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): def listen_to_db_commands(self):
cmdprocessor = DBCommandProcessor(self) cmdprocessor = DBCommandProcessor(self)

View File

@@ -1,20 +0,0 @@
window.addEventListener('load', () => {
const url = window.location;
setInterval(() => {
const ajax = new XMLHttpRequest();
ajax.onreadystatechange = () => {
if (ajax.readyState !== 4) { return; }
// Create a fake DOM using the returned HTML
const domParser = new DOMParser();
const fakeDOM = domParser.parseFromString(ajax.responseText, 'text/html');
// Update item and location trackers
document.getElementById('inventory-table').innerHTML = fakeDOM.getElementById('inventory-table').innerHTML;
document.getElementById('location-table').innerHTML = fakeDOM.getElementById('location-table').innerHTML;
};
ajax.open('GET', url);
ajax.send();
}, 15000)
});

View File

@@ -0,0 +1,82 @@
window.addEventListener('load', () => {
// Reload tracker
const update = () => {
const room = document.getElementById('tracker-wrapper').getAttribute('data-tracker');
const request = new Request('/api/tracker/' + room);
fetch(request)
.then(response => response.json())
.then(data => {
// update locations blocks
for (const location of data.checked_locations) {
document.getElementById(location).classList.add('acquired');
}
// update totals checks done
let total_checks_ele = document.getElementById('total-checks');
const total_checks = document.getElementsByClassName('location').length;
let checks_done = data.checked_locations.length;
total_checks_ele.innerText = 'Total Checks Done: ' + checks_done + '/' + total_checks;
// update item and icons blocks
// update icons block
if (data.icons.length > 0) {
for (let item in data.icons) {
if (data.progressive_names.length > 0) {
for (let item_category in data.progressive_names) {
let i = 0;
for (let current_item in current_name) {
if (current_item === item) {
let doc_item = document.getElementById(item_category)
doc_item.children[0].src = data.icons[item];
if (item in data.items_received) {
doc_item.children[0].classList.add('acquired');
doc_item.children[1].innerText = item_category;
}
}
}
}
} else {
if (item in data.items_received) {
let current_item = document.getElementById(item);
current_item.children[0].classList.add('acquired');
current_item.children[0].src = data.icons[item];
current_item.children[1].innerText = item;
}
}
}
} else {
for (const item in data.items_received) {
if (document.getElementById(item)) {
let current_item = document.getElementById(item);
current_item.innerText = item + data.items_received[item];
}
}
}
});
}
update()
setInterval(update, 30000);
// Collapsible regions section
const regions = document.getElementsByClassName('regions-column');
for (let i = 0; i < regions.length; i++) {
let region_name = regions[i].id;
const tab_header = document.getElementById(region_name+'-header');
const locations = document.getElementById(region_name+'-locations');
// toggle locations display
regions[i].addEventListener('click', function(event) {
if (tab_header.innerHTML.includes("▼")) {
locations.classList.remove('hidden');
// change header text
tab_header.innerHTML = tab_header.innerHTML.replace('▼', '▲');
} else {
locations.classList.add('hidden');
// change header text
tab_header.innerHTML = tab_header.innerHTML.replace('▲', '▼');
}
});
}
});

View File

@@ -0,0 +1,82 @@
window.addEventListener('load', () => {
// Reload tracker
const update = () => {
const room = document.getElementById('tracker-wrapper').getAttribute('data-tracker');
const request = new Request('/api/tracker/' + room);
fetch(request)
.then(response => response.json())
.then(data => {
// update locations blocks
for (const location of data.checked_locations) {
document.getElementById(location).classList.add('acquired');
}
// update totals checks done
let total_checks_ele = document.getElementById('total-checks');
const total_checks = document.getElementsByClassName('location').length;
let checks_done = data.checked_locations.length;
total_checks_ele.innerText = 'Total Checks Done: ' + checks_done + '/' + total_checks;
// update item and icons blocks
// update icons block
if (data.icons.length > 0) {
for (let item in data.icons) {
if (data.progressive_names.length > 0) {
for (let item_category in data.progressive_names) {
let i = 0;
for (let current_item in current_name) {
if (current_item === item) {
let doc_item = document.getElementById(item_category)
doc_item.children[0].src = data.icons[item];
if (item in data.items_received) {
doc_item.children[0].classList.add('acquired');
doc_item.children[1].innerText = item_category;
}
}
}
}
} else {
if (item in data.items_received) {
let current_item = document.getElementById(item);
current_item.children[0].classList.add('acquired');
current_item.children[0].src = data.icons[item];
current_item.children[1].innerText = item;
}
}
}
} else {
for (const item in data.items_received) {
if (document.getElementById(item)) {
let current_item = document.getElementById(item);
current_item.innerText = item + data.items_received[item];
}
}
}
});
}
update()
setInterval(update, 30000);
// Collapsible regions section
const regions = document.getElementsByClassName('regions-column');
for (let i = 0; i < regions.length; i++) {
let region_name = regions[i].id;
const tab_header = document.getElementById(region_name+'-header');
const locations = document.getElementById(region_name+'-locations');
// toggle locations display
regions[i].addEventListener('click', function(event) {
if (tab_header.innerHTML.includes("▼")) {
locations.classList.remove('hidden');
// change header text
tab_header.innerHTML = tab_header.innerHTML.replace('▼', '▲');
} else {
locations.classList.add('hidden');
// change header text
tab_header.innerHTML = tab_header.innerHTML.replace('▲', '▼');
}
});
}
});

View File

@@ -0,0 +1,150 @@
/* CSS Overrides */
.dirt-wrapper{
background-color: #897249;
}
.dirt-wrapper h1{}
.grass-wrapper{
background-color: #3fb24a;
}
.grass-wrapper h1{}
.grassFlowers-wrapper{
background-color: #3fb24a;
}
.grassFlowers-wrapper h1{}
.ice-wrapper{
background-color: #afe0ef;
}
.ice-wrapper h1{}
.jungle-wrapper{
background-color: #2a7808;
}
.jungle-wrapper h1{}
.ocean-wrapper{
background-color: #3667b1;
}
.ocean-wrapper h1{}
.partyTime-wrapper{
background-color: #3a0f69;
color: #ffffff;
}
.partyTime-wrapper h1{}
/* Actual Styles */
h1 {
font-size: 20px;
color: #ffffff;
padding: 5px;
text-align: center;
text-shadow: 1px 1px black;
}
h2 {
padding: 8px;
}
#player-keys-tracker{
width: 600px;
}
#items-container{
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-evenly;
padding: 5px;
}
#items-container div{
margin: 0;
padding: 0;
}
.image-container{
display: absolute;
height: 75px;
width: 75px;
}
.bottom-text{
position: relative;
align-items: bottom;
text-align: center;
}
.icon{
height: 100%;
position: relative;
left: 15px;
max-width: 45px;
max-height: 45px;
filter: grayscale(100%) contrast(75%) brightness(40%);
}
.icon.acquired{
filter: none;
}
.total-checks{
text-align: center;
padding: 5px;
font-size: 18px;
}
.locations-container{
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 5px;
margin-left: 50px;
margin-right: 50px;
}
.location.acquired{
text-decoration: line-through;
filter: none;
}
.regions-container{
display: flex;
flex-direction: column;
flex-wrap: wrap;
justify-content: space-evenly;
padding: 5px;
text-align: center;
}
.regions-header{
font-size: 18px;
padding: 15px;
cursor: pointer;
text-align: center;
}
.hidden{
display: none;
}
.button-link{
display: block;
width: 100%;
height: 30px;
text-align: center;
text-decoration: none;
line-height: 30px;
background-color: lightgrey;
cursor: pointer;
color: inherit;
}

View File

@@ -51,6 +51,17 @@ table.dataTable{
color: #000000; color: #000000;
} }
table.dataTable img.icon{
height: 100%;
max-width: 60px;
max-height: 60px;
filter: grayscale(100%) contrast(75%) brightness(50%);
}
table.dataTable img.acquired{
filter: none;
}
table.dataTable thead{ table.dataTable thead{
font-family: LexendDeca-Regular, sans-serif; font-family: LexendDeca-Regular, sans-serif;
} }

View File

@@ -1,86 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{ player_name }}&apos;s Tracker</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/globalStyles.css") }}"/>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/lttp-tracker.css") }}"/>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/lttp-tracker.js") }}"></script>
</head>
<body>
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
<table id="inventory-table">
<tr>
<td><img src="{{ bow_url }}" class="{{ 'acquired' if bow_acquired }}" /></td>
<td><img src="{{ icons["Blue Boomerang"] }}" class="{{ 'acquired' if 'Blue Boomerang' in acquired_items }}" /></td>
<td><img src="{{ icons["Red Boomerang"] }}" class="{{ 'acquired' if 'Red Boomerang' in acquired_items }}" /></td>
<td><img src="{{ icons["Hookshot"] }}" class="{{ 'acquired' if 'Hookshot' in acquired_items }}" /></td>
<td><img src="{{ icons["Magic Powder"] }}" class="powder-fix {{ 'acquired' if 'Magic Powder' in acquired_items }}" /></td>
</tr>
<tr>
<td><img src="{{ icons["Fire Rod"] }}" class="{{ 'acquired' if "Fire Rod" in acquired_items }}" /></td>
<td><img src="{{ icons["Ice Rod"] }}" class="{{ 'acquired' if "Ice Rod" in acquired_items }}" /></td>
<td><img src="{{ icons["Bombos"] }}" class="{{ 'acquired' if "Bombos" in acquired_items }}" /></td>
<td><img src="{{ icons["Ether"] }}" class="{{ 'acquired' if "Ether" in acquired_items }}" /></td>
<td><img src="{{ icons["Quake"] }}" class="{{ 'acquired' if "Quake" in acquired_items }}" /></td>
</tr>
<tr>
<td><img src="{{ icons["Lamp"] }}" class="{{ 'acquired' if "Lamp" in acquired_items }}" /></td>
<td><img src="{{ icons["Hammer"] }}" class="{{ 'acquired' if "Hammer" in acquired_items }}" /></td>
<td><img src="{{ icons["Flute"] }}" class="{{ 'acquired' if "Flute" in acquired_items }}" /></td>
<td><img src="{{ icons["Bug Catching Net"] }}" class="{{ 'acquired' if "Bug Catching Net" in acquired_items }}" /></td>
<td><img src="{{ icons["Book of Mudora"] }}" class="{{ 'acquired' if "Book of Mudora" in acquired_items }}" /></td>
</tr>
<tr>
<td><img src="{{ icons["Bottle"] }}" class="{{ 'acquired' if "Bottle" in acquired_items }}" /></td>
<td><img src="{{ icons["Cane of Somaria"] }}" class="{{ 'acquired' if "Cane of Somaria" in acquired_items }}" /></td>
<td><img src="{{ icons["Cane of Byrna"] }}" class="{{ 'acquired' if "Cane of Byrna" in acquired_items }}" /></td>
<td><img src="{{ icons["Cape"] }}" class="{{ 'acquired' if "Cape" in acquired_items }}" /></td>
<td><img src="{{ icons["Magic Mirror"] }}" class="{{ 'acquired' if "Magic Mirror" in acquired_items }}" /></td>
</tr>
<tr>
<td><img src="{{ icons["Pegasus Boots"] }}" class="{{ 'acquired' if "Pegasus Boots" in acquired_items }}" /></td>
<td><img src="{{ glove_url }}" class="{{ 'acquired' if glove_acquired }}" /></td>
<td><img src="{{ icons["Flippers"] }}" class="{{ 'acquired' if "Flippers" in acquired_items }}" /></td>
<td><img src="{{ icons["Moon Pearl"] }}" class="{{ 'acquired' if "Moon Pearl" in acquired_items }}" /></td>
<td><img src="{{ icons["Mushroom"] }}" class="{{ 'acquired' if "Mushroom" in acquired_items }}" /></td>
</tr>
<tr>
<td><img src="{{ sword_url }}" class="{{ 'acquired' if sword_acquired }}" /></td>
<td><img src="{{ shield_url }}" class="{{ 'acquired' if shield_acquired }}" /></td>
<td><img src="{{ mail_url }}" class="acquired" /></td>
<td><img src="{{ icons["Shovel"] }}" class="{{ 'acquired' if "Shovel" in acquired_items }}" /></td>
<td><img src="{{ icons["Triforce"] }}" class="{{ 'acquired' if "Triforce" in acquired_items }}" /></td>
</tr>
</table>
<table id="location-table">
<tr>
<th></th>
<th class="counter"><img src="{{ icons["Chest"] }}" /></th>
{% if key_locations and "Universal" not in key_locations %}
<th class="counter"><img src="{{ icons["Small Key"] }}" /></th>
{% endif %}
{% if big_key_locations %}
<th><img src="{{ icons["Big Key"] }}" /></th>
{% endif %}
</tr>
{% for area in sp_areas %}
<tr>
<td>{{ area }}</td>
<td class="counter">{{ checks_done[area] }} / {{ checks_in_area[area] }}</td>
{% if key_locations and "Universal" not in key_locations %}
<td class="counter">
{{ inventory[small_key_ids[area]] if area in key_locations else '—' }}
</td>
{% endif %}
{% if big_key_locations %}
<td>
{{ '✔' if area in big_key_locations and inventory[big_key_ids[area]] else ('—' if area not in big_key_locations else '') }}
</td>
{% endif %}
</tr>
{% endfor %}
</table>
</div>
</body>
</html>

View File

@@ -50,7 +50,7 @@
No file to download for this game. No file to download for this game.
{% endif %} {% endif %}
</td> </td>
<td><a href="{{ url_for("getPlayerTracker", tracker=room.tracker, tracked_team=0, tracked_player=patch.player_id) }}">Tracker</a></td> <td><a href="{{ url_for("get_player_tracker", tracker=room.tracker, tracked_team=0, tracked_player=patch.player_id) }}">Tracker</a></td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

@@ -2,9 +2,9 @@
{% block head %} {% block head %}
{{ super() }} {{ super() }}
<title>{{ player_name }}&apos;s Tracker</title> <title>{{ player_name }}&apos;s Tracker</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tracker.css") }}"/> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/trackers/tracker.css") }}"/>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/jquery.scrollsync.js") }}"></script> <script type="application/ecmascript" src="{{ url_for('static', filename="assets/jquery.scrollsync.js") }}"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/tracker.js") }}"></script> <script type="application/ecmascript" src="{{ url_for('static', filename="assets/trackers/tracker.js") }}"></script>
{% endblock %} {% endblock %}
{% block body %} {% block body %}
@@ -13,6 +13,9 @@
<div id="tracker-header-bar"> <div id="tracker-header-bar">
<input placeholder="Search" id="search"/> <input placeholder="Search" id="search"/>
<span class="info">This tracker will automatically update itself periodically.</span> <span class="info">This tracker will automatically update itself periodically.</span>
<a href="/tracker/{{ room.tracker|suuid }}/{{ team }}/{{ player }}" class="button-link">
Go to Styled Tracker
</a>
</div> </div>
<div class="table-wrapper"> <div class="table-wrapper">
<table class="table non-unique-item-table"> <table class="table non-unique-item-table">

View File

@@ -2,8 +2,8 @@
<html lang="en"> <html lang="en">
<head> <head>
<title>{{ player_name }}&apos;s Tracker</title> <title>{{ player_name }}&apos;s Tracker</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/minecraftTracker.css') }}"/> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/trackers/minecraftTracker.css') }}"/>
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/minecraftTracker.js') }}"></script> <script type="application/ecmascript" src="{{ url_for('static', filename='assets/trackers/minecraftTracker.js') }}"></script>
<link rel="stylesheet" media="screen" href="https://fontlibrary.org//face/minecraftia" type="text/css"/> <link rel="stylesheet" media="screen" href="https://fontlibrary.org//face/minecraftia" type="text/css"/>
</head> </head>

View File

@@ -2,9 +2,9 @@
{% block head %} {% block head %}
{{ super() }} {{ super() }}
<title>Multiworld Tracker</title> <title>Multiworld Tracker</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tracker.css") }}"/> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/trackers/tracker.css") }}"/>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/jquery.scrollsync.js") }}"></script> <script type="application/ecmascript" src="{{ url_for('static', filename="assets/jquery.scrollsync.js") }}"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/tracker.js") }}"></script> <script type="application/ecmascript" src="{{ url_for('static', filename="assets/trackers/tracker.js") }}"></script>
{% endblock %} {% endblock %}
{% block body %} {% block body %}
@@ -44,7 +44,7 @@
<tbody> <tbody>
{%- for player, items in players.items() -%} {%- for player, items in players.items() -%}
<tr> <tr>
<td><a href="{{ url_for("getPlayerTracker", tracker=room.tracker, <td><a href="{{ url_for("get_player_tracker", tracker=room.tracker,
tracked_team=team, tracked_player=player)}}">{{ loop.index }}</a></td> tracked_team=team, tracked_player=player)}}">{{ loop.index }}</a></td>
{%- if (team, loop.index) in video -%} {%- if (team, loop.index) in video -%}
{%- if video[(team, loop.index)][0] == "Twitch" -%} {%- if video[(team, loop.index)][0] == "Twitch" -%}
@@ -121,7 +121,7 @@
<tbody> <tbody>
{%- for player, checks in players.items() -%} {%- for player, checks in players.items() -%}
<tr> <tr>
<td><a href="{{ url_for("getPlayerTracker", tracker=room.tracker, <td><a href="{{ url_for("get_player_tracker", tracker=room.tracker,
tracked_team=team, tracked_player=player)}}">{{ loop.index }}</a></td> tracked_team=team, tracked_player=player)}}">{{ loop.index }}</a></td>
<td>{{ player_names[(team, loop.index)]|e }}</td> <td>{{ player_names[(team, loop.index)]|e }}</td>
{%- for area in ordered_areas -%} {%- for area in ordered_areas -%}

View File

@@ -0,0 +1,99 @@
{% block head %}
<!--suppress XmlDuplicatedId -->
<title>{{ player_name }}&apos;s Tracker</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/trackers/playerTracker.css') }}"/>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/tooltip.css') }}"/>
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/trackers/playerTracker.js') }}"></script>
{% endblock %}
{% block body %}
<div id="tracker-wrapper" class="{{ theme }}-wrapper" data-tracker="{{ room.tracker|suuid }}/{{ team }}/{{ player }}">
<a href="/generic_tracker/{{ room.tracker|suuid }}/{{ team }}/{{ player }}" class="button-link">
Go to Generic Tracker
</a>
{% if icons %}
{% block icons_render %}
<h1>Items</h1>
<div id="items-container">
{%- for item in icons %}
<div class="image-container tooltip" id="{{ item }}" data-tooltip="{{ item }}">
<img
src="{{ icons[item] }}"
class="icon tooltip {{ 'acquired' if item in received_items }}"
/>
</div>
{%- endfor %}
</div>
{% endblock %}
{% else %}
{% block item_names_render %}
<h1 class="items-header">Items</h1>
<div class="items-container">
{%- for item in received_items|sort -%}
<div class="item" id="{{ item }}">
{{ item }}
{% if all_progression_items[item] > 1 %}
{{ received_items[item] }}
{% else %}
{% endif %}
</div>
{%- endfor -%}
</div>
{% endblock %}
{% endif %}
{# div for total checks done as percentage. Probably needs to be put somewhere else but I liked how it looked here #}
<div class="total-checks" id="total-checks">
Total Checks Done: {{ checked_locations|length }}/{{ locations|length }}
</div>
{% if regions %}
{% block regions_render %}
<div class="regions-container">
{% for region in regions %}
<div class="regions-column" id="{{ region }}">
<h1 class="regions-header" id="{{ region }}-header">{{ region }} ▼ {{ checks_done[region]|length }} / {{ regions[region]|length }}</h1>
<div class="location-column hidden" id="{{ region }}-locations">
{%- for location in regions[region] %}
<div class="location {{ 'acquired' if location in checked_locations }}" id="{{ location }}">{{ location }}</div>
{%- endfor %}
</div>
</div>
{% endfor %}
</div>
{% endblock %}
{% else %}
{% block locations_render %}
<h1>Locations</h1>
<div class="locations-container" id="locations-container">
{% for location in locations %}
<div class="location {{ 'acquired' if name in checked_locations }}" id="{{ location }}">
{{ location }}
</div>
{% endfor %}
</div>
{% endblock %}
{% endif %}
</div>
{% endblock %}

View File

@@ -2,8 +2,8 @@
<html lang="en"> <html lang="en">
<head> <head>
<title>{{ player_name }}&apos;s Tracker</title> <title>{{ player_name }}&apos;s Tracker</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/supermetroidTracker.css') }}"/> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/trackers/supermetroidTracker.css') }}"/>
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/supermetroidTracker.js') }}"></script> <script type="application/ecmascript" src="{{ url_for('static', filename='assets/trackers/supermetroidTracker.js') }}"></script>
</head> </head>
<body> <body>

View File

@@ -2,8 +2,8 @@
<html lang="en"> <html lang="en">
<head> <head>
<title>{{ player_name }}&apos;s Tracker</title> <title>{{ player_name }}&apos;s Tracker</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/timespinnerTracker.css') }}"/> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/trackers/timespinnerTracker.css') }}"/>
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/timespinnerTracker.js') }}"></script> <script type="application/ecmascript" src="{{ url_for('static', filename='assets/trackers/timespinnerTracker.js') }}"></script>
</head> </head>
<body> <body>

View File

@@ -0,0 +1,77 @@
{% block head %}
<!--suppress XmlDuplicatedId -->
<title>{{ player_name }}&apos;s Keys Tracker</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/trackers/playerTracker.css') }}"/>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/tooltip.css') }}" />
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/trackers/zeldaKeysTracker.js') }}"/></script>
{% endblock %}
{# this tracker is mostly similar to the generic player tracker but
also adds a table with the key and checks counts for each region in the middle #}
{% block body %}
<div id="tracker-wrapper" class="{{ theme }}-wrapper" data-tracker="{{ room.tracker|suuid }}/{{ team }}/{{ player }}">
<a href="/generic_tracker/{{ room.tracker|suuid }}/{{ team }}/{{ player }}" class="button-link">
Go to Generic Tracker
</a>
<h1>Items</h1>
<div id="items-container">
{% for item in icons %}
{% if item not in ['Small Key', 'Big Key'] %}
<div class="image-container tooltip" id="{{ item }}" data-tooltip="{{ item }}">
<img
src="{{ icons[item] }}"
class="icon tooltip {{ 'acquired' if item in received_items }}"
/>
</div>
{% endif %}
{% endfor %}
</div>
<div class="total-checks" id="total-checks">
Total Checks Done: {{ checked_locations|length }}/{{ locations|length }}
</div>
<table id="regions-column">
<tr class="keys-icons">
<td><img src="{{icons['Small Key']}}" class="icon tooltip acquired" id="small-key-icon"/></td>
<td><img src="{{icons['Big Key']}}" class="icon tooltip acquired" id="big-key-icon"/></td>
<td class="right-align">Total</td>
</tr>
{% for region in regions %}
<tr class="regions-column" id="{{ region }}">
<td id="{{ region }}-header">{{ region }} ▼</td>
{% if region in region_keys %}
{%- if region_keys[region]|length > 1 %}
<td class="smallkeys">{{ received_items[region_keys[region][0]] if region_keys[region][0] in received_items else '-' }}</td>
<td class="bigkeys">{{ received_items[region_keys[region][1]] if region_keys[region][1] in received_items else '-' }}</td>
{%- else %}
{% if 'Small Key' in region_keys[region][0] %}
<td class="smallkeys">{{ received_items[region_keys[region][0]] if region_keys[region][0] in received_items else '-' }}</td>
<td class="bigkeys">-</td>
{% else %}
<td class="smallkeys">-</td>
<td class="bigkeys">{{ received_items[region_keys[region][0]] if region_keys[region][0] in received_items else '-' }}</td>
{% endif %}
{%- endif%}
{% else %}
<td class="smallkeys">-</td>
<td class="bigkeys">-</td>
{% endif %}
<td class="counter">{{ checks_done[region]|length }} / {{ regions[region]|length }}</td>
</tr>
<tbody class="locations hidden" id="{{ region }}-locations">
{% for location in regions[region] %}
<tr>
<td class="location {{ 'acquired' if location in checked_locations }}" id="{{ location }}">
{{ location }}
</td>
</tr>
{% endfor %}
</tbody>
{% endfor %}
</div>
</div>
{% endblock %}

View File

@@ -1,6 +1,6 @@
import collections import collections
import typing import typing
from typing import Counter, Optional, Dict, Any, Tuple from typing import Counter, Optional, Dict, Any, Tuple, Set, List, TYPE_CHECKING
from flask import render_template from flask import render_template
from werkzeug.exceptions import abort from werkzeug.exceptions import abort
@@ -11,9 +11,53 @@ from worlds.alttp import Items
from WebHostLib import app, cache, Room from WebHostLib import app, cache, Room
from Utils import restricted_loads from Utils import restricted_loads
from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name
from MultiServer import Context from worlds.AutoWorld import AutoWorldRegister
from MultiServer import get_item_name_from_id, Context
from NetUtils import SlotType from NetUtils import SlotType
class PlayerTracker:
"""This class will create a basic 'prettier' tracker for each world using their themes automatically. This
can be overridden to customize how it will appear. Can provide icons and custom regions. The html used is also
a jinja template that can be overridden if you want your tracker to look different in certain aspects. To render
icons and regions add dictionaries to the relevant attributes of the tracker_info. To customize the layout of
your icons you can create a new html in your world and extend playerTracker.html and overwrite the icons_render
block then change the tracker_info template attribute to your template."""
template: str = 'playerTracker.html'
icons: Dict[str, str] = {}
progressive_items: List[str] = []
progressive_names: Dict[str, List[str]] = {}
regions: Dict[str, List[str]] = {}
checks_done: Dict[str, Set[str]] = {}
room: Any
team: int
player: int
name: str
all_locations: Set[str]
checked_locations: Set[str]
all_prog_items: Counter[str]
items_received: Counter[str]
received_prog_items: Counter[str]
slot_data: Dict[any, any]
theme: str
region_keys: Dict[str, str] = {}
def __init__(self, room: Any, team: int, player: int, name: str, all_locations: Set[str],
checked_locations: set, all_progression_items: Counter[str], items_received: Counter[str],
slot_data: Dict[any, any]):
self.room = room
self.team = team
self.player = player
self.name = name
self.all_locations = all_locations
self.checked_locations = checked_locations
self.all_prog_items = all_progression_items
self.items_received = items_received
self.slot_data = slot_data
alttp_icons = { alttp_icons = {
"Blue Shield": r"https://www.zeldadungeon.net/wiki/images/8/85/Fighters-Shield.png", "Blue Shield": r"https://www.zeldadungeon.net/wiki/images/8/85/Fighters-Shield.png",
"Red Shield": r"https://www.zeldadungeon.net/wiki/images/5/55/Fire-Shield.png", "Red Shield": r"https://www.zeldadungeon.net/wiki/images/5/55/Fire-Shield.png",
@@ -288,7 +332,7 @@ def get_static_room_data(room: Room):
@app.route('/tracker/<suuid:tracker>/<int:tracked_team>/<int:tracked_player>') @app.route('/tracker/<suuid:tracker>/<int:tracked_team>/<int:tracked_player>')
@cache.memoize(timeout=60) # multisave is currently created at most every minute @cache.memoize(timeout=60) # multisave is currently created at most every minute
def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int, want_generic: bool = False): def get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, want_generic: bool = False):
# Team and player must be positive and greater than zero # Team and player must be positive and greater than zero
if tracked_team < 0 or tracked_player < 1: if tracked_team < 0 or tracked_player < 1:
abort(404) abort(404)
@@ -297,13 +341,78 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int, want
if not room: if not room:
abort(404) abort(404)
# Collect seed information and pare it down to a single player player_tracker, multisave, inventory, seed_checks_in_area, lttp_checks_done, \
slot_data, games, player_name, display_icons = fill_tracker_data(room, tracked_team, tracked_player)
game_name = games[tracked_player]
# TODO move all games in game_specific_trackers to new system
if game_name in game_specific_trackers and not want_generic:
specific_tracker = game_specific_trackers.get(game_name, None)
return specific_tracker(multisave, room, player_tracker.all_locations, inventory, tracked_team, tracked_player,
player_name, seed_checks_in_area, lttp_checks_done, slot_data[tracked_player])
elif game_name in AutoWorldRegister.world_types and not want_generic:
return render_template(
"trackers/" + player_tracker.template,
all_progression_items=player_tracker.all_prog_items,
player=tracked_player,
team=tracked_team,
room=player_tracker.room,
player_name=player_tracker.name,
checked_locations=sorted(player_tracker.checked_locations),
locations=sorted(player_tracker.all_locations),
theme=player_tracker.theme,
icons=display_icons,
regions=player_tracker.regions,
checks_done=player_tracker.checks_done,
region_keys=player_tracker.region_keys
)
else:
return __renderGenericTracker(multisave, room, player_tracker.all_locations, inventory, tracked_team, tracked_player, player_name, seed_checks_in_area, lttp_checks_done)
@app.route('/generic_tracker/<suuid:tracker>/<int:tracked_team>/<int:tracked_player>')
@cache.memoize(timeout=60)
def get_generic_tracker(tracker: UUID, tracked_team: int, tracked_player: int):
return get_player_tracker(tracker, tracked_team, tracked_player, True)
def get_tracker_icons_and_regions(player_tracker: PlayerTracker) -> Dict[str, str]:
"""this function allows multiple icons to be used for the same item but it does require the world to submit both
a progressive_items list and the icons dict together"""
display_icons: Dict[str, str] = {}
if player_tracker.progressive_names and player_tracker.icons:
for item in player_tracker.progressive_items:
if item in player_tracker.progressive_names:
level = min(player_tracker.items_received[item], len(player_tracker.progressive_names[item]) - 1)
display_name = player_tracker.progressive_names[item][level]
if display_name in player_tracker.icons:
display_icons[item] = player_tracker.icons[display_name]
else:
display_icons[item] = player_tracker.icons[item]
else:
display_icons[item] = player_tracker.icons[item]
else:
if player_tracker.progressive_items and player_tracker.icons:
for item in player_tracker.progressive_items:
display_icons[item] = player_tracker.icons[item]
if player_tracker.regions:
for region in player_tracker.regions:
for location in region:
if location in player_tracker.checked_locations:
player_tracker.checks_done.setdefault(region, set()).add(location)
return display_icons
def fill_tracker_data(room: Room, tracked_team: int, tracked_player: int) -> Tuple:
"""Collect seed information and pare it down to a single player"""
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \ locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \
precollected_items, games, slot_data, groups = get_static_room_data(room) precollected_items, games, slot_data, groups = get_static_room_data(room)
player_name = names[tracked_team][tracked_player - 1] player_name = names[tracked_team][tracked_player - 1]
location_to_area = player_location_to_area[tracked_player] location_to_area = player_location_to_area[tracked_player]
inventory = collections.Counter() inventory = collections.Counter()
checks_done = {loc_name: 0 for loc_name in default_locations} lttp_checks_done = {loc_name: 0 for loc_name in default_locations}
# Add starting items to inventory # Add starting items to inventory
starting_items = precollected_items[tracked_player] starting_items = precollected_items[tracked_player]
@@ -321,6 +430,7 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int, want
if tracked_player in group_members: if tracked_player in group_members:
slots_aimed_at_player.add(group_id) slots_aimed_at_player.add(group_id)
checked_locations = set()
# Add items to player inventory # Add items to player inventory
for (ms_team, ms_player), locations_checked in multisave.get("location_checks", {}).items(): for (ms_team, ms_player), locations_checked in multisave.get("location_checks", {}).items():
# Skip teams and players not matching the request # Skip teams and players not matching the request
@@ -332,383 +442,52 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int, want
item, recipient, flags = player_locations[location] item, recipient, flags = player_locations[location]
if recipient in slots_aimed_at_player: # a check done for the tracked player if recipient in slots_aimed_at_player: # a check done for the tracked player
attribute_item_solo(inventory, item) attribute_item_solo(inventory, item)
if ms_player == tracked_player: # a check done by the tracked player if ms_player == tracked_player: # a check done by the tracked player
checks_done[location_to_area[location]] += 1 lttp_checks_done[location_to_area[location]] += 1
checks_done["Total"] += 1 lttp_checks_done["Total"] += 1
specific_tracker = game_specific_trackers.get(games[tracked_player], None) checked_locations.add(lookup_any_location_id_to_name[location])
if specific_tracker and not want_generic:
return specific_tracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name, prog_items = collections.Counter
seed_checks_in_area, checks_done, slot_data[tracked_player]) all_location_names = set()
else:
return __renderGenericTracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name, all_location_names = {lookup_any_location_id_to_name[id] for id in locations[tracked_player]}
seed_checks_in_area, checks_done) prog_items = collections.Counter()
for player in locations:
for location in locations[player]:
item, recipient, flags = locations[player][location]
if recipient == player:
if flags & 1:
item_name = lookup_any_item_id_to_name[item]
prog_items[item_name] += 1
items_received = collections.Counter()
for id in inventory:
items_received[lookup_any_item_id_to_name[id]] = inventory[id]
player_tracker = PlayerTracker(
room,
tracked_team,
tracked_player,
player_name,
all_location_names,
checked_locations,
prog_items,
items_received,
slot_data[tracked_player]
)
# grab webworld and apply its theme to the tracker
webworld = AutoWorldRegister.world_types[games[tracked_player]].web
player_tracker.theme = webworld.theme
# allow the world to add information to the tracker class
webworld.modify_tracker(player_tracker)
display_icons = get_tracker_icons_and_regions(player_tracker)
return player_tracker, multisave, inventory, seed_checks_in_area, lttp_checks_done, slot_data, games, player_name, display_icons
@app.route('/generic_tracker/<suuid:tracker>/<int:tracked_team>/<int:tracked_player>') def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations: set,
def get_generic_tracker(tracker: UUID, tracked_team: int, tracked_player: int):
return getPlayerTracker(tracker, tracked_team, tracked_player, True)
def __renderAlttpTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
inventory: Counter, team: int, player: int, player_name: str,
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict) -> str:
# Note the presence of the triforce item
game_state = multisave.get("client_game_state", {}).get((team, player), 0)
if game_state == 30:
inventory[106] = 1 # Triforce
# Progressive items need special handling for icons and class
progressive_items = {
"Progressive Sword": 94,
"Progressive Glove": 97,
"Progressive Bow": 100,
"Progressive Mail": 96,
"Progressive Shield": 95,
}
progressive_names = {
"Progressive Sword": [None, 'Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword'],
"Progressive Glove": [None, 'Power Glove', 'Titan Mitts'],
"Progressive Bow": [None, "Bow", "Silver Bow"],
"Progressive Mail": ["Green Mail", "Blue Mail", "Red Mail"],
"Progressive Shield": [None, "Blue Shield", "Red Shield", "Mirror Shield"]
}
# Determine which icon to use
display_data = {}
for item_name, item_id in progressive_items.items():
level = min(inventory[item_id], len(progressive_names[item_name]) - 1)
display_name = progressive_names[item_name][level]
acquired = True
if not display_name:
acquired = False
display_name = progressive_names[item_name][level + 1]
base_name = item_name.split(maxsplit=1)[1].lower()
display_data[base_name + "_acquired"] = acquired
display_data[base_name + "_url"] = alttp_icons[display_name]
# The single player tracker doesn't care about overworld, underworld, and total checks. Maybe it should?
sp_areas = ordered_areas[2:15]
player_big_key_locations = set()
player_small_key_locations = set()
for loc_data in locations.values():
for values in loc_data.values():
item_id, item_player, flags = values
if item_player == player:
if item_id in ids_big_key:
player_big_key_locations.add(ids_big_key[item_id])
elif item_id in ids_small_key:
player_small_key_locations.add(ids_small_key[item_id])
return render_template("lttpTracker.html", inventory=inventory,
player_name=player_name, room=room, icons=alttp_icons, checks_done=checks_done,
checks_in_area=seed_checks_in_area[player],
acquired_items={lookup_any_item_id_to_name[id] for id in inventory},
small_key_ids=small_key_ids, big_key_ids=big_key_ids, sp_areas=sp_areas,
key_locations=player_small_key_locations,
big_key_locations=player_big_key_locations,
**display_data)
def __renderMinecraftTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
inventory: Counter, team: int, player: int, playerName: str,
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict) -> str:
icons = {
"Wooden Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d2/Wooden_Pickaxe_JE3_BE3.png",
"Stone Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c4/Stone_Pickaxe_JE2_BE2.png",
"Iron Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d1/Iron_Pickaxe_JE3_BE2.png",
"Diamond Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/e/e7/Diamond_Pickaxe_JE3_BE3.png",
"Wooden Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d5/Wooden_Sword_JE2_BE2.png",
"Stone Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b1/Stone_Sword_JE2_BE2.png",
"Iron Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/8/8e/Iron_Sword_JE2_BE2.png",
"Diamond Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/4/44/Diamond_Sword_JE3_BE3.png",
"Leather Tunic": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b7/Leather_Tunic_JE4_BE2.png",
"Iron Chestplate": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/31/Iron_Chestplate_JE2_BE2.png",
"Diamond Chestplate": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/e/e0/Diamond_Chestplate_JE3_BE2.png",
"Iron Ingot": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fc/Iron_Ingot_JE3_BE2.png",
"Block of Iron": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/7e/Block_of_Iron_JE4_BE3.png",
"Brewing Stand": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b3/Brewing_Stand_%28empty%29_JE10.png",
"Ender Pearl": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/f6/Ender_Pearl_JE3_BE2.png",
"Bucket": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fc/Bucket_JE2_BE2.png",
"Bow": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/a/ab/Bow_%28Pull_2%29_JE1_BE1.png",
"Shield": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c6/Shield_JE2_BE1.png",
"Red Bed": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/6/6a/Red_Bed_%28N%29.png",
"Netherite Scrap": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/33/Netherite_Scrap_JE2_BE1.png",
"Flint and Steel": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/94/Flint_and_Steel_JE4_BE2.png",
"Enchanting Table": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/31/Enchanting_Table.gif",
"Fishing Rod": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/7f/Fishing_Rod_JE2_BE2.png",
"Campfire": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/91/Campfire_JE2_BE2.gif",
"Water Bottle": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/75/Water_Bottle_JE2_BE2.png",
"Spyglass": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c1/Spyglass_JE2_BE1.png",
}
minecraft_location_ids = {
"Story": [42073, 42023, 42027, 42039, 42002, 42009, 42010, 42070,
42041, 42049, 42004, 42031, 42025, 42029, 42051, 42077],
"Nether": [42017, 42044, 42069, 42058, 42034, 42060, 42066, 42076, 42064, 42071, 42021,
42062, 42008, 42061, 42033, 42011, 42006, 42019, 42000, 42040, 42001, 42015, 42014],
"The End": [42052, 42005, 42012, 42032, 42030, 42042, 42018, 42038, 42046],
"Adventure": [42047, 42050, 42096, 42097, 42098, 42059, 42055, 42072, 42003, 42035, 42016, 42020,
42048, 42054, 42068, 42043, 42074, 42075, 42024, 42026, 42037, 42045, 42056, 42099, 42100],
"Husbandry": [42065, 42067, 42078, 42022, 42007, 42079, 42013, 42028, 42036,
42057, 42063, 42053, 42102, 42101, 42092, 42093, 42094, 42095],
"Archipelago": [42080, 42081, 42082, 42083, 42084, 42085, 42086, 42087, 42088, 42089, 42090, 42091],
}
display_data = {}
# Determine display for progressive items
progressive_items = {
"Progressive Tools": 45013,
"Progressive Weapons": 45012,
"Progressive Armor": 45014,
"Progressive Resource Crafting": 45001
}
progressive_names = {
"Progressive Tools": ["Wooden Pickaxe", "Stone Pickaxe", "Iron Pickaxe", "Diamond Pickaxe"],
"Progressive Weapons": ["Wooden Sword", "Stone Sword", "Iron Sword", "Diamond Sword"],
"Progressive Armor": ["Leather Tunic", "Iron Chestplate", "Diamond Chestplate"],
"Progressive Resource Crafting": ["Iron Ingot", "Iron Ingot", "Block of Iron"]
}
for item_name, item_id in progressive_items.items():
level = min(inventory[item_id], len(progressive_names[item_name]) - 1)
display_name = progressive_names[item_name][level]
base_name = item_name.split(maxsplit=1)[1].lower().replace(' ', '_')
display_data[base_name + "_url"] = icons[display_name]
# Multi-items
multi_items = {
"3 Ender Pearls": 45029,
"8 Netherite Scrap": 45015
}
for item_name, item_id in multi_items.items():
base_name = item_name.split()[-1].lower()
count = inventory[item_id]
if count >= 0:
display_data[base_name + "_count"] = count
# Victory condition
game_state = multisave.get("client_game_state", {}).get((team, player), 0)
display_data['game_finished'] = game_state == 30
# Turn location IDs into advancement tab counts
checked_locations = multisave.get("location_checks", {}).get((team, player), set())
lookup_name = lambda id: lookup_any_location_id_to_name[id]
location_info = {tab_name: {lookup_name(id): (id in checked_locations) for id in tab_locations}
for tab_name, tab_locations in minecraft_location_ids.items()}
checks_done = {tab_name: len([id for id in tab_locations if id in checked_locations])
for tab_name, tab_locations in minecraft_location_ids.items()}
checks_done['Total'] = len(checked_locations)
checks_in_area = {tab_name: len(tab_locations) for tab_name, tab_locations in minecraft_location_ids.items()}
checks_in_area['Total'] = sum(checks_in_area.values())
return render_template("minecraftTracker.html",
inventory=inventory, icons=icons,
acquired_items={lookup_any_item_id_to_name[id] for id in inventory if
id in lookup_any_item_id_to_name},
player=player, team=team, room=room, player_name=playerName,
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
**display_data)
def __renderOoTTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
inventory: Counter, team: int, player: int, playerName: str,
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict) -> str:
icons = {
"Fairy Ocarina": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/97/OoT_Fairy_Ocarina_Icon.png",
"Ocarina of Time": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Ocarina_of_Time_Icon.png",
"Slingshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/32/OoT_Fairy_Slingshot_Icon.png",
"Boomerang": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/d/d5/OoT_Boomerang_Icon.png",
"Bottle": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/fc/OoT_Bottle_Icon.png",
"Rutos Letter": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/OoT_Letter_Icon.png",
"Bombs": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/11/OoT_Bomb_Icon.png",
"Bombchus": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/36/OoT_Bombchu_Icon.png",
"Lens of Truth": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/05/OoT_Lens_of_Truth_Icon.png",
"Bow": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/9a/OoT_Fairy_Bow_Icon.png",
"Hookshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/77/OoT_Hookshot_Icon.png",
"Longshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/a/a4/OoT_Longshot_Icon.png",
"Megaton Hammer": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/93/OoT_Megaton_Hammer_Icon.png",
"Fire Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/1e/OoT_Fire_Arrow_Icon.png",
"Ice Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/3c/OoT_Ice_Arrow_Icon.png",
"Light Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/76/OoT_Light_Arrow_Icon.png",
"Dins Fire": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/d/da/OoT_Din%27s_Fire_Icon.png",
"Farores Wind": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/7a/OoT_Farore%27s_Wind_Icon.png",
"Nayrus Love": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/be/OoT_Nayru%27s_Love_Icon.png",
"Kokiri Sword": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/5/53/OoT_Kokiri_Sword_Icon.png",
"Biggoron Sword": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/2e/OoT_Giant%27s_Knife_Icon.png",
"Mirror Shield": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b0/OoT_Mirror_Shield_Icon_2.png",
"Goron Bracelet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b7/OoT_Goron%27s_Bracelet_Icon.png",
"Silver Gauntlets": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b9/OoT_Silver_Gauntlets_Icon.png",
"Golden Gauntlets": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/6/6a/OoT_Golden_Gauntlets_Icon.png",
"Goron Tunic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/1c/OoT_Goron_Tunic_Icon.png",
"Zora Tunic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/2c/OoT_Zora_Tunic_Icon.png",
"Silver Scale": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Silver_Scale_Icon.png",
"Gold Scale": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/95/OoT_Golden_Scale_Icon.png",
"Iron Boots": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/34/OoT_Iron_Boots_Icon.png",
"Hover Boots": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/22/OoT_Hover_Boots_Icon.png",
"Adults Wallet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/f9/OoT_Adult%27s_Wallet_Icon.png",
"Giants Wallet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/8/87/OoT_Giant%27s_Wallet_Icon.png",
"Small Magic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/9f/OoT3D_Magic_Jar_Icon.png",
"Large Magic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/3e/OoT3D_Large_Magic_Jar_Icon.png",
"Gerudo Membership Card": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Gerudo_Token_Icon.png",
"Gold Skulltula Token": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/47/OoT_Token_Icon.png",
"Triforce Piece": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/0b/SS_Triforce_Piece_Icon.png",
"Triforce": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/6/68/ALttP_Triforce_Title_Sprite.png",
"Zeldas Lullaby": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
"Eponas Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
"Sarias Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
"Suns Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
"Song of Time": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
"Song of Storms": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
"Minuet of Forest": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/e/e4/Green_Note.png",
"Bolero of Fire": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/f0/Red_Note.png",
"Serenade of Water": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/0f/Blue_Note.png",
"Requiem of Spirit": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/a/a4/Orange_Note.png",
"Nocturne of Shadow": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/97/Purple_Note.png",
"Prelude of Light": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/90/Yellow_Note.png",
"Small Key": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/e/e5/OoT_Small_Key_Icon.png",
"Boss Key": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/40/OoT_Boss_Key_Icon.png",
}
display_data = {}
# Determine display for progressive items
progressive_items = {
"Progressive Hookshot": 66128,
"Progressive Strength Upgrade": 66129,
"Progressive Wallet": 66133,
"Progressive Scale": 66134,
"Magic Meter": 66138,
"Ocarina": 66139,
}
progressive_names = {
"Progressive Hookshot": ["Hookshot", "Hookshot", "Longshot"],
"Progressive Strength Upgrade": ["Goron Bracelet", "Goron Bracelet", "Silver Gauntlets", "Golden Gauntlets"],
"Progressive Wallet": ["Adults Wallet", "Adults Wallet", "Giants Wallet", "Giants Wallet"],
"Progressive Scale": ["Silver Scale", "Silver Scale", "Gold Scale"],
"Magic Meter": ["Small Magic", "Small Magic", "Large Magic"],
"Ocarina": ["Fairy Ocarina", "Fairy Ocarina", "Ocarina of Time"]
}
for item_name, item_id in progressive_items.items():
level = min(inventory[item_id], len(progressive_names[item_name])-1)
display_name = progressive_names[item_name][level]
if item_name.startswith("Progressive"):
base_name = item_name.split(maxsplit=1)[1].lower().replace(' ', '_')
else:
base_name = item_name.lower().replace(' ', '_')
display_data[base_name+"_url"] = icons[display_name]
if base_name == "hookshot":
display_data['hookshot_length'] = {0: '', 1: 'H', 2: 'L'}.get(level)
if base_name == "wallet":
display_data['wallet_size'] = {0: '99', 1: '200', 2: '500', 3: '999'}.get(level)
# Determine display for bottles. Show letter if it's obtained, determine bottle count
bottle_ids = [66015, 66020, 66021, 66140, 66141, 66142, 66143, 66144, 66145, 66146, 66147, 66148]
display_data['bottle_count'] = min(sum(map(lambda item_id: inventory[item_id], bottle_ids)), 4)
display_data['bottle_url'] = icons['Rutos Letter'] if inventory[66021] > 0 else icons['Bottle']
# Determine bombchu display
display_data['has_bombchus'] = any(map(lambda item_id: inventory[item_id] > 0, [66003, 66106, 66107, 66137]))
# Multi-items
multi_items = {
"Gold Skulltula Token": 66091,
"Triforce Piece": 66202,
}
for item_name, item_id in multi_items.items():
base_name = item_name.split()[-1].lower()
count = inventory[item_id]
display_data[base_name+"_count"] = inventory[item_id]
# Gather dungeon locations
area_id_ranges = {
"Overworld": (67000, 67280),
"Deku Tree": (67281, 67303),
"Dodongo's Cavern": (67304, 67334),
"Jabu Jabu's Belly": (67335, 67359),
"Bottom of the Well": (67360, 67384),
"Forest Temple": (67385, 67420),
"Fire Temple": (67421, 67457),
"Water Temple": (67458, 67484),
"Shadow Temple": (67485, 67532),
"Spirit Temple": (67533, 67582),
"Ice Cavern": (67583, 67596),
"Gerudo Training Ground": (67597, 67635),
"Thieves' Hideout": (67259, 67263),
"Ganon's Castle": (67636, 67673),
}
def lookup_and_trim(id, area):
full_name = lookup_any_location_id_to_name[id]
if id == 67673:
return full_name[13:] # Ganons Tower Boss Key Chest
if area not in ["Overworld", "Thieves' Hideout"]:
# trim dungeon name. leaves an extra space that doesn't display, or trims fully for DC/Jabu/GC
return full_name[len(area):]
return full_name
checked_locations = multisave.get("location_checks", {}).get((team, player), set()).intersection(set(locations[player]))
location_info = {area: {lookup_and_trim(id, area): id in checked_locations for id in range(min_id, max_id+1) if id in locations[player]}
for area, (min_id, max_id) in area_id_ranges.items()}
checks_done = {area: len(list(filter(lambda x: x, location_info[area].values()))) for area in area_id_ranges}
checks_in_area = {area: len([id for id in range(min_id, max_id+1) if id in locations[player]])
for area, (min_id, max_id) in area_id_ranges.items()}
# Remove Thieves' Hideout checks from Overworld, since it's in the middle of the range
checks_in_area["Overworld"] -= checks_in_area["Thieves' Hideout"]
checks_done["Overworld"] -= checks_done["Thieves' Hideout"]
for loc in location_info["Thieves' Hideout"]:
del location_info["Overworld"][loc]
checks_done['Total'] = sum(checks_done.values())
checks_in_area['Total'] = sum(checks_in_area.values())
# Give skulltulas on non-tracked locations
non_tracked_locations = multisave.get("location_checks", {}).get((team, player), set()).difference(set(locations[player]))
for id in non_tracked_locations:
if "GS" in lookup_and_trim(id, ''):
display_data["token_count"] += 1
# Gather small and boss key info
small_key_counts = {
"Forest Temple": inventory[66175],
"Fire Temple": inventory[66176],
"Water Temple": inventory[66177],
"Spirit Temple": inventory[66178],
"Shadow Temple": inventory[66179],
"Bottom of the Well": inventory[66180],
"Gerudo Training Ground": inventory[66181],
"Thieves' Hideout": inventory[66182],
"Ganon's Castle": inventory[66183],
}
boss_key_counts = {
"Forest Temple": '' if inventory[66149] else '',
"Fire Temple": '' if inventory[66150] else '',
"Water Temple": '' if inventory[66151] else '',
"Spirit Temple": '' if inventory[66152] else '',
"Shadow Temple": '' if inventory[66153] else '',
"Ganon's Castle": '' if inventory[66154] else '',
}
# Victory condition
game_state = multisave.get("client_game_state", {}).get((team, player), 0)
display_data['game_finished'] = game_state == 30
return render_template("ootTracker.html",
inventory=inventory, player=player, team=team, room=room, player_name=playerName,
icons=icons, acquired_items={lookup_any_item_id_to_name[id] for id in inventory},
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
small_key_counts=small_key_counts, boss_key_counts=boss_key_counts,
**display_data)
def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
inventory: Counter, team: int, player: int, playerName: str, inventory: Counter, team: int, player: int, playerName: str,
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict[str, Any]) -> str: seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict[str, Any]) -> str:
@@ -808,13 +587,13 @@ def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations:
acquired_items = {lookup_any_item_id_to_name[id] for id in inventory if id in lookup_any_item_id_to_name} acquired_items = {lookup_any_item_id_to_name[id] for id in inventory if id in lookup_any_item_id_to_name}
options = {k for k, v in slot_data.items() if v} options = {k for k, v in slot_data.items() if v}
return render_template("timespinnerTracker.html", return render_template("trackers/" + "timespinnerTracker.html",
inventory=inventory, icons=icons, acquired_items=acquired_items, inventory=inventory, icons=icons, acquired_items=acquired_items,
player=player, team=team, room=room, player_name=playerName, player=player, team=team, room=room, player_name=playerName,
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info, checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
options=options, **display_data) options=options, **display_data)
def __renderSuperMetroidTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]], def __renderSuperMetroidTracker(multisave: Dict[str, Any], room: Room, locations: set,
inventory: Counter, team: int, player: int, playerName: str, inventory: Counter, team: int, player: int, playerName: str,
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict) -> str: seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict) -> str:
@@ -889,6 +668,7 @@ def __renderSuperMetroidTracker(multisave: Dict[str, Any], room: Room, locations
for item_name, item_id in multi_items.items(): for item_name, item_id in multi_items.items():
base_name = item_name.split()[0].lower() base_name = item_name.split()[0].lower()
count = inventory[item_id]
display_data[base_name+"_count"] = inventory[item_id] display_data[base_name+"_count"] = inventory[item_id]
# Victory condition # Victory condition
@@ -906,7 +686,7 @@ def __renderSuperMetroidTracker(multisave: Dict[str, Any], room: Room, locations
checks_in_area = {tab_name: len(tab_locations) for tab_name, tab_locations in supermetroid_location_ids.items()} checks_in_area = {tab_name: len(tab_locations) for tab_name, tab_locations in supermetroid_location_ids.items()}
checks_in_area['Total'] = sum(checks_in_area.values()) checks_in_area['Total'] = sum(checks_in_area.values())
return render_template("supermetroidTracker.html", return render_template("trackers/" + "supermetroidTracker.html",
inventory=inventory, icons=icons, inventory=inventory, icons=icons,
acquired_items={lookup_any_item_id_to_name[id] for id in inventory if acquired_items={lookup_any_item_id_to_name[id] for id in inventory if
id in lookup_any_item_id_to_name}, id in lookup_any_item_id_to_name},
@@ -914,7 +694,8 @@ def __renderSuperMetroidTracker(multisave: Dict[str, Any], room: Room, locations
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info, checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
**display_data) **display_data)
def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: set,
inventory: Counter, team: int, player: int, playerName: str, inventory: Counter, team: int, player: int, playerName: str,
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int]) -> str: seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int]) -> str:
@@ -929,11 +710,11 @@ def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dic
for order_index, networkItem in enumerate(ordered_items, start=1): for order_index, networkItem in enumerate(ordered_items, start=1):
player_received_items[networkItem.item] = order_index player_received_items[networkItem.item] = order_index
return render_template("genericTracker.html", return render_template("trackers/" + "genericTracker.html",
inventory=inventory, inventory=inventory,
player=player, team=team, room=room, player_name=playerName, player=player, team=team, room=room, player_name=playerName,
checked_locations=checked_locations, checked_locations=checked_locations,
not_checked_locations=set(locations[player]) - checked_locations, not_checked_locations=locations - checked_locations,
received_items=player_received_items) received_items=player_received_items)
@@ -975,9 +756,9 @@ def getTracker(tracker: UUID):
continue continue
item, recipient, flags = player_locations[location] item, recipient, flags = player_locations[location]
if recipient in names: if recipient in names:
attribute_item(inventory, team, recipient, item) attribute_item(inventory, team, recipient, item)
checks_done[team][player][player_location_to_area[player][location]] += 1 checks_done[team][player][player_location_to_area[player][location]] += 1
checks_done[team][player]["Total"] += 1 checks_done[team][player]["Total"] += 1
@@ -1021,7 +802,7 @@ def getTracker(tracker: UUID):
for (team, player), data in multisave.get("video", []): for (team, player), data in multisave.get("video", []):
video[(team, player)] = data video[(team, player)] = data
return render_template("tracker.html", inventory=inventory, get_item_name_from_id=lookup_any_item_id_to_name, return render_template("trackers/" + "multiworldTracker.html", inventory=inventory, get_item_name_from_id=lookup_any_item_id_to_name,
lookup_id_to_name=Items.lookup_id_to_name, player_names=player_names, lookup_id_to_name=Items.lookup_id_to_name, player_names=player_names,
tracking_names=tracking_names, tracking_ids=tracking_ids, room=room, icons=alttp_icons, tracking_names=tracking_names, tracking_ids=tracking_ids, room=room, icons=alttp_icons,
multi_items=multi_items, checks_done=checks_done, ordered_areas=ordered_areas, multi_items=multi_items, checks_done=checks_done, ordered_areas=ordered_areas,
@@ -1032,9 +813,6 @@ def getTracker(tracker: UUID):
game_specific_trackers: typing.Dict[str, typing.Callable] = { game_specific_trackers: typing.Dict[str, typing.Callable] = {
"Minecraft": __renderMinecraftTracker,
"Ocarina of Time": __renderOoTTracker,
"Timespinner": __renderTimespinnerTracker, "Timespinner": __renderTimespinnerTracker,
"A Link to the Past": __renderAlttpTracker,
"Super Metroid": __renderSuperMetroidTracker "Super Metroid": __renderSuperMetroidTracker
} }

View File

@@ -23,3 +23,10 @@ No metadata is specified yet.
## Extra Data ## Extra Data
The zip can contain arbitrary files in addition what was specified above. 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.

View File

@@ -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 Each location has a `name` and an `id` (a.k.a. "code" or "address"), is placed
in a Region and has access rules. in a Region and has access rules.
The name needs to be unique in each game, the ID needs to be unique across all The name needs to be unique in each game and must not be numeric (has to
games and is best in the same range as the item IDs. 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. 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. 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 priority and moved around to meet defined rules and accomplish progression
balancing. 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). Special items with ID `None` can mark events (read below).
Other classifications include 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. Conventionally, your world class is placed in that file.
World classes must inherit from the `World` class in `/worlds/AutoWorld.py`, 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. AP will pick up your world automatically due to the `AutoWorld` implementation.
### Requirements ### Requirements
If your world needs specific python packages, they can be listed in If your world needs specific python packages, they can be listed in
`world/[world_name]/requirements.txt`. `world/[world_name]/requirements.txt`. ModuleUpdate.py will automatically
See [pip documentation](https://pip.pypa.io/en/stable/cli/pip_install/#requirements-file-format) pick up and install them.
See [pip documentation](https://pip.pypa.io/en/stable/cli/pip_install/#requirements-file-format).
### Relative Imports ### 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` When imported names pile up it may be easier to use `from . import Options`
and access the variable as `Options.mygame_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 ### Your Item Type
Each world uses its own subclass of `BaseClasses.Item`. The constuctor can be 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 `default = <number>` to set the default selection. Aliases can be set by
defining a property `alias_<name> = <same number>`. 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 ```python
option_off = 0 option_off = 0
option_on = 1 option_on = 1
option_some = 2 option_some = 2
alias_false = 0 alias_disabled = 0
alias_true = 1 alias_enabled = 1
default = 0 default = 0
``` ```
@@ -323,7 +331,7 @@ mygame_options: typing.Dict[str, type(Option)] = {
```python ```python
# __init__.py # __init__.py
from ..AutoWorld import World from worlds.AutoWorld import World
from .Options import mygame_options # import the options dict from .Options import mygame_options # import the options dict
class MyGameWorld(World): 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 .Options import mygame_options # the options we defined earlier
from .Items import mygame_items # data used below to add items to the World from .Items import mygame_items # data used below to add items to the World
from .Locations import mygame_locations # same as above 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 BaseClasses import Region, Location, Entrance, Item, RegionType, ItemClassification
from Utils import get_options, output_path from Utils import get_options, output_path
@@ -553,7 +561,7 @@ def generate_basic(self) -> None:
### Setting Rules ### Setting Rules
```python ```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 from Items import get_item_type
def set_rules(self) -> None: 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. properties to the `BaseClasses.CollectionState` state object.
When importing a file that defines a class that inherits from 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 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 the name of the implementing world. This is due to sharing a namespace with all
other logic mixins. other logic mixins.
@@ -622,7 +630,7 @@ Please do this with caution and only when neccessary.
```python ```python
# Logic.py # Logic.py
from ..AutoWorld import LogicMixin from worlds.AutoWorld import LogicMixin
class MyGameLogic(LogicMixin): class MyGameLogic(LogicMixin):
def _mygame_has_key(self, world: MultiWorld, player: int): def _mygame_has_key(self, world: MultiWorld, player: int):
@@ -633,7 +641,7 @@ class MyGameLogic(LogicMixin):
```python ```python
# __init__.py # __init__.py
from ..generic.Rules import set_rule from worlds.generic.Rules import set_rule
import .Logic # apply the mixin by importing its file import .Logic # apply the mixin by importing its file
class MyGameWorld(World): class MyGameWorld(World):

View File

@@ -196,7 +196,7 @@ begin
begin begin
// Is the installed version at least the packaged one ? // Is the installed version at least the packaged one ?
Log('VC Redist x64 Version : found ' + strVersion); Log('VC Redist x64 Version : found ' + strVersion);
Result := (CompareStr(strVersion, 'v14.29.30037') < 0); Result := (CompareStr(strVersion, 'v14.32.31332') < 0);
end end
else else
begin begin

View File

@@ -371,13 +371,13 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
distribute_items_restrictive(multi_world) 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.assertFalse(locations[0].event)
self.assertEqual(locations[1].item, prog_items[0]) self.assertEqual(locations[1].item, prog_items[0])
self.assertTrue(locations[1].event) self.assertTrue(locations[1].event)
self.assertEqual(locations[2].item, prog_items[1]) self.assertEqual(locations[2].item, prog_items[1])
self.assertTrue(locations[2].event) 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) self.assertFalse(locations[3].event)
def test_excluded_distribute(self): def test_excluded_distribute(self):
@@ -500,8 +500,8 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
removed_item: list[Item] = [] removed_item: list[Item] = []
removed_location: list[Location] = [] removed_location: list[Location] = []
def fill_hook(progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool, restitempool, fill_locations): def fill_hook(progitempool, usefulitempool, filleritempool, fill_locations):
removed_item.append(restitempool.pop(0)) removed_item.append(filleritempool.pop(0))
removed_location.append(fill_locations.pop(0)) removed_location.append(fill_locations.pop(0))
multi_world.worlds[player1.id].fill_hook = fill_hook multi_world.worlds[player1.id].fill_hook = fill_hook

20
test/general/TestNames.py Normal file
View 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.")

View File

@@ -14,9 +14,20 @@ class TestFileGeneration(unittest.TestCase):
def testOptions(self): def testOptions(self):
WebHost.create_options_files() 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"))) 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): def testTutorial(self):
WebHost.create_ordered_tutorials_file() WebHost.create_ordered_tutorials_file()
self.assertTrue(os.path.exists(os.path.join(self.correct_path, "static", "generated", "tutorials.json"))) self.assertTrue(os.path.exists(os.path.join(self.correct_path, "static", "generated", "tutorials.json")))

View File

@@ -115,6 +115,16 @@ class WebWorld:
bug_report_page: Optional[str] bug_report_page: Optional[str]
"""display a link to a bug report page, most likely a link to a GitHub issue page.""" """display a link to a bug report page, most likely a link to a GitHub issue page."""
if TYPE_CHECKING:
from WebHostLib.tracker import PlayerTracker
else:
PlayerTracker = object
def modify_tracker(self, tracker: PlayerTracker):
"""Can use this to modify tracker data and add icons and regions dictionaries to
allow them to render on the game's tracker page."""
pass
class World(metaclass=AutoWorldRegister): class World(metaclass=AutoWorldRegister):
"""A World object encompasses a game's Items, Locations, Rules and additional data or functionality required. """A World object encompasses a game's Items, Locations, Rules and additional data or functionality required.
@@ -221,10 +231,8 @@ class World(metaclass=AutoWorldRegister):
@classmethod @classmethod
def fill_hook(cls, def fill_hook(cls,
progitempool: List["Item"], progitempool: List["Item"],
nonexcludeditempool: List["Item"], usefulitempool: List["Item"],
localrestitempool: Dict[int, List["Item"]], filleritempool: List["Item"],
nonlocalrestitempool: Dict[int, List["Item"]],
restitempool: List["Item"],
fill_locations: List["Location"]) -> None: fill_locations: List["Location"]) -> None:
"""Special method that gets called as part of distribute_items_restrictive (main fill). """Special method that gets called as part of distribute_items_restrictive (main fill).
This gets called once per present world type.""" This gets called once per present world type."""
@@ -242,6 +250,11 @@ class World(metaclass=AutoWorldRegister):
"""Fill in the slot_data field in the Connected network package.""" """Fill in the slot_data field in the Connected network package."""
return {} 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? def modify_multidata(self, multidata: Dict[str, Any]) -> None: # TODO: TypedDict for multidata?
"""For deeper modification of server multidata.""" """For deeper modification of server multidata."""
pass pass

View File

@@ -27,7 +27,8 @@ class WorldSource(typing.NamedTuple):
world_sources: typing.List[WorldSource] = [] world_sources: typing.List[WorldSource] = []
file: os.DirEntry # for me (Berserker) at least, PyCharm doesn't seem to infer the type correctly file: os.DirEntry # for me (Berserker) at least, PyCharm doesn't seem to infer the type correctly
for file in os.scandir(folder): 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(): if file.is_dir():
world_sources.append(WorldSource(file.name)) world_sources.append(WorldSource(file.name))
elif file.is_file() and file.name.endswith(".apworld"): elif file.is_file() and file.name.endswith(".apworld"):

View File

@@ -1,8 +1,9 @@
import logging import logging
from typing import Optional from typing import Optional, Union, List, Tuple, Callable, Dict
from BaseClasses import Boss from BaseClasses import Boss
from Fill import FillError from Fill import FillError
from .Options import Bosses
def BossFactory(boss: str, player: int) -> Optional[Boss]: 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) raise Exception('Unknown Boss: %s', boss)
def ArmosKnightsDefeatRule(state, player: int): def ArmosKnightsDefeatRule(state, player: int) -> bool:
# Magic amounts are probably a bit overkill # Magic amounts are probably a bit overkill
return ( return (
state.has_melee_weapon(player) or state.has_melee_weapon(player) or
@@ -25,7 +26,7 @@ def ArmosKnightsDefeatRule(state, player: int):
state.has('Red Boomerang', player)) state.has('Red Boomerang', player))
def LanmolasDefeatRule(state, player: int): def LanmolasDefeatRule(state, player: int) -> bool:
return ( return (
state.has_melee_weapon(player) or state.has_melee_weapon(player) or
state.has('Fire Rod', player) or state.has('Fire Rod', player) or
@@ -35,16 +36,16 @@ def LanmolasDefeatRule(state, player: int):
state.can_shoot_arrows(player)) state.can_shoot_arrows(player))
def MoldormDefeatRule(state, player: int): def MoldormDefeatRule(state, player: int) -> bool:
return state.has_melee_weapon(player) return state.has_melee_weapon(player)
def HelmasaurKingDefeatRule(state, player: int): def HelmasaurKingDefeatRule(state, player: int) -> bool:
# TODO: technically possible with the hammer # TODO: technically possible with the hammer
return state.has_sword(player) or state.can_shoot_arrows(player) 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): if not state.has('Hookshot', player):
return False return False
# TODO: ideally we would have a check for bow and silvers, which combined with the # 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)))) (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 ( return (
state.has_melee_weapon(player) or state.has_melee_weapon(player) or
(state.has('Fire Rod', player) and state.can_extend_magic(player, 10)) 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) 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 ( return (
( (
state.has('Fire Rod', player) or 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) 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)): if not (state.has('Fire Rod', player) and state.has('Ice Rod', player)):
return False return False
return state.has('Hammer', player) or state.has('Tempered Sword', player) or state.has('Golden Sword', player) or \ 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)) (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) 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]: if state.world.swordless[player]:
return state.has('Hammer', player) and \ return state.has('Hammer', player) and \
state.has_fire_source(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) 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), 'Armos Knights': ('Armos', ArmosKnightsDefeatRule),
'Lanmolas': ('Lanmola', LanmolasDefeatRule), 'Lanmolas': ('Lanmola', LanmolasDefeatRule),
'Moldorm': ('Moldorm', MoldormDefeatRule), 'Moldorm': ('Moldorm', MoldormDefeatRule),
@@ -147,7 +148,7 @@ boss_table = {
'Agahnim2': ('Agahnim2', AgahnimDefeatRule) 'Agahnim2': ('Agahnim2', AgahnimDefeatRule)
} }
boss_location_table = [ boss_location_table: List[Tuple[str, str]] = [
('Ganons Tower', 'top'), ('Ganons Tower', 'top'),
('Tower of Hera', None), ('Tower of Hera', None),
('Skull Woods', 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: def can_place_boss(boss: str, dungeon_name: str, level: Optional[str] = None) -> bool:
# blacklist approach # blacklist approach
if boss in {"Agahnim", "Agahnim2", "Ganon"}: 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 return True
restrictive_boss_locations = {}
restrictive_boss_locations: Dict[Tuple[str, str], bool] = {}
for location in boss_location_table: for location in boss_location_table:
restrictive_boss_locations[location] = not all(can_place_boss(boss, *location) restrictive_boss_locations[location] = not all(can_place_boss(boss, *location)
for boss in boss_table if not boss.startswith("Agahnim")) 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': if location == 'Ganons Tower' and world.mode[player] == 'inverted':
location = 'Inverted Ganons Tower' location = 'Inverted Ganons Tower'
logging.debug('Placing boss %s at %s', boss, location + (' (' + level + ')' if level else '')) logging.debug('Placing boss %s at %s', boss, location + (' (' + level + ')' if level else ''))
world.get_dungeon(location, player).bosses[level] = BossFactory(boss, player) 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 '') 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 return
# Most to least restrictive order # Most to least restrictive order
boss_locations = boss_location_table.copy() if not remaining_locations and not already_placed_bosses:
world.random.shuffle(boss_locations) remaining_locations = boss_location_table.copy()
boss_locations.sort(key= lambda location: -int(restrictive_boss_locations[location])) 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 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']] placeable_bosses = [boss for boss in all_bosses if boss not in ['Agahnim', 'Agahnim2', 'Ganon']]
shuffle_mode = world.boss_shuffle[player] if boss_shuffle == Bosses.option_basic or boss_shuffle == Bosses.option_full:
already_placed_bosses = [] if boss_shuffle == Bosses.option_basic: # vanilla bosses shuffled
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
bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm'] bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm']
else: # all bosses present, the three duplicates chosen at random else: # all bosses present, the three duplicates chosen at random
bosses = placeable_bosses + world.random.sample(placeable_bosses, 3) 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) logging.debug('Bosses chosen %s', bosses)
world.random.shuffle(bosses) world.random.shuffle(bosses)
for loc, level in boss_locations: for loc, level in remaining_locations:
for _ in range(len(bosses)): for _ in range(len(bosses)):
boss = bosses.pop() boss = bosses.pop()
if can_place_boss(boss, loc, level): if can_place_boss(boss, loc, level):
@@ -272,8 +289,8 @@ def place_bosses(world, player: int):
place_boss(world, player, boss, loc, level) place_boss(world, player, boss, loc, level)
elif shuffle_mode == "chaos": # all bosses chosen at random elif boss_shuffle == Bosses.option_chaos: # all bosses chosen at random
for loc, level in boss_locations: for loc, level in remaining_locations:
try: try:
boss = world.random.choice( boss = world.random.choice(
[b for b in placeable_bosses if can_place_boss(b, loc, level)]) [b for b in placeable_bosses if can_place_boss(b, loc, level)])
@@ -282,9 +299,9 @@ def place_bosses(world, player: int):
else: else:
place_boss(world, player, boss, loc, level) place_boss(world, player, boss, loc, level)
elif shuffle_mode == "singularity": elif boss_shuffle == Bosses.option_singularity:
primary_boss = world.random.choice(placeable_bosses) 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: if remaining_boss_locations:
# pick a boss to go into the remaining locations # pick a boss to go into the remaining locations
remaining_boss = world.random.choice([boss for boss in placeable_bosses if all( 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: if remaining_boss_locations:
raise Exception("Unfilled boss locations!") raise Exception("Unfilled boss locations!")
else: 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): def place_where_possible(world, player: int, boss: str, boss_locations) -> Tuple[List[Tuple[str, str]], List[str]]:
remainder = [] remainder: List[Tuple[str, str]] = []
placed_bosses = [] placed_bosses: List[str] = []
for loc, level in boss_locations: for loc, level in boss_locations:
# place that boss where it can go # place that boss where it can go
if can_place_boss(boss, loc, level): if can_place_boss(boss, loc, level):

View File

@@ -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) 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) 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: if swords:
sword = world.random.choice(swords) sword = world.random.choice(swords)
world.itempool.remove(sword) world.itempool.remove(sword)

View File

@@ -1,7 +1,7 @@
import typing import typing
from BaseClasses import MultiWorld 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): class Logic(Choice):
@@ -39,8 +39,6 @@ class OpenPyramid(Choice):
option_auto = 3 option_auto = 3
default = option_goal default = option_goal
alias_true = option_open
alias_false = option_closed
alias_yes = option_open alias_yes = option_open
alias_no = option_closed alias_no = option_closed
@@ -140,13 +138,143 @@ class WorldState(Choice):
option_inverted = 2 option_inverted = 2
class Bosses(Choice): class Bosses(TextChoice):
option_vanilla = 0 """Shuffles bosses around to different locations.
option_simple = 1 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_full = 2
option_chaos = 3 option_chaos = 3
option_singularity = 4 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): class Enemies(Choice):
option_vanilla = 0 option_vanilla = 0
@@ -159,8 +287,6 @@ class Progressive(Choice):
option_off = 0 option_off = 0
option_grouped_random = 1 option_grouped_random = 1
option_on = 2 option_on = 2
alias_false = 0
alias_true = 2
default = 2 default = 2
def want_progressives(self, random): def want_progressives(self, random):
@@ -168,8 +294,8 @@ class Progressive(Choice):
class Swordless(Toggle): class Swordless(Toggle):
"""No swords. Curtains in Skull Woods and Agahnim\'s """No swords. Curtains in Skull Woods and Agahnim's
Tower are removed, Agahnim\'s Tower barrier can be Tower are removed, Agahnim's Tower barrier can be
destroyed with hammer. Misery Mire and Turtle Rock destroyed with hammer. Misery Mire and Turtle Rock
can be opened without a sword. Hammer damages Ganon. can be opened without a sword. Hammer damages Ganon.
Ether and Bombos Tablet can be activated with Hammer Ether and Bombos Tablet can be activated with Hammer
@@ -202,8 +328,6 @@ class Hints(Choice):
option_on = 2 option_on = 2
option_full = 3 option_full = 3
default = 2 default = 2
alias_false = 0
alias_true = 2
class Scams(Choice): class Scams(Choice):
@@ -213,7 +337,6 @@ class Scams(Choice):
option_king_zora = 1 option_king_zora = 1
option_bottle_merchant = 2 option_bottle_merchant = 2
option_all = 3 option_all = 3
alias_false = 0
@property @property
def gives_king_zora_hint(self): def gives_king_zora_hint(self):
@@ -293,7 +416,6 @@ class HeartBeep(Choice):
option_half = 2 option_half = 2
option_quarter = 3 option_quarter = 3
option_off = 4 option_off = 4
alias_false = 4
class HeartColor(Choice): class HeartColor(Choice):
@@ -375,6 +497,7 @@ alttp_options: typing.Dict[str, type(Option)] = {
"hints": Hints, "hints": Hints,
"scams": Scams, "scams": Scams,
"restrict_dungeon_item_on_boss": RestrictBossItem, "restrict_dungeon_item_on_boss": RestrictBossItem,
"boss_shuffle": Bosses,
"pot_shuffle": PotShuffle, "pot_shuffle": PotShuffle,
"enemy_shuffle": EnemyShuffle, "enemy_shuffle": EnemyShuffle,
"killable_thieves": KillableThieves, "killable_thieves": KillableThieves,

View File

@@ -4,6 +4,10 @@ import typing
from BaseClasses import Region, Entrance, RegionType 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): def create_regions(world, player):
world.regions += [ world.regions += [

View File

@@ -12,7 +12,8 @@ from .InvertedRegions import create_inverted_regions, mark_dark_world_regions
from .ItemPool import generate_itempool, difficulties from .ItemPool import generate_itempool, difficulties
from .Items import item_init_table, item_name_groups, item_table, GetBeemizerItem from .Items import item_init_table, item_name_groups, item_table, GetBeemizerItem
from .Options import alttp_options, smallkey_shuffle 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, \ from .Rom import LocalRom, patch_rom, patch_race_rom, check_enemizer, patch_enemizer, apply_rom_settings, \
get_hash_string, get_base_rom_path, LttPDeltaPatch get_hash_string, get_base_rom_path, LttPDeltaPatch
from .Rules import set_rules 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], []) extras_list = sum(difficulties['normal'].extras[0:5], [])
class ALTTPWeb(WebWorld): class ALTTPWeb(WebWorld):
setup_en = Tutorial( setup_en = Tutorial(
"Multiworld Setup Tutorial", "Multiworld Setup Tutorial",
@@ -99,6 +101,232 @@ class ALTTPWeb(WebWorld):
tutorials = [setup_en, setup_de, setup_es, setup_fr, msu, msu_es, msu_fr, plando] tutorials = [setup_en, setup_de, setup_es, setup_fr, msu, msu_es, msu_fr, plando]
if typing.TYPE_CHECKING:
from WebHostLib.tracker import PlayerTracker
else:
PlayerTracker = object
def modify_tracker(self, tracker: PlayerTracker):
tracker.template = 'zeldaKeysTracker.html'
tracker.icons = {
"Blue Shield": r"https://www.zeldadungeon.net/wiki/images/8/85/Fighters-Shield.png",
"Red Shield": r"https://www.zeldadungeon.net/wiki/images/5/55/Fire-Shield.png",
"Mirror Shield": r"https://www.zeldadungeon.net/wiki/images/8/84/Mirror-Shield.png",
"Fighter Sword": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/4/40/SFighterSword.png?width=1920",
"Master Sword": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/6/65/SMasterSword.png?width=1920",
"Tempered Sword": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/9/92/STemperedSword.png?width=1920",
"Golden Sword": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/2/28/SGoldenSword.png?width=1920",
"Bow": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Bow_%26_Arrows_Sprite.png?version=5f85a70e6366bf473544ef93b274f74c",
"Silver Bow": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/6/65/Bow.png?width=1920",
"Green Mail": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c9/SGreenTunic.png?width=1920",
"Blue Mail": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/9/98/SBlueTunic.png?width=1920",
"Red Mail": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/7/74/SRedTunic.png?width=1920",
"Power Glove": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/f/f5/SPowerGlove.png?width=1920",
"Titan Mitts": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c1/STitanMitt.png?width=1920",
"Progressive Sword": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/cc/ALttP_Master_Sword_Sprite.png?version=55869db2a20e157cd3b5c8f556097725",
"Pegasus Boots": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Pegasus_Shoes_Sprite.png?version=405f42f97240c9dcd2b71ffc4bebc7f9",
"Progressive Glove": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c1/STitanMitt.png?width=1920",
"Flippers": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/4/4c/ZoraFlippers.png?width=1920",
"Moon Pearl": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Moon_Pearl_Sprite.png?version=d601542d5abcc3e006ee163254bea77e",
"Progressive Bow": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Bow_%26_Arrows_Sprite.png?version=cfb7648b3714cccc80e2b17b2adf00ed",
"Blue Boomerang": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c3/ALttP_Boomerang_Sprite.png?version=96127d163759395eb510b81a556d500e",
"Red Boomerang": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Magical_Boomerang_Sprite.png?version=47cddce7a07bc3e4c2c10727b491f400",
"Hookshot": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/24/Hookshot.png?version=c90bc8e07a52e8090377bd6ef854c18b",
"Mushroom": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/35/ALttP_Mushroom_Sprite.png?version=1f1acb30d71bd96b60a3491e54bbfe59",
"Magic Powder": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Powder_Sprite.png?version=c24e38effbd4f80496d35830ce8ff4ec",
"Fire Rod": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d6/FireRod.png?version=6eabc9f24d25697e2c4cd43ddc8207c0",
"Ice Rod": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d7/ALttP_Ice_Rod_Sprite.png?version=1f944148223d91cfc6a615c92286c3bc",
"Bombos": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/8c/ALttP_Bombos_Medallion_Sprite.png?version=f4d6aba47fb69375e090178f0fc33b26",
"Ether": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/Ether.png?version=34027651a5565fcc5a83189178ab17b5",
"Quake": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/56/ALttP_Quake_Medallion_Sprite.png?version=efd64d451b1831bd59f7b7d6b61b5879",
"Lamp": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Lantern_Sprite.png?version=e76eaa1ec509c9a5efb2916698d5a4ce",
"Hammer": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d1/ALttP_Hammer_Sprite.png?version=e0adec227193818dcaedf587eba34500",
"Shovel": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c4/ALttP_Shovel_Sprite.png?version=e73d1ce0115c2c70eaca15b014bd6f05",
"Flute": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/db/Flute.png?version=ec4982b31c56da2c0c010905c5c60390",
"Bug Catching Net": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/54/Bug-CatchingNet.png?version=4d40e0ee015b687ff75b333b968d8be6",
"Book of Mudora": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/22/ALttP_Book_of_Mudora_Sprite.png?version=11e4632bba54f6b9bf921df06ac93744",
"Bottle": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ef/ALttP_Magic_Bottle_Sprite.png?version=fd98ab04db775270cbe79fce0235777b",
"Cane of Somaria": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e1/ALttP_Cane_of_Somaria_Sprite.png?version=8cc1900dfd887890badffc903bb87943",
"Cane of Byrna": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Cane_of_Byrna_Sprite.png?version=758b607c8cbe2cf1900d42a0b3d0fb54",
"Cape": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1c/ALttP_Magic_Cape_Sprite.png?version=6b77f0d609aab0c751307fc124736832",
"Magic Mirror": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Mirror_Sprite.png?version=e035dbc9cbe2a3bd44aa6d047762b0cc",
"Triforce": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/4/4e/TriforceALttPTitle.png?version=dc398e1293177581c16303e4f9d12a48",
"Small Key": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/f/f1/ALttP_Small_Key_Sprite.png?version=4f35d92842f0de39d969181eea03774e",
"Big Key": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Big_Key_Sprite.png?version=136dfa418ba76c8b4e270f466fc12f4d",
"Chest": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/7/73/ALttP_Treasure_Chest_Sprite.png?version=5f530ecd98dcb22251e146e8049c0dda",
"Light World": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e7/ALttP_Soldier_Green_Sprite.png?version=d650d417934cd707a47e496489c268a6",
"Dark World": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/94/ALttP_Moblin_Sprite.png?version=ebf50e33f4657c377d1606bcc0886ddc",
"Hyrule Castle": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d3/ALttP_Ball_and_Chain_Trooper_Sprite.png?version=1768a87c06d29cc8e7ddd80b9fa516be",
"Agahnims Tower": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1e/ALttP_Agahnim_Sprite.png?version=365956e61b0c2191eae4eddbe591dab5",
"Desert Palace": r"https://www.zeldadungeon.net/wiki/images/2/25/Lanmola-ALTTP-Sprite.png",
"Eastern Palace": r"https://www.zeldadungeon.net/wiki/images/d/dc/RedArmosKnight.png",
"Tower of Hera": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/ALttP_Moldorm_Sprite.png?version=c588257bdc2543468e008a6b30f262a7",
"Palace of Darkness": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Helmasaur_King_Sprite.png?version=ab8a4a1cfd91d4fc43466c56cba30022",
"Swamp Palace": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/7/73/ALttP_Arrghus_Sprite.png?version=b098be3122e53f751b74f4a5ef9184b5",
"Skull Woods": r"https://alttp-wiki.net/images/6/6a/Mothula.png",
"Thieves Town": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/86/ALttP_Blind_the_Thief_Sprite.png?version=3833021bfcd112be54e7390679047222",
"Ice Palace": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Kholdstare_Sprite.png?version=e5a1b0e8b2298e550d85f90bf97045c0",
"Misery Mire": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/85/ALttP_Vitreous_Sprite.png?version=92b2e9cb0aa63f831760f08041d8d8d8",
"Turtle Rock": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/91/ALttP_Trinexx_Sprite.png?version=0cc867d513952aa03edd155597a0c0be",
"Ganons Tower": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Ganon_Sprite.png?version=956f51f054954dfff53c1a9d4f929c74"
}
tracker.regions = {
'Light World': [
'Lost Woods Hideout', 'Lumberjack Tree', 'Mushroom', 'Master Sword Pedestal', 'Bottle Merchant', 'Flute Spot',
'Blind\'s Hideout - Top', 'Blind\'s Hideout - Left', 'Blind\'s Hideout - Right', 'Blind\'s Hideout - Far Left', 'Blind\'s Hideout - Far Right',
'Link\'s House', 'Link\'s Uncle', 'Secret Passage',
'King Zora', 'Zora\'s Ledge', 'Waterfall Fairy - Left', 'Waterfall Fairy - Right',
'King\'s Tomb', 'Graveyard Cave', 'Bonk Rock Cave',
'Sunken Treasure', 'Floodgate Chest', 'Hobo', 'Ice Rod Cave', 'Lake Hylia Island',
'Kakariko Tavern', 'Chicken House', 'Sick Kid',
'Blacksmith', 'Purple Chest', 'Magic Bat',
'Aginah\'s Cave', 'Cave 45', 'Checkerboard Cave',
'Sahasrahla\'s Hut - Left', 'Sahasrahla\'s Hut - Middle', 'Sahasrahla\'s Hut - Right', 'Sahasrahla',
'Kakariko Well - Top', 'Kakariko Well - Left', 'Kakariko Well - Middle', 'Kakariko Well - Right', 'Kakariko Well - Bottom',
'Mini Moldorm Cave - Far Left', 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right', 'Mini Moldorm Cave - Far Right', 'Mini Moldorm Cave - Generous Guy',
'Library', 'Maze Race', 'Potion Shop', 'Desert Ledge',
'Old Man', 'Spectacle Rock',
'Paradox Cave Lower - Far Left', 'Paradox Cave Lower - Left', 'Paradox Cave Lower - Right', 'Paradox Cave Lower - Far Right', 'Paradox Cave Lower - Middle',
'Paradox Cave Upper - Left', 'Paradox Cave Upper - Right',
'Spiral Cave', 'Ether Tablet'
],
'Dark World': [
'Pyramid', 'Catfish', 'Pyramid Fairy - Left', 'Pyramid Fairy - Right',
'Stumpy', 'Digging Game',
'Bombos Tablet',
'Hype Cave - Top', 'Hype Cave - Middle Right', 'Hype Cave - Middle Left', 'Hype Cave - Bottom', 'Hype Cave - Generous Guy',
'Peg Cave', 'Brewery', 'C-Shaped House', 'Chest Game',
'Bumper Cave Ledge',
'Mire Shed - Left', 'Mire Shed - Right',
'Superbunny Cave - Top', 'Superbunny Cave - Bottom',
'Spike Cave', 'Floating Island', 'Mimic Cave',
'Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left',
],
'Desert Palace': [
'Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest',
'Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest',
'Desert Palace - Boss'
],
'Eastern Palace': [
'Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest', 'Eastern Palace - Cannonball Chest',
'Eastern Palace - Big Key Chest', 'Eastern Palace - Map Chest', 'Eastern Palace - Boss'
],
'Hyrule Castle': [
'Hyrule Castle - Map Chest', 'Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Zelda\'s Chest',
'Sewers - Dark Cross', 'Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle', 'Sewers - Secret Room - Right',
'Sanctuary'
],
'Agahnims Tower': [
'Castle Tower - Room 03', 'Castle Tower - Dark Maze'
],
'Tower of Hera': [
'Tower of Hera - Basement Cage', 'Tower of Hera - Map Chest', 'Tower of Hera - Big Key Chest',
'Tower of Hera - Compass Chest', 'Tower of Hera - Big Chest', 'Tower of Hera - Boss'
],
'Swamp Palace': [
'Swamp Palace - Entrance', 'Swamp Palace - Map Chest',
'Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest',
'Swamp Palace - Big Key Chest', 'Swamp Palace - West Chest',
'Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right',
'Swamp Palace - Waterfall Room', 'Swamp Palace - Boss'
],
'Thieves Town': [
'Thieves\' Town - Big Key Chest', 'Thieves\' Town - Map Chest', 'Thieves\' Town - Compass Chest', 'Thieves\' Town - Ambush Chest',
'Thieves\' Town - Attic', 'Thieves\' Town - Big Chest', 'Thieves\' Town - Blind\'s Cell', 'Thieves\' Town - Boss'
],
'Skull Woods': [
'Skull Woods - Map Chest', 'Skull Woods - Pinball Room',
'Skull Woods - Compass Chest', 'Skull Woods - Pot Prison',
'Skull Woods - Big Chest',
'Skull Woods - Big Key Chest',
'Skull Woods - Bridge Room', 'Skull Woods - Boss'
],
'Ice Palace': [
'Ice Palace - Compass Chest', 'Ice Palace - Freezor Chest', 'Ice Palace - Big Chest', 'Ice Palace - Iced T Room',
'Ice Palace - Spike Room', 'Ice Palace - Big Key Chest', 'Ice Palace - Map Chest', 'Ice Palace - Boss'
],
'Misery Mire': [
'Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby', 'Misery Mire - Bridge Chest', 'Misery Mire - Spike Chest',
'Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest', 'Misery Mire - Boss'
],
'Turtle Rock': [
'Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left', 'Turtle Rock - Roller Room - Right',
'Turtle Rock - Chain Chomps', 'Turtle Rock - Big Key Chest', 'Turtle Rock - Big Chest',
'Turtle Rock - Crystaroller Room',
'Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right', 'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right',
'Turtle Rock - Boss'
],
'Palace of Darkness': [
'Palace of Darkness - Shooter Room', 'Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement',
'Palace of Darkness - Big Key Chest',
'Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Map Chest',
'Palace of Darkness - Compass Chest', 'Palace of Darkness - Dark Basement - Left', 'Palace of Darkness - Dark Basement - Right',
'Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Big Chest',
'Palace of Darkness - Harmless Hellway', 'Palace of Darkness - Boss'
],
'Ganons Tower': [
'Ganons Tower - Bob\'s Torch', 'Ganons Tower - Hope Room - Left', 'Ganons Tower - Hope Room - Right',
'Ganons Tower - Tile Room',
'Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right', 'Ganons Tower - Compass Room - Bottom Left', 'Ganons Tower - Compass Room - Bottom Right',
'Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right', 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right',
'Ganons Tower - Map Chest', 'Ganons Tower - Firesnake Room',
'Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right', 'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right',
'Ganons Tower - Bob\'s Chest',
'Ganons Tower - Big Chest', 'Ganons Tower - Big Key Room - Left', 'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Big Key Chest',
]
}
tracker.progressive_items = [
'Progressive Sword',
'Progressive Shield',
'Progressive Mail',
'Progressive Bow',
'Progressive Boomerang',
'Hookshot',
'Magic Powder',
'Mushroom',
'Bottle',
'Lamp',
'Progressive Glove',
'Flippers',
'Moon Pearl',
'Bombos',
'Ether',
'Quake',
'Fire Rod',
'Ice Rod',
'Hammer',
'Book of Mudora',
'Shovel',
'Flute',
'Bug Catching Net',
'Cane of Somaria',
'Cane of Byrna',
'Cape',
'Magic Mirror',
'Small Key',
'Big Key'
]
tracker.progressive_names = {
'Progressive Bow': ['Bow', 'Silver Arrows', 'Silver Bow', 'Progressive Bow (Alt)'],
'Bottle': ['Bottle (Red Potion)', 'Bottle (Green Potion)', 'Bottle (Blue Potion)', 'Bottle (Fairy)', 'Bottle (Bee)', 'Bottle (Good Bee)'],
'Progressive Sword': ['Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword'],
'Progressive Glove': ['Power Glove', 'Titans Mitts'],
'Progressive Shield': ['Blue Shield', 'Red Shield', 'Mirror Shield'],
'Progressive Boomerang': ['Red Boomerang', 'Blue Boomerang'],
'Progressive Mail': ['Green Mail', 'Blue Mail', 'Red Mail'],
'Small Key': [f'Small Key ({region})' for region in tracker.regions.keys() if region not in {'Light World', 'Dark World'}],
'Big Key': [f'Big Key ({region})' for region in tracker.regions.keys() if region not in {'Light World', 'Dark World'}],
}
tracker.region_keys = {
region: [f'Small Key ({region})', f'Big Key ({region})'] for region in tracker.regions.keys() if region not in {'Light World', 'Dark World'}
}
class ALTTPWorld(World): class ALTTPWorld(World):
""" """
@@ -349,7 +577,7 @@ class ALTTPWorld(World):
def use_enemizer(self): def use_enemizer(self):
world = self.world world = self.world
player = self.player 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.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default'
or world.pot_shuffle[player] or world.bush_shuffle[player] or world.pot_shuffle[player] or world.bush_shuffle[player]
or world.killable_thieves[player]) or world.killable_thieves[player])
@@ -410,6 +638,20 @@ class ALTTPWorld(World):
finally: finally:
self.rom_name_available_event.set() # make sure threading continues and errors are collected 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): def modify_multidata(self, multidata: dict):
import base64 import base64
# wait for self.rom_name to be available. # wait for self.rom_name to be available.
@@ -424,8 +666,7 @@ class ALTTPWorld(World):
return ALttPItem(name, self.player, **item_init_table[name]) return ALttPItem(name, self.player, **item_init_table[name])
@classmethod @classmethod
def stage_fill_hook(cls, world, progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool, def stage_fill_hook(cls, world, progitempool, usefulitempool, filleritempool, fill_locations):
restitempool, fill_locations):
trash_counts = {} trash_counts = {}
standard_keyshuffle_players = set() standard_keyshuffle_players = set()
for player in world.get_game_players("A Link to the Past"): for player in world.get_game_players("A Link to the Past"):
@@ -472,26 +713,15 @@ class ALTTPWorld(World):
for player, trash_count in trash_counts.items(): for player, trash_count in trash_counts.items():
gtower_locations = locations_mapping[player] gtower_locations = locations_mapping[player]
world.random.shuffle(gtower_locations) 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() 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 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) world.push_item(spot_to_fill, item_to_place, False)
fill_locations.remove(spot_to_fill) # very slow, unfortunately fill_locations.remove(spot_to_fill) # very slow, unfortunately
trash_count -= 1 trash_count -= 1
def get_filler_item_name(self) -> str: def get_filler_item_name(self) -> str:
if self.world.goal[self.player] == "icerodhunt": if self.world.goal[self.player] == "icerodhunt":
item = "Nothing" item = "Nothing"

View File

@@ -26,10 +26,14 @@
- Example: `Trinexx` - Example: `Trinexx`
- Takes a particular boss and places that boss in any remaining slots in which this boss can function. - 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. - 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: - Boss Shuffle:
- Example: `simple` - Example: `basic`
- Runs a particular boss shuffle mode to finish construction instead of vanilla placement, typically used as - Runs a particular boss shuffle mode to finish construction instead of vanilla placement, typically used as
a last instruction. 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 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) - [Available Arenas](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Bosses.py#L150)

View File

@@ -34,7 +34,8 @@ base_info = {
"factorio_version": "1.1", "factorio_version": "1.1",
"dependencies": [ "dependencies": [
"base >= 1.1.0", "base >= 1.1.0",
"? science-not-invited" "? science-not-invited",
"? factory-levels"
] ]
} }

View File

@@ -137,8 +137,6 @@ class Progressive(Choice):
option_off = 0 option_off = 0
option_grouped_random = 1 option_grouped_random = 1
option_on = 2 option_on = 2
alias_false = 0
alias_true = 2
default = 2 default = 2
def want_progressives(self, random): def want_progressives(self, random):

View File

@@ -7,7 +7,8 @@
"description": "Integration client for the Archipelago Randomizer", "description": "Integration client for the Archipelago Randomizer",
"factorio_version": "1.1", "factorio_version": "1.1",
"dependencies": [ "dependencies": [
"base >= 1.1.0", "base >= 1.1.0",
"? science-not-invited" "? science-not-invited",
] "? factory-levels"
]
} }

View File

@@ -183,6 +183,18 @@ end
data.raw["assembling-machine"]["assembling-machine-1"].crafting_categories = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-3"].crafting_categories) data.raw["assembling-machine"]["assembling-machine-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-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) 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 data.raw["ammo"]["artillery-shell"].stack_size = 10
{# each randomized tech gets set to be invisible, with new nodes added that trigger those #} {# each randomized tech gets set to be invisible, with new nodes added that trigger those #}

View File

@@ -409,7 +409,6 @@ class DeathLink(Choice):
shade: DeathLink functions like a normal death if you do not already have a shade, shadeless otherwise. shade: DeathLink functions like a normal death if you do not already have a shade, shadeless otherwise.
""" """
option_off = 0 option_off = 0
alias_false = 0
alias_no = 0 alias_no = 0
alias_true = 1 alias_true = 1
alias_on = 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 These costs can be in Geo (except Grubfather, Seer and Eggshop), Grubs, Charms, Essence and/or Rancid Eggs
""" """
option_off = 0 option_off = 0
alias_false = 0
alias_no = 0 alias_no = 0
option_on = 1 option_on = 1
alias_true = 1
alias_yes = 1 alias_yes = 1
option_shopsonly = 2 option_shopsonly = 2
option_notshops = 3 option_notshops = 3

View File

@@ -49,6 +49,79 @@ class MinecraftWebWorld(WebWorld):
tutorials = [setup, setup_es, setup_sv] tutorials = [setup, setup_es, setup_sv]
def modify_tracker(self, tracker):
tracker.icons = {
"Wooden Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d2/Wooden_Pickaxe_JE3_BE3.png",
"Stone Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c4/Stone_Pickaxe_JE2_BE2.png",
"Iron Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d1/Iron_Pickaxe_JE3_BE2.png",
"Diamond Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/e/e7/Diamond_Pickaxe_JE3_BE3.png",
"Wooden Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d5/Wooden_Sword_JE2_BE2.png",
"Stone Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b1/Stone_Sword_JE2_BE2.png",
"Iron Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/8/8e/Iron_Sword_JE2_BE2.png",
"Diamond Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/4/44/Diamond_Sword_JE3_BE3.png",
"Leather Tunic": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b7/Leather_Tunic_JE4_BE2.png",
"Iron Chestplate": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/31/Iron_Chestplate_JE2_BE2.png",
"Diamond Chestplate": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/e/e0/Diamond_Chestplate_JE3_BE2.png",
"Iron Ingot": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fc/Iron_Ingot_JE3_BE2.png",
"Block of Iron": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/7e/Block_of_Iron_JE4_BE3.png",
"Brewing": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b3/Brewing_Stand_%28empty%29_JE10.png",
"Ender Pearls": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/f6/Ender_Pearl_JE3_BE2.png",
"Bucket": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fc/Bucket_JE2_BE2.png",
"Archery": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/a/ab/Bow_%28Pull_2%29_JE1_BE1.png",
"Shield": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c6/Shield_JE2_BE1.png",
"Bed": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/6/6a/Red_Bed_%28N%29.png",
"Netherite Scrap": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/33/Netherite_Scrap_JE2_BE1.png",
"Flint and Steel": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/94/Flint_and_Steel_JE4_BE2.png",
"Enchanting": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/31/Enchanting_Table.gif",
"Fishing Rod": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/7f/Fishing_Rod_JE2_BE2.png",
"Campfire": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/91/Campfire_JE2_BE2.gif",
"Bottle": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/75/Water_Bottle_JE2_BE2.png",
"Spyglass": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c1/Spyglass_JE2_BE1.png",
}
tracker.progressive_items = [
"Progressive Tools", "Progressive Weapons", "Progressive Armor", "Progressive Resource Crafting",
"Brewing", "Ender Pearls", "Bucket", "Archery", "Shield", "Bed", "Bottle", "Netherite Scrap",
"Flint and Steel", "Enchanting", "Fishing Rod", "Campfire", "Spyglass"
]
tracker.progressive_names = {
"Progressive Tools": ["Wooden Pickaxe", "Stone Pickaxe", "Iron Pickaxe", "Diamond Pickaxe"],
"Progressive Weapons": ["Wooden Sword", "Stone Sword", "Iron Sword", "Diamond Sword"],
"Progressive Armor": ["Leather Tunic", "Iron Chestplate", "Diamond Chestplate"],
"Progressive Resource Crafting": ["Iron Ingot", "Iron Ingot", "Block of Iron"]
}
tracker.regions = {
"Story": ["Minecraft", "Stone Age", "Getting an Upgrade", "Acquire Hardware", "Suit Up",
"Not Today, Thank You", "Isn't It Iron Pick", "Diamonds!", "Cover Me With Diamonds", "Enchanter",
"Hot Stuff", "Ice Bucket Challenge", "We Need to Go Deeper", "Zombie Doctor", "Eye Spy", "The End?"],
"Nether": ["Nether", "Return to Sender", "Uneasy Alliance", "Those Were the Days", "War Pigs",
"Hidden in the Depths", "Country Lode, Take Me Home", "Cover Me in Debris", "Subspace Bubble",
"A Terrible Fortress", "Spooky Scary SKeleton", "This Boat Has Legs", "Hot Tourist Destinations"],
"The End": ["The End", "Free the End", "The Next Generation", "Remote Getaway",
"The City at the End of the Game", "Sky's the Limit", "Great View From Up Here",
"The End... Again...", "You Need a Mint"],
"Adventure": ["Adventure", "Voluntary Exile", "Is It a Bird?", "Is It a Balloon?", "Is It a Plane?",
"Hero of the Village", "Monster Hunter", "A Throwaway Joke", "Very Very Frightening",
"Take Aim", "Sniper Duel", "Bullseye", "Monsters Hunted", "Postmortal", "What a Deal!",
"Hired Help", "Sticky Situation", "Ol' Betsy", "Two Birds, One Arrow",
"Who's the Pillager Now?", "Arbalistic", "Sweet Dreams", "Adventuring Time", "Surge Protector",
"Light as a Rabbit"],
"Husbandry": ["Husbandry", "Bee Our Guest", "The Parrots and the Bats", "Two by Two", "Best Friends Forever",
"A Complete Catalogue", "Fishy Business", "Tactical Fishing", "Total Beelocation",
"A Seedy Place", "A Balanced Diet", "Serious Dedication", "Whatever Floats Your Goat!",
"Glow and Behold!", "Wax On", "Wax Off", "The Cutest Predator",
"The Healing Power of Friendship"],
"Archipelago": ["Getting Wood", "Time to Mine!", "Hot Topic", "Bake Bread", "The Lie", "On a Rail",
"Time to Strike!", "Cow Tipper", "When Pigs Fly", "Overkill", "Librarian", "Overpowered"]
}
class MinecraftWorld(World): class MinecraftWorld(World):
""" """

View File

@@ -101,7 +101,6 @@ class InteriorEntrances(Choice):
option_off = 0 option_off = 0
option_simple = 1 option_simple = 1
option_all = 2 option_all = 2
alias_false = 0
alias_true = 2 alias_true = 2
@@ -141,7 +140,6 @@ class MixEntrancePools(Choice):
option_off = 0 option_off = 0
option_indoor = 1 option_indoor = 1
option_all = 2 option_all = 2
alias_false = 0
class DecoupleEntrances(Toggle): class DecoupleEntrances(Toggle):
@@ -308,7 +306,6 @@ class ShopShuffle(Choice):
option_off = 0 option_off = 0
option_fixed_number = 1 option_fixed_number = 1
option_random_number = 2 option_random_number = 2
alias_false = 0
class ShopSlots(Range): class ShopSlots(Range):
@@ -326,7 +323,6 @@ class TokenShuffle(Choice):
option_dungeons = 1 option_dungeons = 1
option_overworld = 2 option_overworld = 2
option_all = 3 option_all = 3
alias_false = 0
class ScrubShuffle(Choice): class ScrubShuffle(Choice):
@@ -336,7 +332,6 @@ class ScrubShuffle(Choice):
option_low = 1 option_low = 1
option_regular = 2 option_regular = 2
option_random_prices = 3 option_random_prices = 3
alias_false = 0
alias_affordable = 1 alias_affordable = 1
alias_expensive = 2 alias_expensive = 2
@@ -569,7 +564,6 @@ class Hints(Choice):
option_agony = 2 option_agony = 2
option_always = 3 option_always = 3
default = 3 default = 3
alias_false = 0
class MiscHints(DefaultOnToggle): class MiscHints(DefaultOnToggle):
@@ -673,8 +667,6 @@ class IceTraps(Choice):
option_mayhem = 3 option_mayhem = 3
option_onslaught = 4 option_onslaught = 4
default = 1 default = 1
alias_false = 0
alias_true = 2
alias_extra = 2 alias_extra = 2
@@ -742,7 +734,6 @@ class Music(Choice):
option_normal = 0 option_normal = 0
option_off = 1 option_off = 1
option_randomized = 2 option_randomized = 2
alias_false = 1
class BackgroundMusic(Music): class BackgroundMusic(Music):

View File

@@ -9,7 +9,7 @@ from .Location import OOTLocation, LocationFactory, location_name_to_id
from .Entrance import OOTEntrance from .Entrance import OOTEntrance
from .EntranceShuffle import shuffle_random_entrances, entrance_shuffle_table, EntranceShuffleError from .EntranceShuffle import shuffle_random_entrances, entrance_shuffle_table, EntranceShuffleError
from .Items import OOTItem, item_table, oot_data_to_ap_id, oot_is_item_of_type from .Items import OOTItem, item_table, oot_data_to_ap_id, oot_is_item_of_type
from .ItemPool import generate_itempool, add_dungeon_items, get_junk_item, get_junk_pool from .ItemPool import generate_itempool, add_dungeon_items, get_junk_item, get_junk_pool, normal_bottles
from .Regions import OOTRegion, TimeOfDay from .Regions import OOTRegion, TimeOfDay
from .Rules import set_rules, set_shop_rules, set_entrances_based_rules from .Rules import set_rules, set_shop_rules, set_entrances_based_rules
from .RuleParser import Rule_AST_Transformer from .RuleParser import Rule_AST_Transformer
@@ -87,6 +87,133 @@ class OOTWeb(WebWorld):
tutorials = [setup, setup_es] tutorials = [setup, setup_es]
def modify_tracker(self, tracker):
tracker.template = 'zeldaKeysTracker.html'
tracker.icons = {
"Fairy Ocarina": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/97/OoT_Fairy_Ocarina_Icon.png",
"Ocarina of Time": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Ocarina_of_Time_Icon.png",
"Slingshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/32/OoT_Fairy_Slingshot_Icon.png",
"Boomerang": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/d/d5/OoT_Boomerang_Icon.png",
"Bottle": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/fc/OoT_Bottle_Icon.png",
"Rutos Letter": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/OoT_Letter_Icon.png",
"Bombs": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/11/OoT_Bomb_Icon.png",
"Bombchus": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/36/OoT_Bombchu_Icon.png",
"Lens of Truth": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/05/OoT_Lens_of_Truth_Icon.png",
"Bow": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/9a/OoT_Fairy_Bow_Icon.png",
"Hookshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/77/OoT_Hookshot_Icon.png",
"Longshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/a/a4/OoT_Longshot_Icon.png",
"Megaton Hammer": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/93/OoT_Megaton_Hammer_Icon.png",
"Fire Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/1e/OoT_Fire_Arrow_Icon.png",
"Ice Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/3c/OoT_Ice_Arrow_Icon.png",
"Light Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/76/OoT_Light_Arrow_Icon.png",
"Din's Fire": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/d/da/OoT_Din%27s_Fire_Icon.png",
"Farore's Wind": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/7a/OoT_Farore%27s_Wind_Icon.png",
"Nayru's Love": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/be/OoT_Nayru%27s_Love_Icon.png",
"Kokiri Sword": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/5/53/OoT_Kokiri_Sword_Icon.png",
"Biggoron Sword": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/2e/OoT_Giant%27s_Knife_Icon.png",
"Mirror Shield": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b0/OoT_Mirror_Shield_Icon_2.png",
"Goron Bracelet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b7/OoT_Goron%27s_Bracelet_Icon.png",
"Silver Gauntlets": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b9/OoT_Silver_Gauntlets_Icon.png",
"Golden Gauntlets": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/6/6a/OoT_Golden_Gauntlets_Icon.png",
"Goron Tunic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/1c/OoT_Goron_Tunic_Icon.png",
"Zora Tunic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/2c/OoT_Zora_Tunic_Icon.png",
"Silver Scale": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Silver_Scale_Icon.png",
"Gold Scale": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/95/OoT_Golden_Scale_Icon.png",
"Iron Boots": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/34/OoT_Iron_Boots_Icon.png",
"Hover Boots": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/22/OoT_Hover_Boots_Icon.png",
"Adults Wallet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/f9/OoT_Adult%27s_Wallet_Icon.png",
"Giants Wallet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/8/87/OoT_Giant%27s_Wallet_Icon.png",
"Small Magic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/9f/OoT3D_Magic_Jar_Icon.png",
"Large Magic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/3e/OoT3D_Large_Magic_Jar_Icon.png",
"Gerudo Membership Card": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Gerudo_Token_Icon.png",
"Gold Skulltula Tokens": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/47/OoT_Token_Icon.png",
"Triforce Pieces": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/0b/SS_Triforce_Piece_Icon.png",
"Triforce": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/6/68/ALttP_Triforce_Title_Sprite.png",
"Zelda's Lullaby": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
"Epona's Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
"Saria's Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
"Sun's Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
"Song of Time": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
"Song of Storms": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
"Minuet of Forest": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/e/e4/Green_Note.png",
"Bolero of Fire": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/f0/Red_Note.png",
"Serenade of Water": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/0f/Blue_Note.png",
"Requiem of Spirit": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/a/a4/Orange_Note.png",
"Nocturne of Shadow": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/97/Purple_Note.png",
"Prelude of Light": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/90/Yellow_Note.png",
"Small Key": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/e/e5/OoT_Small_Key_Icon.png",
"Big Key": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/40/OoT_Boss_Key_Icon.png",
}
tracker.progressive_items = [
"Ocarina", "Bombs", "Bow", "Fire Arrows", "Kokiri Sword", "Biggoron Sword", "Mirror Shield",
"Slingshot", "Bombchus", "Progressive Hookshot", "Ice Arrows", "Progressive Strength", "Goron Tunic", "Zora Tunic",
"Boomerang", "Lens of Truth", "Megaton Hammer", "Light Arrows", "Progressive Scale", "Iron Boots", "Hover Boots",
"Bottles", "Din's Fire", "Farore's Wind", "Nayru's Love", "Progressive Wallet", "Magic Meter", "Gerudo Membership Card",
"Zelda's Lullaby", "Epona's Song", "Saria's Song", "Sun's Song", "Song of Time", "Song of Storms", "Gold Skulltula Tokens",
"Minuet of Forest", "Bolero of Fire", "Serenade of Water", "Requiem of Spirit", "Nocturne of Shadow", "Prelude of Light", "Triforce Pieces",
"Small Key", "Big Key"
]
tracker.progressive_names = {
"Progressive Hookshot": ["Hookshot", "Longshot"],
"Progressive Strength": ["Goron Bracelet", "Golden Gauntlets"],
"Progressive Wallet": ["Adults Wallet", "Giants Wallet"],
"Progressive Scale": ["Silver Scale", "Gold Scale"],
"Magic Meter": ["Small Magic", "Large Magic"],
"Ocarina": ["Fairy Ocarina", "Ocarina of Time"],
"Bottles": normal_bottles + ["Rutos Letter"]
}
location_id_to_name = {}
for name, id in location_name_to_id.items():
location_id_to_name[id] = name
tracker.regions = {}
for id in location_id_to_name.keys():
if id in location_name_to_id.values() and location_id_to_name[id] in tracker.all_locations:
if id < 67259:
tracker.regions.setdefault("Overworld", []).append(location_id_to_name[id])
elif id < 67264:
tracker.regions.setdefault("Thieves' Hideout", []).append(location_id_to_name[id])
elif id < 67281:
tracker.regions.setdefault("Overworld", []).append(location_id_to_name[id])
elif id < 67304:
tracker.regions.setdefault("Deku Tree", []).append(location_id_to_name[id])
elif id < 67335:
tracker.regions.setdefault("Dodongo's Cavern", []).append(location_id_to_name[id])
elif id < 67360:
tracker.regions.setdefault("Jabu Jabu's Belly", []).append(location_id_to_name[id])
elif id < 67385:
tracker.regions.setdefault("Bottom of the Well", []).append(location_id_to_name[id])
elif id < 67421:
tracker.regions.setdefault("Forest Temple", []).append(location_id_to_name[id])
elif id < 67458:
tracker.regions.setdefault("Fire Temple", []).append(location_id_to_name[id])
elif id < 67485:
tracker.regions.setdefault("Water Temple", []).append(location_id_to_name[id])
elif id < 67533:
tracker.regions.setdefault("Shadow Temple", []).append(location_id_to_name[id])
elif id < 67583:
tracker.regions.setdefault("Spirit Temple", []).append(location_id_to_name[id])
elif id < 67597:
tracker.regions.setdefault("Ice Cavern", []).append(location_id_to_name[id])
elif id < 67636:
tracker.regions.setdefault("Gerudo Training Ground", []).append(location_id_to_name[id])
elif id < 67674:
tracker.regions.setdefault("Ganon's Castle", []).append(location_id_to_name[id])
tracker.region_keys = {
'Forest Temple': ['Small Key (Forest Temple)', 'Boss Key (Forest Temple)'],
'Fire Temple': ['Small Key (Fire Temple)', 'Boss Key (Forest Temple)'],
'Water Temple': ['Small Key (Water Temple)', 'Boss Key (Water Temple)'],
'Spirit Temple': ['Small Key (Spirit Temple)', 'Boss Key (Spirit Temple)'],
'Shadow Temple': ['Small Key (Shadow Temple)', 'Boss Key (Shadow Temple)'],
'Bottom of the Well': ['Small Key (Bottom of the Well)', 'Boss Key (Bottom of the Well)'],
'Gerudo Training Ground': ['Small Key (Gerudo Training Ground)', 'Boss Key (Gerudo Training Ground)'],
'Thieves Hideout': ['Small Key (Thieves Hideout)', 'Boss Key (Thieves Hideout)'],
'Ganons Castle': ['Small Key (Ganons Castle)', 'Boss Key (Ganons Castle)']
}
class OOTWorld(World): class OOTWorld(World):
""" """

View File

@@ -282,8 +282,7 @@ class SA2BWorld(World):
spoiler_handle.writelines(text) spoiler_handle.writelines(text)
@classmethod @classmethod
def stage_fill_hook(cls, world, progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool, def stage_fill_hook(cls, world, progitempool, usefulitempool, filleritempool, fill_locations):
restitempool, fill_locations):
if world.get_game_players("Sonic Adventure 2 Battle"): if world.get_game_players("Sonic Adventure 2 Battle"):
progitempool.sort( progitempool.sort(
key=lambda item: 0 if (item.name != 'Emblem') else 1) key=lambda item: 0 if (item.name != 'Emblem') else 1)

View File

@@ -122,8 +122,6 @@ class AreaRandomization(Choice):
option_off = 0 option_off = 0
option_light = 1 option_light = 1
option_on = 2 option_on = 2
alias_false = 0
alias_true = 2
default = 0 default = 0
class AreaLayout(Toggle): class AreaLayout(Toggle):

View File

@@ -660,8 +660,7 @@ class SMWorld(World):
loc.address = loc.item.code = None loc.address = loc.item.code = None
@classmethod @classmethod
def stage_fill_hook(cls, world, progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool, def stage_fill_hook(cls, world, progitempool, usefulitempool, filleritempool, fill_locations):
restitempool, fill_locations):
if world.get_game_players("Super Metroid"): if world.get_game_players("Super Metroid"):
progitempool.sort( progitempool.sort(
key=lambda item: 1 if (item.name == 'Morph Ball') else 0) key=lambda item: 1 if (item.name == 'Morph Ball') else 0)

View File

@@ -1,48 +1,57 @@
import typing import typing
from Options import Option, DefaultOnToggle, Range, Toggle, DeathLink, Choice from Options import Option, DefaultOnToggle, Range, Toggle, DeathLink, Choice
class EnableCoinStars(DefaultOnToggle): class EnableCoinStars(DefaultOnToggle):
"""Disable to Ignore 100 Coin Stars. You can still collect them, but they don't do anything""" """Disable to Ignore 100 Coin Stars. You can still collect them, but they don't do anything"""
display_name = "Enable 100 Coin Stars" display_name = "Enable 100 Coin Stars"
class StrictCapRequirements(DefaultOnToggle): class StrictCapRequirements(DefaultOnToggle):
"""If disabled, Stars that expect special caps may have to be acquired without the caps""" """If disabled, Stars that expect special caps may have to be acquired without the caps"""
display_name = "Strict Cap Requirements" display_name = "Strict Cap Requirements"
class StrictCannonRequirements(DefaultOnToggle): 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""" """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" display_name = "Strict Cannon Requirements"
class FirstBowserStarDoorCost(Range): class FirstBowserStarDoorCost(Range):
"""How many stars are required at the Star Door to Bowser in the Dark World""" """How many stars are required at the Star Door to Bowser in the Dark World"""
range_start = 0 range_start = 0
range_end = 50 range_end = 50
default = 8 default = 8
class BasementStarDoorCost(Range): class BasementStarDoorCost(Range):
"""How many stars are required at the Star Door in the Basement""" """How many stars are required at the Star Door in the Basement"""
range_start = 0 range_start = 0
range_end = 70 range_end = 70
default = 30 default = 30
class SecondFloorStarDoorCost(Range): class SecondFloorStarDoorCost(Range):
"""How many stars are required to access the third floor""" """How many stars are required to access the third floor"""
range_start = 0 range_start = 0
range_end = 90 range_end = 90
default = 50 default = 50
class MIPS1Cost(Range): class MIPS1Cost(Range):
"""How many stars are required to spawn MIPS the first time""" """How many stars are required to spawn MIPS the first time"""
range_start = 0 range_start = 0
range_end = 40 range_end = 40
default = 15 default = 15
class MIPS2Cost(Range): class MIPS2Cost(Range):
"""How many stars are required to spawn MIPS the secound time. Must be bigger or equal MIPS1Cost""" """How many stars are required to spawn MIPS the secound time. Must be bigger or equal MIPS1Cost"""
range_start = 0 range_start = 0
range_end = 80 range_end = 80
default = 50 default = 50
class StarsToFinish(Range): class StarsToFinish(Range):
"""How many stars are required at the infinite stairs""" """How many stars are required at the infinite stairs"""
display_name = "Endless Stairs Stars" display_name = "Endless Stairs Stars"
@@ -50,35 +59,40 @@ class StarsToFinish(Range):
range_end = 100 range_end = 100
default = 70 default = 70
class AmountOfStars(Range): class AmountOfStars(Range):
"""How many stars exist. Disabling 100 Coin Stars removes 15 from the Pool. At least max of any Cost set""" """How many stars exist. Disabling 100 Coin Stars removes 15 from the Pool. At least max of any Cost set"""
range_start = 35 range_start = 35
range_end = 120 range_end = 120
default = 120 default = 120
class AreaRandomizer(Choice): class AreaRandomizer(Choice):
"""Randomize Entrances""" """Randomize Entrances"""
display_name = "Entrance Randomizer" display_name = "Entrance Randomizer"
alias_false = 0
option_Off = 0 option_Off = 0
option_Courses_Only = 1 option_Courses_Only = 1
option_Courses_and_Secrets = 2 option_Courses_and_Secrets = 2
class BuddyChecks(Toggle): class BuddyChecks(Toggle):
"""Bob-omb Buddies are checks, Cannon Unlocks are items""" """Bob-omb Buddies are checks, Cannon Unlocks are items"""
display_name = "Bob-omb Buddy Checks" display_name = "Bob-omb Buddy Checks"
class ExclamationBoxes(Choice): class ExclamationBoxes(Choice):
"""Include 1Up Exclamation Boxes during randomization""" """Include 1Up Exclamation Boxes during randomization"""
display_name = "Randomize 1Up !-Blocks" display_name = "Randomize 1Up !-Blocks"
option_Off = 0 option_Off = 0
option_1Ups_Only = 1 option_1Ups_Only = 1
class ProgressiveKeys(DefaultOnToggle): class ProgressiveKeys(DefaultOnToggle):
"""Keys will first grant you access to the Basement, then to the Secound Floor""" """Keys will first grant you access to the Basement, then to the Secound Floor"""
display_name = "Progressive Keys" display_name = "Progressive Keys"
sm64_options: typing.Dict[str,type(Option)] = {
sm64_options: typing.Dict[str, type(Option)] = {
"AreaRandomizer": AreaRandomizer, "AreaRandomizer": AreaRandomizer,
"ProgressiveKeys": ProgressiveKeys, "ProgressiveKeys": ProgressiveKeys,
"EnableCoinStars": EnableCoinStars, "EnableCoinStars": EnableCoinStars,
@@ -93,5 +107,5 @@ sm64_options: typing.Dict[str,type(Option)] = {
"StarsToFinish": StarsToFinish, "StarsToFinish": StarsToFinish,
"death_link": DeathLink, "death_link": DeathLink,
"BuddyChecks": BuddyChecks, "BuddyChecks": BuddyChecks,
"ExclamationBoxes": ExclamationBoxes "ExclamationBoxes": ExclamationBoxes,
} }

View File

@@ -86,6 +86,7 @@ def set_rules(world, player: int, area_connections):
# which would make it impossible to reach downtown area without the cannon. # 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: 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: 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]: if world.StrictCapRequirements[player]:
add_rule(world.get_location("BoB: Mario Wings to the Sky", player), lambda state: state.has("Wing Cap", player)) add_rule(world.get_location("BoB: Mario Wings to the Sky", player), lambda state: state.has("Wing Cap", player))

View File

@@ -9,6 +9,7 @@ from .Regions import create_regions, sm64courses, sm64entrances_s, sm64_internal
from BaseClasses import Item, Tutorial, ItemClassification from BaseClasses import Item, Tutorial, ItemClassification
from ..AutoWorld import World, WebWorld from ..AutoWorld import World, WebWorld
class SM64Web(WebWorld): class SM64Web(WebWorld):
tutorials = [Tutorial( tutorials = [Tutorial(
"Multiworld Setup Guide", "Multiworld Setup Guide",

View File

@@ -107,7 +107,6 @@ class HeartBeepSpeed(Choice):
option_Half = 2 option_Half = 2
option_Normal = 3 option_Normal = 3
option_Double = 4 option_Double = 4
alias_false = 0
default = 3 default = 3
class HeartColor(Choice): class HeartColor(Choice):

View File

@@ -21,8 +21,6 @@ class OffOnFullChoice(Choice):
option_on = 1 option_on = 1
option_full = 2 option_full = 2
alias_chaos = 2 alias_chaos = 2
alias_false = 0
alias_true = 1
class Difficulty(EvermizerFlags, Choice): class Difficulty(EvermizerFlags, Choice):

View File

@@ -3,66 +3,82 @@ from BaseClasses import MultiWorld
from Options import Toggle, DefaultOnToggle, DeathLink, Choice, Range, Option, OptionDict from Options import Toggle, DefaultOnToggle, DeathLink, Choice, Range, Option, OptionDict
from schema import Schema, And, Optional from schema import Schema, And, Optional
class StartWithJewelryBox(Toggle): class StartWithJewelryBox(Toggle):
"Start with Jewelry Box unlocked" "Start with Jewelry Box unlocked"
display_name = "Start with Jewelry Box" display_name = "Start with Jewelry Box"
#class ProgressiveVerticalMovement(Toggle): #class ProgressiveVerticalMovement(Toggle):
# "Always find vertical movement in the following order Succubus Hairpin -> Light Wall -> Celestial Sash" # "Always find vertical movement in the following order Succubus Hairpin -> Light Wall -> Celestial Sash"
# display_name = "Progressive vertical movement" # display_name = "Progressive vertical movement"
#class ProgressiveKeycards(Toggle): #class ProgressiveKeycards(Toggle):
# "Always find Security Keycard's in the following order D -> C -> B -> A" # "Always find Security Keycard's in the following order D -> C -> B -> A"
# display_name = "Progressive keycards" # display_name = "Progressive keycards"
class DownloadableItems(DefaultOnToggle): class DownloadableItems(DefaultOnToggle):
"With the tablet you will be able to download items at terminals" "With the tablet you will be able to download items at terminals"
display_name = "Downloadable items" display_name = "Downloadable items"
class EyeSpy(Toggle): class EyeSpy(Toggle):
"Requires Oculus Ring in inventory to be able to break hidden walls." "Requires Oculus Ring in inventory to be able to break hidden walls."
display_name = "Eye Spy" display_name = "Eye Spy"
class StartWithMeyef(Toggle): class StartWithMeyef(Toggle):
"Start with Meyef, ideal for when you want to play multiplayer." "Start with Meyef, ideal for when you want to play multiplayer."
display_name = "Start with Meyef" display_name = "Start with Meyef"
class QuickSeed(Toggle): class QuickSeed(Toggle):
"Start with Talaria Attachment, Nyoom!" "Start with Talaria Attachment, Nyoom!"
display_name = "Quick seed" display_name = "Quick seed"
class SpecificKeycards(Toggle): class SpecificKeycards(Toggle):
"Keycards can only open corresponding doors" "Keycards can only open corresponding doors"
display_name = "Specific Keycards" display_name = "Specific Keycards"
class Inverted(Toggle): class Inverted(Toggle):
"Start in the past" "Start in the past"
display_name = "Inverted" display_name = "Inverted"
#class StinkyMaw(Toggle): #class StinkyMaw(Toggle):
# "Require gasmask for Maw" # "Require gasmask for Maw"
# display_name = "Stinky Maw" # display_name = "Stinky Maw"
class GyreArchives(Toggle): class GyreArchives(Toggle):
"Gyre locations are in logic. New warps are gated by Merchant Crow and Kobo" "Gyre locations are in logic. New warps are gated by Merchant Crow and Kobo"
display_name = "Gyre Archives" display_name = "Gyre Archives"
class Cantoran(Toggle): class Cantoran(Toggle):
"Cantoran's fight and check are available upon revisiting his room" "Cantoran's fight and check are available upon revisiting his room"
display_name = "Cantoran" display_name = "Cantoran"
class LoreChecks(Toggle): class LoreChecks(Toggle):
"Memories and journal entries contain items." "Memories and journal entries contain items."
display_name = "Lore Checks" display_name = "Lore Checks"
class BossRando(Toggle): class BossRando(Toggle):
"Shuffles the positions of all bosses." "Shuffles the positions of all bosses."
display_name = "Boss Randomization" display_name = "Boss Randomization"
class BossScaling(DefaultOnToggle): class BossScaling(DefaultOnToggle):
"When Boss Rando is enabled, scales the bosses' HP, XP, and ATK to the stats of the location they replace (Reccomended)" "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" display_name = "Scale Random Boss Stats"
class DamageRando(Choice): class DamageRando(Choice):
"Randomly nerfs and buffs some orbs and their associated spells as well as some associated rings." "Randomly nerfs and buffs some orbs and their associated spells as well as some associated rings."
display_name = "Damage Rando" display_name = "Damage Rando"
@@ -73,9 +89,9 @@ class DamageRando(Choice):
option_mostlybuffs = 4 option_mostlybuffs = 4
option_allbuffs = 5 option_allbuffs = 5
option_manual = 6 option_manual = 6
alias_false = 0
alias_true = 2 alias_true = 2
class DamageRandoOverrides(OptionDict): 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" "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({ schema = Schema({
@@ -180,6 +196,7 @@ class DamageRandoOverrides(OptionDict):
"Radiant": { "MinusOdds": 1, "NormalOdds": 1, "PlusOdds": 1 }, "Radiant": { "MinusOdds": 1, "NormalOdds": 1, "PlusOdds": 1 },
} }
class HpCap(Range): class HpCap(Range):
"Sets the number that Lunais's HP maxes out at." "Sets the number that Lunais's HP maxes out at."
display_name = "HP Cap" display_name = "HP Cap"
@@ -187,10 +204,12 @@ class HpCap(Range):
range_end = 999 range_end = 999
default = 999 default = 999
class BossHealing(DefaultOnToggle): class BossHealing(DefaultOnToggle):
"Enables/disables healing after boss fights. NOTE: Currently only applicable when Boss Rando is enabled." "Enables/disables healing after boss fights. NOTE: Currently only applicable when Boss Rando is enabled."
display_name = "Heal After Bosses" display_name = "Heal After Bosses"
class ShopFill(Choice): class ShopFill(Choice):
"""Sets the items for sale in Merchant Crow's shops. """Sets the items for sale in Merchant Crow's shops.
Default: No sunglasses or trendy jacket, but sand vials for sale. Default: No sunglasses or trendy jacket, but sand vials for sale.
@@ -203,10 +222,12 @@ class ShopFill(Choice):
option_vanilla = 2 option_vanilla = 2
option_empty = 3 option_empty = 3
class ShopWarpShards(DefaultOnToggle): class ShopWarpShards(DefaultOnToggle):
"Shops always sell warp shards (when keys possessed), ignoring inventory setting." "Shops always sell warp shards (when keys possessed), ignoring inventory setting."
display_name = "Always Sell Warp Shards" display_name = "Always Sell Warp Shards"
class ShopMultiplier(Range): class ShopMultiplier(Range):
"Multiplier for the cost of items in the shop. Set to 0 for free shops." "Multiplier for the cost of items in the shop. Set to 0 for free shops."
display_name = "Shop Price Multiplier" display_name = "Shop Price Multiplier"
@@ -214,6 +235,7 @@ class ShopMultiplier(Range):
range_end = 10 range_end = 10
default = 1 default = 1
class LootPool(Choice): class LootPool(Choice):
"""Sets the items that drop from enemies (does not apply to boss reward checks) """Sets the items that drop from enemies (does not apply to boss reward checks)
Vanilla: Drops are the same as the base game Vanilla: Drops are the same as the base game
@@ -224,6 +246,7 @@ class LootPool(Choice):
option_randomized = 1 option_randomized = 1
option_empty = 2 option_empty = 2
class DropRateCategory(Choice): class DropRateCategory(Choice):
"""Sets the drop rate when 'Loot Pool' is set to 'Random' """Sets the drop rate when 'Loot Pool' is set to 'Random'
Tiered: Based on item rarity/value Tiered: Based on item rarity/value
@@ -237,6 +260,7 @@ class DropRateCategory(Choice):
option_randomized = 2 option_randomized = 2
option_fixed = 3 option_fixed = 3
class FixedDropRate(Range): class FixedDropRate(Range):
"Base drop rate percentage when 'Drop Rate Category' is set to 'Fixed'" "Base drop rate percentage when 'Drop Rate Category' is set to 'Fixed'"
display_name = "Fixed Drop Rate" display_name = "Fixed Drop Rate"
@@ -244,6 +268,7 @@ class FixedDropRate(Range):
range_end = 100 range_end = 100
default = 5 default = 5
class LootTierDistro(Choice): class LootTierDistro(Choice):
"""Sets how often items of each rarity tier are placed when 'Loot Pool' is set to 'Random' """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 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_full_random = 1
option_inverted_weight = 2 option_inverted_weight = 2
class ShowBestiary(Toggle): class ShowBestiary(Toggle):
"All entries in the bestiary are visible, without needing to kill one of a given enemy first" "All entries in the bestiary are visible, without needing to kill one of a given enemy first"
display_name = "Show Bestiary Entries" display_name = "Show Bestiary Entries"
class ShowDrops(Toggle): class ShowDrops(Toggle):
"All item drops in the bestiary are visible, without needing an enemy to drop one of a given item first" "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" display_name = "Show Bestiary Item Drops"
# Some options that are available in the timespinner randomizer arent currently implemented # Some options that are available in the timespinner randomizer arent currently implemented
timespinner_options: Dict[str, Option] = { timespinner_options: Dict[str, Option] = {
"StartWithJewelryBox": StartWithJewelryBox, "StartWithJewelryBox": StartWithJewelryBox,
@@ -296,9 +324,11 @@ timespinner_options: Dict[str, Option] = {
"DeathLink": DeathLink, "DeathLink": DeathLink,
} }
def is_option_enabled(world: MultiWorld, player: int, name: str) -> bool: def is_option_enabled(world: MultiWorld, player: int, name: str) -> bool:
return get_option_value(world, player, name) > 0 return get_option_value(world, player, name) > 0
def get_option_value(world: MultiWorld, player: int, name: str) -> Union[int, dict]: def get_option_value(world: MultiWorld, player: int, name: str) -> Union[int, dict]:
option = getattr(world, name, None) option = getattr(world, name, None)
if option == None: if option == None:

View File

@@ -15,9 +15,9 @@ class DisableNonRandomizedPuzzles(DefaultOnToggle):
class EarlySecretArea(Toggle): 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")""" (Otherwise known as "UTM", "Caves" or the "Challenge Area")"""
display_name = "Early Secret Area" display_name = "Early Caves"
class ShuffleSymbols(DefaultOnToggle): class ShuffleSymbols(DefaultOnToggle):
@@ -58,15 +58,9 @@ class ShuffleVaultBoxes(Toggle):
display_name = "Shuffle Vault Boxes" 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): 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 """Adds locations into the pool that are guaranteed to become accessible before or at the same time as your goal.
forfeit on victory.""" Use this if you don't play with forfeit on victory."""
display_name = "Shuffle Postgame" display_name = "Shuffle Postgame"
@@ -90,7 +84,7 @@ class MountainLasers(Range):
class ChallengeLasers(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" display_name = "Required Lasers for Challenge"
range_start = 1 range_start = 1
range_end = 11 range_end = 11
@@ -122,7 +116,6 @@ the_witness_options: Dict[str, type] = {
"disable_non_randomized_puzzles": DisableNonRandomizedPuzzles, "disable_non_randomized_puzzles": DisableNonRandomizedPuzzles,
"shuffle_discarded_panels": ShuffleDiscardedPanels, "shuffle_discarded_panels": ShuffleDiscardedPanels,
"shuffle_vault_boxes": ShuffleVaultBoxes, "shuffle_vault_boxes": ShuffleVaultBoxes,
"shuffle_uncommon": ShuffleUncommonLocations,
"shuffle_postgame": ShufflePostgame, "shuffle_postgame": ShufflePostgame,
"victory_condition": VictoryCondition, "victory_condition": VictoryCondition,
"mountain_lasers": MountainLasers, "mountain_lasers": MountainLasers,

View File

@@ -28,38 +28,38 @@ Traps:
610 - Power Surge 610 - Power Surge
Doors: Doors:
1100 - Glass Factory Entry Door (Panel) - 0x01A54 1100 - Glass Factory Entry (Panel) - 0x01A54
1105 - Door to Symmetry Island Lower (Panel) - 0x000B0 1105 - Symmetry Island Lower (Panel) - 0x000B0
1107 - Door to Symmetry Island Upper (Panel) - 0x1C349 1107 - Symmetry Island Upper (Panel) - 0x1C349
1110 - Door to Desert Flood Light Room (Panel) - 0x0C339 1110 - Desert Light Room Entry (Panel) - 0x0C339
1111 - Desert Flood Room Flood Controls (Panel) - 0x1C2DF,0x1831E,0x1C260,0x1831C,0x1C2F3,0x1831D,0x1C2B1,0x1831B 1111 - Desert Flood Controls (Panel) - 0x1C2DF,0x1831E,0x1C260,0x1831C,0x1C2F3,0x1831D,0x1C2B1,0x1831B
1119 - Quarry Door to Mill (Panel) - 0x01E5A,0x01E59 1119 - Quarry Mill Entry (Panel) - 0x01E5A,0x01E59
1120 - Quarry Mill Ramp Controls (Panel) - 0x03678,0x03676 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 1125 - Quarry Boathouse Ramp Height Control (Panel) - 0x03852
1127 - Quarry Boathouse Ramp Horizontal Control (Panel) - 0x03858 1127 - Quarry Boathouse Ramp Horizontal Control (Panel) - 0x03858
1131 - Shadows Door Timer (Panel) - 0x334DB,0x334DC 1131 - Shadows Door Timer (Panel) - 0x334DB,0x334DC
1150 - Monastery Entry Door Left (Panel) - 0x00B10 1150 - Monastery Entry Left (Panel) - 0x00B10
1151 - Monastery Entry Door Right (Panel) - 0x00C92 1151 - Monastery Entry Right (Panel) - 0x00C92
1162 - Town Door to RGB House (Panel) - 0x28998 1162 - Town Tinted Glass Door (Panel) - 0x28998
1163 - Town Door to Church (Panel) - 0x28A0D 1163 - Town Church Entry (Panel) - 0x28A0D
1166 - Town Maze Panel (Drop-Down Staircase) (Panel) - 0x28A79 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 1200 - Treehouse First & Second Doors (Panel) - 0x0288C,0x02886
1202 - Treehouse Third Door (Panel) - 0x0A182 1202 - Treehouse Third Door (Panel) - 0x0A182
1205 - Treehouse Laser House Door Timer (Panel) - 0x2700B,0x334DC 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 1175 - Jungle Popup Wall (Panel) - 0x17CAB
1180 - Bunker Entry Door (Panel) - 0x17C2E 1180 - Bunker Entry (Panel) - 0x17C2E
1183 - Inside Bunker Door to Bunker Proper (Panel) - 0x0A099 1183 - Bunker Tinted Glass Door (Panel) - 0x0A099
1186 - Bunker Elevator Control (Panel) - 0x0A079 1186 - Bunker Elevator Control (Panel) - 0x0A079
1190 - Swamp Entry Door (Panel) - 0x0056E 1190 - Swamp Entry (Panel) - 0x0056E
1192 - Swamp Sliding Bridge (Panel) - 0x00609,0x18488 1192 - Swamp Sliding Bridge (Panel) - 0x00609,0x18488
1195 - Swamp Rotating Bridge (Panel) - 0x181F5 1195 - Swamp Rotating Bridge (Panel) - 0x181F5
1197 - Swamp Maze Control (Panel) - 0x17C0A 1197 - Swamp Maze Control (Panel) - 0x17C0A
1310 - Boat - 0x17CDF,0x17CC8,0x17CA6,0x09DB8,0x17C95,0x0A054 1310 - Boat - 0x17CDF,0x17CC8,0x17CA6,0x09DB8,0x17C95,0x0A054
1400 - Caves Mountain Shortcut - 0x2D73F 1400 - Caves Mountain Shortcut (Door) - 0x2D73F
1500 - Symmetry Laser - 0x00509 1500 - Symmetry Laser - 0x00509
1501 - Desert Laser - 0x012FB,0x01317 1501 - Desert Laser - 0x012FB,0x01317
@@ -73,101 +73,101 @@ Doors:
1509 - Swamp Laser - 0x00BF6 1509 - Swamp Laser - 0x00BF6
1510 - Treehouse Laser - 0x028A4 1510 - Treehouse Laser - 0x028A4
1600 - Outside Tutorial Optional Door - 0x03BA2 1600 - Outside Tutorial Outpost Path (Door) - 0x03BA2
1603 - Outside Tutorial Outpost Entry Door - 0x0A170 1603 - Outside Tutorial Outpost Entry (Door) - 0x0A170
1606 - Outside Tutorial Outpost Exit Door - 0x04CA3 1606 - Outside Tutorial Outpost Exit (Door) - 0x04CA3
1609 - Glass Factory Entry Door - 0x01A29 1609 - Glass Factory Entry (Door) - 0x01A29
1612 - Glass Factory Back Wall - 0x0D7ED 1612 - Glass Factory Back Wall (Door) - 0x0D7ED
1615 - Symmetry Island Lower Door - 0x17F3E 1615 - Symmetry Island Lower (Door) - 0x17F3E
1618 - Symmetry Island Upper Door - 0x18269 1618 - Symmetry Island Upper (Door) - 0x18269
1619 - Orchard Middle Gate - 0x03307 1619 - Orchard First Gate (Door) - 0x03307
1620 - Orchard Final Gate - 0x03313 1620 - Orchard Second Gate (Door) - 0x03313
1621 - Desert Door to Flood Light Room - 0x09FEE 1621 - Desert Light Room Entry (Door) - 0x09FEE
1624 - Desert Door to Pond Room - 0x0C2C3 1624 - Desert Pond Room Entry (Door) - 0x0C2C3
1627 - Desert Door to Water Levels Room - 0x0A24B 1627 - Desert Flood Room Entry (Door) - 0x0A24B
1630 - Desert Door to Elevator Room - 0x0C316 1630 - Desert Elevator Room Entry (Door) - 0x0C316
1633 - Quarry Main Entry 1 - 0x09D6F 1633 - Quarry Entry 1 (Door) - 0x09D6F
1636 - Quarry Main Entry 2 - 0x17C07 1636 - Quarry Entry 2 (Door) - 0x17C07
1639 - Quarry Door to Mill - 0x02010 1639 - Quarry Mill Entry (Door) - 0x02010
1642 - Quarry Mill Side Door - 0x275FF 1642 - Quarry Mill Side Exit (Door) - 0x275FF
1645 - Quarry Mill Rooftop Shortcut - 0x17CE8 1645 - Quarry Mill Roof Exit (Door) - 0x17CE8
1648 - Quarry Mill Stairs - 0x0368A 1648 - Quarry Mill Stairs (Door) - 0x0368A
1651 - Quarry Boathouse Boat Staircase - 0x2769B,0x27163 1651 - Quarry Boathouse Dock (Door) - 0x2769B,0x27163
1653 - Quarry Boathouse First Barrier - 0x17C50 1653 - Quarry Boathouse First Barrier (Door) - 0x17C50
1654 - Quarry Boathouse Shortcut - 0x3865F 1654 - Quarry Boathouse Second Barrier (Door) - 0x3865F
1656 - Shadows Timed Door - 0x19B24 1656 - Shadows Timed Door (Door) - 0x19B24
1657 - Shadows Laser Room Right Door - 0x194B2 1657 - Shadows Laser Entry Right (Door) - 0x194B2
1660 - Shadows Laser Room Left Door - 0x19665 1660 - Shadows Laser Entry Left (Door) - 0x19665
1663 - Shadows Barrier to Quarry - 0x19865,0x0A2DF 1663 - Shadows Quarry Barrier (Door) - 0x19865,0x0A2DF
1666 - Shadows Barrier to Ledge - 0x1855B,0x19ADE 1666 - Shadows Ledge Barrier (Door) - 0x1855B,0x19ADE
1669 - Keep Hedge Maze 1 Exit Door - 0x01954 1669 - Keep Hedge Maze 1 Exit (Door) - 0x01954
1672 - Keep Pressure Plates 1 Exit Door - 0x01BEC 1672 - Keep Pressure Plates 1 Exit (Door) - 0x01BEC
1675 - Keep Hedge Maze 2 Shortcut - 0x018CE 1675 - Keep Hedge Maze 2 Shortcut (Door) - 0x018CE
1678 - Keep Hedge Maze 2 Exit Door - 0x019D8 1678 - Keep Hedge Maze 2 Exit (Door) - 0x019D8
1681 - Keep Hedge Maze 3 Shortcut - 0x019B5 1681 - Keep Hedge Maze 3 Shortcut (Door) - 0x019B5
1684 - Keep Hedge Maze 3 Exit Door - 0x019E6 1684 - Keep Hedge Maze 3 Exit (Door) - 0x019E6
1687 - Keep Hedge Maze 4 Shortcut - 0x0199A 1687 - Keep Hedge Maze 4 Shortcut (Door) - 0x0199A
1690 - Keep Hedge Maze 4 Exit Door - 0x01A0E 1690 - Keep Hedge Maze 4 Exit (Door) - 0x01A0E
1693 - Keep Pressure Plates 2 Exit Door - 0x01BEA 1693 - Keep Pressure Plates 2 Exit (Door) - 0x01BEA
1696 - Keep Pressure Plates 3 Exit Door - 0x01CD5 1696 - Keep Pressure Plates 3 Exit (Door) - 0x01CD5
1699 - Keep Pressure Plates 4 Exit Door - 0x01D40 1699 - Keep Pressure Plates 4 Exit (Door) - 0x01D40
1702 - Keep Shortcut to Shadows - 0x09E3D 1702 - Keep Shadows Shortcut (Door) - 0x09E3D
1705 - Keep Tower Shortcut - 0x04F8F 1705 - Keep Tower Shortcut (Door) - 0x04F8F
1708 - Monastery Shortcut - 0x0364E 1708 - Monastery Shortcut (Door) - 0x0364E
1711 - Monastery Inner Door - 0x0C128 1711 - Monastery Entry Inner (Door) - 0x0C128
1714 - Monastery Outer Door - 0x0C153 1714 - Monastery Entry Outer (Door) - 0x0C153
1717 - Monastery Door to Garden - 0x03750 1717 - Monastery Garden Entry (Door) - 0x03750
1718 - Town Cargo Box Door - 0x0A0C9 1718 - Town Cargo Box Entry (Door) - 0x0A0C9
1720 - Town Wooden Roof Staircase - 0x034F5 1720 - Town Wooden Roof Stairs (Door) - 0x034F5
1723 - Town Tinted Door to RGB House - 0x28A61 1723 - Town Tinted Glass Door (Door) - 0x28A61
1726 - Town Door to Church - 0x03BB0 1726 - Town Church Entry (Door) - 0x03BB0
1729 - Town Maze Staircase - 0x28AA2 1729 - Town Maze Stairs (Door) - 0x28AA2
1732 - Town Windmill Door - 0x1845B 1732 - Town Windmill Entry (Door) - 0x1845B
1735 - Town RGB House Staircase - 0x2897B 1735 - Town RGB House Stairs (Door) - 0x2897B
1738 - Town Tower Blue Panels Door - 0x27798 1738 - Town Tower First Door (Door) - 0x27798
1741 - Town Tower Lattice Door - 0x27799 1741 - Town Tower Third Door (Door) - 0x27799
1744 - Town Tower Environmental Set Door - 0x2779A 1744 - Town Tower Fourth Door (Door) - 0x2779A
1747 - Town Tower Wooden Roof Set Door - 0x2779C 1747 - Town Tower Second Door (Door) - 0x2779C
1750 - Theater Entry Door - 0x17F88 1750 - Theater Entry (Door) - 0x17F88
1753 - Theater Exit Door Left - 0x0A16D 1753 - Theater Exit Left (Door) - 0x0A16D
1756 - Theater Exit Door Right - 0x3CCDF 1756 - Theater Exit Right (Door) - 0x3CCDF
1759 - Jungle Bamboo Shortcut to River - 0x3873B 1759 - Jungle Bamboo Laser Shortcut (Door) - 0x3873B
1760 - Jungle Popup Wall - 0x1475B 1760 - Jungle Popup Wall (Door) - 0x1475B
1762 - River Shortcut to Monastery Garden - 0x0CF2A 1762 - River Monastery Shortcut (Door) - 0x0CF2A
1765 - Bunker Bunker Entry Door - 0x0C2A4 1765 - Bunker Entry (Door) - 0x0C2A4
1768 - Bunker Tinted Glass Door - 0x17C79 1768 - Bunker Tinted Glass Door (Door) - 0x17C79
1771 - Bunker Door to Ultraviolet Room - 0x0C2A3 1771 - Bunker UV Room Entry (Door) - 0x0C2A3
1774 - Bunker Door to Elevator - 0x0A08D 1774 - Bunker Elevator Room Entry (Door) - 0x0A08D
1777 - Swamp Entry Door - 0x00C1C 1777 - Swamp Entry (Door) - 0x00C1C
1780 - Swamp Door to Broken Shapers - 0x184B7 1780 - Swamp Between Bridges First Door - 0x184B7
1783 - Swamp Platform Shortcut Door - 0x38AE6 1783 - Swamp Platform Shortcut Door - 0x38AE6
1786 - Swamp Cyan Water Pump - 0x04B7F 1786 - Swamp Cyan Water Pump (Door) - 0x04B7F
1789 - Swamp Door to Rotated Shapers - 0x18507 1789 - Swamp Between Bridges Second Door - 0x18507
1792 - Swamp Red Water Pump - 0x183F2 1792 - Swamp Red Water Pump (Door) - 0x183F2
1795 - Swamp Red Underwater Exit - 0x305D5 1795 - Swamp Red Underwater Exit (Door) - 0x305D5
1798 - Swamp Blue Water Pump - 0x18482 1798 - Swamp Blue Water Pump (Door) - 0x18482
1801 - Swamp Purple Water Pump - 0x0A1D6 1801 - Swamp Purple Water Pump (Door) - 0x0A1D6
1804 - Swamp Near Laser Shortcut - 0x2D880 1804 - Swamp Laser Shortcut (Door) - 0x2D880
1807 - Treehouse First Door - 0x0C309 1807 - Treehouse First Door (Door) - 0x0C309
1810 - Treehouse Second Door - 0x0C310 1810 - Treehouse Second Door (Door) - 0x0C310
1813 - Treehouse Beyond Yellow Bridge Door - 0x0A181 1813 - Treehouse Third Door (Door) - 0x0A181
1816 - Treehouse Drawbridge - 0x0C32D 1816 - Treehouse Drawbridge (Door) - 0x0C32D
1819 - Treehouse Timed Door to Laser House - 0x0C323 1819 - Treehouse Laser House Entry (Door) - 0x0C323
1822 - Inside Mountain First Layer Exit Door - 0x09E54 1822 - Mountain Floor 1 Exit (Door) - 0x09E54
1825 - Inside Mountain Second Layer Staircase Near - 0x09FFB 1825 - Mountain Floor 2 Staircase Near (Door) - 0x09FFB
1828 - Inside Mountain Second Layer Exit Door - 0x09EDD 1828 - Mountain Floor 2 Exit (Door) - 0x09EDD
1831 - Inside Mountain Second Layer Staircase Far - 0x09E07 1831 - Mountain Floor 2 Staircase Far (Door) - 0x09E07
1834 - Inside Mountain Giant Puzzle Exit Door - 0x09F89 1834 - Mountain Bottom Floor Giant Puzzle Exit (Door) - 0x09F89
1840 - Inside Mountain Door to Final Room - 0x0C141 1840 - Mountain Bottom Floor Final Room Entry (Door) - 0x0C141
1843 - Inside Mountain Bottom Layer Rock - 0x17F33 1843 - Mountain Bottom Floor Rock (Door) - 0x17F33
1846 - Inside Mountain Door to Secret Area - 0x2D77D 1846 - Caves Entry (Door) - 0x2D77D
1849 - Caves Pillar Door - 0x019A5 1849 - Caves Pillar Door (Door) - 0x019A5
1855 - Caves Swamp Shortcut - 0x2D859 1855 - Caves Swamp Shortcut (Door) - 0x2D859
1858 - Challenge Entry Door - 0x0A19A 1858 - Challenge Entry (Door) - 0x0A19A
1861 - Challenge Door to Theater Walkway - 0x0348A 1861 - Challenge Tunnels Entry (Door) - 0x0348A
1864 - Theater Walkway Door to Windmill Interior - 0x27739 1864 - Tunnels Theater Shortcut (Door) - 0x27739
1867 - Theater Walkway Door to Desert Elevator Room - 0x27263 1867 - Tunnels Desert Shortcut (Door) - 0x27263
1870 - Theater Walkway Door to Town - 0x09E87 1870 - Tunnels Town Shortcut (Door) - 0x09E87
1903 - Outside Tutorial Outpost Doors - 0x03BA2,0x0A170,0x04CA3 1903 - Outside Tutorial Outpost Doors - 0x03BA2,0x0A170,0x04CA3
1906 - Symmetry Island Doors - 0x17F3E,0x18269 1906 - Symmetry Island Doors - 0x17F3E,0x18269
@@ -181,18 +181,18 @@ Doors:
1930 - Keep Hedge Maze Doors - 0x01954,0x018CE,0x019D8,0x019B5,0x019E6,0x0199A,0x01A0E 1930 - Keep Hedge Maze Doors - 0x01954,0x018CE,0x019D8,0x019B5,0x019E6,0x0199A,0x01A0E
1933 - Keep Pressure Plates Doors - 0x01BEC,0x01BEA,0x01CD5,0x01D40 1933 - Keep Pressure Plates Doors - 0x01BEC,0x01BEA,0x01CD5,0x01D40
1936 - Keep Shortcuts - 0x09E3D,0x04F8F 1936 - Keep Shortcuts - 0x09E3D,0x04F8F
1939 - Monastery Entry Door - 0x0C128,0x0C153 1939 - Monastery Entry - 0x0C128,0x0C153
1942 - Monastery Shortcuts - 0x0364E,0x03750 1942 - Monastery Shortcuts - 0x0364E,0x03750
1945 - Town Doors - 0x0A0C9,0x034F5,0x28A61,0x03BB0,0x28AA2,0x1845B,0x2897B 1945 - Town Doors - 0x0A0C9,0x034F5,0x28A61,0x03BB0,0x28AA2,0x1845B,0x2897B
1948 - Town Tower Doors - 0x27798,0x27799,0x2779A,0x2779C 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 1954 - Jungle & River Shortcuts - 0x3873B,0x0CF2A
1957 - Bunker Doors - 0x0C2A4,0x17C79,0x0C2A3,0x0A08D 1957 - Bunker Doors - 0x0C2A4,0x17C79,0x0C2A3,0x0A08D
1960 - Swamp Doors - 0x00C1C,0x184B7,0x38AE6,0x18507 1960 - Swamp Doors - 0x00C1C,0x184B7,0x38AE6,0x18507
1963 - Swamp Water Pumps - 0x04B7F,0x183F2,0x305D5,0x18482,0x0A1D6 1963 - Swamp Water Pumps - 0x04B7F,0x183F2,0x305D5,0x18482,0x0A1D6
1966 - Treehouse Entry Doors - 0x0C309,0x0C310,0x0A181 1966 - Treehouse Entry Doors - 0x0C309,0x0C310,0x0A181
1975 - Inside Mountain Second Layer Stairs & Doors - 0x09FFB,0x09EDD,0x09E07 1975 - Mountain Floor 2 Stairs & Doors - 0x09FFB,0x09EDD,0x09E07
1978 - Inside Mountain Bottom Layer Doors to Caves - 0x17F33,0x2D77D 1978 - Mountain Bottom Floor Doors to Caves - 0x17F33,0x2D77D
1981 - Caves Doors to Challenge - 0x019A5,0x0A19A 1981 - Caves Doors to Challenge - 0x019A5,0x0A19A
1984 - Caves Exits to Main Island - 0x2D859,0x2D73F 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

View File

@@ -36,7 +36,7 @@ class WitnessWorld(World):
""" """
game = "The Witness" game = "The Witness"
topology_present = False topology_present = False
data_version = 5 data_version = 7
static_logic = StaticWitnessLogic() static_logic = StaticWitnessLogic()
static_locat = StaticWitnessLocations() static_locat = StaticWitnessLocations()

View File

@@ -30,17 +30,17 @@ class StaticWitnessLocations:
"Outside Tutorial Vault Box", "Outside Tutorial Vault Box",
"Outside Tutorial Discard", "Outside Tutorial Discard",
"Outside Tutorial Dots Introduction 5", "Outside Tutorial Shed Row 5",
"Outside Tutorial Squares Introduction 9", "Outside Tutorial Tree Row 9",
"Glass Factory Discard", "Glass Factory Discard",
"Glass Factory Vertical Symmetry 5", "Glass Factory Back Wall 5",
"Glass Factory Rotational Symmetry 3", "Glass Factory Front 3",
"Glass Factory Melting 3", "Glass Factory Melting 3",
"Symmetry Island Black Dots 5", "Symmetry Island Right 5",
"Symmetry Island Colored Dots 6", "Symmetry Island Back 6",
"Symmetry Island Fading Lines 7", "Symmetry Island Left 7",
"Symmetry Island Scenery Outlines 5", "Symmetry Island Scenery Outlines 5",
"Symmetry Island Laser Panel", "Symmetry Island Laser Panel",
@@ -48,26 +48,28 @@ class StaticWitnessLocations:
"Desert Vault Box", "Desert Vault Box",
"Desert Discard", "Desert Discard",
"Desert Sun Reflection 8", "Desert Surface 8",
"Desert Artificial Light Reflection 3", "Desert Light Room 3",
"Desert Pond Reflection 5", "Desert Pond Room 5",
"Desert Flood Reflection 6", "Desert Flood Room 6",
"Desert Final Bent 3",
"Desert Final Hexagonal",
"Desert Laser Panel", "Desert Laser Panel",
"Quarry Mill Eraser and Dots 6", "Quarry Mill Lower Row 6",
"Quarry Mill Eraser and Squares 8", "Quarry Mill Upper Row 8",
"Quarry Mill Small Squares & Dots & Eraser", "Quarry Mill Control Room Right",
"Quarry Boathouse Intro Shapers", "Quarry Boathouse Intro Right",
"Quarry Boathouse Intro Stars", "Quarry Boathouse Intro Left",
"Quarry Boathouse Eraser and Shapers 5", "Quarry Boathouse Front Row 5",
"Quarry Boathouse Stars & Eraser & Shapers 2", "Quarry Boathouse Back First Row 9",
"Quarry Boathouse Stars & Eraser & Shapers 5", "Quarry Boathouse Back Second Row 3",
"Quarry Discard", "Quarry Discard",
"Quarry Laser Panel", "Quarry Laser Panel",
"Shadows Lower Avoid 8", "Shadows Intro 8",
"Shadows Environmental Avoid 8", "Shadows Far 8",
"Shadows Follow 5", "Shadows Near 5",
"Shadows Laser Panel", "Shadows Laser Panel",
"Keep Hedge Maze 4", "Keep Hedge Maze 4",
@@ -79,44 +81,44 @@ class StaticWitnessLocations:
"Shipwreck Vault Box", "Shipwreck Vault Box",
"Shipwreck Discard", "Shipwreck Discard",
"Monastery Rhombic Avoid 3", "Monastery Outside 3",
"Monastery Branch Follow 2", "Monastery Inside 4",
"Monastery Laser Panel", "Monastery Laser Panel",
"Town Cargo Box Discard", "Town Cargo Box Discard",
"Town Hexagonal Reflection", "Town Tall Hexagonal",
"Town Church Lattice", "Town Church Lattice",
"Town Rooftop Discard", "Town Rooftop Discard",
"Town Symmetry Squares 5 + Dots", "Town Red Rooftop 5",
"Town Full Dot Grid Shapers 5", "Town Wooden Roof Lower Row 5",
"Town Shapers & Dots & Eraser", "Town Wooden Rooftop",
"Town Laser Panel", "Town Laser Panel",
"Theater Discard", "Theater Discard",
"Jungle Discard", "Jungle Discard",
"Jungle Waves 3", "Jungle First Row 3",
"Jungle Waves 7", "Jungle Second Row 4",
"Jungle Popup Wall 6", "Jungle Popup Wall 6",
"Jungle Laser Panel", "Jungle Laser Panel",
"River Vault Box", "River Vault Box",
"Bunker Drawn Squares 5", "Bunker Intro Left 5",
"Bunker Drawn Squares 9", "Bunker Intro Back 4",
"Bunker Drawn Squares through Tinted Glass 3", "Bunker Glass Room 3",
"Bunker Drop-Down Door Squares 2", "Bunker UV Room 2",
"Bunker Laser Panel", "Bunker Laser Panel",
"Swamp Seperatable Shapers 6", "Swamp Intro Front 6",
"Swamp Combinable Shapers 8", "Swamp Intro Back 8",
"Swamp Broken Shapers 4", "Swamp Between Bridges Near Row 4",
"Swamp Cyan Underwater Negative Shapers 5", "Swamp Cyan Underwater 5",
"Swamp Platform Shapers 4", "Swamp Platform Row 4",
"Swamp Rotated Shapers 4", "Swamp Between Bridges Far Row 4",
"Swamp Red Underwater Negative Shapers 4", "Swamp Red Underwater 4",
"Swamp More Rotated Shapers 4", "Swamp Beyond Rotating Bridge 4",
"Swamp Blue Underwater Negative Shapers 5", "Swamp Blue Underwater 5",
"Swamp Laser Panel", "Swamp Laser Panel",
"Treehouse Yellow Bridge 9", "Treehouse Yellow Bridge 9",
@@ -125,73 +127,77 @@ class StaticWitnessLocations:
"Treehouse Green Bridge 7", "Treehouse Green Bridge 7",
"Treehouse Green Bridge Discard", "Treehouse Green Bridge Discard",
"Treehouse Left Orange Bridge 15", "Treehouse Left Orange Bridge 15",
"Treehouse Burnt House Discard", "Treehouse Laser Discard",
"Treehouse Right Orange Bridge 12", "Treehouse Right Orange Bridge 12",
"Treehouse Laser Panel", "Treehouse Laser Panel",
"Mountaintop Discard", "Mountainside Discard",
"Mountaintop Vault Box", "Mountainside Vault Box",
}
UNCOMMON_LOCATIONS = {
"Mountaintop River Shape", "Mountaintop River Shape",
"Tutorial Patio Floor", "Tutorial Patio Floor",
"Quarry Mill Big Squares & Dots & Eraser", "Quarry Mill Control Room Left",
"Theater Tutorial Video", "Theater Tutorial Video",
"Theater Desert Video", "Theater Desert Video",
"Theater Jungle Video", "Theater Jungle Video",
"Theater Shipwreck Video", "Theater Shipwreck Video",
"Theater Mountain Video", "Theater Mountain Video",
"Town RGB Squares", "Town RGB Room Left",
"Town RGB Stars", "Town RGB Room Right",
"Swamp Underwater Back Optional", "Swamp Purple Underwater",
} }
CAVES_LOCATIONS = { CAVES_LOCATIONS = {
"Inside Mountain Caves Dot Grid Triangles 4", "Caves Blue Tunnel Right First 4",
"Inside Mountain Caves Symmetry Triangles", "Caves Blue Tunnel Left First 1",
"Inside Mountain Caves Stars & Squares and Triangles 2", "Caves Blue Tunnel Left Second 5",
"Inside Mountain Caves Shapers and Triangles 2", "Caves Blue Tunnel Right Second 5",
"Inside Mountain Caves Symmetry Shapers", "Caves Blue Tunnel Right Third 1",
"Inside Mountain Caves Broken and Negative Shapers", "Caves Blue Tunnel Left Fourth 1",
"Inside Mountain Caves Broken Shapers", "Caves Blue Tunnel Left Third 1",
"Inside Mountain Caves Rainbow Squares", "Caves First Floor Middle",
"Inside Mountain Caves Squares & Stars and Colored Eraser", "Caves First Floor Right",
"Inside Mountain Caves Rotated Broken Shapers", "Caves First Floor Left",
"Inside Mountain Caves Stars and Squares", "Caves First Floor Grounded",
"Inside Mountain Caves Lone Pillar", "Caves Lone Pillar",
"Inside Mountain Caves Wooden Beam Shapers", "Caves First Wooden Beam",
"Inside Mountain Caves Wooden Beam Squares and Shapers", "Caves Second Wooden Beam",
"Inside Mountain Caves Wooden Beam Stars and Squares", "Caves Third Wooden Beam",
"Inside Mountain Caves Wooden Beam Shapers and Stars", "Caves Fourth Wooden Beam",
"Inside Mountain Caves Upstairs Invisible Dots 8", "Caves Right Upstairs Left Row 8",
"Inside Mountain Caves Upstairs Invisible Dot Symmetry 3", "Caves Right Upstairs Right Row 3",
"Inside Mountain Caves Upstairs Dot Grid Negative Shapers", "Caves Left Upstairs Single",
"Inside Mountain Caves Upstairs Dot Grid Rotated Shapers", "Caves Left Upstairs Left Row 5",
"Theater Walkway Vault Box", "Tunnels Vault Box",
"Inside Mountain Bottom Layer Discard", "Mountain Bottom Floor Discard",
"Theater Challenge Video", "Theater Challenge Video",
} }
MOUNTAIN_UNREACHABLE_FROM_BEHIND = { MOUNTAIN_UNREACHABLE_FROM_BEHIND = {
"Mountaintop Trap Door Triple Exit", "Mountaintop Trap Door Triple Exit",
"Inside Mountain Obscured Vision 5", "Mountain Floor 1 Right Row 5",
"Inside Mountain Moving Background 7", "Mountain Floor 1 Left Row 7",
"Inside Mountain Physically Obstructed 3", "Mountain Floor 1 Back Row 3",
"Inside Mountain Angled Inside Trash 2", "Mountain Floor 1 Trash Pillar 2",
"Inside Mountain Color Cycle 5", "Mountain Floor 2 Near Row 5",
"Inside Mountain Same Solution 6", "Mountain Floor 2 Far Row 6",
} }
MOUNTAIN_REACHABLE_FROM_BEHIND = { MOUNTAIN_REACHABLE_FROM_BEHIND = {
"Inside Mountain Elevator Discard", "Mountain Floor 2 Elevator Discard",
"Inside Mountain Giant Puzzle", "Mountain Bottom Floor Giant Puzzle",
"Inside Mountain Final Room Left Pillar 4", "Mountain Final Room Left Pillar 4",
"Inside Mountain Final Room Right 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() ALL_LOCATIONS_TO_ID = dict()
@@ -241,37 +247,44 @@ class WitnessPlayerLocations:
StaticWitnessLocations.GENERAL_LOCATIONS 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") earlyutm = is_option_enabled(world, player, "early_secret_area")
victory = get_option_value(world, player, "victory_condition") 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") laser_shuffle = get_option_value(world, player, "shuffle_lasers")
postgame = set() postgame = set()
postgame = postgame | StaticWitnessLocations.CAVES_LOCATIONS postgame = postgame | StaticWitnessLocations.CAVES_LOCATIONS
postgame = postgame | StaticWitnessLocations.MOUNTAIN_REACHABLE_FROM_BEHIND postgame = postgame | StaticWitnessLocations.MOUNTAIN_REACHABLE_FROM_BEHIND
postgame = postgame | StaticWitnessLocations.MOUNTAIN_UNREACHABLE_FROM_BEHIND postgame = postgame | StaticWitnessLocations.MOUNTAIN_UNREACHABLE_FROM_BEHIND
postgame = postgame | StaticWitnessLocations.MOUNTAIN_EXTRAS
self.CHECK_LOCATIONS = self.CHECK_LOCATIONS | postgame 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 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 postgame -= StaticWitnessLocations.MOUNTAIN_REACHABLE_FROM_BEHIND
if victory != 2: if mountain_enterable_from_top:
postgame -= StaticWitnessLocations.MOUNTAIN_UNREACHABLE_FROM_BEHIND 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"): if is_option_enabled(world, player, "shuffle_discarded_panels"):
self.PANEL_TYPES_TO_SHUFFLE.add("Discard") self.PANEL_TYPES_TO_SHUFFLE.add("Discard")
if is_option_enabled(world, player, "shuffle_vault_boxes"): if is_option_enabled(world, player, "shuffle_vault_boxes"):
self.PANEL_TYPES_TO_SHUFFLE.add("Vault") 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 self.CHECK_LOCATIONS = self.CHECK_LOCATIONS | player_logic.ADDED_CHECKS
if not is_option_enabled(world, player, "shuffle_postgame"): if not is_option_enabled(world, player, "shuffle_postgame"):

View File

@@ -60,7 +60,10 @@ class WitnessPlayerLogic:
for dependentItem in door_items: for dependentItem in door_items:
all_options.add(items_option.union(dependentItem)) 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"] these_panels = self.DEPENDENT_REQUIREMENTS_BY_HEX[panel_hex]["panels"]
@@ -321,7 +324,7 @@ class WitnessPlayerLogic:
self.VICTORY_LOCATION = "0x0356B" self.VICTORY_LOCATION = "0x0356B"
self.EVENT_ITEM_NAMES = { self.EVENT_ITEM_NAMES = {
"0x01A0F": "Keep Laser Panel (Hedge Mazes) Activates", "0x01A0F": "Keep Laser Panel (Hedge Mazes) Activates",
"0x09D9B": "Monastery Overhead Doors Open", "0x09D9B": "Monastery Shutters Open",
"0x193A6": "Monastery Laser Panel Activates", "0x193A6": "Monastery Laser Panel Activates",
"0x00037": "Monastery Branch Panels Activate", "0x00037": "Monastery Branch Panels Activate",
"0x0A079": "Access to Bunker Laser", "0x0A079": "Access to Bunker Laser",
@@ -332,24 +335,24 @@ class WitnessPlayerLogic:
"0x01D3F": "Keep Laser Panel (Pressure Plates) Activates", "0x01D3F": "Keep Laser Panel (Pressure Plates) Activates",
"0x09F7F": "Mountain Access", "0x09F7F": "Mountain Access",
"0x0367C": "Quarry Laser Mill Requirement Met", "0x0367C": "Quarry Laser Mill Requirement Met",
"0x009A1": "Swamp Rotated Shapers 1 Activates", "0x009A1": "Swamp Between Bridges Far 1 Activates",
"0x00006": "Swamp Cyan Water Drains", "0x00006": "Swamp Cyan Water Drains",
"0x00990": "Swamp Broken Shapers 1 Activates", "0x00990": "Swamp Between Bridges Near Row 1 Activates",
"0x0A8DC": "Lower Avoid 6 Activates", "0x0A8DC": "Intro 6 Activates",
"0x0000A": "Swamp More Rotated Shapers 1 Access", "0x0000A": "Swamp Beyond Rotating Bridge 1 Access",
"0x09E86": "Inside Mountain Second Layer Blue Bridge Access", "0x09E86": "Mountain Floor 2 Blue Bridge Access",
"0x09ED8": "Inside Mountain Second Layer Yellow Bridge Access", "0x09ED8": "Mountain Floor 2 Yellow Bridge Access",
"0x0A3D0": "Quarry Laser Boathouse Requirement Met", "0x0A3D0": "Quarry Laser Boathouse Requirement Met",
"0x00596": "Swamp Red Water Drains", "0x00596": "Swamp Red Water Drains",
"0x00E3A": "Swamp Purple Water Drains", "0x00E3A": "Swamp Purple Water Drains",
"0x0343A": "Door to Symmetry Island Powers On", "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", "0x17CA6": "All Boat Panels Turn On",
"0x17CDF": "All Boat Panels Turn On", "0x17CDF": "All Boat Panels Turn On",
"0x09DB8": "All Boat Panels Turn On", "0x09DB8": "All Boat Panels Turn On",
"0x17C95": "All Boat Panels Turn On", "0x17C95": "All Boat Panels Turn On",
"0x03BB0": "Town Church Lattice Vision From Outside", "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", "0x28A69": "Town Tower 1st Door Opens",
"0x28ACC": "Town Tower 2nd Door Opens", "0x28ACC": "Town Tower 2nd Door Opens",
"0x28AD9": "Town Tower 3rd Door Opens", "0x28AD9": "Town Tower 3rd Door Opens",
@@ -357,9 +360,9 @@ class WitnessPlayerLogic:
"0x03675": "Quarry Mill Ramp Activation From Above", "0x03675": "Quarry Mill Ramp Activation From Above",
"0x03679": "Quarry Mill Lift Lowering While Standing On It", "0x03679": "Quarry Mill Lift Lowering While Standing On It",
"0x2FAF6": "Tutorial Gate Secret Solution Knowledge", "0x2FAF6": "Tutorial Gate Secret Solution Knowledge",
"0x079DF": "Town Hexagonal Reflection Turns On", "0x079DF": "Town Tall Hexagonal Turns On",
"0x17DA2": "Right Orange Bridge Fully Extended", "0x17DA2": "Right Orange Bridge Fully Extended",
"0x19B24": "Shadows Lower Avoid Patterns Visible", "0x19B24": "Shadows Intro Patterns Visible",
"0x2700B": "Open Door to Treehouse Laser House", "0x2700B": "Open Door to Treehouse Laser House",
"0x00055": "Orchard Apple Trees 4 Turns On", "0x00055": "Orchard Apple Trees 4 Turns On",
"0x17DDB": "Left Orange Bridge Fully Extended", "0x17DDB": "Left Orange Bridge Fully Extended",
@@ -369,6 +372,8 @@ class WitnessPlayerLogic:
"0x03481": "Tutorial Video Pattern Knowledge", "0x03481": "Tutorial Video Pattern Knowledge",
"0x03702": "Jungle Video Pattern Knowledge", "0x03702": "Jungle Video Pattern Knowledge",
"0x0356B": "Challenge 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 = { self.ALWAYS_EVENT_NAMES_BY_HEX = {

View File

@@ -73,7 +73,7 @@ class WitnessRegions:
all_locations = all_locations | set(locations_for_this_region) all_locations = all_locations | set(locations_for_this_region)
world.regions += [ 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(): for region_name, region in StaticWitnessLogic.ALL_REGIONS_BY_NAME.items():

View File

@@ -25,84 +25,84 @@ Disabled Locations:
0x00055 (Orchard Apple Tree 3) 0x00055 (Orchard Apple Tree 3)
0x032F7 (Orchard Apple Tree 4) 0x032F7 (Orchard Apple Tree 4)
0x032FF (Orchard Apple Tree 5) 0x032FF (Orchard Apple Tree 5)
0x198B5 (Shadows Lower Avoid 1) 0x198B5 (Shadows Intro 1)
0x198BD (Shadows Lower Avoid 2) 0x198BD (Shadows Intro 2)
0x198BF (Shadows Lower Avoid 3) 0x198BF (Shadows Intro 3)
0x19771 (Shadows Lower Avoid 4) 0x19771 (Shadows Intro 4)
0x0A8DC (Shadows Lower Avoid 5) 0x0A8DC (Shadows Intro 5)
0x0AC74 (Shadows Lower Avoid 6) 0x0AC74 (Shadows Intro 6)
0x0AC7A (Shadows Lower Avoid 7) 0x0AC7A (Shadows Intro 7)
0x0A8E0 (Shadows Lower Avoid 8) 0x0A8E0 (Shadows Intro 8)
0x386FA (Shadows Environmental Avoid 1) 0x386FA (Shadows Far 1)
0x1C33F (Shadows Environmental Avoid 2) 0x1C33F (Shadows Far 2)
0x196E2 (Shadows Environmental Avoid 3) 0x196E2 (Shadows Far 3)
0x1972A (Shadows Environmental Avoid 4) 0x1972A (Shadows Far 4)
0x19809 (Shadows Environmental Avoid 5) 0x19809 (Shadows Far 5)
0x19806 (Shadows Environmental Avoid 6) 0x19806 (Shadows Far 6)
0x196F8 (Shadows Environmental Avoid 7) 0x196F8 (Shadows Far 7)
0x1972F (Shadows Environmental Avoid 8) 0x1972F (Shadows Far 8)
0x19797 (Shadows Follow 1) 0x19797 (Shadows Near 1)
0x1979A (Shadows Follow 2) 0x1979A (Shadows Near 2)
0x197E0 (Shadows Follow 3) 0x197E0 (Shadows Near 3)
0x197E8 (Shadows Follow 4) 0x197E8 (Shadows Near 4)
0x197E5 (Shadows Follow 5) 0x197E5 (Shadows Near 5)
0x19650 (Shadows Laser) 0x19650 (Shadows Laser)
0x00139 (Keep Hedge Maze 1) 0x00139 (Keep Hedge Maze 1)
0x019DC (Keep Hedge Maze 2) 0x019DC (Keep Hedge Maze 2)
0x019E7 (Keep Hedge Maze 3) 0x019E7 (Keep Hedge Maze 3)
0x01A0F (Keep Hedge Maze 4) 0x01A0F (Keep Hedge Maze 4)
0x0360E (Laser Hedges) 0x0360E (Laser Hedges)
0x00B10 (Monastery Door Open Left) 0x00B10 (Monastery Entry Left)
0x00C92 (Monastery Door Open Right) 0x00C92 (Monastery Entry Right)
0x00290 (Monastery Rhombic Avoid 1) 0x00290 (Monastery Outside 1)
0x00038 (Monastery Rhombic Avoid 2) 0x00038 (Monastery Outside 2)
0x00037 (Monastery Rhombic Avoid 3) 0x00037 (Monastery Outside 3)
0x193A7 (Monastery Branch Avoid 1) 0x193A7 (Monastery Inside 1)
0x193AA (Monastery Branch Avoid 2) 0x193AA (Monastery Inside 2)
0x193AB (Monastery Branch Follow 1) 0x193AB (Monastery Inside 3)
0x193A6 (Monastery Branch Follow 2) 0x193A6 (Monastery Inside 4)
0x17CA4 (Monastery Laser) 0x17CA4 (Monastery Laser)
0x18590 (Tree Outlines) - True - Symmetry & Environment 0x18590 (Transparent) - True - Symmetry & Environment
0x28AE3 (Vines Shadows Follow) - 0x18590 - Shadows Follow & Environment 0x28AE3 (Vines) - 0x18590 - Shadows Follow & Environment
0x28938 (Four-way Apple Tree) - 0x28AE3 - Environment 0x28938 (Apple Tree) - 0x28AE3 - Environment
0x079DF (Triple Environmental Puzzle) - 0x28938 - Shadows Avoid & Environment & Reflection 0x079DF (Triple Exit) - 0x28938 - Shadows Avoid & Environment & Reflection
0x28B39 (Hexagonal Reflection) - 0x079DF & 0x2896A - Reflection 0x28B39 (Tall Hexagonal) - 0x079DF & 0x2896A - Reflection
0x03553 (Theater Tutorial Video) 0x03553 (Theater Tutorial Video)
0x03552 (Theater Desert Video) 0x03552 (Theater Desert Video)
0x0354E (Theater Jungle Video) 0x0354E (Theater Jungle Video)
0x03549 (Theater Challenge Video) 0x03549 (Theater Challenge Video)
0x0354F (Theater Shipwreck Video) 0x0354F (Theater Shipwreck Video)
0x03545 (Theater Mountain Video) 0x03545 (Theater Mountain Video)
0x002C4 (Waves 1) 0x002C4 (First Row 1)
0x00767 (Waves 2) 0x00767 (First Row 2)
0x002C6 (Waves 3) 0x002C6 (First Row 3)
0x0070E (Waves 4) 0x0070E (Second Row 1)
0x0070F (Waves 5) 0x0070F (Second Row 2)
0x0087D (Waves 6) 0x0087D (Second Row 3)
0x002C7 (Waves 7) 0x002C7 (Second Row 4)
0x15ADD (River Rhombic Avoid Vault) 0x15ADD (River Outside Vault)
0x03702 (River Vault Box) 0x03702 (River Vault Box)
0x17CAA (Rhombic Avoid to Monastery Garden) 0x17CAA (Monastery Shortcut Panel)
0x17C2E (Door to Bunker) 0x17C2E (Door to Bunker)
0x09F7D (Bunker Drawn Squares 1) 0x09F7D (Bunker Intro Left 1)
0x09FDC (Bunker Drawn Squares 2) 0x09FDC (Bunker Intro Left 2)
0x09FF7 (Bunker Drawn Squares 3) 0x09FF7 (Bunker Intro Left 3)
0x09F82 (Bunker Drawn Squares 4) 0x09F82 (Bunker Intro Left 4)
0x09FF8 (Bunker Drawn Squares 5) 0x09FF8 (Bunker Intro Left 5)
0x09D9F (Bunker Drawn Squares 6) 0x09D9F (Bunker Intro Back 1)
0x09DA1 (Bunker Drawn Squares 7) 0x09DA1 (Bunker Intro Back 2)
0x09DA2 (Bunker Drawn Squares 8) 0x09DA2 (Bunker Intro Back 3)
0x09DAF (Bunker Drawn Squares 9) 0x09DAF (Bunker Intro Back 4)
0x0A010 (Bunker Drawn Squares through Tinted Glass 1) 0x0A010 (Bunker Glass Room 1)
0x0A01B (Bunker Drawn Squares through Tinted Glass 2) 0x0A01B (Bunker Glass Room 2)
0x0A01F (Bunker Drawn Squares through Tinted Glass 3) 0x0A01F (Bunker Glass Room 3)
0x0A099 (Door to Bunker Proper) 0x0A099 (Tinted Glass Door)
0x34BC5 (Bunker Drop-Down Door Open) 0x34BC5 (Bunker Drop-Down Door Open)
0x34BC6 (Bunker Drop-Down Door Close) 0x34BC6 (Bunker Drop-Down Door Close)
0x17E63 (Bunker Drop-Down Door Squares 1) 0x17E63 (Bunker UV Room 1)
0x17E67 (Bunker Drop-Down Door Squares 2) 0x17E67 (Bunker UV Room 2)
0x09DE0 (Bunker Laser) 0x09DE0 (Bunker Laser)
0x0A079 (Bunker Elevator Control) 0x0A079 (Bunker Elevator Control)
0x0042D (Mountaintop River Shape) 0x0042D (Mountaintop River Shape)
0x17CAA (River Door to Garden Panel) 0x17CAA (River Garden Entry Panel)

View File

@@ -1,30 +1,30 @@
Items: Items:
Glass Factory Entry Door (Panel) Glass Factory Entry (Panel)
Door to Symmetry Island Lower (Panel) Symmetry Island Lower (Panel)
Door to Symmetry Island Upper (Panel) Symmetry Island Upper (Panel)
Door to Desert Flood Light Room (Panel) Desert Light Room Entry (Panel)
Desert Flood Room Flood Controls (Panel) Desert Flood Controls (Panel)
Quarry Door to Mill (Panel) Quarry Mill Entry (Panel)
Quarry Mill Ramp 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 Height Control (Panel)
Quarry Boathouse Ramp Horizontal Control (Panel) Quarry Boathouse Ramp Horizontal Control (Panel)
Shadows Door Timer (Panel) Shadows Door Timer (Panel)
Monastery Entry Door Left (Panel) Monastery Entry Left (Panel)
Monastery Entry Door Right (Panel) Monastery Entry Right (Panel)
Town Door to RGB House (Panel) Town Tinted Glass Door (Panel)
Town Door to Church (Panel) Town Church Entry (Panel)
Town Maze Panel (Drop-Down Staircase) (Panel) Town Maze Panel (Drop-Down Staircase) (Panel)
Windmill Door (Panel) Windmill Entry (Panel)
Treehouse First & Second Doors (Panel) Treehouse First & Second Doors (Panel)
Treehouse Third Door (Panel) Treehouse Third Door (Panel)
Treehouse Laser House Door Timer (Panel) Treehouse Laser House Door Timer (Panel)
Treehouse Shortcut Drop-Down Bridge (Panel) Treehouse Drawbridge (Panel)
Jungle Popup Wall (Panel) Jungle Popup Wall (Panel)
Bunker Entry Door (Panel) Bunker Entry (Panel)
Inside Bunker Door to Bunker Proper (Panel) Bunker Tinted Glass Door (Panel)
Bunker Elevator Control (Panel) Bunker Elevator Control (Panel)
Swamp Entry Door (Panel) Swamp Entry (Panel)
Swamp Sliding Bridge (Panel) Swamp Sliding Bridge (Panel)
Swamp Rotating Bridge (Panel) Swamp Rotating Bridge (Panel)
Swamp Maze Control (Panel) Swamp Maze Control (Panel)

View File

@@ -1,128 +1,128 @@
Items: Items:
Outside Tutorial Optional Door Outside Tutorial Outpost Path (Door)
Outside Tutorial Outpost Entry Door Outside Tutorial Outpost Entry (Door)
Outside Tutorial Outpost Exit Door Outside Tutorial Outpost Exit (Door)
Glass Factory Entry Door Glass Factory Entry (Door)
Glass Factory Back Wall Glass Factory Back Wall (Door)
Symmetry Island Lower Door Symmetry Island Lower (Door)
Symmetry Island Upper Door Symmetry Island Upper (Door)
Orchard Middle Gate Orchard First Gate (Door)
Orchard Final Gate Orchard Second Gate (Door)
Desert Door to Flood Light Room Desert Light Room Entry (Door)
Desert Door to Pond Room Desert Pond Room Entry (Door)
Desert Door to Water Levels Room Desert Flood Room Entry (Door)
Desert Door to Elevator Room Desert Elevator Room Entry (Door)
Quarry Main Entry 1 Quarry Entry 1 (Door)
Quarry Main Entry 2 Quarry Entry 2 (Door)
Quarry Door to Mill Quarry Mill Entry (Door)
Quarry Mill Side Door Quarry Mill Side Exit (Door)
Quarry Mill Rooftop Shortcut Quarry Mill Roof Exit (Door)
Quarry Mill Stairs Quarry Mill Stairs (Door)
Quarry Boathouse Boat Staircase Quarry Boathouse Dock (Door)
Quarry Boathouse First Barrier Quarry Boathouse First Barrier (Door)
Quarry Boathouse Shortcut Quarry Boathouse Second Barrier (Door)
Shadows Timed Door Shadows Timed Door (Door)
Shadows Laser Room Right Door Shadows Laser Entry Right (Door)
Shadows Laser Room Left Door Shadows Laser Entry Left (Door)
Shadows Barrier to Quarry Shadows Quarry Barrier (Door)
Shadows Barrier to Ledge Shadows Ledge Barrier (Door)
Keep Hedge Maze 1 Exit Door Keep Hedge Maze 1 Exit (Door)
Keep Pressure Plates 1 Exit Door Keep Pressure Plates 1 Exit (Door)
Keep Hedge Maze 2 Shortcut Keep Hedge Maze 2 Shortcut (Door)
Keep Hedge Maze 2 Exit Door Keep Hedge Maze 2 Exit (Door)
Keep Hedge Maze 3 Shortcut Keep Hedge Maze 3 Shortcut (Door)
Keep Hedge Maze 3 Exit Door Keep Hedge Maze 3 Exit (Door)
Keep Hedge Maze 4 Shortcut Keep Hedge Maze 4 Shortcut (Door)
Keep Hedge Maze 4 Exit Door Keep Hedge Maze 4 Exit (Door)
Keep Pressure Plates 2 Exit Door Keep Pressure Plates 2 Exit (Door)
Keep Pressure Plates 3 Exit Door Keep Pressure Plates 3 Exit (Door)
Keep Pressure Plates 4 Exit Door Keep Pressure Plates 4 Exit (Door)
Keep Shortcut to Shadows Keep Shadows Shortcut (Door)
Keep Tower Shortcut Keep Tower Shortcut (Door)
Monastery Shortcut Monastery Shortcut (Door)
Monastery Inner Door Monastery Entry Inner (Door)
Monastery Outer Door Monastery Entry Outer (Door)
Monastery Door to Garden Monastery Garden Entry (Door)
Town Cargo Box Door Town Cargo Box Entry (Door)
Town Wooden Roof Staircase Town Wooden Roof Stairs (Door)
Town Tinted Door to RGB House Town Tinted Glass Door (Door)
Town Door to Church Town Church Entry (Door)
Town Maze Staircase Town Maze Stairs (Door)
Town Windmill Door Town Windmill Entry (Door)
Town RGB House Staircase Town RGB House Stairs (Door)
Town Tower Blue Panels Door Town Tower First Door (Door)
Town Tower Lattice Door Town Tower Third Door (Door)
Town Tower Environmental Set Door Town Tower Fourth Door (Door)
Town Tower Wooden Roof Set Door Town Tower Second Door (Door)
Theater Entry Door Theater Entry (Door)
Theater Exit Door Left Theater Exit Left (Door)
Theater Exit Door Right Theater Exit Right (Door)
Jungle Bamboo Shortcut to River Jungle Bamboo Laser Shortcut (Door)
Jungle Popup Wall Jungle Popup Wall (Door)
River Shortcut to Monastery Garden River Monastery Shortcut (Door)
Bunker Bunker Entry Door Bunker Entry (Door)
Bunker Tinted Glass Door Bunker Tinted Glass Door (Door)
Bunker Door to Ultraviolet Room Bunker UV Room Entry (Door)
Bunker Door to Elevator Bunker Elevator Room Entry (Door)
Swamp Entry Door Swamp Entry (Door)
Swamp Door to Broken Shapers Swamp Between Bridges First Door
Swamp Platform Shortcut Door Swamp Platform Shortcut Door
Swamp Cyan Water Pump Swamp Cyan Water Pump (Door)
Swamp Door to Rotated Shapers Swamp Between Bridges Second Door
Swamp Red Water Pump Swamp Red Water Pump (Door)
Swamp Red Underwater Exit Swamp Red Underwater Exit (Door)
Swamp Blue Water Pump Swamp Blue Water Pump (Door)
Swamp Purple Water Pump Swamp Purple Water Pump (Door)
Swamp Near Laser Shortcut Swamp Laser Shortcut (Door)
Treehouse First Door Treehouse First Door (Door)
Treehouse Second Door Treehouse Second Door (Door)
Treehouse Beyond Yellow Bridge Door Treehouse Third Door (Door)
Treehouse Drawbridge Treehouse Drawbridge (Door)
Treehouse Timed Door to Laser House Treehouse Laser House Entry (Door)
Inside Mountain First Layer Exit Door Mountain Floor 1 Exit (Door)
Inside Mountain Second Layer Staircase Near Mountain Floor 2 Staircase Near (Door)
Inside Mountain Second Layer Exit Door Mountain Floor 2 Exit (Door)
Inside Mountain Second Layer Staircase Far Mountain Floor 2 Staircase Far (Door)
Inside Mountain Giant Puzzle Exit Door Mountain Bottom Floor Giant Puzzle Exit (Door)
Inside Mountain Door to Final Room Mountain Bottom Floor Final Room Entry (Door)
Inside Mountain Bottom Layer Rock Mountain Bottom Floor Rock (Door)
Inside Mountain Door to Secret Area Caves Entry (Door)
Caves Pillar Door Caves Pillar Door (Door)
Caves Mountain Shortcut Caves Mountain Shortcut (Door)
Caves Swamp Shortcut Caves Swamp Shortcut (Door)
Challenge Entry Door Challenge Entry (Door)
Challenge Door to Theater Walkway Challenge Tunnels Entry (Door)
Theater Walkway Door to Windmill Interior Tunnels Theater Shortcut (Door)
Theater Walkway Door to Desert Elevator Room Tunnels Desert Shortcut (Door)
Theater Walkway Door to Town Tunnels Town Shortcut (Door)
Added Locations: Added Locations:
Outside Tutorial Door to Outpost Panel Outside Tutorial Outpost Entry Panel
Outside Tutorial Exit Door from Outpost Panel Outside Tutorial Outpost Exit Panel
Glass Factory Entry Door Panel Glass Factory Entry Panel
Glass Factory Vertical Symmetry 5 Glass Factory Back Wall 5
Symmetry Island Door to Symmetry Island Lower Panel Symmetry Island Lower Panel
Symmetry Island Door to Symmetry Island Upper Panel Symmetry Island Upper Panel
Orchard Apple Tree 3 Orchard Apple Tree 3
Orchard Apple Tree 5 Orchard Apple Tree 5
Desert Door to Desert Flood Light Room Panel Desert Light Room Entry Panel
Desert Artificial Light Reflection 3 Desert Light Room 3
Desert Door to Water Levels Room Panel Desert Flood Room Entry Panel
Desert Flood Reflection 6 Desert Flood Room 6
Quarry Door to Quarry 1 Panel Quarry Entry 1 Panel
Quarry Door to Quarry 2 Panel Quarry Entry 2 Panel
Quarry Door to Mill Right Quarry Mill Entry Right Panel
Quarry Door to Mill Left Quarry Mill Entry Left Panel
Quarry Mill Ground Floor Shortcut Door Panel Quarry Mill Side Exit Panel
Quarry Mill Door to Outside Quarry Stairs Panel Quarry Mill Roof Exit Panel
Quarry Mill Stair Control Quarry Mill Stair Control
Quarry Boathouse Shortcut Door Panel Quarry Boathouse Second Barrier Panel
Shadows Door Timer Inside Shadows Door Timer Inside
Shadows Door Timer Outside Shadows Door Timer Outside
Shadows Environmental Avoid 8 Shadows Far 8
Shadows Follow 5 Shadows Near 5
Shadows Lower Avoid 3 Shadows Intro 3
Shadows Lower Avoid 5 Shadows Intro 5
Keep Hedge Maze 1 Keep Hedge Maze 1
Keep Pressure Plates 1 Keep Pressure Plates 1
Keep Hedge Maze 2 Keep Hedge Maze 2
@@ -131,71 +131,70 @@ Keep Hedge Maze 4
Keep Pressure Plates 2 Keep Pressure Plates 2
Keep Pressure Plates 3 Keep Pressure Plates 3
Keep Pressure Plates 4 Keep Pressure Plates 4
Keep Shortcut to Shadows Panel Keep Shadows Shortcut Panel
Keep Tower Shortcut to Keep Panel Keep Tower Shortcut Panel
Monastery Shortcut Door Panel Monastery Shortcut Panel
Monastery Door Open Left Monastery Entry Left
Monastery Door Open Right Monastery Entry Right
Monastery Rhombic Avoid 3 Monastery Outside 3
Town Cargo Box Panel Town Cargo Box Entry Panel
Town Full Dot Grid Shapers 5 Town Wooden Roof Lower Row 5
Town Tinted Door Panel Town Tinted Glass Door Panel
Town Door to Church Stars Panel Town Church Entry Panel
Town Maze Stair Control Town Maze Stair Control
Town Windmill Door Panel Town Windmill Entry Panel
Town Sound Room Left
Town Sound Room Right Town Sound Room Right
Town Symmetry Squares 5 + Dots Town Red Rooftop 5
Town Church Lattice Town Church Lattice
Town Hexagonal Reflection Town Tall Hexagonal
Town Shapers & Dots & Eraser Town Wooden Rooftop
Windmill Door to Front of Theater Panel Windmill Theater Entry Panel
Theater Door to Cargo Box Left Panel Theater Exit Left Panel
Theater Door to Cargo Box Right Panel Theater Exit Right Panel
Jungle Shortcut to River Panel Jungle Laser Shortcut Panel
Jungle Popup Wall Control Jungle Popup Wall Control
River Rhombic Avoid to Monastery Garden River Monastery Shortcut Panel
Bunker Bunker Entry Panel Bunker Entry Panel
Bunker Door to Bunker Proper Panel Bunker Tinted Glass Door Panel
Bunker Drawn Squares through Tinted Glass 3 Bunker Glass Room 3
Bunker Drop-Down Door Squares 2 Bunker UV Room 2
Swamp Entry Panel Swamp Entry Panel
Swamp Platform Shapers 4 Swamp Platform Row 4
Swamp Platform Shortcut Right Panel Swamp Platform Shortcut Right Panel
Swamp Blue Underwater Negative Shapers 5 Swamp Blue Underwater 5
Swamp Broken Shapers 4 Swamp Between Bridges Near Row 4
Swamp Cyan Underwater Negative Shapers 5 Swamp Cyan Underwater 5
Swamp Red Underwater Negative Shapers 4 Swamp Red Underwater 4
Swamp More Rotated Shapers 4 Swamp Beyond Rotating Bridge 4
Swamp More Rotated Shapers 4 Swamp Beyond Rotating Bridge 4
Swamp Near Laser Shortcut Right Panel Swamp Laser Shortcut Right Panel
Treehouse First Door Panel Treehouse First Door Panel
Treehouse Second Door Panel Treehouse Second Door Panel
Treehouse Beyond Yellow Bridge Door Panel Treehouse Third Door Panel
Treehouse Bridge Control Treehouse Bridge Control
Treehouse Left Orange Bridge 15 Treehouse Left Orange Bridge 15
Treehouse Right Orange Bridge 12 Treehouse Right Orange Bridge 12
Treehouse Laser House Door Timer Outside Control Treehouse Laser House Door Timer Outside Control
Treehouse Laser House Door Timer Inside Control Treehouse Laser House Door Timer Inside
Inside Mountain Moving Background 7 Mountain Floor 1 Left Row 7
Inside Mountain Obscured Vision 5 Mountain Floor 1 Right Row 5
Inside Mountain Physically Obstructed 3 Mountain Floor 1 Back Row 3
Inside Mountain Angled Inside Trash 2 Mountain Floor 1 Trash Pillar 2
Inside Mountain Color Cycle 5 Mountain Floor 2 Near Row 5
Inside Mountain Light Bridge Controller 2 Mountain Floor 2 Light Bridge Controller Near
Inside Mountain Light Bridge Controller 3 Mountain Floor 2 Light Bridge Controller Far
Inside Mountain Same Solution 6 Mountain Floor 2 Far Row 6
Inside Mountain Giant Puzzle Mountain Bottom Floor Giant Puzzle
Inside Mountain Door to Final Room Left Mountain Bottom Floor Final Room Entry Left
Inside Mountain Door to Final Room Right Mountain Bottom Floor Final Room Entry Right
Inside Mountain Bottom Layer Discard Mountain Bottom Floor Discard
Inside Mountain Rock Control Mountain Bottom Floor Rock Control
Inside Mountain Secret Area Entry Panel Mountain Bottom Floor Caves Entry Panel
Inside Mountain Caves Lone Pillar Caves Lone Pillar
Inside Mountain Caves Shortcut to Mountain Panel Caves Mountain Shortcut Panel
Inside Mountain Caves Shortcut to Swamp Panel Caves Swamp Shortcut Panel
Inside Mountain Caves Challenge Entry Panel Caves Challenge Entry Panel
Challenge Door to Theater Walkway Panel Challenge Tunnels Entry Panel
Theater Walkway Theater Shortcut Panel Tunnels Theater Shortcut Panel
Theater Walkway Desert Shortcut Panel Tunnels Desert Shortcut Panel
Theater Walkway Town Shortcut Panel Tunnels Town Shortcut Panel

View File

@@ -1,104 +1,104 @@
Items: Items:
Outside Tutorial Optional Door Outside Tutorial Outpost Path (Door)
Outside Tutorial Outpost Entry Door Outside Tutorial Outpost Entry (Door)
Outside Tutorial Outpost Exit Door Outside Tutorial Outpost Exit (Door)
Glass Factory Entry Door Glass Factory Entry (Door)
Glass Factory Back Wall Glass Factory Back Wall (Door)
Symmetry Island Lower Door Symmetry Island Lower (Door)
Symmetry Island Upper Door Symmetry Island Upper (Door)
Orchard Middle Gate Orchard First Gate (Door)
Orchard Final Gate Orchard Second Gate (Door)
Desert Door to Flood Light Room Desert Light Room Entry (Door)
Desert Door to Pond Room Desert Pond Room Entry (Door)
Desert Door to Water Levels Room Desert Flood Room Entry (Door)
Desert Door to Elevator Room Desert Elevator Room Entry (Door)
Quarry Main Entry 1 Quarry Entry 1 (Door)
Quarry Main Entry 2 Quarry Entry 2 (Door)
Quarry Door to Mill Quarry Mill Entry (Door)
Quarry Mill Side Door Quarry Mill Side Exit (Door)
Quarry Mill Rooftop Shortcut Quarry Mill Roof Exit (Door)
Quarry Mill Stairs Quarry Mill Stairs (Door)
Quarry Boathouse Boat Staircase Quarry Boathouse Dock (Door)
Quarry Boathouse First Barrier Quarry Boathouse First Barrier (Door)
Quarry Boathouse Shortcut Quarry Boathouse Second Barrier (Door)
Shadows Timed Door Shadows Timed Door (Door)
Shadows Laser Room Right Door Shadows Laser Entry Right (Door)
Shadows Laser Room Left Door Shadows Laser Entry Left (Door)
Shadows Barrier to Quarry Shadows Quarry Barrier (Door)
Shadows Barrier to Ledge Shadows Ledge Barrier (Door)
Keep Hedge Maze 1 Exit Door Keep Hedge Maze 1 Exit (Door)
Keep Pressure Plates 1 Exit Door Keep Pressure Plates 1 Exit (Door)
Keep Hedge Maze 2 Shortcut Keep Hedge Maze 2 Shortcut (Door)
Keep Hedge Maze 2 Exit Door Keep Hedge Maze 2 Exit (Door)
Keep Hedge Maze 3 Shortcut Keep Hedge Maze 3 Shortcut (Door)
Keep Hedge Maze 3 Exit Door Keep Hedge Maze 3 Exit (Door)
Keep Hedge Maze 4 Shortcut Keep Hedge Maze 4 Shortcut (Door)
Keep Hedge Maze 4 Exit Door Keep Hedge Maze 4 Exit (Door)
Keep Pressure Plates 2 Exit Door Keep Pressure Plates 2 Exit (Door)
Keep Pressure Plates 3 Exit Door Keep Pressure Plates 3 Exit (Door)
Keep Pressure Plates 4 Exit Door Keep Pressure Plates 4 Exit (Door)
Keep Shortcut to Shadows Keep Shadows Shortcut (Door)
Keep Tower Shortcut Keep Tower Shortcut (Door)
Monastery Shortcut Monastery Shortcut (Door)
Monastery Inner Door Monastery Entry Inner (Door)
Monastery Outer Door Monastery Entry Outer (Door)
Monastery Door to Garden Monastery Garden Entry (Door)
Town Cargo Box Door Town Cargo Box Entry (Door)
Town Wooden Roof Staircase Town Wooden Roof Stairs (Door)
Town Tinted Door to RGB House Town Tinted Glass Door (Door)
Town Door to Church Town Church Entry (Door)
Town Maze Staircase Town Maze Stairs (Door)
Town Windmill Door Town Windmill Entry (Door)
Town RGB House Staircase Town RGB House Stairs (Door)
Town Tower Blue Panels Door Town Tower First Door (Door)
Town Tower Lattice Door Town Tower Third Door (Door)
Town Tower Environmental Set Door Town Tower Fourth Door (Door)
Town Tower Wooden Roof Set Door Town Tower Second Door (Door)
Theater Entry Door Theater Entry (Door)
Theater Exit Door Left Theater Exit Left (Door)
Theater Exit Door Right Theater Exit Right (Door)
Jungle Bamboo Shortcut to River Jungle Bamboo Laser Shortcut (Door)
Jungle Popup Wall Jungle Popup Wall (Door)
River Shortcut to Monastery Garden River Monastery Shortcut (Door)
Bunker Bunker Entry Door Bunker Entry (Door)
Bunker Tinted Glass Door Bunker Tinted Glass Door (Door)
Bunker Door to Ultraviolet Room Bunker UV Room Entry (Door)
Bunker Door to Elevator Bunker Elevator Room Entry (Door)
Swamp Entry Door Swamp Entry (Door)
Swamp Door to Broken Shapers Swamp Between Bridges First Door
Swamp Platform Shortcut Door Swamp Platform Shortcut Door
Swamp Cyan Water Pump Swamp Cyan Water Pump (Door)
Swamp Door to Rotated Shapers Swamp Between Bridges Second Door
Swamp Red Water Pump Swamp Red Water Pump (Door)
Swamp Red Underwater Exit Swamp Red Underwater Exit (Door)
Swamp Blue Water Pump Swamp Blue Water Pump (Door)
Swamp Purple Water Pump Swamp Purple Water Pump (Door)
Swamp Near Laser Shortcut Swamp Laser Shortcut (Door)
Treehouse First Door Treehouse First Door (Door)
Treehouse Second Door Treehouse Second Door (Door)
Treehouse Beyond Yellow Bridge Door Treehouse Third Door (Door)
Treehouse Drawbridge Treehouse Drawbridge (Door)
Treehouse Timed Door to Laser House Treehouse Laser House Entry (Door)
Inside Mountain First Layer Exit Door Mountain Floor 1 Exit (Door)
Inside Mountain Second Layer Staircase Near Mountain Floor 2 Staircase Near (Door)
Inside Mountain Second Layer Exit Door Mountain Floor 2 Exit (Door)
Inside Mountain Second Layer Staircase Far Mountain Floor 2 Staircase Far (Door)
Inside Mountain Giant Puzzle Exit Door Mountain Bottom Floor Giant Puzzle Exit (Door)
Inside Mountain Door to Final Room Mountain Bottom Floor Final Room Entry (Door)
Inside Mountain Bottom Layer Rock Mountain Bottom Floor Rock (Door)
Inside Mountain Door to Secret Area Caves Entry (Door)
Caves Pillar Door Caves Pillar Door (Door)
Caves Mountain Shortcut Caves Mountain Shortcut (Door)
Caves Swamp Shortcut Caves Swamp Shortcut (Door)
Challenge Entry Door Challenge Entry (Door)
Challenge Door to Theater Walkway Challenge Tunnels Entry (Door)
Theater Walkway Door to Windmill Interior Tunnels Theater Shortcut (Door)
Theater Walkway Door to Desert Elevator Room Tunnels Desert Shortcut (Door)
Theater Walkway Door to Town Tunnels Town Shortcut (Door)
Desert Flood Room Flood Controls (Panel) Desert Flood Controls (Panel)
Quarry Mill Ramp 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 Height Control (Panel)
Quarry Boathouse Ramp Horizontal Control (Panel) Quarry Boathouse Ramp Horizontal Control (Panel)
Bunker Elevator Control (Panel) Bunker Elevator Control (Panel)
@@ -108,32 +108,32 @@ Swamp Maze Control (Panel)
Boat Boat
Added Locations: Added Locations:
Outside Tutorial Door to Outpost Panel Outside Tutorial Outpost Entry Panel
Outside Tutorial Exit Door from Outpost Panel Outside Tutorial Outpost Exit Panel
Glass Factory Entry Door Panel Glass Factory Entry Panel
Glass Factory Vertical Symmetry 5 Glass Factory Back Wall 5
Symmetry Island Door to Symmetry Island Lower Panel Symmetry Island Lower Panel
Symmetry Island Door to Symmetry Island Upper Panel Symmetry Island Upper Panel
Orchard Apple Tree 3 Orchard Apple Tree 3
Orchard Apple Tree 5 Orchard Apple Tree 5
Desert Door to Desert Flood Light Room Panel Desert Light Room Entry Panel
Desert Artificial Light Reflection 3 Desert Light Room 3
Desert Door to Water Levels Room Panel Desert Flood Room Entry Panel
Desert Flood Reflection 6 Desert Flood Room 6
Quarry Door to Quarry 1 Panel Quarry Entry 1 Panel
Quarry Door to Quarry 2 Panel Quarry Entry 2 Panel
Quarry Door to Mill Right Quarry Mill Entry Right Panel
Quarry Door to Mill Left Quarry Mill Entry Left Panel
Quarry Mill Ground Floor Shortcut Door Panel Quarry Mill Side Exit Panel
Quarry Mill Door to Outside Quarry Stairs Panel Quarry Mill Roof Exit Panel
Quarry Mill Stair Control Quarry Mill Stair Control
Quarry Boathouse Shortcut Door Panel Quarry Boathouse Second Barrier Panel
Shadows Door Timer Inside Shadows Door Timer Inside
Shadows Door Timer Outside Shadows Door Timer Outside
Shadows Environmental Avoid 8 Shadows Far 8
Shadows Follow 5 Shadows Near 5
Shadows Lower Avoid 3 Shadows Intro 3
Shadows Lower Avoid 5 Shadows Intro 5
Keep Hedge Maze 1 Keep Hedge Maze 1
Keep Pressure Plates 1 Keep Pressure Plates 1
Keep Hedge Maze 2 Keep Hedge Maze 2
@@ -142,71 +142,70 @@ Keep Hedge Maze 4
Keep Pressure Plates 2 Keep Pressure Plates 2
Keep Pressure Plates 3 Keep Pressure Plates 3
Keep Pressure Plates 4 Keep Pressure Plates 4
Keep Shortcut to Shadows Panel Keep Shadows Shortcut Panel
Keep Tower Shortcut to Keep Panel Keep Tower Shortcut Panel
Monastery Shortcut Door Panel Monastery Shortcut Panel
Monastery Door Open Left Monastery Entry Left
Monastery Door Open Right Monastery Entry Right
Monastery Rhombic Avoid 3 Monastery Outside 3
Town Cargo Box Panel Town Cargo Box Entry Panel
Town Full Dot Grid Shapers 5 Town Wooden Roof Lower Row 5
Town Tinted Door Panel Town Tinted Glass Door Panel
Town Door to Church Stars Panel Town Church Entry Panel
Town Maze Stair Control Town Maze Stair Control
Town Windmill Door Panel Town Windmill Entry Panel
Town Sound Room Left
Town Sound Room Right Town Sound Room Right
Town Symmetry Squares 5 + Dots Town Red Rooftop 5
Town Church Lattice Town Church Lattice
Town Hexagonal Reflection Town Tall Hexagonal
Town Shapers & Dots & Eraser Town Wooden Rooftop
Windmill Door to Front of Theater Panel Windmill Theater Entry Panel
Theater Door to Cargo Box Left Panel Theater Exit Left Panel
Theater Door to Cargo Box Right Panel Theater Exit Right Panel
Jungle Shortcut to River Panel Jungle Laser Shortcut Panel
Jungle Popup Wall Control Jungle Popup Wall Control
River Rhombic Avoid to Monastery Garden River Monastery Shortcut Panel
Bunker Bunker Entry Panel Bunker Entry Panel
Bunker Door to Bunker Proper Panel Bunker Tinted Glass Door Panel
Bunker Drawn Squares through Tinted Glass 3 Bunker Glass Room 3
Bunker Drop-Down Door Squares 2 Bunker UV Room 2
Swamp Entry Panel Swamp Entry Panel
Swamp Platform Shapers 4 Swamp Platform Row 4
Swamp Platform Shortcut Right Panel Swamp Platform Shortcut Right Panel
Swamp Blue Underwater Negative Shapers 5 Swamp Blue Underwater 5
Swamp Broken Shapers 4 Swamp Between Bridges Near Row 4
Swamp Cyan Underwater Negative Shapers 5 Swamp Cyan Underwater 5
Swamp Red Underwater Negative Shapers 4 Swamp Red Underwater 4
Swamp More Rotated Shapers 4 Swamp Beyond Rotating Bridge 4
Swamp More Rotated Shapers 4 Swamp Beyond Rotating Bridge 4
Swamp Near Laser Shortcut Right Panel Swamp Laser Shortcut Right Panel
Treehouse First Door Panel Treehouse First Door Panel
Treehouse Second Door Panel Treehouse Second Door Panel
Treehouse Beyond Yellow Bridge Door Panel Treehouse Third Door Panel
Treehouse Bridge Control Treehouse Bridge Control
Treehouse Left Orange Bridge 15 Treehouse Left Orange Bridge 15
Treehouse Right Orange Bridge 12 Treehouse Right Orange Bridge 12
Treehouse Laser House Door Timer Outside Control Treehouse Laser House Door Timer Outside Control
Treehouse Laser House Door Timer Inside Control Treehouse Laser House Door Timer Inside
Inside Mountain Moving Background 7 Mountain Floor 1 Left Row 7
Inside Mountain Obscured Vision 5 Mountain Floor 1 Right Row 5
Inside Mountain Physically Obstructed 3 Mountain Floor 1 Back Row 3
Inside Mountain Angled Inside Trash 2 Mountain Floor 1 Trash Pillar 2
Inside Mountain Color Cycle 5 Mountain Floor 2 Near Row 5
Inside Mountain Light Bridge Controller 2 Mountain Floor 2 Light Bridge Controller Near
Inside Mountain Light Bridge Controller 3 Mountain Floor 2 Light Bridge Controller Far
Inside Mountain Same Solution 6 Mountain Floor 2 Far Row 6
Inside Mountain Giant Puzzle Mountain Bottom Floor Giant Puzzle
Inside Mountain Door to Final Room Left Mountain Bottom Floor Final Room Entry Left
Inside Mountain Door to Final Room Right Mountain Bottom Floor Final Room Entry Right
Inside Mountain Bottom Layer Discard Mountain Bottom Floor Discard
Inside Mountain Rock Control Mountain Bottom Floor Rock Control
Inside Mountain Secret Area Entry Panel Mountain Bottom Floor Caves Entry Panel
Inside Mountain Caves Lone Pillar Caves Lone Pillar
Inside Mountain Caves Shortcut to Mountain Panel Caves Mountain Shortcut Panel
Inside Mountain Caves Shortcut to Swamp Panel Caves Swamp Shortcut Panel
Inside Mountain Caves Challenge Entry Panel Caves Challenge Entry Panel
Challenge Door to Theater Walkway Panel Challenge Tunnels Entry Panel
Theater Walkway Theater Shortcut Panel Tunnels Theater Shortcut Panel
Theater Walkway Desert Shortcut Panel Tunnels Desert Shortcut Panel
Theater Walkway Town Shortcut Panel Tunnels Town Shortcut Panel

View File

@@ -1,73 +1,73 @@
Items: Items:
Glass Factory Back Wall Glass Factory Back Wall (Door)
Quarry Boathouse Boat Staircase Quarry Boathouse Dock (Door)
Outside Tutorial Outpost Doors Outside Tutorial Outpost Doors
Glass Factory Entry Door Glass Factory Entry (Door)
Symmetry Island Doors Symmetry Island Doors
Orchard Gates Orchard Gates
Desert Doors Desert Doors
Quarry Main Entry Quarry Main Entry
Quarry Door to Mill Quarry Mill Entry (Door)
Quarry Mill Shortcuts Quarry Mill Shortcuts
Quarry Boathouse Barriers Quarry Boathouse Barriers
Shadows Timed Door Shadows Timed Door (Door)
Shadows Laser Room Door Shadows Laser Room Door
Shadows Barriers Shadows Barriers
Keep Hedge Maze Doors Keep Hedge Maze Doors
Keep Pressure Plates Doors Keep Pressure Plates Doors
Keep Shortcuts Keep Shortcuts
Monastery Entry Door Monastery Entry
Monastery Shortcuts Monastery Shortcuts
Town Doors Town Doors
Town Tower Doors Town Tower Doors
Theater Entry Door Theater Entry (Door)
Theater Exit Door Theater Exit
Jungle & River Shortcuts Jungle & River Shortcuts
Jungle Popup Wall Jungle Popup Wall (Door)
Bunker Doors Bunker Doors
Swamp Doors Swamp Doors
Swamp Near Laser Shortcut Swamp Laser Shortcut (Door)
Swamp Water Pumps Swamp Water Pumps
Treehouse Entry Doors Treehouse Entry Doors
Treehouse Drawbridge Treehouse Drawbridge (Door)
Treehouse Timed Door to Laser House Treehouse Laser House Entry (Door)
Inside Mountain First Layer Exit Door Mountain Floor 1 Exit (Door)
Inside Mountain Second Layer Stairs & Doors Mountain Floor 2 Stairs & Doors
Inside Mountain Giant Puzzle Exit Door Mountain Bottom Floor Giant Puzzle Exit (Door)
Inside Mountain Door to Final Room Mountain Bottom Floor Final Room Entry (Door)
Inside Mountain Bottom Layer Doors to Caves Mountain Bottom Floor Doors to Caves
Caves Doors to Challenge Caves Doors to Challenge
Caves Exits to Main Island Caves Exits to Main Island
Challenge Door to Theater Walkway Challenge Tunnels Entry (Door)
Theater Walkway Doors Tunnels Doors
Added Locations: Added Locations:
Outside Tutorial Door to Outpost Panel Outside Tutorial Outpost Entry Panel
Outside Tutorial Exit Door from Outpost Panel Outside Tutorial Outpost Exit Panel
Glass Factory Entry Door Panel Glass Factory Entry Panel
Glass Factory Vertical Symmetry 5 Glass Factory Back Wall 5
Symmetry Island Door to Symmetry Island Lower Panel Symmetry Island Lower Panel
Symmetry Island Door to Symmetry Island Upper Panel Symmetry Island Upper Panel
Orchard Apple Tree 3 Orchard Apple Tree 3
Orchard Apple Tree 5 Orchard Apple Tree 5
Desert Door to Desert Flood Light Room Panel Desert Light Room Entry Panel
Desert Artificial Light Reflection 3 Desert Light Room 3
Desert Door to Water Levels Room Panel Desert Flood Room Entry Panel
Desert Flood Reflection 6 Desert Flood Room 6
Quarry Door to Quarry 1 Panel Quarry Entry 1 Panel
Quarry Door to Quarry 2 Panel Quarry Entry 2 Panel
Quarry Door to Mill Right Quarry Mill Entry Right Panel
Quarry Door to Mill Left Quarry Mill Entry Left Panel
Quarry Mill Ground Floor Shortcut Door Panel Quarry Mill Side Exit Panel
Quarry Mill Door to Outside Quarry Stairs Panel Quarry Mill Roof Exit Panel
Quarry Mill Stair Control Quarry Mill Stair Control
Quarry Boathouse Shortcut Door Panel Quarry Boathouse Second Barrier Panel
Shadows Door Timer Inside Shadows Door Timer Inside
Shadows Door Timer Outside Shadows Door Timer Outside
Shadows Environmental Avoid 8 Shadows Far 8
Shadows Follow 5 Shadows Near 5
Shadows Lower Avoid 3 Shadows Intro 3
Shadows Lower Avoid 5 Shadows Intro 5
Keep Hedge Maze 1 Keep Hedge Maze 1
Keep Pressure Plates 1 Keep Pressure Plates 1
Keep Hedge Maze 2 Keep Hedge Maze 2
@@ -76,71 +76,70 @@ Keep Hedge Maze 4
Keep Pressure Plates 2 Keep Pressure Plates 2
Keep Pressure Plates 3 Keep Pressure Plates 3
Keep Pressure Plates 4 Keep Pressure Plates 4
Keep Shortcut to Shadows Panel Keep Shadows Shortcut Panel
Keep Tower Shortcut to Keep Panel Keep Tower Shortcut Panel
Monastery Shortcut Door Panel Monastery Shortcut Panel
Monastery Door Open Left Monastery Entry Left
Monastery Door Open Right Monastery Entry Right
Monastery Rhombic Avoid 3 Monastery Outside 3
Town Cargo Box Panel Town Cargo Box Entry Panel
Town Full Dot Grid Shapers 5 Town Wooden Roof Lower Row 5
Town Tinted Door Panel Town Tinted Glass Door Panel
Town Door to Church Stars Panel Town Church Entry Panel
Town Maze Stair Control Town Maze Stair Control
Town Windmill Door Panel Town Windmill Entry Panel
Town Sound Room Left
Town Sound Room Right Town Sound Room Right
Town Symmetry Squares 5 + Dots Town Red Rooftop 5
Town Church Lattice Town Church Lattice
Town Hexagonal Reflection Town Tall Hexagonal
Town Shapers & Dots & Eraser Town Wooden Rooftop
Windmill Door to Front of Theater Panel Windmill Theater Entry Panel
Theater Door to Cargo Box Left Panel Theater Exit Left Panel
Theater Door to Cargo Box Right Panel Theater Exit Right Panel
Jungle Shortcut to River Panel Jungle Laser Shortcut Panel
Jungle Popup Wall Control Jungle Popup Wall Control
River Rhombic Avoid to Monastery Garden River Monastery Shortcut Panel
Bunker Bunker Entry Panel Bunker Entry Panel
Bunker Door to Bunker Proper Panel Bunker Tinted Glass Door Panel
Bunker Drawn Squares through Tinted Glass 3 Bunker Glass Room 3
Bunker Drop-Down Door Squares 2 Bunker UV Room 2
Swamp Entry Panel Swamp Entry Panel
Swamp Platform Shapers 4 Swamp Platform Row 4
Swamp Platform Shortcut Right Panel Swamp Platform Shortcut Right Panel
Swamp Blue Underwater Negative Shapers 5 Swamp Blue Underwater 5
Swamp Broken Shapers 4 Swamp Between Bridges Near Row 4
Swamp Cyan Underwater Negative Shapers 5 Swamp Cyan Underwater 5
Swamp Red Underwater Negative Shapers 4 Swamp Red Underwater 4
Swamp More Rotated Shapers 4 Swamp Beyond Rotating Bridge 4
Swamp More Rotated Shapers 4 Swamp Beyond Rotating Bridge 4
Swamp Near Laser Shortcut Right Panel Swamp Laser Shortcut Right Panel
Treehouse First Door Panel Treehouse First Door Panel
Treehouse Second Door Panel Treehouse Second Door Panel
Treehouse Beyond Yellow Bridge Door Panel Treehouse Third Door Panel
Treehouse Bridge Control Treehouse Bridge Control
Treehouse Left Orange Bridge 15 Treehouse Left Orange Bridge 15
Treehouse Right Orange Bridge 12 Treehouse Right Orange Bridge 12
Treehouse Laser House Door Timer Outside Control Treehouse Laser House Door Timer Outside Control
Treehouse Laser House Door Timer Inside Control Treehouse Laser House Door Timer Inside
Inside Mountain Moving Background 7 Mountain Floor 1 Left Row 7
Inside Mountain Obscured Vision 5 Mountain Floor 1 Right Row 5
Inside Mountain Physically Obstructed 3 Mountain Floor 1 Back Row 3
Inside Mountain Angled Inside Trash 2 Mountain Floor 1 Trash Pillar 2
Inside Mountain Color Cycle 5 Mountain Floor 2 Near Row 5
Inside Mountain Light Bridge Controller 2 Mountain Floor 2 Light Bridge Controller Near
Inside Mountain Light Bridge Controller 3 Mountain Floor 2 Light Bridge Controller Far
Inside Mountain Same Solution 6 Mountain Floor 2 Far Row 6
Inside Mountain Giant Puzzle Mountain Bottom Floor Giant Puzzle
Inside Mountain Door to Final Room Left Mountain Bottom Floor Final Room Entry Left
Inside Mountain Door to Final Room Right Mountain Bottom Floor Final Room Entry Right
Inside Mountain Bottom Layer Discard Mountain Bottom Floor Discard
Inside Mountain Rock Control Mountain Bottom Floor Rock Control
Inside Mountain Secret Area Entry Panel Mountain Bottom Floor Caves Entry Panel
Inside Mountain Caves Lone Pillar Caves Lone Pillar
Inside Mountain Caves Shortcut to Mountain Panel Caves Mountain Shortcut Panel
Inside Mountain Caves Shortcut to Swamp Panel Caves Swamp Shortcut Panel
Inside Mountain Caves Challenge Entry Panel Caves Challenge Entry Panel
Challenge Door to Theater Walkway Panel Challenge Tunnels Entry Panel
Theater Walkway Theater Shortcut Panel Tunnels Theater Shortcut Panel
Theater Walkway Desert Shortcut Panel Tunnels Desert Shortcut Panel
Theater Walkway Town Shortcut Panel Tunnels Town Shortcut Panel

View File

@@ -5,5 +5,5 @@ Starting Inventory:
Caves Exits to Main Island Caves Exits to Main Island
Remove Items: Remove Items:
Caves Mountain Shortcut Caves Mountain Shortcut (Door)
Caves Swamp Shortcut Caves Swamp Shortcut (Door)