diff --git a/Options.py b/Options.py index cec6b054..f8c19edd 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 @@ class PlandoConnections(Option[typing.List[PlandoConnection]], metaclass=Connect 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 @@ class CommonOptions(metaclass=OptionsMetaProperty): 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 @@ class ItemLinks(OptionList): 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 53c3a615..33339daa 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -3,6 +3,7 @@ import json 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 02992b18..dc9026ce 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 76cf0769..415739b8 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 2506cf96..aeb6e864 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 @@ {{ 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 @@ {{ 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 cba38323..7e479809 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 37638c3c..756ef3f3 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 bed375cf..b5f0fd17 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 8d784a5b..bb2fe866 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -128,3 +128,4 @@ from .AutoWorld import AutoWorldRegister 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 302e7e1d..8d6a7fc4 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -16,6 +16,7 @@ from .regions import create_regions 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 1c1f645b..333b3e1e 100644 --- a/worlds/lingo/options.py +++ b/worlds/lingo/options.py @@ -9,8 +9,12 @@ from .items import TRAP_ITEMS 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"