Merge branch 'main' into player-tracker

This commit is contained in:
Chris Wilson
2022-07-18 20:06:04 -04:00
71 changed files with 3867 additions and 1932 deletions

View File

@@ -126,7 +126,6 @@ class MultiWorld():
set_player_attr('beemizer_total_chance', 0)
set_player_attr('beemizer_trap_chance', 0)
set_player_attr('escape_assist', [])
set_player_attr('open_pyramid', False)
set_player_attr('treasure_hunt_icon', 'Triforce Piece')
set_player_attr('treasure_hunt_count', 0)
set_player_attr('clock_mode', False)
@@ -390,20 +389,14 @@ class MultiWorld():
self.state.collect(item, True)
def push_item(self, location: Location, item: Item, collect: bool = True):
if not isinstance(location, Location):
raise RuntimeError(
'Cannot assign item %s to invalid location %s (player %d).' % (item, location, item.player))
assert location.can_fill(self.state, item, False), f"Cannot place {item} into {location}."
location.item = item
item.location = location
item.world = self # try to not have this here anymore and create it with item?
if collect:
self.state.collect(item, location.event, location)
if location.can_fill(self.state, item, False):
location.item = item
item.location = location
item.world = self # try to not have this here anymore
if collect:
self.state.collect(item, location.event, location)
logging.debug('Placed %s at %s', item, location)
else:
raise RuntimeError('Cannot assign item %s to location %s.' % (item, location))
logging.debug('Placed %s at %s', item, location)
def get_entrances(self) -> List[Entrance]:
if self._cached_entrances is None:
@@ -1431,8 +1424,6 @@ class Spoiler():
outfile.write('Entrance Shuffle: %s\n' % self.world.shuffle[player])
if self.world.shuffle[player] != "vanilla":
outfile.write('Entrance Shuffle Seed %s\n' % self.world.worlds[player].er_seed)
outfile.write('Pyramid hole pre-opened: %s\n' % (
'Yes' if self.world.open_pyramid[player] else 'No'))
outfile.write('Shop inventory shuffle: %s\n' %
bool_to_text("i" in self.world.shop_shuffle[player]))
outfile.write('Shop price shuffle: %s\n' %

View File

@@ -583,9 +583,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
ret.goal = goals[goal]
# TODO consider moving open_pyramid to an automatic variable in the core roller, set to True when
# fast ganon + ganon at hole
ret.open_pyramid = get_choice_legacy('open_pyramid', weights, 'goal')
extra_pieces = get_choice_legacy('triforce_pieces_mode', weights, 'available')

View File

@@ -47,7 +47,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
world.item_functionality = args.item_functionality.copy()
world.timer = args.timer.copy()
world.goal = args.goal.copy()
world.open_pyramid = args.open_pyramid.copy()
world.boss_shuffle = args.shufflebosses.copy()
world.enemy_health = args.enemy_health.copy()
world.enemy_damage = args.enemy_damage.copy()
@@ -364,7 +363,8 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
for location in world.get_filled_locations():
if type(location.address) == int:
assert location.item.code is not None, "item code None should be event, " \
"location.address should then also be None"
"location.address should then also be None. Location: " \
f" {location}"
locations_data[location.player][location.address] = \
location.item.code, location.item.player, location.item.flags
if location.name in world.start_location_hints[location.player]:

View File

@@ -720,16 +720,16 @@ def get_players_string(ctx: Context):
return f'{len(auth_clients)} players of {total} connected ' + text[:-1]
def get_status_string(ctx: Context, team: int):
text = "Player Status on your team:"
def get_status_string(ctx: Context, team: int, tag: str):
text = f"Player Status on team {team}:"
for slot in ctx.locations:
connected = len(ctx.clients[team][slot])
death_link = len([client for client in ctx.clients[team][slot] if "DeathLink" in client.tags])
tagged = len([client for client in ctx.clients[team][slot] if tag in client.tags])
completion_text = f"({len(ctx.location_checks[team, slot])}/{len(ctx.locations[slot])})"
death_text = f" {death_link} of which are death link" if connected else ""
tag_text = f" {tagged} of which are tagged {tag}" if connected and tag else ""
goal_text = " and has finished." if ctx.client_game_state[team, slot] == ClientStatus.CLIENT_GOAL else "."
text += f"\n{ctx.get_aliased_name(team, slot)} has {connected} connection{'' if connected == 1 else 's'}" \
f"{death_text}{goal_text} {completion_text}"
f"{tag_text}{goal_text} {completion_text}"
return text
@@ -1113,9 +1113,11 @@ class ClientMessageProcessor(CommonCommandProcessor):
self.output(get_players_string(self.ctx))
return True
def _cmd_status(self) -> bool:
"""Get status information about your team."""
self.output(get_status_string(self.ctx, self.client.team))
def _cmd_status(self, tag:str="") -> bool:
"""Get status information about your team.
Optionally mention a Tag name and get information on who has that Tag.
For example: DeathLink or EnergyLink."""
self.output(get_status_string(self.ctx, self.client.team, tag))
return True
def _cmd_release(self) -> bool:
@@ -1306,6 +1308,8 @@ class ClientMessageProcessor(CommonCommandProcessor):
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:
@@ -1657,6 +1661,14 @@ class ServerCommandProcessor(CommonCommandProcessor):
self.output(get_players_string(self.ctx))
return True
def _cmd_status(self, tag: str = "") -> bool:
"""Get status information about teams.
Optionally mention a Tag name and get information on who has that Tag.
For example: DeathLink or EnergyLink."""
for team in self.ctx.clients:
self.output(get_status_string(self.ctx, team, tag))
return True
def _cmd_exit(self) -> bool:
"""Shutdown the server"""
asyncio.create_task(self.ctx.server.ws_server._close())

View File

@@ -62,7 +62,7 @@ class SNIClientCommandProcessor(ClientCommandProcessor):
def _cmd_snes(self, snes_options: str = "") -> bool:
"""Connect to a snes. Optionally include network address of a snes to connect to,
otherwise show available devices; and a SNES device number if more than one SNES is detected.
Examples: "/snes", "/snes 1", "/snes localhost:8080 1" """
Examples: "/snes", "/snes 1", "/snes localhost:23074 1" """
snes_address = self.ctx.snes_address
snes_device_number = -1
@@ -1296,7 +1296,7 @@ async def main():
parser = get_base_parser()
parser.add_argument('diff_file', default="", type=str, nargs="?",
help='Path to a Archipelago Binary Patch file')
parser.add_argument('--snes', default='localhost:8080', help='Address of the SNI server.')
parser.add_argument('--snes', default='localhost:23074', help='Address of the SNI server.')
parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
args = parser.parse_args()

View File

@@ -19,7 +19,13 @@ from worlds.sc2wol.Items import lookup_id_to_name, item_table
from worlds.sc2wol.Locations import SC2WOL_LOC_ID_OFFSET
from worlds.sc2wol import SC2WoLWorld
from Utils import init_logging
from pathlib import Path
import re
from MultiServer import mark_raw
import ctypes
import sys
from Utils import init_logging, is_windows
if __name__ == "__main__":
init_logging("SC2Client", exception_logger="Client")
@@ -73,6 +79,17 @@ class StarcraftClientProcessor(ClientCommandProcessor):
request_unfinished_missions(self.ctx.checked_locations, self.ctx.mission_req_table, self.ctx.ui, self.ctx)
return True
@mark_raw
def _cmd_set_path(self, path: str = '') -> bool:
"""Manually set the SC2 install directory (if the automatic detection fails)."""
if path:
os.environ["SC2PATH"] = path
check_mod_install()
return True
else:
sc2_logger.warning("When using set_path, you must type the path to your SC2 install directory.")
return False
class SC2Context(CommonContext):
command_processor = StarcraftClientProcessor
@@ -111,6 +128,11 @@ class SC2Context(CommonContext):
for mission in slot_req_table:
self.mission_req_table[mission] = MissionInfo(**slot_req_table[mission])
# Look for and set SC2PATH.
# check_game_install_path() returns True if and only if it finds + sets SC2PATH.
if "SC2PATH" not in os.environ and check_game_install_path():
check_mod_install()
if cmd in {"PrintJSON"}:
if "receiving" in args:
if self.slot_concerns_self(args["receiving"]):
@@ -415,8 +437,9 @@ async def starcraft_launch(ctx: SC2Context, mission_id):
sc2_logger.info(f"Launching {lookup_id_to_mission[mission_id]}. If game does not launch check log file for errors.")
run_game(sc2.maps.get(maps_table[mission_id - 1]), [Bot(Race.Terran, ArchipelagoBot(ctx, mission_id),
name="Archipelago", fullscreen=True)], realtime=True)
with DllDirectory(None):
run_game(sc2.maps.get(maps_table[mission_id - 1]), [Bot(Race.Terran, ArchipelagoBot(ctx, mission_id),
name="Archipelago", fullscreen=True)], realtime=True)
class ArchipelagoBot(sc2.bot_ai.BotAI):
@@ -796,6 +819,101 @@ def initialize_blank_mission_dict(location_table):
return unlocks
def check_game_install_path() -> bool:
# First thing: go to the default location for ExecuteInfo.
# An exception for Windows is included because it's very difficult to find ~\Documents if the user moved it.
if is_windows:
# The next five lines of utterly inscrutable code are brought to you by copy-paste from Stack Overflow.
# https://stackoverflow.com/questions/6227590/finding-the-users-my-documents-path/30924555#
import ctypes.wintypes
CSIDL_PERSONAL = 5 # My Documents
SHGFP_TYPE_CURRENT = 0 # Get current, not default value
buf = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH)
ctypes.windll.shell32.SHGetFolderPathW(None, CSIDL_PERSONAL, None, SHGFP_TYPE_CURRENT, buf)
documentspath = buf.value
einfo = str(documentspath / Path("StarCraft II\\ExecuteInfo.txt"))
else:
einfo = str(sc2.paths.get_home() / Path(sc2.paths.USERPATH[sc2.paths.PF]))
# Check if the file exists.
if os.path.isfile(einfo):
# Open the file and read it, picking out the latest executable's path.
with open(einfo) as f:
content = f.read()
if content:
base = re.search(r" = (.*)Versions", content).group(1)
if os.path.exists(base):
executable = sc2.paths.latest_executeble(Path(base).expanduser() / "Versions")
# Finally, check the path for an actual executable.
# If we find one, great. Set up the SC2PATH.
if os.path.isfile(executable):
sc2_logger.info(f"Found an SC2 install at {base}!")
sc2_logger.debug(f"Latest executable at {executable}.")
os.environ["SC2PATH"] = base
sc2_logger.debug(f"SC2PATH set to {base}.")
return True
else:
sc2_logger.warning(f"We may have found an SC2 install at {base}, but couldn't find {executable}.")
else:
sc2_logger.warning(f"{einfo} pointed to {base}, but we could not find an SC2 install there.")
else:
sc2_logger.warning(f"Couldn't find {einfo}. Please run /set_path with your SC2 install directory.")
return False
def check_mod_install() -> bool:
# Pull up the SC2PATH if set. If not, encourage the user to manually run /set_path.
try:
# Check inside the Mods folder for Archipelago.SC2Mod. If found, tell user. If not, tell user.
if os.path.isfile(modfile := (os.environ["SC2PATH"] / Path("Mods") / Path("Archipelago.SC2Mod"))):
sc2_logger.info(f"Archipelago mod found at {modfile}.")
return True
else:
sc2_logger.warning(f"Archipelago mod could not be found at {modfile}. Please install the mod file there.")
except KeyError:
sc2_logger.warning(f"SC2PATH isn't set. Please run /set_path with the path to your SC2 install.")
return False
class DllDirectory:
# Credit to Black Sliver for this code.
# More info: https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-setdlldirectoryw
_old: typing.Optional[str] = None
_new: typing.Optional[str] = None
def __init__(self, new: typing.Optional[str]):
self._new = new
def __enter__(self):
old = self.get()
if self.set(self._new):
self._old = old
def __exit__(self, *args):
if self._old is not None:
self.set(self._old)
@staticmethod
def get() -> str:
if sys.platform == "win32":
n = ctypes.windll.kernel32.GetDllDirectoryW(0, None)
buf = ctypes.create_unicode_buffer(n)
ctypes.windll.kernel32.GetDllDirectoryW(n, buf)
return buf.value
# NOTE: other OS may support os.environ["LD_LIBRARY_PATH"], but this fix is windows-specific
return None
@staticmethod
def set(s: typing.Optional[str]) -> bool:
if sys.platform == "win32":
return ctypes.windll.kernel32.SetDllDirectoryW(s) != 0
# NOTE: other OS may support os.environ["LD_LIBRARY_PATH"], but this fix is windows-specific
return False
if __name__ == '__main__':
colorama.init()
asyncio.run(main())

View File

@@ -154,8 +154,10 @@ def autogen(config: dict):
while 1:
time.sleep(0.1)
with db_session:
# for update locks the database row(s) during transaction, preventing writes from elsewhere
to_start = select(
generation for generation in Generation if generation.state == STATE_QUEUED)
generation for generation in Generation
if generation.state == STATE_QUEUED).for_update()
for generation in to_start:
launch_generator(generator_pool, generation)
except AlreadyRunningException:

View File

@@ -4,7 +4,7 @@ import random
import json
import zipfile
from collections import Counter
from typing import Dict, Optional as TypeOptional
from typing import Dict, Optional, Any
from Utils import __version__
from flask import request, flash, redirect, url_for, session, render_template
@@ -15,7 +15,7 @@ from BaseClasses import seeddigits, get_seed
from Generate import handle_name, PlandoSettings
import pickle
from .models import *
from .models import Generation, STATE_ERROR, STATE_QUEUED, commit, db_session, Seed, UUID
from WebHostLib import app
from .check import get_yaml_data, roll_options
from .upload import upload_zip_to_db
@@ -30,16 +30,15 @@ def get_meta(options_source: dict) -> dict:
}
plando_options -= {""}
meta = {
server_options = {
"hint_cost": int(options_source.get("hint_cost", 10)),
"forfeit_mode": options_source.get("forfeit_mode", "goal"),
"remaining_mode": options_source.get("remaining_mode", "disabled"),
"collect_mode": options_source.get("collect_mode", "disabled"),
"item_cheat": bool(int(options_source.get("item_cheat", 1))),
"server_password": options_source.get("server_password", None),
"plando_options": list(plando_options)
}
return meta
return {"server_options": server_options, "plando_options": list(plando_options)}
@app.route('/generate', methods=['GET', 'POST'])
@@ -60,13 +59,13 @@ def generate(race=False):
results, gen_options = roll_options(options, meta["plando_options"])
if race:
meta["item_cheat"] = False
meta["remaining_mode"] = "disabled"
meta["server_options"]["item_cheat"] = False
meta["server_options"]["remaining_mode"] = "disabled"
if any(type(result) == str for result in results.values()):
return render_template("checkResult.html", results=results)
elif len(gen_options) > app.config["MAX_ROLL"]:
flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players for now. "
flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players. "
f"If you have a larger group, please generate it yourself and upload it.")
elif len(gen_options) >= app.config["JOB_THRESHOLD"]:
gen = Generation(
@@ -92,23 +91,22 @@ def generate(race=False):
return render_template("generate.html", race=race, version=__version__)
def gen_game(gen_options, meta: TypeOptional[Dict[str, object]] = None, owner=None, sid=None):
def gen_game(gen_options, meta: Optional[Dict[str, Any]] = None, owner=None, sid=None):
if not meta:
meta: Dict[str, object] = {}
meta: Dict[str, Any] = {}
meta.setdefault("server_options", {}).setdefault("hint_cost", 10)
race = meta.setdefault("race", False)
meta.setdefault("hint_cost", 10)
race = meta.get("race", False)
del (meta["race"])
plando_options = meta.get("plando", {"bosses", "items", "connections", "texts"})
del (meta["plando_options"])
try:
target = tempfile.TemporaryDirectory()
playercount = len(gen_options)
seed = get_seed()
random.seed(seed)
if race:
random.seed() # reset to time-based random source
random.seed() # use time-based random source
else:
random.seed(seed)
seedname = "W" + (f"{random.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits))
@@ -120,7 +118,8 @@ def gen_game(gen_options, meta: TypeOptional[Dict[str, object]] = None, owner=No
erargs.outputname = seedname
erargs.outputpath = target.name
erargs.teams = 1
erargs.plando_options = PlandoSettings.from_set(plando_options)
erargs.plando_options = PlandoSettings.from_set(meta.setdefault("plando_options",
{"bosses", "items", "connections", "texts"}))
name_counter = Counter()
for player, (playerfile, settings) in enumerate(gen_options.items(), 1):
@@ -136,7 +135,7 @@ def gen_game(gen_options, meta: TypeOptional[Dict[str, object]] = None, owner=No
erargs.name[player] = handle_name(erargs.name[player], player, name_counter)
if len(set(erargs.name.values())) != len(erargs.name):
raise Exception(f"Names have to be unique. Names: {Counter(erargs.name.values())}")
ERmain(erargs, seed, baked_server_options=meta)
ERmain(erargs, seed, baked_server_options=meta["server_options"])
return upload_to_db(target.name, sid, owner, race)
except BaseException as e:
@@ -148,7 +147,6 @@ def gen_game(gen_options, meta: TypeOptional[Dict[str, object]] = None, owner=No
meta = json.loads(gen.meta)
meta["error"] = (e.__class__.__name__ + ": " + str(e))
gen.meta = json.dumps(meta)
commit()
raise

View File

@@ -27,7 +27,7 @@ class Room(db.Entity):
seed = Required('Seed', index=True)
multisave = Optional(buffer, lazy=True)
show_spoiler = Required(int, default=0) # 0 -> never, 1 -> after completion, -> 2 always
timeout = Required(int, default=lambda: 6 * 60 * 60) # seconds since last activity to shutdown
timeout = Required(int, default=lambda: 2 * 60 * 60) # seconds since last activity to shutdown
tracker = Optional(UUID, index=True)
last_port = Optional(int, default=lambda: 0)

View File

@@ -60,7 +60,7 @@ def create():
for game_name, world in AutoWorldRegister.world_types.items():
all_options = {**world.options, **Options.per_game_common_options}
all_options = {**Options.per_game_common_options, **world.options}
res = Template(open(os.path.join("WebHostLib", "templates", "options.yaml")).read()).render(
options=all_options,
__version__=__version__, game=game_name, yaml_dump=yaml.dump,

View File

@@ -16,9 +16,9 @@
This room has a <a href="{{ url_for("getTracker", tracker=room.tracker) }}">Multiworld Tracker</a> enabled.
<br />
{% endif %}
This room will be closed after {{ room.timeout//60//60 }} hours of inactivity. Should you wish to continue
later,
you can simply refresh this page and the server will be started again.<br>
The server for this room will be paused after {{ room.timeout//60//60 }} hours of inactivity.
Should you wish to continue later,
anyone can simply refresh this page and the server will resume.<br>
{% if room.last_port %}
You can connect to this room by using <span class="interactive"
data-tooltip="This means address/ip is {{ config['PATCH_TARGET'] }} and port is {{ room.last_port }}.">

49
docs/style.md Normal file
View File

@@ -0,0 +1,49 @@
# Style Guide
## Generic
* This guide can be ignored for data files that are not to be viewed in an editor.
* 120 character per line for all source files.
* Avoid white space errors like trailing spaces.
## Python Code
* We mostly follow [PEP8](https://peps.python.org/pep-0008/). Read below to see the differences.
* 120 characters per line. PyCharm does this automatically, other editors can be configured for it.
* Strings in core code will be `"strings"`. In other words: double quote your strings.
* Strings in worlds should use double quotes as well, but imported code may differ.
* Prefer [format string literals](https://peps.python.org/pep-0498/) over string concatenation,
use single quotes inside them: `f"Like {dct['key']}"`
* Use type annotation where possible.
## Markdown
* We almost follow [Google's styleguide](https://google.github.io/styleguide/docguide/style.html).
Read below for differences.
* For existing documents, try to follow its style or ask to completely reformat it.
* 120 characters per line.
* One space between bullet/number and text.
* No lazy numbering.
## HTML
* Indent with 2 spaces for new code.
* kebab-case for ids and classes.
## CSS
* Indent with 2 spaces for new code.
* `{` on the same line as the selector.
* No space between selector and `{`.
## JS
* Indent with 2 spaces.
* Indent `case` inside `switch ` with 2 spaces.
* Use single quotes.
* Semicolons are required after every statement.

View File

@@ -236,7 +236,7 @@ class MyGameLocation(Location):
game: str = "My Game"
# override constructor to automatically mark event locations as such
def __init__(self, player: int, name = '', code = None, parent = None):
def __init__(self, player: int, name = "", code = None, parent = None):
super(MyGameLocation, self).__init__(player, name, code, parent)
self.event = code is None
```
@@ -487,14 +487,14 @@ def create_items(self) -> None:
for item in map(self.create_item, mygame_items):
if item in exclude:
exclude.remove(item) # this is destructive. create unique list above
self.world.itempool.append(self.create_item('nothing'))
self.world.itempool.append(self.create_item("nothing"))
else:
self.world.itempool.append(item)
# itempool and number of locations should match up.
# If this is not the case we want to fill the itempool with junk.
junk = 0 # calculate this based on player settings
self.world.itempool += [self.create_item('nothing') for _ in range(junk)]
self.world.itempool += [self.create_item("nothing") for _ in range(junk)]
```
#### create_regions
@@ -628,7 +628,7 @@ class MyGameLogic(LogicMixin):
def _mygame_has_key(self, world: MultiWorld, player: int):
# Arguments above are free to choose
# it may make sense to use World as argument instead of MultiWorld
return self.has('key', player) # or whatever
return self.has("key", player) # or whatever
```
```python
# __init__.py

View File

@@ -129,6 +129,7 @@ Type: dirifempty; Name: "{app}"
[InstallDelete]
Type: files; Name: "{app}\ArchipelagoLttPClient.exe"
Type: filesandordirs; Name: "{app}\lib\worlds\rogue-legacy*"
[Registry]

View File

@@ -175,12 +175,15 @@ A Link to the Past:
retro_caves:
on: 0 # Zelda-1 like mode. There are randomly placed take-any caves that contain one Sword and choices of Heart Container/Blue Potion.
off: 50
hints: # Vendors: King Zora and Bottle Merchant say what they're selling.
# On/Full: Put item and entrance placement hints on telepathic tiles and some NPCs, Full removes joke hints.
hints: # On/Full: Put item and entrance placement hints on telepathic tiles and some NPCs, Full removes joke hints.
'on': 50
vendors: 0
'off': 0
full: 0
scams: # If on, these Merchants will no longer tell you what they're selling.
'off': 50
'king_zora': 0
'bottle_merchant': 0
'all': 0
swordless:
on: 0 # Your swords are replaced by rupees. Gameplay changes have been made to accommodate this change
off: 1
@@ -273,6 +276,7 @@ A Link to the Past:
p: 0 # Randomize the prices of the items in shop inventories
u: 0 # Shuffle capacity upgrades into the item pool (and allow them to traverse the multiworld)
w: 0 # Consider witch's hut like any other shop and shuffle/randomize it too
P: 0 # Prices of the items in shop inventories cost hearts, arrow, or bombs instead of rupees
ip: 0 # Shuffle inventories and randomize prices
fpu: 0 # Generate new inventories, randomize prices and shuffle capacity upgrades into item pool
uip: 0 # Shuffle inventories, randomize prices and shuffle capacity upgrades into the item pool

View File

@@ -1,5 +1,6 @@
import typing
from BaseClasses import MultiWorld
from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink
@@ -27,6 +28,35 @@ class Goal(Choice):
option_hand_in = 2
class OpenPyramid(Choice):
"""Determines whether the hole at the top of pyramid is open.
Goal will open the pyramid if the goal requires you to kill Ganon, without needing to kill Agahnim 2.
Auto is the same as goal except if Ganon's dropdown is in another location, the hole will be closed."""
display_name = "Open Pyramid Hole"
option_closed = 0
option_open = 1
option_goal = 2
option_auto = 3
default = option_goal
alias_true = option_open
alias_false = option_closed
alias_yes = option_open
alias_no = option_closed
def to_bool(self, world: MultiWorld, player: int) -> bool:
if self.value == self.option_goal:
return world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'}
elif self.value == self.option_auto:
return world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'} \
and (world.shuffle[player] in {'vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed'} or not
world.shuffle_ganon)
elif self.value == self.option_open:
return True
else:
return False
class DungeonItem(Choice):
value: int
option_original_dungeon = 0
@@ -331,6 +361,7 @@ class AllowCollect(Toggle):
alttp_options: typing.Dict[str, type(Option)] = {
"crystals_needed_for_gt": CrystalsTower,
"crystals_needed_for_ganon": CrystalsGanon,
"open_pyramid": OpenPyramid,
"bigkey_shuffle": bigkey_shuffle,
"smallkey_shuffle": smallkey_shuffle,
"compass_shuffle": compass_shuffle,

View File

@@ -1247,7 +1247,7 @@ def patch_rom(world, rom, player, enemized):
rom.write_bytes(0x50563, [0x3F, 0x14]) # disable below ganon chest
rom.write_byte(0x50599, 0x00) # disable below ganon chest
rom.write_bytes(0xE9A5, [0x7E, 0x00, 0x24]) # disable below ganon chest
rom.write_byte(0x18008B, 0x01 if world.open_pyramid[player] else 0x00) # pre-open Pyramid Hole
rom.write_byte(0x18008B, 0x01 if world.open_pyramid[player].to_bool(world, player) else 0x00) # pre-open Pyramid Hole
rom.write_byte(0x18008C, 0x01 if world.crystals_needed_for_gt[
player] == 0 else 0x00) # GT pre-opened if crystal requirement is 0
rom.write_byte(0xF5D73, 0xF0) # bees are catchable

View File

@@ -402,17 +402,6 @@ class ALTTPWorld(World):
def create_regions(self):
player = self.player
world = self.world
if world.open_pyramid[player] == 'goal':
world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt',
'localganontriforcehunt', 'ganonpedestal'}
elif world.open_pyramid[player] == 'auto':
world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt',
'localganontriforcehunt', 'ganonpedestal'} and \
(world.shuffle[player] in {'vanilla', 'dungeonssimple', 'dungeonsfull',
'dungeonscrossed'} or not world.shuffle_ganon)
else:
world.open_pyramid[player] = {'on': True, 'off': False, 'yes': True, 'no': False}.get(
world.open_pyramid[player], 'auto')
world.triforce_pieces_available[player] = max(world.triforce_pieces_available[player],
world.triforce_pieces_required[player])

View File

@@ -1 +1 @@
factorio-rcon-py>=1.2.1
factorio-rcon-py==1.2.1

View File

@@ -1,5 +1,5 @@
from typing import Dict
from BaseClasses import Item, Location, MultiWorld, Tutorial
from BaseClasses import Item, Location, MultiWorld, Tutorial, ItemClassification
from .Items import ItemData, FF1Items, FF1_STARTER_ITEMS, FF1_PROGRESSION_LIST, FF1_BRIDGE
from .Locations import EventId, FF1Locations, generate_rule, CHAOS_TERMINATED_EVENT
from .Options import ff1_options
@@ -55,7 +55,7 @@ class FF1World(World):
rules = get_options(self.world, 'rules', self.player)
menu_region = self.ff1_locations.create_menu_region(self.player, locations, rules)
terminated_event = Location(self.player, CHAOS_TERMINATED_EVENT, EventId, menu_region)
terminated_item = Item(CHAOS_TERMINATED_EVENT, True, EventId, self.player)
terminated_item = Item(CHAOS_TERMINATED_EVENT, ItemClassification.progression, EventId, self.player)
terminated_event.place_locked_item(terminated_item)
items = get_options(self.world, 'items', self.player)
@@ -114,5 +114,6 @@ class FF1World(World):
def get_filler_item_name(self) -> str:
return self.world.random.choice(["Heal", "Pure", "Soft", "Tent", "Cabin", "House"])
def get_options(world: MultiWorld, name: str, player: int):
return getattr(world, name, None)[player].value

