Compare commits

...

18 Commits

Author SHA1 Message Date
PoryGone
a3666f2ae5 SA2B: Fix critical typo #4779 2025-03-30 22:19:24 +02:00
Kaito Sinclaire
c3e000e574 id Tech 1 games: Logic updates (Feb '25) (#4677)
- Across Doom 1993 and Doom 2, any items that are accessible in Ultra-Violence from the start of the level without putting the player in any danger are now considered in logic when that level is first received, without needing any weapons available. This is intended to give generation more possible outs for bad placements.
  - This affects the following maps in Doom 1993:
    - Toxin Refinery (E1M3): 1 location.
    - Command Control (E1M4): 1 location.
    - Computer Station (E1M7): 1 location.
    - Deimos Lab (E2M4): 1 location.
    - Tower of Babel (E2M8): 1 location.
    - Unholy Cathedral (E3M5): 1 location.
  - This affects the following maps in Doom 2:
    - The Waste Tunnels (MAP05): 2 locations.
    - Dead Simple (MAP07): 2 locations.
    - The Pit (MAP09): 1 location.
    - Refueling Base (MAP10): 1 location.
    - Nirvana (MAP21): 1 location, except see below.
    - Icon of Sin (MAP30): 9 locations.
    - Grosse (MAP32): 2 locations.
- Doom 2 has had some more significant logical adjustments made.
  - The following Pro tricks have been added to Pro logic:
    - Circle of Death (MAP11): Lowering the exit wall without the Red key by hitting the switch to do so from the nukage. This makes three items previously locked behind the Red key available early, as well as the exit.
    - Suburbs (MAP16): Reaching the exit without any keys, as the gap between the pillar and the wall is large enough to let you through if you position yourself well. While multiple other squeeze glides exist (for example, you can skip the Yellow key in MAP21 by using one), this one is significantly easier than the rest; it does not require much precision, nor does it require vertical mouse movement.
    - Nirvana (MAP21): Skipping the Blue key, as there is a gigantic gap between the bars that attempt to block you.
    - The Chasm (MAP24): Skipping the Blue key by going extremely far through the nukage and finding one of a couple specific teleporters is now considered a Pro trick, and standard logic now expects the key to be obtained.
  - The following levels have had other logic adjustments:
    - The Waste Tunnels (MAP05): Requirements lowered to Shotgun + Super Shotgun + (Chaingun | Plasma gun).
    - The Crusher (MAP06): Requirements lowered to Shotgun + (Chaingun | Plasma gun) for areas immediately accessible. Going beyond the Blue key door also requires Super Shotgun.
    - The Factory (MAP12): The outdoors area, and the little room to the right of where you start, are accessible in sphere 1. These three items are all easily obtainable with only the pistol. The remaining items that are not in the central area are accessible with (Super Shotgun | Plasma gun), while the items in that area are accessible with Super Shotgun + Chaingun + (Plasma gun | BFG9000). This fixes Episode 2 not having an available sphere 1, and allows solo Episode 2 games.
    - Nirvana (MAP21): As above, the item in the starting room is accessible in sphere 1. Every other item that doesn't require a key is accessible with (Super Shotgun | Plasma gun). The room in which you use the Yellow key is accessible with Super Shotgun + Chaingun + (Plasma gun | BFG9000). This fixes Episode 3 not having an available sphere 1, and allows solo Episode 3 games.
    - The Catacombs (MAP22): The four items in the opening room only require (Shotgun | Super Shotgun | Plasma gun). The rest of the level is as before.
    - Bloodfalls (MAP25): Requirements lowered to Shotgun + Super Shotgun + Chaingun, as this level is unusually easy for its placement in the game. Progressing past the Blue key door additionally requires (Rocket launcher | Plasma gun | BFG9000) solely to deal with the Arch-vile at the end of the level.
    - Wolfenstein (MAP31): Requirements lowered to Chaingun + (Shotgun | Super Shotgun). This is closer to what the game expects from a non-secret hunting player from a pistol start.
- The following logic bugs in Heretic have been fixed:
  - Quay (E5M3): An item in a Blue key locked hallway was previously marked as being in the "Main" region, thus considered to be accessible without that key. It has been moved to the appropriate "Blue" region.
  - Courtyard (E5M4): Logic previously assumed you could reach the Wings of Wrath from the opening room, when that isn't actually possible. Changing this moved some items previously in the "Main" region into a new "Green" region, and items previously in the "Kakis" (Yellow OR Green) are now in a "Yellow" region instead. Fixes #4662.
- For known problematic solo episodes, some additional special cases have been added.
  - Doom 1993, Episode 3: One of either the Shotgun or Chaingun is placed early. Slough of Despair (E3M2) is given as an additional starting level.
  - Doom 2, Episode 3: One of either the Super Shotgun or Plasma gun is placed early.
  - Heretic, Episode 1: The Docks (E1M1) - Yellow key is placed early.
- The following levels (and thus, their items and locations) were renamed, due to typos or other oddities:
  - `Barrels o Fun (MAP23)` -> `Barrels o' Fun (MAP23)`
  - `Wolfenstein2 (MAP31)` -> `Wolfenstein (MAP31)`
  - `Grosse2 (MAP32)` -> `Grosse (MAP32)`
  - `D'Sparil'S Keep (E3M8)` -> `D'Sparil's Keep (E3M8)`
  - `The Aquifier (E3M9)` -> `The Aquifer (E3M9)`
2025-03-29 17:32:33 +01:00
Justus Lind
dd5481930a Muse Dash: Update docs to recommend MelonLoader 0.7.0 rather than 0.6.1 (#4776)
* Tiny version update.

* Update wording because there is no longer a latest button
2025-03-29 01:35:35 +01:00
Scipio Wright
842328c661 TUNIC: Update swamp and atoll fuse logic with weaponry (#4760)
* Update swamp and atoll fuse logic with weaponry

* Add it to the swamp and cath rules too
2025-03-28 21:12:16 +01:00
PoryGone
8f75384e2e SA2B - v2.4 Logic Fixes (#4770)
* Logic tweaks

* Docs updates

* Delete extra file

* One more logic tweak

* Add missing logic change
2025-03-28 21:11:31 +01:00
Fabian Dill
193faa00ce Factorio: fix energylink type back to int (#4768) 2025-03-28 00:28:10 +01:00
Star Rauchenberger
5e5383b399 Lingo: Add painting display names (#4707)
* Lingo: Add painting display names

* Reordered some paintings

* Update generated.dat
2025-03-27 01:32:39 +01:00
threeandthreee
cb6b29dbe3 LADX: fix for unconnected entrances in other worlds #4771 2025-03-25 22:30:25 +01:00
Fabian Dill
82b0819051 Core: ensure requirements files end on newline (#4761) 2025-03-24 22:26:30 +01:00
Jérémie Bolduc
e12ab4afa4 Stardew Valley: Move test option presets to their own file (#4349) 2025-03-24 03:32:34 +01:00
Justus Lind
1416f631cc Core: Add a test that checks all registered patches matches the name of a registered world (#4633)
Co-authored-by: qwint <qwint.42@gmail.com>
2025-03-24 03:30:44 +01:00
Fabian Dill
dbaac47d1e Core: update various requirements (#4731) 2025-03-23 17:24:50 +01:00
Jonathan Tan
cf0ae5e31b The Wind Waker: Implement New Game (#4458)
Adds The Legend of Zelda: The Wind Waker as a supported game in Archipelago. The game uses [LagoLunatic's randomizer](https://github.com/LagoLunatic/wwrando) as its base (regarding logic, options, etc.) and builds from there.
2025-03-23 00:42:17 +01:00
BadMagic100
8891f07362 Core: Allow and require user-provided target name when splitting 1-way entrances for GER (#4746)
* [Core][GER] Allow and require user-provided target name when splitting 1-way entrances

* Move target naming onto a parameter of disconnect_entrance_for_randomization
2025-03-22 20:58:35 +01:00
NewSoupVi
d78974ec59 The Witness: Bump Required Client Version to 0.6.0 (#4763)
The beta client releases already report this.
2025-03-22 20:57:22 +01:00
NewSoupVi
32be26c4d7 The Witness: Make sure the 2025 April Fools feature does not go live with RC3 (#4758) 2025-03-22 20:52:18 +01:00
Jérémie Bolduc
9de49aa419 Stardew Valley: Move all the goal logic into its own file (#4383) 2025-03-22 20:29:16 +01:00
PoryGone
294a67a4b4 SA2B: v2.4 - Minigame Madness (#4663)
Changelog:

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
- 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`
2025-03-22 13:00:07 +01:00
96 changed files with 17303 additions and 3265 deletions

1
.gitignore vendored
View File

@@ -10,6 +10,7 @@
*.apmc
*.apz5
*.aptloz
*.aptww
*.apemerald
*.pyc
*.pyd

View File

@@ -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

View File

@@ -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

View File

@@ -214,6 +214,9 @@
# Wargroove
/worlds/wargroove/ @FlySniper
# The Wind Waker
/worlds/tww/ @tanjo3
# The Witness
/worlds/witness/ @NewSoupVi @blastron

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View 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.")

View 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")

View File

@@ -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

View File

@@ -1,2 +1,2 @@
maseya-z3pr>=1.0.0rc1
xxtea>=3.0.0
xxtea>=3.0.0

View File

@@ -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,

View File

@@ -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",

View File

@@ -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:

View File

@@ -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:

View File

@@ -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', },
}

View File

@@ -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',
},
}

View File

@@ -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)',
]

View File

@@ -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}]},
]

View File

@@ -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):

View File

@@ -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:

View File

@@ -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()

View File

@@ -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()

View File

@@ -514,19 +514,19 @@ item_table: Dict[int, ItemDict] = {
'map': 7},
370259: {'classification': ItemClassification.progression,
'count': 1,
'name': 'The Aquifier (E3M9) - Blue key',
'name': 'The Aquifer (E3M9) - Blue key',
'doom_type': 79,
'episode': 3,
'map': 9},
370260: {'classification': ItemClassification.progression,
'count': 1,
'name': 'The Aquifier (E3M9) - Green key',
'name': 'The Aquifer (E3M9) - Green key',
'doom_type': 73,
'episode': 3,
'map': 9},
370261: {'classification': ItemClassification.progression,
'count': 1,
'name': 'The Aquifier (E3M9) - Yellow key',
'name': 'The Aquifer (E3M9) - Yellow key',
'doom_type': 80,
'episode': 3,
'map': 9},
@@ -1234,37 +1234,37 @@ item_table: Dict[int, ItemDict] = {
'map': 7},
370475: {'classification': ItemClassification.progression,
'count': 1,
'name': "D'Sparil'S Keep (E3M8)",
'name': "D'Sparil's Keep (E3M8)",
'doom_type': -1,
'episode': 3,
'map': 8},
370476: {'classification': ItemClassification.progression,
'count': 1,
'name': "D'Sparil'S Keep (E3M8) - Complete",
'name': "D'Sparil's Keep (E3M8) - Complete",
'doom_type': -2,
'episode': 3,
'map': 8},
370477: {'classification': ItemClassification.filler,
'count': 1,
'name': "D'Sparil'S Keep (E3M8) - Map Scroll",
'name': "D'Sparil's Keep (E3M8) - Map Scroll",
'doom_type': 35,
'episode': 3,
'map': 8},
370478: {'classification': ItemClassification.progression,
'count': 1,
'name': 'The Aquifier (E3M9)',
'name': 'The Aquifer (E3M9)',
'doom_type': -1,
'episode': 3,
'map': 9},
370479: {'classification': ItemClassification.progression,
'count': 1,
'name': 'The Aquifier (E3M9) - Complete',
'name': 'The Aquifer (E3M9) - Complete',
'doom_type': -2,
'episode': 3,
'map': 9},
370480: {'classification': ItemClassification.filler,
'count': 1,
'name': 'The Aquifier (E3M9) - Map Scroll',
'name': 'The Aquifer (E3M9) - Map Scroll',
'doom_type': 35,
'episode': 3,
'map': 9},
@@ -1635,8 +1635,8 @@ item_name_groups: Dict[str, Set[str]] = {
'Ammos': {'Crystal Geode', 'Energy Orb', 'Greater Runes', 'Inferno Orb', 'Pile of Mace Spheres', 'Quiver of Ethereal Arrows', },
'Armors': {'Enchanted Shield', 'Silver Shield', },
'Artifacts': {'Chaos Device', 'Morph Ovum', 'Mystic Urn', 'Quartz Flask', 'Ring of Invincibility', 'Shadowsphere', 'Timebomb of the Ancients', 'Tome of Power', 'Torch', },
'Keys': {'Ambulatory (E4M3) - Blue key', 'Ambulatory (E4M3) - Green key', 'Ambulatory (E4M3) - Yellow key', 'Blockhouse (E4M2) - Blue key', 'Blockhouse (E4M2) - Green key', 'Blockhouse (E4M2) - Yellow key', 'Catafalque (E4M1) - Green key', 'Catafalque (E4M1) - Yellow key', 'Colonnade (E5M6) - Blue key', 'Colonnade (E5M6) - Green key', 'Colonnade (E5M6) - Yellow key', 'Courtyard (E5M4) - Blue key', 'Courtyard (E5M4) - Green key', 'Courtyard (E5M4) - Yellow key', 'Foetid Manse (E5M7) - Blue key', 'Foetid Manse (E5M7) - Green key', 'Foetid Manse (E5M7) - Yellow key', 'Great Stair (E4M5) - Blue key', 'Great Stair (E4M5) - Green key', 'Great Stair (E4M5) - Yellow key', 'Halls of the Apostate (E4M6) - Blue key', 'Halls of the Apostate (E4M6) - Green key', 'Halls of the Apostate (E4M6) - Yellow key', 'Hydratyr (E5M5) - Blue key', 'Hydratyr (E5M5) - Green key', 'Hydratyr (E5M5) - Yellow key', 'Mausoleum (E4M9) - Yellow key', 'Ochre Cliffs (E5M1) - Blue key', 'Ochre Cliffs (E5M1) - Green key', 'Ochre Cliffs (E5M1) - Yellow key', 'Quay (E5M3) - Blue key', 'Quay (E5M3) - Green key', 'Quay (E5M3) - Yellow key', 'Ramparts of Perdition (E4M7) - Blue key', 'Ramparts of Perdition (E4M7) - Green key', 'Ramparts of Perdition (E4M7) - Yellow key', 'Rapids (E5M2) - Green key', 'Rapids (E5M2) - Yellow key', 'Shattered Bridge (E4M8) - Yellow key', "Skein of D'Sparil (E5M9) - Blue key", "Skein of D'Sparil (E5M9) - Green key", "Skein of D'Sparil (E5M9) - Yellow key", 'The Aquifier (E3M9) - Blue key', 'The Aquifier (E3M9) - Green key', 'The Aquifier (E3M9) - Yellow key', 'The Azure Fortress (E3M4) - Green key', 'The Azure Fortress (E3M4) - Yellow key', 'The Catacombs (E2M5) - Blue key', 'The Catacombs (E2M5) - Green key', 'The Catacombs (E2M5) - Yellow key', 'The Cathedral (E1M6) - Green key', 'The Cathedral (E1M6) - Yellow key', 'The Cesspool (E3M2) - Blue key', 'The Cesspool (E3M2) - Green key', 'The Cesspool (E3M2) - Yellow key', 'The Chasm (E3M7) - Blue key', 'The Chasm (E3M7) - Green key', 'The Chasm (E3M7) - Yellow key', 'The Citadel (E1M5) - Blue key', 'The Citadel (E1M5) - Green key', 'The Citadel (E1M5) - Yellow key', 'The Confluence (E3M3) - Blue key', 'The Confluence (E3M3) - Green key', 'The Confluence (E3M3) - Yellow key', 'The Crater (E2M1) - Green key', 'The Crater (E2M1) - Yellow key', 'The Crypts (E1M7) - Blue key', 'The Crypts (E1M7) - Green key', 'The Crypts (E1M7) - Yellow key', 'The Docks (E1M1) - Yellow key', 'The Dungeons (E1M2) - Blue key', 'The Dungeons (E1M2) - Green key', 'The Dungeons (E1M2) - Yellow key', 'The Gatehouse (E1M3) - Green key', 'The Gatehouse (E1M3) - Yellow key', 'The Glacier (E2M9) - Blue key', 'The Glacier (E2M9) - Green key', 'The Glacier (E2M9) - Yellow key', 'The Graveyard (E1M9) - Blue key', 'The Graveyard (E1M9) - Green key', 'The Graveyard (E1M9) - Yellow key', 'The Great Hall (E2M7) - Blue key', 'The Great Hall (E2M7) - Green key', 'The Great Hall (E2M7) - Yellow key', 'The Guard Tower (E1M4) - Green key', 'The Guard Tower (E1M4) - Yellow key', 'The Halls of Fear (E3M6) - Blue key', 'The Halls of Fear (E3M6) - Green key', 'The Halls of Fear (E3M6) - Yellow key', 'The Ice Grotto (E2M4) - Blue key', 'The Ice Grotto (E2M4) - Green key', 'The Ice Grotto (E2M4) - Yellow key', 'The Labyrinth (E2M6) - Blue key', 'The Labyrinth (E2M6) - Green key', 'The Labyrinth (E2M6) - Yellow key', 'The Lava Pits (E2M2) - Green key', 'The Lava Pits (E2M2) - Yellow key', 'The Ophidian Lair (E3M5) - Green key', 'The Ophidian Lair (E3M5) - Yellow key', 'The River of Fire (E2M3) - Blue key', 'The River of Fire (E2M3) - Green key', 'The River of Fire (E2M3) - Yellow key', 'The Storehouse (E3M1) - Green key', 'The Storehouse (E3M1) - Yellow key', },
'Levels': {'Ambulatory (E4M3)', 'Blockhouse (E4M2)', 'Catafalque (E4M1)', 'Colonnade (E5M6)', 'Courtyard (E5M4)', "D'Sparil'S Keep (E3M8)", 'Field of Judgement (E5M8)', 'Foetid Manse (E5M7)', 'Great Stair (E4M5)', 'Halls of the Apostate (E4M6)', "Hell's Maw (E1M8)", 'Hydratyr (E5M5)', 'Mausoleum (E4M9)', 'Ochre Cliffs (E5M1)', 'Quay (E5M3)', 'Ramparts of Perdition (E4M7)', 'Rapids (E5M2)', 'Sepulcher (E4M4)', 'Shattered Bridge (E4M8)', "Skein of D'Sparil (E5M9)", 'The Aquifier (E3M9)', 'The Azure Fortress (E3M4)', 'The Catacombs (E2M5)', 'The Cathedral (E1M6)', 'The Cesspool (E3M2)', 'The Chasm (E3M7)', 'The Citadel (E1M5)', 'The Confluence (E3M3)', 'The Crater (E2M1)', 'The Crypts (E1M7)', 'The Docks (E1M1)', 'The Dungeons (E1M2)', 'The Gatehouse (E1M3)', 'The Glacier (E2M9)', 'The Graveyard (E1M9)', 'The Great Hall (E2M7)', 'The Guard Tower (E1M4)', 'The Halls of Fear (E3M6)', 'The Ice Grotto (E2M4)', 'The Labyrinth (E2M6)', 'The Lava Pits (E2M2)', 'The Ophidian Lair (E3M5)', 'The Portals of Chaos (E2M8)', 'The River of Fire (E2M3)', 'The Storehouse (E3M1)', },
'Map Scrolls': {'Ambulatory (E4M3) - Map Scroll', 'Blockhouse (E4M2) - Map Scroll', 'Catafalque (E4M1) - Map Scroll', 'Colonnade (E5M6) - Map Scroll', 'Courtyard (E5M4) - Map Scroll', "D'Sparil'S Keep (E3M8) - Map Scroll", 'Field of Judgement (E5M8) - Map Scroll', 'Foetid Manse (E5M7) - Map Scroll', 'Great Stair (E4M5) - Map Scroll', 'Halls of the Apostate (E4M6) - Map Scroll', "Hell's Maw (E1M8) - Map Scroll", 'Hydratyr (E5M5) - Map Scroll', 'Mausoleum (E4M9) - Map Scroll', 'Ochre Cliffs (E5M1) - Map Scroll', 'Quay (E5M3) - Map Scroll', 'Ramparts of Perdition (E4M7) - Map Scroll', 'Rapids (E5M2) - Map Scroll', 'Sepulcher (E4M4) - Map Scroll', 'Shattered Bridge (E4M8) - Map Scroll', "Skein of D'Sparil (E5M9) - Map Scroll", 'The Aquifier (E3M9) - Map Scroll', 'The Azure Fortress (E3M4) - Map Scroll', 'The Catacombs (E2M5) - Map Scroll', 'The Cathedral (E1M6) - Map Scroll', 'The Cesspool (E3M2) - Map Scroll', 'The Chasm (E3M7) - Map Scroll', 'The Citadel (E1M5) - Map Scroll', 'The Confluence (E3M3) - Map Scroll', 'The Crater (E2M1) - Map Scroll', 'The Crypts (E1M7) - Map Scroll', 'The Docks (E1M1) - Map Scroll', 'The Dungeons (E1M2) - Map Scroll', 'The Gatehouse (E1M3) - Map Scroll', 'The Glacier (E2M9) - Map Scroll', 'The Graveyard (E1M9) - Map Scroll', 'The Great Hall (E2M7) - Map Scroll', 'The Guard Tower (E1M4) - Map Scroll', 'The Halls of Fear (E3M6) - Map Scroll', 'The Ice Grotto (E2M4) - Map Scroll', 'The Labyrinth (E2M6) - Map Scroll', 'The Lava Pits (E2M2) - Map Scroll', 'The Ophidian Lair (E3M5) - Map Scroll', 'The Portals of Chaos (E2M8) - Map Scroll', 'The River of Fire (E2M3) - Map Scroll', 'The Storehouse (E3M1) - Map Scroll', },
'Keys': {'Ambulatory (E4M3) - Blue key', 'Ambulatory (E4M3) - Green key', 'Ambulatory (E4M3) - Yellow key', 'Blockhouse (E4M2) - Blue key', 'Blockhouse (E4M2) - Green key', 'Blockhouse (E4M2) - Yellow key', 'Catafalque (E4M1) - Green key', 'Catafalque (E4M1) - Yellow key', 'Colonnade (E5M6) - Blue key', 'Colonnade (E5M6) - Green key', 'Colonnade (E5M6) - Yellow key', 'Courtyard (E5M4) - Blue key', 'Courtyard (E5M4) - Green key', 'Courtyard (E5M4) - Yellow key', 'Foetid Manse (E5M7) - Blue key', 'Foetid Manse (E5M7) - Green key', 'Foetid Manse (E5M7) - Yellow key', 'Great Stair (E4M5) - Blue key', 'Great Stair (E4M5) - Green key', 'Great Stair (E4M5) - Yellow key', 'Halls of the Apostate (E4M6) - Blue key', 'Halls of the Apostate (E4M6) - Green key', 'Halls of the Apostate (E4M6) - Yellow key', 'Hydratyr (E5M5) - Blue key', 'Hydratyr (E5M5) - Green key', 'Hydratyr (E5M5) - Yellow key', 'Mausoleum (E4M9) - Yellow key', 'Ochre Cliffs (E5M1) - Blue key', 'Ochre Cliffs (E5M1) - Green key', 'Ochre Cliffs (E5M1) - Yellow key', 'Quay (E5M3) - Blue key', 'Quay (E5M3) - Green key', 'Quay (E5M3) - Yellow key', 'Ramparts of Perdition (E4M7) - Blue key', 'Ramparts of Perdition (E4M7) - Green key', 'Ramparts of Perdition (E4M7) - Yellow key', 'Rapids (E5M2) - Green key', 'Rapids (E5M2) - Yellow key', 'Shattered Bridge (E4M8) - Yellow key', "Skein of D'Sparil (E5M9) - Blue key", "Skein of D'Sparil (E5M9) - Green key", "Skein of D'Sparil (E5M9) - Yellow key", 'The Aquifer (E3M9) - Blue key', 'The Aquifer (E3M9) - Green key', 'The Aquifer (E3M9) - Yellow key', 'The Azure Fortress (E3M4) - Green key', 'The Azure Fortress (E3M4) - Yellow key', 'The Catacombs (E2M5) - Blue key', 'The Catacombs (E2M5) - Green key', 'The Catacombs (E2M5) - Yellow key', 'The Cathedral (E1M6) - Green key', 'The Cathedral (E1M6) - Yellow key', 'The Cesspool (E3M2) - Blue key', 'The Cesspool (E3M2) - Green key', 'The Cesspool (E3M2) - Yellow key', 'The Chasm (E3M7) - Blue key', 'The Chasm (E3M7) - Green key', 'The Chasm (E3M7) - Yellow key', 'The Citadel (E1M5) - Blue key', 'The Citadel (E1M5) - Green key', 'The Citadel (E1M5) - Yellow key', 'The Confluence (E3M3) - Blue key', 'The Confluence (E3M3) - Green key', 'The Confluence (E3M3) - Yellow key', 'The Crater (E2M1) - Green key', 'The Crater (E2M1) - Yellow key', 'The Crypts (E1M7) - Blue key', 'The Crypts (E1M7) - Green key', 'The Crypts (E1M7) - Yellow key', 'The Docks (E1M1) - Yellow key', 'The Dungeons (E1M2) - Blue key', 'The Dungeons (E1M2) - Green key', 'The Dungeons (E1M2) - Yellow key', 'The Gatehouse (E1M3) - Green key', 'The Gatehouse (E1M3) - Yellow key', 'The Glacier (E2M9) - Blue key', 'The Glacier (E2M9) - Green key', 'The Glacier (E2M9) - Yellow key', 'The Graveyard (E1M9) - Blue key', 'The Graveyard (E1M9) - Green key', 'The Graveyard (E1M9) - Yellow key', 'The Great Hall (E2M7) - Blue key', 'The Great Hall (E2M7) - Green key', 'The Great Hall (E2M7) - Yellow key', 'The Guard Tower (E1M4) - Green key', 'The Guard Tower (E1M4) - Yellow key', 'The Halls of Fear (E3M6) - Blue key', 'The Halls of Fear (E3M6) - Green key', 'The Halls of Fear (E3M6) - Yellow key', 'The Ice Grotto (E2M4) - Blue key', 'The Ice Grotto (E2M4) - Green key', 'The Ice Grotto (E2M4) - Yellow key', 'The Labyrinth (E2M6) - Blue key', 'The Labyrinth (E2M6) - Green key', 'The Labyrinth (E2M6) - Yellow key', 'The Lava Pits (E2M2) - Green key', 'The Lava Pits (E2M2) - Yellow key', 'The Ophidian Lair (E3M5) - Green key', 'The Ophidian Lair (E3M5) - Yellow key', 'The River of Fire (E2M3) - Blue key', 'The River of Fire (E2M3) - Green key', 'The River of Fire (E2M3) - Yellow key', 'The Storehouse (E3M1) - Green key', 'The Storehouse (E3M1) - Yellow key', },
'Levels': {'Ambulatory (E4M3)', 'Blockhouse (E4M2)', 'Catafalque (E4M1)', 'Colonnade (E5M6)', 'Courtyard (E5M4)', "D'Sparil's Keep (E3M8)", 'Field of Judgement (E5M8)', 'Foetid Manse (E5M7)', 'Great Stair (E4M5)', 'Halls of the Apostate (E4M6)', "Hell's Maw (E1M8)", 'Hydratyr (E5M5)', 'Mausoleum (E4M9)', 'Ochre Cliffs (E5M1)', 'Quay (E5M3)', 'Ramparts of Perdition (E4M7)', 'Rapids (E5M2)', 'Sepulcher (E4M4)', 'Shattered Bridge (E4M8)', "Skein of D'Sparil (E5M9)", 'The Aquifer (E3M9)', 'The Azure Fortress (E3M4)', 'The Catacombs (E2M5)', 'The Cathedral (E1M6)', 'The Cesspool (E3M2)', 'The Chasm (E3M7)', 'The Citadel (E1M5)', 'The Confluence (E3M3)', 'The Crater (E2M1)', 'The Crypts (E1M7)', 'The Docks (E1M1)', 'The Dungeons (E1M2)', 'The Gatehouse (E1M3)', 'The Glacier (E2M9)', 'The Graveyard (E1M9)', 'The Great Hall (E2M7)', 'The Guard Tower (E1M4)', 'The Halls of Fear (E3M6)', 'The Ice Grotto (E2M4)', 'The Labyrinth (E2M6)', 'The Lava Pits (E2M2)', 'The Ophidian Lair (E3M5)', 'The Portals of Chaos (E2M8)', 'The River of Fire (E2M3)', 'The Storehouse (E3M1)', },
'Map Scrolls': {'Ambulatory (E4M3) - Map Scroll', 'Blockhouse (E4M2) - Map Scroll', 'Catafalque (E4M1) - Map Scroll', 'Colonnade (E5M6) - Map Scroll', 'Courtyard (E5M4) - Map Scroll', "D'Sparil's Keep (E3M8) - Map Scroll", 'Field of Judgement (E5M8) - Map Scroll', 'Foetid Manse (E5M7) - Map Scroll', 'Great Stair (E4M5) - Map Scroll', 'Halls of the Apostate (E4M6) - Map Scroll', "Hell's Maw (E1M8) - Map Scroll", 'Hydratyr (E5M5) - Map Scroll', 'Mausoleum (E4M9) - Map Scroll', 'Ochre Cliffs (E5M1) - Map Scroll', 'Quay (E5M3) - Map Scroll', 'Ramparts of Perdition (E4M7) - Map Scroll', 'Rapids (E5M2) - Map Scroll', 'Sepulcher (E4M4) - Map Scroll', 'Shattered Bridge (E4M8) - Map Scroll', "Skein of D'Sparil (E5M9) - Map Scroll", 'The Aquifer (E3M9) - Map Scroll', 'The Azure Fortress (E3M4) - Map Scroll', 'The Catacombs (E2M5) - Map Scroll', 'The Cathedral (E1M6) - Map Scroll', 'The Cesspool (E3M2) - Map Scroll', 'The Chasm (E3M7) - Map Scroll', 'The Citadel (E1M5) - Map Scroll', 'The Confluence (E3M3) - Map Scroll', 'The Crater (E2M1) - Map Scroll', 'The Crypts (E1M7) - Map Scroll', 'The Docks (E1M1) - Map Scroll', 'The Dungeons (E1M2) - Map Scroll', 'The Gatehouse (E1M3) - Map Scroll', 'The Glacier (E2M9) - Map Scroll', 'The Graveyard (E1M9) - Map Scroll', 'The Great Hall (E2M7) - Map Scroll', 'The Guard Tower (E1M4) - Map Scroll', 'The Halls of Fear (E3M6) - Map Scroll', 'The Ice Grotto (E2M4) - Map Scroll', 'The Labyrinth (E2M6) - Map Scroll', 'The Lava Pits (E2M2) - Map Scroll', 'The Ophidian Lair (E3M5) - Map Scroll', 'The Portals of Chaos (E2M8) - Map Scroll', 'The River of Fire (E2M3) - Map Scroll', 'The Storehouse (E3M1) - Map Scroll', },
'Weapons': {'Dragon Claw', 'Ethereal Crossbow', 'Firemace', 'Gauntlets of the Necromancer', 'Hellstaff', 'Phoenix Rod', },
}

View File

@@ -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',

View File

@@ -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)',

View File

@@ -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",

View File

@@ -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)

View File

@@ -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:

View File

@@ -1 +1 @@
Pymem>=1.10.0
Pymem>=1.10.0

View File

@@ -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)

View File

@@ -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.

View File

@@ -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"]}"

View File

@@ -1 +1 @@
requests >= 2.28.1 # used by client
requests >= 2.28.1 # used by client

View File

@@ -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/`

View File

@@ -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/`

View File

@@ -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 = [

View File

@@ -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:

View File

@@ -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:

View File

@@ -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

View File

@@ -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")

View File

@@ -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

View File

@@ -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
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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))

View File

@@ -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))

View File

@@ -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)

View 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)

View File

@@ -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]

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -1 +1 @@
bsdiff4>=1.2.2
bsdiff4>=1.2.2

View File

@@ -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)

View File

@@ -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
View 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

File diff suppressed because it is too large Load Diff

1114
worlds/tww/Macros.py Normal file

File diff suppressed because it is too large Load Diff

854
worlds/tww/Options.py Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

739
worlds/tww/TWWClient.py Normal file
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

BIN
worlds/tww/assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View 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/).

View 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`.

View 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

View 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",
)

View 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

View 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])

View 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

View File

@@ -0,0 +1 @@
dolphin-memory-engine>=1.3.0

View File

@@ -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

View File

@@ -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

View File

@@ -1 +1 @@
Pymem>=1.13.0
Pymem>=1.13.0