mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-10 01:23:48 -07:00
Compare commits
18 Commits
NewSoupVi-
...
0.6.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3666f2ae5 | ||
|
|
c3e000e574 | ||
|
|
dd5481930a | ||
|
|
842328c661 | ||
|
|
8f75384e2e | ||
|
|
193faa00ce | ||
|
|
5e5383b399 | ||
|
|
cb6b29dbe3 | ||
|
|
82b0819051 | ||
|
|
e12ab4afa4 | ||
|
|
1416f631cc | ||
|
|
dbaac47d1e | ||
|
|
cf0ae5e31b | ||
|
|
8891f07362 | ||
|
|
d78974ec59 | ||
|
|
32be26c4d7 | ||
|
|
9de49aa419 | ||
|
|
294a67a4b4 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,6 +10,7 @@
|
||||
*.apmc
|
||||
*.apz5
|
||||
*.aptloz
|
||||
*.aptww
|
||||
*.apemerald
|
||||
*.pyc
|
||||
*.pyd
|
||||
|
||||
@@ -81,6 +81,7 @@ Currently, the following games are supported:
|
||||
* Castlevania: Circle of the Moon
|
||||
* Inscryption
|
||||
* Civilization VI
|
||||
* The Legend of Zelda: The Wind Waker
|
||||
|
||||
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
|
||||
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
flask>=3.0.3
|
||||
werkzeug>=3.0.6
|
||||
flask>=3.1.0
|
||||
werkzeug>=3.1.3
|
||||
pony>=0.7.19
|
||||
waitress>=3.0.0
|
||||
waitress>=3.0.2
|
||||
Flask-Caching>=2.3.0
|
||||
Flask-Compress>=1.15
|
||||
Flask-Limiter>=3.8.0
|
||||
bokeh>=3.5.2
|
||||
markupsafe>=2.1.5
|
||||
Flask-Compress>=1.17
|
||||
Flask-Limiter>=3.12
|
||||
bokeh>=3.6.3
|
||||
markupsafe>=3.0.2
|
||||
Markdown>=3.7
|
||||
mdx-breakless-lists>=1.0.1
|
||||
|
||||
@@ -214,6 +214,9 @@
|
||||
# Wargroove
|
||||
/worlds/wargroove/ @FlySniper
|
||||
|
||||
# The Wind Waker
|
||||
/worlds/tww/ @tanjo3
|
||||
|
||||
# The Witness
|
||||
/worlds/witness/ @NewSoupVi @blastron
|
||||
|
||||
|
||||
@@ -265,14 +265,19 @@ def bake_target_group_lookup(world: World, get_target_groups: Callable[[int], li
|
||||
return { group: get_target_groups(group) for group in unique_groups }
|
||||
|
||||
|
||||
def disconnect_entrance_for_randomization(entrance: Entrance, target_group: int | None = None) -> None:
|
||||
def disconnect_entrance_for_randomization(entrance: Entrance, target_group: int | None = None,
|
||||
one_way_target_name: str | None = None) -> None:
|
||||
"""
|
||||
Given an entrance in a "vanilla" region graph, splits that entrance to prepare it for randomization
|
||||
in randomize_entrances. This should be done after setting the type and group of the entrance.
|
||||
in randomize_entrances. This should be done after setting the type and group of the entrance. Because it attempts
|
||||
to meet strict entrance naming requirements for coupled mode, this function may produce unintuitive results when
|
||||
called only on a single entrance; it produces eventually-correct outputs only after calling it on all entrances.
|
||||
|
||||
:param entrance: The entrance which will be disconnected in preparation for randomization.
|
||||
:param target_group: The group to assign to the created ER target. If not specified, the group from
|
||||
the original entrance will be copied.
|
||||
:param one_way_target_name: The name of the created ER target if `entrance` is one-way. This argument
|
||||
is required for one-way entrances and is ignored otherwise.
|
||||
"""
|
||||
child_region = entrance.connected_region
|
||||
parent_region = entrance.parent_region
|
||||
@@ -287,8 +292,11 @@ def disconnect_entrance_for_randomization(entrance: Entrance, target_group: int
|
||||
# targets in the child region will be created when the other direction edge is disconnected
|
||||
target = parent_region.create_er_target(entrance.name)
|
||||
else:
|
||||
# for 1-ways, the child region needs a target and coupling/naming is not a concern
|
||||
target = child_region.create_er_target(child_region.name)
|
||||
# for 1-ways, the child region needs a target. naming is not a concern for coupling so we
|
||||
# allow it to be user provided (and require it, to prevent an unhelpful assumed name in pairings)
|
||||
if not one_way_target_name:
|
||||
raise ValueError("Cannot disconnect a one-way entrance without a target name specified")
|
||||
target = child_region.create_er_target(one_way_target_name)
|
||||
target.randomization_type = entrance.randomization_type
|
||||
target.randomization_group = target_group or entrance.randomization_group
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
colorama>=0.4.6
|
||||
websockets>=13.0.1,<14
|
||||
PyYAML>=6.0.2
|
||||
jellyfish>=1.1.0
|
||||
jinja2>=3.1.4
|
||||
jellyfish>=1.1.3
|
||||
jinja2>=3.1.6
|
||||
schema>=0.7.7
|
||||
kivy>=2.3.0
|
||||
bsdiff4>=1.2.4
|
||||
platformdirs>=4.2.2
|
||||
certifi>=2024.12.14
|
||||
cython>=3.0.11
|
||||
cymem>=2.0.8
|
||||
orjson>=3.10.7
|
||||
kivy>=2.3.1
|
||||
bsdiff4>=1.2.6
|
||||
platformdirs>=4.3.6
|
||||
certifi>=2025.1.31
|
||||
cython>=3.0.12
|
||||
cymem>=2.0.11
|
||||
orjson>=3.10.15
|
||||
typing_extensions>=4.12.2
|
||||
|
||||
@@ -148,7 +148,7 @@ class TestDisconnectForRandomization(unittest.TestCase):
|
||||
e.randomization_group = 1
|
||||
e.connect(r2)
|
||||
|
||||
disconnect_entrance_for_randomization(e)
|
||||
disconnect_entrance_for_randomization(e, one_way_target_name="foo")
|
||||
|
||||
self.assertIsNone(e.connected_region)
|
||||
self.assertEqual([], r1.entrances)
|
||||
@@ -158,10 +158,22 @@ class TestDisconnectForRandomization(unittest.TestCase):
|
||||
|
||||
self.assertEqual(1, len(r2.entrances))
|
||||
self.assertIsNone(r2.entrances[0].parent_region)
|
||||
self.assertEqual("r2", r2.entrances[0].name)
|
||||
self.assertEqual("foo", r2.entrances[0].name)
|
||||
self.assertEqual(EntranceType.ONE_WAY, r2.entrances[0].randomization_type)
|
||||
self.assertEqual(1, r2.entrances[0].randomization_group)
|
||||
|
||||
def test_disconnect_default_1way_no_vanilla_target_raises(self):
|
||||
multiworld = generate_test_multiworld()
|
||||
r1 = Region("r1", 1, multiworld)
|
||||
r2 = Region("r2", 1, multiworld)
|
||||
e = r1.create_exit("e")
|
||||
e.randomization_type = EntranceType.ONE_WAY
|
||||
e.randomization_group = 1
|
||||
e.connect(r2)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
disconnect_entrance_for_randomization(e)
|
||||
|
||||
def test_disconnect_uses_alternate_group(self):
|
||||
multiworld = generate_test_multiworld()
|
||||
r1 = Region("r1", 1, multiworld)
|
||||
@@ -171,7 +183,7 @@ class TestDisconnectForRandomization(unittest.TestCase):
|
||||
e.randomization_group = 1
|
||||
e.connect(r2)
|
||||
|
||||
disconnect_entrance_for_randomization(e, 2)
|
||||
disconnect_entrance_for_randomization(e, 2, "foo")
|
||||
|
||||
self.assertIsNone(e.connected_region)
|
||||
self.assertEqual([], r1.entrances)
|
||||
@@ -181,7 +193,7 @@ class TestDisconnectForRandomization(unittest.TestCase):
|
||||
|
||||
self.assertEqual(1, len(r2.entrances))
|
||||
self.assertIsNone(r2.entrances[0].parent_region)
|
||||
self.assertEqual("r2", r2.entrances[0].name)
|
||||
self.assertEqual("foo", r2.entrances[0].name)
|
||||
self.assertEqual(EntranceType.ONE_WAY, r2.entrances[0].randomization_type)
|
||||
self.assertEqual(2, r2.entrances[0].randomization_group)
|
||||
|
||||
|
||||
11
test/general/test_patches.py
Normal file
11
test/general/test_patches.py
Normal file
@@ -0,0 +1,11 @@
|
||||
import unittest
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
from worlds.Files import AutoPatchRegister
|
||||
|
||||
|
||||
class TestPatches(unittest.TestCase):
|
||||
def test_patch_name_matches_game(self) -> None:
|
||||
for game_name in AutoPatchRegister.patch_types:
|
||||
with self.subTest(game=game_name):
|
||||
self.assertIn(game_name, AutoWorldRegister.world_types.keys(),
|
||||
f"Patch '{game_name}' does not match the name of any world.")
|
||||
19
test/general/test_requirements.py
Normal file
19
test/general/test_requirements.py
Normal file
@@ -0,0 +1,19 @@
|
||||
import unittest
|
||||
import os
|
||||
|
||||
|
||||
class TestBase(unittest.TestCase):
|
||||
def test_requirements_file_ends_on_newline(self):
|
||||
"""Test that all requirements files end on a newline"""
|
||||
import Utils
|
||||
requirements_files = [Utils.local_path("requirements.txt"),
|
||||
Utils.local_path("WebHostLib", "requirements.txt")]
|
||||
worlds_path = Utils.local_path("worlds")
|
||||
for entry in os.listdir(worlds_path):
|
||||
requirements_path = os.path.join(worlds_path, entry, "requirements.txt")
|
||||
if os.path.isfile(requirements_path):
|
||||
requirements_files.append(requirements_path)
|
||||
for requirements_file in requirements_files:
|
||||
with self.subTest(path=requirements_file):
|
||||
with open(requirements_file) as f:
|
||||
self.assertEqual(f.read()[-1], "\n")
|
||||
@@ -3,4 +3,4 @@ mpyq>=0.2.5
|
||||
portpicker>=1.5.2
|
||||
aiohttp>=3.8.4
|
||||
loguru>=0.7.0
|
||||
protobuf==3.20.3
|
||||
protobuf==3.20.3
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
maseya-z3pr>=1.0.0rc1
|
||||
xxtea>=3.0.0
|
||||
xxtea>=3.0.0
|
||||
|
||||
@@ -126,7 +126,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 3,
|
||||
'index': 64,
|
||||
'doom_type': 2001,
|
||||
'region': "Toxin Refinery (E1M3) Main"},
|
||||
'region': "Toxin Refinery (E1M3) Start"},
|
||||
351019: {'name': 'Toxin Refinery (E1M3) - Shotgun 2',
|
||||
'episode': 1,
|
||||
'map': 3,
|
||||
@@ -234,7 +234,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 4,
|
||||
'index': 107,
|
||||
'doom_type': 8,
|
||||
'region': "Command Control (E1M4) Main"},
|
||||
'region': "Command Control (E1M4) Start"},
|
||||
351037: {'name': 'Command Control (E1M4) - Shotgun',
|
||||
'episode': 1,
|
||||
'map': 4,
|
||||
@@ -504,7 +504,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 7,
|
||||
'index': 122,
|
||||
'doom_type': 2001,
|
||||
'region': "Computer Station (E1M7) Main"},
|
||||
'region': "Computer Station (E1M7) Start"},
|
||||
351082: {'name': 'Computer Station (E1M7) - Rocket launcher',
|
||||
'episode': 1,
|
||||
'map': 7,
|
||||
@@ -912,7 +912,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 4,
|
||||
'index': 109,
|
||||
'doom_type': 2001,
|
||||
'region': "Deimos Lab (E2M4) Main"},
|
||||
'region': "Deimos Lab (E2M4) Start"},
|
||||
351150: {'name': 'Deimos Lab (E2M4) - Mega Armor',
|
||||
'episode': 2,
|
||||
'map': 4,
|
||||
@@ -1242,7 +1242,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 8,
|
||||
'index': 36,
|
||||
'doom_type': 2019,
|
||||
'region': "Tower of Babel (E2M8) Main"},
|
||||
'region': "Tower of Babel (E2M8) Start"},
|
||||
351205: {'name': 'Fortress of Mystery (E2M9) - Supercharge',
|
||||
'episode': 2,
|
||||
'map': 9,
|
||||
@@ -1638,7 +1638,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 5,
|
||||
'index': 187,
|
||||
'doom_type': 2001,
|
||||
'region': "Unholy Cathedral (E3M5) Main"},
|
||||
'region': "Unholy Cathedral (E3M5) Start"},
|
||||
351271: {'name': 'Unholy Cathedral (E3M5) - Shotgun 2',
|
||||
'episode': 3,
|
||||
'map': 5,
|
||||
|
||||
@@ -33,9 +33,11 @@ regions:List[RegionDict] = [
|
||||
|
||||
# Toxin Refinery (E1M3)
|
||||
{"name":"Toxin Refinery (E1M3) Main",
|
||||
"connects_to_hub":True,
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Toxin Refinery (E1M3) Blue","pro":False}]},
|
||||
"connections":[
|
||||
{"target":"Toxin Refinery (E1M3) Blue","pro":False},
|
||||
{"target":"Toxin Refinery (E1M3) Start","pro":False}]},
|
||||
{"name":"Toxin Refinery (E1M3) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
@@ -46,15 +48,20 @@ regions:List[RegionDict] = [
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Toxin Refinery (E1M3) Blue","pro":False}]},
|
||||
{"name":"Toxin Refinery (E1M3) Start",
|
||||
"connects_to_hub":True,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Toxin Refinery (E1M3) Main","pro":False}]},
|
||||
|
||||
# Command Control (E1M4)
|
||||
{"name":"Command Control (E1M4) Main",
|
||||
"connects_to_hub":True,
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[
|
||||
{"target":"Command Control (E1M4) Blue","pro":False},
|
||||
{"target":"Command Control (E1M4) Yellow","pro":False},
|
||||
{"target":"Command Control (E1M4) Ledge","pro":True}]},
|
||||
{"target":"Command Control (E1M4) Ledge","pro":True},
|
||||
{"target":"Command Control (E1M4) Start","pro":False}]},
|
||||
{"name":"Command Control (E1M4) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
@@ -72,6 +79,10 @@ regions:List[RegionDict] = [
|
||||
{"target":"Command Control (E1M4) Main","pro":False},
|
||||
{"target":"Command Control (E1M4) Blue","pro":False},
|
||||
{"target":"Command Control (E1M4) Yellow","pro":False}]},
|
||||
{"name":"Command Control (E1M4) Start",
|
||||
"connects_to_hub":True,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Command Control (E1M4) Main","pro":False}]},
|
||||
|
||||
# Phobos Lab (E1M5)
|
||||
{"name":"Phobos Lab (E1M5) Main",
|
||||
@@ -126,11 +137,12 @@ regions:List[RegionDict] = [
|
||||
|
||||
# Computer Station (E1M7)
|
||||
{"name":"Computer Station (E1M7) Main",
|
||||
"connects_to_hub":True,
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[
|
||||
{"target":"Computer Station (E1M7) Red","pro":False},
|
||||
{"target":"Computer Station (E1M7) Yellow","pro":False}]},
|
||||
{"target":"Computer Station (E1M7) Yellow","pro":False},
|
||||
{"target":"Computer Station (E1M7) Start","pro":False}]},
|
||||
{"name":"Computer Station (E1M7) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
@@ -150,6 +162,10 @@ regions:List[RegionDict] = [
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Computer Station (E1M7) Yellow","pro":False}]},
|
||||
{"name":"Computer Station (E1M7) Start",
|
||||
"connects_to_hub":True,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Computer Station (E1M7) Main","pro":False}]},
|
||||
|
||||
# Phobos Anomaly (E1M8)
|
||||
{"name":"Phobos Anomaly (E1M8) Main",
|
||||
@@ -238,9 +254,11 @@ regions:List[RegionDict] = [
|
||||
|
||||
# Deimos Lab (E2M4)
|
||||
{"name":"Deimos Lab (E2M4) Main",
|
||||
"connects_to_hub":True,
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[{"target":"Deimos Lab (E2M4) Blue","pro":False}]},
|
||||
"connections":[
|
||||
{"target":"Deimos Lab (E2M4) Blue","pro":False},
|
||||
{"target":"Deimos Lab (E2M4) Start","pro":False}]},
|
||||
{"name":"Deimos Lab (E2M4) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
@@ -251,6 +269,10 @@ regions:List[RegionDict] = [
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[{"target":"Deimos Lab (E2M4) Blue","pro":False}]},
|
||||
{"name":"Deimos Lab (E2M4) Start",
|
||||
"connects_to_hub":True,
|
||||
"episode":2,
|
||||
"connections":[{"target":"Deimos Lab (E2M4) Main","pro":False}]},
|
||||
|
||||
# Command Center (E2M5)
|
||||
{"name":"Command Center (E2M5) Main",
|
||||
@@ -314,9 +336,13 @@ regions:List[RegionDict] = [
|
||||
|
||||
# Tower of Babel (E2M8)
|
||||
{"name":"Tower of Babel (E2M8) Main",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[{"target":"Tower of Babel (E2M8) Start","pro":False}]},
|
||||
{"name":"Tower of Babel (E2M8) Start",
|
||||
"connects_to_hub":True,
|
||||
"episode":2,
|
||||
"connections":[]},
|
||||
"connections":[{"target":"Tower of Babel (E2M8) Main","pro":False}]},
|
||||
|
||||
# Fortress of Mystery (E2M9)
|
||||
{"name":"Fortress of Mystery (E2M9) Main",
|
||||
@@ -392,11 +418,12 @@ regions:List[RegionDict] = [
|
||||
|
||||
# Unholy Cathedral (E3M5)
|
||||
{"name":"Unholy Cathedral (E3M5) Main",
|
||||
"connects_to_hub":True,
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[
|
||||
{"target":"Unholy Cathedral (E3M5) Yellow","pro":False},
|
||||
{"target":"Unholy Cathedral (E3M5) Blue","pro":False}]},
|
||||
{"target":"Unholy Cathedral (E3M5) Blue","pro":False},
|
||||
{"target":"Unholy Cathedral (E3M5) Start","pro":False}]},
|
||||
{"name":"Unholy Cathedral (E3M5) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
@@ -405,6 +432,10 @@ regions:List[RegionDict] = [
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Unholy Cathedral (E3M5) Main","pro":False}]},
|
||||
{"name":"Unholy Cathedral (E3M5) Start",
|
||||
"connects_to_hub":True,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Unholy Cathedral (E3M5) Main","pro":False}]},
|
||||
|
||||
# Mt. Erebus (E3M6)
|
||||
{"name":"Mt. Erebus (E3M6) Main",
|
||||
|
||||
@@ -23,10 +23,6 @@ def set_episode1_rules(player, multiworld, pro):
|
||||
state.has("Nuclear Plant (E1M2) - Red keycard", player, 1))
|
||||
|
||||
# Toxin Refinery (E1M3)
|
||||
set_rule(multiworld.get_entrance("Hub -> Toxin Refinery (E1M3) Main", player), lambda state:
|
||||
(state.has("Toxin Refinery (E1M3)", player, 1)) and
|
||||
(state.has("Shotgun", player, 1) or
|
||||
state.has("Chaingun", player, 1)))
|
||||
set_rule(multiworld.get_entrance("Toxin Refinery (E1M3) Main -> Toxin Refinery (E1M3) Blue", player), lambda state:
|
||||
state.has("Toxin Refinery (E1M3) - Blue keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("Toxin Refinery (E1M3) Blue -> Toxin Refinery (E1M3) Yellow", player), lambda state:
|
||||
@@ -35,12 +31,13 @@ def set_episode1_rules(player, multiworld, pro):
|
||||
state.has("Toxin Refinery (E1M3) - Blue keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("Toxin Refinery (E1M3) Yellow -> Toxin Refinery (E1M3) Blue", player), lambda state:
|
||||
state.has("Toxin Refinery (E1M3) - Yellow keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("Hub -> Toxin Refinery (E1M3) Start", player), lambda state:
|
||||
state.has("Toxin Refinery (E1M3)", player, 1))
|
||||
set_rule(multiworld.get_entrance("Toxin Refinery (E1M3) Start -> Toxin Refinery (E1M3) Main", player), lambda state:
|
||||
state.has("Shotgun", player, 1) or
|
||||
state.has("Chaingun", player, 1))
|
||||
|
||||
# Command Control (E1M4)
|
||||
set_rule(multiworld.get_entrance("Hub -> Command Control (E1M4) Main", player), lambda state:
|
||||
state.has("Command Control (E1M4)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1))
|
||||
set_rule(multiworld.get_entrance("Command Control (E1M4) Main -> Command Control (E1M4) Blue", player), lambda state:
|
||||
state.has("Command Control (E1M4) - Blue keycard", player, 1) or
|
||||
state.has("Command Control (E1M4) - Yellow keycard", player, 1))
|
||||
@@ -50,6 +47,11 @@ def set_episode1_rules(player, multiworld, pro):
|
||||
set_rule(multiworld.get_entrance("Command Control (E1M4) Blue -> Command Control (E1M4) Main", player), lambda state:
|
||||
state.has("Command Control (E1M4) - Yellow keycard", player, 1) or
|
||||
state.has("Command Control (E1M4) - Blue keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("Hub -> Command Control (E1M4) Start", player), lambda state:
|
||||
state.has("Command Control (E1M4)", player, 1))
|
||||
set_rule(multiworld.get_entrance("Command Control (E1M4) Start -> Command Control (E1M4) Main", player), lambda state:
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1))
|
||||
|
||||
# Phobos Lab (E1M5)
|
||||
set_rule(multiworld.get_entrance("Hub -> Phobos Lab (E1M5) Main", player), lambda state:
|
||||
@@ -83,11 +85,6 @@ def set_episode1_rules(player, multiworld, pro):
|
||||
state.has("Central Processing (E1M6) - Yellow keycard", player, 1))
|
||||
|
||||
# Computer Station (E1M7)
|
||||
set_rule(multiworld.get_entrance("Hub -> Computer Station (E1M7) Main", player), lambda state:
|
||||
state.has("Computer Station (E1M7)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Rocket launcher", player, 1))
|
||||
set_rule(multiworld.get_entrance("Computer Station (E1M7) Main -> Computer Station (E1M7) Red", player), lambda state:
|
||||
state.has("Computer Station (E1M7) - Red keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("Computer Station (E1M7) Main -> Computer Station (E1M7) Yellow", player), lambda state:
|
||||
@@ -103,6 +100,12 @@ def set_episode1_rules(player, multiworld, pro):
|
||||
state.has("Computer Station (E1M7) - Red keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("Computer Station (E1M7) Courtyard -> Computer Station (E1M7) Yellow", player), lambda state:
|
||||
state.has("Computer Station (E1M7) - Yellow keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("Hub -> Computer Station (E1M7) Start", player), lambda state:
|
||||
state.has("Computer Station (E1M7)", player, 1))
|
||||
set_rule(multiworld.get_entrance("Computer Station (E1M7) Start -> Computer Station (E1M7) Main", player), lambda state:
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Rocket launcher", player, 1) and
|
||||
state.has("Chaingun", player, 1))
|
||||
|
||||
# Phobos Anomaly (E1M8)
|
||||
set_rule(multiworld.get_entrance("Hub -> Phobos Anomaly (E1M8) Start", player), lambda state:
|
||||
@@ -172,15 +175,16 @@ def set_episode2_rules(player, multiworld, pro):
|
||||
state.has("Refinery (E2M3) - Blue keycard", player, 1))
|
||||
|
||||
# Deimos Lab (E2M4)
|
||||
set_rule(multiworld.get_entrance("Hub -> Deimos Lab (E2M4) Main", player), lambda state:
|
||||
state.has("Deimos Lab (E2M4)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Plasma gun", player, 1))
|
||||
set_rule(multiworld.get_entrance("Deimos Lab (E2M4) Main -> Deimos Lab (E2M4) Blue", player), lambda state:
|
||||
state.has("Deimos Lab (E2M4) - Blue keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("Deimos Lab (E2M4) Blue -> Deimos Lab (E2M4) Yellow", player), lambda state:
|
||||
state.has("Deimos Lab (E2M4) - Yellow keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("Hub -> Deimos Lab (E2M4) Start", player), lambda state:
|
||||
state.has("Deimos Lab (E2M4)", player, 1))
|
||||
set_rule(multiworld.get_entrance("Deimos Lab (E2M4) Start -> Deimos Lab (E2M4) Main", player), lambda state:
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Plasma gun", player, 1) and
|
||||
state.has("Chaingun", player, 1))
|
||||
|
||||
# Command Center (E2M5)
|
||||
set_rule(multiworld.get_entrance("Hub -> Command Center (E2M5) Main", player), lambda state:
|
||||
@@ -238,11 +242,11 @@ def set_episode2_rules(player, multiworld, pro):
|
||||
state.has("Spawning Vats (E2M7) - Red keycard", player, 1))
|
||||
|
||||
# Tower of Babel (E2M8)
|
||||
set_rule(multiworld.get_entrance("Hub -> Tower of Babel (E2M8) Main", player), lambda state:
|
||||
(state.has("Tower of Babel (E2M8)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
set_rule(multiworld.get_entrance("Hub -> Tower of Babel (E2M8) Start", player), lambda state:
|
||||
state.has("Tower of Babel (E2M8)", player, 1))
|
||||
set_rule(multiworld.get_entrance("Tower of Babel (E2M8) Start -> Tower of Babel (E2M8) Main", player), lambda state:
|
||||
(state.has("Chaingun", player, 1) and
|
||||
state.has("Shotgun", player, 1)) and (state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
|
||||
@@ -321,13 +325,6 @@ def set_episode3_rules(player, multiworld, pro):
|
||||
state.has("House of Pain (E3M4) - Yellow skull key", player, 1))
|
||||
|
||||
# Unholy Cathedral (E3M5)
|
||||
set_rule(multiworld.get_entrance("Hub -> Unholy Cathedral (E3M5) Main", player), lambda state:
|
||||
(state.has("Unholy Cathedral (E3M5)", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
set_rule(multiworld.get_entrance("Unholy Cathedral (E3M5) Main -> Unholy Cathedral (E3M5) Yellow", player), lambda state:
|
||||
state.has("Unholy Cathedral (E3M5) - Yellow skull key", player, 1))
|
||||
set_rule(multiworld.get_entrance("Unholy Cathedral (E3M5) Main -> Unholy Cathedral (E3M5) Blue", player), lambda state:
|
||||
@@ -336,6 +333,13 @@ def set_episode3_rules(player, multiworld, pro):
|
||||
state.has("Unholy Cathedral (E3M5) - Blue skull key", player, 1))
|
||||
set_rule(multiworld.get_entrance("Unholy Cathedral (E3M5) Yellow -> Unholy Cathedral (E3M5) Main", player), lambda state:
|
||||
state.has("Unholy Cathedral (E3M5) - Yellow skull key", player, 1))
|
||||
set_rule(multiworld.get_entrance("Hub -> Unholy Cathedral (E3M5) Start", player), lambda state:
|
||||
state.has("Unholy Cathedral (E3M5)", player, 1))
|
||||
set_rule(multiworld.get_entrance("Unholy Cathedral (E3M5) Start -> Unholy Cathedral (E3M5) Main", player), lambda state:
|
||||
(state.has("Chaingun", player, 1) and
|
||||
state.has("Shotgun", player, 1)) and (state.has("Plasma gun", player, 1) or
|
||||
state.has("Rocket launcher", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
|
||||
# Mt. Erebus (E3M6)
|
||||
set_rule(multiworld.get_entrance("Hub -> Mt. Erebus (E3M6) Main", player), lambda state:
|
||||
|
||||
@@ -50,14 +50,14 @@ class DOOM1993World(World):
|
||||
location_name_to_id = {data["name"]: loc_id for loc_id, data in Locations.location_table.items()}
|
||||
location_name_groups = Locations.location_name_groups
|
||||
|
||||
starting_level_for_episode: List[str] = [
|
||||
"Hangar (E1M1)",
|
||||
"Deimos Anomaly (E2M1)",
|
||||
"Hell Keep (E3M1)",
|
||||
"Hell Beneath (E4M1)"
|
||||
]
|
||||
starting_level_for_episode: Dict[int, str] = {
|
||||
1: "Hangar (E1M1)",
|
||||
2: "Deimos Anomaly (E2M1)",
|
||||
3: "Hell Keep (E3M1)",
|
||||
4: "Hell Beneath (E4M1)"
|
||||
}
|
||||
|
||||
boss_level_for_espidoes: List[str] = [
|
||||
all_boss_levels: List[str] = [
|
||||
"Phobos Anomaly (E1M8)",
|
||||
"Tower of Babel (E2M8)",
|
||||
"Dis (E3M8)",
|
||||
@@ -82,6 +82,7 @@ class DOOM1993World(World):
|
||||
def __init__(self, multiworld: MultiWorld, player: int):
|
||||
self.included_episodes = [1, 1, 1, 0]
|
||||
self.location_count = 0
|
||||
self.starting_levels = []
|
||||
|
||||
super().__init__(multiworld, player)
|
||||
|
||||
@@ -99,6 +100,16 @@ class DOOM1993World(World):
|
||||
if self.get_episode_count() == 0:
|
||||
self.included_episodes[0] = 1
|
||||
|
||||
self.starting_levels = [level_name for (episode, level_name) in self.starting_level_for_episode.items()
|
||||
if self.included_episodes[episode - 1]]
|
||||
|
||||
# Solo Episode 3 presents a problem, because Hell Keep has only two locations.
|
||||
# We have to give the player Slough of Despair (E3M2), and also mark a weapon early.
|
||||
if self.get_episode_count() == 1 and self.included_episodes[2]:
|
||||
early_weapon = self.random.choice(["Shotgun", "Chaingun"])
|
||||
self.multiworld.early_items[self.player][early_weapon] = 1
|
||||
self.starting_levels.append("Slough of Despair (E3M2)")
|
||||
|
||||
def create_regions(self):
|
||||
pro = self.options.pro.value
|
||||
|
||||
@@ -152,7 +163,7 @@ class DOOM1993World(World):
|
||||
def completion_rule(self, state: CollectionState):
|
||||
goal_levels = Maps.map_names
|
||||
if self.options.goal.value:
|
||||
goal_levels = self.boss_level_for_espidoes
|
||||
goal_levels = self.all_boss_levels
|
||||
|
||||
for map_name in goal_levels:
|
||||
if map_name + " - Exit" not in self.location_name_to_id:
|
||||
@@ -201,7 +212,7 @@ class DOOM1993World(World):
|
||||
if item["episode"] != -1 and not self.included_episodes[item["episode"] - 1]:
|
||||
continue
|
||||
|
||||
count = item["count"] if item["name"] not in self.starting_level_for_episode else item["count"] - 1
|
||||
count = item["count"] if item["name"] not in self.starting_levels else item["count"] - 1
|
||||
itempool += [self.create_item(item["name"]) for _ in range(count)]
|
||||
|
||||
# Backpack(s) based on options
|
||||
@@ -232,9 +243,8 @@ class DOOM1993World(World):
|
||||
self.location_count -= 1
|
||||
|
||||
# Give starting levels right away
|
||||
for i in range(len(self.included_episodes)):
|
||||
if self.included_episodes[i]:
|
||||
self.multiworld.push_precollected(self.create_item(self.starting_level_for_episode[i]))
|
||||
for map_name in self.starting_levels:
|
||||
self.multiworld.push_precollected(self.create_item(map_name))
|
||||
|
||||
# Give Computer area maps if option selected
|
||||
if self.options.start_with_computer_area_maps.value:
|
||||
|
||||
@@ -412,7 +412,7 @@ item_table: Dict[int, ItemDict] = {
|
||||
'map': 2},
|
||||
360246: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'Barrels o Fun (MAP23) - Yellow skull key',
|
||||
'name': "Barrels o' Fun (MAP23) - Yellow skull key",
|
||||
'doom_type': 39,
|
||||
'episode': 3,
|
||||
'map': 3},
|
||||
@@ -880,19 +880,19 @@ item_table: Dict[int, ItemDict] = {
|
||||
'map': 2},
|
||||
360466: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'Barrels o Fun (MAP23)',
|
||||
'name': "Barrels o' Fun (MAP23)",
|
||||
'doom_type': -1,
|
||||
'episode': 3,
|
||||
'map': 3},
|
||||
360467: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'Barrels o Fun (MAP23) - Complete',
|
||||
'name': "Barrels o' Fun (MAP23) - Complete",
|
||||
'doom_type': -2,
|
||||
'episode': 3,
|
||||
'map': 3},
|
||||
360468: {'classification': ItemClassification.filler,
|
||||
'count': 1,
|
||||
'name': 'Barrels o Fun (MAP23) - Computer area map',
|
||||
'name': "Barrels o' Fun (MAP23) - Computer area map",
|
||||
'doom_type': 2026,
|
||||
'episode': 3,
|
||||
'map': 3},
|
||||
@@ -1024,37 +1024,37 @@ item_table: Dict[int, ItemDict] = {
|
||||
'map': 10},
|
||||
360490: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'Wolfenstein2 (MAP31)',
|
||||
'name': 'Wolfenstein (MAP31)',
|
||||
'doom_type': -1,
|
||||
'episode': 4,
|
||||
'map': 1},
|
||||
360491: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'Wolfenstein2 (MAP31) - Complete',
|
||||
'name': 'Wolfenstein (MAP31) - Complete',
|
||||
'doom_type': -2,
|
||||
'episode': 4,
|
||||
'map': 1},
|
||||
360492: {'classification': ItemClassification.filler,
|
||||
'count': 1,
|
||||
'name': 'Wolfenstein2 (MAP31) - Computer area map',
|
||||
'name': 'Wolfenstein (MAP31) - Computer area map',
|
||||
'doom_type': 2026,
|
||||
'episode': 4,
|
||||
'map': 1},
|
||||
360493: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'Grosse2 (MAP32)',
|
||||
'name': 'Grosse (MAP32)',
|
||||
'doom_type': -1,
|
||||
'episode': 4,
|
||||
'map': 2},
|
||||
360494: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'Grosse2 (MAP32) - Complete',
|
||||
'name': 'Grosse (MAP32) - Complete',
|
||||
'doom_type': -2,
|
||||
'episode': 4,
|
||||
'map': 2},
|
||||
360495: {'classification': ItemClassification.filler,
|
||||
'count': 1,
|
||||
'name': 'Grosse2 (MAP32) - Computer area map',
|
||||
'name': 'Grosse (MAP32) - Computer area map',
|
||||
'doom_type': 2026,
|
||||
'episode': 4,
|
||||
'map': 2},
|
||||
@@ -1087,9 +1087,9 @@ item_table: Dict[int, ItemDict] = {
|
||||
|
||||
item_name_groups: Dict[str, Set[str]] = {
|
||||
'Ammos': {'Box of bullets', 'Box of rockets', 'Box of shotgun shells', 'Energy cell pack', },
|
||||
'Computer area maps': {'Barrels o Fun (MAP23) - Computer area map', 'Bloodfalls (MAP25) - Computer area map', 'Circle of Death (MAP11) - Computer area map', 'Dead Simple (MAP07) - Computer area map', 'Downtown (MAP13) - Computer area map', 'Entryway (MAP01) - Computer area map', 'Gotcha! (MAP20) - Computer area map', 'Grosse2 (MAP32) - Computer area map', 'Icon of Sin (MAP30) - Computer area map', 'Industrial Zone (MAP15) - Computer area map', 'Monster Condo (MAP27) - Computer area map', 'Nirvana (MAP21) - Computer area map', 'Refueling Base (MAP10) - Computer area map', 'Suburbs (MAP16) - Computer area map', 'Tenements (MAP17) - Computer area map', 'The Abandoned Mines (MAP26) - Computer area map', 'The Catacombs (MAP22) - Computer area map', 'The Chasm (MAP24) - Computer area map', 'The Citadel (MAP19) - Computer area map', 'The Courtyard (MAP18) - Computer area map', 'The Crusher (MAP06) - Computer area map', 'The Factory (MAP12) - Computer area map', 'The Focus (MAP04) - Computer area map', 'The Gantlet (MAP03) - Computer area map', 'The Inmost Dens (MAP14) - Computer area map', 'The Living End (MAP29) - Computer area map', 'The Pit (MAP09) - Computer area map', 'The Spirit World (MAP28) - Computer area map', 'The Waste Tunnels (MAP05) - Computer area map', 'Tricks and Traps (MAP08) - Computer area map', 'Underhalls (MAP02) - Computer area map', 'Wolfenstein2 (MAP31) - Computer area map', },
|
||||
'Keys': {'Barrels o Fun (MAP23) - Yellow skull key', 'Bloodfalls (MAP25) - Blue skull key', 'Circle of Death (MAP11) - Blue keycard', 'Circle of Death (MAP11) - Red keycard', 'Downtown (MAP13) - Blue keycard', 'Downtown (MAP13) - Red keycard', 'Downtown (MAP13) - Yellow keycard', 'Industrial Zone (MAP15) - Blue keycard', 'Industrial Zone (MAP15) - Red keycard', 'Industrial Zone (MAP15) - Yellow keycard', 'Monster Condo (MAP27) - Blue skull key', 'Monster Condo (MAP27) - Red skull key', 'Monster Condo (MAP27) - Yellow skull key', 'Nirvana (MAP21) - Blue skull key', 'Nirvana (MAP21) - Red skull key', 'Nirvana (MAP21) - Yellow skull key', 'Refueling Base (MAP10) - Blue keycard', 'Refueling Base (MAP10) - Yellow keycard', 'Suburbs (MAP16) - Blue skull key', 'Suburbs (MAP16) - Red skull key', 'Tenements (MAP17) - Blue keycard', 'Tenements (MAP17) - Red keycard', 'Tenements (MAP17) - Yellow skull key', 'The Abandoned Mines (MAP26) - Blue keycard', 'The Abandoned Mines (MAP26) - Red keycard', 'The Abandoned Mines (MAP26) - Yellow keycard', 'The Catacombs (MAP22) - Blue skull key', 'The Catacombs (MAP22) - Red skull key', 'The Chasm (MAP24) - Blue keycard', 'The Chasm (MAP24) - Red keycard', 'The Citadel (MAP19) - Blue skull key', 'The Citadel (MAP19) - Red skull key', 'The Citadel (MAP19) - Yellow skull key', 'The Courtyard (MAP18) - Blue skull key', 'The Courtyard (MAP18) - Yellow skull key', 'The Crusher (MAP06) - Blue keycard', 'The Crusher (MAP06) - Red keycard', 'The Crusher (MAP06) - Yellow keycard', 'The Factory (MAP12) - Blue keycard', 'The Factory (MAP12) - Yellow keycard', 'The Focus (MAP04) - Blue keycard', 'The Focus (MAP04) - Red keycard', 'The Focus (MAP04) - Yellow keycard', 'The Gantlet (MAP03) - Blue keycard', 'The Gantlet (MAP03) - Red keycard', 'The Inmost Dens (MAP14) - Blue skull key', 'The Inmost Dens (MAP14) - Red skull key', 'The Pit (MAP09) - Blue keycard', 'The Pit (MAP09) - Yellow keycard', 'The Spirit World (MAP28) - Red skull key', 'The Spirit World (MAP28) - Yellow skull key', 'The Waste Tunnels (MAP05) - Blue keycard', 'The Waste Tunnels (MAP05) - Red keycard', 'The Waste Tunnels (MAP05) - Yellow keycard', 'Tricks and Traps (MAP08) - Red skull key', 'Tricks and Traps (MAP08) - Yellow skull key', 'Underhalls (MAP02) - Blue keycard', 'Underhalls (MAP02) - Red keycard', },
|
||||
'Levels': {'Barrels o Fun (MAP23)', 'Bloodfalls (MAP25)', 'Circle of Death (MAP11)', 'Dead Simple (MAP07)', 'Downtown (MAP13)', 'Entryway (MAP01)', 'Gotcha! (MAP20)', 'Grosse2 (MAP32)', 'Icon of Sin (MAP30)', 'Industrial Zone (MAP15)', 'Monster Condo (MAP27)', 'Nirvana (MAP21)', 'Refueling Base (MAP10)', 'Suburbs (MAP16)', 'Tenements (MAP17)', 'The Abandoned Mines (MAP26)', 'The Catacombs (MAP22)', 'The Chasm (MAP24)', 'The Citadel (MAP19)', 'The Courtyard (MAP18)', 'The Crusher (MAP06)', 'The Factory (MAP12)', 'The Focus (MAP04)', 'The Gantlet (MAP03)', 'The Inmost Dens (MAP14)', 'The Living End (MAP29)', 'The Pit (MAP09)', 'The Spirit World (MAP28)', 'The Waste Tunnels (MAP05)', 'Tricks and Traps (MAP08)', 'Underhalls (MAP02)', 'Wolfenstein2 (MAP31)', },
|
||||
'Computer area maps': {"Barrels o' Fun (MAP23) - Computer area map", 'Bloodfalls (MAP25) - Computer area map', 'Circle of Death (MAP11) - Computer area map', 'Dead Simple (MAP07) - Computer area map', 'Downtown (MAP13) - Computer area map', 'Entryway (MAP01) - Computer area map', 'Gotcha! (MAP20) - Computer area map', 'Grosse (MAP32) - Computer area map', 'Icon of Sin (MAP30) - Computer area map', 'Industrial Zone (MAP15) - Computer area map', 'Monster Condo (MAP27) - Computer area map', 'Nirvana (MAP21) - Computer area map', 'Refueling Base (MAP10) - Computer area map', 'Suburbs (MAP16) - Computer area map', 'Tenements (MAP17) - Computer area map', 'The Abandoned Mines (MAP26) - Computer area map', 'The Catacombs (MAP22) - Computer area map', 'The Chasm (MAP24) - Computer area map', 'The Citadel (MAP19) - Computer area map', 'The Courtyard (MAP18) - Computer area map', 'The Crusher (MAP06) - Computer area map', 'The Factory (MAP12) - Computer area map', 'The Focus (MAP04) - Computer area map', 'The Gantlet (MAP03) - Computer area map', 'The Inmost Dens (MAP14) - Computer area map', 'The Living End (MAP29) - Computer area map', 'The Pit (MAP09) - Computer area map', 'The Spirit World (MAP28) - Computer area map', 'The Waste Tunnels (MAP05) - Computer area map', 'Tricks and Traps (MAP08) - Computer area map', 'Underhalls (MAP02) - Computer area map', 'Wolfenstein (MAP31) - Computer area map', },
|
||||
'Keys': {"Barrels o' Fun (MAP23) - Yellow skull key", 'Bloodfalls (MAP25) - Blue skull key', 'Circle of Death (MAP11) - Blue keycard', 'Circle of Death (MAP11) - Red keycard', 'Downtown (MAP13) - Blue keycard', 'Downtown (MAP13) - Red keycard', 'Downtown (MAP13) - Yellow keycard', 'Industrial Zone (MAP15) - Blue keycard', 'Industrial Zone (MAP15) - Red keycard', 'Industrial Zone (MAP15) - Yellow keycard', 'Monster Condo (MAP27) - Blue skull key', 'Monster Condo (MAP27) - Red skull key', 'Monster Condo (MAP27) - Yellow skull key', 'Nirvana (MAP21) - Blue skull key', 'Nirvana (MAP21) - Red skull key', 'Nirvana (MAP21) - Yellow skull key', 'Refueling Base (MAP10) - Blue keycard', 'Refueling Base (MAP10) - Yellow keycard', 'Suburbs (MAP16) - Blue skull key', 'Suburbs (MAP16) - Red skull key', 'Tenements (MAP17) - Blue keycard', 'Tenements (MAP17) - Red keycard', 'Tenements (MAP17) - Yellow skull key', 'The Abandoned Mines (MAP26) - Blue keycard', 'The Abandoned Mines (MAP26) - Red keycard', 'The Abandoned Mines (MAP26) - Yellow keycard', 'The Catacombs (MAP22) - Blue skull key', 'The Catacombs (MAP22) - Red skull key', 'The Chasm (MAP24) - Blue keycard', 'The Chasm (MAP24) - Red keycard', 'The Citadel (MAP19) - Blue skull key', 'The Citadel (MAP19) - Red skull key', 'The Citadel (MAP19) - Yellow skull key', 'The Courtyard (MAP18) - Blue skull key', 'The Courtyard (MAP18) - Yellow skull key', 'The Crusher (MAP06) - Blue keycard', 'The Crusher (MAP06) - Red keycard', 'The Crusher (MAP06) - Yellow keycard', 'The Factory (MAP12) - Blue keycard', 'The Factory (MAP12) - Yellow keycard', 'The Focus (MAP04) - Blue keycard', 'The Focus (MAP04) - Red keycard', 'The Focus (MAP04) - Yellow keycard', 'The Gantlet (MAP03) - Blue keycard', 'The Gantlet (MAP03) - Red keycard', 'The Inmost Dens (MAP14) - Blue skull key', 'The Inmost Dens (MAP14) - Red skull key', 'The Pit (MAP09) - Blue keycard', 'The Pit (MAP09) - Yellow keycard', 'The Spirit World (MAP28) - Red skull key', 'The Spirit World (MAP28) - Yellow skull key', 'The Waste Tunnels (MAP05) - Blue keycard', 'The Waste Tunnels (MAP05) - Red keycard', 'The Waste Tunnels (MAP05) - Yellow keycard', 'Tricks and Traps (MAP08) - Red skull key', 'Tricks and Traps (MAP08) - Yellow skull key', 'Underhalls (MAP02) - Blue keycard', 'Underhalls (MAP02) - Red keycard', },
|
||||
'Levels': {"Barrels o' Fun (MAP23)", 'Bloodfalls (MAP25)', 'Circle of Death (MAP11)', 'Dead Simple (MAP07)', 'Downtown (MAP13)', 'Entryway (MAP01)', 'Gotcha! (MAP20)', 'Grosse (MAP32)', 'Icon of Sin (MAP30)', 'Industrial Zone (MAP15)', 'Monster Condo (MAP27)', 'Nirvana (MAP21)', 'Refueling Base (MAP10)', 'Suburbs (MAP16)', 'Tenements (MAP17)', 'The Abandoned Mines (MAP26)', 'The Catacombs (MAP22)', 'The Chasm (MAP24)', 'The Citadel (MAP19)', 'The Courtyard (MAP18)', 'The Crusher (MAP06)', 'The Factory (MAP12)', 'The Focus (MAP04)', 'The Gantlet (MAP03)', 'The Inmost Dens (MAP14)', 'The Living End (MAP29)', 'The Pit (MAP09)', 'The Spirit World (MAP28)', 'The Waste Tunnels (MAP05)', 'Tricks and Traps (MAP08)', 'Underhalls (MAP02)', 'Wolfenstein (MAP31)', },
|
||||
'Powerups': {'Armor', 'Berserk', 'Invulnerability', 'Mega Armor', 'Megasphere', 'Partial invisibility', 'Supercharge', },
|
||||
'Weapons': {'BFG9000', 'Chaingun', 'Chainsaw', 'Plasma gun', 'Rocket launcher', 'Shotgun', 'Super Shotgun', },
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 5,
|
||||
'index': 46,
|
||||
'doom_type': 82,
|
||||
'region': "The Waste Tunnels (MAP05) Main"},
|
||||
'region': "The Waste Tunnels (MAP05) Start"},
|
||||
361028: {'name': 'The Waste Tunnels (MAP05) - Blue keycard',
|
||||
'episode': 1,
|
||||
'map': 5,
|
||||
@@ -234,7 +234,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 5,
|
||||
'index': 202,
|
||||
'doom_type': 2001,
|
||||
'region': "The Waste Tunnels (MAP05) Main"},
|
||||
'region': "The Waste Tunnels (MAP05) Start"},
|
||||
361037: {'name': 'The Waste Tunnels (MAP05) - Berserk',
|
||||
'episode': 1,
|
||||
'map': 5,
|
||||
@@ -360,7 +360,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 7,
|
||||
'index': 8,
|
||||
'doom_type': 82,
|
||||
'region': "Dead Simple (MAP07) Main"},
|
||||
'region': "Dead Simple (MAP07) Start"},
|
||||
361058: {'name': 'Dead Simple (MAP07) - Chaingun',
|
||||
'episode': 1,
|
||||
'map': 7,
|
||||
@@ -378,7 +378,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 7,
|
||||
'index': 43,
|
||||
'doom_type': 8,
|
||||
'region': "Dead Simple (MAP07) Main"},
|
||||
'region': "Dead Simple (MAP07) Start"},
|
||||
361061: {'name': 'Dead Simple (MAP07) - Berserk',
|
||||
'episode': 1,
|
||||
'map': 7,
|
||||
@@ -570,7 +570,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 9,
|
||||
'index': 26,
|
||||
'doom_type': 2019,
|
||||
'region': "The Pit (MAP09) Main"},
|
||||
'region': "The Pit (MAP09) Start"},
|
||||
361093: {'name': 'The Pit (MAP09) - Supercharge',
|
||||
'episode': 1,
|
||||
'map': 9,
|
||||
@@ -678,7 +678,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 10,
|
||||
'index': 99,
|
||||
'doom_type': 2001,
|
||||
'region': "Refueling Base (MAP10) Main"},
|
||||
'region': "Refueling Base (MAP10) Start"},
|
||||
361111: {'name': 'Refueling Base (MAP10) - Chaingun',
|
||||
'episode': 1,
|
||||
'map': 10,
|
||||
@@ -846,31 +846,31 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 11,
|
||||
'index': 88,
|
||||
'doom_type': 8,
|
||||
'region': "Circle of Death (MAP11) Red"},
|
||||
'region': "Circle of Death (MAP11) Ending"},
|
||||
361139: {'name': 'Circle of Death (MAP11) - Supercharge 2',
|
||||
'episode': 1,
|
||||
'map': 11,
|
||||
'index': 108,
|
||||
'doom_type': 2013,
|
||||
'region': "Circle of Death (MAP11) Red"},
|
||||
'region': "Circle of Death (MAP11) Ending"},
|
||||
361140: {'name': 'Circle of Death (MAP11) - BFG9000',
|
||||
'episode': 1,
|
||||
'map': 11,
|
||||
'index': 110,
|
||||
'doom_type': 2006,
|
||||
'region': "Circle of Death (MAP11) Red"},
|
||||
'region': "Circle of Death (MAP11) Ending"},
|
||||
361141: {'name': 'Circle of Death (MAP11) - Exit',
|
||||
'episode': 1,
|
||||
'map': 11,
|
||||
'index': -1,
|
||||
'doom_type': -1,
|
||||
'region': "Circle of Death (MAP11) Red"},
|
||||
'region': "Circle of Death (MAP11) Ending"},
|
||||
361142: {'name': 'The Factory (MAP12) - Shotgun',
|
||||
'episode': 2,
|
||||
'map': 1,
|
||||
'index': 14,
|
||||
'doom_type': 2001,
|
||||
'region': "The Factory (MAP12) Main"},
|
||||
'region': "The Factory (MAP12) Outdoors"},
|
||||
361143: {'name': 'The Factory (MAP12) - Berserk',
|
||||
'episode': 2,
|
||||
'map': 1,
|
||||
@@ -888,13 +888,13 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 1,
|
||||
'index': 52,
|
||||
'doom_type': 2013,
|
||||
'region': "The Factory (MAP12) Main"},
|
||||
'region': "The Factory (MAP12) Indoors"},
|
||||
361146: {'name': 'The Factory (MAP12) - Blue keycard',
|
||||
'episode': 2,
|
||||
'map': 1,
|
||||
'index': 54,
|
||||
'doom_type': 5,
|
||||
'region': "The Factory (MAP12) Main"},
|
||||
'region': "The Factory (MAP12) Indoors"},
|
||||
361147: {'name': 'The Factory (MAP12) - Armor',
|
||||
'episode': 2,
|
||||
'map': 1,
|
||||
@@ -912,31 +912,31 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 1,
|
||||
'index': 83,
|
||||
'doom_type': 2013,
|
||||
'region': "The Factory (MAP12) Main"},
|
||||
'region': "The Factory (MAP12) Indoors"},
|
||||
361150: {'name': 'The Factory (MAP12) - Armor 2',
|
||||
'episode': 2,
|
||||
'map': 1,
|
||||
'index': 92,
|
||||
'doom_type': 2018,
|
||||
'region': "The Factory (MAP12) Main"},
|
||||
'region': "The Factory (MAP12) Outdoors"},
|
||||
361151: {'name': 'The Factory (MAP12) - Partial invisibility',
|
||||
'episode': 2,
|
||||
'map': 1,
|
||||
'index': 93,
|
||||
'doom_type': 2024,
|
||||
'region': "The Factory (MAP12) Main"},
|
||||
'region': "The Factory (MAP12) Outdoors"},
|
||||
361152: {'name': 'The Factory (MAP12) - Berserk 2',
|
||||
'episode': 2,
|
||||
'map': 1,
|
||||
'index': 107,
|
||||
'doom_type': 2023,
|
||||
'region': "The Factory (MAP12) Main"},
|
||||
'region': "The Factory (MAP12) Indoors"},
|
||||
361153: {'name': 'The Factory (MAP12) - Yellow keycard',
|
||||
'episode': 2,
|
||||
'map': 1,
|
||||
'index': 123,
|
||||
'doom_type': 6,
|
||||
'region': "The Factory (MAP12) Main"},
|
||||
'region': "The Factory (MAP12) Indoors"},
|
||||
361154: {'name': 'The Factory (MAP12) - BFG9000',
|
||||
'episode': 2,
|
||||
'map': 1,
|
||||
@@ -954,7 +954,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 1,
|
||||
'index': 192,
|
||||
'doom_type': 82,
|
||||
'region': "The Factory (MAP12) Main"},
|
||||
'region': "The Factory (MAP12) Indoors"},
|
||||
361157: {'name': 'The Factory (MAP12) - Exit',
|
||||
'episode': 2,
|
||||
'map': 1,
|
||||
@@ -1812,7 +1812,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 1,
|
||||
'index': 70,
|
||||
'doom_type': 82,
|
||||
'region': "Nirvana (MAP21) Main"},
|
||||
'region': "Nirvana (MAP21) Start"},
|
||||
361300: {'name': 'Nirvana (MAP21) - Rocket launcher',
|
||||
'episode': 3,
|
||||
'map': 1,
|
||||
@@ -1884,7 +1884,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 2,
|
||||
'index': 28,
|
||||
'doom_type': 2001,
|
||||
'region': "The Catacombs (MAP22) Main"},
|
||||
'region': "The Catacombs (MAP22) Early"},
|
||||
361312: {'name': 'The Catacombs (MAP22) - Berserk',
|
||||
'episode': 3,
|
||||
'map': 2,
|
||||
@@ -1896,103 +1896,103 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 2,
|
||||
'index': 83,
|
||||
'doom_type': 2004,
|
||||
'region': "The Catacombs (MAP22) Main"},
|
||||
'region': "The Catacombs (MAP22) Early"},
|
||||
361314: {'name': 'The Catacombs (MAP22) - Supercharge',
|
||||
'episode': 3,
|
||||
'map': 2,
|
||||
'index': 118,
|
||||
'doom_type': 2013,
|
||||
'region': "The Catacombs (MAP22) Main"},
|
||||
'region': "The Catacombs (MAP22) Early"},
|
||||
361315: {'name': 'The Catacombs (MAP22) - Armor',
|
||||
'episode': 3,
|
||||
'map': 2,
|
||||
'index': 119,
|
||||
'doom_type': 2018,
|
||||
'region': "The Catacombs (MAP22) Main"},
|
||||
'region': "The Catacombs (MAP22) Early"},
|
||||
361316: {'name': 'The Catacombs (MAP22) - Exit',
|
||||
'episode': 3,
|
||||
'map': 2,
|
||||
'index': -1,
|
||||
'doom_type': -1,
|
||||
'region': "The Catacombs (MAP22) Red"},
|
||||
361317: {'name': 'Barrels o Fun (MAP23) - Shotgun',
|
||||
361317: {'name': "Barrels o' Fun (MAP23) - Shotgun",
|
||||
'episode': 3,
|
||||
'map': 3,
|
||||
'index': 136,
|
||||
'doom_type': 2001,
|
||||
'region': "Barrels o Fun (MAP23) Main"},
|
||||
361318: {'name': 'Barrels o Fun (MAP23) - Berserk',
|
||||
'region': "Barrels o' Fun (MAP23) Main"},
|
||||
361318: {'name': "Barrels o' Fun (MAP23) - Berserk",
|
||||
'episode': 3,
|
||||
'map': 3,
|
||||
'index': 222,
|
||||
'doom_type': 2023,
|
||||
'region': "Barrels o Fun (MAP23) Main"},
|
||||
361319: {'name': 'Barrels o Fun (MAP23) - Backpack',
|
||||
'region': "Barrels o' Fun (MAP23) Main"},
|
||||
361319: {'name': "Barrels o' Fun (MAP23) - Backpack",
|
||||
'episode': 3,
|
||||
'map': 3,
|
||||
'index': 223,
|
||||
'doom_type': 8,
|
||||
'region': "Barrels o Fun (MAP23) Main"},
|
||||
361320: {'name': 'Barrels o Fun (MAP23) - Computer area map',
|
||||
'region': "Barrels o' Fun (MAP23) Main"},
|
||||
361320: {'name': "Barrels o' Fun (MAP23) - Computer area map",
|
||||
'episode': 3,
|
||||
'map': 3,
|
||||
'index': 224,
|
||||
'doom_type': 2026,
|
||||
'region': "Barrels o Fun (MAP23) Main"},
|
||||
361321: {'name': 'Barrels o Fun (MAP23) - Armor',
|
||||
'region': "Barrels o' Fun (MAP23) Main"},
|
||||
361321: {'name': "Barrels o' Fun (MAP23) - Armor",
|
||||
'episode': 3,
|
||||
'map': 3,
|
||||
'index': 249,
|
||||
'doom_type': 2018,
|
||||
'region': "Barrels o Fun (MAP23) Main"},
|
||||
361322: {'name': 'Barrels o Fun (MAP23) - Rocket launcher',
|
||||
'region': "Barrels o' Fun (MAP23) Main"},
|
||||
361322: {'name': "Barrels o' Fun (MAP23) - Rocket launcher",
|
||||
'episode': 3,
|
||||
'map': 3,
|
||||
'index': 264,
|
||||
'doom_type': 2003,
|
||||
'region': "Barrels o Fun (MAP23) Main"},
|
||||
361323: {'name': 'Barrels o Fun (MAP23) - Megasphere',
|
||||
'region': "Barrels o' Fun (MAP23) Main"},
|
||||
361323: {'name': "Barrels o' Fun (MAP23) - Megasphere",
|
||||
'episode': 3,
|
||||
'map': 3,
|
||||
'index': 266,
|
||||
'doom_type': 83,
|
||||
'region': "Barrels o Fun (MAP23) Main"},
|
||||
361324: {'name': 'Barrels o Fun (MAP23) - Supercharge',
|
||||
'region': "Barrels o' Fun (MAP23) Main"},
|
||||
361324: {'name': "Barrels o' Fun (MAP23) - Supercharge",
|
||||
'episode': 3,
|
||||
'map': 3,
|
||||
'index': 277,
|
||||
'doom_type': 2013,
|
||||
'region': "Barrels o Fun (MAP23) Main"},
|
||||
361325: {'name': 'Barrels o Fun (MAP23) - Backpack 2',
|
||||
'region': "Barrels o' Fun (MAP23) Main"},
|
||||
361325: {'name': "Barrels o' Fun (MAP23) - Backpack 2",
|
||||
'episode': 3,
|
||||
'map': 3,
|
||||
'index': 301,
|
||||
'doom_type': 8,
|
||||
'region': "Barrels o Fun (MAP23) Main"},
|
||||
361326: {'name': 'Barrels o Fun (MAP23) - Yellow skull key',
|
||||
'region': "Barrels o' Fun (MAP23) Main"},
|
||||
361326: {'name': "Barrels o' Fun (MAP23) - Yellow skull key",
|
||||
'episode': 3,
|
||||
'map': 3,
|
||||
'index': 307,
|
||||
'doom_type': 39,
|
||||
'region': "Barrels o Fun (MAP23) Main"},
|
||||
361327: {'name': 'Barrels o Fun (MAP23) - BFG9000',
|
||||
'region': "Barrels o' Fun (MAP23) Main"},
|
||||
361327: {'name': "Barrels o' Fun (MAP23) - BFG9000",
|
||||
'episode': 3,
|
||||
'map': 3,
|
||||
'index': 342,
|
||||
'doom_type': 2006,
|
||||
'region': "Barrels o Fun (MAP23) Main"},
|
||||
361328: {'name': 'Barrels o Fun (MAP23) - Exit',
|
||||
'region': "Barrels o' Fun (MAP23) Main"},
|
||||
361328: {'name': "Barrels o' Fun (MAP23) - Exit",
|
||||
'episode': 3,
|
||||
'map': 3,
|
||||
'index': -1,
|
||||
'doom_type': -1,
|
||||
'region': "Barrels o Fun (MAP23) Yellow"},
|
||||
'region': "Barrels o' Fun (MAP23) Yellow"},
|
||||
361329: {'name': 'The Chasm (MAP24) - Plasma gun',
|
||||
'episode': 3,
|
||||
'map': 4,
|
||||
'index': 5,
|
||||
'doom_type': 2004,
|
||||
'region': "The Chasm (MAP24) Main"},
|
||||
'region': "The Chasm (MAP24) Blue"},
|
||||
361330: {'name': 'The Chasm (MAP24) - Shotgun',
|
||||
'episode': 3,
|
||||
'map': 4,
|
||||
@@ -2004,7 +2004,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 4,
|
||||
'index': 12,
|
||||
'doom_type': 2022,
|
||||
'region': "The Chasm (MAP24) Main"},
|
||||
'region': "The Chasm (MAP24) Blue"},
|
||||
361332: {'name': 'The Chasm (MAP24) - Rocket launcher',
|
||||
'episode': 3,
|
||||
'map': 4,
|
||||
@@ -2022,7 +2022,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 4,
|
||||
'index': 31,
|
||||
'doom_type': 8,
|
||||
'region': "The Chasm (MAP24) Main"},
|
||||
'region': "The Chasm (MAP24) Blue"},
|
||||
361335: {'name': 'The Chasm (MAP24) - Berserk',
|
||||
'episode': 3,
|
||||
'map': 4,
|
||||
@@ -2034,19 +2034,19 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 4,
|
||||
'index': 155,
|
||||
'doom_type': 2023,
|
||||
'region': "The Chasm (MAP24) Main"},
|
||||
'region': "The Chasm (MAP24) Blue"},
|
||||
361337: {'name': 'The Chasm (MAP24) - Armor',
|
||||
'episode': 3,
|
||||
'map': 4,
|
||||
'index': 169,
|
||||
'doom_type': 2018,
|
||||
'region': "The Chasm (MAP24) Main"},
|
||||
'region': "The Chasm (MAP24) Blue"},
|
||||
361338: {'name': 'The Chasm (MAP24) - Red keycard',
|
||||
'episode': 3,
|
||||
'map': 4,
|
||||
'index': 261,
|
||||
'doom_type': 13,
|
||||
'region': "The Chasm (MAP24) Main"},
|
||||
'region': "The Chasm (MAP24) Blue"},
|
||||
361339: {'name': 'The Chasm (MAP24) - BFG9000',
|
||||
'episode': 3,
|
||||
'map': 4,
|
||||
@@ -2064,7 +2064,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 4,
|
||||
'index': 355,
|
||||
'doom_type': 83,
|
||||
'region': "The Chasm (MAP24) Main"},
|
||||
'region': "The Chasm (MAP24) Blue"},
|
||||
361342: {'name': 'The Chasm (MAP24) - Megasphere 2',
|
||||
'episode': 3,
|
||||
'map': 4,
|
||||
@@ -2082,7 +2082,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 5,
|
||||
'index': 6,
|
||||
'doom_type': 82,
|
||||
'region': "Bloodfalls (MAP25) Main"},
|
||||
'region': "Bloodfalls (MAP25) Start"},
|
||||
361345: {'name': 'Bloodfalls (MAP25) - Partial invisibility',
|
||||
'episode': 3,
|
||||
'map': 5,
|
||||
@@ -2664,55 +2664,55 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 10,
|
||||
'index': 40,
|
||||
'doom_type': 2006,
|
||||
'region': "Icon of Sin (MAP30) Main"},
|
||||
'region': "Icon of Sin (MAP30) Start"},
|
||||
361442: {'name': 'Icon of Sin (MAP30) - Chaingun',
|
||||
'episode': 3,
|
||||
'map': 10,
|
||||
'index': 41,
|
||||
'doom_type': 2002,
|
||||
'region': "Icon of Sin (MAP30) Main"},
|
||||
'region': "Icon of Sin (MAP30) Start"},
|
||||
361443: {'name': 'Icon of Sin (MAP30) - Chainsaw',
|
||||
'episode': 3,
|
||||
'map': 10,
|
||||
'index': 42,
|
||||
'doom_type': 2005,
|
||||
'region': "Icon of Sin (MAP30) Main"},
|
||||
'region': "Icon of Sin (MAP30) Start"},
|
||||
361444: {'name': 'Icon of Sin (MAP30) - Plasma gun',
|
||||
'episode': 3,
|
||||
'map': 10,
|
||||
'index': 43,
|
||||
'doom_type': 2004,
|
||||
'region': "Icon of Sin (MAP30) Main"},
|
||||
'region': "Icon of Sin (MAP30) Start"},
|
||||
361445: {'name': 'Icon of Sin (MAP30) - Rocket launcher',
|
||||
'episode': 3,
|
||||
'map': 10,
|
||||
'index': 44,
|
||||
'doom_type': 2003,
|
||||
'region': "Icon of Sin (MAP30) Main"},
|
||||
'region': "Icon of Sin (MAP30) Start"},
|
||||
361446: {'name': 'Icon of Sin (MAP30) - Shotgun',
|
||||
'episode': 3,
|
||||
'map': 10,
|
||||
'index': 45,
|
||||
'doom_type': 2001,
|
||||
'region': "Icon of Sin (MAP30) Main"},
|
||||
'region': "Icon of Sin (MAP30) Start"},
|
||||
361447: {'name': 'Icon of Sin (MAP30) - Super Shotgun',
|
||||
'episode': 3,
|
||||
'map': 10,
|
||||
'index': 46,
|
||||
'doom_type': 82,
|
||||
'region': "Icon of Sin (MAP30) Main"},
|
||||
'region': "Icon of Sin (MAP30) Start"},
|
||||
361448: {'name': 'Icon of Sin (MAP30) - Backpack',
|
||||
'episode': 3,
|
||||
'map': 10,
|
||||
'index': 47,
|
||||
'doom_type': 8,
|
||||
'region': "Icon of Sin (MAP30) Main"},
|
||||
'region': "Icon of Sin (MAP30) Start"},
|
||||
361449: {'name': 'Icon of Sin (MAP30) - Megasphere',
|
||||
'episode': 3,
|
||||
'map': 10,
|
||||
'index': 64,
|
||||
'doom_type': 83,
|
||||
'region': "Icon of Sin (MAP30) Main"},
|
||||
'region': "Icon of Sin (MAP30) Start"},
|
||||
361450: {'name': 'Icon of Sin (MAP30) - Megasphere 2',
|
||||
'episode': 3,
|
||||
'map': 10,
|
||||
@@ -2731,179 +2731,179 @@ location_table: Dict[int, LocationDict] = {
|
||||
'index': -1,
|
||||
'doom_type': -1,
|
||||
'region': "Icon of Sin (MAP30) Main"},
|
||||
361453: {'name': 'Wolfenstein2 (MAP31) - Rocket launcher',
|
||||
361453: {'name': 'Wolfenstein (MAP31) - Rocket launcher',
|
||||
'episode': 4,
|
||||
'map': 1,
|
||||
'index': 110,
|
||||
'doom_type': 2003,
|
||||
'region': "Wolfenstein2 (MAP31) Main"},
|
||||
361454: {'name': 'Wolfenstein2 (MAP31) - Shotgun',
|
||||
'region': "Wolfenstein (MAP31) Main"},
|
||||
361454: {'name': 'Wolfenstein (MAP31) - Shotgun',
|
||||
'episode': 4,
|
||||
'map': 1,
|
||||
'index': 139,
|
||||
'doom_type': 2001,
|
||||
'region': "Wolfenstein2 (MAP31) Main"},
|
||||
361455: {'name': 'Wolfenstein2 (MAP31) - Berserk',
|
||||
'region': "Wolfenstein (MAP31) Main"},
|
||||
361455: {'name': 'Wolfenstein (MAP31) - Berserk',
|
||||
'episode': 4,
|
||||
'map': 1,
|
||||
'index': 263,
|
||||
'doom_type': 2023,
|
||||
'region': "Wolfenstein2 (MAP31) Main"},
|
||||
361456: {'name': 'Wolfenstein2 (MAP31) - Supercharge',
|
||||
'region': "Wolfenstein (MAP31) Main"},
|
||||
361456: {'name': 'Wolfenstein (MAP31) - Supercharge',
|
||||
'episode': 4,
|
||||
'map': 1,
|
||||
'index': 278,
|
||||
'doom_type': 2013,
|
||||
'region': "Wolfenstein2 (MAP31) Main"},
|
||||
361457: {'name': 'Wolfenstein2 (MAP31) - Chaingun',
|
||||
'region': "Wolfenstein (MAP31) Main"},
|
||||
361457: {'name': 'Wolfenstein (MAP31) - Chaingun',
|
||||
'episode': 4,
|
||||
'map': 1,
|
||||
'index': 305,
|
||||
'doom_type': 2002,
|
||||
'region': "Wolfenstein2 (MAP31) Main"},
|
||||
361458: {'name': 'Wolfenstein2 (MAP31) - Super Shotgun',
|
||||
'region': "Wolfenstein (MAP31) Main"},
|
||||
361458: {'name': 'Wolfenstein (MAP31) - Super Shotgun',
|
||||
'episode': 4,
|
||||
'map': 1,
|
||||
'index': 308,
|
||||
'doom_type': 82,
|
||||
'region': "Wolfenstein2 (MAP31) Main"},
|
||||
361459: {'name': 'Wolfenstein2 (MAP31) - Partial invisibility',
|
||||
'region': "Wolfenstein (MAP31) Main"},
|
||||
361459: {'name': 'Wolfenstein (MAP31) - Partial invisibility',
|
||||
'episode': 4,
|
||||
'map': 1,
|
||||
'index': 309,
|
||||
'doom_type': 2024,
|
||||
'region': "Wolfenstein2 (MAP31) Main"},
|
||||
361460: {'name': 'Wolfenstein2 (MAP31) - Megasphere',
|
||||
'region': "Wolfenstein (MAP31) Main"},
|
||||
361460: {'name': 'Wolfenstein (MAP31) - Megasphere',
|
||||
'episode': 4,
|
||||
'map': 1,
|
||||
'index': 310,
|
||||
'doom_type': 83,
|
||||
'region': "Wolfenstein2 (MAP31) Main"},
|
||||
361461: {'name': 'Wolfenstein2 (MAP31) - Backpack',
|
||||
'region': "Wolfenstein (MAP31) Main"},
|
||||
361461: {'name': 'Wolfenstein (MAP31) - Backpack',
|
||||
'episode': 4,
|
||||
'map': 1,
|
||||
'index': 311,
|
||||
'doom_type': 8,
|
||||
'region': "Wolfenstein2 (MAP31) Main"},
|
||||
361462: {'name': 'Wolfenstein2 (MAP31) - Backpack 2',
|
||||
'region': "Wolfenstein (MAP31) Main"},
|
||||
361462: {'name': 'Wolfenstein (MAP31) - Backpack 2',
|
||||
'episode': 4,
|
||||
'map': 1,
|
||||
'index': 312,
|
||||
'doom_type': 8,
|
||||
'region': "Wolfenstein2 (MAP31) Main"},
|
||||
361463: {'name': 'Wolfenstein2 (MAP31) - Backpack 3',
|
||||
'region': "Wolfenstein (MAP31) Main"},
|
||||
361463: {'name': 'Wolfenstein (MAP31) - Backpack 3',
|
||||
'episode': 4,
|
||||
'map': 1,
|
||||
'index': 313,
|
||||
'doom_type': 8,
|
||||
'region': "Wolfenstein2 (MAP31) Main"},
|
||||
361464: {'name': 'Wolfenstein2 (MAP31) - Backpack 4',
|
||||
'region': "Wolfenstein (MAP31) Main"},
|
||||
361464: {'name': 'Wolfenstein (MAP31) - Backpack 4',
|
||||
'episode': 4,
|
||||
'map': 1,
|
||||
'index': 314,
|
||||
'doom_type': 8,
|
||||
'region': "Wolfenstein2 (MAP31) Main"},
|
||||
361465: {'name': 'Wolfenstein2 (MAP31) - BFG9000',
|
||||
'region': "Wolfenstein (MAP31) Main"},
|
||||
361465: {'name': 'Wolfenstein (MAP31) - BFG9000',
|
||||
'episode': 4,
|
||||
'map': 1,
|
||||
'index': 315,
|
||||
'doom_type': 2006,
|
||||
'region': "Wolfenstein2 (MAP31) Main"},
|
||||
361466: {'name': 'Wolfenstein2 (MAP31) - Plasma gun',
|
||||
'region': "Wolfenstein (MAP31) Main"},
|
||||
361466: {'name': 'Wolfenstein (MAP31) - Plasma gun',
|
||||
'episode': 4,
|
||||
'map': 1,
|
||||
'index': 316,
|
||||
'doom_type': 2004,
|
||||
'region': "Wolfenstein2 (MAP31) Main"},
|
||||
361467: {'name': 'Wolfenstein2 (MAP31) - Exit',
|
||||
'region': "Wolfenstein (MAP31) Main"},
|
||||
361467: {'name': 'Wolfenstein (MAP31) - Exit',
|
||||
'episode': 4,
|
||||
'map': 1,
|
||||
'index': -1,
|
||||
'doom_type': -1,
|
||||
'region': "Wolfenstein2 (MAP31) Main"},
|
||||
361468: {'name': 'Grosse2 (MAP32) - Plasma gun',
|
||||
'region': "Wolfenstein (MAP31) Main"},
|
||||
361468: {'name': 'Grosse (MAP32) - Plasma gun',
|
||||
'episode': 4,
|
||||
'map': 2,
|
||||
'index': 33,
|
||||
'doom_type': 2004,
|
||||
'region': "Grosse2 (MAP32) Main"},
|
||||
361469: {'name': 'Grosse2 (MAP32) - Rocket launcher',
|
||||
'region': "Grosse (MAP32) Main"},
|
||||
361469: {'name': 'Grosse (MAP32) - Rocket launcher',
|
||||
'episode': 4,
|
||||
'map': 2,
|
||||
'index': 57,
|
||||
'doom_type': 2003,
|
||||
'region': "Grosse2 (MAP32) Main"},
|
||||
361470: {'name': 'Grosse2 (MAP32) - Invulnerability',
|
||||
'region': "Grosse (MAP32) Start"},
|
||||
361470: {'name': 'Grosse (MAP32) - Invulnerability',
|
||||
'episode': 4,
|
||||
'map': 2,
|
||||
'index': 70,
|
||||
'doom_type': 2022,
|
||||
'region': "Grosse2 (MAP32) Main"},
|
||||
361471: {'name': 'Grosse2 (MAP32) - Super Shotgun',
|
||||
'region': "Grosse (MAP32) Main"},
|
||||
361471: {'name': 'Grosse (MAP32) - Super Shotgun',
|
||||
'episode': 4,
|
||||
'map': 2,
|
||||
'index': 74,
|
||||
'doom_type': 82,
|
||||
'region': "Grosse2 (MAP32) Main"},
|
||||
361472: {'name': 'Grosse2 (MAP32) - BFG9000',
|
||||
'region': "Grosse (MAP32) Main"},
|
||||
361472: {'name': 'Grosse (MAP32) - BFG9000',
|
||||
'episode': 4,
|
||||
'map': 2,
|
||||
'index': 75,
|
||||
'doom_type': 2006,
|
||||
'region': "Grosse2 (MAP32) Main"},
|
||||
361473: {'name': 'Grosse2 (MAP32) - Megasphere',
|
||||
'region': "Grosse (MAP32) Main"},
|
||||
361473: {'name': 'Grosse (MAP32) - Megasphere',
|
||||
'episode': 4,
|
||||
'map': 2,
|
||||
'index': 78,
|
||||
'doom_type': 83,
|
||||
'region': "Grosse2 (MAP32) Main"},
|
||||
361474: {'name': 'Grosse2 (MAP32) - Chaingun',
|
||||
'region': "Grosse (MAP32) Main"},
|
||||
361474: {'name': 'Grosse (MAP32) - Chaingun',
|
||||
'episode': 4,
|
||||
'map': 2,
|
||||
'index': 79,
|
||||
'doom_type': 2002,
|
||||
'region': "Grosse2 (MAP32) Main"},
|
||||
361475: {'name': 'Grosse2 (MAP32) - Chaingun 2',
|
||||
'region': "Grosse (MAP32) Main"},
|
||||
361475: {'name': 'Grosse (MAP32) - Chaingun 2',
|
||||
'episode': 4,
|
||||
'map': 2,
|
||||
'index': 80,
|
||||
'doom_type': 2002,
|
||||
'region': "Grosse2 (MAP32) Main"},
|
||||
361476: {'name': 'Grosse2 (MAP32) - Chaingun 3',
|
||||
'region': "Grosse (MAP32) Main"},
|
||||
361476: {'name': 'Grosse (MAP32) - Chaingun 3',
|
||||
'episode': 4,
|
||||
'map': 2,
|
||||
'index': 81,
|
||||
'doom_type': 2002,
|
||||
'region': "Grosse2 (MAP32) Main"},
|
||||
361477: {'name': 'Grosse2 (MAP32) - Berserk',
|
||||
'region': "Grosse (MAP32) Main"},
|
||||
361477: {'name': 'Grosse (MAP32) - Berserk',
|
||||
'episode': 4,
|
||||
'map': 2,
|
||||
'index': 82,
|
||||
'doom_type': 2023,
|
||||
'region': "Grosse2 (MAP32) Main"},
|
||||
361478: {'name': 'Grosse2 (MAP32) - Exit',
|
||||
'region': "Grosse (MAP32) Start"},
|
||||
361478: {'name': 'Grosse (MAP32) - Exit',
|
||||
'episode': 4,
|
||||
'map': 2,
|
||||
'index': -1,
|
||||
'doom_type': -1,
|
||||
'region': "Grosse2 (MAP32) Main"},
|
||||
'region': "Grosse (MAP32) Main"},
|
||||
}
|
||||
|
||||
|
||||
location_name_groups: Dict[str, Set[str]] = {
|
||||
'Barrels o Fun (MAP23)': {
|
||||
'Barrels o Fun (MAP23) - Armor',
|
||||
'Barrels o Fun (MAP23) - BFG9000',
|
||||
'Barrels o Fun (MAP23) - Backpack',
|
||||
'Barrels o Fun (MAP23) - Backpack 2',
|
||||
'Barrels o Fun (MAP23) - Berserk',
|
||||
'Barrels o Fun (MAP23) - Computer area map',
|
||||
'Barrels o Fun (MAP23) - Exit',
|
||||
'Barrels o Fun (MAP23) - Megasphere',
|
||||
'Barrels o Fun (MAP23) - Rocket launcher',
|
||||
'Barrels o Fun (MAP23) - Shotgun',
|
||||
'Barrels o Fun (MAP23) - Supercharge',
|
||||
'Barrels o Fun (MAP23) - Yellow skull key',
|
||||
"Barrels o' Fun (MAP23)": {
|
||||
"Barrels o' Fun (MAP23) - Armor",
|
||||
"Barrels o' Fun (MAP23) - BFG9000",
|
||||
"Barrels o' Fun (MAP23) - Backpack",
|
||||
"Barrels o' Fun (MAP23) - Backpack 2",
|
||||
"Barrels o' Fun (MAP23) - Berserk",
|
||||
"Barrels o' Fun (MAP23) - Computer area map",
|
||||
"Barrels o' Fun (MAP23) - Exit",
|
||||
"Barrels o' Fun (MAP23) - Megasphere",
|
||||
"Barrels o' Fun (MAP23) - Rocket launcher",
|
||||
"Barrels o' Fun (MAP23) - Shotgun",
|
||||
"Barrels o' Fun (MAP23) - Supercharge",
|
||||
"Barrels o' Fun (MAP23) - Yellow skull key",
|
||||
},
|
||||
'Bloodfalls (MAP25)': {
|
||||
'Bloodfalls (MAP25) - Armor',
|
||||
@@ -2998,18 +2998,18 @@ location_name_groups: Dict[str, Set[str]] = {
|
||||
'Gotcha! (MAP20) - Supercharge 3',
|
||||
'Gotcha! (MAP20) - Supercharge 4',
|
||||
},
|
||||
'Grosse2 (MAP32)': {
|
||||
'Grosse2 (MAP32) - BFG9000',
|
||||
'Grosse2 (MAP32) - Berserk',
|
||||
'Grosse2 (MAP32) - Chaingun',
|
||||
'Grosse2 (MAP32) - Chaingun 2',
|
||||
'Grosse2 (MAP32) - Chaingun 3',
|
||||
'Grosse2 (MAP32) - Exit',
|
||||
'Grosse2 (MAP32) - Invulnerability',
|
||||
'Grosse2 (MAP32) - Megasphere',
|
||||
'Grosse2 (MAP32) - Plasma gun',
|
||||
'Grosse2 (MAP32) - Rocket launcher',
|
||||
'Grosse2 (MAP32) - Super Shotgun',
|
||||
'Grosse (MAP32)': {
|
||||
'Grosse (MAP32) - BFG9000',
|
||||
'Grosse (MAP32) - Berserk',
|
||||
'Grosse (MAP32) - Chaingun',
|
||||
'Grosse (MAP32) - Chaingun 2',
|
||||
'Grosse (MAP32) - Chaingun 3',
|
||||
'Grosse (MAP32) - Exit',
|
||||
'Grosse (MAP32) - Invulnerability',
|
||||
'Grosse (MAP32) - Megasphere',
|
||||
'Grosse (MAP32) - Plasma gun',
|
||||
'Grosse (MAP32) - Rocket launcher',
|
||||
'Grosse (MAP32) - Super Shotgun',
|
||||
},
|
||||
'Icon of Sin (MAP30)': {
|
||||
'Icon of Sin (MAP30) - BFG9000',
|
||||
@@ -3417,22 +3417,22 @@ location_name_groups: Dict[str, Set[str]] = {
|
||||
'Underhalls (MAP02) - Red keycard',
|
||||
'Underhalls (MAP02) - Super Shotgun',
|
||||
},
|
||||
'Wolfenstein2 (MAP31)': {
|
||||
'Wolfenstein2 (MAP31) - BFG9000',
|
||||
'Wolfenstein2 (MAP31) - Backpack',
|
||||
'Wolfenstein2 (MAP31) - Backpack 2',
|
||||
'Wolfenstein2 (MAP31) - Backpack 3',
|
||||
'Wolfenstein2 (MAP31) - Backpack 4',
|
||||
'Wolfenstein2 (MAP31) - Berserk',
|
||||
'Wolfenstein2 (MAP31) - Chaingun',
|
||||
'Wolfenstein2 (MAP31) - Exit',
|
||||
'Wolfenstein2 (MAP31) - Megasphere',
|
||||
'Wolfenstein2 (MAP31) - Partial invisibility',
|
||||
'Wolfenstein2 (MAP31) - Plasma gun',
|
||||
'Wolfenstein2 (MAP31) - Rocket launcher',
|
||||
'Wolfenstein2 (MAP31) - Shotgun',
|
||||
'Wolfenstein2 (MAP31) - Super Shotgun',
|
||||
'Wolfenstein2 (MAP31) - Supercharge',
|
||||
'Wolfenstein (MAP31)': {
|
||||
'Wolfenstein (MAP31) - BFG9000',
|
||||
'Wolfenstein (MAP31) - Backpack',
|
||||
'Wolfenstein (MAP31) - Backpack 2',
|
||||
'Wolfenstein (MAP31) - Backpack 3',
|
||||
'Wolfenstein (MAP31) - Backpack 4',
|
||||
'Wolfenstein (MAP31) - Berserk',
|
||||
'Wolfenstein (MAP31) - Chaingun',
|
||||
'Wolfenstein (MAP31) - Exit',
|
||||
'Wolfenstein (MAP31) - Megasphere',
|
||||
'Wolfenstein (MAP31) - Partial invisibility',
|
||||
'Wolfenstein (MAP31) - Plasma gun',
|
||||
'Wolfenstein (MAP31) - Rocket launcher',
|
||||
'Wolfenstein (MAP31) - Shotgun',
|
||||
'Wolfenstein (MAP31) - Super Shotgun',
|
||||
'Wolfenstein (MAP31) - Supercharge',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ map_names: List[str] = [
|
||||
'Gotcha! (MAP20)',
|
||||
'Nirvana (MAP21)',
|
||||
'The Catacombs (MAP22)',
|
||||
'Barrels o Fun (MAP23)',
|
||||
"Barrels o' Fun (MAP23)",
|
||||
'The Chasm (MAP24)',
|
||||
'Bloodfalls (MAP25)',
|
||||
'The Abandoned Mines (MAP26)',
|
||||
@@ -34,6 +34,6 @@ map_names: List[str] = [
|
||||
'The Spirit World (MAP28)',
|
||||
'The Living End (MAP29)',
|
||||
'Icon of Sin (MAP30)',
|
||||
'Wolfenstein2 (MAP31)',
|
||||
'Grosse2 (MAP32)',
|
||||
'Wolfenstein (MAP31)',
|
||||
'Grosse (MAP32)',
|
||||
]
|
||||
|
||||
@@ -84,11 +84,12 @@ regions:List[RegionDict] = [
|
||||
|
||||
# The Waste Tunnels (MAP05)
|
||||
{"name":"The Waste Tunnels (MAP05) Main",
|
||||
"connects_to_hub":True,
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[
|
||||
{"target":"The Waste Tunnels (MAP05) Red","pro":False},
|
||||
{"target":"The Waste Tunnels (MAP05) Blue","pro":False}]},
|
||||
{"target":"The Waste Tunnels (MAP05) Blue","pro":False},
|
||||
{"target":"The Waste Tunnels (MAP05) Start","pro":False}]},
|
||||
{"name":"The Waste Tunnels (MAP05) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
@@ -103,6 +104,10 @@ regions:List[RegionDict] = [
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"The Waste Tunnels (MAP05) Main","pro":False}]},
|
||||
{"name":"The Waste Tunnels (MAP05) Start",
|
||||
"connects_to_hub":True,
|
||||
"episode":1,
|
||||
"connections":[{"target":"The Waste Tunnels (MAP05) Main","pro":False}]},
|
||||
|
||||
# The Crusher (MAP06)
|
||||
{"name":"The Crusher (MAP06) Main",
|
||||
@@ -129,9 +134,13 @@ regions:List[RegionDict] = [
|
||||
|
||||
# Dead Simple (MAP07)
|
||||
{"name":"Dead Simple (MAP07) Main",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Dead Simple (MAP07) Start","pro":False}]},
|
||||
{"name":"Dead Simple (MAP07) Start",
|
||||
"connects_to_hub":True,
|
||||
"episode":1,
|
||||
"connections":[]},
|
||||
"connections":[{"target":"Dead Simple (MAP07) Main","pro":False}]},
|
||||
|
||||
# Tricks and Traps (MAP08)
|
||||
{"name":"Tricks and Traps (MAP08) Main",
|
||||
@@ -151,11 +160,12 @@ regions:List[RegionDict] = [
|
||||
|
||||
# The Pit (MAP09)
|
||||
{"name":"The Pit (MAP09) Main",
|
||||
"connects_to_hub":True,
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[
|
||||
{"target":"The Pit (MAP09) Yellow","pro":False},
|
||||
{"target":"The Pit (MAP09) Blue","pro":False}]},
|
||||
{"target":"The Pit (MAP09) Blue","pro":False},
|
||||
{"target":"The Pit (MAP09) Start","pro":False}]},
|
||||
{"name":"The Pit (MAP09) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
@@ -164,12 +174,18 @@ regions:List[RegionDict] = [
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"The Pit (MAP09) Main","pro":False}]},
|
||||
{"name":"The Pit (MAP09) Start",
|
||||
"connects_to_hub":True,
|
||||
"episode":1,
|
||||
"connections":[{"target":"The Pit (MAP09) Main","pro":False}]},
|
||||
|
||||
# Refueling Base (MAP10)
|
||||
{"name":"Refueling Base (MAP10) Main",
|
||||
"connects_to_hub":True,
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Refueling Base (MAP10) Yellow","pro":False}]},
|
||||
"connections":[
|
||||
{"target":"Refueling Base (MAP10) Yellow","pro":False},
|
||||
{"target":"Refueling Base (MAP10) Start","pro":False}]},
|
||||
{"name":"Refueling Base (MAP10) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
@@ -180,6 +196,10 @@ regions:List[RegionDict] = [
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Refueling Base (MAP10) Yellow","pro":False}]},
|
||||
{"name":"Refueling Base (MAP10) Start",
|
||||
"connects_to_hub":True,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Refueling Base (MAP10) Main","pro":False}]},
|
||||
|
||||
# Circle of Death (MAP11)
|
||||
{"name":"Circle of Death (MAP11) Main",
|
||||
@@ -187,31 +207,49 @@ regions:List[RegionDict] = [
|
||||
"episode":1,
|
||||
"connections":[
|
||||
{"target":"Circle of Death (MAP11) Blue","pro":False},
|
||||
{"target":"Circle of Death (MAP11) Red","pro":False}]},
|
||||
{"target":"Circle of Death (MAP11) Red","pro":False},
|
||||
{"target":"Circle of Death (MAP11) Ending","pro":True}]},
|
||||
{"name":"Circle of Death (MAP11) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Circle of Death (MAP11) Main","pro":False}]},
|
||||
{"name":"Circle of Death (MAP11) Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[
|
||||
{"target":"Circle of Death (MAP11) Main","pro":False},
|
||||
{"target":"Circle of Death (MAP11) Ending","pro":False}]},
|
||||
{"name":"Circle of Death (MAP11) Ending",
|
||||
"connects_to_hub":False,
|
||||
"episode":1,
|
||||
"connections":[{"target":"Circle of Death (MAP11) Main","pro":False}]},
|
||||
|
||||
# The Factory (MAP12)
|
||||
{"name":"The Factory (MAP12) Main",
|
||||
"connects_to_hub":True,
|
||||
{"name":"The Factory (MAP12) Indoors",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[
|
||||
{"target":"The Factory (MAP12) Yellow","pro":False},
|
||||
{"target":"The Factory (MAP12) Blue","pro":False}]},
|
||||
{"target":"The Factory (MAP12) Blue","pro":False},
|
||||
{"target":"The Factory (MAP12) Main","pro":False}]},
|
||||
{"name":"The Factory (MAP12) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[{"target":"The Factory (MAP12) Main","pro":False}]},
|
||||
"connections":[{"target":"The Factory (MAP12) Indoors","pro":False}]},
|
||||
{"name":"The Factory (MAP12) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[]},
|
||||
{"name":"The Factory (MAP12) Outdoors",
|
||||
"connects_to_hub":True,
|
||||
"episode":2,
|
||||
"connections":[{"target":"The Factory (MAP12) Main","pro":False}]},
|
||||
{"name":"The Factory (MAP12) Main",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[
|
||||
{"target":"The Factory (MAP12) Indoors","pro":False},
|
||||
{"target":"The Factory (MAP12) Outdoors","pro":False}]},
|
||||
|
||||
# Downtown (MAP13)
|
||||
{"name":"Downtown (MAP13) Main",
|
||||
@@ -291,7 +329,8 @@ regions:List[RegionDict] = [
|
||||
"episode":2,
|
||||
"connections":[
|
||||
{"target":"Suburbs (MAP16) Red","pro":False},
|
||||
{"target":"Suburbs (MAP16) Blue","pro":False}]},
|
||||
{"target":"Suburbs (MAP16) Blue","pro":False},
|
||||
{"target":"Suburbs (MAP16) Pro Exit","pro":True}]},
|
||||
{"name":"Suburbs (MAP16) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
@@ -299,7 +338,13 @@ regions:List[RegionDict] = [
|
||||
{"name":"Suburbs (MAP16) Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[{"target":"Suburbs (MAP16) Main","pro":False}]},
|
||||
"connections":[
|
||||
{"target":"Suburbs (MAP16) Main","pro":False},
|
||||
{"target":"Suburbs (MAP16) Pro Exit","pro":False}]},
|
||||
{"name":"Suburbs (MAP16) Pro Exit",
|
||||
"connects_to_hub":False,
|
||||
"episode":2,
|
||||
"connections":[{"target":"Suburbs (MAP16) Red","pro":False}]},
|
||||
|
||||
# Tenements (MAP17)
|
||||
{"name":"Tenements (MAP17) Main",
|
||||
@@ -358,7 +403,7 @@ regions:List[RegionDict] = [
|
||||
|
||||
# Nirvana (MAP21)
|
||||
{"name":"Nirvana (MAP21) Main",
|
||||
"connects_to_hub":True,
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Nirvana (MAP21) Yellow","pro":False}]},
|
||||
{"name":"Nirvana (MAP21) Yellow",
|
||||
@@ -366,19 +411,31 @@ regions:List[RegionDict] = [
|
||||
"episode":3,
|
||||
"connections":[
|
||||
{"target":"Nirvana (MAP21) Main","pro":False},
|
||||
{"target":"Nirvana (MAP21) Magenta","pro":False}]},
|
||||
{"target":"Nirvana (MAP21) Magenta","pro":False},
|
||||
{"target":"Nirvana (MAP21) Pro Magenta","pro":True}]},
|
||||
{"name":"Nirvana (MAP21) Magenta",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Nirvana (MAP21) Yellow","pro":False}]},
|
||||
"connections":[
|
||||
{"target":"Nirvana (MAP21) Yellow","pro":False},
|
||||
{"target":"Nirvana (MAP21) Pro Magenta","pro":False}]},
|
||||
{"name":"Nirvana (MAP21) Start",
|
||||
"connects_to_hub":True,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Nirvana (MAP21) Main","pro":False}]},
|
||||
{"name":"Nirvana (MAP21) Pro Magenta",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Nirvana (MAP21) Magenta","pro":False}]},
|
||||
|
||||
# The Catacombs (MAP22)
|
||||
{"name":"The Catacombs (MAP22) Main",
|
||||
"connects_to_hub":True,
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[
|
||||
{"target":"The Catacombs (MAP22) Blue","pro":False},
|
||||
{"target":"The Catacombs (MAP22) Red","pro":False}]},
|
||||
{"target":"The Catacombs (MAP22) Red","pro":False},
|
||||
{"target":"The Catacombs (MAP22) Early","pro":False}]},
|
||||
{"name":"The Catacombs (MAP22) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
@@ -387,36 +444,59 @@ regions:List[RegionDict] = [
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"The Catacombs (MAP22) Main","pro":False}]},
|
||||
|
||||
# Barrels o Fun (MAP23)
|
||||
{"name":"Barrels o Fun (MAP23) Main",
|
||||
{"name":"The Catacombs (MAP22) Early",
|
||||
"connects_to_hub":True,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Barrels o Fun (MAP23) Yellow","pro":False}]},
|
||||
{"name":"Barrels o Fun (MAP23) Yellow",
|
||||
"connections":[{"target":"The Catacombs (MAP22) Main","pro":False}]},
|
||||
|
||||
# Barrels o' Fun (MAP23)
|
||||
{"name":"Barrels o' Fun (MAP23) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Barrels o' Fun (MAP23) Yellow","pro":False}]},
|
||||
{"name":"Barrels o' Fun (MAP23) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Barrels o Fun (MAP23) Main","pro":False}]},
|
||||
"connections":[{"target":"Barrels o' Fun (MAP23) Main","pro":False}]},
|
||||
|
||||
# The Chasm (MAP24)
|
||||
{"name":"The Chasm (MAP24) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":3,
|
||||
"connections":[{"target":"The Chasm (MAP24) Red","pro":False}]},
|
||||
"connections":[
|
||||
{"target":"The Chasm (MAP24) Blue","pro":False},
|
||||
{"target":"The Chasm (MAP24) Blue Pro","pro":True}]},
|
||||
{"name":"The Chasm (MAP24) Red",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"The Chasm (MAP24) Main","pro":False}]},
|
||||
"connections":[{"target":"The Chasm (MAP24) Blue","pro":False}]},
|
||||
{"name":"The Chasm (MAP24) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[
|
||||
{"target":"The Chasm (MAP24) Red","pro":False},
|
||||
{"target":"The Chasm (MAP24) Main","pro":False},
|
||||
{"target":"The Chasm (MAP24) Blue Pro","pro":False}]},
|
||||
{"name":"The Chasm (MAP24) Blue Pro",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"The Chasm (MAP24) Blue","pro":False}]},
|
||||
|
||||
# Bloodfalls (MAP25)
|
||||
{"name":"Bloodfalls (MAP25) Main",
|
||||
"connects_to_hub":True,
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Bloodfalls (MAP25) Blue","pro":False}]},
|
||||
"connections":[
|
||||
{"target":"Bloodfalls (MAP25) Blue","pro":False},
|
||||
{"target":"Bloodfalls (MAP25) Start","pro":False}]},
|
||||
{"name":"Bloodfalls (MAP25) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Bloodfalls (MAP25) Main","pro":False}]},
|
||||
{"name":"Bloodfalls (MAP25) Start",
|
||||
"connects_to_hub":True,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Bloodfalls (MAP25) Main","pro":False}]},
|
||||
|
||||
# The Abandoned Mines (MAP26)
|
||||
{"name":"The Abandoned Mines (MAP26) Main",
|
||||
@@ -484,19 +564,27 @@ regions:List[RegionDict] = [
|
||||
|
||||
# Icon of Sin (MAP30)
|
||||
{"name":"Icon of Sin (MAP30) Main",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[{"target":"Icon of Sin (MAP30) Start","pro":False}]},
|
||||
{"name":"Icon of Sin (MAP30) Start",
|
||||
"connects_to_hub":True,
|
||||
"episode":3,
|
||||
"connections":[]},
|
||||
"connections":[{"target":"Icon of Sin (MAP30) Main","pro":False}]},
|
||||
|
||||
# Wolfenstein2 (MAP31)
|
||||
{"name":"Wolfenstein2 (MAP31) Main",
|
||||
# Wolfenstein (MAP31)
|
||||
{"name":"Wolfenstein (MAP31) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":4,
|
||||
"connections":[]},
|
||||
|
||||
# Grosse2 (MAP32)
|
||||
{"name":"Grosse2 (MAP32) Main",
|
||||
# Grosse (MAP32)
|
||||
{"name":"Grosse (MAP32) Main",
|
||||
"connects_to_hub":False,
|
||||
"episode":4,
|
||||
"connections":[{"target":"Grosse (MAP32) Start","pro":False}]},
|
||||
{"name":"Grosse (MAP32) Start",
|
||||
"connects_to_hub":True,
|
||||
"episode":4,
|
||||
"connections":[]},
|
||||
"connections":[{"target":"Grosse (MAP32) Main","pro":False}]},
|
||||
]
|
||||
|
||||
@@ -53,14 +53,6 @@ def set_episode1_rules(player, multiworld, pro):
|
||||
state.has("The Focus (MAP04) - Red keycard", player, 1))
|
||||
|
||||
# The Waste Tunnels (MAP05)
|
||||
set_rule(multiworld.get_entrance("Hub -> The Waste Tunnels (MAP05) Main", player), lambda state:
|
||||
(state.has("The Waste Tunnels (MAP05)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
set_rule(multiworld.get_entrance("The Waste Tunnels (MAP05) Main -> The Waste Tunnels (MAP05) Red", player), lambda state:
|
||||
state.has("The Waste Tunnels (MAP05) - Red keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Waste Tunnels (MAP05) Main -> The Waste Tunnels (MAP05) Blue", player), lambda state:
|
||||
@@ -71,18 +63,22 @@ def set_episode1_rules(player, multiworld, pro):
|
||||
state.has("The Waste Tunnels (MAP05) - Blue keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Waste Tunnels (MAP05) Yellow -> The Waste Tunnels (MAP05) Blue", player), lambda state:
|
||||
state.has("The Waste Tunnels (MAP05) - Yellow keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("Hub -> The Waste Tunnels (MAP05) Start", player), lambda state:
|
||||
state.has("The Waste Tunnels (MAP05)", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Waste Tunnels (MAP05) Start -> The Waste Tunnels (MAP05) Main", player), lambda state:
|
||||
(state.has("Shotgun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and (state.has("Chaingun", player, 1) or
|
||||
state.has("Plasma gun", player, 1)))
|
||||
|
||||
# The Crusher (MAP06)
|
||||
set_rule(multiworld.get_entrance("Hub -> The Crusher (MAP06) Main", player), lambda state:
|
||||
(state.has("The Crusher (MAP06)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
state.has("Shotgun", player, 1)) and
|
||||
(state.has("Plasma gun", player, 1) or
|
||||
state.has("Chaingun", player, 1)))
|
||||
set_rule(multiworld.get_entrance("The Crusher (MAP06) Main -> The Crusher (MAP06) Blue", player), lambda state:
|
||||
state.has("The Crusher (MAP06) - Blue keycard", player, 1))
|
||||
state.has("The Crusher (MAP06) - Blue keycard", player, 1) and
|
||||
state.has("Super Shotgun", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Crusher (MAP06) Blue -> The Crusher (MAP06) Red", player), lambda state:
|
||||
state.has("The Crusher (MAP06) - Red keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Crusher (MAP06) Blue -> The Crusher (MAP06) Main", player), lambda state:
|
||||
@@ -95,14 +91,14 @@ def set_episode1_rules(player, multiworld, pro):
|
||||
state.has("The Crusher (MAP06) - Red keycard", player, 1))
|
||||
|
||||
# Dead Simple (MAP07)
|
||||
set_rule(multiworld.get_entrance("Hub -> Dead Simple (MAP07) Main", player), lambda state:
|
||||
(state.has("Dead Simple (MAP07)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
set_rule(multiworld.get_entrance("Hub -> Dead Simple (MAP07) Start", player), lambda state:
|
||||
state.has("Dead Simple (MAP07)", player, 1))
|
||||
set_rule(multiworld.get_entrance("Dead Simple (MAP07) Start -> Dead Simple (MAP07) Main", player), lambda state:
|
||||
(state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Super Shotgun", player, 1)) and (state.has("BFG9000", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
state.has("Rocket launcher", player, 1)))
|
||||
|
||||
# Tricks and Traps (MAP08)
|
||||
set_rule(multiworld.get_entrance("Hub -> Tricks and Traps (MAP08) Main", player), lambda state:
|
||||
@@ -119,34 +115,34 @@ def set_episode1_rules(player, multiworld, pro):
|
||||
state.has("Tricks and Traps (MAP08) - Yellow skull key", player, 1))
|
||||
|
||||
# The Pit (MAP09)
|
||||
set_rule(multiworld.get_entrance("Hub -> The Pit (MAP09) Main", player), lambda state:
|
||||
(state.has("The Pit (MAP09)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
set_rule(multiworld.get_entrance("The Pit (MAP09) Main -> The Pit (MAP09) Yellow", player), lambda state:
|
||||
state.has("The Pit (MAP09) - Yellow keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Pit (MAP09) Main -> The Pit (MAP09) Blue", player), lambda state:
|
||||
state.has("The Pit (MAP09) - Blue keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Pit (MAP09) Yellow -> The Pit (MAP09) Main", player), lambda state:
|
||||
state.has("The Pit (MAP09) - Yellow keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("Hub -> The Pit (MAP09) Start", player), lambda state:
|
||||
state.has("The Pit (MAP09)", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Pit (MAP09) Start -> The Pit (MAP09) Main", player), lambda state:
|
||||
(state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and (state.has("BFG9000", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("Rocket launcher", player, 1)))
|
||||
|
||||
# Refueling Base (MAP10)
|
||||
set_rule(multiworld.get_entrance("Hub -> Refueling Base (MAP10) Main", player), lambda state:
|
||||
(state.has("Refueling Base (MAP10)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
set_rule(multiworld.get_entrance("Refueling Base (MAP10) Main -> Refueling Base (MAP10) Yellow", player), lambda state:
|
||||
state.has("Refueling Base (MAP10) - Yellow keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("Refueling Base (MAP10) Yellow -> Refueling Base (MAP10) Yellow Blue", player), lambda state:
|
||||
state.has("Refueling Base (MAP10) - Blue keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("Hub -> Refueling Base (MAP10) Start", player), lambda state:
|
||||
state.has("Refueling Base (MAP10)", player, 1))
|
||||
set_rule(multiworld.get_entrance("Refueling Base (MAP10) Start -> Refueling Base (MAP10) Main", player), lambda state:
|
||||
(state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and (state.has("BFG9000", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("Rocket launcher", player, 1)))
|
||||
|
||||
# Circle of Death (MAP11)
|
||||
set_rule(multiworld.get_entrance("Hub -> Circle of Death (MAP11) Main", player), lambda state:
|
||||
@@ -165,18 +161,19 @@ def set_episode1_rules(player, multiworld, pro):
|
||||
|
||||
def set_episode2_rules(player, multiworld, pro):
|
||||
# The Factory (MAP12)
|
||||
set_rule(multiworld.get_entrance("Hub -> The Factory (MAP12) Main", player), lambda state:
|
||||
(state.has("The Factory (MAP12)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
set_rule(multiworld.get_entrance("The Factory (MAP12) Main -> The Factory (MAP12) Yellow", player), lambda state:
|
||||
set_rule(multiworld.get_entrance("The Factory (MAP12) Indoors -> The Factory (MAP12) Yellow", player), lambda state:
|
||||
state.has("The Factory (MAP12) - Yellow keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Factory (MAP12) Main -> The Factory (MAP12) Blue", player), lambda state:
|
||||
set_rule(multiworld.get_entrance("The Factory (MAP12) Indoors -> The Factory (MAP12) Blue", player), lambda state:
|
||||
state.has("The Factory (MAP12) - Blue keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("Hub -> The Factory (MAP12) Outdoors", player), lambda state:
|
||||
state.has("The Factory (MAP12)", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Factory (MAP12) Outdoors -> The Factory (MAP12) Main", player), lambda state:
|
||||
state.has("Super Shotgun", player, 1) or
|
||||
state.has("Plasma gun", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Factory (MAP12) Main -> The Factory (MAP12) Indoors", player), lambda state:
|
||||
(state.has("Super Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1)) and (state.has("BFG9000", player, 1) or
|
||||
state.has("Plasma gun", player, 1)))
|
||||
|
||||
# Downtown (MAP13)
|
||||
set_rule(multiworld.get_entrance("Hub -> Downtown (MAP13) Main", player), lambda state:
|
||||
@@ -307,54 +304,56 @@ def set_episode2_rules(player, multiworld, pro):
|
||||
|
||||
def set_episode3_rules(player, multiworld, pro):
|
||||
# Nirvana (MAP21)
|
||||
set_rule(multiworld.get_entrance("Hub -> Nirvana (MAP21) Main", player), lambda state:
|
||||
(state.has("Nirvana (MAP21)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
set_rule(multiworld.get_entrance("Nirvana (MAP21) Main -> Nirvana (MAP21) Yellow", player), lambda state:
|
||||
state.has("Nirvana (MAP21) - Yellow skull key", player, 1))
|
||||
(state.has("Super Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Nirvana (MAP21) - Yellow skull key", player, 1)) and (state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
set_rule(multiworld.get_entrance("Nirvana (MAP21) Yellow -> Nirvana (MAP21) Main", player), lambda state:
|
||||
state.has("Nirvana (MAP21) - Yellow skull key", player, 1))
|
||||
set_rule(multiworld.get_entrance("Nirvana (MAP21) Yellow -> Nirvana (MAP21) Magenta", player), lambda state:
|
||||
state.has("Nirvana (MAP21) - Red skull key", player, 1) and
|
||||
state.has("Nirvana (MAP21) - Blue skull key", player, 1))
|
||||
set_rule(multiworld.get_entrance("Nirvana (MAP21) Magenta -> Nirvana (MAP21) Yellow", player), lambda state:
|
||||
state.has("Nirvana (MAP21) - Red skull key", player, 1) and
|
||||
state.has("Nirvana (MAP21) - Blue skull key", player, 1))
|
||||
set_rule(multiworld.get_entrance("Hub -> Nirvana (MAP21) Start", player), lambda state:
|
||||
state.has("Nirvana (MAP21)", player, 1))
|
||||
set_rule(multiworld.get_entrance("Nirvana (MAP21) Start -> Nirvana (MAP21) Main", player), lambda state:
|
||||
state.has("Super Shotgun", player, 1) or
|
||||
state.has("Plasma gun", player, 1))
|
||||
set_rule(multiworld.get_entrance("Nirvana (MAP21) Pro Magenta -> Nirvana (MAP21) Magenta", player), lambda state:
|
||||
state.has("Nirvana (MAP21) - Red skull key", player, 1))
|
||||
|
||||
# The Catacombs (MAP22)
|
||||
set_rule(multiworld.get_entrance("Hub -> The Catacombs (MAP22) Main", player), lambda state:
|
||||
(state.has("The Catacombs (MAP22)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("BFG9000", player, 1) or
|
||||
state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1)))
|
||||
set_rule(multiworld.get_entrance("The Catacombs (MAP22) Main -> The Catacombs (MAP22) Blue", player), lambda state:
|
||||
state.has("The Catacombs (MAP22) - Blue skull key", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Catacombs (MAP22) Main -> The Catacombs (MAP22) Red", player), lambda state:
|
||||
state.has("The Catacombs (MAP22) - Red skull key", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Catacombs (MAP22) Red -> The Catacombs (MAP22) Main", player), lambda state:
|
||||
state.has("The Catacombs (MAP22) - Red skull key", player, 1))
|
||||
set_rule(multiworld.get_entrance("Hub -> The Catacombs (MAP22) Early", player), lambda state:
|
||||
(state.has("The Catacombs (MAP22)", player, 1)) and
|
||||
(state.has("Shotgun", player, 1) or
|
||||
state.has("Super Shotgun", player, 1) or
|
||||
state.has("Plasma gun", player, 1)))
|
||||
set_rule(multiworld.get_entrance("The Catacombs (MAP22) Early -> The Catacombs (MAP22) Main", player), lambda state:
|
||||
(state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and (state.has("BFG9000", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("Rocket launcher", player, 1)))
|
||||
|
||||
# Barrels o Fun (MAP23)
|
||||
set_rule(multiworld.get_entrance("Hub -> Barrels o Fun (MAP23) Main", player), lambda state:
|
||||
(state.has("Barrels o Fun (MAP23)", player, 1) and
|
||||
# Barrels o' Fun (MAP23)
|
||||
set_rule(multiworld.get_entrance("Hub -> Barrels o' Fun (MAP23) Main", player), lambda state:
|
||||
(state.has("Barrels o' Fun (MAP23)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
set_rule(multiworld.get_entrance("Barrels o Fun (MAP23) Main -> Barrels o Fun (MAP23) Yellow", player), lambda state:
|
||||
state.has("Barrels o Fun (MAP23) - Yellow skull key", player, 1))
|
||||
set_rule(multiworld.get_entrance("Barrels o Fun (MAP23) Yellow -> Barrels o Fun (MAP23) Main", player), lambda state:
|
||||
state.has("Barrels o Fun (MAP23) - Yellow skull key", player, 1))
|
||||
set_rule(multiworld.get_entrance("Barrels o' Fun (MAP23) Main -> Barrels o' Fun (MAP23) Yellow", player), lambda state:
|
||||
state.has("Barrels o' Fun (MAP23) - Yellow skull key", player, 1))
|
||||
set_rule(multiworld.get_entrance("Barrels o' Fun (MAP23) Yellow -> Barrels o' Fun (MAP23) Main", player), lambda state:
|
||||
state.has("Barrels o' Fun (MAP23) - Yellow skull key", player, 1))
|
||||
|
||||
# The Chasm (MAP24)
|
||||
set_rule(multiworld.get_entrance("Hub -> The Chasm (MAP24) Main", player), lambda state:
|
||||
@@ -365,24 +364,26 @@ def set_episode3_rules(player, multiworld, pro):
|
||||
state.has("Plasma gun", player, 1) and
|
||||
state.has("BFG9000", player, 1) and
|
||||
state.has("Super Shotgun", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Chasm (MAP24) Main -> The Chasm (MAP24) Red", player), lambda state:
|
||||
set_rule(multiworld.get_entrance("The Chasm (MAP24) Main -> The Chasm (MAP24) Blue", player), lambda state:
|
||||
state.has("The Chasm (MAP24) - Blue keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Chasm (MAP24) Red -> The Chasm (MAP24) Blue", player), lambda state:
|
||||
state.has("The Chasm (MAP24) - Red keycard", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Chasm (MAP24) Red -> The Chasm (MAP24) Main", player), lambda state:
|
||||
set_rule(multiworld.get_entrance("The Chasm (MAP24) Blue -> The Chasm (MAP24) Red", player), lambda state:
|
||||
state.has("The Chasm (MAP24) - Red keycard", player, 1))
|
||||
|
||||
# Bloodfalls (MAP25)
|
||||
set_rule(multiworld.get_entrance("Hub -> Bloodfalls (MAP25) Main", player), lambda state:
|
||||
state.has("Bloodfalls (MAP25)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Rocket launcher", player, 1) and
|
||||
state.has("Plasma gun", player, 1) and
|
||||
state.has("BFG9000", player, 1) and
|
||||
state.has("Super Shotgun", player, 1))
|
||||
set_rule(multiworld.get_entrance("Bloodfalls (MAP25) Main -> Bloodfalls (MAP25) Blue", player), lambda state:
|
||||
state.has("Bloodfalls (MAP25) - Blue skull key", player, 1))
|
||||
(state.has("Bloodfalls (MAP25) - Blue skull key", player, 1)) and (state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
set_rule(multiworld.get_entrance("Bloodfalls (MAP25) Blue -> Bloodfalls (MAP25) Main", player), lambda state:
|
||||
state.has("Bloodfalls (MAP25) - Blue skull key", player, 1))
|
||||
set_rule(multiworld.get_entrance("Hub -> Bloodfalls (MAP25) Start", player), lambda state:
|
||||
state.has("Bloodfalls (MAP25)", player, 1))
|
||||
set_rule(multiworld.get_entrance("Bloodfalls (MAP25) Start -> Bloodfalls (MAP25) Main", player), lambda state:
|
||||
state.has("Super Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Shotgun", player, 1))
|
||||
|
||||
# The Abandoned Mines (MAP26)
|
||||
set_rule(multiworld.get_entrance("Hub -> The Abandoned Mines (MAP26) Main", player), lambda state:
|
||||
@@ -451,36 +452,34 @@ def set_episode3_rules(player, multiworld, pro):
|
||||
state.has("Super Shotgun", player, 1))
|
||||
|
||||
# Icon of Sin (MAP30)
|
||||
set_rule(multiworld.get_entrance("Hub -> Icon of Sin (MAP30) Main", player), lambda state:
|
||||
state.has("Icon of Sin (MAP30)", player, 1) and
|
||||
state.has("Rocket launcher", player, 1) and
|
||||
set_rule(multiworld.get_entrance("Hub -> Icon of Sin (MAP30) Start", player), lambda state:
|
||||
state.has("Icon of Sin (MAP30)", player, 1))
|
||||
set_rule(multiworld.get_entrance("Icon of Sin (MAP30) Start -> Icon of Sin (MAP30) Main", player), lambda state:
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Rocket launcher", player, 1) and
|
||||
state.has("Plasma gun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("BFG9000", player, 1) and
|
||||
state.has("Super Shotgun", player, 1))
|
||||
|
||||
|
||||
def set_episode4_rules(player, multiworld, pro):
|
||||
# Wolfenstein2 (MAP31)
|
||||
set_rule(multiworld.get_entrance("Hub -> Wolfenstein2 (MAP31) Main", player), lambda state:
|
||||
(state.has("Wolfenstein2 (MAP31)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
# Wolfenstein (MAP31)
|
||||
set_rule(multiworld.get_entrance("Hub -> Wolfenstein (MAP31) Main", player), lambda state:
|
||||
(state.has("Wolfenstein (MAP31)", player, 1) and
|
||||
state.has("Chaingun", player, 1)) and
|
||||
(state.has("Shotgun", player, 1) or
|
||||
state.has("Super Shotgun", player, 1)))
|
||||
|
||||
# Grosse2 (MAP32)
|
||||
set_rule(multiworld.get_entrance("Hub -> Grosse2 (MAP32) Main", player), lambda state:
|
||||
(state.has("Grosse2 (MAP32)", player, 1) and
|
||||
state.has("Shotgun", player, 1) and
|
||||
# Grosse (MAP32)
|
||||
set_rule(multiworld.get_entrance("Hub -> Grosse (MAP32) Start", player), lambda state:
|
||||
state.has("Grosse (MAP32)", player, 1))
|
||||
set_rule(multiworld.get_entrance("Grosse (MAP32) Start -> Grosse (MAP32) Main", player), lambda state:
|
||||
(state.has("Shotgun", player, 1) and
|
||||
state.has("Chaingun", player, 1) and
|
||||
state.has("Super Shotgun", player, 1)) and
|
||||
(state.has("Rocket launcher", player, 1) or
|
||||
state.has("Super Shotgun", player, 1)) and (state.has("BFG9000", player, 1) or
|
||||
state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
state.has("Rocket launcher", player, 1)))
|
||||
|
||||
|
||||
def set_rules(doom_ii_world: "DOOM2World", included_episodes, pro):
|
||||
|
||||
@@ -51,11 +51,11 @@ class DOOM2World(World):
|
||||
location_name_to_id = {data["name"]: loc_id for loc_id, data in Locations.location_table.items()}
|
||||
location_name_groups = Locations.location_name_groups
|
||||
|
||||
starting_level_for_episode: List[str] = [
|
||||
"Entryway (MAP01)",
|
||||
"The Factory (MAP12)",
|
||||
"Nirvana (MAP21)"
|
||||
]
|
||||
starting_level_for_episode: Dict[int, str] = {
|
||||
1: "Entryway (MAP01)",
|
||||
2: "The Factory (MAP12)",
|
||||
3: "Nirvana (MAP21)"
|
||||
}
|
||||
|
||||
# Item ratio that scales depending on episode count. These are the ratio for 3 episode. In DOOM1.
|
||||
# The ratio have been tweaked seem, and feel good.
|
||||
@@ -77,6 +77,7 @@ class DOOM2World(World):
|
||||
def __init__(self, multiworld: MultiWorld, player: int):
|
||||
self.included_episodes = [1, 1, 1, 0]
|
||||
self.location_count = 0
|
||||
self.starting_levels = []
|
||||
|
||||
super().__init__(multiworld, player)
|
||||
|
||||
@@ -95,6 +96,14 @@ class DOOM2World(World):
|
||||
if self.get_episode_count() == 0:
|
||||
self.included_episodes[0] = 1
|
||||
|
||||
self.starting_levels = [level_name for (episode, level_name) in self.starting_level_for_episode.items()
|
||||
if self.included_episodes[episode - 1]]
|
||||
|
||||
# If soloing MAP21-MAP30, we need to mark a weapon as early to help generation succeed
|
||||
if self.get_episode_count() == 1 and self.included_episodes[2]:
|
||||
early_weapon = self.random.choice(["Super Shotgun", "Plasma gun"])
|
||||
self.multiworld.early_items[self.player][early_weapon] = 1
|
||||
|
||||
def create_regions(self):
|
||||
pro = self.options.pro.value
|
||||
|
||||
@@ -193,7 +202,7 @@ class DOOM2World(World):
|
||||
if item["episode"] != -1 and not self.included_episodes[item["episode"] - 1]:
|
||||
continue
|
||||
|
||||
count = item["count"] if item["name"] not in self.starting_level_for_episode else item["count"] - 1
|
||||
count = item["count"] if item["name"] not in self.starting_levels else item["count"] - 1
|
||||
itempool += [self.create_item(item["name"]) for _ in range(count)]
|
||||
|
||||
# Backpack(s) based on options
|
||||
@@ -224,9 +233,8 @@ class DOOM2World(World):
|
||||
self.location_count -= 1
|
||||
|
||||
# Give starting levels right away
|
||||
for i in range(len(self.starting_level_for_episode)):
|
||||
if self.included_episodes[i]:
|
||||
self.multiworld.push_precollected(self.create_item(self.starting_level_for_episode[i]))
|
||||
for map_name in self.starting_levels:
|
||||
self.multiworld.push_precollected(self.create_item(map_name))
|
||||
|
||||
# Give Computer area maps if option selected
|
||||
if start_with_computer_area_maps:
|
||||
|
||||
@@ -255,7 +255,8 @@ async def game_watcher(ctx: FactorioContext):
|
||||
if "DeathLink" in ctx.tags:
|
||||
async_start(ctx.send_death())
|
||||
if ctx.energy_link_increment:
|
||||
in_world_bridges = data["energy_bridges"]
|
||||
# 1 + quality * 0.3 for each bridge
|
||||
in_world_bridges: float = data["energy_bridges"]
|
||||
if in_world_bridges:
|
||||
in_world_energy = data["energy"]
|
||||
if in_world_energy < (ctx.energy_link_increment * in_world_bridges):
|
||||
@@ -263,14 +264,14 @@ async def game_watcher(ctx: FactorioContext):
|
||||
ctx.last_deplete = time.time()
|
||||
async_start(ctx.send_msgs([{
|
||||
"cmd": "Set", "key": ctx.energylink_key, "operations":
|
||||
[{"operation": "add", "value": -ctx.energy_link_increment * in_world_bridges},
|
||||
[{"operation": "add", "value": int(-ctx.energy_link_increment * in_world_bridges)},
|
||||
{"operation": "max", "value": 0}],
|
||||
"last_deplete": ctx.last_deplete
|
||||
}]))
|
||||
# Above Capacity - (len(Bridges) * ENERGY_INCREMENT)
|
||||
elif in_world_energy > (in_world_bridges * ctx.energy_link_increment * 5) - \
|
||||
ctx.energy_link_increment * in_world_bridges:
|
||||
value = ctx.energy_link_increment * in_world_bridges
|
||||
value = int(ctx.energy_link_increment * in_world_bridges)
|
||||
async_start(ctx.send_msgs([{
|
||||
"cmd": "Set", "key": ctx.energylink_key, "operations":
|
||||
[{"operation": "add", "value": value}]
|
||||
@@ -406,7 +407,7 @@ async def get_info(ctx: FactorioContext, rcon_client: factorio_rcon.RCONClient):
|
||||
ctx.auth = info["slot_name"]
|
||||
ctx.seed_name = info["seed_name"]
|
||||
death_link = info["death_link"]
|
||||
ctx.energy_link_increment = info.get("energy_link", 0)
|
||||
ctx.energy_link_increment = int(info.get("energy_link", 0))
|
||||
logger.debug(f"Energy Link Increment: {ctx.energy_link_increment}")
|
||||
if ctx.energy_link_increment and ctx.ui:
|
||||
ctx.ui.enable_energy_link()
|
||||
|
||||
@@ -102,7 +102,7 @@ class Factorio(World):
|
||||
item_name_groups = {
|
||||
"Progressive": set(progressive_tech_table.keys()),
|
||||
}
|
||||
required_client_version = (0, 5, 1)
|
||||
required_client_version = (0, 6, 0)
|
||||
if Utils.version_tuple < required_client_version:
|
||||
raise Exception(f"Update Archipelago to use this world ({game}).")
|
||||
ordered_science_packs: typing.List[str] = MaxSciencePack.get_ordered_science_packs()
|
||||
|
||||
@@ -514,19 +514,19 @@ item_table: Dict[int, ItemDict] = {
|
||||
'map': 7},
|
||||
370259: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'The Aquifier (E3M9) - Blue key',
|
||||
'name': 'The Aquifer (E3M9) - Blue key',
|
||||
'doom_type': 79,
|
||||
'episode': 3,
|
||||
'map': 9},
|
||||
370260: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'The Aquifier (E3M9) - Green key',
|
||||
'name': 'The Aquifer (E3M9) - Green key',
|
||||
'doom_type': 73,
|
||||
'episode': 3,
|
||||
'map': 9},
|
||||
370261: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'The Aquifier (E3M9) - Yellow key',
|
||||
'name': 'The Aquifer (E3M9) - Yellow key',
|
||||
'doom_type': 80,
|
||||
'episode': 3,
|
||||
'map': 9},
|
||||
@@ -1234,37 +1234,37 @@ item_table: Dict[int, ItemDict] = {
|
||||
'map': 7},
|
||||
370475: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': "D'Sparil'S Keep (E3M8)",
|
||||
'name': "D'Sparil's Keep (E3M8)",
|
||||
'doom_type': -1,
|
||||
'episode': 3,
|
||||
'map': 8},
|
||||
370476: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': "D'Sparil'S Keep (E3M8) - Complete",
|
||||
'name': "D'Sparil's Keep (E3M8) - Complete",
|
||||
'doom_type': -2,
|
||||
'episode': 3,
|
||||
'map': 8},
|
||||
370477: {'classification': ItemClassification.filler,
|
||||
'count': 1,
|
||||
'name': "D'Sparil'S Keep (E3M8) - Map Scroll",
|
||||
'name': "D'Sparil's Keep (E3M8) - Map Scroll",
|
||||
'doom_type': 35,
|
||||
'episode': 3,
|
||||
'map': 8},
|
||||
370478: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'The Aquifier (E3M9)',
|
||||
'name': 'The Aquifer (E3M9)',
|
||||
'doom_type': -1,
|
||||
'episode': 3,
|
||||
'map': 9},
|
||||
370479: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'The Aquifier (E3M9) - Complete',
|
||||
'name': 'The Aquifer (E3M9) - Complete',
|
||||
'doom_type': -2,
|
||||
'episode': 3,
|
||||
'map': 9},
|
||||
370480: {'classification': ItemClassification.filler,
|
||||
'count': 1,
|
||||
'name': 'The Aquifier (E3M9) - Map Scroll',
|
||||
'name': 'The Aquifer (E3M9) - Map Scroll',
|
||||
'doom_type': 35,
|
||||
'episode': 3,
|
||||
'map': 9},
|
||||
@@ -1635,8 +1635,8 @@ item_name_groups: Dict[str, Set[str]] = {
|
||||
'Ammos': {'Crystal Geode', 'Energy Orb', 'Greater Runes', 'Inferno Orb', 'Pile of Mace Spheres', 'Quiver of Ethereal Arrows', },
|
||||
'Armors': {'Enchanted Shield', 'Silver Shield', },
|
||||
'Artifacts': {'Chaos Device', 'Morph Ovum', 'Mystic Urn', 'Quartz Flask', 'Ring of Invincibility', 'Shadowsphere', 'Timebomb of the Ancients', 'Tome of Power', 'Torch', },
|
||||
'Keys': {'Ambulatory (E4M3) - Blue key', 'Ambulatory (E4M3) - Green key', 'Ambulatory (E4M3) - Yellow key', 'Blockhouse (E4M2) - Blue key', 'Blockhouse (E4M2) - Green key', 'Blockhouse (E4M2) - Yellow key', 'Catafalque (E4M1) - Green key', 'Catafalque (E4M1) - Yellow key', 'Colonnade (E5M6) - Blue key', 'Colonnade (E5M6) - Green key', 'Colonnade (E5M6) - Yellow key', 'Courtyard (E5M4) - Blue key', 'Courtyard (E5M4) - Green key', 'Courtyard (E5M4) - Yellow key', 'Foetid Manse (E5M7) - Blue key', 'Foetid Manse (E5M7) - Green key', 'Foetid Manse (E5M7) - Yellow key', 'Great Stair (E4M5) - Blue key', 'Great Stair (E4M5) - Green key', 'Great Stair (E4M5) - Yellow key', 'Halls of the Apostate (E4M6) - Blue key', 'Halls of the Apostate (E4M6) - Green key', 'Halls of the Apostate (E4M6) - Yellow key', 'Hydratyr (E5M5) - Blue key', 'Hydratyr (E5M5) - Green key', 'Hydratyr (E5M5) - Yellow key', 'Mausoleum (E4M9) - Yellow key', 'Ochre Cliffs (E5M1) - Blue key', 'Ochre Cliffs (E5M1) - Green key', 'Ochre Cliffs (E5M1) - Yellow key', 'Quay (E5M3) - Blue key', 'Quay (E5M3) - Green key', 'Quay (E5M3) - Yellow key', 'Ramparts of Perdition (E4M7) - Blue key', 'Ramparts of Perdition (E4M7) - Green key', 'Ramparts of Perdition (E4M7) - Yellow key', 'Rapids (E5M2) - Green key', 'Rapids (E5M2) - Yellow key', 'Shattered Bridge (E4M8) - Yellow key', "Skein of D'Sparil (E5M9) - Blue key", "Skein of D'Sparil (E5M9) - Green key", "Skein of D'Sparil (E5M9) - Yellow key", 'The Aquifier (E3M9) - Blue key', 'The Aquifier (E3M9) - Green key', 'The Aquifier (E3M9) - Yellow key', 'The Azure Fortress (E3M4) - Green key', 'The Azure Fortress (E3M4) - Yellow key', 'The Catacombs (E2M5) - Blue key', 'The Catacombs (E2M5) - Green key', 'The Catacombs (E2M5) - Yellow key', 'The Cathedral (E1M6) - Green key', 'The Cathedral (E1M6) - Yellow key', 'The Cesspool (E3M2) - Blue key', 'The Cesspool (E3M2) - Green key', 'The Cesspool (E3M2) - Yellow key', 'The Chasm (E3M7) - Blue key', 'The Chasm (E3M7) - Green key', 'The Chasm (E3M7) - Yellow key', 'The Citadel (E1M5) - Blue key', 'The Citadel (E1M5) - Green key', 'The Citadel (E1M5) - Yellow key', 'The Confluence (E3M3) - Blue key', 'The Confluence (E3M3) - Green key', 'The Confluence (E3M3) - Yellow key', 'The Crater (E2M1) - Green key', 'The Crater (E2M1) - Yellow key', 'The Crypts (E1M7) - Blue key', 'The Crypts (E1M7) - Green key', 'The Crypts (E1M7) - Yellow key', 'The Docks (E1M1) - Yellow key', 'The Dungeons (E1M2) - Blue key', 'The Dungeons (E1M2) - Green key', 'The Dungeons (E1M2) - Yellow key', 'The Gatehouse (E1M3) - Green key', 'The Gatehouse (E1M3) - Yellow key', 'The Glacier (E2M9) - Blue key', 'The Glacier (E2M9) - Green key', 'The Glacier (E2M9) - Yellow key', 'The Graveyard (E1M9) - Blue key', 'The Graveyard (E1M9) - Green key', 'The Graveyard (E1M9) - Yellow key', 'The Great Hall (E2M7) - Blue key', 'The Great Hall (E2M7) - Green key', 'The Great Hall (E2M7) - Yellow key', 'The Guard Tower (E1M4) - Green key', 'The Guard Tower (E1M4) - Yellow key', 'The Halls of Fear (E3M6) - Blue key', 'The Halls of Fear (E3M6) - Green key', 'The Halls of Fear (E3M6) - Yellow key', 'The Ice Grotto (E2M4) - Blue key', 'The Ice Grotto (E2M4) - Green key', 'The Ice Grotto (E2M4) - Yellow key', 'The Labyrinth (E2M6) - Blue key', 'The Labyrinth (E2M6) - Green key', 'The Labyrinth (E2M6) - Yellow key', 'The Lava Pits (E2M2) - Green key', 'The Lava Pits (E2M2) - Yellow key', 'The Ophidian Lair (E3M5) - Green key', 'The Ophidian Lair (E3M5) - Yellow key', 'The River of Fire (E2M3) - Blue key', 'The River of Fire (E2M3) - Green key', 'The River of Fire (E2M3) - Yellow key', 'The Storehouse (E3M1) - Green key', 'The Storehouse (E3M1) - Yellow key', },
|
||||
'Levels': {'Ambulatory (E4M3)', 'Blockhouse (E4M2)', 'Catafalque (E4M1)', 'Colonnade (E5M6)', 'Courtyard (E5M4)', "D'Sparil'S Keep (E3M8)", 'Field of Judgement (E5M8)', 'Foetid Manse (E5M7)', 'Great Stair (E4M5)', 'Halls of the Apostate (E4M6)', "Hell's Maw (E1M8)", 'Hydratyr (E5M5)', 'Mausoleum (E4M9)', 'Ochre Cliffs (E5M1)', 'Quay (E5M3)', 'Ramparts of Perdition (E4M7)', 'Rapids (E5M2)', 'Sepulcher (E4M4)', 'Shattered Bridge (E4M8)', "Skein of D'Sparil (E5M9)", 'The Aquifier (E3M9)', 'The Azure Fortress (E3M4)', 'The Catacombs (E2M5)', 'The Cathedral (E1M6)', 'The Cesspool (E3M2)', 'The Chasm (E3M7)', 'The Citadel (E1M5)', 'The Confluence (E3M3)', 'The Crater (E2M1)', 'The Crypts (E1M7)', 'The Docks (E1M1)', 'The Dungeons (E1M2)', 'The Gatehouse (E1M3)', 'The Glacier (E2M9)', 'The Graveyard (E1M9)', 'The Great Hall (E2M7)', 'The Guard Tower (E1M4)', 'The Halls of Fear (E3M6)', 'The Ice Grotto (E2M4)', 'The Labyrinth (E2M6)', 'The Lava Pits (E2M2)', 'The Ophidian Lair (E3M5)', 'The Portals of Chaos (E2M8)', 'The River of Fire (E2M3)', 'The Storehouse (E3M1)', },
|
||||
'Map Scrolls': {'Ambulatory (E4M3) - Map Scroll', 'Blockhouse (E4M2) - Map Scroll', 'Catafalque (E4M1) - Map Scroll', 'Colonnade (E5M6) - Map Scroll', 'Courtyard (E5M4) - Map Scroll', "D'Sparil'S Keep (E3M8) - Map Scroll", 'Field of Judgement (E5M8) - Map Scroll', 'Foetid Manse (E5M7) - Map Scroll', 'Great Stair (E4M5) - Map Scroll', 'Halls of the Apostate (E4M6) - Map Scroll', "Hell's Maw (E1M8) - Map Scroll", 'Hydratyr (E5M5) - Map Scroll', 'Mausoleum (E4M9) - Map Scroll', 'Ochre Cliffs (E5M1) - Map Scroll', 'Quay (E5M3) - Map Scroll', 'Ramparts of Perdition (E4M7) - Map Scroll', 'Rapids (E5M2) - Map Scroll', 'Sepulcher (E4M4) - Map Scroll', 'Shattered Bridge (E4M8) - Map Scroll', "Skein of D'Sparil (E5M9) - Map Scroll", 'The Aquifier (E3M9) - Map Scroll', 'The Azure Fortress (E3M4) - Map Scroll', 'The Catacombs (E2M5) - Map Scroll', 'The Cathedral (E1M6) - Map Scroll', 'The Cesspool (E3M2) - Map Scroll', 'The Chasm (E3M7) - Map Scroll', 'The Citadel (E1M5) - Map Scroll', 'The Confluence (E3M3) - Map Scroll', 'The Crater (E2M1) - Map Scroll', 'The Crypts (E1M7) - Map Scroll', 'The Docks (E1M1) - Map Scroll', 'The Dungeons (E1M2) - Map Scroll', 'The Gatehouse (E1M3) - Map Scroll', 'The Glacier (E2M9) - Map Scroll', 'The Graveyard (E1M9) - Map Scroll', 'The Great Hall (E2M7) - Map Scroll', 'The Guard Tower (E1M4) - Map Scroll', 'The Halls of Fear (E3M6) - Map Scroll', 'The Ice Grotto (E2M4) - Map Scroll', 'The Labyrinth (E2M6) - Map Scroll', 'The Lava Pits (E2M2) - Map Scroll', 'The Ophidian Lair (E3M5) - Map Scroll', 'The Portals of Chaos (E2M8) - Map Scroll', 'The River of Fire (E2M3) - Map Scroll', 'The Storehouse (E3M1) - Map Scroll', },
|
||||
'Keys': {'Ambulatory (E4M3) - Blue key', 'Ambulatory (E4M3) - Green key', 'Ambulatory (E4M3) - Yellow key', 'Blockhouse (E4M2) - Blue key', 'Blockhouse (E4M2) - Green key', 'Blockhouse (E4M2) - Yellow key', 'Catafalque (E4M1) - Green key', 'Catafalque (E4M1) - Yellow key', 'Colonnade (E5M6) - Blue key', 'Colonnade (E5M6) - Green key', 'Colonnade (E5M6) - Yellow key', 'Courtyard (E5M4) - Blue key', 'Courtyard (E5M4) - Green key', 'Courtyard (E5M4) - Yellow key', 'Foetid Manse (E5M7) - Blue key', 'Foetid Manse (E5M7) - Green key', 'Foetid Manse (E5M7) - Yellow key', 'Great Stair (E4M5) - Blue key', 'Great Stair (E4M5) - Green key', 'Great Stair (E4M5) - Yellow key', 'Halls of the Apostate (E4M6) - Blue key', 'Halls of the Apostate (E4M6) - Green key', 'Halls of the Apostate (E4M6) - Yellow key', 'Hydratyr (E5M5) - Blue key', 'Hydratyr (E5M5) - Green key', 'Hydratyr (E5M5) - Yellow key', 'Mausoleum (E4M9) - Yellow key', 'Ochre Cliffs (E5M1) - Blue key', 'Ochre Cliffs (E5M1) - Green key', 'Ochre Cliffs (E5M1) - Yellow key', 'Quay (E5M3) - Blue key', 'Quay (E5M3) - Green key', 'Quay (E5M3) - Yellow key', 'Ramparts of Perdition (E4M7) - Blue key', 'Ramparts of Perdition (E4M7) - Green key', 'Ramparts of Perdition (E4M7) - Yellow key', 'Rapids (E5M2) - Green key', 'Rapids (E5M2) - Yellow key', 'Shattered Bridge (E4M8) - Yellow key', "Skein of D'Sparil (E5M9) - Blue key", "Skein of D'Sparil (E5M9) - Green key", "Skein of D'Sparil (E5M9) - Yellow key", 'The Aquifer (E3M9) - Blue key', 'The Aquifer (E3M9) - Green key', 'The Aquifer (E3M9) - Yellow key', 'The Azure Fortress (E3M4) - Green key', 'The Azure Fortress (E3M4) - Yellow key', 'The Catacombs (E2M5) - Blue key', 'The Catacombs (E2M5) - Green key', 'The Catacombs (E2M5) - Yellow key', 'The Cathedral (E1M6) - Green key', 'The Cathedral (E1M6) - Yellow key', 'The Cesspool (E3M2) - Blue key', 'The Cesspool (E3M2) - Green key', 'The Cesspool (E3M2) - Yellow key', 'The Chasm (E3M7) - Blue key', 'The Chasm (E3M7) - Green key', 'The Chasm (E3M7) - Yellow key', 'The Citadel (E1M5) - Blue key', 'The Citadel (E1M5) - Green key', 'The Citadel (E1M5) - Yellow key', 'The Confluence (E3M3) - Blue key', 'The Confluence (E3M3) - Green key', 'The Confluence (E3M3) - Yellow key', 'The Crater (E2M1) - Green key', 'The Crater (E2M1) - Yellow key', 'The Crypts (E1M7) - Blue key', 'The Crypts (E1M7) - Green key', 'The Crypts (E1M7) - Yellow key', 'The Docks (E1M1) - Yellow key', 'The Dungeons (E1M2) - Blue key', 'The Dungeons (E1M2) - Green key', 'The Dungeons (E1M2) - Yellow key', 'The Gatehouse (E1M3) - Green key', 'The Gatehouse (E1M3) - Yellow key', 'The Glacier (E2M9) - Blue key', 'The Glacier (E2M9) - Green key', 'The Glacier (E2M9) - Yellow key', 'The Graveyard (E1M9) - Blue key', 'The Graveyard (E1M9) - Green key', 'The Graveyard (E1M9) - Yellow key', 'The Great Hall (E2M7) - Blue key', 'The Great Hall (E2M7) - Green key', 'The Great Hall (E2M7) - Yellow key', 'The Guard Tower (E1M4) - Green key', 'The Guard Tower (E1M4) - Yellow key', 'The Halls of Fear (E3M6) - Blue key', 'The Halls of Fear (E3M6) - Green key', 'The Halls of Fear (E3M6) - Yellow key', 'The Ice Grotto (E2M4) - Blue key', 'The Ice Grotto (E2M4) - Green key', 'The Ice Grotto (E2M4) - Yellow key', 'The Labyrinth (E2M6) - Blue key', 'The Labyrinth (E2M6) - Green key', 'The Labyrinth (E2M6) - Yellow key', 'The Lava Pits (E2M2) - Green key', 'The Lava Pits (E2M2) - Yellow key', 'The Ophidian Lair (E3M5) - Green key', 'The Ophidian Lair (E3M5) - Yellow key', 'The River of Fire (E2M3) - Blue key', 'The River of Fire (E2M3) - Green key', 'The River of Fire (E2M3) - Yellow key', 'The Storehouse (E3M1) - Green key', 'The Storehouse (E3M1) - Yellow key', },
|
||||
'Levels': {'Ambulatory (E4M3)', 'Blockhouse (E4M2)', 'Catafalque (E4M1)', 'Colonnade (E5M6)', 'Courtyard (E5M4)', "D'Sparil's Keep (E3M8)", 'Field of Judgement (E5M8)', 'Foetid Manse (E5M7)', 'Great Stair (E4M5)', 'Halls of the Apostate (E4M6)', "Hell's Maw (E1M8)", 'Hydratyr (E5M5)', 'Mausoleum (E4M9)', 'Ochre Cliffs (E5M1)', 'Quay (E5M3)', 'Ramparts of Perdition (E4M7)', 'Rapids (E5M2)', 'Sepulcher (E4M4)', 'Shattered Bridge (E4M8)', "Skein of D'Sparil (E5M9)", 'The Aquifer (E3M9)', 'The Azure Fortress (E3M4)', 'The Catacombs (E2M5)', 'The Cathedral (E1M6)', 'The Cesspool (E3M2)', 'The Chasm (E3M7)', 'The Citadel (E1M5)', 'The Confluence (E3M3)', 'The Crater (E2M1)', 'The Crypts (E1M7)', 'The Docks (E1M1)', 'The Dungeons (E1M2)', 'The Gatehouse (E1M3)', 'The Glacier (E2M9)', 'The Graveyard (E1M9)', 'The Great Hall (E2M7)', 'The Guard Tower (E1M4)', 'The Halls of Fear (E3M6)', 'The Ice Grotto (E2M4)', 'The Labyrinth (E2M6)', 'The Lava Pits (E2M2)', 'The Ophidian Lair (E3M5)', 'The Portals of Chaos (E2M8)', 'The River of Fire (E2M3)', 'The Storehouse (E3M1)', },
|
||||
'Map Scrolls': {'Ambulatory (E4M3) - Map Scroll', 'Blockhouse (E4M2) - Map Scroll', 'Catafalque (E4M1) - Map Scroll', 'Colonnade (E5M6) - Map Scroll', 'Courtyard (E5M4) - Map Scroll', "D'Sparil's Keep (E3M8) - Map Scroll", 'Field of Judgement (E5M8) - Map Scroll', 'Foetid Manse (E5M7) - Map Scroll', 'Great Stair (E4M5) - Map Scroll', 'Halls of the Apostate (E4M6) - Map Scroll', "Hell's Maw (E1M8) - Map Scroll", 'Hydratyr (E5M5) - Map Scroll', 'Mausoleum (E4M9) - Map Scroll', 'Ochre Cliffs (E5M1) - Map Scroll', 'Quay (E5M3) - Map Scroll', 'Ramparts of Perdition (E4M7) - Map Scroll', 'Rapids (E5M2) - Map Scroll', 'Sepulcher (E4M4) - Map Scroll', 'Shattered Bridge (E4M8) - Map Scroll', "Skein of D'Sparil (E5M9) - Map Scroll", 'The Aquifer (E3M9) - Map Scroll', 'The Azure Fortress (E3M4) - Map Scroll', 'The Catacombs (E2M5) - Map Scroll', 'The Cathedral (E1M6) - Map Scroll', 'The Cesspool (E3M2) - Map Scroll', 'The Chasm (E3M7) - Map Scroll', 'The Citadel (E1M5) - Map Scroll', 'The Confluence (E3M3) - Map Scroll', 'The Crater (E2M1) - Map Scroll', 'The Crypts (E1M7) - Map Scroll', 'The Docks (E1M1) - Map Scroll', 'The Dungeons (E1M2) - Map Scroll', 'The Gatehouse (E1M3) - Map Scroll', 'The Glacier (E2M9) - Map Scroll', 'The Graveyard (E1M9) - Map Scroll', 'The Great Hall (E2M7) - Map Scroll', 'The Guard Tower (E1M4) - Map Scroll', 'The Halls of Fear (E3M6) - Map Scroll', 'The Ice Grotto (E2M4) - Map Scroll', 'The Labyrinth (E2M6) - Map Scroll', 'The Lava Pits (E2M2) - Map Scroll', 'The Ophidian Lair (E3M5) - Map Scroll', 'The Portals of Chaos (E2M8) - Map Scroll', 'The River of Fire (E2M3) - Map Scroll', 'The Storehouse (E3M1) - Map Scroll', },
|
||||
'Weapons': {'Dragon Claw', 'Ethereal Crossbow', 'Firemace', 'Gauntlets of the Necromancer', 'Hellstaff', 'Phoenix Rod', },
|
||||
}
|
||||
|
||||
@@ -3633,300 +3633,300 @@ location_table: Dict[int, LocationDict] = {
|
||||
'index': -1,
|
||||
'doom_type': -1,
|
||||
'region': "The Chasm (E3M7) Blue"},
|
||||
371517: {'name': "D'Sparil'S Keep (E3M8) - Phoenix Rod",
|
||||
371517: {'name': "D'Sparil's Keep (E3M8) - Phoenix Rod",
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 8,
|
||||
'index': 55,
|
||||
'doom_type': 2003,
|
||||
'region': "D'Sparil'S Keep (E3M8) Main"},
|
||||
371518: {'name': "D'Sparil'S Keep (E3M8) - Ethereal Crossbow",
|
||||
'region': "D'Sparil's Keep (E3M8) Main"},
|
||||
371518: {'name': "D'Sparil's Keep (E3M8) - Ethereal Crossbow",
|
||||
'episode': 3,
|
||||
'check_sanity': True,
|
||||
'map': 8,
|
||||
'index': 56,
|
||||
'doom_type': 2001,
|
||||
'region': "D'Sparil'S Keep (E3M8) Main"},
|
||||
371519: {'name': "D'Sparil'S Keep (E3M8) - Dragon Claw",
|
||||
'region': "D'Sparil's Keep (E3M8) Main"},
|
||||
371519: {'name': "D'Sparil's Keep (E3M8) - Dragon Claw",
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 8,
|
||||
'index': 57,
|
||||
'doom_type': 53,
|
||||
'region': "D'Sparil'S Keep (E3M8) Main"},
|
||||
371520: {'name': "D'Sparil'S Keep (E3M8) - Gauntlets of the Necromancer",
|
||||
'region': "D'Sparil's Keep (E3M8) Main"},
|
||||
371520: {'name': "D'Sparil's Keep (E3M8) - Gauntlets of the Necromancer",
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 8,
|
||||
'index': 58,
|
||||
'doom_type': 2005,
|
||||
'region': "D'Sparil'S Keep (E3M8) Main"},
|
||||
371521: {'name': "D'Sparil'S Keep (E3M8) - Hellstaff",
|
||||
'region': "D'Sparil's Keep (E3M8) Main"},
|
||||
371521: {'name': "D'Sparil's Keep (E3M8) - Hellstaff",
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 8,
|
||||
'index': 59,
|
||||
'doom_type': 2004,
|
||||
'region': "D'Sparil'S Keep (E3M8) Main"},
|
||||
371522: {'name': "D'Sparil'S Keep (E3M8) - Bag of Holding",
|
||||
'region': "D'Sparil's Keep (E3M8) Main"},
|
||||
371522: {'name': "D'Sparil's Keep (E3M8) - Bag of Holding",
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 8,
|
||||
'index': 63,
|
||||
'doom_type': 8,
|
||||
'region': "D'Sparil'S Keep (E3M8) Main"},
|
||||
371523: {'name': "D'Sparil'S Keep (E3M8) - Mystic Urn",
|
||||
'region': "D'Sparil's Keep (E3M8) Main"},
|
||||
371523: {'name': "D'Sparil's Keep (E3M8) - Mystic Urn",
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 8,
|
||||
'index': 64,
|
||||
'doom_type': 32,
|
||||
'region': "D'Sparil'S Keep (E3M8) Main"},
|
||||
371524: {'name': "D'Sparil'S Keep (E3M8) - Ring of Invincibility",
|
||||
'region': "D'Sparil's Keep (E3M8) Main"},
|
||||
371524: {'name': "D'Sparil's Keep (E3M8) - Ring of Invincibility",
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 8,
|
||||
'index': 65,
|
||||
'doom_type': 84,
|
||||
'region': "D'Sparil'S Keep (E3M8) Main"},
|
||||
371525: {'name': "D'Sparil'S Keep (E3M8) - Shadowsphere",
|
||||
'region': "D'Sparil's Keep (E3M8) Main"},
|
||||
371525: {'name': "D'Sparil's Keep (E3M8) - Shadowsphere",
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 8,
|
||||
'index': 66,
|
||||
'doom_type': 75,
|
||||
'region': "D'Sparil'S Keep (E3M8) Main"},
|
||||
371526: {'name': "D'Sparil'S Keep (E3M8) - Silver Shield",
|
||||
'region': "D'Sparil's Keep (E3M8) Main"},
|
||||
371526: {'name': "D'Sparil's Keep (E3M8) - Silver Shield",
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 8,
|
||||
'index': 67,
|
||||
'doom_type': 85,
|
||||
'region': "D'Sparil'S Keep (E3M8) Main"},
|
||||
371527: {'name': "D'Sparil'S Keep (E3M8) - Enchanted Shield",
|
||||
'region': "D'Sparil's Keep (E3M8) Main"},
|
||||
371527: {'name': "D'Sparil's Keep (E3M8) - Enchanted Shield",
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 8,
|
||||
'index': 68,
|
||||
'doom_type': 31,
|
||||
'region': "D'Sparil'S Keep (E3M8) Main"},
|
||||
371528: {'name': "D'Sparil'S Keep (E3M8) - Tome of Power",
|
||||
'region': "D'Sparil's Keep (E3M8) Main"},
|
||||
371528: {'name': "D'Sparil's Keep (E3M8) - Tome of Power",
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 8,
|
||||
'index': 69,
|
||||
'doom_type': 86,
|
||||
'region': "D'Sparil'S Keep (E3M8) Main"},
|
||||
371529: {'name': "D'Sparil'S Keep (E3M8) - Tome of Power 2",
|
||||
'region': "D'Sparil's Keep (E3M8) Main"},
|
||||
371529: {'name': "D'Sparil's Keep (E3M8) - Tome of Power 2",
|
||||
'episode': 3,
|
||||
'check_sanity': True,
|
||||
'map': 8,
|
||||
'index': 70,
|
||||
'doom_type': 86,
|
||||
'region': "D'Sparil'S Keep (E3M8) Main"},
|
||||
371530: {'name': "D'Sparil'S Keep (E3M8) - Chaos Device",
|
||||
'region': "D'Sparil's Keep (E3M8) Main"},
|
||||
371530: {'name': "D'Sparil's Keep (E3M8) - Chaos Device",
|
||||
'episode': 3,
|
||||
'check_sanity': True,
|
||||
'map': 8,
|
||||
'index': 71,
|
||||
'doom_type': 36,
|
||||
'region': "D'Sparil'S Keep (E3M8) Main"},
|
||||
371531: {'name': "D'Sparil'S Keep (E3M8) - Tome of Power 3",
|
||||
'region': "D'Sparil's Keep (E3M8) Main"},
|
||||
371531: {'name': "D'Sparil's Keep (E3M8) - Tome of Power 3",
|
||||
'episode': 3,
|
||||
'check_sanity': True,
|
||||
'map': 8,
|
||||
'index': 245,
|
||||
'doom_type': 86,
|
||||
'region': "D'Sparil'S Keep (E3M8) Main"},
|
||||
371532: {'name': "D'Sparil'S Keep (E3M8) - Exit",
|
||||
'region': "D'Sparil's Keep (E3M8) Main"},
|
||||
371532: {'name': "D'Sparil's Keep (E3M8) - Exit",
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 8,
|
||||
'index': -1,
|
||||
'doom_type': -1,
|
||||
'region': "D'Sparil'S Keep (E3M8) Main"},
|
||||
371533: {'name': 'The Aquifier (E3M9) - Blue key',
|
||||
'region': "D'Sparil's Keep (E3M8) Main"},
|
||||
371533: {'name': 'The Aquifer (E3M9) - Blue key',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 12,
|
||||
'doom_type': 79,
|
||||
'region': "The Aquifier (E3M9) Green"},
|
||||
371534: {'name': 'The Aquifier (E3M9) - Green key',
|
||||
'region': "The Aquifer (E3M9) Green"},
|
||||
371534: {'name': 'The Aquifer (E3M9) - Green key',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 13,
|
||||
'doom_type': 73,
|
||||
'region': "The Aquifier (E3M9) Yellow"},
|
||||
371535: {'name': 'The Aquifier (E3M9) - Yellow key',
|
||||
'region': "The Aquifer (E3M9) Yellow"},
|
||||
371535: {'name': 'The Aquifer (E3M9) - Yellow key',
|
||||
'episode': 3,
|
||||
'check_sanity': True,
|
||||
'map': 9,
|
||||
'index': 14,
|
||||
'doom_type': 80,
|
||||
'region': "The Aquifier (E3M9) Main"},
|
||||
371536: {'name': 'The Aquifier (E3M9) - Ethereal Crossbow',
|
||||
'region': "The Aquifer (E3M9) Main"},
|
||||
371536: {'name': 'The Aquifer (E3M9) - Ethereal Crossbow',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 141,
|
||||
'doom_type': 2001,
|
||||
'region': "The Aquifier (E3M9) Main"},
|
||||
371537: {'name': 'The Aquifier (E3M9) - Phoenix Rod',
|
||||
'region': "The Aquifer (E3M9) Main"},
|
||||
371537: {'name': 'The Aquifer (E3M9) - Phoenix Rod',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 142,
|
||||
'doom_type': 2003,
|
||||
'region': "The Aquifier (E3M9) Yellow"},
|
||||
371538: {'name': 'The Aquifier (E3M9) - Dragon Claw',
|
||||
'region': "The Aquifer (E3M9) Yellow"},
|
||||
371538: {'name': 'The Aquifer (E3M9) - Dragon Claw',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 143,
|
||||
'doom_type': 53,
|
||||
'region': "The Aquifier (E3M9) Green"},
|
||||
371539: {'name': 'The Aquifier (E3M9) - Hellstaff',
|
||||
'region': "The Aquifer (E3M9) Green"},
|
||||
371539: {'name': 'The Aquifer (E3M9) - Hellstaff',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 144,
|
||||
'doom_type': 2004,
|
||||
'region': "The Aquifier (E3M9) Green"},
|
||||
371540: {'name': 'The Aquifier (E3M9) - Gauntlets of the Necromancer',
|
||||
'region': "The Aquifer (E3M9) Green"},
|
||||
371540: {'name': 'The Aquifer (E3M9) - Gauntlets of the Necromancer',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 145,
|
||||
'doom_type': 2005,
|
||||
'region': "The Aquifier (E3M9) Green"},
|
||||
371541: {'name': 'The Aquifier (E3M9) - Ring of Invincibility',
|
||||
'region': "The Aquifer (E3M9) Green"},
|
||||
371541: {'name': 'The Aquifer (E3M9) - Ring of Invincibility',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 148,
|
||||
'doom_type': 84,
|
||||
'region': "The Aquifier (E3M9) Yellow"},
|
||||
371542: {'name': 'The Aquifier (E3M9) - Mystic Urn',
|
||||
'region': "The Aquifer (E3M9) Yellow"},
|
||||
371542: {'name': 'The Aquifer (E3M9) - Mystic Urn',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 149,
|
||||
'doom_type': 32,
|
||||
'region': "The Aquifier (E3M9) Green"},
|
||||
371543: {'name': 'The Aquifier (E3M9) - Silver Shield',
|
||||
'region': "The Aquifer (E3M9) Green"},
|
||||
371543: {'name': 'The Aquifer (E3M9) - Silver Shield',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 151,
|
||||
'doom_type': 85,
|
||||
'region': "The Aquifier (E3M9) Main"},
|
||||
371544: {'name': 'The Aquifier (E3M9) - Tome of Power',
|
||||
'region': "The Aquifer (E3M9) Main"},
|
||||
371544: {'name': 'The Aquifer (E3M9) - Tome of Power',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 152,
|
||||
'doom_type': 86,
|
||||
'region': "The Aquifier (E3M9) Main"},
|
||||
371545: {'name': 'The Aquifier (E3M9) - Bag of Holding',
|
||||
'region': "The Aquifer (E3M9) Main"},
|
||||
371545: {'name': 'The Aquifer (E3M9) - Bag of Holding',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 153,
|
||||
'doom_type': 8,
|
||||
'region': "The Aquifier (E3M9) Yellow"},
|
||||
371546: {'name': 'The Aquifier (E3M9) - Morph Ovum',
|
||||
'region': "The Aquifer (E3M9) Yellow"},
|
||||
371546: {'name': 'The Aquifer (E3M9) - Morph Ovum',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 154,
|
||||
'doom_type': 30,
|
||||
'region': "The Aquifier (E3M9) Green"},
|
||||
371547: {'name': 'The Aquifier (E3M9) - Map Scroll',
|
||||
'region': "The Aquifer (E3M9) Green"},
|
||||
371547: {'name': 'The Aquifer (E3M9) - Map Scroll',
|
||||
'episode': 3,
|
||||
'check_sanity': True,
|
||||
'map': 9,
|
||||
'index': 155,
|
||||
'doom_type': 35,
|
||||
'region': "The Aquifier (E3M9) Green"},
|
||||
371548: {'name': 'The Aquifier (E3M9) - Chaos Device',
|
||||
'region': "The Aquifer (E3M9) Green"},
|
||||
371548: {'name': 'The Aquifer (E3M9) - Chaos Device',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 156,
|
||||
'doom_type': 36,
|
||||
'region': "The Aquifier (E3M9) Yellow"},
|
||||
371549: {'name': 'The Aquifier (E3M9) - Enchanted Shield',
|
||||
'region': "The Aquifer (E3M9) Yellow"},
|
||||
371549: {'name': 'The Aquifer (E3M9) - Enchanted Shield',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 157,
|
||||
'doom_type': 31,
|
||||
'region': "The Aquifier (E3M9) Green"},
|
||||
371550: {'name': 'The Aquifier (E3M9) - Tome of Power 2',
|
||||
'region': "The Aquifer (E3M9) Green"},
|
||||
371550: {'name': 'The Aquifer (E3M9) - Tome of Power 2',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 158,
|
||||
'doom_type': 86,
|
||||
'region': "The Aquifier (E3M9) Green"},
|
||||
371551: {'name': 'The Aquifier (E3M9) - Torch',
|
||||
'region': "The Aquifer (E3M9) Green"},
|
||||
371551: {'name': 'The Aquifer (E3M9) - Torch',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 159,
|
||||
'doom_type': 33,
|
||||
'region': "The Aquifier (E3M9) Main"},
|
||||
371552: {'name': 'The Aquifier (E3M9) - Shadowsphere',
|
||||
'region': "The Aquifer (E3M9) Main"},
|
||||
371552: {'name': 'The Aquifer (E3M9) - Shadowsphere',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 160,
|
||||
'doom_type': 75,
|
||||
'region': "The Aquifier (E3M9) Green"},
|
||||
371553: {'name': 'The Aquifier (E3M9) - Silver Shield 2',
|
||||
'region': "The Aquifer (E3M9) Green"},
|
||||
371553: {'name': 'The Aquifer (E3M9) - Silver Shield 2',
|
||||
'episode': 3,
|
||||
'check_sanity': True,
|
||||
'map': 9,
|
||||
'index': 374,
|
||||
'doom_type': 85,
|
||||
'region': "The Aquifier (E3M9) Green"},
|
||||
371554: {'name': 'The Aquifier (E3M9) - Firemace',
|
||||
'region': "The Aquifer (E3M9) Green"},
|
||||
371554: {'name': 'The Aquifer (E3M9) - Firemace',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 478,
|
||||
'doom_type': 2002,
|
||||
'region': "The Aquifier (E3M9) Green"},
|
||||
371555: {'name': 'The Aquifier (E3M9) - Firemace 2',
|
||||
'region': "The Aquifer (E3M9) Green"},
|
||||
371555: {'name': 'The Aquifer (E3M9) - Firemace 2',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 526,
|
||||
'doom_type': 2002,
|
||||
'region': "The Aquifier (E3M9) Green"},
|
||||
371556: {'name': 'The Aquifier (E3M9) - Firemace 3',
|
||||
'region': "The Aquifer (E3M9) Green"},
|
||||
371556: {'name': 'The Aquifer (E3M9) - Firemace 3',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': 527,
|
||||
'doom_type': 2002,
|
||||
'region': "The Aquifier (E3M9) Green"},
|
||||
371557: {'name': 'The Aquifier (E3M9) - Firemace 4',
|
||||
'region': "The Aquifer (E3M9) Green"},
|
||||
371557: {'name': 'The Aquifer (E3M9) - Firemace 4',
|
||||
'episode': 3,
|
||||
'check_sanity': True,
|
||||
'map': 9,
|
||||
'index': 528,
|
||||
'doom_type': 2002,
|
||||
'region': "The Aquifier (E3M9) Yellow"},
|
||||
371558: {'name': 'The Aquifier (E3M9) - Exit',
|
||||
'region': "The Aquifer (E3M9) Yellow"},
|
||||
371558: {'name': 'The Aquifer (E3M9) - Exit',
|
||||
'episode': 3,
|
||||
'check_sanity': False,
|
||||
'map': 9,
|
||||
'index': -1,
|
||||
'doom_type': -1,
|
||||
'region': "The Aquifier (E3M9) Blue"},
|
||||
'region': "The Aquifer (E3M9) Blue"},
|
||||
371559: {'name': 'Catafalque (E4M1) - Yellow key',
|
||||
'episode': 4,
|
||||
'check_sanity': False,
|
||||
@@ -5963,7 +5963,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 3,
|
||||
'index': 213,
|
||||
'doom_type': 2005,
|
||||
'region': "Quay (E5M3) Main"},
|
||||
'region': "Quay (E5M3) Blue"},
|
||||
371850: {'name': 'Quay (E5M3) - Dragon Claw',
|
||||
'episode': 5,
|
||||
'check_sanity': False,
|
||||
@@ -6145,7 +6145,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 4,
|
||||
'index': 3,
|
||||
'doom_type': 79,
|
||||
'region': "Courtyard (E5M4) Main"},
|
||||
'region': "Courtyard (E5M4) Green"},
|
||||
371876: {'name': 'Courtyard (E5M4) - Yellow key',
|
||||
'episode': 5,
|
||||
'check_sanity': False,
|
||||
@@ -6159,7 +6159,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 4,
|
||||
'index': 21,
|
||||
'doom_type': 73,
|
||||
'region': "Courtyard (E5M4) Kakis"},
|
||||
'region': "Courtyard (E5M4) Yellow"},
|
||||
371878: {'name': 'Courtyard (E5M4) - Gauntlets of the Necromancer',
|
||||
'episode': 5,
|
||||
'check_sanity': False,
|
||||
@@ -6187,14 +6187,14 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 4,
|
||||
'index': 87,
|
||||
'doom_type': 2004,
|
||||
'region': "Courtyard (E5M4) Kakis"},
|
||||
'region': "Courtyard (E5M4) Yellow"},
|
||||
371882: {'name': 'Courtyard (E5M4) - Phoenix Rod',
|
||||
'episode': 5,
|
||||
'check_sanity': False,
|
||||
'map': 4,
|
||||
'index': 88,
|
||||
'doom_type': 2003,
|
||||
'region': "Courtyard (E5M4) Main"},
|
||||
'region': "Courtyard (E5M4) Green"},
|
||||
371883: {'name': 'Courtyard (E5M4) - Morph Ovum',
|
||||
'episode': 5,
|
||||
'check_sanity': False,
|
||||
@@ -6229,7 +6229,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 4,
|
||||
'index': 104,
|
||||
'doom_type': 84,
|
||||
'region': "Courtyard (E5M4) Kakis"},
|
||||
'region': "Courtyard (E5M4) Yellow"},
|
||||
371888: {'name': 'Courtyard (E5M4) - Shadowsphere',
|
||||
'episode': 5,
|
||||
'check_sanity': False,
|
||||
@@ -6250,14 +6250,14 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 4,
|
||||
'index': 107,
|
||||
'doom_type': 35,
|
||||
'region': "Courtyard (E5M4) Kakis"},
|
||||
'region': "Courtyard (E5M4) Yellow"},
|
||||
371891: {'name': 'Courtyard (E5M4) - Chaos Device',
|
||||
'episode': 5,
|
||||
'check_sanity': False,
|
||||
'map': 4,
|
||||
'index': 108,
|
||||
'doom_type': 36,
|
||||
'region': "Courtyard (E5M4) Main"},
|
||||
'region': "Courtyard (E5M4) Green"},
|
||||
371892: {'name': 'Courtyard (E5M4) - Tome of Power',
|
||||
'episode': 5,
|
||||
'check_sanity': False,
|
||||
@@ -6278,7 +6278,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 4,
|
||||
'index': 111,
|
||||
'doom_type': 86,
|
||||
'region': "Courtyard (E5M4) Kakis"},
|
||||
'region': "Courtyard (E5M4) Yellow"},
|
||||
371895: {'name': 'Courtyard (E5M4) - Torch',
|
||||
'episode': 5,
|
||||
'check_sanity': False,
|
||||
@@ -6299,7 +6299,7 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 4,
|
||||
'index': 219,
|
||||
'doom_type': 85,
|
||||
'region': "Courtyard (E5M4) Kakis"},
|
||||
'region': "Courtyard (E5M4) Yellow"},
|
||||
371898: {'name': 'Courtyard (E5M4) - Bag of Holding 3',
|
||||
'episode': 5,
|
||||
'check_sanity': False,
|
||||
@@ -7247,23 +7247,23 @@ location_name_groups: Dict[str, Set[str]] = {
|
||||
'Courtyard (E5M4) - Torch',
|
||||
'Courtyard (E5M4) - Yellow key',
|
||||
},
|
||||
"D'Sparil'S Keep (E3M8)": {
|
||||
"D'Sparil'S Keep (E3M8) - Bag of Holding",
|
||||
"D'Sparil'S Keep (E3M8) - Chaos Device",
|
||||
"D'Sparil'S Keep (E3M8) - Dragon Claw",
|
||||
"D'Sparil'S Keep (E3M8) - Enchanted Shield",
|
||||
"D'Sparil'S Keep (E3M8) - Ethereal Crossbow",
|
||||
"D'Sparil'S Keep (E3M8) - Exit",
|
||||
"D'Sparil'S Keep (E3M8) - Gauntlets of the Necromancer",
|
||||
"D'Sparil'S Keep (E3M8) - Hellstaff",
|
||||
"D'Sparil'S Keep (E3M8) - Mystic Urn",
|
||||
"D'Sparil'S Keep (E3M8) - Phoenix Rod",
|
||||
"D'Sparil'S Keep (E3M8) - Ring of Invincibility",
|
||||
"D'Sparil'S Keep (E3M8) - Shadowsphere",
|
||||
"D'Sparil'S Keep (E3M8) - Silver Shield",
|
||||
"D'Sparil'S Keep (E3M8) - Tome of Power",
|
||||
"D'Sparil'S Keep (E3M8) - Tome of Power 2",
|
||||
"D'Sparil'S Keep (E3M8) - Tome of Power 3",
|
||||
"D'Sparil's Keep (E3M8)": {
|
||||
"D'Sparil's Keep (E3M8) - Bag of Holding",
|
||||
"D'Sparil's Keep (E3M8) - Chaos Device",
|
||||
"D'Sparil's Keep (E3M8) - Dragon Claw",
|
||||
"D'Sparil's Keep (E3M8) - Enchanted Shield",
|
||||
"D'Sparil's Keep (E3M8) - Ethereal Crossbow",
|
||||
"D'Sparil's Keep (E3M8) - Exit",
|
||||
"D'Sparil's Keep (E3M8) - Gauntlets of the Necromancer",
|
||||
"D'Sparil's Keep (E3M8) - Hellstaff",
|
||||
"D'Sparil's Keep (E3M8) - Mystic Urn",
|
||||
"D'Sparil's Keep (E3M8) - Phoenix Rod",
|
||||
"D'Sparil's Keep (E3M8) - Ring of Invincibility",
|
||||
"D'Sparil's Keep (E3M8) - Shadowsphere",
|
||||
"D'Sparil's Keep (E3M8) - Silver Shield",
|
||||
"D'Sparil's Keep (E3M8) - Tome of Power",
|
||||
"D'Sparil's Keep (E3M8) - Tome of Power 2",
|
||||
"D'Sparil's Keep (E3M8) - Tome of Power 3",
|
||||
},
|
||||
'Field of Judgement (E5M8)': {
|
||||
'Field of Judgement (E5M8) - Bag of Holding',
|
||||
@@ -7641,33 +7641,33 @@ location_name_groups: Dict[str, Set[str]] = {
|
||||
"Skein of D'Sparil (E5M9) - Torch",
|
||||
"Skein of D'Sparil (E5M9) - Yellow key",
|
||||
},
|
||||
'The Aquifier (E3M9)': {
|
||||
'The Aquifier (E3M9) - Bag of Holding',
|
||||
'The Aquifier (E3M9) - Blue key',
|
||||
'The Aquifier (E3M9) - Chaos Device',
|
||||
'The Aquifier (E3M9) - Dragon Claw',
|
||||
'The Aquifier (E3M9) - Enchanted Shield',
|
||||
'The Aquifier (E3M9) - Ethereal Crossbow',
|
||||
'The Aquifier (E3M9) - Exit',
|
||||
'The Aquifier (E3M9) - Firemace',
|
||||
'The Aquifier (E3M9) - Firemace 2',
|
||||
'The Aquifier (E3M9) - Firemace 3',
|
||||
'The Aquifier (E3M9) - Firemace 4',
|
||||
'The Aquifier (E3M9) - Gauntlets of the Necromancer',
|
||||
'The Aquifier (E3M9) - Green key',
|
||||
'The Aquifier (E3M9) - Hellstaff',
|
||||
'The Aquifier (E3M9) - Map Scroll',
|
||||
'The Aquifier (E3M9) - Morph Ovum',
|
||||
'The Aquifier (E3M9) - Mystic Urn',
|
||||
'The Aquifier (E3M9) - Phoenix Rod',
|
||||
'The Aquifier (E3M9) - Ring of Invincibility',
|
||||
'The Aquifier (E3M9) - Shadowsphere',
|
||||
'The Aquifier (E3M9) - Silver Shield',
|
||||
'The Aquifier (E3M9) - Silver Shield 2',
|
||||
'The Aquifier (E3M9) - Tome of Power',
|
||||
'The Aquifier (E3M9) - Tome of Power 2',
|
||||
'The Aquifier (E3M9) - Torch',
|
||||
'The Aquifier (E3M9) - Yellow key',
|
||||
'The Aquifer (E3M9)': {
|
||||
'The Aquifer (E3M9) - Bag of Holding',
|
||||
'The Aquifer (E3M9) - Blue key',
|
||||
'The Aquifer (E3M9) - Chaos Device',
|
||||
'The Aquifer (E3M9) - Dragon Claw',
|
||||
'The Aquifer (E3M9) - Enchanted Shield',
|
||||
'The Aquifer (E3M9) - Ethereal Crossbow',
|
||||
'The Aquifer (E3M9) - Exit',
|
||||
'The Aquifer (E3M9) - Firemace',
|
||||
'The Aquifer (E3M9) - Firemace 2',
|
||||
'The Aquifer (E3M9) - Firemace 3',
|
||||
'The Aquifer (E3M9) - Firemace 4',
|
||||
'The Aquifer (E3M9) - Gauntlets of the Necromancer',
|
||||
'The Aquifer (E3M9) - Green key',
|
||||
'The Aquifer (E3M9) - Hellstaff',
|
||||
'The Aquifer (E3M9) - Map Scroll',
|
||||
'The Aquifer (E3M9) - Morph Ovum',
|
||||
'The Aquifer (E3M9) - Mystic Urn',
|
||||
'The Aquifer (E3M9) - Phoenix Rod',
|
||||
'The Aquifer (E3M9) - Ring of Invincibility',
|
||||
'The Aquifer (E3M9) - Shadowsphere',
|
||||
'The Aquifer (E3M9) - Silver Shield',
|
||||
'The Aquifer (E3M9) - Silver Shield 2',
|
||||
'The Aquifer (E3M9) - Tome of Power',
|
||||
'The Aquifer (E3M9) - Tome of Power 2',
|
||||
'The Aquifer (E3M9) - Torch',
|
||||
'The Aquifer (E3M9) - Yellow key',
|
||||
},
|
||||
'The Azure Fortress (E3M4)': {
|
||||
'The Azure Fortress (E3M4) - Bag of Holding',
|
||||
|
||||
@@ -29,8 +29,8 @@ map_names: List[str] = [
|
||||
'The Ophidian Lair (E3M5)',
|
||||
'The Halls of Fear (E3M6)',
|
||||
'The Chasm (E3M7)',
|
||||
"D'Sparil'S Keep (E3M8)",
|
||||
'The Aquifier (E3M9)',
|
||||
"D'Sparil's Keep (E3M8)",
|
||||
'The Aquifer (E3M9)',
|
||||
'Catafalque (E4M1)',
|
||||
'Blockhouse (E4M2)',
|
||||
'Ambulatory (E4M3)',
|
||||
|
||||
@@ -520,34 +520,34 @@ regions:List[RegionDict] = [
|
||||
"episode":3,
|
||||
"connections":[{"target":"The Chasm (E3M7) Yellow","pro":False}]},
|
||||
|
||||
# D'Sparil'S Keep (E3M8)
|
||||
{"name":"D'Sparil'S Keep (E3M8) Main",
|
||||
# D'Sparil's Keep (E3M8)
|
||||
{"name":"D'Sparil's Keep (E3M8) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":3,
|
||||
"connections":[]},
|
||||
|
||||
# The Aquifier (E3M9)
|
||||
{"name":"The Aquifier (E3M9) Main",
|
||||
# The Aquifer (E3M9)
|
||||
{"name":"The Aquifer (E3M9) Main",
|
||||
"connects_to_hub":True,
|
||||
"episode":3,
|
||||
"connections":[{"target":"The Aquifier (E3M9) Yellow","pro":False}]},
|
||||
{"name":"The Aquifier (E3M9) Blue",
|
||||
"connections":[{"target":"The Aquifer (E3M9) Yellow","pro":False}]},
|
||||
{"name":"The Aquifer (E3M9) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[]},
|
||||
{"name":"The Aquifier (E3M9) Yellow",
|
||||
{"name":"The Aquifer (E3M9) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[
|
||||
{"target":"The Aquifier (E3M9) Green","pro":False},
|
||||
{"target":"The Aquifier (E3M9) Main","pro":False}]},
|
||||
{"name":"The Aquifier (E3M9) Green",
|
||||
{"target":"The Aquifer (E3M9) Green","pro":False},
|
||||
{"target":"The Aquifer (E3M9) Main","pro":False}]},
|
||||
{"name":"The Aquifer (E3M9) Green",
|
||||
"connects_to_hub":False,
|
||||
"episode":3,
|
||||
"connections":[
|
||||
{"target":"The Aquifier (E3M9) Yellow","pro":False},
|
||||
{"target":"The Aquifier (E3M9) Main","pro":False},
|
||||
{"target":"The Aquifier (E3M9) Blue","pro":False}]},
|
||||
{"target":"The Aquifer (E3M9) Yellow","pro":False},
|
||||
{"target":"The Aquifer (E3M9) Main","pro":False},
|
||||
{"target":"The Aquifer (E3M9) Blue","pro":False}]},
|
||||
|
||||
# Catafalque (E4M1)
|
||||
{"name":"Catafalque (E4M1) Main",
|
||||
@@ -795,16 +795,22 @@ regions:List[RegionDict] = [
|
||||
"connects_to_hub":True,
|
||||
"episode":5,
|
||||
"connections":[
|
||||
{"target":"Courtyard (E5M4) Kakis","pro":False},
|
||||
{"target":"Courtyard (E5M4) Yellow","pro":False},
|
||||
{"target":"Courtyard (E5M4) Blue","pro":False}]},
|
||||
{"name":"Courtyard (E5M4) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":5,
|
||||
"connections":[{"target":"Courtyard (E5M4) Main","pro":False}]},
|
||||
{"name":"Courtyard (E5M4) Kakis",
|
||||
{"name":"Courtyard (E5M4) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":5,
|
||||
"connections":[{"target":"Courtyard (E5M4) Main","pro":False}]},
|
||||
"connections":[
|
||||
{"target":"Courtyard (E5M4) Main","pro":False},
|
||||
{"target":"Courtyard (E5M4) Green","pro":False}]},
|
||||
{"name":"Courtyard (E5M4) Green",
|
||||
"connects_to_hub":False,
|
||||
"episode":5,
|
||||
"connections":[{"target":"Courtyard (E5M4) Yellow","pro":False}]},
|
||||
|
||||
# Hydratyr (E5M5)
|
||||
{"name":"Hydratyr (E5M5) Main",
|
||||
|
||||
@@ -388,9 +388,9 @@ def set_episode3_rules(player, multiworld, pro):
|
||||
set_rule(multiworld.get_entrance("The Chasm (E3M7) Green -> The Chasm (E3M7) Yellow", player), lambda state:
|
||||
state.has("The Chasm (E3M7) - Green key", player, 1))
|
||||
|
||||
# D'Sparil'S Keep (E3M8)
|
||||
set_rule(multiworld.get_entrance("Hub -> D'Sparil'S Keep (E3M8) Main", player), lambda state:
|
||||
state.has("D'Sparil'S Keep (E3M8)", player, 1) and
|
||||
# D'Sparil's Keep (E3M8)
|
||||
set_rule(multiworld.get_entrance("Hub -> D'Sparil's Keep (E3M8) Main", player), lambda state:
|
||||
state.has("D'Sparil's Keep (E3M8)", player, 1) and
|
||||
state.has("Gauntlets of the Necromancer", player, 1) and
|
||||
state.has("Ethereal Crossbow", player, 1) and
|
||||
state.has("Dragon Claw", player, 1) and
|
||||
@@ -398,23 +398,23 @@ def set_episode3_rules(player, multiworld, pro):
|
||||
state.has("Firemace", player, 1) and
|
||||
state.has("Hellstaff", player, 1))
|
||||
|
||||
# The Aquifier (E3M9)
|
||||
set_rule(multiworld.get_entrance("Hub -> The Aquifier (E3M9) Main", player), lambda state:
|
||||
state.has("The Aquifier (E3M9)", player, 1) and
|
||||
# The Aquifer (E3M9)
|
||||
set_rule(multiworld.get_entrance("Hub -> The Aquifer (E3M9) Main", player), lambda state:
|
||||
state.has("The Aquifer (E3M9)", player, 1) and
|
||||
state.has("Gauntlets of the Necromancer", player, 1) and
|
||||
state.has("Ethereal Crossbow", player, 1) and
|
||||
state.has("Dragon Claw", player, 1) and
|
||||
state.has("Phoenix Rod", player, 1) and
|
||||
state.has("Firemace", player, 1) and
|
||||
state.has("Hellstaff", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Aquifier (E3M9) Main -> The Aquifier (E3M9) Yellow", player), lambda state:
|
||||
state.has("The Aquifier (E3M9) - Yellow key", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Aquifier (E3M9) Yellow -> The Aquifier (E3M9) Green", player), lambda state:
|
||||
state.has("The Aquifier (E3M9) - Green key", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Aquifier (E3M9) Yellow -> The Aquifier (E3M9) Main", player), lambda state:
|
||||
state.has("The Aquifier (E3M9) - Yellow key", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Aquifier (E3M9) Green -> The Aquifier (E3M9) Yellow", player), lambda state:
|
||||
state.has("The Aquifier (E3M9) - Green key", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Aquifer (E3M9) Main -> The Aquifer (E3M9) Yellow", player), lambda state:
|
||||
state.has("The Aquifer (E3M9) - Yellow key", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Aquifer (E3M9) Yellow -> The Aquifer (E3M9) Green", player), lambda state:
|
||||
state.has("The Aquifer (E3M9) - Green key", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Aquifer (E3M9) Yellow -> The Aquifer (E3M9) Main", player), lambda state:
|
||||
state.has("The Aquifer (E3M9) - Yellow key", player, 1))
|
||||
set_rule(multiworld.get_entrance("The Aquifer (E3M9) Green -> The Aquifer (E3M9) Yellow", player), lambda state:
|
||||
state.has("The Aquifer (E3M9) - Green key", player, 1))
|
||||
|
||||
|
||||
def set_episode4_rules(player, multiworld, pro):
|
||||
@@ -623,15 +623,17 @@ def set_episode5_rules(player, multiworld, pro):
|
||||
(state.has("Phoenix Rod", player, 1) or
|
||||
state.has("Firemace", player, 1) or
|
||||
state.has("Hellstaff", player, 1)))
|
||||
set_rule(multiworld.get_entrance("Courtyard (E5M4) Main -> Courtyard (E5M4) Kakis", player), lambda state:
|
||||
state.has("Courtyard (E5M4) - Yellow key", player, 1) or
|
||||
state.has("Courtyard (E5M4) - Green key", player, 1))
|
||||
set_rule(multiworld.get_entrance("Courtyard (E5M4) Main -> Courtyard (E5M4) Yellow", player), lambda state:
|
||||
state.has("Courtyard (E5M4) - Yellow key", player, 1))
|
||||
set_rule(multiworld.get_entrance("Courtyard (E5M4) Main -> Courtyard (E5M4) Blue", player), lambda state:
|
||||
state.has("Courtyard (E5M4) - Blue key", player, 1))
|
||||
set_rule(multiworld.get_entrance("Courtyard (E5M4) Blue -> Courtyard (E5M4) Main", player), lambda state:
|
||||
state.has("Courtyard (E5M4) - Blue key", player, 1))
|
||||
set_rule(multiworld.get_entrance("Courtyard (E5M4) Kakis -> Courtyard (E5M4) Main", player), lambda state:
|
||||
state.has("Courtyard (E5M4) - Yellow key", player, 1) or
|
||||
set_rule(multiworld.get_entrance("Courtyard (E5M4) Yellow -> Courtyard (E5M4) Main", player), lambda state:
|
||||
state.has("Courtyard (E5M4) - Yellow key", player, 1))
|
||||
set_rule(multiworld.get_entrance("Courtyard (E5M4) Yellow -> Courtyard (E5M4) Green", player), lambda state:
|
||||
state.has("Courtyard (E5M4) - Green key", player, 1))
|
||||
set_rule(multiworld.get_entrance("Courtyard (E5M4) Green -> Courtyard (E5M4) Yellow", player), lambda state:
|
||||
state.has("Courtyard (E5M4) - Green key", player, 1))
|
||||
|
||||
# Hydratyr (E5M5)
|
||||
|
||||
@@ -49,18 +49,18 @@ class HereticWorld(World):
|
||||
location_name_to_id = {data["name"]: loc_id for loc_id, data in Locations.location_table.items()}
|
||||
location_name_groups = Locations.location_name_groups
|
||||
|
||||
starting_level_for_episode: List[str] = [
|
||||
"The Docks (E1M1)",
|
||||
"The Crater (E2M1)",
|
||||
"The Storehouse (E3M1)",
|
||||
"Catafalque (E4M1)",
|
||||
"Ochre Cliffs (E5M1)"
|
||||
]
|
||||
starting_level_for_episode: Dict[int, str] = {
|
||||
1: "The Docks (E1M1)",
|
||||
2: "The Crater (E2M1)",
|
||||
3: "The Storehouse (E3M1)",
|
||||
4: "Catafalque (E4M1)",
|
||||
5: "Ochre Cliffs (E5M1)"
|
||||
}
|
||||
|
||||
boss_level_for_episode: List[str] = [
|
||||
all_boss_levels: List[str] = [
|
||||
"Hell's Maw (E1M8)",
|
||||
"The Portals of Chaos (E2M8)",
|
||||
"D'Sparil'S Keep (E3M8)",
|
||||
"D'Sparil's Keep (E3M8)",
|
||||
"Shattered Bridge (E4M8)",
|
||||
"Field of Judgement (E5M8)"
|
||||
]
|
||||
@@ -82,6 +82,7 @@ class HereticWorld(World):
|
||||
def __init__(self, multiworld: MultiWorld, player: int):
|
||||
self.included_episodes = [1, 1, 1, 0, 0]
|
||||
self.location_count = 0
|
||||
self.starting_levels = []
|
||||
|
||||
super().__init__(multiworld, player)
|
||||
|
||||
@@ -100,6 +101,14 @@ class HereticWorld(World):
|
||||
if self.get_episode_count() == 0:
|
||||
self.included_episodes[0] = 1
|
||||
|
||||
self.starting_levels = [level_name for (episode, level_name) in self.starting_level_for_episode.items()
|
||||
if self.included_episodes[episode - 1]]
|
||||
|
||||
# For Solo Episode 1, place the Yellow Key for E1M1 early.
|
||||
# Gives the generator five potential placements (plus the forced key) instead of only two.
|
||||
if self.get_episode_count() == 1 and self.included_episodes[0]:
|
||||
self.multiworld.early_items[self.player]["The Docks (E1M1) - Yellow key"] = 1
|
||||
|
||||
def create_regions(self):
|
||||
pro = self.options.pro.value
|
||||
check_sanity = self.options.check_sanity.value
|
||||
@@ -154,7 +163,7 @@ class HereticWorld(World):
|
||||
def completion_rule(self, state: CollectionState):
|
||||
goal_levels = Maps.map_names
|
||||
if self.options.goal.value:
|
||||
goal_levels = self.boss_level_for_episode
|
||||
goal_levels = self.all_boss_levels
|
||||
|
||||
for map_name in goal_levels:
|
||||
if map_name + " - Exit" not in self.location_name_to_id:
|
||||
@@ -203,7 +212,7 @@ class HereticWorld(World):
|
||||
if item["episode"] != -1 and not self.included_episodes[item["episode"] - 1]:
|
||||
continue
|
||||
|
||||
count = item["count"] if item["name"] not in self.starting_level_for_episode else item["count"] - 1
|
||||
count = item["count"] if item["name"] not in self.starting_levels else item["count"] - 1
|
||||
itempool += [self.create_item(item["name"]) for _ in range(count)]
|
||||
|
||||
# Bag(s) of Holding based on options
|
||||
@@ -236,9 +245,8 @@ class HereticWorld(World):
|
||||
self.location_count -= 1
|
||||
|
||||
# Give starting levels right away
|
||||
for i in range(len(self.included_episodes)):
|
||||
if self.included_episodes[i]:
|
||||
self.multiworld.push_precollected(self.create_item(self.starting_level_for_episode[i]))
|
||||
for map_name in self.starting_levels:
|
||||
self.multiworld.push_precollected(self.create_item(map_name))
|
||||
|
||||
# Give Computer area maps if option selected
|
||||
if self.options.start_with_map_scrolls.value:
|
||||
|
||||
@@ -1 +1 @@
|
||||
Pymem>=1.10.0
|
||||
Pymem>=1.10.0
|
||||
|
||||
@@ -310,7 +310,8 @@ class LinksAwakeningWorld(World):
|
||||
|
||||
def opens_new_regions(item):
|
||||
collection_state = base_collection_state.copy()
|
||||
collection_state.collect(item)
|
||||
collection_state.collect(item, prevent_sweep=True)
|
||||
collection_state.sweep_for_advancements(self.get_locations())
|
||||
return len(collection_state.reachable_regions[self.player]) > reachable_count
|
||||
|
||||
start_items = [item for item in itempool if is_possible_start_item(item)]
|
||||
@@ -329,7 +330,7 @@ class LinksAwakeningWorld(World):
|
||||
if entrance_mapping['start_house'] not in ['start_house', 'shop']:
|
||||
start_items = [item for item in start_items if item.name != 'Shovel']
|
||||
base_collection_state = CollectionState(self.multiworld)
|
||||
base_collection_state.update_reachable_regions(self.player)
|
||||
base_collection_state.sweep_for_advancements(self.get_locations())
|
||||
reachable_count = len(base_collection_state.reachable_regions[self.player])
|
||||
start_item = next((item for item in start_items if opens_new_regions(item)), None)
|
||||
|
||||
|
||||
@@ -100,6 +100,8 @@
|
||||
# paintings is an array of paintings in the room. This is used for painting
|
||||
# shuffling.
|
||||
# - id: The internal painting ID from the LINGO map.
|
||||
# - display_name: The name of the painting location when showed in the
|
||||
# tracker. Not needed for disabled paintings.
|
||||
# - enter_only: If true, painting shuffling will not place a warp exit on
|
||||
# this painting.
|
||||
# - exit_only: If true, painting shuffling will not place a warp entrance
|
||||
@@ -226,6 +228,7 @@
|
||||
- HIDDEN
|
||||
paintings:
|
||||
- id: arrows_painting
|
||||
display_name: Overhead Painting
|
||||
exit_only: True
|
||||
orientation: south
|
||||
- id: arrows_painting2
|
||||
@@ -234,7 +237,24 @@
|
||||
- id: arrows_painting3
|
||||
disable: True
|
||||
move: True
|
||||
- id: symmetry_painting_a_starter
|
||||
display_name: Left Near Painting
|
||||
enter_only: True
|
||||
orientation: west
|
||||
move: True
|
||||
required_door:
|
||||
room: The Wondrous (Doorknob)
|
||||
door: Painting Shortcut
|
||||
- id: eyes_yellow_painting2
|
||||
display_name: Left Far Painting
|
||||
enter_only: True
|
||||
orientation: west
|
||||
move: True
|
||||
required_door:
|
||||
room: Outside The Agreeable
|
||||
door: Painting Shortcut
|
||||
- id: garden_painting_tower2
|
||||
display_name: Front Left Painting
|
||||
enter_only: True
|
||||
orientation: north
|
||||
move: True
|
||||
@@ -242,20 +262,15 @@
|
||||
room: Hedge Maze
|
||||
door: Painting Shortcut
|
||||
- id: flower_painting_8
|
||||
display_name: Front Right Painting
|
||||
enter_only: True
|
||||
orientation: north
|
||||
move: True
|
||||
required_door:
|
||||
room: Courtyard
|
||||
door: Painting Shortcut
|
||||
- id: symmetry_painting_a_starter
|
||||
enter_only: True
|
||||
orientation: west
|
||||
move: True
|
||||
required_door:
|
||||
room: The Wondrous (Doorknob)
|
||||
door: Painting Shortcut
|
||||
- id: pencil_painting6
|
||||
display_name: Right Far Painting
|
||||
enter_only: True
|
||||
orientation: east
|
||||
move: True
|
||||
@@ -263,19 +278,13 @@
|
||||
room: Outside The Bold
|
||||
door: Painting Shortcut
|
||||
- id: blueman_painting_3
|
||||
display_name: Right Near Painting
|
||||
enter_only: True
|
||||
orientation: east
|
||||
move: True
|
||||
required_door:
|
||||
room: Outside The Undeterred
|
||||
door: Painting Shortcut
|
||||
- id: eyes_yellow_painting2
|
||||
enter_only: True
|
||||
orientation: west
|
||||
move: True
|
||||
required_door:
|
||||
room: Outside The Agreeable
|
||||
door: Painting Shortcut
|
||||
Hidden Room:
|
||||
entrances:
|
||||
Starting Room:
|
||||
@@ -340,6 +349,7 @@
|
||||
- OPEN
|
||||
paintings:
|
||||
- id: owl_painting
|
||||
display_name: Painting
|
||||
orientation: north
|
||||
The Seeker:
|
||||
entrances:
|
||||
@@ -599,6 +609,7 @@
|
||||
- OPEN
|
||||
paintings:
|
||||
- id: maze_painting
|
||||
display_name: Near Traveled Painting
|
||||
orientation: west
|
||||
sunwarps:
|
||||
- dots: 1
|
||||
@@ -630,6 +641,7 @@
|
||||
door: Eights
|
||||
paintings:
|
||||
- id: smile_painting_6
|
||||
display_name: Painting
|
||||
orientation: north
|
||||
Sunwarps:
|
||||
# This is a special, meta-ish room.
|
||||
@@ -968,6 +980,7 @@
|
||||
required_door:
|
||||
door: Eye Wall
|
||||
- id: smile_painting_4
|
||||
display_name: Near Discerning Painting
|
||||
orientation: south
|
||||
sunwarps:
|
||||
- dots: 1
|
||||
@@ -1068,6 +1081,7 @@
|
||||
tag: midwhite
|
||||
paintings:
|
||||
- id: west_afar
|
||||
display_name: Painting
|
||||
orientation: south
|
||||
The Tenacious:
|
||||
entrances:
|
||||
@@ -1392,6 +1406,7 @@
|
||||
- RIGHT
|
||||
paintings:
|
||||
- id: eyes_yellow_painting
|
||||
display_name: Near Hallway Painting
|
||||
orientation: east
|
||||
sunwarps:
|
||||
- dots: 6
|
||||
@@ -1451,6 +1466,7 @@
|
||||
- FIRE
|
||||
paintings:
|
||||
- id: pencil_painting7
|
||||
display_name: Compass Room Painting
|
||||
orientation: north
|
||||
Dread Hallway:
|
||||
entrances:
|
||||
@@ -1698,6 +1714,7 @@
|
||||
- GAZE
|
||||
paintings:
|
||||
- id: garden_painting_tower
|
||||
display_name: Painting
|
||||
orientation: north
|
||||
The Fearless (First Floor):
|
||||
entrances:
|
||||
@@ -2077,6 +2094,7 @@
|
||||
panel: A
|
||||
paintings:
|
||||
- id: crown_painting
|
||||
display_name: Near Achievement Painting
|
||||
orientation: east
|
||||
Eight Alcove:
|
||||
entrances:
|
||||
@@ -2088,6 +2106,7 @@
|
||||
door: Eight Door (Outside The Initiated)
|
||||
paintings:
|
||||
- id: eight_painting2
|
||||
display_name: Eight Alcove Painting
|
||||
orientation: north
|
||||
Eight Room:
|
||||
entrances:
|
||||
@@ -2108,6 +2127,7 @@
|
||||
tag: forbid
|
||||
paintings:
|
||||
- id: eight_painting
|
||||
display_name: Eight Room Painting
|
||||
orientation: south
|
||||
exit_only: True
|
||||
required: True
|
||||
@@ -2340,8 +2360,10 @@
|
||||
panel: YELLOW
|
||||
paintings:
|
||||
- id: arrows_painting_6
|
||||
display_name: Left Painting
|
||||
orientation: east
|
||||
- id: flower_painting_5
|
||||
display_name: Right Painting
|
||||
orientation: south
|
||||
sunwarps:
|
||||
- dots: 2
|
||||
@@ -2430,6 +2452,7 @@
|
||||
door: Eights
|
||||
paintings:
|
||||
- id: smile_painting_8
|
||||
display_name: Hot Crusts Painting
|
||||
orientation: north
|
||||
sunwarps:
|
||||
- dots: 2
|
||||
@@ -2531,10 +2554,13 @@
|
||||
- SIZE (Big)
|
||||
paintings:
|
||||
- id: hi_solved_painting3
|
||||
display_name: Cellar Replica Painting
|
||||
orientation: south
|
||||
- id: hi_solved_painting2
|
||||
display_name: Cellar Painting
|
||||
orientation: south
|
||||
- id: east_afar
|
||||
display_name: Seasons Area Painting
|
||||
orientation: north
|
||||
Orange Tower Sixth Floor:
|
||||
entrances:
|
||||
@@ -2546,25 +2572,35 @@
|
||||
painting: True
|
||||
paintings:
|
||||
- id: arrows_painting_10
|
||||
display_name: Back Left Painting
|
||||
orientation: east
|
||||
- id: owl_painting_3
|
||||
orientation: north
|
||||
- id: clock_painting
|
||||
orientation: west
|
||||
- id: scenery_painting_5d_2
|
||||
display_name: Left Near Painting
|
||||
orientation: south
|
||||
- id: symmetry_painting_b_7
|
||||
orientation: north
|
||||
- id: panda_painting_2
|
||||
display_name: Left Middle Painting
|
||||
orientation: south
|
||||
- id: crown_painting2
|
||||
orientation: north
|
||||
- id: colors_painting2
|
||||
display_name: Left Far Painting
|
||||
orientation: south
|
||||
- id: cherry_painting2
|
||||
orientation: east
|
||||
- id: hi_solved_painting
|
||||
- id: clock_painting
|
||||
display_name: Front Left Painting
|
||||
orientation: west
|
||||
- id: hi_solved_painting
|
||||
display_name: Front Right Painting
|
||||
orientation: west
|
||||
- id: crown_painting2
|
||||
display_name: Right Far Painting
|
||||
orientation: north
|
||||
- id: owl_painting_3
|
||||
display_name: Right Middle Painting
|
||||
orientation: north
|
||||
- id: symmetry_painting_b_7
|
||||
display_name: Right Near Painting
|
||||
orientation: north
|
||||
- id: cherry_painting2
|
||||
display_name: Back Right Painting
|
||||
orientation: east
|
||||
Ending Area:
|
||||
entrances:
|
||||
Orange Tower Sixth Floor:
|
||||
@@ -2660,6 +2696,7 @@
|
||||
panel: MASTERY
|
||||
paintings:
|
||||
- id: map_painting2
|
||||
display_name: Painting
|
||||
orientation: north
|
||||
enter_only: True # otherwise you might just skip the whole game!
|
||||
req_blocked_when_no_doors: True # owl hallway in vanilla doors
|
||||
@@ -2755,6 +2792,7 @@
|
||||
non_counting: True
|
||||
paintings:
|
||||
- id: arrows_painting_11
|
||||
display_name: Painting
|
||||
orientation: east
|
||||
req_blocked_when_no_doors: True # owl hallway in vanilla doors
|
||||
Courtyard:
|
||||
@@ -2817,6 +2855,7 @@
|
||||
panel: GREEN
|
||||
paintings:
|
||||
- id: flower_painting_7
|
||||
display_name: Courtyard Painting
|
||||
orientation: north
|
||||
Yellow Backside Area:
|
||||
entrances:
|
||||
@@ -2838,6 +2877,7 @@
|
||||
door: Nines
|
||||
paintings:
|
||||
- id: blueman_painting
|
||||
display_name: Near Nine Painting
|
||||
orientation: east
|
||||
First Second Third Fourth:
|
||||
# We are separating this door + its panels into its own room because they
|
||||
@@ -3173,6 +3213,7 @@
|
||||
achievement: The Colorful
|
||||
paintings:
|
||||
- id: arrows_painting_12
|
||||
display_name: Painting
|
||||
orientation: north
|
||||
progression:
|
||||
Progressive Colorful:
|
||||
@@ -3296,13 +3337,17 @@
|
||||
- STRAYS
|
||||
paintings:
|
||||
- id: arrows_painting_8
|
||||
display_name: Near Maze Painting
|
||||
orientation: south
|
||||
- id: maze_painting_2
|
||||
display_name: Maze Side Middle Painting
|
||||
orientation: north
|
||||
- id: owl_painting_2
|
||||
display_name: Orange Side Middle Painting
|
||||
orientation: south
|
||||
required_when_no_doors: True
|
||||
- id: clock_painting_4
|
||||
display_name: Near Orange Painting
|
||||
orientation: north
|
||||
Outside The Initiated:
|
||||
entrances:
|
||||
@@ -3490,8 +3535,10 @@
|
||||
- OXEN
|
||||
paintings:
|
||||
- id: clock_painting_5
|
||||
display_name: Brown Puzzles Painting
|
||||
orientation: east
|
||||
- id: smile_painting_1
|
||||
display_name: Near Eight Painting
|
||||
orientation: north
|
||||
sunwarps:
|
||||
- dots: 3
|
||||
@@ -3866,8 +3913,10 @@
|
||||
- BEGIN
|
||||
paintings:
|
||||
- id: pencil_painting2
|
||||
display_name: Near Bold Painting
|
||||
orientation: west
|
||||
- id: north_missing2
|
||||
display_name: Directions Area Painting
|
||||
orientation: north
|
||||
The Bold:
|
||||
entrances:
|
||||
@@ -4189,12 +4238,14 @@
|
||||
panel: FOUR
|
||||
paintings:
|
||||
- id: maze_painting_3
|
||||
display_name: Near Four Painting
|
||||
enter_only: True
|
||||
orientation: north
|
||||
move: True
|
||||
required_door:
|
||||
door: Green Painting
|
||||
- id: blueman_painting_2
|
||||
display_name: Near Undeterred Painting
|
||||
orientation: east
|
||||
sunwarps:
|
||||
- dots: 4
|
||||
@@ -4557,6 +4608,7 @@
|
||||
panel: NINE
|
||||
paintings:
|
||||
- id: smile_painting_5
|
||||
display_name: Near Eight Painting
|
||||
enter_only: True
|
||||
orientation: east
|
||||
required_door:
|
||||
@@ -4742,10 +4794,13 @@
|
||||
- LEARN
|
||||
paintings:
|
||||
- id: smile_painting_7
|
||||
display_name: Near Turn/Return Painting
|
||||
orientation: south
|
||||
- id: flower_painting_4
|
||||
display_name: Back Area Right Painting
|
||||
orientation: south
|
||||
- id: pencil_painting3
|
||||
display_name: Back Area Left Painting
|
||||
enter_only: True
|
||||
orientation: east
|
||||
move: True
|
||||
@@ -4753,8 +4808,10 @@
|
||||
room: Number Hunt
|
||||
door: First Six
|
||||
- id: boxes_painting
|
||||
display_name: Near Directions Painting
|
||||
orientation: south
|
||||
- id: cherry_painting
|
||||
display_name: Alcove Painting
|
||||
orientation: east
|
||||
sunwarps:
|
||||
- dots: 6
|
||||
@@ -4848,8 +4905,10 @@
|
||||
- GREEN
|
||||
paintings:
|
||||
- id: arrows_painting_7
|
||||
display_name: Near Sunwarp Painting
|
||||
orientation: east
|
||||
- id: fruitbowl_painting3
|
||||
display_name: Hidden Painting
|
||||
orientation: west
|
||||
enter_only: True
|
||||
required_door:
|
||||
@@ -4888,6 +4947,7 @@
|
||||
tag: forbid
|
||||
paintings:
|
||||
- id: colors_painting
|
||||
display_name: Painting
|
||||
orientation: south
|
||||
The Bearer:
|
||||
entrances:
|
||||
@@ -5369,6 +5429,7 @@
|
||||
panel: ANTECHAMBER
|
||||
paintings:
|
||||
- id: pencil_painting5
|
||||
display_name: Left Painting
|
||||
orientation: south
|
||||
The Steady (Lemon):
|
||||
entrances:
|
||||
@@ -5391,6 +5452,7 @@
|
||||
- MELON
|
||||
paintings:
|
||||
- id: pencil_painting4
|
||||
display_name: Right Painting
|
||||
orientation: south
|
||||
The Steady (Topaz):
|
||||
entrances:
|
||||
@@ -6012,6 +6074,7 @@
|
||||
panel: NIGHT
|
||||
paintings:
|
||||
- id: smile_painting_9
|
||||
display_name: Smiley Painting
|
||||
orientation: north
|
||||
exit_only: True
|
||||
The Artistic (Panda):
|
||||
@@ -6124,6 +6187,7 @@
|
||||
panel: BOWELS
|
||||
paintings:
|
||||
- id: panda_painting_3
|
||||
display_name: Panda Painting
|
||||
exit_only: True
|
||||
orientation: south
|
||||
required_when_no_doors: True
|
||||
@@ -6235,6 +6299,7 @@
|
||||
panel: THING
|
||||
paintings:
|
||||
- id: boxes_painting2
|
||||
display_name: Lattice Painting
|
||||
orientation: south
|
||||
exit_only: True
|
||||
required_when_no_doors: True
|
||||
@@ -6344,6 +6409,7 @@
|
||||
panel: ROOT
|
||||
paintings:
|
||||
- id: cherry_painting3
|
||||
display_name: Apple Painting
|
||||
orientation: north
|
||||
exit_only: True
|
||||
required_when_no_doors: True
|
||||
@@ -6490,8 +6556,10 @@
|
||||
- NEAR
|
||||
paintings:
|
||||
- id: eye_painting_2
|
||||
display_name: Near Pillar Painting
|
||||
orientation: west
|
||||
- id: smile_painting_2
|
||||
display_name: Near Window Painting
|
||||
orientation: north
|
||||
Far Window:
|
||||
entrances:
|
||||
@@ -6512,6 +6580,7 @@
|
||||
door: Exit
|
||||
paintings:
|
||||
- id: arrows_painting_5
|
||||
display_name: Lobby Painting
|
||||
orientation: east
|
||||
Outside The Wondrous:
|
||||
entrances:
|
||||
@@ -6562,9 +6631,11 @@
|
||||
panel: SHRINK
|
||||
paintings:
|
||||
- id: symmetry_painting_a_1
|
||||
display_name: Doorknob Upper Painting
|
||||
orientation: east
|
||||
exit_only: True
|
||||
- id: symmetry_painting_b_1
|
||||
display_name: Doorknob Lower Painting
|
||||
orientation: south
|
||||
The Wondrous (Bookcase):
|
||||
entrances:
|
||||
@@ -6576,6 +6647,7 @@
|
||||
tag: midblue
|
||||
paintings:
|
||||
- id: symmetry_painting_a_3
|
||||
display_name: Bookcase Painting
|
||||
orientation: west
|
||||
exit_only: True
|
||||
- id: symmetry_painting_b_3
|
||||
@@ -6590,6 +6662,7 @@
|
||||
tag: midyellow
|
||||
paintings:
|
||||
- id: symmetry_painting_a_5
|
||||
display_name: Chandelier Painting
|
||||
orientation: east
|
||||
- id: symmetry_painting_b_5
|
||||
disable: True
|
||||
@@ -6603,6 +6676,7 @@
|
||||
tag: botbrown
|
||||
paintings:
|
||||
- id: symmetry_painting_b_4
|
||||
display_name: Window Painting
|
||||
orientation: north
|
||||
exit_only: True
|
||||
- id: symmetry_painting_a_4
|
||||
@@ -6627,8 +6701,10 @@
|
||||
tag: midyellow
|
||||
paintings:
|
||||
- id: symmetry_painting_a_2
|
||||
display_name: Table Lower Painting
|
||||
orientation: west
|
||||
- id: symmetry_painting_b_2
|
||||
display_name: Table Upper Painting
|
||||
orientation: south
|
||||
exit_only: True
|
||||
required: True
|
||||
@@ -6669,6 +6745,7 @@
|
||||
- Achievement
|
||||
paintings:
|
||||
- id: arrows_painting_9
|
||||
display_name: Exit Painting
|
||||
enter_only: True
|
||||
orientation: south
|
||||
move: True
|
||||
@@ -6676,9 +6753,11 @@
|
||||
door: Exit
|
||||
req_blocked_when_no_doors: True # the wondrous (table) in vanilla doors
|
||||
- id: symmetry_painting_a_6
|
||||
display_name: Fireplace Upper Painting
|
||||
orientation: west
|
||||
exit_only: True
|
||||
- id: symmetry_painting_b_6
|
||||
display_name: Fireplace Lower Painting
|
||||
orientation: north
|
||||
req_blocked_when_no_doors: True # the wondrous (table) in vanilla doors
|
||||
Arrow Garden:
|
||||
@@ -6700,6 +6779,7 @@
|
||||
tag: midwhite
|
||||
paintings:
|
||||
- id: flower_painting_6
|
||||
display_name: Painting
|
||||
orientation: south
|
||||
Hallway Room (1):
|
||||
entrances:
|
||||
@@ -6758,6 +6838,7 @@
|
||||
- TOWER
|
||||
paintings:
|
||||
- id: panda_painting
|
||||
display_name: Painting
|
||||
orientation: south
|
||||
progression:
|
||||
Progressive Hallway Room:
|
||||
@@ -6945,6 +7026,7 @@
|
||||
tag: midwhite
|
||||
paintings:
|
||||
- id: south_afar
|
||||
display_name: Painting
|
||||
orientation: south
|
||||
Outside The Wanderer:
|
||||
entrances:
|
||||
@@ -7123,16 +7205,21 @@
|
||||
panels:
|
||||
- ORDER
|
||||
paintings:
|
||||
- id: smile_painting_3
|
||||
orientation: west
|
||||
- id: flower_painting_2
|
||||
display_name: Left Near Painting
|
||||
orientation: east
|
||||
- id: scenery_painting_0a
|
||||
orientation: north
|
||||
- id: map_painting
|
||||
display_name: Left Far Painting
|
||||
orientation: east
|
||||
- id: fruitbowl_painting4
|
||||
display_name: Center Front Painting
|
||||
orientation: south
|
||||
- id: scenery_painting_0a
|
||||
display_name: Center Back Painting
|
||||
orientation: north
|
||||
- id: smile_painting_3
|
||||
display_name: Right Far Painting
|
||||
orientation: west
|
||||
progression:
|
||||
Progressive Art Gallery:
|
||||
doors:
|
||||
@@ -7493,6 +7580,7 @@
|
||||
panel: WORD
|
||||
paintings:
|
||||
- id: arrows_painting_3
|
||||
display_name: Circle Painting
|
||||
orientation: north
|
||||
Rhyme Room (Looped Square):
|
||||
entrances:
|
||||
@@ -7675,6 +7763,7 @@
|
||||
- INNOVATIVE (Bottom)
|
||||
paintings:
|
||||
- id: arrows_painting_4
|
||||
display_name: Target Painting
|
||||
orientation: north
|
||||
Room Room:
|
||||
# This is a bit of a weird room. You can't really get to it from the roof.
|
||||
@@ -7944,8 +8033,10 @@
|
||||
- CAT
|
||||
paintings:
|
||||
- id: arrows_painting_2
|
||||
display_name: Left Painting
|
||||
orientation: east
|
||||
- id: clock_painting_2
|
||||
display_name: Right Painting
|
||||
orientation: east
|
||||
exit_only: True
|
||||
required: True
|
||||
@@ -8022,6 +8113,7 @@
|
||||
tag: midbrown
|
||||
paintings:
|
||||
- id: clock_painting_3
|
||||
display_name: Painting
|
||||
orientation: east
|
||||
req_blocked: True # outside the wise (with or without door shuffle)
|
||||
The Red:
|
||||
@@ -8492,6 +8584,7 @@
|
||||
- OPTICS
|
||||
paintings:
|
||||
- id: hi_solved_painting4
|
||||
display_name: Painting
|
||||
orientation: south
|
||||
req_blocked_when_no_doors: True # owl hallway in vanilla doors
|
||||
Challenge Room:
|
||||
|
||||
Binary file not shown.
@@ -50,7 +50,7 @@ directives = Set["entrances", "panels", "doors", "panel_doors", "paintings", "su
|
||||
panel_directives = Set["id", "required_room", "required_door", "required_panel", "colors", "check", "exclude_reduce", "tag", "link", "subtag", "achievement", "copy_to_sign", "non_counting", "hunt", "location_name"]
|
||||
door_directives = Set["id", "painting_id", "panels", "item_name", "item_group", "location_name", "skip_location", "skip_item", "door_group", "include_reduce", "event", "warp_id"]
|
||||
panel_door_directives = Set["panels", "item_name", "panel_group"]
|
||||
painting_directives = Set["id", "enter_only", "exit_only", "orientation", "required_door", "required", "required_when_no_doors", "move", "req_blocked", "req_blocked_when_no_doors"]
|
||||
painting_directives = Set["id", "display_name", "enter_only", "exit_only", "orientation", "required_door", "required", "required_when_no_doors", "move", "req_blocked", "req_blocked_when_no_doors"]
|
||||
|
||||
non_counting = 0
|
||||
|
||||
@@ -314,6 +314,10 @@ config.each do |room_name, room|
|
||||
next
|
||||
end
|
||||
|
||||
unless painting.include? "display_name" then
|
||||
puts "#{room_name} - #{painting["id"] || "painting"} :::: Missing display name"
|
||||
end
|
||||
|
||||
if painting.include?("orientation") then
|
||||
unless ["north", "south", "east", "west"].include? painting["orientation"] then
|
||||
puts "#{room_name} - #{painting["id"] || "painting"} :::: Invalid orientation #{painting["orientation"]}"
|
||||
|
||||
@@ -1 +1 @@
|
||||
requests >= 2.28.1 # used by client
|
||||
requests >= 2.28.1 # used by client
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
2. Choose the automated tab, click the select button and browse to `MuseDash.exe`.
|
||||
- You can find the folder in steam by finding the game in your library, right clicking it and choosing *Manage→Browse Local Files*.
|
||||
- If you click the bar at the top telling you your current folder, this will give you a path you can copy. If you paste that into the window popped up by **MelonLoader**, it will automatically go to the same folder.
|
||||
3. Uncheck "Latest" and select v0.6.1. Then click install.
|
||||
3. Select v0.7.0. Then click install.
|
||||
4. Run the game once, and wait until you get to the Muse Dash start screen before exiting.
|
||||
5. Download the latest [Muse Dash Archipelago Mod](https://github.com/DeamonHunter/ArchipelagoMuseDash/releases/latest) and then extract that into the newly created `/Mods/` folder in MuseDash's install location.
|
||||
- All files must be under the `/Mods/` folder and not within a sub folder inside of `/Mods/`
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
2. Elije la pestaña "automated", haz clic en el botón "select" y busca tu `MuseDash.exe`.
|
||||
- Puedes encontrar la carpeta en Steam buscando el juego en tu biblioteca, haciendo clic derecho sobre el y elegir *Administrar→Ver archivos locales*.
|
||||
- Si haces clic en la barra superior que te indica la carpeta en la que estas, te dará la dirección de ésta para que puedas copiarla. Al pegar esa dirección en la ventana que **MelonLoader** abre, irá automaticamente a esa carpeta.
|
||||
3. Desmarca "Latest" y selecciona v0.6.1. Luego haz clic en "install".
|
||||
3. Selecciona v0.7.0. Luego haz clic en "install".
|
||||
4. Ejecuta el juego una vez, y espera hasta que aparezca la pantalla de inicio de Muse Dash antes de cerrarlo.
|
||||
5. Descarga la última version de [Muse Dash Archipelago Mod](https://github.com/DeamonHunter/ArchipelagoMuseDash/releases/latest) y extraelo en la nueva carpeta creada llamada `/Mods/`, localizada en la carpeta de instalación de Muse Dash.
|
||||
- Todos los archivos deben ir directamente en la carpeta `/Mods/`, y NO en una subcarpeta dentro de la carpeta `/Mods/`
|
||||
|
||||
@@ -150,6 +150,26 @@ sample_chao_names = [
|
||||
"Hubert",
|
||||
"Corvus",
|
||||
"Nigel",
|
||||
"Benjamin",
|
||||
"Gooey",
|
||||
"Maddy",
|
||||
"AFGNCAAP",
|
||||
"Reinhardt",
|
||||
"Claire",
|
||||
"Yoshi",
|
||||
"Peasley",
|
||||
"Faux",
|
||||
"Naija",
|
||||
"Kaiba",
|
||||
"Hat Kid",
|
||||
"TzTokJad",
|
||||
"Sora",
|
||||
"WoodMan",
|
||||
"Yachty",
|
||||
"Grieve",
|
||||
"Portia",
|
||||
"Graves",
|
||||
"Kaycee",
|
||||
]
|
||||
|
||||
totally_real_item_names = [
|
||||
@@ -240,6 +260,35 @@ totally_real_item_names = [
|
||||
"Ladder",
|
||||
|
||||
"Visible Dots",
|
||||
|
||||
"CooCoo",
|
||||
|
||||
"Blueberry",
|
||||
|
||||
"Ear of Luigi",
|
||||
|
||||
"Mega Nut",
|
||||
|
||||
"DUELIST ALLIANCE",
|
||||
"DUEL OVERLOAD",
|
||||
"POWER OF THE ELEMENTS",
|
||||
"S:P Little Knight",
|
||||
"Red-Eyes Dark Dragoon",
|
||||
|
||||
"Fire Hat",
|
||||
|
||||
"Area: Taverly",
|
||||
"Area: Meiyerditch",
|
||||
"Fire Cape",
|
||||
|
||||
"Donald Zeta Flare",
|
||||
|
||||
"Category One of a Kind",
|
||||
"Category Fuller House",
|
||||
|
||||
"Passive Camoflage",
|
||||
|
||||
"Earth Card",
|
||||
]
|
||||
|
||||
all_exits = [
|
||||
|
||||
@@ -1,6 +1,84 @@
|
||||
# Sonic Adventure 2 Battle - Changelog
|
||||
|
||||
|
||||
## v2.4 - Minigame Madness
|
||||
|
||||
### Features:
|
||||
- New Goal
|
||||
- Minigame Madness
|
||||
- Win a certain number of each type of Minigame Trap, then defeat the Finalhazard to win!
|
||||
- How many of each Minigame are required can be set by an Option
|
||||
- When the required amount of a Minigame has been received, that Minigame can be replayed in the Chao World Lobby
|
||||
- New optional Location Checks
|
||||
- Bigsanity
|
||||
- Go fishing with Big in each stage for a Location Check
|
||||
- Itemboxsanity
|
||||
- Either Extra Life Boxes or All Item Boxes
|
||||
- New Items
|
||||
- New Traps
|
||||
- Literature Trap
|
||||
- Controller Drift Trap
|
||||
- Poison Trap
|
||||
- Bee Trap
|
||||
- New Minigame Traps
|
||||
- Breakout Trap
|
||||
- Fishing Trap
|
||||
- Trivia Trap
|
||||
- Pokemon Trivia Trap
|
||||
- Pokemon Count Trap
|
||||
- Number Sequence Trap
|
||||
- Light Up Path Trap
|
||||
- Pinball Trap
|
||||
- Math Quiz Trap
|
||||
- Snake Trap
|
||||
- Input Sequence Trap
|
||||
- Trap Link
|
||||
- When you receive a trap, you send a copy of it to every other player with Trap Link enabled
|
||||
- Boss Gate Plando
|
||||
- Expert Logic Difficulty
|
||||
- Use at your own risk. This difficulty requires complete mastery of SA2.
|
||||
- Missions can now be enabled and disabled per-character, instead of just per-style
|
||||
- Minigame Difficulty can now be set to "Chaos", which selects a new difficulty randomly per-trap received
|
||||
|
||||
### Quality of Life:
|
||||
- Gate Stages and Mission Orders are now displayed in the spoiler log
|
||||
- Additional play stats are saved and displayed with the randomizer credits
|
||||
- Stage Locations progress UI now displays in multiple pages when Itemboxsanity is enabled
|
||||
- Current stage mission order and progress are now shown when paused in-level
|
||||
- Chaos Emeralds are now shown when paused in-level
|
||||
- Location Name Groups were created
|
||||
- Moved SA2B to the new Options system
|
||||
- Option Presets were created
|
||||
- Error Messages are more obvious
|
||||
|
||||
### Bug Fixes:
|
||||
- Added missing `Dry Lagoon - 12 Animals` location
|
||||
- Flying Dog boss should no longer crash when you have done at least 3 Intermediate Kart Races
|
||||
- Invincibility can no longer be received in the King Boom Boo fight, preventing a crash
|
||||
- Chaos Emeralds should no longer disproportionately end up in Cannon's Core or the final Level Gate
|
||||
- Going into submenus from the pause menu should no longer reset traps
|
||||
- `Sonic - Magic Gloves` are now plural
|
||||
- Junk items will no longer cause a crash when in a falling state
|
||||
- Saves should no longer incorrectly be marked as not matching the connected server
|
||||
- Fixed miscellaneous crashes
|
||||
- Chao Garden:
|
||||
- Prevent races from occasionally becoming uncompletable when using the "Prize Only" option
|
||||
- Properly allow Hero Chao to participate in Dark Races
|
||||
- Don't allow the Chao Garden to send locations when connected to an invalid server
|
||||
- Prevent the Chao Garden from resetting your life count
|
||||
- Fix Chao World Entrance Shuffle causing inaccessible Neutral Garden
|
||||
- Fix pressing the 'B' button to take you to the proper location in Chao World Entrance Shuffle
|
||||
- Prevent Chao Karate progress icon overflow
|
||||
- Prevent changing Chao Timescale while paused or while a Minigame is active
|
||||
- Logic Fixes:
|
||||
- `Mission Street - Chao Key 1` (Hard Logic) now requires no upgrades
|
||||
- `Mission Street - Chao Key 2` (Hard Logic) now requires no upgrades
|
||||
- `Crazy Gadget - Hidden 1` (Standard Logic) now requires `Sonic - Bounce Bracelet` instead of `Sonic - Light Shoes`
|
||||
- `Lost Colony - Hidden 1` (Standard Logic) now requires `Eggman - Jet Engine`
|
||||
- `Mad Space - Gold Beetle` (Standard Logic) now only requires `Rouge - Iron Boots`
|
||||
- `Cosmic Wall - Gold Beetle` (Standard and Hard Logic) now only requires `Eggman - Jet Engine`
|
||||
|
||||
|
||||
## v2.3 - The Chao Update
|
||||
|
||||
### Features:
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
import typing
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from worlds.AutoWorld import World
|
||||
|
||||
speed_characters_1 = "Sonic vs Shadow 1"
|
||||
speed_characters_2 = "Sonic vs Shadow 2"
|
||||
mech_characters_1 = "Tails vs Eggman 1"
|
||||
mech_characters_2 = "Tails vs Eggman 2"
|
||||
hunt_characters_1 = "Knuckles vs Rouge 1"
|
||||
big_foot = "F-6t BIG FOOT"
|
||||
hot_shot = "B-3x HOT SHOT"
|
||||
flying_dog = "R-1/A FLYING DOG"
|
||||
egg_golem_sonic = "Egg Golem (Sonic)"
|
||||
egg_golem_eggman = "Egg Golem (Eggman)"
|
||||
king_boom_boo = "King Boom Boo"
|
||||
from .Names import LocationName
|
||||
from .Options import GateBossPlando
|
||||
|
||||
|
||||
speed_characters_1 = "sonic vs shadow 1"
|
||||
speed_characters_2 = "sonic vs shadow 2"
|
||||
mech_characters_1 = "tails vs eggman 1"
|
||||
mech_characters_2 = "tails vs eggman 2"
|
||||
hunt_characters_1 = "knuckles vs rouge 1"
|
||||
big_foot = "big foot"
|
||||
hot_shot = "hot shot"
|
||||
flying_dog = "flying dog"
|
||||
egg_golem_sonic = "egg golem (sonic)"
|
||||
egg_golem_eggman = "egg golem (eggman)"
|
||||
king_boom_boo = "king boom boo"
|
||||
|
||||
gate_bosses_no_requirements_table = {
|
||||
speed_characters_1: 0,
|
||||
@@ -45,44 +50,83 @@ all_gate_bosses_table = {
|
||||
}
|
||||
|
||||
|
||||
boss_id_to_name = {
|
||||
0: "Sonic vs Shadow 1",
|
||||
1: "Sonic vs Shadow 2",
|
||||
2: "Tails vs Eggman 1",
|
||||
3: "Tails vs Eggman 2",
|
||||
4: "Knuckles vs Rouge 1",
|
||||
5: "F-6t BIG FOOT",
|
||||
6: "B-3x HOT SHOT",
|
||||
7: "R-1/A FLYING DOG",
|
||||
8: "Egg Golem (Sonic)",
|
||||
9: "Egg Golem (Eggman)",
|
||||
10: "King Boom Boo",
|
||||
11: "Sonic vs Shadow 1",
|
||||
12: "Sonic vs Shadow 2",
|
||||
13: "Tails vs Eggman 1",
|
||||
14: "Tails vs Eggman 2",
|
||||
15: "Knuckles vs Rouge 1",
|
||||
}
|
||||
|
||||
def get_boss_name(boss: int):
|
||||
for key, value in gate_bosses_no_requirements_table.items():
|
||||
if value == boss:
|
||||
return key
|
||||
for key, value in gate_bosses_with_requirements_table.items():
|
||||
if value == boss:
|
||||
return key
|
||||
for key, value in extra_boss_rush_bosses_table.items():
|
||||
if value == boss:
|
||||
return key
|
||||
return boss_id_to_name[boss]
|
||||
|
||||
|
||||
def boss_has_requirement(boss: int):
|
||||
return boss >= len(gate_bosses_no_requirements_table)
|
||||
|
||||
|
||||
def get_gate_bosses(multiworld: MultiWorld, world: World):
|
||||
def get_gate_bosses(world: World):
|
||||
selected_bosses: typing.List[int] = []
|
||||
boss_gates: typing.List[int] = []
|
||||
available_bosses: typing.List[str] = list(gate_bosses_no_requirements_table.keys())
|
||||
multiworld.random.shuffle(available_bosses)
|
||||
halfway = False
|
||||
world.random.shuffle(available_bosses)
|
||||
|
||||
gate_boss_plando: typing.Union[int, str] = world.options.gate_boss_plando.value
|
||||
plando_bosses = ["None", "None", "None", "None", "None"]
|
||||
if isinstance(gate_boss_plando, str):
|
||||
# boss plando
|
||||
options = gate_boss_plando.split(";")
|
||||
gate_boss_plando = GateBossPlando.options[options.pop()]
|
||||
for option in options:
|
||||
if "-" in option:
|
||||
loc, boss = option.split("-")
|
||||
boss_num = LocationName.boss_gate_names[loc]
|
||||
|
||||
if boss_num >= world.options.number_of_level_gates.value:
|
||||
# Don't reject bosses plando'd into gate bosses that won't exist
|
||||
pass
|
||||
|
||||
if boss in plando_bosses:
|
||||
# TODO: Raise error here. Duplicates not allowed
|
||||
pass
|
||||
|
||||
plando_bosses[boss_num] = boss
|
||||
|
||||
if boss in available_bosses:
|
||||
available_bosses.remove(boss)
|
||||
|
||||
for x in range(world.options.number_of_level_gates):
|
||||
if (not halfway) and ((x + 1) / world.options.number_of_level_gates) > 0.5:
|
||||
if ("king boom boo" not in selected_bosses) and ("king boom boo" not in available_bosses) and ((x + 1) / world.options.number_of_level_gates) > 0.5:
|
||||
available_bosses.extend(gate_bosses_with_requirements_table)
|
||||
multiworld.random.shuffle(available_bosses)
|
||||
halfway = True
|
||||
selected_bosses.append(all_gate_bosses_table[available_bosses[0]])
|
||||
world.random.shuffle(available_bosses)
|
||||
|
||||
chosen_boss = available_bosses[0]
|
||||
if plando_bosses[x] != "None":
|
||||
available_bosses.append(plando_bosses[x])
|
||||
chosen_boss = plando_bosses[x]
|
||||
|
||||
selected_bosses.append(all_gate_bosses_table[chosen_boss])
|
||||
boss_gates.append(x + 1)
|
||||
available_bosses.remove(available_bosses[0])
|
||||
available_bosses.remove(chosen_boss)
|
||||
|
||||
bosses: typing.Dict[int, int] = dict(zip(boss_gates, selected_bosses))
|
||||
|
||||
return bosses
|
||||
|
||||
|
||||
def get_boss_rush_bosses(multiworld: MultiWorld, world: World):
|
||||
def get_boss_rush_bosses(world: World):
|
||||
|
||||
if world.options.boss_rush_shuffle == 0:
|
||||
boss_list_o = list(range(0, 16))
|
||||
@@ -92,21 +136,21 @@ def get_boss_rush_bosses(multiworld: MultiWorld, world: World):
|
||||
elif world.options.boss_rush_shuffle == 1:
|
||||
boss_list_o = list(range(0, 16))
|
||||
boss_list_s = boss_list_o.copy()
|
||||
multiworld.random.shuffle(boss_list_s)
|
||||
world.random.shuffle(boss_list_s)
|
||||
|
||||
return dict(zip(boss_list_o, boss_list_s))
|
||||
elif world.options.boss_rush_shuffle == 2:
|
||||
boss_list_o = list(range(0, 16))
|
||||
boss_list_s = [multiworld.random.choice(boss_list_o) for i in range(0, 16)]
|
||||
boss_list_s = [world.random.choice(boss_list_o) for i in range(0, 16)]
|
||||
if 10 not in boss_list_s:
|
||||
boss_list_s[multiworld.random.randint(0, 15)] = 10
|
||||
boss_list_s[world.random.randint(0, 15)] = 10
|
||||
|
||||
return dict(zip(boss_list_o, boss_list_s))
|
||||
elif world.options.boss_rush_shuffle == 3:
|
||||
boss_list_o = list(range(0, 16))
|
||||
boss_list_s = [multiworld.random.choice(boss_list_o)] * len(boss_list_o)
|
||||
boss_list_s = [world.random.choice(boss_list_o)] * len(boss_list_o)
|
||||
if 10 not in boss_list_s:
|
||||
boss_list_s[multiworld.random.randint(0, 15)] = 10
|
||||
boss_list_s[world.random.randint(0, 15)] = 10
|
||||
|
||||
return dict(zip(boss_list_o, boss_list_s))
|
||||
else:
|
||||
|
||||
@@ -2,7 +2,6 @@ import typing
|
||||
|
||||
from BaseClasses import Item, ItemClassification
|
||||
from .Names import ItemName
|
||||
from worlds.alttp import ALTTPWorld
|
||||
|
||||
|
||||
class ItemData(typing.NamedTuple):
|
||||
@@ -14,7 +13,7 @@ class ItemData(typing.NamedTuple):
|
||||
|
||||
|
||||
class SA2BItem(Item):
|
||||
game: str = "Sonic Adventure 2: Battle"
|
||||
game: str = "Sonic Adventure 2 Battle"
|
||||
|
||||
def __init__(self, name, classification: ItemClassification, code: int = None, player: int = None):
|
||||
super(SA2BItem, self).__init__(name, classification, code, player)
|
||||
@@ -73,19 +72,36 @@ junk_table = {
|
||||
}
|
||||
|
||||
trap_table = {
|
||||
ItemName.omochao_trap: ItemData(0xFF0030, False, True),
|
||||
ItemName.timestop_trap: ItemData(0xFF0031, False, True),
|
||||
ItemName.confuse_trap: ItemData(0xFF0032, False, True),
|
||||
ItemName.tiny_trap: ItemData(0xFF0033, False, True),
|
||||
ItemName.gravity_trap: ItemData(0xFF0034, False, True),
|
||||
ItemName.exposition_trap: ItemData(0xFF0035, False, True),
|
||||
#ItemName.darkness_trap: ItemData(0xFF0036, False, True),
|
||||
ItemName.ice_trap: ItemData(0xFF0037, False, True),
|
||||
ItemName.slow_trap: ItemData(0xFF0038, False, True),
|
||||
ItemName.cutscene_trap: ItemData(0xFF0039, False, True),
|
||||
ItemName.reverse_trap: ItemData(0xFF003A, False, True),
|
||||
ItemName.omochao_trap: ItemData(0xFF0030, False, True),
|
||||
ItemName.timestop_trap: ItemData(0xFF0031, False, True),
|
||||
ItemName.confuse_trap: ItemData(0xFF0032, False, True),
|
||||
ItemName.tiny_trap: ItemData(0xFF0033, False, True),
|
||||
ItemName.gravity_trap: ItemData(0xFF0034, False, True),
|
||||
ItemName.exposition_trap: ItemData(0xFF0035, False, True),
|
||||
#ItemName.darkness_trap: ItemData(0xFF0036, False, True),
|
||||
ItemName.ice_trap: ItemData(0xFF0037, False, True),
|
||||
ItemName.slow_trap: ItemData(0xFF0038, False, True),
|
||||
ItemName.cutscene_trap: ItemData(0xFF0039, False, True),
|
||||
ItemName.reverse_trap: ItemData(0xFF003A, False, True),
|
||||
ItemName.literature_trap: ItemData(0xFF003B, False, True),
|
||||
ItemName.controller_drift_trap: ItemData(0xFF003C, False, True),
|
||||
ItemName.poison_trap: ItemData(0xFF003D, False, True),
|
||||
ItemName.bee_trap: ItemData(0xFF003E, False, True),
|
||||
}
|
||||
|
||||
ItemName.pong_trap: ItemData(0xFF0050, False, True),
|
||||
minigame_trap_table = {
|
||||
ItemName.pong_trap: ItemData(0xFF0050, False, True),
|
||||
ItemName.breakout_trap: ItemData(0xFF0051, False, True),
|
||||
ItemName.fishing_trap: ItemData(0xFF0052, False, True),
|
||||
ItemName.trivia_trap: ItemData(0xFF0053, False, True),
|
||||
ItemName.pokemon_trivia_trap: ItemData(0xFF0054, False, True),
|
||||
ItemName.pokemon_count_trap: ItemData(0xFF0055, False, True),
|
||||
ItemName.number_sequence_trap: ItemData(0xFF0056, False, True),
|
||||
ItemName.light_up_path_trap: ItemData(0xFF0057, False, True),
|
||||
ItemName.pinball_trap: ItemData(0xFF0058, False, True),
|
||||
ItemName.math_quiz_trap: ItemData(0xFF0059, False, True),
|
||||
ItemName.snake_trap: ItemData(0xFF005A, False, True),
|
||||
ItemName.input_sequence_trap: ItemData(0xFF005B, False, True),
|
||||
}
|
||||
|
||||
emeralds_table = {
|
||||
@@ -235,7 +251,7 @@ chaos_drives_table = {
|
||||
}
|
||||
|
||||
event_table = {
|
||||
ItemName.maria: ItemData(0xFF001D, True),
|
||||
ItemName.maria: ItemData(None, True),
|
||||
}
|
||||
|
||||
# Complete item table.
|
||||
@@ -244,6 +260,7 @@ item_table = {
|
||||
**upgrades_table,
|
||||
**junk_table,
|
||||
**trap_table,
|
||||
**minigame_trap_table,
|
||||
**emeralds_table,
|
||||
**eggs_table,
|
||||
**fruits_table,
|
||||
@@ -251,7 +268,6 @@ item_table = {
|
||||
**hats_table,
|
||||
**animals_table,
|
||||
**chaos_drives_table,
|
||||
**event_table,
|
||||
}
|
||||
|
||||
lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_table.items() if data.code}
|
||||
@@ -263,7 +279,12 @@ item_groups: typing.Dict[str, str] = {
|
||||
"Seeds": list(seeds_table.keys()),
|
||||
"Hats": list(hats_table.keys()),
|
||||
"Traps": list(trap_table.keys()),
|
||||
"Minigames": list(minigame_trap_table.keys()),
|
||||
}
|
||||
|
||||
ALTTPWorld.pedestal_credit_texts[item_table[ItemName.sonic_light_shoes].code] = "and the Soap Shoes"
|
||||
ALTTPWorld.pedestal_credit_texts[item_table[ItemName.shadow_air_shoes].code] = "and the Soap Shoes"
|
||||
try:
|
||||
from worlds.alttp import ALTTPWorld
|
||||
ALTTPWorld.pedestal_credit_texts[item_table[ItemName.sonic_light_shoes].code] = "and the Soap Shoes"
|
||||
ALTTPWorld.pedestal_credit_texts[item_table[ItemName.shadow_air_shoes].code] = "and the Soap Shoes"
|
||||
except ModuleNotFoundError:
|
||||
pass
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -119,11 +119,14 @@ mission_orders: typing.List[typing.List[int]] = [
|
||||
[4, 5, 3, 2, 1],
|
||||
]
|
||||
|
||||
### 0: Speed
|
||||
### 1: Mech
|
||||
### 2: Hunt
|
||||
### 3: Kart
|
||||
### 4: Cannon's Core
|
||||
### 0: Sonic
|
||||
### 1: Tails
|
||||
### 2: Knuckles
|
||||
### 3: Shadow
|
||||
### 4: Eggman
|
||||
### 5: Rouge
|
||||
### 6: Kart
|
||||
### 7: Cannon's Core
|
||||
level_styles: typing.List[int] = [
|
||||
0,
|
||||
2,
|
||||
@@ -133,7 +136,7 @@ level_styles: typing.List[int] = [
|
||||
2,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
6,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
@@ -142,22 +145,22 @@ level_styles: typing.List[int] = [
|
||||
0,
|
||||
0,
|
||||
|
||||
1,
|
||||
2,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
2,
|
||||
0,
|
||||
3,
|
||||
0,
|
||||
2,
|
||||
1,
|
||||
0,
|
||||
|
||||
4,
|
||||
5,
|
||||
4,
|
||||
3,
|
||||
5,
|
||||
4,
|
||||
4,
|
||||
5,
|
||||
3,
|
||||
6,
|
||||
3,
|
||||
5,
|
||||
4,
|
||||
3,
|
||||
|
||||
7,
|
||||
]
|
||||
|
||||
stage_name_prefixes: typing.List[str] = [
|
||||
@@ -201,21 +204,33 @@ def get_mission_count_table(multiworld: MultiWorld, world: World, player: int):
|
||||
for level in range(31):
|
||||
mission_count_table[level] = 0
|
||||
else:
|
||||
speed_active_missions = 1
|
||||
mech_active_missions = 1
|
||||
hunt_active_missions = 1
|
||||
sonic_active_missions = 1
|
||||
tails_active_missions = 1
|
||||
knuckles_active_missions = 1
|
||||
shadow_active_missions = 1
|
||||
eggman_active_missions = 1
|
||||
rouge_active_missions = 1
|
||||
kart_active_missions = 1
|
||||
cannons_core_active_missions = 1
|
||||
|
||||
for i in range(2,6):
|
||||
if getattr(world.options, "speed_mission_" + str(i), None):
|
||||
speed_active_missions += 1
|
||||
if getattr(world.options, "sonic_mission_" + str(i), None):
|
||||
sonic_active_missions += 1
|
||||
|
||||
if getattr(world.options, "mech_mission_" + str(i), None):
|
||||
mech_active_missions += 1
|
||||
if getattr(world.options, "tails_mission_" + str(i), None):
|
||||
tails_active_missions += 1
|
||||
|
||||
if getattr(world.options, "hunt_mission_" + str(i), None):
|
||||
hunt_active_missions += 1
|
||||
if getattr(world.options, "knuckles_mission_" + str(i), None):
|
||||
knuckles_active_missions += 1
|
||||
|
||||
if getattr(world.options, "shadow_mission_" + str(i), None):
|
||||
shadow_active_missions += 1
|
||||
|
||||
if getattr(world.options, "eggman_mission_" + str(i), None):
|
||||
eggman_active_missions += 1
|
||||
|
||||
if getattr(world.options, "rouge_mission_" + str(i), None):
|
||||
rouge_active_missions += 1
|
||||
|
||||
if getattr(world.options, "kart_mission_" + str(i), None):
|
||||
kart_active_missions += 1
|
||||
@@ -223,16 +238,22 @@ def get_mission_count_table(multiworld: MultiWorld, world: World, player: int):
|
||||
if getattr(world.options, "cannons_core_mission_" + str(i), None):
|
||||
cannons_core_active_missions += 1
|
||||
|
||||
speed_active_missions = min(speed_active_missions, world.options.speed_mission_count.value)
|
||||
mech_active_missions = min(mech_active_missions, world.options.mech_mission_count.value)
|
||||
hunt_active_missions = min(hunt_active_missions, world.options.hunt_mission_count.value)
|
||||
sonic_active_missions = min(sonic_active_missions, world.options.sonic_mission_count.value)
|
||||
tails_active_missions = min(tails_active_missions, world.options.tails_mission_count.value)
|
||||
knuckles_active_missions = min(knuckles_active_missions, world.options.knuckles_mission_count.value)
|
||||
shadow_active_missions = min(shadow_active_missions, world.options.sonic_mission_count.value)
|
||||
eggman_active_missions = min(eggman_active_missions, world.options.eggman_mission_count.value)
|
||||
rouge_active_missions = min(rouge_active_missions, world.options.rouge_mission_count.value)
|
||||
kart_active_missions = min(kart_active_missions, world.options.kart_mission_count.value)
|
||||
cannons_core_active_missions = min(cannons_core_active_missions, world.options.cannons_core_mission_count.value)
|
||||
|
||||
active_missions: typing.List[typing.List[int]] = [
|
||||
speed_active_missions,
|
||||
mech_active_missions,
|
||||
hunt_active_missions,
|
||||
sonic_active_missions,
|
||||
tails_active_missions,
|
||||
knuckles_active_missions,
|
||||
shadow_active_missions,
|
||||
eggman_active_missions,
|
||||
rouge_active_missions,
|
||||
kart_active_missions,
|
||||
cannons_core_active_missions
|
||||
]
|
||||
@@ -252,22 +273,34 @@ def get_mission_table(multiworld: MultiWorld, world: World, player: int):
|
||||
for level in range(31):
|
||||
mission_table[level] = 0
|
||||
else:
|
||||
speed_active_missions: typing.List[int] = [1]
|
||||
mech_active_missions: typing.List[int] = [1]
|
||||
hunt_active_missions: typing.List[int] = [1]
|
||||
sonic_active_missions: typing.List[int] = [1]
|
||||
tails_active_missions: typing.List[int] = [1]
|
||||
knuckles_active_missions: typing.List[int] = [1]
|
||||
shadow_active_missions: typing.List[int] = [1]
|
||||
eggman_active_missions: typing.List[int] = [1]
|
||||
rouge_active_missions: typing.List[int] = [1]
|
||||
kart_active_missions: typing.List[int] = [1]
|
||||
cannons_core_active_missions: typing.List[int] = [1]
|
||||
|
||||
# Add included missions
|
||||
for i in range(2,6):
|
||||
if getattr(world.options, "speed_mission_" + str(i), None):
|
||||
speed_active_missions.append(i)
|
||||
if getattr(world.options, "sonic_mission_" + str(i), None):
|
||||
sonic_active_missions.append(i)
|
||||
|
||||
if getattr(world.options, "mech_mission_" + str(i), None):
|
||||
mech_active_missions.append(i)
|
||||
if getattr(world.options, "tails_mission_" + str(i), None):
|
||||
tails_active_missions.append(i)
|
||||
|
||||
if getattr(world.options, "hunt_mission_" + str(i), None):
|
||||
hunt_active_missions.append(i)
|
||||
if getattr(world.options, "knuckles_mission_" + str(i), None):
|
||||
knuckles_active_missions.append(i)
|
||||
|
||||
if getattr(world.options, "shadow_mission_" + str(i), None):
|
||||
shadow_active_missions.append(i)
|
||||
|
||||
if getattr(world.options, "eggman_mission_" + str(i), None):
|
||||
eggman_active_missions.append(i)
|
||||
|
||||
if getattr(world.options, "rouge_mission_" + str(i), None):
|
||||
rouge_active_missions.append(i)
|
||||
|
||||
if getattr(world.options, "kart_mission_" + str(i), None):
|
||||
kart_active_missions.append(i)
|
||||
@@ -276,9 +309,12 @@ def get_mission_table(multiworld: MultiWorld, world: World, player: int):
|
||||
cannons_core_active_missions.append(i)
|
||||
|
||||
active_missions: typing.List[typing.List[int]] = [
|
||||
speed_active_missions,
|
||||
mech_active_missions,
|
||||
hunt_active_missions,
|
||||
sonic_active_missions,
|
||||
tails_active_missions,
|
||||
knuckles_active_missions,
|
||||
shadow_active_missions,
|
||||
eggman_active_missions,
|
||||
rouge_active_missions,
|
||||
kart_active_missions,
|
||||
cannons_core_active_missions
|
||||
]
|
||||
@@ -328,13 +364,60 @@ def get_mission_table(multiworld: MultiWorld, world: World, player: int):
|
||||
|
||||
|
||||
def get_first_and_last_cannons_core_missions(mission_map: typing.Dict[int, int], mission_count_map: typing.Dict[int, int]):
|
||||
mission_count = mission_count_map[30]
|
||||
mission_order: typing.List[int] = mission_orders[mission_map[30]]
|
||||
stage_prefix: str = stage_name_prefixes[30]
|
||||
mission_count = mission_count_map[30]
|
||||
mission_order: typing.List[int] = mission_orders[mission_map[30]]
|
||||
stage_prefix: str = stage_name_prefixes[30]
|
||||
|
||||
first_mission_number = mission_order[0]
|
||||
last_mission_number = mission_order[mission_count - 1]
|
||||
first_location_name: str = stage_prefix + str(first_mission_number)
|
||||
last_location_name: str = stage_prefix + str(last_mission_number)
|
||||
first_mission_number = mission_order[0]
|
||||
last_mission_number = mission_order[mission_count - 1]
|
||||
first_location_name: str = stage_prefix + str(first_mission_number)
|
||||
last_location_name: str = stage_prefix + str(last_mission_number)
|
||||
|
||||
return first_location_name, last_location_name
|
||||
return first_location_name, last_location_name
|
||||
|
||||
|
||||
def print_mission_orders_to_spoiler(mission_map: typing.Dict[int, int],
|
||||
mission_count_map: typing.Dict[int, int],
|
||||
shuffled_region_list: typing.Dict[int, int],
|
||||
levels_per_gate: typing.Dict[int, int],
|
||||
player_name: str,
|
||||
spoiler_handle: typing.TextIO):
|
||||
spoiler_handle.write("\n")
|
||||
header_text = "SA2 Mission Orders for {}:\n"
|
||||
header_text = header_text.format(player_name)
|
||||
spoiler_handle.write(header_text)
|
||||
|
||||
level_index = 0
|
||||
for gate_idx in range(len(levels_per_gate)):
|
||||
gate_len = levels_per_gate[gate_idx]
|
||||
gate_levels = shuffled_region_list[int(level_index):int(level_index+gate_len)]
|
||||
gate_levels.sort()
|
||||
|
||||
gate_text = "Gate {}:\n"
|
||||
gate_text = gate_text.format(gate_idx)
|
||||
spoiler_handle.write(gate_text)
|
||||
|
||||
for i in range(len(gate_levels)):
|
||||
stage = gate_levels[i]
|
||||
mission_count = mission_count_map[stage]
|
||||
mission_order: typing.List[int] = mission_orders[mission_map[stage]]
|
||||
stage_prefix: str = stage_name_prefixes[stage]
|
||||
|
||||
for mission in range(mission_count):
|
||||
stage_prefix += str(mission_order[mission]) + " "
|
||||
|
||||
spoiler_handle.write(stage_prefix)
|
||||
spoiler_handle.write("\n")
|
||||
|
||||
level_index += gate_len
|
||||
spoiler_handle.write("\n")
|
||||
|
||||
mission_count = mission_count_map[30]
|
||||
mission_order: typing.List[int] = mission_orders[mission_map[30]]
|
||||
stage_prefix: str = stage_name_prefixes[30]
|
||||
|
||||
for mission in range(mission_count):
|
||||
stage_prefix += str(mission_order[mission]) + " "
|
||||
|
||||
spoiler_handle.write(stage_prefix)
|
||||
spoiler_handle.write("\n\n")
|
||||
|
||||
@@ -5,7 +5,7 @@ emblem = "Emblem"
|
||||
market_token = "Chao Coin"
|
||||
|
||||
# Upgrade Definitions
|
||||
sonic_gloves = "Sonic - Magic Glove"
|
||||
sonic_gloves = "Sonic - Magic Gloves"
|
||||
sonic_light_shoes = "Sonic - Light Shoes"
|
||||
sonic_ancient_light = "Sonic - Ancient Light"
|
||||
sonic_bounce_bracelet = "Sonic - Bounce Bracelet"
|
||||
@@ -51,19 +51,34 @@ invincibility = "Invincibility"
|
||||
|
||||
|
||||
# Traps
|
||||
omochao_trap = "OmoTrap"
|
||||
timestop_trap = "Chaos Control Trap"
|
||||
confuse_trap = "Confusion Trap"
|
||||
tiny_trap = "Tiny Trap"
|
||||
gravity_trap = "Gravity Trap"
|
||||
exposition_trap = "Exposition Trap"
|
||||
darkness_trap = "Darkness Trap"
|
||||
ice_trap = "Ice Trap"
|
||||
slow_trap = "Slow Trap"
|
||||
cutscene_trap = "Cutscene Trap"
|
||||
reverse_trap = "Reverse Trap"
|
||||
omochao_trap = "OmoTrap"
|
||||
timestop_trap = "Chaos Control Trap"
|
||||
confuse_trap = "Confusion Trap"
|
||||
tiny_trap = "Tiny Trap"
|
||||
gravity_trap = "Gravity Trap"
|
||||
exposition_trap = "Exposition Trap"
|
||||
darkness_trap = "Darkness Trap"
|
||||
ice_trap = "Ice Trap"
|
||||
slow_trap = "Slow Trap"
|
||||
cutscene_trap = "Cutscene Trap"
|
||||
reverse_trap = "Reverse Trap"
|
||||
literature_trap = "Literature Trap"
|
||||
controller_drift_trap = "Controller Drift Trap"
|
||||
poison_trap = "Poison Trap"
|
||||
bee_trap = "Bee Trap"
|
||||
|
||||
pong_trap = "Pong Trap"
|
||||
pong_trap = "Pong Trap"
|
||||
breakout_trap = "Breakout Trap"
|
||||
fishing_trap = "Fishing Trap"
|
||||
trivia_trap = "Trivia Trap"
|
||||
pokemon_trivia_trap = "Pokemon Trivia Trap"
|
||||
pokemon_count_trap = "Pokemon Count Trap"
|
||||
number_sequence_trap = "Number Sequence Trap"
|
||||
light_up_path_trap = "Light Up Path Trap"
|
||||
pinball_trap = "Pinball Trap"
|
||||
math_quiz_trap = "Math Quiz Trap"
|
||||
snake_trap = "Snake Trap"
|
||||
input_sequence_trap = "Input Sequence Trap"
|
||||
|
||||
|
||||
# Chaos Emeralds
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,8 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from Options import Choice, Range, Toggle, DeathLink, DefaultOnToggle, OptionGroup, PerGameCommonOptions
|
||||
from Options import Choice, Range, Option, OptionGroup, Toggle, DeathLink, DefaultOnToggle, PerGameCommonOptions, PlandoBosses
|
||||
|
||||
from .Names import LocationName
|
||||
|
||||
|
||||
class Goal(Choice):
|
||||
@@ -22,6 +24,8 @@ class Goal(Choice):
|
||||
Boss Rush Chaos Emerald Hunt: Find the Seven Chaos Emeralds, then beat all of the bosses in the Boss Rush, ending with Finalhazard
|
||||
|
||||
Chaos Chao: Raise a Chaos Chao to win
|
||||
|
||||
Minigame Madness: Win a certain amount of each Minigame Trap, then defeat Finalhazard
|
||||
"""
|
||||
display_name = "Goal"
|
||||
option_biolizard = 0
|
||||
@@ -32,6 +36,7 @@ class Goal(Choice):
|
||||
option_cannons_core_boss_rush = 5
|
||||
option_boss_rush_chaos_emerald_hunt = 6
|
||||
option_chaos_chao = 7
|
||||
option_minigame_madness = 8
|
||||
default = 0
|
||||
|
||||
@classmethod
|
||||
@@ -71,6 +76,66 @@ class BossRushShuffle(Choice):
|
||||
default = 0
|
||||
|
||||
|
||||
class GateBossPlando(PlandoBosses):
|
||||
"""
|
||||
Possible Locations:
|
||||
"Gate 1 Boss"
|
||||
"Gate 2 Boss"
|
||||
"Gate 3 Boss"
|
||||
"Gate 4 Boss"
|
||||
"Gate 5 Boss"
|
||||
|
||||
Possible Bosses:
|
||||
"Sonic vs Shadow 1"
|
||||
"Sonic vs Shadow 2"
|
||||
"Tails vs Eggman 1"
|
||||
"Tails vs Eggman 2"
|
||||
"Knuckles vs Rouge 1"
|
||||
"BIG FOOT"
|
||||
"HOT SHOT"
|
||||
"FLYING DOG"
|
||||
"Egg Golem (Sonic)"
|
||||
"Egg Golem (Eggman)"
|
||||
"King Boom Boo"
|
||||
"""
|
||||
bosses = frozenset(LocationName.boss_names.keys())
|
||||
|
||||
locations = frozenset(LocationName.boss_gate_names.keys())
|
||||
|
||||
duplicate_bosses = False
|
||||
|
||||
@classmethod
|
||||
def can_place_boss(cls, boss: str, location: str) -> bool:
|
||||
return True
|
||||
|
||||
display_name = "Boss Shuffle"
|
||||
option_plando = 0
|
||||
|
||||
|
||||
class MinigameMadnessRequirement(Range):
|
||||
"""
|
||||
Determines how many of each Minigame Trap must be won (for Minigame Madness goal)
|
||||
|
||||
Receiving this many of a Minigame Trap will allow you to replay that minigame at-will in the Chao World lobby
|
||||
"""
|
||||
display_name = "Minigame Madness Trap Requirement"
|
||||
range_start = 1
|
||||
range_end = 10
|
||||
default = 3
|
||||
|
||||
|
||||
class MinigameMadnessMinimum(Range):
|
||||
"""
|
||||
Determines the minimum number of each Minigame Trap that are created (for Minigame Madness goal)
|
||||
|
||||
At least this many of each trap will be created as "Progression Traps", regardless of other trap option selections
|
||||
"""
|
||||
display_name = "Minigame Madness Trap Minimum"
|
||||
range_start = 1
|
||||
range_end = 10
|
||||
default = 5
|
||||
|
||||
|
||||
class BaseTrapWeight(Choice):
|
||||
"""
|
||||
Base Class for Trap Weights
|
||||
@@ -159,6 +224,34 @@ class ReverseTrapWeight(BaseTrapWeight):
|
||||
display_name = "Reverse Trap Weight"
|
||||
|
||||
|
||||
class LiteratureTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of receiving a trap which forces you to read
|
||||
"""
|
||||
display_name = "Literature Trap Weight"
|
||||
|
||||
|
||||
class ControllerDriftTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of receiving a trap which causes your control sticks to drift
|
||||
"""
|
||||
display_name = "Controller Drift Trap Weight"
|
||||
|
||||
|
||||
class PoisonTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of receiving a trap which causes you to lose rings over time
|
||||
"""
|
||||
display_name = "Poison Trap Weight"
|
||||
|
||||
|
||||
class BeeTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of receiving a trap which spawns a swarm of bees
|
||||
"""
|
||||
display_name = "Bee Trap Weight"
|
||||
|
||||
|
||||
class PongTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of receiving a trap which forces you to play a Pong minigame
|
||||
@@ -166,14 +259,106 @@ class PongTrapWeight(BaseTrapWeight):
|
||||
display_name = "Pong Trap Weight"
|
||||
|
||||
|
||||
class BreakoutTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of receiving a trap which forces you to play a Breakout minigame
|
||||
"""
|
||||
display_name = "Breakout Trap Weight"
|
||||
|
||||
|
||||
class FishingTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of receiving a trap which forces you to play a Fishing minigame
|
||||
"""
|
||||
display_name = "Fishing Trap Weight"
|
||||
|
||||
|
||||
class TriviaTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of receiving a trap which forces you to play a Trivia minigame
|
||||
"""
|
||||
display_name = "Trivia Trap Weight"
|
||||
|
||||
|
||||
class PokemonTriviaTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of receiving a trap which forces you to play a Pokemon Trivia minigame
|
||||
"""
|
||||
display_name = "Pokemon Trivia Trap Weight"
|
||||
|
||||
|
||||
class PokemonCountTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of receiving a trap which forces you to play a Pokemon Count minigame
|
||||
"""
|
||||
display_name = "Pokemon Count Trap Weight"
|
||||
|
||||
|
||||
class NumberSequenceTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of receiving a trap which forces you to play a Number Sequence minigame
|
||||
"""
|
||||
display_name = "Number Sequence Trap Weight"
|
||||
|
||||
|
||||
class LightUpPathTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of receiving a trap which forces you to play a Light Up Path minigame
|
||||
"""
|
||||
display_name = "Light Up Path Trap Weight"
|
||||
|
||||
|
||||
class PinballTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of receiving a trap which forces you to play a Pinball minigame
|
||||
"""
|
||||
display_name = "Pinball Trap Weight"
|
||||
|
||||
|
||||
class MathQuizTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of receiving a trap which forces you to solve a math problem
|
||||
"""
|
||||
display_name = "Math Quiz Trap Weight"
|
||||
|
||||
|
||||
class SnakeTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of receiving a trap which forces you to play a Snake minigame
|
||||
"""
|
||||
display_name = "Snake Trap Weight"
|
||||
|
||||
|
||||
class InputSequenceTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of receiving a trap which forces you to press a sequence of inputs
|
||||
"""
|
||||
display_name = "Input Sequence Trap Weight"
|
||||
|
||||
|
||||
class MinigameTrapDifficulty(Choice):
|
||||
"""
|
||||
How difficult any Minigame-style traps are
|
||||
Chaos causes the difficulty to be random per-minigame
|
||||
"""
|
||||
display_name = "Minigame Trap Difficulty"
|
||||
option_easy = 0
|
||||
option_medium = 1
|
||||
option_hard = 2
|
||||
option_chaos = 3
|
||||
default = 1
|
||||
|
||||
|
||||
class BigFishingDifficulty(Choice):
|
||||
"""
|
||||
How difficult Big's Fishing Minigames are
|
||||
Chaos causes the difficulty to be random per-minigame
|
||||
"""
|
||||
display_name = "Big Fishing Difficulty"
|
||||
option_easy = 0
|
||||
option_medium = 1
|
||||
option_hard = 2
|
||||
option_chaos = 3
|
||||
default = 1
|
||||
|
||||
|
||||
@@ -197,7 +382,7 @@ class TrapFillPercentage(Range):
|
||||
default = 0
|
||||
|
||||
|
||||
class Keysanity(Toggle):
|
||||
class Keysanity(DefaultOnToggle):
|
||||
"""
|
||||
Determines whether picking up Chao Keys grants checks
|
||||
(86 Locations)
|
||||
@@ -225,7 +410,7 @@ class Whistlesanity(Choice):
|
||||
default = 0
|
||||
|
||||
|
||||
class Beetlesanity(Toggle):
|
||||
class Beetlesanity(DefaultOnToggle):
|
||||
"""
|
||||
Determines whether destroying Gold Beetles grants checks
|
||||
(27 Locations)
|
||||
@@ -244,13 +429,35 @@ class Omosanity(Toggle):
|
||||
class Animalsanity(Toggle):
|
||||
"""
|
||||
Determines whether unique counts of animals grant checks.
|
||||
(421 Locations)
|
||||
(422 Locations)
|
||||
|
||||
ALL animals must be collected in a single run of a mission to get all checks.
|
||||
"""
|
||||
display_name = "Animalsanity"
|
||||
|
||||
|
||||
class ItemBoxsanity(Choice):
|
||||
"""
|
||||
Determines whether collecting Item Boxes grants checks
|
||||
None: No Item Boxes grant checks
|
||||
Extra Lives: Extra Life Boxes grant checks (94 Locations)
|
||||
All: All Item Boxes grant checks (502 Locations Total)
|
||||
"""
|
||||
display_name = "Itemboxsanity"
|
||||
option_none = 0
|
||||
option_extra_lives = 1
|
||||
option_all = 2
|
||||
default = 0
|
||||
|
||||
|
||||
class Bigsanity(Toggle):
|
||||
"""
|
||||
Determines whether helping Big fish grants checks.
|
||||
(32 Locations)
|
||||
"""
|
||||
display_name = "Bigsanity"
|
||||
|
||||
|
||||
class KartRaceChecks(Choice):
|
||||
"""
|
||||
Determines whether Kart Race Mode grants checks
|
||||
@@ -313,7 +520,7 @@ class LevelGateCosts(Choice):
|
||||
option_low = 0
|
||||
option_medium = 1
|
||||
option_high = 2
|
||||
default = 2
|
||||
default = 0
|
||||
|
||||
|
||||
class MaximumEmblemCap(Range):
|
||||
@@ -523,109 +730,214 @@ class BaseMissionCount(Range):
|
||||
default = 2
|
||||
|
||||
|
||||
class SpeedMissionCount(BaseMissionCount):
|
||||
class SonicMissionCount(BaseMissionCount):
|
||||
"""
|
||||
The number of active missions to include for Sonic and Shadow stages
|
||||
The number of active missions to include for Sonic stages
|
||||
"""
|
||||
display_name = "Speed Mission Count"
|
||||
display_name = "Sonic Mission Count"
|
||||
|
||||
|
||||
class SpeedMission2(DefaultOnToggle):
|
||||
class SonicMission2(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Sonic and Shadow 100 rings missions should be included
|
||||
Determines if the Sonic 100 rings missions should be included
|
||||
"""
|
||||
display_name = "Speed Mission 2"
|
||||
display_name = "Sonic Mission 2"
|
||||
|
||||
|
||||
class SpeedMission3(DefaultOnToggle):
|
||||
class SonicMission3(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Sonic and Shadow lost chao missions should be included
|
||||
Determines if the Sonic lost chao missions should be included
|
||||
"""
|
||||
display_name = "Speed Mission 3"
|
||||
display_name = "Sonic Mission 3"
|
||||
|
||||
|
||||
class SpeedMission4(DefaultOnToggle):
|
||||
class SonicMission4(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Sonic and Shadow time trial missions should be included
|
||||
Determines if the Sonic time trial missions should be included
|
||||
"""
|
||||
display_name = "Speed Mission 4"
|
||||
display_name = "Sonic Mission 4"
|
||||
|
||||
|
||||
class SpeedMission5(DefaultOnToggle):
|
||||
class SonicMission5(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Sonic and Shadow hard missions should be included
|
||||
Determines if the Sonic hard missions should be included
|
||||
"""
|
||||
display_name = "Speed Mission 5"
|
||||
display_name = "Sonic Mission 5"
|
||||
|
||||
|
||||
class MechMissionCount(BaseMissionCount):
|
||||
class ShadowMissionCount(BaseMissionCount):
|
||||
"""
|
||||
The number of active missions to include for Tails and Eggman stages
|
||||
The number of active missions to include for Shadow stages
|
||||
"""
|
||||
display_name = "Mech Mission Count"
|
||||
display_name = "Shadow Mission Count"
|
||||
|
||||
|
||||
class MechMission2(DefaultOnToggle):
|
||||
class ShadowMission2(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Tails and Eggman 100 rings missions should be included
|
||||
Determines if the Shadow 100 rings missions should be included
|
||||
"""
|
||||
display_name = "Mech Mission 2"
|
||||
display_name = "Shadow Mission 2"
|
||||
|
||||
|
||||
class MechMission3(DefaultOnToggle):
|
||||
class ShadowMission3(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Tails and Eggman lost chao missions should be included
|
||||
Determines if the Shadow lost chao missions should be included
|
||||
"""
|
||||
display_name = "Mech Mission 3"
|
||||
display_name = "Shadow Mission 3"
|
||||
|
||||
|
||||
class MechMission4(DefaultOnToggle):
|
||||
class ShadowMission4(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Tails and Eggman time trial missions should be included
|
||||
Determines if the Shadow time trial missions should be included
|
||||
"""
|
||||
display_name = "Mech Mission 4"
|
||||
display_name = "Shadow Mission 4"
|
||||
|
||||
|
||||
class MechMission5(DefaultOnToggle):
|
||||
class ShadowMission5(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Tails and Eggman hard missions should be included
|
||||
Determines if the Shadow hard missions should be included
|
||||
"""
|
||||
display_name = "Mech Mission 5"
|
||||
display_name = "Shadow Mission 5"
|
||||
|
||||
|
||||
class HuntMissionCount(BaseMissionCount):
|
||||
class TailsMissionCount(BaseMissionCount):
|
||||
"""
|
||||
The number of active missions to include for Knuckles and Rouge stages
|
||||
The number of active missions to include for Tails stages
|
||||
"""
|
||||
display_name = "Hunt Mission Count"
|
||||
display_name = "Tails Mission Count"
|
||||
|
||||
|
||||
class HuntMission2(DefaultOnToggle):
|
||||
class TailsMission2(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Knuckles and Rouge 100 rings missions should be included
|
||||
Determines if the Tails 100 rings missions should be included
|
||||
"""
|
||||
display_name = "Hunt Mission 2"
|
||||
display_name = "Tails Mission 2"
|
||||
|
||||
|
||||
class HuntMission3(DefaultOnToggle):
|
||||
class TailsMission3(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Knuckles and Rouge lost chao missions should be included
|
||||
Determines if the Tails lost chao missions should be included
|
||||
"""
|
||||
display_name = "Hunt Mission 3"
|
||||
display_name = "Tails Mission 3"
|
||||
|
||||
|
||||
class HuntMission4(DefaultOnToggle):
|
||||
class TailsMission4(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Knuckles and Rouge time trial missions should be included
|
||||
Determines if the Tails time trial missions should be included
|
||||
"""
|
||||
display_name = "Hunt Mission 4"
|
||||
display_name = "Tails Mission 4"
|
||||
|
||||
|
||||
class HuntMission5(DefaultOnToggle):
|
||||
class TailsMission5(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Knuckles and Rouge hard missions should be included
|
||||
Determines if the Tails hard missions should be included
|
||||
"""
|
||||
display_name = "Hunt Mission 5"
|
||||
display_name = "Tails Mission 5"
|
||||
|
||||
|
||||
class EggmanMissionCount(BaseMissionCount):
|
||||
"""
|
||||
The number of active missions to include for Eggman stages
|
||||
"""
|
||||
display_name = "Eggman Mission Count"
|
||||
|
||||
|
||||
class EggmanMission2(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Eggman 100 rings missions should be included
|
||||
"""
|
||||
display_name = "Eggman Mission 2"
|
||||
|
||||
|
||||
class EggmanMission3(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Eggman lost chao missions should be included
|
||||
"""
|
||||
display_name = "Eggman Mission 3"
|
||||
|
||||
|
||||
class EggmanMission4(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Eggman time trial missions should be included
|
||||
"""
|
||||
display_name = "Eggman Mission 4"
|
||||
|
||||
|
||||
class EggmanMission5(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Eggman hard missions should be included
|
||||
"""
|
||||
display_name = "Eggman Mission 5"
|
||||
|
||||
|
||||
class KnucklesMissionCount(BaseMissionCount):
|
||||
"""
|
||||
The number of active missions to include for Knuckles stages
|
||||
"""
|
||||
display_name = "Knuckles Mission Count"
|
||||
|
||||
|
||||
class KnucklesMission2(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Knuckles 100 rings missions should be included
|
||||
"""
|
||||
display_name = "Knuckles Mission 2"
|
||||
|
||||
|
||||
class KnucklesMission3(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Knuckles lost chao missions should be included
|
||||
"""
|
||||
display_name = "Knuckles Mission 3"
|
||||
|
||||
|
||||
class KnucklesMission4(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Knuckles time trial missions should be included
|
||||
"""
|
||||
display_name = "Knuckles Mission 4"
|
||||
|
||||
|
||||
class KnucklesMission5(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Knuckles hard missions should be included
|
||||
"""
|
||||
display_name = "Knuckles Mission 5"
|
||||
|
||||
|
||||
class RougeMissionCount(BaseMissionCount):
|
||||
"""
|
||||
The number of active missions to include for Rouge stages
|
||||
"""
|
||||
display_name = "Rouge Mission Count"
|
||||
|
||||
|
||||
class RougeMission2(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Rouge 100 rings missions should be included
|
||||
"""
|
||||
display_name = "Rouge Mission 2"
|
||||
|
||||
|
||||
class RougeMission3(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Rouge lost chao missions should be included
|
||||
"""
|
||||
display_name = "Rouge Mission 3"
|
||||
|
||||
|
||||
class RougeMission4(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Rouge time trial missions should be included
|
||||
"""
|
||||
display_name = "Rouge Mission 4"
|
||||
|
||||
|
||||
class RougeMission5(DefaultOnToggle):
|
||||
"""
|
||||
Determines if the Rouge hard missions should be included
|
||||
"""
|
||||
display_name = "Rouge Mission 5"
|
||||
|
||||
|
||||
class KartMissionCount(BaseMissionCount):
|
||||
@@ -706,7 +1018,7 @@ class RingLoss(Choice):
|
||||
|
||||
Modern: You lose 20 rings when hit
|
||||
|
||||
OHKO: You die immediately when hit (NOTE: Some Hard Logic tricks may require damage boosts!)
|
||||
OHKO: You die immediately when hit (NOTE: Some Hard or Expert Logic tricks may require damage boosts!)
|
||||
"""
|
||||
display_name = "Ring Loss"
|
||||
option_classic = 0
|
||||
@@ -729,6 +1041,16 @@ class RingLink(Toggle):
|
||||
display_name = "Ring Link"
|
||||
|
||||
|
||||
class TrapLink(Toggle):
|
||||
"""
|
||||
Whether your received traps are linked to other players
|
||||
|
||||
You will also receive any linked traps from other players with Trap Link enabled,
|
||||
if you have a weight above "none" set for that trap
|
||||
"""
|
||||
display_name = "Trap Link"
|
||||
|
||||
|
||||
class SADXMusic(Choice):
|
||||
"""
|
||||
Whether the randomizer will include Sonic Adventure DX Music in the music pool
|
||||
@@ -823,11 +1145,14 @@ class LogicDifficulty(Choice):
|
||||
|
||||
Standard: The logic assumes the "intended" usage of Upgrades to progress through levels
|
||||
|
||||
Hard: Some simple skips or sequence breaks may be required
|
||||
Hard: Some simple skips or sequence breaks may be required, but no out-of-bounds
|
||||
|
||||
Expert: If it is humanly possible, it may be required
|
||||
"""
|
||||
display_name = "Logic Difficulty"
|
||||
option_standard = 0
|
||||
option_hard = 1
|
||||
option_expert = 2
|
||||
default = 0
|
||||
|
||||
|
||||
@@ -835,6 +1160,8 @@ sa2b_option_groups = [
|
||||
OptionGroup("General Options", [
|
||||
Goal,
|
||||
BossRushShuffle,
|
||||
MinigameMadnessRequirement,
|
||||
MinigameMadnessMinimum,
|
||||
LogicDifficulty,
|
||||
RequiredRank,
|
||||
MaximumEmblemCap,
|
||||
@@ -854,6 +1181,8 @@ sa2b_option_groups = [
|
||||
Beetlesanity,
|
||||
Omosanity,
|
||||
Animalsanity,
|
||||
ItemBoxsanity,
|
||||
Bigsanity,
|
||||
KartRaceChecks,
|
||||
]),
|
||||
OptionGroup("Chao", [
|
||||
@@ -885,29 +1214,68 @@ sa2b_option_groups = [
|
||||
SlowTrapWeight,
|
||||
CutsceneTrapWeight,
|
||||
ReverseTrapWeight,
|
||||
LiteratureTrapWeight,
|
||||
ControllerDriftTrapWeight,
|
||||
PoisonTrapWeight,
|
||||
BeeTrapWeight,
|
||||
]),
|
||||
OptionGroup("Minigames", [
|
||||
PongTrapWeight,
|
||||
BreakoutTrapWeight,
|
||||
FishingTrapWeight,
|
||||
TriviaTrapWeight,
|
||||
PokemonTriviaTrapWeight,
|
||||
PokemonCountTrapWeight,
|
||||
NumberSequenceTrapWeight,
|
||||
LightUpPathTrapWeight,
|
||||
PinballTrapWeight,
|
||||
MathQuizTrapWeight,
|
||||
SnakeTrapWeight,
|
||||
InputSequenceTrapWeight,
|
||||
MinigameTrapDifficulty,
|
||||
BigFishingDifficulty,
|
||||
]),
|
||||
OptionGroup("Speed Missions", [
|
||||
SpeedMissionCount,
|
||||
SpeedMission2,
|
||||
SpeedMission3,
|
||||
SpeedMission4,
|
||||
SpeedMission5,
|
||||
OptionGroup("Sonic Missions", [
|
||||
SonicMissionCount,
|
||||
SonicMission2,
|
||||
SonicMission3,
|
||||
SonicMission4,
|
||||
SonicMission5,
|
||||
]),
|
||||
OptionGroup("Mech Missions", [
|
||||
MechMissionCount,
|
||||
MechMission2,
|
||||
MechMission3,
|
||||
MechMission4,
|
||||
MechMission5,
|
||||
OptionGroup("Shadow Missions", [
|
||||
ShadowMissionCount,
|
||||
ShadowMission2,
|
||||
ShadowMission3,
|
||||
ShadowMission4,
|
||||
ShadowMission5,
|
||||
]),
|
||||
OptionGroup("Hunt Missions", [
|
||||
HuntMissionCount,
|
||||
HuntMission2,
|
||||
HuntMission3,
|
||||
HuntMission4,
|
||||
HuntMission5,
|
||||
OptionGroup("Tails Missions", [
|
||||
TailsMissionCount,
|
||||
TailsMission2,
|
||||
TailsMission3,
|
||||
TailsMission4,
|
||||
TailsMission5,
|
||||
]),
|
||||
OptionGroup("Eggman Missions", [
|
||||
EggmanMissionCount,
|
||||
EggmanMission2,
|
||||
EggmanMission3,
|
||||
EggmanMission4,
|
||||
EggmanMission5,
|
||||
]),
|
||||
OptionGroup("Knuckles Missions", [
|
||||
KnucklesMissionCount,
|
||||
KnucklesMission2,
|
||||
KnucklesMission3,
|
||||
KnucklesMission4,
|
||||
KnucklesMission5,
|
||||
]),
|
||||
OptionGroup("Rouge Missions", [
|
||||
RougeMissionCount,
|
||||
RougeMission2,
|
||||
RougeMission3,
|
||||
RougeMission4,
|
||||
RougeMission5,
|
||||
]),
|
||||
OptionGroup("Kart Missions", [
|
||||
KartMissionCount,
|
||||
@@ -931,11 +1299,13 @@ sa2b_option_groups = [
|
||||
]),
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class SA2BOptions(PerGameCommonOptions):
|
||||
goal: Goal
|
||||
boss_rush_shuffle: BossRushShuffle
|
||||
minigame_madness_requirement: MinigameMadnessRequirement
|
||||
minigame_madness_minimum: MinigameMadnessMinimum
|
||||
gate_boss_plando: GateBossPlando
|
||||
logic_difficulty: LogicDifficulty
|
||||
required_rank: RequiredRank
|
||||
max_emblem_cap: MaximumEmblemCap
|
||||
@@ -953,6 +1323,8 @@ class SA2BOptions(PerGameCommonOptions):
|
||||
beetlesanity: Beetlesanity
|
||||
omosanity: Omosanity
|
||||
animalsanity: Animalsanity
|
||||
itemboxsanity: ItemBoxsanity
|
||||
bigsanity: Bigsanity
|
||||
kart_race_checks: KartRaceChecks
|
||||
|
||||
black_market_slots: BlackMarketSlots
|
||||
@@ -983,31 +1355,65 @@ class SA2BOptions(PerGameCommonOptions):
|
||||
slow_trap_weight: SlowTrapWeight
|
||||
cutscene_trap_weight: CutsceneTrapWeight
|
||||
reverse_trap_weight: ReverseTrapWeight
|
||||
literature_trap_weight: LiteratureTrapWeight
|
||||
controller_drift_trap_weight: ControllerDriftTrapWeight
|
||||
poison_trap_weight: PoisonTrapWeight
|
||||
bee_trap_weight: BeeTrapWeight
|
||||
pong_trap_weight: PongTrapWeight
|
||||
breakout_trap_weight: BreakoutTrapWeight
|
||||
fishing_trap_weight: FishingTrapWeight
|
||||
trivia_trap_weight: TriviaTrapWeight
|
||||
pokemon_trivia_trap_weight: PokemonTriviaTrapWeight
|
||||
pokemon_count_trap_weight: PokemonCountTrapWeight
|
||||
number_sequence_trap_weight: NumberSequenceTrapWeight
|
||||
light_up_path_trap_weight: LightUpPathTrapWeight
|
||||
pinball_trap_weight: PinballTrapWeight
|
||||
math_quiz_trap_weight: MathQuizTrapWeight
|
||||
snake_trap_weight: SnakeTrapWeight
|
||||
input_sequence_trap_weight: InputSequenceTrapWeight
|
||||
minigame_trap_difficulty: MinigameTrapDifficulty
|
||||
big_fishing_difficulty: BigFishingDifficulty
|
||||
|
||||
sadx_music: SADXMusic
|
||||
music_shuffle: MusicShuffle
|
||||
voice_shuffle: VoiceShuffle
|
||||
narrator: Narrator
|
||||
|
||||
speed_mission_count: SpeedMissionCount
|
||||
speed_mission_2: SpeedMission2
|
||||
speed_mission_3: SpeedMission3
|
||||
speed_mission_4: SpeedMission4
|
||||
speed_mission_5: SpeedMission5
|
||||
sonic_mission_count: SonicMissionCount
|
||||
sonic_mission_2: SonicMission2
|
||||
sonic_mission_3: SonicMission3
|
||||
sonic_mission_4: SonicMission4
|
||||
sonic_mission_5: SonicMission5
|
||||
|
||||
mech_mission_count: MechMissionCount
|
||||
mech_mission_2: MechMission2
|
||||
mech_mission_3: MechMission3
|
||||
mech_mission_4: MechMission4
|
||||
mech_mission_5: MechMission5
|
||||
shadow_mission_count: ShadowMissionCount
|
||||
shadow_mission_2: ShadowMission2
|
||||
shadow_mission_3: ShadowMission3
|
||||
shadow_mission_4: ShadowMission4
|
||||
shadow_mission_5: ShadowMission5
|
||||
|
||||
hunt_mission_count: HuntMissionCount
|
||||
hunt_mission_2: HuntMission2
|
||||
hunt_mission_3: HuntMission3
|
||||
hunt_mission_4: HuntMission4
|
||||
hunt_mission_5: HuntMission5
|
||||
tails_mission_count: TailsMissionCount
|
||||
tails_mission_2: TailsMission2
|
||||
tails_mission_3: TailsMission3
|
||||
tails_mission_4: TailsMission4
|
||||
tails_mission_5: TailsMission5
|
||||
|
||||
eggman_mission_count: EggmanMissionCount
|
||||
eggman_mission_2: EggmanMission2
|
||||
eggman_mission_3: EggmanMission3
|
||||
eggman_mission_4: EggmanMission4
|
||||
eggman_mission_5: EggmanMission5
|
||||
|
||||
knuckles_mission_count: KnucklesMissionCount
|
||||
knuckles_mission_2: KnucklesMission2
|
||||
knuckles_mission_3: KnucklesMission3
|
||||
knuckles_mission_4: KnucklesMission4
|
||||
knuckles_mission_5: KnucklesMission5
|
||||
|
||||
rouge_mission_count: RougeMissionCount
|
||||
rouge_mission_2: RougeMission2
|
||||
rouge_mission_3: RougeMission3
|
||||
rouge_mission_4: RougeMission4
|
||||
rouge_mission_5: RougeMission5
|
||||
|
||||
kart_mission_count: KartMissionCount
|
||||
kart_mission_2: KartMission2
|
||||
@@ -1022,4 +1428,5 @@ class SA2BOptions(PerGameCommonOptions):
|
||||
cannons_core_mission_5: CannonsCoreMission5
|
||||
|
||||
ring_link: RingLink
|
||||
trap_link: TrapLink
|
||||
death_link: DeathLink
|
||||
|
||||
502
worlds/sa2b/Presets.py
Normal file
502
worlds/sa2b/Presets.py
Normal file
@@ -0,0 +1,502 @@
|
||||
from typing import Dict, Any
|
||||
|
||||
from .Options import *
|
||||
|
||||
minsanity = {
|
||||
"goal": Goal.option_chaos_chao,
|
||||
"max_emblem_cap": MaximumEmblemCap.range_start,
|
||||
|
||||
"keysanity": False,
|
||||
"whistlesanity": Whistlesanity.option_none,
|
||||
"beetlesanity": False,
|
||||
"omosanity": False,
|
||||
"animalsanity": False,
|
||||
"itemboxsanity": ItemBoxsanity.option_none,
|
||||
"bigsanity": False,
|
||||
"kart_race_checks": KartRaceChecks.option_none,
|
||||
|
||||
"junk_fill_percentage": 0,
|
||||
|
||||
"sonic_mission_count": BaseMissionCount.range_start,
|
||||
"sonic_mission_2": False,
|
||||
"sonic_mission_3": False,
|
||||
"sonic_mission_4": False,
|
||||
"sonic_mission_5": False,
|
||||
|
||||
"shadow_mission_count": BaseMissionCount.range_start,
|
||||
"shadow_mission_2": False,
|
||||
"shadow_mission_3": False,
|
||||
"shadow_mission_4": False,
|
||||
"shadow_mission_5": False,
|
||||
|
||||
"tails_mission_count": BaseMissionCount.range_start,
|
||||
"tails_mission_2": False,
|
||||
"tails_mission_3": False,
|
||||
"tails_mission_4": False,
|
||||
"tails_mission_5": False,
|
||||
|
||||
"eggman_mission_count": BaseMissionCount.range_start,
|
||||
"eggman_mission_2": False,
|
||||
"eggman_mission_3": False,
|
||||
"eggman_mission_4": False,
|
||||
"eggman_mission_5": False,
|
||||
|
||||
"knuckles_mission_count": BaseMissionCount.range_start,
|
||||
"knuckles_mission_2": False,
|
||||
"knuckles_mission_3": False,
|
||||
"knuckles_mission_4": False,
|
||||
"knuckles_mission_5": False,
|
||||
|
||||
"rouge_mission_count": BaseMissionCount.range_start,
|
||||
"rouge_mission_2": False,
|
||||
"rouge_mission_3": False,
|
||||
"rouge_mission_4": False,
|
||||
"rouge_mission_5": False,
|
||||
|
||||
"kart_mission_count": BaseMissionCount.range_start,
|
||||
"kart_mission_2": False,
|
||||
"kart_mission_3": False,
|
||||
"kart_mission_4": False,
|
||||
"kart_mission_5": False,
|
||||
|
||||
"cannons_core_mission_count": BaseMissionCount.range_start,
|
||||
"cannons_core_mission_2": False,
|
||||
"cannons_core_mission_3": False,
|
||||
"cannons_core_mission_4": False,
|
||||
"cannons_core_mission_5": False,
|
||||
}
|
||||
|
||||
chao_centric = {
|
||||
"goal": Goal.option_chaos_chao,
|
||||
|
||||
"keysanity": False,
|
||||
"whistlesanity": Whistlesanity.option_none,
|
||||
"beetlesanity": False,
|
||||
"omosanity": False,
|
||||
"animalsanity": False,
|
||||
"itemboxsanity": ItemBoxsanity.option_none,
|
||||
"bigsanity": False,
|
||||
"kart_race_checks": KartRaceChecks.option_none,
|
||||
|
||||
"black_market_slots": BlackMarketSlots.range_end,
|
||||
"black_market_unlock_costs": BlackMarketUnlockCosts.option_high,
|
||||
"chao_race_difficulty": ChaoRaceDifficulty.option_expert,
|
||||
"chao_karate_difficulty": ChaoKarateDifficulty.option_super,
|
||||
"chao_stadium_checks": ChaoStadiumChecks.option_all,
|
||||
"chao_animal_parts": True,
|
||||
"chao_stats": ChaoStats.range_end,
|
||||
"chao_stats_frequency": 1,
|
||||
"chao_stats_stamina": True,
|
||||
"chao_stats_hidden": True,
|
||||
"chao_kindergarten": ChaoKindergarten.option_full,
|
||||
|
||||
"junk_fill_percentage": 50,
|
||||
|
||||
"sonic_mission_count": BaseMissionCount.range_start,
|
||||
"sonic_mission_2": False,
|
||||
"sonic_mission_3": False,
|
||||
"sonic_mission_4": False,
|
||||
"sonic_mission_5": False,
|
||||
|
||||
"shadow_mission_count": BaseMissionCount.range_start,
|
||||
"shadow_mission_2": False,
|
||||
"shadow_mission_3": False,
|
||||
"shadow_mission_4": False,
|
||||
"shadow_mission_5": False,
|
||||
|
||||
"tails_mission_count": BaseMissionCount.range_start,
|
||||
"tails_mission_2": False,
|
||||
"tails_mission_3": False,
|
||||
"tails_mission_4": False,
|
||||
"tails_mission_5": False,
|
||||
|
||||
"eggman_mission_count": BaseMissionCount.range_start,
|
||||
"eggman_mission_2": False,
|
||||
"eggman_mission_3": False,
|
||||
"eggman_mission_4": False,
|
||||
"eggman_mission_5": False,
|
||||
|
||||
"knuckles_mission_count": BaseMissionCount.range_start,
|
||||
"knuckles_mission_2": False,
|
||||
"knuckles_mission_3": False,
|
||||
"knuckles_mission_4": False,
|
||||
"knuckles_mission_5": False,
|
||||
|
||||
"rouge_mission_count": BaseMissionCount.range_start,
|
||||
"rouge_mission_2": False,
|
||||
"rouge_mission_3": False,
|
||||
"rouge_mission_4": False,
|
||||
"rouge_mission_5": False,
|
||||
|
||||
"kart_mission_count": BaseMissionCount.range_start,
|
||||
"kart_mission_2": False,
|
||||
"kart_mission_3": False,
|
||||
"kart_mission_4": False,
|
||||
"kart_mission_5": False,
|
||||
|
||||
"cannons_core_mission_count": BaseMissionCount.range_start,
|
||||
"cannons_core_mission_2": False,
|
||||
"cannons_core_mission_3": False,
|
||||
"cannons_core_mission_4": False,
|
||||
"cannons_core_mission_5": False,
|
||||
}
|
||||
|
||||
allsanity_no_chao = {
|
||||
"goal": Goal.option_cannons_core_boss_rush,
|
||||
"boss_rush_shuffle": BossRushShuffle.option_chaos,
|
||||
"minigame_madness_requirement": MinigameMadnessRequirement.range_end,
|
||||
"minigame_madness_minimum": MinigameMadnessMinimum.range_end,
|
||||
"max_emblem_cap": MaximumEmblemCap.range_end,
|
||||
|
||||
"mission_shuffle": True,
|
||||
"required_cannons_core_missions": RequiredCannonsCoreMissions.option_all_active,
|
||||
"emblem_percentage_for_cannons_core": EmblemPercentageForCannonsCore.range_end,
|
||||
"number_of_level_gates": NumberOfLevelGates.range_end,
|
||||
"level_gate_costs": LevelGateCosts.option_high,
|
||||
|
||||
"keysanity": True,
|
||||
"whistlesanity": Whistlesanity.option_both,
|
||||
"beetlesanity": True,
|
||||
"omosanity": True,
|
||||
"animalsanity": True,
|
||||
"itemboxsanity": ItemBoxsanity.option_all,
|
||||
"bigsanity": True,
|
||||
"kart_race_checks": KartRaceChecks.option_full,
|
||||
|
||||
"junk_fill_percentage": 25,
|
||||
"trap_fill_percentage": 25,
|
||||
"omochao_trap_weight": BaseTrapWeight.option_high,
|
||||
"timestop_trap_weight": BaseTrapWeight.option_high,
|
||||
"confusion_trap_weight": BaseTrapWeight.option_high,
|
||||
"tiny_trap_weight": BaseTrapWeight.option_high,
|
||||
"gravity_trap_weight": BaseTrapWeight.option_high,
|
||||
"exposition_trap_weight": BaseTrapWeight.option_high,
|
||||
"ice_trap_weight": BaseTrapWeight.option_high,
|
||||
"slow_trap_weight": BaseTrapWeight.option_high,
|
||||
"cutscene_trap_weight": BaseTrapWeight.option_high,
|
||||
"reverse_trap_weight": BaseTrapWeight.option_high,
|
||||
"literature_trap_weight": BaseTrapWeight.option_high,
|
||||
"controller_drift_trap_weight": BaseTrapWeight.option_high,
|
||||
"poison_trap_weight": BaseTrapWeight.option_high,
|
||||
"bee_trap_weight": BaseTrapWeight.option_high,
|
||||
"pong_trap_weight": BaseTrapWeight.option_high,
|
||||
"breakout_trap_weight": BaseTrapWeight.option_high,
|
||||
"fishing_trap_weight": BaseTrapWeight.option_high,
|
||||
"trivia_trap_weight": BaseTrapWeight.option_high,
|
||||
"pokemon_trivia_trap_weight": BaseTrapWeight.option_high,
|
||||
"pokemon_count_trap_weight": BaseTrapWeight.option_high,
|
||||
"number_sequence_trap_weight": BaseTrapWeight.option_high,
|
||||
"light_up_path_trap_weight": BaseTrapWeight.option_high,
|
||||
"pinball_trap_weight": BaseTrapWeight.option_high,
|
||||
"math_quiz_trap_weight": BaseTrapWeight.option_high,
|
||||
"snake_trap_weight": BaseTrapWeight.option_high,
|
||||
"input_sequence_trap_weight": BaseTrapWeight.option_high,
|
||||
"minigame_trap_difficulty": MinigameTrapDifficulty.option_chaos,
|
||||
"big_fishing_difficulty": BigFishingDifficulty.option_chaos,
|
||||
|
||||
"music_shuffle": MusicShuffle.option_full,
|
||||
"voice_shuffle": VoiceShuffle.option_shuffled,
|
||||
|
||||
"sonic_mission_count": BaseMissionCount.range_end,
|
||||
"sonic_mission_2": True,
|
||||
"sonic_mission_3": True,
|
||||
"sonic_mission_4": True,
|
||||
"sonic_mission_5": True,
|
||||
|
||||
"shadow_mission_count": BaseMissionCount.range_end,
|
||||
"shadow_mission_2": True,
|
||||
"shadow_mission_3": True,
|
||||
"shadow_mission_4": True,
|
||||
"shadow_mission_5": True,
|
||||
|
||||
"tails_mission_count": BaseMissionCount.range_end,
|
||||
"tails_mission_2": True,
|
||||
"tails_mission_3": True,
|
||||
"tails_mission_4": True,
|
||||
"tails_mission_5": True,
|
||||
|
||||
"eggman_mission_count": BaseMissionCount.range_end,
|
||||
"eggman_mission_2": True,
|
||||
"eggman_mission_3": True,
|
||||
"eggman_mission_4": True,
|
||||
"eggman_mission_5": True,
|
||||
|
||||
"knuckles_mission_count": BaseMissionCount.range_end,
|
||||
"knuckles_mission_2": True,
|
||||
"knuckles_mission_3": True,
|
||||
"knuckles_mission_4": True,
|
||||
"knuckles_mission_5": True,
|
||||
|
||||
"rouge_mission_count": BaseMissionCount.range_end,
|
||||
"rouge_mission_2": True,
|
||||
"rouge_mission_3": True,
|
||||
"rouge_mission_4": True,
|
||||
"rouge_mission_5": True,
|
||||
|
||||
"kart_mission_count": BaseMissionCount.range_end,
|
||||
"kart_mission_2": True,
|
||||
"kart_mission_3": True,
|
||||
"kart_mission_4": True,
|
||||
"kart_mission_5": True,
|
||||
|
||||
"cannons_core_mission_count": BaseMissionCount.range_end,
|
||||
"cannons_core_mission_2": True,
|
||||
"cannons_core_mission_3": True,
|
||||
"cannons_core_mission_4": True,
|
||||
"cannons_core_mission_5": True,
|
||||
}
|
||||
|
||||
allsanity = {
|
||||
"goal": Goal.option_cannons_core_boss_rush,
|
||||
"boss_rush_shuffle": BossRushShuffle.option_chaos,
|
||||
"minigame_madness_requirement": MinigameMadnessRequirement.range_end,
|
||||
"minigame_madness_minimum": MinigameMadnessMinimum.range_end,
|
||||
"max_emblem_cap": MaximumEmblemCap.range_end,
|
||||
|
||||
"mission_shuffle": True,
|
||||
"required_cannons_core_missions": RequiredCannonsCoreMissions.option_all_active,
|
||||
"emblem_percentage_for_cannons_core": EmblemPercentageForCannonsCore.range_end,
|
||||
"number_of_level_gates": NumberOfLevelGates.range_end,
|
||||
"level_gate_costs": LevelGateCosts.option_high,
|
||||
|
||||
"keysanity": True,
|
||||
"whistlesanity": Whistlesanity.option_both,
|
||||
"beetlesanity": True,
|
||||
"omosanity": True,
|
||||
"animalsanity": True,
|
||||
"itemboxsanity": ItemBoxsanity.option_all,
|
||||
"bigsanity": True,
|
||||
"kart_race_checks": KartRaceChecks.option_full,
|
||||
|
||||
"black_market_slots": BlackMarketSlots.range_end,
|
||||
"black_market_unlock_costs": BlackMarketUnlockCosts.option_high,
|
||||
"chao_race_difficulty": ChaoRaceDifficulty.option_expert,
|
||||
"chao_karate_difficulty": ChaoKarateDifficulty.option_super,
|
||||
"chao_stadium_checks": ChaoStadiumChecks.option_all,
|
||||
"chao_animal_parts": True,
|
||||
"chao_stats": ChaoStats.range_end,
|
||||
"chao_stats_frequency": 1,
|
||||
"chao_stats_stamina": True,
|
||||
"chao_stats_hidden": True,
|
||||
"chao_kindergarten": ChaoKindergarten.option_full,
|
||||
|
||||
"junk_fill_percentage": 25,
|
||||
"trap_fill_percentage": 25,
|
||||
"omochao_trap_weight": BaseTrapWeight.option_high,
|
||||
"timestop_trap_weight": BaseTrapWeight.option_high,
|
||||
"confusion_trap_weight": BaseTrapWeight.option_high,
|
||||
"tiny_trap_weight": BaseTrapWeight.option_high,
|
||||
"gravity_trap_weight": BaseTrapWeight.option_high,
|
||||
"exposition_trap_weight": BaseTrapWeight.option_high,
|
||||
"ice_trap_weight": BaseTrapWeight.option_high,
|
||||
"slow_trap_weight": BaseTrapWeight.option_high,
|
||||
"cutscene_trap_weight": BaseTrapWeight.option_high,
|
||||
"reverse_trap_weight": BaseTrapWeight.option_high,
|
||||
"literature_trap_weight": BaseTrapWeight.option_high,
|
||||
"controller_drift_trap_weight": BaseTrapWeight.option_high,
|
||||
"poison_trap_weight": BaseTrapWeight.option_high,
|
||||
"bee_trap_weight": BaseTrapWeight.option_high,
|
||||
"pong_trap_weight": BaseTrapWeight.option_high,
|
||||
"breakout_trap_weight": BaseTrapWeight.option_high,
|
||||
"fishing_trap_weight": BaseTrapWeight.option_high,
|
||||
"trivia_trap_weight": BaseTrapWeight.option_high,
|
||||
"pokemon_trivia_trap_weight": BaseTrapWeight.option_high,
|
||||
"pokemon_count_trap_weight": BaseTrapWeight.option_high,
|
||||
"number_sequence_trap_weight": BaseTrapWeight.option_high,
|
||||
"light_up_path_trap_weight": BaseTrapWeight.option_high,
|
||||
"pinball_trap_weight": BaseTrapWeight.option_high,
|
||||
"math_quiz_trap_weight": BaseTrapWeight.option_high,
|
||||
"snake_trap_weight": BaseTrapWeight.option_high,
|
||||
"input_sequence_trap_weight": BaseTrapWeight.option_high,
|
||||
"minigame_trap_difficulty": MinigameTrapDifficulty.option_chaos,
|
||||
"big_fishing_difficulty": BigFishingDifficulty.option_chaos,
|
||||
|
||||
"music_shuffle": MusicShuffle.option_full,
|
||||
"voice_shuffle": VoiceShuffle.option_shuffled,
|
||||
|
||||
"sonic_mission_count": BaseMissionCount.range_end,
|
||||
"sonic_mission_2": True,
|
||||
"sonic_mission_3": True,
|
||||
"sonic_mission_4": True,
|
||||
"sonic_mission_5": True,
|
||||
|
||||
"shadow_mission_count": BaseMissionCount.range_end,
|
||||
"shadow_mission_2": True,
|
||||
"shadow_mission_3": True,
|
||||
"shadow_mission_4": True,
|
||||
"shadow_mission_5": True,
|
||||
|
||||
"tails_mission_count": BaseMissionCount.range_end,
|
||||
"tails_mission_2": True,
|
||||
"tails_mission_3": True,
|
||||
"tails_mission_4": True,
|
||||
"tails_mission_5": True,
|
||||
|
||||
"eggman_mission_count": BaseMissionCount.range_end,
|
||||
"eggman_mission_2": True,
|
||||
"eggman_mission_3": True,
|
||||
"eggman_mission_4": True,
|
||||
"eggman_mission_5": True,
|
||||
|
||||
"knuckles_mission_count": BaseMissionCount.range_end,
|
||||
"knuckles_mission_2": True,
|
||||
"knuckles_mission_3": True,
|
||||
"knuckles_mission_4": True,
|
||||
"knuckles_mission_5": True,
|
||||
|
||||
"rouge_mission_count": BaseMissionCount.range_end,
|
||||
"rouge_mission_2": True,
|
||||
"rouge_mission_3": True,
|
||||
"rouge_mission_4": True,
|
||||
"rouge_mission_5": True,
|
||||
|
||||
"kart_mission_count": BaseMissionCount.range_end,
|
||||
"kart_mission_2": True,
|
||||
"kart_mission_3": True,
|
||||
"kart_mission_4": True,
|
||||
"kart_mission_5": True,
|
||||
|
||||
"cannons_core_mission_count": BaseMissionCount.range_end,
|
||||
"cannons_core_mission_2": True,
|
||||
"cannons_core_mission_3": True,
|
||||
"cannons_core_mission_4": True,
|
||||
"cannons_core_mission_5": True,
|
||||
}
|
||||
|
||||
all_random = {
|
||||
"goal": "random",
|
||||
"boss_rush_shuffle": "random",
|
||||
"minigame_madness_requirement": "random",
|
||||
"minigame_madness_minimum": "random",
|
||||
"logic_difficulty": "random",
|
||||
"required_rank": "random",
|
||||
"max_emblem_cap": "random",
|
||||
"ring_loss": "random",
|
||||
|
||||
"mission_shuffle": "random",
|
||||
"required_cannons_core_missions": "random",
|
||||
"emblem_percentage_for_cannons_core": "random",
|
||||
"number_of_level_gates": "random",
|
||||
"level_gate_distribution": "random",
|
||||
"level_gate_costs": "random",
|
||||
|
||||
"keysanity": "random",
|
||||
"whistlesanity": "random",
|
||||
"beetlesanity": "random",
|
||||
"omosanity": "random",
|
||||
"animalsanity": "random",
|
||||
"itemboxsanity": "random",
|
||||
"bigsanity": "random",
|
||||
"kart_race_checks": "random",
|
||||
|
||||
"black_market_slots": "random",
|
||||
"black_market_unlock_costs": "random",
|
||||
"black_market_price_multiplier": "random",
|
||||
"chao_race_difficulty": "random",
|
||||
"chao_karate_difficulty": "random",
|
||||
"chao_stadium_checks": "random",
|
||||
"chao_animal_parts": "random",
|
||||
"chao_stats": "random",
|
||||
"chao_stats_frequency": "random",
|
||||
"chao_stats_stamina": "random",
|
||||
"chao_stats_hidden": "random",
|
||||
"chao_kindergarten": "random",
|
||||
"shuffle_starting_chao_eggs": "random",
|
||||
"chao_entrance_randomization": "random",
|
||||
|
||||
"junk_fill_percentage": "random",
|
||||
"trap_fill_percentage": "random",
|
||||
"omochao_trap_weight": "random",
|
||||
"timestop_trap_weight": "random",
|
||||
"confusion_trap_weight": "random",
|
||||
"tiny_trap_weight": "random",
|
||||
"gravity_trap_weight": "random",
|
||||
"exposition_trap_weight": "random",
|
||||
"ice_trap_weight": "random",
|
||||
"slow_trap_weight": "random",
|
||||
"cutscene_trap_weight": "random",
|
||||
"reverse_trap_weight": "random",
|
||||
"literature_trap_weight": "random",
|
||||
"controller_drift_trap_weight": "random",
|
||||
"poison_trap_weight": "random",
|
||||
"bee_trap_weight": "random",
|
||||
"pong_trap_weight": "random",
|
||||
"breakout_trap_weight": "random",
|
||||
"fishing_trap_weight": "random",
|
||||
"trivia_trap_weight": "random",
|
||||
"pokemon_trivia_trap_weight": "random",
|
||||
"pokemon_count_trap_weight": "random",
|
||||
"number_sequence_trap_weight": "random",
|
||||
"light_up_path_trap_weight": "random",
|
||||
"pinball_trap_weight": "random",
|
||||
"math_quiz_trap_weight": "random",
|
||||
"snake_trap_weight": "random",
|
||||
"input_sequence_trap_weight": "random",
|
||||
"minigame_trap_difficulty": "random",
|
||||
"big_fishing_difficulty": "random",
|
||||
|
||||
"sadx_music": "random",
|
||||
"music_shuffle": "random",
|
||||
"voice_shuffle": "random",
|
||||
"narrator": "random",
|
||||
|
||||
"sonic_mission_count": "random",
|
||||
"sonic_mission_2": "random",
|
||||
"sonic_mission_3": "random",
|
||||
"sonic_mission_4": "random",
|
||||
"sonic_mission_5": "random",
|
||||
|
||||
"shadow_mission_count": "random",
|
||||
"shadow_mission_2": "random",
|
||||
"shadow_mission_3": "random",
|
||||
"shadow_mission_4": "random",
|
||||
"shadow_mission_5": "random",
|
||||
|
||||
"tails_mission_count": "random",
|
||||
"tails_mission_2": "random",
|
||||
"tails_mission_3": "random",
|
||||
"tails_mission_4": "random",
|
||||
"tails_mission_5": "random",
|
||||
|
||||
"eggman_mission_count": "random",
|
||||
"eggman_mission_2": "random",
|
||||
"eggman_mission_3": "random",
|
||||
"eggman_mission_4": "random",
|
||||
"eggman_mission_5": "random",
|
||||
|
||||
"knuckles_mission_count": "random",
|
||||
"knuckles_mission_2": "random",
|
||||
"knuckles_mission_3": "random",
|
||||
"knuckles_mission_4": "random",
|
||||
"knuckles_mission_5": "random",
|
||||
|
||||
"rouge_mission_count": "random",
|
||||
"rouge_mission_2": "random",
|
||||
"rouge_mission_3": "random",
|
||||
"rouge_mission_4": "random",
|
||||
"rouge_mission_5": "random",
|
||||
|
||||
"kart_mission_count": "random",
|
||||
"kart_mission_2": "random",
|
||||
"kart_mission_3": "random",
|
||||
"kart_mission_4": "random",
|
||||
"kart_mission_5": "random",
|
||||
|
||||
"cannons_core_mission_count": "random",
|
||||
"cannons_core_mission_2": "random",
|
||||
"cannons_core_mission_3": "random",
|
||||
"cannons_core_mission_4": "random",
|
||||
"cannons_core_mission_5": "random",
|
||||
|
||||
"ring_link": "random",
|
||||
"trap_link": "random",
|
||||
"death_link": "random",
|
||||
}
|
||||
|
||||
sa2b_options_presets: Dict[str, Dict[str, Any]] = {
|
||||
"Minsanity": minsanity,
|
||||
"Chao-centric": chao_centric,
|
||||
"Allsanity No Chao": allsanity_no_chao,
|
||||
"Allsanity": allsanity,
|
||||
"All Random": all_random,
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
2115
worlds/sa2b/Rules.py
2115
worlds/sa2b/Rules.py
File diff suppressed because it is too large
Load Diff
@@ -8,12 +8,13 @@ from worlds.AutoWorld import WebWorld, World
|
||||
from .AestheticData import chao_name_conversion, sample_chao_names, totally_real_item_names, \
|
||||
all_exits, all_destinations, multi_rooms, single_rooms, room_to_exits_map, exit_to_room_map, valid_kindergarten_exits
|
||||
from .GateBosses import get_gate_bosses, get_boss_rush_bosses, get_boss_name
|
||||
from .Items import SA2BItem, ItemData, item_table, upgrades_table, emeralds_table, junk_table, trap_table, item_groups, \
|
||||
eggs_table, fruits_table, seeds_table, hats_table, animals_table, chaos_drives_table
|
||||
from .Locations import SA2BLocation, all_locations, setup_locations, chao_animal_event_location_table, black_market_location_table
|
||||
from .Missions import get_mission_table, get_mission_count_table, get_first_and_last_cannons_core_missions
|
||||
from .Items import SA2BItem, ItemData, item_table, upgrades_table, emeralds_table, junk_table, minigame_trap_table, item_groups, \
|
||||
eggs_table, fruits_table, seeds_table, hats_table, animals_table, chaos_drives_table, event_table
|
||||
from .Locations import SA2BLocation, all_locations, location_groups, setup_locations, chao_animal_event_location_table, black_market_location_table
|
||||
from .Missions import get_mission_table, get_mission_count_table, get_first_and_last_cannons_core_missions, print_mission_orders_to_spoiler
|
||||
from .Names import ItemName, LocationName
|
||||
from .Options import SA2BOptions, sa2b_option_groups
|
||||
from .Presets import sa2b_options_presets
|
||||
from .Regions import create_regions, shuffleable_regions, connect_regions, LevelGate, gate_0_whitelist_regions, \
|
||||
gate_0_blacklist_regions
|
||||
from .Rules import set_rules
|
||||
@@ -33,6 +34,7 @@ class SA2BWeb(WebWorld):
|
||||
|
||||
tutorials = [setup_en]
|
||||
option_groups = sa2b_option_groups
|
||||
options_presets = sa2b_options_presets
|
||||
|
||||
|
||||
def check_for_impossible_shuffle(shuffled_levels: typing.List[int], gate_0_range: int, multiworld: MultiWorld):
|
||||
@@ -60,11 +62,14 @@ class SA2BWorld(World):
|
||||
topology_present = False
|
||||
|
||||
item_name_groups = item_groups
|
||||
location_name_groups = location_groups
|
||||
item_name_to_id = {name: data.code for name, data in item_table.items()}
|
||||
location_name_to_id = all_locations
|
||||
|
||||
location_table: typing.Dict[str, int]
|
||||
|
||||
shuffled_region_list: typing.List[int]
|
||||
levels_per_gate: typing.List[int]
|
||||
mission_map: typing.Dict[int, int]
|
||||
mission_count_map: typing.Dict[int, int]
|
||||
emblems_for_cannons_core: int
|
||||
@@ -78,7 +83,7 @@ class SA2BWorld(World):
|
||||
|
||||
def fill_slot_data(self) -> dict:
|
||||
return {
|
||||
"ModVersion": 203,
|
||||
"ModVersion": 204,
|
||||
"Goal": self.options.goal.value,
|
||||
"MusicMap": self.generate_music_data(),
|
||||
"VoiceMap": self.generate_voice_data(),
|
||||
@@ -89,14 +94,20 @@ class SA2BWorld(World):
|
||||
"MusicShuffle": self.options.music_shuffle.value,
|
||||
"Narrator": self.options.narrator.value,
|
||||
"MinigameTrapDifficulty": self.options.minigame_trap_difficulty.value,
|
||||
"BigFishingDifficulty": self.options.big_fishing_difficulty.value,
|
||||
"RingLoss": self.options.ring_loss.value,
|
||||
"RingLink": self.options.ring_link.value,
|
||||
"TrapLink": self.options.trap_link.value,
|
||||
"RequiredRank": self.options.required_rank.value,
|
||||
"MinigameMadnessAmount": self.options.minigame_madness_requirement.value,
|
||||
"LogicDifficulty": self.options.logic_difficulty.value,
|
||||
"ChaoKeys": self.options.keysanity.value,
|
||||
"Whistlesanity": self.options.whistlesanity.value,
|
||||
"GoldBeetles": self.options.beetlesanity.value,
|
||||
"OmochaoChecks": self.options.omosanity.value,
|
||||
"AnimalChecks": self.options.animalsanity.value,
|
||||
"ItemBoxChecks": self.options.itemboxsanity.value,
|
||||
"BigChecks": self.options.bigsanity.value,
|
||||
"KartRaceChecks": self.options.kart_race_checks.value,
|
||||
"ChaoStadiumChecks": self.options.chao_stadium_checks.value,
|
||||
"ChaoRaceDifficulty": self.options.chao_race_difficulty.value,
|
||||
@@ -122,6 +133,7 @@ class SA2BWorld(World):
|
||||
"GateCosts": self.gate_costs,
|
||||
"GateBosses": self.gate_bosses,
|
||||
"BossRushMap": self.boss_rush_map,
|
||||
"ActiveTraps": self.output_active_traps(),
|
||||
"PlayerNum": self.player,
|
||||
}
|
||||
|
||||
@@ -151,12 +163,42 @@ class SA2BWorld(World):
|
||||
|
||||
valid_trap_weights = self.options.exposition_trap_weight.value + \
|
||||
self.options.reverse_trap_weight.value + \
|
||||
self.options.pong_trap_weight.value
|
||||
self.options.literature_trap_weight.value + \
|
||||
self.options.controller_drift_trap_weight.value + \
|
||||
self.options.poison_trap_weight.value + \
|
||||
self.options.bee_trap_weight.value + \
|
||||
self.options.pong_trap_weight.value + \
|
||||
self.options.breakout_trap_weight.value + \
|
||||
self.options.fishing_trap_weight.value + \
|
||||
self.options.trivia_trap_weight.value + \
|
||||
self.options.pokemon_trivia_trap_weight.value + \
|
||||
self.options.pokemon_count_trap_weight.value + \
|
||||
self.options.number_sequence_trap_weight.value + \
|
||||
self.options.light_up_path_trap_weight.value + \
|
||||
self.options.pinball_trap_weight.value + \
|
||||
self.options.math_quiz_trap_weight.value + \
|
||||
self.options.snake_trap_weight.value + \
|
||||
self.options.input_sequence_trap_weight.value
|
||||
|
||||
if valid_trap_weights == 0:
|
||||
self.options.exposition_trap_weight.value = 4
|
||||
self.options.reverse_trap_weight.value = 4
|
||||
self.options.literature_trap_weight.value = 4
|
||||
self.options.controller_drift_trap_weight.value = 4
|
||||
self.options.poison_trap_weight.value = 4
|
||||
self.options.bee_trap_weight.value = 4
|
||||
self.options.pong_trap_weight.value = 4
|
||||
self.options.breakout_trap_weight.value = 4
|
||||
self.options.fishing_trap_weight.value = 4
|
||||
self.options.trivia_trap_weight.value = 4
|
||||
self.options.pokemon_trivia_trap_weight.value = 4
|
||||
self.options.pokemon_count_trap_weight.value = 4
|
||||
self.options.number_sequence_trap_weight.value = 4
|
||||
self.options.light_up_path_trap_weight.value = 4
|
||||
self.options.pinball_trap_weight.value = 4
|
||||
self.options.math_quiz_trap_weight.value = 4
|
||||
self.options.snake_trap_weight.value = 4
|
||||
self.options.input_sequence_trap_weight.value = 4
|
||||
|
||||
if self.options.kart_race_checks.value == 0:
|
||||
self.options.kart_race_checks.value = 2
|
||||
@@ -164,8 +206,8 @@ class SA2BWorld(World):
|
||||
self.gate_bosses = {}
|
||||
self.boss_rush_map = {}
|
||||
else:
|
||||
self.gate_bosses = get_gate_bosses(self.multiworld, self)
|
||||
self.boss_rush_map = get_boss_rush_bosses(self.multiworld, self)
|
||||
self.gate_bosses = get_gate_bosses(self)
|
||||
self.boss_rush_map = get_boss_rush_bosses(self)
|
||||
|
||||
def create_regions(self):
|
||||
self.mission_map = get_mission_table(self.multiworld, self, self.player)
|
||||
@@ -177,7 +219,7 @@ class SA2BWorld(World):
|
||||
# Not Generate Basic
|
||||
self.black_market_costs = dict()
|
||||
|
||||
if self.options.goal.value in [0, 2, 4, 5, 6]:
|
||||
if self.options.goal.value in [0, 2, 4, 5, 6, 8]:
|
||||
self.multiworld.get_location(LocationName.finalhazard, self.player).place_locked_item(self.create_item(ItemName.maria))
|
||||
elif self.options.goal.value == 1:
|
||||
self.multiworld.get_location(LocationName.green_hill, self.player).place_locked_item(self.create_item(ItemName.maria))
|
||||
@@ -202,7 +244,7 @@ class SA2BWorld(World):
|
||||
if self.options.goal.value != 3:
|
||||
# Fill item pool with all required items
|
||||
for item in {**upgrades_table}:
|
||||
itempool += [self.create_item(item, False, self.options.goal.value)]
|
||||
itempool += [self.create_item(item, None, self.options.goal.value)]
|
||||
|
||||
if self.options.goal.value in [1, 2, 6]:
|
||||
# Some flavor of Chaos Emerald Hunt
|
||||
@@ -212,6 +254,25 @@ class SA2BWorld(World):
|
||||
# Black Market
|
||||
itempool += [self.create_item(ItemName.market_token) for _ in range(self.options.black_market_slots.value)]
|
||||
|
||||
if self.options.goal.value in [8]:
|
||||
available_locations: int = total_required_locations - len(itempool) - self.options.number_of_level_gates.value
|
||||
|
||||
while (self.options.minigame_madness_requirement.value * len(minigame_trap_table)) > available_locations:
|
||||
self.options.minigame_madness_requirement.value -= 1
|
||||
|
||||
while (self.options.minigame_madness_minimum.value * len(minigame_trap_table)) > available_locations:
|
||||
self.options.minigame_madness_minimum.value -= 1
|
||||
|
||||
traps_to_create: int = max(self.options.minigame_madness_minimum.value, self.options.minigame_madness_requirement.value)
|
||||
|
||||
# Minigame Madness
|
||||
for item in {**minigame_trap_table}:
|
||||
for i in range(traps_to_create):
|
||||
classification: ItemClassification = ItemClassification.trap
|
||||
if i < self.options.minigame_madness_requirement.value:
|
||||
classification |= ItemClassification.progression
|
||||
itempool.append(self.create_item(item, classification))
|
||||
|
||||
black_market_unlock_mult = 1.0
|
||||
if self.options.black_market_unlock_costs.value == 0:
|
||||
black_market_unlock_mult = 0.5
|
||||
@@ -235,12 +296,12 @@ class SA2BWorld(World):
|
||||
elif self.options.level_gate_costs.value == 1:
|
||||
gate_cost_mult = 0.8
|
||||
|
||||
shuffled_region_list = list(range(30))
|
||||
self.shuffled_region_list = list(range(30))
|
||||
emblem_requirement_list = list()
|
||||
self.multiworld.random.shuffle(shuffled_region_list)
|
||||
levels_per_gate = self.get_levels_per_gate()
|
||||
self.multiworld.random.shuffle(self.shuffled_region_list)
|
||||
self.levels_per_gate = self.get_levels_per_gate()
|
||||
|
||||
check_for_impossible_shuffle(shuffled_region_list, math.ceil(levels_per_gate[0]), self.multiworld)
|
||||
check_for_impossible_shuffle(self.shuffled_region_list, math.ceil(self.levels_per_gate[0]), self.multiworld)
|
||||
levels_added_to_gate = 0
|
||||
total_levels_added = 0
|
||||
current_gate = 0
|
||||
@@ -250,11 +311,11 @@ class SA2BWorld(World):
|
||||
gates = list()
|
||||
gates.append(LevelGate(0))
|
||||
for i in range(30):
|
||||
gates[current_gate].gate_levels.append(shuffled_region_list[i])
|
||||
gates[current_gate].gate_levels.append(self.shuffled_region_list[i])
|
||||
emblem_requirement_list.append(current_gate_emblems)
|
||||
levels_added_to_gate += 1
|
||||
total_levels_added += 1
|
||||
if levels_added_to_gate >= levels_per_gate[current_gate]:
|
||||
if levels_added_to_gate >= self.levels_per_gate[current_gate]:
|
||||
current_gate += 1
|
||||
if current_gate > self.options.number_of_level_gates.value:
|
||||
current_gate = self.options.number_of_level_gates.value
|
||||
@@ -265,18 +326,19 @@ class SA2BWorld(World):
|
||||
self.gate_costs[current_gate] = current_gate_emblems
|
||||
levels_added_to_gate = 0
|
||||
|
||||
self.region_emblem_map = dict(zip(shuffled_region_list, emblem_requirement_list))
|
||||
self.region_emblem_map = dict(zip(self.shuffled_region_list, emblem_requirement_list))
|
||||
|
||||
first_cannons_core_mission, final_cannons_core_mission = get_first_and_last_cannons_core_missions(self.mission_map, self.mission_count_map)
|
||||
|
||||
connect_regions(self.multiworld, self, self.player, gates, self.emblems_for_cannons_core, self.gate_bosses, self.boss_rush_map, first_cannons_core_mission, final_cannons_core_mission)
|
||||
|
||||
max_required_emblems = max(max(emblem_requirement_list), self.emblems_for_cannons_core)
|
||||
max_required_emblems = min(int(max_required_emblems * 1.1), total_emblem_count)
|
||||
itempool += [self.create_item(ItemName.emblem) for _ in range(max_required_emblems)]
|
||||
|
||||
non_required_emblems = (total_emblem_count - max_required_emblems)
|
||||
junk_count = math.floor(non_required_emblems * (self.options.junk_fill_percentage.value / 100.0))
|
||||
itempool += [self.create_item(ItemName.emblem, True) for _ in range(non_required_emblems - junk_count)]
|
||||
itempool += [self.create_item(ItemName.emblem, ItemClassification.filler) for _ in range(non_required_emblems - junk_count)]
|
||||
|
||||
# Carve Traps out of junk_count
|
||||
trap_weights = []
|
||||
@@ -291,7 +353,22 @@ class SA2BWorld(World):
|
||||
trap_weights += ([ItemName.slow_trap] * self.options.slow_trap_weight.value)
|
||||
trap_weights += ([ItemName.cutscene_trap] * self.options.cutscene_trap_weight.value)
|
||||
trap_weights += ([ItemName.reverse_trap] * self.options.reverse_trap_weight.value)
|
||||
trap_weights += ([ItemName.literature_trap] * self.options.literature_trap_weight.value)
|
||||
trap_weights += ([ItemName.controller_drift_trap] * self.options.controller_drift_trap_weight.value)
|
||||
trap_weights += ([ItemName.poison_trap] * self.options.poison_trap_weight.value)
|
||||
trap_weights += ([ItemName.bee_trap] * self.options.bee_trap_weight.value)
|
||||
trap_weights += ([ItemName.pong_trap] * self.options.pong_trap_weight.value)
|
||||
trap_weights += ([ItemName.breakout_trap] * self.options.breakout_trap_weight.value)
|
||||
trap_weights += ([ItemName.fishing_trap] * self.options.fishing_trap_weight.value)
|
||||
trap_weights += ([ItemName.trivia_trap] * self.options.trivia_trap_weight.value)
|
||||
trap_weights += ([ItemName.pokemon_trivia_trap] * self.options.pokemon_trivia_trap_weight.value)
|
||||
trap_weights += ([ItemName.pokemon_count_trap] * self.options.pokemon_count_trap_weight.value)
|
||||
trap_weights += ([ItemName.number_sequence_trap] * self.options.number_sequence_trap_weight.value)
|
||||
trap_weights += ([ItemName.light_up_path_trap] * self.options.light_up_path_trap_weight.value)
|
||||
trap_weights += ([ItemName.pinball_trap] * self.options.pinball_trap_weight.value)
|
||||
trap_weights += ([ItemName.math_quiz_trap] * self.options.math_quiz_trap_weight.value)
|
||||
trap_weights += ([ItemName.snake_trap] * self.options.snake_trap_weight.value)
|
||||
trap_weights += ([ItemName.input_sequence_trap] * self.options.input_sequence_trap_weight.value)
|
||||
|
||||
junk_count += extra_junk_count
|
||||
trap_count = 0 if (len(trap_weights) == 0) else math.ceil(junk_count * (self.options.trap_fill_percentage.value / 100.0))
|
||||
@@ -347,11 +424,15 @@ class SA2BWorld(World):
|
||||
|
||||
|
||||
|
||||
def create_item(self, name: str, force_non_progression=False, goal=0) -> Item:
|
||||
data = item_table[name]
|
||||
def create_item(self, name: str, force_classification=None, goal=0) -> Item:
|
||||
data = None
|
||||
if name in event_table:
|
||||
data = event_table[name]
|
||||
else:
|
||||
data = item_table[name]
|
||||
|
||||
if force_non_progression:
|
||||
classification = ItemClassification.filler
|
||||
if force_classification is not None:
|
||||
classification = force_classification
|
||||
elif name == ItemName.emblem or \
|
||||
name in emeralds_table.keys() or \
|
||||
(name == ItemName.knuckles_shovel_claws and goal in [4, 5]):
|
||||
@@ -380,9 +461,16 @@ class SA2BWorld(World):
|
||||
set_rules(self.multiworld, self, self.player, self.gate_bosses, self.boss_rush_map, self.mission_map, self.mission_count_map, self.black_market_costs)
|
||||
|
||||
def write_spoiler(self, spoiler_handle: typing.TextIO):
|
||||
print_mission_orders_to_spoiler(self.mission_map,
|
||||
self.mission_count_map,
|
||||
self.shuffled_region_list,
|
||||
self.levels_per_gate,
|
||||
self.multiworld.player_name[self.player],
|
||||
spoiler_handle)
|
||||
|
||||
if self.options.number_of_level_gates.value > 0 or self.options.goal.value in [4, 5, 6]:
|
||||
spoiler_handle.write("\n")
|
||||
header_text = "Sonic Adventure 2 Bosses for {}:\n"
|
||||
header_text = "SA2 Bosses for {}:\n"
|
||||
header_text = header_text.format(self.multiworld.player_name[self.player])
|
||||
spoiler_handle.write(header_text)
|
||||
|
||||
@@ -435,20 +523,20 @@ class SA2BWorld(World):
|
||||
continue
|
||||
level_region = exit.connected_region
|
||||
for location in level_region.locations:
|
||||
er_hint_data[location.address] = gate_name
|
||||
if location.address != None:
|
||||
er_hint_data[location.address] = gate_name
|
||||
|
||||
for i in range(self.options.black_market_slots.value):
|
||||
location = self.multiworld.get_location(LocationName.chao_black_market_base + str(i + 1), self.player)
|
||||
er_hint_data[location.address] = str(self.black_market_costs[i]) + " " + str(ItemName.market_token)
|
||||
|
||||
|
||||
hint_data[self.player] = er_hint_data
|
||||
|
||||
@classmethod
|
||||
def stage_fill_hook(cls, multiworld: MultiWorld, progitempool, usefulitempool, filleritempool, fill_locations):
|
||||
if multiworld.get_game_players("Sonic Adventure 2 Battle"):
|
||||
progitempool.sort(
|
||||
key=lambda item: 0 if (item.name != 'Emblem') else 1)
|
||||
key=lambda item: 0 if ("Emblem" in item.name and item.game == "Sonic Adventure 2 Battle") else 1)
|
||||
|
||||
def get_levels_per_gate(self) -> list:
|
||||
levels_per_gate = list()
|
||||
@@ -486,6 +574,39 @@ class SA2BWorld(World):
|
||||
|
||||
return levels_per_gate
|
||||
|
||||
def output_active_traps(self) -> typing.Dict[int, int]:
|
||||
trap_data = {}
|
||||
|
||||
trap_data[0x30] = self.options.omochao_trap_weight.value
|
||||
trap_data[0x31] = self.options.timestop_trap_weight.value
|
||||
trap_data[0x32] = self.options.confusion_trap_weight.value
|
||||
trap_data[0x33] = self.options.tiny_trap_weight.value
|
||||
trap_data[0x34] = self.options.gravity_trap_weight.value
|
||||
trap_data[0x35] = self.options.exposition_trap_weight.value
|
||||
trap_data[0x37] = self.options.ice_trap_weight.value
|
||||
trap_data[0x38] = self.options.slow_trap_weight.value
|
||||
trap_data[0x39] = self.options.cutscene_trap_weight.value
|
||||
trap_data[0x3A] = self.options.reverse_trap_weight.value
|
||||
trap_data[0x3B] = self.options.literature_trap_weight.value
|
||||
trap_data[0x3C] = self.options.controller_drift_trap_weight.value
|
||||
trap_data[0x3D] = self.options.poison_trap_weight.value
|
||||
trap_data[0x3E] = self.options.bee_trap_weight.value
|
||||
|
||||
trap_data[0x50] = self.options.pong_trap_weight.value
|
||||
trap_data[0x51] = self.options.breakout_trap_weight.value
|
||||
trap_data[0x52] = self.options.fishing_trap_weight.value
|
||||
trap_data[0x53] = self.options.trivia_trap_weight.value
|
||||
trap_data[0x54] = self.options.pokemon_trivia_trap_weight.value
|
||||
trap_data[0x55] = self.options.pokemon_count_trap_weight.value
|
||||
trap_data[0x56] = self.options.number_sequence_trap_weight.value
|
||||
trap_data[0x57] = self.options.light_up_path_trap_weight.value
|
||||
trap_data[0x58] = self.options.pinball_trap_weight.value
|
||||
trap_data[0x59] = self.options.math_quiz_trap_weight.value
|
||||
trap_data[0x5A] = self.options.snake_trap_weight.value
|
||||
trap_data[0x5B] = self.options.input_sequence_trap_weight.value
|
||||
|
||||
return trap_data
|
||||
|
||||
def any_chao_locations_active(self) -> bool:
|
||||
if self.options.chao_race_difficulty.value > 0 or \
|
||||
self.options.chao_karate_difficulty.value > 0 or \
|
||||
@@ -686,7 +807,6 @@ class SA2BWorld(World):
|
||||
exit_choice = self.random.choice(valid_kindergarten_exits)
|
||||
exit_room = exit_to_room_map[exit_choice]
|
||||
all_exits_copy.remove(exit_choice)
|
||||
multi_rooms_copy.remove(exit_room)
|
||||
|
||||
destination = 0x06
|
||||
single_rooms_copy.remove(destination)
|
||||
@@ -723,7 +843,8 @@ class SA2BWorld(World):
|
||||
|
||||
er_layout[exit_choice] = destination
|
||||
|
||||
reverse_exit = self.random.choice(room_to_exits_map[destination])
|
||||
possible_reverse_exits = [exit for exit in room_to_exits_map[destination] if exit in all_exits_copy]
|
||||
reverse_exit = self.random.choice(possible_reverse_exits)
|
||||
|
||||
er_layout[reverse_exit] = exit_room
|
||||
|
||||
|
||||
@@ -129,7 +129,10 @@ If you wish to use the `SADX Music` option of the Randomizer, you must own a cop
|
||||
- If you enabled an `SADX Music` option, then most likely the music data was not copied properly into the mod folder (See Additional Options for instructions).
|
||||
|
||||
- Mission 1 is missing a texture in the stage select UI.
|
||||
- Most likely another mod is conflicting and overwriting the texture pack. It is recommeded to have the SA2B Archipelago mod load last in the mod manager.
|
||||
- Most likely another mod is conflicting and overwriting the texture pack. It is recommended to have the SA2B Archipelago mod load last in the mod manager.
|
||||
|
||||
- Minigame trap is un-winnable
|
||||
- If you are using the SA2 Input Controls mod, it conflicts with certain minigames such as the Input Sequence Trap and medium difficulty Fishing Trap. Disabling the SA2 Input Controls mod should resolve the issue.
|
||||
|
||||
## Save File Safeguard (Advanced Option)
|
||||
|
||||
|
||||
@@ -214,67 +214,67 @@ class StardewValleyWorld(World):
|
||||
def setup_victory(self):
|
||||
if self.options.goal == Goal.option_community_center:
|
||||
self.create_event_location(location_table[GoalName.community_center],
|
||||
self.logic.bundle.can_complete_community_center,
|
||||
self.logic.goal.can_complete_community_center(),
|
||||
Event.victory)
|
||||
elif self.options.goal == Goal.option_grandpa_evaluation:
|
||||
self.create_event_location(location_table[GoalName.grandpa_evaluation],
|
||||
self.logic.can_finish_grandpa_evaluation(),
|
||||
self.logic.goal.can_finish_grandpa_evaluation(),
|
||||
Event.victory)
|
||||
elif self.options.goal == Goal.option_bottom_of_the_mines:
|
||||
self.create_event_location(location_table[GoalName.bottom_of_the_mines],
|
||||
True_(),
|
||||
self.logic.goal.can_complete_bottom_of_the_mines(),
|
||||
Event.victory)
|
||||
elif self.options.goal == Goal.option_cryptic_note:
|
||||
self.create_event_location(location_table[GoalName.cryptic_note],
|
||||
self.logic.quest.can_complete_quest("Cryptic Note"),
|
||||
self.logic.goal.can_complete_cryptic_note(),
|
||||
Event.victory)
|
||||
elif self.options.goal == Goal.option_master_angler:
|
||||
self.create_event_location(location_table[GoalName.master_angler],
|
||||
self.logic.fishing.can_catch_every_fish_for_fishsanity(),
|
||||
self.logic.goal.can_complete_master_angler(),
|
||||
Event.victory)
|
||||
elif self.options.goal == Goal.option_complete_collection:
|
||||
self.create_event_location(location_table[GoalName.complete_museum],
|
||||
self.logic.museum.can_complete_museum(),
|
||||
self.logic.goal.can_complete_complete_collection(),
|
||||
Event.victory)
|
||||
elif self.options.goal == Goal.option_full_house:
|
||||
self.create_event_location(location_table[GoalName.full_house],
|
||||
(self.logic.relationship.has_children(2) & self.logic.relationship.can_reproduce()),
|
||||
self.logic.goal.can_complete_full_house(),
|
||||
Event.victory)
|
||||
elif self.options.goal == Goal.option_greatest_walnut_hunter:
|
||||
self.create_event_location(location_table[GoalName.greatest_walnut_hunter],
|
||||
self.logic.walnut.has_walnut(130),
|
||||
self.logic.goal.can_complete_greatest_walnut_hunter(),
|
||||
Event.victory)
|
||||
elif self.options.goal == Goal.option_protector_of_the_valley:
|
||||
self.create_event_location(location_table[GoalName.protector_of_the_valley],
|
||||
self.logic.monster.can_complete_all_monster_slaying_goals(),
|
||||
self.logic.goal.can_complete_protector_of_the_valley(),
|
||||
Event.victory)
|
||||
elif self.options.goal == Goal.option_full_shipment:
|
||||
self.create_event_location(location_table[GoalName.full_shipment],
|
||||
self.logic.shipping.can_ship_everything_in_slot(self.get_all_location_names()),
|
||||
self.logic.goal.can_complete_full_shipment(self.get_all_location_names()),
|
||||
Event.victory)
|
||||
elif self.options.goal == Goal.option_gourmet_chef:
|
||||
self.create_event_location(location_table[GoalName.gourmet_chef],
|
||||
self.logic.cooking.can_cook_everything,
|
||||
self.logic.goal.can_complete_gourmet_chef(),
|
||||
Event.victory)
|
||||
elif self.options.goal == Goal.option_craft_master:
|
||||
self.create_event_location(location_table[GoalName.craft_master],
|
||||
self.logic.crafting.can_craft_everything,
|
||||
self.logic.goal.can_complete_craft_master(),
|
||||
Event.victory)
|
||||
elif self.options.goal == Goal.option_legend:
|
||||
self.create_event_location(location_table[GoalName.legend],
|
||||
self.logic.money.can_have_earned_total(10_000_000),
|
||||
self.logic.goal.can_complete_legend(),
|
||||
Event.victory)
|
||||
elif self.options.goal == Goal.option_mystery_of_the_stardrops:
|
||||
self.create_event_location(location_table[GoalName.mystery_of_the_stardrops],
|
||||
self.logic.has_all_stardrops(),
|
||||
self.logic.goal.can_complete_mystery_of_the_stardrop(),
|
||||
Event.victory)
|
||||
elif self.options.goal == Goal.option_allsanity:
|
||||
self.create_event_location(location_table[GoalName.allsanity],
|
||||
HasProgressionPercent(self.player, 100),
|
||||
self.logic.goal.can_complete_allsanity(),
|
||||
Event.victory)
|
||||
elif self.options.goal == Goal.option_perfection:
|
||||
self.create_event_location(location_table[GoalName.perfection],
|
||||
HasProgressionPercent(self.player, 100),
|
||||
self.logic.goal.can_complete_perfection(),
|
||||
Event.victory)
|
||||
|
||||
self.multiworld.completion_condition[self.player] = lambda state: state.has(Event.victory, self.player)
|
||||
|
||||
@@ -13,12 +13,9 @@ from .relationship_logic import RelationshipLogicMixin
|
||||
from .season_logic import SeasonLogicMixin
|
||||
from .skill_logic import SkillLogicMixin
|
||||
from ..data.recipe_data import RecipeSource, StarterSource, ShopSource, SkillSource, FriendshipSource, \
|
||||
QueenOfSauceSource, CookingRecipe, ShopFriendshipSource, \
|
||||
all_cooking_recipes_by_name
|
||||
QueenOfSauceSource, CookingRecipe, ShopFriendshipSource
|
||||
from ..data.recipe_source import CutsceneSource, ShopTradeSource
|
||||
from ..locations import locations_by_tag, LocationTags
|
||||
from ..options import Chefsanity
|
||||
from ..options import ExcludeGingerIsland
|
||||
from ..stardew_rule import StardewRule, True_, False_
|
||||
from ..strings.region_names import LogicRegion
|
||||
from ..strings.skill_names import Skill
|
||||
@@ -92,17 +89,3 @@ BuildingLogicMixin, RelationshipLogicMixin, SkillLogicMixin, CookingLogicMixin]]
|
||||
@cache_self1
|
||||
def received_recipe(self, meal_name: str):
|
||||
return self.logic.received(f"{meal_name} Recipe")
|
||||
|
||||
@cached_property
|
||||
def can_cook_everything(self) -> StardewRule:
|
||||
cooksanity_prefix = "Cook "
|
||||
all_recipes_names = []
|
||||
exclude_island = self.options.exclude_ginger_island == ExcludeGingerIsland.option_true
|
||||
for location in locations_by_tag[LocationTags.COOKSANITY]:
|
||||
if exclude_island and LocationTags.GINGER_ISLAND in location.tags:
|
||||
continue
|
||||
if location.mod_name and location.mod_name not in self.options.mods:
|
||||
continue
|
||||
all_recipes_names.append(location.name[len(cooksanity_prefix):])
|
||||
all_recipes = [all_cooking_recipes_by_name[recipe_name] for recipe_name in all_recipes_names]
|
||||
return self.logic.and_(*(self.logic.cooking.can_cook(recipe) for recipe in all_recipes))
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from functools import cached_property
|
||||
from typing import Union
|
||||
|
||||
from Utils import cache_self1
|
||||
@@ -12,11 +11,10 @@ from .relationship_logic import RelationshipLogicMixin
|
||||
from .skill_logic import SkillLogicMixin
|
||||
from .special_order_logic import SpecialOrderLogicMixin
|
||||
from .. import options
|
||||
from ..data.craftable_data import CraftingRecipe, all_crafting_recipes_by_name
|
||||
from ..data.craftable_data import CraftingRecipe
|
||||
from ..data.recipe_source import CutsceneSource, ShopTradeSource, ArchipelagoSource, LogicSource, SpecialOrderSource, \
|
||||
FestivalShopSource, QuestSource, StarterSource, ShopSource, SkillSource, MasterySource, FriendshipSource, SkillCraftsanitySource
|
||||
from ..locations import locations_by_tag, LocationTags
|
||||
from ..options import Craftsanity, SpecialOrderLocations, ExcludeGingerIsland
|
||||
from ..options import Craftsanity, SpecialOrderLocations
|
||||
from ..stardew_rule import StardewRule, True_, False_
|
||||
from ..strings.region_names import Region
|
||||
|
||||
@@ -71,7 +69,8 @@ SkillLogicMixin, SpecialOrderLogicMixin, CraftingLogicMixin, QuestLogicMixin]]):
|
||||
if isinstance(recipe.source, ShopSource):
|
||||
return self.logic.money.can_spend_at(recipe.source.region, recipe.source.price)
|
||||
if isinstance(recipe.source, SkillCraftsanitySource):
|
||||
return self.logic.skill.has_level(recipe.source.skill, recipe.source.level) & self.logic.skill.can_earn_level(recipe.source.skill, recipe.source.level)
|
||||
return self.logic.skill.has_level(recipe.source.skill, recipe.source.level) & self.logic.skill.can_earn_level(recipe.source.skill,
|
||||
recipe.source.level)
|
||||
if isinstance(recipe.source, SkillSource):
|
||||
return self.logic.skill.has_level(recipe.source.skill, recipe.source.level)
|
||||
if isinstance(recipe.source, MasterySource):
|
||||
@@ -95,23 +94,3 @@ SkillLogicMixin, SpecialOrderLogicMixin, CraftingLogicMixin, QuestLogicMixin]]):
|
||||
@cache_self1
|
||||
def received_recipe(self, item_name: str):
|
||||
return self.logic.received(f"{item_name} Recipe")
|
||||
|
||||
@cached_property
|
||||
def can_craft_everything(self) -> StardewRule:
|
||||
craftsanity_prefix = "Craft "
|
||||
all_recipes_names = []
|
||||
exclude_island = self.options.exclude_ginger_island == ExcludeGingerIsland.option_true
|
||||
exclude_masteries = not self.content.features.skill_progression.are_masteries_shuffled
|
||||
for location in locations_by_tag[LocationTags.CRAFTSANITY]:
|
||||
if not location.name.startswith(craftsanity_prefix):
|
||||
continue
|
||||
if exclude_island and LocationTags.GINGER_ISLAND in location.tags:
|
||||
continue
|
||||
# FIXME Remove when recipes are in content packs
|
||||
if exclude_masteries and LocationTags.REQUIRES_MASTERIES in location.tags:
|
||||
continue
|
||||
if location.mod_name and location.mod_name not in self.options.mods:
|
||||
continue
|
||||
all_recipes_names.append(location.name[len(craftsanity_prefix):])
|
||||
all_recipes = [all_crafting_recipes_by_name[recipe_name] for recipe_name in all_recipes_names]
|
||||
return self.logic.and_(*(self.logic.crafting.can_craft(recipe) for recipe in all_recipes))
|
||||
|
||||
@@ -29,7 +29,7 @@ class FishingLogicMixin(BaseLogicMixin):
|
||||
|
||||
|
||||
class FishingLogic(BaseLogic[Union[HasLogicMixin, FishingLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, ToolLogicMixin,
|
||||
SkillLogicMixin]]):
|
||||
SkillLogicMixin]]):
|
||||
def can_fish_in_freshwater(self) -> StardewRule:
|
||||
return self.logic.skill.can_fish() & self.logic.region.can_reach_any((Region.forest, Region.town, Region.mountain))
|
||||
|
||||
@@ -97,19 +97,5 @@ class FishingLogic(BaseLogic[Union[HasLogicMixin, FishingLogicMixin, ReceivedLog
|
||||
|
||||
return self.logic.and_(*rules)
|
||||
|
||||
def can_catch_every_fish_for_fishsanity(self) -> StardewRule:
|
||||
if not self.content.features.fishsanity.is_enabled:
|
||||
return self.can_catch_every_fish()
|
||||
|
||||
rules = [self.has_max_fishing()]
|
||||
|
||||
rules.extend(
|
||||
self.logic.fishing.can_catch_fish_for_fishsanity(fish)
|
||||
for fish in self.content.fishes.values()
|
||||
if self.content.features.fishsanity.is_included(fish)
|
||||
)
|
||||
|
||||
return self.logic.and_(*rules)
|
||||
|
||||
def has_specific_bait(self, fish: FishItem) -> StardewRule:
|
||||
return self.can_catch_fish(fish) & self.logic.has(Machine.bait_maker)
|
||||
|
||||
173
worlds/stardew_valley/logic/goal_logic.py
Normal file
173
worlds/stardew_valley/logic/goal_logic.py
Normal file
@@ -0,0 +1,173 @@
|
||||
import typing
|
||||
|
||||
from .base_logic import BaseLogic, BaseLogicMixin
|
||||
from ..data.craftable_data import all_crafting_recipes_by_name
|
||||
from ..data.recipe_data import all_cooking_recipes_by_name
|
||||
from ..locations import LocationTags, locations_by_tag
|
||||
from ..mods.mod_data import ModNames
|
||||
from ..options import options
|
||||
from ..stardew_rule import StardewRule
|
||||
from ..strings.building_names import Building
|
||||
from ..strings.quest_names import Quest
|
||||
from ..strings.season_names import Season
|
||||
from ..strings.wallet_item_names import Wallet
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from .logic import StardewLogic
|
||||
else:
|
||||
StardewLogic = object
|
||||
|
||||
|
||||
class GoalLogicMixin(BaseLogicMixin):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.goal = GoalLogic(*args, **kwargs)
|
||||
|
||||
|
||||
class GoalLogic(BaseLogic[StardewLogic]):
|
||||
|
||||
def can_complete_community_center(self) -> StardewRule:
|
||||
return self.logic.bundle.can_complete_community_center
|
||||
|
||||
def can_finish_grandpa_evaluation(self) -> StardewRule:
|
||||
# https://stardewvalleywiki.com/Grandpa
|
||||
rules_worth_a_point = [
|
||||
self.logic.money.can_have_earned_total(50_000),
|
||||
self.logic.money.can_have_earned_total(100_000),
|
||||
self.logic.money.can_have_earned_total(200_000),
|
||||
self.logic.money.can_have_earned_total(300_000),
|
||||
self.logic.money.can_have_earned_total(500_000),
|
||||
self.logic.money.can_have_earned_total(1_000_000), # first point
|
||||
self.logic.money.can_have_earned_total(1_000_000), # second point
|
||||
self.logic.skill.has_total_level(30),
|
||||
self.logic.skill.has_total_level(50),
|
||||
self.logic.museum.can_complete_museum(),
|
||||
# Catching every fish not expected
|
||||
# Shipping every item not expected
|
||||
self.logic.relationship.can_get_married() & self.logic.building.has_house(2),
|
||||
self.logic.relationship.has_hearts_with_n(5, 8), # 5 Friends
|
||||
self.logic.relationship.has_hearts_with_n(10, 8), # 10 friends
|
||||
self.logic.pet.has_pet_hearts(5), # Max Pet
|
||||
self.logic.bundle.can_complete_community_center, # 1 point for Community Center Completion
|
||||
self.logic.bundle.can_complete_community_center, # Ceremony first point
|
||||
self.logic.bundle.can_complete_community_center, # Ceremony second point
|
||||
self.logic.received(Wallet.skull_key),
|
||||
self.logic.wallet.has_rusty_key(),
|
||||
]
|
||||
return self.logic.count(12, *rules_worth_a_point)
|
||||
|
||||
def can_complete_bottom_of_the_mines(self) -> StardewRule:
|
||||
# The location is in the bottom of the mines region, so no actual rule is required
|
||||
return self.logic.true_
|
||||
|
||||
def can_complete_cryptic_note(self) -> StardewRule:
|
||||
return self.logic.quest.can_complete_quest(Quest.cryptic_note)
|
||||
|
||||
def can_complete_master_angler(self) -> StardewRule:
|
||||
if not self.content.features.fishsanity.is_enabled:
|
||||
return self.logic.fishing.can_catch_every_fish()
|
||||
|
||||
rules = [self.logic.fishing.has_max_fishing()]
|
||||
|
||||
rules.extend(
|
||||
self.logic.fishing.can_catch_fish_for_fishsanity(fish)
|
||||
for fish in self.content.fishes.values()
|
||||
if self.content.features.fishsanity.is_included(fish)
|
||||
)
|
||||
|
||||
return self.logic.and_(*rules)
|
||||
|
||||
def can_complete_complete_collection(self) -> StardewRule:
|
||||
return self.logic.museum.can_complete_museum()
|
||||
|
||||
def can_complete_full_house(self) -> StardewRule:
|
||||
return self.logic.relationship.has_children(2) & self.logic.relationship.can_reproduce()
|
||||
|
||||
def can_complete_greatest_walnut_hunter(self) -> StardewRule:
|
||||
return self.logic.walnut.has_walnut(130)
|
||||
|
||||
def can_complete_protector_of_the_valley(self) -> StardewRule:
|
||||
return self.logic.monster.can_complete_all_monster_slaying_goals()
|
||||
|
||||
def can_complete_full_shipment(self, all_location_names_in_slot: list[str]) -> StardewRule:
|
||||
if self.options.shipsanity == options.Shipsanity.option_none:
|
||||
return self.logic.shipping.can_ship_everything()
|
||||
|
||||
rules = [self.logic.building.has_building(Building.shipping_bin)]
|
||||
|
||||
for shipsanity_location in locations_by_tag[LocationTags.SHIPSANITY]:
|
||||
if shipsanity_location.name not in all_location_names_in_slot:
|
||||
continue
|
||||
rules.append(self.logic.region.can_reach_location(shipsanity_location.name))
|
||||
return self.logic.and_(*rules)
|
||||
|
||||
def can_complete_gourmet_chef(self) -> StardewRule:
|
||||
cooksanity_prefix = "Cook "
|
||||
all_recipes_names = []
|
||||
exclude_island = self.options.exclude_ginger_island == options.ExcludeGingerIsland.option_true
|
||||
for location in locations_by_tag[LocationTags.COOKSANITY]:
|
||||
if exclude_island and LocationTags.GINGER_ISLAND in location.tags:
|
||||
continue
|
||||
if location.mod_name and location.mod_name not in self.options.mods:
|
||||
continue
|
||||
all_recipes_names.append(location.name[len(cooksanity_prefix):])
|
||||
all_recipes = [all_cooking_recipes_by_name[recipe_name] for recipe_name in all_recipes_names]
|
||||
return self.logic.and_(*(self.logic.cooking.can_cook(recipe) for recipe in all_recipes))
|
||||
|
||||
def can_complete_craft_master(self) -> StardewRule:
|
||||
craftsanity_prefix = "Craft "
|
||||
all_recipes_names = []
|
||||
exclude_island = self.options.exclude_ginger_island == options.ExcludeGingerIsland.option_true
|
||||
exclude_masteries = not self.content.features.skill_progression.are_masteries_shuffled
|
||||
for location in locations_by_tag[LocationTags.CRAFTSANITY]:
|
||||
if not location.name.startswith(craftsanity_prefix):
|
||||
continue
|
||||
if exclude_island and LocationTags.GINGER_ISLAND in location.tags:
|
||||
continue
|
||||
# FIXME Remove when recipes are in content packs
|
||||
if exclude_masteries and LocationTags.REQUIRES_MASTERIES in location.tags:
|
||||
continue
|
||||
if location.mod_name and location.mod_name not in self.options.mods:
|
||||
continue
|
||||
all_recipes_names.append(location.name[len(craftsanity_prefix):])
|
||||
all_recipes = [all_crafting_recipes_by_name[recipe_name] for recipe_name in all_recipes_names]
|
||||
return self.logic.and_(*(self.logic.crafting.can_craft(recipe) for recipe in all_recipes))
|
||||
|
||||
def can_complete_legend(self) -> StardewRule:
|
||||
return self.logic.money.can_have_earned_total(10_000_000)
|
||||
|
||||
def can_complete_mystery_of_the_stardrop(self) -> StardewRule:
|
||||
other_rules = []
|
||||
number_of_stardrops_to_receive = 0
|
||||
number_of_stardrops_to_receive += 1 # The Mines level 100
|
||||
number_of_stardrops_to_receive += 1 # Old Master Cannoli
|
||||
number_of_stardrops_to_receive += 1 # Museum Stardrop
|
||||
number_of_stardrops_to_receive += 1 # Krobus Stardrop
|
||||
|
||||
# Master Angler Stardrop
|
||||
if self.content.features.fishsanity.is_enabled:
|
||||
number_of_stardrops_to_receive += 1
|
||||
else:
|
||||
other_rules.append(self.logic.fishing.can_catch_every_fish())
|
||||
|
||||
if self.options.festival_locations == options.FestivalLocations.option_disabled: # Fair Stardrop
|
||||
other_rules.append(self.logic.season.has(Season.fall))
|
||||
else:
|
||||
number_of_stardrops_to_receive += 1
|
||||
|
||||
# Spouse Stardrop
|
||||
if self.content.features.friendsanity.is_enabled:
|
||||
number_of_stardrops_to_receive += 1
|
||||
else:
|
||||
other_rules.append(self.logic.relationship.has_hearts_with_any_bachelor(13))
|
||||
|
||||
if ModNames.deepwoods in self.options.mods: # Petting the Unicorn
|
||||
number_of_stardrops_to_receive += 1
|
||||
|
||||
return self.logic.received("Stardrop", number_of_stardrops_to_receive) & self.logic.and_(*other_rules, allow_empty=True)
|
||||
|
||||
def can_complete_allsanity(self) -> StardewRule:
|
||||
return self.logic.has_progress_percent(100)
|
||||
|
||||
def can_complete_perfection(self) -> StardewRule:
|
||||
return self.logic.has_progress_percent(100)
|
||||
@@ -1,5 +1,5 @@
|
||||
from .base_logic import BaseLogic
|
||||
from ..stardew_rule import StardewRule, And, Or, Has, Count, true_, false_
|
||||
from ..stardew_rule import StardewRule, And, Or, Has, Count, true_, false_, HasProgressionPercent
|
||||
|
||||
|
||||
class HasLogicMixin(BaseLogic[None]):
|
||||
@@ -23,6 +23,12 @@ class HasLogicMixin(BaseLogic[None]):
|
||||
def has_n(self, *items: str, count: int):
|
||||
return self.count(count, *(self.has(item) for item in items))
|
||||
|
||||
def has_progress_percent(self, percent: int):
|
||||
assert percent >= 0, "Can't have a negative progress percent"
|
||||
assert percent <= 100, "Can't have a progress percent over 100"
|
||||
|
||||
return HasProgressionPercent(self.player, percent)
|
||||
|
||||
@staticmethod
|
||||
def count(count: int, *rules: StardewRule) -> StardewRule:
|
||||
assert rules, "Can't create a Count conditions without rules"
|
||||
@@ -47,8 +53,14 @@ class HasLogicMixin(BaseLogic[None]):
|
||||
return Count(rules, count)
|
||||
|
||||
@staticmethod
|
||||
def and_(*rules: StardewRule) -> StardewRule:
|
||||
assert rules, "Can't create a And conditions without rules"
|
||||
def and_(*rules: StardewRule, allow_empty: bool = False) -> StardewRule:
|
||||
"""
|
||||
:param rules: The rules to combine
|
||||
:param allow_empty: If True, return true_ when no rules are given. Otherwise, raise an error.
|
||||
"""
|
||||
if not rules:
|
||||
assert allow_empty, "Can't create a And conditions without rules"
|
||||
return true_
|
||||
|
||||
if len(rules) == 1:
|
||||
return rules[0]
|
||||
@@ -56,8 +68,14 @@ class HasLogicMixin(BaseLogic[None]):
|
||||
return And(*rules)
|
||||
|
||||
@staticmethod
|
||||
def or_(*rules: StardewRule) -> StardewRule:
|
||||
assert rules, "Can't create a Or conditions without rules"
|
||||
def or_(*rules: StardewRule, allow_empty: bool = False) -> StardewRule:
|
||||
"""
|
||||
:param rules: The rules to combine
|
||||
:param allow_empty: If True, return false_ when no rules are given. Otherwise, raise an error.
|
||||
"""
|
||||
if not rules:
|
||||
assert allow_empty, "Can't create a Or conditions without rules"
|
||||
return false_
|
||||
|
||||
if len(rules) == 1:
|
||||
return rules[0]
|
||||
|
||||
@@ -19,6 +19,7 @@ from .farming_logic import FarmingLogicMixin
|
||||
from .festival_logic import FestivalLogicMixin
|
||||
from .fishing_logic import FishingLogicMixin
|
||||
from .gift_logic import GiftLogicMixin
|
||||
from .goal_logic import GoalLogicMixin
|
||||
from .grind_logic import GrindLogicMixin
|
||||
from .harvesting_logic import HarvestingLogicMixin
|
||||
from .has_logic import HasLogicMixin
|
||||
@@ -50,8 +51,7 @@ from ..data.museum_data import all_museum_items
|
||||
from ..data.recipe_data import all_cooking_recipes
|
||||
from ..mods.logic.magic_logic import MagicLogicMixin
|
||||
from ..mods.logic.mod_logic import ModLogicMixin
|
||||
from ..mods.mod_data import ModNames
|
||||
from ..options import ExcludeGingerIsland, FestivalLocations, StardewValleyOptions
|
||||
from ..options import ExcludeGingerIsland, StardewValleyOptions
|
||||
from ..stardew_rule import False_, True_, StardewRule
|
||||
from ..strings.animal_names import Animal
|
||||
from ..strings.animal_product_names import AnimalProduct
|
||||
@@ -93,7 +93,7 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin
|
||||
CombatLogicMixin, MagicLogicMixin, MonsterLogicMixin, ToolLogicMixin, PetLogicMixin, QualityLogicMixin,
|
||||
SkillLogicMixin, FarmingLogicMixin, BundleLogicMixin, FishingLogicMixin, MineLogicMixin, CookingLogicMixin, AbilityLogicMixin,
|
||||
SpecialOrderLogicMixin, QuestLogicMixin, CraftingLogicMixin, ModLogicMixin, HarvestingLogicMixin, SourceLogicMixin,
|
||||
RequirementLogicMixin, BookLogicMixin, GrindLogicMixin, FestivalLogicMixin, WalnutLogicMixin):
|
||||
RequirementLogicMixin, BookLogicMixin, GrindLogicMixin, FestivalLogicMixin, WalnutLogicMixin, GoalLogicMixin):
|
||||
player: int
|
||||
options: StardewValleyOptions
|
||||
content: StardewContent
|
||||
@@ -375,71 +375,11 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin
|
||||
def can_smelt(self, item: str) -> StardewRule:
|
||||
return self.has(Machine.furnace) & self.has(item)
|
||||
|
||||
def can_finish_grandpa_evaluation(self) -> StardewRule:
|
||||
# https://stardewvalleywiki.com/Grandpa
|
||||
rules_worth_a_point = [
|
||||
self.money.can_have_earned_total(50000), # 50 000g
|
||||
self.money.can_have_earned_total(100000), # 100 000g
|
||||
self.money.can_have_earned_total(200000), # 200 000g
|
||||
self.money.can_have_earned_total(300000), # 300 000g
|
||||
self.money.can_have_earned_total(500000), # 500 000g
|
||||
self.money.can_have_earned_total(1000000), # 1 000 000g first point
|
||||
self.money.can_have_earned_total(1000000), # 1 000 000g second point
|
||||
self.skill.has_total_level(30), # Total Skills: 30
|
||||
self.skill.has_total_level(50), # Total Skills: 50
|
||||
self.museum.can_complete_museum(), # Completing the museum for a point
|
||||
# Catching every fish not expected
|
||||
# Shipping every item not expected
|
||||
self.relationship.can_get_married() & self.building.has_house(2),
|
||||
self.relationship.has_hearts_with_n(5, 8), # 5 Friends
|
||||
self.relationship.has_hearts_with_n(10, 8), # 10 friends
|
||||
self.pet.has_pet_hearts(5), # Max Pet
|
||||
self.bundle.can_complete_community_center, # Community Center Completion
|
||||
self.bundle.can_complete_community_center, # CC Ceremony first point
|
||||
self.bundle.can_complete_community_center, # CC Ceremony second point
|
||||
self.received(Wallet.skull_key), # Skull Key obtained
|
||||
self.wallet.has_rusty_key(), # Rusty key obtained
|
||||
]
|
||||
return self.count(12, *rules_worth_a_point)
|
||||
|
||||
def has_island_trader(self) -> StardewRule:
|
||||
if self.options.exclude_ginger_island == ExcludeGingerIsland.option_true:
|
||||
return False_()
|
||||
return self.region.can_reach(Region.island_trader)
|
||||
|
||||
def has_all_stardrops(self) -> StardewRule:
|
||||
other_rules = []
|
||||
number_of_stardrops_to_receive = 0
|
||||
number_of_stardrops_to_receive += 1 # The Mines level 100
|
||||
number_of_stardrops_to_receive += 1 # Old Master Cannoli
|
||||
number_of_stardrops_to_receive += 1 # Museum Stardrop
|
||||
number_of_stardrops_to_receive += 1 # Krobus Stardrop
|
||||
|
||||
# Master Angler Stardrop
|
||||
if self.content.features.fishsanity.is_enabled:
|
||||
number_of_stardrops_to_receive += 1
|
||||
else:
|
||||
other_rules.append(self.fishing.can_catch_every_fish())
|
||||
|
||||
if self.options.festival_locations == FestivalLocations.option_disabled: # Fair Stardrop
|
||||
other_rules.append(self.season.has(Season.fall))
|
||||
else:
|
||||
number_of_stardrops_to_receive += 1
|
||||
|
||||
# Spouse Stardrop
|
||||
if self.content.features.friendsanity.is_enabled:
|
||||
number_of_stardrops_to_receive += 1
|
||||
else:
|
||||
other_rules.append(self.relationship.has_hearts_with_any_bachelor(13))
|
||||
|
||||
if ModNames.deepwoods in self.options.mods: # Petting the Unicorn
|
||||
number_of_stardrops_to_receive += 1
|
||||
|
||||
if not other_rules:
|
||||
return self.received("Stardrop", number_of_stardrops_to_receive)
|
||||
|
||||
return self.received("Stardrop", number_of_stardrops_to_receive) & self.logic.and_(*other_rules)
|
||||
|
||||
def has_abandoned_jojamart(self) -> StardewRule:
|
||||
return self.received(CommunityUpgrade.movie_theater, 1)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from functools import cached_property
|
||||
from typing import Union, List
|
||||
from typing import Union
|
||||
|
||||
from Utils import cache_self1
|
||||
from .base_logic import BaseLogic, BaseLogicMixin
|
||||
@@ -8,7 +8,7 @@ from .has_logic import HasLogicMixin
|
||||
from .received_logic import ReceivedLogicMixin
|
||||
from .region_logic import RegionLogicMixin
|
||||
from ..locations import LocationTags, locations_by_tag
|
||||
from ..options import ExcludeGingerIsland, Shipsanity
|
||||
from ..options import ExcludeGingerIsland
|
||||
from ..options import SpecialOrderLocations
|
||||
from ..stardew_rule import StardewRule
|
||||
from ..strings.building_names import Building
|
||||
@@ -45,15 +45,3 @@ class ShippingLogic(BaseLogic[Union[ReceivedLogicMixin, ShippingLogicMixin, Buil
|
||||
continue
|
||||
all_items_to_ship.append(location.name[len(shipsanity_prefix):])
|
||||
return self.logic.building.has_building(Building.shipping_bin) & self.logic.has_all(*all_items_to_ship)
|
||||
|
||||
def can_ship_everything_in_slot(self, all_location_names_in_slot: List[str]) -> StardewRule:
|
||||
if self.options.shipsanity == Shipsanity.option_none:
|
||||
return self.logic.shipping.can_ship_everything()
|
||||
|
||||
rules = [self.logic.building.has_building(Building.shipping_bin)]
|
||||
|
||||
for shipsanity_location in locations_by_tag[LocationTags.SHIPSANITY]:
|
||||
if shipsanity_location.name not in all_location_names_in_slot:
|
||||
continue
|
||||
rules.append(self.logic.region.can_reach_location(shipsanity_location.name))
|
||||
return self.logic.and_(*rules)
|
||||
|
||||
@@ -3,4 +3,4 @@ from .options import StardewValleyOption, Goal, FarmType, StartingMoney, ProfitM
|
||||
ArcadeMachineLocations, SpecialOrderLocations, QuestLocations, Fishsanity, Museumsanity, Monstersanity, Shipsanity, Cooksanity, Chefsanity, Craftsanity, \
|
||||
Friendsanity, FriendsanityHeartSize, Booksanity, Walnutsanity, NumberOfMovementBuffs, EnabledFillerBuffs, ExcludeGingerIsland, TrapItems, \
|
||||
MultipleDaySleepEnabled, MultipleDaySleepCost, ExperienceMultiplier, FriendshipMultiplier, DebrisMultiplier, QuickStart, Gifting, Mods, BundlePlando, \
|
||||
StardewValleyOptions
|
||||
StardewValleyOptions, enabled_mods, disabled_mods, all_mods
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from . import SVTestBase, minimal_locations_maximal_items
|
||||
from . import SVTestBase
|
||||
from .assertion import WorldAssertMixin
|
||||
from .options.presets import minimal_locations_maximal_items
|
||||
from .. import options
|
||||
from ..mods.mod_data import ModNames
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from BaseClasses import MultiWorld, get_seed
|
||||
from . import setup_solo_multiworld, SVTestCase, allsanity_no_mods_6_x_x, get_minsanity_options, solo_multiworld
|
||||
from . import setup_solo_multiworld, SVTestCase, solo_multiworld
|
||||
from .options.presets import allsanity_no_mods_6_x_x, get_minsanity_options
|
||||
from .. import StardewValleyWorld
|
||||
from ..items import Group, item_table
|
||||
from ..options import Friendsanity, SeasonRandomization, Museumsanity, Shipsanity, Goal
|
||||
|
||||
@@ -3,7 +3,9 @@ import unittest
|
||||
from unittest import TestCase, SkipTest
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from . import RuleAssertMixin, setup_solo_multiworld, allsanity_mods_6_x_x, minimal_locations_maximal_items
|
||||
from . import setup_solo_multiworld
|
||||
from .assertion import RuleAssertMixin
|
||||
from .options.presets import allsanity_mods_6_x_x, minimal_locations_maximal_items
|
||||
from .. import StardewValleyWorld
|
||||
from ..data.bundle_data import all_bundle_items_except_money
|
||||
from ..logic.logic import StardewLogic
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from . import SVTestBase, allsanity_no_mods_6_x_x, \
|
||||
allsanity_mods_6_x_x, minimal_locations_maximal_items, minimal_locations_maximal_items_with_island, get_minsanity_options, default_6_x_x, \
|
||||
allsanity_mods_6_x_x_exclude_disabled
|
||||
from . import SVTestBase
|
||||
from .options.presets import default_6_x_x, allsanity_no_mods_6_x_x, allsanity_mods_6_x_x_exclude_disabled, get_minsanity_options, \
|
||||
minimal_locations_maximal_items, minimal_locations_maximal_items_with_island
|
||||
from .. import location_table
|
||||
from ..items import Group, item_table
|
||||
|
||||
|
||||
@@ -2,9 +2,10 @@ import itertools
|
||||
|
||||
from BaseClasses import ItemClassification
|
||||
from Options import NamedRange
|
||||
from . import SVTestCase, allsanity_no_mods_6_x_x, allsanity_mods_6_x_x, solo_multiworld, SVTestBase
|
||||
from . import SVTestCase, solo_multiworld, SVTestBase
|
||||
from .assertion import WorldAssertMixin
|
||||
from .long.option_names import all_option_choices
|
||||
from .options.presets import allsanity_no_mods_6_x_x, allsanity_mods_6_x_x
|
||||
from .. import items_by_group, Group, StardewValleyWorld
|
||||
from ..locations import locations_by_tag, LocationTags, location_table
|
||||
from ..options import ExcludeGingerIsland, ToolProgression, Goal, SeasonRandomization, TrapItems, SpecialOrderLocations, ArcadeMachineLocations
|
||||
|
||||
@@ -11,9 +11,8 @@ from test.general import gen_steps, setup_solo_multiworld as setup_base_solo_mul
|
||||
from worlds.AutoWorld import call_all
|
||||
from .assertion import RuleAssertMixin
|
||||
from .options.utils import fill_namespace_with_default, parse_class_option_keys, fill_dataclass_with_default
|
||||
from .. import StardewValleyWorld, options, StardewItem
|
||||
from .. import StardewValleyWorld, StardewItem
|
||||
from ..options import StardewValleyOption
|
||||
from ..options.options import enabled_mods
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -21,169 +20,6 @@ DEFAULT_TEST_SEED = get_seed()
|
||||
logger.info(f"Default Test Seed: {DEFAULT_TEST_SEED}")
|
||||
|
||||
|
||||
def default_6_x_x():
|
||||
return {
|
||||
options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.default,
|
||||
options.BackpackProgression.internal_name: options.BackpackProgression.default,
|
||||
options.Booksanity.internal_name: options.Booksanity.default,
|
||||
options.BuildingProgression.internal_name: options.BuildingProgression.default,
|
||||
options.BundlePrice.internal_name: options.BundlePrice.default,
|
||||
options.BundleRandomization.internal_name: options.BundleRandomization.default,
|
||||
options.Chefsanity.internal_name: options.Chefsanity.default,
|
||||
options.Cooksanity.internal_name: options.Cooksanity.default,
|
||||
options.Craftsanity.internal_name: options.Craftsanity.default,
|
||||
options.Cropsanity.internal_name: options.Cropsanity.default,
|
||||
options.ElevatorProgression.internal_name: options.ElevatorProgression.default,
|
||||
options.EntranceRandomization.internal_name: options.EntranceRandomization.default,
|
||||
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.default,
|
||||
options.FestivalLocations.internal_name: options.FestivalLocations.default,
|
||||
options.Fishsanity.internal_name: options.Fishsanity.default,
|
||||
options.Friendsanity.internal_name: options.Friendsanity.default,
|
||||
options.FriendsanityHeartSize.internal_name: options.FriendsanityHeartSize.default,
|
||||
options.Goal.internal_name: options.Goal.default,
|
||||
options.Mods.internal_name: options.Mods.default,
|
||||
options.Monstersanity.internal_name: options.Monstersanity.default,
|
||||
options.Museumsanity.internal_name: options.Museumsanity.default,
|
||||
options.NumberOfMovementBuffs.internal_name: options.NumberOfMovementBuffs.default,
|
||||
options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.default,
|
||||
options.QuestLocations.internal_name: options.QuestLocations.default,
|
||||
options.SeasonRandomization.internal_name: options.SeasonRandomization.default,
|
||||
options.Shipsanity.internal_name: options.Shipsanity.default,
|
||||
options.SkillProgression.internal_name: options.SkillProgression.default,
|
||||
options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.default,
|
||||
options.ToolProgression.internal_name: options.ToolProgression.default,
|
||||
options.TrapItems.internal_name: options.TrapItems.default,
|
||||
options.Walnutsanity.internal_name: options.Walnutsanity.default
|
||||
}
|
||||
|
||||
|
||||
def allsanity_no_mods_6_x_x():
|
||||
return {
|
||||
options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_full_shuffling,
|
||||
options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive,
|
||||
options.Booksanity.internal_name: options.Booksanity.option_all,
|
||||
options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive,
|
||||
options.BundlePrice.internal_name: options.BundlePrice.option_expensive,
|
||||
options.BundleRandomization.internal_name: options.BundleRandomization.option_thematic,
|
||||
options.Chefsanity.internal_name: options.Chefsanity.option_all,
|
||||
options.Cooksanity.internal_name: options.Cooksanity.option_all,
|
||||
options.Craftsanity.internal_name: options.Craftsanity.option_all,
|
||||
options.Cropsanity.internal_name: options.Cropsanity.option_enabled,
|
||||
options.ElevatorProgression.internal_name: options.ElevatorProgression.option_progressive,
|
||||
options.EntranceRandomization.internal_name: options.EntranceRandomization.option_disabled,
|
||||
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false,
|
||||
options.FestivalLocations.internal_name: options.FestivalLocations.option_hard,
|
||||
options.Fishsanity.internal_name: options.Fishsanity.option_all,
|
||||
options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage,
|
||||
options.FriendsanityHeartSize.internal_name: 1,
|
||||
options.Goal.internal_name: options.Goal.option_perfection,
|
||||
options.Mods.internal_name: frozenset(),
|
||||
options.Monstersanity.internal_name: options.Monstersanity.option_progressive_goals,
|
||||
options.Museumsanity.internal_name: options.Museumsanity.option_all,
|
||||
options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_all,
|
||||
options.NumberOfMovementBuffs.internal_name: 12,
|
||||
options.QuestLocations.internal_name: 56,
|
||||
options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized,
|
||||
options.Shipsanity.internal_name: options.Shipsanity.option_everything,
|
||||
options.SkillProgression.internal_name: options.SkillProgression.option_progressive_with_masteries,
|
||||
options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi,
|
||||
options.ToolProgression.internal_name: options.ToolProgression.option_progressive,
|
||||
options.TrapItems.internal_name: options.TrapItems.option_nightmare,
|
||||
options.Walnutsanity.internal_name: options.Walnutsanity.preset_all
|
||||
}
|
||||
|
||||
|
||||
def allsanity_mods_6_x_x():
|
||||
allsanity = allsanity_no_mods_6_x_x()
|
||||
allsanity.update({options.Mods.internal_name: frozenset(options.Mods.valid_keys)})
|
||||
return allsanity
|
||||
|
||||
|
||||
def allsanity_mods_6_x_x_exclude_disabled():
|
||||
allsanity = allsanity_no_mods_6_x_x()
|
||||
allsanity.update({options.Mods.internal_name: frozenset(enabled_mods)})
|
||||
return allsanity
|
||||
|
||||
|
||||
def get_minsanity_options():
|
||||
return {
|
||||
options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_disabled,
|
||||
options.BackpackProgression.internal_name: options.BackpackProgression.option_vanilla,
|
||||
options.Booksanity.internal_name: options.Booksanity.option_none,
|
||||
options.BuildingProgression.internal_name: options.BuildingProgression.option_vanilla,
|
||||
options.BundlePrice.internal_name: options.BundlePrice.option_very_cheap,
|
||||
options.BundleRandomization.internal_name: options.BundleRandomization.option_vanilla,
|
||||
options.Chefsanity.internal_name: options.Chefsanity.option_none,
|
||||
options.Cooksanity.internal_name: options.Cooksanity.option_none,
|
||||
options.Craftsanity.internal_name: options.Craftsanity.option_none,
|
||||
options.Cropsanity.internal_name: options.Cropsanity.option_disabled,
|
||||
options.ElevatorProgression.internal_name: options.ElevatorProgression.option_vanilla,
|
||||
options.EntranceRandomization.internal_name: options.EntranceRandomization.option_disabled,
|
||||
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true,
|
||||
options.FestivalLocations.internal_name: options.FestivalLocations.option_disabled,
|
||||
options.Fishsanity.internal_name: options.Fishsanity.option_none,
|
||||
options.Friendsanity.internal_name: options.Friendsanity.option_none,
|
||||
options.FriendsanityHeartSize.internal_name: 8,
|
||||
options.Goal.internal_name: options.Goal.option_bottom_of_the_mines,
|
||||
options.Mods.internal_name: frozenset(),
|
||||
options.Monstersanity.internal_name: options.Monstersanity.option_none,
|
||||
options.Museumsanity.internal_name: options.Museumsanity.option_none,
|
||||
options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_none,
|
||||
options.NumberOfMovementBuffs.internal_name: 0,
|
||||
options.QuestLocations.internal_name: -1,
|
||||
options.SeasonRandomization.internal_name: options.SeasonRandomization.option_disabled,
|
||||
options.Shipsanity.internal_name: options.Shipsanity.option_none,
|
||||
options.SkillProgression.internal_name: options.SkillProgression.option_vanilla,
|
||||
options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_vanilla,
|
||||
options.ToolProgression.internal_name: options.ToolProgression.option_vanilla,
|
||||
options.TrapItems.internal_name: options.TrapItems.option_no_traps,
|
||||
options.Walnutsanity.internal_name: options.Walnutsanity.preset_none
|
||||
}
|
||||
|
||||
|
||||
def minimal_locations_maximal_items():
|
||||
min_max_options = {
|
||||
options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_disabled,
|
||||
options.BackpackProgression.internal_name: options.BackpackProgression.option_vanilla,
|
||||
options.Booksanity.internal_name: options.Booksanity.option_none,
|
||||
options.BuildingProgression.internal_name: options.BuildingProgression.option_vanilla,
|
||||
options.BundlePrice.internal_name: options.BundlePrice.option_expensive,
|
||||
options.BundleRandomization.internal_name: options.BundleRandomization.option_shuffled,
|
||||
options.Chefsanity.internal_name: options.Chefsanity.option_none,
|
||||
options.Cooksanity.internal_name: options.Cooksanity.option_none,
|
||||
options.Craftsanity.internal_name: options.Craftsanity.option_none,
|
||||
options.Cropsanity.internal_name: options.Cropsanity.option_disabled,
|
||||
options.ElevatorProgression.internal_name: options.ElevatorProgression.option_vanilla,
|
||||
options.EntranceRandomization.internal_name: options.EntranceRandomization.option_disabled,
|
||||
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true,
|
||||
options.FestivalLocations.internal_name: options.FestivalLocations.option_disabled,
|
||||
options.Fishsanity.internal_name: options.Fishsanity.option_none,
|
||||
options.Friendsanity.internal_name: options.Friendsanity.option_none,
|
||||
options.FriendsanityHeartSize.internal_name: 8,
|
||||
options.Goal.internal_name: options.Goal.option_craft_master,
|
||||
options.Mods.internal_name: frozenset(),
|
||||
options.Monstersanity.internal_name: options.Monstersanity.option_none,
|
||||
options.Museumsanity.internal_name: options.Museumsanity.option_none,
|
||||
options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_all,
|
||||
options.NumberOfMovementBuffs.internal_name: 12,
|
||||
options.QuestLocations.internal_name: -1,
|
||||
options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized,
|
||||
options.Shipsanity.internal_name: options.Shipsanity.option_none,
|
||||
options.SkillProgression.internal_name: options.SkillProgression.option_vanilla,
|
||||
options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_vanilla,
|
||||
options.ToolProgression.internal_name: options.ToolProgression.option_vanilla,
|
||||
options.TrapItems.internal_name: options.TrapItems.option_nightmare,
|
||||
options.Walnutsanity.internal_name: options.Walnutsanity.preset_none
|
||||
}
|
||||
return min_max_options
|
||||
|
||||
|
||||
def minimal_locations_maximal_items_with_island():
|
||||
min_max_options = minimal_locations_maximal_items()
|
||||
min_max_options.update({options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false})
|
||||
return min_max_options
|
||||
|
||||
|
||||
class SVTestCase(unittest.TestCase):
|
||||
# Set False to not skip some 'extra' tests
|
||||
skip_base_tests: bool = True
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import random
|
||||
|
||||
from BaseClasses import get_seed, ItemClassification
|
||||
from .. import SVTestBase, SVTestCase, allsanity_mods_6_x_x, fill_dataclass_with_default
|
||||
from .. import SVTestBase, SVTestCase
|
||||
from ..assertion import ModAssertMixin, WorldAssertMixin
|
||||
from ..options.presets import allsanity_mods_6_x_x
|
||||
from ..options.utils import fill_dataclass_with_default
|
||||
from ... import options, items, Group, create_content
|
||||
from ...mods.mod_data import ModNames
|
||||
from ...options import SkillProgression, Walnutsanity
|
||||
|
||||
164
worlds/stardew_valley/test/options/presets.py
Normal file
164
worlds/stardew_valley/test/options/presets.py
Normal file
@@ -0,0 +1,164 @@
|
||||
from ... import options
|
||||
|
||||
|
||||
def default_6_x_x():
|
||||
return {
|
||||
options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.default,
|
||||
options.BackpackProgression.internal_name: options.BackpackProgression.default,
|
||||
options.Booksanity.internal_name: options.Booksanity.default,
|
||||
options.BuildingProgression.internal_name: options.BuildingProgression.default,
|
||||
options.BundlePrice.internal_name: options.BundlePrice.default,
|
||||
options.BundleRandomization.internal_name: options.BundleRandomization.default,
|
||||
options.Chefsanity.internal_name: options.Chefsanity.default,
|
||||
options.Cooksanity.internal_name: options.Cooksanity.default,
|
||||
options.Craftsanity.internal_name: options.Craftsanity.default,
|
||||
options.Cropsanity.internal_name: options.Cropsanity.default,
|
||||
options.ElevatorProgression.internal_name: options.ElevatorProgression.default,
|
||||
options.EntranceRandomization.internal_name: options.EntranceRandomization.default,
|
||||
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.default,
|
||||
options.FestivalLocations.internal_name: options.FestivalLocations.default,
|
||||
options.Fishsanity.internal_name: options.Fishsanity.default,
|
||||
options.Friendsanity.internal_name: options.Friendsanity.default,
|
||||
options.FriendsanityHeartSize.internal_name: options.FriendsanityHeartSize.default,
|
||||
options.Goal.internal_name: options.Goal.default,
|
||||
options.Mods.internal_name: options.Mods.default,
|
||||
options.Monstersanity.internal_name: options.Monstersanity.default,
|
||||
options.Museumsanity.internal_name: options.Museumsanity.default,
|
||||
options.NumberOfMovementBuffs.internal_name: options.NumberOfMovementBuffs.default,
|
||||
options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.default,
|
||||
options.QuestLocations.internal_name: options.QuestLocations.default,
|
||||
options.SeasonRandomization.internal_name: options.SeasonRandomization.default,
|
||||
options.Shipsanity.internal_name: options.Shipsanity.default,
|
||||
options.SkillProgression.internal_name: options.SkillProgression.default,
|
||||
options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.default,
|
||||
options.ToolProgression.internal_name: options.ToolProgression.default,
|
||||
options.TrapItems.internal_name: options.TrapItems.default,
|
||||
options.Walnutsanity.internal_name: options.Walnutsanity.default
|
||||
}
|
||||
|
||||
|
||||
def allsanity_no_mods_6_x_x():
|
||||
return {
|
||||
options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_full_shuffling,
|
||||
options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive,
|
||||
options.Booksanity.internal_name: options.Booksanity.option_all,
|
||||
options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive,
|
||||
options.BundlePrice.internal_name: options.BundlePrice.option_expensive,
|
||||
options.BundleRandomization.internal_name: options.BundleRandomization.option_thematic,
|
||||
options.Chefsanity.internal_name: options.Chefsanity.option_all,
|
||||
options.Cooksanity.internal_name: options.Cooksanity.option_all,
|
||||
options.Craftsanity.internal_name: options.Craftsanity.option_all,
|
||||
options.Cropsanity.internal_name: options.Cropsanity.option_enabled,
|
||||
options.ElevatorProgression.internal_name: options.ElevatorProgression.option_progressive,
|
||||
options.EntranceRandomization.internal_name: options.EntranceRandomization.option_disabled,
|
||||
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false,
|
||||
options.FestivalLocations.internal_name: options.FestivalLocations.option_hard,
|
||||
options.Fishsanity.internal_name: options.Fishsanity.option_all,
|
||||
options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage,
|
||||
options.FriendsanityHeartSize.internal_name: 1,
|
||||
options.Goal.internal_name: options.Goal.option_perfection,
|
||||
options.Mods.internal_name: frozenset(),
|
||||
options.Monstersanity.internal_name: options.Monstersanity.option_progressive_goals,
|
||||
options.Museumsanity.internal_name: options.Museumsanity.option_all,
|
||||
options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_all,
|
||||
options.NumberOfMovementBuffs.internal_name: 12,
|
||||
options.QuestLocations.internal_name: 56,
|
||||
options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized,
|
||||
options.Shipsanity.internal_name: options.Shipsanity.option_everything,
|
||||
options.SkillProgression.internal_name: options.SkillProgression.option_progressive_with_masteries,
|
||||
options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi,
|
||||
options.ToolProgression.internal_name: options.ToolProgression.option_progressive,
|
||||
options.TrapItems.internal_name: options.TrapItems.option_nightmare,
|
||||
options.Walnutsanity.internal_name: options.Walnutsanity.preset_all
|
||||
}
|
||||
|
||||
|
||||
def allsanity_mods_6_x_x_exclude_disabled():
|
||||
allsanity = allsanity_no_mods_6_x_x()
|
||||
allsanity.update({options.Mods.internal_name: frozenset(options.enabled_mods)})
|
||||
return allsanity
|
||||
|
||||
|
||||
def allsanity_mods_6_x_x():
|
||||
allsanity = allsanity_no_mods_6_x_x()
|
||||
allsanity.update({options.Mods.internal_name: frozenset(options.all_mods)})
|
||||
return allsanity
|
||||
|
||||
|
||||
def get_minsanity_options():
|
||||
return {
|
||||
options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_disabled,
|
||||
options.BackpackProgression.internal_name: options.BackpackProgression.option_vanilla,
|
||||
options.Booksanity.internal_name: options.Booksanity.option_none,
|
||||
options.BuildingProgression.internal_name: options.BuildingProgression.option_vanilla,
|
||||
options.BundlePrice.internal_name: options.BundlePrice.option_very_cheap,
|
||||
options.BundleRandomization.internal_name: options.BundleRandomization.option_vanilla,
|
||||
options.Chefsanity.internal_name: options.Chefsanity.option_none,
|
||||
options.Cooksanity.internal_name: options.Cooksanity.option_none,
|
||||
options.Craftsanity.internal_name: options.Craftsanity.option_none,
|
||||
options.Cropsanity.internal_name: options.Cropsanity.option_disabled,
|
||||
options.ElevatorProgression.internal_name: options.ElevatorProgression.option_vanilla,
|
||||
options.EntranceRandomization.internal_name: options.EntranceRandomization.option_disabled,
|
||||
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true,
|
||||
options.FestivalLocations.internal_name: options.FestivalLocations.option_disabled,
|
||||
options.Fishsanity.internal_name: options.Fishsanity.option_none,
|
||||
options.Friendsanity.internal_name: options.Friendsanity.option_none,
|
||||
options.FriendsanityHeartSize.internal_name: 8,
|
||||
options.Goal.internal_name: options.Goal.option_bottom_of_the_mines,
|
||||
options.Mods.internal_name: frozenset(),
|
||||
options.Monstersanity.internal_name: options.Monstersanity.option_none,
|
||||
options.Museumsanity.internal_name: options.Museumsanity.option_none,
|
||||
options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_none,
|
||||
options.NumberOfMovementBuffs.internal_name: 0,
|
||||
options.QuestLocations.internal_name: -1,
|
||||
options.SeasonRandomization.internal_name: options.SeasonRandomization.option_disabled,
|
||||
options.Shipsanity.internal_name: options.Shipsanity.option_none,
|
||||
options.SkillProgression.internal_name: options.SkillProgression.option_vanilla,
|
||||
options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_vanilla,
|
||||
options.ToolProgression.internal_name: options.ToolProgression.option_vanilla,
|
||||
options.TrapItems.internal_name: options.TrapItems.option_no_traps,
|
||||
options.Walnutsanity.internal_name: options.Walnutsanity.preset_none
|
||||
}
|
||||
|
||||
|
||||
def minimal_locations_maximal_items():
|
||||
min_max_options = {
|
||||
options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_disabled,
|
||||
options.BackpackProgression.internal_name: options.BackpackProgression.option_vanilla,
|
||||
options.Booksanity.internal_name: options.Booksanity.option_none,
|
||||
options.BuildingProgression.internal_name: options.BuildingProgression.option_vanilla,
|
||||
options.BundlePrice.internal_name: options.BundlePrice.option_expensive,
|
||||
options.BundleRandomization.internal_name: options.BundleRandomization.option_shuffled,
|
||||
options.Chefsanity.internal_name: options.Chefsanity.option_none,
|
||||
options.Cooksanity.internal_name: options.Cooksanity.option_none,
|
||||
options.Craftsanity.internal_name: options.Craftsanity.option_none,
|
||||
options.Cropsanity.internal_name: options.Cropsanity.option_disabled,
|
||||
options.ElevatorProgression.internal_name: options.ElevatorProgression.option_vanilla,
|
||||
options.EntranceRandomization.internal_name: options.EntranceRandomization.option_disabled,
|
||||
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true,
|
||||
options.FestivalLocations.internal_name: options.FestivalLocations.option_disabled,
|
||||
options.Fishsanity.internal_name: options.Fishsanity.option_none,
|
||||
options.Friendsanity.internal_name: options.Friendsanity.option_none,
|
||||
options.FriendsanityHeartSize.internal_name: 8,
|
||||
options.Goal.internal_name: options.Goal.option_craft_master,
|
||||
options.Mods.internal_name: frozenset(),
|
||||
options.Monstersanity.internal_name: options.Monstersanity.option_none,
|
||||
options.Museumsanity.internal_name: options.Museumsanity.option_none,
|
||||
options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_all,
|
||||
options.NumberOfMovementBuffs.internal_name: 12,
|
||||
options.QuestLocations.internal_name: -1,
|
||||
options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized,
|
||||
options.Shipsanity.internal_name: options.Shipsanity.option_none,
|
||||
options.SkillProgression.internal_name: options.SkillProgression.option_vanilla,
|
||||
options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_vanilla,
|
||||
options.ToolProgression.internal_name: options.ToolProgression.option_vanilla,
|
||||
options.TrapItems.internal_name: options.TrapItems.option_nightmare,
|
||||
options.Walnutsanity.internal_name: options.Walnutsanity.preset_none
|
||||
}
|
||||
return min_max_options
|
||||
|
||||
|
||||
def minimal_locations_maximal_items_with_island():
|
||||
min_max_options = minimal_locations_maximal_items()
|
||||
min_max_options.update({options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false})
|
||||
return min_max_options
|
||||
@@ -8,7 +8,8 @@ from typing import List
|
||||
from BaseClasses import get_seed
|
||||
from Fill import distribute_items_restrictive, balance_multiworld_progression
|
||||
from worlds import AutoWorld
|
||||
from .. import SVTestCase, minimal_locations_maximal_items, setup_multiworld, default_6_x_x, allsanity_no_mods_6_x_x, allsanity_mods_6_x_x
|
||||
from .. import SVTestCase, setup_multiworld
|
||||
from ..options.presets import default_6_x_x, allsanity_no_mods_6_x_x, allsanity_mods_6_x_x, minimal_locations_maximal_items
|
||||
|
||||
assert default_6_x_x
|
||||
assert allsanity_no_mods_6_x_x
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from .. import SVTestBase, allsanity_mods_6_x_x
|
||||
from .. import SVTestBase
|
||||
from ..options.presets import allsanity_mods_6_x_x
|
||||
from ...stardew_rule import HasProgressionPercent
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import argparse
|
||||
import json
|
||||
|
||||
from .. import setup_solo_multiworld
|
||||
from ..options.presets import allsanity_mods_6_x_x
|
||||
from ...options import FarmType, EntranceRandomization
|
||||
from ...test import setup_solo_multiworld, allsanity_mods_6_x_x
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import unittest
|
||||
from unittest.mock import Mock
|
||||
|
||||
from .. import SVTestBase, allsanity_mods_6_x_x, fill_namespace_with_default
|
||||
from .. import SVTestBase, fill_namespace_with_default
|
||||
from ..options.presets import allsanity_mods_6_x_x
|
||||
from ... import STARDEW_VALLEY, FarmType, BundleRandomization, EntranceRandomization
|
||||
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
bsdiff4>=1.2.2
|
||||
bsdiff4>=1.2.2
|
||||
|
||||
@@ -689,7 +689,9 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
atoll_statue = regions["Ruined Atoll"].connect(
|
||||
connecting_region=regions["Ruined Atoll Statue"],
|
||||
rule=lambda state: has_ability(prayer, state, world)
|
||||
and (has_ladder("Ladders in South Atoll", state, world)
|
||||
and ((has_ladder("Ladders in South Atoll", state, world)
|
||||
and state.has_any((laurels, grapple), player)
|
||||
and (has_sword(state, player) or state.has_any((fire_wand, gun), player)))
|
||||
# shoot fuse and have the shot hit you mid-LS
|
||||
or (can_ladder_storage(state, world) and state.has(fire_wand, player)
|
||||
and options.ladder_storage >= LadderStorage.option_hard)))
|
||||
@@ -1083,6 +1085,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
swamp_mid_to_cath = regions["Swamp Mid"].connect(
|
||||
connecting_region=regions["Swamp to Cathedral Main Entrance Region"],
|
||||
rule=lambda state: (has_ability(prayer, state, world)
|
||||
and (has_sword(state, player))
|
||||
and (state.has(laurels, player)
|
||||
# blam yourself in the face with a wand shot off the fuse
|
||||
or (can_ladder_storage(state, world) and state.has(fire_wand, player)
|
||||
|
||||
@@ -125,7 +125,8 @@ def set_region_rules(world: "TunicWorld") -> None:
|
||||
# there's some boxes in the way
|
||||
and (has_melee(state, player) or state.has_any((gun, grapple, fire_wand), player)))
|
||||
world.get_entrance("Ruined Atoll -> Library").access_rule = \
|
||||
lambda state: state.has_any({grapple, laurels}, player) and has_ability(prayer, state, world)
|
||||
lambda state: (state.has_any({grapple, laurels}, player) and has_ability(prayer, state, world)
|
||||
and (has_sword(state, player) or state.has_any((fire_wand, gun), player)))
|
||||
world.get_entrance("Overworld -> Quarry").access_rule = \
|
||||
lambda state: (has_sword(state, player) or state.has(fire_wand, player)) \
|
||||
and (state.has_any({grapple, laurels, gun}, player) or can_ladder_storage(state, world))
|
||||
@@ -141,7 +142,7 @@ def set_region_rules(world: "TunicWorld") -> None:
|
||||
world.get_entrance("Lower Quarry -> Rooted Ziggurat").access_rule = \
|
||||
lambda state: state.has(grapple, player) and has_ability(prayer, state, world)
|
||||
world.get_entrance("Swamp -> Cathedral").access_rule = \
|
||||
lambda state: (state.has(laurels, player) and has_ability(prayer, state, world)) \
|
||||
lambda state: (state.has(laurels, player) and has_ability(prayer, state, world) and has_sword(state, player)) \
|
||||
or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)
|
||||
world.get_entrance("Overworld -> Spirit Arena").access_rule = \
|
||||
lambda state: ((state.has(gold_hexagon, player, options.hexagon_goal.value) if options.hexagon_quest.value
|
||||
|
||||
373
worlds/tww/Items.py
Normal file
373
worlds/tww/Items.py
Normal file
@@ -0,0 +1,373 @@
|
||||
from collections.abc import Iterable
|
||||
from typing import TYPE_CHECKING, NamedTuple, Optional
|
||||
|
||||
from BaseClasses import Item
|
||||
from BaseClasses import ItemClassification as IC
|
||||
from worlds.AutoWorld import World
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .randomizers.Dungeons import Dungeon
|
||||
|
||||
|
||||
def item_factory(items: str | Iterable[str], world: World) -> Item | list[Item]:
|
||||
"""
|
||||
Create items based on their names.
|
||||
Depending on the input, this function can return a single item or a list of items.
|
||||
|
||||
:param items: The name or names of the items to create.
|
||||
:param world: The game world.
|
||||
:raises KeyError: If an unknown item name is provided.
|
||||
:return: A single item or a list of items.
|
||||
"""
|
||||
ret: list[Item] = []
|
||||
singleton = False
|
||||
if isinstance(items, str):
|
||||
items = [items]
|
||||
singleton = True
|
||||
for item in items:
|
||||
if item in ITEM_TABLE:
|
||||
ret.append(world.create_item(item))
|
||||
else:
|
||||
raise KeyError(f"Unknown item {item}")
|
||||
|
||||
return ret[0] if singleton else ret
|
||||
|
||||
|
||||
class TWWItemData(NamedTuple):
|
||||
"""
|
||||
This class represents the data for an item in The Wind Waker.
|
||||
|
||||
:param type: The type of the item (e.g., "Item", "Dungeon Item").
|
||||
:param classification: The item's classification (progression, useful, filler).
|
||||
:param code: The unique code identifier for the item.
|
||||
:param quantity: The number of this item available.
|
||||
:param item_id: The ID used to represent the item in-game.
|
||||
"""
|
||||
|
||||
type: str
|
||||
classification: IC
|
||||
code: Optional[int]
|
||||
quantity: int
|
||||
item_id: Optional[int]
|
||||
|
||||
|
||||
class TWWItem(Item):
|
||||
"""
|
||||
This class represents an item in The Wind Waker.
|
||||
|
||||
:param name: The item's name.
|
||||
:param player: The ID of the player who owns the item.
|
||||
:param data: The data associated with this item.
|
||||
:param classification: Optional classification to override the default.
|
||||
"""
|
||||
|
||||
game: str = "The Wind Waker"
|
||||
type: Optional[str]
|
||||
dungeon: Optional["Dungeon"] = None
|
||||
|
||||
def __init__(self, name: str, player: int, data: TWWItemData, classification: Optional[IC] = None) -> None:
|
||||
super().__init__(
|
||||
name,
|
||||
data.classification if classification is None else classification,
|
||||
None if data.code is None else TWWItem.get_apid(data.code),
|
||||
player,
|
||||
)
|
||||
|
||||
self.type = data.type
|
||||
self.item_id = data.item_id
|
||||
|
||||
@staticmethod
|
||||
def get_apid(code: int) -> int:
|
||||
"""
|
||||
Compute the Archipelago ID for the given item code.
|
||||
|
||||
:param code: The unique code for the item.
|
||||
:return: The computed Archipelago ID.
|
||||
"""
|
||||
base_id: int = 2322432
|
||||
return base_id + code
|
||||
|
||||
@property
|
||||
def dungeon_item(self) -> Optional[str]:
|
||||
"""
|
||||
Determine if the item is a dungeon item and, if so, returns its type.
|
||||
|
||||
:return: The type of dungeon item, or `None` if it is not a dungeon item.
|
||||
"""
|
||||
if self.type in ("Small Key", "Big Key", "Map", "Compass"):
|
||||
return self.type
|
||||
return None
|
||||
|
||||
|
||||
ITEM_TABLE: dict[str, TWWItemData] = {
|
||||
"Telescope": TWWItemData("Item", IC.useful, 0, 1, 0x20),
|
||||
# "Boat's Sail": TWWItemData("Item", IC.progression, 1, 1, 0x78), # noqa: E131
|
||||
"Wind Waker": TWWItemData("Item", IC.progression, 2, 1, 0x22),
|
||||
"Grappling Hook": TWWItemData("Item", IC.progression, 3, 1, 0x25),
|
||||
"Spoils Bag": TWWItemData("Item", IC.progression, 4, 1, 0x24),
|
||||
"Boomerang": TWWItemData("Item", IC.progression, 5, 1, 0x2D),
|
||||
"Deku Leaf": TWWItemData("Item", IC.progression, 6, 1, 0x34),
|
||||
"Tingle Tuner": TWWItemData("Item", IC.progression, 7, 1, 0x21),
|
||||
"Iron Boots": TWWItemData("Item", IC.progression, 8, 1, 0x29),
|
||||
"Magic Armor": TWWItemData("Item", IC.progression, 9, 1, 0x2A),
|
||||
"Bait Bag": TWWItemData("Item", IC.progression, 10, 1, 0x2C),
|
||||
"Bombs": TWWItemData("Item", IC.progression, 11, 1, 0x31),
|
||||
"Delivery Bag": TWWItemData("Item", IC.progression, 12, 1, 0x30),
|
||||
"Hookshot": TWWItemData("Item", IC.progression, 13, 1, 0x2F),
|
||||
"Skull Hammer": TWWItemData("Item", IC.progression, 14, 1, 0x33),
|
||||
"Power Bracelets": TWWItemData("Item", IC.progression, 15, 1, 0x28),
|
||||
|
||||
"Hero's Charm": TWWItemData("Item", IC.useful, 16, 1, 0x43),
|
||||
"Hurricane Spin": TWWItemData("Item", IC.useful, 17, 1, 0xAA),
|
||||
"Dragon Tingle Statue": TWWItemData("Item", IC.progression, 18, 1, 0xA3),
|
||||
"Forbidden Tingle Statue": TWWItemData("Item", IC.progression, 19, 1, 0xA4),
|
||||
"Goddess Tingle Statue": TWWItemData("Item", IC.progression, 20, 1, 0xA5),
|
||||
"Earth Tingle Statue": TWWItemData("Item", IC.progression, 21, 1, 0xA6),
|
||||
"Wind Tingle Statue": TWWItemData("Item", IC.progression, 22, 1, 0xA7),
|
||||
|
||||
"Wind's Requiem": TWWItemData("Item", IC.progression, 23, 1, 0x6D),
|
||||
"Ballad of Gales": TWWItemData("Item", IC.progression, 24, 1, 0x6E),
|
||||
"Command Melody": TWWItemData("Item", IC.progression, 25, 1, 0x6F),
|
||||
"Earth God's Lyric": TWWItemData("Item", IC.progression, 26, 1, 0x70),
|
||||
"Wind God's Aria": TWWItemData("Item", IC.progression, 27, 1, 0x71),
|
||||
"Song of Passing": TWWItemData("Item", IC.progression, 28, 1, 0x72),
|
||||
|
||||
"Triforce Shard 1": TWWItemData("Item", IC.progression, 29, 1, 0x61),
|
||||
"Triforce Shard 2": TWWItemData("Item", IC.progression, 30, 1, 0x62),
|
||||
"Triforce Shard 3": TWWItemData("Item", IC.progression, 31, 1, 0x63),
|
||||
"Triforce Shard 4": TWWItemData("Item", IC.progression, 32, 1, 0x64),
|
||||
"Triforce Shard 5": TWWItemData("Item", IC.progression, 33, 1, 0x65),
|
||||
"Triforce Shard 6": TWWItemData("Item", IC.progression, 34, 1, 0x66),
|
||||
"Triforce Shard 7": TWWItemData("Item", IC.progression, 35, 1, 0x67),
|
||||
"Triforce Shard 8": TWWItemData("Item", IC.progression, 36, 1, 0x68),
|
||||
|
||||
"Skull Necklace": TWWItemData("Item", IC.filler, 37, 9, 0x45),
|
||||
"Boko Baba Seed": TWWItemData("Item", IC.filler, 38, 1, 0x46),
|
||||
"Golden Feather": TWWItemData("Item", IC.filler, 39, 9, 0x47),
|
||||
"Knight's Crest": TWWItemData("Item", IC.filler, 40, 3, 0x48),
|
||||
"Red Chu Jelly": TWWItemData("Item", IC.filler, 41, 1, 0x49),
|
||||
"Green Chu Jelly": TWWItemData("Item", IC.filler, 42, 1, 0x4A),
|
||||
"Joy Pendant": TWWItemData("Item", IC.filler, 43, 20, 0x1F),
|
||||
"All-Purpose Bait": TWWItemData("Item", IC.filler, 44, 1, 0x82),
|
||||
"Hyoi Pear": TWWItemData("Item", IC.filler, 45, 4, 0x83),
|
||||
|
||||
"Note to Mom": TWWItemData("Item", IC.progression, 46, 1, 0x99),
|
||||
"Maggie's Letter": TWWItemData("Item", IC.progression, 47, 1, 0x9A),
|
||||
"Moblin's Letter": TWWItemData("Item", IC.progression, 48, 1, 0x9B),
|
||||
"Cabana Deed": TWWItemData("Item", IC.progression, 49, 1, 0x9C),
|
||||
"Fill-Up Coupon": TWWItemData("Item", IC.useful, 50, 1, 0x9E),
|
||||
|
||||
"Nayru's Pearl": TWWItemData("Item", IC.progression, 51, 1, 0x69),
|
||||
"Din's Pearl": TWWItemData("Item", IC.progression, 52, 1, 0x6A),
|
||||
"Farore's Pearl": TWWItemData("Item", IC.progression, 53, 1, 0x6B),
|
||||
|
||||
"Progressive Sword": TWWItemData("Item", IC.progression, 54, 4, 0x38),
|
||||
"Progressive Shield": TWWItemData("Item", IC.progression, 55, 2, 0x3B),
|
||||
"Progressive Picto Box": TWWItemData("Item", IC.progression, 56, 2, 0x23),
|
||||
"Progressive Bow": TWWItemData("Item", IC.progression, 57, 3, 0x27),
|
||||
"Progressive Magic Meter": TWWItemData("Item", IC.progression, 58, 2, 0xB1),
|
||||
"Quiver Capacity Upgrade": TWWItemData("Item", IC.progression, 59, 2, 0xAF),
|
||||
"Bomb Bag Capacity Upgrade": TWWItemData("Item", IC.useful, 60, 2, 0xAD),
|
||||
"Wallet Capacity Upgrade": TWWItemData("Item", IC.progression, 61, 2, 0xAB),
|
||||
"Empty Bottle": TWWItemData("Item", IC.progression, 62, 4, 0x50),
|
||||
|
||||
"Triforce Chart 1": TWWItemData("Item", IC.progression_skip_balancing, 63, 1, 0xFE),
|
||||
"Triforce Chart 2": TWWItemData("Item", IC.progression_skip_balancing, 64, 1, 0xFD),
|
||||
"Triforce Chart 3": TWWItemData("Item", IC.progression_skip_balancing, 65, 1, 0xFC),
|
||||
"Triforce Chart 4": TWWItemData("Item", IC.progression_skip_balancing, 66, 1, 0xFB),
|
||||
"Triforce Chart 5": TWWItemData("Item", IC.progression_skip_balancing, 67, 1, 0xFA),
|
||||
"Triforce Chart 6": TWWItemData("Item", IC.progression_skip_balancing, 68, 1, 0xF9),
|
||||
"Triforce Chart 7": TWWItemData("Item", IC.progression_skip_balancing, 69, 1, 0xF8),
|
||||
"Triforce Chart 8": TWWItemData("Item", IC.progression_skip_balancing, 70, 1, 0xF7),
|
||||
"Treasure Chart 1": TWWItemData("Item", IC.progression_skip_balancing, 71, 1, 0xE7),
|
||||
"Treasure Chart 2": TWWItemData("Item", IC.progression_skip_balancing, 72, 1, 0xEE),
|
||||
"Treasure Chart 3": TWWItemData("Item", IC.progression_skip_balancing, 73, 1, 0xE0),
|
||||
"Treasure Chart 4": TWWItemData("Item", IC.progression_skip_balancing, 74, 1, 0xE1),
|
||||
"Treasure Chart 5": TWWItemData("Item", IC.progression_skip_balancing, 75, 1, 0xF2),
|
||||
"Treasure Chart 6": TWWItemData("Item", IC.progression_skip_balancing, 76, 1, 0xEA),
|
||||
"Treasure Chart 7": TWWItemData("Item", IC.progression_skip_balancing, 77, 1, 0xCC),
|
||||
"Treasure Chart 8": TWWItemData("Item", IC.progression_skip_balancing, 78, 1, 0xD4),
|
||||
"Treasure Chart 9": TWWItemData("Item", IC.progression_skip_balancing, 79, 1, 0xDA),
|
||||
"Treasure Chart 10": TWWItemData("Item", IC.progression_skip_balancing, 80, 1, 0xDE),
|
||||
"Treasure Chart 11": TWWItemData("Item", IC.progression_skip_balancing, 81, 1, 0xF6),
|
||||
"Treasure Chart 12": TWWItemData("Item", IC.progression_skip_balancing, 82, 1, 0xE9),
|
||||
"Treasure Chart 13": TWWItemData("Item", IC.progression_skip_balancing, 83, 1, 0xCF),
|
||||
"Treasure Chart 14": TWWItemData("Item", IC.progression_skip_balancing, 84, 1, 0xDD),
|
||||
"Treasure Chart 15": TWWItemData("Item", IC.progression_skip_balancing, 85, 1, 0xF5),
|
||||
"Treasure Chart 16": TWWItemData("Item", IC.progression_skip_balancing, 86, 1, 0xE3),
|
||||
"Treasure Chart 17": TWWItemData("Item", IC.progression_skip_balancing, 87, 1, 0xD7),
|
||||
"Treasure Chart 18": TWWItemData("Item", IC.progression_skip_balancing, 88, 1, 0xE4),
|
||||
"Treasure Chart 19": TWWItemData("Item", IC.progression_skip_balancing, 89, 1, 0xD1),
|
||||
"Treasure Chart 20": TWWItemData("Item", IC.progression_skip_balancing, 90, 1, 0xF3),
|
||||
"Treasure Chart 21": TWWItemData("Item", IC.progression_skip_balancing, 91, 1, 0xCE),
|
||||
"Treasure Chart 22": TWWItemData("Item", IC.progression_skip_balancing, 92, 1, 0xD9),
|
||||
"Treasure Chart 23": TWWItemData("Item", IC.progression_skip_balancing, 93, 1, 0xF1),
|
||||
"Treasure Chart 24": TWWItemData("Item", IC.progression_skip_balancing, 94, 1, 0xEB),
|
||||
"Treasure Chart 25": TWWItemData("Item", IC.progression_skip_balancing, 95, 1, 0xD6),
|
||||
"Treasure Chart 26": TWWItemData("Item", IC.progression_skip_balancing, 96, 1, 0xD3),
|
||||
"Treasure Chart 27": TWWItemData("Item", IC.progression_skip_balancing, 97, 1, 0xCD),
|
||||
"Treasure Chart 28": TWWItemData("Item", IC.progression_skip_balancing, 98, 1, 0xE2),
|
||||
"Treasure Chart 29": TWWItemData("Item", IC.progression_skip_balancing, 99, 1, 0xE6),
|
||||
"Treasure Chart 30": TWWItemData("Item", IC.progression_skip_balancing, 100, 1, 0xF4),
|
||||
"Treasure Chart 31": TWWItemData("Item", IC.progression_skip_balancing, 101, 1, 0xF0),
|
||||
"Treasure Chart 32": TWWItemData("Item", IC.progression_skip_balancing, 102, 1, 0xD0),
|
||||
"Treasure Chart 33": TWWItemData("Item", IC.progression_skip_balancing, 103, 1, 0xEF),
|
||||
"Treasure Chart 34": TWWItemData("Item", IC.progression_skip_balancing, 104, 1, 0xE5),
|
||||
"Treasure Chart 35": TWWItemData("Item", IC.progression_skip_balancing, 105, 1, 0xE8),
|
||||
"Treasure Chart 36": TWWItemData("Item", IC.progression_skip_balancing, 106, 1, 0xD8),
|
||||
"Treasure Chart 37": TWWItemData("Item", IC.progression_skip_balancing, 107, 1, 0xD5),
|
||||
"Treasure Chart 38": TWWItemData("Item", IC.progression_skip_balancing, 108, 1, 0xED),
|
||||
"Treasure Chart 39": TWWItemData("Item", IC.progression_skip_balancing, 109, 1, 0xEC),
|
||||
"Treasure Chart 40": TWWItemData("Item", IC.progression_skip_balancing, 110, 1, 0xDF),
|
||||
"Treasure Chart 41": TWWItemData("Item", IC.progression_skip_balancing, 111, 1, 0xD2),
|
||||
|
||||
"Tingle's Chart": TWWItemData("Item", IC.filler, 112, 1, 0xDC),
|
||||
"Ghost Ship Chart": TWWItemData("Item", IC.progression, 113, 1, 0xDB),
|
||||
"Octo Chart": TWWItemData("Item", IC.filler, 114, 1, 0xCA),
|
||||
"Great Fairy Chart": TWWItemData("Item", IC.filler, 115, 1, 0xC9),
|
||||
"Secret Cave Chart": TWWItemData("Item", IC.filler, 116, 1, 0xC6),
|
||||
"Light Ring Chart": TWWItemData("Item", IC.filler, 117, 1, 0xC5),
|
||||
"Platform Chart": TWWItemData("Item", IC.filler, 118, 1, 0xC4),
|
||||
"Beedle's Chart": TWWItemData("Item", IC.filler, 119, 1, 0xC3),
|
||||
"Submarine Chart": TWWItemData("Item", IC.filler, 120, 1, 0xC2),
|
||||
|
||||
"Green Rupee": TWWItemData("Item", IC.filler, 121, 1, 0x01),
|
||||
"Blue Rupee": TWWItemData("Item", IC.filler, 122, 2, 0x02),
|
||||
"Yellow Rupee": TWWItemData("Item", IC.filler, 123, 3, 0x03),
|
||||
"Red Rupee": TWWItemData("Item", IC.filler, 124, 8, 0x04),
|
||||
"Purple Rupee": TWWItemData("Item", IC.filler, 125, 10, 0x05),
|
||||
"Orange Rupee": TWWItemData("Item", IC.useful, 126, 15, 0x06),
|
||||
"Silver Rupee": TWWItemData("Item", IC.useful, 127, 20, 0x0F),
|
||||
"Rainbow Rupee": TWWItemData("Item", IC.useful, 128, 1, 0xB8),
|
||||
|
||||
"Piece of Heart": TWWItemData("Item", IC.useful, 129, 44, 0x07),
|
||||
"Heart Container": TWWItemData("Item", IC.useful, 130, 6, 0x08),
|
||||
|
||||
"DRC Big Key": TWWItemData("Big Key", IC.progression, 131, 1, 0x14),
|
||||
"DRC Small Key": TWWItemData("Small Key", IC.progression, 132, 4, 0x13),
|
||||
"FW Big Key": TWWItemData("Big Key", IC.progression, 133, 1, 0x40),
|
||||
"FW Small Key": TWWItemData("Small Key", IC.progression, 134, 1, 0x1D),
|
||||
"TotG Big Key": TWWItemData("Big Key", IC.progression, 135, 1, 0x5C),
|
||||
"TotG Small Key": TWWItemData("Small Key", IC.progression, 136, 2, 0x5B),
|
||||
"ET Big Key": TWWItemData("Big Key", IC.progression, 138, 1, 0x74),
|
||||
"ET Small Key": TWWItemData("Small Key", IC.progression, 139, 3, 0x73),
|
||||
"WT Big Key": TWWItemData("Big Key", IC.progression, 140, 1, 0x81),
|
||||
"WT Small Key": TWWItemData("Small Key", IC.progression, 141, 2, 0x77),
|
||||
"DRC Dungeon Map": TWWItemData("Map", IC.filler, 142, 1, 0x1B),
|
||||
"DRC Compass": TWWItemData("Compass", IC.filler, 143, 1, 0x1C),
|
||||
"FW Dungeon Map": TWWItemData("Map", IC.filler, 144, 1, 0x41),
|
||||
"FW Compass": TWWItemData("Compass", IC.filler, 145, 1, 0x5A),
|
||||
"TotG Dungeon Map": TWWItemData("Map", IC.filler, 146, 1, 0x5D),
|
||||
"TotG Compass": TWWItemData("Compass", IC.filler, 147, 1, 0x5E),
|
||||
"FF Dungeon Map": TWWItemData("Map", IC.filler, 148, 1, 0x5F),
|
||||
"FF Compass": TWWItemData("Compass", IC.filler, 149, 1, 0x60),
|
||||
"ET Dungeon Map": TWWItemData("Map", IC.filler, 150, 1, 0x75),
|
||||
"ET Compass": TWWItemData("Compass", IC.filler, 151, 1, 0x76),
|
||||
"WT Dungeon Map": TWWItemData("Map", IC.filler, 152, 1, 0x84),
|
||||
"WT Compass": TWWItemData("Compass", IC.filler, 153, 1, 0x85),
|
||||
|
||||
"Victory": TWWItemData("Event", IC.progression, None, 1, None),
|
||||
}
|
||||
|
||||
ISLAND_NUMBER_TO_CHART_NAME = {
|
||||
1: "Treasure Chart 25",
|
||||
2: "Treasure Chart 7",
|
||||
3: "Treasure Chart 24",
|
||||
4: "Triforce Chart 2",
|
||||
5: "Treasure Chart 11",
|
||||
6: "Triforce Chart 7",
|
||||
7: "Treasure Chart 13",
|
||||
8: "Treasure Chart 41",
|
||||
9: "Treasure Chart 29",
|
||||
10: "Treasure Chart 22",
|
||||
11: "Treasure Chart 18",
|
||||
12: "Treasure Chart 30",
|
||||
13: "Treasure Chart 39",
|
||||
14: "Treasure Chart 19",
|
||||
15: "Treasure Chart 8",
|
||||
16: "Treasure Chart 2",
|
||||
17: "Treasure Chart 10",
|
||||
18: "Treasure Chart 26",
|
||||
19: "Treasure Chart 3",
|
||||
20: "Treasure Chart 37",
|
||||
21: "Treasure Chart 27",
|
||||
22: "Treasure Chart 38",
|
||||
23: "Triforce Chart 1",
|
||||
24: "Treasure Chart 21",
|
||||
25: "Treasure Chart 6",
|
||||
26: "Treasure Chart 14",
|
||||
27: "Treasure Chart 34",
|
||||
28: "Treasure Chart 5",
|
||||
29: "Treasure Chart 28",
|
||||
30: "Treasure Chart 35",
|
||||
31: "Triforce Chart 3",
|
||||
32: "Triforce Chart 6",
|
||||
33: "Treasure Chart 1",
|
||||
34: "Treasure Chart 20",
|
||||
35: "Treasure Chart 36",
|
||||
36: "Treasure Chart 23",
|
||||
37: "Treasure Chart 12",
|
||||
38: "Treasure Chart 16",
|
||||
39: "Treasure Chart 4",
|
||||
40: "Treasure Chart 17",
|
||||
41: "Treasure Chart 31",
|
||||
42: "Triforce Chart 5",
|
||||
43: "Treasure Chart 9",
|
||||
44: "Triforce Chart 4",
|
||||
45: "Treasure Chart 40",
|
||||
46: "Triforce Chart 8",
|
||||
47: "Treasure Chart 15",
|
||||
48: "Treasure Chart 32",
|
||||
49: "Treasure Chart 33",
|
||||
}
|
||||
|
||||
|
||||
LOOKUP_ID_TO_NAME: dict[int, str] = {
|
||||
TWWItem.get_apid(data.code): item for item, data in ITEM_TABLE.items() if data.code is not None
|
||||
}
|
||||
|
||||
item_name_groups = {
|
||||
"Songs": {
|
||||
"Wind's Requiem",
|
||||
"Ballad of Gales",
|
||||
"Command Melody",
|
||||
"Earth God's Lyric",
|
||||
"Wind God's Aria",
|
||||
"Song of Passing",
|
||||
},
|
||||
"Mail": {
|
||||
"Note to Mom",
|
||||
"Maggie's Letter",
|
||||
"Moblin's Letter",
|
||||
},
|
||||
"Special Charts": {
|
||||
"Tingle's Chart",
|
||||
"Ghost Ship Chart",
|
||||
"Octo Chart",
|
||||
"Great Fairy Chart",
|
||||
"Secret Cave Chart",
|
||||
"Light Ring Chart",
|
||||
"Platform Chart",
|
||||
"Beedle's Chart",
|
||||
"Submarine Chart",
|
||||
},
|
||||
}
|
||||
# generic groups, (Name, substring)
|
||||
_simple_groups = {
|
||||
("Tingle Statues", "Tingle Statue"),
|
||||
("Shards", "Shard"),
|
||||
("Pearls", "Pearl"),
|
||||
("Triforce Charts", "Triforce Chart"),
|
||||
("Treasure Charts", "Treasure Chart"),
|
||||
("Small Keys", "Small Key"),
|
||||
("Big Keys", "Big Key"),
|
||||
("Rupees", "Rupee"),
|
||||
("Dungeon Items", "Compass"),
|
||||
("Dungeon Items", "Map"),
|
||||
}
|
||||
for basename, substring in _simple_groups:
|
||||
if basename not in item_name_groups:
|
||||
item_name_groups[basename] = set()
|
||||
for itemname in ITEM_TABLE:
|
||||
if substring in itemname:
|
||||
item_name_groups[basename].add(itemname)
|
||||
1272
worlds/tww/Locations.py
Normal file
1272
worlds/tww/Locations.py
Normal file
File diff suppressed because it is too large
Load Diff
1114
worlds/tww/Macros.py
Normal file
1114
worlds/tww/Macros.py
Normal file
File diff suppressed because it is too large
Load Diff
854
worlds/tww/Options.py
Normal file
854
worlds/tww/Options.py
Normal file
@@ -0,0 +1,854 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from Options import (
|
||||
Choice,
|
||||
DeathLink,
|
||||
DefaultOnToggle,
|
||||
OptionGroup,
|
||||
OptionSet,
|
||||
PerGameCommonOptions,
|
||||
Range,
|
||||
StartInventoryPool,
|
||||
Toggle,
|
||||
)
|
||||
|
||||
from .Locations import DUNGEON_NAMES
|
||||
|
||||
|
||||
class Dungeons(DefaultOnToggle):
|
||||
"""
|
||||
This controls whether dungeon locations are randomized.
|
||||
"""
|
||||
|
||||
display_name = "Dungeons"
|
||||
|
||||
|
||||
class TingleChests(Toggle):
|
||||
"""
|
||||
Tingle Chests are hidden in dungeons and must be bombed to make them appear. (2 in DRC, 1 each in FW, TotG, ET, and
|
||||
WT). This controls whether they are randomized.
|
||||
"""
|
||||
|
||||
display_name = "Tingle Chests"
|
||||
|
||||
|
||||
class DungeonSecrets(Toggle):
|
||||
"""
|
||||
DRC, FW, TotG, ET, and WT each contain 2-3 secret items (11 in total). This controls whether these are randomized.
|
||||
|
||||
The items are relatively well-hidden (they aren't in chests), so don't select this option unless you're prepared to
|
||||
search each dungeon high and low!
|
||||
"""
|
||||
|
||||
display_name = "Dungeon Secrets"
|
||||
|
||||
|
||||
class PuzzleSecretCaves(DefaultOnToggle):
|
||||
"""
|
||||
This controls whether the rewards from puzzle-focused secret caves are randomized locations.
|
||||
"""
|
||||
|
||||
display_name = "Puzzle Secret Caves"
|
||||
|
||||
|
||||
class CombatSecretCaves(Toggle):
|
||||
"""
|
||||
This controls whether the rewards from combat-focused secret caves (besides Savage Labyrinth) are randomized
|
||||
locations.
|
||||
"""
|
||||
|
||||
display_name = "Combat Secret Caves"
|
||||
|
||||
|
||||
class SavageLabyrinth(Toggle):
|
||||
"""
|
||||
This controls whether the two locations in Savage Labyrinth are randomized.
|
||||
"""
|
||||
|
||||
display_name = "Savage Labyrinth"
|
||||
|
||||
|
||||
class GreatFairies(DefaultOnToggle):
|
||||
"""
|
||||
This controls whether the items given by Great Fairies are randomized.
|
||||
"""
|
||||
|
||||
display_name = "Great Fairies"
|
||||
|
||||
|
||||
class ShortSidequests(Toggle):
|
||||
"""
|
||||
This controls whether sidequests that can be completed quickly are randomized.
|
||||
"""
|
||||
|
||||
display_name = "Short Sidequests"
|
||||
|
||||
|
||||
class LongSidequests(Toggle):
|
||||
"""
|
||||
This controls whether long sidequests (e.g., Lenzo's assistant, withered trees, goron trading) are randomized.
|
||||
"""
|
||||
|
||||
display_name = "Long Sidequests"
|
||||
|
||||
|
||||
class SpoilsTrading(Toggle):
|
||||
"""
|
||||
This controls whether the items you get by trading in spoils to NPCs are randomized.
|
||||
"""
|
||||
|
||||
display_name = "Spoils Trading"
|
||||
|
||||
|
||||
class Minigames(Toggle):
|
||||
"""
|
||||
This controls whether most minigames are randomized (auctions, mail sorting, barrel shooting, bird-man contest).
|
||||
"""
|
||||
|
||||
display_name = "Minigames"
|
||||
|
||||
|
||||
class Battlesquid(Toggle):
|
||||
"""
|
||||
This controls whether the Windfall battleship minigame is randomized.
|
||||
"""
|
||||
|
||||
display_name = "Battlesquid Minigame"
|
||||
|
||||
|
||||
class FreeGifts(DefaultOnToggle):
|
||||
"""
|
||||
This controls whether gifts freely given by NPCs are randomized (Tott, Salvage Corp, imprisoned Tingle).
|
||||
"""
|
||||
|
||||
display_name = "Free Gifts"
|
||||
|
||||
|
||||
class Mail(Toggle):
|
||||
"""
|
||||
This controls whether items received from the mail are randomized.
|
||||
"""
|
||||
|
||||
display_name = "Mail"
|
||||
|
||||
|
||||
class PlatformsRafts(Toggle):
|
||||
"""
|
||||
This controls whether lookout platforms and rafts are randomized.
|
||||
"""
|
||||
|
||||
display_name = "Lookout Platforms and Rafts"
|
||||
|
||||
|
||||
class Submarines(Toggle):
|
||||
"""
|
||||
This controls whether submarines are randomized.
|
||||
"""
|
||||
|
||||
display_name = "Submarines"
|
||||
|
||||
|
||||
class EyeReefChests(Toggle):
|
||||
"""
|
||||
This controls whether the chests that appear after clearing out the eye reefs are randomized.
|
||||
"""
|
||||
|
||||
display_name = "Eye Reef Chests"
|
||||
|
||||
|
||||
class BigOctosGunboats(Toggle):
|
||||
"""
|
||||
This controls whether the items dropped by Big Octos and Gunboats are randomized.
|
||||
"""
|
||||
|
||||
display_name = "Big Octos and Gunboats"
|
||||
|
||||
|
||||
class TriforceCharts(Toggle):
|
||||
"""
|
||||
This controls whether the sunken treasure chests marked on Triforce Charts are randomized.
|
||||
"""
|
||||
|
||||
display_name = "Sunken Treasure (From Triforce Charts)"
|
||||
|
||||
|
||||
class TreasureCharts(Toggle):
|
||||
"""
|
||||
This controls whether the sunken treasure chests marked on Treasure Charts are randomized.
|
||||
"""
|
||||
|
||||
display_name = "Sunken Treasure (From Treasure Charts)"
|
||||
|
||||
|
||||
class ExpensivePurchases(DefaultOnToggle):
|
||||
"""
|
||||
This controls whether items that cost many Rupees are randomized (Rock Spire shop, auctions, Tingle's letter,
|
||||
trading quest).
|
||||
"""
|
||||
|
||||
display_name = "Expensive Purchases"
|
||||
|
||||
|
||||
class IslandPuzzles(Toggle):
|
||||
"""
|
||||
This controls whether various island puzzles are randomized (e.g., chests hidden in unusual places).
|
||||
"""
|
||||
|
||||
display_name = "Island Puzzles"
|
||||
|
||||
|
||||
class Misc(Toggle):
|
||||
"""
|
||||
Miscellaneous locations that don't fit into any of the above categories (outdoors chests, wind shrine, Cyclos, etc).
|
||||
This controls whether these are randomized.
|
||||
"""
|
||||
|
||||
display_name = "Miscellaneous"
|
||||
|
||||
|
||||
class DungeonItem(Choice):
|
||||
"""
|
||||
This is the base class for the shuffle options for dungeon items.
|
||||
"""
|
||||
|
||||
value: int
|
||||
option_startwith = 0
|
||||
option_vanilla = 1
|
||||
option_dungeon = 2
|
||||
option_any_dungeon = 3
|
||||
option_local = 4
|
||||
option_keylunacy = 5
|
||||
default = 2
|
||||
|
||||
@property
|
||||
def in_dungeon(self) -> bool:
|
||||
"""
|
||||
Return whether the item should be shuffled into a dungeon.
|
||||
|
||||
:return: Whether the item is shuffled into a dungeon.
|
||||
"""
|
||||
return self.value in (2, 3)
|
||||
|
||||
|
||||
class RandomizeMapCompass(DungeonItem):
|
||||
"""
|
||||
Controls how dungeon maps and compasses are randomized.
|
||||
|
||||
- **Start With Maps & Compasses:** You will start the game with the dungeon maps and compasses for all dungeons.
|
||||
- **Vanilla Maps & Compasses:** Dungeon maps and compasses will be kept in their vanilla location (non-randomized).
|
||||
- **Own Dungeon Maps & Compasses:** Dungeon maps and compasses will be randomized locally within their own dungeon.
|
||||
- **Any Dungeon Maps & Compasses:** Dungeon maps and compasses will be randomized locally within any dungeon.
|
||||
- **Local Maps & Compasses:** Dungeon maps and compasses will be randomized locally anywhere.
|
||||
- **Key-Lunacy:** Dungeon maps and compasses can be found anywhere, without restriction.
|
||||
"""
|
||||
|
||||
item_name_group = "Dungeon Items"
|
||||
display_name = "Randomize Maps & Compasses"
|
||||
default = 2
|
||||
|
||||
|
||||
class RandomizeSmallKeys(DungeonItem):
|
||||
"""
|
||||
Controls how small keys are randomized.
|
||||
|
||||
- **Start With Small Keys:** You will start the game with the small keys for all dungeons.
|
||||
- **Vanilla Small Keys:** Small keys will be kept in their vanilla location (non-randomized).
|
||||
- **Own Dungeon Small Keys:** Small keys will be randomized locally within their own dungeon.
|
||||
- **Any Dungeon Small Keys:** Small keys will be randomized locally within any dungeon.
|
||||
- **Local Small Keys:** Small keys will be randomized locally anywhere.
|
||||
- **Key-Lunacy:** Small keys can be found in any progression location, if dungeons are randomized.
|
||||
"""
|
||||
|
||||
item_name_group = "Small Keys"
|
||||
display_name = "Randomize Small Keys"
|
||||
default = 2
|
||||
|
||||
|
||||
class RandomizeBigKeys(DungeonItem):
|
||||
"""
|
||||
Controls how big keys are randomized.
|
||||
|
||||
- **Start With Big Keys:** You will start the game with the big keys for all dungeons.
|
||||
- **Vanilla Big Keys:** Big keys will be kept in their vanilla location (non-randomized).
|
||||
- **Own Dungeon Big Keys:** Big keys will be randomized locally within their own dungeon.
|
||||
- **Any Dungeon Big Keys:** Big keys will be randomized locally within any dungeon.
|
||||
- **Local Big Keys:** Big keys will be randomized locally anywhere.
|
||||
- **Key-Lunacy:** Big keys can be found in any progression location, if dungeons are randomized.
|
||||
"""
|
||||
|
||||
item_name_group = "Big Keys"
|
||||
display_name = "Randomize Big Keys"
|
||||
default = 2
|
||||
|
||||
|
||||
class SwordMode(Choice):
|
||||
"""
|
||||
Controls whether you start with the Hero's Sword, the Hero's Sword is randomized, or if there are no swords in the
|
||||
entire game.
|
||||
|
||||
- **Start with Hero's Sword:** You will start the game with the basic Hero's Sword already in your inventory.
|
||||
- **No Starting Sword:** You will start the game with no sword, and have to find it somewhere in the world like
|
||||
other randomized items.
|
||||
- **Swords Optional:** You will start the game with no sword, but they'll still be randomized. However, they are not
|
||||
necessary to beat the game. The Hyrule Barrier will be gone, Phantom Ganon in FF is vulnerable to Skull Hammer,
|
||||
and the logic does not expect you to have a sword.
|
||||
- **Swordless:** You will start the game with no sword, and won't be able to find it anywhere. You have to beat the
|
||||
entire game using other items as weapons instead of the sword. (Note that Phantom Ganon in FF becomes vulnerable
|
||||
to Skull Hammer in this mode.)
|
||||
"""
|
||||
|
||||
display_name = "Sword Mode"
|
||||
option_start_with_sword = 0
|
||||
option_no_starting_sword = 1
|
||||
option_swords_optional = 2
|
||||
option_swordless = 3
|
||||
default = 0
|
||||
|
||||
|
||||
class RequiredBosses(Toggle):
|
||||
"""
|
||||
In this mode, you will not be allowed to beat the game until certain randomly-chosen bosses are defeated. Nothing in
|
||||
dungeons for other bosses will ever be required.
|
||||
|
||||
You can see which islands have the required bosses on them by opening the sea chart and checking which islands have
|
||||
blue quest markers.
|
||||
"""
|
||||
|
||||
display_name = "Required Bosses Mode"
|
||||
|
||||
|
||||
class NumRequiredBosses(Range):
|
||||
"""
|
||||
Select the number of randomly-chosen bosses that are required in Required Bosses Mode.
|
||||
|
||||
The door to Puppet Ganon will not unlock until you've defeated all of these bosses. Nothing in dungeons for other
|
||||
bosses will ever be required.
|
||||
"""
|
||||
|
||||
display_name = "Number of Required Bosses"
|
||||
range_start = 1
|
||||
range_end = 6
|
||||
default = 4
|
||||
|
||||
|
||||
class IncludedDungeons(OptionSet):
|
||||
"""
|
||||
A list of dungeons that should always be included when required bosses mode is on.
|
||||
"""
|
||||
|
||||
display_name = "Included Dungeons"
|
||||
valid_keys = frozenset(DUNGEON_NAMES)
|
||||
|
||||
|
||||
class ExcludedDungeons(OptionSet):
|
||||
"""
|
||||
A list of dungeons that should always be excluded when required bosses mode is on.
|
||||
"""
|
||||
|
||||
display_name = "Excluded Dungeons"
|
||||
valid_keys = frozenset(DUNGEON_NAMES)
|
||||
|
||||
|
||||
class ChestTypeMatchesContents(Toggle):
|
||||
"""
|
||||
Changes the chest type to reflect its contents. A metal chest has a progress item, a wooden chest has a non-progress
|
||||
item or a consumable, and a green chest has a potentially required dungeon key.
|
||||
"""
|
||||
|
||||
display_name = "Chest Type Matches Contents"
|
||||
|
||||
|
||||
class TrapChests(Toggle):
|
||||
"""
|
||||
**DEV NOTE:** This option is currently unimplemented and will be ignored.
|
||||
|
||||
Allows the randomizer to place several trapped chests across the game that do not give you items. Perfect for
|
||||
spicing up any run!
|
||||
"""
|
||||
|
||||
display_name = "Enable Trap Chests"
|
||||
|
||||
|
||||
class HeroMode(Toggle):
|
||||
"""
|
||||
In Hero Mode, you take four times more damage than normal and heart refills will not drop.
|
||||
"""
|
||||
|
||||
display_name = "Hero Mode"
|
||||
|
||||
|
||||
class LogicObscurity(Choice):
|
||||
"""
|
||||
Obscure tricks are ways of obtaining items that are not obvious and may involve thinking outside the box.
|
||||
|
||||
This option controls the maximum difficulty of obscure tricks the randomizer will require you to do to beat the
|
||||
game.
|
||||
"""
|
||||
|
||||
display_name = "Obscure Tricks Required"
|
||||
option_none = 0
|
||||
option_normal = 1
|
||||
option_hard = 2
|
||||
option_very_hard = 3
|
||||
default = 0
|
||||
|
||||
|
||||
class LogicPrecision(Choice):
|
||||
"""
|
||||
Precise tricks are ways of obtaining items that involve difficult inputs such as accurate aiming or perfect timing.
|
||||
|
||||
This option controls the maximum difficulty of precise tricks the randomizer will require you to do to beat the
|
||||
game.
|
||||
"""
|
||||
|
||||
display_name = "Precise Tricks Required"
|
||||
option_none = 0
|
||||
option_normal = 1
|
||||
option_hard = 2
|
||||
option_very_hard = 3
|
||||
default = 0
|
||||
|
||||
|
||||
class EnableTunerLogic(Toggle):
|
||||
"""
|
||||
If enabled, the randomizer can logically expect the Tingle Tuner for Tingle Chests.
|
||||
|
||||
The randomizer behavior of logically expecting Bombs/bomb flowers to spawn in Tingle Chests remains unchanged.
|
||||
"""
|
||||
|
||||
display_name = "Enable Tuner Logic"
|
||||
|
||||
|
||||
class RandomizeDungeonEntrances(Toggle):
|
||||
"""
|
||||
Shuffles around which dungeon entrances take you into which dungeons.
|
||||
|
||||
(No effect on Forsaken Fortress or Ganon's Tower.)
|
||||
"""
|
||||
|
||||
display_name = "Randomize Dungeons"
|
||||
|
||||
|
||||
class RandomizeSecretCavesEntrances(Toggle):
|
||||
"""
|
||||
Shuffles around which secret cave entrances take you into which secret caves.
|
||||
"""
|
||||
|
||||
display_name = "Randomize Secret Caves"
|
||||
|
||||
|
||||
class RandomizeMinibossEntrances(Toggle):
|
||||
"""
|
||||
Allows dungeon miniboss doors to act as entrances to be randomized.
|
||||
|
||||
If on with random dungeon entrances, dungeons may nest within each other, forming chains of connected dungeons.
|
||||
"""
|
||||
|
||||
display_name = "Randomize Nested Minibosses"
|
||||
|
||||
|
||||
class RandomizeBossEntrances(Toggle):
|
||||
"""
|
||||
Allows dungeon boss doors to act as entrances to be randomized.
|
||||
|
||||
If on with random dungeon entrances, dungeons may nest within each other, forming chains of connected dungeons.
|
||||
"""
|
||||
|
||||
display_name = "Randomize Nested Bosses"
|
||||
|
||||
|
||||
class RandomizeSecretCaveInnerEntrances(Toggle):
|
||||
"""
|
||||
Allows the pit in Ice Ring Isle's secret cave and the rear exit out of Cliff Plateau Isles' secret cave to act as
|
||||
entrances to be randomized."""
|
||||
|
||||
display_name = "Randomize Inner Secret Caves"
|
||||
|
||||
|
||||
class RandomizeFairyFountainEntrances(Toggle):
|
||||
"""
|
||||
Allows the pits that lead down into Fairy Fountains to act as entrances to be randomized.
|
||||
"""
|
||||
|
||||
display_name = "Randomize Fairy Fountains"
|
||||
|
||||
|
||||
class MixEntrances(Choice):
|
||||
"""
|
||||
Controls how the different types (pools) of randomized entrances should be shuffled.
|
||||
|
||||
- **Separate Pools:** Each pool of randomized entrances will shuffle into itself (e.g., dungeons into dungeons).
|
||||
- **Mix Pools:** All pools of randomized entrances will be combined into one pool to be shuffled.
|
||||
"""
|
||||
|
||||
display_name = "Mix Entrances"
|
||||
option_separate_pools = 0
|
||||
option_mix_pools = 1
|
||||
default = 0
|
||||
|
||||
|
||||
class RandomizeEnemies(Toggle):
|
||||
"""
|
||||
Randomizes the placement of non-boss enemies.
|
||||
|
||||
This option is an *incomplete* option from the base randomizer and **may result in unbeatable seeds! Use at your own
|
||||
risk!**
|
||||
"""
|
||||
|
||||
display_name = "Randomize Enemies"
|
||||
|
||||
|
||||
# class RandomizeMusic(Toggle):
|
||||
# """
|
||||
# Shuffles around all the music in the game. This affects background music, combat music, fanfares, etc.
|
||||
# """
|
||||
|
||||
# display_name = "Randomize Music"
|
||||
|
||||
|
||||
class RandomizeStartingIsland(Toggle):
|
||||
"""
|
||||
Randomizes which island you start the game on.
|
||||
"""
|
||||
|
||||
display_name = "Randomize Starting Island"
|
||||
|
||||
|
||||
class RandomizeCharts(Toggle):
|
||||
"""
|
||||
Randomizes which sector is drawn on each Triforce/Treasure Chart.
|
||||
"""
|
||||
|
||||
display_name = "Randomize Charts"
|
||||
|
||||
|
||||
class HoHoHints(DefaultOnToggle):
|
||||
"""
|
||||
**DEV NOTE:** This option is currently unimplemented and will be ignored.
|
||||
|
||||
Places hints on Old Man Ho Ho. Old Man Ho Ho appears at 10 different islands in the game. Talk to Old Man Ho Ho to
|
||||
get hints.
|
||||
"""
|
||||
|
||||
display_name = "Place Hints on Old Man Ho Ho"
|
||||
|
||||
|
||||
class FishmenHints(DefaultOnToggle):
|
||||
"""
|
||||
**DEV NOTE:** This option is currently unimplemented and will be ignored.
|
||||
|
||||
Places hints on the fishmen. There is one fishman at each of the 49 islands of the Great Sea. Each fishman must be
|
||||
fed an All-Purpose Bait before he will give a hint.
|
||||
"""
|
||||
|
||||
display_name = "Place Hints on Fishmen"
|
||||
|
||||
|
||||
class KoRLHints(Toggle):
|
||||
"""
|
||||
**DEV NOTE:** This option is currently unimplemented and will be ignored.
|
||||
|
||||
Places hints on the King of Red Lions. Talk to the King of Red Lions to get hints.
|
||||
"""
|
||||
|
||||
display_name = "Place Hints on King of Red Lions"
|
||||
|
||||
|
||||
class NumItemHints(Range):
|
||||
"""
|
||||
**DEV NOTE:** This option is currently unimplemented and will be ignored.
|
||||
|
||||
The number of item hints that will be placed. Item hints tell you which area contains a particular progress item in
|
||||
this seed.
|
||||
|
||||
If multiple hint placement options are selected, the hint count will be split evenly among the placement options.
|
||||
"""
|
||||
|
||||
display_name = "Item Hints"
|
||||
range_start = 0
|
||||
range_end = 15
|
||||
default = 15
|
||||
|
||||
|
||||
class NumLocationHints(Range):
|
||||
"""
|
||||
**DEV NOTE:** This option is currently unimplemented and will be ignored.
|
||||
|
||||
The number of location hints that will be placed. Location hints tell you what item is at a specific location in
|
||||
this seed.
|
||||
|
||||
If multiple hint placement options are selected, the hint count will be split evenly among the placement options.
|
||||
"""
|
||||
|
||||
display_name = "Location Hints"
|
||||
range_start = 0
|
||||
range_end = 15
|
||||
default = 5
|
||||
|
||||
|
||||
class NumBarrenHints(Range):
|
||||
"""
|
||||
**DEV NOTE:** This option is currently unimplemented and will be ignored.
|
||||
|
||||
The number of barren hints that will be placed. Barren hints tell you that an area does not contain any required
|
||||
items in this seed.
|
||||
|
||||
If multiple hint placement options are selected, the hint count will be split evenly among the placement options.
|
||||
"""
|
||||
|
||||
display_name = "Barren Hints"
|
||||
range_start = 0
|
||||
range_end = 15
|
||||
default = 0
|
||||
|
||||
|
||||
class NumPathHints(Range):
|
||||
"""
|
||||
**DEV NOTE:** This option is currently unimplemented and will be ignored.
|
||||
|
||||
The number of path hints that will be placed. Path hints tell you that an area contains an item that is required to
|
||||
reach a particular goal in this seed.
|
||||
|
||||
If multiple hint placement options are selected, the hint count will be split evenly among the placement options.
|
||||
"""
|
||||
|
||||
display_name = "Path Hints"
|
||||
range_start = 0
|
||||
range_end = 15
|
||||
default = 0
|
||||
|
||||
|
||||
class PrioritizeRemoteHints(Toggle):
|
||||
"""
|
||||
**DEV NOTE:** This option is currently unimplemented and will be ignored.
|
||||
|
||||
When this option is selected, certain locations that are out of the way and time-consuming to complete will take
|
||||
precedence over normal location hints."""
|
||||
|
||||
display_name = "Prioritize Remote Location Hints"
|
||||
|
||||
|
||||
class SwiftSail(DefaultOnToggle):
|
||||
"""
|
||||
Sailing speed is doubled and the direction of the wind is always at your back as long as the sail is out.
|
||||
"""
|
||||
|
||||
display_name = "Swift Sail"
|
||||
|
||||
|
||||
class InstantTextBoxes(DefaultOnToggle):
|
||||
"""
|
||||
Text appears instantly. Also, the B button is changed to instantly skip through text as long as you hold it down.
|
||||
"""
|
||||
|
||||
display_name = "Instant Text Boxes"
|
||||
|
||||
|
||||
class RevealFullSeaChart(DefaultOnToggle):
|
||||
"""
|
||||
Start the game with the sea chart fully drawn out.
|
||||
"""
|
||||
|
||||
display_name = "Reveal Full Sea Chart"
|
||||
|
||||
|
||||
class AddShortcutWarpsBetweenDungeons(Toggle):
|
||||
"""
|
||||
Adds new warp pots that act as shortcuts connecting dungeons to each other directly. (DRC, FW, TotG, and separately
|
||||
FF, ET, WT.)
|
||||
|
||||
Each pot must be unlocked before it can be used, so you cannot use them to access dungeons
|
||||
you wouldn't already have access to.
|
||||
"""
|
||||
|
||||
display_name = "Add Shortcut Warps Between Dungeons"
|
||||
|
||||
|
||||
class SkipRematchBosses(DefaultOnToggle):
|
||||
"""
|
||||
Removes the door in Ganon's Tower that only unlocks when you defeat the rematch versions of Gohma, Kalle Demos,
|
||||
Jalhalla, and Molgera.
|
||||
"""
|
||||
|
||||
display_name = "Skip Boss Rematches"
|
||||
|
||||
|
||||
class RemoveMusic(Toggle):
|
||||
"""
|
||||
Mutes all ingame music.
|
||||
"""
|
||||
|
||||
display_name = "Remove Music"
|
||||
|
||||
|
||||
@dataclass
|
||||
class TWWOptions(PerGameCommonOptions):
|
||||
"""
|
||||
A data class that encapsulates all configuration options for The Wind Waker.
|
||||
"""
|
||||
|
||||
start_inventory_from_pool: StartInventoryPool
|
||||
progression_dungeons: Dungeons
|
||||
progression_tingle_chests: TingleChests
|
||||
progression_dungeon_secrets: DungeonSecrets
|
||||
progression_puzzle_secret_caves: PuzzleSecretCaves
|
||||
progression_combat_secret_caves: CombatSecretCaves
|
||||
progression_savage_labyrinth: SavageLabyrinth
|
||||
progression_great_fairies: GreatFairies
|
||||
progression_short_sidequests: ShortSidequests
|
||||
progression_long_sidequests: LongSidequests
|
||||
progression_spoils_trading: SpoilsTrading
|
||||
progression_minigames: Minigames
|
||||
progression_battlesquid: Battlesquid
|
||||
progression_free_gifts: FreeGifts
|
||||
progression_mail: Mail
|
||||
progression_platforms_rafts: PlatformsRafts
|
||||
progression_submarines: Submarines
|
||||
progression_eye_reef_chests: EyeReefChests
|
||||
progression_big_octos_gunboats: BigOctosGunboats
|
||||
progression_triforce_charts: TriforceCharts
|
||||
progression_treasure_charts: TreasureCharts
|
||||
progression_expensive_purchases: ExpensivePurchases
|
||||
progression_island_puzzles: IslandPuzzles
|
||||
progression_misc: Misc
|
||||
randomize_mapcompass: RandomizeMapCompass
|
||||
randomize_smallkeys: RandomizeSmallKeys
|
||||
randomize_bigkeys: RandomizeBigKeys
|
||||
sword_mode: SwordMode
|
||||
required_bosses: RequiredBosses
|
||||
num_required_bosses: NumRequiredBosses
|
||||
included_dungeons: IncludedDungeons
|
||||
excluded_dungeons: ExcludedDungeons
|
||||
chest_type_matches_contents: ChestTypeMatchesContents
|
||||
# trap_chests: TrapChests
|
||||
hero_mode: HeroMode
|
||||
logic_obscurity: LogicObscurity
|
||||
logic_precision: LogicPrecision
|
||||
enable_tuner_logic: EnableTunerLogic
|
||||
randomize_dungeon_entrances: RandomizeDungeonEntrances
|
||||
randomize_secret_cave_entrances: RandomizeSecretCavesEntrances
|
||||
randomize_miniboss_entrances: RandomizeMinibossEntrances
|
||||
randomize_boss_entrances: RandomizeBossEntrances
|
||||
randomize_secret_cave_inner_entrances: RandomizeSecretCaveInnerEntrances
|
||||
randomize_fairy_fountain_entrances: RandomizeFairyFountainEntrances
|
||||
mix_entrances: MixEntrances
|
||||
randomize_enemies: RandomizeEnemies
|
||||
# randomize_music: RandomizeMusic
|
||||
randomize_starting_island: RandomizeStartingIsland
|
||||
randomize_charts: RandomizeCharts
|
||||
# hoho_hints: HoHoHints
|
||||
# fishmen_hints: FishmenHints
|
||||
# korl_hints: KoRLHints
|
||||
# num_item_hints: NumItemHints
|
||||
# num_location_hints: NumLocationHints
|
||||
# num_barren_hints: NumBarrenHints
|
||||
# num_path_hints: NumPathHints
|
||||
# prioritize_remote_hints: PrioritizeRemoteHints
|
||||
swift_sail: SwiftSail
|
||||
instant_text_boxes: InstantTextBoxes
|
||||
reveal_full_sea_chart: RevealFullSeaChart
|
||||
add_shortcut_warps_between_dungeons: AddShortcutWarpsBetweenDungeons
|
||||
skip_rematch_bosses: SkipRematchBosses
|
||||
remove_music: RemoveMusic
|
||||
death_link: DeathLink
|
||||
|
||||
|
||||
tww_option_groups: list[OptionGroup] = [
|
||||
OptionGroup(
|
||||
"Progression Locations",
|
||||
[
|
||||
Dungeons,
|
||||
DungeonSecrets,
|
||||
TingleChests,
|
||||
PuzzleSecretCaves,
|
||||
CombatSecretCaves,
|
||||
SavageLabyrinth,
|
||||
IslandPuzzles,
|
||||
GreatFairies,
|
||||
Submarines,
|
||||
PlatformsRafts,
|
||||
ShortSidequests,
|
||||
LongSidequests,
|
||||
SpoilsTrading,
|
||||
EyeReefChests,
|
||||
BigOctosGunboats,
|
||||
Misc,
|
||||
Minigames,
|
||||
Battlesquid,
|
||||
FreeGifts,
|
||||
Mail,
|
||||
ExpensivePurchases,
|
||||
TriforceCharts,
|
||||
TreasureCharts,
|
||||
],
|
||||
),
|
||||
OptionGroup(
|
||||
"Item Randomizer Modes",
|
||||
[
|
||||
SwordMode,
|
||||
RandomizeMapCompass,
|
||||
RandomizeSmallKeys,
|
||||
RandomizeBigKeys,
|
||||
ChestTypeMatchesContents,
|
||||
# TrapChests,
|
||||
],
|
||||
),
|
||||
OptionGroup(
|
||||
"Entrance Randomizer Options",
|
||||
[
|
||||
RandomizeDungeonEntrances,
|
||||
RandomizeBossEntrances,
|
||||
RandomizeMinibossEntrances,
|
||||
RandomizeSecretCavesEntrances,
|
||||
RandomizeSecretCaveInnerEntrances,
|
||||
RandomizeFairyFountainEntrances,
|
||||
MixEntrances,
|
||||
],
|
||||
),
|
||||
OptionGroup(
|
||||
"Other Randomizers",
|
||||
[
|
||||
RandomizeStartingIsland,
|
||||
RandomizeCharts,
|
||||
# RandomizeMusic,
|
||||
],
|
||||
),
|
||||
OptionGroup(
|
||||
"Convenience Tweaks",
|
||||
[
|
||||
SwiftSail,
|
||||
InstantTextBoxes,
|
||||
RevealFullSeaChart,
|
||||
SkipRematchBosses,
|
||||
AddShortcutWarpsBetweenDungeons,
|
||||
RemoveMusic,
|
||||
],
|
||||
),
|
||||
OptionGroup(
|
||||
"Required Bosses",
|
||||
[
|
||||
RequiredBosses,
|
||||
NumRequiredBosses,
|
||||
IncludedDungeons,
|
||||
ExcludedDungeons,
|
||||
],
|
||||
start_collapsed=True,
|
||||
),
|
||||
OptionGroup(
|
||||
"Difficulty Options",
|
||||
[
|
||||
HeroMode,
|
||||
LogicObscurity,
|
||||
LogicPrecision,
|
||||
EnableTunerLogic,
|
||||
],
|
||||
start_collapsed=True,
|
||||
),
|
||||
OptionGroup(
|
||||
"Work-in-Progress Options",
|
||||
[
|
||||
RandomizeEnemies,
|
||||
],
|
||||
start_collapsed=True,
|
||||
),
|
||||
]
|
||||
138
worlds/tww/Presets.py
Normal file
138
worlds/tww/Presets.py
Normal file
@@ -0,0 +1,138 @@
|
||||
from typing import Any
|
||||
|
||||
tww_options_presets: dict[str, dict[str, Any]] = {
|
||||
"Tournament S7": {
|
||||
"progression_dungeon_secrets": True,
|
||||
"progression_combat_secret_caves": True,
|
||||
"progression_short_sidequests": True,
|
||||
"progression_spoils_trading": True,
|
||||
"progression_big_octos_gunboats": True,
|
||||
"progression_mail": True,
|
||||
"progression_island_puzzles": True,
|
||||
"progression_misc": True,
|
||||
"randomize_mapcompass": "startwith",
|
||||
"required_bosses": True,
|
||||
"num_required_bosses": 3,
|
||||
"chest_type_matches_contents": True,
|
||||
"logic_obscurity": "hard",
|
||||
"randomize_starting_island": True,
|
||||
"add_shortcut_warps_between_dungeons": True,
|
||||
"start_inventory_from_pool": {
|
||||
"Telescope": 1,
|
||||
"Wind Waker": 1,
|
||||
"Goddess Tingle Statue": 1,
|
||||
"Earth Tingle Statue": 1,
|
||||
"Wind Tingle Statue": 1,
|
||||
"Wind's Requiem": 1,
|
||||
"Ballad of Gales": 1,
|
||||
"Earth God's Lyric": 1,
|
||||
"Wind God's Aria": 1,
|
||||
"Song of Passing": 1,
|
||||
"Progressive Magic Meter": 2,
|
||||
},
|
||||
"start_location_hints": ["Ganon's Tower - Maze Chest"],
|
||||
"exclude_locations": [
|
||||
"Outset Island - Orca - Give 10 Knight's Crests",
|
||||
"Outset Island - Great Fairy",
|
||||
"Windfall Island - Chu Jelly Juice Shop - Give 15 Green Chu Jelly",
|
||||
"Windfall Island - Mrs. Marie - Give 21 Joy Pendants",
|
||||
"Windfall Island - Mrs. Marie - Give 40 Joy Pendants",
|
||||
"Windfall Island - Maggie's Father - Give 20 Skull Necklaces",
|
||||
"Dragon Roost Island - Rito Aerie - Give Hoskit 20 Golden Feathers",
|
||||
"Fire Mountain - Big Octo",
|
||||
"Mailbox - Letter from Hoskit's Girlfriend",
|
||||
"Private Oasis - Big Octo",
|
||||
"Stone Watcher Island - Cave",
|
||||
"Overlook Island - Cave",
|
||||
"Thorned Fairy Island - Great Fairy",
|
||||
"Eastern Fairy Island - Great Fairy",
|
||||
"Western Fairy Island - Great Fairy",
|
||||
"Southern Fairy Island - Great Fairy",
|
||||
"Northern Fairy Island - Great Fairy",
|
||||
"Tingle Island - Big Octo",
|
||||
"Diamond Steppe Island - Big Octo",
|
||||
"Rock Spire Isle - Beedle's Special Shop Ship - 500 Rupee Item",
|
||||
"Rock Spire Isle - Beedle's Special Shop Ship - 950 Rupee Item",
|
||||
"Rock Spire Isle - Beedle's Special Shop Ship - 900 Rupee Item",
|
||||
"Shark Island - Cave",
|
||||
"Seven-Star Isles - Big Octo",
|
||||
],
|
||||
},
|
||||
"Miniblins 2025": {
|
||||
"progression_great_fairies": False,
|
||||
"progression_short_sidequests": True,
|
||||
"progression_mail": True,
|
||||
"progression_expensive_purchases": False,
|
||||
"progression_island_puzzles": True,
|
||||
"progression_misc": True,
|
||||
"randomize_mapcompass": "startwith",
|
||||
"required_bosses": True,
|
||||
"num_required_bosses": 2,
|
||||
"chest_type_matches_contents": True,
|
||||
"randomize_starting_island": True,
|
||||
"add_shortcut_warps_between_dungeons": True,
|
||||
"start_inventory_from_pool": {
|
||||
"Telescope": 1,
|
||||
"Wind Waker": 1,
|
||||
"Wind's Requiem": 1,
|
||||
"Ballad of Gales": 1,
|
||||
"Command Melody": 1,
|
||||
"Earth God's Lyric": 1,
|
||||
"Wind God's Aria": 1,
|
||||
"Song of Passing": 1,
|
||||
"Nayru's Pearl": 1,
|
||||
"Din's Pearl": 1,
|
||||
"Progressive Shield": 1,
|
||||
"Progressive Magic Meter": 2,
|
||||
"Quiver Capacity Upgrade": 1,
|
||||
"Bomb Bag Capacity Upgrade": 1,
|
||||
"Piece of Heart": 12,
|
||||
},
|
||||
"start_location_hints": ["Ganon's Tower - Maze Chest"],
|
||||
"exclude_locations": [
|
||||
"Outset Island - Jabun's Cave",
|
||||
"Windfall Island - Jail - Tingle - First Gift",
|
||||
"Windfall Island - Jail - Tingle - Second Gift",
|
||||
"Windfall Island - Jail - Maze Chest",
|
||||
"Windfall Island - Maggie - Delivery Reward",
|
||||
"Windfall Island - Cafe Bar - Postman",
|
||||
"Windfall Island - Zunari - Stock Exotic Flower in Zunari's Shop",
|
||||
"Tingle Island - Ankle - Reward for All Tingle Statues",
|
||||
"Horseshoe Island - Play Golf",
|
||||
],
|
||||
},
|
||||
"Mixed Pools": {
|
||||
"progression_tingle_chests": True,
|
||||
"progression_dungeon_secrets": True,
|
||||
"progression_combat_secret_caves": True,
|
||||
"progression_short_sidequests": True,
|
||||
"progression_mail": True,
|
||||
"progression_submarines": True,
|
||||
"progression_expensive_purchases": False,
|
||||
"progression_island_puzzles": True,
|
||||
"progression_misc": True,
|
||||
"randomize_mapcompass": "startwith",
|
||||
"required_bosses": True,
|
||||
"num_required_bosses": 6,
|
||||
"chest_type_matches_contents": True,
|
||||
"randomize_dungeon_entrances": True,
|
||||
"randomize_secret_cave_entrances": True,
|
||||
"randomize_miniboss_entrances": True,
|
||||
"randomize_boss_entrances": True,
|
||||
"randomize_secret_cave_inner_entrances": True,
|
||||
"randomize_fairy_fountain_entrances": True,
|
||||
"mix_entrances": "mix_pools",
|
||||
"randomize_starting_island": True,
|
||||
"add_shortcut_warps_between_dungeons": True,
|
||||
"start_inventory_from_pool": {
|
||||
"Telescope": 1,
|
||||
"Wind Waker": 1,
|
||||
"Wind's Requiem": 1,
|
||||
"Ballad of Gales": 1,
|
||||
"Earth God's Lyric": 1,
|
||||
"Wind God's Aria": 1,
|
||||
"Song of Passing": 1,
|
||||
},
|
||||
"start_location_hints": ["Ganon's Tower - Maze Chest", "Shark Island - Cave"],
|
||||
},
|
||||
}
|
||||
1414
worlds/tww/Rules.py
Normal file
1414
worlds/tww/Rules.py
Normal file
File diff suppressed because it is too large
Load Diff
739
worlds/tww/TWWClient.py
Normal file
739
worlds/tww/TWWClient.py
Normal file
@@ -0,0 +1,739 @@
|
||||
import asyncio
|
||||
import time
|
||||
import traceback
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
import dolphin_memory_engine
|
||||
|
||||
import Utils
|
||||
from CommonClient import ClientCommandProcessor, CommonContext, get_base_parser, gui_enabled, logger, server_loop
|
||||
from NetUtils import ClientStatus
|
||||
|
||||
from .Items import ITEM_TABLE, LOOKUP_ID_TO_NAME
|
||||
from .Locations import ISLAND_NAME_TO_SALVAGE_BIT, LOCATION_TABLE, TWWLocation, TWWLocationData, TWWLocationType
|
||||
from .randomizers.Charts import ISLAND_NUMBER_TO_NAME
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import kvui
|
||||
|
||||
CONNECTION_REFUSED_GAME_STATUS = (
|
||||
"Dolphin failed to connect. Please load a randomized ROM for The Wind Waker. Trying again in 5 seconds..."
|
||||
)
|
||||
CONNECTION_REFUSED_SAVE_STATUS = (
|
||||
"Dolphin failed to connect. Please load into the save file. Trying again in 5 seconds..."
|
||||
)
|
||||
CONNECTION_LOST_STATUS = (
|
||||
"Dolphin connection was lost. Please restart your emulator and make sure The Wind Waker is running."
|
||||
)
|
||||
CONNECTION_CONNECTED_STATUS = "Dolphin connected successfully."
|
||||
CONNECTION_INITIAL_STATUS = "Dolphin connection has not been initiated."
|
||||
|
||||
|
||||
# This address is used to check/set the player's health for DeathLink.
|
||||
CURR_HEALTH_ADDR = 0x803C4C0A
|
||||
|
||||
# These addresses are used for the Moblin's Letter check.
|
||||
LETTER_BASE_ADDR = 0x803C4C8E
|
||||
LETTER_OWND_ADDR = 0x803C4C98
|
||||
|
||||
# These addresses are used to check flags for locations.
|
||||
CHARTS_BITFLD_ADDR = 0x803C4CFC
|
||||
BASE_CHESTS_BITFLD_ADDR = 0x803C4F88
|
||||
BASE_SWITCHES_BITFLD_ADDR = 0x803C4F8C
|
||||
BASE_PICKUPS_BITFLD_ADDR = 0x803C4F9C
|
||||
CURR_STAGE_CHESTS_BITFLD_ADDR = 0x803C5380
|
||||
CURR_STAGE_SWITCHES_BITFLD_ADDR = 0x803C5384
|
||||
CURR_STAGE_PICKUPS_BITFLD_ADDR = 0x803C5394
|
||||
|
||||
# The expected index for the following item that should be received. Uses event bits 0x60 and 0x61.
|
||||
EXPECTED_INDEX_ADDR = 0x803C528C
|
||||
|
||||
# These bytes contain whether the player has been rewarded for finding a particular Tingle statue.
|
||||
TINGLE_STATUE_1_ADDR = 0x803C523E # 0x40 is the bit for the Dragon Tingle statue.
|
||||
TINGLE_STATUE_2_ADDR = 0x803C5249 # 0x0F are the bits for the remaining Tingle statues.
|
||||
|
||||
# This address contains the current stage ID.
|
||||
CURR_STAGE_ID_ADDR = 0x803C53A4
|
||||
|
||||
# This address is used to check the stage name to verify that the player is in-game before sending items.
|
||||
CURR_STAGE_NAME_ADDR = 0x803C9D3C
|
||||
|
||||
# This is an array of length 0x10 where each element is a byte and contains item IDs for items to give the player.
|
||||
# 0xFF represents no item. The array is read and cleared every frame.
|
||||
GIVE_ITEM_ARRAY_ADDR = 0x803FE87C
|
||||
|
||||
# This is the address that holds the player's slot name.
|
||||
# This way, the player does not have to manually authenticate their slot name.
|
||||
SLOT_NAME_ADDR = 0x803FE8A0
|
||||
|
||||
# This address is the start of an array that we use to inform us of which charts lead where.
|
||||
# The array is of length 49, and each element is two bytes. The index represents the chart's original destination, and
|
||||
# the value represents the new destination.
|
||||
# The chart name is inferrable from the chart's original destination.
|
||||
CHARTS_MAPPING_ADDR = 0x803FE8E0
|
||||
|
||||
# This address contains the most recent spawn ID from which the player spawned.
|
||||
MOST_RECENT_SPAWN_ID_ADDR = 0x803C9D44
|
||||
|
||||
# This address contains the most recent room number the player spawned in.
|
||||
MOST_RECENT_ROOM_NUMBER_ADDR = 0x803C9D46
|
||||
|
||||
# Values used to detect exiting onto the highest isle in Cliff Plateau Isles.
|
||||
# 42. Starting at 1 and going left to right, top to bottom, Cliff Plateau Isles is the 42nd square in the sea stage.
|
||||
CLIFF_PLATEAU_ISLES_ROOM_NUMBER = 0x2A
|
||||
CLIFF_PLATEAU_ISLES_HIGHEST_ISLE_SPAWN_ID = 1 # As a note, the lower isle's spawn ID is 2.
|
||||
# The dummy stage name used to identify the highest isle in Cliff Plateau Isles.
|
||||
CLIFF_PLATEAU_ISLES_HIGHEST_ISLE_DUMMY_STAGE_NAME = "CliPlaH"
|
||||
|
||||
# Data storage key
|
||||
AP_VISITED_STAGE_NAMES_KEY_FORMAT = "tww_visited_stages_%i"
|
||||
|
||||
|
||||
class TWWCommandProcessor(ClientCommandProcessor):
|
||||
"""
|
||||
Command Processor for The Wind Waker client commands.
|
||||
|
||||
This class handles commands specific to The Wind Waker.
|
||||
"""
|
||||
|
||||
def __init__(self, ctx: CommonContext):
|
||||
"""
|
||||
Initialize the command processor with the provided context.
|
||||
|
||||
:param ctx: Context for the client.
|
||||
"""
|
||||
super().__init__(ctx)
|
||||
|
||||
def _cmd_dolphin(self) -> None:
|
||||
"""
|
||||
Display the current Dolphin emulator connection status.
|
||||
"""
|
||||
if isinstance(self.ctx, TWWContext):
|
||||
logger.info(f"Dolphin Status: {self.ctx.dolphin_status}")
|
||||
|
||||
|
||||
class TWWContext(CommonContext):
|
||||
"""
|
||||
The context for The Wind Waker client.
|
||||
|
||||
This class manages all interactions with the Dolphin emulator and the Archipelago server for The Wind Waker.
|
||||
"""
|
||||
|
||||
command_processor = TWWCommandProcessor
|
||||
game: str = "The Wind Waker"
|
||||
items_handling: int = 0b111
|
||||
|
||||
def __init__(self, server_address: Optional[str], password: Optional[str]) -> None:
|
||||
"""
|
||||
Initialize the TWW context.
|
||||
|
||||
:param server_address: Address of the Archipelago server.
|
||||
:param password: Password for server authentication.
|
||||
"""
|
||||
|
||||
super().__init__(server_address, password)
|
||||
self.dolphin_sync_task: Optional[asyncio.Task[None]] = None
|
||||
self.dolphin_status: str = CONNECTION_INITIAL_STATUS
|
||||
self.awaiting_rom: bool = False
|
||||
self.has_send_death: bool = False
|
||||
|
||||
# Bitfields used for checking locations.
|
||||
self.charts_bitfield: int
|
||||
self.chests_bitfields: dict[int, int]
|
||||
self.switches_bitfields: dict[int, int]
|
||||
self.pickups_bitfields: dict[int, int]
|
||||
self.curr_stage_chests_bitfield: int
|
||||
self.curr_stage_switches_bitfield: int
|
||||
self.curr_stage_pickups_bitfield: int
|
||||
|
||||
# Keep track of whether the player has yet received their first progressive magic meter.
|
||||
self.received_magic: bool = False
|
||||
|
||||
# A dictionary that maps salvage locations to their sunken treasure bit.
|
||||
self.salvage_locations_map: dict[str, int] = {}
|
||||
|
||||
# Name of the current stage as read from the game's memory. Sent to trackers whenever its value changes to
|
||||
# facilitate automatically switching to the map of the current stage.
|
||||
self.current_stage_name: str = ""
|
||||
|
||||
# Set of visited stages. A dictionary (used as a set) of all visited stages is set in the server's data storage
|
||||
# and updated when the player visits a new stage for the first time. To track which stages are new and need to
|
||||
# cause the server's data storage to update, the TWW AP Client keeps track of the visited stages in a set.
|
||||
# Trackers can request the dictionary from data storage to see which stages the player has visited.
|
||||
# It starts as `None` until it has been read from the server.
|
||||
self.visited_stage_names: Optional[set[str]] = None
|
||||
|
||||
# Length of the item get array in memory.
|
||||
self.len_give_item_array: int = 0x10
|
||||
|
||||
async def disconnect(self, allow_autoreconnect: bool = False) -> None:
|
||||
"""
|
||||
Disconnect the client from the server and reset game state variables.
|
||||
|
||||
:param allow_autoreconnect: Allow the client to auto-reconnect to the server. Defaults to `False`.
|
||||
|
||||
"""
|
||||
self.auth = None
|
||||
self.salvage_locations_map = {}
|
||||
self.current_stage_name = ""
|
||||
self.visited_stage_names = None
|
||||
await super().disconnect(allow_autoreconnect)
|
||||
|
||||
async def server_auth(self, password_requested: bool = False) -> None:
|
||||
"""
|
||||
Authenticate with the Archipelago server.
|
||||
|
||||
:param password_requested: Whether the server requires a password. Defaults to `False`.
|
||||
"""
|
||||
if password_requested and not self.password:
|
||||
await super().server_auth(password_requested)
|
||||
if not self.auth:
|
||||
if self.awaiting_rom:
|
||||
return
|
||||
self.awaiting_rom = True
|
||||
logger.info("Awaiting connection to Dolphin to get player information.")
|
||||
return
|
||||
await self.send_connect()
|
||||
|
||||
def on_package(self, cmd: str, args: dict[str, Any]) -> None:
|
||||
"""
|
||||
Handle incoming packages from the server.
|
||||
|
||||
:param cmd: The command received from the server.
|
||||
:param args: The command arguments.
|
||||
"""
|
||||
if cmd == "Connected":
|
||||
self.update_salvage_locations_map()
|
||||
if "death_link" in args["slot_data"]:
|
||||
Utils.async_start(self.update_death_link(bool(args["slot_data"]["death_link"])))
|
||||
# Request the connected slot's dictionary (used as a set) of visited stages.
|
||||
visited_stages_key = AP_VISITED_STAGE_NAMES_KEY_FORMAT % self.slot
|
||||
Utils.async_start(self.send_msgs([{"cmd": "Get", "keys": [visited_stages_key]}]))
|
||||
elif cmd == "Retrieved":
|
||||
requested_keys_dict = args["keys"]
|
||||
# Read the connected slot's dictionary (used as a set) of visited stages.
|
||||
if self.slot is not None:
|
||||
visited_stages_key = AP_VISITED_STAGE_NAMES_KEY_FORMAT % self.slot
|
||||
if visited_stages_key in requested_keys_dict:
|
||||
visited_stages = requested_keys_dict[visited_stages_key]
|
||||
# If it has not been set before, the value in the response will be `None`.
|
||||
visited_stage_names = set() if visited_stages is None else set(visited_stages.keys())
|
||||
# If the current stage name is not in the set, send a message to update the dictionary on the
|
||||
# server.
|
||||
current_stage_name = self.current_stage_name
|
||||
if current_stage_name and current_stage_name not in visited_stage_names:
|
||||
visited_stage_names.add(current_stage_name)
|
||||
Utils.async_start(self.update_visited_stages(current_stage_name))
|
||||
self.visited_stage_names = visited_stage_names
|
||||
|
||||
def on_deathlink(self, data: dict[str, Any]) -> None:
|
||||
"""
|
||||
Handle a DeathLink event.
|
||||
|
||||
:param data: The data associated with the DeathLink event.
|
||||
"""
|
||||
super().on_deathlink(data)
|
||||
_give_death(self)
|
||||
|
||||
def make_gui(self) -> type["kvui.GameManager"]:
|
||||
"""
|
||||
Initialize the GUI for The Wind Waker client.
|
||||
|
||||
:return: The client's GUI.
|
||||
"""
|
||||
ui = super().make_gui()
|
||||
ui.base_title = "Archipelago The Wind Waker Client"
|
||||
return ui
|
||||
|
||||
async def update_visited_stages(self, newly_visited_stage_name: str) -> None:
|
||||
"""
|
||||
Update the server's data storage of the visited stage names to include the newly visited stage name.
|
||||
|
||||
:param newly_visited_stage_name: The name of the stage recently visited.
|
||||
"""
|
||||
if self.slot is not None:
|
||||
visited_stages_key = AP_VISITED_STAGE_NAMES_KEY_FORMAT % self.slot
|
||||
await self.send_msgs(
|
||||
[
|
||||
{
|
||||
"cmd": "Set",
|
||||
"key": visited_stages_key,
|
||||
"default": {},
|
||||
"want_reply": False,
|
||||
"operations": [{"operation": "update", "value": {newly_visited_stage_name: True}}],
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
def update_salvage_locations_map(self) -> None:
|
||||
"""
|
||||
Update the client's mapping of salvage locations to their bitfield bit.
|
||||
|
||||
This is necessary for the client to handle randomized charts correctly.
|
||||
"""
|
||||
self.salvage_locations_map = {}
|
||||
for offset in range(49):
|
||||
island_name = ISLAND_NUMBER_TO_NAME[offset + 1]
|
||||
salvage_bit = ISLAND_NAME_TO_SALVAGE_BIT[island_name]
|
||||
|
||||
shuffled_island_number = read_short(CHARTS_MAPPING_ADDR + offset * 2)
|
||||
shuffled_island_name = ISLAND_NUMBER_TO_NAME[shuffled_island_number]
|
||||
salvage_location_name = f"{shuffled_island_name} - Sunken Treasure"
|
||||
|
||||
self.salvage_locations_map[salvage_location_name] = salvage_bit
|
||||
|
||||
|
||||
def read_short(console_address: int) -> int:
|
||||
"""
|
||||
Read a 2-byte short from Dolphin memory.
|
||||
|
||||
:param console_address: Address to read from.
|
||||
:return: The value read from memory.
|
||||
"""
|
||||
return int.from_bytes(dolphin_memory_engine.read_bytes(console_address, 2), byteorder="big")
|
||||
|
||||
|
||||
def write_short(console_address: int, value: int) -> None:
|
||||
"""
|
||||
Write a 2-byte short to Dolphin memory.
|
||||
|
||||
:param console_address: Address to write to.
|
||||
:param value: Value to write.
|
||||
"""
|
||||
dolphin_memory_engine.write_bytes(console_address, value.to_bytes(2, byteorder="big"))
|
||||
|
||||
|
||||
def read_string(console_address: int, strlen: int) -> str:
|
||||
"""
|
||||
Read a string from Dolphin memory.
|
||||
|
||||
:param console_address: Address to start reading from.
|
||||
:param strlen: Length of the string to read.
|
||||
:return: The string.
|
||||
"""
|
||||
return dolphin_memory_engine.read_bytes(console_address, strlen).split(b"\0", 1)[0].decode()
|
||||
|
||||
|
||||
def _give_death(ctx: TWWContext) -> None:
|
||||
"""
|
||||
Trigger the player's death in-game by setting their current health to zero.
|
||||
|
||||
:param ctx: The Wind Waker client context.
|
||||
"""
|
||||
if (
|
||||
ctx.slot is not None
|
||||
and dolphin_memory_engine.is_hooked()
|
||||
and ctx.dolphin_status == CONNECTION_CONNECTED_STATUS
|
||||
and check_ingame()
|
||||
):
|
||||
ctx.has_send_death = True
|
||||
write_short(CURR_HEALTH_ADDR, 0)
|
||||
|
||||
|
||||
def _give_item(ctx: TWWContext, item_name: str) -> bool:
|
||||
"""
|
||||
Give an item to the player in-game.
|
||||
|
||||
:param ctx: The Wind Waker client context.
|
||||
:param item_name: Name of the item to give.
|
||||
:return: Whether the item was successfully given.
|
||||
"""
|
||||
if not check_ingame() or dolphin_memory_engine.read_byte(CURR_STAGE_ID_ADDR) == 0xFF:
|
||||
return False
|
||||
|
||||
item_id = ITEM_TABLE[item_name].item_id
|
||||
|
||||
# Loop through the item array, placing the item in an empty slot.
|
||||
for idx in range(ctx.len_give_item_array):
|
||||
slot = dolphin_memory_engine.read_byte(GIVE_ITEM_ARRAY_ADDR + idx)
|
||||
if slot == 0xFF:
|
||||
# Special case: Use a different item ID for the second progressive magic meter.
|
||||
if item_name == "Progressive Magic Meter":
|
||||
if ctx.received_magic:
|
||||
item_id = 0xB2
|
||||
else:
|
||||
ctx.received_magic = True
|
||||
dolphin_memory_engine.write_byte(GIVE_ITEM_ARRAY_ADDR + idx, item_id)
|
||||
return True
|
||||
|
||||
# If unable to place the item in the array, return `False`.
|
||||
return False
|
||||
|
||||
|
||||
async def give_items(ctx: TWWContext) -> None:
|
||||
"""
|
||||
Give the player all outstanding items they have yet to receive.
|
||||
|
||||
:param ctx: The Wind Waker client context.
|
||||
"""
|
||||
if check_ingame() and dolphin_memory_engine.read_byte(CURR_STAGE_ID_ADDR) != 0xFF:
|
||||
# Read the expected index of the player, which is the index of the next item they're expecting to receive.
|
||||
# The expected index starts at 0 for a fresh save file.
|
||||
expected_idx = read_short(EXPECTED_INDEX_ADDR)
|
||||
|
||||
# Check if there are new items.
|
||||
received_items = ctx.items_received
|
||||
if len(received_items) <= expected_idx:
|
||||
# There are no new items.
|
||||
return
|
||||
|
||||
# Loop through items to give.
|
||||
# Give the player all items at an index greater than or equal to the expected index.
|
||||
for idx, item in enumerate(received_items[expected_idx:], start=expected_idx):
|
||||
# Attempt to give the item and increment the expected index.
|
||||
while not _give_item(ctx, LOOKUP_ID_TO_NAME[item.item]):
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
# Increment the expected index.
|
||||
write_short(EXPECTED_INDEX_ADDR, idx + 1)
|
||||
|
||||
|
||||
def check_special_location(location_name: str, data: TWWLocationData) -> bool:
|
||||
"""
|
||||
Check that the player has checked a given location.
|
||||
This function handles locations that require special logic.
|
||||
|
||||
:param location_name: The name of the location.
|
||||
:param data: The data associated with the location.
|
||||
:raises NotImplementedError: If an unknown location name is provided.
|
||||
"""
|
||||
checked = False
|
||||
|
||||
# For "Windfall Island - Lenzo's House - Become Lenzo's Assistant"
|
||||
# 0x6 is delivered the final picture for Lenzo, 0x7 is a day has passed since becoming his assistant
|
||||
# Either is fine for sending the check, so check both conditions.
|
||||
if location_name == "Windfall Island - Lenzo's House - Become Lenzo's Assistant":
|
||||
checked = (
|
||||
dolphin_memory_engine.read_byte(data.address) & 0x6 == 0x6
|
||||
or dolphin_memory_engine.read_byte(data.address) & 0x7 == 0x7
|
||||
)
|
||||
|
||||
# The "Windfall Island - Maggie - Delivery Reward" flag remains unknown.
|
||||
# However, as a temporary workaround, we can check if the player had Moblin's letter at some point, but it's no
|
||||
# longer in their Delivery Bag.
|
||||
elif location_name == "Windfall Island - Maggie - Delivery Reward":
|
||||
was_moblins_owned = (dolphin_memory_engine.read_word(LETTER_OWND_ADDR) >> 15) & 1
|
||||
dbag_contents = [dolphin_memory_engine.read_byte(LETTER_BASE_ADDR + offset) for offset in range(8)]
|
||||
checked = was_moblins_owned and 0x9B not in dbag_contents
|
||||
|
||||
# For Letter from Hoskit's Girlfriend, we need to check two bytes.
|
||||
# 0x1 = Golden Feathers delivered, 0x2 = Mail sent by Hoskit's Girlfriend, 0x3 = Mail read by Link
|
||||
elif location_name == "Mailbox - Letter from Hoskit's Girlfriend":
|
||||
checked = dolphin_memory_engine.read_byte(data.address) & 0x3 == 0x3
|
||||
|
||||
# For Letter from Baito's Mother, we need to check two bytes.
|
||||
# 0x1 = Note to Mom sent, 0x2 = Mail sent by Baito's Mother, 0x3 = Mail read by Link
|
||||
elif location_name == "Mailbox - Letter from Baito's Mother":
|
||||
checked = dolphin_memory_engine.read_byte(data.address) & 0x3 == 0x3
|
||||
|
||||
# For Letter from Grandma, we need to check two bytes.
|
||||
# 0x1 = Grandma saved, 0x2 = Mail sent by Grandma, 0x3 = Mail read by Link
|
||||
elif location_name == "Mailbox - Letter from Grandma":
|
||||
checked = dolphin_memory_engine.read_byte(data.address) & 0x3 == 0x3
|
||||
|
||||
# We check if the bits for turning all five statues are set for the Ankle's reward.
|
||||
# For some reason, the bit for the Dragon Tingle Statue is separate from the rest.
|
||||
elif location_name == "Tingle Island - Ankle - Reward for All Tingle Statues":
|
||||
dragon_tingle_statue_rewarded = dolphin_memory_engine.read_byte(TINGLE_STATUE_1_ADDR) & 0x40 == 0x40
|
||||
other_tingle_statues_rewarded = dolphin_memory_engine.read_byte(TINGLE_STATUE_2_ADDR) & 0x0F == 0x0F
|
||||
checked = dragon_tingle_statue_rewarded and other_tingle_statues_rewarded
|
||||
|
||||
else:
|
||||
raise NotImplementedError(f"Unknown special location: {location_name}")
|
||||
|
||||
return checked
|
||||
|
||||
|
||||
def check_regular_location(ctx: TWWContext, curr_stage_id: int, data: TWWLocationData) -> bool:
|
||||
"""
|
||||
Check that the player has checked a given location.
|
||||
This function handles locations that only require checking that a particular bit is set.
|
||||
|
||||
The check looks at the saved data for the stage at which the location is located and the data for the current stage.
|
||||
In the latter case, this data includes data that has not yet been written to the saved data.
|
||||
|
||||
:param ctx: The Wind Waker client context.
|
||||
:param curr_stage_id: The current stage at which the player is.
|
||||
:param data: The data associated with the location.
|
||||
:raises NotImplementedError: If a location with an unknown type is provided.
|
||||
"""
|
||||
checked = False
|
||||
|
||||
# Check the saved bitfields for the stage.
|
||||
if data.type == TWWLocationType.CHEST:
|
||||
checked = bool((ctx.chests_bitfields[data.stage_id] >> data.bit) & 1)
|
||||
elif data.type == TWWLocationType.SWTCH:
|
||||
checked = bool((ctx.switches_bitfields[data.stage_id] >> data.bit) & 1)
|
||||
elif data.type == TWWLocationType.PCKUP:
|
||||
checked = bool((ctx.pickups_bitfields[data.stage_id] >> data.bit) & 1)
|
||||
else:
|
||||
raise NotImplementedError(f"Unknown location type: {data.type}")
|
||||
|
||||
# If the location is in the current stage, check the bitfields for the current stage as well.
|
||||
if not checked and curr_stage_id == data.stage_id:
|
||||
if data.type == TWWLocationType.CHEST:
|
||||
checked = bool((ctx.curr_stage_chests_bitfield >> data.bit) & 1)
|
||||
elif data.type == TWWLocationType.SWTCH:
|
||||
checked = bool((ctx.curr_stage_switches_bitfield >> data.bit) & 1)
|
||||
elif data.type == TWWLocationType.PCKUP:
|
||||
checked = bool((ctx.curr_stage_pickups_bitfield >> data.bit) & 1)
|
||||
else:
|
||||
raise NotImplementedError(f"Unknown location type: {data.type}")
|
||||
|
||||
return checked
|
||||
|
||||
|
||||
async def check_locations(ctx: TWWContext) -> None:
|
||||
"""
|
||||
Iterate through all locations and check whether the player has checked each location.
|
||||
|
||||
Update the server with all newly checked locations since the last update. If the player has completed the goal,
|
||||
notify the server.
|
||||
|
||||
:param ctx: The Wind Waker client context.
|
||||
"""
|
||||
# Read the bitfield for sunken treasure locations.
|
||||
ctx.charts_bitfield = int.from_bytes(dolphin_memory_engine.read_bytes(CHARTS_BITFLD_ADDR, 8), byteorder="big")
|
||||
|
||||
# Read the bitfields once before the loop to speed things up a bit.
|
||||
ctx.chests_bitfields = {}
|
||||
ctx.switches_bitfields = {}
|
||||
ctx.pickups_bitfields = {}
|
||||
for stage_id in range(0xE):
|
||||
chest_bitfield_addr = BASE_CHESTS_BITFLD_ADDR + (0x24 * stage_id)
|
||||
switches_bitfield_addr = BASE_SWITCHES_BITFLD_ADDR + (0x24 * stage_id)
|
||||
pickups_bitfield_addr = BASE_PICKUPS_BITFLD_ADDR + (0x24 * stage_id)
|
||||
|
||||
ctx.chests_bitfields[stage_id] = int(dolphin_memory_engine.read_word(chest_bitfield_addr))
|
||||
ctx.switches_bitfields[stage_id] = int.from_bytes(
|
||||
dolphin_memory_engine.read_bytes(switches_bitfield_addr, 10), byteorder="big"
|
||||
)
|
||||
ctx.pickups_bitfields[stage_id] = int(dolphin_memory_engine.read_word(pickups_bitfield_addr))
|
||||
|
||||
ctx.curr_stage_chests_bitfield = int(dolphin_memory_engine.read_word(CURR_STAGE_CHESTS_BITFLD_ADDR))
|
||||
ctx.curr_stage_switches_bitfield = int.from_bytes(
|
||||
dolphin_memory_engine.read_bytes(CURR_STAGE_SWITCHES_BITFLD_ADDR, 10), byteorder="big"
|
||||
)
|
||||
ctx.curr_stage_pickups_bitfield = int(dolphin_memory_engine.read_word(CURR_STAGE_PICKUPS_BITFLD_ADDR))
|
||||
|
||||
# We check which locations are currently checked on the current stage.
|
||||
curr_stage_id = dolphin_memory_engine.read_byte(CURR_STAGE_ID_ADDR)
|
||||
|
||||
# Loop through all locations to see if each has been checked.
|
||||
for location, data in LOCATION_TABLE.items():
|
||||
checked = False
|
||||
if data.type == TWWLocationType.CHART:
|
||||
assert location in ctx.salvage_locations_map, f'Location "{location}" salvage bit not set!'
|
||||
salvage_bit = ctx.salvage_locations_map[location]
|
||||
checked = bool((ctx.charts_bitfield >> salvage_bit) & 1)
|
||||
elif data.type == TWWLocationType.BOCTO:
|
||||
assert data.address is not None
|
||||
checked = bool((read_short(data.address) >> data.bit) & 1)
|
||||
elif data.type == TWWLocationType.EVENT:
|
||||
checked = bool((dolphin_memory_engine.read_byte(data.address) >> data.bit) & 1)
|
||||
elif data.type == TWWLocationType.SPECL:
|
||||
checked = check_special_location(location, data)
|
||||
else:
|
||||
checked = check_regular_location(ctx, curr_stage_id, data)
|
||||
|
||||
if checked:
|
||||
if data.code is None:
|
||||
if not ctx.finished_game:
|
||||
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||
ctx.finished_game = True
|
||||
else:
|
||||
ctx.locations_checked.add(TWWLocation.get_apid(data.code))
|
||||
|
||||
# Send the list of newly-checked locations to the server.
|
||||
locations_checked = ctx.locations_checked.difference(ctx.checked_locations)
|
||||
if locations_checked:
|
||||
await ctx.send_msgs([{"cmd": "LocationChecks", "locations": locations_checked}])
|
||||
|
||||
|
||||
async def check_current_stage_changed(ctx: TWWContext) -> None:
|
||||
"""
|
||||
Check if the player has moved to a new stage.
|
||||
If so, update all trackers with the new stage name.
|
||||
If the stage has never been visited, additionally update the server.
|
||||
|
||||
:param ctx: The Wind Waker client context.
|
||||
"""
|
||||
new_stage_name = read_string(CURR_STAGE_NAME_ADDR, 8)
|
||||
|
||||
# Special handling is required for the Cliff Plateau Isles Inner Cave exit, which exits out onto the sea stage
|
||||
# rather than a unique stage.
|
||||
if (
|
||||
new_stage_name == "sea"
|
||||
and dolphin_memory_engine.read_byte(MOST_RECENT_ROOM_NUMBER_ADDR) == CLIFF_PLATEAU_ISLES_ROOM_NUMBER
|
||||
and read_short(MOST_RECENT_SPAWN_ID_ADDR) == CLIFF_PLATEAU_ISLES_HIGHEST_ISLE_SPAWN_ID
|
||||
):
|
||||
new_stage_name = CLIFF_PLATEAU_ISLES_HIGHEST_ISLE_DUMMY_STAGE_NAME
|
||||
|
||||
current_stage_name = ctx.current_stage_name
|
||||
if new_stage_name != current_stage_name:
|
||||
ctx.current_stage_name = new_stage_name
|
||||
# Send a Bounced message containing the new stage name to all trackers connected to the current slot.
|
||||
data_to_send = {"tww_stage_name": new_stage_name}
|
||||
message = {
|
||||
"cmd": "Bounce",
|
||||
"slots": [ctx.slot],
|
||||
"data": data_to_send,
|
||||
}
|
||||
await ctx.send_msgs([message])
|
||||
|
||||
# If the stage has never been visited before, update the server's data storage to indicate that it has been
|
||||
# visited.
|
||||
visited_stage_names = ctx.visited_stage_names
|
||||
if visited_stage_names is not None and new_stage_name not in visited_stage_names:
|
||||
visited_stage_names.add(new_stage_name)
|
||||
await ctx.update_visited_stages(new_stage_name)
|
||||
|
||||
|
||||
async def check_alive() -> bool:
|
||||
"""
|
||||
Check if the player is currently alive in-game.
|
||||
|
||||
:return: `True` if the player is alive, otherwise `False`.
|
||||
"""
|
||||
cur_health = read_short(CURR_HEALTH_ADDR)
|
||||
return cur_health > 0
|
||||
|
||||
|
||||
async def check_death(ctx: TWWContext) -> None:
|
||||
"""
|
||||
Check if the player is currently dead in-game.
|
||||
If DeathLink is on, notify the server of the player's death.
|
||||
|
||||
:return: `True` if the player is dead, otherwise `False`.
|
||||
"""
|
||||
if ctx.slot is not None and check_ingame():
|
||||
cur_health = read_short(CURR_HEALTH_ADDR)
|
||||
if cur_health <= 0:
|
||||
if not ctx.has_send_death and time.time() >= ctx.last_death_link + 3:
|
||||
ctx.has_send_death = True
|
||||
await ctx.send_death(ctx.player_names[ctx.slot] + " ran out of hearts.")
|
||||
else:
|
||||
ctx.has_send_death = False
|
||||
|
||||
|
||||
def check_ingame() -> bool:
|
||||
"""
|
||||
Check if the player is currently in-game.
|
||||
|
||||
:return: `True` if the player is in-game, otherwise `False`.
|
||||
"""
|
||||
return read_string(CURR_STAGE_NAME_ADDR, 8) not in ["", "sea_T", "Name"]
|
||||
|
||||
|
||||
async def dolphin_sync_task(ctx: TWWContext) -> None:
|
||||
"""
|
||||
The task loop for managing the connection to Dolphin.
|
||||
|
||||
While connected, read the emulator's memory to look for any relevant changes made by the player in the game.
|
||||
|
||||
:param ctx: The Wind Waker client context.
|
||||
"""
|
||||
logger.info("Starting Dolphin connector. Use /dolphin for status information.")
|
||||
sleep_time = 0.0
|
||||
while not ctx.exit_event.is_set():
|
||||
if sleep_time > 0.0:
|
||||
try:
|
||||
# ctx.watcher_event gets set when receiving ReceivedItems or LocationInfo, or when shutting down.
|
||||
await asyncio.wait_for(ctx.watcher_event.wait(), sleep_time)
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
sleep_time = 0.0
|
||||
ctx.watcher_event.clear()
|
||||
|
||||
try:
|
||||
if dolphin_memory_engine.is_hooked() and ctx.dolphin_status == CONNECTION_CONNECTED_STATUS:
|
||||
if not check_ingame():
|
||||
# Reset the give item array while not in the game.
|
||||
dolphin_memory_engine.write_bytes(GIVE_ITEM_ARRAY_ADDR, bytes([0xFF] * ctx.len_give_item_array))
|
||||
sleep_time = 0.1
|
||||
continue
|
||||
if ctx.slot is not None:
|
||||
if "DeathLink" in ctx.tags:
|
||||
await check_death(ctx)
|
||||
await give_items(ctx)
|
||||
await check_locations(ctx)
|
||||
await check_current_stage_changed(ctx)
|
||||
else:
|
||||
if not ctx.auth:
|
||||
ctx.auth = read_string(SLOT_NAME_ADDR, 0x40)
|
||||
if ctx.awaiting_rom:
|
||||
await ctx.server_auth()
|
||||
sleep_time = 0.1
|
||||
else:
|
||||
if ctx.dolphin_status == CONNECTION_CONNECTED_STATUS:
|
||||
logger.info("Connection to Dolphin lost, reconnecting...")
|
||||
ctx.dolphin_status = CONNECTION_LOST_STATUS
|
||||
logger.info("Attempting to connect to Dolphin...")
|
||||
dolphin_memory_engine.hook()
|
||||
if dolphin_memory_engine.is_hooked():
|
||||
if dolphin_memory_engine.read_bytes(0x80000000, 6) != b"GZLE99":
|
||||
logger.info(CONNECTION_REFUSED_GAME_STATUS)
|
||||
ctx.dolphin_status = CONNECTION_REFUSED_GAME_STATUS
|
||||
dolphin_memory_engine.un_hook()
|
||||
sleep_time = 5
|
||||
else:
|
||||
logger.info(CONNECTION_CONNECTED_STATUS)
|
||||
ctx.dolphin_status = CONNECTION_CONNECTED_STATUS
|
||||
ctx.locations_checked = set()
|
||||
else:
|
||||
logger.info("Connection to Dolphin failed, attempting again in 5 seconds...")
|
||||
ctx.dolphin_status = CONNECTION_LOST_STATUS
|
||||
await ctx.disconnect()
|
||||
sleep_time = 5
|
||||
continue
|
||||
except Exception:
|
||||
dolphin_memory_engine.un_hook()
|
||||
logger.info("Connection to Dolphin failed, attempting again in 5 seconds...")
|
||||
logger.error(traceback.format_exc())
|
||||
ctx.dolphin_status = CONNECTION_LOST_STATUS
|
||||
await ctx.disconnect()
|
||||
sleep_time = 5
|
||||
continue
|
||||
|
||||
|
||||
def main(connect: Optional[str] = None, password: Optional[str] = None) -> None:
|
||||
"""
|
||||
Run the main async loop for the Wind Waker client.
|
||||
|
||||
:param connect: Address of the Archipelago server.
|
||||
:param password: Password for server authentication.
|
||||
"""
|
||||
Utils.init_logging("The Wind Waker Client")
|
||||
|
||||
async def _main(connect: Optional[str], password: Optional[str]) -> None:
|
||||
ctx = TWWContext(connect, password)
|
||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
||||
if gui_enabled:
|
||||
ctx.run_gui()
|
||||
ctx.run_cli()
|
||||
await asyncio.sleep(1)
|
||||
|
||||
ctx.dolphin_sync_task = asyncio.create_task(dolphin_sync_task(ctx), name="DolphinSync")
|
||||
|
||||
await ctx.exit_event.wait()
|
||||
# Wake the sync task, if it is currently sleeping, so it can start shutting down when it sees that the
|
||||
# exit_event is set.
|
||||
ctx.watcher_event.set()
|
||||
ctx.server_address = None
|
||||
|
||||
await ctx.shutdown()
|
||||
|
||||
if ctx.dolphin_sync_task:
|
||||
await ctx.dolphin_sync_task
|
||||
|
||||
import colorama
|
||||
|
||||
colorama.init()
|
||||
asyncio.run(_main(connect, password))
|
||||
colorama.deinit()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = get_base_parser()
|
||||
args = parser.parse_args()
|
||||
main(args.connect, args.password)
|
||||
598
worlds/tww/__init__.py
Normal file
598
worlds/tww/__init__.py
Normal file
@@ -0,0 +1,598 @@
|
||||
import os
|
||||
import zipfile
|
||||
from base64 import b64encode
|
||||
from collections.abc import Mapping
|
||||
from typing import Any, ClassVar
|
||||
|
||||
import yaml
|
||||
|
||||
from BaseClasses import Item
|
||||
from BaseClasses import ItemClassification as IC
|
||||
from BaseClasses import MultiWorld, Region, Tutorial
|
||||
from Options import Toggle
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from worlds.Files import APContainer, AutoPatchRegister
|
||||
from worlds.generic.Rules import add_item_rule
|
||||
from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components, icon_paths, launch_subprocess
|
||||
|
||||
from .Items import ISLAND_NUMBER_TO_CHART_NAME, ITEM_TABLE, TWWItem, item_name_groups
|
||||
from .Locations import LOCATION_TABLE, TWWFlag, TWWLocation
|
||||
from .Options import TWWOptions, tww_option_groups
|
||||
from .Presets import tww_options_presets
|
||||
from .randomizers.Charts import ISLAND_NUMBER_TO_NAME, ChartRandomizer
|
||||
from .randomizers.Dungeons import Dungeon, create_dungeons
|
||||
from .randomizers.Entrances import ALL_EXITS, BOSS_EXIT_TO_DUNGEON, MINIBOSS_EXIT_TO_DUNGEON, EntranceRandomizer
|
||||
from .randomizers.ItemPool import generate_itempool
|
||||
from .randomizers.RequiredBosses import RequiredBossesRandomizer
|
||||
from .Rules import set_rules
|
||||
|
||||
VERSION: tuple[int, int, int] = (3, 0, 0)
|
||||
|
||||
|
||||
def run_client() -> None:
|
||||
"""
|
||||
Launch the The Wind Waker client.
|
||||
"""
|
||||
print("Running The Wind Waker Client")
|
||||
from .TWWClient import main
|
||||
|
||||
launch_subprocess(main, name="TheWindWakerClient")
|
||||
|
||||
|
||||
components.append(
|
||||
Component(
|
||||
"The Wind Waker Client",
|
||||
func=run_client,
|
||||
component_type=Type.CLIENT,
|
||||
file_identifier=SuffixIdentifier(".aptww"),
|
||||
icon="The Wind Waker",
|
||||
)
|
||||
)
|
||||
icon_paths["The Wind Waker"] = "ap:worlds.tww/assets/icon.png"
|
||||
|
||||
|
||||
class TWWContainer(APContainer, metaclass=AutoPatchRegister):
|
||||
"""
|
||||
This class defines the container file for The Wind Waker.
|
||||
"""
|
||||
|
||||
game: str = "The Wind Waker"
|
||||
patch_file_ending: str = ".aptww"
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
if "data" in kwargs:
|
||||
self.data = kwargs["data"]
|
||||
del kwargs["data"]
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None:
|
||||
"""
|
||||
Write the contents of the container file.
|
||||
"""
|
||||
super().write_contents(opened_zipfile)
|
||||
|
||||
# Record the data for the game under the key `plando`.
|
||||
opened_zipfile.writestr("plando", b64encode(bytes(yaml.safe_dump(self.data, sort_keys=False), "utf-8")))
|
||||
|
||||
|
||||
class TWWWeb(WebWorld):
|
||||
"""
|
||||
This class handles the web interface for The Wind Waker.
|
||||
|
||||
The web interface includes the setup guide and the options page for generating YAMLs.
|
||||
"""
|
||||
|
||||
tutorials = [
|
||||
Tutorial(
|
||||
"Multiworld Setup Guide",
|
||||
"A guide to setting up the Archipelago The Wind Waker software on your computer.",
|
||||
"English",
|
||||
"setup_en.md",
|
||||
"setup/en",
|
||||
["tanjo3", "Lunix"],
|
||||
)
|
||||
]
|
||||
theme = "ocean"
|
||||
options_presets = tww_options_presets
|
||||
option_groups = tww_option_groups
|
||||
rich_text_options_doc = True
|
||||
|
||||
|
||||
class TWWWorld(World):
|
||||
"""
|
||||
Legend has it that whenever evil has appeared, a hero named Link has arisen to defeat it. The legend continues on
|
||||
the surface of a vast and mysterious sea as Link sets sail in his most epic, awe-inspiring adventure yet. Aided by a
|
||||
magical conductor's baton called the Wind Waker, he will face unimaginable monsters, explore puzzling dungeons, and
|
||||
meet a cast of unforgettable characters as he searches for his kidnapped sister.
|
||||
"""
|
||||
|
||||
options_dataclass = TWWOptions
|
||||
options: TWWOptions
|
||||
|
||||
game: ClassVar[str] = "The Wind Waker"
|
||||
topology_present: bool = True
|
||||
|
||||
item_name_to_id: ClassVar[dict[str, int]] = {
|
||||
name: TWWItem.get_apid(data.code) for name, data in ITEM_TABLE.items() if data.code is not None
|
||||
}
|
||||
location_name_to_id: ClassVar[dict[str, int]] = {
|
||||
name: TWWLocation.get_apid(data.code) for name, data in LOCATION_TABLE.items() if data.code is not None
|
||||
}
|
||||
|
||||
item_name_groups: ClassVar[dict[str, set[str]]] = item_name_groups
|
||||
|
||||
required_client_version: tuple[int, int, int] = (0, 5, 1)
|
||||
|
||||
web: ClassVar[TWWWeb] = TWWWeb()
|
||||
|
||||
origin_region_name: str = "The Great Sea"
|
||||
|
||||
create_items = generate_itempool
|
||||
|
||||
logic_rematch_bosses_skipped: bool
|
||||
logic_in_swordless_mode: bool
|
||||
logic_in_required_bosses_mode: bool
|
||||
logic_obscure_1: bool
|
||||
logic_obscure_2: bool
|
||||
logic_obscure_3: bool
|
||||
logic_precise_1: bool
|
||||
logic_precise_2: bool
|
||||
logic_precise_3: bool
|
||||
logic_tuner_logic_enabled: bool
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.progress_locations: set[str] = set()
|
||||
self.nonprogress_locations: set[str] = set()
|
||||
|
||||
self.dungeon_local_item_names: set[str] = set()
|
||||
self.dungeon_specific_item_names: set[str] = set()
|
||||
self.dungeons: dict[str, Dungeon] = {}
|
||||
|
||||
self.item_classification_overrides: dict[str, IC] = {}
|
||||
|
||||
self.useful_pool: list[str] = []
|
||||
self.filler_pool: list[str] = []
|
||||
|
||||
self.charts = ChartRandomizer(self)
|
||||
self.entrances = EntranceRandomizer(self)
|
||||
self.boss_reqs = RequiredBossesRandomizer(self)
|
||||
|
||||
def _determine_item_classification_overrides(self) -> None:
|
||||
"""
|
||||
Determine item classification overrides. The classification of an item may be affected by which options are
|
||||
enabled or disabled.
|
||||
"""
|
||||
options = self.options
|
||||
item_classification_overrides = self.item_classification_overrides
|
||||
|
||||
# Override certain items to be filler depending on user options.
|
||||
# TODO: Calculate filler items dynamically
|
||||
override_as_filler = []
|
||||
if not options.progression_dungeons:
|
||||
override_as_filler.extend(item_name_groups["Small Keys"] | item_name_groups["Big Keys"])
|
||||
override_as_filler.extend(("Command Melody", "Earth God's Lyric", "Wind God's Aria"))
|
||||
if not options.progression_short_sidequests:
|
||||
override_as_filler.extend(("Maggie's Letter", "Moblin's Letter"))
|
||||
if not (options.progression_short_sidequests or options.progression_long_sidequests):
|
||||
override_as_filler.append("Progressive Picto Box")
|
||||
if not options.progression_spoils_trading:
|
||||
override_as_filler.append("Spoils Bag")
|
||||
if not options.progression_triforce_charts:
|
||||
override_as_filler.extend(item_name_groups["Triforce Charts"])
|
||||
if not options.progression_treasure_charts:
|
||||
override_as_filler.extend(item_name_groups["Treasure Charts"])
|
||||
if not options.progression_misc:
|
||||
override_as_filler.extend(item_name_groups["Tingle Statues"])
|
||||
|
||||
for item_name in override_as_filler:
|
||||
item_classification_overrides[item_name] = IC.filler
|
||||
|
||||
# Override certain items to be useful depending on user options.
|
||||
# TODO: Calculate useful items dynamically
|
||||
override_as_useful = []
|
||||
if not options.progression_big_octos_gunboats:
|
||||
override_as_useful.append("Quiver Capacity Upgrade")
|
||||
if options.sword_mode in ("swords_optional", "swordless"):
|
||||
override_as_useful.append("Progressive Sword")
|
||||
if not options.enable_tuner_logic:
|
||||
override_as_useful.append("Tingle Tuner")
|
||||
|
||||
for item_name in override_as_useful:
|
||||
item_classification_overrides[item_name] = IC.useful
|
||||
|
||||
def _determine_progress_and_nonprogress_locations(self) -> tuple[set[str], set[str]]:
|
||||
"""
|
||||
Determine which locations are progress and nonprogress in the world based on the player's options.
|
||||
|
||||
:return: A tuple of two sets, the first containing the names of the progress locations and the second containing
|
||||
the names of the nonprogress locations.
|
||||
"""
|
||||
|
||||
def add_flag(option: Toggle, flag: TWWFlag) -> TWWFlag:
|
||||
return flag if option else TWWFlag.ALWAYS
|
||||
|
||||
options = self.options
|
||||
|
||||
enabled_flags = TWWFlag.ALWAYS
|
||||
enabled_flags |= add_flag(options.progression_dungeons, TWWFlag.DUNGEON | TWWFlag.BOSS)
|
||||
enabled_flags |= add_flag(options.progression_tingle_chests, TWWFlag.TNGL_CT)
|
||||
enabled_flags |= add_flag(options.progression_dungeon_secrets, TWWFlag.DG_SCRT)
|
||||
enabled_flags |= add_flag(options.progression_puzzle_secret_caves, TWWFlag.PZL_CVE)
|
||||
enabled_flags |= add_flag(options.progression_combat_secret_caves, TWWFlag.CBT_CVE)
|
||||
enabled_flags |= add_flag(options.progression_savage_labyrinth, TWWFlag.SAVAGE)
|
||||
enabled_flags |= add_flag(options.progression_great_fairies, TWWFlag.GRT_FRY)
|
||||
enabled_flags |= add_flag(options.progression_short_sidequests, TWWFlag.SHRT_SQ)
|
||||
enabled_flags |= add_flag(options.progression_long_sidequests, TWWFlag.LONG_SQ)
|
||||
enabled_flags |= add_flag(options.progression_spoils_trading, TWWFlag.SPOILS)
|
||||
enabled_flags |= add_flag(options.progression_minigames, TWWFlag.MINIGME)
|
||||
enabled_flags |= add_flag(options.progression_battlesquid, TWWFlag.SPLOOSH)
|
||||
enabled_flags |= add_flag(options.progression_free_gifts, TWWFlag.FREE_GF)
|
||||
enabled_flags |= add_flag(options.progression_mail, TWWFlag.MAILBOX)
|
||||
enabled_flags |= add_flag(options.progression_platforms_rafts, TWWFlag.PLTFRMS)
|
||||
enabled_flags |= add_flag(options.progression_submarines, TWWFlag.SUBMRIN)
|
||||
enabled_flags |= add_flag(options.progression_eye_reef_chests, TWWFlag.EYE_RFS)
|
||||
enabled_flags |= add_flag(options.progression_big_octos_gunboats, TWWFlag.BG_OCTO)
|
||||
enabled_flags |= add_flag(options.progression_expensive_purchases, TWWFlag.XPENSVE)
|
||||
enabled_flags |= add_flag(options.progression_island_puzzles, TWWFlag.ISLND_P)
|
||||
enabled_flags |= add_flag(options.progression_misc, TWWFlag.MISCELL)
|
||||
|
||||
progress_locations: set[str] = set()
|
||||
nonprogress_locations: set[str] = set()
|
||||
for location, data in LOCATION_TABLE.items():
|
||||
if data.flags & enabled_flags == data.flags:
|
||||
progress_locations.add(location)
|
||||
else:
|
||||
nonprogress_locations.add(location)
|
||||
assert progress_locations.isdisjoint(nonprogress_locations)
|
||||
|
||||
return progress_locations, nonprogress_locations
|
||||
|
||||
@staticmethod
|
||||
def _get_classification_name(classification: IC) -> str:
|
||||
"""
|
||||
Return a string representation of the item's highest-order classification.
|
||||
|
||||
:param classification: The item's classification.
|
||||
:return: A string representation of the item's highest classification. The order of classification is
|
||||
progression > trap > useful > filler.
|
||||
"""
|
||||
|
||||
if IC.progression in classification:
|
||||
return "progression"
|
||||
elif IC.trap in classification:
|
||||
return "trap"
|
||||
elif IC.useful in classification:
|
||||
return "useful"
|
||||
else:
|
||||
return "filler"
|
||||
|
||||
def generate_early(self) -> None:
|
||||
"""
|
||||
Run before any general steps of the MultiWorld other than options.
|
||||
"""
|
||||
options = self.options
|
||||
|
||||
# Only randomize secret cave inner entrances if both puzzle secret caves and combat secret caves are enabled.
|
||||
if not (options.progression_puzzle_secret_caves and options.progression_combat_secret_caves):
|
||||
options.randomize_secret_cave_inner_entrances.value = False
|
||||
|
||||
# Determine which locations are progression and which are not from options.
|
||||
self.progress_locations, self.nonprogress_locations = self._determine_progress_and_nonprogress_locations()
|
||||
|
||||
for dungeon_item in ["randomize_smallkeys", "randomize_bigkeys", "randomize_mapcompass"]:
|
||||
option = getattr(options, dungeon_item)
|
||||
if option == "local":
|
||||
options.local_items.value |= self.item_name_groups[option.item_name_group]
|
||||
elif option.in_dungeon:
|
||||
self.dungeon_local_item_names |= self.item_name_groups[option.item_name_group]
|
||||
if option == "dungeon":
|
||||
self.dungeon_specific_item_names |= self.item_name_groups[option.item_name_group]
|
||||
else:
|
||||
options.local_items.value |= self.dungeon_local_item_names
|
||||
|
||||
# Resolve logic options and set them onto the world instance for faster lookup in logic rules.
|
||||
self.logic_rematch_bosses_skipped = bool(options.skip_rematch_bosses.value)
|
||||
self.logic_in_swordless_mode = options.sword_mode in ("swords_optional", "swordless")
|
||||
self.logic_in_required_bosses_mode = bool(options.required_bosses.value)
|
||||
self.logic_obscure_3 = options.logic_obscurity == "very_hard"
|
||||
self.logic_obscure_2 = self.logic_obscure_3 or options.logic_obscurity == "hard"
|
||||
self.logic_obscure_1 = self.logic_obscure_2 or options.logic_obscurity == "normal"
|
||||
self.logic_precise_3 = options.logic_precision == "very_hard"
|
||||
self.logic_precise_2 = self.logic_precise_3 or options.logic_precision == "hard"
|
||||
self.logic_precise_1 = self.logic_precise_2 or options.logic_precision == "normal"
|
||||
self.logic_tuner_logic_enabled = bool(options.enable_tuner_logic.value)
|
||||
|
||||
# Determine any item classification overrides based on user options.
|
||||
self._determine_item_classification_overrides()
|
||||
|
||||
def create_regions(self) -> None:
|
||||
"""
|
||||
Create and connect regions for the The Wind Waker world.
|
||||
|
||||
This method first randomizes the charts and picks the required bosses if these options are enabled.
|
||||
It then loops through all the world's progress locations and creates the locations, assigning dungeon locations
|
||||
to their respective dungeons.
|
||||
Finally, the flags for sunken treasure locations are updated as appropriate, and the entrances are randomized
|
||||
if that option is enabled.
|
||||
"""
|
||||
multiworld = self.multiworld
|
||||
player = self.player
|
||||
options = self.options
|
||||
|
||||
# "The Great Sea" region contains all locations that are not in a randomizable region.
|
||||
great_sea_region = Region("The Great Sea", player, multiworld)
|
||||
multiworld.regions.append(great_sea_region)
|
||||
|
||||
# Add all randomizable regions.
|
||||
for _exit in ALL_EXITS:
|
||||
multiworld.regions.append(Region(_exit.unique_name, player, multiworld))
|
||||
|
||||
# Set up sunken treasure locations, randomizing the charts if necessary.
|
||||
self.charts.setup_progress_sunken_treasure_locations()
|
||||
|
||||
# Select required bosses.
|
||||
if options.required_bosses:
|
||||
self.boss_reqs.randomize_required_bosses()
|
||||
self.progress_locations -= self.boss_reqs.banned_locations
|
||||
self.nonprogress_locations |= self.boss_reqs.banned_locations
|
||||
|
||||
# Create the dungeon classes.
|
||||
create_dungeons(self)
|
||||
|
||||
# Assign each location to their region.
|
||||
# Progress locations are sorted for deterministic results.
|
||||
for location_name in sorted(self.progress_locations):
|
||||
data = LOCATION_TABLE[location_name]
|
||||
|
||||
region = self.get_region(data.region)
|
||||
location = TWWLocation(player, location_name, region, data)
|
||||
|
||||
# Additionally, assign dungeon locations to the appropriate dungeon.
|
||||
if region.name in self.dungeons:
|
||||
location.dungeon = self.dungeons[region.name]
|
||||
elif region.name in MINIBOSS_EXIT_TO_DUNGEON and not options.randomize_miniboss_entrances:
|
||||
location.dungeon = self.dungeons[MINIBOSS_EXIT_TO_DUNGEON[region.name]]
|
||||
elif region.name in BOSS_EXIT_TO_DUNGEON and not options.randomize_boss_entrances:
|
||||
location.dungeon = self.dungeons[BOSS_EXIT_TO_DUNGEON[region.name]]
|
||||
elif location.name in [
|
||||
"Forsaken Fortress - Phantom Ganon",
|
||||
"Forsaken Fortress - Chest Outside Upper Jail Cell",
|
||||
"Forsaken Fortress - Chest Inside Lower Jail Cell",
|
||||
"Forsaken Fortress - Chest Guarded By Bokoblin",
|
||||
"Forsaken Fortress - Chest on Bed",
|
||||
]:
|
||||
location.dungeon = self.dungeons["Forsaken Fortress"]
|
||||
region.locations.append(location)
|
||||
|
||||
# Correct the flags of the sunken treasure locations if the charts are randomized.
|
||||
self.charts.update_chart_location_flags()
|
||||
|
||||
# Connect the regions in the multiworld. Randomize entrances to exits if the option is set.
|
||||
self.entrances.randomize_entrances()
|
||||
|
||||
def set_rules(self) -> None:
|
||||
"""
|
||||
Set access and item rules on locations.
|
||||
"""
|
||||
# Set the access rules for all progression locations.
|
||||
set_rules(self)
|
||||
|
||||
# Ban the Bait Bag slot from having bait.
|
||||
# Beedle's shop does not work correctly if the same item is in multiple slots in the same shop.
|
||||
if "The Great Sea - Beedle's Shop Ship - 20 Rupee Item" in self.progress_locations:
|
||||
beedle_20 = self.get_location("The Great Sea - Beedle's Shop Ship - 20 Rupee Item")
|
||||
add_item_rule(beedle_20, lambda item: item.name not in ["All-Purpose Bait", "Hyoi Pear"])
|
||||
|
||||
# For the same reason, the same item should not appear more than once on the Rock Spire Isle shop ship.
|
||||
# All non-TWW items use the same item (Father's Letter), so at most one non-TWW item can appear in the shop.
|
||||
# The rest must be (unique, but not necessarily local) TWW items.
|
||||
locations = [f"Rock Spire Isle - Beedle's Special Shop Ship - {v} Rupee Item" for v in [500, 950, 900]]
|
||||
if all(loc in self.progress_locations for loc in locations):
|
||||
rock_spire_shop_ship_locations = [self.get_location(location_name) for location_name in locations]
|
||||
|
||||
for i in range(len(rock_spire_shop_ship_locations)):
|
||||
curr_loc = rock_spire_shop_ship_locations[i]
|
||||
other_locs = rock_spire_shop_ship_locations[:i] + rock_spire_shop_ship_locations[i + 1:]
|
||||
|
||||
add_item_rule(
|
||||
curr_loc,
|
||||
lambda item, locations=other_locs: (
|
||||
item.game == "The Wind Waker"
|
||||
and all(location.item is None or item.name != location.item.name for location in locations)
|
||||
)
|
||||
or (
|
||||
item.game != "The Wind Waker"
|
||||
and all(
|
||||
location.item is None or location.item.game == "The Wind Waker" for location in locations
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def stage_set_rules(cls, multiworld: MultiWorld) -> None:
|
||||
"""
|
||||
Class method used to modify the rules for The Wind Waker dungeon locations.
|
||||
|
||||
:param multiworld: The MultiWorld.
|
||||
"""
|
||||
from .randomizers.Dungeons import modify_dungeon_location_rules
|
||||
|
||||
# Set additional rules on dungeon locations as necessary.
|
||||
modify_dungeon_location_rules(multiworld)
|
||||
|
||||
@classmethod
|
||||
def stage_pre_fill(cls, multiworld: MultiWorld) -> None:
|
||||
"""
|
||||
Class method used to correctly place dungeon items for The Wind Waker worlds.
|
||||
|
||||
:param multiworld: The MultiWorld.
|
||||
"""
|
||||
from .randomizers.Dungeons import fill_dungeons_restrictive
|
||||
|
||||
fill_dungeons_restrictive(multiworld)
|
||||
|
||||
def generate_output(self, output_directory: str) -> None:
|
||||
"""
|
||||
Create the output APTWW file that is used to randomize the ISO.
|
||||
|
||||
:param output_directory: The output directory for the APTWW file.
|
||||
"""
|
||||
multiworld = self.multiworld
|
||||
player = self.player
|
||||
|
||||
# Determine the current arrangement for charts.
|
||||
# Create a list where the original island number is the index, and the value is the new island number.
|
||||
# Without randomized charts, this array would be just an ordered list of the numbers 1 to 49.
|
||||
# With randomized charts, the new island number is where the chart for the original island now leads.
|
||||
chart_name_to_island_number = {
|
||||
chart_name: island_number for island_number, chart_name in self.charts.island_number_to_chart_name.items()
|
||||
}
|
||||
charts_mapping: list[int] = []
|
||||
for i in range(1, 49 + 1):
|
||||
original_chart_name = ISLAND_NUMBER_TO_CHART_NAME[i]
|
||||
new_island_number = chart_name_to_island_number[original_chart_name]
|
||||
charts_mapping.append(new_island_number)
|
||||
|
||||
# Output seed name and slot number to seed RNG in randomizer client.
|
||||
output_data = {
|
||||
"Version": list(VERSION),
|
||||
"Seed": multiworld.seed_name,
|
||||
"Slot": player,
|
||||
"Name": self.player_name,
|
||||
"Options": self.options.as_dict(*self.options_dataclass.type_hints),
|
||||
"Required Bosses": self.boss_reqs.required_boss_item_locations,
|
||||
"Locations": {},
|
||||
"Entrances": {},
|
||||
"Charts": charts_mapping,
|
||||
}
|
||||
|
||||
# Output which item has been placed at each location.
|
||||
output_locations = output_data["Locations"]
|
||||
locations = multiworld.get_locations(player)
|
||||
for location in locations:
|
||||
if location.name != "Defeat Ganondorf":
|
||||
if location.item:
|
||||
item_info = {
|
||||
"player": location.item.player,
|
||||
"name": location.item.name,
|
||||
"game": location.item.game,
|
||||
"classification": self._get_classification_name(location.item.classification),
|
||||
}
|
||||
else:
|
||||
item_info = {"name": "Nothing", "game": "The Wind Waker", "classification": "filler"}
|
||||
output_locations[location.name] = item_info
|
||||
|
||||
# Output the mapping of entrances to exits.
|
||||
output_entrances = output_data["Entrances"]
|
||||
for zone_entrance, zone_exit in self.entrances.done_entrances_to_exits.items():
|
||||
output_entrances[zone_entrance.entrance_name] = zone_exit.unique_name
|
||||
|
||||
# Output the plando details to file.
|
||||
aptww = TWWContainer(
|
||||
path=os.path.join(
|
||||
output_directory, f"{multiworld.get_out_file_name_base(player)}{TWWContainer.patch_file_ending}"
|
||||
),
|
||||
player=player,
|
||||
player_name=self.player_name,
|
||||
data=output_data,
|
||||
)
|
||||
aptww.write()
|
||||
|
||||
def extend_hint_information(self, hint_data: dict[int, dict[int, str]]) -> None:
|
||||
"""
|
||||
Fill in additional information text into locations, displayed when hinted.
|
||||
|
||||
:param hint_data: A dictionary of mapping a player ID to a dictionary mapping location IDs to the extra hint
|
||||
information text. This dictionary should be modified as a side-effect of this method.
|
||||
"""
|
||||
# Create a mapping of island names to numbers for sunken treasure hints.
|
||||
island_name_to_number = {v: k for k, v in ISLAND_NUMBER_TO_NAME.items()}
|
||||
|
||||
hint_data[self.player] = {}
|
||||
for location in self.multiworld.get_locations(self.player):
|
||||
if location.address is not None and location.item is not None:
|
||||
# Regardless of ER settings, always hint at the outermost entrance for every "interior" location.
|
||||
zone_exit = self.entrances.get_zone_exit_for_item_location(location.name)
|
||||
if zone_exit is not None:
|
||||
outermost_entrance = self.entrances.get_outermost_entrance_for_exit(zone_exit)
|
||||
assert outermost_entrance is not None and outermost_entrance.island_name is not None
|
||||
hint_data[self.player][location.address] = outermost_entrance.island_name
|
||||
|
||||
# Hint at which chart leads to the sunken treasure for these locations.
|
||||
if location.name.endswith(" - Sunken Treasure"):
|
||||
island_name = location.name.removesuffix(" - Sunken Treasure")
|
||||
island_number = island_name_to_number[island_name]
|
||||
chart_name = self.charts.island_number_to_chart_name[island_number]
|
||||
hint_data[self.player][location.address] = chart_name
|
||||
|
||||
def create_item(self, name: str) -> TWWItem:
|
||||
"""
|
||||
Create an item for this world type and player.
|
||||
|
||||
:param name: The name of the item to create.
|
||||
:raises KeyError: If an invalid item name is provided.
|
||||
"""
|
||||
if name in ITEM_TABLE:
|
||||
return TWWItem(name, self.player, ITEM_TABLE[name], self.item_classification_overrides.get(name))
|
||||
raise KeyError(f"Invalid item name: {name}")
|
||||
|
||||
def get_filler_item_name(self, strict: bool = True) -> str:
|
||||
"""
|
||||
This method is called when the item pool needs to be filled with additional items to match the location count.
|
||||
|
||||
:param strict: Whether the item should be one strictly classified as filler. Defaults to `True`.
|
||||
:return: The name of a filler item from this world.
|
||||
"""
|
||||
# If there are still useful items to place, place those first.
|
||||
if not strict and len(self.useful_pool) > 0:
|
||||
return self.useful_pool.pop()
|
||||
|
||||
# If there are still vanilla filler items to place, place those first.
|
||||
if len(self.filler_pool) > 0:
|
||||
return self.filler_pool.pop()
|
||||
|
||||
# Use the same weights for filler items used in the base randomizer.
|
||||
filler_consumables = ["Yellow Rupee", "Red Rupee", "Purple Rupee", "Joy Pendant"]
|
||||
filler_weights = [3, 7, 10, 3]
|
||||
if not strict:
|
||||
filler_consumables.append("Orange Rupee")
|
||||
filler_weights.append(15)
|
||||
return self.multiworld.random.choices(filler_consumables, weights=filler_weights, k=1)[0]
|
||||
|
||||
def get_pre_fill_items(self) -> list[Item]:
|
||||
"""
|
||||
Return items that need to be collected when creating a fresh `all_state` but don't exist in the multiworld's
|
||||
item pool.
|
||||
|
||||
:return: A list of pre-fill items.
|
||||
"""
|
||||
res = []
|
||||
if self.dungeon_local_item_names:
|
||||
for dungeon in self.dungeons.values():
|
||||
for item in dungeon.all_items:
|
||||
if item.name in self.dungeon_local_item_names:
|
||||
res.append(item)
|
||||
return res
|
||||
|
||||
def fill_slot_data(self) -> Mapping[str, Any]:
|
||||
"""
|
||||
Return the `slot_data` field that will be in the `Connected` network package.
|
||||
|
||||
This is a way the generator can give custom data to the client.
|
||||
The client will receive this as JSON in the `Connected` response.
|
||||
|
||||
:return: A dictionary to be sent to the client when it connects to the server.
|
||||
"""
|
||||
slot_data = self.options.as_dict(*self.options_dataclass.type_hints)
|
||||
|
||||
# Add entrances to `slot_data`. This is the same data that is written to the .aptww file.
|
||||
entrances = {
|
||||
zone_entrance.entrance_name: zone_exit.unique_name
|
||||
for zone_entrance, zone_exit in self.entrances.done_entrances_to_exits.items()
|
||||
}
|
||||
slot_data["entrances"] = entrances
|
||||
|
||||
return slot_data
|
||||
BIN
worlds/tww/assets/icon.ico
Normal file
BIN
worlds/tww/assets/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 216 KiB |
BIN
worlds/tww/assets/icon.png
Normal file
BIN
worlds/tww/assets/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
120
worlds/tww/docs/en_The Wind Waker.md
Normal file
120
worlds/tww/docs/en_The Wind Waker.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# The Wind Waker
|
||||
|
||||
## Where is the options page?
|
||||
|
||||
The [player options page for this game](../player-options) contains all the options you need to configure and export a
|
||||
config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
|
||||
Items get shuffled between the different locations in the game, so each playthrough is unique. Randomized locations
|
||||
include chests, items received from NPC, and treasure salvaged from the ocean floor. The randomizer also includes
|
||||
quality-of-life features such as a fully opened world, removing many cutscenes, increased sailing speed, and more.
|
||||
|
||||
## Which locations get shuffled?
|
||||
|
||||
Only locations put into logic by the world's settings will be randomized. The remaining locations in the game will have
|
||||
a yellow Rupee, which includes a message that the location is not randomized.
|
||||
|
||||
## What is the goal of The Wind Waker?
|
||||
|
||||
Reach and defeat Ganondorf atop Ganon's Tower. This will require all eight shards of the Triforce of Courage, the
|
||||
fully-powered Master Sword (unless it's swordless mode), Light Arrows, and any other items necessary to reach Ganondorf.
|
||||
|
||||
## What does another world's item look like in TWW?
|
||||
|
||||
Items belonging to other non-TWW worlds are represented by Father's Letter (the letter Medli gives you to give to
|
||||
Komali), an unused item in the randomizer.
|
||||
|
||||
## When the player receives an item, what happens?
|
||||
|
||||
When the player receives an item, it will automatically be added to Link's inventory. Unlike many other Zelda
|
||||
randomizers, Link **will not** hold the item above his head.
|
||||
|
||||
## I need help! What do I do?
|
||||
|
||||
Refer to the [FAQ](https://lagolunatic.github.io/wwrando/faq/) first. Then, try the troubleshooting steps in the
|
||||
[setup guide](/tutorial/The%20Wind%20Waker/setup/en). If you are still stuck, please ask in the Wind Waker channel in
|
||||
the Archipelago server.
|
||||
|
||||
## Known issues
|
||||
|
||||
- Randomized freestanding rupees, spoils, and bait will also be given to the player picking up the item. The item will
|
||||
be sent properly, but the collecting player will receive an extra copy.
|
||||
- Demo items (items which are held over Link's head) which are **not** randomized, such as rupees from salvages from
|
||||
random light rings or rewards from minigames, will not work.
|
||||
- Item get messages for progressive items received on locations that send earlier than intended will be incorrect. This
|
||||
does not affect gameplay.
|
||||
- The Heart Piece count in item get messages will be off by one. This does not affect gameplay.
|
||||
- It has been reported that item links can be buggy. Nothing game-breaking, but do be aware of it.
|
||||
|
||||
Feel free to report any other issues or suggest improvements in the Wind Waker channel in the Archipelago server!
|
||||
|
||||
## Tips and Tricks
|
||||
|
||||
### Where are dungeon secrets found in the dungeons?
|
||||
|
||||
[This document](https://docs.google.com/document/d/1LrjGr6W9970XEA-pzl8OhwnqMqTbQaxCX--M-kdsLos/edit?usp=sharing) has
|
||||
images of each of the dungeon secrets.
|
||||
|
||||
### What exactly do the obscure and precise difficulty options do?
|
||||
|
||||
The `logic_obscurity` and `logic_precision` options modify the randomizer's logic to put various tricks and techniques
|
||||
into logic.
|
||||
[This document](https://docs.google.com/spreadsheets/d/14ToE1SvNr9yRRqU4GK2qxIsuDUs9Edegik3wUbLtzH8/edit?usp=sharing)
|
||||
neatly lists the changes that are made. The options are progressive, so, for instance, hard obscure difficulty includes
|
||||
both normal and hard obscure tricks. Some changes require a combination of both options. For example, to put having the
|
||||
Forsaken Fortress cannons blow the door up for you into logic requires both obscure and precise difficulty to be set to
|
||||
at least normal.
|
||||
|
||||
### What are the different options presets?
|
||||
|
||||
A few presets are available on the [player options page](../player-options) for your convenience.
|
||||
|
||||
- **Tournament S7**: These are (as close to as possible) the settings used in the WWR Racing Server's
|
||||
[Season 7 Tournament](https://docs.google.com/document/d/1mJj7an-DvpYilwNt-DdlFOy1fz5_NMZaPZvHeIekplc).
|
||||
The preset features 3 required bosses and hard obscurity difficulty, and while the list of enabled progression options
|
||||
may seem intimidating, the preset also excludes several locations.
|
||||
- **Miniblins 2025**: These are (as close to as possible) the settings used in the WWR Racing Server's
|
||||
[2025 Season of Minblins](https://docs.google.com/document/d/19vT68eU6PepD2BD2ZjR9ikElfqs8pXfqQucZ-TcscV8). This
|
||||
preset is great if you're new to Wind Waker! There aren't too many locations in the world, and you only need to
|
||||
complete two dungeons. You also start with many convenience items, such as double magic, a capacity upgrade for your
|
||||
bow and bombs, and six hearts.
|
||||
- **Mixed Pools**: These are the settings used in the WWR Racing Server's
|
||||
[Mixed Pools Co-op Tournament](https://docs.google.com/document/d/1YGPTtEgP978TIi0PUAD792OtZbE2jBQpI8XCAy63qpg). This
|
||||
preset features full entrance rando and includes many locations behind a randomized entrance. There are also a bunch
|
||||
of overworld locations, as these settings were intended to be played in a two-person co-op team. The preset also has 6
|
||||
required bosses, but since entrance pools are randomized, the bosses could be found anywhere! Check your Sea Chart to
|
||||
find out which island the bosses are on.
|
||||
|
||||
## Planned Features
|
||||
|
||||
- Dynamic CTMC based on enabled options
|
||||
- Hint implementation from base randomizer (hint placement options and hint types)
|
||||
- Integration with Archipelago's hint system (e.g., auction hints)
|
||||
- EnergyLink support
|
||||
- Swift Sail logic as an option
|
||||
- Continued bugfixes
|
||||
|
||||
## Credits
|
||||
|
||||
This randomizer would not be possible without the help from:
|
||||
|
||||
- BigSharkZ: (icon artwork)
|
||||
- Celeste (Maëlle): (logic and typo fixes, additional programming)
|
||||
- Chavu: (logic difficulty document)
|
||||
- CrainWWR: (multiworld and Dolphin memory assistance, additional programming)
|
||||
- Cyb3R: (reference for `TWWClient`)
|
||||
- DeamonHunter: (additional programming)
|
||||
- Dev5ter: (initial TWW AP implmentation)
|
||||
- Gamma / SageOfMirrors: (additional programming)
|
||||
- LagoLunatic: (base randomizer, additional assistance)
|
||||
- Lunix: (Linux support, additional programming)
|
||||
- Mysteryem: (tracker support, additional programming)
|
||||
- Necrofitz: (additional documentation)
|
||||
- Ouro: (tracker support)
|
||||
- tal (matzahTalSoup): (dungeon secrets guide)
|
||||
- Tubamann: (additional programming)
|
||||
|
||||
The Archipelago logo © 2022 by Krista Corkos and Christopher Wilson, licensed under
|
||||
[CC BY-NC 4.0](http://creativecommons.org/licenses/by-nc/4.0/).
|
||||
67
worlds/tww/docs/setup_en.md
Normal file
67
worlds/tww/docs/setup_en.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Setup Guide for The Wind Waker Archipelago
|
||||
|
||||
Welcome to The Wind Waker Archipelago! This guide will help you set up the randomizer and play your first multiworld.
|
||||
If you're playing The Wind Waker, you must follow a few simple steps to get started.
|
||||
|
||||
## Requirements
|
||||
|
||||
You'll need the following components to be able to play with The Wind Waker:
|
||||
* Install [Dolphin Emulator](https://dolphin-emu.org/download/). **We recommend using the latest release.**
|
||||
* For Linux users, you can use the flatpak package
|
||||
[available on Flathub](https://flathub.org/apps/org.DolphinEmu.dolphin-emu).
|
||||
* The 2.5.0 version of the [TWW AP Randomizer Build](https://github.com/tanjo3/wwrando/releases/tag/ap_2.5.0).
|
||||
* A The Wind Waker ISO (North American version), probably named "Legend of Zelda, The - The Wind Waker (USA).iso".
|
||||
|
||||
Optionally, you can also download:
|
||||
* [Wind Waker Tracker](https://github.com/Mysteryem/ww-poptracker/releases/latest)
|
||||
* Requires [PopTracker](https://github.com/black-sliver/PopTracker/releases)
|
||||
* [Custom Wind Waker Player Models](https://github.com/Sage-of-Mirrors/Custom-Wind-Waker-Player-Models)
|
||||
|
||||
## Setting Up a YAML
|
||||
|
||||
All players playing The Wind Waker must provide the room host with a YAML file containing the settings for their world.
|
||||
Visit the [The Wind Waker options page](/games/The%20Wind%20Waker/player-options) to generate a YAML with your desired
|
||||
options. Only locations categorized under the options enabled under "Progression Locations" will be randomized in your
|
||||
world. Once you're happy with your settings, provide the room host with your YAML file and proceed to the next step.
|
||||
|
||||
## Connecting to a Room
|
||||
|
||||
The multiworld host will provide you a link to download your `aptww` file or a zip file containing everyone's files. The
|
||||
`aptww` file should be named `P#_<name>_XXXXX.aptww`, where `#` is your player ID, `<name>` is your player name, and
|
||||
`XXXXX` is the room ID. The host should also provide you with the room's server name and port number.
|
||||
|
||||
Once you do, follow these steps to connect to the room:
|
||||
1. Run the TWW AP Randomizer Build. If this is the first time you've opened the randomizer, you'll need to specify the
|
||||
path to your The Wind Waker ISO and the output folder for the randomized ISO. These will be saved for the next time you
|
||||
open the program.
|
||||
2. Modify any cosmetic convenience tweaks and player customization options as desired.
|
||||
3. For the APTWW file, browse and locate the path to your `aptww` file.
|
||||
4. Click `Randomize` at the bottom-right. This randomizes the ISO and puts it in the output folder you specified. The
|
||||
file will be named `TWW AP_YYYYY_P# (<name>).iso`, where `YYYYY` is the seed name, `#` is your player ID, and `<name>`
|
||||
is your player (slot) name. Verify that the values are correct for the multiworld.
|
||||
5. Open Dolphin and use it to open the randomized ISO.
|
||||
6. Start `ArchipelagoLauncher.exe` (without `.exe` on Linux) and choose `The Wind Waker Client`, which will open the
|
||||
text client. If Dolphin is not already open, or you have yet to start a new file, you will be prompted to do so.
|
||||
* Once you've opened the ISO in Dolphin, the client should say "Dolphin connected successfully.".
|
||||
7. Connect to the room by entering the server name and port number at the top and pressing `Connect`. For rooms hosted
|
||||
on the website, this will be `archipelago.gg:<port>`, where `<port>` is the port number. If a game is hosted from the
|
||||
`ArchipelagoServer.exe` (without `.exe` on Linux), the port number will default to `38281` but may be changed in the
|
||||
`host.yaml`.
|
||||
8. If you've opened a ROM corresponding to the multiworld to which you are connected, it should authenticate your slot
|
||||
name automatically when you start a new save file.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
* Ensure you are running the same version of Archipelago on which the multiworld was generated.
|
||||
* Ensure `tww.apworld` is not in your Archipelago installation's `custom_worlds` folder.
|
||||
* Ensure you are using the correct randomizer build for the version of Archipelago you are using. The build should
|
||||
provide an error message directing you to the correct version. You can also look at the release notes of TWW AP builds
|
||||
[here](https://github.com/tanjo3/wwrando/releases) to see which versions of Archipelago each build is compatible with.
|
||||
* If you encounter issues with authenticating, ensure that the randomized ROM is open in Dolphin and corresponds to the
|
||||
multiworld to which you are connecting.
|
||||
* Ensure that you do not have any Dolphin cheats or codes enabled. Some cheats or codes can unexpectedly interfere with
|
||||
emulation and make troubleshooting errors difficult.
|
||||
* If you get an error message, ensure that `Enable Emulated Memory Size Override` in Dolphin (under `Options` >
|
||||
`Configuration` > `Advanced`) is **disabled**.
|
||||
* If you run with a custom GC boot menu, you'll need to skip it by going to `Options` > `Configuration` > `GameCube`
|
||||
and checking `Skip Main Menu`.
|
||||
125
worlds/tww/randomizers/Charts.py
Normal file
125
worlds/tww/randomizers/Charts.py
Normal file
@@ -0,0 +1,125 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ..Items import ISLAND_NUMBER_TO_CHART_NAME
|
||||
from ..Locations import TWWFlag, TWWLocation
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .. import TWWWorld
|
||||
|
||||
ISLAND_NUMBER_TO_NAME: dict[int, str] = {
|
||||
1: "Forsaken Fortress Sector",
|
||||
2: "Star Island",
|
||||
3: "Northern Fairy Island",
|
||||
4: "Gale Isle",
|
||||
5: "Crescent Moon Island",
|
||||
6: "Seven-Star Isles",
|
||||
7: "Overlook Island",
|
||||
8: "Four-Eye Reef",
|
||||
9: "Mother and Child Isles",
|
||||
10: "Spectacle Island",
|
||||
11: "Windfall Island",
|
||||
12: "Pawprint Isle",
|
||||
13: "Dragon Roost Island",
|
||||
14: "Flight Control Platform",
|
||||
15: "Western Fairy Island",
|
||||
16: "Rock Spire Isle",
|
||||
17: "Tingle Island",
|
||||
18: "Northern Triangle Island",
|
||||
19: "Eastern Fairy Island",
|
||||
20: "Fire Mountain",
|
||||
21: "Star Belt Archipelago",
|
||||
22: "Three-Eye Reef",
|
||||
23: "Greatfish Isle",
|
||||
24: "Cyclops Reef",
|
||||
25: "Six-Eye Reef",
|
||||
26: "Tower of the Gods Sector",
|
||||
27: "Eastern Triangle Island",
|
||||
28: "Thorned Fairy Island",
|
||||
29: "Needle Rock Isle",
|
||||
30: "Islet of Steel",
|
||||
31: "Stone Watcher Island",
|
||||
32: "Southern Triangle Island",
|
||||
33: "Private Oasis",
|
||||
34: "Bomb Island",
|
||||
35: "Bird's Peak Rock",
|
||||
36: "Diamond Steppe Island",
|
||||
37: "Five-Eye Reef",
|
||||
38: "Shark Island",
|
||||
39: "Southern Fairy Island",
|
||||
40: "Ice Ring Isle",
|
||||
41: "Forest Haven",
|
||||
42: "Cliff Plateau Isles",
|
||||
43: "Horseshoe Island",
|
||||
44: "Outset Island",
|
||||
45: "Headstone Island",
|
||||
46: "Two-Eye Reef",
|
||||
47: "Angular Isles",
|
||||
48: "Boating Course",
|
||||
49: "Five-Star Isles",
|
||||
}
|
||||
|
||||
|
||||
class ChartRandomizer:
|
||||
"""
|
||||
This class handles the randomization of charts.
|
||||
Each chart points to a specific island on the map, and this randomizer shuffles these mappings.
|
||||
|
||||
:param world: The Wind Waker game world.
|
||||
"""
|
||||
|
||||
def __init__(self, world: "TWWWorld") -> None:
|
||||
self.world = world
|
||||
self.multiworld = world.multiworld
|
||||
|
||||
self.island_number_to_chart_name = ISLAND_NUMBER_TO_CHART_NAME.copy()
|
||||
|
||||
def setup_progress_sunken_treasure_locations(self) -> None:
|
||||
"""
|
||||
Create the locations for sunken treasure locations and update them as progression and non-progression
|
||||
appropriately. If the option is enabled, randomize which charts point to which sector.
|
||||
"""
|
||||
options = self.world.options
|
||||
|
||||
original_item_names = list(self.island_number_to_chart_name.values())
|
||||
|
||||
# Shuffles the list of island numbers if charts are randomized.
|
||||
# The shuffled island numbers determine which sector each chart points to.
|
||||
shuffled_island_numbers = list(self.island_number_to_chart_name.keys())
|
||||
if options.randomize_charts:
|
||||
self.world.random.shuffle(shuffled_island_numbers)
|
||||
|
||||
for original_item_name in reversed(original_item_names):
|
||||
# Assign each chart to its new island.
|
||||
shuffled_island_number = shuffled_island_numbers.pop()
|
||||
self.island_number_to_chart_name[shuffled_island_number] = original_item_name
|
||||
|
||||
# Additionally, determine if that location is a progress location or not.
|
||||
island_name = ISLAND_NUMBER_TO_NAME[shuffled_island_number]
|
||||
island_location = f"{island_name} - Sunken Treasure"
|
||||
if options.progression_triforce_charts or options.progression_treasure_charts:
|
||||
if original_item_name.startswith("Triforce Chart "):
|
||||
if options.progression_triforce_charts:
|
||||
self.world.progress_locations.add(island_location)
|
||||
self.world.nonprogress_locations.remove(island_location)
|
||||
else:
|
||||
if options.progression_treasure_charts:
|
||||
self.world.progress_locations.add(island_location)
|
||||
self.world.nonprogress_locations.remove(island_location)
|
||||
else:
|
||||
self.world.nonprogress_locations.add(island_location)
|
||||
|
||||
def update_chart_location_flags(self) -> None:
|
||||
"""
|
||||
Update the flags for sunken treasure locations based on the current chart mappings.
|
||||
"""
|
||||
for shuffled_island_number, item_name in self.island_number_to_chart_name.items():
|
||||
island_name = ISLAND_NUMBER_TO_NAME[shuffled_island_number]
|
||||
island_location_str = f"{island_name} - Sunken Treasure"
|
||||
|
||||
if island_location_str in self.world.progress_locations:
|
||||
island_location = self.world.get_location(island_location_str)
|
||||
assert isinstance(island_location, TWWLocation)
|
||||
if item_name.startswith("Triforce Chart "):
|
||||
island_location.flags = TWWFlag.TRI_CHT
|
||||
else:
|
||||
island_location.flags = TWWFlag.TRE_CHT
|
||||
284
worlds/tww/randomizers/Dungeons.py
Normal file
284
worlds/tww/randomizers/Dungeons.py
Normal file
@@ -0,0 +1,284 @@
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
from BaseClasses import CollectionState, Item, Location, MultiWorld
|
||||
from Fill import fill_restrictive
|
||||
from worlds.generic.Rules import add_item_rule
|
||||
|
||||
from ..Items import item_factory
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .. import TWWWorld
|
||||
|
||||
|
||||
class Dungeon:
|
||||
"""
|
||||
This class represents a dungeon in The Wind Waker, including its dungeon items.
|
||||
|
||||
:param name: The name of the dungeon.
|
||||
:param big_key: The big key item for the dungeon.
|
||||
:param small_keys: A list of small key items for the dungeon.
|
||||
:param dungeon_items: A list of other items specific to the dungeon.
|
||||
:param player: The ID of the player associated with the dungeon.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
big_key: Optional[Item],
|
||||
small_keys: list[Item],
|
||||
dungeon_items: list[Item],
|
||||
player: int,
|
||||
):
|
||||
self.name = name
|
||||
self.big_key = big_key
|
||||
self.small_keys = small_keys
|
||||
self.dungeon_items = dungeon_items
|
||||
self.player = player
|
||||
|
||||
@property
|
||||
def keys(self) -> list[Item]:
|
||||
"""
|
||||
Retrieve all the keys for the dungeon.
|
||||
|
||||
:return: A list of Small Keys and the Big Key (if it exists).
|
||||
"""
|
||||
return self.small_keys + ([self.big_key] if self.big_key else [])
|
||||
|
||||
@property
|
||||
def all_items(self) -> list[Item]:
|
||||
"""
|
||||
Retrieve all items associated with the dungeon.
|
||||
|
||||
:return: A list of all items associated with the dungeon.
|
||||
"""
|
||||
return self.dungeon_items + self.keys
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
"""
|
||||
Check equality between this dungeon and another object.
|
||||
|
||||
:param other: The object to compare.
|
||||
:return: `True` if the other object is a Dungeon with the same name and player, `False` otherwise.
|
||||
"""
|
||||
if isinstance(other, Dungeon):
|
||||
return self.name == other.name and self.player == other.player
|
||||
return False
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""
|
||||
Provide a string representation of the dungeon.
|
||||
|
||||
:return: A string representing the dungeon.
|
||||
"""
|
||||
return self.__str__()
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""
|
||||
Convert the dungeon to a human-readable string.
|
||||
|
||||
:return: A string in the format "<name> (Player <player>)".
|
||||
"""
|
||||
return f"{self.name} (Player {self.player})"
|
||||
|
||||
|
||||
def create_dungeons(world: "TWWWorld") -> None:
|
||||
"""
|
||||
Create and assign dungeons to the given world based on game options.
|
||||
|
||||
:param world: The Wind Waker game world.
|
||||
"""
|
||||
player = world.player
|
||||
options = world.options
|
||||
|
||||
def make_dungeon(name: str, big_key: Optional[Item], small_keys: list[Item], dungeon_items: list[Item]) -> Dungeon:
|
||||
dungeon = Dungeon(name, big_key, small_keys, dungeon_items, player)
|
||||
for item in dungeon.all_items:
|
||||
item.dungeon = dungeon
|
||||
return dungeon
|
||||
|
||||
if options.progression_dungeons:
|
||||
if not options.required_bosses or "Dragon Roost Cavern" in world.boss_reqs.required_dungeons:
|
||||
world.dungeons["Dragon Roost Cavern"] = make_dungeon(
|
||||
"Dragon Roost Cavern",
|
||||
item_factory("DRC Big Key", world),
|
||||
item_factory(["DRC Small Key"] * 4, world),
|
||||
item_factory(["DRC Dungeon Map", "DRC Compass"], world),
|
||||
)
|
||||
|
||||
if not options.required_bosses or "Forbidden Woods" in world.boss_reqs.required_dungeons:
|
||||
world.dungeons["Forbidden Woods"] = make_dungeon(
|
||||
"Forbidden Woods",
|
||||
item_factory("FW Big Key", world),
|
||||
item_factory(["FW Small Key"] * 1, world),
|
||||
item_factory(["FW Dungeon Map", "FW Compass"], world),
|
||||
)
|
||||
|
||||
if not options.required_bosses or "Tower of the Gods" in world.boss_reqs.required_dungeons:
|
||||
world.dungeons["Tower of the Gods"] = make_dungeon(
|
||||
"Tower of the Gods",
|
||||
item_factory("TotG Big Key", world),
|
||||
item_factory(["TotG Small Key"] * 2, world),
|
||||
item_factory(["TotG Dungeon Map", "TotG Compass"], world),
|
||||
)
|
||||
|
||||
if not options.required_bosses or "Forsaken Fortress" in world.boss_reqs.required_dungeons:
|
||||
world.dungeons["Forsaken Fortress"] = make_dungeon(
|
||||
"Forsaken Fortress",
|
||||
None,
|
||||
[],
|
||||
item_factory(["FF Dungeon Map", "FF Compass"], world),
|
||||
)
|
||||
|
||||
if not options.required_bosses or "Earth Temple" in world.boss_reqs.required_dungeons:
|
||||
world.dungeons["Earth Temple"] = make_dungeon(
|
||||
"Earth Temple",
|
||||
item_factory("ET Big Key", world),
|
||||
item_factory(["ET Small Key"] * 3, world),
|
||||
item_factory(["ET Dungeon Map", "ET Compass"], world),
|
||||
)
|
||||
|
||||
if not options.required_bosses or "Wind Temple" in world.boss_reqs.required_dungeons:
|
||||
world.dungeons["Wind Temple"] = make_dungeon(
|
||||
"Wind Temple",
|
||||
item_factory("WT Big Key", world),
|
||||
item_factory(["WT Small Key"] * 2, world),
|
||||
item_factory(["WT Dungeon Map", "WT Compass"], world),
|
||||
)
|
||||
|
||||
|
||||
def get_dungeon_item_pool(multiworld: MultiWorld) -> list[Item]:
|
||||
"""
|
||||
Retrieve the item pool for all The Wind Waker dungeons in the multiworld.
|
||||
|
||||
:param multiworld: The MultiWorld instance.
|
||||
:return: List of dungeon items across all The Wind Waker dungeons.
|
||||
"""
|
||||
return [
|
||||
item for world in multiworld.get_game_worlds("The Wind Waker") for item in get_dungeon_item_pool_player(world)
|
||||
]
|
||||
|
||||
|
||||
def get_dungeon_item_pool_player(world: "TWWWorld") -> list[Item]:
|
||||
"""
|
||||
Retrieve the item pool for all dungeons specific to a player.
|
||||
|
||||
:param world: The Wind Waker game world.
|
||||
:return: List of items in the player's dungeons.
|
||||
"""
|
||||
return [item for dungeon in world.dungeons.values() for item in dungeon.all_items]
|
||||
|
||||
|
||||
def get_unfilled_dungeon_locations(multiworld: MultiWorld) -> list[Location]:
|
||||
"""
|
||||
Retrieve all unfilled The Wind Waker dungeon locations in the multiworld.
|
||||
|
||||
:param multiworld: The MultiWorld instance.
|
||||
:return: List of unfilled The Wind Waker dungeon locations.
|
||||
"""
|
||||
return [
|
||||
location
|
||||
for world in multiworld.get_game_worlds("The Wind Waker")
|
||||
for location in multiworld.get_locations(world.player)
|
||||
if location.dungeon and not location.item
|
||||
]
|
||||
|
||||
|
||||
def modify_dungeon_location_rules(multiworld: MultiWorld) -> None:
|
||||
"""
|
||||
Modify the rules for The Wind Waker dungeon locations based on specific player-requested constraints.
|
||||
|
||||
:param multiworld: The MultiWorld instance.
|
||||
"""
|
||||
localized: set[tuple[int, str]] = set()
|
||||
dungeon_specific: set[tuple[int, str]] = set()
|
||||
for subworld in multiworld.get_game_worlds("The Wind Waker"):
|
||||
player = subworld.player
|
||||
if player not in multiworld.groups:
|
||||
localized |= {(player, item_name) for item_name in subworld.dungeon_local_item_names}
|
||||
dungeon_specific |= {(player, item_name) for item_name in subworld.dungeon_specific_item_names}
|
||||
|
||||
if localized:
|
||||
in_dungeon_items = [item for item in get_dungeon_item_pool(multiworld) if (item.player, item.name) in localized]
|
||||
if in_dungeon_items:
|
||||
locations = [location for location in get_unfilled_dungeon_locations(multiworld)]
|
||||
|
||||
for location in locations:
|
||||
if dungeon_specific:
|
||||
# Special case: If Dragon Roost Cavern has its own small keys, then ensure the first chest isn't the
|
||||
# Big Key. This is to avoid placing the Big Key there during fill and resulting in a costly swap.
|
||||
if location.name == "Dragon Roost Cavern - First Room":
|
||||
add_item_rule(
|
||||
location,
|
||||
lambda item: item.name != "DRC Big Key"
|
||||
or (item.player, "DRC Small Key") in dungeon_specific,
|
||||
)
|
||||
|
||||
# Add item rule to ensure dungeon items are in their own dungeon when they should be.
|
||||
add_item_rule(
|
||||
location,
|
||||
lambda item, dungeon=location.dungeon: not (item.player, item.name) in dungeon_specific
|
||||
or item.dungeon is dungeon,
|
||||
)
|
||||
|
||||
|
||||
def fill_dungeons_restrictive(multiworld: MultiWorld) -> None:
|
||||
"""
|
||||
Correctly fill The Wind Waker dungeons in the multiworld.
|
||||
|
||||
:param multiworld: The MultiWorld instance.
|
||||
"""
|
||||
localized: set[tuple[int, str]] = set()
|
||||
dungeon_specific: set[tuple[int, str]] = set()
|
||||
for subworld in multiworld.get_game_worlds("The Wind Waker"):
|
||||
player = subworld.player
|
||||
if player not in multiworld.groups:
|
||||
localized |= {(player, item_name) for item_name in subworld.dungeon_local_item_names}
|
||||
dungeon_specific |= {(player, item_name) for item_name in subworld.dungeon_specific_item_names}
|
||||
|
||||
if localized:
|
||||
in_dungeon_items = [item for item in get_dungeon_item_pool(multiworld) if (item.player, item.name) in localized]
|
||||
if in_dungeon_items:
|
||||
locations = [location for location in get_unfilled_dungeon_locations(multiworld)]
|
||||
multiworld.random.shuffle(locations)
|
||||
|
||||
# Dungeon-locked items have to be placed first so as not to run out of space for dungeon-locked items.
|
||||
# Subsort in the order Big Key, Small Key, Other before placing dungeon items.
|
||||
sort_order = {"Big Key": 3, "Small Key": 2}
|
||||
in_dungeon_items.sort(
|
||||
key=lambda item: sort_order.get(item.type, 1)
|
||||
+ (5 if (item.player, item.name) in dungeon_specific else 0)
|
||||
)
|
||||
|
||||
# Construct a partial `all_state` that contains only the items from `get_pre_fill_items` that aren't in a
|
||||
# dungeon.
|
||||
in_dungeon_player_ids = {item.player for item in in_dungeon_items}
|
||||
all_state_base = CollectionState(multiworld)
|
||||
for item in multiworld.itempool:
|
||||
multiworld.worlds[item.player].collect(all_state_base, item)
|
||||
pre_fill_items = []
|
||||
for player in in_dungeon_player_ids:
|
||||
pre_fill_items += multiworld.worlds[player].get_pre_fill_items()
|
||||
for item in in_dungeon_items:
|
||||
try:
|
||||
pre_fill_items.remove(item)
|
||||
except ValueError:
|
||||
# `pre_fill_items` should be a subset of `in_dungeon_items`, but just in case.
|
||||
pass
|
||||
for item in pre_fill_items:
|
||||
multiworld.worlds[item.player].collect(all_state_base, item)
|
||||
all_state_base.sweep_for_advancements()
|
||||
|
||||
# Remove the completion condition so that minimal-accessibility words place keys correctly.
|
||||
for player in (item.player for item in in_dungeon_items):
|
||||
if all_state_base.has("Victory", player):
|
||||
all_state_base.remove(multiworld.worlds[player].create_item("Victory"))
|
||||
|
||||
fill_restrictive(
|
||||
multiworld,
|
||||
all_state_base,
|
||||
locations,
|
||||
in_dungeon_items,
|
||||
lock=True,
|
||||
allow_excluded=True,
|
||||
name="TWW Dungeon Items",
|
||||
)
|
||||
878
worlds/tww/randomizers/Entrances.py
Normal file
878
worlds/tww/randomizers/Entrances.py
Normal file
@@ -0,0 +1,878 @@
|
||||
from collections import defaultdict
|
||||
from collections.abc import Generator
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, ClassVar, Optional
|
||||
|
||||
from Fill import FillError
|
||||
from Options import OptionError
|
||||
|
||||
from .. import Macros
|
||||
from ..Locations import LOCATION_TABLE, TWWFlag, split_location_name_by_zone
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .. import TWWWorld
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ZoneEntrance:
|
||||
"""
|
||||
A data class that encapsulates information about a zone entrance.
|
||||
"""
|
||||
|
||||
entrance_name: str
|
||||
island_name: Optional[str] = None
|
||||
nested_in: Optional["ZoneExit"] = None
|
||||
|
||||
@property
|
||||
def is_nested(self) -> bool:
|
||||
"""
|
||||
Determine if this entrance is nested within another entrance.
|
||||
|
||||
:return: `True` if the entrance is nested, `False` otherwise.
|
||||
"""
|
||||
return self.nested_in is not None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""
|
||||
Provide a string representation of the zone exit.
|
||||
|
||||
:return: A string representing the zone exit.
|
||||
"""
|
||||
return f"ZoneEntrance('{self.entrance_name}')"
|
||||
|
||||
all: ClassVar[dict[str, "ZoneEntrance"]] = {}
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
ZoneEntrance.all[self.entrance_name] = self
|
||||
|
||||
# Must be an island entrance XOR must be a nested entrance.
|
||||
assert (self.island_name is None) ^ (self.nested_in is None)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ZoneExit:
|
||||
"""
|
||||
A data class that encapsulates information about a zone exit.
|
||||
"""
|
||||
|
||||
unique_name: str
|
||||
zone_name: Optional[str] = None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""
|
||||
Provide a string representation of the zone exit.
|
||||
|
||||
:return: A string representing the zone exit.
|
||||
"""
|
||||
return f"ZoneExit('{self.unique_name}')"
|
||||
|
||||
all: ClassVar[dict[str, "ZoneExit"]] = {}
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
ZoneExit.all[self.unique_name] = self
|
||||
|
||||
|
||||
DUNGEON_ENTRANCES: list[ZoneEntrance] = [
|
||||
ZoneEntrance("Dungeon Entrance on Dragon Roost Island", "Dragon Roost Island"),
|
||||
ZoneEntrance("Dungeon Entrance in Forest Haven Sector", "Forest Haven"),
|
||||
ZoneEntrance("Dungeon Entrance in Tower of the Gods Sector", "Tower of the Gods Sector"),
|
||||
ZoneEntrance("Dungeon Entrance on Headstone Island", "Headstone Island"),
|
||||
ZoneEntrance("Dungeon Entrance on Gale Isle", "Gale Isle"),
|
||||
]
|
||||
DUNGEON_EXITS: list[ZoneExit] = [
|
||||
ZoneExit("Dragon Roost Cavern", "Dragon Roost Cavern"),
|
||||
ZoneExit("Forbidden Woods", "Forbidden Woods"),
|
||||
ZoneExit("Tower of the Gods", "Tower of the Gods"),
|
||||
ZoneExit("Earth Temple", "Earth Temple"),
|
||||
ZoneExit("Wind Temple", "Wind Temple"),
|
||||
]
|
||||
|
||||
MINIBOSS_ENTRANCES: list[ZoneEntrance] = [
|
||||
ZoneEntrance("Miniboss Entrance in Forbidden Woods", nested_in=ZoneExit.all["Forbidden Woods"]),
|
||||
ZoneEntrance("Miniboss Entrance in Tower of the Gods", nested_in=ZoneExit.all["Tower of the Gods"]),
|
||||
ZoneEntrance("Miniboss Entrance in Earth Temple", nested_in=ZoneExit.all["Earth Temple"]),
|
||||
ZoneEntrance("Miniboss Entrance in Wind Temple", nested_in=ZoneExit.all["Wind Temple"]),
|
||||
ZoneEntrance("Miniboss Entrance in Hyrule Castle", "Tower of the Gods Sector"),
|
||||
]
|
||||
MINIBOSS_EXITS: list[ZoneExit] = [
|
||||
ZoneExit("Forbidden Woods Miniboss Arena"),
|
||||
ZoneExit("Tower of the Gods Miniboss Arena"),
|
||||
ZoneExit("Earth Temple Miniboss Arena"),
|
||||
ZoneExit("Wind Temple Miniboss Arena"),
|
||||
ZoneExit("Master Sword Chamber"),
|
||||
]
|
||||
|
||||
BOSS_ENTRANCES: list[ZoneEntrance] = [
|
||||
ZoneEntrance("Boss Entrance in Dragon Roost Cavern", nested_in=ZoneExit.all["Dragon Roost Cavern"]),
|
||||
ZoneEntrance("Boss Entrance in Forbidden Woods", nested_in=ZoneExit.all["Forbidden Woods"]),
|
||||
ZoneEntrance("Boss Entrance in Tower of the Gods", nested_in=ZoneExit.all["Tower of the Gods"]),
|
||||
ZoneEntrance("Boss Entrance in Forsaken Fortress", "Forsaken Fortress Sector"),
|
||||
ZoneEntrance("Boss Entrance in Earth Temple", nested_in=ZoneExit.all["Earth Temple"]),
|
||||
ZoneEntrance("Boss Entrance in Wind Temple", nested_in=ZoneExit.all["Wind Temple"]),
|
||||
]
|
||||
BOSS_EXITS: list[ZoneExit] = [
|
||||
ZoneExit("Gohma Boss Arena"),
|
||||
ZoneExit("Kalle Demos Boss Arena"),
|
||||
ZoneExit("Gohdan Boss Arena"),
|
||||
ZoneExit("Helmaroc King Boss Arena"),
|
||||
ZoneExit("Jalhalla Boss Arena"),
|
||||
ZoneExit("Molgera Boss Arena"),
|
||||
]
|
||||
|
||||
SECRET_CAVE_ENTRANCES: list[ZoneEntrance] = [
|
||||
ZoneEntrance("Secret Cave Entrance on Outset Island", "Outset Island"),
|
||||
ZoneEntrance("Secret Cave Entrance on Dragon Roost Island", "Dragon Roost Island"),
|
||||
ZoneEntrance("Secret Cave Entrance on Fire Mountain", "Fire Mountain"),
|
||||
ZoneEntrance("Secret Cave Entrance on Ice Ring Isle", "Ice Ring Isle"),
|
||||
ZoneEntrance("Secret Cave Entrance on Private Oasis", "Private Oasis"),
|
||||
ZoneEntrance("Secret Cave Entrance on Needle Rock Isle", "Needle Rock Isle"),
|
||||
ZoneEntrance("Secret Cave Entrance on Angular Isles", "Angular Isles"),
|
||||
ZoneEntrance("Secret Cave Entrance on Boating Course", "Boating Course"),
|
||||
ZoneEntrance("Secret Cave Entrance on Stone Watcher Island", "Stone Watcher Island"),
|
||||
ZoneEntrance("Secret Cave Entrance on Overlook Island", "Overlook Island"),
|
||||
ZoneEntrance("Secret Cave Entrance on Bird's Peak Rock", "Bird's Peak Rock"),
|
||||
ZoneEntrance("Secret Cave Entrance on Pawprint Isle", "Pawprint Isle"),
|
||||
ZoneEntrance("Secret Cave Entrance on Pawprint Isle Side Isle", "Pawprint Isle"),
|
||||
ZoneEntrance("Secret Cave Entrance on Diamond Steppe Island", "Diamond Steppe Island"),
|
||||
ZoneEntrance("Secret Cave Entrance on Bomb Island", "Bomb Island"),
|
||||
ZoneEntrance("Secret Cave Entrance on Rock Spire Isle", "Rock Spire Isle"),
|
||||
ZoneEntrance("Secret Cave Entrance on Shark Island", "Shark Island"),
|
||||
ZoneEntrance("Secret Cave Entrance on Cliff Plateau Isles", "Cliff Plateau Isles"),
|
||||
ZoneEntrance("Secret Cave Entrance on Horseshoe Island", "Horseshoe Island"),
|
||||
ZoneEntrance("Secret Cave Entrance on Star Island", "Star Island"),
|
||||
]
|
||||
SECRET_CAVE_EXITS: list[ZoneExit] = [
|
||||
ZoneExit("Savage Labyrinth", zone_name="Outset Island"),
|
||||
ZoneExit("Dragon Roost Island Secret Cave", zone_name="Dragon Roost Island"),
|
||||
ZoneExit("Fire Mountain Secret Cave", zone_name="Fire Mountain"),
|
||||
ZoneExit("Ice Ring Isle Secret Cave", zone_name="Ice Ring Isle"),
|
||||
ZoneExit("Cabana Labyrinth", zone_name="Private Oasis"),
|
||||
ZoneExit("Needle Rock Isle Secret Cave", zone_name="Needle Rock Isle"),
|
||||
ZoneExit("Angular Isles Secret Cave", zone_name="Angular Isles"),
|
||||
ZoneExit("Boating Course Secret Cave", zone_name="Boating Course"),
|
||||
ZoneExit("Stone Watcher Island Secret Cave", zone_name="Stone Watcher Island"),
|
||||
ZoneExit("Overlook Island Secret Cave", zone_name="Overlook Island"),
|
||||
ZoneExit("Bird's Peak Rock Secret Cave", zone_name="Bird's Peak Rock"),
|
||||
ZoneExit("Pawprint Isle Chuchu Cave", zone_name="Pawprint Isle"),
|
||||
ZoneExit("Pawprint Isle Wizzrobe Cave"),
|
||||
ZoneExit("Diamond Steppe Island Warp Maze Cave", zone_name="Diamond Steppe Island"),
|
||||
ZoneExit("Bomb Island Secret Cave", zone_name="Bomb Island"),
|
||||
ZoneExit("Rock Spire Isle Secret Cave", zone_name="Rock Spire Isle"),
|
||||
ZoneExit("Shark Island Secret Cave", zone_name="Shark Island"),
|
||||
ZoneExit("Cliff Plateau Isles Secret Cave", zone_name="Cliff Plateau Isles"),
|
||||
ZoneExit("Horseshoe Island Secret Cave", zone_name="Horseshoe Island"),
|
||||
ZoneExit("Star Island Secret Cave", zone_name="Star Island"),
|
||||
]
|
||||
|
||||
SECRET_CAVE_INNER_ENTRANCES: list[ZoneEntrance] = [
|
||||
ZoneEntrance("Inner Entrance in Ice Ring Isle Secret Cave", nested_in=ZoneExit.all["Ice Ring Isle Secret Cave"]),
|
||||
ZoneEntrance(
|
||||
"Inner Entrance in Cliff Plateau Isles Secret Cave", nested_in=ZoneExit.all["Cliff Plateau Isles Secret Cave"]
|
||||
),
|
||||
]
|
||||
SECRET_CAVE_INNER_EXITS: list[ZoneExit] = [
|
||||
ZoneExit("Ice Ring Isle Inner Cave"),
|
||||
ZoneExit("Cliff Plateau Isles Inner Cave"),
|
||||
]
|
||||
|
||||
FAIRY_FOUNTAIN_ENTRANCES: list[ZoneEntrance] = [
|
||||
ZoneEntrance("Fairy Fountain Entrance on Outset Island", "Outset Island"),
|
||||
ZoneEntrance("Fairy Fountain Entrance on Thorned Fairy Island", "Thorned Fairy Island"),
|
||||
ZoneEntrance("Fairy Fountain Entrance on Eastern Fairy Island", "Eastern Fairy Island"),
|
||||
ZoneEntrance("Fairy Fountain Entrance on Western Fairy Island", "Western Fairy Island"),
|
||||
ZoneEntrance("Fairy Fountain Entrance on Southern Fairy Island", "Southern Fairy Island"),
|
||||
ZoneEntrance("Fairy Fountain Entrance on Northern Fairy Island", "Northern Fairy Island"),
|
||||
]
|
||||
FAIRY_FOUNTAIN_EXITS: list[ZoneExit] = [
|
||||
ZoneExit("Outset Fairy Fountain"),
|
||||
ZoneExit("Thorned Fairy Fountain", zone_name="Thorned Fairy Island"),
|
||||
ZoneExit("Eastern Fairy Fountain", zone_name="Eastern Fairy Island"),
|
||||
ZoneExit("Western Fairy Fountain", zone_name="Western Fairy Island"),
|
||||
ZoneExit("Southern Fairy Fountain", zone_name="Southern Fairy Island"),
|
||||
ZoneExit("Northern Fairy Fountain", zone_name="Northern Fairy Island"),
|
||||
]
|
||||
|
||||
DUNGEON_INNER_EXITS: list[ZoneExit] = (
|
||||
MINIBOSS_EXITS
|
||||
+ BOSS_EXITS
|
||||
)
|
||||
|
||||
ALL_ENTRANCES: list[ZoneEntrance] = (
|
||||
DUNGEON_ENTRANCES
|
||||
+ MINIBOSS_ENTRANCES
|
||||
+ BOSS_ENTRANCES
|
||||
+ SECRET_CAVE_ENTRANCES
|
||||
+ SECRET_CAVE_INNER_ENTRANCES
|
||||
+ FAIRY_FOUNTAIN_ENTRANCES
|
||||
)
|
||||
ALL_EXITS: list[ZoneExit] = (
|
||||
DUNGEON_EXITS
|
||||
+ MINIBOSS_EXITS
|
||||
+ BOSS_EXITS
|
||||
+ SECRET_CAVE_EXITS
|
||||
+ SECRET_CAVE_INNER_EXITS
|
||||
+ FAIRY_FOUNTAIN_EXITS
|
||||
)
|
||||
|
||||
ENTRANCE_RANDOMIZABLE_ITEM_LOCATION_TYPES: list[TWWFlag] = [
|
||||
TWWFlag.DUNGEON,
|
||||
TWWFlag.PZL_CVE,
|
||||
TWWFlag.CBT_CVE,
|
||||
TWWFlag.SAVAGE,
|
||||
TWWFlag.GRT_FRY,
|
||||
]
|
||||
ITEM_LOCATION_NAME_TO_EXIT_OVERRIDES: dict[str, ZoneExit] = {
|
||||
"Forbidden Woods - Mothula Miniboss Room": ZoneExit.all["Forbidden Woods Miniboss Arena"],
|
||||
"Tower of the Gods - Darknut Miniboss Room": ZoneExit.all["Tower of the Gods Miniboss Arena"],
|
||||
"Earth Temple - Stalfos Miniboss Room": ZoneExit.all["Earth Temple Miniboss Arena"],
|
||||
"Wind Temple - Wizzrobe Miniboss Room": ZoneExit.all["Wind Temple Miniboss Arena"],
|
||||
"Hyrule - Master Sword Chamber": ZoneExit.all["Master Sword Chamber"],
|
||||
|
||||
"Dragon Roost Cavern - Gohma Heart Container": ZoneExit.all["Gohma Boss Arena"],
|
||||
"Forbidden Woods - Kalle Demos Heart Container": ZoneExit.all["Kalle Demos Boss Arena"],
|
||||
"Tower of the Gods - Gohdan Heart Container": ZoneExit.all["Gohdan Boss Arena"],
|
||||
"Forsaken Fortress - Helmaroc King Heart Container": ZoneExit.all["Helmaroc King Boss Arena"],
|
||||
"Earth Temple - Jalhalla Heart Container": ZoneExit.all["Jalhalla Boss Arena"],
|
||||
"Wind Temple - Molgera Heart Container": ZoneExit.all["Molgera Boss Arena"],
|
||||
|
||||
"Pawprint Isle - Wizzrobe Cave": ZoneExit.all["Pawprint Isle Wizzrobe Cave"],
|
||||
|
||||
"Ice Ring Isle - Inner Cave - Chest": ZoneExit.all["Ice Ring Isle Inner Cave"],
|
||||
"Cliff Plateau Isles - Highest Isle": ZoneExit.all["Cliff Plateau Isles Inner Cave"],
|
||||
|
||||
"Outset Island - Great Fairy": ZoneExit.all["Outset Fairy Fountain"],
|
||||
}
|
||||
|
||||
MINIBOSS_EXIT_TO_DUNGEON: dict[str, str] = {
|
||||
"Forbidden Woods Miniboss Arena": "Forbidden Woods",
|
||||
"Tower of the Gods Miniboss Arena": "Tower of the Gods",
|
||||
"Earth Temple Miniboss Arena": "Earth Temple",
|
||||
"Wind Temple Miniboss Arena": "Wind Temple",
|
||||
}
|
||||
|
||||
BOSS_EXIT_TO_DUNGEON: dict[str, str] = {
|
||||
"Gohma Boss Arena": "Dragon Roost Cavern",
|
||||
"Kalle Demos Boss Arena": "Forbidden Woods",
|
||||
"Gohdan Boss Arena": "Tower of the Gods",
|
||||
"Helmaroc King Boss Arena": "Forsaken Fortress",
|
||||
"Jalhalla Boss Arena": "Earth Temple",
|
||||
"Molgera Boss Arena": "Wind Temple",
|
||||
}
|
||||
|
||||
VANILLA_ENTRANCES_TO_EXITS: dict[str, str] = {
|
||||
"Dungeon Entrance on Dragon Roost Island": "Dragon Roost Cavern",
|
||||
"Dungeon Entrance in Forest Haven Sector": "Forbidden Woods",
|
||||
"Dungeon Entrance in Tower of the Gods Sector": "Tower of the Gods",
|
||||
"Dungeon Entrance on Headstone Island": "Earth Temple",
|
||||
"Dungeon Entrance on Gale Isle": "Wind Temple",
|
||||
|
||||
"Miniboss Entrance in Forbidden Woods": "Forbidden Woods Miniboss Arena",
|
||||
"Miniboss Entrance in Tower of the Gods": "Tower of the Gods Miniboss Arena",
|
||||
"Miniboss Entrance in Earth Temple": "Earth Temple Miniboss Arena",
|
||||
"Miniboss Entrance in Wind Temple": "Wind Temple Miniboss Arena",
|
||||
"Miniboss Entrance in Hyrule Castle": "Master Sword Chamber",
|
||||
|
||||
"Boss Entrance in Dragon Roost Cavern": "Gohma Boss Arena",
|
||||
"Boss Entrance in Forbidden Woods": "Kalle Demos Boss Arena",
|
||||
"Boss Entrance in Tower of the Gods": "Gohdan Boss Arena",
|
||||
"Boss Entrance in Forsaken Fortress": "Helmaroc King Boss Arena",
|
||||
"Boss Entrance in Earth Temple": "Jalhalla Boss Arena",
|
||||
"Boss Entrance in Wind Temple": "Molgera Boss Arena",
|
||||
|
||||
"Secret Cave Entrance on Outset Island": "Savage Labyrinth",
|
||||
"Secret Cave Entrance on Dragon Roost Island": "Dragon Roost Island Secret Cave",
|
||||
"Secret Cave Entrance on Fire Mountain": "Fire Mountain Secret Cave",
|
||||
"Secret Cave Entrance on Ice Ring Isle": "Ice Ring Isle Secret Cave",
|
||||
"Secret Cave Entrance on Private Oasis": "Cabana Labyrinth",
|
||||
"Secret Cave Entrance on Needle Rock Isle": "Needle Rock Isle Secret Cave",
|
||||
"Secret Cave Entrance on Angular Isles": "Angular Isles Secret Cave",
|
||||
"Secret Cave Entrance on Boating Course": "Boating Course Secret Cave",
|
||||
"Secret Cave Entrance on Stone Watcher Island": "Stone Watcher Island Secret Cave",
|
||||
"Secret Cave Entrance on Overlook Island": "Overlook Island Secret Cave",
|
||||
"Secret Cave Entrance on Bird's Peak Rock": "Bird's Peak Rock Secret Cave",
|
||||
"Secret Cave Entrance on Pawprint Isle": "Pawprint Isle Chuchu Cave",
|
||||
"Secret Cave Entrance on Pawprint Isle Side Isle": "Pawprint Isle Wizzrobe Cave",
|
||||
"Secret Cave Entrance on Diamond Steppe Island": "Diamond Steppe Island Warp Maze Cave",
|
||||
"Secret Cave Entrance on Bomb Island": "Bomb Island Secret Cave",
|
||||
"Secret Cave Entrance on Rock Spire Isle": "Rock Spire Isle Secret Cave",
|
||||
"Secret Cave Entrance on Shark Island": "Shark Island Secret Cave",
|
||||
"Secret Cave Entrance on Cliff Plateau Isles": "Cliff Plateau Isles Secret Cave",
|
||||
"Secret Cave Entrance on Horseshoe Island": "Horseshoe Island Secret Cave",
|
||||
"Secret Cave Entrance on Star Island": "Star Island Secret Cave",
|
||||
|
||||
"Inner Entrance in Ice Ring Isle Secret Cave": "Ice Ring Isle Inner Cave",
|
||||
"Inner Entrance in Cliff Plateau Isles Secret Cave": "Cliff Plateau Isles Inner Cave",
|
||||
|
||||
"Fairy Fountain Entrance on Outset Island": "Outset Fairy Fountain",
|
||||
"Fairy Fountain Entrance on Thorned Fairy Island": "Thorned Fairy Fountain",
|
||||
"Fairy Fountain Entrance on Eastern Fairy Island": "Eastern Fairy Fountain",
|
||||
"Fairy Fountain Entrance on Western Fairy Island": "Western Fairy Fountain",
|
||||
"Fairy Fountain Entrance on Southern Fairy Island": "Southern Fairy Fountain",
|
||||
"Fairy Fountain Entrance on Northern Fairy Island": "Northern Fairy Fountain",
|
||||
}
|
||||
|
||||
|
||||
class EntranceRandomizer:
|
||||
"""
|
||||
This class handles the logic for The Wind Waker entrance randomizer.
|
||||
|
||||
We reference the logic from the base randomizer with some modifications to suit it for Archipelago.
|
||||
Reference: https://github.com/LagoLunatic/wwrando/blob/master/randomizers/entrances.py
|
||||
|
||||
:param world: The Wind Waker game world.
|
||||
"""
|
||||
|
||||
def __init__(self, world: "TWWWorld"):
|
||||
self.world = world
|
||||
self.multiworld = world.multiworld
|
||||
self.player = world.player
|
||||
|
||||
self.item_location_to_containing_zone_exit: dict[str, ZoneExit] = {}
|
||||
self.zone_exit_to_logically_dependent_item_locations: dict[ZoneExit, list[str]] = defaultdict(list)
|
||||
self.register_mappings_between_item_locations_and_zone_exits()
|
||||
|
||||
self.done_entrances_to_exits: dict[ZoneEntrance, ZoneExit] = {}
|
||||
self.done_exits_to_entrances: dict[ZoneExit, ZoneEntrance] = {}
|
||||
|
||||
for entrance_name, exit_name in VANILLA_ENTRANCES_TO_EXITS.items():
|
||||
zone_entrance = ZoneEntrance.all[entrance_name]
|
||||
zone_exit = ZoneExit.all[exit_name]
|
||||
self.done_entrances_to_exits[zone_entrance] = zone_exit
|
||||
self.done_exits_to_entrances[zone_exit] = zone_entrance
|
||||
|
||||
self.banned_exits: list[ZoneExit] = []
|
||||
self.islands_with_a_banned_dungeon: set[str] = set()
|
||||
|
||||
def randomize_entrances(self) -> None:
|
||||
"""
|
||||
Randomize entrances for The Wind Waker.
|
||||
"""
|
||||
self.init_banned_exits()
|
||||
|
||||
for relevant_entrances, relevant_exits in self.get_all_entrance_sets_to_be_randomized():
|
||||
self.randomize_one_set_of_entrances(relevant_entrances, relevant_exits)
|
||||
|
||||
self.finalize_all_randomized_sets_of_entrances()
|
||||
|
||||
def init_banned_exits(self) -> None:
|
||||
"""
|
||||
Initialize the list of banned exits for the randomizer.
|
||||
|
||||
Dungeon exits in banned dungeons should be prohibited from being randomized.
|
||||
Additionally, if dungeon entrances are not randomized, we can now note which island holds these banned dungeons.
|
||||
"""
|
||||
options = self.world.options
|
||||
|
||||
if options.required_bosses:
|
||||
for zone_exit in BOSS_EXITS:
|
||||
assert zone_exit.unique_name.endswith(" Boss Arena")
|
||||
boss_name = zone_exit.unique_name.removesuffix(" Boss Arena")
|
||||
if boss_name in self.world.boss_reqs.banned_bosses:
|
||||
self.banned_exits.append(zone_exit)
|
||||
for zone_exit in DUNGEON_EXITS:
|
||||
dungeon_name = zone_exit.unique_name
|
||||
if dungeon_name in self.world.boss_reqs.banned_dungeons:
|
||||
self.banned_exits.append(zone_exit)
|
||||
for zone_exit in MINIBOSS_EXITS:
|
||||
if zone_exit == ZoneExit.all["Master Sword Chamber"]:
|
||||
# Hyrule cannot be chosen as a banned dungeon.
|
||||
continue
|
||||
assert zone_exit.unique_name.endswith(" Miniboss Arena")
|
||||
dungeon_name = zone_exit.unique_name.removesuffix(" Miniboss Arena")
|
||||
if dungeon_name in self.world.boss_reqs.banned_dungeons:
|
||||
self.banned_exits.append(zone_exit)
|
||||
|
||||
if not options.randomize_dungeon_entrances:
|
||||
# If dungeon entrances are not randomized, `islands_with_a_banned_dungeon` can be initialized early since
|
||||
# it's preset and won't be updated later since we won't randomize the dungeon entrances.
|
||||
for en in DUNGEON_ENTRANCES:
|
||||
if self.done_entrances_to_exits[en].unique_name in self.world.boss_reqs.banned_dungeons:
|
||||
assert en.island_name is not None
|
||||
self.islands_with_a_banned_dungeon.add(en.island_name)
|
||||
|
||||
def randomize_one_set_of_entrances(
|
||||
self, relevant_entrances: list[ZoneEntrance], relevant_exits: list[ZoneExit]
|
||||
) -> None:
|
||||
"""
|
||||
Randomize a single set of entrances and their corresponding exits.
|
||||
|
||||
:param relevant_entrances: A list of entrances to be randomized.
|
||||
:param relevant_exits: A list of exits corresponding to the entrances.
|
||||
"""
|
||||
# Keep miniboss and boss entrances vanilla in non-required bosses' dungeons.
|
||||
for zone_entrance in relevant_entrances.copy():
|
||||
zone_exit = self.done_entrances_to_exits[zone_entrance]
|
||||
if zone_exit in self.banned_exits and zone_exit in DUNGEON_INNER_EXITS:
|
||||
relevant_entrances.remove(zone_entrance)
|
||||
else:
|
||||
del self.done_entrances_to_exits[zone_entrance]
|
||||
for zone_exit in relevant_exits.copy():
|
||||
if zone_exit in self.banned_exits and zone_exit in DUNGEON_INNER_EXITS:
|
||||
relevant_exits.remove(zone_exit)
|
||||
else:
|
||||
del self.done_exits_to_entrances[zone_exit]
|
||||
|
||||
self.multiworld.random.shuffle(relevant_entrances)
|
||||
|
||||
# We calculate which exits are terminal (the end of a nested chain) per set instead of for all entrances.
|
||||
# This is so that, for example, Ice Ring Isle counts as terminal when its inner cave is not being randomized.
|
||||
non_terminal_exits = []
|
||||
for en in relevant_entrances:
|
||||
if en.nested_in is not None and en.nested_in not in non_terminal_exits:
|
||||
non_terminal_exits.append(en.nested_in)
|
||||
terminal_exits = {ex for ex in relevant_exits if ex not in non_terminal_exits}
|
||||
|
||||
remaining_entrances = relevant_entrances.copy()
|
||||
remaining_exits = relevant_exits.copy()
|
||||
|
||||
nonprogress_entrances, nonprogress_exits = self.split_nonprogress_entrances_and_exits(
|
||||
remaining_entrances, remaining_exits
|
||||
)
|
||||
if nonprogress_entrances:
|
||||
for en in nonprogress_entrances:
|
||||
remaining_entrances.remove(en)
|
||||
for ex in nonprogress_exits:
|
||||
remaining_exits.remove(ex)
|
||||
self.randomize_one_set_of_exits(nonprogress_entrances, nonprogress_exits, terminal_exits)
|
||||
|
||||
self.randomize_one_set_of_exits(remaining_entrances, remaining_exits, terminal_exits)
|
||||
|
||||
def check_if_one_exit_is_progress(self, zone_exit: ZoneExit) -> bool:
|
||||
"""
|
||||
Determine if the zone exit leads to progress locations in the world.
|
||||
|
||||
:param zone_exit: The zone exit to check.
|
||||
:return: Whether the zone exit leads to progress locations.
|
||||
"""
|
||||
locs_for_exit = self.zone_exit_to_logically_dependent_item_locations[zone_exit]
|
||||
assert locs_for_exit, f"Could not find any item locations corresponding to zone exit: {zone_exit.unique_name}"
|
||||
|
||||
# Banned required bosses mode dungeons still technically count as progress locations, so filter them out
|
||||
# separately first.
|
||||
nonbanned_locs = [loc for loc in locs_for_exit if loc not in self.world.boss_reqs.banned_locations]
|
||||
progress_locs = [loc for loc in nonbanned_locs if loc not in self.world.nonprogress_locations]
|
||||
return bool(progress_locs)
|
||||
|
||||
def split_nonprogress_entrances_and_exits(
|
||||
self, relevant_entrances: list[ZoneEntrance], relevant_exits: list[ZoneExit]
|
||||
) -> tuple[list[ZoneEntrance], list[ZoneExit]]:
|
||||
"""
|
||||
Splits the entrance and exit lists into two pairs: ones that should be considered nonprogress on this seed (will
|
||||
never lead to any progress items) and ones that should be regarded as potentially required.
|
||||
|
||||
This is so we can effectively randomize these two pairs separately without convoluted logic to ensure they don't
|
||||
connect.
|
||||
|
||||
:param relevant_entrances: A list of entrances.
|
||||
:param relevant_exits: A list of exits corresponding to the entrances.
|
||||
:raises FillError: If the number of randomizable entrances does not equal the number of randomizable exits.
|
||||
"""
|
||||
nonprogress_exits = [ex for ex in relevant_exits if not self.check_if_one_exit_is_progress(ex)]
|
||||
nonprogress_entrances = [
|
||||
en
|
||||
for en in relevant_entrances
|
||||
if en.nested_in is not None
|
||||
and (
|
||||
(en.nested_in in nonprogress_exits)
|
||||
# The area this entrance is nested in is not randomized, but we still need to determine whether it's
|
||||
# progression.
|
||||
or (en.nested_in not in relevant_exits and not self.check_if_one_exit_is_progress(en.nested_in))
|
||||
)
|
||||
]
|
||||
|
||||
# At this point, `nonprogress_entrances` includes only the inner entrances nested inside the main exits, not any
|
||||
# island entrances on the sea. So, we need to select `N` random island entrances to allow all of the nonprogress
|
||||
# exits to be accessible, where `N` is the difference between the number of entrances and exits we currently
|
||||
# have.
|
||||
possible_island_entrances = [en for en in relevant_entrances if en.island_name is not None]
|
||||
|
||||
# We need special logic to handle Forsaken Fortress, as it is the only island entrance inside a dungeon.
|
||||
ff_boss_entrance = ZoneEntrance.all["Boss Entrance in Forsaken Fortress"]
|
||||
if ff_boss_entrance in possible_island_entrances:
|
||||
if self.world.options.progression_dungeons:
|
||||
if "Forsaken Fortress" in self.world.boss_reqs.banned_dungeons:
|
||||
ff_progress = False
|
||||
else:
|
||||
ff_progress = True
|
||||
else:
|
||||
ff_progress = False
|
||||
|
||||
if ff_progress:
|
||||
# If it's progress, don't allow it to be randomly chosen to lead to nonprogress exits.
|
||||
possible_island_entrances.remove(ff_boss_entrance)
|
||||
else:
|
||||
# If it's not progress, manually mark it as such, and don't allow it to be chosen randomly.
|
||||
nonprogress_entrances.append(ff_boss_entrance)
|
||||
possible_island_entrances.remove(ff_boss_entrance)
|
||||
|
||||
num_island_entrances_needed = len(nonprogress_exits) - len(nonprogress_entrances)
|
||||
if num_island_entrances_needed > len(possible_island_entrances):
|
||||
raise FillError("Not enough island entrances left to split entrances.")
|
||||
|
||||
for _ in range(num_island_entrances_needed):
|
||||
# Note: `relevant_entrances` is already shuffled, so we can just take the first result from
|
||||
# `possible_island_entrances`—it's the same as picking one randomly.
|
||||
nonprogress_island_entrance = possible_island_entrances.pop(0)
|
||||
nonprogress_entrances.append(nonprogress_island_entrance)
|
||||
|
||||
assert len(nonprogress_entrances) == len(nonprogress_exits)
|
||||
|
||||
return nonprogress_entrances, nonprogress_exits
|
||||
|
||||
def randomize_one_set_of_exits(
|
||||
self, relevant_entrances: list[ZoneEntrance], relevant_exits: list[ZoneExit], terminal_exits: set[ZoneExit]
|
||||
) -> None:
|
||||
"""
|
||||
Randomize a single set of entrances and their corresponding exits.
|
||||
|
||||
:param relevant_entrances: A list of entrances to be randomized.
|
||||
:param relevant_exits: A list of exits corresponding to the entrances.
|
||||
:param terminal_exits: A set of exits which do not contain any entrances.
|
||||
:raises FillError: If there are no valid exits to assign to an entrance.
|
||||
"""
|
||||
options = self.world.options
|
||||
|
||||
remaining_entrances = relevant_entrances.copy()
|
||||
remaining_exits = relevant_exits.copy()
|
||||
|
||||
doing_banned = False
|
||||
if any(ex in self.banned_exits for ex in relevant_exits):
|
||||
doing_banned = True
|
||||
|
||||
if options.required_bosses and not doing_banned:
|
||||
# Prioritize entrances that share an island with an entrance randomized to lead into a
|
||||
# required-bosses-mode-banned dungeon. (e.g., DRI, Pawprint, Outset, TotG sector.)
|
||||
# This is because we need to prevent these islands from having a required boss or anything that could lead
|
||||
# to a required boss. If we don't do this first, we can get backed into a corner where there is no other
|
||||
# option left.
|
||||
entrances_not_on_unique_islands = []
|
||||
for zone_entrance in relevant_entrances:
|
||||
if zone_entrance.is_nested:
|
||||
continue
|
||||
if zone_entrance.island_name in self.islands_with_a_banned_dungeon:
|
||||
# This island was already used on a previous call to `randomize_one_set_of_exits`.
|
||||
entrances_not_on_unique_islands.append(zone_entrance)
|
||||
continue
|
||||
for zone_entrance in entrances_not_on_unique_islands:
|
||||
remaining_entrances.remove(zone_entrance)
|
||||
remaining_entrances = entrances_not_on_unique_islands + remaining_entrances
|
||||
|
||||
while remaining_entrances:
|
||||
# Filter out boss entrances that aren't yet accessible from the sea.
|
||||
# We don't want to connect these to anything yet or we risk creating an infinite loop.
|
||||
possible_remaining_entrances = [
|
||||
en for en in remaining_entrances if self.get_outermost_entrance_for_entrance(en) is not None
|
||||
]
|
||||
zone_entrance = possible_remaining_entrances.pop(0)
|
||||
remaining_entrances.remove(zone_entrance)
|
||||
|
||||
possible_remaining_exits = remaining_exits.copy()
|
||||
|
||||
if len(possible_remaining_entrances) == 0 and len(remaining_entrances) > 0:
|
||||
# If this is the last entrance we have left to attach exits to, we can't place a terminal exit here.
|
||||
# Terminal exits do not create another entrance, so one would leave us with no possible way to continue
|
||||
# placing the remaining exits on future loops.
|
||||
possible_remaining_exits = [ex for ex in possible_remaining_exits if ex not in terminal_exits]
|
||||
|
||||
if options.required_bosses and zone_entrance.island_name is not None and not doing_banned:
|
||||
# Prevent required bosses (and non-terminal exits, which could lead to required bosses) from appearing
|
||||
# on islands where we already placed a banned boss or dungeon.
|
||||
# This can happen with DRI and Pawprint, as these islands have two entrances. This would be bad because
|
||||
# the required bosses mode's dungeon markers only tell you what island the required dungeons are on, not
|
||||
# which of the two entrances to enter.
|
||||
# So, if a banned dungeon is placed on DRI's main entrance, we will have to fill DRI's pit entrance with
|
||||
# either a miniboss or one of the caves that does not have a nested entrance inside. We allow multiple
|
||||
# banned and required dungeons on a single island.
|
||||
if zone_entrance.island_name in self.islands_with_a_banned_dungeon:
|
||||
possible_remaining_exits = [
|
||||
ex
|
||||
for ex in possible_remaining_exits
|
||||
if ex in terminal_exits and ex not in (DUNGEON_EXITS + BOSS_EXITS)
|
||||
]
|
||||
|
||||
if not possible_remaining_exits:
|
||||
raise FillError(f"No valid exits to place for entrance: {zone_entrance.entrance_name}")
|
||||
|
||||
zone_exit = self.multiworld.random.choice(possible_remaining_exits)
|
||||
remaining_exits.remove(zone_exit)
|
||||
|
||||
self.done_entrances_to_exits[zone_entrance] = zone_exit
|
||||
self.done_exits_to_entrances[zone_exit] = zone_entrance
|
||||
|
||||
if zone_exit in self.banned_exits:
|
||||
# Keep track of which islands have a required bosses mode banned dungeon to avoid marker overlap.
|
||||
if zone_exit in DUNGEON_EXITS + BOSS_EXITS:
|
||||
# We only keep track of dungeon exits and boss exits, not miniboss exits.
|
||||
# Banned miniboss exits can share an island with required dungeons/bosses.
|
||||
outer_entrance = self.get_outermost_entrance_for_entrance(zone_entrance)
|
||||
|
||||
# Because we filter above so that we always assign entrances from the sea inwards, we can assume
|
||||
# that when we assign an entrance, it has a path back to the sea.
|
||||
# If we're assigning a non-terminal entrance, any nested entrances will get assigned after this one,
|
||||
# and we'll run through this code again (so we can reason based on `zone_exit` only instead of
|
||||
# having to recurse through the nested exits to find banned dungeons/bosses).
|
||||
assert outer_entrance and outer_entrance.island_name is not None
|
||||
self.islands_with_a_banned_dungeon.add(outer_entrance.island_name)
|
||||
|
||||
def finalize_all_randomized_sets_of_entrances(self) -> None:
|
||||
"""
|
||||
Finalize all randomized entrance sets.
|
||||
|
||||
For all entrance-exit pairs, this function adds a connection with the appropriate access rule to the world.
|
||||
"""
|
||||
|
||||
def get_access_rule(entrance: ZoneEntrance) -> str:
|
||||
snake_case_region = entrance.entrance_name.lower().replace("'", "").replace(" ", "_")
|
||||
return getattr(Macros, f"can_access_{snake_case_region}")
|
||||
|
||||
# Connect each entrance-exit pair in the multiworld with the access rule for the entrance.
|
||||
# The Great Sea is the parent_region for many entrances, so get it in advance.
|
||||
great_sea_region = self.world.get_region("The Great Sea")
|
||||
for zone_entrance, zone_exit in self.done_entrances_to_exits.items():
|
||||
# Get the parent region of the entrance.
|
||||
if zone_entrance.island_name is not None:
|
||||
# Entrances with an `island_name` are found in The Great Sea.
|
||||
parent_region = great_sea_region
|
||||
else:
|
||||
# All other entrances must be nested within some other region.
|
||||
parent_region = self.world.get_region(zone_entrance.nested_in.unique_name)
|
||||
exit_region_name = zone_exit.unique_name
|
||||
exit_region = self.world.get_region(exit_region_name)
|
||||
parent_region.connect(
|
||||
exit_region,
|
||||
# The default name uses the "parent_region -> connecting_region", but the parent_region would not be
|
||||
# useful for spoiler paths or debugging, so use the entrance name at the start.
|
||||
f"{zone_entrance.entrance_name} -> {exit_region_name}",
|
||||
rule=lambda state, rule=get_access_rule(zone_entrance): rule(state, self.player),
|
||||
)
|
||||
|
||||
if __debug__ and self.world.options.required_bosses:
|
||||
# Ensure we didn't accidentally place a banned boss and a required boss on the same island.
|
||||
banned_island_names = set(
|
||||
self.get_entrance_zone_for_boss(boss_name) for boss_name in self.world.boss_reqs.banned_bosses
|
||||
)
|
||||
required_island_names = set(
|
||||
self.get_entrance_zone_for_boss(boss_name) for boss_name in self.world.boss_reqs.required_bosses
|
||||
)
|
||||
assert not banned_island_names & required_island_names
|
||||
|
||||
def register_mappings_between_item_locations_and_zone_exits(self) -> None:
|
||||
"""
|
||||
Map item locations to their corresponding zone exits.
|
||||
"""
|
||||
for loc_name in list(LOCATION_TABLE.keys()):
|
||||
zone_exit = self.get_zone_exit_for_item_location(loc_name)
|
||||
if zone_exit is not None:
|
||||
self.item_location_to_containing_zone_exit[loc_name] = zone_exit
|
||||
self.zone_exit_to_logically_dependent_item_locations[zone_exit].append(loc_name)
|
||||
|
||||
if loc_name == "The Great Sea - Withered Trees":
|
||||
# This location isn't inside a zone exit, but it does logically require the player to be able to reach
|
||||
# a different item location inside one.
|
||||
sub_zone_exit = self.get_zone_exit_for_item_location("Cliff Plateau Isles - Highest Isle")
|
||||
if sub_zone_exit is not None:
|
||||
self.zone_exit_to_logically_dependent_item_locations[sub_zone_exit].append(loc_name)
|
||||
|
||||
def get_all_entrance_sets_to_be_randomized(
|
||||
self,
|
||||
) -> Generator[tuple[list[ZoneEntrance], list[ZoneExit]], None, None]:
|
||||
"""
|
||||
Retrieve all entrance-exit pairs that need to be randomized.
|
||||
|
||||
:raises OptionError: If an invalid randomization option is set in the world's options.
|
||||
:return: A generator that yields sets of entrances and exits to be randomized.
|
||||
"""
|
||||
options = self.world.options
|
||||
|
||||
dungeons = bool(options.randomize_dungeon_entrances)
|
||||
minibosses = bool(options.randomize_miniboss_entrances)
|
||||
bosses = bool(options.randomize_boss_entrances)
|
||||
secret_caves = bool(options.randomize_secret_cave_entrances)
|
||||
inner_caves = bool(options.randomize_secret_cave_inner_entrances)
|
||||
fountains = bool(options.randomize_fairy_fountain_entrances)
|
||||
|
||||
mix_entrances = options.mix_entrances
|
||||
if mix_entrances == "separate_pools":
|
||||
if dungeons:
|
||||
yield self.get_one_entrance_set(dungeons=dungeons)
|
||||
if minibosses:
|
||||
yield self.get_one_entrance_set(minibosses=minibosses)
|
||||
if bosses:
|
||||
yield self.get_one_entrance_set(bosses=bosses)
|
||||
if secret_caves:
|
||||
yield self.get_one_entrance_set(caves=secret_caves)
|
||||
if inner_caves:
|
||||
yield self.get_one_entrance_set(inner_caves=inner_caves)
|
||||
if fountains:
|
||||
yield self.get_one_entrance_set(fountains=fountains)
|
||||
elif mix_entrances == "mix_pools":
|
||||
yield self.get_one_entrance_set(
|
||||
dungeons=dungeons,
|
||||
minibosses=minibosses,
|
||||
bosses=bosses,
|
||||
caves=secret_caves,
|
||||
inner_caves=inner_caves,
|
||||
fountains=fountains,
|
||||
)
|
||||
else:
|
||||
raise OptionError(f"Invalid entrance randomization option: {mix_entrances}")
|
||||
|
||||
def get_one_entrance_set(
|
||||
self,
|
||||
*,
|
||||
dungeons: bool = False,
|
||||
caves: bool = False,
|
||||
minibosses: bool = False,
|
||||
bosses: bool = False,
|
||||
inner_caves: bool = False,
|
||||
fountains: bool = False,
|
||||
) -> tuple[list[ZoneEntrance], list[ZoneExit]]:
|
||||
"""
|
||||
Retrieve a single set of entrance-exit pairs that need to be randomized.
|
||||
|
||||
:param dungeons: Whether to include dungeon entrances and exits. Defaults to `False`.
|
||||
:param caves: Whether to include secret cave entrances and exits. Defaults to `False`.
|
||||
:param minibosses: Whether to include miniboss entrances and exits. Defaults to `False`.
|
||||
:param bosses: Whether to include boss entrances and exits. Defaults to `False`.
|
||||
:param inner_caves: Whether to include inner cave entrances and exits. Defaults to `False`.
|
||||
:param fountains: Whether to include fairy fountain entrances and exits. Defaults to `False`.
|
||||
:return: A tuple of lists of entrances and exits that should be randomized together.
|
||||
"""
|
||||
relevant_entrances: list[ZoneEntrance] = []
|
||||
relevant_exits: list[ZoneExit] = []
|
||||
if dungeons:
|
||||
relevant_entrances += DUNGEON_ENTRANCES
|
||||
relevant_exits += DUNGEON_EXITS
|
||||
if minibosses:
|
||||
relevant_entrances += MINIBOSS_ENTRANCES
|
||||
relevant_exits += MINIBOSS_EXITS
|
||||
if bosses:
|
||||
relevant_entrances += BOSS_ENTRANCES
|
||||
relevant_exits += BOSS_EXITS
|
||||
if caves:
|
||||
relevant_entrances += SECRET_CAVE_ENTRANCES
|
||||
relevant_exits += SECRET_CAVE_EXITS
|
||||
if inner_caves:
|
||||
relevant_entrances += SECRET_CAVE_INNER_ENTRANCES
|
||||
relevant_exits += SECRET_CAVE_INNER_EXITS
|
||||
if fountains:
|
||||
relevant_entrances += FAIRY_FOUNTAIN_ENTRANCES
|
||||
relevant_exits += FAIRY_FOUNTAIN_EXITS
|
||||
return relevant_entrances, relevant_exits
|
||||
|
||||
def get_outermost_entrance_for_exit(self, zone_exit: ZoneExit) -> Optional[ZoneEntrance]:
|
||||
"""
|
||||
Unrecurses nested dungeons to determine a given exit's outermost (island) entrance.
|
||||
|
||||
:param zone_exit: The given exit.
|
||||
:return: The outermost (island) entrance for the exit, or `None` if entrances have yet to be randomized.
|
||||
"""
|
||||
zone_entrance = self.done_exits_to_entrances[zone_exit]
|
||||
return self.get_outermost_entrance_for_entrance(zone_entrance)
|
||||
|
||||
def get_outermost_entrance_for_entrance(self, zone_entrance: ZoneEntrance) -> Optional[ZoneEntrance]:
|
||||
"""
|
||||
Unrecurses nested dungeons to determine a given entrance's outermost (island) entrance.
|
||||
|
||||
:param zone_exit: The given entrance.
|
||||
:return: The outermost (island) entrance for the entrance, or `None` if entrances have yet to be randomized.
|
||||
"""
|
||||
seen_entrances = self.get_all_entrances_on_path_to_entrance(zone_entrance)
|
||||
if seen_entrances is None:
|
||||
# Undecided.
|
||||
return None
|
||||
outermost_entrance = seen_entrances[-1]
|
||||
return outermost_entrance
|
||||
|
||||
def get_all_entrances_on_path_to_entrance(self, zone_entrance: ZoneEntrance) -> Optional[list[ZoneEntrance]]:
|
||||
"""
|
||||
Unrecurses nested dungeons to build a list of all entrances leading to a given entrance.
|
||||
|
||||
:param zone_exit: The given entrance.
|
||||
:return: A list of entrances leading to the given entrance, or `None` if entrances have yet to be randomized.
|
||||
"""
|
||||
seen_entrances: list[ZoneEntrance] = []
|
||||
while zone_entrance.is_nested:
|
||||
if zone_entrance in seen_entrances:
|
||||
path_str = ", ".join([e.entrance_name for e in seen_entrances])
|
||||
raise FillError(f"Entrances are in an infinite loop: {path_str}")
|
||||
seen_entrances.append(zone_entrance)
|
||||
if zone_entrance.nested_in not in self.done_exits_to_entrances:
|
||||
# Undecided.
|
||||
return None
|
||||
zone_entrance = self.done_exits_to_entrances[zone_entrance.nested_in]
|
||||
seen_entrances.append(zone_entrance)
|
||||
return seen_entrances
|
||||
|
||||
def is_item_location_behind_randomizable_entrance(self, location_name: str) -> bool:
|
||||
"""
|
||||
Determine if the location is behind a randomizable entrance.
|
||||
|
||||
:param location_name: The location to check.
|
||||
:return: `True` if the location is behind a randomizable entrance, `False` otherwise.
|
||||
"""
|
||||
loc_zone_name, _ = split_location_name_by_zone(location_name)
|
||||
if loc_zone_name in ["Ganon's Tower", "Mailbox"]:
|
||||
# Ganon's Tower and the handful of Mailbox locations that depend on beating dungeon bosses are considered
|
||||
# "Dungeon" location types by the logic, but the entrance randomizer does not need to consider them.
|
||||
# Although the mail locations are technically locked behind dungeons, we can still ignore them here because
|
||||
# if all of the locations in the dungeon itself are nonprogress, then any mail depending on that dungeon
|
||||
# should also be enforced as nonprogress by other parts of the code.
|
||||
return False
|
||||
|
||||
types = LOCATION_TABLE[location_name].flags
|
||||
is_boss = TWWFlag.BOSS in types
|
||||
if loc_zone_name == "Forsaken Fortress" and not is_boss:
|
||||
# Special case. FF is a dungeon that is not randomized, except for the boss arena.
|
||||
return False
|
||||
|
||||
is_big_octo = TWWFlag.BG_OCTO in types
|
||||
if is_big_octo:
|
||||
# The Big Octo Great Fairy is the only Great Fairy location that is not also a Fairy Fountain.
|
||||
return False
|
||||
|
||||
# In the general case, we check if the location has a type corresponding to exits that can be randomized.
|
||||
if any(t in types for t in ENTRANCE_RANDOMIZABLE_ITEM_LOCATION_TYPES):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_zone_exit_for_item_location(self, location_name: str) -> Optional[ZoneExit]:
|
||||
"""
|
||||
Retrieve the zone exit for a given location.
|
||||
|
||||
:param location_name: The name of the location.
|
||||
:raises Exception: If a location exit override should be used instead.
|
||||
:return: The zone exit for the location or `None` if the location is not behind a randomizable entrance.
|
||||
"""
|
||||
if not self.is_item_location_behind_randomizable_entrance(location_name):
|
||||
return None
|
||||
|
||||
zone_exit = ITEM_LOCATION_NAME_TO_EXIT_OVERRIDES.get(location_name, None)
|
||||
if zone_exit is not None:
|
||||
return zone_exit
|
||||
|
||||
loc_zone_name, _ = split_location_name_by_zone(location_name)
|
||||
possible_exits = [ex for ex in ZoneExit.all.values() if ex.zone_name == loc_zone_name]
|
||||
if len(possible_exits) == 0:
|
||||
return None
|
||||
elif len(possible_exits) == 1:
|
||||
return possible_exits[0]
|
||||
else:
|
||||
raise Exception(
|
||||
f"Multiple zone exits share the same zone name: {loc_zone_name!r}. "
|
||||
"Use a location exit override instead."
|
||||
)
|
||||
|
||||
def get_entrance_zone_for_boss(self, boss_name: str) -> str:
|
||||
"""
|
||||
Retrieve the entrance zone for a given boss.
|
||||
|
||||
:param boss_name: The name of the boss.
|
||||
:return: The name of the island on which the boss is located.
|
||||
"""
|
||||
boss_arena_name = f"{boss_name} Boss Arena"
|
||||
zone_exit = ZoneExit.all[boss_arena_name]
|
||||
outermost_entrance = self.get_outermost_entrance_for_exit(zone_exit)
|
||||
assert outermost_entrance is not None and outermost_entrance.island_name is not None
|
||||
return outermost_entrance.island_name
|
||||
205
worlds/tww/randomizers/ItemPool.py
Normal file
205
worlds/tww/randomizers/ItemPool.py
Normal file
@@ -0,0 +1,205 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from BaseClasses import ItemClassification as IC
|
||||
from Fill import FillError
|
||||
|
||||
from ..Items import ITEM_TABLE, item_factory
|
||||
from ..Options import DungeonItem
|
||||
from .Dungeons import get_dungeon_item_pool_player
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .. import TWWWorld
|
||||
|
||||
VANILLA_DUNGEON_ITEM_LOCATIONS: dict[str, list[str]] = {
|
||||
"DRC Small Key": [
|
||||
"Dragon Roost Cavern - First Room",
|
||||
"Dragon Roost Cavern - Boarded Up Chest",
|
||||
"Dragon Roost Cavern - Rat Room Boarded Up Chest",
|
||||
"Dragon Roost Cavern - Bird's Nest",
|
||||
],
|
||||
"FW Small Key": [
|
||||
"Forbidden Woods - Vine Maze Right Chest"
|
||||
],
|
||||
"TotG Small Key": [
|
||||
"Tower of the Gods - Hop Across Floating Boxes",
|
||||
"Tower of the Gods - Floating Platforms Room"
|
||||
],
|
||||
"ET Small Key": [
|
||||
"Earth Temple - Transparent Chest in First Crypt",
|
||||
"Earth Temple - Casket in Second Crypt",
|
||||
"Earth Temple - End of Foggy Room With Floormasters",
|
||||
],
|
||||
"WT Small Key": [
|
||||
"Wind Temple - Spike Wall Room - First Chest",
|
||||
"Wind Temple - Chest Behind Seven Armos"
|
||||
],
|
||||
|
||||
"DRC Big Key": ["Dragon Roost Cavern - Big Key Chest"],
|
||||
"FW Big Key": ["Forbidden Woods - Big Key Chest"],
|
||||
"TotG Big Key": ["Tower of the Gods - Big Key Chest"],
|
||||
"ET Big Key": ["Earth Temple - Big Key Chest"],
|
||||
"WT Big Key": ["Wind Temple - Big Key Chest"],
|
||||
|
||||
"DRC Dungeon Map": ["Dragon Roost Cavern - Alcove With Water Jugs"],
|
||||
"FW Dungeon Map": ["Forbidden Woods - First Room"],
|
||||
"TotG Dungeon Map": ["Tower of the Gods - Chest Behind Bombable Walls"],
|
||||
"FF Dungeon Map": ["Forsaken Fortress - Chest Outside Upper Jail Cell"],
|
||||
"ET Dungeon Map": ["Earth Temple - Transparent Chest In Warp Pot Room"],
|
||||
"WT Dungeon Map": ["Wind Temple - Chest In Many Cyclones Room"],
|
||||
|
||||
"DRC Compass": ["Dragon Roost Cavern - Rat Room"],
|
||||
"FW Compass": ["Forbidden Woods - Vine Maze Left Chest"],
|
||||
"TotG Compass": ["Tower of the Gods - Skulls Room Chest"],
|
||||
"FF Compass": ["Forsaken Fortress - Chest Guarded By Bokoblin"],
|
||||
"ET Compass": ["Earth Temple - Chest In Three Blocks Room"],
|
||||
"WT Compass": ["Wind Temple - Chest In Middle Of Hub Room"],
|
||||
}
|
||||
|
||||
|
||||
def generate_itempool(world: "TWWWorld") -> None:
|
||||
"""
|
||||
Generate the item pool for the world.
|
||||
|
||||
:param world: The Wind Waker game world.
|
||||
"""
|
||||
multiworld = world.multiworld
|
||||
|
||||
# Get the core pool of items.
|
||||
pool, precollected_items = get_pool_core(world)
|
||||
|
||||
# Add precollected items to the multiworld's `precollected_items` list.
|
||||
for item in precollected_items:
|
||||
multiworld.push_precollected(item_factory(item, world))
|
||||
|
||||
# Place a "Victory" item on "Defeat Ganondorf" for the spoiler log.
|
||||
world.get_location("Defeat Ganondorf").place_locked_item(item_factory("Victory", world))
|
||||
|
||||
# Create the pool of the remaining shuffled items.
|
||||
items = item_factory(pool, world)
|
||||
world.random.shuffle(items)
|
||||
|
||||
multiworld.itempool += items
|
||||
|
||||
# Dungeon items should already be created, so handle those separately.
|
||||
handle_dungeon_items(world)
|
||||
|
||||
|
||||
def get_pool_core(world: "TWWWorld") -> tuple[list[str], list[str]]:
|
||||
"""
|
||||
Get the core pool of items and precollected items for the world.
|
||||
|
||||
:param world: The Wind Waker game world.
|
||||
:return: A tuple of the item pool and precollected items.
|
||||
"""
|
||||
pool: list[str] = []
|
||||
precollected_items: list[str] = []
|
||||
|
||||
# Split items into three different pools: progression, useful, and filler.
|
||||
progression_pool: list[str] = []
|
||||
useful_pool: list[str] = []
|
||||
filler_pool: list[str] = []
|
||||
for item, data in ITEM_TABLE.items():
|
||||
if data.type == "Item":
|
||||
adjusted_classification = world.item_classification_overrides.get(item)
|
||||
classification = data.classification if adjusted_classification is None else adjusted_classification
|
||||
|
||||
if classification & IC.progression:
|
||||
progression_pool.extend([item] * data.quantity)
|
||||
elif classification & IC.useful:
|
||||
useful_pool.extend([item] * data.quantity)
|
||||
else:
|
||||
filler_pool.extend([item] * data.quantity)
|
||||
|
||||
# Assign useful and filler items to item pools in the world.
|
||||
world.random.shuffle(useful_pool)
|
||||
world.random.shuffle(filler_pool)
|
||||
world.useful_pool = useful_pool
|
||||
world.filler_pool = filler_pool
|
||||
|
||||
# Add filler items to place into excluded locations.
|
||||
pool.extend([world.get_filler_item_name() for _ in world.options.exclude_locations])
|
||||
|
||||
# The remaining of items left to place should be the same as the number of non-excluded locations in the world.
|
||||
nonexcluded_locations = [
|
||||
location
|
||||
for location in world.multiworld.get_locations(world.player)
|
||||
if location.name not in world.options.exclude_locations
|
||||
]
|
||||
num_items_left_to_place = len(nonexcluded_locations) - 1
|
||||
|
||||
# Account for the dungeon items that have already been created.
|
||||
for dungeon in world.dungeons.values():
|
||||
num_items_left_to_place -= len(dungeon.all_items)
|
||||
|
||||
# All progression items are added to the item pool.
|
||||
if len(progression_pool) > num_items_left_to_place:
|
||||
raise FillError(
|
||||
"There are insufficient locations to place progression items! "
|
||||
f"Trying to place {len(progression_pool)} items in only {num_items_left_to_place} locations."
|
||||
)
|
||||
pool.extend(progression_pool)
|
||||
num_items_left_to_place -= len(progression_pool)
|
||||
|
||||
# If the player starts with a sword, add one to the precollected items list and remove one from the item pool.
|
||||
if world.options.sword_mode == "start_with_sword":
|
||||
precollected_items.append("Progressive Sword")
|
||||
num_items_left_to_place += 1
|
||||
pool.remove("Progressive Sword")
|
||||
# Or, if it's swordless mode, remove all swords from the item pool.
|
||||
elif world.options.sword_mode == "swordless":
|
||||
while "Progressive Sword" in pool:
|
||||
num_items_left_to_place += 1
|
||||
pool.remove("Progressive Sword")
|
||||
|
||||
# Place useful items, then filler items to fill out the remaining locations.
|
||||
pool.extend([world.get_filler_item_name(strict=False) for _ in range(num_items_left_to_place)])
|
||||
|
||||
return pool, precollected_items
|
||||
|
||||
|
||||
def handle_dungeon_items(world: "TWWWorld") -> None:
|
||||
"""
|
||||
Handle the placement of dungeon items in the world.
|
||||
|
||||
:param world: The Wind Waker game world.
|
||||
"""
|
||||
player = world.player
|
||||
multiworld = world.multiworld
|
||||
options = world.options
|
||||
|
||||
dungeon_items = [
|
||||
item
|
||||
for item in get_dungeon_item_pool_player(world)
|
||||
if item.name not in multiworld.worlds[player].dungeon_local_item_names
|
||||
]
|
||||
|
||||
for x in range(len(dungeon_items) - 1, -1, -1):
|
||||
item = dungeon_items[x]
|
||||
|
||||
# Consider dungeon items in non-required dungeons as filler.
|
||||
if item.dungeon.name in world.boss_reqs.banned_dungeons:
|
||||
item.classification = IC.filler
|
||||
|
||||
option: DungeonItem
|
||||
if item.type == "Big Key":
|
||||
option = options.randomize_bigkeys
|
||||
elif item.type == "Small Key":
|
||||
option = options.randomize_smallkeys
|
||||
else:
|
||||
option = options.randomize_mapcompass
|
||||
|
||||
if option == "startwith":
|
||||
dungeon_items.pop(x)
|
||||
multiworld.push_precollected(item)
|
||||
multiworld.itempool.append(item_factory(world.get_filler_item_name(), world))
|
||||
elif option == "vanilla":
|
||||
for location_name in VANILLA_DUNGEON_ITEM_LOCATIONS[item.name]:
|
||||
location = world.get_location(location_name)
|
||||
if location.item is None:
|
||||
dungeon_items.pop(x)
|
||||
location.place_locked_item(item)
|
||||
break
|
||||
else:
|
||||
raise FillError(f"Could not place dungeon item in vanilla location: {item}")
|
||||
|
||||
multiworld.itempool.extend([item for item in dungeon_items])
|
||||
121
worlds/tww/randomizers/RequiredBosses.py
Normal file
121
worlds/tww/randomizers/RequiredBosses.py
Normal file
@@ -0,0 +1,121 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from Options import OptionError
|
||||
|
||||
from ..Locations import DUNGEON_NAMES, LOCATION_TABLE, TWWFlag, split_location_name_by_zone
|
||||
from ..Options import TWWOptions
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .. import TWWWorld
|
||||
|
||||
|
||||
class RequiredBossesRandomizer:
|
||||
"""
|
||||
This class handles the randomization of the required bosses in The Wind Waker game based on user options.
|
||||
|
||||
If the option is on, the required bosses must be defeated as part of the unlock condition of Puppet Ganon's door.
|
||||
The quadrants in which the bosses are located are marked on the player's Sea Chart.
|
||||
|
||||
:param world: The Wind Waker game world.
|
||||
"""
|
||||
|
||||
def __init__(self, world: "TWWWorld"):
|
||||
self.world = world
|
||||
self.multiworld = world.multiworld
|
||||
|
||||
self.required_boss_item_locations: list[str] = []
|
||||
self.required_dungeons: set[str] = set()
|
||||
self.required_bosses: list[str] = []
|
||||
self.banned_locations: set[str] = set()
|
||||
self.banned_dungeons: set[str] = set()
|
||||
self.banned_bosses: list[str] = []
|
||||
|
||||
def validate_boss_options(self, options: TWWOptions) -> None:
|
||||
"""
|
||||
Validate the user-defined boss options to ensure logical consistency.
|
||||
|
||||
:param options: The game options set by the user.
|
||||
:raises OptionError: If the boss options are inconsistent.
|
||||
"""
|
||||
if not options.progression_dungeons:
|
||||
raise OptionError("You cannot make bosses required when progression dungeons are disabled.")
|
||||
|
||||
if len(options.included_dungeons.value & options.excluded_dungeons.value) != 0:
|
||||
raise OptionError(
|
||||
"A conflict was found in the lists of required and banned dungeons for required bosses mode."
|
||||
)
|
||||
|
||||
def randomize_required_bosses(self) -> None:
|
||||
"""
|
||||
Randomize the required bosses based on user-defined constraints and options.
|
||||
|
||||
:raises OptionError: If the randomization fails to meet user-defined constraints.
|
||||
"""
|
||||
options = self.world.options
|
||||
|
||||
# Validate constraints on required bosses options.
|
||||
self.validate_boss_options(options)
|
||||
|
||||
# If the user enforces a dungeon location to be priority, consider that when selecting required bosses.
|
||||
dungeon_names = set(DUNGEON_NAMES)
|
||||
required_dungeons = options.included_dungeons.value
|
||||
for location_name in options.priority_locations.value:
|
||||
dungeon_name, _ = split_location_name_by_zone(location_name)
|
||||
if dungeon_name in dungeon_names:
|
||||
required_dungeons.add(dungeon_name)
|
||||
|
||||
# Ensure we aren't prioritizing more dungeon locations than the requested number of required bosses.
|
||||
num_required_bosses = options.num_required_bosses
|
||||
if len(required_dungeons) > num_required_bosses:
|
||||
raise OptionError(
|
||||
"Could not select required bosses to satisfy options set by the user. "
|
||||
"There are more dungeons with priority locations than the desired number of required bosses."
|
||||
)
|
||||
|
||||
# Ensure that after removing excluded dungeons, we still have enough to satisfy user options.
|
||||
num_remaining = num_required_bosses - len(required_dungeons)
|
||||
remaining_dungeon_options = dungeon_names - required_dungeons - options.excluded_dungeons.value
|
||||
if len(remaining_dungeon_options) < num_remaining:
|
||||
raise OptionError(
|
||||
"Could not select required bosses to satisfy options set by the user. "
|
||||
"After removing the excluded dungeons, there are not enough to meet the desired number of required "
|
||||
"bosses."
|
||||
)
|
||||
|
||||
# Finish selecting required bosses.
|
||||
required_dungeons.update(self.world.random.sample(sorted(remaining_dungeon_options), num_remaining))
|
||||
|
||||
# Exclude locations that are not in the dungeon of a required boss.
|
||||
banned_dungeons = dungeon_names - required_dungeons
|
||||
for location_name, location_data in LOCATION_TABLE.items():
|
||||
dungeon_name, _ = split_location_name_by_zone(location_name)
|
||||
if dungeon_name in banned_dungeons and TWWFlag.DUNGEON in location_data.flags:
|
||||
self.banned_locations.add(location_name)
|
||||
elif location_name == "Mailbox - Letter from Orca" and "Forbidden Woods" in banned_dungeons:
|
||||
self.banned_locations.add(location_name)
|
||||
elif location_name == "Mailbox - Letter from Baito" and "Earth Temple" in banned_dungeons:
|
||||
self.banned_locations.add(location_name)
|
||||
elif location_name == "Mailbox - Letter from Aryll" and "Forsaken Fortress" in banned_dungeons:
|
||||
self.banned_locations.add(location_name)
|
||||
elif location_name == "Mailbox - Letter from Tingle" and "Forsaken Fortress" in banned_dungeons:
|
||||
self.banned_locations.add(location_name)
|
||||
for location_name in self.banned_locations:
|
||||
self.world.nonprogress_locations.add(location_name)
|
||||
|
||||
# Record the item location names for required bosses.
|
||||
self.required_boss_item_locations = []
|
||||
self.required_bosses = []
|
||||
self.banned_bosses = []
|
||||
possible_boss_item_locations = [loc for loc, data in LOCATION_TABLE.items() if TWWFlag.BOSS in data.flags]
|
||||
for location_name in possible_boss_item_locations:
|
||||
dungeon_name, specific_location_name = split_location_name_by_zone(location_name)
|
||||
assert specific_location_name.endswith(" Heart Container")
|
||||
boss_name = specific_location_name.removesuffix(" Heart Container")
|
||||
|
||||
if dungeon_name in required_dungeons:
|
||||
self.required_boss_item_locations.append(location_name)
|
||||
self.required_bosses.append(boss_name)
|
||||
else:
|
||||
self.banned_bosses.append(boss_name)
|
||||
self.required_dungeons = required_dungeons
|
||||
self.banned_dungeons = banned_dungeons
|
||||
1
worlds/tww/requirements.txt
Normal file
1
worlds/tww/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
dolphin-memory-engine>=1.3.0
|
||||
@@ -63,7 +63,7 @@ class WitnessWorld(World):
|
||||
item_name_groups = static_witness_items.ITEM_GROUPS
|
||||
location_name_groups = static_witness_locations.AREA_LOCATION_GROUPS
|
||||
|
||||
required_client_version = (0, 5, 1)
|
||||
required_client_version = (0, 6, 0)
|
||||
|
||||
player_logic: WitnessPlayerLogic
|
||||
player_locations: WitnessPlayerLocations
|
||||
|
||||
@@ -262,6 +262,10 @@ def is_easter_time() -> bool:
|
||||
# Thus, we just take a range from the earliest to latest possible easter dates.
|
||||
|
||||
today = date.today()
|
||||
|
||||
if today < date(2025, 3, 31): # Don't go live early if 0.6.0 RC3 happens, with a little leeway
|
||||
return False
|
||||
|
||||
earliest_easter_day = date(today.year, 3, 20) # Earliest possible is 3/22 + 2 day buffer for Good Friday
|
||||
last_easter_day = date(today.year, 4, 26) # Latest possible is 4/25 + 1 day buffer for Easter Monday
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
Pymem>=1.13.0
|
||||
Pymem>=1.13.0
|
||||
|
||||
Reference in New Issue
Block a user