View File

@@ -1088,10 +1088,10 @@ def get_pool_core(world):
placed_items['Hideout Jail Guard (4 Torches)'] = 'Recovery Heart'
skip_in_spoiler_locations.extend(['Hideout Jail Guard (2 Torches)', 'Hideout Jail Guard (3 Torches)', 'Hideout Jail Guard (4 Torches)'])
else:
placed_items['Hideout Jail Guard (1 Torch)'] = 'Small Key (Gerudo Fortress)'
placed_items['Hideout Jail Guard (2 Torches)'] = 'Small Key (Gerudo Fortress)'
placed_items['Hideout Jail Guard (3 Torches)'] = 'Small Key (Gerudo Fortress)'
placed_items['Hideout Jail Guard (4 Torches)'] = 'Small Key (Gerudo Fortress)'
placed_items['Hideout Jail Guard (1 Torch)'] = 'Small Key (Thieves Hideout)'
placed_items['Hideout Jail Guard (2 Torches)'] = 'Small Key (Thieves Hideout)'
placed_items['Hideout Jail Guard (3 Torches)'] = 'Small Key (Thieves Hideout)'
placed_items['Hideout Jail Guard (4 Torches)'] = 'Small Key (Thieves Hideout)'
if world.shuffle_gerudo_card and world.gerudo_fortress != 'open':
pool.append('Gerudo Membership Card')

View File

