diff --git a/Fill.py b/Fill.py
index d9919c1338..d8147b2eac 100644
--- a/Fill.py
+++ b/Fill.py
@@ -35,8 +35,8 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
"""
:param multiworld: Multiworld to be filled.
:param base_state: State assumed before fill.
- :param locations: Locations to be filled with item_pool
- :param item_pool: Items to fill into the locations
+ :param locations: Locations to be filled with item_pool, gets mutated by removing locations that get filled.
+ :param item_pool: Items to fill into the locations, gets mutated by removing items that get placed.
:param single_player_placement: if true, can speed up placement if everything belongs to a single player
:param lock: locations are set to locked as they are filled
:param swap: if true, swaps of already place items are done in the event of a dead end
@@ -220,7 +220,8 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
def remaining_fill(multiworld: MultiWorld,
locations: typing.List[Location],
itempool: typing.List[Item],
- name: str = "Remaining") -> None:
+ name: str = "Remaining",
+ move_unplaceable_to_start_inventory: bool = False) -> None:
unplaced_items: typing.List[Item] = []
placements: typing.List[Location] = []
swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter()
@@ -284,13 +285,21 @@ def remaining_fill(multiworld: MultiWorld,
if unplaced_items and locations:
# There are leftover unplaceable items and locations that won't accept them
- raise FillError(f"No more spots to place {len(unplaced_items)} items. Remaining locations are invalid.\n"
- f"Unplaced items:\n"
- f"{', '.join(str(item) for item in unplaced_items)}\n"
- f"Unfilled locations:\n"
- f"{', '.join(str(location) for location in locations)}\n"
- f"Already placed {len(placements)}:\n"
- f"{', '.join(str(place) for place in placements)}")
+ if move_unplaceable_to_start_inventory:
+ last_batch = []
+ for item in unplaced_items:
+ logging.debug(f"Moved {item} to start_inventory to prevent fill failure.")
+ multiworld.push_precollected(item)
+ last_batch.append(multiworld.worlds[item.player].create_filler())
+ remaining_fill(multiworld, locations, unplaced_items, name + " Start Inventory Retry")
+ else:
+ raise FillError(f"No more spots to place {len(unplaced_items)} items. Remaining locations are invalid.\n"
+ f"Unplaced items:\n"
+ f"{', '.join(str(item) for item in unplaced_items)}\n"
+ f"Unfilled locations:\n"
+ f"{', '.join(str(location) for location in locations)}\n"
+ f"Already placed {len(placements)}:\n"
+ f"{', '.join(str(place) for place in placements)}")
itempool.extend(unplaced_items)
@@ -420,7 +429,8 @@ def distribute_early_items(multiworld: MultiWorld,
return fill_locations, itempool
-def distribute_items_restrictive(multiworld: MultiWorld) -> None:
+def distribute_items_restrictive(multiworld: MultiWorld,
+ panic_method: typing.Literal["swap", "raise", "start_inventory"] = "swap") -> None:
fill_locations = sorted(multiworld.get_unfilled_locations())
multiworld.random.shuffle(fill_locations)
# get items to distribute
@@ -470,8 +480,29 @@ def distribute_items_restrictive(multiworld: MultiWorld) -> None:
if progitempool:
# "advancement/progression fill"
- fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, single_player_placement=multiworld.players == 1,
- name="Progression")
+ if panic_method == "swap":
+ fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool,
+ swap=True,
+ on_place=mark_for_locking, name="Progression", single_player_placement=multiworld.players == 1)
+ elif panic_method == "raise":
+ fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool,
+ swap=False,
+ on_place=mark_for_locking, name="Progression", single_player_placement=multiworld.players == 1)
+ elif panic_method == "start_inventory":
+ fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool,
+ swap=False, allow_partial=True,
+ on_place=mark_for_locking, name="Progression", single_player_placement=multiworld.players == 1)
+ if progitempool:
+ for item in progitempool:
+ logging.debug(f"Moved {item} to start_inventory to prevent fill failure.")
+ multiworld.push_precollected(item)
+ filleritempool.append(multiworld.worlds[item.player].create_filler())
+ logging.warning(f"{len(progitempool)} items moved to start inventory,"
+ f" due to failure in Progression fill step.")
+ progitempool[:] = []
+
+ else:
+ raise ValueError(f"Generator Panic Method {panic_method} not recognized.")
if progitempool:
raise FillError(
f"Not enough locations for progression items. "
@@ -486,7 +517,9 @@ def distribute_items_restrictive(multiworld: MultiWorld) -> None:
inaccessible_location_rules(multiworld, multiworld.state, defaultlocations)
- remaining_fill(multiworld, excludedlocations, filleritempool, "Remaining Excluded")
+ remaining_fill(multiworld, excludedlocations, filleritempool, "Remaining Excluded",
+ move_unplaceable_to_start_inventory=panic_method=="start_inventory")
+
if excludedlocations:
raise FillError(
f"Not enough filler items for excluded locations. "
@@ -495,7 +528,8 @@ def distribute_items_restrictive(multiworld: MultiWorld) -> None:
restitempool = filleritempool + usefulitempool
- remaining_fill(multiworld, defaultlocations, restitempool)
+ remaining_fill(multiworld, defaultlocations, restitempool,
+ move_unplaceable_to_start_inventory=panic_method=="start_inventory")
unplaced = restitempool
unfilled = defaultlocations
diff --git a/Main.py b/Main.py
index 1be91a8bb2..8b15a57a69 100644
--- a/Main.py
+++ b/Main.py
@@ -13,7 +13,7 @@ import worlds
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, Region
from Fill import balance_multiworld_progression, distribute_items_restrictive, distribute_planned, flood_items
from Options import StartInventoryPool
-from Utils import __version__, output_path, version_tuple
+from Utils import __version__, output_path, version_tuple, get_settings
from settings import get_settings
from worlds import AutoWorld
from worlds.generic.Rules import exclusion_rules, locality_rules
@@ -272,7 +272,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
if multiworld.algorithm == 'flood':
flood_items(multiworld) # different algo, biased towards early game progress items
elif multiworld.algorithm == 'balanced':
- distribute_items_restrictive(multiworld)
+ distribute_items_restrictive(multiworld, get_settings().generator.panic_method)
AutoWorld.call_all(multiworld, 'post_fill')
diff --git a/README.md b/README.md
index a944ba5f11..4633c99c66 100644
--- a/README.md
+++ b/README.md
@@ -67,6 +67,7 @@ Currently, the following games are supported:
* Yoshi's Island
* Mario & Luigi: Superstar Saga
* Bomb Rush Cyberfunk
+* Aquaria
* Yu-Gi-Oh! Ultimate Masters: World Championship Tournament 2006
* A Hat in Time
diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py
index bc9f74bace..16769b7a76 100644
--- a/WebHostLib/customserver.py
+++ b/WebHostLib/customserver.py
@@ -270,15 +270,19 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
await ctx.shutdown_task
except (KeyboardInterrupt, SystemExit):
- pass
- except Exception:
+ if ctx.saving:
+ ctx._save()
+ except Exception as e:
with db_session:
room = Room.get(id=room_id)
room.last_port = -1
+ logger.exception(e)
raise
+ else:
+ if ctx.saving:
+ ctx._save()
finally:
try:
- ctx._save()
with (db_session):
# ensure the Room does not spin up again on its own, minute of safety buffer
room = Room.get(id=room_id)
diff --git a/settings.py b/settings.py
index b463c5a047..9d1c0904dd 100644
--- a/settings.py
+++ b/settings.py
@@ -665,6 +665,14 @@ class GeneratorOptions(Group):
OFF = 0
ON = 1
+ class PanicMethod(str):
+ """
+ What to do if the current item placements appear unsolvable.
+ raise -> Raise an exception and abort.
+ swap -> Attempt to fix it by swapping prior placements around. (Default)
+ start_inventory -> Move remaining items to start_inventory, generate additional filler items to fill locations.
+ """
+
enemizer_path: EnemizerPath = EnemizerPath("EnemizerCLI/EnemizerCLI.Core") # + ".exe" is implied on Windows
player_files_path: PlayerFilesPath = PlayerFilesPath("Players")
players: Players = Players(0)
@@ -673,6 +681,7 @@ class GeneratorOptions(Group):
spoiler: Spoiler = Spoiler(3)
race: Race = Race(0)
plando_options: PlandoOptions = PlandoOptions("bosses, connections, texts")
+ panic_method: PanicMethod = PanicMethod("swap")
class SNIOptions(Group):
diff --git a/worlds/aquaria/Items.py b/worlds/aquaria/Items.py
index 7233484684..5494c87e8c 100644
--- a/worlds/aquaria/Items.py
+++ b/worlds/aquaria/Items.py
@@ -8,14 +8,16 @@ from typing import Optional
from enum import Enum
from BaseClasses import Item, ItemClassification
+
class ItemType(Enum):
"""
- Used to indicate to the multi-world if an item is usefull or not
+ Used to indicate to the multi-world if an item is useful or not
"""
NORMAL = 0
PROGRESSION = 1
JUNK = 2
+
class ItemGroup(Enum):
"""
Used to group items
@@ -28,6 +30,7 @@ class ItemGroup(Enum):
SONG = 5
TURTLE = 6
+
class AquariaItem(Item):
"""
A single item in the Aquaria game.
@@ -40,22 +43,23 @@ class AquariaItem(Item):
"""
Initialisation of the Item
:param name: The name of the item
- :param classification: If the item is usefull or not
+ :param classification: If the item is useful or not
:param code: The ID of the item (if None, it is an event)
:param player: The ID of the player in the multiworld
"""
super().__init__(name, classification, code, player)
+
class ItemData:
"""
Data of an item.
"""
- id:int
- count:int
- type:ItemType
- group:ItemGroup
+ id: int
+ count: int
+ type: ItemType
+ group: ItemGroup
- def __init__(self, id:int, count:int, type:ItemType, group:ItemGroup):
+ def __init__(self, id: int, count: int, type: ItemType, group: ItemGroup):
"""
Initialisation of the item data
@param id: The item ID
@@ -68,6 +72,7 @@ class ItemData:
self.type = type
self.group = group
+
"""Information data for every (not event) item."""
item_table = {
# name: ID, Nb, Item Type, Item Group
@@ -207,4 +212,3 @@ item_table = {
"Transturtle Simon says": ItemData(698132, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_forest05
"Transturtle Arnassi ruins": ItemData(698133, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_seahorse
}
-
diff --git a/worlds/aquaria/Locations.py b/worlds/aquaria/Locations.py
index 824b98a362..e4f6f104cc 100644
--- a/worlds/aquaria/Locations.py
+++ b/worlds/aquaria/Locations.py
@@ -43,7 +43,7 @@ class AquariaLocations:
locations_home_water = {
"Home water, bulb below the grouper fish": 698058,
- "Home water, bulb in the path bellow Nautilus Prime": 698059,
+ "Home water, bulb in the path below Nautilus Prime": 698059,
"Home water, bulb in the little room above the grouper fish": 698060,
"Home water, bulb in the end of the left path from the verse cave": 698061,
"Home water, bulb in the top left path": 698062,
@@ -129,7 +129,7 @@ class AquariaLocations:
locations_openwater_bl = {
"Open water bottom left area, bulb behind the chomper fish": 698011,
- "Open water bottom left area, bulb inside the downest fish pass": 698010,
+ "Open water bottom left area, bulb inside the lowest fish pass": 698010,
}
locations_skeleton_path = {
@@ -226,7 +226,7 @@ class AquariaLocations:
"Mithalas cathedral, third urn in the path behind the flesh vein": 698146,
"Mithalas cathedral, one of the urns in the top right room": 698147,
"Mithalas cathedral, Mithalan Dress": 698189,
- "Mithalas cathedral right area, urn bellow the left entrance": 698198,
+ "Mithalas cathedral right area, urn below the left entrance": 698198,
}
locations_cathedral_underground = {
@@ -457,7 +457,7 @@ class AquariaLocations:
locations_body_l = {
"The body left area, first bulb in the top face room": 698066,
"The body left area, second bulb in the top face room": 698069,
- "The body left area, bulb bellow the water stream": 698067,
+ "The body left area, bulb below the water stream": 698067,
"The body left area, bulb in the top path to the top face room": 698068,
"The body left area, bulb in the bottom face room": 698070,
}
diff --git a/worlds/aquaria/Options.py b/worlds/aquaria/Options.py
index 5c4936e44b..9a49e915b9 100644
--- a/worlds/aquaria/Options.py
+++ b/worlds/aquaria/Options.py
@@ -10,9 +10,8 @@ from Options import Toggle, Choice, Range, DeathLink, PerGameCommonOptions, Defa
class IngredientRandomizer(Choice):
"""
- Randomize Ingredients. Select if the simple ingredients (that does not have
- a recipe) should be randomized. If 'common_ingredients' is selected, the
- randomization will exclude the "Red Bulb", "Special Bulb" and "Rukh Egg".
+ Select if the simple ingredients (that do not have a recipe) should be randomized.
+ If "Common Ingredients" is selected, the randomization will exclude the "Red Bulb", "Special Bulb" and "Rukh Egg".
"""
display_name = "Randomize Ingredients"
option_off = 0
@@ -29,27 +28,25 @@ class DishRandomizer(Toggle):
class TurtleRandomizer(Choice):
"""Randomize the transportation turtle."""
display_name = "Turtle Randomizer"
- option_no_turtle_randomization = 0
- option_randomize_all_turtle = 1
- option_randomize_turtle_other_than_the_final_one = 2
+ option_none = 0
+ option_all = 1
+ option_all_except_final = 2
default = 2
class EarlyEnergyForm(DefaultOnToggle):
- """
- Force the Energy Form to be in a location before leaving the areas around the Home Water.
- """
+ """ Force the Energy Form to be in a location early in the game """
display_name = "Early Energy Form"
class AquarianTranslation(Toggle):
- """Translate to English the Aquarian scripture in the game."""
+ """Translate the Aquarian scripture in the game into English."""
display_name = "Translate Aquarian"
class BigBossesToBeat(Range):
"""
- A number of big bosses to beat before having access to the creator (the final boss). The big bosses are
+ The number of big bosses to beat before having access to the creator (the final boss). The big bosses are
"Fallen God", "Mithalan God", "Drunian God", "Sun God" and "The Golem".
"""
display_name = "Big bosses to beat"
@@ -60,12 +57,12 @@ class BigBossesToBeat(Range):
class MiniBossesToBeat(Range):
"""
- A number of Minibosses to beat before having access to the creator (the final boss). Mini bosses are
+ The number of minibosses to beat before having access to the creator (the final boss). The minibosses are
"Nautilus Prime", "Blaster Peg Prime", "Mergog", "Mithalan priests", "Octopus Prime", "Crabbius Maximus",
- "Mantis Shrimp Prime" and "King Jellyfish God Prime". Note that the Energy statue and Simon says are not
- mini bosses.
+ "Mantis Shrimp Prime" and "King Jellyfish God Prime".
+ Note that the Energy Statue and Simon Says are not minibosses.
"""
- display_name = "Mini bosses to beat"
+ display_name = "Minibosses to beat"
range_start = 0
range_end = 8
default = 0
@@ -73,47 +70,50 @@ class MiniBossesToBeat(Range):
class Objective(Choice):
"""
- The game objective can be only to kill the creator or to kill the creator
- and having obtained the three every secret memories
+ The game objective can be to kill the creator or to kill the creator after obtaining all three secret memories.
"""
display_name = "Objective"
option_kill_the_creator = 0
option_obtain_secrets_and_kill_the_creator = 1
default = 0
+
class SkipFirstVision(Toggle):
"""
- The first vision in the game; where Naija transform to Energy Form and get fload by enemy; is quite cool but
+ The first vision in the game, where Naija transforms into Energy Form and gets flooded by enemies, is quite cool but
can be quite long when you already know what is going on. This option can be used to skip this vision.
"""
- display_name = "Skip first Naija's vision"
+ display_name = "Skip Naija's first vision"
+
class NoProgressionHardOrHiddenLocation(Toggle):
"""
- Make sure that there is no progression items at hard to get or hard to find locations.
- Those locations that will be very High location (that need beast form, soup and skill to get), every
- location in the bubble cave, locations that need you to cross a false wall without any indication, Arnassi
- race, bosses and mini-bosses. Usefull for those that want a casual run.
+ Make sure that there are no progression items at hard-to-reach or hard-to-find locations.
+ Those locations are very High locations (that need beast form, soup and skill to get),
+ every location in the bubble cave, locations where need you to cross a false wall without any indication,
+ the Arnassi race, bosses and minibosses. Useful for those that want a more casual run.
"""
display_name = "No progression in hard or hidden locations"
+
class LightNeededToGetToDarkPlaces(DefaultOnToggle):
"""
- Make sure that the sun form or the dumbo pet can be aquired before getting to dark places. Be aware that navigating
- in dark place without light is extremely difficult.
+ Make sure that the sun form or the dumbo pet can be acquired before getting to dark places.
+ Be aware that navigating in dark places without light is extremely difficult.
"""
display_name = "Light needed to get to dark places"
+
class BindSongNeededToGetUnderRockBulb(Toggle):
"""
- Make sure that the bind song can be aquired before having to obtain sing bulb under rocks.
+ Make sure that the bind song can be acquired before having to obtain sing bulbs under rocks.
"""
display_name = "Bind song needed to get sing bulbs under rocks"
class UnconfineHomeWater(Choice):
"""
- Open the way out of Home water area so that Naija can go to open water and beyond without the bind song.
+ Open the way out of the Home water area so that Naija can go to open water and beyond without the bind song.
"""
display_name = "Unconfine Home Water Area"
option_off = 0
diff --git a/worlds/aquaria/Regions.py b/worlds/aquaria/Regions.py
index d16ef9f334..5956e0ca84 100755
--- a/worlds/aquaria/Regions.py
+++ b/worlds/aquaria/Regions.py
@@ -5,7 +5,7 @@ Description: Used to manage Regions in the Aquaria game multiworld randomizer
"""
from typing import Dict, Optional
-from BaseClasses import MultiWorld, Region, Entrance, ItemClassification, LocationProgressType, CollectionState
+from BaseClasses import MultiWorld, Region, Entrance, ItemClassification, CollectionState
from .Items import AquariaItem
from .Locations import AquariaLocations, AquariaLocation
from .Options import AquariaOptions
@@ -223,8 +223,6 @@ class AquariaRegions:
region.add_locations(locations, AquariaLocation)
return region
-
-
def __create_home_water_area(self) -> None:
"""
Create the `verse_cave`, `home_water` and `song_cave*` regions
@@ -941,7 +939,7 @@ class AquariaRegions:
"""
Add secrets events to the `world`
"""
- self.__add_event_location(self.first_secret, # Doit ajouter une région pour le "first secret"
+ self.__add_event_location(self.first_secret, # Doit ajouter une région pour le "first secret"
"First secret",
"First secret obtained")
self.__add_event_location(self.mithalas_city,
@@ -1095,12 +1093,10 @@ class AquariaRegions:
add_rule(self.multiworld.get_entrance("Veil left of sun temple to Sun temple left area", self.player),
lambda state: _has_light(state, self.player) or _has_sun_crystal(state, self.player))
-
-
def __adjusting_manual_rules(self) -> None:
add_rule(self.multiworld.get_location("Mithalas cathedral, Mithalan Dress", self.player),
lambda state: _has_beast_form(state, self.player))
- add_rule(self.multiworld.get_location("Open water bottom left area, bulb inside the downest fish pass", self.player),
+ add_rule(self.multiworld.get_location("Open water bottom left area, bulb inside the lowest fish pass", self.player),
lambda state: _has_fish_form(state, self.player))
add_rule(self.multiworld.get_location("Kelp forest bottom left area, Walker baby", self.player),
lambda state: _has_spirit_form(state, self.player))
@@ -1122,7 +1118,7 @@ class AquariaRegions:
self.player), lambda state: _has_energy_form(state, self.player))
add_rule(self.multiworld.get_location("Home water, bulb in the bottom left room", self.player),
lambda state: _has_bind_song(state, self.player))
- add_rule(self.multiworld.get_location("Home water, bulb in the path bellow Nautilus Prime", self.player),
+ add_rule(self.multiworld.get_location("Home water, bulb in the path below Nautilus Prime", self.player),
lambda state: _has_bind_song(state, self.player))
add_rule(self.multiworld.get_location("Naija's home, bulb after the energy door", self.player),
lambda state: _has_energy_form(state, self.player))
@@ -1133,9 +1129,6 @@ class AquariaRegions:
lambda state: _has_fish_form(state, self.player) and
_has_spirit_form(state, self.player))
-
-
-
def __no_progression_hard_or_hidden_location(self) -> None:
self.multiworld.get_location("Energy temple boss area, Fallen god tooth",
self.player).item_rule =\
@@ -1242,11 +1235,7 @@ class AquariaRegions:
add_rule(self.multiworld.get_entrance("Home Water to Open water top left area", self.player),
lambda state: _has_bind_song(state, self.player) and _has_energy_form(state, self.player))
if options.early_energy_form:
- add_rule(self.multiworld.get_entrance("Home Water to Home water transturtle room", self.player),
- lambda state: _has_energy_form(state, self.player))
- if options.early_energy_form:
- add_rule(self.multiworld.get_entrance("Home Water to Open water top left area", self.player),
- lambda state: _has_energy_form(state, self.player))
+ self.multiworld.early_items[self.player]["Energy form"] = 1
if options.no_progression_hard_or_hidden_locations:
self.__no_progression_hard_or_hidden_location()
diff --git a/worlds/aquaria/__init__.py b/worlds/aquaria/__init__.py
index e87e8c8b30..7c92d33a9c 100644
--- a/worlds/aquaria/__init__.py
+++ b/worlds/aquaria/__init__.py
@@ -5,7 +5,7 @@ Description: Main module for Aquaria game multiworld randomizer
"""
from typing import List, Dict, ClassVar, Any
-from ..AutoWorld import World, WebWorld
+from worlds.AutoWorld import World, WebWorld
from BaseClasses import Tutorial, MultiWorld, ItemClassification
from .Items import item_table, AquariaItem, ItemType, ItemGroup
from .Locations import location_table
@@ -114,7 +114,7 @@ class AquariaWorld(World):
def create_item(self, name: str) -> AquariaItem:
"""
- Create an AquariaItem using `name' as item name.
+ Create an AquariaItem using 'name' as item name.
"""
result: AquariaItem
try:
diff --git a/worlds/aquaria/docs/en_Aquaria.md b/worlds/aquaria/docs/en_Aquaria.md
index aa095b8356..c37f27568d 100644
--- a/worlds/aquaria/docs/en_Aquaria.md
+++ b/worlds/aquaria/docs/en_Aquaria.md
@@ -11,39 +11,39 @@ options page link: [Aquaria Player Options Page](../player-options).
## What does randomization do to this game?
The locations in the randomizer are:
-- All sing bulbs;
-- All Mithalas Urns;
-- All Sunken City crates;
-- Collectible treasure locations (including pet eggs and costumes);
-- Beating Simon says;
-- Li cave;
-- Every Transportation Turtle (also called transturtle);
-- Locations where you get songs,
- * Erulian spirit cristal,
- * Energy status mini-boss,
- * Beating Mithalan God boss,
- * Fish cave puzzle,
- * Beating Drunian God boss,
- * Beating Sun God boss,
- * Breaking Li cage in the body
+- All sing bulbs
+- All Mithalas Urns
+- All Sunken City crates
+- Collectible treasure locations (including pet eggs and costumes)
+- Beating Simon says
+- Li cave
+- Every Transportation Turtle (also called transturtle)
+- Locations where you get songs:
+ * Erulian spirit cristal
+ * Energy status mini-boss
+ * Beating Mithalan God boss
+ * Fish cave puzzle
+ * Beating Drunian God boss
+ * Beating Sun God boss
+ * Breaking Li cage in the body
Note that, unlike the vanilla game, when opening sing bulbs, Mithalas urns and Sunken City crates,
-nothing will come out of them. The moment those bulbs, urns and crates are opened, the location is considered received.
+nothing will come out of them. The moment those bulbs, urns and crates are opened, the location is considered checked.
The items in the randomizer are:
-- Dishes (used to learn recipes*);
-- Some ingredients;
-- The Wok (third plate used to cook 3 ingredients recipes everywhere);
-- All collectible treasure (including pet eggs and costumes);
-- Li and Li song;
-- All songs (other than Li's song since it is learned when Li is obtained);
-- Transportation to transturtles.
+- Dishes (used to learn recipes)*
+- Some ingredients
+- The Wok (third plate used to cook 3-ingredient recipes everywhere)
+- All collectible treasure (including pet eggs and costumes)
+- Li and Li's song
+- All songs (other than Li's song since it is learned when Li is obtained)
+- Transportation to transturtles
Also, there is the option to randomize every ingredient drops (from fishes, monsters
or plants).
-*Note that, unlike in the vanilla game, the recipes for dishes (other than the Sea Loaf)
-cannot be cooked (and learn) before being obtained as randomized items. Also, enemies and plants
+* Note that, unlike in the vanilla game, the recipes for dishes (other than the Sea Loaf)
+cannot be cooked (or learned) before being obtained as randomized items. Also, enemies and plants
that drop dishes that have not been learned before will drop ingredients of this dish instead.
## What is the goal of the game?
@@ -57,8 +57,8 @@ Any items specified above can be in another player's world.
No visuals are shown when finding locations other than collectible treasure.
For those treasures, the visual of the treasure is visually unchanged.
After collecting a location check, a message will be shown to inform the player
-what has been collected, and who will receive it.
+what has been collected and who will receive it.
## When the player receives an item, what happens?
When you receive an item, a message will pop up to inform you where you received
-the item from, and which one it is.
\ No newline at end of file
+the item from and which one it was.
\ No newline at end of file
diff --git a/worlds/aquaria/docs/setup_en.md b/worlds/aquaria/docs/setup_en.md
index 435761e3f8..34196757a3 100644
--- a/worlds/aquaria/docs/setup_en.md
+++ b/worlds/aquaria/docs/setup_en.md
@@ -2,9 +2,12 @@
## Required Software
-- The original Aquaria Game (buyable from a lot of online game seller);
+- The original Aquaria Game (purchasable from most online game stores)
- The [Aquaria randomizer](https://github.com/tioui/Aquaria_Randomizer/releases)
-- Optional, for sending [commands](/tutorial/Archipelago/commands/en) like `!hint`: the TextClient from [the most recent Archipelago release](https://github.com/ArchipelagoMW/Archipelago/releases)
+
+## Optional Software
+
+- For sending [commands](/tutorial/Archipelago/commands/en) like `!hint`: the TextClient from [the most recent Archipelago release](https://github.com/ArchipelagoMW/Archipelago/releases)
## Installation and execution Procedures
@@ -13,10 +16,9 @@
First, you should copy the original Aquaria folder game. The randomizer will possibly modify the game so that
the original game will stop working. Copying the folder will guarantee that the original game keeps on working.
Also, in Windows, the save files are stored in the Aquaria folder. So copying the Aquaria folder for every Multiworld
-game you play will make sure that every game has their own save game.
+game you play will make sure that every game has its own save game.
-Unzip the Aquaria randomizer release and copy all unzipped files in the Aquaria game folder. The unzipped files
-are those:
+Unzip the Aquaria randomizer release and copy all unzipped files in the Aquaria game folder. The unzipped files are:
- aquaria_randomizer.exe
- OpenAL32.dll
- override (directory)
@@ -25,11 +27,11 @@ are those:
- wrap_oal.dll
- cacert.pem
-If there is a conflict between file in the original game folder and the unzipped files, you should override
-the original files with the one of the unzipped randomizer.
+If there is a conflict between files in the original game folder and the unzipped files, you should overwrite
+the original files with the ones from the unzipped randomizer.
Finally, to launch the randomizer, you must use the command line interface (you can open the command line interface
-by writing `cmd` in the address bar of the Windows file explorer). Here is the command line to use to start the
+by typing `cmd` in the address bar of the Windows File Explorer). Here is the command line used to start the
randomizer:
```bash
@@ -44,8 +46,8 @@ aquaria_randomizer.exe --name YourName --server theServer:thePort --password th
### Linux when using the AppImage
-If you use the AppImage, just copy it in the Aquaria game folder. You then have to make it executable. You
-can do that from command line by using
+If you use the AppImage, just copy it into the Aquaria game folder. You then have to make it executable. You
+can do that from command line by using:
```bash
chmod +x Aquaria_Randomizer-*.AppImage
@@ -65,7 +67,7 @@ or, if the room has a password:
./Aquaria_Randomizer-*.AppImage --name YourName --server theServer:thePort --password thePassword
```
-Note that you should not have multiple Aquaria_Randomizer AppImage file in the same folder. If this situation occurred,
+Note that you should not have multiple Aquaria_Randomizer AppImage file in the same folder. If this situation occurs,
the preceding commands will launch the game multiple times.
### Linux when using the tar file
@@ -73,24 +75,23 @@ the preceding commands will launch the game multiple times.
First, you should copy the original Aquaria folder game. The randomizer will possibly modify the game so that
the original game will stop working. Copying the folder will guarantee that the original game keeps on working.
-Untar the Aquaria randomizer release and copy all extracted files in the Aquaria game folder. The extracted
-files are those:
+Untar the Aquaria randomizer release and copy all extracted files in the Aquaria game folder. The extracted files are:
- aquaria_randomizer
- override (directory)
- usersettings.xml
- cacert.pem
-If there is a conflict between file in the original game folder and the extracted files, you should override
-the original files with the one of the extracted randomizer files.
+If there is a conflict between files in the original game folder and the extracted files, you should overwrite
+the original files with the ones from the extracted randomizer files.
-Then, you should use your system package manager to install liblua5, libogg, libvorbis, libopenal and libsdl2.
+Then, you should use your system package manager to install `liblua5`, `libogg`, `libvorbis`, `libopenal` and `libsdl2`.
On Debian base system (like Ubuntu), you can use the following command:
```bash
sudo apt install liblua5.1-0-dev libogg-dev libvorbis-dev libopenal-dev libsdl2-dev
```
-Also, if there is some `.so` files in the Aquaria original game folder (`libgcc_s.so.1`, `libopenal.so.1`,
+Also, if there are certain `.so` files in the original Aquaria game folder (`libgcc_s.so.1`, `libopenal.so.1`,
`libSDL-1.2.so.0` and `libstdc++.so.6`), you should remove them from the Aquaria Randomizer game folder. Those are
old libraries that will not work on the recent build of the randomizer.
@@ -106,7 +107,7 @@ or, if the room has a password:
./aquaria_randomizer --name YourName --server theServer:thePort --password thePassword
```
-Note: If you have a permission denied error when using the command line, you can use this command line to be
+Note: If you get a permission denied error when using the command line, you can use this command to be
sure that your executable has executable permission:
```bash
diff --git a/worlds/aquaria/test/__init__.py b/worlds/aquaria/test/__init__.py
index 75dfd73802..ba42ac6d2c 100644
--- a/worlds/aquaria/test/__init__.py
+++ b/worlds/aquaria/test/__init__.py
@@ -25,7 +25,7 @@ after_home_water_locations = [
"Open water top right area, bulb in the turtle room",
"Open water top right area, Transturtle",
"Open water bottom left area, bulb behind the chomper fish",
- "Open water bottom left area, bulb inside the downest fish pass",
+ "Open water bottom left area, bulb inside the lowest fish pass",
"Open water skeleton path, bulb close to the right exit",
"Open water skeleton path, bulb behind the chomper fish",
"Open water skeleton path, King skull",
@@ -82,7 +82,7 @@ after_home_water_locations = [
"Mithalas cathedral, third urn in the path behind the flesh vein",
"Mithalas cathedral, one of the urns in the top right room",
"Mithalas cathedral, Mithalan Dress",
- "Mithalas cathedral right area, urn bellow the left entrance",
+ "Mithalas cathedral right area, urn below the left entrance",
"Cathedral underground, bulb in the center part",
"Cathedral underground, first bulb in the top left part",
"Cathedral underground, second bulb in the top left part",
@@ -178,7 +178,7 @@ after_home_water_locations = [
"The body main area, bulb on the main path blocking tube",
"The body left area, first bulb in the top face room",
"The body left area, second bulb in the top face room",
- "The body left area, bulb bellow the water stream",
+ "The body left area, bulb below the water stream",
"The body left area, bulb in the top path to the top face room",
"The body left area, bulb in the bottom face room",
"The body right area, bulb in the top face room",
diff --git a/worlds/aquaria/test/test_bind_song_access.py b/worlds/aquaria/test/test_bind_song_access.py
index b3a5c95c4d..b137d48ca9 100644
--- a/worlds/aquaria/test/test_bind_song_access.py
+++ b/worlds/aquaria/test/test_bind_song_access.py
@@ -18,7 +18,7 @@ class BindSongAccessTest(AquariaTestBase):
"""Test locations that require Bind song"""
locations = [
"Verse cave right area, Big Seed",
- "Home water, bulb in the path bellow Nautilus Prime",
+ "Home water, bulb in the path below Nautilus Prime",
"Home water, bulb in the bottom left room",
"Home water, Nautilus Egg",
"Song cave, Verse egg",
diff --git a/worlds/aquaria/test/test_bind_song_option_access.py b/worlds/aquaria/test/test_bind_song_option_access.py
index 9405b83e8e..522a064b62 100644
--- a/worlds/aquaria/test/test_bind_song_option_access.py
+++ b/worlds/aquaria/test/test_bind_song_option_access.py
@@ -24,7 +24,7 @@ class BindSongOptionAccessTest(AquariaTestBase):
"Song cave, bulb under the rock close to the song door",
"Song cave, bulb under the rock in the path to the singing statues",
"Naija's home, bulb under the rock at the right of the main path",
- "Home water, bulb in the path bellow Nautilus Prime",
+ "Home water, bulb in the path below Nautilus Prime",
"Home water, bulb in the bottom left room",
"Home water, Nautilus Egg",
"Song cave, Verse egg",
diff --git a/worlds/aquaria/test/test_energy_form_access.py b/worlds/aquaria/test/test_energy_form_access.py
index 17fb8d3b45..edfe8a3f6c 100644
--- a/worlds/aquaria/test/test_energy_form_access.py
+++ b/worlds/aquaria/test/test_energy_form_access.py
@@ -39,7 +39,7 @@ class EnergyFormAccessTest(AquariaTestBase):
"Mithalas cathedral, third urn in the path behind the flesh vein",
"Mithalas cathedral, one of the urns in the top right room",
"Mithalas cathedral, Mithalan Dress",
- "Mithalas cathedral right area, urn bellow the left entrance",
+ "Mithalas cathedral right area, urn below the left entrance",
"Cathedral boss area, beating Mithalan God",
"Kelp Forest top left area, bulb close to the Verse egg",
"Kelp forest top left area, Verse egg",
@@ -67,7 +67,6 @@ class EnergyFormAccessTest(AquariaTestBase):
"First secret",
"Sunken City cleared",
"Objective complete",
-
]
items = [["Energy form"]]
self.assertAccessDependency(locations, items)
\ No newline at end of file
diff --git a/worlds/aquaria/test/test_energy_form_access_option.py b/worlds/aquaria/test/test_energy_form_access_option.py
deleted file mode 100644
index 4dcbce6770..0000000000
--- a/worlds/aquaria/test/test_energy_form_access_option.py
+++ /dev/null
@@ -1,31 +0,0 @@
-"""
-Author: Louis M
-Date: Thu, 18 Apr 2024 18:45:56 +0000
-Description: Unit test used to test accessibility of locations with and without the bind song (with the early
- energy form option)
-"""
-
-from worlds.aquaria.test import AquariaTestBase, after_home_water_locations
-
-
-class EnergyFormAccessTest(AquariaTestBase):
- """Unit test used to test accessibility of locations with and without the energy form"""
- options = {
- "early_energy_form": True,
- }
-
- def test_energy_form_location(self) -> None:
- """Test locations that require Energy form with early energy song enable"""
- locations = [
- "Home water, Nautilus Egg",
- "Naija's home, bulb after the energy door",
- "Energy temple first area, bulb in the bottom room blocked by a rock",
- "Energy temple second area, bulb under the rock",
- "Energy temple bottom entrance, Krotite armor",
- "Energy temple third area, bulb in the bottom path",
- "Energy temple boss area, Fallen god tooth",
- "Energy temple blaster room, Blaster egg",
- *after_home_water_locations
- ]
- items = [["Energy form"]]
- self.assertAccessDependency(locations, items)
\ No newline at end of file
diff --git a/worlds/aquaria/test/test_fish_form_access.py b/worlds/aquaria/test/test_fish_form_access.py
index e6c24cf03f..3077237172 100644
--- a/worlds/aquaria/test/test_fish_form_access.py
+++ b/worlds/aquaria/test/test_fish_form_access.py
@@ -21,7 +21,7 @@ class FishFormAccessTest(AquariaTestBase):
"Mithalas city, urn inside a home fish pass",
"Kelp Forest top right area, bulb in the top fish pass",
"The veil bottom area, Verse egg",
- "Open water bottom left area, bulb inside the downest fish pass",
+ "Open water bottom left area, bulb inside the lowest fish pass",
"Kelp Forest top left area, bulb close to the Verse egg",
"Kelp forest top left area, Verse egg",
"Mermog cave, bulb in the left part of the cave",
diff --git a/worlds/aquaria/test/test_li_song_access.py b/worlds/aquaria/test/test_li_song_access.py
index 74f385ab78..e26d5b5fcd 100644
--- a/worlds/aquaria/test/test_li_song_access.py
+++ b/worlds/aquaria/test/test_li_song_access.py
@@ -27,7 +27,7 @@ class LiAccessTest(AquariaTestBase):
"The body main area, bulb on the main path blocking tube",
"The body left area, first bulb in the top face room",
"The body left area, second bulb in the top face room",
- "The body left area, bulb bellow the water stream",
+ "The body left area, bulb below the water stream",
"The body left area, bulb in the top path to the top face room",
"The body left area, bulb in the bottom face room",
"The body right area, bulb in the top face room",
diff --git a/worlds/aquaria/test/test_nature_form_access.py b/worlds/aquaria/test/test_nature_form_access.py
index 07d4377b33..89e7ceecbb 100644
--- a/worlds/aquaria/test/test_nature_form_access.py
+++ b/worlds/aquaria/test/test_nature_form_access.py
@@ -41,7 +41,7 @@ class NatureFormAccessTest(AquariaTestBase):
"The body main area, bulb on the main path blocking tube",
"The body left area, first bulb in the top face room",
"The body left area, second bulb in the top face room",
- "The body left area, bulb bellow the water stream",
+ "The body left area, bulb below the water stream",
"The body left area, bulb in the top path to the top face room",
"The body left area, bulb in the bottom face room",
"The body right area, bulb in the top face room",
diff --git a/worlds/bomb_rush_cyberfunk/Options.py b/worlds/bomb_rush_cyberfunk/Options.py
index 87fc2ca99c..80831d0645 100644
--- a/worlds/bomb_rush_cyberfunk/Options.py
+++ b/worlds/bomb_rush_cyberfunk/Options.py
@@ -9,7 +9,9 @@ else:
class Logic(Choice):
- """Choose the logic used by the randomizer."""
+ """
+ Choose the logic used by the randomizer.
+ """
display_name = "Logic"
option_glitchless = 0
option_glitched = 1
@@ -17,26 +19,38 @@ class Logic(Choice):
class SkipIntro(DefaultOnToggle):
- """Skips escaping the police station.
- Graffiti spots tagged during the intro will not unlock items."""
+ """
+ Skips escaping the police station.
+
+ Graffiti spots tagged during the intro will not unlock items.
+ """
display_name = "Skip Intro"
class SkipDreams(Toggle):
- """Skips the dream sequences at the end of each chapter.
- This can be changed later in the options menu inside the Archipelago phone app."""
+ """
+ Skips the dream sequences at the end of each chapter.
+
+ This can be changed later in the options menu inside the Archipelago phone app.
+ """
display_name = "Skip Dreams"
class SkipHands(Toggle):
- """Skips spraying the lion statue hands after the dream in Chapter 5."""
+ """
+ Skips spraying the lion statue hands after the dream in Chapter 5.
+ """
display_name = "Skip Statue Hands"
class TotalRep(Range):
- """Change the total amount of REP in your world.
+ """
+ Change the total amount of REP in your world.
+
At least 960 REP is needed to finish the game.
- Will be rounded to the nearest number divisible by 8."""
+
+ Will be rounded to the nearest number divisible by 8.
+ """
display_name = "Total REP"
range_start = 1000
range_end = 2000
@@ -74,12 +88,16 @@ class TotalRep(Range):
class EndingREP(Toggle):
- """Changes the final boss to require 1000 REP instead of 960 REP to start."""
+ """
+ Changes the final boss to require 1000 REP instead of 960 REP to start.
+ """
display_name = "Extra REP Required"
class StartStyle(Choice):
- """Choose which movestyle to start with."""
+ """
+ Choose which movestyle to start with.
+ """
display_name = "Starting Movestyle"
option_skateboard = 2
option_inline_skates = 3
@@ -88,17 +106,22 @@ class StartStyle(Choice):
class LimitedGraffiti(Toggle):
- """Each graffiti design can only be used a limited number of times before being removed from your inventory.
- In some cases, such as completing a dream, using graffiti to defeat enemies, or spraying over your own graffiti,
- uses will not be counted.
- If enabled, doing graffiti is disabled during crew battles, to prevent softlocking."""
+ """
+ Each graffiti design can only be used a limited number of times before being removed from your inventory.
+
+ In some cases, such as completing a dream, using graffiti to defeat enemies, or spraying over your own graffiti, uses will not be counted.
+
+ If enabled, doing graffiti is disabled during crew battles, to prevent softlocking.
+ """
display_name = "Limited Graffiti"
class SGraffiti(Choice):
- """Choose if small graffiti should be separate, meaning that you will need to switch characters every time you run
- out, or combined, meaning that unlocking new characters will add 5 uses that any character can use.
- Has no effect if Limited Graffiti is disabled."""
+ """
+ Choose if small graffiti should be separate, meaning that you will need to switch characters every time you run out, or combined, meaning that unlocking new characters will add 5 uses that any character can use.
+
+ Has no effect if Limited Graffiti is disabled.
+ """
display_name = "Small Graffiti Uses"
option_separate = 0
option_combined = 1
@@ -106,19 +129,27 @@ class SGraffiti(Choice):
class JunkPhotos(Toggle):
- """Skip taking pictures of Polo for items."""
+ """
+ Skip taking pictures of Polo for items.
+ """
display_name = "Skip Polo Photos"
class DontSavePhotos(Toggle):
- """Photos taken with the Camera app will not be saved.
- This can be changed later in the options menu inside the Archipelago phone app."""
+ """
+ Photos taken with the Camera app will not be saved.
+
+ This can be changed later in the options menu inside the Archipelago phone app.
+ """
display_name = "Don't Save Photos"
class ScoreDifficulty(Choice):
- """Alters the score required to win score challenges and crew battles.
- This can be changed later in the options menu inside the Archipelago phone app."""
+ """
+ Alters the score required to win score challenges and crew battles.
+
+ This can be changed later in the options menu inside the Archipelago phone app.
+ """
display_name = "Score Difficulty"
option_normal = 0
option_medium = 1
@@ -129,10 +160,14 @@ class ScoreDifficulty(Choice):
class DamageMultiplier(Range):
- """Multiplies all damage received.
+ """
+ Multiplies all damage received.
+
At 3x, most damage will OHKO the player, including falling into pits.
At 6x, all damage will OHKO the player.
- This can be changed later in the options menu inside the Archipelago phone app."""
+
+ This can be changed later in the options menu inside the Archipelago phone app.
+ """
display_name = "Damage Multiplier"
range_start = 1
range_end = 6
@@ -140,8 +175,11 @@ class DamageMultiplier(Range):
class BRCDeathLink(DeathLink):
- """When you die, everyone dies. The reverse is also true.
- This can be changed later in the options menu inside the Archipelago phone app."""
+ """
+ When you die, everyone dies. The reverse is also true.
+
+ This can be changed later in the options menu inside the Archipelago phone app.
+ """
@dataclass
diff --git a/worlds/celeste64/Options.py b/worlds/celeste64/Options.py
index a2d142b8a5..9a67e7d7d4 100644
--- a/worlds/celeste64/Options.py
+++ b/worlds/celeste64/Options.py
@@ -1,6 +1,6 @@
from dataclasses import dataclass
-from Options import Choice, Range, Toggle, DeathLink, PerGameCommonOptions
+from Options import Choice, Range, Toggle, DeathLink, OptionGroup, PerGameCommonOptions
class DeathLinkAmnesty(Range):
@@ -47,7 +47,9 @@ class MoveShuffle(Toggle):
- Air Dash
- Skid Jump
- Climb
+
NOTE: Having Move Shuffle and Standard Logic Difficulty will guarantee that one of the four Move items will be immediately accessible
+
WARNING: Combining Move Shuffle and Hard Logic Difficulty can require very difficult tricks
"""
display_name = "Move Shuffle"
@@ -75,7 +77,9 @@ class Carsanity(Toggle):
class BadelineChaserSource(Choice):
"""
What type of action causes more Badeline Chasers to start spawning
+
Locations: The number of locations you've checked contributes to Badeline Chasers
+
Strawberries: The number of Strawberry items you've received contributes to Badeline Chasers
"""
display_name = "Badeline Chaser Source"
@@ -86,7 +90,9 @@ class BadelineChaserSource(Choice):
class BadelineChaserFrequency(Range):
"""
How many of the `Badeline Chaser Source` actions must occur to make each Badeline Chaser start spawning
+
NOTE: Choosing `0` disables Badeline Chasers entirely
+
WARNING: Turning on Badeline Chasers alongside Move Shuffle could result in extremely difficult situations
"""
display_name = "Badeline Chaser Frequency"
@@ -104,6 +110,24 @@ class BadelineChaserSpeed(Range):
default = 3
+celeste_64_option_groups = [
+ OptionGroup("Goal Options", [
+ TotalStrawberries,
+ StrawberriesRequiredPercentage,
+ ]),
+ OptionGroup("Sanity Options", [
+ Friendsanity,
+ Signsanity,
+ Carsanity,
+ ]),
+ OptionGroup("Badeline Chasers", [
+ BadelineChaserSource,
+ BadelineChaserFrequency,
+ BadelineChaserSpeed,
+ ]),
+]
+
+
@dataclass
class Celeste64Options(PerGameCommonOptions):
death_link: DeathLink
diff --git a/worlds/celeste64/__init__.py b/worlds/celeste64/__init__.py
index d7e074623b..7786e38123 100644
--- a/worlds/celeste64/__init__.py
+++ b/worlds/celeste64/__init__.py
@@ -7,7 +7,7 @@ from .Items import Celeste64Item, unlockable_item_data_table, move_item_data_tab
from .Locations import Celeste64Location, strawberry_location_data_table, friend_location_data_table,\
sign_location_data_table, car_location_data_table, location_table
from .Names import ItemName, LocationName
-from .Options import Celeste64Options
+from .Options import Celeste64Options, celeste_64_option_groups
class Celeste64WebWorld(WebWorld):
@@ -24,6 +24,8 @@ class Celeste64WebWorld(WebWorld):
tutorials = [setup_en]
+ option_groups = celeste_64_option_groups
+
class Celeste64World(World):
"""Relive the magic of Celeste Mountain alongside Madeline in this small, heartfelt 3D platformer.
diff --git a/worlds/dkc3/Options.py b/worlds/dkc3/Options.py
index 06be30cf15..b114a503b9 100644
--- a/worlds/dkc3/Options.py
+++ b/worlds/dkc3/Options.py
@@ -1,13 +1,15 @@
from dataclasses import dataclass
import typing
-from Options import Choice, Range, Option, Toggle, DeathLink, DefaultOnToggle, OptionList, PerGameCommonOptions
+from Options import Choice, Range, Toggle, DeathLink, DefaultOnToggle, OptionGroup, PerGameCommonOptions
class Goal(Choice):
"""
Determines the goal of the seed
+
Knautilus: Scuttle the Knautilus in Krematoa and defeat Baron K. Roolenstein
+
Banana Bird Hunt: Find a certain number of Banana Birds and rescue their mother
"""
display_name = "Goal"
@@ -26,6 +28,7 @@ class IncludeTradeSequence(Toggle):
class DKCoinsForGyrocopter(Range):
"""
How many DK Coins are needed to unlock the Gyrocopter
+
Note: Achieving this number before unlocking the Turbo Ski will cause the game to grant you a
one-time upgrade to the next non-unlocked boat, until you return to Funky. Logic does not assume
that you will use this.
@@ -93,6 +96,7 @@ class LevelShuffle(Toggle):
class Difficulty(Choice):
"""
Which Difficulty Level to use
+
NORML: The Normal Difficulty
HARDR: Many DK Barrels are removed
TUFST: Most DK Barrels and all Midway Barrels are removed
@@ -159,19 +163,40 @@ class StartingLifeCount(Range):
default = 5
+dkc3_option_groups = [
+ OptionGroup("Goal Options", [
+ Goal,
+ KrematoaBonusCoinCost,
+ PercentageOfExtraBonusCoins,
+ NumberOfBananaBirds,
+ PercentageOfBananaBirds,
+ ]),
+ OptionGroup("Aesthetics", [
+ Autosave,
+ MERRY,
+ MusicShuffle,
+ KongPaletteSwap,
+ StartingLifeCount,
+ ]),
+]
+
+
@dataclass
class DKC3Options(PerGameCommonOptions):
#death_link: DeathLink # Disabled
- goal: Goal
#include_trade_sequence: IncludeTradeSequence # Disabled
- dk_coins_for_gyrocopter: DKCoinsForGyrocopter
+
+ goal: Goal
krematoa_bonus_coin_cost: KrematoaBonusCoinCost
percentage_of_extra_bonus_coins: PercentageOfExtraBonusCoins
number_of_banana_birds: NumberOfBananaBirds
percentage_of_banana_birds: PercentageOfBananaBirds
+
+ dk_coins_for_gyrocopter: DKCoinsForGyrocopter
kongsanity: KONGsanity
level_shuffle: LevelShuffle
difficulty: Difficulty
+
autosave: Autosave
merry: MERRY
music_shuffle: MusicShuffle
diff --git a/worlds/dkc3/__init__.py b/worlds/dkc3/__init__.py
index b0e153dcd2..f298114905 100644
--- a/worlds/dkc3/__init__.py
+++ b/worlds/dkc3/__init__.py
@@ -4,20 +4,21 @@ import typing
import math
import threading
-import settings
from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification
from Options import PerGameCommonOptions
-from .Items import DKC3Item, ItemData, item_table, inventory_table, junk_table
-from .Locations import DKC3Location, all_locations, setup_locations
-from .Options import DKC3Options
-from .Regions import create_regions, connect_regions
-from .Levels import level_list
-from .Rules import set_rules
-from .Names import ItemName, LocationName
-from .Client import DKC3SNIClient
-from worlds.AutoWorld import WebWorld, World
-from .Rom import LocalRom, patch_rom, get_base_rom_path, DKC3DeltaPatch
import Patch
+import settings
+from worlds.AutoWorld import WebWorld, World
+
+from .Client import DKC3SNIClient
+from .Items import DKC3Item, ItemData, item_table, inventory_table, junk_table
+from .Levels import level_list
+from .Locations import DKC3Location, all_locations, setup_locations
+from .Names import ItemName, LocationName
+from .Options import DKC3Options, dkc3_option_groups
+from .Regions import create_regions, connect_regions
+from .Rom import LocalRom, patch_rom, get_base_rom_path, DKC3DeltaPatch
+from .Rules import set_rules
class DK3Settings(settings.Group):
@@ -41,9 +42,11 @@ class DKC3Web(WebWorld):
"setup/en",
["PoryGone"]
)
-
+
tutorials = [setup_en]
+ option_groups = dkc3_option_groups
+
class DKC3World(World):
"""
diff --git a/worlds/hylics2/Options.py b/worlds/hylics2/Options.py
index 0c50fb42be..db9c316a7b 100644
--- a/worlds/hylics2/Options.py
+++ b/worlds/hylics2/Options.py
@@ -1,25 +1,38 @@
from dataclasses import dataclass
from Options import Choice, Removed, Toggle, DefaultOnToggle, DeathLink, PerGameCommonOptions
+
class PartyShuffle(Toggle):
- """Shuffles party members into the pool.
- Note that enabling this can potentially increase both the difficulty and length of a run."""
+ """
+ Shuffles party members into the item pool.
+
+ Note that enabling this can significantly increase both the difficulty and length of a run.
+ """
display_name = "Shuffle Party Members"
+
class GestureShuffle(Choice):
- """Choose where gestures will appear in the item pool."""
+ """
+ Choose where gestures will appear in the item pool.
+ """
display_name = "Shuffle Gestures"
option_anywhere = 0
option_tvs_only = 1
option_default_locations = 2
default = 0
+
class MedallionShuffle(Toggle):
- """Shuffles red medallions into the pool."""
+ """
+ Shuffles red medallions into the item pool.
+ """
display_name = "Shuffle Red Medallions"
+
class StartLocation(Choice):
- """Select the starting location from 1 of 4 positions."""
+ """
+ Select the starting location from 1 of 4 positions.
+ """
display_name = "Start Location"
option_waynehouse = 0
option_viewaxs_edifice = 1
@@ -35,14 +48,23 @@ class StartLocation(Choice):
return "TV Island"
return super().get_option_name(value)
+
class ExtraLogic(DefaultOnToggle):
- """Include some extra items in logic (CHARGE UP, 1x PAPER CUP) to prevent the game from becoming too difficult."""
+ """
+ Include some extra items in logic (CHARGE UP, 1x PAPER CUP) to prevent the game from becoming too difficult.
+ """
display_name = "Extra Items in Logic"
+
class Hylics2DeathLink(DeathLink):
- """When you die, everyone dies. The reverse is also true.
+ """
+ When you die, everyone dies. The reverse is also true.
+
Note that this also includes death by using the PERISH gesture.
- Can be toggled via in-game console command "/deathlink"."""
+
+ Can be toggled via in-game console command "/deathlink".
+ """
+
@dataclass
class Hylics2Options(PerGameCommonOptions):
diff --git a/worlds/mlss/Client.py b/worlds/mlss/Client.py
index a1cd43afba..1f08b85610 100644
--- a/worlds/mlss/Client.py
+++ b/worlds/mlss/Client.py
@@ -48,10 +48,6 @@ class MLSSClient(BizHawkClient):
rom_name_bytes = await bizhawk.read(ctx.bizhawk_ctx, [(0xA0, 14, "ROM")])
rom_name = bytes([byte for byte in rom_name_bytes[0] if byte != 0]).decode("UTF-8")
if not rom_name.startswith("MARIO&LUIGIUA8"):
- logger.info(
- "ERROR: You have opened a game that is not Mario & Luigi Superstar Saga. "
- "Please make sure you are opening the correct ROM."
- )
return False
except UnicodeDecodeError:
return False
diff --git a/worlds/sa2b/Options.py b/worlds/sa2b/Options.py
index b298042692..438e59de5e 100644
--- a/worlds/sa2b/Options.py
+++ b/worlds/sa2b/Options.py
@@ -1,18 +1,26 @@
-import typing
+from dataclasses import dataclass
-from Options import Choice, Range, Option, Toggle, DeathLink, DefaultOnToggle, OptionList
+from Options import Choice, Range, Toggle, DeathLink, DefaultOnToggle, OptionGroup, PerGameCommonOptions
class Goal(Choice):
"""
Determines the goal of the seed
+
Biolizard: Finish Cannon's Core and defeat the Biolizard and Finalhazard
+
Chaos Emerald Hunt: Find the Seven Chaos Emeralds and reach Green Hill Zone
+
Finalhazard Chaos Emerald Hunt: Find the Seven Chaos Emeralds and reach Green Hill Zone, then defeat Finalhazard
+
Grand Prix: Win every race in Kart Race Mode (all standard levels are disabled)
+
Boss Rush: Beat all of the bosses in the Boss Rush, ending with Finalhazard
+
Cannon's Core Boss Rush: Beat Cannon's Core, then beat all of the bosses in the Boss Rush, ending with Finalhazard
+
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
"""
display_name = "Goal"
@@ -46,9 +54,13 @@ class MissionShuffle(Toggle):
class BossRushShuffle(Choice):
"""
Determines how bosses in Boss Rush Mode are shuffled
+
Vanilla: Bosses appear in the Vanilla ordering
+
Shuffled: The same bosses appear, but in a random order
+
Chaos: Each boss is randomly chosen separately (one will always be King Boom Boo)
+
Singularity: One boss is chosen and placed in every slot (one will always be replaced with King Boom Boo)
"""
display_name = "Boss Rush Shuffle"
@@ -196,9 +208,13 @@ class Keysanity(Toggle):
class Whistlesanity(Choice):
"""
Determines whether whistling at various spots grants checks
+
None: No Whistle Spots grant checks
+
Pipes: Whistling at Pipes grants checks (97 Locations)
+
Hidden: Whistling at Hidden Whistle Spots grants checks (32 Locations)
+
Both: Whistling at both Pipes and Hidden Whistle Spots grants checks (129 Locations)
"""
display_name = "Whistlesanity"
@@ -228,8 +244,9 @@ class Omosanity(Toggle):
class Animalsanity(Toggle):
"""
Determines whether unique counts of animals grant checks.
- ALL animals must be collected in a single run of a mission to get all checks.
(421 Locations)
+
+ ALL animals must be collected in a single run of a mission to get all checks.
"""
display_name = "Animalsanity"
@@ -237,8 +254,11 @@ class Animalsanity(Toggle):
class KartRaceChecks(Choice):
"""
Determines whether Kart Race Mode grants checks
+
None: No Kart Races grant checks
+
Mini: Each Kart Race difficulty must be beaten only once
+
Full: Every Character must separately beat each Kart Race difficulty
"""
display_name = "Kart Race Checks"
@@ -271,8 +291,11 @@ class NumberOfLevelGates(Range):
class LevelGateDistribution(Choice):
"""
Determines how levels are distributed between level gate regions
+
Early: Earlier regions will have more levels than later regions
+
Even: Levels will be evenly distributed between all regions
+
Late: Later regions will have more levels than earlier regions
"""
display_name = "Level Gate Distribution"
@@ -296,7 +319,9 @@ class LevelGateCosts(Choice):
class MaximumEmblemCap(Range):
"""
Determines the maximum number of emblems that can be in the item pool.
+
If fewer available locations exist in the pool than this number, the number of available locations will be used instead.
+
Gate and Cannon's Core costs will be calculated based off of that number.
"""
display_name = "Max Emblem Cap"
@@ -321,9 +346,13 @@ class RequiredRank(Choice):
class ChaoRaceDifficulty(Choice):
"""
Determines the number of Chao Race difficulty levels included. Easier difficulty settings means fewer Chao Race checks
+
None: No Chao Races have checks
+
Beginner: Beginner Races
+
Intermediate: Beginner, Challenge, Hero, and Dark Races
+
Expert: Beginner, Challenge, Hero, Dark and Jewel Races
"""
display_name = "Chao Race Difficulty"
@@ -350,9 +379,10 @@ class ChaoKarateDifficulty(Choice):
class ChaoStadiumChecks(Choice):
"""
Determines which Chao Stadium activities grant checks
+
All: Each individual race and karate fight grants a check
- Prize: Only the races which grant Chao Toys grant checks (final race of each Beginner and Jewel cup, 4th, 8th, and
- 12th Challenge Races, 2nd and 4th Hero and Dark Races, final fight of each Karate difficulty)
+
+ Prize: Only the races which grant Chao Toys grant checks (final race of each Beginner and Jewel cup, 4th, 8th, and 12th Challenge Races, 2nd and 4th Hero and Dark Races, final fight of each Karate difficulty)
"""
display_name = "Chao Stadium Checks"
option_all = 0
@@ -374,6 +404,7 @@ class ChaoStats(Range):
class ChaoStatsFrequency(Range):
"""
Determines how many levels in each Chao Stat grant checks (up to the maximum set in the `chao_stats` option)
+
`1` means every level is included, `2` means every other level is included, `3` means every third, and so on
"""
display_name = "Chao Stats Frequency"
@@ -408,8 +439,11 @@ class ChaoKindergarten(Choice):
"""
Determines whether learning the lessons from the Kindergarten Classroom grants checks
(WARNING: VERY SLOW)
+
None: No Kindergarten classes have checks
+
Basics: One class from each category (Drawing, Dance, Song, and Instrument) is a check (4 Locations)
+
Full: Every class is a check (23 Locations)
"""
display_name = "Chao Kindergarten Checks"
@@ -443,8 +477,8 @@ class BlackMarketUnlockCosts(Choice):
class BlackMarketPriceMultiplier(Range):
"""
Determines how many rings the Black Market items cost
- The base ring costs of items in the Black Market range from 50-100,
- and are then multiplied by this value
+
+ The base ring costs of items in the Black Market range from 50-100, and are then multiplied by this value
"""
display_name = "Black Market Price Multiplier"
range_start = 0
@@ -469,7 +503,9 @@ class ChaoEntranceRandomization(Toggle):
class RequiredCannonsCoreMissions(Choice):
"""
Determines how many Cannon's Core missions must be completed (for Biolizard or Cannon's Core goals)
+
First: Only the first mission must be completed
+
All Active: All active Cannon's Core missions must be completed
"""
display_name = "Required Cannon's Core Missions"
@@ -665,8 +701,11 @@ class CannonsCoreMission5(DefaultOnToggle):
class RingLoss(Choice):
"""
How taking damage is handled
+
Classic: You lose all of your rings when hit
+
Modern: You lose 20 rings when hit
+
OHKO: You die immediately when hit (NOTE: Some Hard Logic tricks may require damage boosts!)
"""
display_name = "Ring Loss"
@@ -693,9 +732,13 @@ class RingLink(Toggle):
class SADXMusic(Choice):
"""
Whether the randomizer will include Sonic Adventure DX Music in the music pool
+
SA2B: Only SA2B music will be played
+
SADX: Only SADX music will be played
+
Both: Both SA2B and SADX music will be played
+
NOTE: This option requires the player to own a PC copy of SADX and to follow the addition steps in the setup guide.
"""
display_name = "SADX Music"
@@ -715,9 +758,13 @@ class SADXMusic(Choice):
class MusicShuffle(Choice):
"""
What type of Music Shuffle is used
+
None: No music is shuffled.
+
Levels: Level music is shuffled.
+
Full: Level, Menu, and Additional music is shuffled.
+
Singularity: Level, Menu, and Additional music is all replaced with a single random song.
"""
display_name = "Music Shuffle Type"
@@ -731,10 +778,15 @@ class MusicShuffle(Choice):
class VoiceShuffle(Choice):
"""
What type of Voice Shuffle is used
+
None: No voices are shuffled.
+
Shuffled: Voices are shuffled.
+
Rude: Voices are shuffled, but some are replaced with rude words.
+
Chao: All voices are replaced with chao sounds.
+
Singularity: All voices are replaced with a single random voice.
"""
display_name = "Voice Shuffle Type"
@@ -768,7 +820,9 @@ class Narrator(Choice):
class LogicDifficulty(Choice):
"""
What set of Upgrade Requirement logic to use
+
Standard: The logic assumes the "intended" usage of Upgrades to progress through levels
+
Hard: Some simple skips or sequence breaks may be required
"""
display_name = "Logic Difficulty"
@@ -777,96 +831,195 @@ class LogicDifficulty(Choice):
default = 0
-sa2b_options: typing.Dict[str, type(Option)] = {
- "goal": Goal,
+sa2b_option_groups = [
+ OptionGroup("General Options", [
+ Goal,
+ BossRushShuffle,
+ LogicDifficulty,
+ RequiredRank,
+ MaximumEmblemCap,
+ RingLoss,
+ ]),
+ OptionGroup("Stages", [
+ MissionShuffle,
+ EmblemPercentageForCannonsCore,
+ RequiredCannonsCoreMissions,
+ NumberOfLevelGates,
+ LevelGateCosts,
+ LevelGateDistribution,
+ ]),
+ OptionGroup("Sanity Options", [
+ Keysanity,
+ Whistlesanity,
+ Beetlesanity,
+ Omosanity,
+ Animalsanity,
+ KartRaceChecks,
+ ]),
+ OptionGroup("Chao", [
+ BlackMarketSlots,
+ BlackMarketUnlockCosts,
+ BlackMarketPriceMultiplier,
+ ChaoRaceDifficulty,
+ ChaoKarateDifficulty,
+ ChaoStadiumChecks,
+ ChaoAnimalParts,
+ ChaoStats,
+ ChaoStatsFrequency,
+ ChaoStatsStamina,
+ ChaoStatsHidden,
+ ChaoKindergarten,
+ ShuffleStartingChaoEggs,
+ ChaoEntranceRandomization,
+ ]),
+ OptionGroup("Junk and Traps", [
+ JunkFillPercentage,
+ TrapFillPercentage,
+ OmochaoTrapWeight,
+ TimestopTrapWeight,
+ ConfusionTrapWeight,
+ TinyTrapWeight,
+ GravityTrapWeight,
+ ExpositionTrapWeight,
+ IceTrapWeight,
+ SlowTrapWeight,
+ CutsceneTrapWeight,
+ ReverseTrapWeight,
+ PongTrapWeight,
+ MinigameTrapDifficulty,
+ ]),
+ OptionGroup("Speed Missions", [
+ SpeedMissionCount,
+ SpeedMission2,
+ SpeedMission3,
+ SpeedMission4,
+ SpeedMission5,
+ ]),
+ OptionGroup("Mech Missions", [
+ MechMissionCount,
+ MechMission2,
+ MechMission3,
+ MechMission4,
+ MechMission5,
+ ]),
+ OptionGroup("Hunt Missions", [
+ HuntMissionCount,
+ HuntMission2,
+ HuntMission3,
+ HuntMission4,
+ HuntMission5,
+ ]),
+ OptionGroup("Kart Missions", [
+ KartMissionCount,
+ KartMission2,
+ KartMission3,
+ KartMission4,
+ KartMission5,
+ ]),
+ OptionGroup("Cannon's Core Missions", [
+ CannonsCoreMissionCount,
+ CannonsCoreMission2,
+ CannonsCoreMission3,
+ CannonsCoreMission4,
+ CannonsCoreMission5,
+ ]),
+ OptionGroup("Aesthetics", [
+ SADXMusic,
+ MusicShuffle,
+ VoiceShuffle,
+ Narrator,
+ ]),
+]
- "mission_shuffle": MissionShuffle,
- "boss_rush_shuffle": BossRushShuffle,
- "keysanity": Keysanity,
- "whistlesanity": Whistlesanity,
- "beetlesanity": Beetlesanity,
- "omosanity": Omosanity,
- "animalsanity": Animalsanity,
- "kart_race_checks": KartRaceChecks,
+@dataclass
+class SA2BOptions(PerGameCommonOptions):
+ goal: Goal
+ boss_rush_shuffle: BossRushShuffle
+ logic_difficulty: LogicDifficulty
+ required_rank: RequiredRank
+ max_emblem_cap: MaximumEmblemCap
+ ring_loss: RingLoss
- "logic_difficulty": LogicDifficulty,
- "required_rank": RequiredRank,
- "required_cannons_core_missions": RequiredCannonsCoreMissions,
+ mission_shuffle: MissionShuffle
+ required_cannons_core_missions: RequiredCannonsCoreMissions
+ emblem_percentage_for_cannons_core: EmblemPercentageForCannonsCore
+ number_of_level_gates: NumberOfLevelGates
+ level_gate_distribution: LevelGateDistribution
+ level_gate_costs: LevelGateCosts
- "emblem_percentage_for_cannons_core": EmblemPercentageForCannonsCore,
- "number_of_level_gates": NumberOfLevelGates,
- "level_gate_distribution": LevelGateDistribution,
- "level_gate_costs": LevelGateCosts,
- "max_emblem_cap": MaximumEmblemCap,
+ keysanity: Keysanity
+ whistlesanity: Whistlesanity
+ beetlesanity: Beetlesanity
+ omosanity: Omosanity
+ animalsanity: Animalsanity
+ kart_race_checks: KartRaceChecks
- "chao_race_difficulty": ChaoRaceDifficulty,
- "chao_karate_difficulty": ChaoKarateDifficulty,
- "chao_stadium_checks": ChaoStadiumChecks,
- "chao_stats": ChaoStats,
- "chao_stats_frequency": ChaoStatsFrequency,
- "chao_stats_stamina": ChaoStatsStamina,
- "chao_stats_hidden": ChaoStatsHidden,
- "chao_animal_parts": ChaoAnimalParts,
- "chao_kindergarten": ChaoKindergarten,
- "black_market_slots": BlackMarketSlots,
- "black_market_unlock_costs": BlackMarketUnlockCosts,
- "black_market_price_multiplier": BlackMarketPriceMultiplier,
- "shuffle_starting_chao_eggs": ShuffleStartingChaoEggs,
- "chao_entrance_randomization": ChaoEntranceRandomization,
+ black_market_slots: BlackMarketSlots
+ black_market_unlock_costs: BlackMarketUnlockCosts
+ black_market_price_multiplier: BlackMarketPriceMultiplier
+ chao_race_difficulty: ChaoRaceDifficulty
+ chao_karate_difficulty: ChaoKarateDifficulty
+ chao_stadium_checks: ChaoStadiumChecks
+ chao_animal_parts: ChaoAnimalParts
+ chao_stats: ChaoStats
+ chao_stats_frequency: ChaoStatsFrequency
+ chao_stats_stamina: ChaoStatsStamina
+ chao_stats_hidden: ChaoStatsHidden
+ chao_kindergarten: ChaoKindergarten
+ shuffle_starting_chao_eggs: ShuffleStartingChaoEggs
+ chao_entrance_randomization: ChaoEntranceRandomization
- "junk_fill_percentage": JunkFillPercentage,
- "trap_fill_percentage": TrapFillPercentage,
- "omochao_trap_weight": OmochaoTrapWeight,
- "timestop_trap_weight": TimestopTrapWeight,
- "confusion_trap_weight": ConfusionTrapWeight,
- "tiny_trap_weight": TinyTrapWeight,
- "gravity_trap_weight": GravityTrapWeight,
- "exposition_trap_weight": ExpositionTrapWeight,
- #"darkness_trap_weight": DarknessTrapWeight,
- "ice_trap_weight": IceTrapWeight,
- "slow_trap_weight": SlowTrapWeight,
- "cutscene_trap_weight": CutsceneTrapWeight,
- "reverse_trap_weight": ReverseTrapWeight,
- "pong_trap_weight": PongTrapWeight,
- "minigame_trap_difficulty": MinigameTrapDifficulty,
+ junk_fill_percentage: JunkFillPercentage
+ trap_fill_percentage: TrapFillPercentage
+ omochao_trap_weight: OmochaoTrapWeight
+ timestop_trap_weight: TimestopTrapWeight
+ confusion_trap_weight: ConfusionTrapWeight
+ tiny_trap_weight: TinyTrapWeight
+ gravity_trap_weight: GravityTrapWeight
+ exposition_trap_weight: ExpositionTrapWeight
+ #darkness_trap_weight: DarknessTrapWeight
+ ice_trap_weight: IceTrapWeight
+ slow_trap_weight: SlowTrapWeight
+ cutscene_trap_weight: CutsceneTrapWeight
+ reverse_trap_weight: ReverseTrapWeight
+ pong_trap_weight: PongTrapWeight
+ minigame_trap_difficulty: MinigameTrapDifficulty
- "sadx_music": SADXMusic,
- "music_shuffle": MusicShuffle,
- "voice_shuffle": VoiceShuffle,
- "narrator": Narrator,
- "ring_loss": RingLoss,
+ 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,
+ speed_mission_count: SpeedMissionCount
+ speed_mission_2: SpeedMission2
+ speed_mission_3: SpeedMission3
+ speed_mission_4: SpeedMission4
+ speed_mission_5: SpeedMission5
- "mech_mission_count": MechMissionCount,
- "mech_mission_2": MechMission2,
- "mech_mission_3": MechMission3,
- "mech_mission_4": MechMission4,
- "mech_mission_5": MechMission5,
+ mech_mission_count: MechMissionCount
+ mech_mission_2: MechMission2
+ mech_mission_3: MechMission3
+ mech_mission_4: MechMission4
+ mech_mission_5: MechMission5
- "hunt_mission_count": HuntMissionCount,
- "hunt_mission_2": HuntMission2,
- "hunt_mission_3": HuntMission3,
- "hunt_mission_4": HuntMission4,
- "hunt_mission_5": HuntMission5,
+ hunt_mission_count: HuntMissionCount
+ hunt_mission_2: HuntMission2
+ hunt_mission_3: HuntMission3
+ hunt_mission_4: HuntMission4
+ hunt_mission_5: HuntMission5
- "kart_mission_count": KartMissionCount,
- "kart_mission_2": KartMission2,
- "kart_mission_3": KartMission3,
- "kart_mission_4": KartMission4,
- "kart_mission_5": KartMission5,
+ kart_mission_count: KartMissionCount
+ kart_mission_2: KartMission2
+ kart_mission_3: KartMission3
+ kart_mission_4: KartMission4
+ kart_mission_5: KartMission5
- "cannons_core_mission_count": CannonsCoreMissionCount,
- "cannons_core_mission_2": CannonsCoreMission2,
- "cannons_core_mission_3": CannonsCoreMission3,
- "cannons_core_mission_4": CannonsCoreMission4,
- "cannons_core_mission_5": CannonsCoreMission5,
+ cannons_core_mission_count: CannonsCoreMissionCount
+ cannons_core_mission_2: CannonsCoreMission2
+ cannons_core_mission_3: CannonsCoreMission3
+ cannons_core_mission_4: CannonsCoreMission4
+ cannons_core_mission_5: CannonsCoreMission5
- "ring_link": RingLink,
- "death_link": DeathLink,
-}
+ ring_link: RingLink
+ death_link: DeathLink
diff --git a/worlds/sa2b/__init__.py b/worlds/sa2b/__init__.py
index 7d77aebc4c..6279aa9432 100644
--- a/worlds/sa2b/__init__.py
+++ b/worlds/sa2b/__init__.py
@@ -3,20 +3,20 @@ import math
import logging
from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification
+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 .Options import sa2b_options
+from .Missions import get_mission_table, get_mission_count_table, get_first_and_last_cannons_core_missions
+from .Names import ItemName, LocationName
+from .Options import SA2BOptions, sa2b_option_groups
from .Regions import create_regions, shuffleable_regions, connect_regions, LevelGate, gate_0_whitelist_regions, \
gate_0_blacklist_regions
from .Rules import set_rules
-from .Names import ItemName, LocationName
-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 worlds.AutoWorld import WebWorld, World
-from .GateBosses import get_gate_bosses, get_boss_rush_bosses, get_boss_name
-from .Missions import get_mission_table, get_mission_count_table, get_first_and_last_cannons_core_missions
-import Patch
class SA2BWeb(WebWorld):
@@ -30,8 +30,9 @@ class SA2BWeb(WebWorld):
"setup/en",
["RaspberrySpaceJam", "PoryGone", "Entiss"]
)
-
+
tutorials = [setup_en]
+ option_groups = sa2b_option_groups
def check_for_impossible_shuffle(shuffled_levels: typing.List[int], gate_0_range: int, multiworld: MultiWorld):
@@ -54,7 +55,8 @@ class SA2BWorld(World):
Sonic Adventure 2 Battle is an action platforming game. Play as Sonic, Tails, Knuckles, Shadow, Rouge, and Eggman across 31 stages and prevent the destruction of the earth.
"""
game: str = "Sonic Adventure 2 Battle"
- option_definitions = sa2b_options
+ options_dataclass = SA2BOptions
+ options: SA2BOptions
topology_present = False
data_version = 7
diff --git a/worlds/smw/Options.py b/worlds/smw/Options.py
index ab7fcccdba..545b3c931b 100644
--- a/worlds/smw/Options.py
+++ b/worlds/smw/Options.py
@@ -1,12 +1,14 @@
from dataclasses import dataclass
-from Options import Choice, Range, Toggle, DeathLink, DefaultOnToggle, PerGameCommonOptions
+from Options import Choice, Range, Toggle, DeathLink, DefaultOnToggle, OptionGroup, PerGameCommonOptions
class Goal(Choice):
"""
Determines the goal of the seed
+
Bowser: Defeat Koopalings, reach Bowser's Castle and defeat Bowser
+
Yoshi Egg Hunt: Find a certain number of Yoshi Eggs
"""
display_name = "Goal"
@@ -28,7 +30,9 @@ class BossesRequired(Range):
class NumberOfYoshiEggs(Range):
"""
Maximum possible number of Yoshi Eggs that will be in the item pool
+
If fewer available locations exist in the pool than this number, the number of available locations will be used instead.
+
Required Percentage of Yoshi Eggs will be calculated based off of that number.
"""
display_name = "Max Number of Yoshi Eggs"
@@ -64,7 +68,9 @@ class MoonChecks(Toggle):
class Hidden1UpChecks(Toggle):
"""
Whether collecting a hidden 1-Up mushroom in a level will grant a check
+
These checks are considered cryptic as there's no actual indicator that they're in their respective places
+
Enable this option at your own risk
"""
display_name = "Hidden 1-Up Checks"
@@ -80,7 +86,9 @@ class BonusBlockChecks(Toggle):
class Blocksanity(Toggle):
"""
Whether hitting a block with an item or coin inside will grant a check
+
Note that some blocks are excluded due to how the option and the game works!
+
Exclusion list:
* Blocks in Top Secret Area & Front Door/Bowser Castle
* Blocks that are unreachable unless you glitch your way in
@@ -91,10 +99,15 @@ class Blocksanity(Toggle):
class BowserCastleDoors(Choice):
"""
How the doors of Bowser's Castle behave
+
Vanilla: Front and Back Doors behave as vanilla
+
Fast: Both doors behave as the Back Door
+
Slow: Both doors behave as the Front Door
+
"Front Door" rooms depend on the `bowser_castle_rooms` option
+
"Back Door" only requires going through the dark hallway to Bowser
"""
display_name = "Bowser Castle Doors"
@@ -107,10 +120,15 @@ class BowserCastleDoors(Choice):
class BowserCastleRooms(Choice):
"""
How the rooms of Bowser's Castle Front Door behave
+
Vanilla: You can choose which rooms to enter, as in vanilla
+
Random Two Room: Two random rooms are chosen
+
Random Five Room: Five random rooms are chosen
+
Gauntlet: All eight rooms must be cleared
+
Labyrinth: Which room leads to Bowser?
"""
display_name = "Bowser Castle Rooms"
@@ -125,9 +143,13 @@ class BowserCastleRooms(Choice):
class BossShuffle(Choice):
"""
How bosses are shuffled
+
None: Bosses are not shuffled
+
Simple: Four Reznors and the seven Koopalings are shuffled around
+
Full: Each boss location gets a fully random boss
+
Singularity: One or two bosses are chosen and placed at every boss location
"""
display_name = "Boss Shuffle"
@@ -148,6 +170,7 @@ class LevelShuffle(Toggle):
class ExcludeSpecialZone(Toggle):
"""
If active, this option will prevent any progression items from being placed in Special Zone levels.
+
Additionally, if Level Shuffle is active, Special Zone levels will not be shuffled away from their vanilla tiles.
"""
display_name = "Exclude Special Zone"
@@ -155,9 +178,10 @@ class ExcludeSpecialZone(Toggle):
class SwapDonutGhostHouseExits(Toggle):
"""
- If enabled, this option will swap which overworld direction the two exits of the level at the Donut Ghost House
- overworld tile go:
+ If enabled, this option will swap which overworld direction the two exits of the level at the Donut Ghost House overworld tile go:
+
False: Normal Exit goes up, Secret Exit goes right.
+
True: Normal Exit goes right, Secret Exit goes up.
"""
display_name = "Swap Donut GH Exits"
@@ -258,6 +282,7 @@ class Autosave(DefaultOnToggle):
class EarlyClimb(Toggle):
"""
Force Climb to appear early in the seed as a local item.
+
This is particularly useful to prevent BK when Level Shuffle is disabled
"""
display_name = "Early Climb"
@@ -277,9 +302,13 @@ class OverworldSpeed(Choice):
class MusicShuffle(Choice):
"""
Music shuffle type
+
None: No Music is shuffled
+
Consistent: Each music track is consistently shuffled throughout the game
+
Full: Each individual level has a random music track
+
Singularity: The entire game uses one song for overworld and one song for levels
"""
display_name = "Music Shuffle"
@@ -293,9 +322,13 @@ class MusicShuffle(Choice):
class SFXShuffle(Choice):
"""
Shuffles almost every instance of sound effect playback
+
Archipelago elements that play sound effects aren't randomized
+
None: No SFX are shuffled
+
Full: Each individual SFX call has a random SFX
+
Singularity: The entire game uses one SFX for every SFX call
"""
display_name = "Sound Effect Shuffle"
@@ -324,8 +357,11 @@ class MarioPalette(Choice):
class LevelPaletteShuffle(Choice):
"""
Whether to shuffle level palettes
+
Off: Do not shuffle palettes
+
On Legacy: Uses only the palette sets from the original game
+
On Curated: Uses custom, hand-crafted palette sets
"""
display_name = "Level Palette Shuffle"
@@ -338,8 +374,11 @@ class LevelPaletteShuffle(Choice):
class OverworldPaletteShuffle(Choice):
"""
Whether to shuffle overworld palettes
+
Off: Do not shuffle palettes
+
On Legacy: Uses only the palette sets from the original game
+
On Curated: Uses custom, hand-crafted palette sets
"""
display_name = "Overworld Palette Shuffle"
@@ -359,6 +398,52 @@ class StartingLifeCount(Range):
default = 5
+smw_option_groups = [
+ OptionGroup("Goal Options", [
+ Goal,
+ BossesRequired,
+ NumberOfYoshiEggs,
+ PercentageOfYoshiEggs,
+ ]),
+ OptionGroup("Sanity Options", [
+ DragonCoinChecks,
+ MoonChecks,
+ Hidden1UpChecks,
+ BonusBlockChecks,
+ Blocksanity,
+ ]),
+ OptionGroup("Level Shuffling", [
+ LevelShuffle,
+ ExcludeSpecialZone,
+ BowserCastleDoors,
+ BowserCastleRooms,
+ BossShuffle,
+ SwapDonutGhostHouseExits,
+ ]),
+ OptionGroup("Junk and Traps", [
+ JunkFillPercentage,
+ TrapFillPercentage,
+ IceTrapWeight,
+ StunTrapWeight,
+ LiteratureTrapWeight,
+ TimerTrapWeight,
+ ReverseTrapWeight,
+ ThwimpTrapWeight,
+ ]),
+ OptionGroup("Aesthetics", [
+ DisplayReceivedItemPopups,
+ Autosave,
+ OverworldSpeed,
+ MusicShuffle,
+ SFXShuffle,
+ MarioPalette,
+ LevelPaletteShuffle,
+ OverworldPaletteShuffle,
+ StartingLifeCount,
+ ]),
+]
+
+
@dataclass
class SMWOptions(PerGameCommonOptions):
death_link: DeathLink
diff --git a/worlds/smw/Presets.py b/worlds/smw/Presets.py
new file mode 100644
index 0000000000..17a80e3efc
--- /dev/null
+++ b/worlds/smw/Presets.py
@@ -0,0 +1,57 @@
+from typing import Dict, Any
+
+all_random = {
+ "goal": "random",
+ "bosses_required": "random",
+ "max_yoshi_egg_cap": "random",
+ "percentage_of_yoshi_eggs": "random",
+ "dragon_coin_checks": "random",
+ "moon_checks": "random",
+ "hidden_1up_checks": "random",
+ "bonus_block_checks": "random",
+ "blocksanity": "random",
+ "bowser_castle_doors": "random",
+ "bowser_castle_rooms": "random",
+ "level_shuffle": "random",
+ "exclude_special_zone": "random",
+ "boss_shuffle": "random",
+ "swap_donut_gh_exits": "random",
+ "display_received_item_popups": "random",
+ "junk_fill_percentage": "random",
+ "trap_fill_percentage": "random",
+ "ice_trap_weight": "random",
+ "stun_trap_weight": "random",
+ "literature_trap_weight": "random",
+ "timer_trap_weight": "random",
+ "reverse_trap_weight": "random",
+ "thwimp_trap_weight": "random",
+ "autosave": "random",
+ "early_climb": "random",
+ "overworld_speed": "random",
+ "music_shuffle": "random",
+ "sfx_shuffle": "random",
+ "mario_palette": "random",
+ "level_palette_shuffle": "random",
+ "overworld_palette_shuffle": "random",
+ "starting_life_count": "random",
+}
+
+allsanity = {
+ "dragon_coin_checks": True,
+ "moon_checks": True,
+ "hidden_1up_checks": True,
+ "bonus_block_checks": True,
+ "blocksanity": True,
+ "level_shuffle": True,
+ "boss_shuffle": "full",
+ "music_shuffle": "full",
+ "sfx_shuffle": "full",
+ "mario_palette": "random",
+ "level_palette_shuffle": "on_curated",
+ "overworld_palette_shuffle": "on_curated",
+}
+
+smw_options_presets: Dict[str, Dict[str, Any]] = {
+ "All Random": all_random,
+ "Allsanity": allsanity,
+}
diff --git a/worlds/smw/__init__.py b/worlds/smw/__init__.py
index 875491a8d0..97fc84f003 100644
--- a/worlds/smw/__init__.py
+++ b/worlds/smw/__init__.py
@@ -6,17 +6,19 @@ import settings
import threading
from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification
-from .Items import SMWItem, ItemData, item_table, junk_table
-from .Locations import SMWLocation, all_locations, setup_locations, special_zone_level_names, special_zone_dragon_coin_names, special_zone_hidden_1up_names, special_zone_blocksanity_names
-from .Options import SMWOptions
-from .Regions import create_regions, connect_regions
-from .Levels import full_level_list, generate_level_list, location_id_to_level_id
-from .Rules import set_rules
-from worlds.generic.Rules import add_rule, exclusion_rules
-from .Names import ItemName, LocationName
-from .Client import SMWSNIClient
from worlds.AutoWorld import WebWorld, World
+from worlds.generic.Rules import add_rule, exclusion_rules
+
+from .Client import SMWSNIClient
+from .Items import SMWItem, ItemData, item_table, junk_table
+from .Levels import full_level_list, generate_level_list, location_id_to_level_id
+from .Locations import SMWLocation, all_locations, setup_locations, special_zone_level_names, special_zone_dragon_coin_names, special_zone_hidden_1up_names, special_zone_blocksanity_names
+from .Names import ItemName, LocationName
+from .Options import SMWOptions, smw_option_groups
+from .Presets import smw_options_presets
+from .Regions import create_regions, connect_regions
from .Rom import LocalRom, patch_rom, get_base_rom_path, SMWDeltaPatch
+from .Rules import set_rules
class SMWSettings(settings.Group):
@@ -40,9 +42,12 @@ class SMWWeb(WebWorld):
"setup/en",
["PoryGone"]
)
-
+
tutorials = [setup_en]
+ option_groups = smw_option_groups
+ options_presets = smw_options_presets
+
class SMWWorld(World):
"""
diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py
index 20fbd82df2..8e8957144d 100644
--- a/worlds/tunic/__init__.py
+++ b/worlds/tunic/__init__.py
@@ -8,7 +8,7 @@ from .er_rules import set_er_location_rules
from .regions import tunic_regions
from .er_scripts import create_er_regions
from .er_data import portal_mapping
-from .options import TunicOptions, EntranceRando
+from .options import TunicOptions, EntranceRando, tunic_option_groups
from worlds.AutoWorld import WebWorld, World
from worlds.generic import PlandoConnection
from decimal import Decimal, ROUND_HALF_UP
@@ -27,6 +27,7 @@ class TunicWeb(WebWorld):
]
theme = "grassFlowers"
game = "TUNIC"
+ option_groups = tunic_option_groups
class TunicItem(Item):
diff --git a/worlds/tunic/options.py b/worlds/tunic/options.py
index 605bb065fd..1f12b5053d 100644
--- a/worlds/tunic/options.py
+++ b/worlds/tunic/options.py
@@ -1,30 +1,38 @@
from dataclasses import dataclass
-from Options import DefaultOnToggle, Toggle, StartInventoryPool, Choice, Range, TextChoice, PerGameCommonOptions
+from Options import (DefaultOnToggle, Toggle, StartInventoryPool, Choice, Range, TextChoice, PerGameCommonOptions,
+ OptionGroup)
class SwordProgression(DefaultOnToggle):
- """Adds four sword upgrades to the item pool that will progressively grant stronger melee weapons, including two new swords with increased range and attack power."""
+ """
+ Adds four sword upgrades to the item pool that will progressively grant stronger melee weapons, including two new swords with increased range and attack power.
+ """
internal_name = "sword_progression"
display_name = "Sword Progression"
class StartWithSword(Toggle):
- """Start with a sword in the player's inventory. Does not count towards Sword Progression."""
+ """
+ Start with a sword in the player's inventory. Does not count towards Sword Progression.
+ """
internal_name = "start_with_sword"
display_name = "Start With Sword"
class KeysBehindBosses(Toggle):
- """Places the three hexagon keys behind their respective boss fight in your world."""
+ """
+ Places the three hexagon keys behind their respective boss fight in your world.
+ """
internal_name = "keys_behind_bosses"
display_name = "Keys Behind Bosses"
class AbilityShuffling(Toggle):
- """Locks the usage of Prayer, Holy Cross*, and the Icebolt combo until the relevant pages of the manual have been found.
+ """
+ Locks the usage of Prayer, Holy Cross*, and the Icebolt combo until the relevant pages of the manual have been found.
If playing Hexagon Quest, abilities are instead randomly unlocked after obtaining 25%, 50%, and 75% of the required Hexagon goal amount.
- *Certain Holy Cross usages are still allowed, such as the free bomb codes, the seeking spell, and other player-facing codes.
+ * Certain Holy Cross usages are still allowed, such as the free bomb codes, the seeking spell, and other player-facing codes.
"""
internal_name = "ability_shuffling"
display_name = "Shuffle Abilities"
@@ -37,9 +45,9 @@ class LogicRules(Choice):
No Major Glitches: Sneaky Laurels zips, ice grapples through doors, shooting the west bell, and boss quick kills are included in logic.
* Ice grappling through the Ziggurat door is not in logic since you will get stuck in there without Prayer.
Unrestricted: Logic in No Major Glitches, as well as ladder storage to get to certain places early.
- *Torch is given to the player at the start of the game due to the high softlock potential with various tricks. Using the torch is not required in logic.
- *Using Ladder Storage to get to individual chests is not in logic to avoid tedium.
- *Getting knocked out of the air by enemies during Ladder Storage to reach places is not in logic, except for in Rooted Ziggurat Lower. This is so you're not punished for playing with enemy rando on.
+ * Torch is given to the player at the start of the game due to the high softlock potential with various tricks. Using the torch is not required in logic.
+ * Using Ladder Storage to get to individual chests is not in logic to avoid tedium.
+ * Getting knocked out of the air by enemies during Ladder Storage to reach places is not in logic, except for in Rooted Ziggurat Lower. This is so you're not punished for playing with enemy rando on.
"""
internal_name = "logic_rules"
display_name = "Logic Rules"
@@ -52,21 +60,27 @@ class LogicRules(Choice):
class Lanternless(Toggle):
- """Choose whether you require the Lantern for dark areas.
- When enabled, the Lantern is marked as Useful instead of Progression."""
+ """
+ Choose whether you require the Lantern for dark areas.
+ When enabled, the Lantern is marked as Useful instead of Progression.
+ """
internal_name = "lanternless"
display_name = "Lanternless"
class Maskless(Toggle):
- """Choose whether you require the Scavenger's Mask for Lower Quarry.
- When enabled, the Scavenger's Mask is marked as Useful instead of Progression."""
+ """
+ Choose whether you require the Scavenger's Mask for Lower Quarry.
+ When enabled, the Scavenger's Mask is marked as Useful instead of Progression.
+ """
internal_name = "maskless"
display_name = "Maskless"
class FoolTraps(Choice):
- """Replaces low-to-medium value money rewards in the item pool with fool traps, which cause random negative effects to the player."""
+ """
+ Replaces low-to-medium value money rewards in the item pool with fool traps, which cause random negative effects to the player.
+ """
internal_name = "fool_traps"
display_name = "Fool Traps"
option_off = 0
@@ -77,13 +91,17 @@ class FoolTraps(Choice):
class HexagonQuest(Toggle):
- """An alternate goal that shuffles Gold "Questagon" items into the item pool and allows the game to be completed after collecting the required number of them."""
+ """
+ An alternate goal that shuffles Gold "Questagon" items into the item pool and allows the game to be completed after collecting the required number of them.
+ """
internal_name = "hexagon_quest"
display_name = "Hexagon Quest"
class HexagonGoal(Range):
- """How many Gold Questagons are required to complete the game on Hexagon Quest."""
+ """
+ How many Gold Questagons are required to complete the game on Hexagon Quest.
+ """
internal_name = "hexagon_goal"
display_name = "Gold Hexagons Required"
range_start = 15
@@ -92,7 +110,9 @@ class HexagonGoal(Range):
class ExtraHexagonPercentage(Range):
- """How many extra Gold Questagons are shuffled into the item pool, taken as a percentage of the goal amount."""
+ """
+ How many extra Gold Questagons are shuffled into the item pool, taken as a percentage of the goal amount.
+ """
internal_name = "extra_hexagon_percentage"
display_name = "Percentage of Extra Gold Hexagons"
range_start = 0
@@ -118,16 +138,20 @@ class EntranceRando(TextChoice):
class FixedShop(Toggle):
- """Forces the Windmill entrance to lead to a shop, and removes the remaining shops from the pool.
+ """
+ Forces the Windmill entrance to lead to a shop, and removes the remaining shops from the pool.
Adds another entrance in Rooted Ziggurat Lower to keep an even number of entrances.
- Has no effect if Entrance Rando is not enabled."""
+ Has no effect if Entrance Rando is not enabled.
+ """
internal_name = "fixed_shop"
display_name = "Fewer Shops in Entrance Rando"
class LaurelsLocation(Choice):
- """Force the Hero's Laurels to be placed at a location in your world.
- For if you want to avoid or specify early or late Laurels."""
+ """
+ Force the Hero's Laurels to be placed at a location in your world.
+ For if you want to avoid or specify early or late Laurels.
+ """
internal_name = "laurels_location"
display_name = "Laurels Location"
option_anywhere = 0
@@ -138,9 +162,12 @@ class LaurelsLocation(Choice):
class ShuffleLadders(Toggle):
- """Turns several ladders in the game into items that must be found before they can be climbed on.
+ """
+ Turns several ladders in the game into items that must be found before they can be climbed on.
Adds more layers of progression to the game by blocking access to many areas early on.
- "Ladders were a mistake." —Andrew Shouldice"""
+ "Ladders were a mistake."
+ —Andrew Shouldice
+ """
internal_name = "shuffle_ladders"
display_name = "Shuffle Ladders"
@@ -163,3 +190,12 @@ class TunicOptions(PerGameCommonOptions):
lanternless: Lanternless
maskless: Maskless
laurels_location: LaurelsLocation
+
+
+tunic_option_groups = [
+ OptionGroup("Logic Options", [
+ LogicRules,
+ Lanternless,
+ Maskless,
+ ])
+]
diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py
index f47ab57d5e..ecab25db3d 100644
--- a/worlds/witness/__init__.py
+++ b/worlds/witness/__init__.py
@@ -16,7 +16,7 @@ from .data.item_definition_classes import DoorItemDefinition, ItemData
from .data.utils import get_audio_logs
from .hints import CompactItemData, create_all_hints, make_compact_hint_data, make_laser_hints
from .locations import WitnessPlayerLocations, static_witness_locations
-from .options import TheWitnessOptions
+from .options import TheWitnessOptions, witness_option_groups
from .player_items import WitnessItem, WitnessPlayerItems
from .player_logic import WitnessPlayerLogic
from .presets import witness_option_presets
@@ -36,6 +36,7 @@ class WitnessWebWorld(WebWorld):
)]
options_presets = witness_option_presets
+ option_groups = witness_option_groups
class WitnessWorld(World):
diff --git a/worlds/witness/options.py b/worlds/witness/options.py
index 63f98faea4..00b58ab869 100644
--- a/worlds/witness/options.py
+++ b/worlds/witness/options.py
@@ -2,7 +2,7 @@ from dataclasses import dataclass
from schema import And, Schema
-from Options import Choice, DefaultOnToggle, OptionDict, PerGameCommonOptions, Range, Toggle
+from Options import Choice, DefaultOnToggle, OptionDict, OptionGroup, PerGameCommonOptions, Range, Toggle
from .data import static_logic as static_witness_logic
from .data.item_definition_classes import ItemCategory, WeightedItemDefinition
@@ -61,9 +61,9 @@ class ShuffleLasers(Choice):
class ShuffleDoors(Choice):
"""
If on, opening doors, moving bridges etc. will require a "key".
- If set to "panels", the panel on the door will be locked until receiving its corresponding key.
- If set to "doors", the door will open immediately upon receiving its key. Door panels are added as location checks.
- "Mixed" includes all doors from "doors", and all control panels (bridges, elevators etc.) from "panels".
+ - Panels: The panel on the door will be locked until receiving its corresponding key.
+ - Doors: The door will open immediately upon receiving its key. Door panels are added as location checks.
+ - Mixed: Includes all doors from "doors", and all control panels (bridges, elevators etc.) from "panels".
"""
display_name = "Shuffle Doors"
option_off = 0
@@ -74,8 +74,10 @@ class ShuffleDoors(Choice):
class DoorGroupings(Choice):
"""
- If set to "none", there will be one key for each door, potentially resulting in upwards of 120 keys being added to the item pool.
- If set to "regional", all doors in the same general region will open at once with a single key, reducing the amount of door items and complexity.
+ Controls how door items are grouped.
+
+ - None: There will be one key for each door, potentially resulting in upwards of 120 keys being added to the item pool.
+ - Regional: - All doors in the same general region will open at once with a single key, reducing the amount of door items and complexity.
"""
display_name = "Door Groupings"
option_off = 0
@@ -108,8 +110,8 @@ class ShuffleVaultBoxes(Toggle):
class ShuffleEnvironmentalPuzzles(Choice):
"""
Adds Environmental/Obelisk Puzzles into the location pool.
- If set to "individual", every Environmental Puzzle sends an item.
- If set to "Obelisk Sides", completing every puzzle on one side of an Obelisk sends an item.
+ - Individual: Every Environmental Puzzle sends an item.
+ - Obelisk Sides: Completing every puzzle on one side of an Obelisk sends an item.
Note: In Obelisk Sides, any EPs excluded through another option will be pre-completed on their Obelisk.
"""
@@ -129,9 +131,9 @@ class ShuffleDog(Toggle):
class EnvironmentalPuzzlesDifficulty(Choice):
"""
When "Shuffle Environmental Puzzles" is on, this setting governs which EPs are eligible for the location pool.
- If set to "eclipse", every EP in the game is eligible, including the 1-hour-long "Theater Eclipse EP".
- If set to "tedious", Theater Eclipse EP is excluded from the location pool.
- If set to "normal", several other difficult or long EPs are excluded as well.
+ - Eclipse: Every EP in the game is eligible, including the 1-hour-long "Theater Eclipse EP".
+ - Tedious Theater Eclipse EP is excluded from the location pool.
+ - Normal: several other difficult or long EPs are excluded as well.
"""
display_name = "Environmental Puzzles Difficulty"
option_normal = 0
@@ -159,10 +161,10 @@ class ShufflePostgame(Toggle):
class VictoryCondition(Choice):
"""
Set the victory condition for this world.
- Elevator: Start the elevator at the bottom of the mountain (requires Mountain Lasers).
- Challenge: Beat the secret Challenge (requires Challenge Lasers).
- Mountain Box Short: Input the short solution to the Mountaintop Box (requires Mountain Lasers).
- Mountain Box Long: Input the long solution to the Mountaintop Box (requires Challenge Lasers).
+ - Elevator: Start the elevator at the bottom of the mountain (requires Mountain Lasers).
+ - Challenge: Beat the secret Challenge (requires Challenge Lasers).
+ - Mountain Box Short: Input the short solution to the Mountaintop Box (requires Mountain Lasers).
+ - Mountain Box Long: Input the long solution to the Mountaintop Box (requires Challenge Lasers).
It is important to note that while the Mountain Box requires Desert Laser to be redirected in Town for that laser
to count, the laser locks on the Elevator and Challenge Timer panels do not.
@@ -332,3 +334,45 @@ class TheWitnessOptions(PerGameCommonOptions):
laser_hints: LaserHints
death_link: DeathLink
death_link_amnesty: DeathLinkAmnesty
+
+
+witness_option_groups = [
+ OptionGroup("Puzzles & Goal", [
+ PuzzleRandomization,
+ VictoryCondition,
+ MountainLasers,
+ ChallengeLasers,
+ ]),
+ OptionGroup("Locations", [
+ ShuffleDiscardedPanels,
+ ShuffleVaultBoxes,
+ ShuffleEnvironmentalPuzzles,
+ EnvironmentalPuzzlesDifficulty,
+ ShufflePostgame,
+ DisableNonRandomizedPuzzles,
+ ]),
+ OptionGroup("Progression Items", [
+ ShuffleSymbols,
+ ShuffleDoors,
+ DoorGroupings,
+ ShuffleLasers,
+ ShuffleBoat,
+ ObeliskKeys,
+ ]),
+ OptionGroup("Filler Items", [
+ PuzzleSkipAmount,
+ TrapPercentage,
+ TrapWeights
+ ]),
+ OptionGroup("Hints", [
+ HintAmount,
+ AreaHintPercentage,
+ LaserHints
+ ]),
+ OptionGroup("Misc", [
+ EarlyCaves,
+ ElevatorsComeToYou,
+ DeathLink,
+ DeathLinkAmnesty,
+ ])
+]