WebHost, Core: Developer-defined game option presets. (#2143)
This commit is contained in:
parent
3619abc7ca
commit
79ad54623b
|
@ -3,11 +3,8 @@ import logging
|
|||
import os
|
||||
import typing
|
||||
|
||||
import yaml
|
||||
from jinja2 import Template
|
||||
|
||||
import Options
|
||||
from Utils import __version__, local_path
|
||||
from Utils import local_path
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
|
||||
handled_in_js = {"start_inventory", "local_items", "non_local_items", "start_hints", "start_location_hints",
|
||||
|
@ -28,7 +25,7 @@ def create():
|
|||
weighted_options = {
|
||||
"baseOptions": {
|
||||
"description": "Generated by https://archipelago.gg/",
|
||||
"name": "Player",
|
||||
"name": "",
|
||||
"game": {},
|
||||
},
|
||||
"games": {},
|
||||
|
@ -43,7 +40,7 @@ def create():
|
|||
"baseOptions": {
|
||||
"description": f"Generated by https://archipelago.gg/ for {game_name}",
|
||||
"game": game_name,
|
||||
"name": "Player",
|
||||
"name": "",
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -117,10 +114,46 @@ def create():
|
|||
}
|
||||
|
||||
else:
|
||||
logging.debug(f"{option} not exported to Web options.")
|
||||
logging.debug(f"{option} not exported to Web Options.")
|
||||
|
||||
player_options["gameOptions"] = game_options
|
||||
|
||||
player_options["presetOptions"] = {}
|
||||
for preset_name, preset in world.web.options_presets.items():
|
||||
player_options["presetOptions"][preset_name] = {}
|
||||
for option_name, option_value in preset.items():
|
||||
# Random range type settings are not valid.
|
||||
assert (not str(option_value).startswith("random-")), \
|
||||
f"Invalid preset value '{option_value}' for '{option_name}' in '{preset_name}'. Special random " \
|
||||
f"values are not supported for presets."
|
||||
|
||||
# Normal random is supported, but needs to be handled explicitly.
|
||||
if option_value == "random":
|
||||
player_options["presetOptions"][preset_name][option_name] = option_value
|
||||
continue
|
||||
|
||||
option = world.options_dataclass.type_hints[option_name].from_any(option_value)
|
||||
if isinstance(option, Options.SpecialRange) and isinstance(option_value, str):
|
||||
assert option_value in option.special_range_names, \
|
||||
f"Invalid preset value '{option_value}' for '{option_name}' in '{preset_name}'. " \
|
||||
f"Expected {option.special_range_names.keys()} or {option.range_start}-{option.range_end}."
|
||||
|
||||
# Still use the true value for the option, not the name.
|
||||
player_options["presetOptions"][preset_name][option_name] = option.value
|
||||
elif isinstance(option, Options.Range):
|
||||
player_options["presetOptions"][preset_name][option_name] = option.value
|
||||
elif isinstance(option_value, str):
|
||||
# For Choice and Toggle options, the value should be the name of the option. This is to prevent
|
||||
# setting a preset for an option with an overridden from_text method that would normally be okay,
|
||||
# but would not be okay for the webhost's current implementation of player options UI.
|
||||
assert option.name_lookup[option.value] == option_value, \
|
||||
f"Invalid option value '{option_value}' for '{option_name}' in preset '{preset_name}'. " \
|
||||
f"Values must not be resolved to a different option via option.from_text (or an alias)."
|
||||
player_options["presetOptions"][preset_name][option_name] = option.current_key
|
||||
else:
|
||||
# int and bool values are fine, just resolve them to the current key for webhost.
|
||||
player_options["presetOptions"][preset_name][option_name] = option.current_key
|
||||
|
||||
os.makedirs(os.path.join(target_folder, 'player-options'), exist_ok=True)
|
||||
|
||||
with open(os.path.join(target_folder, 'player-options', game_name + ".json"), "w") as f:
|
||||
|
|
|
@ -16,8 +16,9 @@ window.addEventListener('load', () => {
|
|||
}
|
||||
|
||||
if (optionHash !== md5(JSON.stringify(results))) {
|
||||
showUserMessage("Your options are out of date! Click here to update them! Be aware this will reset " +
|
||||
"them all to default.");
|
||||
showUserMessage(
|
||||
'Your options are out of date! Click here to update them! Be aware this will reset them all to default.'
|
||||
);
|
||||
document.getElementById('user-message').addEventListener('click', resetOptions);
|
||||
}
|
||||
|
||||
|
@ -36,6 +37,17 @@ window.addEventListener('load', () => {
|
|||
const nameInput = document.getElementById('player-name');
|
||||
nameInput.addEventListener('keyup', (event) => updateBaseOption(event));
|
||||
nameInput.value = playerOptions.name;
|
||||
|
||||
// Presets
|
||||
const presetSelect = document.getElementById('game-options-preset');
|
||||
presetSelect.addEventListener('change', (event) => setPresets(results, event.target.value));
|
||||
for (const preset in results['presetOptions']) {
|
||||
const presetOption = document.createElement('option');
|
||||
presetOption.innerText = preset;
|
||||
presetSelect.appendChild(presetOption);
|
||||
}
|
||||
presetSelect.value = localStorage.getItem(`${gameName}-preset`);
|
||||
results['presetOptions']['__default'] = {};
|
||||
}).catch((e) => {
|
||||
console.error(e);
|
||||
const url = new URL(window.location.href);
|
||||
|
@ -45,7 +57,8 @@ window.addEventListener('load', () => {
|
|||
|
||||
const resetOptions = () => {
|
||||
localStorage.removeItem(gameName);
|
||||
localStorage.removeItem(`${gameName}-hash`)
|
||||
localStorage.removeItem(`${gameName}-hash`);
|
||||
localStorage.removeItem(`${gameName}-preset`);
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
|
@ -77,6 +90,10 @@ const createDefaultOptions = (optionData) => {
|
|||
}
|
||||
localStorage.setItem(gameName, JSON.stringify(newOptions));
|
||||
}
|
||||
|
||||
if (!localStorage.getItem(`${gameName}-preset`)) {
|
||||
localStorage.setItem(`${gameName}-preset`, '__default');
|
||||
}
|
||||
};
|
||||
|
||||
const buildUI = (optionData) => {
|
||||
|
@ -84,8 +101,11 @@ const buildUI = (optionData) => {
|
|||
const leftGameOpts = {};
|
||||
const rightGameOpts = {};
|
||||
Object.keys(optionData.gameOptions).forEach((key, index) => {
|
||||
if (index < Object.keys(optionData.gameOptions).length / 2) { leftGameOpts[key] = optionData.gameOptions[key]; }
|
||||
else { rightGameOpts[key] = optionData.gameOptions[key]; }
|
||||
if (index < Object.keys(optionData.gameOptions).length / 2) {
|
||||
leftGameOpts[key] = optionData.gameOptions[key];
|
||||
} else {
|
||||
rightGameOpts[key] = optionData.gameOptions[key];
|
||||
}
|
||||
});
|
||||
document.getElementById('game-options-left').appendChild(buildOptionsTable(leftGameOpts));
|
||||
document.getElementById('game-options-right').appendChild(buildOptionsTable(rightGameOpts));
|
||||
|
@ -129,16 +149,17 @@ const buildOptionsTable = (options, romOpts = false) => {
|
|||
select.setAttribute('data-key', option);
|
||||
if (romOpts) { select.setAttribute('data-romOpt', '1'); }
|
||||
options[option].options.forEach((opt) => {
|
||||
const option = document.createElement('option');
|
||||
option.setAttribute('value', opt.value);
|
||||
option.innerText = opt.name;
|
||||
const optionElement = document.createElement('option');
|
||||
optionElement.setAttribute('value', opt.value);
|
||||
optionElement.innerText = opt.name;
|
||||
|
||||
if ((isNaN(currentOptions[gameName][option]) &&
|
||||
(parseInt(opt.value, 10) === parseInt(currentOptions[gameName][option]))) ||
|
||||
(opt.value === currentOptions[gameName][option]))
|
||||
{
|
||||
option.selected = true;
|
||||
optionElement.selected = true;
|
||||
}
|
||||
select.appendChild(option);
|
||||
select.appendChild(optionElement);
|
||||
});
|
||||
select.addEventListener('change', (event) => updateGameOption(event.target));
|
||||
element.appendChild(select);
|
||||
|
@ -162,6 +183,7 @@ const buildOptionsTable = (options, romOpts = false) => {
|
|||
element.classList.add('range-container');
|
||||
|
||||
let range = document.createElement('input');
|
||||
range.setAttribute('id', option);
|
||||
range.setAttribute('type', 'range');
|
||||
range.setAttribute('data-key', option);
|
||||
range.setAttribute('min', options[option].min);
|
||||
|
@ -205,11 +227,11 @@ const buildOptionsTable = (options, romOpts = false) => {
|
|||
let presetOption = document.createElement('option');
|
||||
presetOption.innerText = presetName;
|
||||
presetOption.value = options[option].value_names[presetName];
|
||||
const words = presetOption.innerText.split("_");
|
||||
const words = presetOption.innerText.split('_');
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
words[i] = words[i][0].toUpperCase() + words[i].substring(1);
|
||||
}
|
||||
presetOption.innerText = words.join(" ");
|
||||
presetOption.innerText = words.join(' ');
|
||||
specialRangeSelect.appendChild(presetOption);
|
||||
});
|
||||
let customOption = document.createElement('option');
|
||||
|
@ -294,6 +316,90 @@ const buildOptionsTable = (options, romOpts = false) => {
|
|||
return table;
|
||||
};
|
||||
|
||||
const setPresets = (optionsData, presetName) => {
|
||||
const defaults = optionsData['gameOptions'];
|
||||
const preset = optionsData['presetOptions'][presetName];
|
||||
|
||||
localStorage.setItem(`${gameName}-preset`, presetName);
|
||||
|
||||
if (!preset) {
|
||||
console.error(`No presets defined for preset name: '${presetName}'`);
|
||||
return;
|
||||
}
|
||||
|
||||
const updateOptionElement = (option, presetValue) => {
|
||||
const optionElement = document.querySelector(`#${option}[data-key='${option}']`);
|
||||
const randomElement = document.querySelector(`.randomize-button[data-key='${option}']`);
|
||||
|
||||
if (presetValue === 'random') {
|
||||
randomElement.classList.add('active');
|
||||
optionElement.disabled = true;
|
||||
updateGameOption(randomElement, false);
|
||||
} else {
|
||||
optionElement.value = presetValue;
|
||||
randomElement.classList.remove('active');
|
||||
optionElement.disabled = undefined;
|
||||
updateGameOption(optionElement, false);
|
||||
}
|
||||
};
|
||||
|
||||
for (const option in defaults) {
|
||||
let presetValue = preset[option];
|
||||
if (presetValue === undefined) {
|
||||
// Using the default value if not set in presets.
|
||||
presetValue = defaults[option]['defaultValue'];
|
||||
}
|
||||
|
||||
switch (defaults[option].type) {
|
||||
case 'range':
|
||||
const numberElement = document.querySelector(`#${option}-value`);
|
||||
if (presetValue === 'random') {
|
||||
numberElement.innerText = defaults[option]['defaultValue'] === 'random'
|
||||
? defaults[option]['min'] // A fallback so we don't print 'random' in the UI.
|
||||
: defaults[option]['defaultValue'];
|
||||
} else {
|
||||
numberElement.innerText = presetValue;
|
||||
}
|
||||
|
||||
updateOptionElement(option, presetValue);
|
||||
break;
|
||||
|
||||
case 'select': {
|
||||
updateOptionElement(option, presetValue);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'special_range': {
|
||||
const selectElement = document.querySelector(`select[data-key='${option}']`);
|
||||
const rangeElement = document.querySelector(`input[data-key='${option}']`);
|
||||
const randomElement = document.querySelector(`.randomize-button[data-key='${option}']`);
|
||||
|
||||
if (presetValue === 'random') {
|
||||
randomElement.classList.add('active');
|
||||
selectElement.disabled = true;
|
||||
rangeElement.disabled = true;
|
||||
updateGameOption(randomElement, false);
|
||||
} else {
|
||||
rangeElement.value = presetValue;
|
||||
selectElement.value = Object.values(defaults[option]['value_names']).includes(parseInt(presetValue)) ?
|
||||
parseInt(presetValue) : 'custom';
|
||||
document.getElementById(`${option}-value`).innerText = presetValue;
|
||||
|
||||
randomElement.classList.remove('active');
|
||||
selectElement.disabled = undefined;
|
||||
rangeElement.disabled = undefined;
|
||||
updateGameOption(rangeElement, false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
console.warn(`Ignoring preset value for unknown option type: ${defaults[option].type} with name ${option}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toggleRandomize = (event, inputElement, optionalSelectElement = null) => {
|
||||
const active = event.target.classList.contains('active');
|
||||
const randomButton = event.target;
|
||||
|
@ -321,8 +427,15 @@ const updateBaseOption = (event) => {
|
|||
localStorage.setItem(gameName, JSON.stringify(options));
|
||||
};
|
||||
|
||||
const updateGameOption = (optionElement) => {
|
||||
const updateGameOption = (optionElement, toggleCustomPreset = true) => {
|
||||
const options = JSON.parse(localStorage.getItem(gameName));
|
||||
|
||||
if (toggleCustomPreset) {
|
||||
localStorage.setItem(`${gameName}-preset`, '__custom');
|
||||
const presetElement = document.getElementById('game-options-preset');
|
||||
presetElement.value = '__custom';
|
||||
}
|
||||
|
||||
if (optionElement.classList.contains('randomize-button')) {
|
||||
// If the event passed in is the randomize button, then we know what we must do.
|
||||
options[gameName][optionElement.getAttribute('data-key')] = 'random';
|
||||
|
@ -336,7 +449,21 @@ const updateGameOption = (optionElement) => {
|
|||
|
||||
const exportOptions = () => {
|
||||
const options = JSON.parse(localStorage.getItem(gameName));
|
||||
if (!options.name || options.name.toLowerCase() === 'player' || options.name.trim().length === 0) {
|
||||
const preset = localStorage.getItem(`${gameName}-preset`);
|
||||
switch (preset) {
|
||||
case '__default':
|
||||
options['description'] = `Generated by https://archipelago.gg with the default preset.`;
|
||||
break;
|
||||
|
||||
case '__custom':
|
||||
options['description'] = `Generated by https://archipelago.gg.`;
|
||||
break;
|
||||
|
||||
default:
|
||||
options['description'] = `Generated by https://archipelago.gg with the ${preset} preset.`;
|
||||
}
|
||||
|
||||
if (!options.name || options.name.trim().length === 0) {
|
||||
return showUserMessage('You must enter a player name!');
|
||||
}
|
||||
const yamlText = jsyaml.safeDump(options, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`);
|
||||
|
|
|
@ -90,6 +90,31 @@ html{
|
|||
flex-direction: row;
|
||||
}
|
||||
|
||||
#player-options #meta-options {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
#player-options div {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#player-options #meta-options label {
|
||||
display: inline-block;
|
||||
min-width: 180px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#player-options #meta-options input,
|
||||
#player-options #meta-options select {
|
||||
box-sizing: border-box;
|
||||
min-width: 150px;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
#player-options .left, #player-options .right{
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
@ -188,6 +213,12 @@ html{
|
|||
border-radius: 0;
|
||||
}
|
||||
|
||||
#player-options #meta-options {
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
#player-options #game-options{
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
|
|
|
@ -28,10 +28,24 @@
|
|||
<a href="/static/generated/configs/{{ game }}.yaml">template file for this game</a>.
|
||||
</p>
|
||||
|
||||
<p><label for="player-name">Please enter your player name. This will appear in-game as you send and receive
|
||||
items if you are playing in a MultiWorld.</label><br />
|
||||
<input id="player-name" placeholder="Player Name" data-key="name" maxlength="16" />
|
||||
</p>
|
||||
<div id="meta-options">
|
||||
<div>
|
||||
<label for="player-name">
|
||||
Player Name: <span class="interactive" data-tooltip="This is the name you use to connect with your game. This is also known as your 'slot name'.">(?)</span>
|
||||
</label>
|
||||
<input id="player-name" placeholder="Player" data-key="name" maxlength="16" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="game-options-preset">
|
||||
Options Preset: <span class="interactive" data-tooltip="Select from a list of developer-curated presets (if any) or reset all options to their defaults.">(?)</span>
|
||||
</label>
|
||||
<select id="game-options-preset">
|
||||
<option value="__default">Defaults</option>
|
||||
<option value="__custom" hidden>Custom</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<h2>Game Options</h2>
|
||||
<div id="game-options">
|
||||
|
|
|
@ -73,6 +73,53 @@ for your world specifically on the webhost:
|
|||
`game_info_languages` (optional) List of strings for defining the existing gameinfo pages your game supports. The documents must be
|
||||
prefixed with the same string as defined here. Default already has 'en'.
|
||||
|
||||
`options_presets` (optional) A `Dict[str, Dict[str, Any]]` where the keys are the names of the presets and the values
|
||||
are the options to be set for that preset. The options are defined as a `Dict[str, Any]` where the keys are the names of
|
||||
the options and the values are the values to be set for that option. These presets will be available for users to select from on the game's options page.
|
||||
|
||||
Note: The values must be a non-aliased value for the option type and can only include the following option types:
|
||||
|
||||
- If you have a `Range`/`SpecialRange` option, the value should be an `int` between the `range_start` and `range_end`
|
||||
values.
|
||||
- If you have a `SpecialRange` option, the value can alternatively be a `str` that is one of the
|
||||
`special_range_names` keys.
|
||||
- If you have a `Choice` option, the value should be a `str` that is one of the `option_<name>` values.
|
||||
- If you have a `Toggle`/`DefaultOnToggle` option, the value should be a `bool`.
|
||||
- `random` is also a valid value for any of these option types.
|
||||
|
||||
`OptionDict`, `OptionList`, `OptionSet`, `FreeText`, or custom `Option`-derived classes are not supported for presets on the webhost at this time.
|
||||
|
||||
Here is an example of a defined preset:
|
||||
```python
|
||||
# presets.py
|
||||
options_presets = {
|
||||
"Limited Potential": {
|
||||
"progression_balancing": 0,
|
||||
"fairy_chests_per_zone": 2,
|
||||
"starting_class": "random",
|
||||
"chests_per_zone": 30,
|
||||
"vendors": "normal",
|
||||
"architect": "disabled",
|
||||
"gold_gain_multiplier": "half",
|
||||
"number_of_children": 2,
|
||||
"free_diary_on_generation": False,
|
||||
"health_pool": 10,
|
||||
"mana_pool": 10,
|
||||
"attack_pool": 10,
|
||||
"magic_damage_pool": 10,
|
||||
"armor_pool": 5,
|
||||
"equip_pool": 10,
|
||||
"crit_chance_pool": 5,
|
||||
"crit_damage_pool": 5,
|
||||
}
|
||||
}
|
||||
|
||||
# __init__.py
|
||||
class RLWeb(WebWorld):
|
||||
options_presets = options_presets
|
||||
# ...
|
||||
```
|
||||
|
||||
### MultiWorld Object
|
||||
|
||||
The `MultiWorld` object references the whole multiworld (all items and locations
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
import unittest
|
||||
|
||||
from worlds import AutoWorldRegister
|
||||
from Options import Choice, SpecialRange, Toggle, Range
|
||||
|
||||
|
||||
class TestOptionPresets(unittest.TestCase):
|
||||
def test_option_presets_have_valid_options(self):
|
||||
"""Test that all predefined option presets are valid options."""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
presets = world_type.web.options_presets
|
||||
for preset_name, preset in presets.items():
|
||||
for option_name, option_value in preset.items():
|
||||
with self.subTest(game=game_name, preset=preset_name, option=option_name):
|
||||
try:
|
||||
option = world_type.options_dataclass.type_hints[option_name].from_any(option_value)
|
||||
supported_types = [Choice, Toggle, Range, SpecialRange]
|
||||
if not any([issubclass(option.__class__, t) for t in supported_types]):
|
||||
self.fail(f"'{option_name}' in preset '{preset_name}' for game '{game_name}' "
|
||||
f"is not a supported type for webhost. "
|
||||
f"Supported types: {', '.join([t.__name__ for t in supported_types])}")
|
||||
except AssertionError as ex:
|
||||
self.fail(f"Option '{option_name}': '{option_value}' in preset '{preset_name}' for game "
|
||||
f"'{game_name}' is not valid. Error: {ex}")
|
||||
except KeyError as ex:
|
||||
self.fail(f"Option '{option_name}' in preset '{preset_name}' for game '{game_name}' is "
|
||||
f"not a defined option. Error: {ex}")
|
||||
|
||||
def test_option_preset_values_are_explicitly_defined(self):
|
||||
"""Test that option preset values are not a special flavor of 'random' or use from_text to resolve another
|
||||
value.
|
||||
"""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
presets = world_type.web.options_presets
|
||||
for preset_name, preset in presets.items():
|
||||
for option_name, option_value in preset.items():
|
||||
with self.subTest(game=game_name, preset=preset_name, option=option_name):
|
||||
# Check for non-standard random values.
|
||||
self.assertFalse(
|
||||
str(option_value).startswith("random-"),
|
||||
f"'{option_name}': '{option_value}' in preset '{preset_name}' for game '{game_name}' "
|
||||
f"is not supported for webhost. Special random values are not supported for presets."
|
||||
)
|
||||
|
||||
option = world_type.options_dataclass.type_hints[option_name].from_any(option_value)
|
||||
|
||||
# Check for from_text resolving to a different value. ("random" is allowed though.)
|
||||
if option_value != "random" and isinstance(option_value, str):
|
||||
# Allow special named values for SpecialRange option presets.
|
||||
if isinstance(option, SpecialRange):
|
||||
self.assertTrue(
|
||||
option_value in option.special_range_names,
|
||||
f"Invalid preset '{option_name}': '{option_value}' in preset '{preset_name}' "
|
||||
f"for game '{game_name}'. Expected {option.special_range_names.keys()} or "
|
||||
f"{option.range_start}-{option.range_end}."
|
||||
)
|
||||
else:
|
||||
self.assertTrue(
|
||||
option.name_lookup.get(option.value, None) == option_value,
|
||||
f"'{option_name}': '{option_value}' in preset '{preset_name}' for game "
|
||||
f"'{game_name}' is not supported for webhost. Values must not be resolved to a "
|
||||
f"different option via option.from_text (or an alias)."
|
||||
)
|
|
@ -186,6 +186,9 @@ class WebWorld:
|
|||
bug_report_page: Optional[str]
|
||||
"""display a link to a bug report page, most likely a link to a GitHub issue page."""
|
||||
|
||||
options_presets: Dict[str, Dict[str, Any]] = {}
|
||||
"""A dictionary containing a collection of developer-defined game option presets."""
|
||||
|
||||
|
||||
class World(metaclass=AutoWorldRegister):
|
||||
"""A World object encompasses a game's Items, Locations, Rules and additional data or functionality required.
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
from typing import Any, Dict
|
||||
|
||||
from .Options import Architect, GoldGainMultiplier, Vendors
|
||||
|
||||
rl_options_presets: Dict[str, Dict[str, Any]] = {
|
||||
# Example preset using only literal values.
|
||||
"Unknown Fate": {
|
||||
"progression_balancing": "random",
|
||||
"accessibility": "random",
|
||||
"starting_gender": "random",
|
||||
"starting_class": "random",
|
||||
"new_game_plus": "random",
|
||||
"fairy_chests_per_zone": "random",
|
||||
"chests_per_zone": "random",
|
||||
"universal_fairy_chests": "random",
|
||||
"universal_chests": "random",
|
||||
"vendors": "random",
|
||||
"architect": "random",
|
||||
"architect_fee": "random",
|
||||
"disable_charon": "random",
|
||||
"require_purchasing": "random",
|
||||
"progressive_blueprints": "random",
|
||||
"gold_gain_multiplier": "random",
|
||||
"number_of_children": "random",
|
||||
"free_diary_on_generation": "random",
|
||||
"khidr": "random",
|
||||
"alexander": "random",
|
||||
"leon": "random",
|
||||
"herodotus": "random",
|
||||
"health_pool": "random",
|
||||
"mana_pool": "random",
|
||||
"attack_pool": "random",
|
||||
"magic_damage_pool": "random",
|
||||
"armor_pool": "random",
|
||||
"equip_pool": "random",
|
||||
"crit_chance_pool": "random",
|
||||
"crit_damage_pool": "random",
|
||||
"allow_default_names": False,
|
||||
"death_link": "random",
|
||||
},
|
||||
# A preset I actually use, using some literal values and some from the option itself.
|
||||
"Limited Potential": {
|
||||
"progression_balancing": "disabled",
|
||||
"fairy_chests_per_zone": 2,
|
||||
"starting_class": "random",
|
||||
"chests_per_zone": 30,
|
||||
"vendors": Vendors.option_normal,
|
||||
"architect": Architect.option_disabled,
|
||||
"gold_gain_multiplier": GoldGainMultiplier.option_half,
|
||||
"number_of_children": 2,
|
||||
"free_diary_on_generation": False,
|
||||
"health_pool": 10,
|
||||
"mana_pool": 10,
|
||||
"attack_pool": 10,
|
||||
"magic_damage_pool": 10,
|
||||
"armor_pool": 5,
|
||||
"equip_pool": 10,
|
||||
"crit_chance_pool": 5,
|
||||
"crit_damage_pool": 5,
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ from worlds.AutoWorld import WebWorld, World
|
|||
from .Items import RLItem, RLItemData, event_item_table, get_items_by_category, item_table
|
||||
from .Locations import RLLocation, location_table
|
||||
from .Options import rl_options
|
||||
from .Presets import rl_options_presets
|
||||
from .Regions import create_regions
|
||||
from .Rules import set_rules
|
||||
|
||||
|
@ -22,6 +23,7 @@ class RLWeb(WebWorld):
|
|||
)]
|
||||
bug_report_page = "https://github.com/ThePhar/RogueLegacyRandomizer/issues/new?assignees=&labels=bug&template=" \
|
||||
"report-an-issue---.md&title=%5BIssue%5D"
|
||||
options_presets = rl_options_presets
|
||||
|
||||
|
||||
class RLWorld(World):
|
||||
|
|
Loading…
Reference in New Issue