@@ -6,7 +6,7 @@ class TotalLocations(Range):
"""Number of location checks which are added to the Risk of Rain playthrough."""
display_name = "Total Locations"
range_start = 10
range_end = 500
range_end = 250
default = 20
@@ -36,10 +36,14 @@ class AllowLunarItems(DefaultOnToggle):
class StartWithRevive(DefaultOnToggle):
"""Start the game with a `Dio's Best Friend` item."""
display_name = "Start with a Revive"
class FinalStageDeath(DefaultOnToggle):
"""Death on the final boss stage counts as a win."""
display_name = "Final Stage Death is Win"
class GreenScrap(Range):
"""Weight of Green Scraps in the item pool."""
"""Weight of Green Scraps in the item pool. (Ignored unless Item Weight Presets is 'No')"""
display_name = "Green Scraps"
range_start = 0
range_end = 100
@@ -47,7 +51,7 @@ class GreenScrap(Range):
class RedScrap(Range):
"""Weight of Red Scraps in the item pool."""
"""Weight of Red Scraps in the item pool. (Ignored unless Item Weight Presets is 'No')"""
display_name = "Red Scraps"
range_start = 0
range_end = 100
@@ -55,7 +59,7 @@ class RedScrap(Range):
class YellowScrap(Range):
"""Weight of yellow scraps in the item pool."""
"""Weight of yellow scraps in the item pool. (Ignored unless Item Weight Presets is 'No')"""
display_name = "Yellow Scraps"
range_start = 0
range_end = 100
@@ -63,7 +67,7 @@ class YellowScrap(Range):
class WhiteScrap(Range):
"""Weight of white scraps in the item pool."""
"""Weight of white scraps in the item pool. (Ignored unless Item Weight Presets is 'No')"""
display_name = "White Scraps"
range_start = 0
range_end = 100
@@ -71,7 +75,7 @@ class WhiteScrap(Range):
class CommonItem(Range):
"""Weight of common items in the item pool."""
"""Weight of common items in the item pool. (Ignored unless Item Weight Presets is 'No')"""
display_name = "Common Items"
range_start = 0
range_end = 100
@@ -79,7 +83,7 @@ class CommonItem(Range):
class UncommonItem(Range):
"""Weight of uncommon items in the item pool."""
"""Weight of uncommon items in the item pool. (Ignored unless Item Weight Presets is 'No')"""
display_name = "Uncommon Items"
range_start = 0
range_end = 100
@@ -87,7 +91,7 @@ class UncommonItem(Range):
class LegendaryItem(Range):
"""Weight of legendary items in the item pool."""
"""Weight of legendary items in the item pool. (Ignored unless Item Weight Presets is 'No')"""
display_name = "Legendary Items"
range_start = 0
range_end = 100
@@ -95,7 +99,7 @@ class LegendaryItem(Range):
class BossItem(Range):
"""Weight of boss items in the item pool."""
"""Weight of boss items in the item pool. (Ignored unless Item Weight Presets is 'No')"""
display_name = "Boss Items"
range_start = 0
range_end = 100
@@ -103,7 +107,7 @@ class BossItem(Range):
class LunarItem(Range):
"""Weight of lunar items in the item pool."""
"""Weight of lunar items in the item pool. (Ignored unless Item Weight Presets is 'No')"""
display_name = "Lunar Items"
range_start = 0
range_end = 100
@@ -111,7 +115,7 @@ class LunarItem(Range):
class Equipment(Range):
"""Weight of equipment items in the item pool."""
"""Weight of equipment items in the item pool. (Ignored unless Item Weight Presets is 'No')"""
display_name = "Equipment"
range_start = 0
range_end = 100
@@ -122,15 +126,16 @@ class ItemPoolPresetToggle(DefaultOnToggle):
"""Will use the item weight presets when set to true, otherwise will use the custom set item pool weights."""
display_name = "Item Weight Presets"
class ItemWeights(Choice):
"""Preset choices for determining the weights of the item pool.<br>
New is a test for a potential adjustment to the default weights.<br>
Uncommon puts a large number of uncommon items in the pool.<br>
Legendary puts a large number of legendary items in the pool.<br>
Lunartic makes everything a lunar item.<br>
Chaos generates the pool completely at random with rarer items having a slight cap to prevent this option being too easy.<br>
No Scraps removes all scrap items from the item pool.<br>
Even generates the item pool with every item having an even weight.<br>
"""Preset choices for determining the weights of the item pool.
New is a test for a potential adjustment to the default weights.
Uncommon puts a large number of uncommon items in the pool.
Legendary puts a large number of legendary items in the pool.
Lunartic makes everything a lunar item.
Chaos generates the pool completely at random with rarer items having a slight cap to prevent this option being too easy.
No Scraps removes all scrap items from the item pool.
Even generates the item pool with every item having an even weight.
Scraps Only will be only scrap items in the item pool."""
display_name = "Item Weights"
option_default = 0
@@ -143,7 +148,8 @@ class ItemWeights(Choice):
option_even = 7
option_scraps_only = 8
#define a dictionary for the weights of the generated item pool.
# define a dictionary for the weights of the generated item pool.
ror2_weights: typing.Dict[str, type(Option)] = {
"green_scrap": GreenScrap,
"red_scrap": RedScrap,
@@ -161,6 +167,7 @@ ror2_options: typing.Dict[str, type(Option)] = {
"total_locations": TotalLocations,
"total_revivals": TotalRevivals,
"start_with_revive": StartWithRevive,
"final_stage_death": FinalStageDeath,
"item_pickup_step": ItemPickupStep,
"enable_lunar": AllowLunarItems,
"item_weights": ItemWeights,

View File

@@ -110,15 +110,16 @@ class RiskOfRainWorld(World):
"seed": "".join(self.world.slot_seeds[self.player].choice(string.digits) for i in range(16)),
"totalLocations": self.world.total_locations[self.player].value,
"totalRevivals": self.world.total_revivals[self.player].value,
"startWithDio": self.world.start_with_revive[self.player].value
"startWithDio": self.world.start_with_revive[self.player].value,
"FinalStageDeath": self.world.final_stage_death[self.player].value
}
def create_item(self, name: str) -> Item:
item_id = item_table[name]
item = RiskOfRainItem(name, ItemClassification.filler, item_id, self.player)
if name == 'Dio\'s Best Friend':
if name == "Dio's Best Friend":
item.classification = ItemClassification.progression
elif name == 'Equipment':
elif name in {"Equipment", "Legendary Item"}:
item.classification = ItemClassification.useful
return item

View File

@@ -12,6 +12,32 @@ functionality in which certain chests (made clear via a location check progress
multiworld. The items that _would have been_ in those chests will be returned to the Risk of Rain player via grants by
other players in other worlds.
## What is the goal of Risk of Rain 2 in Archipelago?
Just like in the original game, any way to "beat the game or obliterate" counts as a win. By default, if you die while
on a final boss stage, that also counts as a win. (You can turn this off in your player settings.) **You do not need to
complete all the location checks** to win; any item you don't collect is automatically sent out to the multiworld when
you meet your goal.
If you die before you accomplish your goal, you can start a new run. You will start the run with any items that you
received from other players. Any items that you picked up the "normal" way will be lost.
Note, you can play Simulacrum mode as part of an Archipelago, but you can't achieve any of the victory conditions in
Simulacrum. So you could, for example, collect most of your items through a Simulacrum run, then finish a normal mode
run while keeping the items you received via the multiworld.
## Can you play multiplayer?
Yes! You can have a single multiplayer instance as one world in the multiworld. All the players involved need to have
the Archipelago mod, but only the host needs to configure the Archipelago settings. When someone finds an item for your
world, all the connected players will receive a copy of the item, and the location check bar will increase whenever any
player finds an item in Risk of Rain.
You cannot have players with different player slots in the same co-op game instance. Only the host's Archipelago
settings apply, so each Risk of Rain 2 player slot in the multiworld needs to be a separate game instance. You could,
for example, have two players trade off hosting and making progress on each other's player slot, but a single co-op
instance can't make progress towards multiple player slots in the multiworld.
## What Risk of Rain items can appear in other players' worlds?
The Risk of Rain items are:
@@ -31,13 +57,34 @@ in-game item of that tier will appear in the Risk of Rain player's inventory. If
the player already has an equipment item equipped then the _item that was equipped_ will be dropped on the ground and _
the new equipment_ will take it's place. (If you want the old one back, pick it up.)
### How many items are there?
Since a Risk of Rain 2 run can go on indefinitely, you have to configure how many collectible items (also known as
"checks") the game has for purposes of Archipelago when you set up a multiworld. You can configure anywhere from **10
to 250** items. The number of items will be randomized between all players, so you may want to adjust the number and
item pickup step based on how many items the other players in the multiworld have. (Around 100 seems to be a good
ballpark if you want to have a similar number of items to most other games.)
After you have completed the specified number of checks, you won't send anything else to the multiworld. You can
receive up to the specified number of randomized items from the multiworld as the players find them. In either case,
you can continue to collect items as normal in Risk of Rain 2 if you've already found all your location checks.
## What does another world's item look like in Risk of Rain?
When the Risk of Rain player fills up their location check bar then the next spawned item will become an item grant for
another player's world. The item in Risk of Rain will disappear in a poof of smoke and the grant will automatically go
out to the multiworld.
another player's world (or possibly get sent back to yourself). The item in Risk of Rain will disappear in a poof of
smoke and the grant will automatically go out to the multiworld.
## What is the item pickup step?
The item pickup step is a YAML setting which allows you to set how many items you need to spawn before the _next_ item
that is spawned disappears (in a poof of smoke) and goes out to the multiworld.
## Is Archipelago compatible with other Risk of Rain 2 mods?
Mostly, yes. Not every mod will work; in particular, anything that causes items to go directly into your inventory
rather than spawning onto the map will interfere with the way the Archipelago mod works. However, many common mods work
just fine with Archipelago.
For competitive play, of course, you should only use mods that are agreed-upon by the competitors so that you don't
have an unfair advantage.

View File

@@ -7,6 +7,8 @@ import threading
import base64
from typing import Set, List, TextIO
from worlds.sm.variaRandomizer.graph.graph_utils import GraphUtils
logger = logging.getLogger("Super Metroid")
from .Locations import lookup_name_to_id as locations_lookup_name_to_id
@@ -654,11 +656,10 @@ class SMLocation(Location):
def can_comeback(self, state: CollectionState, item: Item):
randoExec = state.world.worlds[self.player].variaRando.randoExec
for key in locationsDict[self.name].AccessFrom.keys():
if (randoExec.areaGraph.canAccess( state.smbm[self.player],
key,
randoExec.graphSettings.startAP,
state.smbm[self.player].maxDiff,
None)):
if (randoExec.areaGraph.canAccessList( state.smbm[self.player],
key,
[randoExec.graphSettings.startAP, 'Landing Site'] if not GraphUtils.isStandardStart(randoExec.graphSettings.startAP) else ['Landing Site'],
state.smbm[self.player].maxDiff)):
return True
return False

View File

@@ -23,7 +23,8 @@ certain items to your own world.
## What does another world's item look like in Super Metroid?
A unique item sprite has been added to the game to represent items belonging to another world.
Two unique item sprites have been added to the game to represent items belonging to another world. Progression items have
a small up arrow on the sprite and non-progression don't.
## When the player receives an item, what happens?

View File

@@ -367,6 +367,22 @@ class AccessGraph(object):
#print("canAccess: {}".format(can))
return can
# test access from an access point to a list of others, given an optional item
def canAccessList(self, smbm, srcAccessPointName, destAccessPointNameList, maxDiff, item=None):
if item is not None:
smbm.addItem(item)
#print("canAccess: item: {}, src: {}, dest: {}".format(item, srcAccessPointName, destAccessPointName))
destAccessPointList = [self.accessPoints[destAccessPointName] for destAccessPointName in destAccessPointNameList]
srcAccessPoint = self.accessPoints[srcAccessPointName]
availAccessPoints = self.getAvailableAccessPoints(srcAccessPoint, smbm, maxDiff, item)
can = any(ap in availAccessPoints for ap in destAccessPointList)
# if not can:
# self.log.debug("canAccess KO: avail = {}".format([ap.Name for ap in availAccessPoints.keys()]))
if item is not None:
smbm.removeItem(item)
#print("canAccess: {}".format(can))
return can
# returns a list of AccessPoint instances from srcAccessPointName to destAccessPointName
# (not including source ap)
# or None if no possible path

View File

@@ -341,6 +341,8 @@ class VariaRandomizer:
if preset == 'custom':
PresetLoader.factory(world.custom_preset[player].value).load(self.player)
elif preset == 'varia_custom':
if len(world.varia_custom_preset[player].value) == 0:
raise Exception("varia_custom was chosen but varia_custom_preset is missing.")
url = 'https://randommetroidsolver.pythonanywhere.com/presetWebService'
preset_name = next(iter(world.varia_custom_preset[player].value))
payload = '{{"preset": "{}"}}'.format(preset_name)

View File

@@ -13,9 +13,28 @@ class StrictCannonRequirements(DefaultOnToggle):
"""If disabled, Stars that expect cannons may have to be acquired without them. Only makes a difference if Buddy Checks are enabled"""
display_name = "Strict Cannon Requirements"
class FirstBowserStarDoorCost(Range):
"""How many stars are required at the Star Door to Bowser in the Dark World"""
range_start = 0
range_end = 20
default = 8
class BasementStarDoorCost(Range):
"""How many stars are required at the Star Door in the Basement"""
range_start = 0
range_end = 50
default = 30
class SecondFloorStarDoorCost(Range):
"""How many stars are required to access the third floor"""
range_start = 0
range_end = 50
default = 50
class StarsToFinish(Range):
"""How many stars are required at the infinite stairs"""
range_start = 50
display_name = "Endless Stairs Stars"
range_start = 0
range_end = 100
default = 70
@@ -43,6 +62,9 @@ sm64_options: typing.Dict[str,type(Option)] = {
"EnableCoinStars": EnableCoinStars,
"StrictCapRequirements": StrictCapRequirements,
"StrictCannonRequirements": StrictCannonRequirements,
"FirstBowserStarDoorCost": FirstBowserStarDoorCost,
"BasementStarDoorCost": BasementStarDoorCost,
"SecondFloorStarDoorCost": SecondFloorStarDoorCost,
"StarsToFinish": StarsToFinish,
"ExtraStars": ExtraStars,
"death_link": DeathLink,

View File

@@ -11,6 +11,8 @@ sm64courses = ["Bob-omb Battlefield", "Whomp's Fortress", "Jolly Roger Bay", "Co
"Wet-Dry World",
"Tall, Tall Mountain", "Tiny-Huge Island", "Tick Tock Clock", "Rainbow Ride"]
# sm64paintings is list of strings for quick reference for Painting IDs (NOT warp node IDs!)
sm64paintings = ["BOB", "WF", "JRB", "CCM", "BBH", "HMC", "LLL", "SSL", "DDD", "SL", "WDW", "TTM", "THI Tiny", "THI Huge", "TTC", "RR"]
def create_regions(world: MultiWorld, player: int):
regSS = Region("Menu", RegionType.Generic, "Castle Area", player, world)

View File

@@ -1,40 +1,50 @@
from ..generic.Rules import add_rule
from .Regions import connect_regions, sm64courses
from .Regions import connect_regions, sm64courses, sm64paintings
def set_rules(world, player: int, area_connections):
courseshuffle = list(range(len(sm64courses)))
entrance_ids = list(range(len(sm64paintings)))
destination_courses = list(range(13)) + [12,13,14] # Two instances of Destination Course THI
if world.AreaRandomizer[player]:
world.random.shuffle(courseshuffle)
area_connections.update({index: value for index, value in enumerate(courseshuffle)})
world.random.shuffle(entrance_ids)
temp_assign = dict(zip(entrance_ids,destination_courses)) # Used for Rules only
connect_regions(world, player, "Menu", sm64courses[area_connections[0]])
connect_regions(world, player, "Menu", sm64courses[area_connections[1]], lambda state: state.has("Power Star", player, 1))
connect_regions(world, player, "Menu", sm64courses[area_connections[2]], lambda state: state.has("Power Star", player, 3))
connect_regions(world, player, "Menu", sm64courses[area_connections[3]], lambda state: state.has("Power Star", player, 3))
connect_regions(world, player, "Menu", "Bowser in the Dark World", lambda state: state.has("Power Star", player, 8))
connect_regions(world, player, "Menu", sm64courses[area_connections[4]], lambda state: state.has("Power Star", player, 12))
# Destination Format: LVL | AREA with LVL = Course ID, 0-indexed, AREA = Area as used in sm64 code
area_connections.update({entrance: (destination_course*10 + 1) for entrance, destination_course in temp_assign.items()})
for i in range(len(area_connections)):
if (int(area_connections[i]/10) == 12):
# Change first occurence of course 12 (THI) to Area 2 (THI Tiny)
area_connections[i] = 12*10 + 2
break
connect_regions(world, player, "Menu", sm64courses[temp_assign[0]])
connect_regions(world, player, "Menu", sm64courses[temp_assign[1]], lambda state: state.has("Power Star", player, 1))
connect_regions(world, player, "Menu", sm64courses[temp_assign[2]], lambda state: state.has("Power Star", player, 3))
connect_regions(world, player, "Menu", sm64courses[temp_assign[3]], lambda state: state.has("Power Star", player, 3))
connect_regions(world, player, "Menu", "Bowser in the Dark World", lambda state: state.has("Power Star", player, world.FirstBowserStarDoorCost[player].value))
connect_regions(world, player, "Menu", sm64courses[temp_assign[4]], lambda state: state.has("Power Star", player, 12))
connect_regions(world, player, "Menu", "Basement", lambda state: state.has("Basement Key", player) or state.has("Progressive Key", player, 1))
connect_regions(world, player, "Basement", sm64courses[area_connections[5]])
connect_regions(world, player, "Basement", sm64courses[area_connections[6]])
connect_regions(world, player, "Basement", sm64courses[area_connections[7]])
connect_regions(world, player, "Basement", sm64courses[area_connections[8]], lambda state: state.has("Power Star", player, 30))
connect_regions(world, player, "Basement", "Bowser in the Fire Sea", lambda state: state.has("Power Star", player, 30) and
connect_regions(world, player, "Basement", sm64courses[temp_assign[5]])
connect_regions(world, player, "Basement", sm64courses[temp_assign[6]])
connect_regions(world, player, "Basement", sm64courses[temp_assign[7]])
connect_regions(world, player, "Basement", sm64courses[temp_assign[8]], lambda state: state.has("Power Star", player, world.BasementStarDoorCost[player].value))
connect_regions(world, player, "Basement", "Bowser in the Fire Sea", lambda state: state.has("Power Star", player, world.BasementStarDoorCost[player].value) and
state.can_reach("DDD: Board Bowser's Sub", 'Location', player))
connect_regions(world, player, "Menu", "Second Floor", lambda state: state.has("Second Floor Key", player) or state.has("Progressive Key", player, 2))
connect_regions(world, player, "Second Floor", sm64courses[area_connections[9]])
connect_regions(world, player, "Second Floor", sm64courses[area_connections[10]])
connect_regions(world, player, "Second Floor", sm64courses[area_connections[11]])
connect_regions(world, player, "Second Floor", sm64courses[area_connections[12]])
connect_regions(world, player, "Second Floor", sm64courses[temp_assign[9]])
connect_regions(world, player, "Second Floor", sm64courses[temp_assign[10]])
connect_regions(world, player, "Second Floor", sm64courses[temp_assign[11]])
connect_regions(world, player, "Second Floor", sm64courses[temp_assign[12]]) # THI Tiny
connect_regions(world, player, "Second Floor", sm64courses[temp_assign[13]]) # THI Huge
connect_regions(world, player, "Second Floor", "Third Floor", lambda state: state.has("Power Star", player, 50))
connect_regions(world, player, "Second Floor", "Third Floor", lambda state: state.has("Power Star", player, world.SecondFloorStarDoorCost[player].value))
connect_regions(world, player, "Third Floor", sm64courses[area_connections[13]])
connect_regions(world, player, "Third Floor", sm64courses[area_connections[14]])
connect_regions(world, player, "Third Floor", sm64courses[temp_assign[14]])
connect_regions(world, player, "Third Floor", sm64courses[temp_assign[15]])
#Special Rules for some Locations
add_rule(world.get_location("Tower of the Wing Cap Switch", player), lambda state: state.has("Power Star", player, 10))

View File

@@ -5,13 +5,10 @@ from .Items import item_table, cannon_item_table, SM64Item
from .Locations import location_table, SM64Location
from .Options import sm64_options
from .Rules import set_rules
from .Regions import create_regions, sm64courses
from .Regions import create_regions, sm64courses, sm64paintings
from BaseClasses import Item, Tutorial, ItemClassification
from ..AutoWorld import World, WebWorld
client_version = 1
class SM64Web(WebWorld):
tutorials = [Tutorial(
"Multiworld Setup Guide",
@@ -38,6 +35,8 @@ class SM64World(World):
location_name_to_id = location_table
data_version = 6
required_client_version = (0,3,0)
forced_auto_forfeit = False
area_connections: typing.Dict[int, int]
@@ -55,10 +54,10 @@ class SM64World(World):
set_rules(self.world, self.player, self.area_connections)
if self.topology_present:
# Write area_connections to spoiler log
for painting_id, course_id in self.area_connections.items():
for painting_id, destination in self.area_connections.items():
self.world.spoiler.set_entrance(
sm64courses[painting_id] + " Painting",
sm64courses[course_id],
sm64paintings[painting_id] + " Painting",
sm64courses[destination // 10],
'entrance', self.player)
def create_item(self, name: str) -> Item:
@@ -115,6 +114,9 @@ class SM64World(World):
def fill_slot_data(self):
return {
"AreaRando": self.area_connections,
"FirstBowserDoorCost": self.world.FirstBowserStarDoorCost[self.player].value,
"BasementDoorCost": self.world.BasementStarDoorCost[self.player].value,
"SecondFloorCost": self.world.SecondFloorStarDoorCost[self.player].value,
"StarsToFinish": self.world.StarsToFinish[self.player].value,
"DeathLink": self.world.death_link[self.player].value,
}
@@ -143,8 +145,8 @@ class SM64World(World):
def modify_multidata(self, multidata):
if self.topology_present:
er_hint_data = {}
for painting_id, course_id in self.area_connections.items():
region = self.world.get_region(sm64courses[course_id], self.player)
for painting_id, destination in self.area_connections.items():
region = self.world.get_region(sm64courses[destination // 10], self.player)
for location in region.locations:
er_hint_data[location.address] = sm64courses[painting_id]
er_hint_data[location.address] = sm64paintings[painting_id]
multidata['er_hint_data'][self.player] = er_hint_data

View File

@@ -619,6 +619,7 @@ class Patch:
if (self.myWorld.Config.Keysanity):
self.patches.append((Snes(0x40003B), [ 1 ])) #// MapMode #$00 = Always On (default) - #$01 = Require Map Item
self.patches.append((Snes(0x400045), [ 0x0f ])) #// display ----dcba a: Small Keys, b: Big Key, c: Map, d: Compass
self.patches.append((Snes(0x40016A), [ 0x01 ])) #// enable local item dialog boxes for dungeon and keycard items
def WriteSMKeyCardDoors(self):
if (not self.myWorld.Config.Keysanity):

View File

@@ -380,6 +380,10 @@ Items:
Keycard: |-
A key from
the future?
Something: |-
A small victory!
default: |-
Don't waste
your time!

View File

@@ -27,14 +27,18 @@ class SMZ3CollectionState(metaclass=AutoLogicRegister):
# for unit tests where MultiWorld is instantiated before worlds
if hasattr(parent, "state"):
self.smz3state = {player: TotalSMZ3Item.Progression([]) for player in parent.get_game_players("SMZ3")}
for player, group in parent.groups.items():
if (group["game"] == "SMZ3"):
self.smz3state[player] = TotalSMZ3Item.Progression([])
if player not in parent.state.smz3state:
parent.state.smz3state[player] = TotalSMZ3Item.Progression([])
else:
self.smz3state = {}
def copy_mixin(self, ret) -> CollectionState:
ret.smz3state = {player: copy.deepcopy(self.smz3state[player]) for player in self.world.get_game_players("SMZ3")}
ret.smz3state = {player: copy.deepcopy(self.smz3state[player]) for player in self.smz3state}
return ret
class SMZ3Web(WebWorld):
tutorials = [Tutorial(
"Multiworld Setup Guide",
@@ -106,6 +110,7 @@ class SMZ3World(World):
niceItems = TotalSMZ3Item.Item.CreateNicePool(self.smz3World)
junkItems = TotalSMZ3Item.Item.CreateJunkPool(self.smz3World)
allJunkItems = niceItems + junkItems
self.junkItemsNames = [item.Type.name for item in junkItems]
if (self.smz3World.Config.Keysanity):
progressionItems = self.progression + self.dungeon + self.keyCardsItems
@@ -256,11 +261,11 @@ class SMZ3World(World):
base_combined_rom = basepatch.apply(base_combined_rom)
patcher = TotalSMZ3Patch(self.smz3World,
[world.smz3World for key, world in self.world.worlds.items() if isinstance(world, SMZ3World)],
[world.smz3World for key, world in self.world.worlds.items() if isinstance(world, SMZ3World) and hasattr(world, "smz3World")],
self.world.seed_name,
self.world.seed,
self.local_random,
self.world.world_name_lookup,
{v: k for k, v in self.world.player_name.items()},
next(iter(loc.player for loc in self.world.get_locations() if (loc.item.name == "SilverArrows" and loc.item.player == self.player))))
patches = patcher.Create(self.smz3World.Config)
patches.update(self.apply_sm_custom_sprite())
@@ -312,7 +317,7 @@ class SMZ3World(World):
return slot_data
def collect(self, state: CollectionState, item: Item) -> bool:
state.smz3state[item.player].Add([TotalSMZ3Item.Item(TotalSMZ3Item.ItemType[item.name], self.smz3World)])
state.smz3state[self.player].Add([TotalSMZ3Item.Item(TotalSMZ3Item.ItemType[item.name], self.smz3World if hasattr(self, "smz3World") else None)])
if item.advancement:
state.prog_items[item.name, item.player] += 1
return True # indicate that a logical state change has occured
@@ -321,7 +326,7 @@ class SMZ3World(World):
def remove(self, state: CollectionState, item: Item) -> bool:
name = self.collect_item(state, item, True)
if name:
state.smz3state[item.player].Remove([TotalSMZ3Item.Item(TotalSMZ3Item.ItemType[item.name], self.smz3World)])
state.smz3state[item.player].Remove([TotalSMZ3Item.Item(TotalSMZ3Item.ItemType[item.name], self.smz3World if hasattr(self, "smz3World") else None)])
state.prog_items[name, item.player] -= 1
if state.prog_items[name, item.player] < 1:
del (state.prog_items[name, item.player])
@@ -330,7 +335,9 @@ class SMZ3World(World):
def create_item(self, name: str) -> Item:
return SMZ3Item(name, ItemClassification.progression,
TotalSMZ3Item.ItemType[name], self.item_name_to_id[name], player = self.player)
TotalSMZ3Item.ItemType[name], self.item_name_to_id[name],
self.player,
TotalSMZ3Item.Item(TotalSMZ3Item.ItemType[name], self))
def pre_fill(self):
from Fill import fill_restrictive
@@ -364,6 +371,9 @@ class SMZ3World(World):
else:
return []
def get_filler_item_name(self) -> str:
return self.world.random.choice(self.junkItemsNames)
def write_spoiler(self, spoiler_handle: TextIO):
self.world.spoiler.unreachables.update(self.unreachable)

Binary file not shown.

View File

@@ -23,7 +23,8 @@ certain items to your own world.
## What does another world's item look like in Super Metroid?
A unique item sprite has been added to the game to represent items belonging to another world.
Two unique item sprites have been added to the game to represent items belonging to another world. Progression items have
a small up arrow on the sprite and non-progression don't.
## What does another world's item look like in LttP?

View File

@@ -1,5 +1,6 @@
from BaseClasses import MultiWorld
from ..AutoWorld import LogicMixin
from .Options import EnergyCore
from typing import Set
# TODO: Options may preset certain progress steps (i.e. P_ROCK_SKIP), set in generate_early?
@@ -8,9 +9,9 @@ from . import pyevermizer
# TODO: resolve/flatten/expand rules to get rid of recursion below where possible
# Logic.rules are all rules including locations, excluding those with no progress (i.e. locations that only drop items)
rules = [rule for rule in pyevermizer.get_logic() if len(rule.provides) > 0]
# Logic.items are all items excluding non-progression items and duplicates
# Logic.items are all items and extra items excluding non-progression items and duplicates
item_names: Set[str] = set()
items = [item for item in filter(lambda item: item.progression, pyevermizer.get_items())
items = [item for item in filter(lambda item: item.progression, pyevermizer.get_items() + pyevermizer.get_extra_items())
if item.name not in item_names and not item_names.add(item.name)]
@@ -47,4 +48,9 @@ class SecretOfEvermoreLogic(LogicMixin):
"""
Returns True if count of one of evermizer's progress steps is reached based on collected items. i.e. 2 * P_DE
"""
if progress == pyevermizer.P_ENERGY_CORE: # logic is shared between worlds, so we override in the call
w = world.worlds[player]
if w.energy_core == EnergyCore.option_fragments:
progress = pyevermizer.P_CORE_FRAGMENT
count = w.required_fragments
return self._soe_count(progress, world, player, count) >= count

View File

@@ -37,6 +37,32 @@ class Difficulty(EvermizerFlags, Choice):
flags = ['e', 'n', 'h', 'x']
class EnergyCore(EvermizerFlags, Choice):
"""How to obtain the Energy Core"""
display_name = "Energy Core"
option_vanilla = 0
option_shuffle = 1
option_fragments = 2
default = 1
flags = ['z', '', 'Z']
class RequiredFragments(Range):
"""Required fragment count for Energy Core = Fragments"""
display_name = "Required Fragments"
range_start = 1
range_end = 99
default = 10
class AvailableFragments(Range):
"""Placed fragment count for Energy Core = Fragments"""
display_name = "Available Fragments"
range_start = 1
range_end = 99
default = 11
class MoneyModifier(Range):
"""Money multiplier in %"""
display_name = "Money Modifier"
@@ -186,10 +212,15 @@ class TrapChanceOHKO(TrapChance):
class SoEProgressionBalancing(ProgressionBalancing):
default = 30
__doc__ = ProgressionBalancing.__doc__.replace(f"default {ProgressionBalancing.default}", f"default {default}")
special_range_names = {**ProgressionBalancing.special_range_names, "normal": default}
soe_options: typing.Dict[str, type(Option)] = {
"difficulty": Difficulty,
"energy_core": EnergyCore,
"required_fragments": RequiredFragments,
"available_fragments": AvailableFragments,
"money_modifier": MoneyModifier,
"exp_modifier": ExpModifier,
"fix_sequence": FixSequence,

View File

@@ -16,7 +16,7 @@ except ImportError:
from . import pyevermizer # as part of the source tree
from . import Logic # load logic mixin
from .Options import soe_options
from .Options import soe_options, EnergyCore, RequiredFragments, AvailableFragments
from .Patch import SoEDeltaPatch, get_base_rom_path
"""
@@ -52,7 +52,6 @@ Item grouping currently supports
* Ingredients - Matches all ingredient drops
* Alchemy - Matches all alchemy formulas
* Weapons - Matches all weapons but Bazooka, Bone Crusher, Neutron Blade
* Bazooka - Matches all bazookas (currently only one)
* Traps - Matches all traps
"""
@@ -63,12 +62,14 @@ _id_offset: typing.Dict[int, int] = {
pyevermizer.CHECK_GOURD: _id_base + 100, # gourds 64100..64399
pyevermizer.CHECK_NPC: _id_base + 400, # npc 64400..64499
# TODO: sniff 64500..64799
pyevermizer.CHECK_TRAP: _id_base + 900, # npc 64900..64999
pyevermizer.CHECK_EXTRA: _id_base + 800, # extra items 64800..64899
pyevermizer.CHECK_TRAP: _id_base + 900, # trap 64900..64999
}
# cache native evermizer items and locations
_items = pyevermizer.get_items()
_traps = pyevermizer.get_traps()
_extras = pyevermizer.get_extra_items() # items that are not placed by default
_locations = pyevermizer.get_locations()
# fix up texts for AP
for _loc in _locations:
@@ -104,7 +105,7 @@ def _get_location_mapping() -> typing.Tuple[typing.Dict[str, int], typing.Dict[i
def _get_item_mapping() -> typing.Tuple[typing.Dict[str, int], typing.Dict[int, pyevermizer.Item]]:
name_to_id = {}
id_to_raw = {}
for item in itertools.chain(_items, _traps):
for item in itertools.chain(_items, _extras, _traps):
if item.name in name_to_id:
continue
ap_id = _id_offset[item.type] + item.index
@@ -127,7 +128,6 @@ def _get_item_grouping() -> typing.Dict[str, typing.Set[str]]:
groups['Alchemy'] = set(item.name for item in _items if item.type == pyevermizer.CHECK_ALCHEMY)
groups['Weapons'] = {'Spider Claw', 'Horn Spear', 'Gladiator Sword', 'Bronze Axe', 'Bronze Spear', 'Crusader Sword',
'Lance (Weapon)', 'Knight Basher', 'Atom Smasher', 'Laser Lance'}
groups['Bazooka'] = {'Bazooka+Shells / Shining Armor / 5k Gold'}
groups['Traps'] = {trap.name for trap in _traps}
return groups
@@ -136,7 +136,8 @@ class SoEWebWorld(WebWorld):
theme = 'jungle'
tutorials = [Tutorial(
"Multiworld Setup Guide",
"A guide to playing Secret of Evermore randomizer. This guide covers single-player, multiworld and related software.",
"A guide to playing Secret of Evermore randomizer. This guide covers single-player, multiworld and related"
" software.",
"English",
"multiworld_en.md",
"multiworld/en",
@@ -153,9 +154,9 @@ class SoEWorld(World):
options = soe_options
topology_present = False
remote_items = False
data_version = 2
data_version = 3
web = SoEWebWorld()
required_client_version = (0, 2, 6)
required_client_version = (0, 3, 3)
item_name_to_id, item_id_to_raw = _get_item_mapping()
location_name_to_id, location_id_to_raw = _get_location_mapping()
@@ -165,6 +166,9 @@ class SoEWorld(World):
evermizer_seed: int
connect_name: str
energy_core: int
available_fragments: int
required_fragments: int
_halls_ne_chest_names: typing.List[str] = [loc.name for loc in _locations if 'Halls NE' in loc.name]
@@ -172,6 +176,14 @@ class SoEWorld(World):
self.connect_name_available_event = threading.Event()
super(SoEWorld, self).__init__(*args, **kwargs)
def generate_early(self) -> None:
# store option values that change logic
self.energy_core = self.world.energy_core[self.player].value
self.required_fragments = self.world.required_fragments[self.player].value
if self.required_fragments > self.world.available_fragments[self.player].value:
self.world.available_fragments[self.player].value = self.required_fragments
self.available_fragments = self.world.available_fragments[self.player].value
def create_event(self, event: str) -> Item:
return SoEItem(event, ItemClassification.progression, None, self.player)
@@ -182,6 +194,8 @@ class SoEWorld(World):
classification = ItemClassification.trap
elif item.progression:
classification = ItemClassification.progression
elif item.useful:
classification = ItemClassification.useful
else:
classification = ItemClassification.filler
@@ -208,9 +222,33 @@ class SoEWorld(World):
self.world.get_entrance('New Game', self.player).connect(self.world.get_region('Ingame', self.player))
def create_items(self):
# add items to the pool
items = list(map(lambda item: self.create_item(item), _items))
# add regular items to the pool
exclusions: typing.List[str] = []
if self.energy_core != EnergyCore.option_shuffle:
exclusions.append("Energy Core") # will be placed in generate_basic or replaced by a fragment below
items = list(map(lambda item: self.create_item(item), (item for item in _items if item.name not in exclusions)))
# remove one pair of wings that will be placed in generate_basic
items.remove(self.create_item("Wings"))
def is_ingredient(item):
for ingredient in _ingredients:
if _match_item_name(item, ingredient):
return True
return False
# add energy core fragments to the pool
ingredients = [n for n, item in enumerate(items) if is_ingredient(item)]
if self.energy_core == EnergyCore.option_fragments:
items.append(self.create_item("Energy Core Fragment")) # replaces the vanilla energy core
for _ in range(self.available_fragments - 1):
if len(ingredients) < 1:
break # out of ingredients to replace
r = self.world.random.choice(ingredients)
ingredients.remove(r)
items[r] = self.create_item("Energy Core Fragment")
# add traps to the pool
trap_count = self.world.trap_count[self.player].value
trap_chances = {}
trap_names = {}
@@ -232,13 +270,12 @@ class SoEWorld(World):
return self.create_item(trap_names[t])
v -= c
while trap_count > 0:
r = self.world.random.randrange(len(items))
for ingredient in _ingredients:
if _match_item_name(items[r], ingredient):
items[r] = create_trap()
trap_count -= 1
break
for _ in range(trap_count):
if len(ingredients) < 1:
break # out of ingredients to replace
r = self.world.random.choice(ingredients)
ingredients.remove(r)
items[r] = create_trap()
self.world.itempool += items
@@ -271,7 +308,10 @@ class SoEWorld(World):
wings_location = self.world.random.choice(self._halls_ne_chest_names)
wings_item = self.create_item('Wings')
self.world.get_location(wings_location, self.player).place_locked_item(wings_item)
self.world.itempool.remove(wings_item)
# place energy core at vanilla location for vanilla mode
if self.energy_core == EnergyCore.option_vanilla:
energy_core = self.create_item('Energy Core')
self.world.get_location('Energy Core #285', self.player).place_locked_item(energy_core)
# generate stuff for later
self.evermizer_seed = self.world.random.randint(0, 2 ** 16 - 1) # TODO: make this an option for "full" plando?
@@ -286,9 +326,12 @@ class SoEWorld(World):
try:
money = self.world.money_modifier[self.player].value
exp = self.world.exp_modifier[self.player].value
switches = []
switches: typing.List[str] = []
if self.world.death_link[self.player].value:
switches.append("--death-link")
if self.energy_core == EnergyCore.option_fragments:
switches.extend(('--available-fragments', str(self.available_fragments),
'--required-fragments', str(self.required_fragments)))
rom_file = get_base_rom_path()
out_base = output_path(output_directory, f'AP_{self.world.seed_name}_P{self.player}_'
f'{self.world.get_file_safe_player_name(self.player)}')

View File

@@ -1,14 +1,14 @@
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp38-cp38-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.8'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp39-cp39-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.9'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp310-cp310-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.10'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.8'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.9'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.10'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.8'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.9'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.10'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp38-cp38-macosx_10_9_x86_64.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.8'
#https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp39-cp39-macosx_10_9_x86_64.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.9'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp39-cp39-macosx_10_9_universal2.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.9'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp310-cp310-macosx_10_9_universal2.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.10'
#https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2.tar.gz#egg=pyevermizer; python_version == '3.11'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp38-cp38-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.8'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp39-cp39-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.9'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp310-cp310-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.10'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.8'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.9'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.10'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.8'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.9'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.10'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp38-cp38-macosx_10_9_x86_64.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.8'
#https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp39-cp39-macosx_10_9_x86_64.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.9'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp39-cp39-macosx_10_9_universal2.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.9'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp310-cp310-macosx_10_9_universal2.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.10'
#https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3.tar.gz#egg=pyevermizer; python_version == '3.11'

View File

@@ -0,0 +1,82 @@
from typing import Dict, Set, List
# EN Locale Creature Name to rough depth in meters found at
all_creatures: Dict[str, int] = {
"Gasopod": 0,
"Bladderfish": 0,
"Ancient Floater": 0,
"Skyray": 0,
"Garryfish": 0,
"Peeper": 0,
"Shuttlebug": 0,
"Rabbit Ray": 0,
"Stalker": 0,
"Floater": 0,
"Holefish": 0,
"Cave Crawler": 0,
"Hoopfish": 0,
"Crashfish": 0,
"Hoverfish": 0,
"Spadefish": 0,
"Reefback Leviathan": 0,
"Reaper Leviathan": 0,
"Warper": 0,
"Boomerang": 0,
"Biter": 200,
"Sand Shark": 200,
"Bleeder": 200,
"Crabsnake": 300,
"Jellyray": 300,
"Oculus": 300,
"Mesmer": 300,
"Eyeye": 300,
"Reginald": 400,
"Sea Treader Leviathan": 400,
"Crabsquid": 400,
"Ampeel": 400,
"Boneshark": 400,
"Rockgrub": 400,
"Ghost Leviathan": 500,
"Ghost Leviathan Juvenile": 500,
"Spinefish": 600,
"Blighter": 600,
"Blood Crawler": 600,
"Ghostray": 1000,
"Amoeboid": 1000,
"River Prowler": 1000,
"Red Eyeye": 1300,
"Magmarang": 1300,
"Crimson Ray": 1300,
"Lava Larva": 1300,
"Lava Lizard": 1300,
"Sea Dragon Leviathan": 1300,
"Sea Emperor Leviathan": 1700,
"Sea Emperor Juvenile": 1700,
# "Cuddlefish": 300, # maybe at some point, needs hatching in containment chamber (20 real-life minutes)
}
# be nice and make these require Stasis Rifle
aggressive: Set[str] = {
"Cave Crawler", # is very easy without Stasis Rifle, but included for consistency
"Crashfish",
"Bleeder",
"Mesmer",
"Reaper Leviathan",
"Crabsquid",
"Warper",
"Crabsnake",
"Ampeel",
"Boneshark",
"Lava Lizard",
"Sea Dragon Leviathan",
"River Prowler",
}
suffix: str = " Scan"
creature_locations: Dict[str, int] = {
creature+suffix: creature_id for creature_id, creature in enumerate(all_creatures, start=34000)
}
all_creatures_presorted: List[str] = sorted(all_creatures)

View File

@@ -1,23 +1,353 @@
import json
import os
from BaseClasses import ItemClassification
from typing import TypedDict, Dict, Set
with open(os.path.join(os.path.dirname(__file__), 'items.json'), 'r') as file:
item_table = json.loads(file.read())
lookup_id_to_name = {}
lookup_name_to_item = {}
advancement_item_names = set()
non_advancement_item_names = set()
class ItemDict(TypedDict):
classification: ItemClassification
count: int
name: str
tech_type: str
for item in item_table:
item_name = item["name"]
lookup_id_to_name[item["id"]] = item_name
lookup_name_to_item[item_name] = item
if item["progression"]:
item_table: Dict[int, ItemDict] = {
35000: {'classification': ItemClassification.useful,
'count': 1,
'name': 'Compass',
'tech_type': 'Compass'},
35001: {'classification': ItemClassification.progression,
'count': 1,
'name': 'Lightweight High Capacity Tank',
'tech_type': 'PlasteelTank'},
35002: {'classification': ItemClassification.progression,
'count': 1,
'name': 'Vehicle Upgrade Console',
'tech_type': 'BaseUpgradeConsole'},
35003: {'classification': ItemClassification.progression,
'count': 1,
'name': 'Ultra Glide Fins',
'tech_type': 'UltraGlideFins'},
35004: {'classification': ItemClassification.useful,
'count': 1,
'name': 'Cyclops Sonar Upgrade',
'tech_type': 'CyclopsSonarModule'},
35005: {'classification': ItemClassification.useful,
'count': 1,
'name': 'Reinforced Dive Suit',
'tech_type': 'ReinforcedDiveSuit'},
35006: {'classification': ItemClassification.useful,
'count': 1,
'name': 'Cyclops Thermal Reactor Module',
'tech_type': 'CyclopsThermalReactorModule'},
35007: {'classification': ItemClassification.filler,
'count': 1,
'name': 'Stillsuit',
'tech_type': 'Stillsuit'},
35008: {'classification': ItemClassification.filler,
'count': 2,
'name': 'Alien Containment Fragment',
'tech_type': 'BaseWaterParkFragment'},
35009: {'classification': ItemClassification.useful,
'count': 1,
'name': 'Creature Decoy',
'tech_type': 'CyclopsDecoy'},
35010: {'classification': ItemClassification.useful,
'count': 1,
'name': 'Cyclops Fire Suppression System',
'tech_type': 'CyclopsFireSuppressionModule'},
35011: {'classification': ItemClassification.useful,
'count': 1,
'name': 'Swim Charge Fins',
'tech_type': 'SwimChargeFins'},
35012: {'classification': ItemClassification.useful,
'count': 1,
'name': 'Repulsion Cannon',
'tech_type': 'RepulsionCannon'},
35013: {'classification': ItemClassification.useful,
'count': 1,
'name': 'Cyclops Decoy Tube Upgrade',
'tech_type': 'CyclopsDecoyModule'},
35014: {'classification': ItemClassification.progression,
'count': 1,
'name': 'Cyclops Shield Generator',
'tech_type': 'CyclopsShieldModule'},
35015: {'classification': ItemClassification.progression,
'count': 1,
'name': 'Cyclops Depth Module MK1',
'tech_type': 'CyclopsHullModule1'},
35016: {'classification': ItemClassification.useful,
'count': 1,
'name': 'Cyclops Docking Bay Repair Module',
'tech_type': 'CyclopsSeamothRepairModule'},
35017: {'classification': ItemClassification.useful,
'count': 2,
'name': 'Battery Charger fragment',
'tech_type': 'BatteryChargerFragment'},
35018: {'classification': ItemClassification.filler,
'count': 2,
'name': 'Beacon Fragment',
'tech_type': 'BeaconFragment'},
35019: {'classification': ItemClassification.useful,
'count': 2,
'name': 'Bioreactor Fragment',
'tech_type': 'BaseBioReactorFragment'},
35020: {'classification': ItemClassification.progression,
'count': 3,
'name': 'Cyclops Bridge Fragment',
'tech_type': 'CyclopsBridgeFragment'},
35021: {'classification': ItemClassification.progression,
'count': 3,
'name': 'Cyclops Engine Fragment',
'tech_type': 'CyclopsEngineFragment'},
35022: {'classification': ItemClassification.progression,
'count': 3,
'name': 'Cyclops Hull Fragment',
'tech_type': 'CyclopsHullFragment'},
35023: {'classification': ItemClassification.filler,
'count': 2,
'name': 'Grav Trap Fragment',
'tech_type': 'GravSphereFragment'},
35024: {'classification': ItemClassification.progression,
'count': 3,
'name': 'Laser Cutter Fragment',
'tech_type': 'LaserCutterFragment'},
35025: {'classification': ItemClassification.filler,
'count': 1,
'name': 'Light Stick Fragment',
'tech_type': 'TechlightFragment'},
35026: {'classification': ItemClassification.progression,
'count': 3,
'name': 'Mobile Vehicle Bay Fragment',
'tech_type': 'ConstructorFragment'},
35027: {'classification': ItemClassification.progression,
'count': 3,
'name': 'Modification Station Fragment',
'tech_type': 'WorkbenchFragment'},
35028: {'classification': ItemClassification.progression,
'count': 2,
'name': 'Moonpool Fragment',
'tech_type': 'MoonpoolFragment'},
35029: {'classification': ItemClassification.useful,
'count': 3,
'name': 'Nuclear Reactor Fragment',
'tech_type': 'BaseNuclearReactorFragment'},
35030: {'classification': ItemClassification.useful,
'count': 2,
'name': 'Power Cell Charger Fragment',
'tech_type': 'PowerCellChargerFragment'},
35031: {'classification': ItemClassification.filler,
'count': 1,
'name': 'Power Transmitter Fragment',
'tech_type': 'PowerTransmitterFragment'},
35032: {'classification': ItemClassification.progression,
'count': 4,
'name': 'Prawn Suit Fragment',
'tech_type': 'ExosuitFragment'},
35033: {'classification': ItemClassification.useful,
'count': 2,
'name': 'Prawn Suit Drill Arm Fragment',
'tech_type': 'ExosuitDrillArmFragment'},
35034: {'classification': ItemClassification.useful,
'count': 2,
'name': 'Prawn Suit Grappling Arm Fragment',
'tech_type': 'ExosuitGrapplingArmFragment'},
35035: {'classification': ItemClassification.useful,
'count': 2,
'name': 'Prawn Suit Propulsion Cannon Fragment',
'tech_type': 'ExosuitPropulsionArmFragment'},
35036: {'classification': ItemClassification.useful,
'count': 2,
'name': 'Prawn Suit Torpedo Arm Fragment',
'tech_type': 'ExosuitTorpedoArmFragment'},
35037: {'classification': ItemClassification.useful,
'count': 3,
'name': 'Scanner Room Fragment',
'tech_type': 'BaseMapRoomFragment'},
35038: {'classification': ItemClassification.progression,
'count': 5,
'name': 'Seamoth Fragment',
'tech_type': 'SeamothFragment'},
35039: {'classification': ItemClassification.progression,
'count': 2,
'name': 'Stasis Rifle Fragment',
'tech_type': 'StasisRifleFragment'},
35040: {'classification': ItemClassification.useful,
'count': 2,
'name': 'Thermal Plant Fragment',
'tech_type': 'ThermalPlantFragment'},
35041: {'classification': ItemClassification.progression,
'count': 2,
'name': 'Seaglide Fragment',
'tech_type': 'SeaglideFragment'},
35042: {'classification': ItemClassification.progression,
'count': 1,
'name': 'Radiation Suit',
'tech_type': 'RadiationSuit'},
35043: {'classification': ItemClassification.progression,
'count': 2,
'name': 'Propulsion Cannon Fragment',
'tech_type': 'PropulsionCannonFragment'},
35044: {'classification': ItemClassification.progression,
'count': 1,
'name': 'Neptune Launch Platform',
'tech_type': 'RocketBase'},
35045: {'classification': ItemClassification.progression,
'count': 1,
'name': 'Ion Power Cell',
'tech_type': 'PrecursorIonPowerCell'},
35046: {'classification': ItemClassification.filler,
'count': 2,
'name': 'Exterior Growbed Fragment',
'tech_type': 'FarmingTrayFragment'},
35047: {'classification': ItemClassification.filler,
'count': 1,
'name': 'Picture Frame',
'tech_type': 'PictureFrameFragment'},
35048: {'classification': ItemClassification.filler,
'count': 2,
'name': 'Bench Fragment',
'tech_type': 'BenchFragment'},
35049: {'classification': ItemClassification.filler,
'count': 1,
'name': 'Basic Plant Pot',
'tech_type': 'PlanterPotFragment'},
35050: {'classification': ItemClassification.filler,
'count': 1,
'name': 'Interior Growbed',
'tech_type': 'PlanterBoxFragment'},
35051: {'classification': ItemClassification.filler,
'count': 1,
'name': 'Plant Shelf',
'tech_type': 'PlanterShelfFragment'},
35052: {'classification': ItemClassification.filler,
'count': 2,
'name': 'Observatory Fragment',
'tech_type': 'BaseObservatoryFragment'},
35053: {'classification': ItemClassification.filler,
'count': 2,
'name': 'Multipurpose Room Fragment',
'tech_type': 'BaseRoomFragment'},
35054: {'classification': ItemClassification.useful,
'count': 2,
'name': 'Bulkhead Fragment',
'tech_type': 'BaseBulkheadFragment'},
35055: {'classification': ItemClassification.filler,
'count': 1,
'name': 'Spotlight',
'tech_type': 'Spotlight'},
35056: {'classification': ItemClassification.filler,
'count': 2,
'name': 'Desk',
'tech_type': 'StarshipDesk'},
35057: {'classification': ItemClassification.filler,
'count': 1,
'name': 'Swivel Chair',
'tech_type': 'StarshipChair'},
35058: {'classification': ItemClassification.filler,
'count': 1,
'name': 'Office Chair',
'tech_type': 'StarshipChair2'},
35059: {'classification': ItemClassification.filler,
'count': 1,
'name': 'Command Chair',
'tech_type': 'StarshipChair3'},
35060: {'classification': ItemClassification.filler,
'count': 2,
'name': 'Counter',
'tech_type': 'LabCounter'},
35061: {'classification': ItemClassification.filler,
'count': 1,
'name': 'Single Bed',
'tech_type': 'NarrowBed'},
35062: {'classification': ItemClassification.filler,
'count': 1,
'name': 'Basic Double Bed',
'tech_type': 'Bed1'},
35063: {'classification': ItemClassification.filler,
'count': 1,
'name': 'Quilted Double Bed',
'tech_type': 'Bed2'},
35064: {'classification': ItemClassification.filler,
'count': 2,
'name': 'Coffee Vending Machine',
'tech_type': 'CoffeeVendingMachine'},
35065: {'classification': ItemClassification.filler,
'count': 2,
'name': 'Trash Can',
'tech_type': 'Trashcans'},
35066: {'classification': ItemClassification.filler,
'count': 1,
'name': 'Floodlight',
'tech_type': 'Techlight'},
35067: {'classification': ItemClassification.filler,
'count': 1,
'name': 'Bar Table',
'tech_type': 'BarTable'},
35068: {'classification': ItemClassification.filler,
'count': 1,
'name': 'Vending Machine',
'tech_type': 'VendingMachine'},
35069: {'classification': ItemClassification.filler,
'count': 1,
'name': 'Single Wall Shelf',
'tech_type': 'SingleWallShelf'},
35070: {'classification': ItemClassification.filler,
'count': 1,
'name': 'Wall Shelves',
'tech_type': 'WallShelves'},
35071: {'classification': ItemClassification.filler,
'count': 1,
'name': 'Round Plant Pot',
'tech_type': 'PlanterPot2'},
35072: {'classification': ItemClassification.filler,
'count': 1,
'name': 'Chic Plant Pot',
'tech_type': 'PlanterPot3'},
35073: {'classification': ItemClassification.filler,
'count': 1,
'name': 'Nuclear Waste Disposal',
'tech_type': 'LabTrashcan'},
35074: {'classification': ItemClassification.filler,
'count': 1,
'name': 'Wall Planter',
'tech_type': 'BasePlanter'},
35075: {'classification': ItemClassification.progression,
'count': 1,
'name': 'Ion Battery',
'tech_type': 'PrecursorIonBattery'},
35076: {'classification': ItemClassification.progression,
'count': 1,
'name': 'Neptune Gantry',
'tech_type': 'RocketBaseLadder'},
35077: {'classification': ItemClassification.progression,
'count': 1,
'name': 'Neptune Boosters',
'tech_type': 'RocketStage1'},
35078: {'classification': ItemClassification.progression,
'count': 1,
'name': 'Neptune Fuel Reserve',
'tech_type': 'RocketStage2'},
35079: {'classification': ItemClassification.progression,
'count': 1,
'name': 'Neptune Cockpit',
'tech_type': 'RocketStage3'},
35080: {'classification': ItemClassification.filler,
'count': 1,
'name': 'Water Filtration Machine',
'tech_type': 'BaseFiltrationMachine'}}
advancement_item_names: Set[str] = set()
non_advancement_item_names: Set[str] = set()
for item_id, item_data in item_table.items():
item_name = item_data["name"]
if ItemClassification.progression in item_data["classification"]:
advancement_item_names.add(item_name)
else:
non_advancement_item_names.add(item_name)
lookup_id_to_name[None] = "Victory"
if False: # turn to True to export for Subnautica mod
payload = {item_id: item_data["tech_type"] for item_id, item_data in item_table.items()}
import json
lookup_name_to_id = {name: id for id, name in lookup_id_to_name.items()}
with open("items.json", "w") as f:
json.dump(payload, f)

View File

@@ -1,12 +1,570 @@
import json
import os
from typing import Dict, TypedDict, List
with open(os.path.join(os.path.dirname(__file__), 'locations.json'), 'r') as file:
location_table = json.loads(file.read())
lookup_id_to_name = {}
for item in location_table:
lookup_id_to_name[item["id"]] = item["name"]
class Vector(TypedDict):
x: float
y: float
z: float
lookup_id_to_name[None] = "Neptune Launch"
lookup_name_to_id = {name: id for id, name in lookup_id_to_name.items()}
class LocationDict(TypedDict, total=False):
name: str
can_slip_through: bool
need_laser_cutter: bool
position: Vector
need_propulsion_cannon: bool
events: List[str] = ["Neptune Launch", "Disable Quarantine", "Full Infection", "Repair Aurora Drive"]
location_table: Dict[int, LocationDict] = {
33000: {'can_slip_through': False,
'name': 'Blood Kelp Trench Wreck - Outside Databox',
'need_laser_cutter': False,
'position': {'x': -1234.3, 'y': -349.7, 'z': -396.0}},
33001: {'can_slip_through': False,
'name': 'Blood Kelp Trench Wreck - Inside Databox',
'need_laser_cutter': False,
'position': {'x': -1208.0, 'y': -349.6, 'z': -383.0}},
33002: {'can_slip_through': False,
'name': 'Blood Kelp Trench Wreck - PDA',
'need_laser_cutter': False,
'position': {'x': -1210.6, 'y': -340.7, 'z': -393.4}},
33003: {'can_slip_through': False,
'name': 'Bulb Zone West Wreck - Outside Databox',
'need_laser_cutter': False,
'position': {'x': 903.8, 'y': -220.3, 'z': 590.9}},
33004: {'can_slip_through': False,
'name': 'Bulb Zone West Wreck - Under Databox',
'need_laser_cutter': False,
'position': {'x': 910.9, 'y': -201.8, 'z': 623.5}},
33005: {'can_slip_through': False,
'name': 'Bulb Zone West Wreck - Inside Databox',
'need_laser_cutter': True,
'position': {'x': 914.9, 'y': -202.1, 'z': 611.8}},
33006: {'can_slip_through': False,
'name': 'Bulb Zone West Wreck - PDA',
'need_laser_cutter': True,
'position': {'x': 912.6, 'y': -202.0, 'z': 609.5}},
33007: {'can_slip_through': False,
'name': 'Bulb Zone East Wreck - Databox',
'need_laser_cutter': False,
'position': {'x': 1327.1, 'y': -234.9, 'z': 575.8}},
33008: {'can_slip_through': False,
'name': 'Dunes North Wreck - Outside Databox',
'need_laser_cutter': False,
'position': {'x': -1407.7, 'y': -344.2, 'z': 721.5}},
33009: {'can_slip_through': False,
'name': 'Dunes North Wreck - Office Databox',
'need_laser_cutter': False,
'position': {'x': -1393.9, 'y': -329.7, 'z': 733.5}},
33010: {'can_slip_through': False,
'name': 'Dunes North Wreck - PDA',
'need_laser_cutter': False,
'position': {'x': -1396.3, 'y': -330.8, 'z': 730.0}},
33011: {'can_slip_through': False,
'name': 'Dunes North Wreck - Cargo Databox',
'need_laser_cutter': True,
'position': {'x': -1409.8, 'y': -332.4, 'z': 706.9}},
33012: {'can_slip_through': False,
'name': 'Dunes West Wreck - Databox',
'need_laser_cutter': False,
'position': {'x': -1626.2, 'y': -357.5, 'z': 99.5}},
33013: {'can_slip_through': False,
'name': 'Dunes East Wreck - Outside Databox',
'need_laser_cutter': False,
'position': {'x': -1196.3, 'y': -223.0, 'z': 12.5}},
33014: {'can_slip_through': False,
'name': 'Dunes East Wreck - Inside Databox',
'need_laser_cutter': False,
'position': {'x': -1206.4, 'y': -225.6, 'z': 4.0}},
33015: {'can_slip_through': False,
'name': 'Grand Reef North Wreck - Outside Databox',
'need_laser_cutter': False,
'position': {'x': -269.7, 'y': -262.8, 'z': -764.3}},
33016: {'can_slip_through': False,
'name': 'Grand Reef North Wreck - Elevator Databox',
'need_laser_cutter': True,
'position': {'x': -285.8, 'y': -240.2, 'z': -786.5}},
33017: {'can_slip_through': False,
'name': 'Grand Reef North Wreck - Bottom Databox',
'need_laser_cutter': False,
'position': {'x': -285.2, 'y': -262.4, 'z': -788.4}},
33018: {'can_slip_through': False,
'name': 'Grand Reef North Wreck - Hangar PDA',
'need_laser_cutter': False,
'position': {'x': -272.5, 'y': -254.7, 'z': -788.5}},
33019: {'can_slip_through': False,
'name': 'Grand Reef South Wreck - Trench Databox',
'need_laser_cutter': False,
'position': {'x': -850.9, 'y': -473.2, 'z': -1414.6}},
33020: {'can_slip_through': False,
'name': 'Grand Reef South Wreck - Comms Databox',
'need_laser_cutter': True,
'position': {'x': -889.4, 'y': -433.8, 'z': -1424.8}},
33021: {'can_slip_through': False,
'name': 'Grand Reef South Wreck - Outside Databox',
'need_laser_cutter': False,
'position': {'x': -862.4, 'y': -437.5, 'z': -1444.1}},
33022: {'can_slip_through': False,
'name': 'Grand Reef South Wreck - PDA',
'need_laser_cutter': False,
'position': {'x': -887.9, 'y': -446.0, 'z': -1422.7}},
33023: {'can_slip_through': False,
'name': 'Grassy Plateaus South Wreck - Databox',
'need_laser_cutter': False,
'position': {'x': -23.3, 'y': -105.8, 'z': -604.2}},
33024: {'can_slip_through': False,
'name': 'Grassy Plateaus South Wreck - PDA',
'need_laser_cutter': False,
'position': {'x': -27.3, 'y': -106.8, 'z': -607.2}},
33025: {'can_slip_through': True,
'name': 'Grassy Plateaus East Wreck - Breach Databox',
'need_laser_cutter': True,
'position': {'x': 313.9, 'y': -91.8, 'z': 432.6}},
33026: {'can_slip_through': True,
'name': 'Grassy Plateaus East Wreck - Hangar Databox',
'need_laser_cutter': True,
'position': {'x': 319.4, 'y': -104.3, 'z': 441.5}},
33027: {'can_slip_through': False,
'name': 'Grassy Plateaus West Wreck - Locker PDA',
'need_laser_cutter': False,
'position': {'x': -632.3, 'y': -75.0, 'z': -8.9}},
33028: {'can_slip_through': False,
'name': 'Grassy Plateaus West Wreck - Data Terminal',
'need_laser_cutter': False,
'position': {'x': -664.4, 'y': -97.8, 'z': -8.0}},
33029: {'can_slip_through': False,
'name': 'Grassy Plateaus West Wreck - Databox',
'need_laser_cutter': True,
'position': {'x': -421.4, 'y': -107.8, 'z': -266.5}},
33030: {'can_slip_through': False,
'name': 'Safe Shallows Wreck - PDA',
'need_laser_cutter': False,
'position': {'x': -44.0, 'y': -29.1, 'z': -403.6}},
33031: {'can_slip_through': False,
'name': 'Kelp Forest Wreck - Databox',
'need_laser_cutter': False,
'position': {'x': -317.6, 'y': -78.8, 'z': 247.4}},
33032: {'can_slip_through': False,
'name': 'Kelp Forest Wreck - PDA',
'need_laser_cutter': False,
'position': {'x': 63.2, 'y': -38.5, 'z': 382.9}},
33033: {'can_slip_through': False,
'name': 'Mountains West Wreck - Outside Databox',
'need_laser_cutter': False,
'position': {'x': 740.3, 'y': -389.2, 'z': 1179.8}},
33034: {'can_slip_through': False,
'name': 'Mountains West Wreck - Data Terminal',
'need_laser_cutter': True,
'position': {'x': 703.7, 'y': -365.9, 'z': 1199.3}},
33035: {'can_slip_through': False,
'name': 'Mountains West Wreck - Hangar Databox',
'need_laser_cutter': True,
'position': {'x': 698.2, 'y': -350.8, 'z': 1186.9}},
33036: {'can_slip_through': False,
'name': 'Mountains West Wreck - Office Databox',
'need_laser_cutter': False,
'position': {'x': 676.3, 'y': -343.6, 'z': 1204.6}},
33037: {'can_slip_through': False,
'name': 'Mountains East Wreck - Comms Databox',
'need_laser_cutter': False,
'position': {'x': 1068.5, 'y': -283.4, 'z': 1345.3}},
33038: {'can_slip_through': False,
'name': 'Mountains East Wreck - Outside Databox',
'need_laser_cutter': False,
'position': {'x': 1075.7, 'y': -288.9, 'z': 1321.8}},
33039: {'can_slip_through': False,
'name': 'Northwestern Mushroom Forest Wreck - Cargo Databox',
'need_laser_cutter': True,
'position': {'x': -655.1, 'y': -109.6, 'z': 791.0}},
33040: {'can_slip_through': False,
'name': 'Northwestern Mushroom Forest Wreck - Office Databox',
'need_laser_cutter': False,
'position': {'x': -663.4, 'y': -111.9, 'z': 777.9}},
33041: {'can_slip_through': False,
'name': 'Northwestern Mushroom Forest Wreck - PDA',
'need_laser_cutter': False,
'position': {'x': -662.2, 'y': -113.4, 'z': 777.7}},
33042: {'can_slip_through': False,
'name': "Sea Treader's Path Wreck - Outside Databox",
'need_laser_cutter': False,
'position': {'x': -1161.1, 'y': -191.7, 'z': -758.3}},
33043: {'can_slip_through': False,
'name': "Sea Treader's Path Wreck - Hangar Databox",
'need_laser_cutter': True,
'position': {'x': -1129.5, 'y': -155.2, 'z': -729.3}},
33044: {'can_slip_through': False,
'name': "Sea Treader's Path Wreck - Lobby Databox",
'need_laser_cutter': False,
'position': {'x': -1115.9, 'y': -175.3, 'z': -724.5}},
33045: {'can_slip_through': False,
'name': "Sea Treader's Path Wreck - PDA",
'need_laser_cutter': False,
'position': {'x': -1136.8, 'y': -157.0, 'z': -734.6}},
33046: {'can_slip_through': False,
'name': 'Sparse Reef Wreck - Locker Databox',
'need_laser_cutter': True,
'position': {'x': -789.8, 'y': -216.1, 'z': -711.0}},
33047: {'can_slip_through': False,
'name': 'Sparse Reef Wreck - Outside Databox',
'need_laser_cutter': False,
'position': {'x': -810.7, 'y': -209.3, 'z': -685.5}},
33048: {'can_slip_through': False,
'name': 'Sparse Reef Wreck - Lab Databox',
'need_laser_cutter': True,
'position': {'x': -795.5, 'y': -204.1, 'z': -774.7}},
33049: {'can_slip_through': False,
'name': 'Underwater Islands Wreck - Outside Databox',
'need_laser_cutter': False,
'position': {'x': -170.8, 'y': -187.6, 'z': 880.7}},
33050: {'can_slip_through': False,
'name': 'Underwater Islands Wreck - Hangar Databox',
'need_laser_cutter': True,
'position': {'x': -138.4, 'y': -193.6, 'z': 888.7}},
33051: {'can_slip_through': False,
'name': 'Underwater Islands Wreck - Data Terminal',
'need_laser_cutter': True,
'position': {'x': -130.7, 'y': -193.2, 'z': 883.3}},
33052: {'can_slip_through': False,
'name': 'Underwater Islands Wreck - Cable Databox',
'need_laser_cutter': False,
'position': {'x': -137.8, 'y': -193.4, 'z': 879.4}},
33053: {'can_slip_through': False,
'name': 'Underwater Islands Wreck - Pipes Databox 1',
'need_laser_cutter': False,
'need_propulsion_cannon': True,
'position': {'x': -124.4, 'y': -200.7, 'z': 853.0}},
33054: {'can_slip_through': False,
'name': 'Underwater Islands Wreck - Pipes Databox 2',
'need_laser_cutter': False,
'need_propulsion_cannon': True,
'position': {'x': -126.8, 'y': -201.1, 'z': 852.1}},
33055: {'can_slip_through': False,
'name': 'Degasi Seabase - Deep Grand Reef - Bedroom Databox',
'need_laser_cutter': False,
'position': {'x': -643.8, 'y': -509.9, 'z': -941.9}},
33056: {'can_slip_through': False,
'name': 'Degasi Seabase - Deep Grand Reef - Observatory Databox',
'need_laser_cutter': False,
'position': {'x': -635.1, 'y': -502.7, 'z': -951.4}},
33057: {'can_slip_through': False,
'name': 'Degasi Seabase - Deep Grand Reef - Bedroom PDA',
'need_laser_cutter': False,
'position': {'x': -645.8, 'y': -508.7, 'z': -943.0}},
33058: {'can_slip_through': False,
'name': 'Degasi Seabase - Deep Grand Reef - Outside PDA',
'need_laser_cutter': False,
'position': {'x': -630.5, 'y': -511.1, 'z': -936.1}},
33059: {'can_slip_through': False,
'name': 'Degasi Seabase - Deep Grand Reef - Observatory PDA',
'need_laser_cutter': False,
'position': {'x': -647.7, 'y': -502.6, 'z': -935.8}},
33060: {'can_slip_through': False,
'name': 'Degasi Seabase - Deep Grand Reef - Lab PDA',
'need_laser_cutter': False,
'position': {'x': -639.6, 'y': -505.9, 'z': -946.6}},
33061: {'can_slip_through': False,
'name': 'Floating Island - Lake PDA',
'need_laser_cutter': False,
'position': {'x': -707.2, 'y': 0.5, 'z': -1096.7}},
33062: {'can_slip_through': False,
'name': 'Degasi Seabase - Floating Island - Databox',
'need_laser_cutter': False,
'position': {'x': -765.7, 'y': 17.6, 'z': -1116.4}},
33063: {'can_slip_through': False,
'name': 'Degasi Seabase - Floating Island - Room PDA',
'need_laser_cutter': False,
'position': {'x': -754.9, 'y': 14.6, 'z': -1108.9}},
33064: {'can_slip_through': False,
'name': 'Degasi Seabase - Floating Island - Green Wall PDA',
'need_laser_cutter': False,
'position': {'x': -765.3, 'y': 14.1, 'z': -1115.0}},
33065: {'can_slip_through': False,
'name': 'Degasi Seabase - Floating Island - Corridor PDA',
'need_laser_cutter': False,
'position': {'x': -758.6, 'y': 14.1, 'z': -1111.3}},
33066: {'can_slip_through': False,
'name': 'Degasi Seabase - Floating Island - North Observatory PDA',
'need_laser_cutter': False,
'position': {'x': -805.4, 'y': 76.9, 'z': -1055.7}},
33067: {'can_slip_through': False,
'name': 'Degasi Seabase - Floating Island - South Observatory PDA',
'need_laser_cutter': False,
'position': {'x': -715.9, 'y': 75.4, 'z': -1168.8}},
33068: {'can_slip_through': False,
'name': 'Jellyshroom Cave - PDA',
'need_laser_cutter': False,
'position': {'x': -540.5, 'y': -250.8, 'z': -83.4}},
33069: {'can_slip_through': False,
'name': 'Degasi Seabase - Jellyshroom Cave - Bedroom Databox',
'need_laser_cutter': False,
'position': {'x': 110.6, 'y': -264.9, 'z': -369.0}},
33070: {'can_slip_through': False,
'name': 'Degasi Seabase - Jellyshroom Cave - Detached PDA',
'need_laser_cutter': False,
'position': {'x': 80.6, 'y': -268.6, 'z': -358.3}},
33071: {'can_slip_through': False,
'name': 'Degasi Seabase - Jellyshroom Cave - Office PDA',
'need_laser_cutter': False,
'position': {'x': 78.2, 'y': -265.0, 'z': -373.4}},
33072: {'can_slip_through': False,
'name': 'Degasi Seabase - Jellyshroom Cave - Locker PDA',
'need_laser_cutter': False,
'position': {'x': 85.1, 'y': -264.1, 'z': -372.8}},
33073: {'can_slip_through': False,
'name': 'Degasi Seabase - Jellyshroom Cave - Bedroom PDA',
'need_laser_cutter': False,
'position': {'x': 112.3, 'y': -264.9, 'z': -369.3}},
33074: {'can_slip_through': False,
'name': 'Degasi Seabase - Jellyshroom Cave - Observatory PDA',
'need_laser_cutter': False,
'position': {'x': 95.5, 'y': -258.9, 'z': -366.5}},
33075: {'can_slip_through': False,
'name': 'Lifepod 2 - Databox',
'need_laser_cutter': False,
'position': {'x': -483.6, 'y': -504.7, 'z': 1326.6}},
33076: {'can_slip_through': False,
'name': 'Lifepod 2 - PDA',
'need_laser_cutter': False,
'position': {'x': -481.4, 'y': -503.6, 'z': 1324.1}},
33077: {'can_slip_through': False,
'name': 'Lifepod 3 - Databox',
'need_laser_cutter': False,
'position': {'x': -34.2, 'y': -22.4, 'z': 410.5}},
33078: {'can_slip_through': False,
'name': 'Lifepod 3 - PDA',
'need_laser_cutter': False,
'position': {'x': -33.8, 'y': -22.5, 'z': 408.8}},
33079: {'can_slip_through': False,
'name': 'Lifepod 4 - Databox',
'need_laser_cutter': False,
'position': {'x': 712.4, 'y': -3.4, 'z': 160.8}},
33080: {'can_slip_through': False,
'name': 'Lifepod 4 - PDA',
'need_laser_cutter': False,
'position': {'x': 712.0, 'y': -3.5, 'z': 161.5}},
33081: {'can_slip_through': False,
'name': 'Lifepod 6 - Databox',
'need_laser_cutter': False,
'position': {'x': 358.7, 'y': -117.1, 'z': 306.8}},
33082: {'can_slip_through': False,
'name': 'Lifepod 6 - Inside PDA',
'need_laser_cutter': False,
'position': {'x': 361.8, 'y': -116.2, 'z': 309.5}},
33083: {'can_slip_through': False,
'name': 'Lifepod 6 - Outside PDA',
'need_laser_cutter': False,
'position': {'x': 359.9, 'y': -117.0, 'z': 312.1}},
33084: {'can_slip_through': False,
'name': 'Lifepod 7 - PDA',
'need_laser_cutter': False,
'position': {'x': -56.0, 'y': -182.0, 'z': -1039.0}},
33085: {'can_slip_through': False,
'name': 'Lifepod 12 - Databox',
'need_laser_cutter': False,
'position': {'x': 1119.5, 'y': -271.7, 'z': 561.7}},
33086: {'can_slip_through': False,
'name': 'Lifepod 12 - PDA',
'need_laser_cutter': False,
'position': {'x': 1116.1, 'y': -271.3, 'z': 566.9}},
33087: {'can_slip_through': False,
'name': 'Lifepod 13 - Databox',
'need_laser_cutter': False,
'position': {'x': -926.4, 'y': -185.2, 'z': 501.8}},
33088: {'can_slip_through': False,
'name': 'Lifepod 13 - PDA',
'need_laser_cutter': False,
'position': {'x': -926.8, 'y': -184.4, 'z': 506.6}},
33089: {'can_slip_through': False,
'name': 'Lifepod 17 - PDA',
'need_laser_cutter': False,
'position': {'x': -514.5, 'y': -98.1, 'z': -56.5}},
33090: {'can_slip_through': False,
'name': 'Lifepod 19 - Databox',
'need_laser_cutter': False,
'position': {'x': -809.8, 'y': -302.2, 'z': -876.9}},
33091: {'can_slip_through': False,
'name': 'Lifepod 19 - Outside PDA',
'need_laser_cutter': False,
'position': {'x': -806.1, 'y': -294.1, 'z': -866.0}},
33092: {'can_slip_through': False,
'name': 'Lifepod 19 - Inside PDA',
'need_laser_cutter': False,
'position': {'x': -810.5, 'y': -299.4, 'z': -873.1}},
33093: {'can_slip_through': False,
'name': 'Aurora Seamoth Bay - Upgrade Console',
'need_laser_cutter': False,
'need_propulsion_cannon': True,
'position': {'x': 903.5, 'y': -0.2, 'z': 16.1}},
33094: {'can_slip_through': False,
'name': 'Aurora Drive Room - Upgrade Console',
'need_laser_cutter': False,
'need_propulsion_cannon': True,
'position': {'x': 872.5, 'y': 2.7, 'z': -0.7}},
33095: {'can_slip_through': False,
'name': 'Aurora Prawn Suit Bay - Upgrade Console',
'need_laser_cutter': True,
'need_propulsion_cannon': True,
'position': {'x': 991.6, 'y': 3.2, 'z': -31.0}},
33096: {'can_slip_through': False,
'name': 'Aurora - Office PDA',
'need_laser_cutter': False,
'position': {'x': 952.1, 'y': 41.2, 'z': 113.9}},
33097: {'can_slip_through': False,
'name': 'Aurora - Corridor PDA',
'need_laser_cutter': False,
'position': {'x': 977.2, 'y': 39.1, 'z': 83.0}},
33098: {'can_slip_through': False,
'name': 'Aurora - Cargo Bay PDA',
'need_laser_cutter': False,
'need_propulsion_cannon': True,
'position': {'x': 954.9, 'y': 11.2, 'z': 3.4}},
33099: {'can_slip_through': False,
'name': 'Aurora - Seamoth Bay PDA',
'need_laser_cutter': False,
'need_propulsion_cannon': True,
'position': {'x': 907.1, 'y': -1.5, 'z': 15.3}},
33100: {'can_slip_through': False,
'name': 'Aurora - Medkit Locker PDA',
'need_laser_cutter': True,
'need_propulsion_cannon': True,
'position': {'x': 951.8, 'y': -2.3, 'z': -34.7}},
33101: {'can_slip_through': False,
'name': 'Aurora - Locker PDA',
'need_laser_cutter': True,
'need_propulsion_cannon': True,
'position': {'x': 952.0, 'y': -3.7, 'z': -23.4}},
33102: {'can_slip_through': False,
'name': 'Aurora - Canteen PDA',
'need_laser_cutter': True,
'need_propulsion_cannon': True,
'position': {'x': 986.5, 'y': 9.6, 'z': -48.6}},
33103: {'can_slip_through': False,
'name': 'Aurora - Cabin 4 PDA',
'need_laser_cutter': True,
'need_propulsion_cannon': True,
'position': {'x': 951.3, 'y': 11.2, 'z': -51.0}},
33104: {'can_slip_through': False,
'name': 'Aurora - Cabin 7 PDA',
'need_laser_cutter': True,
'need_propulsion_cannon': True,
'position': {'x': 967.1, 'y': 10.4, 'z': -47.4}},
33105: {'can_slip_through': False,
'name': 'Aurora - Cabin 1 PDA',
'need_laser_cutter': True,
'need_propulsion_cannon': True,
'position': {'x': 964.1, 'y': 11.1, 'z': -61.9}},
33106: {'can_slip_through': False,
'name': 'Aurora - Captain PDA',
'need_laser_cutter': True,
'need_propulsion_cannon': True,
'position': {'x': 971.2, 'y': 10.8, 'z': -70.4}},
33107: {'can_slip_through': False,
'name': 'Aurora - Ring PDA',
'need_laser_cutter': False,
'need_propulsion_cannon': True,
'position': {'x': 1033.6, 'y': -8.5, 'z': 16.2}},
33108: {'can_slip_through': False,
'name': 'Aurora - Lab PDA',
'need_laser_cutter': False,
'need_propulsion_cannon': True,
'position': {'x': 1032.5, 'y': -7.8, 'z': 32.4}},
33109: {'can_slip_through': False,
'name': 'Aurora - Office Data Terminal',
'need_laser_cutter': False,
'position': {'x': 945.8, 'y': 40.8, 'z': 115.1}},
33110: {'can_slip_through': False,
'name': 'Aurora - Captain Data Terminal',
'need_laser_cutter': True,
'need_propulsion_cannon': True,
'position': {'x': 974.8, 'y': 10.0, 'z': -77.0}},
33111: {'can_slip_through': False,
'name': 'Aurora - Battery Room Data Terminal',
'need_laser_cutter': True,
'need_propulsion_cannon': True,
'position': {'x': 1040.8, 'y': -11.4, 'z': -3.4}},
33112: {'can_slip_through': False,
'name': 'Aurora - Lab Data Terminal',
'need_laser_cutter': False,
'need_propulsion_cannon': True,
'position': {'x': 1029.5, 'y': -8.7, 'z': 35.9}},
33113: {'can_slip_through': False,
'name': "Quarantine Enforcement Platform's - Upper Alien Data "
'Terminal',
'need_laser_cutter': False,
'position': {'x': 432.2, 'y': 3.0, 'z': 1193.2}},
33114: {'can_slip_through': False,
'name': "Quarantine Enforcement Platform's - Mid Alien Data Terminal",
'need_laser_cutter': False,
'position': {'x': 474.4, 'y': -4.5, 'z': 1224.4}},
33115: {'can_slip_through': False,
'name': 'Dunes Sanctuary - Alien Data Terminal',
'need_laser_cutter': False,
'position': {'x': -1224.2, 'y': -400.4, 'z': 1057.9}},
33116: {'can_slip_through': False,
'name': 'Deep Sparse Reef Sanctuary - Alien Data Terminal',
'need_laser_cutter': False,
'position': {'x': -895.5, 'y': -311.6, 'z': -838.1}},
33117: {'can_slip_through': False,
'name': 'Northern Blood Kelp Zone Sanctuary - Alien Data Terminal',
'need_laser_cutter': False,
'position': {'x': -642.9, 'y': -563.5, 'z': 1485.5}},
33118: {'can_slip_through': False,
'name': 'Lost River Laboratory Cache - Alien Data Terminal',
'need_laser_cutter': False,
'position': {'x': -1112.3, 'y': -687.3, 'z': -695.5}},
33119: {'can_slip_through': False,
'name': 'Disease Research Facility - Upper Alien Data Terminal',
'need_laser_cutter': False,
'position': {'x': -280.2, 'y': -804.3, 'z': 305.1}},
33120: {'can_slip_through': False,
'name': 'Disease Research Facility - Mid Alien Data Terminal',
'need_laser_cutter': False,
'position': {'x': -267.9, 'y': -806.6, 'z': 250.0}},
33121: {'can_slip_through': False,
'name': 'Disease Research Facility - Lower Alien Data Terminal',
'need_laser_cutter': False,
'position': {'x': -286.2, 'y': -815.6, 'z': 297.8}},
33122: {'can_slip_through': False,
'name': 'Alien Thermal Plant - Entrance Alien Data Terminal',
'need_laser_cutter': False,
'position': {'x': -71.3, 'y': -1227.2, 'z': 104.8}},
33123: {'can_slip_through': False,
'name': 'Alien Thermal Plant - Green Alien Data Terminal',
'need_laser_cutter': False,
'position': {'x': -38.7, 'y': -1226.6, 'z': 111.8}},
33124: {'can_slip_through': False,
'name': 'Alien Thermal Plant - Yellow Alien Data Terminal',
'need_laser_cutter': False,
'position': {'x': -30.4, 'y': -1220.3, 'z': 111.8}},
33125: {'can_slip_through': False,
'name': "Primary Containment Facility's Antechamber - Alien Data "
'Terminal',
'need_laser_cutter': False,
'position': {'x': 245.8, 'y': -1430.6, 'z': -311.5}},
33126: {'can_slip_through': False,
'name': "Primary Containment Facility's Egg Laboratory - Alien Data "
'Terminal',
'need_laser_cutter': False,
'position': {'x': 165.5, 'y': -1442.4, 'z': -385.8}},
33127: {'can_slip_through': False,
'name': "Primary Containment Facility's Pipe Room - Alien Data "
'Terminal',
'need_laser_cutter': False,
'position': {'x': 348.7, 'y': -1443.5, 'z': -291.9}},
33128: {'can_slip_through': False,
'name': 'Grassy Plateaus West Wreck - Beam PDA',
'need_laser_cutter': True,
'position': {'x': -641.8, 'y': -111.3, 'z': -19.7}},
33129: {'can_slip_through': False,
'name': 'Floating Island - Cave Entrance PDA',
'need_laser_cutter': False,
'position': {'x': -748.9, 'y': 14.4, 'z': -1179.5}}}
if False: # turn to True to export for Subnautica mod
payload = {location_id: location_data["position"] for location_id, location_data in location_table.items()}
import json
with open("locations.json", "w") as f:
json.dump(payload, f)

View File

@@ -1,14 +1,46 @@
from Options import Choice
from Options import Choice, Range
from .Creatures import all_creatures
class ItemPool(Choice):
"""Valuable item pool moves all not progression relevant items to starting inventory and
creates random duplicates of important items in their place."""
"""Valuable item pool leaves all filler items in their vanilla locations and
creates random duplicates of important items into freed spots."""
display_name = "Item Pool"
option_standard = 0
option_valuable = 1
class Goal(Choice):
"""Goal to complete.
Launch: Leave the planet.
Free: Disable quarantine.
Infected: Reach maximum infection level.
Drive: Repair the Aurora's Drive Core"""
auto_display_name = True
display_name = "Goal"
option_launch = 0
option_free = 1
option_infected = 2
option_drive = 3
def get_event_name(self) -> str:
return {
self.option_launch: "Neptune Launch",
self.option_infected: "Full Infection",
self.option_free: "Disable Quarantine",
self.option_drive: "Repair Aurora Drive"
}[self.value]
class CreatureScans(Range):
"""Place items on specific creature scans.
Warning: Includes aggressive Leviathans."""
display_name = "Creature Scans"
range_end = len(all_creatures)
options = {
"item_pool": ItemPool
"item_pool": ItemPool,
"goal": Goal,
"creature_scans": CreatureScans
}

