From c61505baf69cc598c78a8a65a2efa7d6f6c63f41 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Fri, 14 Jun 2024 15:53:42 -0700 Subject: [PATCH] WebHost/Core/Lingo: Render option documentation as reStructuredText in the WebView (#3511) * Render option documentation as reStructuredText in the WebView This means that options can use the standard Python documentation format, while producing much nicer-looking documentation in the WebView with things like emphasis, lists, and so on. * Opt existing worlds out of rich option docs This avoids breaking the rendering of existing option docs which were written with the old plain text rendering in mind, while also allowing new options to default to the rich text rendering instead. * Use reStructuredText formatting for Lingo Options docstrings * Disable raw and file insertion RST directives * Update doc comments per code review * Make rich text docs opt-in * Put rich_text_options_doc on WebWorld * Document rich text API * Code review * Update docs/options api.md Co-authored-by: Doug Hoskisson * Update Options.py Co-authored-by: Doug Hoskisson --------- Co-authored-by: Chris Wilson Co-authored-by: Doug Hoskisson --- Options.py | 54 ++++++-- WebHostLib/options.py | 17 +++ WebHostLib/static/styles/tooltip.css | 54 +++++--- .../templates/playerOptions/macros.html | 19 ++- .../playerOptions/playerOptions.html | 14 +- docs/options api.md | 44 +++++++ docs/world api.md | 6 + worlds/AutoWorld.py | 15 +++ worlds/__init__.py | 1 + worlds/lingo/__init__.py | 1 + worlds/lingo/options.py | 120 ++++++++++++------ 11 files changed, 265 insertions(+), 80 deletions(-) diff --git a/Options.py b/Options.py index cec6b0549386..f8c19edd5dbd 100644 --- a/Options.py +++ b/Options.py @@ -126,6 +126,23 @@ class Option(typing.Generic[T], metaclass=AssembleOptions): # can be weighted between selections supports_weighting = True + rich_text_doc: typing.Optional[bool] = None + """Whether the WebHost should render the Option's docstring as rich text. + + If this is True, the Option's docstring is interpreted as reStructuredText_, + the standard Python markup format. In the WebHost, it's rendered to HTML so + that lists, emphasis, and other rich text features are displayed properly. + + If this is False, the docstring is instead interpreted as plain text, and + displayed as-is on the WebHost with whitespace preserved. + + If this is None, it inherits the value of `World.rich_text_options_doc`. For + backwards compatibility, this defaults to False, but worlds are encouraged to + set it to True and use reStructuredText for their Option documentation. + + .. _reStructuredText: https://docutils.sourceforge.io/rst.html + """ + # filled by AssembleOptions: name_lookup: typing.ClassVar[typing.Dict[T, str]] # type: ignore # https://github.com/python/typing/discussions/1460 the reason for this type: ignore @@ -1127,10 +1144,13 @@ def __len__(self) -> int: class Accessibility(Choice): """Set rules for reachability of your items/locations. - Locations: ensure everything can be reached and acquired. - Items: ensure all logically relevant items can be acquired. - Minimal: ensure what is needed to reach your goal can be acquired.""" + + - **Locations:** ensure everything can be reached and acquired. + - **Items:** ensure all logically relevant items can be acquired. + - **Minimal:** ensure what is needed to reach your goal can be acquired. + """ display_name = "Accessibility" + rich_text_doc = True option_locations = 0 option_items = 1 option_minimal = 2 @@ -1139,14 +1159,15 @@ class Accessibility(Choice): class ProgressionBalancing(NamedRange): - """ - A system that can move progression earlier, to try and prevent the player from getting stuck and bored early. + """A system that can move progression earlier, to try and prevent the player from getting stuck and bored early. + A lower setting means more getting stuck. A higher setting means less getting stuck. """ default = 50 range_start = 0 range_end = 99 display_name = "Progression Balancing" + rich_text_doc = True special_range_names = { "disabled": 0, "normal": 50, @@ -1211,29 +1232,36 @@ def as_dict(self, *option_names: str, casing: str = "snake") -> typing.Dict[str, class LocalItems(ItemSet): """Forces these items to be in their native world.""" display_name = "Local Items" + rich_text_doc = True class NonLocalItems(ItemSet): """Forces these items to be outside their native world.""" display_name = "Non-local Items" + rich_text_doc = True class StartInventory(ItemDict): """Start with these items.""" verify_item_name = True display_name = "Start Inventory" + rich_text_doc = True class StartInventoryPool(StartInventory): """Start with these items and don't place them in the world. - The game decides what the replacement items will be.""" + + The game decides what the replacement items will be. + """ verify_item_name = True display_name = "Start Inventory from Pool" + rich_text_doc = True class StartHints(ItemSet): - """Start with these item's locations prefilled into the !hint command.""" + """Start with these item's locations prefilled into the ``!hint`` command.""" display_name = "Start Hints" + rich_text_doc = True class LocationSet(OptionSet): @@ -1242,28 +1270,33 @@ class LocationSet(OptionSet): class StartLocationHints(LocationSet): - """Start with these locations and their item prefilled into the !hint command""" + """Start with these locations and their item prefilled into the ``!hint`` command.""" display_name = "Start Location Hints" + rich_text_doc = True class ExcludeLocations(LocationSet): - """Prevent these locations from having an important item""" + """Prevent these locations from having an important item.""" display_name = "Excluded Locations" + rich_text_doc = True class PriorityLocations(LocationSet): - """Prevent these locations from having an unimportant item""" + """Prevent these locations from having an unimportant item.""" display_name = "Priority Locations" + rich_text_doc = True class DeathLink(Toggle): """When you die, everyone dies. Of course the reverse is true too.""" display_name = "Death Link" + rich_text_doc = True class ItemLinks(OptionList): """Share part of your item pool with other players.""" display_name = "Item Links" + rich_text_doc = True default = [] schema = Schema([ { @@ -1330,6 +1363,7 @@ def verify(self, world: typing.Type[World], player_name: str, plando_options: "P class Removed(FreeText): """This Option has been Removed.""" + rich_text_doc = True default = "" visibility = Visibility.none diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 53c3a6151b82..33339daa1983 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -3,6 +3,7 @@ import os from textwrap import dedent from typing import Dict, Union +from docutils.core import publish_parts import yaml from flask import redirect, render_template, request, Response @@ -66,6 +67,22 @@ def filter_dedent(text: str) -> str: return dedent(text).strip("\n ") +@app.template_filter("rst_to_html") +def filter_rst_to_html(text: str) -> str: + """Converts reStructuredText (such as a Python docstring) to HTML.""" + if text.startswith(" ") or text.startswith("\t"): + text = dedent(text) + elif "\n" in text: + lines = text.splitlines() + text = lines[0] + "\n" + dedent("\n".join(lines[1:])) + + return publish_parts(text, writer_name='html', settings=None, settings_overrides={ + 'raw_enable': False, + 'file_insertion_enabled': False, + 'output_encoding': 'unicode' + })['body'] + + @app.template_test("ordered") def test_ordered(obj): return isinstance(obj, collections.abc.Sequence) diff --git a/WebHostLib/static/styles/tooltip.css b/WebHostLib/static/styles/tooltip.css index 02992b188bc9..dc9026ce6c3d 100644 --- a/WebHostLib/static/styles/tooltip.css +++ b/WebHostLib/static/styles/tooltip.css @@ -12,12 +12,12 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top, */ /* Base styles for the element that has a tooltip */ -[data-tooltip], .tooltip { +[data-tooltip], .tooltip-container { position: relative; } /* Base styles for the entire tooltip */ -[data-tooltip]:before, [data-tooltip]:after, .tooltip:before, .tooltip:after { +[data-tooltip]:before, [data-tooltip]:after, .tooltip-container:before, .tooltip { position: absolute; visibility: hidden; opacity: 0; @@ -39,14 +39,15 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top, pointer-events: none; } -[data-tooltip]:hover:before, [data-tooltip]:hover:after, .tooltip:hover:before, .tooltip:hover:after{ +[data-tooltip]:hover:before, [data-tooltip]:hover:after, .tooltip-container:hover:before, +.tooltip-container:hover .tooltip { visibility: visible; opacity: 1; word-break: break-word; } /** Directional arrow styles */ -.tooltip:before, [data-tooltip]:before { +[data-tooltip]:before, .tooltip-container:before { z-index: 10000; border: 6px solid transparent; background: transparent; @@ -54,7 +55,7 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top, } /** Content styles */ -.tooltip:after, [data-tooltip]:after { +[data-tooltip]:after, .tooltip { width: 260px; z-index: 10000; padding: 8px; @@ -63,24 +64,26 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top, background-color: hsla(0, 0%, 20%, 0.9); color: #fff; content: attr(data-tooltip); - white-space: pre-wrap; font-size: 14px; line-height: 1.2; } -[data-tooltip]:before, [data-tooltip]:after{ +[data-tooltip]:after { + white-space: pre-wrap; +} + +[data-tooltip]:before, [data-tooltip]:after, .tooltip-container:before, .tooltip { visibility: hidden; opacity: 0; pointer-events: none; } -[data-tooltip]:before, [data-tooltip]:after, .tooltip:before, .tooltip:after, -.tooltip-top:before, .tooltip-top:after { +[data-tooltip]:before, [data-tooltip]:after, .tooltip-container:before, .tooltip { bottom: 100%; left: 50%; } -[data-tooltip]:before, .tooltip:before, .tooltip-top:before { +[data-tooltip]:before, .tooltip-container:before { margin-left: -6px; margin-bottom: -12px; border-top-color: #000; @@ -88,19 +91,19 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top, } /** Horizontally align tooltips on the top and bottom */ -[data-tooltip]:after, .tooltip:after, .tooltip-top:after { +[data-tooltip]:after, .tooltip { margin-left: -80px; } -[data-tooltip]:hover:before, [data-tooltip]:hover:after, .tooltip:hover:before, .tooltip:hover:after, -.tooltip-top:hover:before, .tooltip-top:hover:after { +[data-tooltip]:hover:before, [data-tooltip]:hover:after, .tooltip-container:hover:before, +.tooltip-container:hover .tooltip { -webkit-transform: translateY(-12px); -moz-transform: translateY(-12px); transform: translateY(-12px); } /** Tooltips on the left */ -.tooltip-left:before, .tooltip-left:after { +.tooltip-left:before, [data-tooltip].tooltip-left:after, .tooltip-left .tooltip { right: 100%; bottom: 50%; left: auto; @@ -115,14 +118,14 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top, border-left-color: hsla(0, 0%, 20%, 0.9); } -.tooltip-left:hover:before, .tooltip-left:hover:after { +.tooltip-left:hover:before, [data-tooltip].tooltip-left:hover:after, .tooltip-left:hover .tooltip { -webkit-transform: translateX(-12px); -moz-transform: translateX(-12px); transform: translateX(-12px); } /** Tooltips on the bottom */ -.tooltip-bottom:before, .tooltip-bottom:after { +.tooltip-bottom:before, [data-tooltip].tooltip-bottom:after, .tooltip-bottom .tooltip { top: 100%; bottom: auto; left: 50%; @@ -136,14 +139,15 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top, border-bottom-color: hsla(0, 0%, 20%, 0.9); } -.tooltip-bottom:hover:before, .tooltip-bottom:hover:after { +.tooltip-bottom:hover:before, [data-tooltip].tooltip-bottom:hover:after, +.tooltip-bottom:hover .tooltip { -webkit-transform: translateY(12px); -moz-transform: translateY(12px); transform: translateY(12px); } /** Tooltips on the right */ -.tooltip-right:before, .tooltip-right:after { +.tooltip-right:before, [data-tooltip].tooltip-right:after, .tooltip-right .tooltip { bottom: 50%; left: 100%; } @@ -156,7 +160,8 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top, border-right-color: hsla(0, 0%, 20%, 0.9); } -.tooltip-right:hover:before, .tooltip-right:hover:after { +.tooltip-right:hover:before, [data-tooltip].tooltip-right:hover:after, +.tooltip-right:hover .tooltip { -webkit-transform: translateX(12px); -moz-transform: translateX(12px); transform: translateX(12px); @@ -168,7 +173,16 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top, } /** Center content vertically for tooltips ont he left and right */ -.tooltip-left:after, .tooltip-right:after { +[data-tooltip].tooltip-left:after, [data-tooltip].tooltip-right:after, +.tooltip-left .tooltip, .tooltip-right .tooltip { margin-left: 0; margin-bottom: -16px; } + +.tooltip ul, .tooltip ol { + padding-left: 1rem; +} + +.tooltip :last-child { + margin-bottom: 0; +} diff --git a/WebHostLib/templates/playerOptions/macros.html b/WebHostLib/templates/playerOptions/macros.html index 76cf07699456..415739b861a1 100644 --- a/WebHostLib/templates/playerOptions/macros.html +++ b/WebHostLib/templates/playerOptions/macros.html @@ -111,7 +111,7 @@ {% endmacro %} -{% macro ItemDict(option_name, option, world) %} +{% macro ItemDict(option_name, option) %} {{ OptionTitle(option_name, option) }}
{% for item_name in (option.valid_keys|sort if (option.valid_keys|length > 0) else world.item_names|sort) %} @@ -135,7 +135,7 @@
{% endmacro %} -{% macro LocationSet(option_name, option, world) %} +{% macro LocationSet(option_name, option) %} {{ OptionTitle(option_name, option) }}
{% for group_name in world.location_name_groups.keys()|sort %} @@ -158,7 +158,7 @@
{% endmacro %} -{% macro ItemSet(option_name, option, world) %} +{% macro ItemSet(option_name, option) %} {{ OptionTitle(option_name, option) }}
{% for group_name in world.item_name_groups.keys()|sort %} @@ -196,7 +196,18 @@ {% macro OptionTitle(option_name, option) %} {% endmacro %} diff --git a/WebHostLib/templates/playerOptions/playerOptions.html b/WebHostLib/templates/playerOptions/playerOptions.html index 2506cf9619e6..aeb6e864a54b 100644 --- a/WebHostLib/templates/playerOptions/playerOptions.html +++ b/WebHostLib/templates/playerOptions/playerOptions.html @@ -1,5 +1,5 @@ {% extends 'pageWrapper.html' %} -{% import 'playerOptions/macros.html' as inputs %} +{% import 'playerOptions/macros.html' as inputs with context %} {% block head %} {{ world_name }} Options @@ -94,16 +94,16 @@

Player Options

{{ inputs.FreeText(option_name, option) }} {% elif issubclass(option, Options.ItemDict) and option.verify_item_name %} - {{ inputs.ItemDict(option_name, option, world) }} + {{ inputs.ItemDict(option_name, option) }} {% elif issubclass(option, Options.OptionList) and option.valid_keys %} {{ inputs.OptionList(option_name, option) }} {% elif issubclass(option, Options.LocationSet) and option.verify_location_name %} - {{ inputs.LocationSet(option_name, option, world) }} + {{ inputs.LocationSet(option_name, option) }} {% elif issubclass(option, Options.ItemSet) and option.verify_item_name %} - {{ inputs.ItemSet(option_name, option, world) }} + {{ inputs.ItemSet(option_name, option) }} {% elif issubclass(option, Options.OptionSet) and option.valid_keys %} {{ inputs.OptionSet(option_name, option) }} @@ -134,16 +134,16 @@

Player Options

{{ inputs.FreeText(option_name, option) }} {% elif issubclass(option, Options.ItemDict) and option.verify_item_name %} - {{ inputs.ItemDict(option_name, option, world) }} + {{ inputs.ItemDict(option_name, option) }} {% elif issubclass(option, Options.OptionList) and option.valid_keys %} {{ inputs.OptionList(option_name, option) }} {% elif issubclass(option, Options.LocationSet) and option.verify_location_name %} - {{ inputs.LocationSet(option_name, option, world) }} + {{ inputs.LocationSet(option_name, option) }} {% elif issubclass(option, Options.ItemSet) and option.verify_item_name %} - {{ inputs.ItemSet(option_name, option, world) }} + {{ inputs.ItemSet(option_name, option) }} {% elif issubclass(option, Options.OptionSet) and option.valid_keys %} {{ inputs.OptionSet(option_name, option) }} diff --git a/docs/options api.md b/docs/options api.md index cba383232b67..7e479809ee6a 100644 --- a/docs/options api.md +++ b/docs/options api.md @@ -85,6 +85,50 @@ class ExampleWorld(World): options: ExampleGameOptions ``` +### Option Documentation + +Options' [docstrings] are used as their user-facing documentation. They're displayed on the WebHost setup page when a +user hovers over the yellow "(?)" icon, and included in the YAML templates generated for each game. + +[docstrings]: /docs/world%20api.md#docstrings + +The WebHost can display Option documentation either as plain text with all whitespace preserved (other than the base +indentation), or as HTML generated from the standard Python [reStructuredText] format. Although plain text is the +default for backwards compatibility, world authors are encouraged to write their Option documentation as +reStructuredText and enable rich text rendering by setting `World.rich_text_options_doc = True`. + +[reStructuredText]: https://docutils.sourceforge.io/rst.html + +```python +from worlds.AutoWorld import WebWorld + + +class ExampleWebWorld(WebWorld): + # Render all this world's options as rich text. + rich_text_options_doc = True +``` + +You can set a single option to use rich or plain text by setting +`Option.rich_text_doc`. + +```python +from Options import Toggle, Range, Choice, PerGameCommonOptions + + +class Difficulty(Choice): + """Sets overall game difficulty. + + - **Easy:** All enemies die in one hit. + - **Normal:** Enemies and the player both have normal health bars. + - **Hard:** The player dies in one hit.""" + display_name = "Difficulty" + rich_text_doc = True + option_easy = 0 + option_normal = 1 + option_hard = 2 + default = 1 +``` + ### Option Groups Options may be categorized into groups for display on the WebHost. Option groups are displayed in the order specified by your world on the player-options and weighted-options pages. In the generated template files, there will be a comment diff --git a/docs/world api.md b/docs/world api.md index 37638c3c66cc..756ef3f31fdd 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -56,6 +56,12 @@ webhost: * `options_page` can be changed to a link instead of an AP-generated options page. +* `rich_text_options_doc` controls whether [Option documentation] uses plain text (`False`) or rich text (`True`). It + defaults to `False`, but world authors are encouraged to set it to `True` for nicer-looking documentation that looks + good on both the WebHost and the YAML template. + + [Option documentation]: /docs/options%20api.md#option-documentation + * `theme` to be used for your game-specific AP pages. Available themes: | dirt | grass (default) | grassFlowers | ice | jungle | ocean | partyTime | stone | diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index bed375cf080a..b5f0fd172053 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -223,6 +223,21 @@ class WebWorld(metaclass=WebWorldRegister): option_groups: ClassVar[List[OptionGroup]] = [] """Ordered list of option groupings. Any options not set in a group will be placed in a pre-built "Game Options".""" + rich_text_options_doc = False + """Whether the WebHost should render Options' docstrings as rich text. + + If this is True, Options' docstrings are interpreted as reStructuredText_, + the standard Python markup format. In the WebHost, they're rendered to HTML + so that lists, emphasis, and other rich text features are displayed + properly. + + If this is False, the docstrings are instead interpreted as plain text, and + displayed as-is on the WebHost with whitespace preserved. For backwards + compatibility, this is the default. + + .. _reStructuredText: https://docutils.sourceforge.io/rst.html + """ + location_descriptions: Dict[str, str] = {} """An optional map from location names (or location group names) to brief descriptions for users.""" diff --git a/worlds/__init__.py b/worlds/__init__.py index 8d784a5ba438..bb2fe866d02d 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -128,3 +128,4 @@ def load(self) -> bool: network_data_package: DataPackage = { "games": {world_name: world.get_data_package_data() for world_name, world in AutoWorldRegister.world_types.items()}, } + diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py index 302e7e1d85b2..8d6a7fc4ebee 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -16,6 +16,7 @@ class LingoWebWorld(WebWorld): option_groups = lingo_option_groups + rich_text_options_doc = True theme = "grass" tutorials = [Tutorial( "Multiworld Setup Guide", diff --git a/worlds/lingo/options.py b/worlds/lingo/options.py index 1c1f645b8613..333b3e1ef08e 100644 --- a/worlds/lingo/options.py +++ b/worlds/lingo/options.py @@ -9,8 +9,12 @@ class ShuffleDoors(Choice): """If on, opening doors will require their respective "keys". - In "simple", doors are sorted into logical groups, which are all opened by receiving an item. - In "complex", the items are much more granular, and will usually only open a single door each.""" + + - **Simple:** Doors are sorted into logical groups, which are all opened by + receiving an item. + - **Complex:** The items are much more granular, and will usually only open + a single door each. + """ display_name = "Shuffle Doors" option_none = 0 option_simple = 1 @@ -19,24 +23,37 @@ class ShuffleDoors(Choice): class ProgressiveOrangeTower(DefaultOnToggle): """When "Shuffle Doors" is on, this setting governs the manner in which the Orange Tower floors open up. - If off, there is an item for each floor of the tower, and each floor's item is the only one needed to access that floor. - If on, there are six progressive items, which open up the tower from the bottom floor upward. + + - **Off:** There is an item for each floor of the tower, and each floor's + item is the only one needed to access that floor. + - **On:** There are six progressive items, which open up the tower from the + bottom floor upward. """ display_name = "Progressive Orange Tower" class ProgressiveColorful(DefaultOnToggle): """When "Shuffle Doors" is on "complex", this setting governs the manner in which The Colorful opens up. - If off, there is an item for each room of The Colorful, meaning that random rooms in the middle of the sequence can open up without giving you access to them. - If on, there are ten progressive items, which open up the sequence from White forward.""" + + - **Off:** There is an item for each room of The Colorful, meaning that + random rooms in the middle of the sequence can open up without giving you + access to them. + - **On:** There are ten progressive items, which open up the sequence from + White forward. + """ display_name = "Progressive Colorful" class LocationChecks(Choice): """Determines what locations are available. - On "normal", there will be a location check for each panel set that would ordinarily open a door, as well as for achievement panels and a small handful of other panels. - On "reduced", many of the locations that are associated with opening doors are removed. - On "insanity", every individual panel in the game is a location check.""" + + - **Normal:** There will be a location check for each panel set that would + ordinarily open a door, as well as for achievement panels and a small + handful of other panels. + - **Reduced:** Many of the locations that are associated with opening doors + are removed. + - **Insanity:** Every individual panel in the game is a location check. + """ display_name = "Location Checks" option_normal = 0 option_reduced = 1 @@ -44,16 +61,20 @@ class LocationChecks(Choice): class ShuffleColors(DefaultOnToggle): - """ - If on, an item is added to the pool for every puzzle color (besides White). - You will need to unlock the requisite colors in order to be able to solve puzzles of that color. + """If on, an item is added to the pool for every puzzle color (besides White). + + You will need to unlock the requisite colors in order to be able to solve + puzzles of that color. """ display_name = "Shuffle Colors" class ShufflePanels(Choice): """If on, the puzzles on each panel are randomized. - On "rearrange", the puzzles are the same as the ones in the base game, but are placed in different areas.""" + + On "rearrange", the puzzles are the same as the ones in the base game, but + are placed in different areas. + """ display_name = "Shuffle Panels" option_none = 0 option_rearrange = 1 @@ -66,22 +87,26 @@ class ShufflePaintings(Toggle): class EnablePilgrimage(Toggle): """Determines how the pilgrimage works. - If on, you are required to complete a pilgrimage in order to access the Pilgrim Antechamber. - If off, the pilgrimage will be deactivated, and the sun painting will be added to the pool, even if door shuffle is off.""" + + - **On:** You are required to complete a pilgrimage in order to access the + Pilgrim Antechamber. + - **Off:** The pilgrimage will be deactivated, and the sun painting will be + added to the pool, even if door shuffle is off. + """ display_name = "Enable Pilgrimage" class PilgrimageAllowsRoofAccess(DefaultOnToggle): - """ - If on, you may use the Crossroads roof access during a pilgrimage (and you may be expected to do so). + """If on, you may use the Crossroads roof access during a pilgrimage (and you may be expected to do so). + Otherwise, pilgrimage will be deactivated when going up the stairs. """ display_name = "Allow Roof Access for Pilgrimage" class PilgrimageAllowsPaintings(DefaultOnToggle): - """ - If on, you may use paintings during a pilgrimage (and you may be expected to do so). + """If on, you may use paintings during a pilgrimage (and you may be expected to do so). + Otherwise, pilgrimage will be deactivated when going through a painting. """ display_name = "Allow Paintings for Pilgrimage" @@ -89,11 +114,17 @@ class PilgrimageAllowsPaintings(DefaultOnToggle): class SunwarpAccess(Choice): """Determines how access to sunwarps works. - On "normal", all sunwarps are enabled from the start. - On "disabled", all sunwarps are disabled. Pilgrimage must be disabled when this is used. - On "unlock", sunwarps start off disabled, and all six activate once you receive an item. - On "individual", sunwarps start off disabled, and each has a corresponding item that unlocks it. - On "progressive", sunwarps start off disabled, and they unlock in order using a progressive item.""" + + - **Normal:** All sunwarps are enabled from the start. + - **Disabled:** All sunwarps are disabled. Pilgrimage must be disabled when + this is used. + - **Unlock:** Sunwarps start off disabled, and all six activate once you + receive an item. + - **Individual:** Sunwarps start off disabled, and each has a corresponding + item that unlocks it. + - **Progressive:** Sunwarps start off disabled, and they unlock in order + using a progressive item. + """ display_name = "Sunwarp Access" option_normal = 0 option_disabled = 1 @@ -109,10 +140,16 @@ class ShuffleSunwarps(Toggle): class VictoryCondition(Choice): """Change the victory condition. - On "the_end", the goal is to solve THE END at the top of the tower. - On "the_master", the goal is to solve THE MASTER at the top of the tower, after getting the number of achievements specified in the Mastery Achievements option. - On "level_2", the goal is to solve LEVEL 2 in the second room, after solving the number of panels specified in the Level 2 Requirement option. - On "pilgrimage", the goal is to solve PILGRIM in the Pilgrim Antechamber, typically after performing a Pilgrimage.""" + + - **The End:** the goal is to solve THE END at the top of the tower. + - **The Master:** The goal is to solve THE MASTER at the top of the tower, + after getting the number of achievements specified in the Mastery + Achievements option. + - **Level 2:** The goal is to solve LEVEL 2 in the second room, after + solving the number of panels specified in the Level 2 Requirement option. + - **Pilgrimage:** The goal is to solve PILGRIM in the Pilgrim Antechamber, + typically after performing a Pilgrimage. + """ display_name = "Victory Condition" option_the_end = 0 option_the_master = 1 @@ -122,9 +159,12 @@ class VictoryCondition(Choice): class MasteryAchievements(Range): """The number of achievements required to unlock THE MASTER. - In the base game, 21 achievements are needed. - If you include The Scientific and The Unchallenged, which are in the base game but are not counted for mastery, 23 would be required. - If you include the custom achievement (The Wanderer), 24 would be required. + + - In the base game, 21 achievements are needed. + - If you include The Scientific and The Unchallenged, which are in the base + game but are not counted for mastery, 23 would be required. + - If you include the custom achievement (The Wanderer), 24 would be + required. """ display_name = "Mastery Achievements" range_start = 1 @@ -134,9 +174,10 @@ class MasteryAchievements(Range): class Level2Requirement(Range): """The number of panel solves required to unlock LEVEL 2. - In the base game, 223 are needed. - Note that this count includes ANOTHER TRY. - When set to 1, the panel hunt is disabled, and you can access LEVEL 2 for free. + + In the base game, 223 are needed. Note that this count includes ANOTHER TRY. + When set to 1, the panel hunt is disabled, and you can access LEVEL 2 for + free. """ display_name = "Level 2 Requirement" range_start = 1 @@ -145,9 +186,10 @@ class Level2Requirement(Range): class EarlyColorHallways(Toggle): - """ - When on, a painting warp to the color hallways area will appear in the starting room. - This lets you avoid being trapped in the starting room for long periods of time when door shuffle is on. + """When on, a painting warp to the color hallways area will appear in the starting room. + + This lets you avoid being trapped in the starting room for long periods of + time when door shuffle is on. """ display_name = "Early Color Hallways" @@ -161,8 +203,8 @@ class TrapPercentage(Range): class TrapWeights(OptionDict): - """ - Specify the distribution of traps that should be placed into the pool. + """Specify the distribution of traps that should be placed into the pool. + If you don't want a specific type of trap, set the weight to zero. """ display_name = "Trap Weights"