mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-20 22:35:30 -07:00
Compare commits
37 Commits
revert-401
...
NewSoupVi-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eaf352daaf | ||
|
|
29b34ca9fd | ||
|
|
d97ee5d209 | ||
|
|
c2bd9df0f7 | ||
|
|
112bfe0933 | ||
|
|
96b500679d | ||
|
|
258ea10c52 | ||
|
|
043ba418ec | ||
|
|
894a8571ee | ||
|
|
874197d940 | ||
|
|
d3ed40cd4d | ||
|
|
a29ba4a6c4 | ||
|
|
fe06fe075e | ||
|
|
de58cb03da | ||
|
|
3204680662 | ||
|
|
07e896508c | ||
|
|
2d3faea713 | ||
|
|
7c89a83d19 | ||
|
|
16f8b41cb9 | ||
|
|
7d506990f5 | ||
|
|
aadcb4c903 | ||
|
|
daf94fcdb2 | ||
|
|
1cef659b78 | ||
|
|
25381ef2c2 | ||
|
|
5927926314 | ||
|
|
2a11d9fec3 | ||
|
|
82c44aaa22 | ||
|
|
a7b483e4b7 | ||
|
|
917335ec54 | ||
|
|
6e59ee2926 | ||
|
|
3c9270d802 | ||
|
|
c4bbcf9890 | ||
|
|
8dbecf3d57 | ||
|
|
0de1369ec5 | ||
|
|
fa95ae4b24 | ||
|
|
2065246186 | ||
|
|
ca1b3df45b |
3
Fill.py
3
Fill.py
@@ -531,7 +531,8 @@ def distribute_items_restrictive(multiworld: MultiWorld,
|
||||
if progitempool:
|
||||
raise FillError(
|
||||
f"Not enough locations for progression items. "
|
||||
f"There are {len(progitempool)} more progression items than there are available locations.",
|
||||
f"There are {len(progitempool)} more progression items than there are available locations.\n"
|
||||
f"Unfilled locations:\n{multiworld.get_unfilled_locations()}.",
|
||||
multiworld=multiworld,
|
||||
)
|
||||
accessibility_corrections(multiworld, multiworld.state, defaultlocations)
|
||||
|
||||
@@ -500,7 +500,8 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
|
||||
for option_key in game_weights:
|
||||
if option_key in {"triggers", *valid_keys}:
|
||||
continue
|
||||
logging.warning(f"{option_key} is not a valid option name for {ret.game} and is not present in triggers.")
|
||||
logging.warning(f"{option_key} is not a valid option name for {ret.game} and is not present in triggers "
|
||||
f"for player {ret.name}.")
|
||||
if PlandoOptions.items in plando_options:
|
||||
ret.plando_items = copy.deepcopy(game_weights.get("plando_items", []))
|
||||
if ret.game == "A Link to the Past":
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,7 +1,7 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 LLCoolDave
|
||||
Copyright (c) 2022 Berserker66
|
||||
Copyright (c) 2025 Berserker66
|
||||
Copyright (c) 2022 CaitSith2
|
||||
Copyright (c) 2021 LegendaryLinux
|
||||
|
||||
|
||||
@@ -444,7 +444,7 @@ class Context:
|
||||
|
||||
self.slot_info = decoded_obj["slot_info"]
|
||||
self.games = {slot: slot_info.game for slot, slot_info in self.slot_info.items()}
|
||||
self.groups = {slot: slot_info.group_members for slot, slot_info in self.slot_info.items()
|
||||
self.groups = {slot: set(slot_info.group_members) for slot, slot_info in self.slot_info.items()
|
||||
if slot_info.type == SlotType.group}
|
||||
|
||||
self.clients = {0: {}}
|
||||
|
||||
@@ -10,7 +10,7 @@ import websockets
|
||||
from Utils import ByValue, Version
|
||||
|
||||
|
||||
class HintStatus(enum.IntEnum):
|
||||
class HintStatus(ByValue, enum.IntEnum):
|
||||
HINT_FOUND = 0
|
||||
HINT_UNSPECIFIED = 1
|
||||
HINT_NO_PRIORITY = 10
|
||||
|
||||
@@ -243,6 +243,9 @@ class SNIContext(CommonContext):
|
||||
# Once the games handled by SNIClient gets made to be remote items,
|
||||
# this will no longer be needed.
|
||||
async_start(self.send_msgs([{"cmd": "LocationScouts", "locations": list(new_locations)}]))
|
||||
|
||||
if self.client_handler is not None:
|
||||
self.client_handler.on_package(self, cmd, args)
|
||||
|
||||
def run_gui(self) -> None:
|
||||
from kvui import GameManager
|
||||
|
||||
11
Utils.py
11
Utils.py
@@ -152,8 +152,15 @@ def home_path(*path: str) -> str:
|
||||
if hasattr(home_path, 'cached_path'):
|
||||
pass
|
||||
elif sys.platform.startswith('linux'):
|
||||
home_path.cached_path = os.path.expanduser('~/Archipelago')
|
||||
os.makedirs(home_path.cached_path, 0o700, exist_ok=True)
|
||||
xdg_data_home = os.getenv('XDG_DATA_HOME', os.path.expanduser('~/.local/share'))
|
||||
home_path.cached_path = xdg_data_home + '/Archipelago'
|
||||
if not os.path.isdir(home_path.cached_path):
|
||||
legacy_home_path = os.path.expanduser('~/Archipelago')
|
||||
if os.path.isdir(legacy_home_path):
|
||||
os.renames(legacy_home_path, home_path.cached_path)
|
||||
os.symlink(home_path.cached_path, legacy_home_path)
|
||||
else:
|
||||
os.makedirs(home_path.cached_path, 0o700, exist_ok=True)
|
||||
else:
|
||||
# not implemented
|
||||
home_path.cached_path = local_path() # this will generate the same exceptions we got previously
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% block footer %}
|
||||
<footer id="island-footer">
|
||||
<div id="copyright-notice">Copyright 2024 Archipelago</div>
|
||||
<div id="copyright-notice">Copyright 2025 Archipelago</div>
|
||||
<div id="links">
|
||||
<a href="/sitemap">Site Map</a>
|
||||
-
|
||||
|
||||
@@ -147,3 +147,8 @@
|
||||
rectangle: self.x-2, self.y-2, self.width+4, self.height+4
|
||||
<ServerToolTip>:
|
||||
pos_hint: {'center_y': 0.5, 'center_x': 0.5}
|
||||
<AutocompleteHintInput>
|
||||
size_hint_y: None
|
||||
height: dp(30)
|
||||
multiline: False
|
||||
write_tab: False
|
||||
|
||||
@@ -152,7 +152,7 @@
|
||||
/worlds/saving_princess/ @LeonarthCG
|
||||
|
||||
# Shivers
|
||||
/worlds/shivers/ @GodlFire
|
||||
/worlds/shivers/ @GodlFire @korydondzila
|
||||
|
||||
# A Short Hike
|
||||
/worlds/shorthike/ @chandler05 @BrandenEK
|
||||
|
||||
65
kvui.py
65
kvui.py
@@ -40,7 +40,7 @@ from kivy.core.image import ImageLoader, ImageLoaderBase, ImageData
|
||||
from kivy.base import ExceptionHandler, ExceptionManager
|
||||
from kivy.clock import Clock
|
||||
from kivy.factory import Factory
|
||||
from kivy.properties import BooleanProperty, ObjectProperty
|
||||
from kivy.properties import BooleanProperty, ObjectProperty, NumericProperty
|
||||
from kivy.metrics import dp
|
||||
from kivy.effects.scroll import ScrollEffect
|
||||
from kivy.uix.widget import Widget
|
||||
@@ -64,6 +64,7 @@ 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
|
||||
|
||||
fade_in_animation = Animation(opacity=0, duration=0) + Animation(opacity=1, duration=0.25)
|
||||
@@ -305,6 +306,50 @@ class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
|
||||
""" Respond to the selection of items in the view. """
|
||||
self.selected = is_selected
|
||||
|
||||
|
||||
class AutocompleteHintInput(TextInput):
|
||||
min_chars = NumericProperty(3)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.dropdown = DropDown()
|
||||
self.dropdown.bind(on_select=lambda instance, x: setattr(self, 'text', x))
|
||||
self.bind(on_text_validate=self.on_message)
|
||||
|
||||
def on_message(self, instance):
|
||||
App.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
|
||||
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
|
||||
return self.dropdown.select("".join(text_frag for text_frag in split_text
|
||||
if not text_frag.startswith("[")))
|
||||
lowered = value.lower()
|
||||
for item_name in item_names:
|
||||
try:
|
||||
index = item_name.lower().index(lowered)
|
||||
except ValueError:
|
||||
pass # substring not found
|
||||
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)
|
||||
else:
|
||||
self.dropdown.dismiss()
|
||||
|
||||
|
||||
class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
||||
selected = BooleanProperty(False)
|
||||
striped = BooleanProperty(False)
|
||||
@@ -570,8 +615,10 @@ class GameManager(App):
|
||||
# show Archipelago tab if other logging is present
|
||||
self.tabs.add_widget(panel)
|
||||
|
||||
hint_panel = self.add_client_tab("Hints", HintLog(self.json_to_kivy_parser))
|
||||
hint_panel = self.add_client_tab("Hints", HintLayout())
|
||||
self.hint_log = HintLog(self.json_to_kivy_parser)
|
||||
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"
|
||||
@@ -698,7 +745,7 @@ class GameManager(App):
|
||||
|
||||
def update_hints(self):
|
||||
hints = self.ctx.stored_data.get(f"_read_hints_{self.ctx.team}_{self.ctx.slot}", [])
|
||||
self.log_panels["Hints"].refresh_hints(hints)
|
||||
self.hint_log.refresh_hints(hints)
|
||||
|
||||
# default F1 keybind, opens a settings menu, that seems to break the layout engine once closed
|
||||
def open_settings(self, *largs):
|
||||
@@ -753,6 +800,17 @@ class UILog(RecycleView):
|
||||
element.height = element.texture_size[1]
|
||||
|
||||
|
||||
class HintLayout(BoxLayout):
|
||||
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.add_widget(AutocompleteHintInput())
|
||||
self.add_widget(boxlayout)
|
||||
|
||||
|
||||
status_names: typing.Dict[HintStatus, str] = {
|
||||
HintStatus.HINT_FOUND: "Found",
|
||||
HintStatus.HINT_UNSPECIFIED: "Unspecified",
|
||||
@@ -769,6 +827,7 @@ status_colors: typing.Dict[HintStatus, str] = {
|
||||
}
|
||||
|
||||
|
||||
|
||||
class HintLog(RecycleView):
|
||||
header = {
|
||||
"receiving": {"text": "[u]Receiving Player[/u]"},
|
||||
|
||||
@@ -7,7 +7,7 @@ schema>=0.7.7
|
||||
kivy>=2.3.0
|
||||
bsdiff4>=1.2.4
|
||||
platformdirs>=4.2.2
|
||||
certifi>=2024.8.30
|
||||
certifi>=2024.12.14
|
||||
cython>=3.0.11
|
||||
cymem>=2.0.8
|
||||
orjson>=3.10.7
|
||||
|
||||
@@ -86,3 +86,7 @@ class SNIClient(abc.ABC, metaclass=AutoSNIClientRegister):
|
||||
async def deathlink_kill_player(self, ctx: SNIContext) -> None:
|
||||
""" override this with implementation to kill player """
|
||||
pass
|
||||
|
||||
def on_package(self, ctx: SNIContext, cmd: str, args: Dict[str, Any]) -> None:
|
||||
""" override this with code to handle packages from the server """
|
||||
pass
|
||||
|
||||
@@ -18,16 +18,42 @@ class Type(Enum):
|
||||
|
||||
|
||||
class Component:
|
||||
"""
|
||||
A Component represents a process launchable by Archipelago Launcher, either by a User action in the GUI,
|
||||
by resolving an archipelago://user:pass@host:port link from the WebHost, by resolving a patch file's metadata,
|
||||
or by using a component name arg while running the Launcher in CLI i.e. `ArchipelagoLauncher.exe "Text Client"`
|
||||
|
||||
Expected to be appended to LauncherComponents.component list to be used.
|
||||
"""
|
||||
display_name: str
|
||||
"""Used as the GUI button label and the component name in the CLI args"""
|
||||
type: Type
|
||||
"""
|
||||
Enum "Type" classification of component intent, for filtering in the Launcher GUI
|
||||
If not set in the constructor, it will be inferred by display_name
|
||||
"""
|
||||
script_name: Optional[str]
|
||||
"""Recommended to use func instead; Name of file to run when the component is called"""
|
||||
frozen_name: Optional[str]
|
||||
"""Recommended to use func instead; Name of the frozen executable file for this component"""
|
||||
icon: str # just the name, no suffix
|
||||
"""Lookup ID for the icon path in LauncherComponents.icon_paths"""
|
||||
cli: bool
|
||||
"""Bool to control if the component gets launched in an appropriate Terminal for the OS"""
|
||||
func: Optional[Callable]
|
||||
"""
|
||||
Function that gets called when the component gets launched
|
||||
Any arg besides the component name arg is passed into the func as well, so handling *args is suggested
|
||||
"""
|
||||
file_identifier: Optional[Callable[[str], bool]]
|
||||
"""
|
||||
Function that is run against patch file arg to identify which component is appropriate to launch
|
||||
If the function is an Instance of SuffixIdentifier the suffixes will also be valid for the Open Patch component
|
||||
"""
|
||||
game_name: Optional[str]
|
||||
"""Game name to identify component when handling launch links from WebHost"""
|
||||
supports_uri: Optional[bool]
|
||||
"""Bool to identify if a component supports being launched by launch links from WebHost"""
|
||||
|
||||
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,
|
||||
|
||||
@@ -592,9 +592,9 @@ def global_rules(multiworld: MultiWorld, player: int):
|
||||
lambda state: can_kill_most_things(state, player, 8) and has_fire_source(state, player) and state.multiworld.get_entrance('Ganons Tower Torch Rooms', player).parent_region.dungeon.bosses['middle'].can_defeat(state))
|
||||
set_rule(multiworld.get_location('Ganons Tower - Mini Helmasaur Key Drop', player), lambda state: can_kill_most_things(state, player, 1))
|
||||
set_rule(multiworld.get_location('Ganons Tower - Pre-Moldorm Chest', player),
|
||||
lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 7))
|
||||
lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 7) and can_use_bombs(state, player))
|
||||
set_rule(multiworld.get_entrance('Ganons Tower Moldorm Door', player),
|
||||
lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 8))
|
||||
lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 8) and can_use_bombs(state, player))
|
||||
set_rule(multiworld.get_entrance('Ganons Tower Moldorm Gap', player),
|
||||
lambda state: state.has('Hookshot', player) and state.multiworld.get_entrance('Ganons Tower Moldorm Gap', player).parent_region.dungeon.bosses['top'].can_defeat(state))
|
||||
set_defeat_dungeon_boss_rule(multiworld.get_location('Agahnim 2', player))
|
||||
|
||||
@@ -130,19 +130,21 @@ class TestGanonsTower(TestDungeon):
|
||||
|
||||
["Ganons Tower - Pre-Moldorm Chest", False, []],
|
||||
["Ganons Tower - Pre-Moldorm Chest", False, [], ['Progressive Bow']],
|
||||
["Ganons Tower - Pre-Moldorm Chest", False, [], ['Bomb Upgrade (50)']],
|
||||
["Ganons Tower - Pre-Moldorm Chest", False, [], ['Big Key (Ganons Tower)']],
|
||||
["Ganons Tower - Pre-Moldorm Chest", False, [], ['Lamp', 'Fire Rod']],
|
||||
["Ganons Tower - Pre-Moldorm Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp']],
|
||||
["Ganons Tower - Pre-Moldorm Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod']],
|
||||
["Ganons Tower - Pre-Moldorm Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp']],
|
||||
["Ganons Tower - Pre-Moldorm Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod']],
|
||||
|
||||
["Ganons Tower - Validation Chest", False, []],
|
||||
["Ganons Tower - Validation Chest", False, [], ['Hookshot']],
|
||||
["Ganons Tower - Validation Chest", False, [], ['Progressive Bow']],
|
||||
["Ganons Tower - Validation Chest", False, [], ['Bomb Upgrade (50)']],
|
||||
["Ganons Tower - Validation Chest", False, [], ['Big Key (Ganons Tower)']],
|
||||
["Ganons Tower - Validation Chest", False, [], ['Lamp', 'Fire Rod']],
|
||||
["Ganons Tower - Validation Chest", False, [], ['Progressive Sword', 'Hammer']],
|
||||
["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp', 'Hookshot', 'Progressive Sword']],
|
||||
["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Hookshot', 'Progressive Sword']],
|
||||
["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp', 'Hookshot', 'Hammer']],
|
||||
["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Hookshot', 'Hammer']],
|
||||
["Ganons Tower - Validation Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp', 'Hookshot', 'Progressive Sword']],
|
||||
["Ganons Tower - Validation Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Hookshot', 'Progressive Sword']],
|
||||
["Ganons Tower - Validation Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp', 'Hookshot', 'Hammer']],
|
||||
["Ganons Tower - Validation Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Hookshot', 'Hammer']],
|
||||
])
|
||||
@@ -4,14 +4,17 @@ import random
|
||||
|
||||
|
||||
class ChoiceIsRandom(Choice):
|
||||
randomized: bool = False
|
||||
randomized: bool
|
||||
|
||||
def __init__(self, value: int, randomized: bool = False):
|
||||
super().__init__(value)
|
||||
self.randomized = randomized
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, text: str) -> Choice:
|
||||
text = text.lower()
|
||||
if text == "random":
|
||||
cls.randomized = True
|
||||
return cls(random.choice(list(cls.name_lookup)))
|
||||
return cls(random.choice(list(cls.name_lookup)), True)
|
||||
for option_name, value in cls.options.items():
|
||||
if option_name == text:
|
||||
return cls(value)
|
||||
|
||||
@@ -37,8 +37,8 @@ base_info = {
|
||||
"description": "Integration client for the Archipelago Randomizer",
|
||||
"factorio_version": "2.0",
|
||||
"dependencies": [
|
||||
"base >= 2.0.15",
|
||||
"? quality >= 2.0.15",
|
||||
"base >= 2.0.28",
|
||||
"? quality >= 2.0.28",
|
||||
"! space-age",
|
||||
"? science-not-invited",
|
||||
"? factory-levels"
|
||||
|
||||
@@ -63,17 +63,19 @@ class FactorioElement:
|
||||
|
||||
|
||||
class Technology(FactorioElement): # maybe make subclass of Location?
|
||||
has_modifier: bool
|
||||
factorio_id: int
|
||||
progressive: Tuple[str]
|
||||
unlocks: Union[Set[str], bool] # bool case is for progressive technologies
|
||||
modifiers: list[str]
|
||||
|
||||
def __init__(self, technology_name: str, factorio_id: int, progressive: Tuple[str] = (),
|
||||
has_modifier: bool = False, unlocks: Union[Set[str], bool] = None):
|
||||
modifiers: list[str] = None, unlocks: Union[Set[str], bool] = None):
|
||||
self.name = technology_name
|
||||
self.factorio_id = factorio_id
|
||||
self.progressive = progressive
|
||||
self.has_modifier = has_modifier
|
||||
if modifiers is None:
|
||||
modifiers = []
|
||||
self.modifiers = modifiers
|
||||
if unlocks:
|
||||
self.unlocks = unlocks
|
||||
else:
|
||||
@@ -82,6 +84,10 @@ class Technology(FactorioElement): # maybe make subclass of Location?
|
||||
def __hash__(self):
|
||||
return self.factorio_id
|
||||
|
||||
@property
|
||||
def has_modifier(self) -> bool:
|
||||
return bool(self.modifiers)
|
||||
|
||||
def get_custom(self, world, allowed_packs: Set[str], player: int) -> CustomTechnology:
|
||||
return CustomTechnology(self, world, allowed_packs, player)
|
||||
|
||||
@@ -191,13 +197,14 @@ class Machine(FactorioElement):
|
||||
|
||||
|
||||
recipe_sources: Dict[str, Set[str]] = {} # recipe_name -> technology source
|
||||
mining_with_fluid_sources: set[str] = set()
|
||||
|
||||
# recipes and technologies can share names in Factorio
|
||||
for technology_name, data in sorted(techs_future.result().items()):
|
||||
technology = Technology(
|
||||
technology_name,
|
||||
factorio_tech_id,
|
||||
has_modifier=data["has_modifier"],
|
||||
modifiers=data.get("modifiers", []),
|
||||
unlocks=set(data["unlocks"]) - start_unlocked_recipes,
|
||||
)
|
||||
factorio_tech_id += 1
|
||||
@@ -205,7 +212,8 @@ for technology_name, data in sorted(techs_future.result().items()):
|
||||
technology_table[technology_name] = technology
|
||||
for recipe_name in technology.unlocks:
|
||||
recipe_sources.setdefault(recipe_name, set()).add(technology_name)
|
||||
|
||||
if "mining-with-fluid" in technology.modifiers:
|
||||
mining_with_fluid_sources.add(technology_name)
|
||||
del techs_future
|
||||
|
||||
recipes = {}
|
||||
@@ -221,6 +229,8 @@ for resource_name, resource_data in resources_future.result().items():
|
||||
"energy": resource_data["mining_time"],
|
||||
"category": resource_data["category"]
|
||||
}
|
||||
if "required_fluid" in resource_data:
|
||||
recipe_sources.setdefault(f"mining-{resource_name}", set()).update(mining_with_fluid_sources)
|
||||
del resources_future
|
||||
|
||||
for recipe_name, recipe_data in raw_recipes.items():
|
||||
@@ -431,7 +441,9 @@ for root in sorted_rows:
|
||||
factorio_tech_id += 1
|
||||
progressive_technology = Technology(root, factorio_tech_id,
|
||||
tuple(progressive),
|
||||
has_modifier=any(technology_table[tech].has_modifier for tech in progressive),
|
||||
modifiers=sorted(set.union(
|
||||
*(set(technology_table[tech].modifiers) for tech in progressive)
|
||||
)),
|
||||
unlocks=any(technology_table[tech].unlocks for tech in progressive),)
|
||||
progressive_tech_table[root] = progressive_technology.factorio_id
|
||||
progressive_technology_table[root] = progressive_technology
|
||||
|
||||
@@ -445,6 +445,10 @@ end
|
||||
|
||||
script.on_event(defines.events.on_player_main_inventory_changed, update_player_event)
|
||||
|
||||
-- Update players when the cutscene is cancelled or finished. (needed for skins_factored)
|
||||
script.on_event(defines.events.on_cutscene_cancelled, update_player_event)
|
||||
script.on_event(defines.events.on_cutscene_finished, update_player_event)
|
||||
|
||||
function add_samples(force, name, count)
|
||||
local function add_to_table(t)
|
||||
if count <= 0 then
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% from "macros.lua" import dict_to_recipe, variable_to_lua %}
|
||||
-- this file gets written automatically by the Archipelago Randomizer and is in its raw form a Jinja2 Template
|
||||
require('lib')
|
||||
data.raw["item"]["rocket-part"].hidden = false
|
||||
data.raw["rocket-silo"]["rocket-silo"].fluid_boxes = {
|
||||
{
|
||||
production_type = "input",
|
||||
@@ -162,6 +163,7 @@ data.raw["ammo"]["artillery-shell"].stack_size = 10
|
||||
{# each randomized tech gets set to be invisible, with new nodes added that trigger those #}
|
||||
{%- for original_tech_name in base_tech_table -%}
|
||||
technologies["{{ original_tech_name }}"].hidden = true
|
||||
technologies["{{ original_tech_name }}"].hidden_in_factoriopedia = true
|
||||
{% endfor %}
|
||||
{%- for location, item in locations %}
|
||||
{#- the tech researched by the local player #}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -260,7 +260,8 @@ def create_items(self) -> None:
|
||||
items.append(i)
|
||||
|
||||
for item_group in ("Key Items", "Spells", "Armors", "Helms", "Shields", "Accessories", "Weapons"):
|
||||
for item in self.item_name_groups[item_group]:
|
||||
# Sort for deterministic order
|
||||
for item in sorted(self.item_name_groups[item_group]):
|
||||
add_item(item)
|
||||
|
||||
if self.options.brown_boxes == "include":
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Final Fantasy Mystic Quest
|
||||
|
||||
## Game page in other languages:
|
||||
* [Français](/games/Final%20Fantasy%20Mystic%20Quest/info/fr)
|
||||
* [Français](/games/Final%20Fantasy%20Mystic%20Quest/info/fr)
|
||||
|
||||
## Where is the options page?
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ including the exclamation point.
|
||||
- `!countdown <number of seconds>` Starts a countdown using the given seconds value. Useful for synchronizing starts.
|
||||
Defaults to 10 seconds if no argument is provided.
|
||||
- `!alias <alias>` Sets your alias, which allows you to use commands with the alias rather than your provided name.
|
||||
`!alias` on its own will reset the alias to the player's original name.
|
||||
- `!admin <command>` Executes a command as if you typed it into the server console. Remote administration must be
|
||||
enabled.
|
||||
|
||||
@@ -65,6 +66,7 @@ including the exclamation point.
|
||||
argument is provided.
|
||||
- `/option <option name> <option value>` Set a server option. For a list of options, use the `/options` command.
|
||||
- `/alias <player name> <alias name>` Assign a player an alias, allowing you to reference the player by the alias in commands.
|
||||
`!alias <player name>` on its own will reset the alias to the player's original name.
|
||||
|
||||
|
||||
### Collect/Release
|
||||
|
||||
@@ -132,7 +132,13 @@ splitter_pattern = re.compile(r'(?<!^)(?=[A-Z])')
|
||||
for option_name, option_data in pool_options.items():
|
||||
extra_data = {"__module__": __name__, "items": option_data[0], "locations": option_data[1]}
|
||||
if option_name in option_docstrings:
|
||||
extra_data["__doc__"] = option_docstrings[option_name]
|
||||
if option_name == "RandomizeFocus":
|
||||
# pool options for focus are just lying
|
||||
count = 1
|
||||
else:
|
||||
count = len([loc for loc in option_data[1] if loc != "Start"])
|
||||
extra_data["__doc__"] = option_docstrings[option_name] + \
|
||||
f"\n This option adds approximately {count} location{'s' if count != 1 else ''}."
|
||||
if option_name in default_on:
|
||||
option = type(option_name, (DefaultOnToggle,), extra_data)
|
||||
else:
|
||||
@@ -213,6 +219,7 @@ class MaximumEssencePrice(MinimumEssencePrice):
|
||||
class MinimumEggPrice(Range):
|
||||
"""The minimum rancid egg price in the range of prices that an item should cost from Jiji.
|
||||
Only takes effect if the EggSlotShops option is greater than 0."""
|
||||
rich_text_doc = False
|
||||
display_name = "Minimum Egg Price"
|
||||
range_start = 1
|
||||
range_end = 20
|
||||
@@ -222,6 +229,7 @@ class MinimumEggPrice(Range):
|
||||
class MaximumEggPrice(MinimumEggPrice):
|
||||
"""The maximum rancid egg price in the range of prices that an item should cost from Jiji.
|
||||
Only takes effect if the EggSlotShops option is greater than 0."""
|
||||
rich_text_doc = False
|
||||
display_name = "Maximum Egg Price"
|
||||
default = 10
|
||||
|
||||
@@ -265,6 +273,7 @@ class RandomCharmCosts(NamedRange):
|
||||
Set to -1 or vanilla for vanilla costs.
|
||||
Set to -2 or shuffle to shuffle around the vanilla costs to different charms."""
|
||||
|
||||
rich_text_doc = False
|
||||
display_name = "Randomize Charm Notch Costs"
|
||||
range_start = 0
|
||||
range_end = 240
|
||||
@@ -437,6 +446,7 @@ class Goal(Choice):
|
||||
class GrubHuntGoal(NamedRange):
|
||||
"""The amount of grubs required to finish Grub Hunt.
|
||||
On 'All' any grubs from item links replacements etc. will be counted"""
|
||||
rich_text_doc = False
|
||||
display_name = "Grub Hunt Goal"
|
||||
range_start = 1
|
||||
range_end = 46
|
||||
@@ -446,7 +456,7 @@ class GrubHuntGoal(NamedRange):
|
||||
|
||||
class WhitePalace(Choice):
|
||||
"""
|
||||
Whether or not to include White Palace or not. Note: Even if excluded, the King Fragment check may still be
|
||||
Whether or not to include White Palace or not. Note: Even if excluded, the King Fragment check may still be
|
||||
required if charms are vanilla.
|
||||
"""
|
||||
display_name = "White Palace"
|
||||
@@ -483,6 +493,7 @@ class DeathLinkShade(Choice):
|
||||
** Self-death shade behavior is not changed; if a self-death normally creates a shade in vanilla, it will override
|
||||
your existing shade, if any.
|
||||
"""
|
||||
rich_text_doc = False
|
||||
option_vanilla = 0
|
||||
option_shadeless = 1
|
||||
option_shade = 2
|
||||
@@ -497,6 +508,7 @@ class DeathLinkBreaksFragileCharms(Toggle):
|
||||
** Self-death fragile charm behavior is not changed; if a self-death normally breaks fragile charms in vanilla, it
|
||||
will continue to do so.
|
||||
"""
|
||||
rich_text_doc = False
|
||||
display_name = "Deathlink Breaks Fragile Charms"
|
||||
|
||||
|
||||
@@ -515,6 +527,7 @@ class CostSanity(Choice):
|
||||
|
||||
These costs can be in Geo (except Grubfather, Seer and Eggshop), Grubs, Charms, Essence and/or Rancid Eggs
|
||||
"""
|
||||
rich_text_doc = False
|
||||
option_off = 0
|
||||
alias_no = 0
|
||||
option_on = 1
|
||||
|
||||
@@ -134,7 +134,9 @@ shop_cost_types: typing.Dict[str, typing.Tuple[str, ...]] = {
|
||||
|
||||
|
||||
class HKWeb(WebWorld):
|
||||
setup_en = Tutorial(
|
||||
rich_text_options_doc = True
|
||||
|
||||
setup_en = Tutorial(
|
||||
"Mod Setup and Use Guide",
|
||||
"A guide to playing Hollow Knight with Archipelago.",
|
||||
"English",
|
||||
@@ -143,7 +145,7 @@ class HKWeb(WebWorld):
|
||||
["Ijwu"]
|
||||
)
|
||||
|
||||
setup_pt_br = Tutorial(
|
||||
setup_pt_br = Tutorial(
|
||||
setup_en.tutorial_name,
|
||||
setup_en.description,
|
||||
"Português Brasileiro",
|
||||
|
||||
@@ -7,18 +7,21 @@
|
||||
|
||||
<h2 style="text-transform:none";>Required Software:</h2>
|
||||
|
||||
`Kingdom Hearts II Final Mix` from the [Epic Games Store](https://store.epicgames.com/en-US/discover/kingdom-hearts) or [Steam](https://store.steampowered.com/app/2552430/KINGDOM_HEARTS_HD_1525_ReMIX/)
|
||||
Kingdom Hearts II Final Mix from the [Epic Games Store](https://store.epicgames.com/en-US/discover/kingdom-hearts) or [Steam](https://store.steampowered.com/app/2552430/KINGDOM_HEARTS_HD_1525_ReMIX/)
|
||||
|
||||
- Follow this Guide to set up these requirements [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/)
|
||||
1. `Version 3.3.0 or greater OpenKH Mod Manager with Panacea`
|
||||
2. `Lua Backend from the OpenKH Mod Manager`
|
||||
3. `Install the mod KH2FM-Mods-Num/GoA-ROM-Edition using OpenKH Mod Manager`
|
||||
1. Version 3.4.0 or greater OpenKH Mod Manager with Panacea
|
||||
2. Lua Backend from the OpenKH Mod Manager
|
||||
3. Install the mod `KH2FM-Mods-Num/GoA-ROM-Edition` using OpenKH Mod Manager
|
||||
- Needed for Archipelago
|
||||
1. [`ArchipelagoKH2Client.exe`](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
2. `Install the Archipelago Companion mod from JaredWeakStrike/APCompanion using OpenKH Mod Manager`
|
||||
3. `Install the Archipelago Quality Of Life mod from JaredWeakStrike/AP_QOL using OpenKH Mod Manager`
|
||||
4. `Install the mod from KH2FM-Mods-equations19/auto-save using OpenKH Mod Manager`
|
||||
5. `AP Randomizer Seed`
|
||||
1. [ArchipelagoKH2Client.exe](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
2. Install the Archipelago Companion mod from `JaredWeakStrike/APCompanion` using OpenKH Mod Manager
|
||||
3. Install the mod from `KH2FM-Mods-equations19/auto-save` using OpenKH Mod Manager
|
||||
4. Install the mod from `KH2FM-Mods-equations19/KH2-Lua-Library` using OpenKH Mod Manager
|
||||
5. AP Randomizer Seed
|
||||
- Optional Quality of Life Mods for Archipelago
|
||||
1. Optionally Install the Archipelago Quality Of Life mod from `JaredWeakStrike/AP_QOL` using OpenKH Mod Manager
|
||||
2. Optionally Install the Quality Of Life mod from `shananas/BearSkip` using OpenKH Mod Manager
|
||||
|
||||
<h3 style="text-transform:none";>Required: Archipelago Companion Mod</h3>
|
||||
|
||||
@@ -26,15 +29,21 @@ Load this mod just like the <b>GoA ROM</b> you did during the KH2 Rando setup. `
|
||||
Have this mod second-highest priority below the .zip seed.<br>
|
||||
This mod is based upon Num's Garden of Assemblege Mod and requires it to work. Without Num this could not be possible.
|
||||
|
||||
<h3 style="text-transform:none";>Required: Auto Save Mod</h3>
|
||||
<h3 style="text-transform:none";>Required: Auto Save Mod and KH2 Lua Library</h3>
|
||||
|
||||
Load this mod just like the GoA ROM you did during the KH2 Rando setup. `KH2FM-Mods-equations19/auto-save` Location doesn't matter, required in case of crashes. See [Best Practices](en#best-practices) on how to load the auto save
|
||||
Load these mods just like you loaded the GoA ROM mod during the KH2 Rando setup. `KH2FM-Mods-equations19/auto-save` and `KH2FM-Mods-equations19/KH2-Lua-Library` Location doesn't matter, required in case of crashes. See [Best Practices](en#best-practices) on how to load the auto save
|
||||
|
||||
<h3 style="text-transform:none";>Optional QoL Mods: AP QoL and Bear Skip</h3>
|
||||
|
||||
`JaredWeakStrike/AP_QOL` Makes the urns minigames much faster, makes Cavern of Remembrance orbs drop significantly more drive orbs for refilling drive/leveling master form, skips the animation when using the bulky vendor RC, skips carpet escape auto scroller in Agrabah 2, and prevents the wardrobe in the Beasts Castle wardrobe push minigame from waking up while being pushed.
|
||||
|
||||
`shananas/BearSkip` Skips all minigames in 100 Acre Woods except the Spooky Cave minigame since there are chests in Spooky Cave you can only get during the minigame. For Spooky Cave, Pooh is moved to the other side of the invisible wall that prevents you from using his RC to finish the minigame.
|
||||
|
||||
<h3 style="text-transform:none";>Installing A Seed</h3>
|
||||
|
||||
When you generate a game you will see a download link for a KH2 .zip seed on the room page. Download the seed then open OpenKH Mod Manager and click the green plus and `Select and install Mod Archive`.<br>
|
||||
When you generate a game you will see a download link for a KH2 .zip seed on the room page. Download the seed then open OpenKH Mod Manager and click the green plus and "Select and install Mod Archive".<br>
|
||||
Make sure the seed is on the top of the list (Highest Priority)<br>
|
||||
After Installing the seed click `Mod Loader -> Build/Build and Run`. Every slot is a unique mod to install and will be needed be repatched for different slots/rooms.
|
||||
After Installing the seed click "Mod Loader -> Build/Build and Run". Every slot is a unique mod to install and will be needed be repatched for different slots/rooms.
|
||||
|
||||
<h2 style="text-transform:none";>Optional Software:</h2>
|
||||
|
||||
@@ -48,18 +57,21 @@ After Installing the seed click `Mod Loader -> Build/Build and Run`. Every slot
|
||||
|
||||
<h2 style="text-transform:none";>Using the KH2 Client</h2>
|
||||
|
||||
Once you have started the game through OpenKH Mod Manager and are on the title screen run the [ArchipelagoKH2Client.exe](https://github.com/ArchipelagoMW/Archipelago/releases).<br>
|
||||
Start the game through OpenKH Mod Manager. If starting a new run, enter the Garden of Assemblage from a new save. If returning to a run, load the save and enter the Garden of Assemblage. Then run the [ArchipelagoKH2Client.exe](https://github.com/ArchipelagoMW/Archipelago/releases).<br>
|
||||
When you successfully connect to the server the client will automatically hook into the game to send/receive checks. <br>
|
||||
If the client ever loses connection to the game, it will also disconnect from the server and you will need to reconnect.<br>
|
||||
`Make sure the game is open whenever you try to connect the client to the server otherwise it will immediately disconnect you.`<br>
|
||||
|
||||
Make sure the game is open whenever you try to connect the client to the server otherwise it will immediately disconnect you.<br>
|
||||
|
||||
Most checks will be sent to you anywhere outside a load or cutscene.<br>
|
||||
`If you obtain magic, you will need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable.`
|
||||
|
||||
If you obtain magic, you will need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable.
|
||||
|
||||
<h2 style="text-transform:none";>KH2 Client should look like this: </h2>
|
||||
|
||||

|
||||
|
||||
Enter `The room's port number` into the top box <b> where the x's are</b> and press "Connect". Follow the prompts there and you should be connected
|
||||
Enter The room's port number into the top box <b> where the x's are</b> and press "Connect". Follow the prompts there and you should be connected
|
||||
|
||||
<h2 style="text-transform:none";>Common Pitfalls</h2>
|
||||
|
||||
@@ -102,7 +114,7 @@ This pack will handle logic, received items, checked locations and autotabbing f
|
||||
- Why is my Client giving me a "Cannot Open Process: " error?
|
||||
- Due to how the client reads kingdom hearts 2 memory some people's computer flags it as a virus. Run the client as admin.
|
||||
- Why is my HP/MP continuously increasing without stopping?
|
||||
- You do not have `JaredWeakStrike/APCompanion` set up correctly. Make sure it is above the `GoA ROM Mod` in the mod manager.
|
||||
- You do not have `JaredWeakStrike/APCompanion` set up correctly. Make sure it is above the GoA ROM Edition Mod in the mod manager.
|
||||
- Why is my HP/MP continuously increasing without stopping when I have the APCompanion Mod?
|
||||
- You have a leftover GOA lua script in your `Documents\KINGDOM HEARTS HD 1.5+2.5 ReMIX\scripts\KH2`.
|
||||
- Why am I missing worlds/portals in the GoA?
|
||||
@@ -110,9 +122,9 @@ This pack will handle logic, received items, checked locations and autotabbing f
|
||||
- Why did I not load into the correct visit?
|
||||
- You need to trigger a cutscene or visit The World That Never Was for it to register that you have received the item.
|
||||
- What versions of Kingdom Hearts 2 are supported?
|
||||
- Currently the `only` supported versions are `Epic Games Version 1.0.0.9_WW` and `Steam Build Version 14716933`.
|
||||
- Currently the only supported versions are Epic Games Version 1.0.0.10_WW and Steam Build Version 15194255.
|
||||
- Why am I getting wallpapered while going into a world for the first time?
|
||||
- Your `Lua Backend` was not configured correctly. Look over the step in the [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/) guide.
|
||||
- Your Lua Backend was not configured correctly. Look over the step in the [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/) guide.
|
||||
- Why am I not getting magic?
|
||||
- If you obtain magic, you will need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable.
|
||||
- Why did I crash after picking my dream weapon?
|
||||
@@ -124,7 +136,7 @@ This pack will handle logic, received items, checked locations and autotabbing f
|
||||
- You will need to get the `JaredWeakStrike/APCompanion` (you can find how to get this if you scroll up)
|
||||
- Why am I not sending or receiving items?
|
||||
- Make sure you are connected to the KH2 client and the correct room (for more information scroll up)
|
||||
- Why should I install the auto save mod at `KH2FM-Mods-equations19/auto-save`?
|
||||
- Because Kingdom Hearts 2 is prone to crashes and will keep you from losing your progress.
|
||||
- Why should I install the auto save mod at `KH2FM-Mods-equations19/auto-save` and `KH2FM-Mods-equations19/KH2-Lua-Library`?
|
||||
- Because Kingdom Hearts 2 is prone to crashes and will keep you from losing your progress. Both mods are needed for auto save to work.
|
||||
- How do I load an auto save?
|
||||
- To load an auto-save, hold down the Select or your equivalent on your prefered controller while choosing a file. Make sure to hold the button down the whole time.
|
||||
|
||||
@@ -381,7 +381,7 @@ class MessengerWorld(World):
|
||||
return
|
||||
# the messenger client calls into AP with specific args, so check the out path matches what the client sends
|
||||
out_path = output_path(multiworld.get_out_file_name_base(1) + ".aptm")
|
||||
if "The Messenger\\Archipelago\\output" not in out_path:
|
||||
if "Messenger\\Archipelago\\output" not in out_path:
|
||||
return
|
||||
import orjson
|
||||
data = {
|
||||
|
||||
@@ -749,8 +749,8 @@ location_data = [
|
||||
LocationData("Cinnabar Gym", "Super Nerd 2", None, rom_addresses["Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_2_ITEM"], EventFlag(372), inclusion=trainersanity),
|
||||
LocationData("Cinnabar Gym", "Burglar 2", None, rom_addresses["Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_3_ITEM"], EventFlag(371), inclusion=trainersanity),
|
||||
LocationData("Cinnabar Gym", "Super Nerd 3", None, rom_addresses["Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_4_ITEM"], EventFlag(370), inclusion=trainersanity),
|
||||
LocationData("Cinnabar Gym", "Super Nerd 4", None, rom_addresses["Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_5_ITEM"], EventFlag(369), inclusion=trainersanity),
|
||||
LocationData("Cinnabar Gym", "Super Nerd 5", None, rom_addresses["Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_6_ITEM"], EventFlag(368), inclusion=trainersanity),
|
||||
LocationData("Cinnabar Gym", "Burglar 3", None, rom_addresses["Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_5_ITEM"], EventFlag(369), inclusion=trainersanity),
|
||||
LocationData("Cinnabar Gym", "Super Nerd 4", None, rom_addresses["Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_6_ITEM"], EventFlag(368), inclusion=trainersanity),
|
||||
|
||||
LocationData("Celadon Prize Corner", "Item Prize 1", "TM23 Dragon Rage", rom_addresses["Prize_Item_A"], EventFlag(0x69a), inclusion=prizesanity),
|
||||
LocationData("Celadon Prize Corner", "Item Prize 2", "TM15 Hyper Beam", rom_addresses["Prize_Item_B"], EventFlag(0x69B), inclusion=prizesanity),
|
||||
|
||||
@@ -1718,7 +1718,7 @@ def create_regions(world):
|
||||
connect(multiworld, player, "Vermilion City", "Vermilion City-Dock", lambda state: state.has("S.S. Ticket", player))
|
||||
connect(multiworld, player, "Vermilion City", "Route 11")
|
||||
connect(multiworld, player, "Route 12-N", "Route 12-S", lambda state: logic.can_surf(state, world, player))
|
||||
connect(multiworld, player, "Route 12-W", "Route 11-E", lambda state: state.has("Poke Flute", player))
|
||||
connect(multiworld, player, "Route 12-W", "Route 11-E")
|
||||
connect(multiworld, player, "Route 12-W", "Route 12-N", lambda state: state.has("Poke Flute", player))
|
||||
connect(multiworld, player, "Route 12-W", "Route 12-S", lambda state: state.has("Poke Flute", player))
|
||||
connect(multiworld, player, "Route 12-S", "Route 12-Grass", lambda state: logic.can_cut(state, world, player), one_way=True)
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
import os
|
||||
import json
|
||||
import os
|
||||
import pkgutil
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def load_data_file(*args) -> dict:
|
||||
fname = "/".join(["data", *args])
|
||||
return json.loads(pkgutil.get_data(__name__, fname).decode())
|
||||
|
||||
|
||||
def relative_years_from_today(dt2: datetime) -> int:
|
||||
today = datetime.now()
|
||||
years = today.year - dt2.year
|
||||
if today.month < dt2.month or (today.month == dt2.month and today.day < dt2.day):
|
||||
years -= 1
|
||||
return years
|
||||
|
||||
|
||||
location_id_offset: int = 27000
|
||||
|
||||
location_info = load_data_file("locations.json")
|
||||
location_name_to_id = {name: location_id_offset + index \
|
||||
for index, name in enumerate(location_info["all_locations"])}
|
||||
|
||||
location_name_to_id = {name: location_id_offset + index for index, name in enumerate(location_info["all_locations"])}
|
||||
exclusion_info = load_data_file("excluded_locations.json")
|
||||
|
||||
region_info = load_data_file("regions.json")
|
||||
years_since_sep_30_1980 = relative_years_from_today(datetime.fromisoformat("1980-09-30"))
|
||||
|
||||
@@ -1,132 +1,198 @@
|
||||
import enum
|
||||
from typing import NamedTuple, Optional
|
||||
|
||||
from BaseClasses import Item, ItemClassification
|
||||
import typing
|
||||
from . import Constants
|
||||
|
||||
|
||||
class ShiversItem(Item):
|
||||
game: str = "Shivers"
|
||||
|
||||
class ItemData(typing.NamedTuple):
|
||||
code: int
|
||||
type: str
|
||||
|
||||
class ItemType(enum.Enum):
|
||||
POT = "pot"
|
||||
POT_COMPLETE = "pot-complete"
|
||||
POT_DUPLICATE = "pot-duplicate"
|
||||
POT_COMPLETE_DUPLICATE = "pot-complete-duplicate"
|
||||
KEY = "key"
|
||||
KEY_OPTIONAL = "key-optional"
|
||||
ABILITY = "ability"
|
||||
FILLER = "filler"
|
||||
IXUPI_AVAILABILITY = "ixupi-availability"
|
||||
GOAL = "goal"
|
||||
|
||||
|
||||
class ItemData(NamedTuple):
|
||||
code: Optional[int]
|
||||
type: ItemType
|
||||
classification: ItemClassification = ItemClassification.progression
|
||||
|
||||
|
||||
SHIVERS_ITEM_ID_OFFSET = 27000
|
||||
|
||||
item_table = {
|
||||
#Pot Pieces
|
||||
"Water Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 0, "pot"),
|
||||
"Wax Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 1, "pot"),
|
||||
"Ash Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 2, "pot"),
|
||||
"Oil Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 3, "pot"),
|
||||
"Cloth Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 4, "pot"),
|
||||
"Wood Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 5, "pot"),
|
||||
"Crystal Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 6, "pot"),
|
||||
"Lightning Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 7, "pot"),
|
||||
"Sand Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 8, "pot"),
|
||||
"Metal Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 9, "pot"),
|
||||
"Water Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 10, "pot"),
|
||||
"Wax Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 11, "pot"),
|
||||
"Ash Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 12, "pot"),
|
||||
"Oil Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 13, "pot"),
|
||||
"Cloth Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 14, "pot"),
|
||||
"Wood Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 15, "pot"),
|
||||
"Crystal Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 16, "pot"),
|
||||
"Lightning Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 17, "pot"),
|
||||
"Sand Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 18, "pot"),
|
||||
"Metal Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 19, "pot"),
|
||||
"Water Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 20, "pot_type2"),
|
||||
"Wax Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 21, "pot_type2"),
|
||||
"Ash Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 22, "pot_type2"),
|
||||
"Oil Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 23, "pot_type2"),
|
||||
"Cloth Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 24, "pot_type2"),
|
||||
"Wood Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 25, "pot_type2"),
|
||||
"Crystal Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 26, "pot_type2"),
|
||||
"Lightning Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 27, "pot_type2"),
|
||||
"Sand Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 28, "pot_type2"),
|
||||
"Metal Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 29, "pot_type2"),
|
||||
|
||||
#Keys
|
||||
"Key for Office Elevator": ItemData(SHIVERS_ITEM_ID_OFFSET + 30, "key"),
|
||||
"Key for Bedroom Elevator": ItemData(SHIVERS_ITEM_ID_OFFSET + 31, "key"),
|
||||
"Key for Three Floor Elevator": ItemData(SHIVERS_ITEM_ID_OFFSET + 32, "key"),
|
||||
"Key for Workshop": ItemData(SHIVERS_ITEM_ID_OFFSET + 33, "key"),
|
||||
"Key for Office": ItemData(SHIVERS_ITEM_ID_OFFSET + 34, "key"),
|
||||
"Key for Prehistoric Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 35, "key"),
|
||||
"Key for Greenhouse Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 36, "key"),
|
||||
"Key for Ocean Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 37, "key"),
|
||||
"Key for Projector Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 38, "key"),
|
||||
"Key for Generator Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 39, "key"),
|
||||
"Key for Egypt Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 40, "key"),
|
||||
"Key for Library Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 41, "key"),
|
||||
"Key for Shaman Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 42, "key"),
|
||||
"Key for UFO Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 43, "key"),
|
||||
"Key for Torture Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 44, "key"),
|
||||
"Key for Puzzle Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 45, "key"),
|
||||
"Key for Bedroom": ItemData(SHIVERS_ITEM_ID_OFFSET + 46, "key"),
|
||||
"Key for Underground Lake Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 47, "key"),
|
||||
"Key for Janitor Closet": ItemData(SHIVERS_ITEM_ID_OFFSET + 48, "key"),
|
||||
"Key for Front Door": ItemData(SHIVERS_ITEM_ID_OFFSET + 49, "key-optional"),
|
||||
|
||||
#Abilities
|
||||
"Crawling": ItemData(SHIVERS_ITEM_ID_OFFSET + 50, "ability"),
|
||||
|
||||
#Event Items
|
||||
"Victory": ItemData(SHIVERS_ITEM_ID_OFFSET + 60, "victory"),
|
||||
|
||||
#Duplicate pot pieces for fill_Restrictive
|
||||
"Water Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 70, "potduplicate"),
|
||||
"Wax Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 71, "potduplicate"),
|
||||
"Ash Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 72, "potduplicate"),
|
||||
"Oil Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 73, "potduplicate"),
|
||||
"Cloth Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 74, "potduplicate"),
|
||||
"Wood Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 75, "potduplicate"),
|
||||
"Crystal Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 76, "potduplicate"),
|
||||
"Lightning Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 77, "potduplicate"),
|
||||
"Sand Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 78, "potduplicate"),
|
||||
"Metal Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 79, "potduplicate"),
|
||||
"Water Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 80, "potduplicate"),
|
||||
"Wax Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 81, "potduplicate"),
|
||||
"Ash Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 82, "potduplicate"),
|
||||
"Oil Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 83, "potduplicate"),
|
||||
"Cloth Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 84, "potduplicate"),
|
||||
"Wood Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 85, "potduplicate"),
|
||||
"Crystal Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 86, "potduplicate"),
|
||||
"Lightning Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 87, "potduplicate"),
|
||||
"Sand Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 88, "potduplicate"),
|
||||
"Metal Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 89, "potduplicate"),
|
||||
"Water Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 140, "potduplicate_type2"),
|
||||
"Wax Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 141, "potduplicate_type2"),
|
||||
"Ash Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 142, "potduplicate_type2"),
|
||||
"Oil Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 143, "potduplicate_type2"),
|
||||
"Cloth Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 144, "potduplicate_type2"),
|
||||
"Wood Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 145, "potduplicate_type2"),
|
||||
"Crystal Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 146, "potduplicate_type2"),
|
||||
"Lightning Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 147, "potduplicate_type2"),
|
||||
"Sand Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 148, "potduplicate_type2"),
|
||||
"Metal Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 149, "potduplicate_type2"),
|
||||
|
||||
#Filler
|
||||
"Empty": ItemData(SHIVERS_ITEM_ID_OFFSET + 90, "filler"),
|
||||
"Easier Lyre": ItemData(SHIVERS_ITEM_ID_OFFSET + 91, "filler", ItemClassification.filler),
|
||||
"Water Always Available in Lobby": ItemData(SHIVERS_ITEM_ID_OFFSET + 92, "filler2", ItemClassification.filler),
|
||||
"Wax Always Available in Library": ItemData(SHIVERS_ITEM_ID_OFFSET + 93, "filler2", ItemClassification.filler),
|
||||
"Wax Always Available in Anansi Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 94, "filler2", ItemClassification.filler),
|
||||
"Wax Always Available in Shaman Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 95, "filler2", ItemClassification.filler),
|
||||
"Ash Always Available in Office": ItemData(SHIVERS_ITEM_ID_OFFSET + 96, "filler2", ItemClassification.filler),
|
||||
"Ash Always Available in Burial Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 97, "filler2", ItemClassification.filler),
|
||||
"Oil Always Available in Prehistoric Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 98, "filler2", ItemClassification.filler),
|
||||
"Cloth Always Available in Egypt": ItemData(SHIVERS_ITEM_ID_OFFSET + 99, "filler2", ItemClassification.filler),
|
||||
"Cloth Always Available in Burial Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 100, "filler2", ItemClassification.filler),
|
||||
"Wood Always Available in Workshop": ItemData(SHIVERS_ITEM_ID_OFFSET + 101, "filler2", ItemClassification.filler),
|
||||
"Wood Always Available in Blue Maze": ItemData(SHIVERS_ITEM_ID_OFFSET + 102, "filler2", ItemClassification.filler),
|
||||
"Wood Always Available in Pegasus Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 103, "filler2", ItemClassification.filler),
|
||||
"Wood Always Available in Gods Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 104, "filler2", ItemClassification.filler),
|
||||
"Crystal Always Available in Lobby": ItemData(SHIVERS_ITEM_ID_OFFSET + 105, "filler2", ItemClassification.filler),
|
||||
"Crystal Always Available in Ocean": ItemData(SHIVERS_ITEM_ID_OFFSET + 106, "filler2", ItemClassification.filler),
|
||||
"Sand Always Available in Greenhouse Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 107, "filler2", ItemClassification.filler),
|
||||
"Sand Always Available in Ocean": ItemData(SHIVERS_ITEM_ID_OFFSET + 108, "filler2", ItemClassification.filler),
|
||||
"Metal Always Available in Projector Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 109, "filler2", ItemClassification.filler),
|
||||
"Metal Always Available in Bedroom": ItemData(SHIVERS_ITEM_ID_OFFSET + 110, "filler2", ItemClassification.filler),
|
||||
"Metal Always Available in Prehistoric": ItemData(SHIVERS_ITEM_ID_OFFSET + 111, "filler2", ItemClassification.filler),
|
||||
"Heal": ItemData(SHIVERS_ITEM_ID_OFFSET + 112, "filler3", ItemClassification.filler)
|
||||
|
||||
# To allow for an item with a name that changes over time (once a year)
|
||||
# while keeping the id unique we can generate a small range of them.
|
||||
goal_items = {
|
||||
f"Mt. Pleasant Tribune: {Constants.years_since_sep_30_1980 + year_offset} year Old Mystery Solved!": ItemData(
|
||||
SHIVERS_ITEM_ID_OFFSET + 100 + Constants.years_since_sep_30_1980 + year_offset, ItemType.GOAL
|
||||
) for year_offset in range(-1, 2)
|
||||
}
|
||||
|
||||
item_table = {
|
||||
# Pot Pieces
|
||||
"Water Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 0, ItemType.POT),
|
||||
"Wax Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 1, ItemType.POT),
|
||||
"Ash Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 2, ItemType.POT),
|
||||
"Oil Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 3, ItemType.POT),
|
||||
"Cloth Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 4, ItemType.POT),
|
||||
"Wood Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 5, ItemType.POT),
|
||||
"Crystal Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 6, ItemType.POT),
|
||||
"Lightning Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 7, ItemType.POT),
|
||||
"Sand Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 8, ItemType.POT),
|
||||
"Metal Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 9, ItemType.POT),
|
||||
"Water Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 10, ItemType.POT),
|
||||
"Wax Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 11, ItemType.POT),
|
||||
"Ash Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 12, ItemType.POT),
|
||||
"Oil Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 13, ItemType.POT),
|
||||
"Cloth Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 14, ItemType.POT),
|
||||
"Wood Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 15, ItemType.POT),
|
||||
"Crystal Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 16, ItemType.POT),
|
||||
"Lightning Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 17, ItemType.POT),
|
||||
"Sand Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 18, ItemType.POT),
|
||||
"Metal Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 19, ItemType.POT),
|
||||
"Water Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 20, ItemType.POT_COMPLETE),
|
||||
"Wax Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 21, ItemType.POT_COMPLETE),
|
||||
"Ash Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 22, ItemType.POT_COMPLETE),
|
||||
"Oil Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 23, ItemType.POT_COMPLETE),
|
||||
"Cloth Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 24, ItemType.POT_COMPLETE),
|
||||
"Wood Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 25, ItemType.POT_COMPLETE),
|
||||
"Crystal Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 26, ItemType.POT_COMPLETE),
|
||||
"Lightning Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 27, ItemType.POT_COMPLETE),
|
||||
"Sand Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 28, ItemType.POT_COMPLETE),
|
||||
"Metal Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 29, ItemType.POT_COMPLETE),
|
||||
|
||||
# Keys
|
||||
"Key for Office Elevator": ItemData(SHIVERS_ITEM_ID_OFFSET + 30, ItemType.KEY),
|
||||
"Key for Bedroom Elevator": ItemData(SHIVERS_ITEM_ID_OFFSET + 31, ItemType.KEY),
|
||||
"Key for Three Floor Elevator": ItemData(SHIVERS_ITEM_ID_OFFSET + 32, ItemType.KEY),
|
||||
"Key for Workshop": ItemData(SHIVERS_ITEM_ID_OFFSET + 33, ItemType.KEY),
|
||||
"Key for Office": ItemData(SHIVERS_ITEM_ID_OFFSET + 34, ItemType.KEY),
|
||||
"Key for Prehistoric Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 35, ItemType.KEY),
|
||||
"Key for Greenhouse": ItemData(SHIVERS_ITEM_ID_OFFSET + 36, ItemType.KEY),
|
||||
"Key for Ocean Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 37, ItemType.KEY),
|
||||
"Key for Projector Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 38, ItemType.KEY),
|
||||
"Key for Generator Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 39, ItemType.KEY),
|
||||
"Key for Egypt Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 40, ItemType.KEY),
|
||||
"Key for Library": ItemData(SHIVERS_ITEM_ID_OFFSET + 41, ItemType.KEY),
|
||||
"Key for Shaman Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 42, ItemType.KEY),
|
||||
"Key for UFO Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 43, ItemType.KEY),
|
||||
"Key for Torture Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 44, ItemType.KEY),
|
||||
"Key for Puzzle Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 45, ItemType.KEY),
|
||||
"Key for Bedroom": ItemData(SHIVERS_ITEM_ID_OFFSET + 46, ItemType.KEY),
|
||||
"Key for Underground Lake": ItemData(SHIVERS_ITEM_ID_OFFSET + 47, ItemType.KEY),
|
||||
"Key for Janitor Closet": ItemData(SHIVERS_ITEM_ID_OFFSET + 48, ItemType.KEY),
|
||||
"Key for Front Door": ItemData(SHIVERS_ITEM_ID_OFFSET + 49, ItemType.KEY_OPTIONAL),
|
||||
|
||||
# Abilities
|
||||
"Crawling": ItemData(SHIVERS_ITEM_ID_OFFSET + 50, ItemType.ABILITY),
|
||||
|
||||
# Duplicate pot pieces for fill_Restrictive
|
||||
"Water Pot Bottom DUPE": ItemData(None, ItemType.POT_DUPLICATE),
|
||||
"Wax Pot Bottom DUPE": ItemData(None, ItemType.POT_DUPLICATE),
|
||||
"Ash Pot Bottom DUPE": ItemData(None, ItemType.POT_DUPLICATE),
|
||||
"Oil Pot Bottom DUPE": ItemData(None, ItemType.POT_DUPLICATE),
|
||||
"Cloth Pot Bottom DUPE": ItemData(None, ItemType.POT_DUPLICATE),
|
||||
"Wood Pot Bottom DUPE": ItemData(None, ItemType.POT_DUPLICATE),
|
||||
"Crystal Pot Bottom DUPE": ItemData(None, ItemType.POT_DUPLICATE),
|
||||
"Lightning Pot Bottom DUPE": ItemData(None, ItemType.POT_DUPLICATE),
|
||||
"Sand Pot Bottom DUPE": ItemData(None, ItemType.POT_DUPLICATE),
|
||||
"Metal Pot Bottom DUPE": ItemData(None, ItemType.POT_DUPLICATE),
|
||||
"Water Pot Top DUPE": ItemData(None, ItemType.POT_DUPLICATE),
|
||||
"Wax Pot Top DUPE": ItemData(None, ItemType.POT_DUPLICATE),
|
||||
"Ash Pot Top DUPE": ItemData(None, ItemType.POT_DUPLICATE),
|
||||
"Oil Pot Top DUPE": ItemData(None, ItemType.POT_DUPLICATE),
|
||||
"Cloth Pot Top DUPE": ItemData(None, ItemType.POT_DUPLICATE),
|
||||
"Wood Pot Top DUPE": ItemData(None, ItemType.POT_DUPLICATE),
|
||||
"Crystal Pot Top DUPE": ItemData(None, ItemType.POT_DUPLICATE),
|
||||
"Lightning Pot Top DUPE": ItemData(None, ItemType.POT_DUPLICATE),
|
||||
"Sand Pot Top DUPE": ItemData(None, ItemType.POT_DUPLICATE),
|
||||
"Metal Pot Top DUPE": ItemData(None, ItemType.POT_DUPLICATE),
|
||||
"Water Pot Complete DUPE": ItemData(None, ItemType.POT_COMPLETE_DUPLICATE),
|
||||
"Wax Pot Complete DUPE": ItemData(None, ItemType.POT_COMPLETE_DUPLICATE),
|
||||
"Ash Pot Complete DUPE": ItemData(None, ItemType.POT_COMPLETE_DUPLICATE),
|
||||
"Oil Pot Complete DUPE": ItemData(None, ItemType.POT_COMPLETE_DUPLICATE),
|
||||
"Cloth Pot Complete DUPE": ItemData(None, ItemType.POT_COMPLETE_DUPLICATE),
|
||||
"Wood Pot Complete DUPE": ItemData(None, ItemType.POT_COMPLETE_DUPLICATE),
|
||||
"Crystal Pot Complete DUPE": ItemData(None, ItemType.POT_COMPLETE_DUPLICATE),
|
||||
"Lightning Pot Complete DUPE": ItemData(None, ItemType.POT_COMPLETE_DUPLICATE),
|
||||
"Sand Pot Complete DUPE": ItemData(None, ItemType.POT_COMPLETE_DUPLICATE),
|
||||
"Metal Pot Complete DUPE": ItemData(None, ItemType.POT_COMPLETE_DUPLICATE),
|
||||
|
||||
# Filler
|
||||
"Empty": ItemData(None, ItemType.FILLER, ItemClassification.filler),
|
||||
"Easier Lyre": ItemData(SHIVERS_ITEM_ID_OFFSET + 91, ItemType.FILLER, ItemClassification.useful),
|
||||
"Water Always Available in Lobby": ItemData(
|
||||
SHIVERS_ITEM_ID_OFFSET + 92, ItemType.IXUPI_AVAILABILITY, ItemClassification.filler
|
||||
),
|
||||
"Wax Always Available in Library": ItemData(
|
||||
SHIVERS_ITEM_ID_OFFSET + 93, ItemType.IXUPI_AVAILABILITY, ItemClassification.filler
|
||||
),
|
||||
"Wax Always Available in Anansi Room": ItemData(
|
||||
SHIVERS_ITEM_ID_OFFSET + 94, ItemType.IXUPI_AVAILABILITY, ItemClassification.filler
|
||||
),
|
||||
"Wax Always Available in Shaman Room": ItemData(
|
||||
SHIVERS_ITEM_ID_OFFSET + 95, ItemType.IXUPI_AVAILABILITY, ItemClassification.filler
|
||||
),
|
||||
"Ash Always Available in Office": ItemData(
|
||||
SHIVERS_ITEM_ID_OFFSET + 96, ItemType.IXUPI_AVAILABILITY, ItemClassification.filler
|
||||
),
|
||||
"Ash Always Available in Burial Room": ItemData(
|
||||
SHIVERS_ITEM_ID_OFFSET + 97, ItemType.IXUPI_AVAILABILITY, ItemClassification.filler
|
||||
),
|
||||
"Oil Always Available in Prehistoric Room": ItemData(
|
||||
SHIVERS_ITEM_ID_OFFSET + 98, ItemType.IXUPI_AVAILABILITY, ItemClassification.filler
|
||||
),
|
||||
"Cloth Always Available in Egypt": ItemData(
|
||||
SHIVERS_ITEM_ID_OFFSET + 99, ItemType.IXUPI_AVAILABILITY, ItemClassification.filler
|
||||
),
|
||||
"Cloth Always Available in Burial Room": ItemData(
|
||||
SHIVERS_ITEM_ID_OFFSET + 100, ItemType.IXUPI_AVAILABILITY, ItemClassification.filler
|
||||
),
|
||||
"Wood Always Available in Workshop": ItemData(
|
||||
SHIVERS_ITEM_ID_OFFSET + 101, ItemType.IXUPI_AVAILABILITY, ItemClassification.filler
|
||||
),
|
||||
"Wood Always Available in Blue Maze": ItemData(
|
||||
SHIVERS_ITEM_ID_OFFSET + 102, ItemType.IXUPI_AVAILABILITY, ItemClassification.filler
|
||||
),
|
||||
"Wood Always Available in Pegasus Room": ItemData(
|
||||
SHIVERS_ITEM_ID_OFFSET + 103, ItemType.IXUPI_AVAILABILITY, ItemClassification.filler
|
||||
),
|
||||
"Wood Always Available in Gods Room": ItemData(
|
||||
SHIVERS_ITEM_ID_OFFSET + 104, ItemType.IXUPI_AVAILABILITY, ItemClassification.filler
|
||||
),
|
||||
"Crystal Always Available in Lobby": ItemData(
|
||||
SHIVERS_ITEM_ID_OFFSET + 105, ItemType.IXUPI_AVAILABILITY, ItemClassification.filler
|
||||
),
|
||||
"Crystal Always Available in Ocean": ItemData(
|
||||
SHIVERS_ITEM_ID_OFFSET + 106, ItemType.IXUPI_AVAILABILITY, ItemClassification.filler
|
||||
),
|
||||
"Sand Always Available in Greenhouse": ItemData(
|
||||
SHIVERS_ITEM_ID_OFFSET + 107, ItemType.IXUPI_AVAILABILITY, ItemClassification.filler
|
||||
),
|
||||
"Sand Always Available in Ocean": ItemData(
|
||||
SHIVERS_ITEM_ID_OFFSET + 108, ItemType.IXUPI_AVAILABILITY, ItemClassification.filler
|
||||
),
|
||||
"Metal Always Available in Projector Room": ItemData(
|
||||
SHIVERS_ITEM_ID_OFFSET + 109, ItemType.IXUPI_AVAILABILITY, ItemClassification.filler
|
||||
),
|
||||
"Metal Always Available in Bedroom": ItemData(
|
||||
SHIVERS_ITEM_ID_OFFSET + 110, ItemType.IXUPI_AVAILABILITY, ItemClassification.filler
|
||||
),
|
||||
"Metal Always Available in Prehistoric": ItemData(
|
||||
SHIVERS_ITEM_ID_OFFSET + 111, ItemType.IXUPI_AVAILABILITY, ItemClassification.filler
|
||||
),
|
||||
"Heal": ItemData(SHIVERS_ITEM_ID_OFFSET + 112, ItemType.FILLER, ItemClassification.filler),
|
||||
|
||||
# Goal items
|
||||
**goal_items
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
from Options import Choice, DefaultOnToggle, Toggle, PerGameCommonOptions, Range
|
||||
from dataclasses import dataclass
|
||||
|
||||
from Options import (
|
||||
Choice, DefaultOnToggle, ExcludeLocations, LocalItems, NonLocalItems, OptionGroup, PerGameCommonOptions,
|
||||
PriorityLocations, Range, StartHints, StartInventory, StartLocationHints, Toggle,
|
||||
)
|
||||
from . import ItemType, item_table
|
||||
from .Constants import location_info
|
||||
|
||||
|
||||
class IxupiCapturesNeeded(Range):
|
||||
"""
|
||||
@@ -11,12 +17,13 @@ class IxupiCapturesNeeded(Range):
|
||||
range_end = 10
|
||||
default = 10
|
||||
|
||||
|
||||
class LobbyAccess(Choice):
|
||||
"""
|
||||
Chooses how keys needed to reach the lobby are placed.
|
||||
- Normal: Keys are placed anywhere
|
||||
- Early: Keys are placed early
|
||||
- Local: Keys are placed locally
|
||||
- Local: Keys are placed locally and early
|
||||
"""
|
||||
display_name = "Lobby Access"
|
||||
option_normal = 0
|
||||
@@ -24,16 +31,19 @@ class LobbyAccess(Choice):
|
||||
option_local = 2
|
||||
default = 1
|
||||
|
||||
|
||||
class PuzzleHintsRequired(DefaultOnToggle):
|
||||
"""
|
||||
If turned on puzzle hints/solutions will be available before the corresponding puzzle is required.
|
||||
|
||||
For example: The Red Door puzzle will be logically required only after access to the Beth's Address Book which gives you the solution.
|
||||
For example: The Red Door puzzle will be logically required only after obtaining access to Beth's Address Book
|
||||
which gives you the solution.
|
||||
|
||||
Turning this off allows for greater randomization.
|
||||
"""
|
||||
display_name = "Puzzle Hints Required"
|
||||
|
||||
|
||||
class InformationPlaques(Toggle):
|
||||
"""
|
||||
Adds Information Plaques as checks.
|
||||
@@ -41,12 +51,14 @@ class InformationPlaques(Toggle):
|
||||
"""
|
||||
display_name = "Include Information Plaques"
|
||||
|
||||
|
||||
class FrontDoorUsable(Toggle):
|
||||
"""
|
||||
Adds a key to unlock the front door of the museum.
|
||||
"""
|
||||
display_name = "Front Door Usable"
|
||||
|
||||
|
||||
class ElevatorsStaySolved(DefaultOnToggle):
|
||||
"""
|
||||
Adds elevators as checks and will remain open upon solving them.
|
||||
@@ -54,12 +66,15 @@ class ElevatorsStaySolved(DefaultOnToggle):
|
||||
"""
|
||||
display_name = "Elevators Stay Solved"
|
||||
|
||||
|
||||
class EarlyBeth(DefaultOnToggle):
|
||||
"""
|
||||
Beth's body is open at the start of the game. This allows any pot piece to be placed in the slide and early checks on the second half of the final riddle.
|
||||
Beth's body is open at the start of the game.
|
||||
This allows any pot piece to be placed in the slide and early checks on the second half of the final riddle.
|
||||
"""
|
||||
display_name = "Early Beth"
|
||||
|
||||
|
||||
class EarlyLightning(Toggle):
|
||||
"""
|
||||
Allows lightning to be captured at any point in the game. You will still need to capture all ten Ixupi for victory.
|
||||
@@ -67,6 +82,7 @@ class EarlyLightning(Toggle):
|
||||
"""
|
||||
display_name = "Early Lightning"
|
||||
|
||||
|
||||
class LocationPotPieces(Choice):
|
||||
"""
|
||||
Chooses where pot pieces will be located within the multiworld.
|
||||
@@ -78,6 +94,8 @@ class LocationPotPieces(Choice):
|
||||
option_own_world = 0
|
||||
option_different_world = 1
|
||||
option_any_world = 2
|
||||
default = 2
|
||||
|
||||
|
||||
class FullPots(Choice):
|
||||
"""
|
||||
@@ -92,6 +110,13 @@ class FullPots(Choice):
|
||||
option_mixed = 2
|
||||
|
||||
|
||||
class IxupiCapturesPriority(DefaultOnToggle):
|
||||
"""
|
||||
Ixupi captures are set to priority locations. This forces a progression item into these locations if possible.
|
||||
"""
|
||||
display_name = "Ixupi Captures are Priority"
|
||||
|
||||
|
||||
class PuzzleCollectBehavior(Choice):
|
||||
"""
|
||||
Defines what happens to puzzles on collect.
|
||||
@@ -107,6 +132,46 @@ class PuzzleCollectBehavior(Choice):
|
||||
default = 1
|
||||
|
||||
|
||||
# Need to override the default options to remove the goal items and goal locations so that they do not show on web.
|
||||
valid_item_keys = [name for name, data in item_table.items() if data.type != ItemType.GOAL and data.code is not None]
|
||||
valid_location_keys = [name for name in location_info["all_locations"] if name != "Mystery Solved"]
|
||||
|
||||
|
||||
class ShiversLocalItems(LocalItems):
|
||||
__doc__ = LocalItems.__doc__
|
||||
valid_keys = valid_item_keys
|
||||
|
||||
|
||||
class ShiversNonLocalItems(NonLocalItems):
|
||||
__doc__ = NonLocalItems.__doc__
|
||||
valid_keys = valid_item_keys
|
||||
|
||||
|
||||
class ShiversStartInventory(StartInventory):
|
||||
__doc__ = StartInventory.__doc__
|
||||
valid_keys = valid_item_keys
|
||||
|
||||
|
||||
class ShiversStartHints(StartHints):
|
||||
__doc__ = StartHints.__doc__
|
||||
valid_keys = valid_item_keys
|
||||
|
||||
|
||||
class ShiversStartLocationHints(StartLocationHints):
|
||||
__doc__ = StartLocationHints.__doc__
|
||||
valid_keys = valid_location_keys
|
||||
|
||||
|
||||
class ShiversExcludeLocations(ExcludeLocations):
|
||||
__doc__ = ExcludeLocations.__doc__
|
||||
valid_keys = valid_location_keys
|
||||
|
||||
|
||||
class ShiversPriorityLocations(PriorityLocations):
|
||||
__doc__ = PriorityLocations.__doc__
|
||||
valid_keys = valid_location_keys
|
||||
|
||||
|
||||
@dataclass
|
||||
class ShiversOptions(PerGameCommonOptions):
|
||||
ixupi_captures_needed: IxupiCapturesNeeded
|
||||
@@ -119,4 +184,27 @@ class ShiversOptions(PerGameCommonOptions):
|
||||
early_lightning: EarlyLightning
|
||||
location_pot_pieces: LocationPotPieces
|
||||
full_pots: FullPots
|
||||
ixupi_captures_priority: IxupiCapturesPriority
|
||||
puzzle_collect_behavior: PuzzleCollectBehavior
|
||||
local_items: ShiversLocalItems
|
||||
non_local_items: ShiversNonLocalItems
|
||||
start_inventory: ShiversStartInventory
|
||||
start_hints: ShiversStartHints
|
||||
start_location_hints: ShiversStartLocationHints
|
||||
exclude_locations: ShiversExcludeLocations
|
||||
priority_locations: ShiversPriorityLocations
|
||||
|
||||
|
||||
shivers_option_groups = [
|
||||
OptionGroup(
|
||||
"Item & Location Options", [
|
||||
ShiversLocalItems,
|
||||
ShiversNonLocalItems,
|
||||
ShiversStartInventory,
|
||||
ShiversStartHints,
|
||||
ShiversStartLocationHints,
|
||||
ShiversExcludeLocations,
|
||||
ShiversPriorityLocations
|
||||
], True,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,66 +1,69 @@
|
||||
from typing import Dict, TYPE_CHECKING
|
||||
from collections.abc import Callable
|
||||
from typing import Dict, TYPE_CHECKING
|
||||
|
||||
from BaseClasses import CollectionState
|
||||
from worlds.generic.Rules import forbid_item
|
||||
from . import Constants
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import ShiversWorld
|
||||
|
||||
|
||||
def water_capturable(state: CollectionState, player: int) -> bool:
|
||||
return state.has_all({"Water Pot Bottom", "Water Pot Top", "Water Pot Bottom DUPE", "Water Pot Top DUPE"}, player) or \
|
||||
state.has_all({"Water Pot Complete", "Water Pot Complete DUPE"}, player)
|
||||
return state.has_all({"Water Pot Bottom", "Water Pot Top", "Water Pot Bottom DUPE", "Water Pot Top DUPE"}, player) \
|
||||
or state.has_all({"Water Pot Complete", "Water Pot Complete DUPE"}, player)
|
||||
|
||||
|
||||
def wax_capturable(state: CollectionState, player: int) -> bool:
|
||||
return state.has_all({"Wax Pot Bottom", "Wax Pot Top", "Wax Pot Bottom DUPE", "Wax Pot Top DUPE"}, player) or \
|
||||
state.has_all({"Wax Pot Complete", "Wax Pot Complete DUPE"}, player)
|
||||
return state.has_all({"Wax Pot Bottom", "Wax Pot Top", "Wax Pot Bottom DUPE", "Wax Pot Top DUPE"}, player) \
|
||||
or state.has_all({"Wax Pot Complete", "Wax Pot Complete DUPE"}, player)
|
||||
|
||||
|
||||
def ash_capturable(state: CollectionState, player: int) -> bool:
|
||||
return state.has_all({"Ash Pot Bottom", "Ash Pot Top", "Ash Pot Bottom DUPE", "Ash Pot Top DUPE"}, player) or \
|
||||
state.has_all({"Ash Pot Complete", "Ash Pot Complete DUPE"}, player)
|
||||
return state.has_all({"Ash Pot Bottom", "Ash Pot Top", "Ash Pot Bottom DUPE", "Ash Pot Top DUPE"}, player) \
|
||||
or state.has_all({"Ash Pot Complete", "Ash Pot Complete DUPE"}, player)
|
||||
|
||||
|
||||
def oil_capturable(state: CollectionState, player: int) -> bool:
|
||||
return state.has_all({"Oil Pot Bottom", "Oil Pot Top", "Oil Pot Bottom DUPE", "Oil Pot Top DUPE"}, player) or \
|
||||
state.has_all({"Oil Pot Complete", "Oil Pot Complete DUPE"}, player)
|
||||
return state.has_all({"Oil Pot Bottom", "Oil Pot Top", "Oil Pot Bottom DUPE", "Oil Pot Top DUPE"}, player) \
|
||||
or state.has_all({"Oil Pot Complete", "Oil Pot Complete DUPE"}, player)
|
||||
|
||||
|
||||
def cloth_capturable(state: CollectionState, player: int) -> bool:
|
||||
return state.has_all({"Cloth Pot Bottom", "Cloth Pot Top", "Cloth Pot Bottom DUPE", "Cloth Pot Top DUPE"}, player) or \
|
||||
state.has_all({"Cloth Pot Complete", "Cloth Pot Complete DUPE"}, player)
|
||||
return state.has_all({"Cloth Pot Bottom", "Cloth Pot Top", "Cloth Pot Bottom DUPE", "Cloth Pot Top DUPE"}, player) \
|
||||
or state.has_all({"Cloth Pot Complete", "Cloth Pot Complete DUPE"}, player)
|
||||
|
||||
|
||||
def wood_capturable(state: CollectionState, player: int) -> bool:
|
||||
return state.has_all({"Wood Pot Bottom", "Wood Pot Top", "Wood Pot Bottom DUPE", "Wood Pot Top DUPE"}, player) or \
|
||||
state.has_all({"Wood Pot Complete", "Wood Pot Complete DUPE"}, player)
|
||||
return state.has_all({"Wood Pot Bottom", "Wood Pot Top", "Wood Pot Bottom DUPE", "Wood Pot Top DUPE"}, player) \
|
||||
or state.has_all({"Wood Pot Complete", "Wood Pot Complete DUPE"}, player)
|
||||
|
||||
|
||||
def crystal_capturable(state: CollectionState, player: int) -> bool:
|
||||
return state.has_all({"Crystal Pot Bottom", "Crystal Pot Top", "Crystal Pot Bottom DUPE", "Crystal Pot Top DUPE"}, player) or \
|
||||
state.has_all({"Crystal Pot Complete", "Crystal Pot Complete DUPE"}, player)
|
||||
return state.has_all(
|
||||
{"Crystal Pot Bottom", "Crystal Pot Top", "Crystal Pot Bottom DUPE", "Crystal Pot Top DUPE"}, player) \
|
||||
or state.has_all({"Crystal Pot Complete", "Crystal Pot Complete DUPE"}, player)
|
||||
|
||||
|
||||
def sand_capturable(state: CollectionState, player: int) -> bool:
|
||||
return state.has_all({"Sand Pot Bottom", "Sand Pot Top", "Sand Pot Bottom DUPE", "Sand Pot Top DUPE"}, player) or \
|
||||
state.has_all({"Sand Pot Complete", "Sand Pot Complete DUPE"}, player)
|
||||
return state.has_all({"Sand Pot Bottom", "Sand Pot Top", "Sand Pot Bottom DUPE", "Sand Pot Top DUPE"}, player) \
|
||||
or state.has_all({"Sand Pot Complete", "Sand Pot Complete DUPE"}, player)
|
||||
|
||||
|
||||
def metal_capturable(state: CollectionState, player: int) -> bool:
|
||||
return state.has_all({"Metal Pot Bottom", "Metal Pot Top", "Metal Pot Bottom DUPE", "Metal Pot Top DUPE"}, player) or \
|
||||
state.has_all({"Metal Pot Complete", "Metal Pot Complete DUPE"}, player)
|
||||
return state.has_all({"Metal Pot Bottom", "Metal Pot Top", "Metal Pot Bottom DUPE", "Metal Pot Top DUPE"}, player) \
|
||||
or state.has_all({"Metal Pot Complete", "Metal Pot Complete DUPE"}, player)
|
||||
|
||||
|
||||
def lightning_capturable(state: CollectionState, player: int) -> bool:
|
||||
return (first_nine_ixupi_capturable(state, player) or state.multiworld.worlds[player].options.early_lightning.value) \
|
||||
and (state.has_all({"Lightning Pot Bottom", "Lightning Pot Top", "Lightning Pot Bottom DUPE", "Lightning Pot Top DUPE"}, player) or \
|
||||
state.has_all({"Lightning Pot Complete", "Lightning Pot Complete DUPE"}, player))
|
||||
def lightning_capturable(state: CollectionState, world: "ShiversWorld", player: int) -> bool:
|
||||
return (first_nine_ixupi_capturable(state, player) or world.options.early_lightning) \
|
||||
and (state.has_all(
|
||||
{"Lightning Pot Bottom", "Lightning Pot Top", "Lightning Pot Bottom DUPE", "Lightning Pot Top DUPE"},
|
||||
player) or state.has_all({"Lightning Pot Complete", "Lightning Pot Complete DUPE"}, player))
|
||||
|
||||
|
||||
def beths_body_available(state: CollectionState, player: int) -> bool:
|
||||
return (first_nine_ixupi_capturable(state, player) or state.multiworld.worlds[player].options.early_beth.value) \
|
||||
and state.can_reach("Generator", "Region", player)
|
||||
def beths_body_available(state: CollectionState, world: "ShiversWorld", player: int) -> bool:
|
||||
return first_nine_ixupi_capturable(state, player) or world.options.early_beth
|
||||
|
||||
|
||||
def first_nine_ixupi_capturable(state: CollectionState, player: int) -> bool:
|
||||
@@ -71,13 +74,22 @@ def first_nine_ixupi_capturable(state: CollectionState, player: int) -> bool:
|
||||
and metal_capturable(state, player)
|
||||
|
||||
|
||||
def all_skull_dials_available(state: CollectionState, player: int) -> bool:
|
||||
return state.can_reach("Prehistoric", "Region", player) and state.can_reach("Tar River", "Region", player) \
|
||||
and state.can_reach("Egypt", "Region", player) and state.can_reach("Burial", "Region", player) \
|
||||
and state.can_reach("Gods Room", "Region", player) and state.can_reach("Werewolf", "Region", player)
|
||||
def all_skull_dials_set(state: CollectionState, player: int) -> bool:
|
||||
return state.has_all([
|
||||
"Set Skull Dial: Prehistoric",
|
||||
"Set Skull Dial: Tar River",
|
||||
"Set Skull Dial: Egypt",
|
||||
"Set Skull Dial: Burial",
|
||||
"Set Skull Dial: Gods Room",
|
||||
"Set Skull Dial: Werewolf"
|
||||
], player)
|
||||
|
||||
|
||||
def get_rules_lookup(player: int):
|
||||
def completion_condition(state: CollectionState, player: int) -> bool:
|
||||
return state.has(f"Mt. Pleasant Tribune: {Constants.years_since_sep_30_1980} year Old Mystery Solved!", player)
|
||||
|
||||
|
||||
def get_rules_lookup(world: "ShiversWorld", player: int):
|
||||
rules_lookup: Dict[str, Dict[str, Callable[[CollectionState], bool]]] = {
|
||||
"entrances": {
|
||||
"To Office Elevator From Underground Blue Tunnels": lambda state: state.has("Key for Office Elevator", player),
|
||||
@@ -90,48 +102,58 @@ def get_rules_lookup(player: int):
|
||||
"To Workshop": lambda state: state.has("Key for Workshop", player),
|
||||
"To Lobby From Office": lambda state: state.has("Key for Office", player),
|
||||
"To Office From Lobby": lambda state: state.has("Key for Office", player),
|
||||
"To Library From Lobby": lambda state: state.has("Key for Library Room", player),
|
||||
"To Lobby From Library": lambda state: state.has("Key for Library Room", player),
|
||||
"To Library From Lobby": lambda state: state.has("Key for Library", player),
|
||||
"To Lobby From Library": lambda state: state.has("Key for Library", player),
|
||||
"To Prehistoric From Lobby": lambda state: state.has("Key for Prehistoric Room", player),
|
||||
"To Lobby From Prehistoric": lambda state: state.has("Key for Prehistoric Room", player),
|
||||
"To Greenhouse": lambda state: state.has("Key for Greenhouse Room", player),
|
||||
"To Greenhouse": lambda state: state.has("Key for Greenhouse", player),
|
||||
"To Ocean From Prehistoric": lambda state: state.has("Key for Ocean Room", player),
|
||||
"To Prehistoric From Ocean": lambda state: state.has("Key for Ocean Room", player),
|
||||
"To Projector Room": lambda state: state.has("Key for Projector Room", player),
|
||||
"To Generator": lambda state: state.has("Key for Generator Room", player),
|
||||
"To Generator From Maintenance Tunnels": lambda state: state.has("Key for Generator Room", player),
|
||||
"To Lobby From Egypt": lambda state: state.has("Key for Egypt Room", player),
|
||||
"To Egypt From Lobby": lambda state: state.has("Key for Egypt Room", player),
|
||||
"To Janitor Closet": lambda state: state.has("Key for Janitor Closet", player),
|
||||
"To Shaman From Burial": lambda state: state.has("Key for Shaman Room", player),
|
||||
"To Burial From Shaman": lambda state: state.has("Key for Shaman Room", player),
|
||||
"To Norse Stone From Gods Room": lambda state: state.has("Aligned Planets", player),
|
||||
"To Inventions From UFO": lambda state: state.has("Key for UFO Room", player),
|
||||
"To UFO From Inventions": lambda state: state.has("Key for UFO Room", player),
|
||||
"To Orrery From UFO": lambda state: state.has("Viewed Fortune", player),
|
||||
"To Torture From Inventions": lambda state: state.has("Key for Torture Room", player),
|
||||
"To Inventions From Torture": lambda state: state.has("Key for Torture Room", player),
|
||||
"To Torture": lambda state: state.has("Key for Puzzle Room", player),
|
||||
"To Puzzle Room Mastermind From Torture": lambda state: state.has("Key for Puzzle Room", player),
|
||||
"To Bedroom": lambda state: state.has("Key for Bedroom", player),
|
||||
"To Underground Lake From Underground Tunnels": lambda state: state.has("Key for Underground Lake Room", player),
|
||||
"To Underground Tunnels From Underground Lake": lambda state: state.has("Key for Underground Lake Room", player),
|
||||
"To Underground Lake From Underground Tunnels": lambda state: state.has("Key for Underground Lake", player),
|
||||
"To Underground Tunnels From Underground Lake": lambda state: state.has("Key for Underground Lake", player),
|
||||
"To Outside From Lobby": lambda state: state.has("Key for Front Door", player),
|
||||
"To Lobby From Outside": lambda state: state.has("Key for Front Door", player),
|
||||
"To Maintenance Tunnels From Theater Back Hallways": lambda state: state.has("Crawling", player),
|
||||
"To Maintenance Tunnels From Theater Back Hallway": lambda state: state.has("Crawling", player),
|
||||
"To Blue Maze From Egypt": lambda state: state.has("Crawling", player),
|
||||
"To Egypt From Blue Maze": lambda state: state.has("Crawling", player),
|
||||
"To Lobby From Tar River": lambda state: (state.has("Crawling", player) and oil_capturable(state, player)),
|
||||
"To Tar River From Lobby": lambda state: (state.has("Crawling", player) and oil_capturable(state, player) and state.can_reach("Tar River", "Region", player)),
|
||||
"To Burial From Egypt": lambda state: state.can_reach("Egypt", "Region", player),
|
||||
"To Gods Room From Anansi": lambda state: state.can_reach("Gods Room", "Region", player),
|
||||
"To Slide Room": lambda state: all_skull_dials_available(state, player),
|
||||
"To Lobby From Slide Room": lambda state: beths_body_available(state, player),
|
||||
"To Water Capture From Janitor Closet": lambda state: cloth_capturable(state, player)
|
||||
"To Lobby From Tar River": lambda state: state.has("Crawling", player) and oil_capturable(state, player),
|
||||
"To Tar River From Lobby": lambda state: state.has("Crawling", player) and oil_capturable(state, player) and state.can_reach_region("Tar River", player),
|
||||
"To Burial From Egypt": lambda state: state.can_reach_region("Egypt", player),
|
||||
"To Gods Room From Anansi": lambda state: state.can_reach_region("Gods Room", player),
|
||||
"To Slide Room": lambda state: all_skull_dials_set(state, player),
|
||||
"To Lobby From Slide Room": lambda state: state.has("Lost Your Head", player),
|
||||
"To Water Capture From Janitor Closet": lambda state: cloth_capturable(state, player),
|
||||
"To Victory": lambda state: (
|
||||
(water_capturable(state, player) + wax_capturable(state, player) + ash_capturable(state, player)
|
||||
+ oil_capturable(state, player) + cloth_capturable(state, player) + wood_capturable(state, player)
|
||||
+ crystal_capturable(state, player) + sand_capturable(state, player) + metal_capturable(state, player)
|
||||
+ lightning_capturable(state, world, player)) >= world.options.ixupi_captures_needed.value
|
||||
)
|
||||
},
|
||||
"locations_required": {
|
||||
"Puzzle Solved Anansi Musicbox": lambda state: state.can_reach("Clock Tower", "Region", player),
|
||||
"Accessible: Storage: Janitor Closet": lambda state: cloth_capturable(state, player),
|
||||
"Accessible: Storage: Tar River": lambda state: oil_capturable(state, player),
|
||||
"Accessible: Storage: Theater": lambda state: state.can_reach("Projector Room", "Region", player),
|
||||
"Accessible: Storage: Slide": lambda state: beths_body_available(state, player) and state.can_reach("Slide Room", "Region", player),
|
||||
"Puzzle Solved Anansi Music Box": lambda state: state.has("Set Song", player),
|
||||
"Storage: Anansi Music Box": lambda state: state.has("Set Song", player),
|
||||
"Storage: Clock Tower": lambda state: state.has("Set Time", player),
|
||||
"Storage: Janitor Closet": lambda state: cloth_capturable(state, player),
|
||||
"Storage: Tar River": lambda state: oil_capturable(state, player),
|
||||
"Storage: Theater": lambda state: state.has("Viewed Theater Movie", player),
|
||||
"Storage: Slide": lambda state: state.has("Lost Your Head", player) and state.can_reach_region("Slide Room", player),
|
||||
"Ixupi Captured Water": lambda state: water_capturable(state, player),
|
||||
"Ixupi Captured Wax": lambda state: wax_capturable(state, player),
|
||||
"Ixupi Captured Ash": lambda state: ash_capturable(state, player),
|
||||
@@ -141,32 +163,28 @@ def get_rules_lookup(player: int):
|
||||
"Ixupi Captured Crystal": lambda state: crystal_capturable(state, player),
|
||||
"Ixupi Captured Sand": lambda state: sand_capturable(state, player),
|
||||
"Ixupi Captured Metal": lambda state: metal_capturable(state, player),
|
||||
"Final Riddle: Planets Aligned": lambda state: state.can_reach("Fortune Teller", "Region", player),
|
||||
"Final Riddle: Norse God Stone Message": lambda state: (state.can_reach("Fortune Teller", "Region", player) and state.can_reach("UFO", "Region", player)),
|
||||
"Final Riddle: Beth's Body Page 17": lambda state: beths_body_available(state, player),
|
||||
"Final Riddle: Guillotine Dropped": lambda state: beths_body_available(state, player),
|
||||
"Puzzle Solved Skull Dial Door": lambda state: all_skull_dials_available(state, player),
|
||||
},
|
||||
"locations_puzzle_hints": {
|
||||
"Puzzle Solved Clock Tower Door": lambda state: state.can_reach("Three Floor Elevator", "Region", player),
|
||||
"Puzzle Solved Clock Chains": lambda state: state.can_reach("Bedroom", "Region", player),
|
||||
"Puzzle Solved Shaman Drums": lambda state: state.can_reach("Clock Tower", "Region", player),
|
||||
"Puzzle Solved Red Door": lambda state: state.can_reach("Maintenance Tunnels", "Region", player),
|
||||
"Puzzle Solved UFO Symbols": lambda state: state.can_reach("Library", "Region", player),
|
||||
"Puzzle Solved Maze Door": lambda state: state.can_reach("Projector Room", "Region", player),
|
||||
"Puzzle Solved Theater Door": lambda state: state.can_reach("Underground Lake", "Region", player),
|
||||
"Puzzle Solved Columns of RA": lambda state: state.can_reach("Underground Lake", "Region", player),
|
||||
"Final Riddle: Guillotine Dropped": lambda state: (beths_body_available(state, player) and state.can_reach("Underground Lake", "Region", player))
|
||||
},
|
||||
"Puzzle Solved Skull Dial Door": lambda state: all_skull_dials_set(state, player),
|
||||
},
|
||||
"puzzle_hints_required": {
|
||||
"Puzzle Solved Clock Tower Door": lambda state: state.can_reach_region("Three Floor Elevator", player),
|
||||
"Puzzle Solved Shaman Drums": lambda state: state.can_reach_region("Clock Tower", player),
|
||||
"Puzzle Solved Red Door": lambda state: state.can_reach_region("Maintenance Tunnels", player),
|
||||
"Puzzle Solved UFO Symbols": lambda state: state.can_reach_region("Library", player),
|
||||
"Storage: UFO": lambda state: state.can_reach_region("Library", player),
|
||||
"Puzzle Solved Maze Door": lambda state: state.has("Viewed Theater Movie", player),
|
||||
"Puzzle Solved Theater Door": lambda state: state.has("Viewed Egyptian Hieroglyphics Explained", player),
|
||||
"Puzzle Solved Columns of RA": lambda state: state.has("Viewed Egyptian Hieroglyphics Explained", player),
|
||||
"Puzzle Solved Atlantis": lambda state: state.can_reach_region("Office", player),
|
||||
},
|
||||
"elevators": {
|
||||
"Puzzle Solved Office Elevator": lambda state: ((state.can_reach("Underground Lake", "Region", player) or state.can_reach("Office", "Region", player))
|
||||
and state.has("Key for Office Elevator", player)),
|
||||
"Puzzle Solved Bedroom Elevator": lambda state: (state.can_reach("Office", "Region", player) and state.has_all({"Key for Bedroom Elevator","Crawling"}, player)),
|
||||
"Puzzle Solved Three Floor Elevator": lambda state: ((state.can_reach("Maintenance Tunnels", "Region", player) or state.can_reach("Blue Maze", "Region", player))
|
||||
and state.has("Key for Three Floor Elevator", player))
|
||||
},
|
||||
"Puzzle Solved Office Elevator": lambda state: (state.can_reach_region("Underground Lake", player) or state.can_reach_region("Office", player))
|
||||
and state.has("Key for Office Elevator", player),
|
||||
"Puzzle Solved Bedroom Elevator": lambda state: state.has_all({"Key for Bedroom Elevator", "Crawling"}, player),
|
||||
"Puzzle Solved Three Floor Elevator": lambda state: (state.can_reach_region("Maintenance Tunnels", player) or state.can_reach_region("Blue Maze", player))
|
||||
and state.has("Key for Three Floor Elevator", player)
|
||||
},
|
||||
"lightning": {
|
||||
"Ixupi Captured Lightning": lambda state: lightning_capturable(state, player)
|
||||
"Ixupi Captured Lightning": lambda state: lightning_capturable(state, world, player)
|
||||
}
|
||||
}
|
||||
return rules_lookup
|
||||
@@ -176,69 +194,128 @@ def set_rules(world: "ShiversWorld") -> None:
|
||||
multiworld = world.multiworld
|
||||
player = world.player
|
||||
|
||||
rules_lookup = get_rules_lookup(player)
|
||||
rules_lookup = get_rules_lookup(world, player)
|
||||
# Set required entrance rules
|
||||
for entrance_name, rule in rules_lookup["entrances"].items():
|
||||
multiworld.get_entrance(entrance_name, player).access_rule = rule
|
||||
world.get_entrance(entrance_name).access_rule = rule
|
||||
|
||||
world.get_region("Clock Tower Staircase").connect(
|
||||
world.get_region("Clock Chains"),
|
||||
"To Clock Chains From Clock Tower Staircase",
|
||||
lambda state: state.can_reach_region("Bedroom", player) if world.options.puzzle_hints_required.value else True
|
||||
)
|
||||
|
||||
world.get_region("Generator").connect(
|
||||
world.get_region("Beth's Body"),
|
||||
"To Beth's Body From Generator",
|
||||
lambda state: beths_body_available(state, world, player) and (
|
||||
(state.has("Viewed Norse Stone", player) and state.can_reach_region("Theater", player))
|
||||
if world.options.puzzle_hints_required.value else True
|
||||
)
|
||||
)
|
||||
|
||||
world.get_region("Torture").connect(
|
||||
world.get_region("Guillotine"),
|
||||
"To Guillotine From Torture",
|
||||
lambda state: state.has("Viewed Page 17", player) and (
|
||||
state.has("Viewed Egyptian Hieroglyphics Explained", player)
|
||||
if world.options.puzzle_hints_required.value else True
|
||||
)
|
||||
)
|
||||
|
||||
# Set required location rules
|
||||
for location_name, rule in rules_lookup["locations_required"].items():
|
||||
multiworld.get_location(location_name, player).access_rule = rule
|
||||
world.get_location(location_name).access_rule = rule
|
||||
|
||||
world.get_location("Jukebox").access_rule = lambda state: (
|
||||
state.can_reach_region("Clock Tower", player) and (
|
||||
state.can_reach_region("Anansi", player)
|
||||
if world.options.puzzle_hints_required.value else True
|
||||
)
|
||||
)
|
||||
|
||||
# Set option location rules
|
||||
if world.options.puzzle_hints_required.value:
|
||||
for location_name, rule in rules_lookup["locations_puzzle_hints"].items():
|
||||
multiworld.get_location(location_name, player).access_rule = rule
|
||||
for location_name, rule in rules_lookup["puzzle_hints_required"].items():
|
||||
world.get_location(location_name).access_rule = rule
|
||||
|
||||
world.get_entrance("To Theater From Lobby").access_rule = lambda state: state.has(
|
||||
"Viewed Egyptian Hieroglyphics Explained", player
|
||||
)
|
||||
|
||||
world.get_entrance("To Clock Tower Staircase From Theater Back Hallway").access_rule = lambda state: state.can_reach_region("Three Floor Elevator", player)
|
||||
multiworld.register_indirect_condition(
|
||||
world.get_region("Three Floor Elevator"),
|
||||
world.get_entrance("To Clock Tower Staircase From Theater Back Hallway")
|
||||
)
|
||||
|
||||
world.get_entrance("To Gods Room From Shaman").access_rule = lambda state: state.can_reach_region(
|
||||
"Clock Tower", player
|
||||
)
|
||||
multiworld.register_indirect_condition(
|
||||
world.get_region("Clock Tower"), world.get_entrance("To Gods Room From Shaman")
|
||||
)
|
||||
|
||||
world.get_entrance("To Anansi From Gods Room").access_rule = lambda state: state.can_reach_region(
|
||||
"Maintenance Tunnels", player
|
||||
)
|
||||
multiworld.register_indirect_condition(
|
||||
world.get_region("Maintenance Tunnels"), world.get_entrance("To Anansi From Gods Room")
|
||||
)
|
||||
|
||||
world.get_entrance("To Maze From Maze Staircase").access_rule = lambda \
|
||||
state: state.can_reach_region("Projector Room", player)
|
||||
multiworld.register_indirect_condition(
|
||||
world.get_region("Projector Room"), world.get_entrance("To Maze From Maze Staircase")
|
||||
)
|
||||
|
||||
multiworld.register_indirect_condition(
|
||||
world.get_region("Bedroom"), world.get_entrance("To Clock Chains From Clock Tower Staircase")
|
||||
)
|
||||
multiworld.register_indirect_condition(
|
||||
world.get_region("Theater"), world.get_entrance("To Beth's Body From Generator")
|
||||
)
|
||||
|
||||
if world.options.elevators_stay_solved.value:
|
||||
for location_name, rule in rules_lookup["elevators"].items():
|
||||
multiworld.get_location(location_name, player).access_rule = rule
|
||||
world.get_location(location_name).access_rule = rule
|
||||
if world.options.early_lightning.value:
|
||||
for location_name, rule in rules_lookup["lightning"].items():
|
||||
multiworld.get_location(location_name, player).access_rule = rule
|
||||
world.get_location(location_name).access_rule = rule
|
||||
|
||||
# Register indirect conditions
|
||||
multiworld.register_indirect_condition(world.get_region("Burial"), world.get_entrance("To Slide Room"))
|
||||
multiworld.register_indirect_condition(world.get_region("Egypt"), world.get_entrance("To Slide Room"))
|
||||
multiworld.register_indirect_condition(world.get_region("Gods Room"), world.get_entrance("To Slide Room"))
|
||||
multiworld.register_indirect_condition(world.get_region("Prehistoric"), world.get_entrance("To Slide Room"))
|
||||
multiworld.register_indirect_condition(world.get_region("Tar River"), world.get_entrance("To Slide Room"))
|
||||
multiworld.register_indirect_condition(world.get_region("Werewolf"), world.get_entrance("To Slide Room"))
|
||||
multiworld.register_indirect_condition(world.get_region("Prehistoric"), world.get_entrance("To Tar River From Lobby"))
|
||||
|
||||
# forbid cloth in janitor closet and oil in tar river
|
||||
forbid_item(multiworld.get_location("Accessible: Storage: Janitor Closet", player), "Cloth Pot Bottom DUPE", player)
|
||||
forbid_item(multiworld.get_location("Accessible: Storage: Janitor Closet", player), "Cloth Pot Top DUPE", player)
|
||||
forbid_item(multiworld.get_location("Accessible: Storage: Janitor Closet", player), "Cloth Pot Complete DUPE", player)
|
||||
forbid_item(multiworld.get_location("Accessible: Storage: Tar River", player), "Oil Pot Bottom DUPE", player)
|
||||
forbid_item(multiworld.get_location("Accessible: Storage: Tar River", player), "Oil Pot Top DUPE", player)
|
||||
forbid_item(multiworld.get_location("Accessible: Storage: Tar River", player), "Oil Pot Complete DUPE", player)
|
||||
forbid_item(world.get_location("Storage: Janitor Closet"), "Cloth Pot Bottom DUPE", player)
|
||||
forbid_item(world.get_location("Storage: Janitor Closet"), "Cloth Pot Top DUPE", player)
|
||||
forbid_item(world.get_location("Storage: Janitor Closet"), "Cloth Pot Complete DUPE", player)
|
||||
forbid_item(world.get_location("Storage: Tar River"), "Oil Pot Bottom DUPE", player)
|
||||
forbid_item(world.get_location("Storage: Tar River"), "Oil Pot Top DUPE", player)
|
||||
forbid_item(world.get_location("Storage: Tar River"), "Oil Pot Complete DUPE", player)
|
||||
|
||||
# Filler Item Forbids
|
||||
forbid_item(multiworld.get_location("Puzzle Solved Lyre", player), "Easier Lyre", player)
|
||||
forbid_item(multiworld.get_location("Ixupi Captured Water", player), "Water Always Available in Lobby", player)
|
||||
forbid_item(multiworld.get_location("Ixupi Captured Wax", player), "Wax Always Available in Library", player)
|
||||
forbid_item(multiworld.get_location("Ixupi Captured Wax", player), "Wax Always Available in Anansi Room", player)
|
||||
forbid_item(multiworld.get_location("Ixupi Captured Wax", player), "Wax Always Available in Shaman Room", player)
|
||||
forbid_item(multiworld.get_location("Ixupi Captured Ash", player), "Ash Always Available in Office", player)
|
||||
forbid_item(multiworld.get_location("Ixupi Captured Ash", player), "Ash Always Available in Burial Room", player)
|
||||
forbid_item(multiworld.get_location("Ixupi Captured Oil", player), "Oil Always Available in Prehistoric Room", player)
|
||||
forbid_item(multiworld.get_location("Ixupi Captured Cloth", player), "Cloth Always Available in Egypt", player)
|
||||
forbid_item(multiworld.get_location("Ixupi Captured Cloth", player), "Cloth Always Available in Burial Room", player)
|
||||
forbid_item(multiworld.get_location("Ixupi Captured Wood", player), "Wood Always Available in Workshop", player)
|
||||
forbid_item(multiworld.get_location("Ixupi Captured Wood", player), "Wood Always Available in Blue Maze", player)
|
||||
forbid_item(multiworld.get_location("Ixupi Captured Wood", player), "Wood Always Available in Pegasus Room", player)
|
||||
forbid_item(multiworld.get_location("Ixupi Captured Wood", player), "Wood Always Available in Gods Room", player)
|
||||
forbid_item(multiworld.get_location("Ixupi Captured Crystal", player), "Crystal Always Available in Lobby", player)
|
||||
forbid_item(multiworld.get_location("Ixupi Captured Crystal", player), "Crystal Always Available in Ocean", player)
|
||||
forbid_item(multiworld.get_location("Ixupi Captured Sand", player), "Sand Always Available in Plants Room", player)
|
||||
forbid_item(multiworld.get_location("Ixupi Captured Sand", player), "Sand Always Available in Ocean", player)
|
||||
forbid_item(multiworld.get_location("Ixupi Captured Metal", player), "Metal Always Available in Projector Room", player)
|
||||
forbid_item(multiworld.get_location("Ixupi Captured Metal", player), "Metal Always Available in Bedroom", player)
|
||||
forbid_item(multiworld.get_location("Ixupi Captured Metal", player), "Metal Always Available in Prehistoric", player)
|
||||
forbid_item(world.get_location("Puzzle Solved Lyre"), "Easier Lyre", player)
|
||||
forbid_item(world.get_location("Ixupi Captured Water"), "Water Always Available in Lobby", player)
|
||||
forbid_item(world.get_location("Ixupi Captured Wax"), "Wax Always Available in Library", player)
|
||||
forbid_item(world.get_location("Ixupi Captured Wax"), "Wax Always Available in Anansi Room", player)
|
||||
forbid_item(world.get_location("Ixupi Captured Wax"), "Wax Always Available in Shaman Room", player)
|
||||
forbid_item(world.get_location("Ixupi Captured Ash"), "Ash Always Available in Office", player)
|
||||
forbid_item(world.get_location("Ixupi Captured Ash"), "Ash Always Available in Burial Room", player)
|
||||
forbid_item(world.get_location("Ixupi Captured Oil"), "Oil Always Available in Prehistoric Room", player)
|
||||
forbid_item(world.get_location("Ixupi Captured Cloth"), "Cloth Always Available in Egypt", player)
|
||||
forbid_item(world.get_location("Ixupi Captured Cloth"), "Cloth Always Available in Burial Room", player)
|
||||
forbid_item(world.get_location("Ixupi Captured Wood"), "Wood Always Available in Workshop", player)
|
||||
forbid_item(world.get_location("Ixupi Captured Wood"), "Wood Always Available in Blue Maze", player)
|
||||
forbid_item(world.get_location("Ixupi Captured Wood"), "Wood Always Available in Pegasus Room", player)
|
||||
forbid_item(world.get_location("Ixupi Captured Wood"), "Wood Always Available in Gods Room", player)
|
||||
forbid_item(world.get_location("Ixupi Captured Crystal"), "Crystal Always Available in Lobby", player)
|
||||
forbid_item(world.get_location("Ixupi Captured Crystal"), "Crystal Always Available in Ocean", player)
|
||||
forbid_item(world.get_location("Ixupi Captured Sand"), "Sand Always Available in Plants Room", player)
|
||||
forbid_item(world.get_location("Ixupi Captured Sand"), "Sand Always Available in Ocean", player)
|
||||
forbid_item(world.get_location("Ixupi Captured Metal"), "Metal Always Available in Projector Room", player)
|
||||
forbid_item(world.get_location("Ixupi Captured Metal"), "Metal Always Available in Bedroom", player)
|
||||
forbid_item(world.get_location("Ixupi Captured Metal"), "Metal Always Available in Prehistoric", player)
|
||||
|
||||
# Set completion condition
|
||||
multiworld.completion_condition[player] = lambda state: ((
|
||||
water_capturable(state, player) + wax_capturable(state, player) + ash_capturable(state, player) \
|
||||
+ oil_capturable(state, player) + cloth_capturable(state, player) + wood_capturable(state, player) \
|
||||
+ crystal_capturable(state, player) + sand_capturable(state, player) + metal_capturable(state, player) \
|
||||
+ lightning_capturable(state, player)) >= world.options.ixupi_captures_needed.value)
|
||||
multiworld.completion_condition[player] = lambda state: completion_condition(state, player)
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
from typing import List
|
||||
from .Items import item_table, ShiversItem
|
||||
from .Rules import set_rules
|
||||
from BaseClasses import Item, Tutorial, Region, Location
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from BaseClasses import Item, ItemClassification, Location, Region, Tutorial
|
||||
from Fill import fill_restrictive
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from . import Constants, Rules
|
||||
from .Options import ShiversOptions
|
||||
from .Items import ItemType, SHIVERS_ITEM_ID_OFFSET, ShiversItem, item_table
|
||||
from .Options import ShiversOptions, shivers_option_groups
|
||||
from .Rules import set_rules
|
||||
|
||||
|
||||
class ShiversWeb(WebWorld):
|
||||
@@ -15,12 +16,15 @@ class ShiversWeb(WebWorld):
|
||||
"English",
|
||||
"setup_en.md",
|
||||
"setup/en",
|
||||
["GodlFire", "Mathx2"]
|
||||
["GodlFire", "Cynbel_Terreus"]
|
||||
)]
|
||||
option_groups = shivers_option_groups
|
||||
|
||||
|
||||
class ShiversWorld(World):
|
||||
"""
|
||||
Shivers is a horror themed point and click adventure. Explore the mysteries of Windlenot's Museum of the Strange and Unusual.
|
||||
Shivers is a horror themed point and click adventure.
|
||||
Explore the mysteries of Windlenot's Museum of the Strange and Unusual.
|
||||
"""
|
||||
|
||||
game = "Shivers"
|
||||
@@ -28,24 +32,41 @@ class ShiversWorld(World):
|
||||
web = ShiversWeb()
|
||||
options_dataclass = ShiversOptions
|
||||
options: ShiversOptions
|
||||
|
||||
set_rules = set_rules
|
||||
item_name_to_id = {name: data.code for name, data in item_table.items()}
|
||||
location_name_to_id = Constants.location_name_to_id
|
||||
shivers_item_id_offset = 27000
|
||||
storage_placements = []
|
||||
pot_completed_list: List[int]
|
||||
|
||||
|
||||
def generate_early(self):
|
||||
self.pot_completed_list = []
|
||||
|
||||
# Pot piece shuffle location:
|
||||
if self.options.location_pot_pieces == "own_world":
|
||||
self.options.local_items.value |= {name for name, data in item_table.items() if
|
||||
data.type in [ItemType.POT, ItemType.POT_COMPLETE]}
|
||||
elif self.options.location_pot_pieces == "different_world":
|
||||
self.options.non_local_items.value |= {name for name, data in item_table.items() if
|
||||
data.type in [ItemType.POT, ItemType.POT_COMPLETE]}
|
||||
|
||||
# Ixupi captures priority locations:
|
||||
if self.options.ixupi_captures_priority:
|
||||
self.options.priority_locations.value |= (
|
||||
{name for name in self.location_names if name.startswith('Ixupi Captured')}
|
||||
)
|
||||
|
||||
def create_item(self, name: str) -> Item:
|
||||
data = item_table[name]
|
||||
return ShiversItem(name, data.classification, data.code, self.player)
|
||||
|
||||
def create_event(self, region_name: str, event_name: str) -> None:
|
||||
region = self.multiworld.get_region(region_name, self.player)
|
||||
loc = ShiversLocation(self.player, event_name, None, region)
|
||||
loc.place_locked_item(self.create_event_item(event_name))
|
||||
def create_event_location(self, region_name: str, location_name: str, event_name: Optional[str] = None) -> None:
|
||||
region = self.get_region(region_name)
|
||||
loc = ShiversLocation(self.player, location_name, None, region)
|
||||
if event_name is not None:
|
||||
loc.place_locked_item(ShiversItem(event_name, ItemClassification.progression, None, self.player))
|
||||
else:
|
||||
loc.place_locked_item(ShiversItem(location_name, ItemClassification.progression, None, self.player))
|
||||
loc.show_in_spoiler = False
|
||||
region.locations.append(loc)
|
||||
|
||||
def create_regions(self) -> None:
|
||||
@@ -56,162 +77,185 @@ class ShiversWorld(World):
|
||||
for exit_name in exits:
|
||||
r.create_exit(exit_name)
|
||||
|
||||
|
||||
# Bind mandatory connections
|
||||
for entr_name, region_name in Constants.region_info["mandatory_connections"]:
|
||||
e = self.multiworld.get_entrance(entr_name, self.player)
|
||||
r = self.multiworld.get_region(region_name, self.player)
|
||||
e = self.get_entrance(entr_name)
|
||||
r = self.get_region(region_name)
|
||||
e.connect(r)
|
||||
|
||||
# Locations
|
||||
# Build exclusion list
|
||||
self.removed_locations = set()
|
||||
removed_locations = set()
|
||||
if not self.options.include_information_plaques:
|
||||
self.removed_locations.update(Constants.exclusion_info["plaques"])
|
||||
removed_locations.update(Constants.exclusion_info["plaques"])
|
||||
if not self.options.elevators_stay_solved:
|
||||
self.removed_locations.update(Constants.exclusion_info["elevators"])
|
||||
removed_locations.update(Constants.exclusion_info["elevators"])
|
||||
if not self.options.early_lightning:
|
||||
self.removed_locations.update(Constants.exclusion_info["lightning"])
|
||||
removed_locations.update(Constants.exclusion_info["lightning"])
|
||||
|
||||
# Add locations
|
||||
for region_name, locations in Constants.location_info["locations_by_region"].items():
|
||||
region = self.multiworld.get_region(region_name, self.player)
|
||||
region = self.get_region(region_name)
|
||||
for loc_name in locations:
|
||||
if loc_name not in self.removed_locations:
|
||||
if loc_name not in removed_locations:
|
||||
loc = ShiversLocation(self.player, loc_name, self.location_name_to_id.get(loc_name, None), region)
|
||||
region.locations.append(loc)
|
||||
|
||||
self.create_event_location("Prehistoric", "Set Skull Dial: Prehistoric")
|
||||
self.create_event_location("Tar River", "Set Skull Dial: Tar River")
|
||||
self.create_event_location("Egypt", "Set Skull Dial: Egypt")
|
||||
self.create_event_location("Burial", "Set Skull Dial: Burial")
|
||||
self.create_event_location("Gods Room", "Set Skull Dial: Gods Room")
|
||||
self.create_event_location("Werewolf", "Set Skull Dial: Werewolf")
|
||||
self.create_event_location("Projector Room", "Viewed Theater Movie")
|
||||
self.create_event_location("Clock Chains", "Clock Chains", "Set Time")
|
||||
self.create_event_location("Clock Tower", "Jukebox", "Set Song")
|
||||
self.create_event_location("Fortune Teller", "Viewed Fortune")
|
||||
self.create_event_location("Orrery", "Orrery", "Aligned Planets")
|
||||
self.create_event_location("Norse Stone", "Norse Stone", "Viewed Norse Stone")
|
||||
self.create_event_location("Beth's Body", "Beth's Body", "Viewed Page 17")
|
||||
self.create_event_location("Windlenot's Body", "Windlenot's Body", "Viewed Egyptian Hieroglyphics Explained")
|
||||
self.create_event_location("Guillotine", "Guillotine", "Lost Your Head")
|
||||
|
||||
def create_items(self) -> None:
|
||||
#Add items to item pool
|
||||
itempool = []
|
||||
# Add items to item pool
|
||||
item_pool = []
|
||||
for name, data in item_table.items():
|
||||
if data.type in {"key", "ability", "filler2"}:
|
||||
itempool.append(self.create_item(name))
|
||||
if data.type in [ItemType.KEY, ItemType.ABILITY, ItemType.IXUPI_AVAILABILITY]:
|
||||
item_pool.append(self.create_item(name))
|
||||
|
||||
# Pot pieces/Completed/Mixed:
|
||||
for i in range(10):
|
||||
if self.options.full_pots == "pieces":
|
||||
itempool.append(self.create_item(self.item_id_to_name[self.shivers_item_id_offset + i]))
|
||||
itempool.append(self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 10 + i]))
|
||||
elif self.options.full_pots == "complete":
|
||||
itempool.append(self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 20 + i]))
|
||||
else:
|
||||
# Roll for if pieces or a complete pot will be used.
|
||||
# Pot Pieces
|
||||
if self.options.full_pots == "pieces":
|
||||
item_pool += [self.create_item(name) for name, data in item_table.items() if data.type == ItemType.POT]
|
||||
elif self.options.full_pots == "complete":
|
||||
item_pool += [self.create_item(name) for name, data in item_table.items() if
|
||||
data.type == ItemType.POT_COMPLETE]
|
||||
else:
|
||||
# Roll for if pieces or a complete pot will be used.
|
||||
# Pot Pieces
|
||||
pieces = [self.create_item(name) for name, data in item_table.items() if data.type == ItemType.POT]
|
||||
complete = [self.create_item(name) for name, data in item_table.items() if
|
||||
data.type == ItemType.POT_COMPLETE]
|
||||
for i in range(10):
|
||||
if self.random.randint(0, 1) == 0:
|
||||
self.pot_completed_list.append(0)
|
||||
itempool.append(self.create_item(self.item_id_to_name[self.shivers_item_id_offset + i]))
|
||||
itempool.append(self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 10 + i]))
|
||||
item_pool.append(pieces[i])
|
||||
item_pool.append(pieces[i + 10])
|
||||
# Completed Pot
|
||||
else:
|
||||
self.pot_completed_list.append(1)
|
||||
itempool.append(self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 20 + i]))
|
||||
item_pool.append(complete[i])
|
||||
|
||||
#Add Filler
|
||||
itempool += [self.create_item("Easier Lyre") for i in range(9)]
|
||||
# Add Easier Lyre
|
||||
item_pool += [self.create_item("Easier Lyre") for _ in range(9)]
|
||||
|
||||
#Extra filler is random between Heals and Easier Lyre. Heals weighted 95%.
|
||||
filler_needed = len(self.multiworld.get_unfilled_locations(self.player)) - 24 - len(itempool)
|
||||
itempool += [self.random.choices([self.create_item("Heal"), self.create_item("Easier Lyre")], weights=[95, 5])[0] for i in range(filler_needed)]
|
||||
# Place library escape items. Choose a location to place the escape item
|
||||
library_region = self.get_region("Library")
|
||||
library_location = self.random.choice(
|
||||
[loc for loc in library_region.locations if not loc.name.startswith("Storage: ")]
|
||||
)
|
||||
|
||||
#Place library escape items. Choose a location to place the escape item
|
||||
library_region = self.multiworld.get_region("Library", self.player)
|
||||
librarylocation = self.random.choice([loc for loc in library_region.locations if not loc.name.startswith("Accessible:")])
|
||||
|
||||
#Roll for which escape items will be placed in the Library
|
||||
# Roll for which escape items will be placed in the Library
|
||||
library_random = self.random.randint(1, 3)
|
||||
if library_random == 1:
|
||||
librarylocation.place_locked_item(self.create_item("Crawling"))
|
||||
if library_random == 1:
|
||||
library_location.place_locked_item(self.create_item("Crawling"))
|
||||
item_pool = [item for item in item_pool if item.name != "Crawling"]
|
||||
elif library_random == 2:
|
||||
library_location.place_locked_item(self.create_item("Key for Library"))
|
||||
item_pool = [item for item in item_pool if item.name != "Key for Library"]
|
||||
elif library_random == 3:
|
||||
library_location.place_locked_item(self.create_item("Key for Three Floor Elevator"))
|
||||
library_location_2 = self.random.choice(
|
||||
[loc for loc in library_region.locations if
|
||||
not loc.name.startswith("Storage: ") and loc != library_location]
|
||||
)
|
||||
library_location_2.place_locked_item(self.create_item("Key for Egypt Room"))
|
||||
item_pool = [item for item in item_pool if
|
||||
item.name not in ["Key for Three Floor Elevator", "Key for Egypt Room"]]
|
||||
|
||||
itempool = [item for item in itempool if item.name != "Crawling"]
|
||||
|
||||
elif library_random == 2:
|
||||
librarylocation.place_locked_item(self.create_item("Key for Library Room"))
|
||||
|
||||
itempool = [item for item in itempool if item.name != "Key for Library Room"]
|
||||
elif library_random == 3:
|
||||
librarylocation.place_locked_item(self.create_item("Key for Three Floor Elevator"))
|
||||
|
||||
librarylocationkeytwo = self.random.choice([loc for loc in library_region.locations if not loc.name.startswith("Accessible:") and loc != librarylocation])
|
||||
librarylocationkeytwo.place_locked_item(self.create_item("Key for Egypt Room"))
|
||||
|
||||
itempool = [item for item in itempool if item.name not in ["Key for Three Floor Elevator", "Key for Egypt Room"]]
|
||||
|
||||
#If front door option is on, determine which set of keys will be used for lobby access and add front door key to item pool
|
||||
lobby_access_keys = 1
|
||||
# If front door option is on, determine which set of keys will
|
||||
# be used for lobby access and add front door key to item pool
|
||||
lobby_access_keys = 0
|
||||
if self.options.front_door_usable:
|
||||
lobby_access_keys = self.random.randint(1, 2)
|
||||
itempool += [self.create_item("Key for Front Door")]
|
||||
lobby_access_keys = self.random.randint(0, 1)
|
||||
item_pool.append(self.create_item("Key for Front Door"))
|
||||
else:
|
||||
itempool += [self.create_item("Heal")]
|
||||
item_pool.append(self.create_item("Heal"))
|
||||
|
||||
self.multiworld.itempool += itempool
|
||||
def set_lobby_access_keys(items: Dict[str, int]):
|
||||
if lobby_access_keys == 0:
|
||||
items["Key for Underground Lake"] = 1
|
||||
items["Key for Office Elevator"] = 1
|
||||
items["Key for Office"] = 1
|
||||
else:
|
||||
items["Key for Front Door"] = 1
|
||||
|
||||
#Lobby acess:
|
||||
# Lobby access:
|
||||
if self.options.lobby_access == "early":
|
||||
if lobby_access_keys == 1:
|
||||
self.multiworld.early_items[self.player]["Key for Underground Lake Room"] = 1
|
||||
self.multiworld.early_items[self.player]["Key for Office Elevator"] = 1
|
||||
self.multiworld.early_items[self.player]["Key for Office"] = 1
|
||||
elif lobby_access_keys == 2:
|
||||
self.multiworld.early_items[self.player]["Key for Front Door"] = 1
|
||||
if self.options.lobby_access == "local":
|
||||
if lobby_access_keys == 1:
|
||||
self.multiworld.local_early_items[self.player]["Key for Underground Lake Room"] = 1
|
||||
self.multiworld.local_early_items[self.player]["Key for Office Elevator"] = 1
|
||||
self.multiworld.local_early_items[self.player]["Key for Office"] = 1
|
||||
elif lobby_access_keys == 2:
|
||||
self.multiworld.local_early_items[self.player]["Key for Front Door"] = 1
|
||||
set_lobby_access_keys(self.multiworld.early_items[self.player])
|
||||
elif self.options.lobby_access == "local":
|
||||
set_lobby_access_keys(self.multiworld.local_early_items[self.player])
|
||||
|
||||
#Pot piece shuffle location:
|
||||
if self.options.location_pot_pieces == "own_world":
|
||||
self.options.local_items.value |= {name for name, data in item_table.items() if data.type == "pot" or data.type == "pot_type2"}
|
||||
if self.options.location_pot_pieces == "different_world":
|
||||
self.options.non_local_items.value |= {name for name, data in item_table.items() if data.type == "pot" or data.type == "pot_type2"}
|
||||
goal_item_code = SHIVERS_ITEM_ID_OFFSET + 100 + Constants.years_since_sep_30_1980
|
||||
for name, data in item_table.items():
|
||||
if data.type == ItemType.GOAL and data.code == goal_item_code:
|
||||
goal = self.create_item(name)
|
||||
self.get_location("Mystery Solved").place_locked_item(goal)
|
||||
|
||||
# Extra filler is random between Heals and Easier Lyre. Heals weighted 95%.
|
||||
filler_needed = len(self.multiworld.get_unfilled_locations(self.player)) - len(item_pool) - 23
|
||||
item_pool += map(self.create_item, self.random.choices(
|
||||
["Heal", "Easier Lyre"], weights=[95, 5], k=filler_needed
|
||||
))
|
||||
|
||||
self.multiworld.itempool += item_pool
|
||||
|
||||
def pre_fill(self) -> None:
|
||||
# Prefills event storage locations with duplicate pots
|
||||
storagelocs = []
|
||||
storageitems = []
|
||||
self.storage_placements = []
|
||||
storage_locs = []
|
||||
storage_items = []
|
||||
|
||||
for locations in Constants.location_info["locations_by_region"].values():
|
||||
for loc_name in locations:
|
||||
if loc_name.startswith("Accessible: "):
|
||||
storagelocs.append(self.multiworld.get_location(loc_name, self.player))
|
||||
if loc_name.startswith("Storage: "):
|
||||
storage_locs.append(self.get_location(loc_name))
|
||||
|
||||
#Pot pieces/Completed/Mixed:
|
||||
# Pot pieces/Completed/Mixed:
|
||||
if self.options.full_pots == "pieces":
|
||||
storageitems += [self.create_item(name) for name, data in item_table.items() if data.type == 'potduplicate']
|
||||
storage_items += [self.create_item(name) for name, data in item_table.items() if
|
||||
data.type == ItemType.POT_DUPLICATE]
|
||||
elif self.options.full_pots == "complete":
|
||||
storageitems += [self.create_item(name) for name, data in item_table.items() if data.type == 'potduplicate_type2']
|
||||
storageitems += [self.create_item("Empty") for i in range(10)]
|
||||
storage_items += [self.create_item(name) for name, data in item_table.items() if
|
||||
data.type == ItemType.POT_COMPLETE_DUPLICATE]
|
||||
storage_items += [self.create_item("Empty") for _ in range(10)]
|
||||
else:
|
||||
pieces = [self.create_item(name) for name, data in item_table.items() if
|
||||
data.type == ItemType.POT_DUPLICATE]
|
||||
complete = [self.create_item(name) for name, data in item_table.items() if
|
||||
data.type == ItemType.POT_COMPLETE_DUPLICATE]
|
||||
for i in range(10):
|
||||
#Pieces
|
||||
# Pieces
|
||||
if self.pot_completed_list[i] == 0:
|
||||
storageitems += [self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 70 + i])]
|
||||
storageitems += [self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 80 + i])]
|
||||
#Complete
|
||||
storage_items.append(pieces[i])
|
||||
storage_items.append(pieces[i + 10])
|
||||
# Complete
|
||||
else:
|
||||
storageitems += [self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 140 + i])]
|
||||
storageitems += [self.create_item("Empty")]
|
||||
storage_items.append(complete[i])
|
||||
storage_items.append(self.create_item("Empty"))
|
||||
|
||||
storageitems += [self.create_item("Empty") for i in range(3)]
|
||||
storage_items += [self.create_item("Empty") for _ in range(3)]
|
||||
|
||||
state = self.multiworld.get_all_state(True)
|
||||
|
||||
self.random.shuffle(storagelocs)
|
||||
self.random.shuffle(storageitems)
|
||||
|
||||
fill_restrictive(self.multiworld, state, storagelocs.copy(), storageitems, True, True)
|
||||
self.random.shuffle(storage_locs)
|
||||
self.random.shuffle(storage_items)
|
||||
|
||||
self.storage_placements = {location.name: location.item.name for location in storagelocs}
|
||||
fill_restrictive(self.multiworld, state, storage_locs.copy(), storage_items, True, True)
|
||||
|
||||
set_rules = set_rules
|
||||
self.storage_placements = {location.name.replace("Storage: ", ""): location.item.name.replace(" DUPE", "") for
|
||||
location in storage_locs}
|
||||
|
||||
def fill_slot_data(self) -> dict:
|
||||
|
||||
return {
|
||||
"StoragePlacements": self.storage_placements,
|
||||
"ExcludedLocations": list(self.options.exclude_locations.value),
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"Information Plaque: (Ocean) Poseidon",
|
||||
"Information Plaque: (Ocean) Colossus of Rhodes",
|
||||
"Information Plaque: (Ocean) Poseidon's Temple",
|
||||
"Information Plaque: (Underground Maze) Subterranean World",
|
||||
"Information Plaque: (Underground Maze Staircase) Subterranean World",
|
||||
"Information Plaque: (Underground Maze) Dero",
|
||||
"Information Plaque: (Egypt) Tomb of the Ixupi",
|
||||
"Information Plaque: (Egypt) The Sphinx",
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"Puzzle Solved Fortune Teller Door",
|
||||
"Puzzle Solved Alchemy",
|
||||
"Puzzle Solved UFO Symbols",
|
||||
"Puzzle Solved Anansi Musicbox",
|
||||
"Puzzle Solved Anansi Music Box",
|
||||
"Puzzle Solved Gallows",
|
||||
"Puzzle Solved Mastermind",
|
||||
"Puzzle Solved Marble Flipper",
|
||||
@@ -54,7 +54,7 @@
|
||||
"Final Riddle: Norse God Stone Message",
|
||||
"Final Riddle: Beth's Body Page 17",
|
||||
"Final Riddle: Guillotine Dropped",
|
||||
"Puzzle Hint Found: Combo Lock in Mailbox",
|
||||
"Puzzle Hint Found: Mailbox",
|
||||
"Puzzle Hint Found: Orange Symbol",
|
||||
"Puzzle Hint Found: Silver Symbol",
|
||||
"Puzzle Hint Found: Green Symbol",
|
||||
@@ -113,15 +113,19 @@
|
||||
"Puzzle Solved Office Elevator",
|
||||
"Puzzle Solved Bedroom Elevator",
|
||||
"Puzzle Solved Three Floor Elevator",
|
||||
"Ixupi Captured Lightning"
|
||||
"Ixupi Captured Lightning",
|
||||
"Puzzle Solved Combination Lock",
|
||||
"Puzzle Hint Found: Beth's Note",
|
||||
"Mystery Solved"
|
||||
],
|
||||
"locations_by_region": {
|
||||
"Outside": [
|
||||
"Puzzle Solved Combination Lock",
|
||||
"Puzzle Solved Gears",
|
||||
"Puzzle Solved Stone Henge",
|
||||
"Puzzle Solved Office Elevator",
|
||||
"Puzzle Solved Three Floor Elevator",
|
||||
"Puzzle Hint Found: Combo Lock in Mailbox",
|
||||
"Puzzle Hint Found: Mailbox",
|
||||
"Puzzle Hint Found: Orange Symbol",
|
||||
"Puzzle Hint Found: Silver Symbol",
|
||||
"Puzzle Hint Found: Green Symbol",
|
||||
@@ -130,32 +134,42 @@
|
||||
"Puzzle Hint Found: Tan Symbol"
|
||||
],
|
||||
"Underground Lake": [
|
||||
"Flashback Memory Obtained Windlenot's Ghost",
|
||||
"Flashback Memory Obtained Windlenot's Ghost"
|
||||
],
|
||||
"Windlenot's Body": [
|
||||
"Flashback Memory Obtained Egyptian Hieroglyphics Explained"
|
||||
],
|
||||
"Office": [
|
||||
"Flashback Memory Obtained Scrapbook",
|
||||
"Accessible: Storage: Desk Drawer",
|
||||
"Storage: Desk Drawer",
|
||||
"Puzzle Hint Found: Atlantis Map",
|
||||
"Puzzle Hint Found: Tape Recorder Heard",
|
||||
"Puzzle Solved Bedroom Elevator"
|
||||
],
|
||||
"Workshop": [
|
||||
"Puzzle Solved Workshop Drawers",
|
||||
"Accessible: Storage: Workshop Drawers",
|
||||
"Storage: Workshop Drawers",
|
||||
"Puzzle Hint Found: Basilisk Bone Fragments"
|
||||
],
|
||||
"Bedroom": [
|
||||
"Flashback Memory Obtained Professor Windlenot's Diary"
|
||||
],
|
||||
"Lobby": [
|
||||
"Puzzle Solved Theater Door",
|
||||
"Flashback Memory Obtained Museum Brochure",
|
||||
"Information Plaque: (Lobby) Jade Skull",
|
||||
"Information Plaque: (Lobby) Transforming Masks",
|
||||
"Storage: Slide",
|
||||
"Storage: Transforming Mask"
|
||||
],
|
||||
"Library": [
|
||||
"Puzzle Solved Library Statue",
|
||||
"Flashback Memory Obtained In Search of the Unexplained",
|
||||
"Flashback Memory Obtained South American Pictographs",
|
||||
"Flashback Memory Obtained Mythology of the Stars",
|
||||
"Flashback Memory Obtained Black Book",
|
||||
"Accessible: Storage: Library Cabinet",
|
||||
"Accessible: Storage: Library Statue"
|
||||
"Storage: Library Cabinet",
|
||||
"Storage: Library Statue"
|
||||
],
|
||||
"Maintenance Tunnels": [
|
||||
"Flashback Memory Obtained Beth's Address Book"
|
||||
@@ -163,37 +177,46 @@
|
||||
"Three Floor Elevator": [
|
||||
"Puzzle Hint Found: Elevator Writing"
|
||||
],
|
||||
"Lobby": [
|
||||
"Puzzle Solved Theater Door",
|
||||
"Flashback Memory Obtained Museum Brochure",
|
||||
"Information Plaque: (Lobby) Jade Skull",
|
||||
"Information Plaque: (Lobby) Transforming Masks",
|
||||
"Accessible: Storage: Slide",
|
||||
"Accessible: Storage: Transforming Mask"
|
||||
],
|
||||
"Generator": [
|
||||
"Final Riddle: Beth's Body Page 17",
|
||||
"Ixupi Captured Lightning"
|
||||
],
|
||||
"Theater Back Hallways": [
|
||||
"Beth's Body": [
|
||||
"Final Riddle: Beth's Body Page 17"
|
||||
],
|
||||
"Theater": [
|
||||
"Storage: Theater",
|
||||
"Puzzle Hint Found: Beth's Note"
|
||||
],
|
||||
"Theater Back Hallway": [
|
||||
"Puzzle Solved Clock Tower Door"
|
||||
],
|
||||
"Clock Tower Staircase": [
|
||||
"Clock Chains": [
|
||||
"Puzzle Solved Clock Chains"
|
||||
],
|
||||
"Clock Tower": [
|
||||
"Flashback Memory Obtained Beth's Ghost",
|
||||
"Accessible: Storage: Clock Tower",
|
||||
"Storage: Clock Tower",
|
||||
"Puzzle Hint Found: Shaman Security Camera"
|
||||
],
|
||||
"Projector Room": [
|
||||
"Flashback Memory Obtained Theater Movie"
|
||||
],
|
||||
"Prehistoric": [
|
||||
"Information Plaque: (Prehistoric) Bronze Unicorn",
|
||||
"Information Plaque: (Prehistoric) Griffin",
|
||||
"Information Plaque: (Prehistoric) Eagles Nest",
|
||||
"Information Plaque: (Prehistoric) Large Spider",
|
||||
"Information Plaque: (Prehistoric) Starfish",
|
||||
"Storage: Eagles Nest"
|
||||
],
|
||||
"Greenhouse": [
|
||||
"Storage: Greenhouse"
|
||||
],
|
||||
"Ocean": [
|
||||
"Puzzle Solved Atlantis",
|
||||
"Puzzle Solved Organ",
|
||||
"Flashback Memory Obtained Museum Blueprints",
|
||||
"Accessible: Storage: Ocean",
|
||||
"Storage: Ocean",
|
||||
"Puzzle Hint Found: Sirens Song Heard",
|
||||
"Information Plaque: (Ocean) Quartz Crystal",
|
||||
"Information Plaque: (Ocean) Poseidon",
|
||||
@@ -204,10 +227,14 @@
|
||||
"Information Plaque: (Underground Maze Staircase) Subterranean World",
|
||||
"Puzzle Solved Maze Door"
|
||||
],
|
||||
"Tar River": [
|
||||
"Storage: Tar River",
|
||||
"Information Plaque: (Underground Maze) Dero"
|
||||
],
|
||||
"Egypt": [
|
||||
"Puzzle Solved Columns of RA",
|
||||
"Puzzle Solved Burial Door",
|
||||
"Accessible: Storage: Egypt",
|
||||
"Storage: Egypt",
|
||||
"Puzzle Hint Found: Egyptian Sphinx Heard",
|
||||
"Information Plaque: (Egypt) Tomb of the Ixupi",
|
||||
"Information Plaque: (Egypt) The Sphinx",
|
||||
@@ -216,7 +243,7 @@
|
||||
"Burial": [
|
||||
"Puzzle Solved Chinese Solitaire",
|
||||
"Flashback Memory Obtained Merrick's Notebook",
|
||||
"Accessible: Storage: Chinese Solitaire",
|
||||
"Storage: Chinese Solitaire",
|
||||
"Information Plaque: (Burial) Norse Burial Ship",
|
||||
"Information Plaque: (Burial) Paracas Burial Bundles",
|
||||
"Information Plaque: (Burial) Spectacular Coffins of Ghana",
|
||||
@@ -225,15 +252,14 @@
|
||||
],
|
||||
"Shaman": [
|
||||
"Puzzle Solved Shaman Drums",
|
||||
"Accessible: Storage: Shaman Hut",
|
||||
"Storage: Shaman Hut",
|
||||
"Information Plaque: (Shaman) Witch Doctors of the Congo",
|
||||
"Information Plaque: (Shaman) Sarombe doctor of Mozambique"
|
||||
],
|
||||
"Gods Room": [
|
||||
"Puzzle Solved Lyre",
|
||||
"Puzzle Solved Red Door",
|
||||
"Accessible: Storage: Lyre",
|
||||
"Final Riddle: Norse God Stone Message",
|
||||
"Storage: Lyre",
|
||||
"Information Plaque: (Gods) Fisherman's Canoe God",
|
||||
"Information Plaque: (Gods) Mayan Gods",
|
||||
"Information Plaque: (Gods) Thor",
|
||||
@@ -242,6 +268,9 @@
|
||||
"Information Plaque: (Gods) Sumerian Lyre",
|
||||
"Information Plaque: (Gods) Chuen"
|
||||
],
|
||||
"Norse Stone": [
|
||||
"Final Riddle: Norse God Stone Message"
|
||||
],
|
||||
"Blue Maze": [
|
||||
"Puzzle Solved Fortune Teller Door"
|
||||
],
|
||||
@@ -251,35 +280,46 @@
|
||||
],
|
||||
"Inventions": [
|
||||
"Puzzle Solved Alchemy",
|
||||
"Accessible: Storage: Alchemy"
|
||||
"Storage: Alchemy"
|
||||
],
|
||||
"UFO": [
|
||||
"Puzzle Solved UFO Symbols",
|
||||
"Accessible: Storage: UFO",
|
||||
"Final Riddle: Planets Aligned",
|
||||
"Storage: UFO",
|
||||
"Information Plaque: (UFO) Coincidence or Extraterrestrial Visits?",
|
||||
"Information Plaque: (UFO) Planets",
|
||||
"Information Plaque: (UFO) Astronomical Construction",
|
||||
"Information Plaque: (UFO) Aliens"
|
||||
],
|
||||
"Orrery": [
|
||||
"Final Riddle: Planets Aligned"
|
||||
],
|
||||
"Janitor Closet": [
|
||||
"Storage: Janitor Closet"
|
||||
],
|
||||
"Werewolf": [
|
||||
"Information Plaque: (Werewolf) Lycanthropy"
|
||||
],
|
||||
"Pegasus": [
|
||||
"Information Plaque: (Pegasus) Cyclops"
|
||||
],
|
||||
"Anansi": [
|
||||
"Puzzle Solved Anansi Musicbox",
|
||||
"Puzzle Solved Anansi Music Box",
|
||||
"Flashback Memory Obtained Ancient Astrology",
|
||||
"Accessible: Storage: Skeleton",
|
||||
"Accessible: Storage: Anansi",
|
||||
"Storage: Skeleton",
|
||||
"Storage: Anansi Music Box",
|
||||
"Information Plaque: (Anansi) African Creation Myth",
|
||||
"Information Plaque: (Anansi) Apophis the Serpent",
|
||||
"Information Plaque: (Anansi) Death",
|
||||
"Information Plaque: (Pegasus) Cyclops",
|
||||
"Information Plaque: (Werewolf) Lycanthropy"
|
||||
"Information Plaque: (Anansi) Death"
|
||||
],
|
||||
"Torture": [
|
||||
"Puzzle Solved Gallows",
|
||||
"Accessible: Storage: Gallows",
|
||||
"Final Riddle: Guillotine Dropped",
|
||||
"Storage: Gallows",
|
||||
"Puzzle Hint Found: Gallows Information Plaque",
|
||||
"Information Plaque: (Torture) Guillotine"
|
||||
],
|
||||
"Guillotine": [
|
||||
"Final Riddle: Guillotine Dropped"
|
||||
],
|
||||
"Puzzle Room Mastermind": [
|
||||
"Puzzle Solved Mastermind",
|
||||
"Puzzle Hint Found: Mastermind Information Plaque"
|
||||
@@ -287,29 +327,8 @@
|
||||
"Puzzle Room Marbles": [
|
||||
"Puzzle Solved Marble Flipper"
|
||||
],
|
||||
"Prehistoric": [
|
||||
"Information Plaque: (Prehistoric) Bronze Unicorn",
|
||||
"Information Plaque: (Prehistoric) Griffin",
|
||||
"Information Plaque: (Prehistoric) Eagles Nest",
|
||||
"Information Plaque: (Prehistoric) Large Spider",
|
||||
"Information Plaque: (Prehistoric) Starfish",
|
||||
"Accessible: Storage: Eagles Nest"
|
||||
],
|
||||
"Tar River": [
|
||||
"Accessible: Storage: Tar River",
|
||||
"Information Plaque: (Underground Maze) Dero"
|
||||
],
|
||||
"Theater": [
|
||||
"Accessible: Storage: Theater"
|
||||
],
|
||||
"Greenhouse": [
|
||||
"Accessible: Storage: Greenhouse"
|
||||
],
|
||||
"Janitor Closet": [
|
||||
"Accessible: Storage: Janitor Closet"
|
||||
],
|
||||
"Skull Dial Bridge": [
|
||||
"Accessible: Storage: Skull Bridge",
|
||||
"Skull Bridge": [
|
||||
"Storage: Skull Bridge",
|
||||
"Puzzle Solved Skull Dial Door"
|
||||
],
|
||||
"Water Capture": [
|
||||
@@ -338,6 +357,9 @@
|
||||
],
|
||||
"Metal Capture": [
|
||||
"Ixupi Captured Metal"
|
||||
],
|
||||
"Victory": [
|
||||
"Mystery Solved"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,22 +4,25 @@
|
||||
["Registry", ["To Outside From Registry"]],
|
||||
["Outside", ["To Underground Tunnels From Outside", "To Lobby From Outside"]],
|
||||
["Underground Tunnels", ["To Underground Lake From Underground Tunnels", "To Outside From Underground"]],
|
||||
["Underground Lake", ["To Underground Tunnels From Underground Lake", "To Underground Blue Tunnels From Underground Lake"]],
|
||||
["Underground Lake", ["To Underground Tunnels From Underground Lake", "To Windlenot's Body From Underground Lake", "To Underground Blue Tunnels From Underground Lake"]],
|
||||
["Windlenot's Body", ["To Underground Lake From Windlenot's Body"]],
|
||||
["Underground Blue Tunnels", ["To Underground Lake From Underground Blue Tunnels", "To Office Elevator From Underground Blue Tunnels"]],
|
||||
["Office Elevator", ["To Underground Blue Tunnels From Office Elevator","To Office From Office Elevator"]],
|
||||
["Office", ["To Office Elevator From Office", "To Workshop", "To Lobby From Office", "To Bedroom Elevator From Office", "To Ash Capture From Office"]],
|
||||
["Workshop", ["To Office From Workshop", "To Wood Capture From Workshop"]],
|
||||
["Bedroom Elevator", ["To Office From Bedroom Elevator", "To Bedroom"]],
|
||||
["Bedroom", ["To Bedroom Elevator From Bedroom", "To Metal Capture From Bedroom"]],
|
||||
["Lobby", ["To Office From Lobby", "To Library From Lobby", "To Theater From Lobby", "To Prehistoric From Lobby", "To Egypt From Lobby", "To Tar River From Lobby", "To Outside From Lobby", "To Water Capture From Lobby", "To Crystal Capture From Lobby"]],
|
||||
["Lobby", ["To Office From Lobby", "To Library From Lobby", "To Theater From Lobby", "To Prehistoric From Lobby", "To Egypt From Lobby", "To Tar River From Lobby", "To Outside From Lobby", "To Water Capture From Lobby", "To Crystal Capture From Lobby", "To Victory"]],
|
||||
["Library", ["To Lobby From Library", "To Maintenance Tunnels From Library", "To Wax Capture From Library"]],
|
||||
["Maintenance Tunnels", ["To Library From Maintenance Tunnels", "To Three Floor Elevator From Maintenance Tunnels", "To Generator"]],
|
||||
["Maintenance Tunnels", ["To Library From Maintenance Tunnels", "To Three Floor Elevator From Maintenance Tunnels", "To Generator From Maintenance Tunnels"]],
|
||||
["Generator", ["To Maintenance Tunnels From Generator"]],
|
||||
["Theater", ["To Lobby From Theater", "To Theater Back Hallways From Theater"]],
|
||||
["Theater Back Hallways", ["To Theater From Theater Back Hallways", "To Clock Tower Staircase From Theater Back Hallways", "To Maintenance Tunnels From Theater Back Hallways", "To Projector Room"]],
|
||||
["Clock Tower Staircase", ["To Theater Back Hallways From Clock Tower Staircase", "To Clock Tower"]],
|
||||
["Beth's Body", ["To Generator From Beth's Body"]],
|
||||
["Theater", ["To Lobby From Theater", "To Theater Back Hallway From Theater"]],
|
||||
["Theater Back Hallway", ["To Theater From Theater Back Hallway", "To Clock Tower Staircase From Theater Back Hallway", "To Maintenance Tunnels From Theater Back Hallway", "To Projector Room"]],
|
||||
["Clock Tower Staircase", ["To Theater Back Hallway From Clock Tower Staircase", "To Clock Tower"]],
|
||||
["Clock Chains", ["To Clock Tower Staircase From Clock Chains"]],
|
||||
["Clock Tower", ["To Clock Tower Staircase From Clock Tower"]],
|
||||
["Projector Room", ["To Theater Back Hallways From Projector Room", "To Metal Capture From Projector Room"]],
|
||||
["Projector Room", ["To Theater Back Hallway From Projector Room", "To Metal Capture From Projector Room"]],
|
||||
["Prehistoric", ["To Lobby From Prehistoric", "To Greenhouse", "To Ocean From Prehistoric", "To Oil Capture From Prehistoric", "To Metal Capture From Prehistoric"]],
|
||||
["Greenhouse", ["To Prehistoric From Greenhouse", "To Sand Capture From Greenhouse"]],
|
||||
["Ocean", ["To Prehistoric From Ocean", "To Maze Staircase From Ocean", "To Crystal Capture From Ocean", "To Sand Capture From Ocean"]],
|
||||
@@ -28,22 +31,26 @@
|
||||
["Tar River", ["To Maze From Tar River", "To Lobby From Tar River", "To Oil Capture From Tar River"]],
|
||||
["Egypt", ["To Lobby From Egypt", "To Burial From Egypt", "To Blue Maze From Egypt", "To Cloth Capture From Egypt"]],
|
||||
["Burial", ["To Egypt From Burial", "To Shaman From Burial", "To Ash Capture From Burial", "To Cloth Capture From Burial"]],
|
||||
["Shaman", ["To Burial From Shaman", "To Gods Room", "To Wax Capture From Shaman"]],
|
||||
["Gods Room", ["To Shaman From Gods Room", "To Anansi From Gods Room", "To Wood Capture From Gods Room"]],
|
||||
["Anansi", ["To Gods Room From Anansi", "To Werewolf From Anansi", "To Wax Capture From Anansi", "To Wood Capture From Anansi"]],
|
||||
["Werewolf", ["To Anansi From Werewolf", "To Night Staircase From Werewolf"]],
|
||||
["Night Staircase", ["To Werewolf From Night Staircase", "To Janitor Closet", "To UFO"]],
|
||||
["Shaman", ["To Burial From Shaman", "To Gods Room From Shaman", "To Wax Capture From Shaman"]],
|
||||
["Gods Room", ["To Shaman From Gods Room", "To Anansi From Gods Room", "To Wood Capture From Gods Room", "To Norse Stone From Gods Room"]],
|
||||
["Norse Stone", ["To Gods Room From Norse Stone"]],
|
||||
["Anansi", ["To Gods Room From Anansi", "To Pegasus From Anansi", "To Wax Capture From Anansi"]],
|
||||
["Pegasus", ["To Anansi From Pegasus", "To Werewolf From Pegasus", "To Wood Capture From Pegasus"]],
|
||||
["Werewolf", ["To Pegasus From Werewolf", "To Night Staircase From Werewolf"]],
|
||||
["Night Staircase", ["To Werewolf From Night Staircase", "To Janitor Closet", "To UFO From Night Staircase"]],
|
||||
["Janitor Closet", ["To Night Staircase From Janitor Closet", "To Water Capture From Janitor Closet", "To Cloth Capture From Janitor Closet"]],
|
||||
["UFO", ["To Night Staircase From UFO", "To Inventions From UFO"]],
|
||||
["UFO", ["To Night Staircase From UFO", "To Orrery From UFO", "To Inventions From UFO"]],
|
||||
["Orrery", ["To UFO From Orrery"]],
|
||||
["Blue Maze", ["To Egypt From Blue Maze", "To Three Floor Elevator From Blue Maze Bottom", "To Three Floor Elevator From Blue Maze Top", "To Fortune Teller", "To Inventions From Blue Maze", "To Wood Capture From Blue Maze"]],
|
||||
["Three Floor Elevator", ["To Maintenance Tunnels From Three Floor Elevator", "To Blue Maze From Three Floor Elevator"]],
|
||||
["Fortune Teller", ["To Blue Maze From Fortune Teller"]],
|
||||
["Inventions", ["To Blue Maze From Inventions", "To UFO From Inventions", "To Torture From Inventions"]],
|
||||
["Torture", ["To Inventions From Torture", "To Puzzle Room Mastermind From Torture"]],
|
||||
["Guillotine", ["To Torture From Guillotine"]],
|
||||
["Puzzle Room Mastermind", ["To Torture", "To Puzzle Room Marbles From Puzzle Room Mastermind"]],
|
||||
["Puzzle Room Marbles", ["To Puzzle Room Mastermind From Puzzle Room Marbles", "To Skull Dial Bridge From Puzzle Room Marbles"]],
|
||||
["Skull Dial Bridge", ["To Puzzle Room Marbles From Skull Dial Bridge", "To Slide Room"]],
|
||||
["Slide Room", ["To Skull Dial Bridge From Slide Room", "To Lobby From Slide Room"]],
|
||||
["Puzzle Room Marbles", ["To Puzzle Room Mastermind From Puzzle Room Marbles", "To Skull Bridge From Puzzle Room Marbles"]],
|
||||
["Skull Bridge", ["To Puzzle Room Marbles From Skull Bridge", "To Slide Room"]],
|
||||
["Slide Room", ["To Skull Bridge From Slide Room", "To Lobby From Slide Room"]],
|
||||
["Water Capture", []],
|
||||
["Wax Capture", []],
|
||||
["Ash Capture", []],
|
||||
@@ -52,17 +59,20 @@
|
||||
["Wood Capture", []],
|
||||
["Crystal Capture", []],
|
||||
["Sand Capture", []],
|
||||
["Metal Capture", []]
|
||||
["Metal Capture", []],
|
||||
["Victory", []]
|
||||
],
|
||||
"mandatory_connections": [
|
||||
["To Registry", "Registry"],
|
||||
["To Registry", "Registry"],
|
||||
["To Outside From Registry", "Outside"],
|
||||
["To Outside From Underground", "Outside"],
|
||||
["To Outside From Lobby", "Outside"],
|
||||
["To Underground Tunnels From Outside", "Underground Tunnels"],
|
||||
["To Underground Tunnels From Underground Lake", "Underground Tunnels"],
|
||||
["To Underground Lake From Underground Tunnels", "Underground Lake"],
|
||||
["To Underground Lake From Windlenot's Body", "Underground Lake"],
|
||||
["To Underground Lake From Underground Blue Tunnels", "Underground Lake"],
|
||||
["To Windlenot's Body From Underground Lake", "Windlenot's Body"],
|
||||
["To Underground Blue Tunnels From Underground Lake", "Underground Blue Tunnels"],
|
||||
["To Underground Blue Tunnels From Office Elevator", "Underground Blue Tunnels"],
|
||||
["To Office Elevator From Underground Blue Tunnels", "Office Elevator"],
|
||||
@@ -86,7 +96,7 @@
|
||||
["To Library From Lobby", "Library"],
|
||||
["To Library From Maintenance Tunnels", "Library"],
|
||||
["To Theater From Lobby", "Theater" ],
|
||||
["To Theater From Theater Back Hallways", "Theater"],
|
||||
["To Theater From Theater Back Hallway", "Theater"],
|
||||
["To Prehistoric From Lobby", "Prehistoric"],
|
||||
["To Prehistoric From Greenhouse", "Prehistoric"],
|
||||
["To Prehistoric From Ocean", "Prehistoric"],
|
||||
@@ -96,15 +106,17 @@
|
||||
["To Maintenance Tunnels From Generator", "Maintenance Tunnels"],
|
||||
["To Maintenance Tunnels From Three Floor Elevator", "Maintenance Tunnels"],
|
||||
["To Maintenance Tunnels From Library", "Maintenance Tunnels"],
|
||||
["To Maintenance Tunnels From Theater Back Hallways", "Maintenance Tunnels"],
|
||||
["To Maintenance Tunnels From Theater Back Hallway", "Maintenance Tunnels"],
|
||||
["To Three Floor Elevator From Maintenance Tunnels", "Three Floor Elevator"],
|
||||
["To Three Floor Elevator From Blue Maze Bottom", "Three Floor Elevator"],
|
||||
["To Three Floor Elevator From Blue Maze Top", "Three Floor Elevator"],
|
||||
["To Generator", "Generator"],
|
||||
["To Theater Back Hallways From Theater", "Theater Back Hallways"],
|
||||
["To Theater Back Hallways From Clock Tower Staircase", "Theater Back Hallways"],
|
||||
["To Theater Back Hallways From Projector Room", "Theater Back Hallways"],
|
||||
["To Clock Tower Staircase From Theater Back Hallways", "Clock Tower Staircase"],
|
||||
["To Generator From Maintenance Tunnels", "Generator"],
|
||||
["To Generator From Beth's Body", "Generator"],
|
||||
["To Theater Back Hallway From Theater", "Theater Back Hallway"],
|
||||
["To Theater Back Hallway From Clock Tower Staircase", "Theater Back Hallway"],
|
||||
["To Theater Back Hallway From Projector Room", "Theater Back Hallway"],
|
||||
["To Clock Tower Staircase From Theater Back Hallway", "Clock Tower Staircase"],
|
||||
["To Clock Tower Staircase From Clock Chains", "Clock Tower Staircase"],
|
||||
["To Clock Tower Staircase From Clock Tower", "Clock Tower Staircase"],
|
||||
["To Projector Room", "Projector Room"],
|
||||
["To Clock Tower", "Clock Tower"],
|
||||
@@ -125,30 +137,37 @@
|
||||
["To Blue Maze From Egypt", "Blue Maze"],
|
||||
["To Shaman From Burial", "Shaman"],
|
||||
["To Shaman From Gods Room", "Shaman"],
|
||||
["To Gods Room", "Gods Room" ],
|
||||
["To Gods Room From Shaman", "Gods Room" ],
|
||||
["To Gods Room From Norse Stone", "Gods Room" ],
|
||||
["To Gods Room From Anansi", "Gods Room"],
|
||||
["To Norse Stone From Gods Room", "Norse Stone" ],
|
||||
["To Anansi From Gods Room", "Anansi"],
|
||||
["To Anansi From Werewolf", "Anansi"],
|
||||
["To Werewolf From Anansi", "Werewolf"],
|
||||
["To Anansi From Pegasus", "Anansi"],
|
||||
["To Pegasus From Anansi", "Pegasus"],
|
||||
["To Pegasus From Werewolf", "Pegasus"],
|
||||
["To Werewolf From Pegasus", "Werewolf"],
|
||||
["To Werewolf From Night Staircase", "Werewolf"],
|
||||
["To Night Staircase From Werewolf", "Night Staircase"],
|
||||
["To Night Staircase From Janitor Closet", "Night Staircase"],
|
||||
["To Night Staircase From UFO", "Night Staircase"],
|
||||
["To Janitor Closet", "Janitor Closet"],
|
||||
["To UFO", "UFO"],
|
||||
["To UFO From Night Staircase", "UFO"],
|
||||
["To UFO From Orrery", "UFO"],
|
||||
["To UFO From Inventions", "UFO"],
|
||||
["To Orrery From UFO", "Orrery"],
|
||||
["To Inventions From UFO", "Inventions"],
|
||||
["To Inventions From Blue Maze", "Inventions"],
|
||||
["To Inventions From Torture", "Inventions"],
|
||||
["To Fortune Teller", "Fortune Teller"],
|
||||
["To Torture", "Torture"],
|
||||
["To Torture From Guillotine", "Torture"],
|
||||
["To Torture From Inventions", "Torture"],
|
||||
["To Puzzle Room Mastermind From Torture", "Puzzle Room Mastermind"],
|
||||
["To Puzzle Room Mastermind From Puzzle Room Marbles", "Puzzle Room Mastermind"],
|
||||
["To Puzzle Room Marbles From Puzzle Room Mastermind", "Puzzle Room Marbles"],
|
||||
["To Puzzle Room Marbles From Skull Dial Bridge", "Puzzle Room Marbles"],
|
||||
["To Skull Dial Bridge From Puzzle Room Marbles", "Skull Dial Bridge"],
|
||||
["To Skull Dial Bridge From Slide Room", "Skull Dial Bridge"],
|
||||
["To Puzzle Room Marbles From Skull Bridge", "Puzzle Room Marbles"],
|
||||
["To Skull Bridge From Puzzle Room Marbles", "Skull Bridge"],
|
||||
["To Skull Bridge From Slide Room", "Skull Bridge"],
|
||||
["To Slide Room", "Slide Room"],
|
||||
["To Wax Capture From Library", "Wax Capture"],
|
||||
["To Wax Capture From Shaman", "Wax Capture"],
|
||||
@@ -164,7 +183,7 @@
|
||||
["To Cloth Capture From Janitor Closet", "Cloth Capture"],
|
||||
["To Wood Capture From Workshop", "Wood Capture"],
|
||||
["To Wood Capture From Gods Room", "Wood Capture"],
|
||||
["To Wood Capture From Anansi", "Wood Capture"],
|
||||
["To Wood Capture From Pegasus", "Wood Capture"],
|
||||
["To Wood Capture From Blue Maze", "Wood Capture"],
|
||||
["To Crystal Capture From Lobby", "Crystal Capture"],
|
||||
["To Crystal Capture From Ocean", "Crystal Capture"],
|
||||
@@ -172,6 +191,7 @@
|
||||
["To Sand Capture From Ocean", "Sand Capture"],
|
||||
["To Metal Capture From Bedroom", "Metal Capture"],
|
||||
["To Metal Capture From Projector Room", "Metal Capture"],
|
||||
["To Metal Capture From Prehistoric", "Metal Capture"]
|
||||
["To Metal Capture From Prehistoric", "Metal Capture"],
|
||||
["To Victory", "Victory"]
|
||||
]
|
||||
}
|
||||
|
||||
@@ -27,5 +27,4 @@ Victory is achieved when the player has captured the required number Ixupi set i
|
||||
|
||||
## Encountered a bug?
|
||||
|
||||
Please contact GodlFire on Discord for bugs related to Shivers world generation.<br>
|
||||
Please contact GodlFire or mouse on Discord for bugs related to the Shivers Randomizer.
|
||||
Please contact GodlFire or Cynbel_Terreus on Discord for bugs related to Shivers world generation or the Shivers Randomizer.
|
||||
|
||||
@@ -7,6 +7,11 @@
|
||||
- [ScummVM](https://www.scummvm.org/downloads/) version 2.7.0 or later
|
||||
- [Shivers Randomizer](https://github.com/GodlFire/Shivers-Randomizer-CSharp/releases/latest) Latest release version
|
||||
|
||||
## Optional Software
|
||||
|
||||
- [PopTracker](https://github.com/black-sliver/PopTracker/releases/)
|
||||
- [Jax's Shivers PopTracker pack](https://github.com/blazik-barth/Shivers-Tracker/releases/)
|
||||
|
||||
## Setup ScummVM for Shivers
|
||||
|
||||
### GOG version of Shivers
|
||||
@@ -57,4 +62,4 @@ validator page: [YAML Validation page](/mysterycheck)
|
||||
- Every puzzle
|
||||
- Every puzzle hint/solution
|
||||
- Every document that is considered a Flashback
|
||||
- Optionally information plaques.
|
||||
- Optionally information plaques
|
||||
|
||||
@@ -4,18 +4,59 @@ import os
|
||||
import json
|
||||
import Utils
|
||||
from Utils import read_snes_rom
|
||||
from worlds.Files import APDeltaPatch
|
||||
from worlds.Files import APPatchExtension, APProcedurePatch, APTokenMixin, APTokenTypes
|
||||
from .variaRandomizer.utils.utils import openFile
|
||||
|
||||
SMJUHASH = '21f3e98df4780ee1c667b84e57d88675'
|
||||
SM_ROM_MAX_PLAYERID = 65535
|
||||
SM_ROM_PLAYERDATA_COUNT = 202
|
||||
|
||||
class SMDeltaPatch(APDeltaPatch):
|
||||
class SMPatchExtensions(APPatchExtension):
|
||||
game = "Super Metroid"
|
||||
|
||||
@staticmethod
|
||||
def write_crc(caller: APProcedurePatch, rom: bytes) -> bytes:
|
||||
def checksum_mirror_sum(start, length, mask = 0x800000):
|
||||
while not(length & mask) and mask:
|
||||
mask >>= 1
|
||||
|
||||
part1 = sum(start[:mask]) & 0xFFFF
|
||||
part2 = 0
|
||||
|
||||
next_length = length - mask
|
||||
if next_length:
|
||||
part2 = checksum_mirror_sum(start[mask:], next_length, mask >> 1)
|
||||
|
||||
while (next_length < mask):
|
||||
next_length += next_length
|
||||
part2 += part2
|
||||
|
||||
return (part1 + part2) & 0xFFFF
|
||||
|
||||
def write_bytes(buffer, startaddress: int, values):
|
||||
buffer[startaddress:startaddress + len(values)] = values
|
||||
|
||||
buffer = bytearray(rom)
|
||||
crc = checksum_mirror_sum(buffer, len(buffer))
|
||||
inv = crc ^ 0xFFFF
|
||||
write_bytes(buffer, 0x7FDC, [inv & 0xFF, (inv >> 8) & 0xFF, crc & 0xFF, (crc >> 8) & 0xFF])
|
||||
return bytes(buffer)
|
||||
|
||||
class SMProcedurePatch(APProcedurePatch, APTokenMixin):
|
||||
hash = SMJUHASH
|
||||
game = "Super Metroid"
|
||||
patch_file_ending = ".apsm"
|
||||
|
||||
procedure = [
|
||||
("apply_tokens", ["token_data.bin"]),
|
||||
("write_crc", [])
|
||||
]
|
||||
|
||||
def write_tokens(self, patches):
|
||||
for addr, data in patches.items():
|
||||
self.write_token(APTokenTypes.WRITE, addr, bytes(data))
|
||||
self.write_file("token_data.bin", self.get_token_binary())
|
||||
|
||||
@classmethod
|
||||
def get_source_data(cls) -> bytes:
|
||||
return get_base_rom_bytes()
|
||||
|
||||
@@ -17,7 +17,7 @@ logger = logging.getLogger("Super Metroid")
|
||||
|
||||
from .Options import SMOptions
|
||||
from .Client import SMSNIClient
|
||||
from .Rom import get_base_rom_path, SM_ROM_MAX_PLAYERID, SM_ROM_PLAYERDATA_COUNT, SMDeltaPatch, get_sm_symbols
|
||||
from .Rom import SM_ROM_MAX_PLAYERID, SM_ROM_PLAYERDATA_COUNT, SMProcedurePatch, get_sm_symbols
|
||||
import Utils
|
||||
|
||||
from .variaRandomizer.logic.smboolmanager import SMBoolManager
|
||||
@@ -40,7 +40,7 @@ class SMSettings(settings.Group):
|
||||
"""File name of the v1.0 J rom"""
|
||||
description = "Super Metroid (JU) ROM"
|
||||
copy_to = "Super Metroid (JU).sfc"
|
||||
md5s = [SMDeltaPatch.hash]
|
||||
md5s = [SMProcedurePatch.hash]
|
||||
|
||||
rom_file: RomFile = RomFile(RomFile.copy_to)
|
||||
|
||||
@@ -120,12 +120,6 @@ class SMWorld(World):
|
||||
self.locations = {}
|
||||
super().__init__(world, player)
|
||||
|
||||
@classmethod
|
||||
def stage_assert_generate(cls, multiworld: MultiWorld):
|
||||
rom_file = get_base_rom_path()
|
||||
if not os.path.exists(rom_file):
|
||||
raise FileNotFoundError(rom_file)
|
||||
|
||||
def generate_early(self):
|
||||
Logic.factory('vanilla')
|
||||
|
||||
@@ -802,23 +796,19 @@ class SMWorld(World):
|
||||
romPatcher.end()
|
||||
|
||||
def generate_output(self, output_directory: str):
|
||||
self.variaRando.args.rom = get_base_rom_path()
|
||||
outfilebase = self.multiworld.get_out_file_name_base(self.player)
|
||||
outputFilename = os.path.join(output_directory, f"{outfilebase}.sfc")
|
||||
|
||||
try:
|
||||
self.variaRando.PatchRom(outputFilename, self.APPrePatchRom, self.APPostPatchRom)
|
||||
self.write_crc(outputFilename)
|
||||
patcher = self.variaRando.PatchRom(self.APPrePatchRom, self.APPostPatchRom)
|
||||
self.rom_name = self.romName
|
||||
|
||||
patch = SMProcedurePatch(player=self.player, player_name=self.multiworld.player_name[self.player])
|
||||
patch.write_tokens(patcher.romFile.getPatchDict())
|
||||
rom_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}"
|
||||
f"{patch.patch_file_ending}")
|
||||
patch.write(rom_path)
|
||||
|
||||
except:
|
||||
raise
|
||||
else:
|
||||
patch = SMDeltaPatch(os.path.splitext(outputFilename)[0] + SMDeltaPatch.patch_file_ending, player=self.player,
|
||||
player_name=self.multiworld.player_name[self.player], patched_path=outputFilename)
|
||||
patch.write()
|
||||
finally:
|
||||
if os.path.exists(outputFilename):
|
||||
os.unlink(outputFilename)
|
||||
self.rom_name_available_event.set() # make sure threading continues and errors are collected
|
||||
|
||||
def checksum_mirror_sum(self, start, length, mask = 0x800000):
|
||||
|
||||
@@ -680,7 +680,7 @@ class VariaRandomizer:
|
||||
#dumpErrorMsg(args.output, self.randoExec.errorMsg)
|
||||
raise Exception("Can't generate " + self.fileName + " with the given parameters: {}".format(self.randoExec.errorMsg))
|
||||
|
||||
def PatchRom(self, outputFilename, customPrePatchApply = None, customPostPatchApply = None):
|
||||
def PatchRom(self, customPrePatchApply = None, customPostPatchApply = None) -> RomPatcher:
|
||||
args = self.args
|
||||
optErrMsgs = self.optErrMsgs
|
||||
|
||||
@@ -758,9 +758,9 @@ class VariaRandomizer:
|
||||
# args.output is not None: generate local json named args.output
|
||||
if args.rom is not None:
|
||||
# patch local rom
|
||||
romFileName = args.rom
|
||||
shutil.copyfile(romFileName, outputFilename)
|
||||
romPatcher = RomPatcher(settings=patcherSettings, romFileName=outputFilename, magic=args.raceMagic, player=self.player)
|
||||
# romFileName = args.rom
|
||||
# shutil.copyfile(romFileName, outputFilename)
|
||||
romPatcher = RomPatcher(settings=patcherSettings, magic=args.raceMagic, player=self.player)
|
||||
else:
|
||||
romPatcher = RomPatcher(settings=patcherSettings, magic=args.raceMagic)
|
||||
|
||||
@@ -779,24 +779,12 @@ class VariaRandomizer:
|
||||
#msg = randoExec.errorMsg
|
||||
msg = ''
|
||||
|
||||
if args.rom is None: # web mode
|
||||
data = romPatcher.romFile.data
|
||||
self.fileName = '{}.sfc'.format(self.fileName)
|
||||
data["fileName"] = self.fileName
|
||||
# error msg in json to be displayed by the web site
|
||||
data["errorMsg"] = msg
|
||||
# replaced parameters to update stats in database
|
||||
if len(self.forcedArgs) > 0:
|
||||
data["forcedArgs"] = self.forcedArgs
|
||||
with open(outputFilename, 'w') as jsonFile:
|
||||
json.dump(data, jsonFile)
|
||||
else: # CLI mode
|
||||
if msg != "":
|
||||
print(msg)
|
||||
return romPatcher
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
traceback.print_exc(file=sys.stdout)
|
||||
raise Exception("Error patching {}: ({}: {})".format(outputFilename, type(e).__name__, e))
|
||||
raise Exception("Error patching: ({}: {})".format(type(e).__name__, e))
|
||||
#dumpErrorMsg(args.output, msg)
|
||||
|
||||
# if stuck == True:
|
||||
|
||||
@@ -21,10 +21,23 @@ class IPS_Patch(object):
|
||||
def toDict(self):
|
||||
ret = {}
|
||||
for record in self.records:
|
||||
if 'rle_count' in record:
|
||||
ret[record['address']] = [int.from_bytes(record['data'],'little')]*record['rle_count']
|
||||
if record['address'] in ret.keys():
|
||||
if 'rle_count' in record:
|
||||
if len(ret[record['address']]) > record['rle_count']:
|
||||
ret[record['address']][:record['rle_count']] = [int.from_bytes(record['data'],'little')]*record['rle_count']
|
||||
else:
|
||||
ret[record['address']] = [int.from_bytes(record['data'],'little')]*record['rle_count']
|
||||
else:
|
||||
size = len(record['data'])
|
||||
if len(ret[record['address']]) > size:
|
||||
ret[record['address']][:size] = [int(b) for b in record['data']]
|
||||
else:
|
||||
ret[record['address']] = [int(b) for b in record['data']]
|
||||
else:
|
||||
ret[record['address']] = [int(b) for b in record['data']]
|
||||
if 'rle_count' in record:
|
||||
ret[record['address']] = [int.from_bytes(record['data'],'little')]*record['rle_count']
|
||||
else:
|
||||
ret[record['address']] = [int(b) for b in record['data']]
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -86,7 +86,67 @@ class ROM(object):
|
||||
self.seek(self.maxAddress + BANK_SIZE - off - 1)
|
||||
self.writeByte(0xff)
|
||||
assert (self.maxAddress % BANK_SIZE) == 0
|
||||
|
||||
|
||||
class FakeROM(ROM):
|
||||
# to have the same code for real ROM and the webservice
|
||||
def __init__(self, data={}):
|
||||
super(FakeROM, self).__init__()
|
||||
self.data = data
|
||||
self.ipsPatches = []
|
||||
|
||||
def write(self, bytes):
|
||||
for byte in bytes:
|
||||
self.data[self.address] = byte
|
||||
self.inc()
|
||||
|
||||
def read(self, byteCount):
|
||||
bytes = []
|
||||
for i in range(byteCount):
|
||||
bytes.append(self.data[self.address])
|
||||
self.inc()
|
||||
|
||||
return bytes
|
||||
|
||||
def ipsPatch(self, ipsPatches):
|
||||
self.ipsPatches += ipsPatches
|
||||
|
||||
# generate ips from self data
|
||||
def ips(self):
|
||||
groupedData = {}
|
||||
startAddress = -1
|
||||
prevAddress = -1
|
||||
curData = []
|
||||
for address in sorted(self.data):
|
||||
if address == prevAddress + 1:
|
||||
curData.append(self.data[address])
|
||||
prevAddress = address
|
||||
else:
|
||||
if len(curData) > 0:
|
||||
groupedData[startAddress] = curData
|
||||
startAddress = address
|
||||
prevAddress = address
|
||||
curData = [self.data[startAddress]]
|
||||
if startAddress != -1:
|
||||
groupedData[startAddress] = curData
|
||||
|
||||
return IPS_Patch(groupedData)
|
||||
|
||||
# generate final IPS for web patching with first the IPS patches, then written data
|
||||
def close(self):
|
||||
self.mergedIPS = IPS_Patch()
|
||||
for ips in self.ipsPatches:
|
||||
self.mergedIPS.append(ips)
|
||||
self.mergedIPS.append(self.ips())
|
||||
#patchData = mergedIPS.encode()
|
||||
#self.data = {}
|
||||
#self.data["ips"] = base64.b64encode(patchData).decode()
|
||||
#if mergedIPS.truncate_length is not None:
|
||||
# self.data["truncate_length"] = mergedIPS.truncate_length
|
||||
#self.data["max_size"] = mergedIPS.max_size
|
||||
|
||||
def getPatchDict(self):
|
||||
return self.mergedIPS.toDict()
|
||||
|
||||
class RealROM(ROM):
|
||||
def __init__(self, name):
|
||||
super(RealROM, self).__init__()
|
||||
|
||||
@@ -7,7 +7,7 @@ from ..utils.doorsmanager import DoorsManager, IndicatorFlag
|
||||
from ..utils.objectives import Objectives
|
||||
from ..graph.graph_utils import GraphUtils, getAccessPoint, locIdsByAreaAddresses, graphAreas
|
||||
from ..logic.logic import Logic
|
||||
from ..rom.rom import RealROM, snes_to_pc, pc_to_snes
|
||||
from ..rom.rom import FakeROM, snes_to_pc, pc_to_snes
|
||||
from ..rom.addresses import Addresses
|
||||
from ..rom.rom_patches import RomPatches
|
||||
from ..patches.patchaccess import PatchAccess
|
||||
@@ -52,10 +52,10 @@ class RomPatcher:
|
||||
def __init__(self, settings=None, romFileName=None, magic=None, player=0):
|
||||
self.log = log.get('RomPatcher')
|
||||
self.settings = settings
|
||||
self.romFileName = romFileName
|
||||
#self.romFileName = romFileName
|
||||
self.patchAccess = PatchAccess()
|
||||
self.race = None
|
||||
self.romFile = RealROM(romFileName)
|
||||
self.romFile = FakeROM()
|
||||
#if magic is not None:
|
||||
# from rom.race_mode import RaceModePatcher
|
||||
# self.race = RaceModePatcher(self, magic)
|
||||
@@ -312,7 +312,7 @@ class RomPatcher:
|
||||
self.applyStartAP(self.settings["startLocation"], plms, doors)
|
||||
self.applyPLMs(plms)
|
||||
except Exception as e:
|
||||
raise Exception("Error patching {}. ({})".format(self.romFileName, e))
|
||||
raise Exception("Error patching. ({})".format(e))
|
||||
|
||||
def applyIPSPatch(self, patchName, patchDict=None, ipsDir=None):
|
||||
if patchDict is None:
|
||||
@@ -493,6 +493,7 @@ class RomPatcher:
|
||||
|
||||
def commitIPS(self):
|
||||
self.romFile.ipsPatch(self.ipsPatches)
|
||||
self.ipsPatches = []
|
||||
|
||||
def writeSeed(self, seed):
|
||||
random.seed(seed)
|
||||
|
||||
@@ -772,6 +772,7 @@ if 'unittest' in sys.modules.keys() or 'pytest' in sys.modules.keys():
|
||||
|
||||
class Mods(OptionSet):
|
||||
"""List of mods that will be included in the shuffling."""
|
||||
visibility = Visibility.all & ~Visibility.simple_ui
|
||||
internal_name = "mods"
|
||||
display_name = "Mods"
|
||||
valid_keys = {ModNames.deepwoods, ModNames.tractor, ModNames.big_backpack,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import itertools
|
||||
from typing import List, Dict, Any, cast
|
||||
|
||||
@@ -10,13 +9,11 @@ from . import items
|
||||
from . import locations
|
||||
from . import creatures
|
||||
from . import options
|
||||
from .items import item_table, group_items, items_by_type, ItemType
|
||||
from .items import item_table, group_items
|
||||
from .rules import set_rules
|
||||
|
||||
logger = logging.getLogger("Subnautica")
|
||||
|
||||
|
||||
class SubnaticaWeb(WebWorld):
|
||||
class SubnauticaWeb(WebWorld):
|
||||
tutorials = [Tutorial(
|
||||
"Multiworld Setup Guide",
|
||||
"A guide to setting up the Subnautica randomizer connected to an Archipelago Multiworld",
|
||||
@@ -38,7 +35,7 @@ class SubnauticaWorld(World):
|
||||
You must find a cure for yourself, build an escape rocket, and leave the planet.
|
||||
"""
|
||||
game = "Subnautica"
|
||||
web = SubnaticaWeb()
|
||||
web = SubnauticaWeb()
|
||||
|
||||
item_name_to_id = {data.name: item_id for item_id, data in items.item_table.items()}
|
||||
location_name_to_id = all_locations
|
||||
|
||||
@@ -90,6 +90,10 @@ class TunicWorld(World):
|
||||
item_link_locations: Dict[int, Dict[str, List[Tuple[int, str]]]] = {}
|
||||
player_item_link_locations: Dict[str, List[Location]]
|
||||
|
||||
using_ut: bool # so we can check if we're using UT only once
|
||||
passthrough: Dict[str, Any]
|
||||
ut_can_gen_without_yaml = True # class var that tells it to ignore the player yaml
|
||||
|
||||
def generate_early(self) -> None:
|
||||
if self.options.logic_rules >= LogicRules.option_no_major_glitches:
|
||||
self.options.laurels_zips.value = LaurelsZips.option_true
|
||||
@@ -113,23 +117,28 @@ class TunicWorld(World):
|
||||
# Universal tracker stuff, shouldn't do anything in standard gen
|
||||
if hasattr(self.multiworld, "re_gen_passthrough"):
|
||||
if "TUNIC" in self.multiworld.re_gen_passthrough:
|
||||
passthrough = self.multiworld.re_gen_passthrough["TUNIC"]
|
||||
self.options.start_with_sword.value = passthrough["start_with_sword"]
|
||||
self.options.keys_behind_bosses.value = passthrough["keys_behind_bosses"]
|
||||
self.options.sword_progression.value = passthrough["sword_progression"]
|
||||
self.options.ability_shuffling.value = passthrough["ability_shuffling"]
|
||||
self.options.laurels_zips.value = passthrough["laurels_zips"]
|
||||
self.options.ice_grappling.value = passthrough["ice_grappling"]
|
||||
self.options.ladder_storage.value = passthrough["ladder_storage"]
|
||||
self.options.ladder_storage_without_items = passthrough["ladder_storage_without_items"]
|
||||
self.options.lanternless.value = passthrough["lanternless"]
|
||||
self.options.maskless.value = passthrough["maskless"]
|
||||
self.options.hexagon_quest.value = passthrough["hexagon_quest"]
|
||||
self.options.entrance_rando.value = passthrough["entrance_rando"]
|
||||
self.options.shuffle_ladders.value = passthrough["shuffle_ladders"]
|
||||
self.using_ut = True
|
||||
self.passthrough = self.multiworld.re_gen_passthrough["TUNIC"]
|
||||
self.options.start_with_sword.value = self.passthrough["start_with_sword"]
|
||||
self.options.keys_behind_bosses.value = self.passthrough["keys_behind_bosses"]
|
||||
self.options.sword_progression.value = self.passthrough["sword_progression"]
|
||||
self.options.ability_shuffling.value = self.passthrough["ability_shuffling"]
|
||||
self.options.laurels_zips.value = self.passthrough["laurels_zips"]
|
||||
self.options.ice_grappling.value = self.passthrough["ice_grappling"]
|
||||
self.options.ladder_storage.value = self.passthrough["ladder_storage"]
|
||||
self.options.ladder_storage_without_items = self.passthrough["ladder_storage_without_items"]
|
||||
self.options.lanternless.value = self.passthrough["lanternless"]
|
||||
self.options.maskless.value = self.passthrough["maskless"]
|
||||
self.options.hexagon_quest.value = self.passthrough["hexagon_quest"]
|
||||
self.options.entrance_rando.value = self.passthrough["entrance_rando"]
|
||||
self.options.shuffle_ladders.value = self.passthrough["shuffle_ladders"]
|
||||
self.options.fixed_shop.value = self.options.fixed_shop.option_false
|
||||
self.options.laurels_location.value = self.options.laurels_location.option_anywhere
|
||||
self.options.combat_logic.value = passthrough["combat_logic"]
|
||||
self.options.combat_logic.value = self.passthrough["combat_logic"]
|
||||
else:
|
||||
self.using_ut = False
|
||||
else:
|
||||
self.using_ut = False
|
||||
|
||||
@classmethod
|
||||
def stage_generate_early(cls, multiworld: MultiWorld) -> None:
|
||||
@@ -284,12 +293,14 @@ class TunicWorld(World):
|
||||
|
||||
remove_filler(items_to_create[gold_hexagon])
|
||||
|
||||
for hero_relic in item_name_groups["Hero Relics"]:
|
||||
# Sort for deterministic order
|
||||
for hero_relic in sorted(item_name_groups["Hero Relics"]):
|
||||
tunic_items.append(self.create_item(hero_relic, ItemClassification.useful))
|
||||
items_to_create[hero_relic] = 0
|
||||
|
||||
if not self.options.ability_shuffling:
|
||||
for page in item_name_groups["Abilities"]:
|
||||
# Sort for deterministic order
|
||||
for page in sorted(item_name_groups["Abilities"]):
|
||||
if items_to_create[page] > 0:
|
||||
tunic_items.append(self.create_item(page, ItemClassification.useful))
|
||||
items_to_create[page] = 0
|
||||
@@ -329,12 +340,10 @@ class TunicWorld(World):
|
||||
self.ability_unlocks = randomize_ability_unlocks(self.random, self.options)
|
||||
|
||||
# stuff for universal tracker support, can be ignored for standard gen
|
||||
if hasattr(self.multiworld, "re_gen_passthrough"):
|
||||
if "TUNIC" in self.multiworld.re_gen_passthrough:
|
||||
passthrough = self.multiworld.re_gen_passthrough["TUNIC"]
|
||||
self.ability_unlocks["Pages 24-25 (Prayer)"] = passthrough["Hexagon Quest Prayer"]
|
||||
self.ability_unlocks["Pages 42-43 (Holy Cross)"] = passthrough["Hexagon Quest Holy Cross"]
|
||||
self.ability_unlocks["Pages 52-53 (Icebolt)"] = passthrough["Hexagon Quest Icebolt"]
|
||||
if self.using_ut:
|
||||
self.ability_unlocks["Pages 24-25 (Prayer)"] = self.passthrough["Hexagon Quest Prayer"]
|
||||
self.ability_unlocks["Pages 42-43 (Holy Cross)"] = self.passthrough["Hexagon Quest Holy Cross"]
|
||||
self.ability_unlocks["Pages 52-53 (Icebolt)"] = self.passthrough["Hexagon Quest Icebolt"]
|
||||
|
||||
# Ladders and Combat Logic uses ER rules with vanilla connections for easier maintenance
|
||||
if self.options.entrance_rando or self.options.shuffle_ladders or self.options.combat_logic:
|
||||
|
||||
@@ -1675,7 +1675,7 @@ def set_er_location_rules(world: "TunicWorld") -> None:
|
||||
|
||||
# Beneath the Vault
|
||||
set_rule(world.get_location("Beneath the Fortress - Bridge"),
|
||||
lambda state: has_melee(state, player) or state.has_any({laurels, fire_wand}, player))
|
||||
lambda state: has_melee(state, player) or state.has_any((laurels, fire_wand, ice_dagger, gun), player))
|
||||
|
||||
# Quarry
|
||||
set_rule(world.get_location("Quarry - [Central] Above Ladder Dash Chest"),
|
||||
|
||||
@@ -177,7 +177,7 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
|
||||
logic_tricks: Tuple[bool, int, int] = (laurels_zips, ice_grappling, ladder_storage)
|
||||
|
||||
# marking that you don't immediately have laurels
|
||||
if laurels_location == "10_fairies" and not hasattr(world.multiworld, "re_gen_passthrough"):
|
||||
if laurels_location == "10_fairies" and not world.using_ut:
|
||||
has_laurels = False
|
||||
|
||||
shop_count = 6
|
||||
@@ -191,9 +191,8 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
|
||||
break
|
||||
|
||||
# If using Universal Tracker, restore portal_map. Could be cleaner, but it does not matter for UT even a little bit
|
||||
if hasattr(world.multiworld, "re_gen_passthrough"):
|
||||
if "TUNIC" in world.multiworld.re_gen_passthrough:
|
||||
portal_map = portal_mapping.copy()
|
||||
if world.using_ut:
|
||||
portal_map = portal_mapping.copy()
|
||||
|
||||
# create separate lists for dead ends and non-dead ends
|
||||
for portal in portal_map:
|
||||
@@ -232,25 +231,24 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
|
||||
plando_connections = world.seed_groups[world.options.entrance_rando.value]["plando"]
|
||||
|
||||
# universal tracker support stuff, don't need to care about region dependency
|
||||
if hasattr(world.multiworld, "re_gen_passthrough"):
|
||||
if "TUNIC" in world.multiworld.re_gen_passthrough:
|
||||
plando_connections.clear()
|
||||
# universal tracker stuff, won't do anything in normal gen
|
||||
for portal1, portal2 in world.multiworld.re_gen_passthrough["TUNIC"]["Entrance Rando"].items():
|
||||
portal_name1 = ""
|
||||
portal_name2 = ""
|
||||
if world.using_ut:
|
||||
plando_connections.clear()
|
||||
# universal tracker stuff, won't do anything in normal gen
|
||||
for portal1, portal2 in world.passthrough["Entrance Rando"].items():
|
||||
portal_name1 = ""
|
||||
portal_name2 = ""
|
||||
|
||||
for portal in portal_mapping:
|
||||
if portal.scene_destination() == portal1:
|
||||
portal_name1 = portal.name
|
||||
# connected_regions.update(add_dependent_regions(portal.region, logic_rules))
|
||||
if portal.scene_destination() == portal2:
|
||||
portal_name2 = portal.name
|
||||
# connected_regions.update(add_dependent_regions(portal.region, logic_rules))
|
||||
# shops have special handling
|
||||
if not portal_name2 and portal2 == "Shop, Previous Region_":
|
||||
portal_name2 = "Shop Portal"
|
||||
plando_connections.append(PlandoConnection(portal_name1, portal_name2, "both"))
|
||||
for portal in portal_mapping:
|
||||
if portal.scene_destination() == portal1:
|
||||
portal_name1 = portal.name
|
||||
# connected_regions.update(add_dependent_regions(portal.region, logic_rules))
|
||||
if portal.scene_destination() == portal2:
|
||||
portal_name2 = portal.name
|
||||
# connected_regions.update(add_dependent_regions(portal.region, logic_rules))
|
||||
# shops have special handling
|
||||
if not portal_name2 and portal2 == "Shop, Previous Region_":
|
||||
portal_name2 = "Shop Portal"
|
||||
plando_connections.append(PlandoConnection(portal_name1, portal_name2, "both"))
|
||||
|
||||
non_dead_end_regions = set()
|
||||
for region_name, region_info in world.er_regions.items():
|
||||
@@ -362,7 +360,7 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
|
||||
# if we have plando connections, our connected regions may change somewhat
|
||||
connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_tricks)
|
||||
|
||||
if fixed_shop and not hasattr(world.multiworld, "re_gen_passthrough"):
|
||||
if fixed_shop and not world.using_ut:
|
||||
portal1 = None
|
||||
for portal in two_plus:
|
||||
if portal.scene_destination() == "Overworld Redux, Windmill_":
|
||||
@@ -392,7 +390,7 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
|
||||
fail_count = 0
|
||||
while len(connected_regions) < len(non_dead_end_regions):
|
||||
# if this is universal tracker, just break immediately and move on
|
||||
if hasattr(world.multiworld, "re_gen_passthrough"):
|
||||
if world.using_ut:
|
||||
break
|
||||
# if the connected regions length stays unchanged for too long, it's stuck in a loop
|
||||
# should, hopefully, only ever occur if someone plandos connections poorly
|
||||
@@ -445,9 +443,8 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
|
||||
random_object.shuffle(two_plus)
|
||||
|
||||
# for universal tracker, we want to skip shop gen
|
||||
if hasattr(world.multiworld, "re_gen_passthrough"):
|
||||
if "TUNIC" in world.multiworld.re_gen_passthrough:
|
||||
shop_count = 0
|
||||
if world.using_ut:
|
||||
shop_count = 0
|
||||
|
||||
for i in range(shop_count):
|
||||
portal1 = two_plus.pop()
|
||||
@@ -462,7 +459,7 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
|
||||
# connect dead ends to random non-dead ends
|
||||
# none of the key events are in dead ends, so we don't need to do gate_before_switch
|
||||
while len(dead_ends) > 0:
|
||||
if hasattr(world.multiworld, "re_gen_passthrough"):
|
||||
if world.using_ut:
|
||||
break
|
||||
portal1 = two_plus.pop()
|
||||
portal2 = dead_ends.pop()
|
||||
@@ -470,7 +467,7 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
|
||||
# then randomly connect the remaining portals to each other
|
||||
# every region is accessible, so gate_before_switch is not necessary
|
||||
while len(two_plus) > 1:
|
||||
if hasattr(world.multiworld, "re_gen_passthrough"):
|
||||
if world.using_ut:
|
||||
break
|
||||
portal1 = two_plus.pop()
|
||||
portal2 = two_plus.pop()
|
||||
|
||||
@@ -323,7 +323,7 @@ def set_location_rules(world: "TunicWorld") -> None:
|
||||
|
||||
# Beneath the Vault
|
||||
set_rule(world.get_location("Beneath the Fortress - Bridge"),
|
||||
lambda state: has_melee(state, player) or state.has_any({laurels, fire_wand}, player))
|
||||
lambda state: has_melee(state, player) or state.has_any((laurels, fire_wand, ice_dagger, gun), player))
|
||||
set_rule(world.get_location("Beneath the Fortress - Obscured Behind Waterfall"),
|
||||
lambda state: has_melee(state, player) and has_lantern(state, world))
|
||||
|
||||
|
||||
@@ -106,6 +106,7 @@ class StaticWitnessLogicObj:
|
||||
"entityType": location_id,
|
||||
"locationType": None,
|
||||
"area": current_area,
|
||||
"order": len(self.ENTITIES_BY_HEX),
|
||||
}
|
||||
|
||||
self.ENTITIES_BY_NAME[self.ENTITIES_BY_HEX[entity_hex]["checkName"]] = self.ENTITIES_BY_HEX[entity_hex]
|
||||
@@ -186,6 +187,7 @@ class StaticWitnessLogicObj:
|
||||
"entityType": entity_type,
|
||||
"locationType": location_type,
|
||||
"area": current_area,
|
||||
"order": len(self.ENTITIES_BY_HEX),
|
||||
}
|
||||
|
||||
self.ENTITY_ID_TO_NAME[entity_hex] = full_entity_name
|
||||
|
||||
@@ -114,7 +114,7 @@ class WitnessPlayerRegions:
|
||||
if k not in player_logic.UNREACHABLE_REGIONS
|
||||
}
|
||||
|
||||
event_locations_per_region = defaultdict(list)
|
||||
event_locations_per_region = defaultdict(dict)
|
||||
|
||||
for event_location, event_item_and_entity in player_logic.EVENT_ITEM_PAIRS.items():
|
||||
region = static_witness_logic.ENTITIES_BY_HEX[event_item_and_entity[1]]["region"]
|
||||
@@ -122,20 +122,33 @@ class WitnessPlayerRegions:
|
||||
region_name = "Entry"
|
||||
else:
|
||||
region_name = region["name"]
|
||||
event_locations_per_region[region_name].append(event_location)
|
||||
order = self.reference_logic.ENTITIES_BY_HEX[event_item_and_entity[1]]["order"]
|
||||
event_locations_per_region[region_name][event_location] = order
|
||||
|
||||
for region_name, region in regions_to_create.items():
|
||||
locations_for_this_region = [
|
||||
self.reference_logic.ENTITIES_BY_HEX[panel]["checkName"] for panel in region["entities"]
|
||||
if self.reference_logic.ENTITIES_BY_HEX[panel]["checkName"]
|
||||
in self.player_locations.CHECK_LOCATION_TABLE
|
||||
location_entities_for_this_region = [
|
||||
self.reference_logic.ENTITIES_BY_HEX[entity] for entity in region["entities"]
|
||||
]
|
||||
locations_for_this_region = {
|
||||
entity["checkName"]: entity["order"] for entity in location_entities_for_this_region
|
||||
if entity["checkName"] in self.player_locations.CHECK_LOCATION_TABLE
|
||||
}
|
||||
|
||||
locations_for_this_region += event_locations_per_region[region_name]
|
||||
events = event_locations_per_region[region_name]
|
||||
locations_for_this_region.update(events)
|
||||
|
||||
# First, sort by keys.
|
||||
locations_for_this_region = dict(sorted(locations_for_this_region.items()))
|
||||
|
||||
# Then, sort by game order (values)
|
||||
locations_for_this_region = dict(sorted(
|
||||
locations_for_this_region.items(),
|
||||
key=lambda location_name_and_order: location_name_and_order[1]
|
||||
))
|
||||
|
||||
all_locations = all_locations | set(locations_for_this_region)
|
||||
|
||||
new_region = create_region(world, region_name, self.player_locations, locations_for_this_region)
|
||||
new_region = create_region(world, region_name, self.player_locations, list(locations_for_this_region))
|
||||
|
||||
regions_by_name[region_name] = new_region
|
||||
|
||||
|
||||
@@ -176,7 +176,7 @@ class ZorkGrandInquisitorWorld(World):
|
||||
|
||||
if start_with_hotspot_items:
|
||||
item: ZorkGrandInquisitorItems
|
||||
for item in items_with_tag(ZorkGrandInquisitorTags.HOTSPOT):
|
||||
for item in sorted(items_with_tag(ZorkGrandInquisitorTags.HOTSPOT), key=lambda item: item.name):
|
||||
self.multiworld.push_precollected(self.create_item(item.value))
|
||||
|
||||
def create_item(self, name: str) -> ZorkGrandInquisitorItem:
|
||||
|
||||
Reference in New Issue
Block a user