View File

@@ -1,8 +0,0 @@
def create_regions(world, player: int):
from . import create_region
from .Locations import lookup_name_to_id as location_lookup_name_to_id
world.regions += [
create_region(world, player, 'Menu', None, ['Lifepod 5']),
create_region(world, player, 'Planet 4546B', [location for location in location_lookup_name_to_id])
]

View File

@@ -1,113 +1,122 @@
from ..generic.Rules import set_rule
from .Locations import location_table
import logging
from typing import TYPE_CHECKING
from worlds.generic.Rules import set_rule
from .Locations import location_table, LocationDict
from .Creatures import all_creatures, aggressive, suffix
import math
if TYPE_CHECKING:
from . import SubnauticaWorld
def has_seaglide(state, player):
def has_seaglide(state, player: int):
return state.has("Seaglide Fragment", player, 2)
def has_modification_station(state, player):
def has_modification_station(state, player: int):
return state.has("Modification Station Fragment", player, 3)
def has_mobile_vehicle_bay(state, player):
def has_mobile_vehicle_bay(state, player: int):
return state.has("Mobile Vehicle Bay Fragment", player, 3)
def has_moonpool(state, player):
def has_moonpool(state, player: int):
return state.has("Moonpool Fragment", player, 2)
def has_vehicle_upgrade_console(state, player):
def has_vehicle_upgrade_console(state, player: int):
return state.has("Vehicle Upgrade Console", player) and \
has_moonpool(state, player)
def has_seamoth(state, player):
def has_seamoth(state, player: int):
return state.has("Seamoth Fragment", player, 3) and \
has_mobile_vehicle_bay(state, player)
def has_seamoth_depth_module_mk1(state, player):
def has_seamoth_depth_module_mk1(state, player: int):
return has_vehicle_upgrade_console(state, player)
def has_seamoth_depth_module_mk2(state, player):
def has_seamoth_depth_module_mk2(state, player: int):
return has_seamoth_depth_module_mk1(state, player) and \
has_modification_station(state, player)
def has_seamoth_depth_module_mk3(state, player):
def has_seamoth_depth_module_mk3(state, player: int):
return has_seamoth_depth_module_mk2(state, player) and \
has_modification_station(state, player)
def has_cyclops_bridge(state, player):
def has_cyclops_bridge(state, player: int):
return state.has("Cyclops Bridge Fragment", player, 3)
def has_cyclops_engine(state, player):
def has_cyclops_engine(state, player: int):
return state.has("Cyclops Engine Fragment", player, 3)
def has_cyclops_hull(state, player):
def has_cyclops_hull(state, player: int):
return state.has("Cyclops Hull Fragment", player, 3)
def has_cyclops(state, player):
def has_cyclops(state, player: int):
return has_cyclops_bridge(state, player) and \
has_cyclops_engine(state, player) and \
has_cyclops_hull(state, player) and \
has_mobile_vehicle_bay(state, player)
def has_cyclops_depth_module_mk1(state, player):
def has_cyclops_depth_module_mk1(state, player: int):
return state.has("Cyclops Depth Module MK1", player) and \
has_modification_station(state, player)
def has_cyclops_depth_module_mk2(state, player):
def has_cyclops_depth_module_mk2(state, player: int):
return has_cyclops_depth_module_mk1(state, player) and \
has_modification_station(state, player)
def has_cyclops_depth_module_mk3(state, player):
def has_cyclops_depth_module_mk3(state, player: int):
return has_cyclops_depth_module_mk2(state, player) and \
has_modification_station(state, player)
def has_prawn(state, player):
def has_prawn(state, player: int):
return state.has("Prawn Suit Fragment", player, 4) and \
has_mobile_vehicle_bay(state, player)
def has_praw_propulsion_arm(state, player):
def has_praw_propulsion_arm(state, player: int):
return state.has("Prawn Suit Propulsion Cannon Fragment", player, 2) and \
has_vehicle_upgrade_console(state, player)
def has_prawn_depth_module_mk1(state, player):
def has_prawn_depth_module_mk1(state, player: int):
return has_vehicle_upgrade_console(state, player)
def has_prawn_depth_module_mk2(state, player):
def has_prawn_depth_module_mk2(state, player: int):
return has_prawn_depth_module_mk1(state, player) and \
has_modification_station(state, player)
def has_laser_cutter(state, player):
def has_laser_cutter(state, player: int):
return state.has("Laser Cutter Fragment", player, 3)
def has_stasis_rile(state, player: int):
return state.has("Stasis Rifle Fragment", player, 2)
# Either we have propulsion cannon, or prawn + propulsion cannon arm
def has_propulsion_cannon(state, player):
def has_propulsion_cannon(state, player: int):
return state.has("Propulsion Cannon Fragment", player, 2) or \
(has_prawn(state, player) and has_praw_propulsion_arm(state, player))
def has_cyclops_shield(state, player):
def has_cyclops_shield(state, player: int):
return has_cyclops(state, player) and \
state.has("Cyclops Shield Generator", player)
@@ -120,7 +129,7 @@ def has_cyclops_shield(state, player):
# negligeable with from high capacity tank. 430m -> 460m
# Fins are not used when using seaglide
#
def get_max_swim_depth(state, player):
def get_max_swim_depth(state, player: int):
# TODO, Make this a difficulty setting.
# Only go up to 200m without any submarines for now.
return 200
@@ -131,7 +140,7 @@ def get_max_swim_depth(state, player):
# has_ultra_glide_fins = state.has("Ultra Glide Fins", player)
# max_depth = 400 # More like 430m. Give some room
# if has_seaglide(state, player):
# if has_seaglide(state, player: int):
# if has_ultra_high_capacity_tank:
# max_depth = 750 # It's about 50m more. Give some room
# else:
@@ -147,7 +156,7 @@ def get_max_swim_depth(state, player):
# return max_depth
def get_seamoth_max_depth(state, player):
def get_seamoth_max_depth(state, player: int):
if has_seamoth(state, player):
if has_seamoth_depth_module_mk3(state, player):
return 900
@@ -187,7 +196,7 @@ def get_prawn_max_depth(state, player):
return 0
def get_max_depth(state, player):
def get_max_depth(state, player: int):
# TODO, Difficulty option, we can add vehicle depth + swim depth
# But at this point, we have to consider traver distance in caves, not
# just depth
@@ -197,54 +206,82 @@ def get_max_depth(state, player):
get_prawn_max_depth(state, player))
def can_access_location(state, player, loc):
pos_x = loc.get("position").get("x")
pos_y = loc.get("position").get("y")
pos_z = loc.get("position").get("z")
depth = -pos_y # y-up
map_center_dist = math.sqrt(pos_x ** 2 + pos_z ** 2)
aurora_dist = math.sqrt((pos_x - 1038.0) ** 2 + (pos_y - -3.4) ** 2 + (pos_z - -163.1) ** 2)
need_radiation_suit = aurora_dist < 950
def can_access_location(state, player: int, loc: LocationDict) -> bool:
need_laser_cutter = loc.get("need_laser_cutter", False)
need_propulsion_cannon = loc.get("need_propulsion_cannon", False)
if need_laser_cutter and not has_laser_cutter(state, player):
return False
if need_radiation_suit and not state.has("Radiation Suit", player):
need_propulsion_cannon = loc.get("need_propulsion_cannon", False)
if need_propulsion_cannon and not has_propulsion_cannon(state, player):
return False
if need_propulsion_cannon and not has_propulsion_cannon(state, player):
pos = loc["position"]
pos_x = pos["x"]
pos_y = pos["y"]
pos_z = pos["z"]
aurora_dist = math.sqrt((pos_x - 1038.0) ** 2 + (pos_y - -3.4) ** 2 + (pos_z - -163.1) ** 2)
need_radiation_suit = aurora_dist < 950
if need_radiation_suit and not state.has("Radiation Suit", player):
return False
# Seaglide doesn't unlock anything specific, but just allows for faster movement.
# Otherwise the game is painfully slow.
map_center_dist = math.sqrt(pos_x ** 2 + pos_z ** 2)
if (map_center_dist > 800 or pos_y < -200) and not has_seaglide(state, player):
return False
depth = -pos_y # y-up
return get_max_depth(state, player) >= depth
def set_location_rule(world, player, loc):
def set_location_rule(world, player: int, loc: LocationDict):
set_rule(world.get_location(loc["name"], player), lambda state: can_access_location(state, player, loc))
def set_rules(world, player):
for loc in location_table:
def can_scan_creature(state, player: int, creature: str) -> bool:
if not has_seaglide(state, player):
return False
if creature in aggressive and not has_stasis_rile(state, player):
return False
return get_max_depth(state, player) >= all_creatures[creature]
def set_creature_rule(world, player, creature_name: str):
set_rule(world.get_location(creature_name + suffix, player),
lambda state: can_scan_creature(state, player, creature_name))
def set_rules(subnautica_world: "SubnauticaWorld"):
player = subnautica_world.player
world = subnautica_world.world
for loc in location_table.values():
set_location_rule(world, player, loc)
# Victory location
set_rule(world.get_location("Neptune Launch", player), lambda state: \
get_max_depth(state, player) >= 1444 and \
has_mobile_vehicle_bay(state, player) and \
state.has('Neptune Launch Platform', player) and \
state.has('Neptune Gantry', player) and \
state.has('Neptune Boosters', player) and \
state.has('Neptune Fuel Reserve', player) and \
state.has('Neptune Cockpit', player) and \
state.has('Ion Power Cell', player) and \
state.has('Ion Battery', player) and \
for creature_name in subnautica_world.creatures_to_scan:
set_creature_rule(world, player, creature_name)
# Victory locations
set_rule(world.get_location("Neptune Launch", player), lambda state:
get_max_depth(state, player) >= 1444 and
has_mobile_vehicle_bay(state, player) and
state.has("Neptune Launch Platform", player) and
state.has("Neptune Gantry", player) and
state.has("Neptune Boosters", player) and
state.has("Neptune Fuel Reserve", player) and
state.has("Neptune Cockpit", player) and
state.has("Ion Power Cell", player) and
state.has("Ion Battery", player) and
has_cyclops_shield(state, player))
world.completion_condition[player] = lambda state: state.has('Victory', player)
set_rule(world.get_location("Disable Quarantine", player), lambda state:
get_max_depth(state, player) >= 1444)
set_rule(world.get_location("Full Infection", player), lambda state:
get_max_depth(state, player) >= 900)
room = world.get_location("Aurora Drive Room - Upgrade Console", player)
set_rule(world.get_location("Repair Aurora Drive", player), lambda state: room.can_reach(state))
world.completion_condition[player] = lambda state: state.has("Victory", player)

View File

@@ -1,18 +1,17 @@
import logging
from typing import List, Dict, Any
from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification, RegionType
from worlds.AutoWorld import World, WebWorld
from . import Items
from . import Locations
from . import Creatures
from . import Options
from .Items import item_table
from .Rules import set_rules
logger = logging.getLogger("Subnautica")
from .Locations import lookup_name_to_id as locations_lookup_name_to_id
from .Items import item_table, lookup_name_to_item, advancement_item_names
from .Items import lookup_name_to_id as items_lookup_name_to_id
from .Regions import create_regions
from .Rules import set_rules
from .Options import options
from BaseClasses import Region, Entrance, Location, MultiWorld, Item, Tutorial, ItemClassification, RegionType
from ..AutoWorld import World, WebWorld
class SubnaticaWeb(WebWorld):
tutorials = [Tutorial(
@@ -25,6 +24,10 @@ class SubnaticaWeb(WebWorld):
)]
all_locations = {data["name"]: loc_id for loc_id, data in Locations.location_table.items()}
all_locations.update(Creatures.creature_locations)
class SubnauticaWorld(World):
"""
Subnautica is an undersea exploration game. Stranded on an alien world, you become infected by
@@ -34,34 +37,56 @@ class SubnauticaWorld(World):
game: str = "Subnautica"
web = SubnaticaWeb()
item_name_to_id = items_lookup_name_to_id
location_name_to_id = locations_lookup_name_to_id
options = options
item_name_to_id = {data["name"]: item_id for item_id, data in Items.item_table.items()}
location_name_to_id = all_locations
options = Options.options
data_version = 2
required_client_version = (0, 1, 9)
data_version = 3
required_client_version = (0, 3, 3)
prefill_items: List[Item]
creatures_to_scan: List[str]
def generate_early(self) -> None:
self.prefill_items = [
self.create_item("Seaglide Fragment"),
self.create_item("Seaglide Fragment")
]
self.creatures_to_scan = self.world.random.sample(Creatures.all_creatures_presorted,
self.world.creature_scans[self.player].value)
def create_regions(self):
self.world.regions += [
self.create_region("Menu", None, ["Lifepod 5"]),
self.create_region("Planet 4546B",
Locations.events +
[location["name"] for location in Locations.location_table.values()] +
[creature+Creatures.suffix for creature in self.creatures_to_scan])
]
# refer to Rules.py
set_rules = set_rules
def generate_basic(self):
# Link regions
self.world.get_entrance('Lifepod 5', self.player).connect(self.world.get_region('Planet 4546B', self.player))
self.world.get_entrance("Lifepod 5", self.player).connect(self.world.get_region("Planet 4546B", self.player))
# Generate item pool
pool = []
neptune_launch_platform = None
extras = 0
valuable = self.world.item_pool[self.player] == "valuable"
for item in item_table:
extras = self.world.creature_scans[self.player].value
valuable = self.world.item_pool[self.player] == Options.ItemPool.option_valuable
for item in item_table.values():
for i in range(item["count"]):
subnautica_item = self.create_item(item["name"])
if item["name"] == "Neptune Launch Platform":
neptune_launch_platform = subnautica_item
elif valuable and not item["progression"]:
self.world.push_precollected(subnautica_item)
elif valuable and ItemClassification.filler == item["classification"]:
extras += 1
else:
pool.append(subnautica_item)
for item_name in self.world.random.choices(sorted(advancement_item_names - {"Neptune Launch Platform"}),
for item_name in self.world.random.choices(sorted(Items.advancement_item_names - {"Neptune Launch Platform"}),
k=extras):
item = self.create_item(item_name)
item.classification = ItemClassification.filler # as it's an extra, just fast-fill it somewhere
@@ -72,39 +97,58 @@ class SubnauticaWorld(World):
# Victory item
self.world.get_location("Aurora - Captain Data Terminal", self.player).place_locked_item(
neptune_launch_platform)
self.world.get_location("Neptune Launch", self.player).place_locked_item(
SubnauticaItem("Victory", ItemClassification.progression, None, player=self.player))
for event in Locations.events:
self.world.get_location(event, self.player).place_locked_item(
SubnauticaItem(event, ItemClassification.progression, None, player=self.player))
# make the goal event the victory "item"
self.world.get_location(self.world.goal[self.player].get_event_name(), self.player).item.name = "Victory"
def set_rules(self):
set_rules(self.world, self.player)
def fill_slot_data(self) -> Dict[str, Any]:
goal: Options.Goal = self.world.goal[self.player]
item_pool: Options.ItemPool = self.world.item_pool[self.player]
vanilla_tech: List[str] = []
if item_pool == Options.ItemPool.option_valuable:
for item in Items.item_table.values():
if item["classification"] == ItemClassification.filler:
vanilla_tech.append(item["tech_type"])
def create_regions(self):
create_regions(self.world, self.player)
slot_data: Dict[str, Any] = {
"goal": goal.current_key,
"vanilla_tech": vanilla_tech,
"creatures_to_scan": self.creatures_to_scan
}
def fill_slot_data(self):
slot_data = {}
return slot_data
def create_item(self, name: str) -> Item:
item = lookup_name_to_item[name]
item_id: int = self.item_name_to_id[name]
return SubnauticaItem(name,
ItemClassification.progression if item["progression"] else ItemClassification.filler,
item["id"], player=self.player)
item_table[item_id]["classification"],
item_id, player=self.player)
def create_region(self, name: str, locations=None, exits=None):
ret = Region(name, RegionType.Generic, name, self.player)
ret.world = self.world
if locations:
for location in locations:
loc_id = self.location_name_to_id.get(location, None)
location = SubnauticaLocation(self.player, location, loc_id, ret)
ret.locations.append(location)
if exits:
for region_exit in exits:
ret.exits.append(Entrance(self.player, region_exit, ret))
return ret
def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
ret = Region(name, RegionType.Generic, name, player)
ret.world = world
if locations:
for location in locations:
loc_id = locations_lookup_name_to_id.get(location, 0)
location = SubnauticaLocation(player, location, loc_id, ret)
ret.locations.append(location)
if exits:
for exit in exits:
ret.exits.append(Entrance(player, exit, ret))
def get_pre_fill_items(self) -> List[Item]:
return self.prefill_items
return ret
def pre_fill(self) -> None:
reachable = self.world.get_reachable_locations(player=self.player)
self.world.random.shuffle(reachable)
items = self.prefill_items.copy()
for item in items:
reachable.pop().place_locked_item(item)
class SubnauticaLocation(Location):
@@ -112,4 +156,4 @@ class SubnauticaLocation(Location):
class SubnauticaItem(Item):
game = "Subnautica"
game: str = "Subnautica"

View File

@@ -1,83 +0,0 @@
[
{ "id": 35000, "count": 1, "progression": false, "tech_type": "Compass", "name": "Compass" },
{ "id": 35001, "count": 1, "progression": true, "tech_type": "PlasteelTank", "name": "Lightweight High Capacity Tank" },
{ "id": 35002, "count": 1, "progression": true, "tech_type": "BaseUpgradeConsole", "name": "Vehicle Upgrade Console" },
{ "id": 35003, "count": 1, "progression": true, "tech_type": "UltraGlideFins", "name": "Ultra Glide Fins" },
{ "id": 35004, "count": 1, "progression": false, "tech_type": "CyclopsSonarModule", "name": "Cyclops Sonar Upgrade" },
{ "id": 35005, "count": 1, "progression": false, "tech_type": "ReinforcedDiveSuit", "name": "Reinforced Dive Suit" },
{ "id": 35006, "count": 1, "progression": false, "tech_type": "CyclopsThermalReactorModule", "name": "Cyclops Thermal Reactor Module" },
{ "id": 35007, "count": 1, "progression": false, "tech_type": "Stillsuit", "name": "Stillsuit" },
{ "id": 35008, "count": 2, "progression": false, "tech_type": "BaseWaterParkFragment", "name": "Alien Containment Fragment" },
{ "id": 35009, "count": 1, "progression": false, "tech_type": "CyclopsDecoy", "name": "Creature Decoy" },
{ "id": 35010, "count": 1, "progression": false, "tech_type": "CyclopsFireSuppressionModule", "name": "Cyclops Fire Suppression System" },
{ "id": 35011, "count": 1, "progression": false, "tech_type": "SwimChargeFins", "name": "Swim Charge Fins" },
{ "id": 35012, "count": 1, "progression": false, "tech_type": "RepulsionCannon", "name": "Repulsion Cannon" },
{ "id": 35013, "count": 1, "progression": false, "tech_type": "CyclopsDecoyModule", "name": "Cyclops Decoy Tube Upgrade" },
{ "id": 35014, "count": 1, "progression": true, "tech_type": "CyclopsShieldModule", "name": "Cyclops Shield Generator" },
{ "id": 35015, "count": 1, "progression": true, "tech_type": "CyclopsHullModule1", "name": "Cyclops Depth Module MK1" },
{ "id": 35016, "count": 1, "progression": false, "tech_type": "CyclopsSeamothRepairModule", "name": "Cyclops Docking Bay Repair Module" },
{ "id": 35017, "count": 2, "progression": false, "tech_type": "BatteryChargerFragment", "name": "Battery Charger fragment" },
{ "id": 35018, "count": 2, "progression": false, "tech_type": "BeaconFragment", "name": "Beacon Fragment" },
{ "id": 35019, "count": 2, "progression": false, "tech_type": "BaseBioReactorFragment", "name": "Bioreactor Fragment" },
{ "id": 35020, "count": 3, "progression": true, "tech_type": "CyclopsBridgeFragment", "name": "Cyclops Bridge Fragment" },
{ "id": 35021, "count": 3, "progression": true, "tech_type": "CyclopsEngineFragment", "name": "Cyclops Engine Fragment" },
{ "id": 35022, "count": 3, "progression": true, "tech_type": "CyclopsHullFragment", "name": "Cyclops Hull Fragment" },
{ "id": 35023, "count": 2, "progression": false, "tech_type": "GravSphereFragment", "name": "Grav Trap Fragment" },
{ "id": 35024, "count": 3, "progression": true, "tech_type": "LaserCutterFragment", "name": "Laser Cutter Fragment" },
{ "id": 35025, "count": 1, "progression": false, "tech_type": "TechlightFragment", "name": "Light Stick Fragment" },
{ "id": 35026, "count": 3, "progression": true, "tech_type": "ConstructorFragment", "name": "Mobile Vehicle Bay Fragment" },
{ "id": 35027, "count": 3, "progression": true, "tech_type": "WorkbenchFragment", "name": "Modification Station Fragment" },
{ "id": 35028, "count": 2, "progression": true, "tech_type": "MoonpoolFragment", "name": "Moonpool Fragment" },
{ "id": 35029, "count": 3, "progression": false, "tech_type": "BaseNuclearReactorFragment", "name": "Nuclear Reactor Fragment" },
{ "id": 35030, "count": 2, "progression": false, "tech_type": "PowerCellChargerFragment", "name": "Power Cell Charger Fragment" },
{ "id": 35031, "count": 1, "progression": false, "tech_type": "PowerTransmitterFragment", "name": "Power Transmitter Fragment" },
{ "id": 35032, "count": 4, "progression": true, "tech_type": "ExosuitFragment", "name": "Prawn Suit Fragment" },
{ "id": 35033, "count": 2, "progression": false, "tech_type": "ExosuitDrillArmFragment", "name": "Prawn Suit Drill Arm Fragment" },
{ "id": 35034, "count": 2, "progression": false, "tech_type": "ExosuitGrapplingArmFragment", "name": "Prawn Suit Grappling Arm Fragment" },
{ "id": 35035, "count": 2, "progression": false, "tech_type": "ExosuitPropulsionArmFragment", "name": "Prawn Suit Propulsion Cannon Fragment" },
{ "id": 35036, "count": 2, "progression": false, "tech_type": "ExosuitTorpedoArmFragment", "name": "Prawn Suit Torpedo Arm Fragment" },
{ "id": 35037, "count": 3, "progression": false, "tech_type": "BaseMapRoomFragment", "name": "Scanner Room Fragment" },
{ "id": 35038, "count": 5, "progression": true, "tech_type": "SeamothFragment", "name": "Seamoth Fragment" },
{ "id": 35039, "count": 2, "progression": false, "tech_type": "StasisRifleFragment", "name": "Stasis Rifle Fragment" },
{ "id": 35040, "count": 2, "progression": false, "tech_type": "ThermalPlantFragment", "name": "Thermal Plant Fragment" },
{ "id": 35041, "count": 4, "progression": true, "tech_type": "SeaglideFragment", "name": "Seaglide Fragment" },
{ "id": 35042, "count": 1, "progression": true, "tech_type": "RadiationSuit", "name": "Radiation Suit" },
{ "id": 35043, "count": 2, "progression": true, "tech_type": "PropulsionCannonFragment", "name": "Propulsion Cannon Fragment" },
{ "id": 35044, "count": 1, "progression": true, "tech_type": "RocketBase", "name": "Neptune Launch Platform" },
{ "id": 35045, "count": 1, "progression": true, "tech_type": "PrecursorIonPowerCell", "name": "Ion Power Cell" },
{ "id": 35046, "count": 2, "progression": false, "tech_type": "FarmingTrayFragment", "name": "Exterior Growbed Fragment" },
{ "id": 35047, "count": 1, "progression": false, "tech_type": "PictureFrameFragment", "name": "Picture Frame" },
{ "id": 35048, "count": 2, "progression": false, "tech_type": "BenchFragment", "name": "Bench Fragment" },
{ "id": 35049, "count": 1, "progression": false, "tech_type": "PlanterPotFragment", "name": "Basic Plant Pot" },
{ "id": 35050, "count": 1, "progression": false, "tech_type": "PlanterBoxFragment", "name": "Interior Growbed" },
{ "id": 35051, "count": 1, "progression": false, "tech_type": "PlanterShelfFragment", "name": "Plant Shelf" },
{ "id": 35052, "count": 2, "progression": false, "tech_type": "BaseObservatoryFragment", "name": "Observatory Fragment" },
{ "id": 35053, "count": 2, "progression": false, "tech_type": "BaseRoomFragment", "name": "Multipurpose Room Fragment" },
{ "id": 35054, "count": 2, "progression": false, "tech_type": "BaseBulkheadFragment", "name": "Bulkhead Fragment" },
{ "id": 35055, "count": 1, "progression": false, "tech_type": "Spotlight", "name": "Spotlight" },
{ "id": 35056, "count": 2, "progression": false, "tech_type": "StarshipDesk", "name": "Desk" },
{ "id": 35057, "count": 1, "progression": false, "tech_type": "StarshipChair", "name": "Swivel Chair" },
{ "id": 35058, "count": 1, "progression": false, "tech_type": "StarshipChair2", "name": "Office Chair" },
{ "id": 35059, "count": 1, "progression": false, "tech_type": "StarshipChair3", "name": "Command Chair" },
{ "id": 35060, "count": 2, "progression": false, "tech_type": "LabCounter", "name": "Counter" },
{ "id": 35061, "count": 1, "progression": false, "tech_type": "NarrowBed", "name": "Single Bed" },
{ "id": 35062, "count": 1, "progression": false, "tech_type": "Bed1", "name": "Basic Double Bed" },
{ "id": 35063, "count": 1, "progression": false, "tech_type": "Bed2", "name": "Quilted Double Bed" },
{ "id": 35064, "count": 2, "progression": false, "tech_type": "CoffeeVendingMachine", "name": "Coffee Vending Machine" },
{ "id": 35065, "count": 2, "progression": false, "tech_type": "Trashcans", "name": "Trash Can" },
{ "id": 35066, "count": 1, "progression": false, "tech_type": "Techlight", "name": "Floodlight" },
{ "id": 35067, "count": 1, "progression": false, "tech_type": "BarTable", "name": "Bar Table" },
{ "id": 35068, "count": 1, "progression": false, "tech_type": "VendingMachine", "name": "Vending Machine" },
{ "id": 35069, "count": 1, "progression": false, "tech_type": "SingleWallShelf", "name": "Single Wall Shelf" },
{ "id": 35070, "count": 1, "progression": false, "tech_type": "WallShelves", "name": "Wall Shelves" },
{ "id": 35071, "count": 1, "progression": false, "tech_type": "PlanterPot2", "name": "Round Plant Pot" },
{ "id": 35072, "count": 1, "progression": false, "tech_type": "PlanterPot3", "name": "Chic Plant Pot" },
{ "id": 35073, "count": 1, "progression": false, "tech_type": "LabTrashcan", "name": "Nuclear Waste Disposal" },
{ "id": 35074, "count": 1, "progression": false, "tech_type": "BasePlanter", "name": "Wall Planter" },
{ "id": 35075, "count": 1, "progression": true, "tech_type": "PrecursorIonBattery", "name": "Ion Battery" },
{ "id": 35076, "count": 1, "progression": true, "tech_type": "RocketBaseLadder", "name": "Neptune Gantry" },
{ "id": 35077, "count": 1, "progression": true, "tech_type": "RocketStage1", "name": "Neptune Boosters" },
{ "id": 35078, "count": 1, "progression": true, "tech_type": "RocketStage2", "name": "Neptune Fuel Reserve" },
{ "id": 35079, "count": 1, "progression": true, "tech_type": "RocketStage3", "name": "Neptune Cockpit" },
{ "id": 35080, "count": 1, "progression": false, "tech_type": "BaseFiltrationMachine", "name": "Water Filtration Machine" }
]

View File

@@ -1,521 +0,0 @@
[
{ "id": 33000, "position": { "x": -1234.3, "y": -349.7, "z": -396.0},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Blood Kelp Trench Wreck - Outside Databox" },
{ "id": 33001, "position": { "x": -1208.0, "y": -349.6, "z": -383.0},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Blood Kelp Trench Wreck - Inside Databox" },
{ "id": 33002, "position": { "x": -1210.6, "y": -340.7, "z": -393.4},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Blood Kelp Trench Wreck - PDA" },
{ "id": 33003, "position": { "x": 903.8, "y": -220.3, "z": 590.9},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Bulb Zone West Wreck - Outside Databox" },
{ "id": 33004, "position": { "x": 910.9, "y": -201.8, "z": 623.5},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Bulb Zone West Wreck - Under Databox" },
{ "id": 33005, "position": { "x": 914.9, "y": -202.1, "z": 611.8},
"need_laser_cutter": true, "can_slip_through": false,
"name": "Bulb Zone West Wreck - Inside Databox" },
{ "id": 33006, "position": { "x": 912.6, "y": -202.0, "z": 609.5},
"need_laser_cutter": true, "can_slip_through": false,
"name": "Bulb Zone West Wreck - PDA" },
{ "id": 33007, "position": { "x": 1327.1, "y": -234.9, "z": 575.8},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Bulb Zone East Wreck - Databox" },
{ "id": 33008, "position": { "x": -1407.7, "y": -344.2, "z": 721.5},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Dunes North Wreck - Outside Databox" },
{ "id": 33009, "position": { "x": -1393.9, "y": -329.7, "z": 733.5},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Dunes North Wreck - Office Databox" },
{ "id": 33010, "position": { "x": -1396.3, "y": -330.8, "z": 730.0},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Dunes North Wreck - PDA" },
{ "id": 33011, "position": { "x": -1409.8, "y": -332.4, "z": 706.9},
"need_laser_cutter": true, "can_slip_through": false,
"name": "Dunes North Wreck - Cargo Databox" },
{ "id": 33012, "position": { "x": -1626.2, "y": -357.5, "z": 99.5},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Dunes West Wreck - Databox" },
{ "id": 33013, "position": { "x": -1196.3, "y": -223.0, "z": 12.5},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Dunes East Wreck - Outside Databox" },
{ "id": 33014, "position": { "x": -1206.4, "y": -225.6, "z": 4.0},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Dunes East Wreck - Inside Databox" },
{ "id": 33015, "position": { "x": -269.7, "y": -262.8, "z": -764.3},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Grand Reef North Wreck - Outside Databox" },
{ "id": 33016, "position": { "x": -285.8, "y": -240.2, "z": -786.5},
"need_laser_cutter": true, "can_slip_through": false,
"name": "Grand Reef North Wreck - Elevator Databox" },
{ "id": 33017, "position": { "x": -285.2, "y": -262.4, "z": -788.4},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Grand Reef North Wreck - Bottom Databox" },
{ "id": 33018, "position": { "x": -272.5, "y": -254.7, "z": -788.5},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Grand Reef North Wreck - Hangar PDA" },
{ "id": 33019, "position": { "x": -850.9, "y": -473.2, "z": -1414.6},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Grand Reef South Wreck - Trench Databox" },
{ "id": 33020, "position": { "x": -889.4, "y": -433.8, "z": -1424.8},
"need_laser_cutter": true, "can_slip_through": false,
"name": "Grand Reef South Wreck - Comms Databox" },
{ "id": 33021, "position": { "x": -862.4, "y": -437.5, "z": -1444.1},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Grand Reef South Wreck - Outside Databox" },
{ "id": 33022, "position": { "x": -887.9, "y": -446.0, "z": -1422.7},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Grand Reef South Wreck - PDA" },
{ "id": 33023, "position": { "x": -23.3, "y": -105.8, "z": -604.2},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Grassy Plateaus South Wreck - Databox" },
{ "id": 33024, "position": { "x": -27.3, "y": -106.8, "z": -607.2},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Grassy Plateaus South Wreck - PDA" },
{ "id": 33025, "position": { "x": 313.9, "y": -91.8, "z": 432.6},
"need_laser_cutter": true, "can_slip_through": true,
"name": "Grassy Plateaus East Wreck - Breach Databox" },
{ "id": 33026, "position": { "x": 319.4, "y": -104.3, "z": 441.5},
"need_laser_cutter": true, "can_slip_through": true,
"name": "Grassy Plateaus East Wreck - Hangar Databox" },
{ "id": 33027, "position": { "x": -632.3, "y": -75.0, "z": -8.9},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Grassy Plateaus West Wreck - Locker PDA" },
{ "id": 33028, "position": { "x": -664.4, "y": -97.8, "z": -8.0},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Grassy Plateaus West Wreck - Data Terminal" },
{ "id": 33029, "position": { "x": -421.4, "y": -107.8, "z": -266.5},
"need_laser_cutter": true, "can_slip_through": false,
"name": "Grassy Plateaus West Wreck - Databox" },
{ "id": 33030, "position": { "x": -44.0, "y": -29.1, "z": -403.6},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Safe Shallows Wreck - PDA" },
{ "id": 33031, "position": { "x": -317.1, "y": -79.0, "z": 248.5},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Kelp Forest Wreck - Databox" },
{ "id": 33032, "position": { "x": 63.2, "y": -38.5, "z": 382.9},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Kelp Forest Wreck - PDA" },
{ "id": 33033, "position": { "x": 740.3, "y": -389.2, "z": 1179.8},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Mountains West Wreck - Outside Databox" },
{ "id": 33034, "position": { "x": 703.7, "y": -365.9, "z": 1199.3},
"need_laser_cutter": true, "can_slip_through": false,
"name": "Mountains West Wreck - Data Terminal" },
{ "id": 33035, "position": { "x": 698.2, "y": -350.8, "z": 1186.9},
"need_laser_cutter": true, "can_slip_through": false,
"name": "Mountains West Wreck - Hangar Databox" },
{ "id": 33036, "position": { "x": 676.3, "y": -343.6, "z": 1204.6},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Mountains West Wreck - Office Databox" },
{ "id": 33037, "position": { "x": 1068.5, "y": -283.4, "z": 1345.3},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Mountains East Wreck - Comms Databox" },
{ "id": 33038, "position": { "x": 1075.7, "y": -288.9, "z": 1321.8},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Mountains East Wreck - Outside Databox" },
{ "id": 33039, "position": { "x": -655.1, "y": -109.6, "z": 791.0},
"need_laser_cutter": true, "can_slip_through": false,
"name": "Northwestern Mushroom Forest Wreck - Cargo Databox" },
{ "id": 33040, "position": { "x": -663.4, "y": -111.9, "z": 777.9},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Northwestern Mushroom Forest Wreck - Office Databox" },
{ "id": 33041, "position": { "x": -662.2, "y": -113.4, "z": 777.7},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Northwestern Mushroom Forest Wreck - PDA" },
{ "id": 33042, "position": { "x": -1161.1, "y": -191.7, "z": -758.3},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Sea Treader's Path Wreck - Outside Databox" },
{ "id": 33043, "position": { "x": -1129.5, "y": -155.2, "z": -729.3},
"need_laser_cutter": true, "can_slip_through": false,
"name": "Sea Treader's Path Wreck - Hangar Databox" },
{ "id": 33044, "position": { "x": -1115.9, "y": -175.3, "z": -724.5},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Sea Treader's Path Wreck - Lobby Databox" },
{ "id": 33045, "position": { "x": -1136.8, "y": -157.0, "z": -734.6},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Sea Treader's Path Wreck - PDA" },
{ "id": 33046, "position": { "x": -789.8, "y": -216.1, "z": -711.0},
"need_laser_cutter": true, "can_slip_through": false,
"name": "Sparse Reef Wreck - Locker Databox" },
{ "id": 33047, "position": { "x": -810.7, "y": -209.3, "z": -685.5},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Sparse Reef Wreck - Outside Databox" },
{ "id": 33048, "position": { "x": -795.5, "y": -204.1, "z": -774.7},
"need_laser_cutter": true, "can_slip_through": false,
"name": "Sparse Reef Wreck - Lab Databox" },
{ "id": 33049, "position": { "x": -170.8, "y": -187.6, "z": 880.7},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Underwater Islands Wreck - Outside Databox" },
{ "id": 33050, "position": { "x": -138.4, "y": -193.6, "z": 888.7},
"need_laser_cutter": true, "can_slip_through": false,
"name": "Underwater Islands Wreck - Hangar Databox" },
{ "id": 33051, "position": { "x": -130.7, "y": -193.2, "z": 883.3},
"need_laser_cutter": true, "can_slip_through": false,
"name": "Underwater Islands Wreck - Data Terminal" },
{ "id": 33052, "position": { "x": -137.8, "y": -193.4, "z": 879.4},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Underwater Islands Wreck - Cable Databox" },
{ "id": 33053, "position": { "x": -124.4, "y": -200.7, "z": 853.0},
"need_laser_cutter": false, "can_slip_through": false, "need_propulsion_cannon": true,
"name": "Underwater Islands Wreck - Pipes Databox 1" },
{ "id": 33054, "position": { "x": -126.8, "y": -201.1, "z": 852.1},
"need_laser_cutter": false, "can_slip_through": false, "need_propulsion_cannon": true,
"name": "Underwater Islands Wreck - Pipes Databox 2" },
{ "id": 33055, "position": { "x": -643.8, "y": -509.9, "z": -941.9},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Degasi Seabase - Deep Grand Reef - Bedroom Databox" },
{ "id": 33056, "position": { "x": -635.1, "y": -502.7, "z": -951.4},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Degasi Seabase - Deep Grand Reef - Observatory Databox" },
{ "id": 33057, "position": { "x": -645.8, "y": -508.7, "z": -943.0},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Degasi Seabase - Deep Grand Reef - Bedroom PDA" },
{ "id": 33058, "position": { "x": -630.5, "y": -511.1, "z": -936.1},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Degasi Seabase - Deep Grand Reef - Outside PDA" },
{ "id": 33059, "position": { "x": -647.7, "y": -502.6, "z": -935.8},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Degasi Seabase - Deep Grand Reef - Observatory PDA" },
{ "id": 33060, "position": { "x": -639.6, "y": -505.9, "z": -946.6},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Degasi Seabase - Deep Grand Reef - Lab PDA" },
{ "id": 33061, "position": { "x": -707.2, "y": 0.5, "z": -1096.7},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Floating Island - Lake PDA" },
{ "id": 33062, "position": { "x": -765.7, "y": 17.6, "z": -1116.4},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Degasi Seabase - Floating Island - Databox" },
{ "id": 33063, "position": { "x": -754.9, "y": 14.6, "z": -1108.9},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Degasi Seabase - Floating Island - Room PDA" },
{ "id": 33064, "position": { "x": -765.3, "y": 14.1, "z": -1115.0},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Degasi Seabase - Floating Island - Green Wall PDA" },
{ "id": 33065, "position": { "x": -758.6, "y": 14.1, "z": -1111.3},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Degasi Seabase - Floating Island - Corridor PDA" },
{ "id": 33066, "position": { "x": -805.4, "y": 76.9, "z": -1055.7},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Degasi Seabase - Floating Island - North Observatory PDA" },
{ "id": 33067, "position": { "x": -715.9, "y": 75.4, "z": -1168.8},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Degasi Seabase - Floating Island - South Observatory PDA" },
{ "id": 33068, "position": { "x": -540.5, "y": -250.8, "z": -83.4},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Jellyshroom Cave - PDA" },
{ "id": 33069, "position": { "x": 110.6, "y": -264.9, "z": -369.0},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Degasi Seabase - Jellyshroom Cave - Bedroom Databox" },
{ "id": 33070, "position": { "x": 80.6, "y": -268.6, "z": -358.3},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Degasi Seabase - Jellyshroom Cave - Detached PDA" },
{ "id": 33071, "position": { "x": 78.2, "y": -265.0, "z": -373.4},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Degasi Seabase - Jellyshroom Cave - Office PDA" },
{ "id": 33072, "position": { "x": 85.1, "y": -264.1, "z": -372.8},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Degasi Seabase - Jellyshroom Cave - Locker PDA" },
{ "id": 33073, "position": { "x": 112.3, "y": -264.9, "z": -369.3},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Degasi Seabase - Jellyshroom Cave - Bedroom PDA" },
{ "id": 33074, "position": { "x": 95.5, "y": -258.9, "z": -366.5},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Degasi Seabase - Jellyshroom Cave - Observatory PDA" },
{ "id": 33075, "position": { "x": -483.6, "y": -504.7, "z": 1326.6},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Lifepod 2 - Databox" },
{ "id": 33076, "position": { "x": -481.4, "y": -503.6, "z": 1324.1},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Lifepod 2 - PDA" },
{ "id": 33077, "position": { "x": -34.2, "y": -22.4, "z": 410.5},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Lifepod 3 - Databox" },
{ "id": 33078, "position": { "x": -33.8, "y": -22.5, "z": 408.8},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Lifepod 3 - PDA" },
{ "id": 33079, "position": { "x": 712.4, "y": -3.4, "z": 160.8},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Lifepod 4 - Databox" },
{ "id": 33080, "position": { "x": 712.0, "y": -3.5, "z": 161.5},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Lifepod 4 - PDA" },
{ "id": 33081, "position": { "x": 358.7, "y": -117.1, "z": 306.8},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Lifepod 6 - Databox" },
{ "id": 33082, "position": { "x": 361.8, "y": -116.2, "z": 309.5},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Lifepod 6 - Inside PDA" },
{ "id": 33083, "position": { "x": 359.9, "y": -117.0, "z": 312.1},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Lifepod 6 - Outside PDA" },
{ "id": 33084, "position": { "x": -56.0, "y": -182.0, "z": -1039.0},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Lifepod 7 - PDA" },
{ "id": 33085, "position": { "x": 1119.5, "y": -271.7, "z": 561.7},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Lifepod 12 - Databox" },
{ "id": 33086, "position": { "x": 1116.1, "y": -271.3, "z": 566.9},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Lifepod 12 - PDA" },
{ "id": 33087, "position": { "x": -926.4, "y": -185.2, "z": 501.8},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Lifepod 13 - Databox" },
{ "id": 33088, "position": { "x": -926.8, "y": -184.4, "z": 506.6},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Lifepod 13 - PDA" },
{ "id": 33089, "position": { "x": -514.5, "y": -98.1, "z": -56.5},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Lifepod 17 - PDA" },
{ "id": 33090, "position": { "x": -809.8, "y": -302.2, "z": -876.9},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Lifepod 19 - Databox" },
{ "id": 33091, "position": { "x": -806.1, "y": -294.1, "z": -866.0},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Lifepod 19 - Outside PDA" },
{ "id": 33092, "position": { "x": -810.5, "y": -299.4, "z": -873.1},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Lifepod 19 - Inside PDA" },
{ "id": 33093, "position": { "x": 903.5, "y": -0.2, "z": 16.1},
"need_laser_cutter": false, "can_slip_through": false, "need_propulsion_cannon": true,
"name": "Aurora Seamoth Bay - Upgrade Console" },
{ "id": 33094, "position": { "x": 872.5, "y": 2.7, "z": -0.7},
"need_laser_cutter": false, "can_slip_through": false, "need_propulsion_cannon": true,
"name": "Aurora Drive Room - Upgrade Console" },
{ "id": 33095, "position": { "x": 991.6, "y": 3.2, "z": -31.0},
"need_laser_cutter": true, "can_slip_through": false, "need_propulsion_cannon": true,
"name": "Aurora Prawn Suit Bay - Upgrade Console" },
{ "id": 33096, "position": { "x": 952.1, "y": 41.2, "z": 113.9},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Aurora - Office PDA" },
{ "id": 33097, "position": { "x": 977.2, "y": 39.1, "z": 83.0},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Aurora - Corridor PDA" },
{ "id": 33098, "position": { "x": 954.9, "y": 11.2, "z": 3.4},
"need_laser_cutter": false, "can_slip_through": false, "need_propulsion_cannon": true,
"name": "Aurora - Cargo Bay PDA" },
{ "id": 33099, "position": { "x": 907.1, "y": -1.5, "z": 15.3},
"need_laser_cutter": false, "can_slip_through": false, "need_propulsion_cannon": true,
"name": "Aurora - Seamoth Bay PDA" },
{ "id": 33100, "position": { "x": 951.8, "y": -2.3, "z": -34.7},
"need_laser_cutter": true, "can_slip_through": false, "need_propulsion_cannon": true,
"name": "Aurora - Medkit Locker PDA" },
{ "id": 33101, "position": { "x": 952.0, "y": -3.7, "z": -23.4},
"need_laser_cutter": true, "can_slip_through": false, "need_propulsion_cannon": true,
"name": "Aurora - Locker PDA" },
{ "id": 33102, "position": { "x": 986.5, "y": 9.6, "z": -48.6},
"need_laser_cutter": true, "can_slip_through": false, "need_propulsion_cannon": true,
"name": "Aurora - Canteen PDA" },
{ "id": 33103, "position": { "x": 951.3, "y": 11.2, "z": -51.0},
"need_laser_cutter": true, "can_slip_through": false, "need_propulsion_cannon": true,
"name": "Aurora - Cabin 4 PDA" },
{ "id": 33104, "position": { "x": 967.1, "y": 10.4, "z": -47.4},
"need_laser_cutter": true, "can_slip_through": false, "need_propulsion_cannon": true,
"name": "Aurora - Cabin 7 PDA" },
{ "id": 33105, "position": { "x": 964.1, "y": 11.1, "z": -61.9},
"need_laser_cutter": true, "can_slip_through": false, "need_propulsion_cannon": true,
"name": "Aurora - Cabin 1 PDA" },
{ "id": 33106, "position": { "x": 971.2, "y": 10.8, "z": -70.4},
"need_laser_cutter": true, "can_slip_through": false, "need_propulsion_cannon": true,
"name": "Aurora - Captain PDA" },
{ "id": 33107, "position": { "x": 1033.6, "y": -8.5, "z": 16.2},
"need_laser_cutter": false, "can_slip_through": false, "need_propulsion_cannon": true,
"name": "Aurora - Ring PDA" },
{ "id": 33108, "position": { "x": 1032.5, "y": -7.8, "z": 32.4},
"need_laser_cutter": false, "can_slip_through": false, "need_propulsion_cannon": true,
"name": "Aurora - Lab PDA" },
{ "id": 33109, "position": { "x": 945.8, "y": 40.8, "z": 115.1},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Aurora - Office Data Terminal" },
{ "id": 33110, "position": { "x": 974.8, "y": 10.0, "z": -77.0},
"need_laser_cutter": true, "can_slip_through": false, "need_propulsion_cannon": true,
"name": "Aurora - Captain Data Terminal" },
{ "id": 33111, "position": { "x": 1040.8, "y": -11.4, "z": -3.4},
"need_laser_cutter": true, "can_slip_through": false, "need_propulsion_cannon": true,
"name": "Aurora - Battery Room Data Terminal" },
{ "id": 33112, "position": { "x": 1029.5, "y": -8.7, "z": 35.9},
"need_laser_cutter": false, "can_slip_through": false, "need_propulsion_cannon": true,
"name": "Aurora - Lab Data Terminal" },
{ "id": 33113, "position": { "x": 432.2, "y": 3.0, "z": 1193.2},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Quarantine Enforcement Platform's - Upper Alien Data Terminal" },
{ "id": 33114, "position": { "x": 474.4, "y": -4.5, "z": 1224.4},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Quarantine Enforcement Platform's - Mid Alien Data Terminal" },
{ "id": 33115, "position": { "x": -1224.2, "y": -400.4, "z": 1057.9},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Dunes Sanctuary - Alien Data Terminal" },
{ "id": 33116, "position": { "x": -895.5, "y": -311.6, "z": -838.1},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Deep Sparse Reef Sanctuary - Alien Data Terminal" },
{ "id": 33117, "position": { "x": -642.9, "y": -563.5, "z": 1485.5},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Northern Blood Kelp Zone Sanctuary - Alien Data Terminal" },
{ "id": 33118, "position": { "x": -1112.3, "y": -687.3, "z": -695.5},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Lost River Laboratory Cache - Alien Data Terminal" },
{ "id": 33119, "position": { "x": -280.2, "y": -804.3, "z": 305.1},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Disease Research Facility - Upper Alien Data Terminal" },
{ "id": 33120, "position": { "x": -267.9, "y": -806.6, "z": 250.0},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Disease Research Facility - Mid Alien Data Terminal" },
{ "id": 33121, "position": { "x": -286.2, "y": -815.6, "z": 297.8},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Disease Research Facility - Lower Alien Data Terminal" },
{ "id": 33122, "position": { "x": -71.3, "y": -1227.2, "z": 104.8},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Alien Thermal Plant - Entrance Alien Data Terminal" },
{ "id": 33123, "position": { "x": -38.7, "y": -1226.6, "z": 111.8},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Alien Thermal Plant - Green Alien Data Terminal" },
{ "id": 33124, "position": { "x": -30.4, "y": -1220.3, "z": 111.8},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Alien Thermal Plant - Yellow Alien Data Terminal" },
{ "id": 33125, "position": { "x": 245.8, "y": -1430.6, "z": -311.5},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Primary Containment Facility's Antechamber - Alien Data Terminal" },
{ "id": 33126, "position": { "x": 165.5, "y": -1442.4, "z": -385.8},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Primary Containment Facility's Egg Laboratory - Alien Data Terminal" },
{ "id": 33127, "position": { "x": 348.7, "y": -1443.5, "z": -291.9},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Primary Containment Facility's Pipe Room - Alien Data Terminal" },
{ "id": 33128, "position": { "x": -641.8, "y": -111.3, "z": -19.7},
"need_laser_cutter": true, "can_slip_through": false,
"name": "Grassy Plateaus West Wreck - Beam PDA" },
{ "id": 33129, "position": { "x": -748.9, "y": 14.4, "z": -1179.5},
"need_laser_cutter": false, "can_slip_through": false,
"name": "Floating Island - Cave Entrance PDA" }
]

View File

@@ -1,30 +0,0 @@
100 - 0x01A54 - None - Glass Factory Entry Door
105 - 0x000B0 - 0x0343A - Door to Symmetry Island Lower
107 - 0x1C349 - 0x00076 - Door to Symmetry Island Upper
110 - 0x0C339 - 0x09F94 - Door to Desert Flood Light Room
111 - 0x1C2DF,0x1831E,0x1C260,0x1831C,0x1C2F3,0x1831D,0x1C2B1,0x1831B - None - Desert Flood Room Flood Controls
120 - 0x03678 - None - Quarry Mill Ramp Control
122 - 0x03679 - 0x014E8 - Quarry Mill Elevator Control
125 - 0x03852 - 0x034D4,0x021D5 - Quarry Boathouse Ramp Height Control
127 - 0x03858 - 0x021AE - Quarry Boathouse Ramp Horizontal Control
131 - 0x334DB,0x334DC - None - Shadows Door Timer
150 - 0x00B10 - None - Monastery Entry Door Left
151 - 0x00C92 - None - Monastery Entry Door Right
162 - 0x28998 - None - Town Door to RGB House
163 - 0x28A0D - 0x28998 - Town Door to Church
166 - 0x28A79 - None - Town Maze Panel (Drop-Down Staircase)
169 - 0x17F5F - None - Windmill Door
200 - 0x0288C - None - Treehouse First & Second Door
202 - 0x0A182 - None - Treehouse Third Door
205 - 0x2700B - None - Treehouse Laser House Door Timer
208 - 0x17CBC - None - Treehouse Shortcut Drop-Down Bridge
175 - 0x17CAB - 0x002C7 - Jungle Popup Wall
180 - 0x17C2E - None - Bunker Entry Door
183 - 0x0A099 - 0x09DAF - Inside Bunker Door to Bunker Proper
186 - 0x0A079 - None - Bunker Elevator Control
190 - 0x0056E - None - Swamp Entry Door
192 - 0x00609,0x18488 - 0x181A9 - Swamp Sliding Bridge
195 - 0x181F5 - None - Swamp Rotating Bridge
197 - 0x17C0A - None - Swamp Maze Control
300 - 0x0042D - None - Mountaintop River Shape Panel (Shortcut to Secret Area)
310 - 0x17CDF,0x17CC8,0x17CA6,0x09DB8,0x17C95,0x0A054 - None - Boat

View File

@@ -1,5 +0,0 @@
Event Items:
Shortcut to Secret Area Opens - 0x0042D
Region Changes:
Inside Mountain Secret Area (Inside Mountain Secret Area) - Inside Mountain Path to Secret Area - 0x00FF8 - Main Island - 0x021D7 | 0x0042D - Main Island - 0x17CF2

View File

@@ -7,37 +7,48 @@ from Options import Toggle, DefaultOnToggle, Option, Range, Choice
# "Play the randomizer in hardmode"
# display_name = "Hard Mode"
# class UnlockSymbols(DefaultOnToggle):
# "All Puzzle symbols of a specific panel need to be unlocked before the panel can be used"
# display_name = "Unlock Symbols"
class DisableNonRandomizedPuzzles(DefaultOnToggle):
"""Disable puzzles that cannot be randomized.
Non randomized puzzles are Shadows, Monastery, and Greenhouse.
"""Disables puzzles that cannot be randomized.
This includes many puzzles that heavily involve the environment, such as Shadows, Monastery or Orchard.
The lasers for those areas will be activated as you solve optional puzzles throughout the island."""
display_name = "Disable non randomized puzzles"
class EarlySecretArea(Toggle):
"""The Mountainside shortcut to the Mountain Secret Area is open from the start.
"""Opens the Mountainside shortcut to the Mountain Secret Area from the start.
(Otherwise known as "UTM", "Caves" or the "Challenge Area")"""
display_name = "Early Secret Area"
class ShuffleSymbols(DefaultOnToggle):
"""You will need to unlock puzzle symbols as items to be able to solve the panels that contain those symbols."""
"""You will need to unlock puzzle symbols as items to be able to solve the panels that contain those symbols.
If you turn this off, there will be no progression items in the game unless you turn on door shuffle."""
display_name = "Shuffle Symbols"
class ShuffleDoors(Toggle):
"""Many doors around the island will have their panels turned off initially.
You will need to find the items that power the panels to open those doors."""
class ShuffleLasers(Toggle):
"""If on, the 11 lasers are turned into items and will activate on their own upon receiving them.
Note: There is a visual bug that can occur with the Desert Laser. It does not affect gameplay - The Laser can still
be redirected as normal, for both applications of redirection."""
display_name = "Shuffle Lasers"
class ShuffleDoors(Choice):
"""If on, opening doors will require their respective "keys".
If set to "panels", those keys will unlock the panels on doors.
In "doors_simple" and "doors_complex", the doors will magically open by themselves upon receiving the key."""
display_name = "Shuffle Doors"
option_none = 0
option_panels = 1
option_doors_simple = 2
option_doors_complex = 3
option_max = 4
class ShuffleDiscardedPanels(Toggle):
"""Discarded Panels will have items on them.
Solving certain Discarded Panels may still be necessary even if off!"""
"""Add Discarded Panels into the location pool.
Solving certain Discarded Panels may still be necessary to beat the game, even if this is off."""
display_name = "Shuffle Discarded Panels"
@@ -52,9 +63,10 @@ class ShuffleUncommonLocations(Toggle):
display_name = "Shuffle Uncommon Locations"
class ShuffleHardLocations(Toggle):
"""Adds some harder locations into the game, e.g. Mountain Secret Area panels"""
display_name = "Shuffle Hard Locations"
class ShufflePostgame(Toggle):
"""Adds locations into the pool that are guaranteed to be locked behind your goal. Use this if you don't play with
forfeit on victory."""
display_name = "Shuffle Postgame"
class VictoryCondition(Choice):
@@ -103,16 +115,17 @@ class PuzzleSkipAmount(Range):
the_witness_options: Dict[str, type] = {
# "hard_mode": HardMode,
"shuffle_symbols": ShuffleSymbols,
"shuffle_doors": ShuffleDoors,
"shuffle_lasers": ShuffleLasers,
"disable_non_randomized_puzzles": DisableNonRandomizedPuzzles,
"shuffle_discarded_panels": ShuffleDiscardedPanels,
"shuffle_vault_boxes": ShuffleVaultBoxes,
"shuffle_uncommon": ShuffleUncommonLocations,
"shuffle_hard": ShuffleHardLocations,
"shuffle_postgame": ShufflePostgame,
"victory_condition": VictoryCondition,
"trap_percentage": TrapPercentage,
"early_secret_area": EarlySecretArea,
# "shuffle_symbols": ShuffleSymbols,
# "shuffle_doors": ShuffleDoors,
"mountain_lasers": MountainLasers,
"challenge_lasers": ChallengeLasers,
"puzzle_skip_amount": PuzzleSkipAmount,

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -35,7 +35,7 @@ class WitnessWorld(World):
"""
game = "The Witness"
topology_present = False
data_version = 2
data_version = 5
static_logic = StaticWitnessLogic()
static_locat = StaticWitnessLocations()
@@ -53,11 +53,18 @@ class WitnessWorld(World):
'seed': self.world.random.randint(0, 1000000),
'victory_location': int(self.player_logic.VICTORY_LOCATION, 16),
'panelhex_to_id': self.locat.CHECK_PANELHEX_TO_ID,
'doorhex_to_id': self.player_logic.DOOR_DICT_FOR_CLIENT,
'door_connections_to_sever': self.player_logic.DOOR_CONNECTIONS_TO_SEVER
'item_id_to_door_hexes': self.items.ITEM_ID_TO_DOOR_HEX,
'door_hexes': self.items.DOORS,
'symbols_not_in_the_game': self.items.SYMBOLS_NOT_IN_THE_GAME
}
def generate_early(self):
if not (is_option_enabled(self.world, self.player, "shuffle_symbols")
or get_option_value(self.world, self.player, "shuffle_doors")
or is_option_enabled(self.world, self.player, "shuffle_lasers")):
raise Exception("This Witness world doesn't have any progression items. Please turn on Symbol Shuffle, Door"
" Shuffle or Laser Shuffle")
self.player_logic = WitnessPlayerLogic(self.world, self.player)
self.locat = WitnessPlayerLocations(self.world, self.player, self.player_logic)
self.items = WitnessPlayerItems(self.locat, self.world, self.player, self.player_logic)
@@ -78,11 +85,11 @@ class WitnessWorld(World):
less_junk = 0
# Put good item on first check if symbol shuffle is on
# symbols = is_option_enabled(self.world, self.player, "shuffle_symbols")
symbols = True
symbols = is_option_enabled(self.world, self.player, "shuffle_symbols")
if symbols:
random_good_item = self.world.random.choice(self.items.GOOD_ITEMS)
first_check = self.world.get_location(
"Tutorial Gate Open", self.player
)
@@ -91,6 +98,10 @@ class WitnessWorld(World):
less_junk = 1
for item in self.player_logic.STARTING_INVENTORY:
self.world.push_precollected(items_by_name[item])
pool.remove(items_by_name[item])
for item in self.items.EXTRA_AMOUNTS:
witness_item = self.create_item(item)
for i in range(0, self.items.EXTRA_AMOUNTS[item]):

