Options: introduce SpecialRange (#630)
* Options: introduce SpecialRange * Include SpecialRange data in player-settings and weighted-settings JSON files * Add support for SpecialRange to player-settings pages * Add support for SpecialRange options to weighted-settings. Also fixed a bug which would cause the page to crash if an unknown setting was detected. Co-authored-by: Chris Wilson <chris@legendserver.info>
This commit is contained in:
parent
84b6ece31d
commit
e7ea827f02
110
Options.py
110
Options.py
|
@ -383,35 +383,7 @@ class Range(NumericOption):
|
||||||
def from_text(cls, text: str) -> Range:
|
def from_text(cls, text: str) -> Range:
|
||||||
text = text.lower()
|
text = text.lower()
|
||||||
if text.startswith("random"):
|
if text.startswith("random"):
|
||||||
if text == "random-low":
|
return cls.weighted_range(text)
|
||||||
return cls(int(round(random.triangular(cls.range_start, cls.range_end, cls.range_start), 0)))
|
|
||||||
elif text == "random-high":
|
|
||||||
return cls(int(round(random.triangular(cls.range_start, cls.range_end, cls.range_end), 0)))
|
|
||||||
elif text == "random-middle":
|
|
||||||
return cls(int(round(random.triangular(cls.range_start, cls.range_end), 0)))
|
|
||||||
elif text.startswith("random-range-"):
|
|
||||||
textsplit = text.split("-")
|
|
||||||
try:
|
|
||||||
random_range = [int(textsplit[len(textsplit) - 2]), int(textsplit[len(textsplit) - 1])]
|
|
||||||
except ValueError:
|
|
||||||
raise ValueError(f"Invalid random range {text} for option {cls.__name__}")
|
|
||||||
random_range.sort()
|
|
||||||
if random_range[0] < cls.range_start or random_range[1] > cls.range_end:
|
|
||||||
raise Exception(
|
|
||||||
f"{random_range[0]}-{random_range[1]} is outside allowed range "
|
|
||||||
f"{cls.range_start}-{cls.range_end} for option {cls.__name__}")
|
|
||||||
if text.startswith("random-range-low"):
|
|
||||||
return cls(int(round(random.triangular(random_range[0], random_range[1], random_range[0]))))
|
|
||||||
elif text.startswith("random-range-middle"):
|
|
||||||
return cls(int(round(random.triangular(random_range[0], random_range[1]))))
|
|
||||||
elif text.startswith("random-range-high"):
|
|
||||||
return cls(int(round(random.triangular(random_range[0], random_range[1], random_range[1]))))
|
|
||||||
else:
|
|
||||||
return cls(int(round(random.randint(random_range[0], random_range[1]))))
|
|
||||||
elif text == "random":
|
|
||||||
return cls(random.randint(cls.range_start, cls.range_end))
|
|
||||||
else:
|
|
||||||
raise Exception(f"random text \"{text}\" did not resolve to a recognized pattern. Acceptable values are: random, random-high, random-middle, random-low, random-range-low-<min>-<max>, random-range-middle-<min>-<max>, random-range-high-<min>-<max>, or random-range-<min>-<max>.")
|
|
||||||
elif text == "default" and hasattr(cls, "default"):
|
elif text == "default" and hasattr(cls, "default"):
|
||||||
return cls(cls.default)
|
return cls(cls.default)
|
||||||
elif text == "high":
|
elif text == "high":
|
||||||
|
@ -429,6 +401,45 @@ class Range(NumericOption):
|
||||||
return cls(0)
|
return cls(0)
|
||||||
return cls(int(text))
|
return cls(int(text))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def weighted_range(cls, text) -> Range:
|
||||||
|
if text == "random-low":
|
||||||
|
return cls(cls.triangular(cls.range_start, cls.range_end, cls.range_start))
|
||||||
|
elif text == "random-high":
|
||||||
|
return cls(cls.triangular(cls.range_start, cls.range_end, cls.range_end))
|
||||||
|
elif text == "random-middle":
|
||||||
|
return cls(cls.triangular(cls.range_start, cls.range_end))
|
||||||
|
elif text.startswith("random-range-"):
|
||||||
|
return cls.custom_range(text)
|
||||||
|
elif text == "random":
|
||||||
|
return cls(random.randint(cls.range_start, cls.range_end))
|
||||||
|
else:
|
||||||
|
raise Exception(f"random text \"{text}\" did not resolve to a recognized pattern. "
|
||||||
|
f"Acceptable values are: random, random-high, random-middle, random-low, "
|
||||||
|
f"random-range-low-<min>-<max>, random-range-middle-<min>-<max>, "
|
||||||
|
f"random-range-high-<min>-<max>, or random-range-<min>-<max>.")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def custom_range(cls, text) -> Range:
|
||||||
|
textsplit = text.split("-")
|
||||||
|
try:
|
||||||
|
random_range = [int(textsplit[len(textsplit) - 2]), int(textsplit[len(textsplit) - 1])]
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(f"Invalid random range {text} for option {cls.__name__}")
|
||||||
|
random_range.sort()
|
||||||
|
if random_range[0] < cls.range_start or random_range[1] > cls.range_end:
|
||||||
|
raise Exception(
|
||||||
|
f"{random_range[0]}-{random_range[1]} is outside allowed range "
|
||||||
|
f"{cls.range_start}-{cls.range_end} for option {cls.__name__}")
|
||||||
|
if text.startswith("random-range-low"):
|
||||||
|
return cls(cls.triangular(random_range[0], random_range[1], random_range[0]))
|
||||||
|
elif text.startswith("random-range-middle"):
|
||||||
|
return cls(cls.triangular(random_range[0], random_range[1]))
|
||||||
|
elif text.startswith("random-range-high"):
|
||||||
|
return cls(cls.triangular(random_range[0], random_range[1], random_range[1]))
|
||||||
|
else:
|
||||||
|
return cls(random.randint(random_range[0], random_range[1]))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_any(cls, data: typing.Any) -> Range:
|
def from_any(cls, data: typing.Any) -> Range:
|
||||||
if type(data) == int:
|
if type(data) == int:
|
||||||
|
@ -442,6 +453,40 @@ class Range(NumericOption):
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return str(self.value)
|
return str(self.value)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def triangular(lower: int, end: int, tri: typing.Optional[int] = None) -> int:
|
||||||
|
return int(round(random.triangular(lower, end, tri), 0))
|
||||||
|
|
||||||
|
|
||||||
|
class SpecialRange(Range):
|
||||||
|
special_range_cutoff = 0
|
||||||
|
special_range_names: typing.Dict[str, int] = {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_text(cls, text: str) -> Range:
|
||||||
|
text = text.lower()
|
||||||
|
if text in cls.special_range_names:
|
||||||
|
return cls(cls.special_range_names[text])
|
||||||
|
return super().from_text(text)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def weighted_range(cls, text) -> Range:
|
||||||
|
if text == "random-low":
|
||||||
|
return cls(cls.triangular(cls.special_range_cutoff, cls.range_end, cls.special_range_cutoff))
|
||||||
|
elif text == "random-high":
|
||||||
|
return cls(cls.triangular(cls.special_range_cutoff, cls.range_end, cls.range_end))
|
||||||
|
elif text == "random-middle":
|
||||||
|
return cls(cls.triangular(cls.special_range_cutoff, cls.range_end))
|
||||||
|
elif text.startswith("random-range-"):
|
||||||
|
return cls.custom_range(text)
|
||||||
|
elif text == "random":
|
||||||
|
return cls(random.randint(cls.special_range_cutoff, cls.range_end))
|
||||||
|
else:
|
||||||
|
raise Exception(f"random text \"{text}\" did not resolve to a recognized pattern. "
|
||||||
|
f"Acceptable values are: random, random-high, random-middle, random-low, "
|
||||||
|
f"random-range-low-<min>-<max>, random-range-middle-<min>-<max>, "
|
||||||
|
f"random-range-high-<min>-<max>, or random-range-<min>-<max>.")
|
||||||
|
|
||||||
|
|
||||||
class VerifyKeys:
|
class VerifyKeys:
|
||||||
valid_keys = frozenset()
|
valid_keys = frozenset()
|
||||||
|
@ -585,13 +630,18 @@ class Accessibility(Choice):
|
||||||
default = 1
|
default = 1
|
||||||
|
|
||||||
|
|
||||||
class ProgressionBalancing(Range):
|
class ProgressionBalancing(SpecialRange):
|
||||||
"""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.
|
||||||
[0-99, default 50] A lower setting means more getting stuck. A higher setting means less getting stuck."""
|
[0-99, default 50] A lower setting means more getting stuck. A higher setting means less getting stuck."""
|
||||||
default = 50
|
default = 50
|
||||||
range_start = 0
|
range_start = 0
|
||||||
range_end = 99
|
range_end = 99
|
||||||
display_name = "Progression Balancing"
|
display_name = "Progression Balancing"
|
||||||
|
special_range_names = {
|
||||||
|
"Disabled": 0,
|
||||||
|
"Normal": 50,
|
||||||
|
"Extreme": 99,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
common_options = {
|
common_options = {
|
||||||
|
|
|
@ -4,6 +4,7 @@ from Utils import __version__
|
||||||
from jinja2 import Template
|
from jinja2 import Template
|
||||||
import yaml
|
import yaml
|
||||||
import json
|
import json
|
||||||
|
import typing
|
||||||
|
|
||||||
from worlds.AutoWorld import AutoWorldRegister
|
from worlds.AutoWorld import AutoWorldRegister
|
||||||
import Options
|
import Options
|
||||||
|
@ -17,13 +18,30 @@ handled_in_js = {"start_inventory", "local_items", "non_local_items", "start_hin
|
||||||
def create():
|
def create():
|
||||||
os.makedirs(os.path.join(target_folder, 'configs'), exist_ok=True)
|
os.makedirs(os.path.join(target_folder, 'configs'), exist_ok=True)
|
||||||
|
|
||||||
def dictify_range(option):
|
def dictify_range(option: typing.Union[Options.Range, Options.SpecialRange]):
|
||||||
data = {option.range_start: 0, option.range_end: 0, "random": 0, "random-low": 0, "random-high": 0,
|
data = {}
|
||||||
option.default: 50}
|
special = getattr(option, "special_range_cutoff", None)
|
||||||
|
if special is not None:
|
||||||
|
data[special] = 0
|
||||||
|
data.update({
|
||||||
|
option.range_start: 0,
|
||||||
|
option.range_end: 0,
|
||||||
|
"random": 0, "random-low": 0, "random-high": 0,
|
||||||
|
option.default: 50
|
||||||
|
})
|
||||||
notes = {
|
notes = {
|
||||||
|
special: "minimum value without special meaning",
|
||||||
option.range_start: "minimum value",
|
option.range_start: "minimum value",
|
||||||
option.range_end: "maximum value"
|
option.range_end: "maximum value"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for name, number in getattr(option, "special_range_names", {}).items():
|
||||||
|
if number in data:
|
||||||
|
data[name] = data[number]
|
||||||
|
del data[number]
|
||||||
|
else:
|
||||||
|
data[name] = 0
|
||||||
|
|
||||||
return data, notes
|
return data, notes
|
||||||
|
|
||||||
def default_converter(default_value):
|
def default_converter(default_value):
|
||||||
|
@ -103,6 +121,12 @@ def create():
|
||||||
"max": option.range_end,
|
"max": option.range_end,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if hasattr(option, "special_range_names"):
|
||||||
|
game_options[option_name]["type"] = 'special_range'
|
||||||
|
game_options[option_name]["value_names"] = {}
|
||||||
|
for key, val in option.special_range_names.items():
|
||||||
|
game_options[option_name]["value_names"][key] = val
|
||||||
|
|
||||||
elif getattr(option, "verify_item_name", False):
|
elif getattr(option, "verify_item_name", False):
|
||||||
game_options[option_name] = {
|
game_options[option_name] = {
|
||||||
"type": "items-list",
|
"type": "items-list",
|
||||||
|
|
|
@ -36,7 +36,8 @@ window.addEventListener('load', () => {
|
||||||
const nameInput = document.getElementById('player-name');
|
const nameInput = document.getElementById('player-name');
|
||||||
nameInput.addEventListener('keyup', (event) => updateBaseSetting(event));
|
nameInput.addEventListener('keyup', (event) => updateBaseSetting(event));
|
||||||
nameInput.value = playerSettings.name;
|
nameInput.value = playerSettings.name;
|
||||||
}).catch(() => {
|
}).catch((e) => {
|
||||||
|
console.error(e);
|
||||||
const url = new URL(window.location.href);
|
const url = new URL(window.location.href);
|
||||||
window.location.replace(`${url.protocol}//${url.hostname}/page-not-found`);
|
window.location.replace(`${url.protocol}//${url.hostname}/page-not-found`);
|
||||||
})
|
})
|
||||||
|
@ -158,6 +159,70 @@ const buildOptionsTable = (settings, romOpts = false) => {
|
||||||
element.appendChild(rangeVal);
|
element.appendChild(rangeVal);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'special_range':
|
||||||
|
element = document.createElement('div');
|
||||||
|
element.classList.add('special-range-container');
|
||||||
|
|
||||||
|
// Build the select element
|
||||||
|
let specialRangeSelect = document.createElement('select');
|
||||||
|
specialRangeSelect.setAttribute('data-key', setting);
|
||||||
|
Object.keys(settings[setting].value_names).forEach((presetName) => {
|
||||||
|
let presetOption = document.createElement('option');
|
||||||
|
presetOption.innerText = presetName;
|
||||||
|
presetOption.value = settings[setting].value_names[presetName];
|
||||||
|
specialRangeSelect.appendChild(presetOption);
|
||||||
|
});
|
||||||
|
let customOption = document.createElement('option');
|
||||||
|
customOption.innerText = 'Custom';
|
||||||
|
customOption.value = 'custom';
|
||||||
|
customOption.selected = true;
|
||||||
|
specialRangeSelect.appendChild(customOption);
|
||||||
|
if (Object.values(settings[setting].value_names).includes(Number(currentSettings[gameName][setting]))) {
|
||||||
|
specialRangeSelect.value = Number(currentSettings[gameName][setting]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build range element
|
||||||
|
let specialRangeWrapper = document.createElement('div');
|
||||||
|
specialRangeWrapper.classList.add('special-range-wrapper');
|
||||||
|
let specialRange = document.createElement('input');
|
||||||
|
specialRange.setAttribute('type', 'range');
|
||||||
|
specialRange.setAttribute('data-key', setting);
|
||||||
|
specialRange.setAttribute('min', settings[setting].min);
|
||||||
|
specialRange.setAttribute('max', settings[setting].max);
|
||||||
|
specialRange.value = currentSettings[gameName][setting];
|
||||||
|
|
||||||
|
// Build rage value element
|
||||||
|
let specialRangeVal = document.createElement('span');
|
||||||
|
specialRangeVal.classList.add('range-value');
|
||||||
|
specialRangeVal.setAttribute('id', `${setting}-value`);
|
||||||
|
specialRangeVal.innerText = currentSettings[gameName][setting] ?? settings[setting].defaultValue;
|
||||||
|
|
||||||
|
// Configure select event listener
|
||||||
|
specialRangeSelect.addEventListener('change', (event) => {
|
||||||
|
if (event.target.value === 'custom') { return; }
|
||||||
|
|
||||||
|
// Update range slider
|
||||||
|
specialRange.value = event.target.value;
|
||||||
|
document.getElementById(`${setting}-value`).innerText = event.target.value;
|
||||||
|
updateGameSetting(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configure range event handler
|
||||||
|
specialRange.addEventListener('change', (event) => {
|
||||||
|
// Update select element
|
||||||
|
specialRangeSelect.value =
|
||||||
|
(Object.values(settings[setting].value_names).includes(parseInt(event.target.value))) ?
|
||||||
|
parseInt(event.target.value) : 'custom';
|
||||||
|
document.getElementById(`${setting}-value`).innerText = event.target.value;
|
||||||
|
updateGameSetting(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
element.appendChild(specialRangeSelect);
|
||||||
|
specialRangeWrapper.appendChild(specialRange);
|
||||||
|
specialRangeWrapper.appendChild(specialRangeVal);
|
||||||
|
element.appendChild(specialRangeWrapper);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.error(`Ignoring unknown setting type: ${settings[setting].type} with name ${setting}`);
|
console.error(`Ignoring unknown setting type: ${settings[setting].type} with name ${setting}`);
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -77,6 +77,7 @@ const createDefaultSettings = (settingData) => {
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'range':
|
case 'range':
|
||||||
|
case 'special_range':
|
||||||
for (let i = setting.min; i <= setting.max; ++i){
|
for (let i = setting.min; i <= setting.max; ++i){
|
||||||
newSettings[game][gameSetting][i] =
|
newSettings[game][gameSetting][i] =
|
||||||
(setting.hasOwnProperty('defaultValue') && setting.defaultValue === i) ? 25 : 0;
|
(setting.hasOwnProperty('defaultValue') && setting.defaultValue === i) ? 25 : 0;
|
||||||
|
@ -285,6 +286,7 @@ const buildWeightedSettingsDiv = (game, settings) => {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'range':
|
case 'range':
|
||||||
|
case 'special_range':
|
||||||
const rangeTable = document.createElement('table');
|
const rangeTable = document.createElement('table');
|
||||||
const rangeTbody = document.createElement('tbody');
|
const rangeTbody = document.createElement('tbody');
|
||||||
|
|
||||||
|
@ -325,6 +327,14 @@ const buildWeightedSettingsDiv = (game, settings) => {
|
||||||
hintText.innerHTML = 'This is a range option. You may enter a valid numerical value in the text box ' +
|
hintText.innerHTML = 'This is a range option. You may enter a valid numerical value in the text box ' +
|
||||||
`below, then press the "Add" button to add a weight for it.<br />Minimum value: ${setting.min}<br />` +
|
`below, then press the "Add" button to add a weight for it.<br />Minimum value: ${setting.min}<br />` +
|
||||||
`Maximum value: ${setting.max}`;
|
`Maximum value: ${setting.max}`;
|
||||||
|
|
||||||
|
if (setting.hasOwnProperty('value_names')) {
|
||||||
|
hintText.innerHTML += '<br /><br />Certain values have special meaning:';
|
||||||
|
Object.keys(setting.value_names).forEach((specialName) => {
|
||||||
|
hintText.innerHTML += `<br />${specialName}: ${setting.value_names[specialName]}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
settingWrapper.appendChild(hintText);
|
settingWrapper.appendChild(hintText);
|
||||||
|
|
||||||
const addOptionDiv = document.createElement('div');
|
const addOptionDiv = document.createElement('div');
|
||||||
|
@ -487,7 +497,7 @@ const buildWeightedSettingsDiv = (game, settings) => {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.error(`Unknown setting type for ${game} setting ${setting}: ${settings[setting].type}`);
|
console.error(`Unknown setting type for ${game} setting ${settingName}: ${setting.type}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -137,6 +137,20 @@ html{
|
||||||
margin-left: 0.25rem;
|
margin-left: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#player-settings table .special-range-container{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-settings table .special-range-wrapper{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-settings table .special-range-wrapper input[type=range]{
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
#player-settings table label{
|
#player-settings table label{
|
||||||
display: block;
|
display: block;
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
|
@ -148,7 +162,7 @@ html{
|
||||||
border: none;
|
border: none;
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
vertical-align: middle;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media all and (max-width: 1000px), all and (orientation: portrait){
|
@media all and (max-width: 1000px), all and (orientation: portrait){
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import typing
|
import typing
|
||||||
from .ExtractedData import logic_options, starts, pool_options
|
from .ExtractedData import logic_options, starts, pool_options
|
||||||
|
|
||||||
from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict
|
from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, SpecialRange
|
||||||
from .Charms import vanilla_costs, names as charm_names
|
from .Charms import vanilla_costs, names as charm_names
|
||||||
|
|
||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
|
@ -208,12 +208,12 @@ class MaximumCharmPrice(MinimumCharmPrice):
|
||||||
default = 20
|
default = 20
|
||||||
|
|
||||||
|
|
||||||
class RandomCharmCosts(Range):
|
class RandomCharmCosts(SpecialRange):
|
||||||
"""Total Notch Cost of all Charms together. Vanilla sums to 90.
|
"""Total Notch Cost of all Charms together. Vanilla sums to 90.
|
||||||
This value is distributed among all charms in a random fashion.
|
This value is distributed among all charms in a random fashion.
|
||||||
Special Cases:
|
Special Cases:
|
||||||
Set to -1 for vanilla costs.
|
Set to -1 or vanilla for vanilla costs.
|
||||||
Set to -2 to shuffle around the vanilla costs to different charms."""
|
Set to -2 or shuffle to shuffle around the vanilla costs to different charms."""
|
||||||
|
|
||||||
display_name = "Randomize Charm Notch Costs"
|
display_name = "Randomize Charm Notch Costs"
|
||||||
range_start = -2
|
range_start = -2
|
||||||
|
@ -221,6 +221,10 @@ class RandomCharmCosts(Range):
|
||||||
default = -1
|
default = -1
|
||||||
vanilla_costs: typing.List[int] = vanilla_costs
|
vanilla_costs: typing.List[int] = vanilla_costs
|
||||||
charm_count: int = len(vanilla_costs)
|
charm_count: int = len(vanilla_costs)
|
||||||
|
special_range_names = {
|
||||||
|
"vanilla": -1,
|
||||||
|
"shuffle": -2
|
||||||
|
}
|
||||||
|
|
||||||
def get_costs(self, random_source: Random) -> typing.List[int]:
|
def get_costs(self, random_source: Random) -> typing.List[int]:
|
||||||
charms: typing.List[int]
|
charms: typing.List[int]
|
||||||
|
|
Loading…
Reference in New Issue