mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-09 09:03:46 -07:00
Compare commits
97 Commits
NewSoupVi-
...
core_use_e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a0ccf9685 | ||
|
|
f0ca56469f | ||
|
|
1873c52aa6 | ||
|
|
ec1e113b4c | ||
|
|
347efac0cd | ||
|
|
b7b5bf58aa | ||
|
|
a324c97815 | ||
|
|
f263a0bc91 | ||
|
|
6a9299018c | ||
|
|
ee471a48bd | ||
|
|
879d7c23b7 | ||
|
|
934b09238e | ||
|
|
1fd8e4435e | ||
|
|
50fd42d0c2 | ||
|
|
399958c881 | ||
|
|
78c93d7e39 | ||
|
|
e3b8a60584 | ||
|
|
b7263edfd0 | ||
|
|
1ee749b352 | ||
|
|
f93734f9e3 | ||
|
|
e211dfa1c2 | ||
|
|
0f7deb1d2a | ||
|
|
f2cb16a5be | ||
|
|
98477e27aa | ||
|
|
4149db1a01 | ||
|
|
9ac921380f | ||
|
|
286e24629f | ||
|
|
ab2efc0c5c | ||
|
|
60d6078e1f | ||
|
|
f94492b2d3 | ||
|
|
f03bb61747 | ||
|
|
dc4e8bae98 | ||
|
|
ac26f8be8b | ||
|
|
8c79499573 | ||
|
|
63fbcc5fc8 | ||
|
|
cad217af19 | ||
|
|
a6ad4a8293 | ||
|
|
503999cb32 | ||
|
|
c2d8f2443e | ||
|
|
4571ed7e2f | ||
|
|
ef5cbd3ba3 | ||
|
|
5c162bd7ce | ||
|
|
7bdaaa25c1 | ||
|
|
9a5a02b654 | ||
|
|
4fea6b6e9b | ||
|
|
bd8b8822ac | ||
|
|
0a44c3ec49 | ||
|
|
3262984386 | ||
|
|
180265c8f4 | ||
|
|
a9b4d33cd2 | ||
|
|
5dfb9b28f7 | ||
|
|
ec75793ac3 | ||
|
|
cd4da36863 | ||
|
|
1749e22569 | ||
|
|
0cce88cfbc | ||
|
|
61e83a300b | ||
|
|
136a13aac7 | ||
|
|
2c90db9ae7 | ||
|
|
507e051a5a | ||
|
|
b5bf9ed1d7 | ||
|
|
215eb7e473 | ||
|
|
f42233699a | ||
|
|
1bec68df4d | ||
|
|
d8576e72eb | ||
|
|
7265468e8d | ||
|
|
d07f36dedd | ||
|
|
364a1b71ec | ||
|
|
daee6d210f | ||
|
|
96be0071e6 | ||
|
|
ff8e1dfb47 | ||
|
|
d26db6f213 | ||
|
|
bb6c753583 | ||
|
|
ca08e4b950 | ||
|
|
5a6b02dbd3 | ||
|
|
14416b1050 | ||
|
|
da4e6fc532 | ||
|
|
57d8b69a6d | ||
|
|
c9d8a8661c | ||
|
|
4a3d23e0e6 | ||
|
|
a3666f2ae5 | ||
|
|
c3e000e574 | ||
|
|
dd5481930a | ||
|
|
842328c661 | ||
|
|
8f75384e2e | ||
|
|
193faa00ce | ||
|
|
5e5383b399 | ||
|
|
cb6b29dbe3 | ||
|
|
82b0819051 | ||
|
|
e12ab4afa4 | ||
|
|
1416f631cc | ||
|
|
dbaac47d1e | ||
|
|
cf0ae5e31b | ||
|
|
8891f07362 | ||
|
|
d78974ec59 | ||
|
|
32be26c4d7 | ||
|
|
9de49aa419 | ||
|
|
294a67a4b4 |
2
.github/workflows/analyze-modified-files.yml
vendored
2
.github/workflows/analyze-modified-files.yml
vendored
@@ -65,7 +65,7 @@ jobs:
|
||||
continue-on-error: false
|
||||
if: env.diff != '' && matrix.task == 'flake8'
|
||||
run: |
|
||||
flake8 --count --select=E9,F63,F7,F82 --show-source --statistics ${{ env.diff }}
|
||||
flake8 --count --select=E9,F63,F7,F82 --ignore F824 --show-source --statistics ${{ env.diff }}
|
||||
|
||||
- name: "flake8: Lint modified files"
|
||||
continue-on-error: true
|
||||
|
||||
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -99,8 +99,8 @@ jobs:
|
||||
if-no-files-found: error
|
||||
retention-days: 7 # keep for 7 days, should be enough
|
||||
|
||||
build-ubuntu2004:
|
||||
runs-on: ubuntu-20.04
|
||||
build-ubuntu2204:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
# - copy code below to release.yml -
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -29,8 +29,8 @@ jobs:
|
||||
# build-release-windows: # this is done by hand because of signing
|
||||
# build-release-macos: # LF volunteer
|
||||
|
||||
build-release-ubuntu2004:
|
||||
runs-on: ubuntu-20.04
|
||||
build-release-ubuntu2204:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Set env
|
||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,6 +10,7 @@
|
||||
*.apmc
|
||||
*.apz5
|
||||
*.aptloz
|
||||
*.aptww
|
||||
*.apemerald
|
||||
*.pyc
|
||||
*.pyd
|
||||
|
||||
@@ -616,7 +616,7 @@ class MultiWorld():
|
||||
locations: Set[Location] = set()
|
||||
events: Set[Location] = set()
|
||||
for location in self.get_filled_locations():
|
||||
if type(location.item.code) is int:
|
||||
if type(location.item.code) is int and type(location.address) is int:
|
||||
locations.add(location)
|
||||
else:
|
||||
events.add(location)
|
||||
@@ -1106,6 +1106,9 @@ class Region:
|
||||
def __len__(self) -> int:
|
||||
return self._list.__len__()
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._list)
|
||||
|
||||
# This seems to not be needed, but that's a bit suspicious.
|
||||
# def __del__(self):
|
||||
# self.clear()
|
||||
@@ -1310,9 +1313,6 @@ class Location:
|
||||
multiworld = self.parent_region.multiworld if self.parent_region and self.parent_region.multiworld else None
|
||||
return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})'
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.name, self.player))
|
||||
|
||||
def __lt__(self, other: Location):
|
||||
return (self.player, self.name) < (other.player, other.name)
|
||||
|
||||
@@ -1416,6 +1416,10 @@ class Item:
|
||||
def flags(self) -> int:
|
||||
return self.classification.as_flag()
|
||||
|
||||
@property
|
||||
def is_event(self) -> bool:
|
||||
return self.code is None
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, Item):
|
||||
return NotImplemented
|
||||
|
||||
@@ -413,7 +413,8 @@ class CommonContext:
|
||||
await self.server.socket.close()
|
||||
if self.server_task is not None:
|
||||
await self.server_task
|
||||
self.ui.update_hints()
|
||||
if self.ui:
|
||||
self.ui.update_hints()
|
||||
|
||||
async def send_msgs(self, msgs: typing.List[typing.Any]) -> None:
|
||||
""" `msgs` JSON serializable """
|
||||
@@ -624,9 +625,6 @@ class CommonContext:
|
||||
|
||||
def consume_network_data_package(self, data_package: dict):
|
||||
self.update_data_package(data_package)
|
||||
current_cache = Utils.persistent_load().get("datapackage", {}).get("games", {})
|
||||
current_cache.update(data_package["games"])
|
||||
Utils.persistent_store("datapackage", "games", current_cache)
|
||||
logger.info(f"Got new ID/Name DataPackage for {', '.join(data_package['games'])}")
|
||||
for game, game_data in data_package["games"].items():
|
||||
Utils.store_data_package_for_checksum(game, game_data)
|
||||
|
||||
21
Fill.py
21
Fill.py
@@ -75,9 +75,11 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
|
||||
items_to_place.append(reachable_items[next_player].pop())
|
||||
|
||||
for item in items_to_place:
|
||||
for p, pool_item in enumerate(item_pool):
|
||||
# The items added into `reachable_items` are placed starting from the end of each deque in
|
||||
# `reachable_items`, so the items being placed are more likely to be found towards the end of `item_pool`.
|
||||
for p, pool_item in enumerate(reversed(item_pool), start=1):
|
||||
if pool_item is item:
|
||||
item_pool.pop(p)
|
||||
del item_pool[-p]
|
||||
break
|
||||
|
||||
maximum_exploration_state = sweep_from_pool(
|
||||
@@ -348,10 +350,10 @@ def accessibility_corrections(multiworld: MultiWorld, state: CollectionState, lo
|
||||
if (location.item is not None and location.item.advancement and location.address is not None and not
|
||||
location.locked and location.item.player not in minimal_players):
|
||||
pool.append(location.item)
|
||||
state.remove(location.item)
|
||||
location.item = None
|
||||
if location in state.advancements:
|
||||
state.advancements.remove(location)
|
||||
state.remove(location.item)
|
||||
locations.append(location)
|
||||
if pool and locations:
|
||||
locations.sort(key=lambda loc: loc.progress_type != LocationProgressType.PRIORITY)
|
||||
@@ -500,13 +502,15 @@ def distribute_items_restrictive(multiworld: MultiWorld,
|
||||
|
||||
if prioritylocations:
|
||||
# "priority fill"
|
||||
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool,
|
||||
maximum_exploration_state = sweep_from_pool(multiworld.state)
|
||||
fill_restrictive(multiworld, maximum_exploration_state, prioritylocations, progitempool,
|
||||
single_player_placement=single_player, swap=False, on_place=mark_for_locking,
|
||||
name="Priority", one_item_per_player=True, allow_partial=True)
|
||||
|
||||
if prioritylocations:
|
||||
# retry with one_item_per_player off because some priority fills can fail to fill with that optimization
|
||||
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool,
|
||||
maximum_exploration_state = sweep_from_pool(multiworld.state)
|
||||
fill_restrictive(multiworld, maximum_exploration_state, prioritylocations, progitempool,
|
||||
single_player_placement=single_player, swap=False, on_place=mark_for_locking,
|
||||
name="Priority Retry", one_item_per_player=False)
|
||||
accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool)
|
||||
@@ -514,14 +518,15 @@ def distribute_items_restrictive(multiworld: MultiWorld,
|
||||
|
||||
if progitempool:
|
||||
# "advancement/progression fill"
|
||||
maximum_exploration_state = sweep_from_pool(multiworld.state)
|
||||
if panic_method == "swap":
|
||||
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=True,
|
||||
fill_restrictive(multiworld, maximum_exploration_state, defaultlocations, progitempool, swap=True,
|
||||
name="Progression", single_player_placement=single_player)
|
||||
elif panic_method == "raise":
|
||||
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=False,
|
||||
fill_restrictive(multiworld, maximum_exploration_state, defaultlocations, progitempool, swap=False,
|
||||
name="Progression", single_player_placement=single_player)
|
||||
elif panic_method == "start_inventory":
|
||||
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=False,
|
||||
fill_restrictive(multiworld, maximum_exploration_state, defaultlocations, progitempool, swap=False,
|
||||
allow_partial=True, name="Progression", single_player_placement=single_player)
|
||||
if progitempool:
|
||||
for item in progitempool:
|
||||
|
||||
35
Generate.py
35
Generate.py
@@ -54,12 +54,22 @@ def mystery_argparse():
|
||||
parser.add_argument("--skip_output", action="store_true",
|
||||
help="Skips generation assertion and output stages and skips multidata and spoiler output. "
|
||||
"Intended for debugging and testing purposes.")
|
||||
parser.add_argument("--spoiler_only", action="store_true",
|
||||
help="Skips generation assertion and multidata, outputting only a spoiler log. "
|
||||
"Intended for debugging and testing purposes.")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.skip_output and args.spoiler_only:
|
||||
parser.error("Cannot mix --skip_output and --spoiler_only")
|
||||
elif args.spoiler == 0 and args.spoiler_only:
|
||||
parser.error("Cannot use --spoiler_only when --spoiler=0. Use --skip_output or set --spoiler to a different value")
|
||||
|
||||
if not os.path.isabs(args.weights_file_path):
|
||||
args.weights_file_path = os.path.join(args.player_files_path, args.weights_file_path)
|
||||
if not os.path.isabs(args.meta_file_path):
|
||||
args.meta_file_path = os.path.join(args.player_files_path, args.meta_file_path)
|
||||
args.plando: PlandoOptions = PlandoOptions.from_option_string(args.plando)
|
||||
|
||||
return args
|
||||
|
||||
|
||||
@@ -108,6 +118,8 @@ def main(args=None) -> Tuple[argparse.Namespace, int]:
|
||||
raise Exception("Cannot mix --sameoptions with --meta")
|
||||
else:
|
||||
meta_weights = None
|
||||
|
||||
|
||||
player_id = 1
|
||||
player_files = {}
|
||||
for file in os.scandir(args.player_files_path):
|
||||
@@ -164,6 +176,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]:
|
||||
erargs.outputpath = args.outputpath
|
||||
erargs.skip_prog_balancing = args.skip_prog_balancing
|
||||
erargs.skip_output = args.skip_output
|
||||
erargs.spoiler_only = args.spoiler_only
|
||||
erargs.name = {}
|
||||
erargs.csv_output = args.csv_output
|
||||
|
||||
@@ -279,22 +292,30 @@ def get_choice(option, root, value=None) -> Any:
|
||||
raise RuntimeError(f"All options specified in \"{option}\" are weighted as zero.")
|
||||
|
||||
|
||||
class SafeDict(dict):
|
||||
def __missing__(self, key):
|
||||
return '{' + key + '}'
|
||||
class SafeFormatter(string.Formatter):
|
||||
def get_value(self, key, args, kwargs):
|
||||
if isinstance(key, int):
|
||||
if key < len(args):
|
||||
return args[key]
|
||||
else:
|
||||
return "{" + str(key) + "}"
|
||||
else:
|
||||
return kwargs.get(key, "{" + key + "}")
|
||||
|
||||
|
||||
def handle_name(name: str, player: int, name_counter: Counter):
|
||||
name_counter[name.lower()] += 1
|
||||
number = name_counter[name.lower()]
|
||||
new_name = "%".join([x.replace("%number%", "{number}").replace("%player%", "{player}") for x in name.split("%%")])
|
||||
new_name = string.Formatter().vformat(new_name, (), SafeDict(number=number,
|
||||
NUMBER=(number if number > 1 else ''),
|
||||
player=player,
|
||||
PLAYER=(player if player > 1 else '')))
|
||||
|
||||
new_name = SafeFormatter().vformat(new_name, (), {"number": number,
|
||||
"NUMBER": (number if number > 1 else ''),
|
||||
"player": player,
|
||||
"PLAYER": (player if player > 1 else '')})
|
||||
# Run .strip twice for edge case where after the initial .slice new_name has a leading whitespace.
|
||||
# Could cause issues for some clients that cannot handle the additional whitespace.
|
||||
new_name = new_name.strip()[:16].strip()
|
||||
|
||||
if new_name == "Archipelago":
|
||||
raise Exception(f"You cannot name yourself \"{new_name}\"")
|
||||
return new_name
|
||||
|
||||
292
Launcher.py
292
Launcher.py
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Archipelago launcher for bundled app.
|
||||
Archipelago Launcher
|
||||
|
||||
* if run with APBP as argument, launch corresponding client.
|
||||
* if run with executable as argument, run it passing argv[2:] as arguments
|
||||
@@ -8,9 +8,7 @@ Archipelago launcher for bundled app.
|
||||
Scroll down to components= to add components to the launcher as well as setup.py
|
||||
"""
|
||||
|
||||
|
||||
import argparse
|
||||
import itertools
|
||||
import logging
|
||||
import multiprocessing
|
||||
import shlex
|
||||
@@ -20,10 +18,11 @@ import urllib.parse
|
||||
import webbrowser
|
||||
from os.path import isfile
|
||||
from shutil import which
|
||||
from typing import Callable, Optional, Sequence, Tuple, Union
|
||||
from typing import Callable, Optional, Sequence, Tuple, Union, Any
|
||||
|
||||
if __name__ == "__main__":
|
||||
import ModuleUpdate
|
||||
|
||||
ModuleUpdate.update()
|
||||
|
||||
import settings
|
||||
@@ -105,7 +104,8 @@ components.extend([
|
||||
Component("Generate Template Options", func=generate_yamls),
|
||||
Component("Archipelago Website", func=lambda: webbrowser.open("https://archipelago.gg/")),
|
||||
Component("Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/8Z65BR2")),
|
||||
Component("Unrated/18+ Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4")),
|
||||
Component("Unrated/18+ Discord Server", icon="discord",
|
||||
func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4")),
|
||||
Component("Browse Files", func=browse_files),
|
||||
])
|
||||
|
||||
@@ -114,7 +114,7 @@ def handle_uri(path: str, launch_args: Tuple[str, ...]) -> None:
|
||||
url = urllib.parse.urlparse(path)
|
||||
queries = urllib.parse.parse_qs(url.query)
|
||||
launch_args = (path, *launch_args)
|
||||
client_component = None
|
||||
client_component = []
|
||||
text_client_component = None
|
||||
if "game" in queries:
|
||||
game = queries["game"][0]
|
||||
@@ -122,49 +122,40 @@ def handle_uri(path: str, launch_args: Tuple[str, ...]) -> None:
|
||||
game = "Archipelago"
|
||||
for component in components:
|
||||
if component.supports_uri and component.game_name == game:
|
||||
client_component = component
|
||||
client_component.append(component)
|
||||
elif component.display_name == "Text Client":
|
||||
text_client_component = component
|
||||
|
||||
if client_component is None:
|
||||
from kvui import MDButton, MDButtonText
|
||||
from kivymd.uix.dialog import MDDialog, MDDialogHeadlineText, MDDialogContentContainer, MDDialogSupportingText
|
||||
from kivymd.uix.divider import MDDivider
|
||||
|
||||
if not client_component:
|
||||
run_component(text_client_component, *launch_args)
|
||||
return
|
||||
else:
|
||||
popup_text = MDDialogSupportingText(text="Select client to open and connect with.")
|
||||
component_buttons = [MDDivider()]
|
||||
for component in [text_client_component, *client_component]:
|
||||
component_buttons.append(MDButton(
|
||||
MDButtonText(text=component.display_name),
|
||||
on_release=lambda *args, comp=component: run_component(comp, *launch_args),
|
||||
style="text"
|
||||
))
|
||||
component_buttons.append(MDDivider())
|
||||
|
||||
from kvui import App, Button, BoxLayout, Label, Window
|
||||
MDDialog(
|
||||
# Headline
|
||||
MDDialogHeadlineText(text="Connect to Multiworld"),
|
||||
# Text
|
||||
popup_text,
|
||||
# Content
|
||||
MDDialogContentContainer(
|
||||
*component_buttons,
|
||||
orientation="vertical"
|
||||
),
|
||||
|
||||
class Popup(App):
|
||||
def __init__(self):
|
||||
self.title = "Connect to Multiworld"
|
||||
self.icon = r"data/icon.png"
|
||||
super().__init__()
|
||||
|
||||
def build(self):
|
||||
layout = BoxLayout(orientation="vertical")
|
||||
layout.add_widget(Label(text="Select client to open and connect with."))
|
||||
button_row = BoxLayout(orientation="horizontal", size_hint=(1, 0.4))
|
||||
|
||||
text_client_button = Button(
|
||||
text=text_client_component.display_name,
|
||||
on_release=lambda *args: run_component(text_client_component, *launch_args)
|
||||
)
|
||||
button_row.add_widget(text_client_button)
|
||||
|
||||
game_client_button = Button(
|
||||
text=client_component.display_name,
|
||||
on_release=lambda *args: run_component(client_component, *launch_args)
|
||||
)
|
||||
button_row.add_widget(game_client_button)
|
||||
|
||||
layout.add_widget(button_row)
|
||||
|
||||
return layout
|
||||
|
||||
def _stop(self, *largs):
|
||||
# see run_gui Launcher _stop comment for details
|
||||
self.root_window.close()
|
||||
super()._stop(*largs)
|
||||
|
||||
Popup().run()
|
||||
).open()
|
||||
|
||||
|
||||
def identify(path: Union[None, str]) -> Tuple[Union[None, str], Union[None, Component]]:
|
||||
@@ -220,100 +211,166 @@ def launch(exe, in_terminal=False):
|
||||
subprocess.Popen(exe)
|
||||
|
||||
|
||||
def create_shortcut(button: Any, component: Component) -> None:
|
||||
from pyshortcuts import make_shortcut
|
||||
script = sys.argv[0]
|
||||
wkdir = Utils.local_path()
|
||||
|
||||
script = f"{script} \"{component.display_name}\""
|
||||
make_shortcut(script, name=f"Archipelago {component.display_name}", icon=local_path("data", "icon.ico"),
|
||||
startmenu=False, terminal=False, working_dir=wkdir)
|
||||
button.menu.dismiss()
|
||||
|
||||
|
||||
refresh_components: Optional[Callable[[], None]] = None
|
||||
|
||||
|
||||
def run_gui():
|
||||
from kvui import App, ContainerLayout, GridLayout, Button, Label, ScrollBox, Widget, ApAsyncImage
|
||||
def run_gui(path: str, args: Any) -> None:
|
||||
from kvui import (ThemedApp, MDFloatLayout, MDGridLayout, ScrollBox)
|
||||
from kivy.properties import ObjectProperty
|
||||
from kivy.core.window import Window
|
||||
from kivy.uix.relativelayout import RelativeLayout
|
||||
from kivy.metrics import dp
|
||||
from kivymd.uix.button import MDIconButton
|
||||
from kivymd.uix.card import MDCard
|
||||
from kivymd.uix.menu import MDDropdownMenu
|
||||
from kivymd.uix.snackbar import MDSnackbar, MDSnackbarText
|
||||
|
||||
class Launcher(App):
|
||||
from kivy.lang.builder import Builder
|
||||
|
||||
class LauncherCard(MDCard):
|
||||
component: Component | None
|
||||
image: str
|
||||
context_button: MDIconButton = ObjectProperty(None)
|
||||
|
||||
def __init__(self, *args, component: Component | None = None, image_path: str = "", **kwargs):
|
||||
self.component = component
|
||||
self.image = image_path
|
||||
super().__init__(args, kwargs)
|
||||
|
||||
class Launcher(ThemedApp):
|
||||
base_title: str = "Archipelago Launcher"
|
||||
container: ContainerLayout
|
||||
grid: GridLayout
|
||||
_tool_layout: Optional[ScrollBox] = None
|
||||
_client_layout: Optional[ScrollBox] = None
|
||||
top_screen: MDFloatLayout = ObjectProperty(None)
|
||||
navigation: MDGridLayout = ObjectProperty(None)
|
||||
grid: MDGridLayout = ObjectProperty(None)
|
||||
button_layout: ScrollBox = ObjectProperty(None)
|
||||
cards: list[LauncherCard]
|
||||
current_filter: Sequence[str | Type] | None
|
||||
|
||||
def __init__(self, ctx=None):
|
||||
def __init__(self, ctx=None, path=None, args=None):
|
||||
self.title = self.base_title + " " + Utils.__version__
|
||||
self.ctx = ctx
|
||||
self.icon = r"data/icon.png"
|
||||
self.favorites = []
|
||||
self.launch_uri = path
|
||||
self.launch_args = args
|
||||
self.cards = []
|
||||
self.current_filter = (Type.CLIENT, Type.TOOL, Type.ADJUSTER, Type.MISC)
|
||||
persistent = Utils.persistent_load()
|
||||
if "launcher" in persistent:
|
||||
if "favorites" in persistent["launcher"]:
|
||||
self.favorites.extend(persistent["launcher"]["favorites"])
|
||||
if "filter" in persistent["launcher"]:
|
||||
if persistent["launcher"]["filter"]:
|
||||
filters = []
|
||||
for filter in persistent["launcher"]["filter"].split(", "):
|
||||
if filter == "favorites":
|
||||
filters.append(filter)
|
||||
else:
|
||||
filters.append(Type[filter])
|
||||
self.current_filter = filters
|
||||
super().__init__()
|
||||
|
||||
def _refresh_components(self) -> None:
|
||||
def set_favorite(self, caller):
|
||||
if caller.component.display_name in self.favorites:
|
||||
self.favorites.remove(caller.component.display_name)
|
||||
caller.icon = "star-outline"
|
||||
else:
|
||||
self.favorites.append(caller.component.display_name)
|
||||
caller.icon = "star"
|
||||
|
||||
def build_button(component: Component) -> Widget:
|
||||
def build_card(self, component: Component) -> LauncherCard:
|
||||
"""
|
||||
Builds a card widget for a given component.
|
||||
|
||||
:param component: The component associated with the button.
|
||||
|
||||
:return: The created Card Widget.
|
||||
"""
|
||||
Builds a button widget for a given component.
|
||||
button_card = LauncherCard(component=component,
|
||||
image_path=icon_paths[component.icon])
|
||||
|
||||
Args:
|
||||
component (Component): The component associated with the button.
|
||||
def open_menu(caller):
|
||||
caller.menu.open()
|
||||
|
||||
Returns:
|
||||
None. The button is added to the parent grid layout.
|
||||
menu_items = [
|
||||
{
|
||||
"text": "Add shortcut on desktop",
|
||||
"leading_icon": "laptop",
|
||||
"on_release": lambda: create_shortcut(button_card.context_button, component)
|
||||
}
|
||||
]
|
||||
button_card.context_button.menu = MDDropdownMenu(caller=button_card.context_button, items=menu_items)
|
||||
button_card.context_button.bind(on_release=open_menu)
|
||||
|
||||
"""
|
||||
button = Button(text=component.display_name, size_hint_y=None, height=40)
|
||||
button.component = component
|
||||
button.bind(on_release=self.component_action)
|
||||
if component.icon != "icon":
|
||||
image = ApAsyncImage(source=icon_paths[component.icon],
|
||||
size=(38, 38), size_hint=(None, 1), pos=(5, 0))
|
||||
box_layout = RelativeLayout(size_hint_y=None, height=40)
|
||||
box_layout.add_widget(button)
|
||||
box_layout.add_widget(image)
|
||||
return box_layout
|
||||
return button
|
||||
return button_card
|
||||
|
||||
def _refresh_components(self, type_filter: Sequence[str | Type] | None = None) -> None:
|
||||
if not type_filter:
|
||||
type_filter = [Type.CLIENT, Type.ADJUSTER, Type.TOOL, Type.MISC]
|
||||
favorites = "favorites" in type_filter
|
||||
|
||||
# clear before repopulating
|
||||
assert self._tool_layout and self._client_layout, "must call `build` first"
|
||||
tool_children = reversed(self._tool_layout.layout.children)
|
||||
assert self.button_layout, "must call `build` first"
|
||||
tool_children = reversed(self.button_layout.layout.children)
|
||||
for child in tool_children:
|
||||
self._tool_layout.layout.remove_widget(child)
|
||||
client_children = reversed(self._client_layout.layout.children)
|
||||
for child in client_children:
|
||||
self._client_layout.layout.remove_widget(child)
|
||||
self.button_layout.layout.remove_widget(child)
|
||||
|
||||
_tools = {c.display_name: c for c in components if c.type == Type.TOOL}
|
||||
_clients = {c.display_name: c for c in components if c.type == Type.CLIENT}
|
||||
_adjusters = {c.display_name: c for c in components if c.type == Type.ADJUSTER}
|
||||
_miscs = {c.display_name: c for c in components if c.type == Type.MISC}
|
||||
cards = [card for card in self.cards if card.component.type in type_filter
|
||||
or favorites and card.component.display_name in self.favorites]
|
||||
|
||||
for (tool, client) in itertools.zip_longest(itertools.chain(
|
||||
_tools.items(), _miscs.items(), _adjusters.items()
|
||||
), _clients.items()):
|
||||
# column 1
|
||||
if tool:
|
||||
self._tool_layout.layout.add_widget(build_button(tool[1]))
|
||||
# column 2
|
||||
if client:
|
||||
self._client_layout.layout.add_widget(build_button(client[1]))
|
||||
self.current_filter = type_filter
|
||||
|
||||
for card in cards:
|
||||
self.button_layout.layout.add_widget(card)
|
||||
|
||||
top = self.button_layout.children[0].y + self.button_layout.children[0].height \
|
||||
- self.button_layout.height
|
||||
scroll_percent = self.button_layout.convert_distance_to_scroll(0, top)
|
||||
self.button_layout.scroll_y = max(0, min(1, scroll_percent[1]))
|
||||
|
||||
def filter_clients(self, caller):
|
||||
self._refresh_components(caller.type)
|
||||
|
||||
def build(self):
|
||||
self.container = ContainerLayout()
|
||||
self.grid = GridLayout(cols=2)
|
||||
self.container.add_widget(self.grid)
|
||||
self.grid.add_widget(Label(text="General", size_hint_y=None, height=40))
|
||||
self.grid.add_widget(Label(text="Clients", size_hint_y=None, height=40))
|
||||
self._tool_layout = ScrollBox()
|
||||
self._tool_layout.layout.orientation = "vertical"
|
||||
self.grid.add_widget(self._tool_layout)
|
||||
self._client_layout = ScrollBox()
|
||||
self._client_layout.layout.orientation = "vertical"
|
||||
self.grid.add_widget(self._client_layout)
|
||||
|
||||
self._refresh_components()
|
||||
self.top_screen = Builder.load_file(Utils.local_path("data/launcher.kv"))
|
||||
self.grid = self.top_screen.ids.grid
|
||||
self.navigation = self.top_screen.ids.navigation
|
||||
self.button_layout = self.top_screen.ids.button_layout
|
||||
self.set_colors()
|
||||
self.top_screen.md_bg_color = self.theme_cls.backgroundColor
|
||||
|
||||
global refresh_components
|
||||
refresh_components = self._refresh_components
|
||||
|
||||
Window.bind(on_drop_file=self._on_drop_file)
|
||||
|
||||
return self.container
|
||||
for component in components:
|
||||
self.cards.append(self.build_card(component))
|
||||
|
||||
self._refresh_components(self.current_filter)
|
||||
|
||||
return self.top_screen
|
||||
|
||||
def on_start(self):
|
||||
if self.launch_uri:
|
||||
handle_uri(self.launch_uri, self.launch_args)
|
||||
self.launch_uri = None
|
||||
self.launch_args = None
|
||||
|
||||
@staticmethod
|
||||
def component_action(button):
|
||||
MDSnackbar(MDSnackbarText(text="Opening in a new window..."), y=dp(24), pos_hint={"center_x": 0.5},
|
||||
size_hint_x=0.5).open()
|
||||
if button.component.func:
|
||||
button.component.func()
|
||||
else:
|
||||
@@ -333,7 +390,13 @@ def run_gui():
|
||||
self.root_window.close()
|
||||
super()._stop(*largs)
|
||||
|
||||
Launcher().run()
|
||||
def on_stop(self):
|
||||
Utils.persistent_store("launcher", "favorites", self.favorites)
|
||||
Utils.persistent_store("launcher", "filter", ", ".join(filter.name if isinstance(filter, Type) else filter
|
||||
for filter in self.current_filter))
|
||||
super().on_stop()
|
||||
|
||||
Launcher(path=path, args=args).run()
|
||||
|
||||
# avoiding Launcher reference leak
|
||||
# and don't try to do something with widgets after window closed
|
||||
@@ -360,16 +423,14 @@ def main(args: Optional[Union[argparse.Namespace, dict]] = None):
|
||||
|
||||
path = args.get("Patch|Game|Component|url", None)
|
||||
if path is not None:
|
||||
if path.startswith("archipelago://"):
|
||||
handle_uri(path, args.get("args", ()))
|
||||
return
|
||||
file, component = identify(path)
|
||||
if file:
|
||||
args['file'] = file
|
||||
if component:
|
||||
args['component'] = component
|
||||
if not component:
|
||||
logging.warning(f"Could not identify Component responsible for {path}")
|
||||
if not path.startswith("archipelago://"):
|
||||
file, component = identify(path)
|
||||
if file:
|
||||
args['file'] = file
|
||||
if component:
|
||||
args['component'] = component
|
||||
if not component:
|
||||
logging.warning(f"Could not identify Component responsible for {path}")
|
||||
|
||||
if args["update_settings"]:
|
||||
update_settings()
|
||||
@@ -378,7 +439,7 @@ def main(args: Optional[Union[argparse.Namespace, dict]] = None):
|
||||
elif "component" in args:
|
||||
run_component(args["component"], *args["args"])
|
||||
elif not args["update_settings"]:
|
||||
run_gui()
|
||||
run_gui(path, args.get("args", ()))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
@@ -400,6 +461,7 @@ if __name__ == '__main__':
|
||||
main(parser.parse_args())
|
||||
|
||||
from worlds.LauncherComponents import processes
|
||||
|
||||
for process in processes:
|
||||
# we await all child processes to close before we tear down the process host
|
||||
# this makes it feel like each one is its own program, as the Launcher is closed now
|
||||
|
||||
@@ -26,6 +26,7 @@ import typing
|
||||
from CommonClient import (CommonContext, get_base_parser, gui_enabled, logger,
|
||||
server_loop)
|
||||
from NetUtils import ClientStatus
|
||||
from worlds.ladx import LinksAwakeningWorld
|
||||
from worlds.ladx.Common import BASE_ID as LABaseID
|
||||
from worlds.ladx.GpsTracker import GpsTracker
|
||||
from worlds.ladx.TrackerConsts import storage_key
|
||||
@@ -139,7 +140,7 @@ class RAGameboy():
|
||||
def set_checks_range(self, checks_start, checks_size):
|
||||
self.checks_start = checks_start
|
||||
self.checks_size = checks_size
|
||||
|
||||
|
||||
def set_location_range(self, location_start, location_size, critical_addresses):
|
||||
self.location_start = location_start
|
||||
self.location_size = location_size
|
||||
@@ -237,7 +238,7 @@ class RAGameboy():
|
||||
self.cache[start:start + len(hram_block)] = hram_block
|
||||
|
||||
self.last_cache_read = time.time()
|
||||
|
||||
|
||||
async def read_memory_block(self, address: int, size: int):
|
||||
block = bytearray()
|
||||
remaining_size = size
|
||||
@@ -245,7 +246,7 @@ class RAGameboy():
|
||||
chunk = await self.async_read_memory(address + len(block), remaining_size)
|
||||
remaining_size -= len(chunk)
|
||||
block += chunk
|
||||
|
||||
|
||||
return block
|
||||
|
||||
async def read_memory_cache(self, addresses):
|
||||
@@ -514,8 +515,8 @@ class LinksAwakeningContext(CommonContext):
|
||||
magpie_task = None
|
||||
won = False
|
||||
|
||||
@property
|
||||
def slot_storage_key(self):
|
||||
@property
|
||||
def slot_storage_key(self):
|
||||
return f"{self.slot_info[self.slot].name}_{storage_key}"
|
||||
|
||||
def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str], magpie: typing.Optional[bool]) -> None:
|
||||
@@ -529,9 +530,7 @@ class LinksAwakeningContext(CommonContext):
|
||||
|
||||
def run_gui(self) -> None:
|
||||
import webbrowser
|
||||
import kvui
|
||||
from kvui import Button, GameManager
|
||||
from kivy.uix.image import Image
|
||||
from kvui import GameManager, ImageButton
|
||||
|
||||
class LADXManager(GameManager):
|
||||
logging_pairs = [
|
||||
@@ -544,21 +543,15 @@ class LinksAwakeningContext(CommonContext):
|
||||
b = super().build()
|
||||
|
||||
if self.ctx.magpie_enabled:
|
||||
button = Button(text="", size=(30, 30), size_hint_x=None,
|
||||
on_press=lambda _: webbrowser.open('https://magpietracker.us/?enable_autotracker=1'))
|
||||
image = Image(size=(16, 16), texture=magpie_logo())
|
||||
button.add_widget(image)
|
||||
|
||||
def set_center(_, center):
|
||||
image.center = center
|
||||
button.bind(center=set_center)
|
||||
|
||||
button = ImageButton(texture=magpie_logo(), fit_mode="cover", image_size=(32, 32), size_hint_x=None,
|
||||
on_press=lambda _: webbrowser.open('https://magpietracker.us/?enable_autotracker=1'))
|
||||
self.connect_layout.add_widget(button)
|
||||
|
||||
return b
|
||||
|
||||
self.ui = LADXManager(self)
|
||||
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
||||
|
||||
|
||||
async def send_new_entrances(self, entrances: typing.Dict[str, str]):
|
||||
# Store the entrances we find on the server for future sessions
|
||||
message = [{
|
||||
@@ -597,12 +590,12 @@ class LinksAwakeningContext(CommonContext):
|
||||
logger.info("victory!")
|
||||
await self.send_msgs(message)
|
||||
self.won = True
|
||||
|
||||
|
||||
async def request_found_entrances(self):
|
||||
await self.send_msgs([{"cmd": "Get", "keys": [self.slot_storage_key]}])
|
||||
|
||||
# Ask for updates so that players can co-op entrances in a seed
|
||||
await self.send_msgs([{"cmd": "SetNotify", "keys": [self.slot_storage_key]}])
|
||||
# Ask for updates so that players can co-op entrances in a seed
|
||||
await self.send_msgs([{"cmd": "SetNotify", "keys": [self.slot_storage_key]}])
|
||||
|
||||
async def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None:
|
||||
if self.ENABLE_DEATHLINK:
|
||||
@@ -638,12 +631,18 @@ class LinksAwakeningContext(CommonContext):
|
||||
if cmd == "Connected":
|
||||
self.game = self.slot_info[self.slot].game
|
||||
self.slot_data = args.get("slot_data", {})
|
||||
|
||||
# This is sent to magpie over local websocket to make its own connection
|
||||
self.slot_data.update({
|
||||
"server_address": self.server_address,
|
||||
"slot_name": self.player_names[self.slot],
|
||||
"password": self.password,
|
||||
})
|
||||
|
||||
# TODO - use watcher_event
|
||||
if cmd == "ReceivedItems":
|
||||
for index, item in enumerate(args["items"], start=args["index"]):
|
||||
self.client.recvd_checks[index] = item
|
||||
|
||||
|
||||
if cmd == "Retrieved" and self.magpie_enabled and self.slot_storage_key in args["keys"]:
|
||||
self.client.gps_tracker.receive_found_entrances(args["keys"][self.slot_storage_key])
|
||||
|
||||
@@ -722,8 +721,10 @@ class LinksAwakeningContext(CommonContext):
|
||||
try:
|
||||
self.magpie.set_checks(self.client.tracker.all_checks)
|
||||
await self.magpie.set_item_tracker(self.client.item_tracker)
|
||||
self.magpie.slot_data = self.slot_data
|
||||
|
||||
if self.slot_data and "slot_data" in self.magpie.features and not self.magpie.has_sent_slot_data:
|
||||
self.magpie.slot_data = self.slot_data
|
||||
await self.magpie.send_slot_data()
|
||||
|
||||
if self.client.gps_tracker.needs_found_entrances:
|
||||
await self.request_found_entrances()
|
||||
self.client.gps_tracker.needs_found_entrances = False
|
||||
@@ -741,8 +742,8 @@ class LinksAwakeningContext(CommonContext):
|
||||
await asyncio.sleep(1.0)
|
||||
|
||||
def run_game(romfile: str) -> None:
|
||||
auto_start = typing.cast(typing.Union[bool, str],
|
||||
Utils.get_options()["ladx_options"].get("rom_start", True))
|
||||
auto_start = LinksAwakeningWorld.settings.rom_start
|
||||
|
||||
if auto_start is True:
|
||||
import webbrowser
|
||||
webbrowser.open(romfile)
|
||||
|
||||
12
Main.py
12
Main.py
@@ -81,7 +81,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
del item_digits, location_digits, item_count, location_count
|
||||
|
||||
# This assertion method should not be necessary to run if we are not outputting any multidata.
|
||||
if not args.skip_output:
|
||||
if not args.skip_output and not args.spoiler_only:
|
||||
AutoWorld.call_stage(multiworld, "assert_generate")
|
||||
|
||||
AutoWorld.call_all(multiworld, "generate_early")
|
||||
@@ -224,6 +224,15 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
logger.info(f'Beginning output...')
|
||||
outfilebase = 'AP_' + multiworld.seed_name
|
||||
|
||||
if args.spoiler_only:
|
||||
if args.spoiler > 1:
|
||||
logger.info('Calculating playthrough.')
|
||||
multiworld.spoiler.create_playthrough(create_paths=args.spoiler > 2)
|
||||
|
||||
multiworld.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase))
|
||||
logger.info('Done. Skipped multidata modification. Total time: %s', time.perf_counter() - start)
|
||||
return multiworld
|
||||
|
||||
output = tempfile.TemporaryDirectory()
|
||||
with output as temp_dir:
|
||||
output_players = [player for player in multiworld.player_ids if AutoWorld.World.generate_output.__code__
|
||||
@@ -306,6 +315,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
game_world.game: worlds.network_data_package["games"][game_world.game]
|
||||
for game_world in multiworld.worlds.values()
|
||||
}
|
||||
data_package["Archipelago"] = worlds.network_data_package["games"]["Archipelago"]
|
||||
|
||||
checks_in_area: Dict[int, Dict[str, Union[int, List[int]]]] = {}
|
||||
|
||||
|
||||
@@ -66,9 +66,13 @@ def pop_from_container(container, value):
|
||||
return container
|
||||
|
||||
|
||||
def update_dict(dictionary, entries):
|
||||
dictionary.update(entries)
|
||||
return dictionary
|
||||
def update_container_unique(container, entries):
|
||||
if isinstance(container, list):
|
||||
existing_container_as_set = set(container)
|
||||
container.extend([entry for entry in entries if entry not in existing_container_as_set])
|
||||
else:
|
||||
container.update(entries)
|
||||
return container
|
||||
|
||||
|
||||
def queue_gc():
|
||||
@@ -109,7 +113,7 @@ modify_functions = {
|
||||
# lists/dicts:
|
||||
"remove": remove_from_list,
|
||||
"pop": pop_from_container,
|
||||
"update": update_dict,
|
||||
"update": update_container_unique,
|
||||
}
|
||||
|
||||
|
||||
@@ -277,25 +281,6 @@ class Context:
|
||||
lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})'))
|
||||
self.non_hintable_names = collections.defaultdict(frozenset)
|
||||
|
||||
self._load_game_data()
|
||||
|
||||
# Data package retrieval
|
||||
def _load_game_data(self):
|
||||
import worlds
|
||||
self.gamespackage = worlds.network_data_package["games"]
|
||||
|
||||
self.item_name_groups = {world_name: world.item_name_groups for world_name, world in
|
||||
worlds.AutoWorldRegister.world_types.items()}
|
||||
self.location_name_groups = {world_name: world.location_name_groups for world_name, world in
|
||||
worlds.AutoWorldRegister.world_types.items()}
|
||||
for world_name, world in worlds.AutoWorldRegister.world_types.items():
|
||||
self.non_hintable_names[world_name] = world.hint_blacklist
|
||||
|
||||
for game_package in self.gamespackage.values():
|
||||
# remove groups from data sent to clients
|
||||
del game_package["item_name_groups"]
|
||||
del game_package["location_name_groups"]
|
||||
|
||||
def _init_game_data(self):
|
||||
for game_name, game_package in self.gamespackage.items():
|
||||
if "checksum" in game_package:
|
||||
@@ -2037,7 +2022,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
value = func(value, operation["value"])
|
||||
ctx.stored_data[args["key"]] = args["value"] = value
|
||||
targets = set(ctx.stored_data_notification_clients[args["key"]])
|
||||
if args.get("want_reply", True):
|
||||
if args.get("want_reply", False):
|
||||
targets.add(client)
|
||||
if targets:
|
||||
ctx.broadcast(targets, [args])
|
||||
|
||||
@@ -81,6 +81,7 @@ Currently, the following games are supported:
|
||||
* Castlevania: Circle of the Moon
|
||||
* Inscryption
|
||||
* Civilization VI
|
||||
* The Legend of Zelda: The Wind Waker
|
||||
|
||||
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
|
||||
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
||||
|
||||
2
Utils.py
2
Utils.py
@@ -47,7 +47,7 @@ class Version(typing.NamedTuple):
|
||||
return ".".join(str(item) for item in self)
|
||||
|
||||
|
||||
__version__ = "0.6.0"
|
||||
__version__ = "0.6.2"
|
||||
version_tuple = tuplize_version(__version__)
|
||||
|
||||
is_linux = sys.platform.startswith("linux")
|
||||
|
||||
@@ -214,17 +214,11 @@ class WargrooveContext(CommonContext):
|
||||
def run_gui(self):
|
||||
"""Import kivy UI system and start running it as self.ui_task."""
|
||||
from kvui import GameManager, HoverBehavior, ServerToolTip
|
||||
from kivy.uix.tabbedpanel import TabbedPanelItem
|
||||
from kivymd.uix.tab import MDTabsItem, MDTabsItemText
|
||||
from kivy.lang import Builder
|
||||
from kivy.uix.button import Button
|
||||
from kivy.uix.togglebutton import ToggleButton
|
||||
from kivy.uix.boxlayout import BoxLayout
|
||||
from kivy.uix.gridlayout import GridLayout
|
||||
from kivy.uix.image import AsyncImage, Image
|
||||
from kivy.uix.stacklayout import StackLayout
|
||||
from kivy.uix.label import Label
|
||||
from kivy.properties import ColorProperty
|
||||
from kivy.uix.image import Image
|
||||
import pkgutil
|
||||
|
||||
class TrackerLayout(BoxLayout):
|
||||
|
||||
@@ -9,7 +9,7 @@ from threading import Event, Thread
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
from pony.orm import db_session, select, commit
|
||||
from pony.orm import db_session, select, commit, PrimaryKey
|
||||
|
||||
from Utils import restricted_loads
|
||||
from .locker import Locker, AlreadyRunningException
|
||||
@@ -36,12 +36,21 @@ def handle_generation_failure(result: BaseException):
|
||||
logging.exception(e)
|
||||
|
||||
|
||||
def _mp_gen_game(gen_options: dict, meta: dict[str, Any] | None = None, owner=None, sid=None) -> PrimaryKey | None:
|
||||
from setproctitle import setproctitle
|
||||
|
||||
setproctitle(f"Generator ({sid})")
|
||||
res = gen_game(gen_options, meta=meta, owner=owner, sid=sid)
|
||||
setproctitle(f"Generator (idle)")
|
||||
return res
|
||||
|
||||
|
||||
def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation):
|
||||
try:
|
||||
meta = json.loads(generation.meta)
|
||||
options = restricted_loads(generation.options)
|
||||
logging.info(f"Generating {generation.id} for {len(options)} players")
|
||||
pool.apply_async(gen_game, (options,),
|
||||
pool.apply_async(_mp_gen_game, (options,),
|
||||
{"meta": meta,
|
||||
"sid": generation.id,
|
||||
"owner": generation.owner},
|
||||
@@ -55,6 +64,10 @@ def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation):
|
||||
|
||||
|
||||
def init_generator(config: dict[str, Any]) -> None:
|
||||
from setproctitle import setproctitle
|
||||
|
||||
setproctitle("Generator (idle)")
|
||||
|
||||
try:
|
||||
import resource
|
||||
except ModuleNotFoundError:
|
||||
|
||||
@@ -227,6 +227,9 @@ def set_up_logging(room_id) -> logging.Logger:
|
||||
def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
||||
cert_file: typing.Optional[str], cert_key_file: typing.Optional[str],
|
||||
host: str, rooms_to_run: multiprocessing.Queue, rooms_shutting_down: multiprocessing.Queue):
|
||||
from setproctitle import setproctitle
|
||||
|
||||
setproctitle(name)
|
||||
Utils.init_logging(name)
|
||||
try:
|
||||
import resource
|
||||
@@ -247,8 +250,23 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
||||
raise Exception("Worlds system should not be loaded in the custom server.")
|
||||
|
||||
import gc
|
||||
ssl_context = load_server_cert(cert_file, cert_key_file) if cert_file else None
|
||||
del cert_file, cert_key_file, ponyconfig
|
||||
|
||||
if not cert_file:
|
||||
def get_ssl_context():
|
||||
return None
|
||||
else:
|
||||
load_date = None
|
||||
ssl_context = load_server_cert(cert_file, cert_key_file)
|
||||
|
||||
def get_ssl_context():
|
||||
nonlocal load_date, ssl_context
|
||||
today = datetime.date.today()
|
||||
if load_date != today:
|
||||
ssl_context = load_server_cert(cert_file, cert_key_file)
|
||||
load_date = today
|
||||
return ssl_context
|
||||
|
||||
del ponyconfig
|
||||
gc.collect() # free intermediate objects used during setup
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
@@ -263,12 +281,12 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
||||
assert ctx.server is None
|
||||
try:
|
||||
ctx.server = websockets.serve(
|
||||
functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context)
|
||||
functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=get_ssl_context())
|
||||
|
||||
await ctx.server
|
||||
except OSError: # likely port in use
|
||||
ctx.server = websockets.serve(
|
||||
functools.partial(server, ctx=ctx), ctx.host, 0, ssl=ssl_context)
|
||||
functools.partial(server, ctx=ctx), ctx.host, 0, ssl=get_ssl_context())
|
||||
|
||||
await ctx.server
|
||||
port = 0
|
||||
|
||||
@@ -135,6 +135,7 @@ def gen_game(gen_options: dict, meta: Optional[Dict[str, Any]] = None, owner=Non
|
||||
{"bosses", "items", "connections", "texts"}))
|
||||
erargs.skip_prog_balancing = False
|
||||
erargs.skip_output = False
|
||||
erargs.spoiler_only = False
|
||||
erargs.csv_output = False
|
||||
|
||||
name_counter = Counter()
|
||||
|
||||
@@ -35,6 +35,12 @@ def start_playing():
|
||||
@app.route('/games/<string:game>/info/<string:lang>')
|
||||
@cache.cached()
|
||||
def game_info(game, lang):
|
||||
try:
|
||||
world = AutoWorldRegister.world_types[game]
|
||||
if lang not in world.web.game_info_languages:
|
||||
raise KeyError("Sorry, this game's info page is not available in that language yet.")
|
||||
except KeyError:
|
||||
return abort(404)
|
||||
return render_template('gameInfo.html', game=game, lang=lang, theme=get_world_theme(game))
|
||||
|
||||
|
||||
@@ -52,6 +58,12 @@ def games():
|
||||
@app.route('/tutorial/<string:game>/<string:file>/<string:lang>')
|
||||
@cache.cached()
|
||||
def tutorial(game, file, lang):
|
||||
try:
|
||||
world = AutoWorldRegister.world_types[game]
|
||||
if lang not in [tut.link.split("/")[1] for tut in world.web.tutorials]:
|
||||
raise KeyError("Sorry, the tutorial is not available in that language yet.")
|
||||
except KeyError:
|
||||
return abort(404)
|
||||
return render_template("tutorial.html", game=game, file=file, lang=lang, theme=get_world_theme(game))
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ from typing import Dict, Union
|
||||
from docutils.core import publish_parts
|
||||
|
||||
import yaml
|
||||
from flask import redirect, render_template, request, Response
|
||||
from flask import redirect, render_template, request, Response, abort
|
||||
|
||||
import Options
|
||||
from Utils import local_path
|
||||
@@ -142,7 +142,10 @@ def weighted_options_old():
|
||||
@app.route("/games/<string:game>/weighted-options")
|
||||
@cache.cached()
|
||||
def weighted_options(game: str):
|
||||
return render_options_page("weightedOptions/weightedOptions.html", game, is_complex=True)
|
||||
try:
|
||||
return render_options_page("weightedOptions/weightedOptions.html", game, is_complex=True)
|
||||
except KeyError:
|
||||
return abort(404)
|
||||
|
||||
|
||||
@app.route("/games/<string:game>/generate-weighted-yaml", methods=["POST"])
|
||||
@@ -197,7 +200,10 @@ def generate_weighted_yaml(game: str):
|
||||
@app.route("/games/<string:game>/player-options")
|
||||
@cache.cached()
|
||||
def player_options(game: str):
|
||||
return render_options_page("playerOptions/playerOptions.html", game, is_complex=False)
|
||||
try:
|
||||
return render_options_page("playerOptions/playerOptions.html", game, is_complex=False)
|
||||
except KeyError:
|
||||
return abort(404)
|
||||
|
||||
|
||||
# YAML generator for player-options
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
flask>=3.0.3
|
||||
werkzeug>=3.0.6
|
||||
flask>=3.1.0
|
||||
werkzeug>=3.1.3
|
||||
pony>=0.7.19
|
||||
waitress>=3.0.0
|
||||
waitress>=3.0.2
|
||||
Flask-Caching>=2.3.0
|
||||
Flask-Compress>=1.15
|
||||
Flask-Limiter>=3.8.0
|
||||
bokeh>=3.5.2
|
||||
markupsafe>=2.1.5
|
||||
Flask-Compress>=1.17
|
||||
Flask-Limiter>=3.12
|
||||
bokeh>=3.6.3
|
||||
markupsafe>=3.0.2
|
||||
Markdown>=3.7
|
||||
mdx-breakless-lists>=1.0.1
|
||||
setproctitle>=1.3.5
|
||||
|
||||
@@ -23,7 +23,6 @@ window.addEventListener('load', () => {
|
||||
showdown.setOption('strikethrough', true);
|
||||
showdown.setOption('literalMidWordUnderscores', true);
|
||||
gameInfo.innerHTML += (new showdown.Converter()).makeHtml(results);
|
||||
adjustHeaderWidth();
|
||||
|
||||
// Reset the id of all header divs to something nicer
|
||||
for (const header of document.querySelectorAll('h1, h2, h3, h4, h5, h6')) {
|
||||
@@ -42,10 +41,5 @@ window.addEventListener('load', () => {
|
||||
scrollTarget?.scrollIntoView();
|
||||
}
|
||||
});
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
gameInfo.innerHTML =
|
||||
`<h2>This page is out of logic!</h2>
|
||||
<h3>Click <a href="${window.location.origin}">here</a> to return to safety.</h3>`;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,4 @@ window.addEventListener('load', () => {
|
||||
document.getElementById('file-input').addEventListener('change', () => {
|
||||
document.getElementById('host-game-form').submit();
|
||||
});
|
||||
|
||||
adjustFooterHeight();
|
||||
});
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
const adjustFooterHeight = () => {
|
||||
// If there is no footer on this page, do nothing
|
||||
const footer = document.getElementById('island-footer');
|
||||
if (!footer) { return; }
|
||||
|
||||
// If the body is taller than the window, also do nothing
|
||||
if (document.body.offsetHeight > window.innerHeight) {
|
||||
footer.style.marginTop = '0';
|
||||
return;
|
||||
}
|
||||
|
||||
// Add a margin-top to the footer to position it at the bottom of the screen
|
||||
const sibling = footer.previousElementSibling;
|
||||
const margin = (window.innerHeight - sibling.offsetTop - sibling.offsetHeight - footer.offsetHeight);
|
||||
if (margin < 1) {
|
||||
footer.style.marginTop = '0';
|
||||
return;
|
||||
}
|
||||
footer.style.marginTop = `${margin}px`;
|
||||
};
|
||||
|
||||
const adjustHeaderWidth = () => {
|
||||
// If there is no header, do nothing
|
||||
const header = document.getElementById('base-header');
|
||||
if (!header) { return; }
|
||||
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.style.width = '100px';
|
||||
tempDiv.style.height = '100px';
|
||||
tempDiv.style.overflow = 'scroll';
|
||||
tempDiv.style.position = 'absolute';
|
||||
tempDiv.style.top = '-500px';
|
||||
document.body.appendChild(tempDiv);
|
||||
const scrollbarWidth = tempDiv.offsetWidth - tempDiv.clientWidth;
|
||||
document.body.removeChild(tempDiv);
|
||||
|
||||
const documentRoot = document.compatMode === 'BackCompat' ? document.body : document.documentElement;
|
||||
const margin = (documentRoot.scrollHeight > documentRoot.clientHeight) ? 0-scrollbarWidth : 0;
|
||||
document.getElementById('base-header-right').style.marginRight = `${margin}px`;
|
||||
};
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
window.addEventListener('resize', adjustFooterHeight);
|
||||
window.addEventListener('resize', adjustHeaderWidth);
|
||||
adjustFooterHeight();
|
||||
adjustHeaderWidth();
|
||||
});
|
||||
@@ -25,7 +25,6 @@ window.addEventListener('load', () => {
|
||||
showdown.setOption('literalMidWordUnderscores', true);
|
||||
showdown.setOption('disableForced4SpacesIndentedSublists', true);
|
||||
tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results);
|
||||
adjustHeaderWidth();
|
||||
|
||||
const title = document.querySelector('h1')
|
||||
if (title) {
|
||||
@@ -49,10 +48,5 @@ window.addEventListener('load', () => {
|
||||
scrollTarget?.scrollIntoView();
|
||||
}
|
||||
});
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
tutorialWrapper.innerHTML =
|
||||
`<h2>This page is out of logic!</h2>
|
||||
<h3>Click <a href="${window.location.origin}/tutorial">here</a> to return to safety.</h3>`;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,6 +36,13 @@ html{
|
||||
|
||||
body{
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: calc(100vh - 110px);
|
||||
}
|
||||
|
||||
main {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
a{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
{% import "macros.html" as macros %}
|
||||
{% set show_footer = True %}
|
||||
|
||||
{% block head %}
|
||||
<title>Page Not Found (404)</title>
|
||||
@@ -13,5 +14,4 @@
|
||||
The page you're looking for doesn't exist.<br />
|
||||
<a href="/">Click here to return to safety.</a>
|
||||
</div>
|
||||
{% include 'islandFooter.html' %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
{% set show_footer = True %}
|
||||
|
||||
{% block head %}
|
||||
<title>Upload Multidata</title>
|
||||
@@ -27,6 +28,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'islandFooter.html' %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
{% set show_footer = True %}
|
||||
|
||||
{% block head %}
|
||||
<title>Archipelago</title>
|
||||
@@ -57,5 +58,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'islandFooter.html' %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -5,26 +5,29 @@
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tooltip.css") }}" />
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/cookieNotice.css") }}" />
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/globalStyles.css") }}" />
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/styleController.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/cookieNotice.js") }}"></script>
|
||||
{% block head %}
|
||||
<title>Archipelago</title>
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div>
|
||||
{% for message in messages | unique %}
|
||||
<div class="user-message">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div>
|
||||
{% for message in messages | unique %}
|
||||
<div class="user-message">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% block body %}
|
||||
{% endblock %}
|
||||
</main>
|
||||
|
||||
{% if show_footer %}
|
||||
{% include "islandFooter.html" %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% block body %}
|
||||
{% endblock %}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
{% import "macros.html" as macros %}
|
||||
{% set show_footer = True %}
|
||||
|
||||
{% block head %}
|
||||
<title>Generation failed, please retry.</title>
|
||||
@@ -15,5 +16,4 @@
|
||||
{{ seed_error }}
|
||||
</div>
|
||||
</div>
|
||||
{% include 'islandFooter.html' %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
{% set show_footer = True %}
|
||||
|
||||
{% block head %}
|
||||
<title>Start Playing</title>
|
||||
@@ -26,6 +27,4 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'islandFooter.html' %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
{% import "macros.html" as macros %}
|
||||
{% set show_footer = True %}
|
||||
|
||||
{% block head %}
|
||||
<title>View Seed {{ seed.id|suuid }}</title>
|
||||
@@ -50,5 +51,4 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'islandFooter.html' %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
{% import "macros.html" as macros %}
|
||||
{% set show_footer = True %}
|
||||
|
||||
{% block head %}
|
||||
<title>Generation in Progress</title>
|
||||
<meta http-equiv="refresh" content="1">
|
||||
<noscript>
|
||||
<meta http-equiv="refresh" content="1">
|
||||
</noscript>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/waitSeed.css") }}"/>
|
||||
{% endblock %}
|
||||
|
||||
@@ -15,5 +18,34 @@
|
||||
Waiting for game to generate, this page auto-refreshes to check.
|
||||
</div>
|
||||
</div>
|
||||
{% include 'islandFooter.html' %}
|
||||
<script>
|
||||
const waitSeedDiv = document.getElementById("wait-seed");
|
||||
async function checkStatus() {
|
||||
try {
|
||||
const response = await fetch("{{ url_for('api.wait_seed_api', seed=seed_id) }}");
|
||||
if (response.status !== 202) {
|
||||
// Seed is ready; reload page to load seed page.
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
waitSeedDiv.innerHTML = `
|
||||
<h1>Generation in Progress</h1>
|
||||
<p>${data.text}</p>
|
||||
`;
|
||||
|
||||
setTimeout(checkStatus, 1000); // Continue polling.
|
||||
} catch (error) {
|
||||
waitSeedDiv.innerHTML = `
|
||||
<h1>Progress Unknown</h1>
|
||||
<p>${error.message}<br />(Last checked: ${new Date().toLocaleTimeString()})</p>
|
||||
`;
|
||||
|
||||
setTimeout(checkStatus, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(checkStatus, 1000);
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -14,23 +14,51 @@
|
||||
salmon: "FA8072" # typically trap item
|
||||
white: "FFFFFF" # not used, if you want to change the generic text color change color in Label
|
||||
orange: "FF7700" # Used for command echo
|
||||
<Label>:
|
||||
color: "FFFFFF"
|
||||
<TabbedPanel>:
|
||||
tab_width: root.width / app.tab_count
|
||||
# KivyMD theming parameters
|
||||
theme_style: "Dark" # Light/Dark
|
||||
primary_palette: "Green" # Many options
|
||||
dynamic_scheme_name: "TONAL_SPOT"
|
||||
dynamic_scheme_contrast: 0.0
|
||||
<MDLabel>:
|
||||
color: self.theme_cls.primaryColor
|
||||
<TooltipLabel>:
|
||||
text_size: self.width, None
|
||||
size_hint_y: None
|
||||
height: self.texture_size[1]
|
||||
adaptive_height: True
|
||||
font_size: dp(20)
|
||||
markup: True
|
||||
halign: "left"
|
||||
<SelectableLabel>:
|
||||
size_hint: 1, None
|
||||
canvas.before:
|
||||
Color:
|
||||
rgba: (.0, 0.9, .1, .3) if self.selected else (0.2, 0.2, 0.2, 1)
|
||||
rgba: (.0, 0.9, .1, .3) if self.selected else self.theme_cls.surfaceContainerLowColor
|
||||
Rectangle:
|
||||
size: self.size
|
||||
pos: self.pos
|
||||
<MarkupDropdownItem>
|
||||
orientation: "vertical"
|
||||
|
||||
MDLabel:
|
||||
text: root.text
|
||||
valign: "center"
|
||||
padding_x: "12dp"
|
||||
shorten: True
|
||||
shorten_from: "right"
|
||||
theme_text_color: "Custom"
|
||||
markup: True
|
||||
text_color:
|
||||
app.theme_cls.onSurfaceVariantColor \
|
||||
if not root.text_color else \
|
||||
root.text_color
|
||||
|
||||
MDDivider:
|
||||
md_bg_color:
|
||||
( \
|
||||
app.theme_cls.outlineVariantColor \
|
||||
if not root.divider_color \
|
||||
else root.divider_color \
|
||||
) \
|
||||
if root.divider else \
|
||||
(0, 0, 0, 0)
|
||||
<UILog>:
|
||||
messages: 1000 # amount of messages stored in client logs.
|
||||
cols: 1
|
||||
@@ -49,7 +77,7 @@
|
||||
<HintLabel>:
|
||||
canvas.before:
|
||||
Color:
|
||||
rgba: (.0, 0.9, .1, .3) if self.selected else (0.2, 0.2, 0.2, 1) if self.striped else (0.18, 0.18, 0.18, 1)
|
||||
rgba: (.0, 0.9, .1, .3) if self.selected else self.theme_cls.surfaceContainerHighColor if self.striped else self.theme_cls.surfaceContainerLowColor
|
||||
Rectangle:
|
||||
size: self.size
|
||||
pos: self.pos
|
||||
@@ -152,3 +180,16 @@
|
||||
height: dp(30)
|
||||
multiline: False
|
||||
write_tab: False
|
||||
<ScrollBox>:
|
||||
layout: layout
|
||||
bar_width: "12dp"
|
||||
scroll_wheel_distance: 40
|
||||
do_scroll_x: False
|
||||
scroll_type: ['bars', 'content']
|
||||
|
||||
MDBoxLayout:
|
||||
id: layout
|
||||
orientation: "vertical"
|
||||
spacing: 10
|
||||
size_hint_y: None
|
||||
height: self.minimum_height
|
||||
|
||||
142
data/launcher.kv
Normal file
142
data/launcher.kv
Normal file
@@ -0,0 +1,142 @@
|
||||
<LauncherCard>:
|
||||
id: main
|
||||
style: "filled"
|
||||
padding: "4dp"
|
||||
size_hint: 1, None
|
||||
height: "75dp"
|
||||
context_button: context
|
||||
|
||||
MDRelativeLayout:
|
||||
ApAsyncImage:
|
||||
source: main.image
|
||||
size: (48, 48)
|
||||
size_hint_y: None
|
||||
pos_hint: {"center_x": 0.1, "center_y": 0.5}
|
||||
|
||||
MDLabel:
|
||||
text: main.component.display_name
|
||||
pos_hint:{"center_x": 0.5, "center_y": 0.75 if main.component.description else 0.65}
|
||||
halign: "center"
|
||||
font_style: "Title"
|
||||
role: "medium"
|
||||
theme_text_color: "Custom"
|
||||
text_color: app.theme_cls.primaryColor
|
||||
|
||||
MDLabel:
|
||||
text: main.component.description
|
||||
pos_hint: {"center_x": 0.5, "center_y": 0.35}
|
||||
halign: "center"
|
||||
role: "small"
|
||||
theme_text_color: "Custom"
|
||||
text_color: app.theme_cls.primaryColor
|
||||
|
||||
MDIconButton:
|
||||
component: main.component
|
||||
icon: "star" if self.component.display_name in app.favorites else "star-outline"
|
||||
style: "standard"
|
||||
pos_hint:{"center_x": 0.85, "center_y": 0.8}
|
||||
theme_text_color: "Custom"
|
||||
text_color: app.theme_cls.primaryColor
|
||||
on_release: app.set_favorite(self)
|
||||
|
||||
MDIconButton:
|
||||
id: context
|
||||
icon: "menu"
|
||||
style: "standard"
|
||||
pos_hint:{"center_x": 0.95, "center_y": 0.8}
|
||||
theme_text_color: "Custom"
|
||||
text_color: app.theme_cls.primaryColor
|
||||
|
||||
MDButton:
|
||||
pos_hint:{"center_x": 0.9, "center_y": 0.25}
|
||||
size_hint_y: None
|
||||
height: "25dp"
|
||||
component: main.component
|
||||
on_release: app.component_action(self)
|
||||
|
||||
MDButtonText:
|
||||
text: "Open"
|
||||
|
||||
|
||||
#:import Type worlds.LauncherComponents.Type
|
||||
MDFloatLayout:
|
||||
id: top_screen
|
||||
|
||||
MDGridLayout:
|
||||
id: grid
|
||||
cols: 2
|
||||
spacing: "5dp"
|
||||
padding: "10dp"
|
||||
|
||||
MDGridLayout:
|
||||
id: navigation
|
||||
cols: 1
|
||||
size_hint_x: 0.25
|
||||
|
||||
MDButton:
|
||||
id: all
|
||||
style: "text"
|
||||
type: (Type.CLIENT, Type.TOOL, Type.ADJUSTER, Type.MISC)
|
||||
on_release: app.filter_clients(self)
|
||||
|
||||
MDButtonIcon:
|
||||
icon: "asterisk"
|
||||
MDButtonText:
|
||||
text: "All"
|
||||
MDButton:
|
||||
id: client
|
||||
style: "text"
|
||||
type: (Type.CLIENT, )
|
||||
on_release: app.filter_clients(self)
|
||||
|
||||
MDButtonIcon:
|
||||
icon: "controller"
|
||||
MDButtonText:
|
||||
text: "Client"
|
||||
MDButton:
|
||||
id: Tool
|
||||
style: "text"
|
||||
type: (Type.TOOL, )
|
||||
on_release: app.filter_clients(self)
|
||||
|
||||
MDButtonIcon:
|
||||
icon: "desktop-classic"
|
||||
MDButtonText:
|
||||
text: "Tool"
|
||||
MDButton:
|
||||
id: adjuster
|
||||
style: "text"
|
||||
type: (Type.ADJUSTER, )
|
||||
on_release: app.filter_clients(self)
|
||||
|
||||
MDButtonIcon:
|
||||
icon: "wrench"
|
||||
MDButtonText:
|
||||
text: "Adjuster"
|
||||
MDButton:
|
||||
id: misc
|
||||
style: "text"
|
||||
type: (Type.MISC, )
|
||||
on_release: app.filter_clients(self)
|
||||
|
||||
MDButtonIcon:
|
||||
icon: "dots-horizontal-circle-outline"
|
||||
MDButtonText:
|
||||
text: "Misc"
|
||||
|
||||
MDButton:
|
||||
id: favorites
|
||||
style: "text"
|
||||
type: ("favorites", )
|
||||
on_release: app.filter_clients(self)
|
||||
|
||||
MDButtonIcon:
|
||||
icon: "star"
|
||||
MDButtonText:
|
||||
text: "Favorites"
|
||||
|
||||
MDNavigationDrawerDivider:
|
||||
|
||||
|
||||
ScrollBox:
|
||||
id: button_layout
|
||||
@@ -214,6 +214,9 @@
|
||||
# Wargroove
|
||||
/worlds/wargroove/ @FlySniper
|
||||
|
||||
# The Wind Waker
|
||||
/worlds/tww/ @tanjo3
|
||||
|
||||
# The Witness
|
||||
/worlds/witness/ @NewSoupVi @blastron
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# Adding Games
|
||||
|
||||
Like all contributions to Archipelago, New Game implementations should follow the [Contributing](/docs/contributing.md)
|
||||
guide.
|
||||
|
||||
Adding a new game to Archipelago has two major parts:
|
||||
|
||||
* Game Modification to communicate with Archipelago server (hereafter referred to as "client")
|
||||
@@ -13,30 +16,51 @@ it will not be detailed here.
|
||||
|
||||
The client is an intermediary program between the game and the Archipelago server. This can either be a direct
|
||||
modification to the game, an external program, or both. This can be implemented in nearly any modern language, but it
|
||||
must fulfill a few requirements in order to function as expected. The specific requirements the game client must follow
|
||||
to behave as expected are:
|
||||
must fulfill a few requirements in order to function as expected. Libraries for most modern languages and the spec for
|
||||
various packets can be found in the [network protocol](/docs/network%20protocol.md) API reference document.
|
||||
|
||||
### Hard Requirements
|
||||
|
||||
In order for the game client to behave as expected, it must be able to perform these functions:
|
||||
|
||||
* Handle both secure and unsecure websocket connections
|
||||
* Detect and react when a location has been "checked" by the player by sending a network packet to the server
|
||||
* Receive and parse network packets when the player receives an item from the server, and reward it to the player on
|
||||
demand
|
||||
* **Any** of your items can be received any number of times, up to and far surpassing those that the game might
|
||||
normally expect from features such as starting inventory, item link replacement, or item cheating
|
||||
* Players and the admin can cheat items to the player at any time with a server command, and these items may not have
|
||||
a player or location attributed to them
|
||||
* Reconnect if the connection is unstable and lost while playing
|
||||
* Be able to change the port for saved connection info
|
||||
* Rooms hosted on the website attempt to reserve their port, but since there are a limited number of ports, this
|
||||
privilege can be lost, requiring the room to be moved to a new port
|
||||
* Reconnect if the connection is unstable and lost while playing
|
||||
* Keep an index for items received in order to resync. The ItemsReceived Packets are a single list with guaranteed
|
||||
order.
|
||||
* Receive items that were sent to the player while they were not connected to the server
|
||||
* The player being able to complete checks while offline and sending them when reconnecting is a good bonus, but not
|
||||
strictly required
|
||||
privilege can be lost, requiring the room to be moved to a new port
|
||||
* Send a status update packet alerting the server that the player has completed their goal
|
||||
|
||||
Libraries for most modern languages and the spec for various packets can be found in the
|
||||
[network protocol](/docs/network%20protocol.md) API reference document.
|
||||
Regarding items and locations, the game client must be able to handle these tasks:
|
||||
|
||||
#### Location Handling
|
||||
|
||||
Send a network packet to the server when it detects a location has been "checked" by the player in-game.
|
||||
|
||||
* If actions were taken in game that would usually trigger a location check, and those actions can only ever be taken
|
||||
once, but the client was not connected when they happened: The client must send those location checks on connection
|
||||
so that they are not permanently lost, e.g. by reading flags in the game state or save file.
|
||||
|
||||
#### Item Handling
|
||||
|
||||
Receive and parse network packets from the server when the player receives an item.
|
||||
|
||||
* It must reward items to the player on demand, as items can come from other players at any time.
|
||||
* It must be able to reward copies of an item, up to and beyond the number the game normally expects. This may happen
|
||||
due to features such as starting inventory, item link replacement, admin commands, or item cheating. **Any** of
|
||||
your items can be received **any** number of times.
|
||||
* Admins and players may use server commands to create items without a player or location attributed to them. The
|
||||
client must be able to handle these items.
|
||||
* It must keep an index for items received in order to resync. The ItemsReceived Packets are a single list with a
|
||||
guaranteed order.
|
||||
* It must be able to receive items that were sent to the player while they were not connected to the server.
|
||||
|
||||
### Encouraged Features
|
||||
|
||||
These are "nice to have" features for a client, but they are not strictly required. It is encouraged to add them
|
||||
if possible.
|
||||
|
||||
* If your client appears in the Archipelago Launcher, you may define an icon for it that differentiates it from
|
||||
other clients. The icon size is 48x48 pixels, but smaller or larger images will scale to that size.
|
||||
|
||||
## World
|
||||
|
||||
@@ -44,35 +68,94 @@ The world is your game integration for the Archipelago generator, webhost, and m
|
||||
information necessary for creating the items and locations to be randomized, the logic for item placement, the
|
||||
datapackage information so other game clients can recognize your game data, and documentation. Your world must be
|
||||
written as a Python package to be loaded by Archipelago. This is currently done by creating a fork of the Archipelago
|
||||
repository and creating a new world package in `/worlds/`. A bare minimum world implementation must satisfy the
|
||||
following requirements:
|
||||
repository and creating a new world package in `/worlds/`.
|
||||
|
||||
* A folder within `/worlds/` that contains an `__init__.py`
|
||||
* A `World` subclass where you create your world and define all of its rules
|
||||
* A unique game name
|
||||
* For webhost documentation and behaviors, a `WebWorld` subclass that must be instantiated in the `World` class
|
||||
definition
|
||||
* The game_info doc must follow the format `{language_code}_{game_name}.md`
|
||||
The base World class can be found in [AutoWorld](/worlds/AutoWorld.py). Methods available for your world to call
|
||||
during generation can be found in [BaseClasses](/BaseClasses.py) and [Fill](/Fill.py). Some examples and documentation
|
||||
regarding the API can be found in the [world api doc](/docs/world%20api.md). Before publishing, make sure to also
|
||||
check out [world maintainer.md](/docs/world%20maintainer.md).
|
||||
|
||||
### Hard Requirements
|
||||
|
||||
A bare minimum world implementation must satisfy the following requirements:
|
||||
|
||||
* It has a folder with the name of your game (or an abbreviation) under `/worlds/`
|
||||
* The `/worlds/{game}` folder contains an `__init__.py`
|
||||
* Any subfolders within `/worlds/{game}` that contain `*.py` files also contain an `__init__.py` for frozen build
|
||||
packaging
|
||||
* The game folder has at least one game_info doc named with follow the format `{language_code}_{game_name}.md`
|
||||
* The game folder has at least one setup doc
|
||||
* There must be a `World` subclass in your game folder (typically in `/worlds/{game}/__init__.py`) where you create
|
||||
your world and define all of its rules and features
|
||||
|
||||
Within the `World` subclass you should also have:
|
||||
|
||||
* A [unique game name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L260)
|
||||
* An [instance](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L295) of a `WebWorld`
|
||||
subclass for webhost documentation and behaviors
|
||||
* In your `WebWorld`, if you wrote a game_info doc in more than one language, override the list of
|
||||
[game info languages](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L210) with the
|
||||
ones you include.
|
||||
* In your `WebWorld`, override the list of
|
||||
[tutorials](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L213) with each tutorial
|
||||
or setup doc you included in the game folder.
|
||||
* A mapping for items and locations defining their names and ids for clients to be able to identify them. These are
|
||||
`item_name_to_id` and `location_name_to_id`, respectively.
|
||||
* Create an item when `create_item` is called both by your code and externally
|
||||
* An `options_dataclass` defining the options players have available to them
|
||||
* A `Region` for your player with the name "Menu" to start from
|
||||
* Create a non-zero number of locations and add them to your regions
|
||||
* Create a non-zero number of items **equal** to the number of locations and add them to the multiworld itempool
|
||||
* All items submitted to the multiworld itempool must not be manually placed by the World. If you need to place specific
|
||||
items, there are multiple ways to do so, but they should not be added to the multiworld itempool.
|
||||
`item_name_to_id` and `location_name_to_id`, respectively.
|
||||
* An implementation of `create_item` that can create an item when called by either your code or by another process
|
||||
within Archipelago
|
||||
* At least one `Region` for your player to start from (i.e. the Origin Region)
|
||||
* The default name of this region is "Menu" but you may configure a different name with
|
||||
[origin_region_name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L298-L299)
|
||||
* A non-zero number of locations, added to your regions
|
||||
* A non-zero number of items **equal** to the number of locations, added to the multiworld itempool
|
||||
* In rare cases, there may be 0-location-0-item games, but this is extremely atypical.
|
||||
* A set
|
||||
[completion condition](https://github.com/ArchipelagoMW/Archipelago/blob/main/BaseClasses.py#L77) (aka "goal") for
|
||||
the player.
|
||||
* Use your player as the index (`multiworld.completion_condition[player]`) for your world's completion goal.
|
||||
|
||||
Notable caveats:
|
||||
* The "Menu" region will always be considered the "start" for the player
|
||||
* The "Menu" region is *always* considered accessible; i.e. the player is expected to always be able to return to the
|
||||
### Encouraged Features
|
||||
|
||||
These are "nice to have" features for a world, but they are not strictly required. It is encouraged to add them
|
||||
if possible.
|
||||
|
||||
* An implementation of
|
||||
[get_filler_item_name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L473)
|
||||
* By default, this function chooses any item name from `item_name_to_id`, so you want to limit it to only the true
|
||||
filler items.
|
||||
* An `options_dataclass` defining the options players have available to them
|
||||
* This should be accompanied by a type hint for `options` with the same class name
|
||||
* A [bug report page](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L220)
|
||||
* A list of [option groups](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L226)
|
||||
for better organization on the webhost
|
||||
* A dictionary of [options presets](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L223)
|
||||
for player convenience
|
||||
* A dictionary of [item name groups](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L273)
|
||||
for player convenience
|
||||
* A dictionary of
|
||||
[location name groups](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L276)
|
||||
for player convenience
|
||||
* Other games may also benefit from your name group dictionaries for hints, features, etc.
|
||||
|
||||
### Discouraged or Prohibited Behavior
|
||||
|
||||
These are behaviors or implementations that are known to cause various issues. Some of these points have notable
|
||||
workarounds or preferred methods which should be used instead:
|
||||
|
||||
* All items submitted to the multiworld itempool must not be manually placed by the World.
|
||||
* If you need to place specific items, there are multiple ways to do so, but they should not be added to the
|
||||
multiworld itempool.
|
||||
* It is not allowed to use `eval` for most reasons, chiefly due to security concerns.
|
||||
* It is discouraged to use PyYAML (i.e. `yaml.load`) directly due to security concerns.
|
||||
* When possible, use `Utils.parse_yaml` instead, as this defaults to the safe loader and the faster C parser.
|
||||
* When submitting regions or items to the multiworld (`multiworld.regions` and `multiworld.itempool` respectively),
|
||||
do **not** use `=` as this will overwrite all elements for all games in the seed.
|
||||
* Instead, use `append`, `extend`, or `+=`.
|
||||
|
||||
### Notable Caveats
|
||||
|
||||
* The Origin Region will always be considered the "start" for the player
|
||||
* The Origin Region is *always* considered accessible; i.e. the player is expected to always be able to return to the
|
||||
start of the game from anywhere
|
||||
* When submitting regions or items to the multiworld (multiworld.regions and multiworld.itempool respectively), use
|
||||
`append`, `extend`, or `+=`. **Do not use `=`**
|
||||
* Regions are simply containers for locations that share similar access rules. They do not have to map to
|
||||
concrete, physical areas within your game and can be more abstract like tech trees or a questline.
|
||||
|
||||
The base World class can be found in [AutoWorld](/worlds/AutoWorld.py). Methods available for your world to call during
|
||||
generation can be found in [BaseClasses](/BaseClasses.py) and [Fill](/Fill.py). Some examples and documentation
|
||||
regarding the API can be found in the [world api doc](/docs/world%20api.md).
|
||||
Before publishing, make sure to also check out [world maintainer.md](/docs/world%20maintainer.md).
|
||||
|
||||
@@ -66,3 +66,22 @@ The reason entrance access rules using `location.can_reach` and `entrance.can_re
|
||||
We recognize it can feel like a trap since it will not alert you when you are missing an indirect condition, and that some games have very complex access rules.
|
||||
As of [PR #3682 (Core: Region handling customization)](https://github.com/ArchipelagoMW/Archipelago/pull/3682) being merged, it is possible for a world to opt out of indirect conditions entirely, instead using the system of checking each entrance whenever a region has been reached, although this does come with a performance cost.
|
||||
Opting out of using indirect conditions should only be used by games that *really* need it. For most games, it should be reasonable to know all entrance → region dependencies, making indirect conditions preferred because they are much faster.
|
||||
|
||||
---
|
||||
|
||||
### I uploaded the generated output of my world to the webhost and webhost is erroring on corrupted multidata
|
||||
|
||||
The error `Could not load multidata. File may be corrupted or incompatible.` occurs when uploading a locally generated
|
||||
file where there is an issue with the multidata contained within it. It may come with a description like
|
||||
`(No module named 'worlds.myworld')` or `(global 'worlds.myworld.names.ItemNames' is forbidden)`
|
||||
|
||||
Pickling is a way to compress python objects such that they can be decompressed and be used to rebuild the
|
||||
python objects. This means that if one of your custom class instances ends up in the multidata, the server would not
|
||||
be able to load that custom class to decompress the data, which can fail either because the custom class is unknown
|
||||
(because it cannot load your world module) or the class it's attempting to import to decompress is deemed unsafe.
|
||||
|
||||
Common situations where this can happen include:
|
||||
* Using Option instances directly in slot_data. Ex: using `options.option_name` instead of `options.option_name.value`.
|
||||
Also, consider using the `options.as_dict("option_name", "option_two")` helper.
|
||||
* Using enums as Location/Item names in the datapackage. When building out `location_name_to_id` and `item_name_to_id`,
|
||||
make sure that you are not using your enum class for either the names or ids in these mappings.
|
||||
|
||||
@@ -470,7 +470,7 @@ The following operations can be applied to a datastorage key
|
||||
| right_shift | Applies a bitwise right-shift to the current value of the key by `value`. |
|
||||
| remove | List only: removes the first instance of `value` found in the list. |
|
||||
| pop | List or Dict: for lists it will remove the index of the `value` given. for dicts it removes the element with the specified key of `value`. |
|
||||
| update | Dict only: Updates the dictionary with the specified elements given in `value` creating new keys, or updating old ones if they previously existed. |
|
||||
| update | List or Dict: Adds the elements of `value` to the container if they weren't already present. In the case of a Dict, already present keys will have their corresponding values updated. |
|
||||
|
||||
### SetNotify
|
||||
Used to register your current session for receiving all [SetReply](#SetReply) packages of certain keys to allow your client to keep track of changes.
|
||||
@@ -756,8 +756,8 @@ Tags are represented as a list of strings, the common client tags follow:
|
||||
### DeathLink
|
||||
A special kind of Bounce packet that can be supported by any AP game. It targets the tag "DeathLink" and carries the following data:
|
||||
|
||||
| Name | Type | Notes |
|
||||
|--------|-------|--------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| time | float | Unix Time Stamp of time of death. |
|
||||
| cause | str | Optional. Text to explain the cause of death. When provided, or checked, this should contain the player name, ex. "Berserker was run over by a train." |
|
||||
| source | str | Name of the player who first died. Can be a slot name, but can also be a name from within a multiplayer game. |
|
||||
| Name | Type | Notes |
|
||||
|--------|-------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| time | float | Unix Time Stamp of time of death. |
|
||||
| cause | str | Optional. Text to explain the cause of death. When provided, or checked, if the string is non-empty, it should contain the player name, ex. "Berserker was run over by a train." |
|
||||
| source | str | Name of the player who first died. Can be a slot name, but can also be a name from within a multiplayer game. |
|
||||
|
||||
@@ -606,8 +606,8 @@ from .items import get_item_type
|
||||
|
||||
def set_rules(self) -> None:
|
||||
# For some worlds this step can be omitted if either a Logic mixin
|
||||
# (see below) is used, it's easier to apply the rules from data during
|
||||
# location generation or everything is in generate_basic
|
||||
# (see below) is used or it's easier to apply the rules from data during
|
||||
# location generation
|
||||
|
||||
# set a simple rule for an region
|
||||
set_rule(self.multiworld.get_entrance("Boss Door", self.player),
|
||||
|
||||
@@ -50,13 +50,15 @@ class EntranceLookup:
|
||||
_random: random.Random
|
||||
_expands_graph_cache: dict[Entrance, bool]
|
||||
_coupled: bool
|
||||
_usable_exits: set[Entrance]
|
||||
|
||||
def __init__(self, rng: random.Random, coupled: bool):
|
||||
def __init__(self, rng: random.Random, coupled: bool, usable_exits: set[Entrance]):
|
||||
self.dead_ends = EntranceLookup.GroupLookup()
|
||||
self.others = EntranceLookup.GroupLookup()
|
||||
self._random = rng
|
||||
self._expands_graph_cache = {}
|
||||
self._coupled = coupled
|
||||
self._usable_exits = usable_exits
|
||||
|
||||
def _can_expand_graph(self, entrance: Entrance) -> bool:
|
||||
"""
|
||||
@@ -95,7 +97,8 @@ class EntranceLookup:
|
||||
# randomizable exits which are not reverse of the incoming entrance.
|
||||
# uncoupled mode is an exception because in this case going back in the door you just came in could
|
||||
# actually lead somewhere new
|
||||
if not exit_.connected_region and (not self._coupled or exit_.name != entrance.name):
|
||||
if (not exit_.connected_region and (not self._coupled or exit_.name != entrance.name)
|
||||
and exit_ in self._usable_exits):
|
||||
self._expands_graph_cache[entrance] = True
|
||||
return True
|
||||
elif exit_.connected_region and exit_.connected_region not in visited:
|
||||
@@ -265,14 +268,19 @@ def bake_target_group_lookup(world: World, get_target_groups: Callable[[int], li
|
||||
return { group: get_target_groups(group) for group in unique_groups }
|
||||
|
||||
|
||||
def disconnect_entrance_for_randomization(entrance: Entrance, target_group: int | None = None) -> None:
|
||||
def disconnect_entrance_for_randomization(entrance: Entrance, target_group: int | None = None,
|
||||
one_way_target_name: str | None = None) -> None:
|
||||
"""
|
||||
Given an entrance in a "vanilla" region graph, splits that entrance to prepare it for randomization
|
||||
in randomize_entrances. This should be done after setting the type and group of the entrance.
|
||||
in randomize_entrances. This should be done after setting the type and group of the entrance. Because it attempts
|
||||
to meet strict entrance naming requirements for coupled mode, this function may produce unintuitive results when
|
||||
called only on a single entrance; it produces eventually-correct outputs only after calling it on all entrances.
|
||||
|
||||
:param entrance: The entrance which will be disconnected in preparation for randomization.
|
||||
:param target_group: The group to assign to the created ER target. If not specified, the group from
|
||||
the original entrance will be copied.
|
||||
:param one_way_target_name: The name of the created ER target if `entrance` is one-way. This argument
|
||||
is required for one-way entrances and is ignored otherwise.
|
||||
"""
|
||||
child_region = entrance.connected_region
|
||||
parent_region = entrance.parent_region
|
||||
@@ -287,8 +295,11 @@ def disconnect_entrance_for_randomization(entrance: Entrance, target_group: int
|
||||
# targets in the child region will be created when the other direction edge is disconnected
|
||||
target = parent_region.create_er_target(entrance.name)
|
||||
else:
|
||||
# for 1-ways, the child region needs a target and coupling/naming is not a concern
|
||||
target = child_region.create_er_target(child_region.name)
|
||||
# for 1-ways, the child region needs a target. naming is not a concern for coupling so we
|
||||
# allow it to be user provided (and require it, to prevent an unhelpful assumed name in pairings)
|
||||
if not one_way_target_name:
|
||||
raise ValueError("Cannot disconnect a one-way entrance without a target name specified")
|
||||
target = child_region.create_er_target(one_way_target_name)
|
||||
target.randomization_type = entrance.randomization_type
|
||||
target.randomization_group = target_group or entrance.randomization_group
|
||||
|
||||
@@ -325,7 +336,6 @@ def randomize_entrances(
|
||||
|
||||
start_time = time.perf_counter()
|
||||
er_state = ERPlacementState(world, coupled)
|
||||
entrance_lookup = EntranceLookup(world.random, coupled)
|
||||
# similar to fill, skip validity checks on entrances if the game is beatable on minimal accessibility
|
||||
perform_validity_check = True
|
||||
|
||||
@@ -341,6 +351,7 @@ def randomize_entrances(
|
||||
|
||||
# used when membership checks are needed on the exit list, e.g. speculative sweep
|
||||
exits_set = set(exits)
|
||||
entrance_lookup = EntranceLookup(world.random, coupled, exits_set)
|
||||
for entrance in er_targets:
|
||||
entrance_lookup.add(entrance)
|
||||
|
||||
|
||||
475
kvui.py
475
kvui.py
@@ -35,8 +35,7 @@ from kivy.config import Config
|
||||
Config.set("input", "mouse", "mouse,disable_multitouch")
|
||||
Config.set("kivy", "exit_on_escape", "0")
|
||||
Config.set("graphics", "multisamples", "0") # multisamples crash old intel drivers
|
||||
|
||||
from kivy.app import App
|
||||
from kivymd.uix.divider import MDDivider
|
||||
from kivy.core.window import Window
|
||||
from kivy.core.clipboard import Clipboard
|
||||
from kivy.core.text.markup import MarkupLabel
|
||||
@@ -46,30 +45,32 @@ from kivy.clock import Clock
|
||||
from kivy.factory import Factory
|
||||
from kivy.properties import BooleanProperty, ObjectProperty, NumericProperty
|
||||
from kivy.metrics import dp
|
||||
from kivy.effects.scroll import ScrollEffect
|
||||
from kivy.uix.widget import Widget
|
||||
from kivy.uix.button import Button
|
||||
from kivy.uix.gridlayout import GridLayout
|
||||
from kivy.uix.layout import Layout
|
||||
from kivy.uix.textinput import TextInput
|
||||
from kivy.uix.scrollview import ScrollView
|
||||
from kivy.uix.recycleview import RecycleView
|
||||
from kivy.uix.tabbedpanel import TabbedPanel, TabbedPanelItem
|
||||
from kivy.uix.boxlayout import BoxLayout
|
||||
from kivy.uix.floatlayout import FloatLayout
|
||||
from kivy.uix.label import Label
|
||||
from kivy.uix.progressbar import ProgressBar
|
||||
from kivy.uix.dropdown import DropDown
|
||||
from kivy.utils import escape_markup
|
||||
from kivy.lang import Builder
|
||||
from kivy.uix.recycleview.views import RecycleDataViewBehavior
|
||||
from kivy.uix.behaviors import FocusBehavior
|
||||
from kivy.uix.behaviors import FocusBehavior, ToggleButtonBehavior
|
||||
from kivy.uix.recycleboxlayout import RecycleBoxLayout
|
||||
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
|
||||
from kivy.animation import Animation
|
||||
from kivy.uix.popup import Popup
|
||||
from kivy.uix.dropdown import DropDown
|
||||
from kivy.uix.image import AsyncImage
|
||||
from kivymd.app import MDApp
|
||||
from kivymd.uix.gridlayout import MDGridLayout
|
||||
from kivymd.uix.floatlayout import MDFloatLayout
|
||||
from kivymd.uix.boxlayout import MDBoxLayout
|
||||
from kivymd.uix.tab.tab import MDTabsPrimary, MDTabsItem, MDTabsItemText, MDTabsCarousel
|
||||
from kivymd.uix.menu import MDDropdownMenu
|
||||
from kivymd.uix.menu.menu import MDDropdownTextItem
|
||||
from kivymd.uix.dropdownitem import MDDropDownItem, MDDropDownItemText
|
||||
from kivymd.uix.button import MDButton, MDButtonText, MDButtonIcon, MDIconButton
|
||||
from kivymd.uix.label import MDLabel, MDIcon
|
||||
from kivymd.uix.recycleview import MDRecycleView
|
||||
from kivymd.uix.textfield.textfield import MDTextField
|
||||
from kivymd.uix.progressindicator import MDLinearProgressIndicator
|
||||
from kivymd.uix.scrollview import MDScrollView
|
||||
from kivymd.uix.tooltip import MDTooltip, MDTooltipPlain
|
||||
|
||||
fade_in_animation = Animation(opacity=0, duration=0) + Animation(opacity=1, duration=0.25)
|
||||
|
||||
@@ -86,6 +87,85 @@ else:
|
||||
remove_between_brackets = re.compile(r"\[.*?]")
|
||||
|
||||
|
||||
class ThemedApp(MDApp):
|
||||
def set_colors(self):
|
||||
text_colors = KivyJSONtoTextParser.TextColors()
|
||||
self.theme_cls.theme_style = getattr(text_colors, "theme_style", "Dark")
|
||||
self.theme_cls.primary_palette = getattr(text_colors, "primary_palette", "Green")
|
||||
self.theme_cls.dynamic_scheme_name = getattr(text_colors, "dynamic_scheme_name", "TONAL_SPOT")
|
||||
self.theme_cls.dynamic_scheme_contrast = getattr(text_colors, "dynamic_scheme_contrast", 0.0)
|
||||
|
||||
|
||||
class ImageIcon(MDButtonIcon, AsyncImage):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(args, kwargs)
|
||||
self.image = ApAsyncImage(**kwargs)
|
||||
self.add_widget(self.image)
|
||||
|
||||
def add_widget(self, widget, index=0, canvas=None):
|
||||
return super(MDIcon, self).add_widget(widget)
|
||||
|
||||
|
||||
class ImageButton(MDIconButton):
|
||||
def __init__(self, **kwargs):
|
||||
image_args = dict()
|
||||
for kwarg in ("fit_mode", "image_size", "color", "source", "texture"):
|
||||
val = kwargs.pop(kwarg, "None")
|
||||
if val != "None":
|
||||
image_args[kwarg.replace("image_", "")] = val
|
||||
super().__init__()
|
||||
self.image = ApAsyncImage(**image_args)
|
||||
|
||||
def set_center(button, center):
|
||||
self.image.center_x = self.center_x
|
||||
self.image.center_y = self.center_y
|
||||
|
||||
self.bind(center=set_center)
|
||||
self.add_widget(self.image)
|
||||
|
||||
def add_widget(self, widget, index=0, canvas=None):
|
||||
return super(MDIcon, self).add_widget(widget)
|
||||
|
||||
|
||||
class ScrollBox(MDScrollView):
|
||||
layout: MDBoxLayout = ObjectProperty(None)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
# thanks kivymd
|
||||
class ToggleButton(MDButton, ToggleButtonBehavior):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ToggleButton, self).__init__(*args, **kwargs)
|
||||
self.bind(state=self._update_bg)
|
||||
|
||||
def _update_bg(self, _, state: str):
|
||||
if self.disabled:
|
||||
return
|
||||
if self.theme_bg_color == "Primary":
|
||||
self.theme_bg_color = "Custom"
|
||||
|
||||
if state == "down":
|
||||
self.md_bg_color = self.theme_cls.primaryColor
|
||||
for child in self.children:
|
||||
if child.theme_text_color == "Primary":
|
||||
child.theme_text_color = "Custom"
|
||||
if child.theme_icon_color == "Primary":
|
||||
child.theme_icon_color = "Custom"
|
||||
child.text_color = self.theme_cls.onPrimaryColor
|
||||
child.icon_color = self.theme_cls.onPrimaryColor
|
||||
else:
|
||||
self.md_bg_color = self.theme_cls.surfaceContainerLowestColor
|
||||
for child in self.children:
|
||||
if child.theme_text_color == "Primary":
|
||||
child.theme_text_color = "Custom"
|
||||
if child.theme_icon_color == "Primary":
|
||||
child.theme_icon_color = "Custom"
|
||||
child.text_color = self.theme_cls.primaryColor
|
||||
child.icon_color = self.theme_cls.primaryColor
|
||||
|
||||
|
||||
# I was surprised to find this didn't already exist in kivy :(
|
||||
class HoverBehavior(object):
|
||||
"""originally from https://stackoverflow.com/a/605348110"""
|
||||
@@ -125,7 +205,7 @@ class HoverBehavior(object):
|
||||
Factory.register("HoverBehavior", HoverBehavior)
|
||||
|
||||
|
||||
class ToolTip(Label):
|
||||
class ToolTip(MDTooltipPlain):
|
||||
pass
|
||||
|
||||
|
||||
@@ -133,49 +213,30 @@ class ServerToolTip(ToolTip):
|
||||
pass
|
||||
|
||||
|
||||
class ScrollBox(ScrollView):
|
||||
layout: BoxLayout
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.layout = BoxLayout(size_hint_y=None)
|
||||
self.layout.bind(minimum_height=self.layout.setter("height"))
|
||||
self.add_widget(self.layout)
|
||||
self.effect_cls = ScrollEffect
|
||||
self.bar_width = dp(12)
|
||||
self.scroll_type = ["content", "bars"]
|
||||
|
||||
|
||||
class HovererableLabel(HoverBehavior, Label):
|
||||
class HovererableLabel(HoverBehavior, MDLabel):
|
||||
pass
|
||||
|
||||
|
||||
class TooltipLabel(HovererableLabel):
|
||||
tooltip = None
|
||||
class TooltipLabel(HovererableLabel, MDTooltip):
|
||||
tooltip_display_delay = 0.1
|
||||
|
||||
def create_tooltip(self, text, x, y):
|
||||
text = text.replace("<br>", "\n").replace("&", "&").replace("&bl;", "[").replace("&br;", "]")
|
||||
if self.tooltip:
|
||||
# update
|
||||
self.tooltip.children[0].text = text
|
||||
else:
|
||||
self.tooltip = FloatLayout()
|
||||
tooltip_label = ToolTip(text=text)
|
||||
self.tooltip.add_widget(tooltip_label)
|
||||
fade_in_animation.start(self.tooltip)
|
||||
App.get_running_app().root.add_widget(self.tooltip)
|
||||
|
||||
# handle left-side boundary to not render off-screen
|
||||
x = max(x, 3 + self.tooltip.children[0].texture_size[0] / 2)
|
||||
|
||||
# position float layout
|
||||
self.tooltip.x = x - self.tooltip.width / 2
|
||||
self.tooltip.y = y - self.tooltip.height / 2 + 48
|
||||
center_x, center_y = self.to_window(self.center_x, self.center_y)
|
||||
self.shift_y = y - center_y
|
||||
shift_x = center_x - x
|
||||
if shift_x > 0:
|
||||
self.shift_left = shift_x
|
||||
else:
|
||||
self.shift_right = shift_x
|
||||
|
||||
def remove_tooltip(self):
|
||||
if self.tooltip:
|
||||
App.get_running_app().root.remove_widget(self.tooltip)
|
||||
self.tooltip = None
|
||||
if self._tooltip:
|
||||
# update
|
||||
self._tooltip.text = text
|
||||
else:
|
||||
self._tooltip = ToolTip(text=text, pos_hint={})
|
||||
self.display_tooltip()
|
||||
|
||||
def on_mouse_pos(self, window, pos):
|
||||
if not self.get_root_window():
|
||||
@@ -202,26 +263,26 @@ class TooltipLabel(HovererableLabel):
|
||||
|
||||
def on_leave(self):
|
||||
self.remove_tooltip()
|
||||
self._tooltip = None
|
||||
|
||||
|
||||
class ServerLabel(HovererableLabel):
|
||||
class ServerLabel(HovererableLabel, MDTooltip):
|
||||
tooltip_display_delay = 0.1
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(HovererableLabel, self).__init__(*args, **kwargs)
|
||||
self.layout = FloatLayout()
|
||||
self.popuplabel = ServerToolTip(text="Test")
|
||||
self.layout.add_widget(self.popuplabel)
|
||||
self._tooltip = ServerToolTip(text="Test")
|
||||
|
||||
def on_enter(self):
|
||||
self.popuplabel.text = self.get_text()
|
||||
App.get_running_app().root.add_widget(self.layout)
|
||||
fade_in_animation.start(self.layout)
|
||||
self._tooltip.text = self.get_text()
|
||||
self.display_tooltip()
|
||||
|
||||
def on_leave(self):
|
||||
App.get_running_app().root.remove_widget(self.layout)
|
||||
self.animation_tooltip_dismiss()
|
||||
|
||||
@property
|
||||
def ctx(self) -> context_type:
|
||||
return App.get_running_app().ctx
|
||||
return MDApp.get_running_app().ctx
|
||||
|
||||
def get_text(self):
|
||||
if self.ctx.server:
|
||||
@@ -262,11 +323,11 @@ class ServerLabel(HovererableLabel):
|
||||
return "No current server connection. \nPlease connect to an Archipelago server."
|
||||
|
||||
|
||||
class MainLayout(GridLayout):
|
||||
class MainLayout(MDGridLayout):
|
||||
pass
|
||||
|
||||
|
||||
class ContainerLayout(FloatLayout):
|
||||
class ContainerLayout(MDFloatLayout):
|
||||
pass
|
||||
|
||||
|
||||
@@ -286,6 +347,11 @@ class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
|
||||
return super(SelectableLabel, self).refresh_view_attrs(
|
||||
rv, index, data)
|
||||
|
||||
def on_size(self, instance_label, size: list) -> None:
|
||||
super().on_size(instance_label, size)
|
||||
if self.parent:
|
||||
self.width = self.parent.width
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
""" Add selection on touch down """
|
||||
if super(SelectableLabel, self).on_touch_down(touch):
|
||||
@@ -296,10 +362,10 @@ class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
|
||||
else:
|
||||
# Not a fan of the following few lines, but they work.
|
||||
temp = MarkupLabel(text=self.text).markup
|
||||
text = "".join(part for part in temp if not part.startswith(("[color", "[/color]", "[ref=", "[/ref]")))
|
||||
cmdinput = App.get_running_app().textinput
|
||||
text = "".join(part for part in temp if not part.startswith("["))
|
||||
cmdinput = MDApp.get_running_app().textinput
|
||||
if not cmdinput.text:
|
||||
input_text = get_input_text_from_response(text, App.get_running_app().last_autofillable_command)
|
||||
input_text = get_input_text_from_response(text, MDApp.get_running_app().last_autofillable_command)
|
||||
if input_text is not None:
|
||||
cmdinput.text = input_text
|
||||
|
||||
@@ -310,30 +376,115 @@ class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
|
||||
""" Respond to the selection of items in the view. """
|
||||
self.selected = is_selected
|
||||
|
||||
|
||||
class AutocompleteHintInput(TextInput):
|
||||
|
||||
class MarkupDropdownTextItem(MDDropdownTextItem):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
for child in self.children:
|
||||
if child.__class__ == MDLabel:
|
||||
child.markup = True
|
||||
print(self.text)
|
||||
# Currently, this only lets us do markup on text that does not have any icons
|
||||
# Create new TextItems as needed
|
||||
|
||||
|
||||
class MarkupDropdown(MDDropdownMenu):
|
||||
def on_items(self, instance, value: list) -> None:
|
||||
"""
|
||||
The method sets the class that will be used to create the menu item.
|
||||
"""
|
||||
|
||||
items = []
|
||||
viewclass = "MarkupDropdownTextItem"
|
||||
|
||||
for data in value:
|
||||
if "viewclass" not in data:
|
||||
if (
|
||||
"leading_icon" not in data
|
||||
and "trailing_icon" not in data
|
||||
and "trailing_text" not in data
|
||||
):
|
||||
viewclass = "MarkupDropdownTextItem"
|
||||
elif (
|
||||
"leading_icon" in data
|
||||
and "trailing_icon" not in data
|
||||
and "trailing_text" not in data
|
||||
):
|
||||
viewclass = "MDDropdownLeadingIconItem"
|
||||
elif (
|
||||
"leading_icon" not in data
|
||||
and "trailing_icon" in data
|
||||
and "trailing_text" not in data
|
||||
):
|
||||
viewclass = "MDDropdownTrailingIconItem"
|
||||
elif (
|
||||
"leading_icon" not in data
|
||||
and "trailing_icon" in data
|
||||
and "trailing_text" in data
|
||||
):
|
||||
viewclass = "MDDropdownTrailingIconTextItem"
|
||||
elif (
|
||||
"leading_icon" in data
|
||||
and "trailing_icon" in data
|
||||
and "trailing_text" in data
|
||||
):
|
||||
viewclass = "MDDropdownLeadingTrailingIconTextItem"
|
||||
elif (
|
||||
"leading_icon" in data
|
||||
and "trailing_icon" in data
|
||||
and "trailing_text" not in data
|
||||
):
|
||||
viewclass = "MDDropdownLeadingTrailingIconItem"
|
||||
elif (
|
||||
"leading_icon" not in data
|
||||
and "trailing_icon" not in data
|
||||
and "trailing_text" in data
|
||||
):
|
||||
viewclass = "MDDropdownTrailingTextItem"
|
||||
elif (
|
||||
"leading_icon" in data
|
||||
and "trailing_icon" not in data
|
||||
and "trailing_text" in data
|
||||
):
|
||||
viewclass = "MDDropdownLeadingIconTrailingTextItem"
|
||||
|
||||
data["viewclass"] = viewclass
|
||||
|
||||
if "height" not in data:
|
||||
data["height"] = dp(48)
|
||||
|
||||
items.append(data)
|
||||
|
||||
self._items = items
|
||||
# Update items in view
|
||||
if hasattr(self, "menu"):
|
||||
self.menu.data = self._items
|
||||
|
||||
|
||||
class AutocompleteHintInput(MDTextField):
|
||||
min_chars = NumericProperty(3)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.dropdown = DropDown()
|
||||
self.dropdown = MarkupDropdown(caller=self, position="bottom", border_margin=dp(24), width=self.width)
|
||||
self.dropdown.bind(on_select=lambda instance, x: setattr(self, 'text', x))
|
||||
self.bind(on_text_validate=self.on_message)
|
||||
self.bind(width=lambda instance, x: setattr(self.dropdown, "width", x))
|
||||
|
||||
def on_message(self, instance):
|
||||
App.get_running_app().commandprocessor("!hint "+instance.text)
|
||||
MDApp.get_running_app().commandprocessor("!hint "+instance.text)
|
||||
|
||||
def on_text(self, instance, value):
|
||||
if len(value) >= self.min_chars:
|
||||
self.dropdown.clear_widgets()
|
||||
ctx: context_type = App.get_running_app().ctx
|
||||
self.dropdown.items.clear()
|
||||
ctx: context_type = MDApp.get_running_app().ctx
|
||||
if not ctx.game:
|
||||
return
|
||||
item_names = ctx.item_names._game_store[ctx.game].values()
|
||||
|
||||
def on_press(button: Button):
|
||||
split_text = MarkupLabel(text=button.text).markup
|
||||
def on_press(text):
|
||||
split_text = MarkupLabel(text=text).markup
|
||||
return self.dropdown.select("".join(text_frag for text_frag in split_text
|
||||
if not text_frag.startswith("[")))
|
||||
lowered = value.lower()
|
||||
@@ -345,20 +496,29 @@ class AutocompleteHintInput(TextInput):
|
||||
else:
|
||||
text = escape_markup(item_name)
|
||||
text = text[:index] + "[b]" + text[index:index+len(value)]+"[/b]"+text[index+len(value):]
|
||||
btn = Button(text=text, size_hint_y=None, height=dp(30), markup=True)
|
||||
btn.bind(on_release=on_press)
|
||||
self.dropdown.add_widget(btn)
|
||||
if not self.dropdown.attach_to:
|
||||
self.dropdown.open(self)
|
||||
self.dropdown.items.append({
|
||||
"text": text,
|
||||
"on_release": lambda: on_press(text),
|
||||
"markup": True
|
||||
})
|
||||
if not self.dropdown.parent:
|
||||
self.dropdown.open()
|
||||
else:
|
||||
self.dropdown.dismiss()
|
||||
|
||||
|
||||
class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
||||
status_icons = {
|
||||
HintStatus.HINT_NO_PRIORITY: "information",
|
||||
HintStatus.HINT_PRIORITY: "exclamation-thick",
|
||||
HintStatus.HINT_AVOID: "alert"
|
||||
}
|
||||
|
||||
|
||||
class HintLabel(RecycleDataViewBehavior, MDBoxLayout):
|
||||
selected = BooleanProperty(False)
|
||||
striped = BooleanProperty(False)
|
||||
index = None
|
||||
dropdown: DropDown
|
||||
dropdown: MDDropdownMenu
|
||||
|
||||
def __init__(self):
|
||||
super(HintLabel, self).__init__()
|
||||
@@ -369,29 +529,28 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
||||
self.entrance_text = ""
|
||||
self.status_text = ""
|
||||
self.hint = {}
|
||||
for child in self.children:
|
||||
child.bind(texture_size=self.set_height)
|
||||
|
||||
ctx = MDApp.get_running_app().ctx
|
||||
menu_items = []
|
||||
|
||||
ctx = App.get_running_app().ctx
|
||||
self.dropdown = DropDown()
|
||||
for status in (HintStatus.HINT_NO_PRIORITY, HintStatus.HINT_PRIORITY, HintStatus.HINT_AVOID):
|
||||
name = status_names[status]
|
||||
status_button = MDDropDownItem(MDDropDownItemText(text=name), size_hint_y=None, height=dp(50))
|
||||
status_button.status = status
|
||||
menu_items.append({
|
||||
"text": name,
|
||||
"leading_icon": status_icons[status],
|
||||
"on_release": lambda x=status: select(self, x)
|
||||
})
|
||||
|
||||
def set_value(button):
|
||||
self.dropdown.select(button.status)
|
||||
self.dropdown = MDDropdownMenu(caller=self.ids["status"], items=menu_items)
|
||||
|
||||
def select(instance, data):
|
||||
ctx.update_hint(self.hint["location"],
|
||||
self.hint["finding_player"],
|
||||
data)
|
||||
|
||||
for status in (HintStatus.HINT_NO_PRIORITY, HintStatus.HINT_PRIORITY, HintStatus.HINT_AVOID):
|
||||
name = status_names[status]
|
||||
status_button = Button(text=name, size_hint_y=None, height=dp(50))
|
||||
status_button.status = status
|
||||
status_button.bind(on_release=set_value)
|
||||
self.dropdown.add_widget(status_button)
|
||||
|
||||
self.dropdown.bind(on_select=select)
|
||||
self.dropdown.bind(on_release=self.dropdown.dismiss)
|
||||
|
||||
def set_height(self, instance, value):
|
||||
self.height = max([child.texture_size[1] for child in self.children])
|
||||
@@ -406,7 +565,6 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
||||
self.entrance_text = data["entrance"]["text"]
|
||||
self.status_text = data["status"]["text"]
|
||||
self.hint = data["status"]["hint"]
|
||||
self.height = self.minimum_height
|
||||
return super(HintLabel, self).refresh_view_attrs(rv, index, data)
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
@@ -419,10 +577,10 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
||||
if status_label.collide_point(*touch.pos):
|
||||
if self.hint["status"] == HintStatus.HINT_FOUND:
|
||||
return
|
||||
ctx = App.get_running_app().ctx
|
||||
ctx = MDApp.get_running_app().ctx
|
||||
if ctx.slot_concerns_self(self.hint["receiving_player"]): # If this player owns this hint
|
||||
# open a dropdown
|
||||
self.dropdown.open(self.ids["status"])
|
||||
self.dropdown.open()
|
||||
elif self.selected:
|
||||
self.parent.clear_selection()
|
||||
else:
|
||||
@@ -431,8 +589,7 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
||||
if self.entrance_text != "Vanilla"
|
||||
else "", ". (", self.status_text.lower(), ")"))
|
||||
temp = MarkupLabel(text).markup
|
||||
text = "".join(
|
||||
part for part in temp if not part.startswith(("[color", "[/color]", "[ref=", "[/ref]")))
|
||||
text = "".join(part for part in temp if not part.startswith("["))
|
||||
Clipboard.copy(escape_markup(text).replace("&", "&").replace("&bl;", "[").replace("&br;", "]"))
|
||||
return self.parent.select_with_touch(self.index, touch)
|
||||
else:
|
||||
@@ -455,7 +612,7 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
||||
else:
|
||||
parent.sort_key = key
|
||||
parent.reversed = False
|
||||
App.get_running_app().update_hints()
|
||||
MDApp.get_running_app().update_hints()
|
||||
|
||||
def apply_selection(self, rv, index, is_selected):
|
||||
""" Respond to the selection of items in the view. """
|
||||
@@ -463,7 +620,7 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
||||
self.selected = is_selected
|
||||
|
||||
|
||||
class ConnectBarTextInput(TextInput):
|
||||
class ConnectBarTextInput(MDTextField):
|
||||
def insert_text(self, substring, from_undo=False):
|
||||
s = substring.replace("\n", "").replace("\r", "")
|
||||
return super(ConnectBarTextInput, self).insert_text(s, from_undo=from_undo)
|
||||
@@ -473,7 +630,7 @@ def is_command_input(string: str) -> bool:
|
||||
return len(string) > 0 and string[0] in "/!"
|
||||
|
||||
|
||||
class CommandPromptTextInput(TextInput):
|
||||
class CommandPromptTextInput(MDTextField):
|
||||
MAXIMUM_HISTORY_MESSAGES = 50
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
@@ -521,7 +678,7 @@ class CommandPromptTextInput(TextInput):
|
||||
|
||||
|
||||
class MessageBox(Popup):
|
||||
class MessageBoxLabel(Label):
|
||||
class MessageBoxLabel(MDLabel):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._label.refresh()
|
||||
@@ -539,14 +696,31 @@ class MessageBox(Popup):
|
||||
self.height += max(0, label.height - 18)
|
||||
|
||||
|
||||
class GameManager(App):
|
||||
class ClientTabs(MDTabsPrimary):
|
||||
carousel: MDTabsCarousel
|
||||
lock_swiping = True
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.carousel = MDTabsCarousel(lock_swiping=True)
|
||||
super().__init__(*args, MDDivider(size_hint_y=None, height=dp(4)), self.carousel, **kwargs)
|
||||
self.size_hint_y = 1
|
||||
|
||||
def remove_tab(self, tab, content=None):
|
||||
if content is None:
|
||||
content = tab.content
|
||||
self.ids.container.remove_widget(tab)
|
||||
self.carousel.remove_widget(content)
|
||||
self.on_size(self, self.size)
|
||||
|
||||
|
||||
class GameManager(ThemedApp):
|
||||
logging_pairs = [
|
||||
("Client", "Archipelago"),
|
||||
]
|
||||
base_title: str = "Archipelago Client"
|
||||
last_autofillable_command: str
|
||||
|
||||
main_area_container: GridLayout
|
||||
main_area_container: MDGridLayout
|
||||
""" subclasses can add more columns beside the tabs """
|
||||
|
||||
def __init__(self, ctx: context_type):
|
||||
@@ -581,18 +755,26 @@ class GameManager(App):
|
||||
return max(1, len(self.tabs.tab_list))
|
||||
return 1
|
||||
|
||||
def on_start(self):
|
||||
def on_start(*args):
|
||||
self.root.md_bg_color = self.theme_cls.backgroundColor
|
||||
super().on_start()
|
||||
Clock.schedule_once(on_start)
|
||||
|
||||
def build(self) -> Layout:
|
||||
self.set_colors()
|
||||
self.container = ContainerLayout()
|
||||
|
||||
self.grid = MainLayout()
|
||||
self.grid.cols = 1
|
||||
self.connect_layout = BoxLayout(orientation="horizontal", size_hint_y=None, height=dp(30))
|
||||
self.connect_layout = MDBoxLayout(orientation="horizontal", size_hint_y=None, height=dp(70),
|
||||
spacing=5, padding=(5, 10))
|
||||
# top part
|
||||
server_label = ServerLabel()
|
||||
server_label = ServerLabel(halign="center")
|
||||
self.connect_layout.add_widget(server_label)
|
||||
self.server_connect_bar = ConnectBarTextInput(text=self.ctx.suggested_address or "archipelago.gg:",
|
||||
size_hint_y=None,
|
||||
height=dp(30), multiline=False, write_tab=False)
|
||||
size_hint_y=None, role="medium",
|
||||
height=dp(70), multiline=False, write_tab=False)
|
||||
|
||||
def connect_bar_validate(sender):
|
||||
if not self.ctx.server:
|
||||
@@ -600,26 +782,31 @@ class GameManager(App):
|
||||
|
||||
self.server_connect_bar.bind(on_text_validate=connect_bar_validate)
|
||||
self.connect_layout.add_widget(self.server_connect_bar)
|
||||
self.server_connect_button = Button(text="Connect", size=(dp(100), dp(30)), size_hint_y=None, size_hint_x=None)
|
||||
self.server_connect_button = MDButton(MDButtonText(text="Connect"), style="filled", size=(dp(100), dp(70)),
|
||||
size_hint_x=None, size_hint_y=None, radius=5, pos_hint={"center_y": 0.55})
|
||||
self.server_connect_button.bind(on_press=self.connect_button_action)
|
||||
self.server_connect_button.height = self.server_connect_bar.height
|
||||
self.connect_layout.add_widget(self.server_connect_button)
|
||||
self.grid.add_widget(self.connect_layout)
|
||||
self.progressbar = ProgressBar(size_hint_y=None, height=3)
|
||||
self.progressbar = MDLinearProgressIndicator(size_hint_y=None, height=3)
|
||||
self.grid.add_widget(self.progressbar)
|
||||
|
||||
# middle part
|
||||
self.tabs = TabbedPanel(size_hint_y=1)
|
||||
self.tabs.default_tab_text = "All"
|
||||
self.tabs = ClientTabs()
|
||||
self.tabs.add_widget(MDTabsItem(MDTabsItemText(text="All" if len(self.logging_pairs) > 1 else "Archipelago")))
|
||||
self.log_panels["All"] = self.tabs.default_tab_content = UILog(*(logging.getLogger(logger_name)
|
||||
for logger_name, name in
|
||||
self.logging_pairs))
|
||||
for logger_name, name in
|
||||
self.logging_pairs))
|
||||
self.tabs.carousel.add_widget(self.tabs.default_tab_content)
|
||||
|
||||
for logger_name, display_name in self.logging_pairs:
|
||||
bridge_logger = logging.getLogger(logger_name)
|
||||
panel = TabbedPanelItem(text=display_name)
|
||||
self.log_panels[display_name] = panel.content = UILog(bridge_logger)
|
||||
self.log_panels[display_name] = UILog(bridge_logger)
|
||||
if len(self.logging_pairs) > 1:
|
||||
panel = MDTabsItem(MDTabsItemText(text=display_name))
|
||||
panel.content = self.log_panels[display_name]
|
||||
# show Archipelago tab if other logging is present
|
||||
self.tabs.carousel.add_widget(panel.content)
|
||||
self.tabs.add_widget(panel)
|
||||
|
||||
hint_panel = self.add_client_tab("Hints", HintLayout())
|
||||
@@ -627,21 +814,20 @@ class GameManager(App):
|
||||
self.log_panels["Hints"] = hint_panel.content
|
||||
hint_panel.content.add_widget(self.hint_log)
|
||||
|
||||
if len(self.logging_pairs) == 1:
|
||||
self.tabs.default_tab_text = "Archipelago"
|
||||
|
||||
self.main_area_container = GridLayout(size_hint_y=1, rows=1)
|
||||
self.main_area_container = MDGridLayout(size_hint_y=1, rows=1)
|
||||
self.main_area_container.add_widget(self.tabs)
|
||||
|
||||
self.grid.add_widget(self.main_area_container)
|
||||
|
||||
# bottom part
|
||||
bottom_layout = BoxLayout(orientation="horizontal", size_hint_y=None, height=dp(30))
|
||||
info_button = Button(size=(dp(100), dp(30)), text="Command:", size_hint_x=None)
|
||||
bottom_layout = MDBoxLayout(orientation="horizontal", size_hint_y=None, height=dp(70), spacing=5, padding=(5, 10))
|
||||
info_button = MDButton(MDButtonText(text="Command:"), radius=5, style="filled", size=(dp(100), dp(70)),
|
||||
size_hint_x=None, size_hint_y=None, pos_hint={"center_y": 0.575})
|
||||
info_button.bind(on_release=self.command_button_action)
|
||||
bottom_layout.add_widget(info_button)
|
||||
self.textinput = CommandPromptTextInput(size_hint_y=None, height=dp(30), multiline=False, write_tab=False)
|
||||
self.textinput.bind(on_text_validate=self.on_message)
|
||||
info_button.height = self.textinput.height
|
||||
self.textinput.text_validate_unfocus = False
|
||||
bottom_layout.add_widget(self.textinput)
|
||||
self.grid.add_widget(bottom_layout)
|
||||
@@ -662,24 +848,26 @@ class GameManager(App):
|
||||
def add_client_tab(self, title: str, content: Widget) -> Widget:
|
||||
"""Adds a new tab to the client window with a given title, and provides a given Widget as its content.
|
||||
Returns the new tab widget, with the provided content being placed on the tab as content."""
|
||||
new_tab = TabbedPanelItem(text=title)
|
||||
new_tab = MDTabsItem(MDTabsItemText(text=title))
|
||||
new_tab.content = content
|
||||
self.tabs.add_widget(new_tab)
|
||||
self.tabs.carousel.add_widget(new_tab.content)
|
||||
return new_tab
|
||||
|
||||
def update_texts(self, dt):
|
||||
if hasattr(self.tabs.content.children[0], "fix_heights"):
|
||||
self.tabs.content.children[0].fix_heights() # TODO: remove this when Kivy fixes this upstream
|
||||
for slide in self.tabs.carousel.slides:
|
||||
if hasattr(slide, "fix_heights"):
|
||||
slide.fix_heights() # TODO: remove this when Kivy fixes this upstream
|
||||
if self.ctx.server:
|
||||
self.title = self.base_title + " " + Utils.__version__ + \
|
||||
f" | Connected to: {self.ctx.server_address} " \
|
||||
f"{'.'.join(str(e) for e in self.ctx.server_version)}"
|
||||
self.server_connect_button.text = "Disconnect"
|
||||
self.server_connect_button._button_text.text = "Disconnect"
|
||||
self.server_connect_bar.readonly = True
|
||||
self.progressbar.max = len(self.ctx.checked_locations) + len(self.ctx.missing_locations)
|
||||
self.progressbar.value = len(self.ctx.checked_locations)
|
||||
else:
|
||||
self.server_connect_button.text = "Connect"
|
||||
self.server_connect_button._button_text.text = "Connect"
|
||||
self.server_connect_bar.readonly = False
|
||||
self.title = self.base_title + " " + Utils.__version__
|
||||
self.progressbar.value = 0
|
||||
@@ -742,8 +930,8 @@ class GameManager(App):
|
||||
|
||||
def enable_energy_link(self):
|
||||
if not hasattr(self, "energy_link_label"):
|
||||
self.energy_link_label = Label(text="Energy Link: Standby",
|
||||
size_hint_x=None, width=150)
|
||||
self.energy_link_label = MDLabel(text="Energy Link: Standby",
|
||||
size_hint_x=None, width=150, halign="center")
|
||||
self.connect_layout.add_widget(self.energy_link_label)
|
||||
|
||||
def set_new_energy_link_value(self):
|
||||
@@ -779,8 +967,9 @@ class LogtoUI(logging.Handler):
|
||||
self.on_log(self.format(record))
|
||||
|
||||
|
||||
class UILog(RecycleView):
|
||||
class UILog(MDRecycleView):
|
||||
messages: typing.ClassVar[int] # comes from kv file
|
||||
adaptive_height = True
|
||||
|
||||
def __init__(self, *loggers_to_handle, **kwargs):
|
||||
super(UILog, self).__init__(**kwargs)
|
||||
@@ -807,16 +996,22 @@ class UILog(RecycleView):
|
||||
element.height = element.texture_size[1]
|
||||
|
||||
|
||||
class HintLayout(BoxLayout):
|
||||
class HintLayout(MDBoxLayout):
|
||||
orientation = "vertical"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
boxlayout = BoxLayout(orientation="horizontal", size_hint_y=None, height=dp(30))
|
||||
boxlayout.add_widget(Label(text="New Hint:", size_hint_x=None, size_hint_y=None, height=dp(30)))
|
||||
boxlayout = MDBoxLayout(orientation="horizontal", size_hint_y=None, height=dp(55))
|
||||
boxlayout.add_widget(MDLabel(text="New Hint:", size_hint_x=None, size_hint_y=None, height=dp(55)))
|
||||
boxlayout.add_widget(AutocompleteHintInput())
|
||||
self.add_widget(boxlayout)
|
||||
|
||||
def fix_heights(self):
|
||||
for child in self.children:
|
||||
fix_func = getattr(child, "fix_heights", None)
|
||||
if fix_func:
|
||||
fix_func()
|
||||
|
||||
|
||||
status_names: typing.Dict[HintStatus, str] = {
|
||||
HintStatus.HINT_FOUND: "Found",
|
||||
@@ -840,8 +1035,7 @@ status_sort_weights: dict[HintStatus, int] = {
|
||||
HintStatus.HINT_PRIORITY: 4,
|
||||
}
|
||||
|
||||
|
||||
class HintLog(RecycleView):
|
||||
class HintLog(MDRecycleView):
|
||||
header = {
|
||||
"receiving": {"text": "[u]Receiving Player[/u]"},
|
||||
"item": {"text": "[u]Item[/u]"},
|
||||
@@ -852,7 +1046,7 @@ class HintLog(RecycleView):
|
||||
"hint": {"receiving_player": -1, "location": -1, "finding_player": -1, "status": ""}},
|
||||
"striped": True,
|
||||
}
|
||||
|
||||
data: list[typing.Any]
|
||||
sort_key: str = ""
|
||||
reversed: bool = True
|
||||
|
||||
@@ -865,7 +1059,7 @@ class HintLog(RecycleView):
|
||||
if not hints: # Fix the scrolling looking visually wrong in some edge cases
|
||||
self.scroll_y = 1.0
|
||||
data = []
|
||||
ctx = App.get_running_app().ctx
|
||||
ctx = MDApp.get_running_app().ctx
|
||||
for hint in hints:
|
||||
if not hint.get("status"): # Allows connecting to old servers
|
||||
hint["status"] = HintStatus.HINT_FOUND if hint["found"] else HintStatus.HINT_UNSPECIFIED
|
||||
@@ -929,7 +1123,8 @@ class ImageLoaderPkgutil(ImageLoaderBase):
|
||||
data = pkgutil.get_data(module, path)
|
||||
return self._bytes_to_data(data)
|
||||
|
||||
def _bytes_to_data(self, data: typing.Union[bytes, bytearray]) -> typing.List[ImageData]:
|
||||
@staticmethod
|
||||
def _bytes_to_data(data: typing.Union[bytes, bytearray]) -> typing.List[ImageData]:
|
||||
loader = next(loader for loader in ImageLoader.loaders if loader.can_load_memory())
|
||||
return loader.load(loader, io.BytesIO(data))
|
||||
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
colorama>=0.4.6
|
||||
websockets>=13.0.1,<14
|
||||
PyYAML>=6.0.2
|
||||
jellyfish>=1.1.0
|
||||
jinja2>=3.1.4
|
||||
jellyfish>=1.1.3
|
||||
jinja2>=3.1.6
|
||||
schema>=0.7.7
|
||||
kivy>=2.3.0
|
||||
bsdiff4>=1.2.4
|
||||
platformdirs>=4.2.2
|
||||
certifi>=2024.12.14
|
||||
cython>=3.0.11
|
||||
cymem>=2.0.8
|
||||
orjson>=3.10.7
|
||||
kivy>=2.3.1
|
||||
bsdiff4>=1.2.6
|
||||
platformdirs>=4.3.6
|
||||
certifi>=2025.1.31
|
||||
cython>=3.0.12
|
||||
cymem>=2.0.11
|
||||
orjson>=3.10.15
|
||||
typing_extensions>=4.12.2
|
||||
pyshortcuts>=1.9.1
|
||||
kivymd @ git+https://github.com/kivymd/KivyMD@5ff9d0d
|
||||
kivymd>=2.0.1.dev0
|
||||
|
||||
9
setup.py
9
setup.py
@@ -19,7 +19,7 @@ from typing import Dict, Iterable, List, Optional, Sequence, Set, Tuple, Union
|
||||
|
||||
|
||||
# This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it
|
||||
requirement = 'cx-Freeze==7.2.0'
|
||||
requirement = 'cx-Freeze==8.0.0'
|
||||
try:
|
||||
import pkg_resources
|
||||
try:
|
||||
@@ -629,12 +629,13 @@ cx_Freeze.setup(
|
||||
ext_modules=cythonize("_speedups.pyx"),
|
||||
options={
|
||||
"build_exe": {
|
||||
"packages": ["worlds", "kivy", "cymem", "websockets"],
|
||||
"packages": ["worlds", "kivy", "cymem", "websockets", "kivymd"],
|
||||
"includes": [],
|
||||
"excludes": ["numpy", "Cython", "PySide2", "PIL",
|
||||
"pandas", "zstandard"],
|
||||
"pandas"],
|
||||
"zip_includes": [],
|
||||
"zip_include_packages": ["*"],
|
||||
"zip_exclude_packages": ["worlds", "sc2"],
|
||||
"zip_exclude_packages": ["worlds", "sc2", "kivymd"],
|
||||
"include_files": [], # broken in cx 6.14.0, we use more special sauce now
|
||||
"include_msvcr": False,
|
||||
"replace_paths": ["*."],
|
||||
|
||||
@@ -65,8 +65,10 @@ class TestEntranceLookup(unittest.TestCase):
|
||||
"""tests that get_targets shuffles targets between groups when requested"""
|
||||
multiworld = generate_test_multiworld()
|
||||
generate_disconnected_region_grid(multiworld, 5)
|
||||
exits_set = set([ex for region in multiworld.get_regions(1)
|
||||
for ex in region.exits if not ex.connected_region])
|
||||
|
||||
lookup = EntranceLookup(multiworld.worlds[1].random, coupled=True)
|
||||
lookup = EntranceLookup(multiworld.worlds[1].random, coupled=True, usable_exits=exits_set)
|
||||
er_targets = [entrance for region in multiworld.get_regions(1)
|
||||
for entrance in region.entrances if not entrance.parent_region]
|
||||
for entrance in er_targets:
|
||||
@@ -86,8 +88,10 @@ class TestEntranceLookup(unittest.TestCase):
|
||||
"""tests that get_targets does not shuffle targets between groups when requested"""
|
||||
multiworld = generate_test_multiworld()
|
||||
generate_disconnected_region_grid(multiworld, 5)
|
||||
exits_set = set([ex for region in multiworld.get_regions(1)
|
||||
for ex in region.exits if not ex.connected_region])
|
||||
|
||||
lookup = EntranceLookup(multiworld.worlds[1].random, coupled=True)
|
||||
lookup = EntranceLookup(multiworld.worlds[1].random, coupled=True, usable_exits=exits_set)
|
||||
er_targets = [entrance for region in multiworld.get_regions(1)
|
||||
for entrance in region.entrances if not entrance.parent_region]
|
||||
for entrance in er_targets:
|
||||
@@ -99,6 +103,30 @@ class TestEntranceLookup(unittest.TestCase):
|
||||
group_order = [prev := group.randomization_group for group in retrieved_targets if prev != group.randomization_group]
|
||||
self.assertEqual([ERTestGroups.TOP, ERTestGroups.BOTTOM], group_order)
|
||||
|
||||
def test_selective_dead_ends(self):
|
||||
"""test that entrances that EntranceLookup has not been told to consider are ignored when finding dead-ends"""
|
||||
multiworld = generate_test_multiworld()
|
||||
generate_disconnected_region_grid(multiworld, 5)
|
||||
exits_set = set([ex for region in multiworld.get_regions(1)
|
||||
for ex in region.exits if not ex.connected_region
|
||||
and ex.name != "region20_right" and ex.name != "region21_left"])
|
||||
|
||||
lookup = EntranceLookup(multiworld.worlds[1].random, coupled=True, usable_exits=exits_set)
|
||||
er_targets = [entrance for region in multiworld.get_regions(1)
|
||||
for entrance in region.entrances if not entrance.parent_region and
|
||||
entrance.name != "region20_right" and entrance.name != "region21_left"]
|
||||
for entrance in er_targets:
|
||||
lookup.add(entrance)
|
||||
# region 20 is the bottom left corner of the grid, and therefore only has a right entrance from region 21
|
||||
# and a top entrance from region 15; since we've told lookup to ignore the right entrance from region 21,
|
||||
# the top entrance from region 15 should be considered a dead-end
|
||||
dead_end_region = multiworld.get_region("region20", 1)
|
||||
for dead_end in dead_end_region.entrances:
|
||||
if dead_end.name == "region20_top":
|
||||
break
|
||||
# there should be only this one dead-end
|
||||
self.assertTrue(dead_end in lookup.dead_ends)
|
||||
self.assertEqual(len(lookup.dead_ends), 1)
|
||||
|
||||
class TestBakeTargetGroupLookup(unittest.TestCase):
|
||||
def test_lookup_generation(self):
|
||||
@@ -148,7 +176,7 @@ class TestDisconnectForRandomization(unittest.TestCase):
|
||||
e.randomization_group = 1
|
||||
e.connect(r2)
|
||||
|
||||
disconnect_entrance_for_randomization(e)
|
||||
disconnect_entrance_for_randomization(e, one_way_target_name="foo")
|
||||
|
||||
self.assertIsNone(e.connected_region)
|
||||
self.assertEqual([], r1.entrances)
|
||||
@@ -158,10 +186,22 @@ class TestDisconnectForRandomization(unittest.TestCase):
|
||||
|
||||
self.assertEqual(1, len(r2.entrances))
|
||||
self.assertIsNone(r2.entrances[0].parent_region)
|
||||
self.assertEqual("r2", r2.entrances[0].name)
|
||||
self.assertEqual("foo", r2.entrances[0].name)
|
||||
self.assertEqual(EntranceType.ONE_WAY, r2.entrances[0].randomization_type)
|
||||
self.assertEqual(1, r2.entrances[0].randomization_group)
|
||||
|
||||
def test_disconnect_default_1way_no_vanilla_target_raises(self):
|
||||
multiworld = generate_test_multiworld()
|
||||
r1 = Region("r1", 1, multiworld)
|
||||
r2 = Region("r2", 1, multiworld)
|
||||
e = r1.create_exit("e")
|
||||
e.randomization_type = EntranceType.ONE_WAY
|
||||
e.randomization_group = 1
|
||||
e.connect(r2)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
disconnect_entrance_for_randomization(e)
|
||||
|
||||
def test_disconnect_uses_alternate_group(self):
|
||||
multiworld = generate_test_multiworld()
|
||||
r1 = Region("r1", 1, multiworld)
|
||||
@@ -171,7 +211,7 @@ class TestDisconnectForRandomization(unittest.TestCase):
|
||||
e.randomization_group = 1
|
||||
e.connect(r2)
|
||||
|
||||
disconnect_entrance_for_randomization(e, 2)
|
||||
disconnect_entrance_for_randomization(e, 2, "foo")
|
||||
|
||||
self.assertIsNone(e.connected_region)
|
||||
self.assertEqual([], r1.entrances)
|
||||
@@ -181,7 +221,7 @@ class TestDisconnectForRandomization(unittest.TestCase):
|
||||
|
||||
self.assertEqual(1, len(r2.entrances))
|
||||
self.assertIsNone(r2.entrances[0].parent_region)
|
||||
self.assertEqual("r2", r2.entrances[0].name)
|
||||
self.assertEqual("foo", r2.entrances[0].name)
|
||||
self.assertEqual(EntranceType.ONE_WAY, r2.entrances[0].randomization_type)
|
||||
self.assertEqual(2, r2.entrances[0].randomization_group)
|
||||
|
||||
|
||||
14
test/general/test_packages.py
Normal file
14
test/general/test_packages.py
Normal file
@@ -0,0 +1,14 @@
|
||||
import unittest
|
||||
import os
|
||||
|
||||
|
||||
class TestPackages(unittest.TestCase):
|
||||
def test_packages_have_init(self):
|
||||
"""Test that all world folders containing .py files also have a __init__.py file,
|
||||
to indicate full package rather than namespace package."""
|
||||
import Utils
|
||||
|
||||
worlds_path = Utils.local_path("worlds")
|
||||
for dirpath, dirnames, filenames in os.walk(worlds_path):
|
||||
with self.subTest(directory=dirpath):
|
||||
self.assertEqual("__init__.py" in filenames, any(file.endswith(".py") for file in filenames))
|
||||
11
test/general/test_patches.py
Normal file
11
test/general/test_patches.py
Normal file
@@ -0,0 +1,11 @@
|
||||
import unittest
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
from worlds.Files import AutoPatchRegister
|
||||
|
||||
|
||||
class TestPatches(unittest.TestCase):
|
||||
def test_patch_name_matches_game(self) -> None:
|
||||
for game_name in AutoPatchRegister.patch_types:
|
||||
with self.subTest(game=game_name):
|
||||
self.assertIn(game_name, AutoWorldRegister.world_types.keys(),
|
||||
f"Patch '{game_name}' does not match the name of any world.")
|
||||
19
test/general/test_requirements.py
Normal file
19
test/general/test_requirements.py
Normal file
@@ -0,0 +1,19 @@
|
||||
import unittest
|
||||
import os
|
||||
|
||||
|
||||
class TestBase(unittest.TestCase):
|
||||
def test_requirements_file_ends_on_newline(self):
|
||||
"""Test that all requirements files end on a newline"""
|
||||
import Utils
|
||||
requirements_files = [Utils.local_path("requirements.txt"),
|
||||
Utils.local_path("WebHostLib", "requirements.txt")]
|
||||
worlds_path = Utils.local_path("worlds")
|
||||
for entry in os.listdir(worlds_path):
|
||||
requirements_path = os.path.join(worlds_path, entry, "requirements.txt")
|
||||
if os.path.isfile(requirements_path):
|
||||
requirements_files.append(requirements_path)
|
||||
for requirements_file in requirements_files:
|
||||
with self.subTest(path=requirements_file):
|
||||
with open(requirements_file) as f:
|
||||
self.assertEqual(f.read()[-1], "\n")
|
||||
@@ -9,7 +9,8 @@ from worlds.LauncherComponents import Component, SuffixIdentifier, Type, compone
|
||||
if TYPE_CHECKING:
|
||||
from SNIClient import SNIContext
|
||||
|
||||
component = Component('SNI Client', 'SNIClient', component_type=Type.CLIENT, file_identifier=SuffixIdentifier(".apsoe"))
|
||||
component = Component('SNI Client', 'SNIClient', component_type=Type.CLIENT, file_identifier=SuffixIdentifier(".apsoe"),
|
||||
description="A client for connecting to SNES consoles via Super Nintendo Interface.")
|
||||
components.append(component)
|
||||
|
||||
|
||||
|
||||
@@ -110,6 +110,16 @@ class AutoLogicRegister(type):
|
||||
elif not item_name.startswith("__"):
|
||||
if hasattr(CollectionState, item_name):
|
||||
raise Exception(f"Name conflict on Logic Mixin {name} trying to overwrite {item_name}")
|
||||
|
||||
assert callable(function) or "init_mixin" in dct, (
|
||||
f"{name} defined class variable {item_name} without also having init_mixin.\n\n"
|
||||
"Explanation:\n"
|
||||
"Class variables that will be mutated need to be inintialized as instance variables in init_mixin.\n"
|
||||
"If your LogicMixin variables aren't actually mutable / you don't intend to mutate them, "
|
||||
"there is no point in using LogixMixin.\n"
|
||||
"LogicMixin exists to track custom state variables that change when items are collected/removed."
|
||||
)
|
||||
|
||||
setattr(CollectionState, item_name, function)
|
||||
return new_class
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ class Component:
|
||||
"""
|
||||
display_name: str
|
||||
"""Used as the GUI button label and the component name in the CLI args"""
|
||||
description: str
|
||||
"""Optional description displayed on the GUI underneath the display name"""
|
||||
type: Type
|
||||
"""
|
||||
Enum "Type" classification of component intent, for filtering in the Launcher GUI
|
||||
@@ -58,8 +60,9 @@ class Component:
|
||||
def __init__(self, display_name: str, script_name: Optional[str] = None, frozen_name: Optional[str] = None,
|
||||
cli: bool = False, icon: str = 'icon', component_type: Optional[Type] = None,
|
||||
func: Optional[Callable] = None, file_identifier: Optional[Callable[[str], bool]] = None,
|
||||
game_name: Optional[str] = None, supports_uri: Optional[bool] = False):
|
||||
game_name: Optional[str] = None, supports_uri: Optional[bool] = False, description: str = "") -> None:
|
||||
self.display_name = display_name
|
||||
self.description = description
|
||||
self.script_name = script_name
|
||||
self.frozen_name = frozen_name or f'Archipelago{script_name}' if script_name else None
|
||||
self.icon = icon
|
||||
@@ -88,7 +91,6 @@ processes = weakref.WeakSet()
|
||||
|
||||
|
||||
def launch_subprocess(func: Callable, name: str | None = None, args: Tuple[str, ...] = ()) -> None:
|
||||
global processes
|
||||
import multiprocessing
|
||||
process = multiprocessing.Process(target=func, name=name, args=args)
|
||||
process.start()
|
||||
|
||||
@@ -3,4 +3,4 @@ mpyq>=0.2.5
|
||||
portpicker>=1.5.2
|
||||
aiohttp>=3.8.4
|
||||
loguru>=0.7.0
|
||||
protobuf==3.20.3
|
||||
protobuf==3.20.3
|
||||
|
||||
@@ -238,14 +238,12 @@ class AdventureWorld(World):
|
||||
|
||||
def create_regions(self) -> None:
|
||||
create_regions(self.options, self.multiworld, self.player, self.dragon_rooms)
|
||||
|
||||
set_rules = set_rules
|
||||
|
||||
def generate_basic(self) -> None:
|
||||
self.multiworld.get_location("Chalice Home", self.player).place_locked_item(
|
||||
self.create_event("Victory", ItemClassification.progression))
|
||||
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
|
||||
|
||||
set_rules = set_rules
|
||||
|
||||
def pre_fill(self):
|
||||
# Place empty items in filler locations here, to limit
|
||||
# the number of exported empty items and the density of stuff in overworld.
|
||||
|
||||
@@ -2,7 +2,7 @@ from typing import List, TYPE_CHECKING, Dict, Any
|
||||
from schema import Schema, Optional
|
||||
from dataclasses import dataclass
|
||||
from worlds.AutoWorld import PerGameCommonOptions
|
||||
from Options import Range, Toggle, DeathLink, Choice, OptionDict, DefaultOnToggle, OptionGroup
|
||||
from Options import Range, Toggle, DeathLink, Choice, OptionDict, DefaultOnToggle, OptionGroup, StartInventoryPool
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import HatInTimeWorld
|
||||
@@ -625,6 +625,8 @@ class ParadeTrapWeight(Range):
|
||||
|
||||
@dataclass
|
||||
class AHITOptions(PerGameCommonOptions):
|
||||
start_inventory_from_pool: StartInventoryPool
|
||||
|
||||
EndGoal: EndGoal
|
||||
ActRandomizer: ActRandomizer
|
||||
ActPlando: ActPlando
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from BaseClasses import Item, ItemClassification, Tutorial, Location, MultiWorld
|
||||
from .Items import item_table, create_item, relic_groups, act_contracts, create_itempool, get_shop_trap_name, \
|
||||
calculate_yarn_costs, alps_hooks
|
||||
calculate_yarn_costs, alps_hooks, junk_weights
|
||||
from .Regions import create_regions, randomize_act_entrances, chapter_act_info, create_events, get_shuffled_region
|
||||
from .Locations import location_table, contract_locations, is_location_valid, get_location_names, TASKSANITY_START_ID, \
|
||||
get_total_locations
|
||||
@@ -78,6 +78,9 @@ class HatInTimeWorld(World):
|
||||
self.nyakuza_thug_items: Dict[str, int] = {}
|
||||
self.badge_seller_count: int = 0
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return self.random.choices(list(junk_weights.keys()), weights=junk_weights.values(), k=1)[0]
|
||||
|
||||
def generate_early(self):
|
||||
adjust_options(self)
|
||||
|
||||
|
||||
@@ -121,6 +121,7 @@ class ALTTPWeb(WebWorld):
|
||||
)
|
||||
|
||||
tutorials = [setup_en, setup_de, setup_es, setup_fr, msu, msu_es, msu_fr, plando, oof_sound]
|
||||
game_info_languages = ["en", "fr"]
|
||||
|
||||
|
||||
class ALTTPWorld(World):
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
maseya-z3pr>=1.0.0rc1
|
||||
xxtea>=3.0.0
|
||||
xxtea>=3.0.0
|
||||
|
||||
@@ -41,6 +41,7 @@ class AquariaWeb(WebWorld):
|
||||
)
|
||||
|
||||
tutorials = [setup, setup_fr]
|
||||
game_info_languages = ["en", "fr"]
|
||||
|
||||
|
||||
class AquariaWorld(World):
|
||||
|
||||
@@ -48,6 +48,10 @@ class CivVIContainer(APContainer, metaclass=AutoPatchRegister):
|
||||
opened_zipfile.writestr(filename, yml)
|
||||
super().write_contents(opened_zipfile)
|
||||
|
||||
def sanitize_value(value: str) -> str:
|
||||
"""Removes values that can cause issues in XML"""
|
||||
return value.replace('"', "'").replace('&', 'and')
|
||||
|
||||
|
||||
def get_cost(world: 'CivVIWorld', location: CivVILocationData) -> int:
|
||||
"""
|
||||
@@ -63,7 +67,7 @@ def get_formatted_player_name(world: 'CivVIWorld', player: int) -> str:
|
||||
Returns the name of the player in the world
|
||||
"""
|
||||
if player != world.player:
|
||||
return f"{world.multiworld.player_name[player]}{apo}s"
|
||||
return sanitize_value(f"{world.multiworld.player_name[player]}{apo}s")
|
||||
return "Your"
|
||||
|
||||
|
||||
@@ -106,7 +110,7 @@ def generate_new_items(world: 'CivVIWorld') -> str:
|
||||
<Row TechnologyType="TECH_BLOCKER" Name="TECH_BLOCKER" EraType="ERA_ANCIENT" UITreeRow="0" Cost="99999" AdvisorType="ADVISOR_GENERIC" Description="Archipelago Tech created to prevent players from researching their own tech. If you can read this, then congrats you have reached the end of your tree before beating the game!"/>
|
||||
{"".join([f'{tab}<Row TechnologyType="{location.name}" '
|
||||
f'Name="{get_formatted_player_name(world, location.item.player)} '
|
||||
f'{location.item.name}" '
|
||||
f'{sanitize_value(location.item.name)}" '
|
||||
f'EraType="{world.location_table[location.name].era_type}" '
|
||||
f'UITreeRow="{world.location_table[location.name].uiTreeRow}" '
|
||||
f'Cost="{get_cost(world, world.location_table[location.name])}" '
|
||||
@@ -122,7 +126,7 @@ def generate_new_items(world: 'CivVIWorld') -> str:
|
||||
<Row CivicType="CIVIC_BLOCKER" Name="CIVIC_BLOCKER" EraType="ERA_ANCIENT" UITreeRow="0" Cost="99999" AdvisorType="ADVISOR_GENERIC" Description="Archipelago Civic created to prevent players from researching their own civics. If you can read this, then congrats you have reached the end of your tree before beating the game!"/>
|
||||
{"".join([f'{tab}<Row CivicType="{location.name}" '
|
||||
f'Name="{get_formatted_player_name(world, location.item.player)} '
|
||||
f'{location.item.name}" '
|
||||
f'{sanitize_value(location.item.name)}" '
|
||||
f'EraType="{world.location_table[location.name].era_type}" '
|
||||
f'UITreeRow="{world.location_table[location.name].uiTreeRow}" '
|
||||
f'Cost="{get_cost(world, world.location_table[location.name])}" '
|
||||
|
||||
0
worlds/civ_6/data/__init__.py
Normal file
0
worlds/civ_6/data/__init__.py
Normal file
@@ -51,7 +51,7 @@ Boosts have logic associated with them in order to verify you can always reach t
|
||||
- I need to kill a unit with a slinger/archer/musketman or some other obsolete unit I can't build anymore, how can I do this?
|
||||
- Don't forget you can go into the Tech Tree and click on a Vanilla tech you've received in order to toggle it on/off. This is necessary in order to pursue some of the boosts if you receive techs in certain orders.
|
||||
- Something happened, and I'm not able to unlock the boost due to game rules!
|
||||
- A few scenarios you may worry about: "Found a religion", "Make an alliance with another player", "Develop an alliance to level 2", "Build a wonder from X Era", to name a few. Any boost that is "miss-able" has been flagged as an "Excluded" location and will not ever receive a progression item. For a list of how each boost is flagged, take a look [here](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/civ_6/data/boosts.json).
|
||||
- A few scenarios you may worry about: "Found a religion", "Make an alliance with another player", "Develop an alliance to level 2", "Build a wonder from X Era", to name a few. Any boost that is "miss-able" has been flagged as an "Excluded" location and will not ever receive a progression item. For a list of how each boost is flagged, take a look [here](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/civ_6/data/boosts.py).
|
||||
- I'm worried that my `PROGRESSIVE_ERA` item is going to be stuck in a boost I won't have time to complete before my maximum unlocked era ends!
|
||||
- The unpredictable timing of boosts and unlocking them can occasionally lead to scenarios where you'll have to first encounter a locked era defeat and then load a previous save. To help reduce the frequency of this, local `PROGRESSIVE_ERA` items will never be located at a boost check.
|
||||
- There's too many boosts, how will I know which one's I should focus on?!
|
||||
|
||||
@@ -14,22 +14,17 @@ The following are required in order to play Civ VI in Archipelago:
|
||||
|
||||
## Enabling the tuner
|
||||
|
||||
Depending on how you installed Civ 6 you will have to navigate to one of the following:
|
||||
|
||||
- `YOUR_USER/Documents/My Games/Sid Meier's Civilization VI/AppOptions.txt`
|
||||
- `YOUR_USER/AppData/Local/Firaxis Games/Sid Meier's Civilization VI/AppOptions.txt`
|
||||
|
||||
Once you have located your `AppOptions.txt`, do a search for `Enable FireTuner`. Set `EnableTuner` to `1` instead of `0`. **NOTE**: While this is active, achievements will be disabled.
|
||||
In the main menu, navigate to the "Game Options" page. On the "Game" menu, make sure that "Tuner (disables achievements)" is enabled.
|
||||
|
||||
## Mod Installation
|
||||
|
||||
1. Download and unzip the latest release of the mod from [GitHub](https://github.com/hesto2/civilization_archipelago_mod/releases/latest).
|
||||
|
||||
2. Copy the folder containing the mod files to your Civ VI mods folder. On Windows, this is usually located at `C:\Users\YOUR_USER\Documents\My Games\Sid Meier's Civilization VI\Mods`.
|
||||
2. Copy the folder containing the mod files to your Civ VI mods folder. On Windows, this is usually located at `C:\Users\YOUR_USER\Documents\My Games\Sid Meier's Civilization VI\Mods`. If you use OneDrive, check if the folder is instead located in your OneDrive file structure.
|
||||
|
||||
3. After the Archipelago host generates a game, you should be given a `.apcivvi` file. Associate the file with the Archipelago Launcher and double click it.
|
||||
|
||||
4. Copy the contents of the new folder it generates (it will have the same name as the `.apcivvi` file) into your Civilization VI Archipelago Mod folder.
|
||||
4. Copy the contents of the new folder it generates (it will have the same name as the `.apcivvi` file) into your Civilization VI Archipelago Mod folder. If double clicking the `.apcivvi` file doesn't generate a folder, you can just rename it to a file ending with `.zip` and extract its contents to a new folder. To do this, right click the `.apcivvi` file and click "Rename", make sure it ends in `.zip`, then right click it again and select "Extract All".
|
||||
|
||||
5. Your finished mod folder should look something like this:
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ class CliqueWebWorld(WebWorld):
|
||||
)
|
||||
|
||||
tutorials = [setup_en, setup_de]
|
||||
game_info_languages = ["en", "de"]
|
||||
|
||||
|
||||
class CliqueWorld(World):
|
||||
|
||||
0
worlds/cv64/data/__init__.py
Normal file
0
worlds/cv64/data/__init__.py
Normal file
@@ -644,6 +644,9 @@ class CV64PatchExtensions(APPatchExtension):
|
||||
# Replace the PowerUp in the Forest Special1 Bridge 3HB rock with an L jewel if 3HBs aren't randomized
|
||||
if not options["multi_hit_breakables"]:
|
||||
rom_data.write_byte(0x10C7A1, 0x03)
|
||||
# Replace the PowerUp in one of the lizard lockers if the lizard locker items aren't randomized.
|
||||
if not options["lizard_locker_items"]:
|
||||
rom_data.write_byte(0xBFCA07, 0x03)
|
||||
# Change the appearance of the Pot-Pourri to that of a larger PowerUp regardless of the above setting, so other
|
||||
# game PermaUps are distinguishable.
|
||||
rom_data.write_int32s(0xEE558, [0x06005F08, 0x3FB00000, 0xFFFFFF00])
|
||||
@@ -714,7 +717,11 @@ class CV64PatchExtensions(APPatchExtension):
|
||||
rom_data.write_int32(0x10CF38, 0x8000FF4D) # CT final room door slab
|
||||
rom_data.write_int32(0x10CF44, 0x1000FF4D) # CT Renon slab
|
||||
rom_data.write_int32(0x10C908, 0x0008FF4D) # Villa foyer chandelier
|
||||
rom_data.write_byte(0x10CF37, 0x04) # pointer for CT final room door slab item data
|
||||
|
||||
# Change the pointer to the Clock Tower final room 3HB door slab drops to not share its values with those of the
|
||||
# 3HB slab near Renon at the top of the room.
|
||||
if options["multi_hit_breakables"]:
|
||||
rom_data.write_byte(0x10CF37, 0x04)
|
||||
|
||||
# Once-per-frame gameplay checks
|
||||
rom_data.write_int32(0x6C848, 0x080FF40D) # J 0x803FD034
|
||||
@@ -1000,6 +1007,7 @@ def write_patch(world: "CV64World", patch: CV64ProcedurePatch, offset_data: Dict
|
||||
"multi_hit_breakables": world.options.multi_hit_breakables.value,
|
||||
"drop_previous_sub_weapon": world.options.drop_previous_sub_weapon.value,
|
||||
"countdown": world.options.countdown.value,
|
||||
"lizard_locker_items": world.options.lizard_locker_items.value,
|
||||
"shopsanity": world.options.shopsanity.value,
|
||||
"panther_dash": world.options.panther_dash.value,
|
||||
"big_toss": world.options.big_toss.value,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import TYPE_CHECKING, Set
|
||||
from typing import TYPE_CHECKING, Set, Optional
|
||||
from .locations import BASE_ID, get_location_names_to_ids
|
||||
from .items import cvcotm_item_info, MAJORS_CLASSIFICATIONS
|
||||
from .locations import cvcotm_location_info
|
||||
@@ -91,6 +91,7 @@ class CastlevaniaCotMClient(BizHawkClient):
|
||||
patch_suffix = ".apcvcotm"
|
||||
sent_initial_packets: bool
|
||||
self_induced_death: bool
|
||||
time_of_sent_death: Optional[float]
|
||||
local_checked_locations: Set[int]
|
||||
client_set_events = {flag_name: False for flag, flag_name in EVENT_FLAG_MAP.items()}
|
||||
killed_dracula_2: bool
|
||||
@@ -139,6 +140,7 @@ class CastlevaniaCotMClient(BizHawkClient):
|
||||
self.sent_initial_packets = False
|
||||
self.local_checked_locations = set()
|
||||
self.self_induced_death = False
|
||||
self.time_of_sent_death = None
|
||||
self.client_set_events = {flag_name: False for flag, flag_name in EVENT_FLAG_MAP.items()}
|
||||
self.killed_dracula_2 = False
|
||||
self.won_battle_arena = False
|
||||
@@ -156,14 +158,16 @@ class CastlevaniaCotMClient(BizHawkClient):
|
||||
return
|
||||
if ctx.slot is None:
|
||||
return
|
||||
if "DeathLink" in args["tags"] and args["data"]["source"] != ctx.slot_info[ctx.slot].name:
|
||||
if "DeathLink" in args["tags"] and args["data"]["time"] != self.time_of_sent_death:
|
||||
if "cause" in args["data"]:
|
||||
cause = args["data"]["cause"]
|
||||
# If the other game sent a death with a blank string for the cause, use the default death message.
|
||||
if cause == "":
|
||||
cause = f"{args['data']['source']} killed you without a word!"
|
||||
if len(cause) > ITEM_NAME_LIMIT + PLAYER_NAME_LIMIT:
|
||||
cause = cause[:ITEM_NAME_LIMIT + PLAYER_NAME_LIMIT]
|
||||
else:
|
||||
# If the other game sent a death with no cause at all, use the default death message.
|
||||
cause = f"{args['data']['source']} killed you without a word!"
|
||||
|
||||
# Highlight the player that killed us in the game's orange text.
|
||||
@@ -259,8 +263,13 @@ class CastlevaniaCotMClient(BizHawkClient):
|
||||
else:
|
||||
area_of_death = DEATHLINK_AREA_NAMES[area]
|
||||
|
||||
# Send the death.
|
||||
await ctx.send_death(f"{ctx.player_names[ctx.slot]} perished in {area_of_death}. Dracula has won!")
|
||||
|
||||
# Record the time in which the death was sent so when we receive the packet we can tell it wasn't our
|
||||
# own death. ctx.on_deathlink overwrites it later, so it MUST be grabbed now.
|
||||
self.time_of_sent_death = ctx.last_death_link
|
||||
|
||||
# Update the Dracula II and Battle Arena events already being done on past separate sessions for if the
|
||||
# player is running the Battle Arena and Dracula goal.
|
||||
if f"castlevania_cotm_events_{ctx.team}_{ctx.slot}" in ctx.stored_data:
|
||||
|
||||
0
worlds/cvcotm/data/__init__.py
Normal file
0
worlds/cvcotm/data/__init__.py
Normal file
@@ -930,7 +930,7 @@ location_tables: Dict[str, List[DS3LocationData]] = {
|
||||
"Great Swamp Ring", miniboss=True), # Giant Crab drop
|
||||
DS3LocationData("RS: Blue Sentinels - Horace", "Blue Sentinels",
|
||||
missable=True, npc=True), # Horace quest
|
||||
DS3LocationData("RS: Crystal Gem - stronghold, lizard", "Crystal Gem"),
|
||||
DS3LocationData("RS: Crystal Gem - stronghold, lizard", "Crystal Gem", lizard=True),
|
||||
DS3LocationData("RS: Fading Soul - woods by Crucifixion Woods bonfire", "Fading Soul",
|
||||
static='03,0:53300210::'),
|
||||
|
||||
|
||||
@@ -98,14 +98,14 @@ def create_trap_items(world, world_options: Options.DLCQuestOptions, trap_needed
|
||||
return traps
|
||||
|
||||
|
||||
def create_items(world, world_options: Options.DLCQuestOptions, locations_count: int, random: Random):
|
||||
def create_items(world, world_options: Options.DLCQuestOptions, locations_count: int, excluded_items: list[str], random: Random):
|
||||
created_items = []
|
||||
if world_options.campaign == Options.Campaign.option_basic or world_options.campaign == Options.Campaign.option_both:
|
||||
create_items_basic(world_options, created_items, world)
|
||||
create_items_basic(world_options, created_items, world, excluded_items)
|
||||
|
||||
if (world_options.campaign == Options.Campaign.option_live_freemium_or_die or
|
||||
world_options.campaign == Options.Campaign.option_both):
|
||||
create_items_lfod(world_options, created_items, world)
|
||||
create_items_lfod(world_options, created_items, world, excluded_items)
|
||||
|
||||
trap_items = create_trap_items(world, world_options, locations_count - len(created_items), random)
|
||||
created_items += trap_items
|
||||
@@ -113,8 +113,12 @@ def create_items(world, world_options: Options.DLCQuestOptions, locations_count:
|
||||
return created_items
|
||||
|
||||
|
||||
def create_items_lfod(world_options, created_items, world):
|
||||
def create_items_lfod(world_options, created_items, world, excluded_items):
|
||||
for item in items_by_group[Group.Freemium]:
|
||||
if item.name in excluded_items:
|
||||
excluded_items.remove(item)
|
||||
continue
|
||||
|
||||
if item.has_any_group(Group.DLC):
|
||||
created_items.append(world.create_item(item))
|
||||
if item.has_any_group(Group.Item) and world_options.item_shuffle == Options.ItemShuffle.option_shuffled:
|
||||
@@ -128,8 +132,12 @@ def create_items_lfod(world_options, created_items, world):
|
||||
create_coin(world_options, created_items, world, 889, 200, Group.Freemium)
|
||||
|
||||
|
||||
def create_items_basic(world_options, created_items, world):
|
||||
def create_items_basic(world_options, created_items, world, excluded_items):
|
||||
for item in items_by_group[Group.DLCQuest]:
|
||||
if item.name in excluded_items:
|
||||
excluded_items.remove(item.name)
|
||||
continue
|
||||
|
||||
if item.has_any_group(Group.DLC):
|
||||
created_items.append(world.create_item(item))
|
||||
if item.has_any_group(Group.Item) and world_options.item_shuffle == Options.ItemShuffle.option_shuffled:
|
||||
|
||||
@@ -34,6 +34,7 @@ class DLCqwebworld(WebWorld):
|
||||
["Deoxis"]
|
||||
)
|
||||
tutorials = [setup_en, setup_fr]
|
||||
game_info_languages = ["en", "fr"]
|
||||
|
||||
|
||||
class DLCqworld(World):
|
||||
@@ -65,10 +66,10 @@ class DLCqworld(World):
|
||||
for location in self.multiworld.get_locations(self.player)
|
||||
if not location.advancement])
|
||||
|
||||
items_to_exclude = [excluded_items
|
||||
items_to_exclude = [excluded_items.name
|
||||
for excluded_items in self.multiworld.precollected_items[self.player]]
|
||||
|
||||
created_items = create_items(self, self.options, locations_count + len(items_to_exclude), self.multiworld.random)
|
||||
created_items = create_items(self, self.options, locations_count, items_to_exclude, self.multiworld.random)
|
||||
|
||||
self.multiworld.itempool += created_items
|
||||
|
||||
@@ -83,9 +84,7 @@ class DLCqworld(World):
|
||||
else:
|
||||
early_items[self.player]["Movement Pack"] = 1
|
||||
|
||||
for item in items_to_exclude:
|
||||
if item in self.multiworld.itempool:
|
||||
self.multiworld.itempool.remove(item)
|
||||
|
||||
|
||||
def precollect_coinsanity(self):
|
||||
if self.options.campaign == Options.Campaign.option_basic:
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import unittest
|
||||
from typing import Dict
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from Options import NamedRange
|
||||
from .option_names import options_to_include
|
||||
from .checks.world_checks import assert_can_win, assert_same_number_items_locations
|
||||
from . import DLCQuestTestBase, setup_dlc_quest_solo_multiworld
|
||||
from .checks.world_checks import assert_can_win, assert_same_number_items_locations
|
||||
from .option_names import options_to_include
|
||||
|
||||
|
||||
def basic_checks(tester: DLCQuestTestBase, multiworld: MultiWorld):
|
||||
@@ -38,6 +39,8 @@ class TestGenerateDynamicOptions(DLCQuestTestBase):
|
||||
basic_checks(self, multiworld)
|
||||
|
||||
def test_given_option_truple_when_generate_then_basic_checks(self):
|
||||
if self.skip_long_tests:
|
||||
raise unittest.SkipTest("Long tests disabled")
|
||||
num_options = len(options_to_include)
|
||||
for option1_index in range(0, num_options):
|
||||
for option2_index in range(option1_index + 1, num_options):
|
||||
@@ -59,6 +62,8 @@ class TestGenerateDynamicOptions(DLCQuestTestBase):
|
||||
basic_checks(self, multiworld)
|
||||
|
||||
def test_given_option_quartet_when_generate_then_basic_checks(self):
|
||||
if self.skip_long_tests:
|
||||
raise unittest.SkipTest("Long tests disabled")
|
||||
num_options = len(options_to_include)
|
||||
for option1_index in range(0, num_options):
|
||||
for option2_index in range(option1_index + 1, num_options):
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
from typing import ClassVar
|
||||
|
||||
from typing import Dict, FrozenSet, Tuple, Any
|
||||
import os
|
||||
from argparse import Namespace
|
||||
from typing import ClassVar
|
||||
from typing import Dict, FrozenSet, Tuple, Any
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from test.bases import WorldTestBase
|
||||
from .. import DLCqworld
|
||||
from test.general import gen_steps, setup_solo_multiworld as setup_base_solo_multiworld
|
||||
from worlds.AutoWorld import call_all
|
||||
from .. import DLCqworld
|
||||
|
||||
|
||||
class DLCQuestTestBase(WorldTestBase):
|
||||
game = "DLCQuest"
|
||||
world: DLCqworld
|
||||
player: ClassVar[int] = 1
|
||||
# Set False to run tests that take long
|
||||
skip_long_tests: bool = True
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
super().setUpClass()
|
||||
cls.skip_long_tests = not bool(os.environ.get("long"))
|
||||
|
||||
def world_setup(self, *args, **kwargs):
|
||||
super().world_setup(*args, **kwargs)
|
||||
|
||||
@@ -126,7 +126,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 3,
|
||||
'index': 64,
|
||||
'doom_type': 2001,
|
||||
'region': "Toxin Refinery (E1M3) Main"},
|
||||
'region': "Toxin Refinery (E1M3) Start"},
|
||||
351019: {'name': 'Toxin Refinery (E1M3) - Shotgun 2',
|
||||
'episode': 1,
|
||||
'map': 3,
|
||||
@@ -234,7 +234,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 4,
|
||||
'index': 107,
|
||||
'doom_type': 8,
|
||||
'region': "Command Control (E1M4) Main"},
|
||||
'region': "Command Control (E1M4) Start"},
|
||||
351037: {'name': 'Command Control (E1M4) - Shotgun',
|
||||
'episode': 1,
|
||||
'map': 4,
|
||||
@@ -504,7 +504,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 7,
|
||||
'index': 122,
|
||||
'doom_type': 2001,
|
||||
'region': "Computer Station (E1M7) Main"},
|
||||
'region': "Computer Station (E1M7) Start"},
|
||||
351082: {'name': 'Computer Station (E1M7) - Rocket launcher',
|
||||
'episode': 1,
|
||||
'map': 7,
|
||||
@@ -912,7 +912,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 4,
|
||||
'index': 109,
|
||||
'doom_type': 2001,
|
||||
'region': "Deimos Lab (E2M4) Main"},
|
||||
'region': "Deimos Lab (E2M4) Start"},
|
||||
351150: {'name': 'Deimos Lab (E2M4) - Mega Armor',
|
||||
'episode': 2,
|
||||
'map': 4,
|
||||
@@ -1242,7 +1242,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 8,
|
||||
'index': 36,
|
||||
'doom_type': 2019,
|
||||
'region': "Tower of Babel (E2M8) Main"},
|
||||
'region': "Tower of Babel (E2M8) Start"},
|
||||
351205: {'name': 'Fortress of Mystery (E2M9) - Supercharge',
|
||||
'episode': 2,
|
||||
'map': 9,
|
||||
@@ -1638,7 +1638,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 5,
|
||||
'index': 187,
|
||||
'doom_type': 2001,
|
||||
'region': "Unholy Cathedral (E3M5) Main"},
|
||||
'region': "Unholy Cathedral (E3M5) Start"},
|
||||
351271: {'name': 'Unholy Cathedral (E3M5) - Shotgun 2',
|
||||
'episode': 3,
|
||||
'map': 5,
|
||||
|
||||
@@ -33,9 +33,11 @@ regions:List[RegionDict] = [
|
||||
|
||||
# Toxin Refinery (E1M3)
|
||||
{"name":"Toxin Refinery (E1M3) Main",
|
||||
"connects_to_hub":True,
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Toxin Refinery (E1M3) Blue","pro":False}]},
|
||||
"connections":[
|
||||
{"target":"Toxin Refinery (E1M3) Blue","pro":False},
|
||||
{"target":"Toxin Refinery (E1M3) Start","pro":False}]},
|
||||
{"name":"Toxin Refinery (E1M3) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
@@ -46,15 +48,20 @@ regions:List[RegionDict] = [
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Toxin Refinery (E1M3) Blue","pro":False}]},
|
||||
{"name":"Toxin Refinery (E1M3) Start",
|
||||
"connects_to_hub":True,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Toxin Refinery (E1M3) Main","pro":False}]},
|
||||
|
||||
# Command Control (E1M4)
|
||||
{"name":"Command Control (E1M4) Main",
|
||||
"connects_to_hub":True,
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[
|
||||
{"target":"Command Control (E1M4) Blue","pro":False},
|
||||
{"target":"Command Control (E1M4) Yellow","pro":False},
|
||||
{"target":"Command Control (E1M4) Ledge","pro":True}]},
|
||||
{"target":"Command Control (E1M4) Ledge","pro":True},
|
||||
{"target":"Command Control (E1M4) Start","pro":False}]},
|
||||
{"name":"Command Control (E1M4) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
@@ -72,6 +79,10 @@ regions:List[RegionDict] = [
|
||||
{"target":"Command Control (E1M4) Main","pro":False},
|
||||
{"target":"Command Control (E1M4) Blue","pro":False},
|
||||
{"target":"Command Control (E1M4) Yellow","pro":False}]},
|
||||
{"name":"Command Control (E1M4) Start",
|
||||
"connects_to_hub":True,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Command Control (E1M4) Main","pro":False}]},
|
||||
|
||||
# Phobos Lab (E1M5)
|
||||
{"name":"Phobos Lab (E1M5) Main",
|
||||
@@ -126,11 +137,12 @@ regions:List[RegionDict] = [
|
||||
|
||||
# Computer Station (E1M7)
|
||||
{"name":"Computer Station (E1M7) Main",
|
||||
"connects_to_hub":True,
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[
|
||||
{"target":"Computer Station (E1M7) Red","pro":False},
|
||||
{"target":"Computer Station (E1M7) Yellow","pro":False}]},
|
||||
{"target":"Computer Station (E1M7) Yellow","pro":False},
|
||||
{"target":"Computer Station (E1M7) Start","pro":False}]},
|
||||
{"name":"Computer Station (E1M7) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
@@ -150,6 +162,10 @@ regions:List[RegionDict] = [
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Computer Station (E1M7) Yellow","pro":False}]},
|
||||
{"name":"Computer Station (E1M7) Start",
|
||||
"connects_to_hub":True,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Computer Station (E1M7) Main","pro":False}]},
|
||||
|
||||
# Phobos Anomaly (E1M8)
|
||||
{"name":"Phobos Anomaly (E1M8) Main",
|
||||
@@ -238,9 +254,11 @@ regions:List[RegionDict] = [
|
||||
|
||||
# Deimos Lab (E2M4)
|
||||
{"name":"Deimos Lab (E2M4) Main",
|
||||
"connects_to_hub":True,
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[{"target":"Deimos Lab (E2M4) Blue","pro":False}]},
|
||||
"connections":[
|
||||
{"target":"Deimos Lab (E2M4) Blue","pro":False},
|
||||
{"target":"Deimos Lab (E2M4) Start","pro":False}]},
|
||||
{"name":"Deimos Lab (E2M4) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
@@ -251,6 +269,10 @@ regions:List[RegionDict] = [
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[{"target":"Deimos Lab (E2M4) Blue","pro":False}]},
|
||||
{"name":"Deimos Lab (E2M4) Start",
|
||||
"connects_to_hub":True,
|
||||
"episode":2,
|
||||
"connections":[{"target":"Deimos Lab (E2M4) Main","pro":False}]},
|
||||
|
||||
# Command Center (E2M5)
|
||||
{"name":"Command Center (E2M5) Main",
|
||||
@@ -314,9 +336,13 @@ regions:List[RegionDict] = [
|
||||
|
||||
# Tower of Babel (E2M8)
|
||||
{"name":"Tower of Babel (E2M8) Main",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[{"target":"Tower of Babel (E2M8) Start","pro":False}]},
|
||||
{"name":"Tower of Babel (E2M8) Start",
|
||||
"connects_to_hub":True,
|
||||
"episode":2,
|
||||
"connections":[]},
|
||||
"connections":[{"target":"Tower of Babel (E2M8) Main","pro":False}]},
|
||||
|
||||
# Fortress of Mystery (E2M9)
|
||||
{"name":"Fortress of Mystery (E2M9) Main",
|
||||
@@ -392,11 +418,12 @@ regions:List[RegionDict] = [
|
||||
|
||||
# Unholy Cathedral (E3M5)
|
||||
{"name":"Unholy Cathedral (E3M5) Main",
|
||||
"connects_to_hub":True,
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[
|
||||
{"target":"Unholy Cathedral (E3M5) Yellow","pro":False},
|
||||
{"target":"Unholy Cathedral (E3M5) Blue","pro":False}]},
|
||||
{"target":"Unholy Cathedral (E3M5) Blue","pro":False},
|
||||
{"target":"Unholy Cathedral (E3M5) Start","pro":False}]},
|
||||
{"name":"Unholy Cathedral (E3M5) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
@@ -405,6 +432,10 @@ regions:List[RegionDict] = [
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Unholy Cathedral (E3M5) Main","pro":False}]},
|
||||
{"name":"Unholy Cathedral (E3M5) Start",
|
||||
"connects_to_hub":True,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Unholy Cathedral (E3M5) Main","pro":False}]},
|
||||
|
||||
# Mt. Erebus (E3M6)
|
||||
{"name":"Mt. Erebus (E3M6) Main",
|
||||
|
||||
@@ -23,10 +23,6 @@ def set_episode1_rules(player, multiworld, pro):
|
||||
state.has("Nuclear Plant (E1M2) - Red keycard", player, 1))
|
||||
|
||||
# Toxin Refinery (E1M3)
|
||||
set_rule(multiworld.get_entrance("Hub -> Toxin Refinery (E1M3) Main", player), lambda state:
|
||||
(state.has("Toxin Refinery (E1M3)", player, 1)) and
|
||||
(state.has("Shotgun", player, 1) or
|
||||
state.has("Chaingun", player, 1)))
|
||||
set_rule(multiworld.get_entrance("Toxin Refinery (E1M3) Main -> Toxin Refinery (E1M3) Blue", player), lambda state:
|
||||
state.has("Toxin Refinery (E1M3) - Blue keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("Toxin Refinery (E1M3) Blue -> Toxin Refinery (E1M3) Yellow", player), lambda state:
|
||||
@@ -35,12 +31,13 @@ def set_episode1_rules(player, multiworld, pro):
|
||||
state.has("Toxin Refinery (E1M3) - Blue keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("Toxin Refinery (E1M3) Yellow -> Toxin Refinery (E1M3) Blue", player), lambda state:
|
||||
state.has("Toxin Refinery (E1M3) - Yellow keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("Hub -> Toxin Refinery (E1M3) Start", player), lambda state:
|
||||
state.has("Toxin Refinery (E1M3)", player, 1))
|
||||
set_rule(multiworld.get_entrance("Toxin Refinery (E1M3) Start -> Toxin Refinery (E1M3) Main", player), lambda state:
|
||||
state.has("Shotgun", player, 1) or
|
||||
state.has("Chaingun", player, 1))
|
||||
|
||||
# Command Control (E1M4)
|
||||
set_rule(multiworld.get_entrance("Hub -> Command Control (E1M4) Main", player), lambda state:
|
||||
state.has("Command Control (E1M4)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1))
|
||||
set_rule(multiworld.get_entrance("Command Control (E1M4) Main -> Command Control (E1M4) Blue", player), lambda state:
|
||||
state.has("Command Control (E1M4) - Blue keycard", player, 1) or
|
||||
state.has("Command Control (E1M4) - Yellow keycard", player, 1))
|
||||
@@ -50,6 +47,11 @@ def set_episode1_rules(player, multiworld, pro):
|
||||
set_rule(multiworld.get_entrance("Command Control (E1M4) Blue -> Command Control (E1M4) Main", player), lambda state:
|
||||
state.has("Command Control (E1M4) - Yellow keycard", player, 1) or
|
||||
state.has("Command Control (E1M4) - Blue keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("Hub -> Command Control (E1M4) Start", player), lambda state:
|
||||
state.has("Command Control (E1M4)", player, 1))
|
||||
set_rule(multiworld.get_entrance("Command Control (E1M4) Start -> Command Control (E1M4) Main", player), lambda state:
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1))
|
||||
|
||||
# Phobos Lab (E1M5)
|
||||
set_rule(multiworld.get_entrance("Hub -> Phobos Lab (E1M5) Main", player), lambda state:
|
||||
@@ -83,11 +85,6 @@ def set_episode1_rules(player, multiworld, pro):
|
||||
state.has("Central Processing (E1M6) - Yellow keycard", player, 1))
|
||||
|
||||
# Computer Station (E1M7)
|
||||
set_rule(multiworld.get_entrance("Hub -> Computer Station (E1M7) Main", player), lambda state:
|
||||
state.has("Computer Station (E1M7)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Rocket launcher", player, 1))
|
||||
set_rule(multiworld.get_entrance("Computer Station (E1M7) Main -> Computer Station (E1M7) Red", player), lambda state:
|
||||
state.has("Computer Station (E1M7) - Red keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("Computer Station (E1M7) Main -> Computer Station (E1M7) Yellow", player), lambda state:
|
||||
@@ -103,6 +100,12 @@ def set_episode1_rules(player, multiworld, pro):
|
||||
state.has("Computer Station (E1M7) - Red keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("Computer Station (E1M7) Courtyard -> Computer Station (E1M7) Yellow", player), lambda state:
|
||||
state.has("Computer Station (E1M7) - Yellow keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("Hub -> Computer Station (E1M7) Start", player), lambda state:
|
||||
state.has("Computer Station (E1M7)", player, 1))
|
||||
set_rule(multiworld.get_entrance("Computer Station (E1M7) Start -> Computer Station (E1M7) Main", player), lambda state:
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Rocket launcher", player, 1) and
|
||||
state.has("Chaingun", player, 1))
|
||||
|
||||
# Phobos Anomaly (E1M8)
|
||||
set_rule(multiworld.get_entrance("Hub -> Phobos Anomaly (E1M8) Start", player), lambda state:
|
||||
@@ -172,15 +175,16 @@ def set_episode2_rules(player, multiworld, pro):
|
||||
state.has("Refinery (E2M3) - Blue keycard", player, 1))
|
||||
|
||||
# Deimos Lab (E2M4)
|
||||
set_rule(multiworld.get_entrance("Hub -> Deimos Lab (E2M4) Main", player), lambda state:
|
||||
state.has("Deimos Lab (E2M4)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Plasma gun", player, 1))
|
||||
set_rule(multiworld.get_entrance("Deimos Lab (E2M4) Main -> Deimos Lab (E2M4) Blue", player), lambda state:
|
||||
state.has("Deimos Lab (E2M4) - Blue keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("Deimos Lab (E2M4) Blue -> Deimos Lab (E2M4) Yellow", player), lambda state:
|
||||
state.has("Deimos Lab (E2M4) - Yellow keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("Hub -> Deimos Lab (E2M4) Start", player), lambda state:
|
||||
state.has("Deimos Lab (E2M4)", player, 1))
|
||||
set_rule(multiworld.get_entrance("Deimos Lab (E2M4) Start -> Deimos Lab (E2M4) Main", player), lambda state:
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Plasma gun", player, 1) and
|
||||
state.has("Chaingun", player, 1))
|
||||
|
||||
# Command Center (E2M5)
|
||||
set_rule(multiworld.get_entrance("Hub -> Command Center (E2M5) Main", player), lambda state:
|
||||
@@ -238,11 +242,11 @@ def set_episode2_rules(player, multiworld, pro):
|
||||
state.has("Spawning Vats (E2M7) - Red keycard", player, 1))
|
||||
|
||||
# Tower of Babel (E2M8)
|
||||
set_rule(multiworld.get_entrance("Hub -> Tower of Babel (E2M8) Main", player), lambda state:
|
||||
(state.has("Tower of Babel (E2M8)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
set_rule(multiworld.get_entrance("Hub -> Tower of Babel (E2M8) Start", player), lambda state:
|
||||
state.has("Tower of Babel (E2M8)", player, 1))
|
||||
set_rule(multiworld.get_entrance("Tower of Babel (E2M8) Start -> Tower of Babel (E2M8) Main", player), lambda state:
|
||||
(state.has("Chaingun", player, 1) and
|
||||
state.has("Shotgun", player, 1)) and (state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
|
||||
@@ -321,13 +325,6 @@ def set_episode3_rules(player, multiworld, pro):
|
||||
state.has("House of Pain (E3M4) - Yellow skull key", player, 1))
|
||||
|
||||
# Unholy Cathedral (E3M5)
|
||||
set_rule(multiworld.get_entrance("Hub -> Unholy Cathedral (E3M5) Main", player), lambda state:
|
||||
(state.has("Unholy Cathedral (E3M5)", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
set_rule(multiworld.get_entrance("Unholy Cathedral (E3M5) Main -> Unholy Cathedral (E3M5) Yellow", player), lambda state:
|
||||
state.has("Unholy Cathedral (E3M5) - Yellow skull key", player, 1))
|
||||
set_rule(multiworld.get_entrance("Unholy Cathedral (E3M5) Main -> Unholy Cathedral (E3M5) Blue", player), lambda state:
|
||||
@@ -336,6 +333,13 @@ def set_episode3_rules(player, multiworld, pro):
|
||||
state.has("Unholy Cathedral (E3M5) - Blue skull key", player, 1))
|
||||
set_rule(multiworld.get_entrance("Unholy Cathedral (E3M5) Yellow -> Unholy Cathedral (E3M5) Main", player), lambda state:
|
||||
state.has("Unholy Cathedral (E3M5) - Yellow skull key", player, 1))
|
||||
set_rule(multiworld.get_entrance("Hub -> Unholy Cathedral (E3M5) Start", player), lambda state:
|
||||
state.has("Unholy Cathedral (E3M5)", player, 1))
|
||||
set_rule(multiworld.get_entrance("Unholy Cathedral (E3M5) Start -> Unholy Cathedral (E3M5) Main", player), lambda state:
|
||||
(state.has("Chaingun", player, 1) and
|
||||
state.has("Shotgun", player, 1)) and (state.has("Plasma gun", player, 1) or
|
||||
state.has("Rocket launcher", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
|
||||
# Mt. Erebus (E3M6)
|
||||
set_rule(multiworld.get_entrance("Hub -> Mt. Erebus (E3M6) Main", player), lambda state:
|
||||
|
||||
@@ -50,14 +50,14 @@ class DOOM1993World(World):
|
||||
location_name_to_id = {data["name"]: loc_id for loc_id, data in Locations.location_table.items()}
|
||||
location_name_groups = Locations.location_name_groups
|
||||
|
||||
starting_level_for_episode: List[str] = [
|
||||
"Hangar (E1M1)",
|
||||
"Deimos Anomaly (E2M1)",
|
||||
"Hell Keep (E3M1)",
|
||||
"Hell Beneath (E4M1)"
|
||||
]
|
||||
starting_level_for_episode: Dict[int, str] = {
|
||||
1: "Hangar (E1M1)",
|
||||
2: "Deimos Anomaly (E2M1)",
|
||||
3: "Hell Keep (E3M1)",
|
||||
4: "Hell Beneath (E4M1)"
|
||||
}
|
||||
|
||||
boss_level_for_espidoes: List[str] = [
|
||||
all_boss_levels: List[str] = [
|
||||
"Phobos Anomaly (E1M8)",
|
||||
"Tower of Babel (E2M8)",
|
||||
"Dis (E3M8)",
|
||||
@@ -82,6 +82,7 @@ class DOOM1993World(World):
|
||||
def __init__(self, multiworld: MultiWorld, player: int):
|
||||
self.included_episodes = [1, 1, 1, 0]
|
||||
self.location_count = 0
|
||||
self.starting_levels = []
|
||||
|
||||
super().__init__(multiworld, player)
|
||||
|
||||
@@ -99,6 +100,16 @@ class DOOM1993World(World):
|
||||
if self.get_episode_count() == 0:
|
||||
self.included_episodes[0] = 1
|
||||
|
||||
self.starting_levels = [level_name for (episode, level_name) in self.starting_level_for_episode.items()
|
||||
if self.included_episodes[episode - 1]]
|
||||
|
||||
# Solo Episode 3 presents a problem, because Hell Keep has only two locations.
|
||||
# We have to give the player Slough of Despair (E3M2), and also mark a weapon early.
|
||||
if self.get_episode_count() == 1 and self.included_episodes[2]:
|
||||
early_weapon = self.random.choice(["Shotgun", "Chaingun"])
|
||||
self.multiworld.early_items[self.player][early_weapon] = 1
|
||||
self.starting_levels.append("Slough of Despair (E3M2)")
|
||||
|
||||
def create_regions(self):
|
||||
pro = self.options.pro.value
|
||||
|
||||
@@ -152,7 +163,7 @@ class DOOM1993World(World):
|
||||
def completion_rule(self, state: CollectionState):
|
||||
goal_levels = Maps.map_names
|
||||
if self.options.goal.value:
|
||||
goal_levels = self.boss_level_for_espidoes
|
||||
goal_levels = self.all_boss_levels
|
||||
|
||||
for map_name in goal_levels:
|
||||
if map_name + " - Exit" not in self.location_name_to_id:
|
||||
@@ -201,7 +212,7 @@ class DOOM1993World(World):
|
||||
if item["episode"] != -1 and not self.included_episodes[item["episode"] - 1]:
|
||||
continue
|
||||
|
||||
count = item["count"] if item["name"] not in self.starting_level_for_episode else item["count"] - 1
|
||||
count = item["count"] if item["name"] not in self.starting_levels else item["count"] - 1
|
||||
itempool += [self.create_item(item["name"]) for _ in range(count)]
|
||||
|
||||
# Backpack(s) based on options
|
||||
@@ -232,9 +243,8 @@ class DOOM1993World(World):
|
||||
self.location_count -= 1
|
||||
|
||||
# Give starting levels right away
|
||||
for i in range(len(self.included_episodes)):
|
||||
if self.included_episodes[i]:
|
||||
self.multiworld.push_precollected(self.create_item(self.starting_level_for_episode[i]))
|
||||
for map_name in self.starting_levels:
|
||||
self.multiworld.push_precollected(self.create_item(map_name))
|
||||
|
||||
# Give Computer area maps if option selected
|
||||
if self.options.start_with_computer_area_maps.value:
|
||||
|
||||
@@ -412,7 +412,7 @@ item_table: Dict[int, ItemDict] = {
|
||||
'map': 2},
|
||||
360246: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'Barrels o Fun (MAP23) - Yellow skull key',
|
||||
'name': "Barrels o' Fun (MAP23) - Yellow skull key",
|
||||
'doom_type': 39,
|
||||
'episode': 3,
|
||||
'map': 3},
|
||||
@@ -880,19 +880,19 @@ item_table: Dict[int, ItemDict] = {
|
||||
'map': 2},
|
||||
360466: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'Barrels o Fun (MAP23)',
|
||||
'name': "Barrels o' Fun (MAP23)",
|
||||
'doom_type': -1,
|
||||
'episode': 3,
|
||||
'map': 3},
|
||||
360467: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'Barrels o Fun (MAP23) - Complete',
|
||||
'name': "Barrels o' Fun (MAP23) - Complete",
|
||||
'doom_type': -2,
|
||||
'episode': 3,
|
||||
'map': 3},
|
||||
360468: {'classification': ItemClassification.filler,
|
||||
'count': 1,
|
||||
'name': 'Barrels o Fun (MAP23) - Computer area map',
|
||||
'name': "Barrels o' Fun (MAP23) - Computer area map",
|
||||
'doom_type': 2026,
|
||||
'episode': 3,
|
||||
'map': 3},
|
||||
@@ -1024,37 +1024,37 @@ item_table: Dict[int, ItemDict] = {
|
||||
'map': 10},
|
||||
360490: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'Wolfenstein2 (MAP31)',
|
||||
'name': 'Wolfenstein (MAP31)',
|
||||
'doom_type': -1,
|
||||
'episode': 4,
|
||||
'map': 1},
|
||||
360491: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'Wolfenstein2 (MAP31) - Complete',
|
||||
'name': 'Wolfenstein (MAP31) - Complete',
|
||||
'doom_type': -2,
|
||||
'episode': 4,
|
||||
'map': 1},
|
||||
360492: {'classification': ItemClassification.filler,
|
||||
'count': 1,
|
||||
'name': 'Wolfenstein2 (MAP31) - Computer area map',
|
||||
'name': 'Wolfenstein (MAP31) - Computer area map',
|
||||
'doom_type': 2026,
|
||||
'episode': 4,
|
||||
'map': 1},
|
||||
360493: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'Grosse2 (MAP32)',
|
||||
'name': 'Grosse (MAP32)',
|
||||
'doom_type': -1,
|
||||
'episode': 4,
|
||||
'map': 2},
|
||||
360494: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'Grosse2 (MAP32) - Complete',
|
||||
'name': 'Grosse (MAP32) - Complete',
|
||||
'doom_type': -2,
|
||||
'episode': 4,
|
||||
'map': 2},
|
||||
360495: {'classification': ItemClassification.filler,
|
||||
'count': 1,
|
||||
'name': 'Grosse2 (MAP32) - Computer area map',
|
||||
'name': 'Grosse (MAP32) - Computer area map',
|
||||
'doom_type': 2026,
|
||||
'episode': 4,
|
||||
'map': 2},
|
||||
@@ -1087,9 +1087,9 @@ item_table: Dict[int, ItemDict] = {
|
||||
|
||||
item_name_groups: Dict[str, Set[str]] = {
|
||||
'Ammos': {'Box of bullets', 'Box of rockets', 'Box of shotgun shells', 'Energy cell pack', },
|
||||
'Computer area maps': {'Barrels o Fun (MAP23) - Computer area map', 'Bloodfalls (MAP25) - Computer area map', 'Circle of Death (MAP11) - Computer area map', 'Dead Simple (MAP07) - Computer area map', 'Downtown (MAP13) - Computer area map', 'Entryway (MAP01) - Computer area map', 'Gotcha! (MAP20) - Computer area map', 'Grosse2 (MAP32) - Computer area map', 'Icon of Sin (MAP30) - Computer area map', 'Industrial Zone (MAP15) - Computer area map', 'Monster Condo (MAP27) - Computer area map', 'Nirvana (MAP21) - Computer area map', 'Refueling Base (MAP10) - Computer area map', 'Suburbs (MAP16) - Computer area map', 'Tenements (MAP17) - Computer area map', 'The Abandoned Mines (MAP26) - Computer area map', 'The Catacombs (MAP22) - Computer area map', 'The Chasm (MAP24) - Computer area map', 'The Citadel (MAP19) - Computer area map', 'The Courtyard (MAP18) - Computer area map', 'The Crusher (MAP06) - Computer area map', 'The Factory (MAP12) - Computer area map', 'The Focus (MAP04) - Computer area map', 'The Gantlet (MAP03) - Computer area map', 'The Inmost Dens (MAP14) - Computer area map', 'The Living End (MAP29) - Computer area map', 'The Pit (MAP09) - Computer area map', 'The Spirit World (MAP28) - Computer area map', 'The Waste Tunnels (MAP05) - Computer area map', 'Tricks and Traps (MAP08) - Computer area map', 'Underhalls (MAP02) - Computer area map', 'Wolfenstein2 (MAP31) - Computer area map', },
|
||||
'Keys': {'Barrels o Fun (MAP23) - Yellow skull key', 'Bloodfalls (MAP25) - Blue skull key', 'Circle of Death (MAP11) - Blue keycard', 'Circle of Death (MAP11) - Red keycard', 'Downtown (MAP13) - Blue keycard', 'Downtown (MAP13) - Red keycard', 'Downtown (MAP13) - Yellow keycard', 'Industrial Zone (MAP15) - Blue keycard', 'Industrial Zone (MAP15) - Red keycard', 'Industrial Zone (MAP15) - Yellow keycard', 'Monster Condo (MAP27) - Blue skull key', 'Monster Condo (MAP27) - Red skull key', 'Monster Condo (MAP27) - Yellow skull key', 'Nirvana (MAP21) - Blue skull key', 'Nirvana (MAP21) - Red skull key', 'Nirvana (MAP21) - Yellow skull key', 'Refueling Base (MAP10) - Blue keycard', 'Refueling Base (MAP10) - Yellow keycard', 'Suburbs (MAP16) - Blue skull key', 'Suburbs (MAP16) - Red skull key', 'Tenements (MAP17) - Blue keycard', 'Tenements (MAP17) - Red keycard', 'Tenements (MAP17) - Yellow skull key', 'The Abandoned Mines (MAP26) - Blue keycard', 'The Abandoned Mines (MAP26) - Red keycard', 'The Abandoned Mines (MAP26) - Yellow keycard', 'The Catacombs (MAP22) - Blue skull key', 'The Catacombs (MAP22) - Red skull key', 'The Chasm (MAP24) - Blue keycard', 'The Chasm (MAP24) - Red keycard', 'The Citadel (MAP19) - Blue skull key', 'The Citadel (MAP19) - Red skull key', 'The Citadel (MAP19) - Yellow skull key', 'The Courtyard (MAP18) - Blue skull key', 'The Courtyard (MAP18) - Yellow skull key', 'The Crusher (MAP06) - Blue keycard', 'The Crusher (MAP06) - Red keycard', 'The Crusher (MAP06) - Yellow keycard', 'The Factory (MAP12) - Blue keycard', 'The Factory (MAP12) - Yellow keycard', 'The Focus (MAP04) - Blue keycard', 'The Focus (MAP04) - Red keycard', 'The Focus (MAP04) - Yellow keycard', 'The Gantlet (MAP03) - Blue keycard', 'The Gantlet (MAP03) - Red keycard', 'The Inmost Dens (MAP14) - Blue skull key', 'The Inmost Dens (MAP14) - Red skull key', 'The Pit (MAP09) - Blue keycard', 'The Pit (MAP09) - Yellow keycard', 'The Spirit World (MAP28) - Red skull key', 'The Spirit World (MAP28) - Yellow skull key', 'The Waste Tunnels (MAP05) - Blue keycard', 'The Waste Tunnels (MAP05) - Red keycard', 'The Waste Tunnels (MAP05) - Yellow keycard', 'Tricks and Traps (MAP08) - Red skull key', 'Tricks and Traps (MAP08) - Yellow skull key', 'Underhalls (MAP02) - Blue keycard', 'Underhalls (MAP02) - Red keycard', },
|
||||
'Levels': {'Barrels o Fun (MAP23)', 'Bloodfalls (MAP25)', 'Circle of Death (MAP11)', 'Dead Simple (MAP07)', 'Downtown (MAP13)', 'Entryway (MAP01)', 'Gotcha! (MAP20)', 'Grosse2 (MAP32)', 'Icon of Sin (MAP30)', 'Industrial Zone (MAP15)', 'Monster Condo (MAP27)', 'Nirvana (MAP21)', 'Refueling Base (MAP10)', 'Suburbs (MAP16)', 'Tenements (MAP17)', 'The Abandoned Mines (MAP26)', 'The Catacombs (MAP22)', 'The Chasm (MAP24)', 'The Citadel (MAP19)', 'The Courtyard (MAP18)', 'The Crusher (MAP06)', 'The Factory (MAP12)', 'The Focus (MAP04)', 'The Gantlet (MAP03)', 'The Inmost Dens (MAP14)', 'The Living End (MAP29)', 'The Pit (MAP09)', 'The Spirit World (MAP28)', 'The Waste Tunnels (MAP05)', 'Tricks and Traps (MAP08)', 'Underhalls (MAP02)', 'Wolfenstein2 (MAP31)', },
|
||||
'Computer area maps': {"Barrels o' Fun (MAP23) - Computer area map", 'Bloodfalls (MAP25) - Computer area map', 'Circle of Death (MAP11) - Computer area map', 'Dead Simple (MAP07) - Computer area map', 'Downtown (MAP13) - Computer area map', 'Entryway (MAP01) - Computer area map', 'Gotcha! (MAP20) - Computer area map', 'Grosse (MAP32) - Computer area map', 'Icon of Sin (MAP30) - Computer area map', 'Industrial Zone (MAP15) - Computer area map', 'Monster Condo (MAP27) - Computer area map', 'Nirvana (MAP21) - Computer area map', 'Refueling Base (MAP10) - Computer area map', 'Suburbs (MAP16) - Computer area map', 'Tenements (MAP17) - Computer area map', 'The Abandoned Mines (MAP26) - Computer area map', 'The Catacombs (MAP22) - Computer area map', 'The Chasm (MAP24) - Computer area map', 'The Citadel (MAP19) - Computer area map', 'The Courtyard (MAP18) - Computer area map', 'The Crusher (MAP06) - Computer area map', 'The Factory (MAP12) - Computer area map', 'The Focus (MAP04) - Computer area map', 'The Gantlet (MAP03) - Computer area map', 'The Inmost Dens (MAP14) - Computer area map', 'The Living End (MAP29) - Computer area map', 'The Pit (MAP09) - Computer area map', 'The Spirit World (MAP28) - Computer area map', 'The Waste Tunnels (MAP05) - Computer area map', 'Tricks and Traps (MAP08) - Computer area map', 'Underhalls (MAP02) - Computer area map', 'Wolfenstein (MAP31) - Computer area map', },
|
||||
'Keys': {"Barrels o' Fun (MAP23) - Yellow skull key", 'Bloodfalls (MAP25) - Blue skull key', 'Circle of Death (MAP11) - Blue keycard', 'Circle of Death (MAP11) - Red keycard', 'Downtown (MAP13) - Blue keycard', 'Downtown (MAP13) - Red keycard', 'Downtown (MAP13) - Yellow keycard', 'Industrial Zone (MAP15) - Blue keycard', 'Industrial Zone (MAP15) - Red keycard', 'Industrial Zone (MAP15) - Yellow keycard', 'Monster Condo (MAP27) - Blue skull key', 'Monster Condo (MAP27) - Red skull key', 'Monster Condo (MAP27) - Yellow skull key', 'Nirvana (MAP21) - Blue skull key', 'Nirvana (MAP21) - Red skull key', 'Nirvana (MAP21) - Yellow skull key', 'Refueling Base (MAP10) - Blue keycard', 'Refueling Base (MAP10) - Yellow keycard', 'Suburbs (MAP16) - Blue skull key', 'Suburbs (MAP16) - Red skull key', 'Tenements (MAP17) - Blue keycard', 'Tenements (MAP17) - Red keycard', 'Tenements (MAP17) - Yellow skull key', 'The Abandoned Mines (MAP26) - Blue keycard', 'The Abandoned Mines (MAP26) - Red keycard', 'The Abandoned Mines (MAP26) - Yellow keycard', 'The Catacombs (MAP22) - Blue skull key', 'The Catacombs (MAP22) - Red skull key', 'The Chasm (MAP24) - Blue keycard', 'The Chasm (MAP24) - Red keycard', 'The Citadel (MAP19) - Blue skull key', 'The Citadel (MAP19) - Red skull key', 'The Citadel (MAP19) - Yellow skull key', 'The Courtyard (MAP18) - Blue skull key', 'The Courtyard (MAP18) - Yellow skull key', 'The Crusher (MAP06) - Blue keycard', 'The Crusher (MAP06) - Red keycard', 'The Crusher (MAP06) - Yellow keycard', 'The Factory (MAP12) - Blue keycard', 'The Factory (MAP12) - Yellow keycard', 'The Focus (MAP04) - Blue keycard', 'The Focus (MAP04) - Red keycard', 'The Focus (MAP04) - Yellow keycard', 'The Gantlet (MAP03) - Blue keycard', 'The Gantlet (MAP03) - Red keycard', 'The Inmost Dens (MAP14) - Blue skull key', 'The Inmost Dens (MAP14) - Red skull key', 'The Pit (MAP09) - Blue keycard', 'The Pit (MAP09) - Yellow keycard', 'The Spirit World (MAP28) - Red skull key', 'The Spirit World (MAP28) - Yellow skull key', 'The Waste Tunnels (MAP05) - Blue keycard', 'The Waste Tunnels (MAP05) - Red keycard', 'The Waste Tunnels (MAP05) - Yellow keycard', 'Tricks and Traps (MAP08) - Red skull key', 'Tricks and Traps (MAP08) - Yellow skull key', 'Underhalls (MAP02) - Blue keycard', 'Underhalls (MAP02) - Red keycard', },
|
||||
'Levels': {"Barrels o' Fun (MAP23)", 'Bloodfalls (MAP25)', 'Circle of Death (MAP11)', 'Dead Simple (MAP07)', 'Downtown (MAP13)', 'Entryway (MAP01)', 'Gotcha! (MAP20)', 'Grosse (MAP32)', 'Icon of Sin (MAP30)', 'Industrial Zone (MAP15)', 'Monster Condo (MAP27)', 'Nirvana (MAP21)', 'Refueling Base (MAP10)', 'Suburbs (MAP16)', 'Tenements (MAP17)', 'The Abandoned Mines (MAP26)', 'The Catacombs (MAP22)', 'The Chasm (MAP24)', 'The Citadel (MAP19)', 'The Courtyard (MAP18)', 'The Crusher (MAP06)', 'The Factory (MAP12)', 'The Focus (MAP04)', 'The Gantlet (MAP03)', 'The Inmost Dens (MAP14)', 'The Living End (MAP29)', 'The Pit (MAP09)', 'The Spirit World (MAP28)', 'The Waste Tunnels (MAP05)', 'Tricks and Traps (MAP08)', 'Underhalls (MAP02)', 'Wolfenstein (MAP31)', },
|
||||
'Powerups': {'Armor', 'Berserk', 'Invulnerability', 'Mega Armor', 'Megasphere', 'Partial invisibility', 'Supercharge', },
|
||||
'Weapons': {'BFG9000', 'Chaingun', 'Chainsaw', 'Plasma gun', 'Rocket launcher', 'Shotgun', 'Super Shotgun', },
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 5,
|
||||
'index': 46,
|
||||
'doom_type': 82,
|
||||
'region': "The Waste Tunnels (MAP05) Main"},
|
||||
'region': "The Waste Tunnels (MAP05) Start"},
|
||||
361028: {'name': 'The Waste Tunnels (MAP05) - Blue keycard',
|
||||
'episode': 1,
|
||||
'map': 5,
|
||||
@@ -234,7 +234,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 5,
|
||||
'index': 202,
|
||||
'doom_type': 2001,
|
||||
'region': "The Waste Tunnels (MAP05) Main"},
|
||||
'region': "The Waste Tunnels (MAP05) Start"},
|
||||
361037: {'name': 'The Waste Tunnels (MAP05) - Berserk',
|
||||
'episode': 1,
|
||||
'map': 5,
|
||||
@@ -360,7 +360,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 7,
|
||||
'index': 8,
|
||||
'doom_type': 82,
|
||||
'region': "Dead Simple (MAP07) Main"},
|
||||
'region': "Dead Simple (MAP07) Start"},
|
||||
361058: {'name': 'Dead Simple (MAP07) - Chaingun',
|
||||
'episode': 1,
|
||||
'map': 7,
|
||||
@@ -378,7 +378,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 7,
|
||||
'index': 43,
|
||||
'doom_type': 8,
|
||||
'region': "Dead Simple (MAP07) Main"},
|
||||
'region': "Dead Simple (MAP07) Start"},
|
||||
361061: {'name': 'Dead Simple (MAP07) - Berserk',
|
||||
'episode': 1,
|
||||
'map': 7,
|
||||
@@ -570,7 +570,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 9,
|
||||
'index': 26,
|
||||
'doom_type': 2019,
|
||||
'region': "The Pit (MAP09) Main"},
|
||||
'region': "The Pit (MAP09) Start"},
|
||||
361093: {'name': 'The Pit (MAP09) - Supercharge',
|
||||
'episode': 1,
|
||||
'map': 9,
|
||||
@@ -678,7 +678,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 10,
|
||||
'index': 99,
|
||||
'doom_type': 2001,
|
||||
'region': "Refueling Base (MAP10) Main"},
|
||||
'region': "Refueling Base (MAP10) Start"},
|
||||
361111: {'name': 'Refueling Base (MAP10) - Chaingun',
|
||||
'episode': 1,
|
||||
'map': 10,
|
||||
@@ -846,31 +846,31 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 11,
|
||||
'index': 88,
|
||||
'doom_type': 8,
|
||||
'region': "Circle of Death (MAP11) Red"},
|
||||
'region': "Circle of Death (MAP11) Ending"},
|
||||
361139: {'name': 'Circle of Death (MAP11) - Supercharge 2',
|
||||
'episode': 1,
|
||||
'map': 11,
|
||||
'index': 108,
|
||||
'doom_type': 2013,
|
||||
'region': "Circle of Death (MAP11) Red"},
|
||||
'region': "Circle of Death (MAP11) Ending"},
|
||||
361140: {'name': 'Circle of Death (MAP11) - BFG9000',
|
||||
'episode': 1,
|
||||
'map': 11,
|
||||
'index': 110,
|
||||
'doom_type': 2006,
|
||||
'region': "Circle of Death (MAP11) Red"},
|
||||
'region': "Circle of Death (MAP11) Ending"},
|
||||
361141: {'name': 'Circle of Death (MAP11) - Exit',
|
||||
'episode': 1,
|
||||
'map': 11,
|
||||
'index': -1,
|
||||
'doom_type': -1,
|
||||
'region': "Circle of Death (MAP11) Red"},
|
||||
'region': "Circle of Death (MAP11) Ending"},
|
||||
361142: {'name': 'The Factory (MAP12) - Shotgun',
|
||||
'episode': 2,
|
||||
'map': 1,
|
||||
'index': 14,
|
||||
'doom_type': 2001,
|
||||
'region': "The Factory (MAP12) Main"},
|
||||
'region': "The Factory (MAP12) Outdoors"},
|
||||
361143: {'name': 'The Factory (MAP12) - Berserk',
|
||||
'episode': 2,
|
||||
'map': 1,
|
||||
@@ -888,13 +888,13 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 1,
|
||||
'index': 52,
|
||||
'doom_type': 2013,
|
||||
'region': "The Factory (MAP12) Main"},
|
||||
'region': "The Factory (MAP12) Indoors"},
|
||||
361146: {'name': 'The Factory (MAP12) - Blue keycard',
|
||||
'episode': 2,
|
||||
'map': 1,
|
||||
'index': 54,
|
||||
'doom_type': 5,
|
||||
'region': "The Factory (MAP12) Main"},
|
||||
'region': "The Factory (MAP12) Indoors"},
|
||||
361147: {'name': 'The Factory (MAP12) - Armor',
|
||||
'episode': 2,
|
||||
'map': 1,
|
||||
@@ -912,31 +912,31 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 1,
|
||||
'index': 83,
|
||||
'doom_type': 2013,
|
||||
'region': "The Factory (MAP12) Main"},
|
||||
'region': "The Factory (MAP12) Indoors"},
|
||||
361150: {'name': 'The Factory (MAP12) - Armor 2',
|
||||
'episode': 2,
|
||||
'map': 1,
|
||||
'index': 92,
|
||||
'doom_type': 2018,
|
||||
'region': "The Factory (MAP12) Main"},
|
||||
'region': "The Factory (MAP12) Outdoors"},
|
||||
361151: {'name': 'The Factory (MAP12) - Partial invisibility',
|
||||
'episode': 2,
|
||||
'map': 1,
|
||||
'index': 93,
|
||||
'doom_type': 2024,
|
||||
'region': "The Factory (MAP12) Main"},
|
||||
'region': "The Factory (MAP12) Outdoors"},
|
||||
361152: {'name': 'The Factory (MAP12) - Berserk 2',
|
||||
'episode': 2,
|
||||
'map': 1,
|
||||
'index': 107,
|
||||
'doom_type': 2023,
|
||||
'region': "The Factory (MAP12) Main"},
|
||||
'region': "The Factory (MAP12) Indoors"},
|
||||
361153: {'name': 'The Factory (MAP12) - Yellow keycard',
|
||||
'episode': 2,
|
||||
'map': 1,
|
||||
'index': 123,
|
||||
'doom_type': 6,
|
||||
'region': "The Factory (MAP12) Main"},
|
||||
'region': "The Factory (MAP12) Indoors"},
|
||||
361154: {'name': 'The Factory (MAP12) - BFG9000',
|
||||
'episode': 2,
|
||||
'map': 1,
|
||||
@@ -954,7 +954,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 1,
|
||||
'index': 192,
|
||||
'doom_type': 82,
|
||||
'region': "The Factory (MAP12) Main"},
|
||||
'region': "The Factory (MAP12) Indoors"},
|
||||
361157: {'name': 'The Factory (MAP12) - Exit',
|
||||
'episode': 2,
|
||||
'map': 1,
|
||||
@@ -1812,7 +1812,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 1,
|
||||
'index': 70,
|
||||
'doom_type': 82,
|
||||
'region': "Nirvana (MAP21) Main"},
|
||||
'region': "Nirvana (MAP21) Start"},
|
||||
361300: {'name': 'Nirvana (MAP21) - Rocket launcher',
|
||||
'episode': 3,
|
||||
'map': 1,
|
||||
@@ -1884,7 +1884,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 2,
|
||||
'index': 28,
|
||||
'doom_type': 2001,
|
||||
'region': "The Catacombs (MAP22) Main"},
|
||||
'region': "The Catacombs (MAP22) Early"},
|
||||
361312: {'name': 'The Catacombs (MAP22) - Berserk',
|
||||
'episode': 3,
|
||||
'map': 2,
|
||||
@@ -1896,103 +1896,103 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 2,
|
||||
'index': 83,
|
||||
'doom_type': 2004,
|
||||
'region': "The Catacombs (MAP22) Main"},
|
||||
'region': "The Catacombs (MAP22) Early"},
|
||||
361314: {'name': 'The Catacombs (MAP22) - Supercharge',
|
||||
'episode': 3,
|
||||
'map': 2,
|
||||
'index': 118,
|
||||
'doom_type': 2013,
|
||||
'region': "The Catacombs (MAP22) Main"},
|
||||
'region': "The Catacombs (MAP22) Early"},
|
||||
361315: {'name': 'The Catacombs (MAP22) - Armor',
|
||||
'episode': 3,
|
||||
'map': 2,
|
||||
'index': 119,
|
||||
'doom_type': 2018,
|
||||
'region': "The Catacombs (MAP22) Main"},
|
||||
'region': "The Catacombs (MAP22) Early"},
|
||||
361316: {'name': 'The Catacombs (MAP22) - Exit',
|
||||
'episode': 3,
|
||||
'map': 2,
|
||||
'index': -1,
|
||||
'doom_type': -1,
|
||||
'region': "The Catacombs (MAP22) Red"},
|
||||
361317: {'name': 'Barrels o Fun (MAP23) - Shotgun',
|
||||
361317: {'name': "Barrels o' Fun (MAP23) - Shotgun",
|
||||
'episode': 3,
|
||||
'map': 3,
|
||||
'index': 136,
|
||||
'doom_type': 2001,
|
||||
'region': "Barrels o Fun (MAP23) Main"},
|
||||
361318: {'name': 'Barrels o Fun (MAP23) - Berserk',
|
||||
'region': "Barrels o' Fun (MAP23) Main"},
|
||||
361318: {'name': "Barrels o' Fun (MAP23) - Berserk",
|
||||
'episode': 3,
|
||||
'map': 3,
|
||||
'index': 222,
|
||||
'doom_type': 2023,
|
||||
'region': "Barrels o Fun (MAP23) Main"},
|
||||
361319: {'name': 'Barrels o Fun (MAP23) - Backpack',
|
||||
'region': "Barrels o' Fun (MAP23) Main"},
|
||||
361319: {'name': "Barrels o' Fun (MAP23) - Backpack",
|
||||
'episode': 3,
|
||||
'map': 3,
|
||||
'index': 223,
|
||||
'doom_type': 8,
|
||||
'region': "Barrels o Fun (MAP23) Main"},
|
||||
361320: {'name': 'Barrels o Fun (MAP23) - Computer area map',
|
||||
'region': "Barrels o' Fun (MAP23) Main"},
|
||||
361320: {'name': "Barrels o' Fun (MAP23) - Computer area map",
|
||||
'episode': 3,
|
||||
'map': 3,
|
||||
'index': 224,
|
||||
'doom_type': 2026,
|
||||
'region': "Barrels o Fun (MAP23) Main"},
|
||||
361321: {'name': 'Barrels o Fun (MAP23) - Armor',
|
||||
'region': "Barrels o' Fun (MAP23) Main"},
|
||||
361321: {'name': "Barrels o' Fun (MAP23) - Armor",
|
||||
'episode': 3,
|
||||
'map': 3,
|
||||
'index': 249,
|
||||
'doom_type': 2018,
|
||||
'region': "Barrels o Fun (MAP23) Main"},
|
||||
361322: {'name': 'Barrels o Fun (MAP23) - Rocket launcher',
|
||||
'region': "Barrels o' Fun (MAP23) Main"},
|
||||
361322: {'name': "Barrels o' Fun (MAP23) - Rocket launcher",
|
||||
'episode': 3,
|
||||
'map': 3,
|
||||
'index': 264,
|
||||
'doom_type': 2003,
|
||||
'region': "Barrels o Fun (MAP23) Main"},
|
||||
361323: {'name': 'Barrels o Fun (MAP23) - Megasphere',
|
||||
'region': "Barrels o' Fun (MAP23) Main"},
|
||||
361323: {'name': "Barrels o' Fun (MAP23) - Megasphere",
|
||||
'episode': 3,
|
||||
'map': 3,
|
||||
'index': 266,
|
||||
'doom_type': 83,
|
||||
'region': "Barrels o Fun (MAP23) Main"},
|
||||
361324: {'name': 'Barrels o Fun (MAP23) - Supercharge',
|
||||
'region': "Barrels o' Fun (MAP23) Main"},
|
||||
361324: {'name': "Barrels o' Fun (MAP23) - Supercharge",
|
||||
'episode': 3,
|
||||
'map': 3,
|
||||
'index': 277,
|
||||
'doom_type': 2013,
|
||||
'region': "Barrels o Fun (MAP23) Main"},
|
||||
361325: {'name': 'Barrels o Fun (MAP23) - Backpack 2',
|
||||
'region': "Barrels o' Fun (MAP23) Main"},
|
||||
361325: {'name': "Barrels o' Fun (MAP23) - Backpack 2",
|
||||
'episode': 3,
|
||||
'map': 3,
|
||||
'index': 301,
|
||||
'doom_type': 8,
|
||||
'region': "Barrels o Fun (MAP23) Main"},
|
||||
361326: {'name': 'Barrels o Fun (MAP23) - Yellow skull key',
|
||||
'region': "Barrels o' Fun (MAP23) Main"},
|
||||
361326: {'name': "Barrels o' Fun (MAP23) - Yellow skull key",
|
||||
'episode': 3,
|
||||
'map': 3,
|
||||
'index': 307,
|
||||
'doom_type': 39,
|
||||
'region': "Barrels o Fun (MAP23) Main"},
|
||||
361327: {'name': 'Barrels o Fun (MAP23) - BFG9000',
|
||||
'region': "Barrels o' Fun (MAP23) Main"},
|
||||
361327: {'name': "Barrels o' Fun (MAP23) - BFG9000",
|
||||
'episode': 3,
|
||||
'map': 3,
|
||||
'index': 342,
|
||||
'doom_type': 2006,
|
||||
'region': "Barrels o Fun (MAP23) Main"},
|
||||
361328: {'name': 'Barrels o Fun (MAP23) - Exit',
|
||||
'region': "Barrels o' Fun (MAP23) Main"},
|
||||
361328: {'name': "Barrels o' Fun (MAP23) - Exit",
|
||||
'episode': 3,
|
||||
'map': 3,
|
||||
'index': -1,
|
||||
'doom_type': -1,
|
||||
'region': "Barrels o Fun (MAP23) Yellow"},
|
||||
'region': "Barrels o' Fun (MAP23) Yellow"},
|
||||
361329: {'name': 'The Chasm (MAP24) - Plasma gun',
|
||||
'episode': 3,
|
||||
'map': 4,
|
||||
'index': 5,
|
||||
'doom_type': 2004,
|
||||
'region': "The Chasm (MAP24) Main"},
|
||||
'region': "The Chasm (MAP24) Blue"},
|
||||
361330: {'name': 'The Chasm (MAP24) - Shotgun',
|
||||
'episode': 3,
|
||||
'map': 4,
|
||||
@@ -2004,7 +2004,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 4,
|
||||
'index': 12,
|
||||
'doom_type': 2022,
|
||||
'region': "The Chasm (MAP24) Main"},
|
||||
'region': "The Chasm (MAP24) Blue"},
|
||||
361332: {'name': 'The Chasm (MAP24) - Rocket launcher',
|
||||
'episode': 3,
|
||||
'map': 4,
|
||||
@@ -2022,7 +2022,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 4,
|
||||
'index': 31,
|
||||
'doom_type': 8,
|
||||
'region': "The Chasm (MAP24) Main"},
|
||||
'region': "The Chasm (MAP24) Blue"},
|
||||
361335: {'name': 'The Chasm (MAP24) - Berserk',
|
||||
'episode': 3,
|
||||
'map': 4,
|
||||
@@ -2034,19 +2034,19 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 4,
|
||||
'index': 155,
|
||||
'doom_type': 2023,
|
||||
'region': "The Chasm (MAP24) Main"},
|
||||
'region': "The Chasm (MAP24) Blue"},
|
||||
361337: {'name': 'The Chasm (MAP24) - Armor',
|
||||
'episode': 3,
|
||||
'map': 4,
|
||||
'index': 169,
|
||||
'doom_type': 2018,
|
||||
'region': "The Chasm (MAP24) Main"},
|
||||
'region': "The Chasm (MAP24) Blue"},
|
||||
361338: {'name': 'The Chasm (MAP24) - Red keycard',
|
||||
'episode': 3,
|
||||
'map': 4,
|
||||
'index': 261,
|
||||
'doom_type': 13,
|
||||
'region': "The Chasm (MAP24) Main"},
|
||||
'region': "The Chasm (MAP24) Blue"},
|
||||
361339: {'name': 'The Chasm (MAP24) - BFG9000',
|
||||
'episode': 3,
|
||||
'map': 4,
|
||||
@@ -2064,7 +2064,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 4,
|
||||
'index': 355,
|
||||
'doom_type': 83,
|
||||
'region': "The Chasm (MAP24) Main"},
|
||||
'region': "The Chasm (MAP24) Blue"},
|
||||
361342: {'name': 'The Chasm (MAP24) - Megasphere 2',
|
||||
'episode': 3,
|
||||
'map': 4,
|
||||
@@ -2082,7 +2082,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 5,
|
||||
'index': 6,
|
||||
'doom_type': 82,
|
||||
'region': "Bloodfalls (MAP25) Main"},
|
||||
'region': "Bloodfalls (MAP25) Start"},
|
||||
361345: {'name': 'Bloodfalls (MAP25) - Partial invisibility',
|
||||
'episode': 3,
|
||||
'map': 5,
|
||||
@@ -2664,55 +2664,55 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 10,
|
||||
'index': 40,
|
||||
'doom_type': 2006,
|
||||
'region': "Icon of Sin (MAP30) Main"},
|
||||
'region': "Icon of Sin (MAP30) Start"},
|
||||
361442: {'name': 'Icon of Sin (MAP30) - Chaingun',
|
||||
'episode': 3,
|
||||
'map': 10,
|
||||
'index': 41,
|
||||
'doom_type': 2002,
|
||||
'region': "Icon of Sin (MAP30) Main"},
|
||||
'region': "Icon of Sin (MAP30) Start"},
|
||||
361443: {'name': 'Icon of Sin (MAP30) - Chainsaw',
|
||||
'episode': 3,
|
||||
'map': 10,
|
||||
'index': 42,
|
||||
'doom_type': 2005,
|
||||
'region': "Icon of Sin (MAP30) Main"},
|
||||
'region': "Icon of Sin (MAP30) Start"},
|
||||
361444: {'name': 'Icon of Sin (MAP30) - Plasma gun',
|
||||
'episode': 3,
|
||||
'map': 10,
|
||||
'index': 43,
|
||||
'doom_type': 2004,
|
||||
'region': "Icon of Sin (MAP30) Main"},
|
||||
'region': "Icon of Sin (MAP30) Start"},
|
||||
361445: {'name': 'Icon of Sin (MAP30) - Rocket launcher',
|
||||
'episode': 3,
|
||||
'map': 10,
|
||||
'index': 44,
|
||||
'doom_type': 2003,
|
||||
'region': "Icon of Sin (MAP30) Main"},
|
||||
'region': "Icon of Sin (MAP30) Start"},
|
||||
361446: {'name': 'Icon of Sin (MAP30) - Shotgun',
|
||||
'episode': 3,
|
||||
'map': 10,
|
||||
'index': 45,
|
||||
'doom_type': 2001,
|
||||
'region': "Icon of Sin (MAP30) Main"},
|
||||
'region': "Icon of Sin (MAP30) Start"},
|
||||
361447: {'name': 'Icon of Sin (MAP30) - Super Shotgun',
|
||||
'episode': 3,
|
||||
'map': 10,
|
||||
'index': 46,
|
||||
'doom_type': 82,
|
||||
'region': "Icon of Sin (MAP30) Main"},
|
||||
'region': "Icon of Sin (MAP30) Start"},
|
||||
361448: {'name': 'Icon of Sin (MAP30) - Backpack',
|
||||
'episode': 3,
|
||||
'map': 10,
|
||||
'index': 47,
|
||||
'doom_type': 8,
|
||||
'region': "Icon of Sin (MAP30) Main"},
|
||||
'region': "Icon of Sin (MAP30) Start"},
|
||||
361449: {'name': 'Icon of Sin (MAP30) - Megasphere',
|
||||
'episode': 3,
|
||||
'map': 10,
|
||||
'index': 64,
|
||||
'doom_type': 83,
|
||||
'region': "Icon of Sin (MAP30) Main"},
|
||||
'region': "Icon of Sin (MAP30) Start"},
|
||||
361450: {'name': 'Icon of Sin (MAP30) - Megasphere 2',
|
||||
'episode': 3,
|
||||
'map': 10,
|
||||
@@ -2731,179 +2731,179 @@ location_table: Dict[int, LocationDict] = {
|
||||
'index': -1,
|
||||
'doom_type': -1,
|
||||
'region': "Icon of Sin (MAP30) Main"},
|
||||
361453: {'name': 'Wolfenstein2 (MAP31) - Rocket launcher',
|
||||
361453: {'name': 'Wolfenstein (MAP31) - Rocket launcher',
|
||||
'episode': 4,
|
||||
'map': 1,
|
||||
'index': 110,
|
||||
'doom_type': 2003,
|
||||
'region': "Wolfenstein2 (MAP31) Main"},
|
||||
361454: {'name': 'Wolfenstein2 (MAP31) - Shotgun',
|
||||
'region': "Wolfenstein (MAP31) Main"},
|
||||
361454: {'name': 'Wolfenstein (MAP31) - Shotgun',
|
||||
'episode': 4,
|
||||
'map': 1,
|
||||
'index': 139,
|
||||
'doom_type': 2001,
|
||||
'region': "Wolfenstein2 (MAP31) Main"},
|
||||
361455: {'name': 'Wolfenstein2 (MAP31) - Berserk',
|
||||
'region': "Wolfenstein (MAP31) Main"},
|
||||
361455: {'name': 'Wolfenstein (MAP31) - Berserk',
|
||||
'episode': 4,
|
||||
'map': 1,
|
||||
'index': 263,
|
||||
'doom_type': 2023,
|
||||
'region': "Wolfenstein2 (MAP31) Main"},
|
||||
361456: {'name': 'Wolfenstein2 (MAP31) - Supercharge',
|
||||
'region': "Wolfenstein (MAP31) Main"},
|
||||
361456: {'name': 'Wolfenstein (MAP31) - Supercharge',
|
||||
'episode': 4,
|
||||
'map': 1,
|
||||
'index': 278,
|
||||
'doom_type': 2013,
|
||||
'region': "Wolfenstein2 (MAP31) Main"},
|
||||
361457: {'name': 'Wolfenstein2 (MAP31) - Chaingun',
|
||||
'region': "Wolfenstein (MAP31) Main"},
|
||||
361457: {'name': 'Wolfenstein (MAP31) - Chaingun',
|
||||
'episode': 4,
|
||||
'map': 1,
|
||||
'index': 305,
|
||||
'doom_type': 2002,
|
||||
'region': "Wolfenstein2 (MAP31) Main"},
|
||||
361458: {'name': 'Wolfenstein2 (MAP31) - Super Shotgun',
|
||||
'region': "Wolfenstein (MAP31) Main"},
|
||||
361458: {'name': 'Wolfenstein (MAP31) - Super Shotgun',
|
||||
'episode': 4,
|
||||
'map': 1,
|
||||
'index': 308,
|
||||
'doom_type': 82,
|
||||
'region': "Wolfenstein2 (MAP31) Main"},
|
||||
361459: {'name': 'Wolfenstein2 (MAP31) - Partial invisibility',
|
||||
'region': "Wolfenstein (MAP31) Main"},
|
||||
361459: {'name': 'Wolfenstein (MAP31) - Partial invisibility',
|
||||
'episode': 4,
|
||||
'map': 1,
|
||||
'index': 309,
|
||||
'doom_type': 2024,
|
||||
'region': "Wolfenstein2 (MAP31) Main"},
|
||||
361460: {'name': 'Wolfenstein2 (MAP31) - Megasphere',
|
||||
'region': "Wolfenstein (MAP31) Main"},
|
||||
361460: {'name': 'Wolfenstein (MAP31) - Megasphere',
|
||||
'episode': 4,
|
||||
'map': 1,
|
||||
'index': 310,
|
||||
'doom_type': 83,
|
||||
'region': "Wolfenstein2 (MAP31) Main"},
|
||||
361461: {'name': 'Wolfenstein2 (MAP31) - Backpack',
|
||||
'region': "Wolfenstein (MAP31) Main"},
|
||||
361461: {'name': 'Wolfenstein (MAP31) - Backpack',
|
||||
'episode': 4,
|
||||
'map': 1,
|
||||
'index': 311,
|
||||
'doom_type': 8,
|
||||
'region': "Wolfenstein2 (MAP31) Main"},
|
||||
361462: {'name': 'Wolfenstein2 (MAP31) - Backpack 2',
|
||||
'region': "Wolfenstein (MAP31) Main"},
|
||||
361462: {'name': 'Wolfenstein (MAP31) - Backpack 2',
|
||||
'episode': 4,
|
||||
'map': 1,
|
||||
'index': 312,
|
||||
'doom_type': 8,
|
||||
'region': "Wolfenstein2 (MAP31) Main"},
|
||||
361463: {'name': 'Wolfenstein2 (MAP31) - Backpack 3',
|
||||
'region': "Wolfenstein (MAP31) Main"},
|
||||
361463: {'name': 'Wolfenstein (MAP31) - Backpack 3',
|
||||
'episode': 4,
|
||||
'map': 1,
|
||||
'index': 313,
|
||||
'doom_type': 8,
|
||||
'region': "Wolfenstein2 (MAP31) Main"},
|
||||
361464: {'name': 'Wolfenstein2 (MAP31) - Backpack 4',
|
||||
'region': "Wolfenstein (MAP31) Main"},
|
||||
361464: {'name': 'Wolfenstein (MAP31) - Backpack 4',
|
||||
'episode': 4,
|
||||
'map': 1,
|
||||
'index': 314,
|
||||
'doom_type': 8,
|
||||
'region': "Wolfenstein2 (MAP31) Main"},
|
||||
361465: {'name': 'Wolfenstein2 (MAP31) - BFG9000',
|
||||
'region': "Wolfenstein (MAP31) Main"},
|
||||
361465: {'name': 'Wolfenstein (MAP31) - BFG9000',
|
||||
'episode': 4,
|
||||
'map': 1,
|
||||
'index': 315,
|
||||
'doom_type': 2006,
|
||||
'region': "Wolfenstein2 (MAP31) Main"},
|
||||
361466: {'name': 'Wolfenstein2 (MAP31) - Plasma gun',
|
||||
'region': "Wolfenstein (MAP31) Main"},
|
||||
361466: {'name': 'Wolfenstein (MAP31) - Plasma gun',
|
||||
'episode': 4,
|
||||
'map': 1,
|
||||
'index': 316,
|
||||
'doom_type': 2004,
|
||||
'region': "Wolfenstein2 (MAP31) Main"},
|
||||
361467: {'name': 'Wolfenstein2 (MAP31) - Exit',
|
||||
'region': "Wolfenstein (MAP31) Main"},
|
||||
361467: {'name': 'Wolfenstein (MAP31) - Exit',
|
||||
'episode': 4,
|
||||
'map': 1,
|
||||
'index': -1,
|
||||
'doom_type': -1,
|
||||
'region': "Wolfenstein2 (MAP31) Main"},
|
||||
361468: {'name': 'Grosse2 (MAP32) - Plasma gun',
|
||||
'region': "Wolfenstein (MAP31) Main"},
|
||||
361468: {'name': 'Grosse (MAP32) - Plasma gun',
|
||||
'episode': 4,
|
||||
'map': 2,
|
||||
'index': 33,
|
||||
'doom_type': 2004,
|
||||
'region': "Grosse2 (MAP32) Main"},
|
||||
361469: {'name': 'Grosse2 (MAP32) - Rocket launcher',
|
||||
'region': "Grosse (MAP32) Main"},
|
||||
361469: {'name': 'Grosse (MAP32) - Rocket launcher',
|
||||
'episode': 4,
|
||||
'map': 2,
|
||||
'index': 57,
|
||||
'doom_type': 2003,
|
||||
'region': "Grosse2 (MAP32) Main"},
|
||||
361470: {'name': 'Grosse2 (MAP32) - Invulnerability',
|
||||
'region': "Grosse (MAP32) Start"},
|
||||
361470: {'name': 'Grosse (MAP32) - Invulnerability',
|
||||
'episode': 4,
|
||||
'map': 2,
|
||||
'index': 70,
|
||||
'doom_type': 2022,
|
||||
'region': "Grosse2 (MAP32) Main"},
|
||||
361471: {'name': 'Grosse2 (MAP32) - Super Shotgun',
|
||||
'region': "Grosse (MAP32) Main"},
|
||||
361471: {'name': 'Grosse (MAP32) - Super Shotgun',
|
||||
'episode': 4,
|
||||
'map': 2,
|
||||
'index': 74,
|
||||
'doom_type': 82,
|
||||
'region': "Grosse2 (MAP32) Main"},
|
||||
361472: {'name': 'Grosse2 (MAP32) - BFG9000',
|
||||
'region': "Grosse (MAP32) Main"},
|
||||
361472: {'name': 'Grosse (MAP32) - BFG9000',
|
||||
'episode': 4,
|
||||
'map': 2,
|
||||
'index': 75,
|
||||
'doom_type': 2006,
|
||||
'region': "Grosse2 (MAP32) Main"},
|
||||
361473: {'name': 'Grosse2 (MAP32) - Megasphere',
|
||||
'region': "Grosse (MAP32) Main"},
|
||||
361473: {'name': 'Grosse (MAP32) - Megasphere',
|
||||
'episode': 4,
|
||||
'map': 2,
|
||||
'index': 78,
|
||||
'doom_type': 83,
|
||||
'region': "Grosse2 (MAP32) Main"},
|
||||
361474: {'name': 'Grosse2 (MAP32) - Chaingun',
|
||||
'region': "Grosse (MAP32) Main"},
|
||||
361474: {'name': 'Grosse (MAP32) - Chaingun',
|
||||
'episode': 4,
|
||||
'map': 2,
|
||||
'index': 79,
|
||||
'doom_type': 2002,
|
||||
'region': "Grosse2 (MAP32) Main"},
|
||||
361475: {'name': 'Grosse2 (MAP32) - Chaingun 2',
|
||||
'region': "Grosse (MAP32) Main"},
|
||||
361475: {'name': 'Grosse (MAP32) - Chaingun 2',
|
||||
'episode': 4,
|
||||
'map': 2,
|
||||
'index': 80,
|
||||
'doom_type': 2002,
|
||||
'region': "Grosse2 (MAP32) Main"},
|
||||
361476: {'name': 'Grosse2 (MAP32) - Chaingun 3',
|
||||
'region': "Grosse (MAP32) Main"},
|
||||
361476: {'name': 'Grosse (MAP32) - Chaingun 3',
|
||||
'episode': 4,
|
||||
'map': 2,
|
||||
'index': 81,
|
||||
'doom_type': 2002,
|
||||
'region': "Grosse2 (MAP32) Main"},
|
||||
361477: {'name': 'Grosse2 (MAP32) - Berserk',
|
||||
'region': "Grosse (MAP32) Main"},
|
||||
361477: {'name': 'Grosse (MAP32) - Berserk',
|
||||
'episode': 4,
|
||||
'map': 2,
|
||||
'index': 82,
|
||||
'doom_type': 2023,
|
||||
'region': "Grosse2 (MAP32) Main"},
|
||||
361478: {'name': 'Grosse2 (MAP32) - Exit',
|
||||
'region': "Grosse (MAP32) Start"},
|
||||
361478: {'name': 'Grosse (MAP32) - Exit',
|
||||
'episode': 4,
|
||||
'map': 2,
|
||||
'index': -1,
|
||||
'doom_type': -1,
|
||||
'region': "Grosse2 (MAP32) Main"},
|
||||
'region': "Grosse (MAP32) Main"},
|
||||
}
|
||||
|
||||
|
||||
location_name_groups: Dict[str, Set[str]] = {
|
||||
'Barrels o Fun (MAP23)': {
|
||||
'Barrels o Fun (MAP23) - Armor',
|
||||
'Barrels o Fun (MAP23) - BFG9000',
|
||||
'Barrels o Fun (MAP23) - Backpack',
|
||||
'Barrels o Fun (MAP23) - Backpack 2',
|
||||
'Barrels o Fun (MAP23) - Berserk',
|
||||
'Barrels o Fun (MAP23) - Computer area map',
|
||||
'Barrels o Fun (MAP23) - Exit',
|
||||
'Barrels o Fun (MAP23) - Megasphere',
|
||||
'Barrels o Fun (MAP23) - Rocket launcher',
|
||||
'Barrels o Fun (MAP23) - Shotgun',
|
||||
'Barrels o Fun (MAP23) - Supercharge',
|
||||
'Barrels o Fun (MAP23) - Yellow skull key',
|
||||
"Barrels o' Fun (MAP23)": {
|
||||
"Barrels o' Fun (MAP23) - Armor",
|
||||
"Barrels o' Fun (MAP23) - BFG9000",
|
||||
"Barrels o' Fun (MAP23) - Backpack",
|
||||
"Barrels o' Fun (MAP23) - Backpack 2",
|
||||
"Barrels o' Fun (MAP23) - Berserk",
|
||||
"Barrels o' Fun (MAP23) - Computer area map",
|
||||
"Barrels o' Fun (MAP23) - Exit",
|
||||
"Barrels o' Fun (MAP23) - Megasphere",
|
||||
"Barrels o' Fun (MAP23) - Rocket launcher",
|
||||
"Barrels o' Fun (MAP23) - Shotgun",
|
||||
"Barrels o' Fun (MAP23) - Supercharge",
|
||||
"Barrels o' Fun (MAP23) - Yellow skull key",
|
||||
},
|
||||
'Bloodfalls (MAP25)': {
|
||||
'Bloodfalls (MAP25) - Armor',
|
||||
@@ -2998,18 +2998,18 @@ location_name_groups: Dict[str, Set[str]] = {
|
||||
'Gotcha! (MAP20) - Supercharge 3',
|
||||
'Gotcha! (MAP20) - Supercharge 4',
|
||||
},
|
||||
'Grosse2 (MAP32)': {
|
||||
'Grosse2 (MAP32) - BFG9000',
|
||||
'Grosse2 (MAP32) - Berserk',
|
||||
'Grosse2 (MAP32) - Chaingun',
|
||||
'Grosse2 (MAP32) - Chaingun 2',
|
||||
'Grosse2 (MAP32) - Chaingun 3',
|
||||
'Grosse2 (MAP32) - Exit',
|
||||
'Grosse2 (MAP32) - Invulnerability',
|
||||
'Grosse2 (MAP32) - Megasphere',
|
||||
'Grosse2 (MAP32) - Plasma gun',
|
||||
'Grosse2 (MAP32) - Rocket launcher',
|
||||
'Grosse2 (MAP32) - Super Shotgun',
|
||||
'Grosse (MAP32)': {
|
||||
'Grosse (MAP32) - BFG9000',
|
||||
'Grosse (MAP32) - Berserk',
|
||||
'Grosse (MAP32) - Chaingun',
|
||||
'Grosse (MAP32) - Chaingun 2',
|
||||
'Grosse (MAP32) - Chaingun 3',
|
||||
'Grosse (MAP32) - Exit',
|
||||
'Grosse (MAP32) - Invulnerability',
|
||||
'Grosse (MAP32) - Megasphere',
|
||||
'Grosse (MAP32) - Plasma gun',
|
||||
'Grosse (MAP32) - Rocket launcher',
|
||||
'Grosse (MAP32) - Super Shotgun',
|
||||
},
|
||||
'Icon of Sin (MAP30)': {
|
||||
'Icon of Sin (MAP30) - BFG9000',
|
||||
@@ -3417,22 +3417,22 @@ location_name_groups: Dict[str, Set[str]] = {
|
||||
'Underhalls (MAP02) - Red keycard',
|
||||
'Underhalls (MAP02) - Super Shotgun',
|
||||
},
|
||||
'Wolfenstein2 (MAP31)': {
|
||||
'Wolfenstein2 (MAP31) - BFG9000',
|
||||
'Wolfenstein2 (MAP31) - Backpack',
|
||||
'Wolfenstein2 (MAP31) - Backpack 2',
|
||||
'Wolfenstein2 (MAP31) - Backpack 3',
|
||||
'Wolfenstein2 (MAP31) - Backpack 4',
|
||||
'Wolfenstein2 (MAP31) - Berserk',
|
||||
'Wolfenstein2 (MAP31) - Chaingun',
|
||||
'Wolfenstein2 (MAP31) - Exit',
|
||||
'Wolfenstein2 (MAP31) - Megasphere',
|
||||
'Wolfenstein2 (MAP31) - Partial invisibility',
|
||||
'Wolfenstein2 (MAP31) - Plasma gun',
|
||||
'Wolfenstein2 (MAP31) - Rocket launcher',
|
||||
'Wolfenstein2 (MAP31) - Shotgun',
|
||||
'Wolfenstein2 (MAP31) - Super Shotgun',
|
||||
'Wolfenstein2 (MAP31) - Supercharge',
|
||||
'Wolfenstein (MAP31)': {
|
||||
'Wolfenstein (MAP31) - BFG9000',
|
||||
'Wolfenstein (MAP31) - Backpack',
|
||||
'Wolfenstein (MAP31) - Backpack 2',
|
||||
'Wolfenstein (MAP31) - Backpack 3',
|
||||
'Wolfenstein (MAP31) - Backpack 4',
|
||||
'Wolfenstein (MAP31) - Berserk',
|
||||
'Wolfenstein (MAP31) - Chaingun',
|
||||
'Wolfenstein (MAP31) - Exit',
|
||||
'Wolfenstein (MAP31) - Megasphere',
|
||||
'Wolfenstein (MAP31) - Partial invisibility',
|
||||
'Wolfenstein (MAP31) - Plasma gun',
|
||||
'Wolfenstein (MAP31) - Rocket launcher',
|
||||
'Wolfenstein (MAP31) - Shotgun',
|
||||
'Wolfenstein (MAP31) - Super Shotgun',
|
||||
'Wolfenstein (MAP31) - Supercharge',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ map_names: List[str] = [
|
||||
'Gotcha! (MAP20)',
|
||||
'Nirvana (MAP21)',
|
||||
'The Catacombs (MAP22)',
|
||||
'Barrels o Fun (MAP23)',
|
||||
"Barrels o' Fun (MAP23)",
|
||||
'The Chasm (MAP24)',
|
||||
'Bloodfalls (MAP25)',
|
||||
'The Abandoned Mines (MAP26)',
|
||||
@@ -34,6 +34,6 @@ map_names: List[str] = [
|
||||
'The Spirit World (MAP28)',
|
||||
'The Living End (MAP29)',
|
||||
'Icon of Sin (MAP30)',
|
||||
'Wolfenstein2 (MAP31)',
|
||||
'Grosse2 (MAP32)',
|
||||
'Wolfenstein (MAP31)',
|
||||
'Grosse (MAP32)',
|
||||
]
|
||||
|
||||
@@ -84,11 +84,12 @@ regions:List[RegionDict] = [
|
||||
|
||||
# The Waste Tunnels (MAP05)
|
||||
{"name":"The Waste Tunnels (MAP05) Main",
|
||||
"connects_to_hub":True,
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[
|
||||
{"target":"The Waste Tunnels (MAP05) Red","pro":False},
|
||||
{"target":"The Waste Tunnels (MAP05) Blue","pro":False}]},
|
||||
{"target":"The Waste Tunnels (MAP05) Blue","pro":False},
|
||||
{"target":"The Waste Tunnels (MAP05) Start","pro":False}]},
|
||||
{"name":"The Waste Tunnels (MAP05) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
@@ -103,6 +104,10 @@ regions:List[RegionDict] = [
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"The Waste Tunnels (MAP05) Main","pro":False}]},
|
||||
{"name":"The Waste Tunnels (MAP05) Start",
|
||||
"connects_to_hub":True,
|
||||
"episode":1,
|
||||
"connections":[{"target":"The Waste Tunnels (MAP05) Main","pro":False}]},
|
||||
|
||||
# The Crusher (MAP06)
|
||||
{"name":"The Crusher (MAP06) Main",
|
||||
@@ -129,9 +134,13 @@ regions:List[RegionDict] = [
|
||||
|
||||
# Dead Simple (MAP07)
|
||||
{"name":"Dead Simple (MAP07) Main",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Dead Simple (MAP07) Start","pro":False}]},
|
||||
{"name":"Dead Simple (MAP07) Start",
|
||||
"connects_to_hub":True,
|
||||
"episode":1,
|
||||
"connections":[]},
|
||||
"connections":[{"target":"Dead Simple (MAP07) Main","pro":False}]},
|
||||
|
||||
# Tricks and Traps (MAP08)
|
||||
{"name":"Tricks and Traps (MAP08) Main",
|
||||
@@ -151,11 +160,12 @@ regions:List[RegionDict] = [
|
||||
|
||||
# The Pit (MAP09)
|
||||
{"name":"The Pit (MAP09) Main",
|
||||
"connects_to_hub":True,
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[
|
||||
{"target":"The Pit (MAP09) Yellow","pro":False},
|
||||
{"target":"The Pit (MAP09) Blue","pro":False}]},
|
||||
{"target":"The Pit (MAP09) Blue","pro":False},
|
||||
{"target":"The Pit (MAP09) Start","pro":False}]},
|
||||
{"name":"The Pit (MAP09) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
@@ -164,12 +174,18 @@ regions:List[RegionDict] = [
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"The Pit (MAP09) Main","pro":False}]},
|
||||
{"name":"The Pit (MAP09) Start",
|
||||
"connects_to_hub":True,
|
||||
"episode":1,
|
||||
"connections":[{"target":"The Pit (MAP09) Main","pro":False}]},
|
||||
|
||||
# Refueling Base (MAP10)
|
||||
{"name":"Refueling Base (MAP10) Main",
|
||||
"connects_to_hub":True,
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Refueling Base (MAP10) Yellow","pro":False}]},
|
||||
"connections":[
|
||||
{"target":"Refueling Base (MAP10) Yellow","pro":False},
|
||||
{"target":"Refueling Base (MAP10) Start","pro":False}]},
|
||||
{"name":"Refueling Base (MAP10) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
@@ -180,6 +196,10 @@ regions:List[RegionDict] = [
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Refueling Base (MAP10) Yellow","pro":False}]},
|
||||
{"name":"Refueling Base (MAP10) Start",
|
||||
"connects_to_hub":True,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Refueling Base (MAP10) Main","pro":False}]},
|
||||
|
||||
# Circle of Death (MAP11)
|
||||
{"name":"Circle of Death (MAP11) Main",
|
||||
@@ -187,31 +207,49 @@ regions:List[RegionDict] = [
|
||||
"episode":1,
|
||||
"connections":[
|
||||
{"target":"Circle of Death (MAP11) Blue","pro":False},
|
||||
{"target":"Circle of Death (MAP11) Red","pro":False}]},
|
||||
{"target":"Circle of Death (MAP11) Red","pro":False},
|
||||
{"target":"Circle of Death (MAP11) Ending","pro":True}]},
|
||||
{"name":"Circle of Death (MAP11) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Circle of Death (MAP11) Main","pro":False}]},
|
||||
{"name":"Circle of Death (MAP11) Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[
|
||||
{"target":"Circle of Death (MAP11) Main","pro":False},
|
||||
{"target":"Circle of Death (MAP11) Ending","pro":False}]},
|
||||
{"name":"Circle of Death (MAP11) Ending",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Circle of Death (MAP11) Main","pro":False}]},
|
||||
|
||||
# The Factory (MAP12)
|
||||
{"name":"The Factory (MAP12) Main",
|
||||
"connects_to_hub":True,
|
||||
{"name":"The Factory (MAP12) Indoors",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[
|
||||
{"target":"The Factory (MAP12) Yellow","pro":False},
|
||||
{"target":"The Factory (MAP12) Blue","pro":False}]},
|
||||
{"target":"The Factory (MAP12) Blue","pro":False},
|
||||
{"target":"The Factory (MAP12) Main","pro":False}]},
|
||||
{"name":"The Factory (MAP12) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[{"target":"The Factory (MAP12) Main","pro":False}]},
|
||||
"connections":[{"target":"The Factory (MAP12) Indoors","pro":False}]},
|
||||
{"name":"The Factory (MAP12) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[]},
|
||||
{"name":"The Factory (MAP12) Outdoors",
|
||||
"connects_to_hub":True,
|
||||
"episode":2,
|
||||
"connections":[{"target":"The Factory (MAP12) Main","pro":False}]},
|
||||
{"name":"The Factory (MAP12) Main",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[
|
||||
{"target":"The Factory (MAP12) Indoors","pro":False},
|
||||
{"target":"The Factory (MAP12) Outdoors","pro":False}]},
|
||||
|
||||
# Downtown (MAP13)
|
||||
{"name":"Downtown (MAP13) Main",
|
||||
@@ -291,7 +329,8 @@ regions:List[RegionDict] = [
|
||||
"episode":2,
|
||||
"connections":[
|
||||
{"target":"Suburbs (MAP16) Red","pro":False},
|
||||
{"target":"Suburbs (MAP16) Blue","pro":False}]},
|
||||
{"target":"Suburbs (MAP16) Blue","pro":False},
|
||||
{"target":"Suburbs (MAP16) Pro Exit","pro":True}]},
|
||||
{"name":"Suburbs (MAP16) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
@@ -299,7 +338,13 @@ regions:List[RegionDict] = [
|
||||
{"name":"Suburbs (MAP16) Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[{"target":"Suburbs (MAP16) Main","pro":False}]},
|
||||
"connections":[
|
||||
{"target":"Suburbs (MAP16) Main","pro":False},
|
||||
{"target":"Suburbs (MAP16) Pro Exit","pro":False}]},
|
||||
{"name":"Suburbs (MAP16) Pro Exit",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[{"target":"Suburbs (MAP16) Red","pro":False}]},
|
||||
|
||||
# Tenements (MAP17)
|
||||
{"name":"Tenements (MAP17) Main",
|
||||
@@ -358,7 +403,7 @@ regions:List[RegionDict] = [
|
||||
|
||||
# Nirvana (MAP21)
|
||||
{"name":"Nirvana (MAP21) Main",
|
||||
"connects_to_hub":True,
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Nirvana (MAP21) Yellow","pro":False}]},
|
||||
{"name":"Nirvana (MAP21) Yellow",
|
||||
@@ -366,19 +411,31 @@ regions:List[RegionDict] = [
|
||||
"episode":3,
|
||||
"connections":[
|
||||
{"target":"Nirvana (MAP21) Main","pro":False},
|
||||
{"target":"Nirvana (MAP21) Magenta","pro":False}]},
|
||||
{"target":"Nirvana (MAP21) Magenta","pro":False},
|
||||
{"target":"Nirvana (MAP21) Pro Magenta","pro":True}]},
|
||||
{"name":"Nirvana (MAP21) Magenta",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Nirvana (MAP21) Yellow","pro":False}]},
|
||||
"connections":[
|
||||
{"target":"Nirvana (MAP21) Yellow","pro":False},
|
||||
{"target":"Nirvana (MAP21) Pro Magenta","pro":False}]},
|
||||
{"name":"Nirvana (MAP21) Start",
|
||||
"connects_to_hub":True,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Nirvana (MAP21) Main","pro":False}]},
|
||||
{"name":"Nirvana (MAP21) Pro Magenta",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Nirvana (MAP21) Magenta","pro":False}]},
|
||||
|
||||
# The Catacombs (MAP22)
|
||||
{"name":"The Catacombs (MAP22) Main",
|
||||
"connects_to_hub":True,
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[
|
||||
{"target":"The Catacombs (MAP22) Blue","pro":False},
|
||||
{"target":"The Catacombs (MAP22) Red","pro":False}]},
|
||||
{"target":"The Catacombs (MAP22) Red","pro":False},
|
||||
{"target":"The Catacombs (MAP22) Early","pro":False}]},
|
||||
{"name":"The Catacombs (MAP22) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
@@ -387,36 +444,59 @@ regions:List[RegionDict] = [
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"The Catacombs (MAP22) Main","pro":False}]},
|
||||
|
||||
# Barrels o Fun (MAP23)
|
||||
{"name":"Barrels o Fun (MAP23) Main",
|
||||
{"name":"The Catacombs (MAP22) Early",
|
||||
"connects_to_hub":True,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Barrels o Fun (MAP23) Yellow","pro":False}]},
|
||||
{"name":"Barrels o Fun (MAP23) Yellow",
|
||||
"connections":[{"target":"The Catacombs (MAP22) Main","pro":False}]},
|
||||
|
||||
# Barrels o' Fun (MAP23)
|
||||
{"name":"Barrels o' Fun (MAP23) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Barrels o' Fun (MAP23) Yellow","pro":False}]},
|
||||
{"name":"Barrels o' Fun (MAP23) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Barrels o Fun (MAP23) Main","pro":False}]},
|
||||
"connections":[{"target":"Barrels o' Fun (MAP23) Main","pro":False}]},
|
||||
|
||||
# The Chasm (MAP24)
|
||||
{"name":"The Chasm (MAP24) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":3,
|
||||
"connections":[{"target":"The Chasm (MAP24) Red","pro":False}]},
|
||||
"connections":[
|
||||
{"target":"The Chasm (MAP24) Blue","pro":False},
|
||||
{"target":"The Chasm (MAP24) Blue Pro","pro":True}]},
|
||||
{"name":"The Chasm (MAP24) Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"The Chasm (MAP24) Main","pro":False}]},
|
||||
"connections":[{"target":"The Chasm (MAP24) Blue","pro":False}]},
|
||||
{"name":"The Chasm (MAP24) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[
|
||||
{"target":"The Chasm (MAP24) Red","pro":False},
|
||||
{"target":"The Chasm (MAP24) Main","pro":False},
|
||||
{"target":"The Chasm (MAP24) Blue Pro","pro":False}]},
|
||||
{"name":"The Chasm (MAP24) Blue Pro",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"The Chasm (MAP24) Blue","pro":False}]},
|
||||
|
||||
# Bloodfalls (MAP25)
|
||||
{"name":"Bloodfalls (MAP25) Main",
|
||||
"connects_to_hub":True,
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Bloodfalls (MAP25) Blue","pro":False}]},
|
||||
"connections":[
|
||||
{"target":"Bloodfalls (MAP25) Blue","pro":False},
|
||||
{"target":"Bloodfalls (MAP25) Start","pro":False}]},
|
||||
{"name":"Bloodfalls (MAP25) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Bloodfalls (MAP25) Main","pro":False}]},
|
||||
{"name":"Bloodfalls (MAP25) Start",
|
||||
"connects_to_hub":True,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Bloodfalls (MAP25) Main","pro":False}]},
|
||||
|
||||
# The Abandoned Mines (MAP26)
|
||||
{"name":"The Abandoned Mines (MAP26) Main",
|
||||
@@ -484,19 +564,27 @@ regions:List[RegionDict] = [
|
||||
|
||||
# Icon of Sin (MAP30)
|
||||
{"name":"Icon of Sin (MAP30) Main",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Icon of Sin (MAP30) Start","pro":False}]},
|
||||
{"name":"Icon of Sin (MAP30) Start",
|
||||
"connects_to_hub":True,
|
||||
"episode":3,
|
||||
"connections":[]},
|
||||
"connections":[{"target":"Icon of Sin (MAP30) Main","pro":False}]},
|
||||
|
||||
# Wolfenstein2 (MAP31)
|
||||
{"name":"Wolfenstein2 (MAP31) Main",
|
||||
# Wolfenstein (MAP31)
|
||||
{"name":"Wolfenstein (MAP31) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":4,
|
||||
"connections":[]},
|
||||
|
||||
# Grosse2 (MAP32)
|
||||
{"name":"Grosse2 (MAP32) Main",
|
||||
# Grosse (MAP32)
|
||||
{"name":"Grosse (MAP32) Main",
|
||||
"connects_to_hub":False,
|
||||
"episode":4,
|
||||
"connections":[{"target":"Grosse (MAP32) Start","pro":False}]},
|
||||
{"name":"Grosse (MAP32) Start",
|
||||
"connects_to_hub":True,
|
||||
"episode":4,
|
||||
"connections":[]},
|
||||
"connections":[{"target":"Grosse (MAP32) Main","pro":False}]},
|
||||
]
|
||||
|
||||
@@ -53,14 +53,6 @@ def set_episode1_rules(player, multiworld, pro):
|
||||
state.has("The Focus (MAP04) - Red keycard", player, 1))
|
||||
|
||||
# The Waste Tunnels (MAP05)
|
||||
set_rule(multiworld.get_entrance("Hub -> The Waste Tunnels (MAP05) Main", player), lambda state:
|
||||
(state.has("The Waste Tunnels (MAP05)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
set_rule(multiworld.get_entrance("The Waste Tunnels (MAP05) Main -> The Waste Tunnels (MAP05) Red", player), lambda state:
|
||||
state.has("The Waste Tunnels (MAP05) - Red keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Waste Tunnels (MAP05) Main -> The Waste Tunnels (MAP05) Blue", player), lambda state:
|
||||
@@ -71,18 +63,22 @@ def set_episode1_rules(player, multiworld, pro):
|
||||
state.has("The Waste Tunnels (MAP05) - Blue keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Waste Tunnels (MAP05) Yellow -> The Waste Tunnels (MAP05) Blue", player), lambda state:
|
||||
state.has("The Waste Tunnels (MAP05) - Yellow keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("Hub -> The Waste Tunnels (MAP05) Start", player), lambda state:
|
||||
state.has("The Waste Tunnels (MAP05)", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Waste Tunnels (MAP05) Start -> The Waste Tunnels (MAP05) Main", player), lambda state:
|
||||
(state.has("Shotgun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and (state.has("Chaingun", player, 1) or
|
||||
state.has("Plasma gun", player, 1)))
|
||||
|
||||
# The Crusher (MAP06)
|
||||
set_rule(multiworld.get_entrance("Hub -> The Crusher (MAP06) Main", player), lambda state:
|
||||
(state.has("The Crusher (MAP06)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
state.has("Shotgun", player, 1)) and
|
||||
(state.has("Plasma gun", player, 1) or
|
||||
state.has("Chaingun", player, 1)))
|
||||
set_rule(multiworld.get_entrance("The Crusher (MAP06) Main -> The Crusher (MAP06) Blue", player), lambda state:
|
||||
state.has("The Crusher (MAP06) - Blue keycard", player, 1))
|
||||
state.has("The Crusher (MAP06) - Blue keycard", player, 1) and
|
||||
state.has("Super Shotgun", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Crusher (MAP06) Blue -> The Crusher (MAP06) Red", player), lambda state:
|
||||
state.has("The Crusher (MAP06) - Red keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Crusher (MAP06) Blue -> The Crusher (MAP06) Main", player), lambda state:
|
||||
@@ -95,14 +91,14 @@ def set_episode1_rules(player, multiworld, pro):
|
||||
state.has("The Crusher (MAP06) - Red keycard", player, 1))
|
||||
|
||||
# Dead Simple (MAP07)
|
||||
set_rule(multiworld.get_entrance("Hub -> Dead Simple (MAP07) Main", player), lambda state:
|
||||
(state.has("Dead Simple (MAP07)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
set_rule(multiworld.get_entrance("Hub -> Dead Simple (MAP07) Start", player), lambda state:
|
||||
state.has("Dead Simple (MAP07)", player, 1))
|
||||
set_rule(multiworld.get_entrance("Dead Simple (MAP07) Start -> Dead Simple (MAP07) Main", player), lambda state:
|
||||
(state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Super Shotgun", player, 1)) and (state.has("BFG9000", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
state.has("Rocket launcher", player, 1)))
|
||||
|
||||
# Tricks and Traps (MAP08)
|
||||
set_rule(multiworld.get_entrance("Hub -> Tricks and Traps (MAP08) Main", player), lambda state:
|
||||
@@ -119,34 +115,34 @@ def set_episode1_rules(player, multiworld, pro):
|
||||
state.has("Tricks and Traps (MAP08) - Yellow skull key", player, 1))
|
||||
|
||||
# The Pit (MAP09)
|
||||
set_rule(multiworld.get_entrance("Hub -> The Pit (MAP09) Main", player), lambda state:
|
||||
(state.has("The Pit (MAP09)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
set_rule(multiworld.get_entrance("The Pit (MAP09) Main -> The Pit (MAP09) Yellow", player), lambda state:
|
||||
state.has("The Pit (MAP09) - Yellow keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Pit (MAP09) Main -> The Pit (MAP09) Blue", player), lambda state:
|
||||
state.has("The Pit (MAP09) - Blue keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Pit (MAP09) Yellow -> The Pit (MAP09) Main", player), lambda state:
|
||||
state.has("The Pit (MAP09) - Yellow keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("Hub -> The Pit (MAP09) Start", player), lambda state:
|
||||
state.has("The Pit (MAP09)", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Pit (MAP09) Start -> The Pit (MAP09) Main", player), lambda state:
|
||||
(state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and (state.has("BFG9000", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("Rocket launcher", player, 1)))
|
||||
|
||||
# Refueling Base (MAP10)
|
||||
set_rule(multiworld.get_entrance("Hub -> Refueling Base (MAP10) Main", player), lambda state:
|
||||
(state.has("Refueling Base (MAP10)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
set_rule(multiworld.get_entrance("Refueling Base (MAP10) Main -> Refueling Base (MAP10) Yellow", player), lambda state:
|
||||
state.has("Refueling Base (MAP10) - Yellow keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("Refueling Base (MAP10) Yellow -> Refueling Base (MAP10) Yellow Blue", player), lambda state:
|
||||
state.has("Refueling Base (MAP10) - Blue keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("Hub -> Refueling Base (MAP10) Start", player), lambda state:
|
||||
state.has("Refueling Base (MAP10)", player, 1))
|
||||
set_rule(multiworld.get_entrance("Refueling Base (MAP10) Start -> Refueling Base (MAP10) Main", player), lambda state:
|
||||
(state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and (state.has("BFG9000", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("Rocket launcher", player, 1)))
|
||||
|
||||
# Circle of Death (MAP11)
|
||||
set_rule(multiworld.get_entrance("Hub -> Circle of Death (MAP11) Main", player), lambda state:
|
||||
@@ -165,18 +161,19 @@ def set_episode1_rules(player, multiworld, pro):
|
||||
|
||||
def set_episode2_rules(player, multiworld, pro):
|
||||
# The Factory (MAP12)
|
||||
set_rule(multiworld.get_entrance("Hub -> The Factory (MAP12) Main", player), lambda state:
|
||||
(state.has("The Factory (MAP12)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
set_rule(multiworld.get_entrance("The Factory (MAP12) Main -> The Factory (MAP12) Yellow", player), lambda state:
|
||||
set_rule(multiworld.get_entrance("The Factory (MAP12) Indoors -> The Factory (MAP12) Yellow", player), lambda state:
|
||||
state.has("The Factory (MAP12) - Yellow keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Factory (MAP12) Main -> The Factory (MAP12) Blue", player), lambda state:
|
||||
set_rule(multiworld.get_entrance("The Factory (MAP12) Indoors -> The Factory (MAP12) Blue", player), lambda state:
|
||||
state.has("The Factory (MAP12) - Blue keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("Hub -> The Factory (MAP12) Outdoors", player), lambda state:
|
||||
state.has("The Factory (MAP12)", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Factory (MAP12) Outdoors -> The Factory (MAP12) Main", player), lambda state:
|
||||
state.has("Super Shotgun", player, 1) or
|
||||
state.has("Plasma gun", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Factory (MAP12) Main -> The Factory (MAP12) Indoors", player), lambda state:
|
||||
(state.has("Super Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1)) and (state.has("BFG9000", player, 1) or
|
||||
state.has("Plasma gun", player, 1)))
|
||||
|
||||
# Downtown (MAP13)
|
||||
set_rule(multiworld.get_entrance("Hub -> Downtown (MAP13) Main", player), lambda state:
|
||||
@@ -307,54 +304,56 @@ def set_episode2_rules(player, multiworld, pro):
|
||||
|
||||
def set_episode3_rules(player, multiworld, pro):
|
||||
# Nirvana (MAP21)
|
||||
set_rule(multiworld.get_entrance("Hub -> Nirvana (MAP21) Main", player), lambda state:
|
||||
(state.has("Nirvana (MAP21)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
set_rule(multiworld.get_entrance("Nirvana (MAP21) Main -> Nirvana (MAP21) Yellow", player), lambda state:
|
||||
state.has("Nirvana (MAP21) - Yellow skull key", player, 1))
|
||||
(state.has("Super Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Nirvana (MAP21) - Yellow skull key", player, 1)) and (state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
set_rule(multiworld.get_entrance("Nirvana (MAP21) Yellow -> Nirvana (MAP21) Main", player), lambda state:
|
||||
state.has("Nirvana (MAP21) - Yellow skull key", player, 1))
|
||||
set_rule(multiworld.get_entrance("Nirvana (MAP21) Yellow -> Nirvana (MAP21) Magenta", player), lambda state:
|
||||
state.has("Nirvana (MAP21) - Red skull key", player, 1) and
|
||||
state.has("Nirvana (MAP21) - Blue skull key", player, 1))
|
||||
set_rule(multiworld.get_entrance("Nirvana (MAP21) Magenta -> Nirvana (MAP21) Yellow", player), lambda state:
|
||||
state.has("Nirvana (MAP21) - Red skull key", player, 1) and
|
||||
state.has("Nirvana (MAP21) - Blue skull key", player, 1))
|
||||
set_rule(multiworld.get_entrance("Hub -> Nirvana (MAP21) Start", player), lambda state:
|
||||
state.has("Nirvana (MAP21)", player, 1))
|
||||
set_rule(multiworld.get_entrance("Nirvana (MAP21) Start -> Nirvana (MAP21) Main", player), lambda state:
|
||||
state.has("Super Shotgun", player, 1) or
|
||||
state.has("Plasma gun", player, 1))
|
||||
set_rule(multiworld.get_entrance("Nirvana (MAP21) Pro Magenta -> Nirvana (MAP21) Magenta", player), lambda state:
|
||||
state.has("Nirvana (MAP21) - Red skull key", player, 1))
|
||||
|
||||
# The Catacombs (MAP22)
|
||||
set_rule(multiworld.get_entrance("Hub -> The Catacombs (MAP22) Main", player), lambda state:
|
||||
(state.has("The Catacombs (MAP22)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("BFG9000", player, 1) or
|
||||
state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1)))
|
||||
set_rule(multiworld.get_entrance("The Catacombs (MAP22) Main -> The Catacombs (MAP22) Blue", player), lambda state:
|
||||
state.has("The Catacombs (MAP22) - Blue skull key", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Catacombs (MAP22) Main -> The Catacombs (MAP22) Red", player), lambda state:
|
||||
state.has("The Catacombs (MAP22) - Red skull key", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Catacombs (MAP22) Red -> The Catacombs (MAP22) Main", player), lambda state:
|
||||
state.has("The Catacombs (MAP22) - Red skull key", player, 1))
|
||||
set_rule(multiworld.get_entrance("Hub -> The Catacombs (MAP22) Early", player), lambda state:
|
||||
(state.has("The Catacombs (MAP22)", player, 1)) and
|
||||
(state.has("Shotgun", player, 1) or
|
||||
state.has("Super Shotgun", player, 1) or
|
||||
state.has("Plasma gun", player, 1)))
|
||||
set_rule(multiworld.get_entrance("The Catacombs (MAP22) Early -> The Catacombs (MAP22) Main", player), lambda state:
|
||||
(state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and (state.has("BFG9000", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("Rocket launcher", player, 1)))
|
||||
|
||||
# Barrels o Fun (MAP23)
|
||||
set_rule(multiworld.get_entrance("Hub -> Barrels o Fun (MAP23) Main", player), lambda state:
|
||||
(state.has("Barrels o Fun (MAP23)", player, 1) and
|
||||
# Barrels o' Fun (MAP23)
|
||||
set_rule(multiworld.get_entrance("Hub -> Barrels o' Fun (MAP23) Main", player), lambda state:
|
||||
(state.has("Barrels o' Fun (MAP23)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
set_rule(multiworld.get_entrance("Barrels o Fun (MAP23) Main -> Barrels o Fun (MAP23) Yellow", player), lambda state:
|
||||
state.has("Barrels o Fun (MAP23) - Yellow skull key", player, 1))
|
||||
set_rule(multiworld.get_entrance("Barrels o Fun (MAP23) Yellow -> Barrels o Fun (MAP23) Main", player), lambda state:
|
||||
state.has("Barrels o Fun (MAP23) - Yellow skull key", player, 1))
|
||||
set_rule(multiworld.get_entrance("Barrels o' Fun (MAP23) Main -> Barrels o' Fun (MAP23) Yellow", player), lambda state:
|
||||
state.has("Barrels o' Fun (MAP23) - Yellow skull key", player, 1))
|
||||
set_rule(multiworld.get_entrance("Barrels o' Fun (MAP23) Yellow -> Barrels o' Fun (MAP23) Main", player), lambda state:
|
||||
state.has("Barrels o' Fun (MAP23) - Yellow skull key", player, 1))
|
||||
|
||||
# The Chasm (MAP24)
|
||||
set_rule(multiworld.get_entrance("Hub -> The Chasm (MAP24) Main", player), lambda state:
|
||||
@@ -365,24 +364,26 @@ def set_episode3_rules(player, multiworld, pro):
|
||||
state.has("Plasma gun", player, 1) and
|
||||
state.has("BFG9000", player, 1) and
|
||||
state.has("Super Shotgun", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Chasm (MAP24) Main -> The Chasm (MAP24) Red", player), lambda state:
|
||||
set_rule(multiworld.get_entrance("The Chasm (MAP24) Main -> The Chasm (MAP24) Blue", player), lambda state:
|
||||
state.has("The Chasm (MAP24) - Blue keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Chasm (MAP24) Red -> The Chasm (MAP24) Blue", player), lambda state:
|
||||
state.has("The Chasm (MAP24) - Red keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Chasm (MAP24) Red -> The Chasm (MAP24) Main", player), lambda state:
|
||||
set_rule(multiworld.get_entrance("The Chasm (MAP24) Blue -> The Chasm (MAP24) Red", player), lambda state:
|
||||
state.has("The Chasm (MAP24) - Red keycard", player, 1))
|
||||
|
||||
# Bloodfalls (MAP25)
|
||||
set_rule(multiworld.get_entrance("Hub -> Bloodfalls (MAP25) Main", player), lambda state:
|
||||
state.has("Bloodfalls (MAP25)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Rocket launcher", player, 1) and
|
||||
state.has("Plasma gun", player, 1) and
|
||||
state.has("BFG9000", player, 1) and
|
||||
state.has("Super Shotgun", player, 1))
|
||||
set_rule(multiworld.get_entrance("Bloodfalls (MAP25) Main -> Bloodfalls (MAP25) Blue", player), lambda state:
|
||||
state.has("Bloodfalls (MAP25) - Blue skull key", player, 1))
|
||||
(state.has("Bloodfalls (MAP25) - Blue skull key", player, 1)) and (state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
set_rule(multiworld.get_entrance("Bloodfalls (MAP25) Blue -> Bloodfalls (MAP25) Main", player), lambda state:
|
||||
state.has("Bloodfalls (MAP25) - Blue skull key", player, 1))
|
||||
set_rule(multiworld.get_entrance("Hub -> Bloodfalls (MAP25) Start", player), lambda state:
|
||||
state.has("Bloodfalls (MAP25)", player, 1))
|
||||
set_rule(multiworld.get_entrance("Bloodfalls (MAP25) Start -> Bloodfalls (MAP25) Main", player), lambda state:
|
||||
state.has("Super Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Shotgun", player, 1))
|
||||
|
||||
# The Abandoned Mines (MAP26)
|
||||
set_rule(multiworld.get_entrance("Hub -> The Abandoned Mines (MAP26) Main", player), lambda state:
|
||||
@@ -451,36 +452,34 @@ def set_episode3_rules(player, multiworld, pro):
|
||||
state.has("Super Shotgun", player, 1))
|
||||
|
||||
# Icon of Sin (MAP30)
|
||||
set_rule(multiworld.get_entrance("Hub -> Icon of Sin (MAP30) Main", player), lambda state:
|
||||
state.has("Icon of Sin (MAP30)", player, 1) and
|
||||
state.has("Rocket launcher", player, 1) and
|
||||
set_rule(multiworld.get_entrance("Hub -> Icon of Sin (MAP30) Start", player), lambda state:
|
||||
state.has("Icon of Sin (MAP30)", player, 1))
|
||||
set_rule(multiworld.get_entrance("Icon of Sin (MAP30) Start -> Icon of Sin (MAP30) Main", player), lambda state:
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Rocket launcher", player, 1) and
|
||||
state.has("Plasma gun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("BFG9000", player, 1) and
|
||||
state.has("Super Shotgun", player, 1))
|
||||
|
||||
|
||||
def set_episode4_rules(player, multiworld, pro):
|
||||
# Wolfenstein2 (MAP31)
|
||||
set_rule(multiworld.get_entrance("Hub -> Wolfenstein2 (MAP31) Main", player), lambda state:
|
||||
(state.has("Wolfenstein2 (MAP31)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
# Wolfenstein (MAP31)
|
||||
set_rule(multiworld.get_entrance("Hub -> Wolfenstein (MAP31) Main", player), lambda state:
|
||||
(state.has("Wolfenstein (MAP31)", player, 1) and
|
||||
state.has("Chaingun", player, 1)) and
|
||||
(state.has("Shotgun", player, 1) or
|
||||
state.has("Super Shotgun", player, 1)))
|
||||
|
||||
# Grosse2 (MAP32)
|
||||
set_rule(multiworld.get_entrance("Hub -> Grosse2 (MAP32) Main", player), lambda state:
|
||||
(state.has("Grosse2 (MAP32)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
# Grosse (MAP32)
|
||||
set_rule(multiworld.get_entrance("Hub -> Grosse (MAP32) Start", player), lambda state:
|
||||
state.has("Grosse (MAP32)", player, 1))
|
||||
set_rule(multiworld.get_entrance("Grosse (MAP32) Start -> Grosse (MAP32) Main", player), lambda state:
|
||||
(state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Super Shotgun", player, 1)) and (state.has("BFG9000", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
state.has("Rocket launcher", player, 1)))
|
||||
|
||||
|
||||
def set_rules(doom_ii_world: "DOOM2World", included_episodes, pro):
|
||||
|
||||
@@ -51,11 +51,11 @@ class DOOM2World(World):
|
||||
location_name_to_id = {data["name"]: loc_id for loc_id, data in Locations.location_table.items()}
|
||||
location_name_groups = Locations.location_name_groups
|
||||
|
||||
starting_level_for_episode: List[str] = [
|
||||
"Entryway (MAP01)",
|
||||
"The Factory (MAP12)",
|
||||
"Nirvana (MAP21)"
|
||||
]
|
||||
starting_level_for_episode: Dict[int, str] = {
|
||||
1: "Entryway (MAP01)",
|
||||
2: "The Factory (MAP12)",
|
||||
3: "Nirvana (MAP21)"
|
||||
}
|
||||
|
||||
# Item ratio that scales depending on episode count. These are the ratio for 3 episode. In DOOM1.
|
||||
# The ratio have been tweaked seem, and feel good.
|
||||
@@ -77,6 +77,7 @@ class DOOM2World(World):
|
||||
def __init__(self, multiworld: MultiWorld, player: int):
|
||||
self.included_episodes = [1, 1, 1, 0]
|
||||
self.location_count = 0
|
||||
self.starting_levels = []
|
||||
|
||||
super().__init__(multiworld, player)
|
||||
|
||||
@@ -95,6 +96,14 @@ class DOOM2World(World):
|
||||
if self.get_episode_count() == 0:
|
||||
self.included_episodes[0] = 1
|
||||
|
||||
self.starting_levels = [level_name for (episode, level_name) in self.starting_level_for_episode.items()
|
||||
if self.included_episodes[episode - 1]]
|
||||
|
||||
# If soloing MAP21-MAP30, we need to mark a weapon as early to help generation succeed
|
||||
if self.get_episode_count() == 1 and self.included_episodes[2]:
|
||||
early_weapon = self.random.choice(["Super Shotgun", "Plasma gun"])
|
||||
self.multiworld.early_items[self.player][early_weapon] = 1
|
||||
|
||||
def create_regions(self):
|
||||
pro = self.options.pro.value
|
||||
|
||||
@@ -193,7 +202,7 @@ class DOOM2World(World):
|
||||
if item["episode"] != -1 and not self.included_episodes[item["episode"] - 1]:
|
||||
continue
|
||||
|
||||
count = item["count"] if item["name"] not in self.starting_level_for_episode else item["count"] - 1
|
||||
count = item["count"] if item["name"] not in self.starting_levels else item["count"] - 1
|
||||
itempool += [self.create_item(item["name"]) for _ in range(count)]
|
||||
|
||||
# Backpack(s) based on options
|
||||
@@ -224,9 +233,8 @@ class DOOM2World(World):
|
||||
self.location_count -= 1
|
||||
|
||||
# Give starting levels right away
|
||||
for i in range(len(self.starting_level_for_episode)):
|
||||
if self.included_episodes[i]:
|
||||
self.multiworld.push_precollected(self.create_item(self.starting_level_for_episode[i]))
|
||||
for map_name in self.starting_levels:
|
||||
self.multiworld.push_precollected(self.create_item(map_name))
|
||||
|
||||
# Give Computer area maps if option selected
|
||||
if start_with_computer_area_maps:
|
||||
|
||||
@@ -255,7 +255,8 @@ async def game_watcher(ctx: FactorioContext):
|
||||
if "DeathLink" in ctx.tags:
|
||||
async_start(ctx.send_death())
|
||||
if ctx.energy_link_increment:
|
||||
in_world_bridges = data["energy_bridges"]
|
||||
# 1 + quality * 0.3 for each bridge
|
||||
in_world_bridges: float = data["energy_bridges"]
|
||||
if in_world_bridges:
|
||||
in_world_energy = data["energy"]
|
||||
if in_world_energy < (ctx.energy_link_increment * in_world_bridges):
|
||||
@@ -263,14 +264,14 @@ async def game_watcher(ctx: FactorioContext):
|
||||
ctx.last_deplete = time.time()
|
||||
async_start(ctx.send_msgs([{
|
||||
"cmd": "Set", "key": ctx.energylink_key, "operations":
|
||||
[{"operation": "add", "value": -ctx.energy_link_increment * in_world_bridges},
|
||||
[{"operation": "add", "value": int(-ctx.energy_link_increment * in_world_bridges)},
|
||||
{"operation": "max", "value": 0}],
|
||||
"last_deplete": ctx.last_deplete
|
||||
}]))
|
||||
# Above Capacity - (len(Bridges) * ENERGY_INCREMENT)
|
||||
elif in_world_energy > (in_world_bridges * ctx.energy_link_increment * 5) - \
|
||||
ctx.energy_link_increment * in_world_bridges:
|
||||
value = ctx.energy_link_increment * in_world_bridges
|
||||
value = int(ctx.energy_link_increment * in_world_bridges)
|
||||
async_start(ctx.send_msgs([{
|
||||
"cmd": "Set", "key": ctx.energylink_key, "operations":
|
||||
[{"operation": "add", "value": value}]
|
||||
@@ -406,7 +407,7 @@ async def get_info(ctx: FactorioContext, rcon_client: factorio_rcon.RCONClient):
|
||||
ctx.auth = info["slot_name"]
|
||||
ctx.seed_name = info["seed_name"]
|
||||
death_link = info["death_link"]
|
||||
ctx.energy_link_increment = info.get("energy_link", 0)
|
||||
ctx.energy_link_increment = int(info.get("energy_link", 0))
|
||||
logger.debug(f"Energy Link Increment: {ctx.energy_link_increment}")
|
||||
if ctx.energy_link_increment and ctx.ui:
|
||||
ctx.ui.enable_energy_link()
|
||||
|
||||
@@ -8,17 +8,20 @@ from schema import Schema, Optional, And, Or, SchemaError
|
||||
from Options import Choice, OptionDict, OptionSet, DefaultOnToggle, Range, DeathLink, Toggle, \
|
||||
StartInventoryPool, PerGameCommonOptions, OptionGroup
|
||||
|
||||
|
||||
# schema helpers
|
||||
class FloatRange:
|
||||
def __init__(self, low, high):
|
||||
self._low = low
|
||||
self._high = high
|
||||
|
||||
def validate(self, value):
|
||||
def validate(self, value) -> float:
|
||||
if not isinstance(value, (float, int)):
|
||||
raise SchemaError(f"should be instance of float or int, but was {value!r}")
|
||||
if not self._low <= value <= self._high:
|
||||
raise SchemaError(f"{value} is not between {self._low} and {self._high}")
|
||||
return float(value)
|
||||
|
||||
|
||||
LuaBool = Or(bool, And(int, lambda n: n in (0, 1)))
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ class Factorio(World):
|
||||
item_name_groups = {
|
||||
"Progressive": set(progressive_tech_table.keys()),
|
||||
}
|
||||
required_client_version = (0, 5, 1)
|
||||
required_client_version = (0, 6, 0)
|
||||
if Utils.version_tuple < required_client_version:
|
||||
raise Exception(f"Update Archipelago to use this world ({game}).")
|
||||
ordered_science_packs: typing.List[str] = MaxSciencePack.get_ordered_science_packs()
|
||||
|
||||
@@ -3,7 +3,6 @@ import settings
|
||||
import base64
|
||||
import threading
|
||||
import requests
|
||||
import yaml
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
from BaseClasses import Tutorial
|
||||
from .Regions import create_regions, location_table, set_rules, stage_set_rules, rooms, non_dead_end_crest_rooms,\
|
||||
@@ -44,6 +43,7 @@ class FFMQWebWorld(WebWorld):
|
||||
)
|
||||
|
||||
tutorials = [setup_en, setup_fr]
|
||||
game_info_languages = ["en", "fr"]
|
||||
|
||||
|
||||
class FFMQWorld(World):
|
||||
@@ -134,7 +134,7 @@ class FFMQWorld(World):
|
||||
errors.append([api_url, err])
|
||||
else:
|
||||
if response.ok:
|
||||
world.rooms = rooms_data[query] = yaml.load(response.text, yaml.Loader)
|
||||
world.rooms = rooms_data[query] = Utils.parse_yaml(response.text)
|
||||
break
|
||||
else:
|
||||
api_urls.remove(api_url)
|
||||
|
||||
0
worlds/ffmq/data/__init__.py
Normal file
0
worlds/ffmq/data/__init__.py
Normal file
@@ -514,19 +514,19 @@ item_table: Dict[int, ItemDict] = {
|
||||
'map': 7},
|
||||
370259: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'The Aquifier (E3M9) - Blue key',
|
||||
'name': 'The Aquifer (E3M9) - Blue key',
|
||||
'doom_type': 79,
|
||||
'episode': 3,
|
||||
'map': 9},
|
||||
370260: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'The Aquifier (E3M9) - Green key',
|
||||
'name': 'The Aquifer (E3M9) - Green key',
|
||||
'doom_type': 73,
|
||||
'episode': 3,
|
||||
'map': 9},
|
||||
370261: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'The Aquifier (E3M9) - Yellow key',
|
||||
'name': 'The Aquifer (E3M9) - Yellow key',
|
||||
'doom_type': 80,
|
||||
'episode': 3,
|
||||
'map': 9},
|
||||
@@ -1234,37 +1234,37 @@ item_table: Dict[int, ItemDict] = {
|
||||
'map': 7},
|
||||
370475: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': "D'Sparil'S Keep (E3M8)",
|
||||
'name': "D'Sparil's Keep (E3M8)",
|
||||
'doom_type': -1,
|
||||
'episode': 3,
|
||||
'map': 8},
|
||||
370476: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': "D'Sparil'S Keep (E3M8) - Complete",
|
||||
'name': "D'Sparil's Keep (E3M8) - Complete",
|
||||
'doom_type': -2,
|
||||
'episode': 3,
|
||||
'map': 8},
|
||||
370477: {'classification': ItemClassification.filler,
|
||||
'count': 1,
|
||||
'name': "D'Sparil'S Keep (E3M8) - Map Scroll",
|
||||
'name': "D'Sparil's Keep (E3M8) - Map Scroll",
|
||||
'doom_type': 35,
|
||||
'episode': 3,
|
||||
'map': 8},
|
||||
370478: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'The Aquifier (E3M9)',
|
||||
'name': 'The Aquifer (E3M9)',
|
||||
'doom_type': -1,
|
||||
'episode': 3,
|
||||
'map': 9},
|
||||
370479: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'The Aquifier (E3M9) - Complete',
|
||||
'name': 'The Aquifer (E3M9) - Complete',
|
||||
'doom_type': -2,
|
||||
'episode': 3,
|
||||
'map': 9},
|
||||
370480: {'classification': ItemClassification.filler,
|
||||
'count': 1,
|
||||
'name': 'The Aquifier (E3M9) - Map Scroll',
|
||||
'name': 'The Aquifer (E3M9) - Map Scroll',
|
||||
'doom_type': 35,
|
||||
'episode': 3,
|
||||
'map': 9},
|
||||
@@ -1635,8 +1635,8 @@ item_name_groups: Dict[str, Set[str]] = {
|
||||
'Ammos': {'Crystal Geode', 'Energy Orb', 'Greater Runes', 'Inferno Orb', 'Pile of Mace Spheres', 'Quiver of Ethereal Arrows', },
|
||||
'Armors': {'Enchanted Shield', 'Silver Shield', },
|
||||
'Artifacts': {'Chaos Device', 'Morph Ovum', 'Mystic Urn', 'Quartz Flask', 'Ring of Invincibility', 'Shadowsphere', 'Timebomb of the Ancients', 'Tome of Power', 'Torch', },
|
||||
'Keys': {'Ambulatory (E4M3) - Blue key', 'Ambulatory (E4M3) - Green key', 'Ambulatory (E4M3) - Yellow key', 'Blockhouse (E4M2) - Blue key', 'Blockhouse (E4M2) - Green key', 'Blockhouse (E4M2) - Yellow key', 'Catafalque (E4M1) - Green key', 'Catafalque (E4M1) - Yellow key', 'Colonnade (E5M6) - Blue key', 'Colonnade (E5M6) - Green key', 'Colonnade (E5M6) - Yellow key', 'Courtyard (E5M4) - Blue key', 'Courtyard (E5M4) - Green key', 'Courtyard (E5M4) - Yellow key', 'Foetid Manse (E5M7) - Blue key', 'Foetid Manse (E5M7) - Green key', 'Foetid Manse (E5M7) - Yellow key', 'Great Stair (E4M5) - Blue key', 'Great Stair (E4M5) - Green key', 'Great Stair (E4M5) - Yellow key', 'Halls of the Apostate (E4M6) - Blue key', 'Halls of the Apostate (E4M6) - Green key', 'Halls of the Apostate (E4M6) - Yellow key', 'Hydratyr (E5M5) - Blue key', 'Hydratyr (E5M5) - Green key', 'Hydratyr (E5M5) - Yellow key', 'Mausoleum (E4M9) - Yellow key', 'Ochre Cliffs (E5M1) - Blue key', 'Ochre Cliffs (E5M1) - Green key', 'Ochre Cliffs (E5M1) - Yellow key', 'Quay (E5M3) - Blue key', 'Quay (E5M3) - Green key', 'Quay (E5M3) - Yellow key', 'Ramparts of Perdition (E4M7) - Blue key', 'Ramparts of Perdition (E4M7) - Green key', 'Ramparts of Perdition (E4M7) - Yellow key', 'Rapids (E5M2) - Green key', 'Rapids (E5M2) - Yellow key', 'Shattered Bridge (E4M8) - Yellow key', "Skein of D'Sparil (E5M9) - Blue key", "Skein of D'Sparil (E5M9) - Green key", "Skein of D'Sparil (E5M9) - Yellow key", 'The Aquifier (E3M9) - Blue key', 'The Aquifier (E3M9) - Green key', 'The Aquifier (E3M9) - Yellow key', 'The Azure Fortress (E3M4) - Green key', 'The Azure Fortress (E3M4) - Yellow key', 'The Catacombs (E2M5) - Blue key', 'The Catacombs (E2M5) - Green key', 'The Catacombs (E2M5) - Yellow key', 'The Cathedral (E1M6) - Green key', 'The Cathedral (E1M6) - Yellow key', 'The Cesspool (E3M2) - Blue key', 'The Cesspool (E3M2) - Green key', 'The Cesspool (E3M2) - Yellow key', 'The Chasm (E3M7) - Blue key', 'The Chasm (E3M7) - Green key', 'The Chasm (E3M7) - Yellow key', 'The Citadel (E1M5) - Blue key', 'The Citadel (E1M5) - Green key', 'The Citadel (E1M5) - Yellow key', 'The Confluence (E3M3) - Blue key', 'The Confluence (E3M3) - Green key', 'The Confluence (E3M3) - Yellow key', 'The Crater (E2M1) - Green key', 'The Crater (E2M1) - Yellow key', 'The Crypts (E1M7) - Blue key', 'The Crypts (E1M7) - Green key', 'The Crypts (E1M7) - Yellow key', 'The Docks (E1M1) - Yellow key', 'The Dungeons (E1M2) - Blue key', 'The Dungeons (E1M2) - Green key', 'The Dungeons (E1M2) - Yellow key', 'The Gatehouse (E1M3) - Green key', 'The Gatehouse (E1M3) - Yellow key', 'The Glacier (E2M9) - Blue key', 'The Glacier (E2M9) - Green key', 'The Glacier (E2M9) - Yellow key', 'The Graveyard (E1M9) - Blue key', 'The Graveyard (E1M9) - Green key', 'The Graveyard (E1M9) - Yellow key', 'The Great Hall (E2M7) - Blue key', 'The Great Hall (E2M7) - Green key', 'The Great Hall (E2M7) - Yellow key', 'The Guard Tower (E1M4) - Green key', 'The Guard Tower (E1M4) - Yellow key', 'The Halls of Fear (E3M6) - Blue key', 'The Halls of Fear (E3M6) - Green key', 'The Halls of Fear (E3M6) - Yellow key', 'The Ice Grotto (E2M4) - Blue key', 'The Ice Grotto (E2M4) - Green key', 'The Ice Grotto (E2M4) - Yellow key', 'The Labyrinth (E2M6) - Blue key', 'The Labyrinth (E2M6) - Green key', 'The Labyrinth (E2M6) - Yellow key', 'The Lava Pits (E2M2) - Green key', 'The Lava Pits (E2M2) - Yellow key', 'The Ophidian Lair (E3M5) - Green key', 'The Ophidian Lair (E3M5) - Yellow key', 'The River of Fire (E2M3) - Blue key', 'The River of Fire (E2M3) - Green key', 'The River of Fire (E2M3) - Yellow key', 'The Storehouse (E3M1) - Green key', 'The Storehouse (E3M1) - Yellow key', },
|
||||
'Levels': {'Ambulatory (E4M3)', 'Blockhouse (E4M2)', 'Catafalque (E4M1)', 'Colonnade (E5M6)', 'Courtyard (E5M4)', "D'Sparil'S Keep (E3M8)", 'Field of Judgement (E5M8)', 'Foetid Manse (E5M7)', 'Great Stair (E4M5)', 'Halls of the Apostate (E4M6)', "Hell's Maw (E1M8)", 'Hydratyr (E5M5)', 'Mausoleum (E4M9)', 'Ochre Cliffs (E5M1)', 'Quay (E5M3)', 'Ramparts of Perdition (E4M7)', 'Rapids (E5M2)', 'Sepulcher (E4M4)', 'Shattered Bridge (E4M8)', "Skein of D'Sparil (E5M9)", 'The Aquifier (E3M9)', 'The Azure Fortress (E3M4)', 'The Catacombs (E2M5)', 'The Cathedral (E1M6)', 'The Cesspool (E3M2)', 'The Chasm (E3M7)', 'The Citadel (E1M5)', 'The Confluence (E3M3)', 'The Crater (E2M1)', 'The Crypts (E1M7)', 'The Docks (E1M1)', 'The Dungeons (E1M2)', 'The Gatehouse (E1M3)', 'The Glacier (E2M9)', 'The Graveyard (E1M9)', 'The Great Hall (E2M7)', 'The Guard Tower (E1M4)', 'The Halls of Fear (E3M6)', 'The Ice Grotto (E2M4)', 'The Labyrinth (E2M6)', 'The Lava Pits (E2M2)', 'The Ophidian Lair (E3M5)', 'The Portals of Chaos (E2M8)', 'The River of Fire (E2M3)', 'The Storehouse (E3M1)', },
|
||||
'Map Scrolls': {'Ambulatory (E4M3) - Map Scroll', 'Blockhouse (E4M2) - Map Scroll', 'Catafalque (E4M1) - Map Scroll', 'Colonnade (E5M6) - Map Scroll', 'Courtyard (E5M4) - Map Scroll', "D'Sparil'S Keep (E3M8) - Map Scroll", 'Field of Judgement (E5M8) - Map Scroll', 'Foetid Manse (E5M7) - Map Scroll', 'Great Stair (E4M5) - Map Scroll', 'Halls of the Apostate (E4M6) - Map Scroll', "Hell's Maw (E1M8) - Map Scroll", 'Hydratyr (E5M5) - Map Scroll', 'Mausoleum (E4M9) - Map Scroll', 'Ochre Cliffs (E5M1) - Map Scroll', 'Quay (E5M3) - Map Scroll', 'Ramparts of Perdition (E4M7) - Map Scroll', 'Rapids (E5M2) - Map Scroll', 'Sepulcher (E4M4) - Map Scroll', 'Shattered Bridge (E4M8) - Map Scroll', "Skein of D'Sparil (E5M9) - Map Scroll", 'The Aquifier (E3M9) - Map Scroll', 'The Azure Fortress (E3M4) - Map Scroll', 'The Catacombs (E2M5) - Map Scroll', 'The Cathedral (E1M6) - Map Scroll', 'The Cesspool (E3M2) - Map Scroll', 'The Chasm (E3M7) - Map Scroll', 'The Citadel (E1M5) - Map Scroll', 'The Confluence (E3M3) - Map Scroll', 'The Crater (E2M1) - Map Scroll', 'The Crypts (E1M7) - Map Scroll', 'The Docks (E1M1) - Map Scroll', 'The Dungeons (E1M2) - Map Scroll', 'The Gatehouse (E1M3) - Map Scroll', 'The Glacier (E2M9) - Map Scroll', 'The Graveyard (E1M9) - Map Scroll', 'The Great Hall (E2M7) - Map Scroll', 'The Guard Tower (E1M4) - Map Scroll', 'The Halls of Fear (E3M6) - Map Scroll', 'The Ice Grotto (E2M4) - Map Scroll', 'The Labyrinth (E2M6) - Map Scroll', 'The Lava Pits (E2M2) - Map Scroll', 'The Ophidian Lair (E3M5) - Map Scroll', 'The Portals of Chaos (E2M8) - Map Scroll', 'The River of Fire (E2M3) - Map Scroll', 'The Storehouse (E3M1) - Map Scroll', },
|
||||
'Keys': {'Ambulatory (E4M3) - Blue key', 'Ambulatory (E4M3) - Green key', 'Ambulatory (E4M3) - Yellow key', 'Blockhouse (E4M2) - Blue key', 'Blockhouse (E4M2) - Green key', 'Blockhouse (E4M2) - Yellow key', 'Catafalque (E4M1) - Green key', 'Catafalque (E4M1) - Yellow key', 'Colonnade (E5M6) - Blue key', 'Colonnade (E5M6) - Green key', 'Colonnade (E5M6) - Yellow key', 'Courtyard (E5M4) - Blue key', 'Courtyard (E5M4) - Green key', 'Courtyard (E5M4) - Yellow key', 'Foetid Manse (E5M7) - Blue key', 'Foetid Manse (E5M7) - Green key', 'Foetid Manse (E5M7) - Yellow key', 'Great Stair (E4M5) - Blue key', 'Great Stair (E4M5) - Green key', 'Great Stair (E4M5) - Yellow key', 'Halls of the Apostate (E4M6) - Blue key', 'Halls of the Apostate (E4M6) - Green key', 'Halls of the Apostate (E4M6) - Yellow key', 'Hydratyr (E5M5) - Blue key', 'Hydratyr (E5M5) - Green key', 'Hydratyr (E5M5) - Yellow key', 'Mausoleum (E4M9) - Yellow key', 'Ochre Cliffs (E5M1) - Blue key', 'Ochre Cliffs (E5M1) - Green key', 'Ochre Cliffs (E5M1) - Yellow key', 'Quay (E5M3) - Blue key', 'Quay (E5M3) - Green key', 'Quay (E5M3) - Yellow key', 'Ramparts of Perdition (E4M7) - Blue key', 'Ramparts of Perdition (E4M7) - Green key', 'Ramparts of Perdition (E4M7) - Yellow key', 'Rapids (E5M2) - Green key', 'Rapids (E5M2) - Yellow key', 'Shattered Bridge (E4M8) - Yellow key', "Skein of D'Sparil (E5M9) - Blue key", "Skein of D'Sparil (E5M9) - Green key", "Skein of D'Sparil (E5M9) - Yellow key", 'The Aquifer (E3M9) - Blue key', 'The Aquifer (E3M9) - Green key', 'The Aquifer (E3M9) - Yellow key', 'The Azure Fortress (E3M4) - Green key', 'The Azure Fortress (E3M4) - Yellow key', 'The Catacombs (E2M5) - Blue key', 'The Catacombs (E2M5) - Green key', 'The Catacombs (E2M5) - Yellow key', 'The Cathedral (E1M6) - Green key', 'The Cathedral (E1M6) - Yellow key', 'The Cesspool (E3M2) - Blue key', 'The Cesspool (E3M2) - Green key', 'The Cesspool (E3M2) - Yellow key', 'The Chasm (E3M7) - Blue key', 'The Chasm (E3M7) - Green key', 'The Chasm (E3M7) - Yellow key', 'The Citadel (E1M5) - Blue key', 'The Citadel (E1M5) - Green key', 'The Citadel (E1M5) - Yellow key', 'The Confluence (E3M3) - Blue key', 'The Confluence (E3M3) - Green key', 'The Confluence (E3M3) - Yellow key', 'The Crater (E2M1) - Green key', 'The Crater (E2M1) - Yellow key', 'The Crypts (E1M7) - Blue key', 'The Crypts (E1M7) - Green key', 'The Crypts (E1M7) - Yellow key', 'The Docks (E1M1) - Yellow key', 'The Dungeons (E1M2) - Blue key', 'The Dungeons (E1M2) - Green key', 'The Dungeons (E1M2) - Yellow key', 'The Gatehouse (E1M3) - Green key', 'The Gatehouse (E1M3) - Yellow key', 'The Glacier (E2M9) - Blue key', 'The Glacier (E2M9) - Green key', 'The Glacier (E2M9) - Yellow key', 'The Graveyard (E1M9) - Blue key', 'The Graveyard (E1M9) - Green key', 'The Graveyard (E1M9) - Yellow key', 'The Great Hall (E2M7) - Blue key', 'The Great Hall (E2M7) - Green key', 'The Great Hall (E2M7) - Yellow key', 'The Guard Tower (E1M4) - Green key', 'The Guard Tower (E1M4) - Yellow key', 'The Halls of Fear (E3M6) - Blue key', 'The Halls of Fear (E3M6) - Green key', 'The Halls of Fear (E3M6) - Yellow key', 'The Ice Grotto (E2M4) - Blue key', 'The Ice Grotto (E2M4) - Green key', 'The Ice Grotto (E2M4) - Yellow key', 'The Labyrinth (E2M6) - Blue key', 'The Labyrinth (E2M6) - Green key', 'The Labyrinth (E2M6) - Yellow key', 'The Lava Pits (E2M2) - Green key', 'The Lava Pits (E2M2) - Yellow key', 'The Ophidian Lair (E3M5) - Green key', 'The Ophidian Lair (E3M5) - Yellow key', 'The River of Fire (E2M3) - Blue key', 'The River of Fire (E2M3) - Green key', 'The River of Fire (E2M3) - Yellow key', 'The Storehouse (E3M1) - Green key', 'The Storehouse (E3M1) - Yellow key', },
|
||||
'Levels': {'Ambulatory (E4M3)', 'Blockhouse (E4M2)', 'Catafalque (E4M1)', 'Colonnade (E5M6)', 'Courtyard (E5M4)', "D'Sparil's Keep (E3M8)", 'Field of Judgement (E5M8)', 'Foetid Manse (E5M7)', 'Great Stair (E4M5)', 'Halls of the Apostate (E4M6)', "Hell's Maw (E1M8)", 'Hydratyr (E5M5)', 'Mausoleum (E4M9)', 'Ochre Cliffs (E5M1)', 'Quay (E5M3)', 'Ramparts of Perdition (E4M7)', 'Rapids (E5M2)', 'Sepulcher (E4M4)', 'Shattered Bridge (E4M8)', "Skein of D'Sparil (E5M9)", 'The Aquifer (E3M9)', 'The Azure Fortress (E3M4)', 'The Catacombs (E2M5)', 'The Cathedral (E1M6)', 'The Cesspool (E3M2)', 'The Chasm (E3M7)', 'The Citadel (E1M5)', 'The Confluence (E3M3)', 'The Crater (E2M1)', 'The Crypts (E1M7)', 'The Docks (E1M1)', 'The Dungeons (E1M2)', 'The Gatehouse (E1M3)', 'The Glacier (E2M9)', 'The Graveyard (E1M9)', 'The Great Hall (E2M7)', 'The Guard Tower (E1M4)', 'The Halls of Fear (E3M6)', 'The Ice Grotto (E2M4)', 'The Labyrinth (E2M6)', 'The Lava Pits (E2M2)', 'The Ophidian Lair (E3M5)', 'The Portals of Chaos (E2M8)', 'The River of Fire (E2M3)', 'The Storehouse (E3M1)', },
|
||||
'Map Scrolls': {'Ambulatory (E4M3) - Map Scroll', 'Blockhouse (E4M2) - Map Scroll', 'Catafalque (E4M1) - Map Scroll', 'Colonnade (E5M6) - Map Scroll', 'Courtyard (E5M4) - Map Scroll', "D'Sparil's Keep (E3M8) - Map Scroll", 'Field of Judgement (E5M8) - Map Scroll', 'Foetid Manse (E5M7) - Map Scroll', 'Great Stair (E4M5) - Map Scroll', 'Halls of the Apostate (E4M6) - Map Scroll', "Hell's Maw (E1M8) - Map Scroll", 'Hydratyr (E5M5) - Map Scroll', 'Mausoleum (E4M9) - Map Scroll', 'Ochre Cliffs (E5M1) - Map Scroll', 'Quay (E5M3) - Map Scroll', 'Ramparts of Perdition (E4M7) - Map Scroll', 'Rapids (E5M2) - Map Scroll', 'Sepulcher (E4M4) - Map Scroll', 'Shattered Bridge (E4M8) - Map Scroll', "Skein of D'Sparil (E5M9) - Map Scroll", 'The Aquifer (E3M9) - Map Scroll', 'The Azure Fortress (E3M4) - Map Scroll', 'The Catacombs (E2M5) - Map Scroll', 'The Cathedral (E1M6) - Map Scroll', 'The Cesspool (E3M2) - Map Scroll', 'The Chasm (E3M7) - Map Scroll', 'The Citadel (E1M5) - Map Scroll', 'The Confluence (E3M3) - Map Scroll', 'The Crater (E2M1) - Map Scroll', 'The Crypts (E1M7) - Map Scroll', 'The Docks (E1M1) - Map Scroll', 'The Dungeons (E1M2) - Map Scroll', 'The Gatehouse (E1M3) - Map Scroll', 'The Glacier (E2M9) - Map Scroll', 'The Graveyard (E1M9) - Map Scroll', 'The Great Hall (E2M7) - Map Scroll', 'The Guard Tower (E1M4) - Map Scroll', 'The Halls of Fear (E3M6) - Map Scroll', 'The Ice Grotto (E2M4) - Map Scroll', 'The Labyrinth (E2M6) - Map Scroll', 'The Lava Pits (E2M2) - Map Scroll', 'The Ophidian Lair (E3M5) - Map Scroll', 'The Portals of Chaos (E2M8) - Map Scroll', 'The River of Fire (E2M3) - Map Scroll', 'The Storehouse (E3M1) - Map Scroll', },
|
||||
'Weapons': {'Dragon Claw', 'Ethereal Crossbow', 'Firemace', 'Gauntlets of the Necromancer', 'Hellstaff', 'Phoenix Rod', },
|
||||
}
|
||||
|
||||
@@ -3633,300 +3633,300 @@ location_table: Dict[int, LocationDict] = {
|
||||
'index': -1,
|
||||
'doom_type': -1,
|
||||
'region': "The Chasm (E3M7) Blue"},
|
||||
371517: {'name': "D'Sparil'S Keep (E3M8) - Phoenix Rod",
|
||||
371517: {'name': "D'Sparil's Keep (E3M8) - Phoenix Rod",
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 8,
|
||||
'index': 55,
|
||||
'doom_type': 2003,
|
||||
'region': "D'Sparil'S Keep (E3M8) Main"},
|
||||
371518: {'name': "D'Sparil'S Keep (E3M8) - Ethereal Crossbow",
|
||||
'region': "D'Sparil's Keep (E3M8) Main"},
|
||||
371518: {'name': "D'Sparil's Keep (E3M8) - Ethereal Crossbow",
|
||||
'episode': 3,
|
||||
'check_sanity': True,
|
||||
'map': 8,
|
||||
'index': 56,
|
||||
'doom_type': 2001,
|
||||
'region': "D'Sparil'S Keep (E3M8) Main"},
|
||||
371519: {'name': "D'Sparil'S Keep (E3M8) - Dragon Claw",
|
||||
'region': "D'Sparil's Keep (E3M8) Main"},
|
||||
371519: {'name': "D'Sparil's Keep (E3M8) - Dragon Claw",
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 8,
|
||||
'index': 57,
|
||||
'doom_type': 53,
|
||||
'region': "D'Sparil'S Keep (E3M8) Main"},
|
||||
371520: {'name': "D'Sparil'S Keep (E3M8) - Gauntlets of the Necromancer",
|
||||
'region': "D'Sparil's Keep (E3M8) Main"},
|
||||
371520: {'name': "D'Sparil's Keep (E3M8) - Gauntlets of the Necromancer",
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 8,
|
||||
'index': 58,
|
||||
'doom_type': 2005,
|
||||
'region': "D'Sparil'S Keep (E3M8) Main"},
|
||||
371521: {'name': "D'Sparil'S Keep (E3M8) - Hellstaff",
|
||||
'region': "D'Sparil's Keep (E3M8) Main"},
|
||||
371521: {'name': "D'Sparil's Keep (E3M8) - Hellstaff",
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 8,
|
||||
'index': 59,
|
||||
'doom_type': 2004,
|
||||
'region': "D'Sparil'S Keep (E3M8) Main"},
|
||||
371522: {'name': "D'Sparil'S Keep (E3M8) - Bag of Holding",
|
||||
'region': "D'Sparil's Keep (E3M8) Main"},
|
||||
371522: {'name': "D'Sparil's Keep (E3M8) - Bag of Holding",
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 8,
|
||||
'index': 63,
|
||||
'doom_type': 8,
|
||||
'region': "D'Sparil'S Keep (E3M8) Main"},
|
||||
371523: {'name': "D'Sparil'S Keep (E3M8) - Mystic Urn",
|
||||
'region': "D'Sparil's Keep (E3M8) Main"},
|
||||
371523: {'name': "D'Sparil's Keep (E3M8) - Mystic Urn",
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 8,
|
||||
'index': 64,
|
||||
'doom_type': 32,
|
||||
'region': "D'Sparil'S Keep (E3M8) Main"},
|
||||
371524: {'name': "D'Sparil'S Keep (E3M8) - Ring of Invincibility",
|
||||
'region': "D'Sparil's Keep (E3M8) Main"},
|
||||
371524: {'name': "D'Sparil's Keep (E3M8) - Ring of Invincibility",
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 8,
|
||||
'index': 65,
|
||||
'doom_type': 84,
|
||||
'region': "D'Sparil'S Keep (E3M8) Main"},
|
||||
371525: {'name': "D'Sparil'S Keep (E3M8) - Shadowsphere",
|
||||
'region': "D'Sparil's Keep (E3M8) Main"},
|
||||
371525: {'name': "D'Sparil's Keep (E3M8) - Shadowsphere",
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 8,
|
||||
'index': 66,
|
||||
'doom_type': 75,
|
||||
'region': "D'Sparil'S Keep (E3M8) Main"},
|
||||
371526: {'name': "D'Sparil'S Keep (E3M8) - Silver Shield",
|
||||
'region': "D'Sparil's Keep (E3M8) Main"},
|
||||
371526: {'name': "D'Sparil's Keep (E3M8) - Silver Shield",
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 8,
|
||||
'index': 67,
|
||||
'doom_type': 85,
|
||||
'region': "D'Sparil'S Keep (E3M8) Main"},
|
||||
371527: {'name': "D'Sparil'S Keep (E3M8) - Enchanted Shield",
|
||||
'region': "D'Sparil's Keep (E3M8) Main"},
|
||||
371527: {'name': "D'Sparil's Keep (E3M8) - Enchanted Shield",
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 8,
|
||||
'index': 68,
|
||||
'doom_type': 31,
|
||||
'region': "D'Sparil'S Keep (E3M8) Main"},
|
||||
371528: {'name': "D'Sparil'S Keep (E3M8) - Tome of Power",
|
||||
'region': "D'Sparil's Keep (E3M8) Main"},
|
||||
371528: {'name': "D'Sparil's Keep (E3M8) - Tome of Power",
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 8,
|
||||
'index': 69,
|
||||
'doom_type': 86,
|
||||
'region': "D'Sparil'S Keep (E3M8) Main"},
|
||||
371529: {'name': "D'Sparil'S Keep (E3M8) - Tome of Power 2",
|
||||
'region': "D'Sparil's Keep (E3M8) Main"},
|
||||
371529: {'name': "D'Sparil's Keep (E3M8) - Tome of Power 2",
|
||||
'episode': 3,
|
||||
'check_sanity': True,
|
||||
'map': 8,
|
||||
'index': 70,
|
||||
'doom_type': 86,
|
||||
'region': "D'Sparil'S Keep (E3M8) Main"},
|
||||
371530: {'name': "D'Sparil'S Keep (E3M8) - Chaos Device",
|
||||
'region': "D'Sparil's Keep (E3M8) Main"},
|
||||
371530: {'name': "D'Sparil's Keep (E3M8) - Chaos Device",
|
||||
'episode': 3,
|
||||
'check_sanity': True,
|
||||
'map': 8,
|
||||
'index': 71,
|
||||
'doom_type': 36,
|
||||
'region': "D'Sparil'S Keep (E3M8) Main"},
|
||||
371531: {'name': "D'Sparil'S Keep (E3M8) - Tome of Power 3",
|
||||
'region': "D'Sparil's Keep (E3M8) Main"},
|
||||
371531: {'name': "D'Sparil's Keep (E3M8) - Tome of Power 3",
|
||||
'episode': 3,
|
||||
'check_sanity': True,
|
||||
'map': 8,
|
||||
'index': 245,
|
||||
'doom_type': 86,
|
||||
'region': "D'Sparil'S Keep (E3M8) Main"},
|
||||
371532: {'name': "D'Sparil'S Keep (E3M8) - Exit",
|
||||
'region': "D'Sparil's Keep (E3M8) Main"},
|
||||
371532: {'name': "D'Sparil's Keep (E3M8) - Exit",
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 8,
|
||||
'index': -1,
|
||||
'doom_type': -1,
|
||||
'region': "D'Sparil'S Keep (E3M8) Main"},
|
||||
371533: {'name': 'The Aquifier (E3M9) - Blue key',
|
||||
'region': "D'Sparil's Keep (E3M8) Main"},
|
||||
371533: {'name': 'The Aquifer (E3M9) - Blue key',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 12,
|
||||
'doom_type': 79,
|
||||
'region': "The Aquifier (E3M9) Green"},
|
||||
371534: {'name': 'The Aquifier (E3M9) - Green key',
|
||||
'region': "The Aquifer (E3M9) Green"},
|
||||
371534: {'name': 'The Aquifer (E3M9) - Green key',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 13,
|
||||
'doom_type': 73,
|
||||
'region': "The Aquifier (E3M9) Yellow"},
|
||||
371535: {'name': 'The Aquifier (E3M9) - Yellow key',
|
||||
'region': "The Aquifer (E3M9) Yellow"},
|
||||
371535: {'name': 'The Aquifer (E3M9) - Yellow key',
|
||||
'episode': 3,
|
||||
'check_sanity': True,
|
||||
'map': 9,
|
||||
'index': 14,
|
||||
'doom_type': 80,
|
||||
'region': "The Aquifier (E3M9) Main"},
|
||||
371536: {'name': 'The Aquifier (E3M9) - Ethereal Crossbow',
|
||||
'region': "The Aquifer (E3M9) Main"},
|
||||
371536: {'name': 'The Aquifer (E3M9) - Ethereal Crossbow',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 141,
|
||||
'doom_type': 2001,
|
||||
'region': "The Aquifier (E3M9) Main"},
|
||||
371537: {'name': 'The Aquifier (E3M9) - Phoenix Rod',
|
||||
'region': "The Aquifer (E3M9) Main"},
|
||||
371537: {'name': 'The Aquifer (E3M9) - Phoenix Rod',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 142,
|
||||
'doom_type': 2003,
|
||||
'region': "The Aquifier (E3M9) Yellow"},
|
||||
371538: {'name': 'The Aquifier (E3M9) - Dragon Claw',
|
||||
'region': "The Aquifer (E3M9) Yellow"},
|
||||
371538: {'name': 'The Aquifer (E3M9) - Dragon Claw',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 143,
|
||||
'doom_type': 53,
|
||||
'region': "The Aquifier (E3M9) Green"},
|
||||
371539: {'name': 'The Aquifier (E3M9) - Hellstaff',
|
||||
'region': "The Aquifer (E3M9) Green"},
|
||||
371539: {'name': 'The Aquifer (E3M9) - Hellstaff',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 144,
|
||||
'doom_type': 2004,
|
||||
'region': "The Aquifier (E3M9) Green"},
|
||||
371540: {'name': 'The Aquifier (E3M9) - Gauntlets of the Necromancer',
|
||||
'region': "The Aquifer (E3M9) Green"},
|
||||
371540: {'name': 'The Aquifer (E3M9) - Gauntlets of the Necromancer',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 145,
|
||||
'doom_type': 2005,
|
||||
'region': "The Aquifier (E3M9) Green"},
|
||||
371541: {'name': 'The Aquifier (E3M9) - Ring of Invincibility',
|
||||
'region': "The Aquifer (E3M9) Green"},
|
||||
371541: {'name': 'The Aquifer (E3M9) - Ring of Invincibility',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 148,
|
||||
'doom_type': 84,
|
||||
'region': "The Aquifier (E3M9) Yellow"},
|
||||
371542: {'name': 'The Aquifier (E3M9) - Mystic Urn',
|
||||
'region': "The Aquifer (E3M9) Yellow"},
|
||||
371542: {'name': 'The Aquifer (E3M9) - Mystic Urn',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 149,
|
||||
'doom_type': 32,
|
||||
'region': "The Aquifier (E3M9) Green"},
|
||||
371543: {'name': 'The Aquifier (E3M9) - Silver Shield',
|
||||
'region': "The Aquifer (E3M9) Green"},
|
||||
371543: {'name': 'The Aquifer (E3M9) - Silver Shield',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 151,
|
||||
'doom_type': 85,
|
||||
'region': "The Aquifier (E3M9) Main"},
|
||||
371544: {'name': 'The Aquifier (E3M9) - Tome of Power',
|
||||
'region': "The Aquifer (E3M9) Main"},
|
||||
371544: {'name': 'The Aquifer (E3M9) - Tome of Power',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 152,
|
||||
'doom_type': 86,
|
||||
'region': "The Aquifier (E3M9) Main"},
|
||||
371545: {'name': 'The Aquifier (E3M9) - Bag of Holding',
|
||||
'region': "The Aquifer (E3M9) Main"},
|
||||
371545: {'name': 'The Aquifer (E3M9) - Bag of Holding',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 153,
|
||||
'doom_type': 8,
|
||||
'region': "The Aquifier (E3M9) Yellow"},
|
||||
371546: {'name': 'The Aquifier (E3M9) - Morph Ovum',
|
||||
'region': "The Aquifer (E3M9) Yellow"},
|
||||
371546: {'name': 'The Aquifer (E3M9) - Morph Ovum',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 154,
|
||||
'doom_type': 30,
|
||||
'region': "The Aquifier (E3M9) Green"},
|
||||
371547: {'name': 'The Aquifier (E3M9) - Map Scroll',
|
||||
'region': "The Aquifer (E3M9) Green"},
|
||||
371547: {'name': 'The Aquifer (E3M9) - Map Scroll',
|
||||
'episode': 3,
|
||||
'check_sanity': True,
|
||||
'map': 9,
|
||||
'index': 155,
|
||||
'doom_type': 35,
|
||||
'region': "The Aquifier (E3M9) Green"},
|
||||
371548: {'name': 'The Aquifier (E3M9) - Chaos Device',
|
||||
'region': "The Aquifer (E3M9) Green"},
|
||||
371548: {'name': 'The Aquifer (E3M9) - Chaos Device',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 156,
|
||||
'doom_type': 36,
|
||||
'region': "The Aquifier (E3M9) Yellow"},
|
||||
371549: {'name': 'The Aquifier (E3M9) - Enchanted Shield',
|
||||
'region': "The Aquifer (E3M9) Yellow"},
|
||||
371549: {'name': 'The Aquifer (E3M9) - Enchanted Shield',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 157,
|
||||
'doom_type': 31,
|
||||
'region': "The Aquifier (E3M9) Green"},
|
||||
371550: {'name': 'The Aquifier (E3M9) - Tome of Power 2',
|
||||
'region': "The Aquifer (E3M9) Green"},
|
||||
371550: {'name': 'The Aquifer (E3M9) - Tome of Power 2',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 158,
|
||||
'doom_type': 86,
|
||||
'region': "The Aquifier (E3M9) Green"},
|
||||
371551: {'name': 'The Aquifier (E3M9) - Torch',
|
||||
'region': "The Aquifer (E3M9) Green"},
|
||||
371551: {'name': 'The Aquifer (E3M9) - Torch',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 159,
|
||||
'doom_type': 33,
|
||||
'region': "The Aquifier (E3M9) Main"},
|
||||
371552: {'name': 'The Aquifier (E3M9) - Shadowsphere',
|
||||
'region': "The Aquifer (E3M9) Main"},
|
||||
371552: {'name': 'The Aquifer (E3M9) - Shadowsphere',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 160,
|
||||
'doom_type': 75,
|
||||
'region': "The Aquifier (E3M9) Green"},
|
||||
371553: {'name': 'The Aquifier (E3M9) - Silver Shield 2',
|
||||
'region': "The Aquifer (E3M9) Green"},
|
||||
371553: {'name': 'The Aquifer (E3M9) - Silver Shield 2',
|
||||
'episode': 3,
|
||||
'check_sanity': True,
|
||||
'map': 9,
|
||||
'index': 374,
|
||||
'doom_type': 85,
|
||||
'region': "The Aquifier (E3M9) Green"},
|
||||
371554: {'name': 'The Aquifier (E3M9) - Firemace',
|
||||
'region': "The Aquifer (E3M9) Green"},
|
||||
371554: {'name': 'The Aquifer (E3M9) - Firemace',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 478,
|
||||
'doom_type': 2002,
|
||||
'region': "The Aquifier (E3M9) Green"},
|
||||
371555: {'name': 'The Aquifier (E3M9) - Firemace 2',
|
||||
'region': "The Aquifer (E3M9) Green"},
|
||||
371555: {'name': 'The Aquifer (E3M9) - Firemace 2',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 526,
|
||||
'doom_type': 2002,
|
||||
'region': "The Aquifier (E3M9) Green"},
|
||||
371556: {'name': 'The Aquifier (E3M9) - Firemace 3',
|
||||
'region': "The Aquifer (E3M9) Green"},
|
||||
371556: {'name': 'The Aquifer (E3M9) - Firemace 3',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 527,
|
||||
'doom_type': 2002,
|
||||
'region': "The Aquifier (E3M9) Green"},
|
||||
371557: {'name': 'The Aquifier (E3M9) - Firemace 4',
|
||||
'region': "The Aquifer (E3M9) Green"},
|
||||
371557: {'name': 'The Aquifer (E3M9) - Firemace 4',
|
||||
'episode': 3,
|
||||
'check_sanity': True,
|
||||
'map': 9,
|
||||
'index': 528,
|
||||
'doom_type': 2002,
|
||||
'region': "The Aquifier (E3M9) Yellow"},
|
||||
371558: {'name': 'The Aquifier (E3M9) - Exit',
|
||||
'region': "The Aquifer (E3M9) Yellow"},
|
||||
371558: {'name': 'The Aquifer (E3M9) - Exit',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': -1,
|
||||
'doom_type': -1,
|
||||
'region': "The Aquifier (E3M9) Blue"},
|
||||
'region': "The Aquifer (E3M9) Blue"},
|
||||
371559: {'name': 'Catafalque (E4M1) - Yellow key',
|
||||
'episode': 4,
|
||||
'check_sanity': False,
|
||||
@@ -5963,7 +5963,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 3,
|
||||
'index': 213,
|
||||
'doom_type': 2005,
|
||||
'region': "Quay (E5M3) Main"},
|
||||
'region': "Quay (E5M3) Blue"},
|
||||
371850: {'name': 'Quay (E5M3) - Dragon Claw',
|
||||
'episode': 5,
|
||||
'check_sanity': False,
|
||||
@@ -6145,7 +6145,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 4,
|
||||
'index': 3,
|
||||
'doom_type': 79,
|
||||
'region': "Courtyard (E5M4) Main"},
|
||||
'region': "Courtyard (E5M4) Green"},
|
||||
371876: {'name': 'Courtyard (E5M4) - Yellow key',
|
||||
'episode': 5,
|
||||
'check_sanity': False,
|
||||
@@ -6159,7 +6159,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 4,
|
||||
'index': 21,
|
||||
'doom_type': 73,
|
||||
'region': "Courtyard (E5M4) Kakis"},
|
||||
'region': "Courtyard (E5M4) Yellow"},
|
||||
371878: {'name': 'Courtyard (E5M4) - Gauntlets of the Necromancer',
|
||||
'episode': 5,
|
||||
'check_sanity': False,
|
||||
@@ -6187,14 +6187,14 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 4,
|
||||
'index': 87,
|
||||
'doom_type': 2004,
|
||||
'region': "Courtyard (E5M4) Kakis"},
|
||||
'region': "Courtyard (E5M4) Yellow"},
|
||||
371882: {'name': 'Courtyard (E5M4) - Phoenix Rod',
|
||||
'episode': 5,
|
||||
'check_sanity': False,
|
||||
'map': 4,
|
||||
'index': 88,
|
||||
'doom_type': 2003,
|
||||
'region': "Courtyard (E5M4) Main"},
|
||||
'region': "Courtyard (E5M4) Green"},
|
||||
371883: {'name': 'Courtyard (E5M4) - Morph Ovum',
|
||||
'episode': 5,
|
||||
'check_sanity': False,
|
||||
@@ -6229,7 +6229,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 4,
|
||||
'index': 104,
|
||||
'doom_type': 84,
|
||||
'region': "Courtyard (E5M4) Kakis"},
|
||||
'region': "Courtyard (E5M4) Yellow"},
|
||||
371888: {'name': 'Courtyard (E5M4) - Shadowsphere',
|
||||
'episode': 5,
|
||||
'check_sanity': False,
|
||||
@@ -6250,14 +6250,14 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 4,
|
||||
'index': 107,
|
||||
'doom_type': 35,
|
||||
'region': "Courtyard (E5M4) Kakis"},
|
||||
'region': "Courtyard (E5M4) Yellow"},
|
||||
371891: {'name': 'Courtyard (E5M4) - Chaos Device',
|
||||
'episode': 5,
|
||||
'check_sanity': False,
|
||||
'map': 4,
|
||||
'index': 108,
|
||||
'doom_type': 36,
|
||||
'region': "Courtyard (E5M4) Main"},
|
||||
'region': "Courtyard (E5M4) Green"},
|
||||
371892: {'name': 'Courtyard (E5M4) - Tome of Power',
|
||||
'episode': 5,
|
||||
'check_sanity': False,
|
||||
@@ -6278,7 +6278,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 4,
|
||||
'index': 111,
|
||||
'doom_type': 86,
|
||||
'region': "Courtyard (E5M4) Kakis"},
|
||||
'region': "Courtyard (E5M4) Yellow"},
|
||||
371895: {'name': 'Courtyard (E5M4) - Torch',
|
||||
'episode': 5,
|
||||
'check_sanity': False,
|
||||
@@ -6299,7 +6299,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 4,
|
||||
'index': 219,
|
||||
'doom_type': 85,
|
||||
'region': "Courtyard (E5M4) Kakis"},
|
||||
'region': "Courtyard (E5M4) Yellow"},
|
||||
371898: {'name': 'Courtyard (E5M4) - Bag of Holding 3',
|
||||
'episode': 5,
|
||||
'check_sanity': False,
|
||||
@@ -7247,23 +7247,23 @@ location_name_groups: Dict[str, Set[str]] = {
|
||||
'Courtyard (E5M4) - Torch',
|
||||
'Courtyard (E5M4) - Yellow key',
|
||||
},
|
||||
"D'Sparil'S Keep (E3M8)": {
|
||||
"D'Sparil'S Keep (E3M8) - Bag of Holding",
|
||||
"D'Sparil'S Keep (E3M8) - Chaos Device",
|
||||
"D'Sparil'S Keep (E3M8) - Dragon Claw",
|
||||
"D'Sparil'S Keep (E3M8) - Enchanted Shield",
|
||||
"D'Sparil'S Keep (E3M8) - Ethereal Crossbow",
|
||||
"D'Sparil'S Keep (E3M8) - Exit",
|
||||
"D'Sparil'S Keep (E3M8) - Gauntlets of the Necromancer",
|
||||
"D'Sparil'S Keep (E3M8) - Hellstaff",
|
||||
"D'Sparil'S Keep (E3M8) - Mystic Urn",
|
||||
"D'Sparil'S Keep (E3M8) - Phoenix Rod",
|
||||
"D'Sparil'S Keep (E3M8) - Ring of Invincibility",
|
||||
"D'Sparil'S Keep (E3M8) - Shadowsphere",
|
||||
"D'Sparil'S Keep (E3M8) - Silver Shield",
|
||||
"D'Sparil'S Keep (E3M8) - Tome of Power",
|
||||
"D'Sparil'S Keep (E3M8) - Tome of Power 2",
|
||||
"D'Sparil'S Keep (E3M8) - Tome of Power 3",
|
||||
"D'Sparil's Keep (E3M8)": {
|
||||
"D'Sparil's Keep (E3M8) - Bag of Holding",
|
||||
"D'Sparil's Keep (E3M8) - Chaos Device",
|
||||
"D'Sparil's Keep (E3M8) - Dragon Claw",
|
||||
"D'Sparil's Keep (E3M8) - Enchanted Shield",
|
||||
"D'Sparil's Keep (E3M8) - Ethereal Crossbow",
|
||||
"D'Sparil's Keep (E3M8) - Exit",
|
||||
"D'Sparil's Keep (E3M8) - Gauntlets of the Necromancer",
|
||||
"D'Sparil's Keep (E3M8) - Hellstaff",
|
||||
"D'Sparil's Keep (E3M8) - Mystic Urn",
|
||||
"D'Sparil's Keep (E3M8) - Phoenix Rod",
|
||||
"D'Sparil's Keep (E3M8) - Ring of Invincibility",
|
||||
"D'Sparil's Keep (E3M8) - Shadowsphere",
|
||||
"D'Sparil's Keep (E3M8) - Silver Shield",
|
||||
"D'Sparil's Keep (E3M8) - Tome of Power",
|
||||
"D'Sparil's Keep (E3M8) - Tome of Power 2",
|
||||
"D'Sparil's Keep (E3M8) - Tome of Power 3",
|
||||
},
|
||||
'Field of Judgement (E5M8)': {
|
||||
'Field of Judgement (E5M8) - Bag of Holding',
|
||||
@@ -7641,33 +7641,33 @@ location_name_groups: Dict[str, Set[str]] = {
|
||||
"Skein of D'Sparil (E5M9) - Torch",
|
||||
"Skein of D'Sparil (E5M9) - Yellow key",
|
||||
},
|
||||
'The Aquifier (E3M9)': {
|
||||
'The Aquifier (E3M9) - Bag of Holding',
|
||||
'The Aquifier (E3M9) - Blue key',
|
||||
'The Aquifier (E3M9) - Chaos Device',
|
||||
'The Aquifier (E3M9) - Dragon Claw',
|
||||
'The Aquifier (E3M9) - Enchanted Shield',
|
||||
'The Aquifier (E3M9) - Ethereal Crossbow',
|
||||
'The Aquifier (E3M9) - Exit',
|
||||
'The Aquifier (E3M9) - Firemace',
|
||||
'The Aquifier (E3M9) - Firemace 2',
|
||||
'The Aquifier (E3M9) - Firemace 3',
|
||||
'The Aquifier (E3M9) - Firemace 4',
|
||||
'The Aquifier (E3M9) - Gauntlets of the Necromancer',
|
||||
'The Aquifier (E3M9) - Green key',
|
||||
'The Aquifier (E3M9) - Hellstaff',
|
||||
'The Aquifier (E3M9) - Map Scroll',
|
||||
'The Aquifier (E3M9) - Morph Ovum',
|
||||
'The Aquifier (E3M9) - Mystic Urn',
|
||||
'The Aquifier (E3M9) - Phoenix Rod',
|
||||
'The Aquifier (E3M9) - Ring of Invincibility',
|
||||
'The Aquifier (E3M9) - Shadowsphere',
|
||||
'The Aquifier (E3M9) - Silver Shield',
|
||||
'The Aquifier (E3M9) - Silver Shield 2',
|
||||
'The Aquifier (E3M9) - Tome of Power',
|
||||
'The Aquifier (E3M9) - Tome of Power 2',
|
||||
'The Aquifier (E3M9) - Torch',
|
||||
'The Aquifier (E3M9) - Yellow key',
|
||||
'The Aquifer (E3M9)': {
|
||||
'The Aquifer (E3M9) - Bag of Holding',
|
||||
'The Aquifer (E3M9) - Blue key',
|
||||
'The Aquifer (E3M9) - Chaos Device',
|
||||
'The Aquifer (E3M9) - Dragon Claw',
|
||||
'The Aquifer (E3M9) - Enchanted Shield',
|
||||
'The Aquifer (E3M9) - Ethereal Crossbow',
|
||||
'The Aquifer (E3M9) - Exit',
|
||||
'The Aquifer (E3M9) - Firemace',
|
||||
'The Aquifer (E3M9) - Firemace 2',
|
||||
'The Aquifer (E3M9) - Firemace 3',
|
||||
'The Aquifer (E3M9) - Firemace 4',
|
||||
'The Aquifer (E3M9) - Gauntlets of the Necromancer',
|
||||
'The Aquifer (E3M9) - Green key',
|
||||
'The Aquifer (E3M9) - Hellstaff',
|
||||
'The Aquifer (E3M9) - Map Scroll',
|
||||
'The Aquifer (E3M9) - Morph Ovum',
|
||||
'The Aquifer (E3M9) - Mystic Urn',
|
||||
'The Aquifer (E3M9) - Phoenix Rod',
|
||||
'The Aquifer (E3M9) - Ring of Invincibility',
|
||||
'The Aquifer (E3M9) - Shadowsphere',
|
||||
'The Aquifer (E3M9) - Silver Shield',
|
||||
'The Aquifer (E3M9) - Silver Shield 2',
|
||||
'The Aquifer (E3M9) - Tome of Power',
|
||||
'The Aquifer (E3M9) - Tome of Power 2',
|
||||
'The Aquifer (E3M9) - Torch',
|
||||
'The Aquifer (E3M9) - Yellow key',
|
||||
},
|
||||
'The Azure Fortress (E3M4)': {
|
||||
'The Azure Fortress (E3M4) - Bag of Holding',
|
||||
|
||||
@@ -29,8 +29,8 @@ map_names: List[str] = [
|
||||
'The Ophidian Lair (E3M5)',
|
||||
'The Halls of Fear (E3M6)',
|
||||
'The Chasm (E3M7)',
|
||||
"D'Sparil'S Keep (E3M8)",
|
||||
'The Aquifier (E3M9)',
|
||||
"D'Sparil's Keep (E3M8)",
|
||||
'The Aquifer (E3M9)',
|
||||
'Catafalque (E4M1)',
|
||||
'Blockhouse (E4M2)',
|
||||
'Ambulatory (E4M3)',
|
||||
|
||||
@@ -520,34 +520,34 @@ regions:List[RegionDict] = [
|
||||
"episode":3,
|
||||
"connections":[{"target":"The Chasm (E3M7) Yellow","pro":False}]},
|
||||
|
||||
# D'Sparil'S Keep (E3M8)
|
||||
{"name":"D'Sparil'S Keep (E3M8) Main",
|
||||
# D'Sparil's Keep (E3M8)
|
||||
{"name":"D'Sparil's Keep (E3M8) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":3,
|
||||
"connections":[]},
|
||||
|
||||
# The Aquifier (E3M9)
|
||||
{"name":"The Aquifier (E3M9) Main",
|
||||
# The Aquifer (E3M9)
|
||||
{"name":"The Aquifer (E3M9) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":3,
|
||||
"connections":[{"target":"The Aquifier (E3M9) Yellow","pro":False}]},
|
||||
{"name":"The Aquifier (E3M9) Blue",
|
||||
"connections":[{"target":"The Aquifer (E3M9) Yellow","pro":False}]},
|
||||
{"name":"The Aquifer (E3M9) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[]},
|
||||
{"name":"The Aquifier (E3M9) Yellow",
|
||||
{"name":"The Aquifer (E3M9) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[
|
||||
{"target":"The Aquifier (E3M9) Green","pro":False},
|
||||
{"target":"The Aquifier (E3M9) Main","pro":False}]},
|
||||
{"name":"The Aquifier (E3M9) Green",
|
||||
{"target":"The Aquifer (E3M9) Green","pro":False},
|
||||
{"target":"The Aquifer (E3M9) Main","pro":False}]},
|
||||
{"name":"The Aquifer (E3M9) Green",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[
|
||||
{"target":"The Aquifier (E3M9) Yellow","pro":False},
|
||||
{"target":"The Aquifier (E3M9) Main","pro":False},
|
||||
{"target":"The Aquifier (E3M9) Blue","pro":False}]},
|
||||
{"target":"The Aquifer (E3M9) Yellow","pro":False},
|
||||
{"target":"The Aquifer (E3M9) Main","pro":False},
|
||||
{"target":"The Aquifer (E3M9) Blue","pro":False}]},
|
||||
|
||||
# Catafalque (E4M1)
|
||||
{"name":"Catafalque (E4M1) Main",
|
||||
@@ -795,16 +795,22 @@ regions:List[RegionDict] = [
|
||||
"connects_to_hub":True,
|
||||
"episode":5,
|
||||
"connections":[
|
||||
{"target":"Courtyard (E5M4) Kakis","pro":False},
|
||||
{"target":"Courtyard (E5M4) Yellow","pro":False},
|
||||
{"target":"Courtyard (E5M4) Blue","pro":False}]},
|
||||
{"name":"Courtyard (E5M4) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":5,
|
||||
"connections":[{"target":"Courtyard (E5M4) Main","pro":False}]},
|
||||
{"name":"Courtyard (E5M4) Kakis",
|
||||
{"name":"Courtyard (E5M4) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":5,
|
||||
"connections":[{"target":"Courtyard (E5M4) Main","pro":False}]},
|
||||
"connections":[
|
||||
{"target":"Courtyard (E5M4) Main","pro":False},
|
||||
{"target":"Courtyard (E5M4) Green","pro":False}]},
|
||||
{"name":"Courtyard (E5M4) Green",
|
||||
"connects_to_hub":False,
|
||||
"episode":5,
|
||||
"connections":[{"target":"Courtyard (E5M4) Yellow","pro":False}]},
|
||||
|
||||
# Hydratyr (E5M5)
|
||||
{"name":"Hydratyr (E5M5) Main",
|
||||
|
||||
@@ -388,9 +388,9 @@ def set_episode3_rules(player, multiworld, pro):
|
||||
set_rule(multiworld.get_entrance("The Chasm (E3M7) Green -> The Chasm (E3M7) Yellow", player), lambda state:
|
||||
state.has("The Chasm (E3M7) - Green key", player, 1))
|
||||
|
||||
# D'Sparil'S Keep (E3M8)
|
||||
set_rule(multiworld.get_entrance("Hub -> D'Sparil'S Keep (E3M8) Main", player), lambda state:
|
||||
state.has("D'Sparil'S Keep (E3M8)", player, 1) and
|
||||
# D'Sparil's Keep (E3M8)
|
||||
set_rule(multiworld.get_entrance("Hub -> D'Sparil's Keep (E3M8) Main", player), lambda state:
|
||||
state.has("D'Sparil's Keep (E3M8)", player, 1) and
|
||||
state.has("Gauntlets of the Necromancer", player, 1) and
|
||||
state.has("Ethereal Crossbow", player, 1) and
|
||||
state.has("Dragon Claw", player, 1) and
|
||||
@@ -398,23 +398,23 @@ def set_episode3_rules(player, multiworld, pro):
|
||||
state.has("Firemace", player, 1) and
|
||||
state.has("Hellstaff", player, 1))
|
||||
|
||||
# The Aquifier (E3M9)
|
||||
set_rule(multiworld.get_entrance("Hub -> The Aquifier (E3M9) Main", player), lambda state:
|
||||
state.has("The Aquifier (E3M9)", player, 1) and
|
||||
# The Aquifer (E3M9)
|
||||
set_rule(multiworld.get_entrance("Hub -> The Aquifer (E3M9) Main", player), lambda state:
|
||||
state.has("The Aquifer (E3M9)", player, 1) and
|
||||
state.has("Gauntlets of the Necromancer", player, 1) and
|
||||
state.has("Ethereal Crossbow", player, 1) and
|
||||
state.has("Dragon Claw", player, 1) and
|
||||
state.has("Phoenix Rod", player, 1) and
|
||||
state.has("Firemace", player, 1) and
|
||||
state.has("Hellstaff", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Aquifier (E3M9) Main -> The Aquifier (E3M9) Yellow", player), lambda state:
|
||||
state.has("The Aquifier (E3M9) - Yellow key", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Aquifier (E3M9) Yellow -> The Aquifier (E3M9) Green", player), lambda state:
|
||||
state.has("The Aquifier (E3M9) - Green key", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Aquifier (E3M9) Yellow -> The Aquifier (E3M9) Main", player), lambda state:
|
||||
state.has("The Aquifier (E3M9) - Yellow key", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Aquifier (E3M9) Green -> The Aquifier (E3M9) Yellow", player), lambda state:
|
||||
state.has("The Aquifier (E3M9) - Green key", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Aquifer (E3M9) Main -> The Aquifer (E3M9) Yellow", player), lambda state:
|
||||
state.has("The Aquifer (E3M9) - Yellow key", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Aquifer (E3M9) Yellow -> The Aquifer (E3M9) Green", player), lambda state:
|
||||
state.has("The Aquifer (E3M9) - Green key", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Aquifer (E3M9) Yellow -> The Aquifer (E3M9) Main", player), lambda state:
|
||||
state.has("The Aquifer (E3M9) - Yellow key", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Aquifer (E3M9) Green -> The Aquifer (E3M9) Yellow", player), lambda state:
|
||||
state.has("The Aquifer (E3M9) - Green key", player, 1))
|
||||
|
||||
|
||||
def set_episode4_rules(player, multiworld, pro):
|
||||
@@ -623,15 +623,17 @@ def set_episode5_rules(player, multiworld, pro):
|
||||
(state.has("Phoenix Rod", player, 1) or
|
||||
state.has("Firemace", player, 1) or
|
||||
state.has("Hellstaff", player, 1)))
|
||||
set_rule(multiworld.get_entrance("Courtyard (E5M4) Main -> Courtyard (E5M4) Kakis", player), lambda state:
|
||||
state.has("Courtyard (E5M4) - Yellow key", player, 1) or
|
||||
state.has("Courtyard (E5M4) - Green key", player, 1))
|
||||
set_rule(multiworld.get_entrance("Courtyard (E5M4) Main -> Courtyard (E5M4) Yellow", player), lambda state:
|
||||
state.has("Courtyard (E5M4) - Yellow key", player, 1))
|
||||
set_rule(multiworld.get_entrance("Courtyard (E5M4) Main -> Courtyard (E5M4) Blue", player), lambda state:
|
||||
state.has("Courtyard (E5M4) - Blue key", player, 1))
|
||||
set_rule(multiworld.get_entrance("Courtyard (E5M4) Blue -> Courtyard (E5M4) Main", player), lambda state:
|
||||
state.has("Courtyard (E5M4) - Blue key", player, 1))
|
||||
set_rule(multiworld.get_entrance("Courtyard (E5M4) Kakis -> Courtyard (E5M4) Main", player), lambda state:
|
||||
state.has("Courtyard (E5M4) - Yellow key", player, 1) or
|
||||
set_rule(multiworld.get_entrance("Courtyard (E5M4) Yellow -> Courtyard (E5M4) Main", player), lambda state:
|
||||
state.has("Courtyard (E5M4) - Yellow key", player, 1))
|
||||
set_rule(multiworld.get_entrance("Courtyard (E5M4) Yellow -> Courtyard (E5M4) Green", player), lambda state:
|
||||
state.has("Courtyard (E5M4) - Green key", player, 1))
|
||||
set_rule(multiworld.get_entrance("Courtyard (E5M4) Green -> Courtyard (E5M4) Yellow", player), lambda state:
|
||||
state.has("Courtyard (E5M4) - Green key", player, 1))
|
||||
|
||||
# Hydratyr (E5M5)
|
||||
|
||||
@@ -49,18 +49,18 @@ class HereticWorld(World):
|
||||
location_name_to_id = {data["name"]: loc_id for loc_id, data in Locations.location_table.items()}
|
||||
location_name_groups = Locations.location_name_groups
|
||||
|
||||
starting_level_for_episode: List[str] = [
|
||||
"The Docks (E1M1)",
|
||||
"The Crater (E2M1)",
|
||||
"The Storehouse (E3M1)",
|
||||
"Catafalque (E4M1)",
|
||||
"Ochre Cliffs (E5M1)"
|
||||
]
|
||||
starting_level_for_episode: Dict[int, str] = {
|
||||
1: "The Docks (E1M1)",
|
||||
2: "The Crater (E2M1)",
|
||||
3: "The Storehouse (E3M1)",
|
||||
4: "Catafalque (E4M1)",
|
||||
5: "Ochre Cliffs (E5M1)"
|
||||
}
|
||||
|
||||
boss_level_for_episode: List[str] = [
|
||||
all_boss_levels: List[str] = [
|
||||
"Hell's Maw (E1M8)",
|
||||
"The Portals of Chaos (E2M8)",
|
||||
"D'Sparil'S Keep (E3M8)",
|
||||
"D'Sparil's Keep (E3M8)",
|
||||
"Shattered Bridge (E4M8)",
|
||||
"Field of Judgement (E5M8)"
|
||||
]
|
||||
@@ -82,6 +82,7 @@ class HereticWorld(World):
|
||||
def __init__(self, multiworld: MultiWorld, player: int):
|
||||
self.included_episodes = [1, 1, 1, 0, 0]
|
||||
self.location_count = 0
|
||||
self.starting_levels = []
|
||||
|
||||
super().__init__(multiworld, player)
|
||||
|
||||
@@ -100,6 +101,14 @@ class HereticWorld(World):
|
||||
if self.get_episode_count() == 0:
|
||||
self.included_episodes[0] = 1
|
||||
|
||||
self.starting_levels = [level_name for (episode, level_name) in self.starting_level_for_episode.items()
|
||||
if self.included_episodes[episode - 1]]
|
||||
|
||||
# For Solo Episode 1, place the Yellow Key for E1M1 early.
|
||||
# Gives the generator five potential placements (plus the forced key) instead of only two.
|
||||
if self.get_episode_count() == 1 and self.included_episodes[0]:
|
||||
self.multiworld.early_items[self.player]["The Docks (E1M1) - Yellow key"] = 1
|
||||
|
||||
def create_regions(self):
|
||||
pro = self.options.pro.value
|
||||
check_sanity = self.options.check_sanity.value
|
||||
@@ -154,7 +163,7 @@ class HereticWorld(World):
|
||||
def completion_rule(self, state: CollectionState):
|
||||
goal_levels = Maps.map_names
|
||||
if self.options.goal.value:
|
||||
goal_levels = self.boss_level_for_episode
|
||||
goal_levels = self.all_boss_levels
|
||||
|
||||
for map_name in goal_levels:
|
||||
if map_name + " - Exit" not in self.location_name_to_id:
|
||||
@@ -203,7 +212,7 @@ class HereticWorld(World):
|
||||
if item["episode"] != -1 and not self.included_episodes[item["episode"] - 1]:
|
||||
continue
|
||||
|
||||
count = item["count"] if item["name"] not in self.starting_level_for_episode else item["count"] - 1
|
||||
count = item["count"] if item["name"] not in self.starting_levels else item["count"] - 1
|
||||
itempool += [self.create_item(item["name"]) for _ in range(count)]
|
||||
|
||||
# Bag(s) of Holding based on options
|
||||
@@ -236,9 +245,8 @@ class HereticWorld(World):
|
||||
self.location_count -= 1
|
||||
|
||||
# Give starting levels right away
|
||||
for i in range(len(self.included_episodes)):
|
||||
if self.included_episodes[i]:
|
||||
self.multiworld.push_precollected(self.create_item(self.starting_level_for_episode[i]))
|
||||
for map_name in self.starting_levels:
|
||||
self.multiworld.push_precollected(self.create_item(map_name))
|
||||
|
||||
# Give Computer area maps if option selected
|
||||
if self.options.start_with_map_scrolls.value:
|
||||
|
||||
@@ -450,7 +450,7 @@ class GrubHuntGoal(NamedRange):
|
||||
display_name = "Grub Hunt Goal"
|
||||
range_start = 1
|
||||
range_end = 46
|
||||
special_range_names = {"all": -1}
|
||||
special_range_names = {"all": -1, "forty_six": 46}
|
||||
default = 46
|
||||
|
||||
|
||||
|
||||
0
worlds/kh2/Names/__init__.py
Normal file
0
worlds/kh2/Names/__init__.py
Normal file
@@ -1 +1 @@
|
||||
Pymem>=1.10.0
|
||||
Pymem>=1.10.0
|
||||
|
||||
0
worlds/ladx/LADXR/__init__.py
Normal file
0
worlds/ladx/LADXR/__init__.py
Normal file
0
worlds/ladx/LADXR/locations/__init__.py
Normal file
0
worlds/ladx/LADXR/locations/__init__.py
Normal file
0
worlds/ladx/LADXR/mapgen/locations/__init__.py
Normal file
0
worlds/ladx/LADXR/mapgen/locations/__init__.py
Normal file
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user