View File

@@ -51,12 +51,15 @@ class StaticWitnessItems:
def __init__(self):
item_tab = dict()
for item in StaticWitnessLogic.ALL_SYMBOL_ITEMS.union(StaticWitnessLogic.ALL_DOOR_ITEMS):
for item in StaticWitnessLogic.ALL_SYMBOL_ITEMS:
if item[0] == "11 Lasers" or item == "7 Lasers":
continue
item_tab[item[0]] = ItemData(158000 + item[1], True, False)
for item in StaticWitnessLogic.ALL_DOOR_ITEMS:
item_tab[item[0]] = ItemData(158000 + item[1], True, False)
for item in StaticWitnessLogic.ALL_TRAPS:
item_tab[item[0]] = ItemData(
158000 + item[1], False, False, True
@@ -89,23 +92,39 @@ class WitnessPlayerItems:
self.ITEM_TABLE = copy.copy(StaticWitnessItems.ALL_ITEM_TABLE)
self.PROGRESSION_TABLE = dict()
self.ITEM_ID_TO_DOOR_HEX = dict()
self.DOORS = set()
self.SYMBOLS_NOT_IN_THE_GAME = set()
self.EXTRA_AMOUNTS = {
"Functioning Brain": 1,
"Puzzle Skip": get_option_value(world, player, "puzzle_skip_amount")
}
for item in StaticWitnessLogic.ALL_SYMBOL_ITEMS.union(StaticWitnessLogic.ALL_DOOR_ITEMS):
if item not in player_logic.PROG_ITEMS_ACTUALLY_IN_THE_GAME:
if item[0] not in player_logic.PROG_ITEMS_ACTUALLY_IN_THE_GAME:
del self.ITEM_TABLE[item[0]]
if item in StaticWitnessLogic.ALL_SYMBOL_ITEMS:
self.SYMBOLS_NOT_IN_THE_GAME.add(StaticWitnessItems.ALL_ITEM_TABLE[item[0]].code)
else:
self.PROGRESSION_TABLE[item[0]] = self.ITEM_TABLE[item[0]]
for entity_hex, items in player_logic.DOOR_ITEMS_BY_ID.items():
entity_hex_int = int(entity_hex, 16)
self.DOORS.add(entity_hex_int)
for item in items:
item_id = StaticWitnessItems.ALL_ITEM_TABLE[item].code
self.ITEM_ID_TO_DOOR_HEX.setdefault(item_id, set()).add(entity_hex_int)
symbols = is_option_enabled(world, player, "shuffle_symbols")
if "shuffle_symbols" not in the_witness_options.keys():
symbols = True
doors = is_option_enabled(world, player, "shuffle_doors")
doors = get_option_value(world, player, "shuffle_doors")
if doors and symbols:
self.GOOD_ITEMS = [
@@ -117,10 +136,10 @@ class WitnessPlayerItems:
"Shapers", "Symmetry"
]
if is_option_enabled(world, player, "shuffle_discarded_panels"):
self.GOOD_ITEMS.append("Triangles")
if not is_option_enabled(world, player, "disable_non_randomized_puzzles"):
self.GOOD_ITEMS.append("Colored Squares")
if is_option_enabled(world, player, "shuffle_discarded_panels"):
self.GOOD_ITEMS.append("Triangles")
if not is_option_enabled(world, player, "disable_non_randomized_puzzles"):
self.GOOD_ITEMS.append("Colored Squares")
for event_location in locat.EVENT_LOCATION_TABLE:
location = player_logic.EVENT_ITEM_PAIRS[event_location]

View File

@@ -2,7 +2,7 @@
Defines constants for different types of locations in the game
"""
from .Options import is_option_enabled
from .Options import is_option_enabled, get_option_value
from .player_logic import StaticWitnessLogic, WitnessPlayerLogic
@@ -42,7 +42,7 @@ class StaticWitnessLocations:
"Symmetry Island Colored Dots 6",
"Symmetry Island Fading Lines 7",
"Symmetry Island Scenery Outlines 5",
"Symmetry Island Laser",
"Symmetry Island Laser Panel",
"Orchard Apple Tree 5",
@@ -52,7 +52,7 @@ class StaticWitnessLocations:
"Desert Artificial Light Reflection 3",
"Desert Pond Reflection 5",
"Desert Flood Reflection 6",
"Desert Laser",
"Desert Laser Panel",
"Quarry Mill Eraser and Dots 6",
"Quarry Mill Eraser and Squares 8",
@@ -63,34 +63,34 @@ class StaticWitnessLocations:
"Quarry Boathouse Stars & Eraser & Shapers 2",
"Quarry Boathouse Stars & Eraser & Shapers 5",
"Quarry Discard",
"Quarry Laser",
"Quarry Laser Panel",
"Shadows Lower Avoid 8",
"Shadows Environmental Avoid 8",
"Shadows Follow 5",
"Shadows Laser",
"Shadows Laser Panel",
"Keep Hedge Maze 4",
"Keep Pressure Plates 4",
"Keep Discard",
"Keep Laser Hedges",
"Keep Laser Pressure Plates",
"Keep Laser Panel Hedges",
"Keep Laser Panel Pressure Plates",
"Shipwreck Vault Box",
"Shipwreck Discard",
"Monastery Rhombic Avoid 3",
"Monastery Branch Follow 2",
"Monastery Laser",
"Monastery Laser Panel",
"Town Cargo Box Discard",
"Town Hexagonal Reflection",
"Town Square Avoid",
"Town Church Lattice",
"Town Rooftop Discard",
"Town Symmetry Squares 5 + Dots",
"Town Full Dot Grid Shapers 5",
"Town Shapers & Dots & Eraser",
"Town Laser",
"Town Laser Panel",
"Theater Discard",
@@ -98,7 +98,7 @@ class StaticWitnessLocations:
"Jungle Waves 3",
"Jungle Waves 7",
"Jungle Popup Wall 6",
"Jungle Laser",
"Jungle Laser Panel",
"River Vault Box",
@@ -106,7 +106,7 @@ class StaticWitnessLocations:
"Bunker Drawn Squares 9",
"Bunker Drawn Squares through Tinted Glass 3",
"Bunker Drop-Down Door Squares 2",
"Bunker Laser",
"Bunker Laser Panel",
"Swamp Seperatable Shapers 6",
"Swamp Combinable Shapers 8",
@@ -117,7 +117,7 @@ class StaticWitnessLocations:
"Swamp Red Underwater Negative Shapers 4",
"Swamp More Rotated Shapers 4",
"Swamp Blue Underwater Negative Shapers 5",
"Swamp Laser",
"Swamp Laser Panel",
"Treehouse Yellow Bridge 9",
"Treehouse First Purple Bridge 5",
@@ -125,21 +125,12 @@ class StaticWitnessLocations:
"Treehouse Green Bridge 7",
"Treehouse Green Bridge Discard",
"Treehouse Left Orange Bridge 15",
"Treehouse Burned House Discard",
"Treehouse Burnt House Discard",
"Treehouse Right Orange Bridge 12",
"Treehouse Laser",
"Treehouse Laser Panel",
"Mountaintop Discard",
"Mountaintop Vault Box",
"Inside Mountain Obscured Vision 5",
"Inside Mountain Moving Background 7",
"Inside Mountain Physically Obstructed 3",
"Inside Mountain Angled Inside Trash 2",
"Inside Mountain Color Cycle 5",
"Inside Mountain Same Solution 6",
"Inside Mountain Elevator Discard",
"Inside Mountain Giant Puzzle",
}
UNCOMMON_LOCATIONS = {
@@ -156,35 +147,53 @@ class StaticWitnessLocations:
"Swamp Underwater Back Optional",
}
HARD_LOCATIONS = {
"Inside Mountain Secret Area Dot Grid Triangles 4",
"Inside Mountain Secret Area Symmetry Triangles",
"Inside Mountain Secret Area Stars & Squares and Triangles 2",
"Inside Mountain Secret Area Shapers and Triangles 2",
"Inside Mountain Secret Area Symmetry Shapers",
"Inside Mountain Secret Area Broken and Negative Shapers",
"Inside Mountain Secret Area Broken Shapers",
CAVES_LOCATIONS = {
"Inside Mountain Caves Dot Grid Triangles 4",
"Inside Mountain Caves Symmetry Triangles",
"Inside Mountain Caves Stars & Squares and Triangles 2",
"Inside Mountain Caves Shapers and Triangles 2",
"Inside Mountain Caves Symmetry Shapers",
"Inside Mountain Caves Broken and Negative Shapers",
"Inside Mountain Caves Broken Shapers",
"Inside Mountain Secret Area Rainbow Squares",
"Inside Mountain Secret Area Squares & Stars and Colored Eraser",
"Inside Mountain Secret Area Rotated Broken Shapers",
"Inside Mountain Secret Area Stars and Squares",
"Inside Mountain Secret Area Lone Pillar",
"Inside Mountain Secret Area Wooden Beam Shapers",
"Inside Mountain Secret Area Wooden Beam Squares and Shapers",
"Inside Mountain Secret Area Wooden Beam Stars and Squares",
"Inside Mountain Secret Area Wooden Beam Shapers and Stars",
"Inside Mountain Secret Area Upstairs Invisible Dots 8",
"Inside Mountain Secret Area Upstairs Invisible Dot Symmetry 3",
"Inside Mountain Secret Area Upstairs Dot Grid Negative Shapers",
"Inside Mountain Secret Area Upstairs Dot Grid Rotated Shapers",
"Inside Mountain Caves Rainbow Squares",
"Inside Mountain Caves Squares & Stars and Colored Eraser",
"Inside Mountain Caves Rotated Broken Shapers",
"Inside Mountain Caves Stars and Squares",
"Inside Mountain Caves Lone Pillar",
"Inside Mountain Caves Wooden Beam Shapers",
"Inside Mountain Caves Wooden Beam Squares and Shapers",
"Inside Mountain Caves Wooden Beam Stars and Squares",
"Inside Mountain Caves Wooden Beam Shapers and Stars",
"Inside Mountain Caves Upstairs Invisible Dots 8",
"Inside Mountain Caves Upstairs Invisible Dot Symmetry 3",
"Inside Mountain Caves Upstairs Dot Grid Negative Shapers",
"Inside Mountain Caves Upstairs Dot Grid Rotated Shapers",
"Challenge Vault Box",
"Theater Walkway Vault Box",
"Inside Mountain Bottom Layer Discard",
"Theater Challenge Video",
}
MOUNTAIN_UNREACHABLE_FROM_BEHIND = {
"Mountaintop Trap Door Triple Exit",
"Inside Mountain Obscured Vision 5",
"Inside Mountain Moving Background 7",
"Inside Mountain Physically Obstructed 3",
"Inside Mountain Angled Inside Trash 2",
"Inside Mountain Color Cycle 5",
"Inside Mountain Same Solution 6",
}
MOUNTAIN_REACHABLE_FROM_BEHIND = {
"Inside Mountain Elevator Discard",
"Inside Mountain Giant Puzzle",
"Inside Mountain Final Room Left Pillar 4",
"Inside Mountain Final Room Right Pillar 4",
}
ALL_LOCATIONS_TO_ID = dict()
@staticmethod
@@ -193,12 +202,7 @@ class StaticWitnessLocations:
Calculates the location ID for any given location
"""
panel_offset = StaticWitnessLogic.CHECKS_BY_HEX[chex]["idOffset"]
type_offset = StaticWitnessLocations.TYPE_OFFSETS[
StaticWitnessLogic.CHECKS_BY_HEX[chex]["panelType"]
]
return StaticWitnessLocations.ID_START + panel_offset + type_offset
return StaticWitnessLogic.CHECKS_BY_HEX[chex]["id"]
@staticmethod
def get_event_name(panel_hex):
@@ -213,6 +217,7 @@ class StaticWitnessLocations:
all_loc_to_id = {
panel_obj["checkName"]: self.get_id(chex)
for chex, panel_obj in StaticWitnessLogic.CHECKS_BY_HEX.items()
if panel_obj["id"]
}
all_loc_to_id = dict(
@@ -229,12 +234,34 @@ class WitnessPlayerLocations:
"""
def __init__(self, world, player, player_logic: WitnessPlayerLogic):
"""Defines locations AFTER logic changes due to options"""
self.PANEL_TYPES_TO_SHUFFLE = {"General", "Laser"}
self.CHECK_LOCATIONS = (
StaticWitnessLocations.GENERAL_LOCATIONS
)
"""Defines locations AFTER logic changes due to options"""
doors = get_option_value(world, player, "shuffle_doors")
earlyutm = is_option_enabled(world, player, "early_secret_area")
victory = get_option_value(world, player, "victory_condition")
lasers = get_option_value(world, player, "challenge_lasers")
laser_shuffle = get_option_value(world, player, "shuffle_lasers")
postgame = set()
postgame = postgame | StaticWitnessLocations.CAVES_LOCATIONS
postgame = postgame | StaticWitnessLocations.MOUNTAIN_REACHABLE_FROM_BEHIND
postgame = postgame | StaticWitnessLocations.MOUNTAIN_UNREACHABLE_FROM_BEHIND
self.CHECK_LOCATIONS = self.CHECK_LOCATIONS | postgame
if earlyutm or doors >= 2 or (victory == 1 and (lasers <= 11 or laser_shuffle)):
postgame -= StaticWitnessLocations.CAVES_LOCATIONS
if doors >= 2:
postgame -= StaticWitnessLocations.MOUNTAIN_REACHABLE_FROM_BEHIND
if victory != 2:
postgame -= StaticWitnessLocations.MOUNTAIN_UNREACHABLE_FROM_BEHIND
if is_option_enabled(world, player, "shuffle_discarded_panels"):
self.PANEL_TYPES_TO_SHUFFLE.add("Discard")
@@ -245,18 +272,11 @@ class WitnessPlayerLocations:
if is_option_enabled(world, player, "shuffle_uncommon"):
self.CHECK_LOCATIONS = self.CHECK_LOCATIONS | StaticWitnessLocations.UNCOMMON_LOCATIONS
if is_option_enabled(world, player, "shuffle_hard"):
self.CHECK_LOCATIONS = self.CHECK_LOCATIONS | StaticWitnessLocations.HARD_LOCATIONS
if is_option_enabled(world, player, "shuffle_symbols") and is_option_enabled(world, player, "shuffle_doors"):
if is_option_enabled(world, player, "disable_non_randomized_puzzles"):
# This particular combination of logic settings leads to logic so restrictive that generation can fail
# Hence, we add some extra sphere 0 locations
self.CHECK_LOCATIONS = self.CHECK_LOCATIONS | StaticWitnessLocations.EXTRA_LOCATIONS
self.CHECK_LOCATIONS = self.CHECK_LOCATIONS | player_logic.ADDED_CHECKS
if not is_option_enabled(world, player, "shuffle_postgame"):
self.CHECK_LOCATIONS -= postgame
self.CHECK_LOCATIONS = self.CHECK_LOCATIONS - {
StaticWitnessLogic.CHECKS_BY_HEX[check_hex]["checkName"]
for check_hex in player_logic.COMPLETELY_DISABLED_CHECKS
@@ -272,7 +292,7 @@ class WitnessPlayerLocations:
)
event_locations = {
p for p in player_logic.NECESSARY_EVENT_PANELS
p for p in player_logic.EVENT_PANELS
}
self.EVENT_LOCATION_TABLE = {

View File

@@ -18,22 +18,15 @@ When the world has parsed its options, a second function is called to finalize t
import copy
from BaseClasses import MultiWorld
from .static_logic import StaticWitnessLogic
from .utils import define_new_region, get_disable_unrandomized_list, parse_lambda, get_early_utm_list
from .utils import define_new_region, get_disable_unrandomized_list, parse_lambda, get_early_utm_list, \
get_symbol_shuffle_list, get_door_panel_shuffle_list, get_doors_complex_list, get_doors_max_list, \
get_doors_simple_list, get_laser_shuffle
from .Options import is_option_enabled, get_option_value, the_witness_options
class WitnessPlayerLogic:
"""WITNESS LOGIC CLASS"""
def update_door_dict(self, panel_hex):
item_id = StaticWitnessLogic.ALL_DOOR_ITEM_IDS_BY_HEX.get(panel_hex)
if item_id is None:
return
self.DOOR_DICT_FOR_CLIENT[panel_hex] = item_id
self.DOOR_CONNECTIONS_TO_SEVER.update(StaticWitnessLogic.CONNECTIONS_TO_SEVER_BY_DOOR_HEX[panel_hex])
def reduce_req_within_region(self, panel_hex):
"""
Panels in this game often only turn on when other panels are solved.
@@ -43,35 +36,42 @@ class WitnessPlayerLogic:
Panels outside of the same region will still be checked manually.
"""
these_items = self.DEPENDENT_REQUIREMENTS_BY_HEX[panel_hex]["items"]
check_obj = StaticWitnessLogic.CHECKS_BY_HEX[panel_hex]
real_items = {item[0] for item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME}
these_items = frozenset({frozenset()})
if check_obj["id"]:
these_items = self.DEPENDENT_REQUIREMENTS_BY_HEX[panel_hex]["items"]
these_items = frozenset({
subset.intersection(real_items)
subset.intersection(self.PROG_ITEMS_ACTUALLY_IN_THE_GAME)
for subset in these_items
})
if panel_hex in self.DOOR_ITEMS_BY_ID:
door_items = frozenset({frozenset([item]) for item in self.DOOR_ITEMS_BY_ID[panel_hex]})
all_options = set()
for items_option in these_items:
for dependentItem in door_items:
all_options.add(items_option.union(dependentItem))
return frozenset(all_options)
these_panels = self.DEPENDENT_REQUIREMENTS_BY_HEX[panel_hex]["panels"]
if StaticWitnessLogic.DOOR_NAMES_BY_HEX.get(panel_hex) in real_items:
self.update_door_dict(panel_hex)
these_panels = frozenset({frozenset()})
if these_panels == frozenset({frozenset()}):
return these_items
all_options = set()
check_obj = StaticWitnessLogic.CHECKS_BY_HEX[panel_hex]
for option in these_panels:
dependent_items_for_option = frozenset({frozenset()})
for option_panel in option:
new_items = set()
dep_obj = StaticWitnessLogic.CHECKS_BY_HEX.get(option_panel)
if option_panel in {"7 Lasers", "11 Lasers"}:
new_items = frozenset({frozenset([option_panel])})
# If a panel turns on when a panel in a different region turns on,
@@ -101,8 +101,34 @@ class WitnessPlayerLogic:
return frozenset(all_options)
def make_single_adjustment(self, adj_type, line):
from . import StaticWitnessItems
"""Makes a single logic adjustment based on additional logic file"""
if adj_type == "Items":
if line not in StaticWitnessItems.ALL_ITEM_TABLE:
raise RuntimeError("Item \"" + line + "\" does not exit.")
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.add(line)
if line in StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT:
panel_hexes = StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT[line][2]
for panel_hex in panel_hexes:
self.DOOR_ITEMS_BY_ID.setdefault(panel_hex, set()).add(line)
return
if adj_type == "Remove Items":
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.discard(line)
if line in StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT:
panel_hexes = StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT[line][2]
for panel_hex in panel_hexes:
if panel_hex in self.DOOR_ITEMS_BY_ID:
self.DOOR_ITEMS_BY_ID[panel_hex].discard(line)
if adj_type == "Starting Inventory":
self.STARTING_INVENTORY.add(line)
if adj_type == "Event Items":
line_split = line.split(" - ")
hex_set = line_split[1].split(",")
@@ -130,18 +156,20 @@ class WitnessPlayerLogic:
if adj_type == "Requirement Changes":
line_split = line.split(" - ")
required_items = parse_lambda(line_split[2])
items_actually_in_the_game = {item[0] for item in StaticWitnessLogic.ALL_SYMBOL_ITEMS}
required_items = frozenset(
subset.intersection(items_actually_in_the_game)
for subset in required_items
)
requirement = {
"panels": parse_lambda(line_split[1]),
"items": required_items
}
if len(line_split) > 2:
required_items = parse_lambda(line_split[2])
items_actually_in_the_game = {item[0] for item in StaticWitnessLogic.ALL_SYMBOL_ITEMS}
required_items = frozenset(
subset.intersection(items_actually_in_the_game)
for subset in required_items
)
requirement["items"] = required_items
self.DEPENDENT_REQUIREMENTS_BY_HEX[line_split[0]] = requirement
return
@@ -151,11 +179,6 @@ class WitnessPlayerLogic:
self.COMPLETELY_DISABLED_CHECKS.add(panel_hex)
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME = {
item for item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME
if item[0] != StaticWitnessLogic.DOOR_NAMES_BY_HEX.get(panel_hex)
}
return
if adj_type == "Region Changes":
@@ -189,18 +212,25 @@ class WitnessPlayerLogic:
adjustment_linesets_in_order.append(get_disable_unrandomized_list())
if is_option_enabled(world, player, "shuffle_symbols") or "shuffle_symbols" not in the_witness_options.keys():
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.update(StaticWitnessLogic.ALL_SYMBOL_ITEMS)
adjustment_linesets_in_order.append(get_symbol_shuffle_list())
if is_option_enabled(world, player, "shuffle_doors"):
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.update(StaticWitnessLogic.ALL_DOOR_ITEMS)
if get_option_value(world, player, "shuffle_doors") == 1:
adjustment_linesets_in_order.append(get_door_panel_shuffle_list())
if get_option_value(world, player, "shuffle_doors") == 2:
adjustment_linesets_in_order.append(get_doors_simple_list())
if get_option_value(world, player, "shuffle_doors") == 3:
adjustment_linesets_in_order.append(get_doors_complex_list())
if get_option_value(world, player, "shuffle_doors") == 4:
adjustment_linesets_in_order.append(get_doors_max_list())
if is_option_enabled(world, player, "early_secret_area"):
adjustment_linesets_in_order.append(get_early_utm_list())
else:
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME = {
item for item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME
if item[0] != "Mountaintop River Shape Power On"
}
if is_option_enabled(world, player, "shuffle_lasers"):
adjustment_linesets_in_order.append(get_laser_shuffle())
for adjustment_lineset in adjustment_linesets_in_order:
current_adjustment_type = None
@@ -233,62 +263,32 @@ class WitnessPlayerLogic:
pair = (name, self.EVENT_ITEM_NAMES[panel])
return pair
def _regions_are_adjacent(self, region1, region2):
for connection in self.CONNECTIONS_BY_REGION_NAME[region1]:
if connection[0] == region2:
return True
for connection in self.CONNECTIONS_BY_REGION_NAME[region2]:
if connection[0] == region1:
return True
return False
def make_event_panel_lists(self):
"""
Special event panel data structures
"""
for region_conn in self.CONNECTIONS_BY_REGION_NAME.values():
for region_and_option in region_conn:
for panelset in region_and_option[1]:
for panel in panelset:
self.EVENT_PANELS_FROM_REGIONS.add(panel)
self.ALWAYS_EVENT_NAMES_BY_HEX[self.VICTORY_LOCATION] = "Victory"
self.ORIGINAL_EVENT_PANELS.update(self.EVENT_PANELS_FROM_PANELS)
self.ORIGINAL_EVENT_PANELS.update(self.EVENT_PANELS_FROM_REGIONS)
for region_name, connections in self.CONNECTIONS_BY_REGION_NAME.items():
for connection in connections:
for panel_req in connection[1]:
for panel in panel_req:
if panel == "TrueOneWay":
continue
for panel in self.EVENT_PANELS_FROM_REGIONS:
for region_name, region in StaticWitnessLogic.ALL_REGIONS_BY_NAME.items():
for connection in self.CONNECTIONS_BY_REGION_NAME[region_name]:
connected_r = connection[0]
if connected_r not in StaticWitnessLogic.ALL_REGIONS_BY_NAME:
continue
if region_name == "Boat" or connected_r == "Boat":
continue
connected_r = StaticWitnessLogic.ALL_REGIONS_BY_NAME[connected_r]
if not any([panel in option for option in connection[1]]):
continue
if panel not in region["panels"] | connected_r["panels"]:
self.NECESSARY_EVENT_PANELS.add(panel)
if StaticWitnessLogic.CHECKS_BY_HEX[panel]["region"]["name"] != region_name:
self.EVENT_PANELS_FROM_REGIONS.add(panel)
for event_panel in self.EVENT_PANELS_FROM_PANELS:
for panel, panel_req in self.REQUIREMENTS_BY_HEX.items():
if any([event_panel in item_set for item_set in panel_req]):
region1 = StaticWitnessLogic.CHECKS_BY_HEX[panel]["region"]["name"]
region2 = StaticWitnessLogic.CHECKS_BY_HEX[event_panel]["region"]["name"]
if not self._regions_are_adjacent(region1, region2):
self.NECESSARY_EVENT_PANELS.add(event_panel)
self.EVENT_PANELS.update(self.EVENT_PANELS_FROM_PANELS)
self.EVENT_PANELS.update(self.EVENT_PANELS_FROM_REGIONS)
for always_hex, always_item in self.ALWAYS_EVENT_NAMES_BY_HEX.items():
self.ALWAYS_EVENT_HEX_CODES.add(always_hex)
self.NECESSARY_EVENT_PANELS.add(always_hex)
self.EVENT_PANELS.add(always_hex)
self.EVENT_ITEM_NAMES[always_hex] = always_item
for panel in self.NECESSARY_EVENT_PANELS:
for panel in self.EVENT_PANELS:
pair = self.make_event_item_pair(panel)
self.EVENT_ITEM_PAIRS[pair[0]] = pair[1]
@@ -297,8 +297,8 @@ class WitnessPlayerLogic:
self.EVENT_PANELS_FROM_REGIONS = set()
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME = set()
self.DOOR_DICT_FOR_CLIENT = dict()
self.DOOR_CONNECTIONS_TO_SEVER = set()
self.DOOR_ITEMS_BY_ID = dict()
self.STARTING_INVENTORY = set()
self.CONNECTIONS_BY_REGION_NAME = copy.copy(StaticWitnessLogic.STATIC_CONNECTIONS_BY_REGION_NAME)
self.DEPENDENT_REQUIREMENTS_BY_HEX = copy.copy(StaticWitnessLogic.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX)
@@ -306,8 +306,7 @@ class WitnessPlayerLogic:
# Determining which panels need to be events is a difficult process.
# At the end, we will have EVENT_ITEM_PAIRS for all the necessary ones.
self.ORIGINAL_EVENT_PANELS = set()
self.NECESSARY_EVENT_PANELS = set()
self.EVENT_PANELS = set()
self.EVENT_ITEM_PAIRS = dict()
self.ALWAYS_EVENT_HEX_CODES = set()
self.COMPLETELY_DISABLED_CHECKS = set()
@@ -320,42 +319,63 @@ class WitnessPlayerLogic:
"0x00037": "Monastery Branch Panels Activate",
"0x0A079": "Access to Bunker Laser",
"0x0A3B5": "Door to Tutorial Discard Opens",
"0x00139": "Keep Hedges 2 Turns On",
"0x019DC": "Keep Hedges 3 Turns On",
"0x019E7": "Keep Hedges 4 Turns On",
"0x01D3F": "Keep Laser Panel (Pressure Plates) Activates",
"0x09F7F": "Mountain Access",
"0x0367C": "Quarry Laser Mill Requirement Met",
"0x009A1": "Swamp Rotating Bridge Near Side",
"0x009A1": "Swamp Rotated Shapers 1 Activates",
"0x00006": "Swamp Cyan Water Drains",
"0x00990": "Swamp Broken Shapers 1 Activates",
"0x0A8DC": "Lower Avoid 6 Activates",
"0x0000A": "Swamp More Rotated Shapers 1 Access",
"0x09ED8": "Inside Mountain Second Layer Both Light Bridges Solved",
"0x09E86": "Inside Mountain Second Layer Blue Bridge Access",
"0x09ED8": "Inside Mountain Second Layer Yellow Bridge Access",
"0x0A3D0": "Quarry Laser Boathouse Requirement Met",
"0x00596": "Swamp Red Water Drains",
"0x28B39": "Town Tower 4th Door Opens",
"0x00E3A": "Swamp Purple Water Drains",
"0x0343A": "Door to Symmetry Island Powers On",
"0xFFF00": "Inside Mountain Bottom Layer Discard Turns On"
"0xFFF00": "Inside Mountain Bottom Layer Discard Turns On",
"0x17CA6": "All Boat Panels Turn On",
"0x17CDF": "All Boat Panels Turn On",
"0x09DB8": "All Boat Panels Turn On",
"0x17C95": "All Boat Panels Turn On",
"0x03BB0": "Town Church Lattice Vision From Outside",
"0x28AC1": "Town Shapers & Dots & Eraser Turns On",
"0x28A69": "Town Tower 1st Door Opens",
"0x28ACC": "Town Tower 2nd Door Opens",
"0x28AD9": "Town Tower 3rd Door Opens",
"0x28B39": "Town Tower 4th Door Opens",
"0x03675": "Quarry Mill Ramp Activation From Above",
"0x03679": "Quarry Mill Lift Lowering While Standing On It",
"0x2FAF6": "Tutorial Gate Secret Solution Knowledge",
"0x079DF": "Town Hexagonal Reflection Turns On",
"0x17DA2": "Right Orange Bridge Fully Extended",
"0x19B24": "Shadows Lower Avoid Patterns Visible",
"0x2700B": "Open Door to Treehouse Laser House",
"0x00055": "Orchard Apple Trees 4 Turns On",
}
self.ALWAYS_EVENT_NAMES_BY_HEX = {
"0x0360D": "Symmetry Laser Activation",
"0x03608": "Desert Laser Activation",
"0x00509": "Symmetry Laser Activation",
"0x012FB": "Desert Laser Activation",
"0x09F98": "Desert Laser Redirection",
"0x03612": "Quarry Laser Activation",
"0x19650": "Shadows Laser Activation",
"0x0360E": "Keep Laser Activation",
"0x03317": "Keep Laser Activation",
"0x17CA4": "Monastery Laser Activation",
"0x032F5": "Town Laser Activation",
"0x03616": "Jungle Laser Activation",
"0x09DE0": "Bunker Laser Activation",
"0x03615": "Swamp Laser Activation",
"0x03613": "Treehouse Laser Activation",
"0x01539": "Quarry Laser Activation",
"0x181B3": "Shadows Laser Activation",
"0x014BB": "Keep Laser Activation",
"0x17C65": "Monastery Laser Activation",
"0x032F9": "Town Laser Activation",
"0x00274": "Jungle Laser Activation",
"0x0C2B2": "Bunker Laser Activation",
"0x00BF6": "Swamp Laser Activation",
"0x028A4": "Treehouse Laser Activation",
"0x03535": "Shipwreck Video Pattern Knowledge",
"0x03542": "Mountain Video Pattern Knowledge",
"0x0339E": "Desert Video Pattern Knowledge",
"0x03481": "Tutorial Video Pattern Knowledge",
"0x03702": "Jungle Video Pattern Knowledge",
"0x2FAF6": "Theater Walkway Video Pattern Knowledge",
"0x0356B": "Challenge Video Pattern Knowledge",
"0x09F7F": "Mountaintop Trap Door Turns On",
"0x17C34": "Mountain Access",
}

View File

@@ -33,6 +33,10 @@ class WitnessRegions:
source_region = world.get_region(source, player)
target_region = world.get_region(target, player)
#print(source_region)
#print(target_region)
#print("---")
connection = Entrance(
player,
source + " to " + target + " via " + str(panel_hex_to_solve_set),
@@ -76,10 +80,17 @@ class WitnessRegions:
for connection in player_logic.CONNECTIONS_BY_REGION_NAME[region_name]:
if connection[0] == "Entry":
continue
self.connect(world, player, region_name,
connection[0], player_logic, connection[1])
self.connect(world, player, connection[0],
region_name, player_logic, connection[1])
if connection[1] == frozenset({frozenset(["TrueOneWay"])}):
self.connect(world, player, region_name, connection[0], player_logic, frozenset({frozenset()}))
continue
for subset in connection[1]:
if all({panel in player_logic.DOOR_ITEMS_BY_ID for panel in subset}):
if all({StaticWitnessLogic.CHECKS_BY_HEX[panel]["id"] is None for panel in subset}):
self.connect(world, player, connection[0], region_name, player_logic, frozenset({subset}))
self.connect(world, player, region_name, connection[0], player_logic, connection[1])
world.get_entrance("The Splashscreen?", player).connect(
world.get_region('First Hallway', player)

View File

@@ -22,6 +22,21 @@ class WitnessLogic(LogicMixin):
def _witness_has_lasers(self, world, player: int, amount: int) -> bool:
lasers = 0
if is_option_enabled(world, player, "shuffle_lasers"):
lasers += int(self.has("Symmetry Laser", player))
lasers += int(self.has("Desert Laser", player)
and self.has("Desert Laser Redirection", player))
lasers += int(self.has("Town Laser", player))
lasers += int(self.has("Monastery Laser", player))
lasers += int(self.has("Keep Laser", player))
lasers += int(self.has("Quarry Laser", player))
lasers += int(self.has("Treehouse Laser", player))
lasers += int(self.has("Jungle Laser", player))
lasers += int(self.has("Bunker Laser", player))
lasers += int(self.has("Swamp Laser", player))
lasers += int(self.has("Shadows Laser", player))
return lasers >= amount
lasers += int(self.has("Symmetry Laser Activation", player))
lasers += int(self.has("Desert Laser Activation", player)
and self.has("Desert Laser Redirection", player))
@@ -48,11 +63,8 @@ class WitnessLogic(LogicMixin):
if (check_name + " Solved" in locat.EVENT_LOCATION_TABLE
and not self.has(player_logic.EVENT_ITEM_PAIRS[check_name + " Solved"], player)):
return False
if panel not in player_logic.ORIGINAL_EVENT_PANELS and not self.can_reach(check_name, "Location", player):
return False
if (panel in player_logic.ORIGINAL_EVENT_PANELS
and check_name + " Solved" not in locat.EVENT_LOCATION_TABLE
and not self._witness_safe_manual_panel_check(panel, world, player, player_logic, locat)):
if (check_name + " Solved" not in locat.EVENT_LOCATION_TABLE
and not self._witness_meets_item_requirements(panel, world, player, player_logic, locat)):
return False
return True
@@ -79,8 +91,10 @@ class WitnessLogic(LogicMixin):
if not self._witness_has_lasers(world, player, get_option_value(world, player, "challenge_lasers")):
valid_option = False
break
elif item in player_logic.ORIGINAL_EVENT_PANELS:
valid_option = self._witness_can_solve_panel(item, world, player, player_logic, locat)
elif item in player_logic.EVENT_PANELS:
if not self._witness_can_solve_panel(item, world, player, player_logic, locat):
valid_option = False
break
elif not self.has(item, player):
valid_option = False
break
@@ -90,24 +104,6 @@ class WitnessLogic(LogicMixin):
return False
def _witness_safe_manual_panel_check(self, panel, world, player, player_logic: WitnessPlayerLogic, locat):
"""
nested can_reach can cause problems, but only if the region being
checked is neither of the two original regions from the first
can_reach.
A nested can_reach is okay here because the only panels this
function is called on are panels that exist on either side of all
connections they are required for.
The spoiler log looks so much nicer this way,
it gets rid of a bunch of event items, only leaving a couple. :)
"""
region = StaticWitnessLogic.CHECKS_BY_HEX[panel]["region"]["name"]
return (
self._witness_meets_item_requirements(panel, world, player, player_logic, locat)
and self.can_reach(region, "Region", player)
)
def _witness_can_solve_panels(self, panel_hex_to_solve_set, world, player, player_logic: WitnessPlayerLogic, locat):
"""
Checks whether a set of panels can be solved.
@@ -120,7 +116,12 @@ class WitnessLogic(LogicMixin):
valid_option = True
for panel in option:
if not self._witness_can_solve_panel(panel, world, player, player_logic, locat):
if panel in player_logic.DOOR_ITEMS_BY_ID:
if all({not self.has(item, player) for item in player_logic.DOOR_ITEMS_BY_ID[panel]}):
valid_option = False
break
elif not self._witness_can_solve_panel(panel, world, player, player_logic, locat):
valid_option = False
break

View File

@@ -1,18 +1,16 @@
Event Items:
Shadows Laser Activation - 0x00021,0x17D28,0x17C71
Keep Laser Activation - 0x03317
Bunker Laser Activation - 0x00061,0x17D01,0x17C42
Monastery Laser Activation - 0x00A5B,0x17CE7,0x17FA9,0x17CA4
Town Tower 4th Door Opens - 0x17CFB,0x3C12B,0x00B8D,0x17CF7
Monastery Laser Activation - 0x00A5B,0x17CE7,0x17FA9,0x17CA4
Bunker Laser Activation - 0x00061,0x17D01,0x17C42
Shadows Laser Activation - 0x00021,0x17D28,0x17C71
Requirement Changes:
0x17CA4 - True - True
0x28B39 - 0x2896A - Reflection
0x17C65 - 0x00A5B | 0x17CE7 | 0x17FA9 | 0x17CA4
0x0C2B2 - 0x00061 | 0x17D01 | 0x17C42
0x181B3 - 0x00021 | 0x17D28 | 0x17C71
0x28B39 - True - Reflection
0x17CAB - True - True
Region Changes:
Quarry (Quarry) - Outside Quarry - 0x17C09 - Quarry Mill - 0x275ED - Quarry Mill - 0x17CAC
Disabled Locations:
0x03505 (Tutorial Gate Close)
0x0C335 (Tutorial Pillar)

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,9 @@
Items:
Caves Exits to Main Island
Starting Inventory:
Caves Exits to Main Island
Remove Items:
Caves Mountain Shortcut
Caves Swamp Shortcut

View File

@@ -0,0 +1,12 @@
Items:
Symmetry Laser
Desert Laser
Keep Laser
Shadows Laser
Quarry Laser
Town Laser
Swamp Laser
Jungle Laser
Bunker Laser
Monastery Laser
Treehouse Laser

View File

@@ -0,0 +1,14 @@
Items:
Dots
Colored Dots
Sound Dots
Symmetry
Triangles
Eraser
Shapers
Rotated Shapers
Negative Shapers
Stars
Stars + Same Colored Symbol
Black/White Squares
Colored Squares

View File

@@ -5,12 +5,11 @@ from .utils import define_new_region, parse_lambda
class StaticWitnessLogic:
ALL_SYMBOL_ITEMS = set()
ALL_DOOR_ITEMS = set()
ALL_DOOR_ITEMS_AS_DICT = dict()
ALL_USEFULS = set()
ALL_TRAPS = set()
ALL_BOOSTS = set()
ALL_DOOR_ITEM_IDS_BY_HEX = dict()
DOOR_NAMES_BY_HEX = dict()
ALL_DOOR_ITEMS = set()
CONNECTIONS_TO_SEVER_BY_DOOR_HEX = dict()
EVENT_PANELS_FROM_REGIONS = set()
@@ -47,35 +46,23 @@ class StaticWitnessLogic:
if line == "Usefuls:":
current_set = self.ALL_USEFULS
continue
if line == "Doors:":
current_set = self.ALL_DOOR_ITEMS
continue
if line == "":
continue
line_split = line.split(" - ")
if current_set is not self.ALL_USEFULS:
current_set.add((line_split[1], int(line_split[0])))
else:
if current_set is self.ALL_USEFULS:
current_set.add((line_split[1], int(line_split[0]), line_split[2] == "True"))
elif current_set is self.ALL_DOOR_ITEMS:
new_door = (line_split[1], int(line_split[0]), frozenset(line_split[2].split(",")))
current_set.add(new_door)
self.ALL_DOOR_ITEMS_AS_DICT[line_split[1]] = new_door
else:
current_set.add((line_split[1], int(line_split[0])))
path = os.path.join(os.path.dirname(__file__), "Door_Shuffle.txt")
with open(path, "r", encoding="utf-8") as file:
for line in file.readlines():
line = line.strip()
line_split = line.split(" - ")
hex_set_split = line_split[1].split(",")
sever_list = line_split[2].split(",")
sever_set = {sever_panel for sever_panel in sever_list if sever_panel != "None"}
for door_hex in hex_set_split:
self.ALL_DOOR_ITEM_IDS_BY_HEX[door_hex] = int(line_split[0])
self.CONNECTIONS_TO_SEVER_BY_DOOR_HEX[door_hex] = sever_set
if len(line_split) > 3:
self.DOOR_NAMES_BY_HEX[door_hex] = line_split[3]
def read_logic_file(self):
"""
Reads the logic file and does the initial population of data structures
@@ -84,10 +71,7 @@ class StaticWitnessLogic:
with open(path, "r", encoding="utf-8") as file:
current_region = dict()
discard_ids = 0
normal_panel_ids = 0
vault_ids = 0
laser_ids = 0
counter = 0
for line in file.readlines():
line = line.strip()
@@ -95,7 +79,7 @@ class StaticWitnessLogic:
if line == "":
continue
if line[0] != "0":
if line[-1] == ":":
new_region_and_connections = define_new_region(line)
current_region = new_region_and_connections[0]
region_name = current_region["name"]
@@ -105,12 +89,33 @@ class StaticWitnessLogic:
line_split = line.split(" - ")
location_id = line_split.pop(0)
check_name_full = line_split.pop(0)
check_hex = check_name_full[0:7]
check_name = check_name_full[9:-1]
required_panel_lambda = line_split.pop(0)
if location_id == "Door" or location_id == "Laser":
self.CHECKS_BY_HEX[check_hex] = {
"checkName": current_region["shortName"] + " " + check_name,
"checkHex": check_hex,
"region": current_region,
"id": None,
"panelType": location_id
}
self.CHECKS_BY_NAME[self.CHECKS_BY_HEX[check_hex]["checkName"]] = self.CHECKS_BY_HEX[check_hex]
self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX[check_hex] = {
"panels": parse_lambda(required_panel_lambda)
}
current_region["panels"].add(check_hex)
continue
required_item_lambda = line_split.pop(0)
laser_names = {
@@ -123,53 +128,14 @@ class StaticWitnessLogic:
if "Discard" in check_name:
location_type = "Discard"
location_id = discard_ids
discard_ids += 1
elif is_vault_or_video or check_name == "Tutorial Gate Close":
location_type = "Vault"
location_id = vault_ids
vault_ids += 1
elif check_name in laser_names:
location_type = "Laser"
location_id = laser_ids
laser_ids += 1
else:
location_type = "General"
if check_hex == "0x012D7": # Compatibility
normal_panel_ids += 1
if check_hex == "0x17E07": # Compatibility
location_id = 112
elif check_hex == "0xFFF00":
location_id = 800
else:
location_id = normal_panel_ids
normal_panel_ids += 1
required_items = parse_lambda(required_item_lambda)
items_actually_in_the_game = {item[0] for item in self.ALL_SYMBOL_ITEMS}
required_items = set(
subset.intersection(items_actually_in_the_game)
for subset in required_items
)
doors_in_the_game = self.ALL_DOOR_ITEM_IDS_BY_HEX.keys()
if check_hex in doors_in_the_game:
door_name = current_region["shortName"] + " " + check_name + " Power On"
if check_hex in self.DOOR_NAMES_BY_HEX.keys():
door_name = self.DOOR_NAMES_BY_HEX[check_hex]
required_items = set(
subset.union(frozenset({door_name}))
for subset in required_items
)
self.ALL_DOOR_ITEMS.add(
(door_name, self.ALL_DOOR_ITEM_IDS_BY_HEX[check_hex])
)
required_items = frozenset(required_items)
@@ -182,7 +148,7 @@ class StaticWitnessLogic:
"checkName": current_region["shortName"] + " " + check_name,
"checkHex": check_hex,
"region": current_region,
"idOffset": location_id,
"id": int(location_id),
"panelType": location_type
}

View File

@@ -98,9 +98,39 @@ def get_adjustment_file(adjustment_file):
@cache_argsless
def get_disable_unrandomized_list():
return get_adjustment_file("Disable_Unrandomized.txt")
return get_adjustment_file("settings/Disable_Unrandomized.txt")
@cache_argsless
def get_early_utm_list():
return get_adjustment_file("Early_UTM.txt")
return get_adjustment_file("settings/Early_UTM.txt")
@cache_argsless
def get_symbol_shuffle_list():
return get_adjustment_file("settings/Symbol_Shuffle.txt")
@cache_argsless
def get_door_panel_shuffle_list():
return get_adjustment_file("settings/Door_Panel_Shuffle.txt")
@cache_argsless
def get_doors_simple_list():
return get_adjustment_file("settings/Doors_Simple.txt")
@cache_argsless
def get_doors_complex_list():
return get_adjustment_file("settings/Doors_Complex.txt")
@cache_argsless
def get_doors_max_list():
return get_adjustment_file("settings/Doors_Max.txt")
@cache_argsless
def get_laser_shuffle():
return get_adjustment_file("settings/Laser_Shuffle.txt")