diff --git a/Options.py b/Options.py
index 9b4f9d99..2e3927aa 100644
--- a/Options.py
+++ b/Options.py
@@ -696,11 +696,19 @@ class Range(NumericOption):
return int(round(random.triangular(lower, end, tri), 0))
-class SpecialRange(Range):
- special_range_cutoff = 0
+class NamedRange(Range):
special_range_names: typing.Dict[str, int] = {}
"""Special Range names have to be all lowercase as matching is done with text.lower()"""
+ def __init__(self, value: int) -> None:
+ if value < self.range_start and value not in self.special_range_names.values():
+ raise Exception(f"{value} is lower than minimum {self.range_start} for option {self.__class__.__name__} " +
+ f"and is also not one of the supported named special values: {self.special_range_names}")
+ elif value > self.range_end and value not in self.special_range_names.values():
+ raise Exception(f"{value} is higher than maximum {self.range_end} for option {self.__class__.__name__} " +
+ f"and is also not one of the supported named special values: {self.special_range_names}")
+ self.value = value
+
@classmethod
def from_text(cls, text: str) -> Range:
text = text.lower()
@@ -708,6 +716,19 @@ class SpecialRange(Range):
return cls(cls.special_range_names[text])
return super().from_text(text)
+
+class SpecialRange(NamedRange):
+ special_range_cutoff = 0
+
+ # TODO: remove class SpecialRange, earliest 3 releases after 0.4.3
+ def __new__(cls, value: int) -> SpecialRange:
+ from Utils import deprecate
+ deprecate(f"Option type {cls.__name__} is a subclass of SpecialRange, which is deprecated and pending removal. "
+ "Consider switching to NamedRange, which supports all use-cases of SpecialRange, and more. In "
+ "NamedRange, range_start specifies the lower end of the regular range, while special values can be "
+ "placed anywhere (below, inside, or above the regular range).")
+ return super().__new__(cls, value)
+
@classmethod
def weighted_range(cls, text) -> Range:
if text == "random-low":
@@ -891,7 +912,7 @@ class Accessibility(Choice):
default = 1
-class ProgressionBalancing(SpecialRange):
+class ProgressionBalancing(NamedRange):
"""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
@@ -1108,7 +1129,7 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
if os.path.isfile(full_path) and full_path.endswith(".yaml"):
os.unlink(full_path)
- def dictify_range(option: typing.Union[Range, SpecialRange]):
+ def dictify_range(option: Range):
data = {option.default: 50}
for sub_option in ["random", "random-low", "random-high"]:
if sub_option != option.default:
diff --git a/WebHostLib/options.py b/WebHostLib/options.py
index 4d17c7fd..0158de7e 100644
--- a/WebHostLib/options.py
+++ b/WebHostLib/options.py
@@ -81,8 +81,8 @@ def create():
"max": option.range_end,
}
- if issubclass(option, Options.SpecialRange):
- game_options[option_name]["type"] = 'special_range'
+ if issubclass(option, Options.NamedRange):
+ game_options[option_name]["type"] = 'named_range'
game_options[option_name]["value_names"] = {}
for key, val in option.special_range_names.items():
game_options[option_name]["value_names"][key] = val
@@ -133,7 +133,7 @@ def create():
continue
option = world.options_dataclass.type_hints[option_name].from_any(option_value)
- if isinstance(option, Options.SpecialRange) and isinstance(option_value, str):
+ if isinstance(option, Options.NamedRange) 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}."
diff --git a/WebHostLib/static/assets/player-options.js b/WebHostLib/static/assets/player-options.js
index 2bb4a3ba..37ba7f98 100644
--- a/WebHostLib/static/assets/player-options.js
+++ b/WebHostLib/static/assets/player-options.js
@@ -216,13 +216,13 @@ const buildOptionsTable = (options, romOpts = false) => {
element.appendChild(randomButton);
break;
- case 'special_range':
+ case 'named_range':
element = document.createElement('div');
- element.classList.add('special-range-container');
+ element.classList.add('named-range-container');
// Build the select element
- let specialRangeSelect = document.createElement('select');
- specialRangeSelect.setAttribute('data-key', option);
+ let namedRangeSelect = document.createElement('select');
+ namedRangeSelect.setAttribute('data-key', option);
Object.keys(options[option].value_names).forEach((presetName) => {
let presetOption = document.createElement('option');
presetOption.innerText = presetName;
@@ -232,58 +232,58 @@ const buildOptionsTable = (options, romOpts = false) => {
words[i] = words[i][0].toUpperCase() + words[i].substring(1);
}
presetOption.innerText = words.join(' ');
- specialRangeSelect.appendChild(presetOption);
+ namedRangeSelect.appendChild(presetOption);
});
let customOption = document.createElement('option');
customOption.innerText = 'Custom';
customOption.value = 'custom';
customOption.selected = true;
- specialRangeSelect.appendChild(customOption);
+ namedRangeSelect.appendChild(customOption);
if (Object.values(options[option].value_names).includes(Number(currentOptions[gameName][option]))) {
- specialRangeSelect.value = Number(currentOptions[gameName][option]);
+ namedRangeSelect.value = Number(currentOptions[gameName][option]);
}
// 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', option);
- specialRange.setAttribute('min', options[option].min);
- specialRange.setAttribute('max', options[option].max);
- specialRange.value = currentOptions[gameName][option];
+ let namedRangeWrapper = document.createElement('div');
+ namedRangeWrapper.classList.add('named-range-wrapper');
+ let namedRange = document.createElement('input');
+ namedRange.setAttribute('type', 'range');
+ namedRange.setAttribute('data-key', option);
+ namedRange.setAttribute('min', options[option].min);
+ namedRange.setAttribute('max', options[option].max);
+ namedRange.value = currentOptions[gameName][option];
// Build rage value element
- let specialRangeVal = document.createElement('span');
- specialRangeVal.classList.add('range-value');
- specialRangeVal.setAttribute('id', `${option}-value`);
- specialRangeVal.innerText = currentOptions[gameName][option] !== 'random' ?
+ let namedRangeVal = document.createElement('span');
+ namedRangeVal.classList.add('range-value');
+ namedRangeVal.setAttribute('id', `${option}-value`);
+ namedRangeVal.innerText = currentOptions[gameName][option] !== 'random' ?
currentOptions[gameName][option] : options[option].defaultValue;
// Configure select event listener
- specialRangeSelect.addEventListener('change', (event) => {
+ namedRangeSelect.addEventListener('change', (event) => {
if (event.target.value === 'custom') { return; }
// Update range slider
- specialRange.value = event.target.value;
+ namedRange.value = event.target.value;
document.getElementById(`${option}-value`).innerText = event.target.value;
updateGameOption(event.target);
});
// Configure range event handler
- specialRange.addEventListener('change', (event) => {
+ namedRange.addEventListener('change', (event) => {
// Update select element
- specialRangeSelect.value =
+ namedRangeSelect.value =
(Object.values(options[option].value_names).includes(parseInt(event.target.value))) ?
parseInt(event.target.value) : 'custom';
document.getElementById(`${option}-value`).innerText = event.target.value;
updateGameOption(event.target);
});
- element.appendChild(specialRangeSelect);
- specialRangeWrapper.appendChild(specialRange);
- specialRangeWrapper.appendChild(specialRangeVal);
- element.appendChild(specialRangeWrapper);
+ element.appendChild(namedRangeSelect);
+ namedRangeWrapper.appendChild(namedRange);
+ namedRangeWrapper.appendChild(namedRangeVal);
+ element.appendChild(namedRangeWrapper);
// Randomize button
randomButton.innerText = '🎲';
@@ -291,15 +291,15 @@ const buildOptionsTable = (options, romOpts = false) => {
randomButton.setAttribute('data-key', option);
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
randomButton.addEventListener('click', (event) => toggleRandomize(
- event, specialRange, specialRangeSelect)
+ event, namedRange, namedRangeSelect)
);
if (currentOptions[gameName][option] === 'random') {
randomButton.classList.add('active');
- specialRange.disabled = true;
- specialRangeSelect.disabled = true;
+ namedRange.disabled = true;
+ namedRangeSelect.disabled = true;
}
- specialRangeWrapper.appendChild(randomButton);
+ namedRangeWrapper.appendChild(randomButton);
break;
default:
diff --git a/WebHostLib/static/assets/weighted-options.js b/WebHostLib/static/assets/weighted-options.js
index 19928327..a2fedb53 100644
--- a/WebHostLib/static/assets/weighted-options.js
+++ b/WebHostLib/static/assets/weighted-options.js
@@ -93,9 +93,10 @@ class WeightedSettings {
});
break;
case 'range':
- case 'special_range':
+ case 'named_range':
this.current[game][gameSetting]['random'] = 0;
this.current[game][gameSetting]['random-low'] = 0;
+ this.current[game][gameSetting]['random-middle'] = 0;
this.current[game][gameSetting]['random-high'] = 0;
if (setting.hasOwnProperty('defaultValue')) {
this.current[game][gameSetting][setting.defaultValue] = 25;
@@ -522,178 +523,185 @@ class GameSettings {
break;
case 'range':
- case 'special_range':
+ case 'named_range':
const rangeTable = document.createElement('table');
const rangeTbody = document.createElement('tbody');
- if (((setting.max - setting.min) + 1) < 11) {
- for (let i=setting.min; i <= setting.max; ++i) {
- const tr = document.createElement('tr');
- const tdLeft = document.createElement('td');
- tdLeft.classList.add('td-left');
- tdLeft.innerText = i;
- tr.appendChild(tdLeft);
+ const hintText = document.createElement('p');
+ hintText.classList.add('hint-text');
+ 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.
Accepted values:
` +
+ `Normal range: ${setting.min} - ${setting.max}`;
- const tdMiddle = document.createElement('td');
- tdMiddle.classList.add('td-middle');
- const range = document.createElement('input');
- range.setAttribute('type', 'range');
- range.setAttribute('id', `${this.name}-${settingName}-${i}-range`);
- range.setAttribute('data-game', this.name);
- range.setAttribute('data-setting', settingName);
- range.setAttribute('data-option', i);
- range.setAttribute('min', 0);
- range.setAttribute('max', 50);
- range.addEventListener('change', (evt) => this.#updateRangeSetting(evt));
- range.value = this.current[settingName][i] || 0;
- tdMiddle.appendChild(range);
- tr.appendChild(tdMiddle);
-
- const tdRight = document.createElement('td');
- tdRight.setAttribute('id', `${this.name}-${settingName}-${i}`)
- tdRight.classList.add('td-right');
- tdRight.innerText = range.value;
- tr.appendChild(tdRight);
-
- rangeTbody.appendChild(tr);
- }
- } else {
- const hintText = document.createElement('p');
- hintText.classList.add('hint-text');
- 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.
Minimum value: ${setting.min}
` +
- `Maximum value: ${setting.max}`;
-
- if (setting.hasOwnProperty('value_names')) {
- hintText.innerHTML += '
Certain values have special meaning:';
- Object.keys(setting.value_names).forEach((specialName) => {
+ const acceptedValuesOutsideRange = [];
+ if (setting.hasOwnProperty('value_names')) {
+ Object.keys(setting.value_names).forEach((specialName) => {
+ if (
+ (setting.value_names[specialName] < setting.min) ||
+ (setting.value_names[specialName] > setting.max)
+ ) {
hintText.innerHTML += `
${specialName}: ${setting.value_names[specialName]}`;
- });
- }
-
- settingWrapper.appendChild(hintText);
-
- const addOptionDiv = document.createElement('div');
- addOptionDiv.classList.add('add-option-div');
- const optionInput = document.createElement('input');
- optionInput.setAttribute('id', `${this.name}-${settingName}-option`);
- optionInput.setAttribute('placeholder', `${setting.min} - ${setting.max}`);
- addOptionDiv.appendChild(optionInput);
- const addOptionButton = document.createElement('button');
- addOptionButton.innerText = 'Add';
- addOptionDiv.appendChild(addOptionButton);
- settingWrapper.appendChild(addOptionDiv);
- optionInput.addEventListener('keydown', (evt) => {
- if (evt.key === 'Enter') { addOptionButton.dispatchEvent(new Event('click')); }
+ acceptedValuesOutsideRange.push(setting.value_names[specialName]);
+ }
});
- addOptionButton.addEventListener('click', () => {
- const optionInput = document.getElementById(`${this.name}-${settingName}-option`);
- let option = optionInput.value;
- if (!option || !option.trim()) { return; }
- option = parseInt(option, 10);
- if ((option < setting.min) || (option > setting.max)) { return; }
- optionInput.value = '';
- if (document.getElementById(`${this.name}-${settingName}-${option}-range`)) { return; }
-
- const tr = document.createElement('tr');
- const tdLeft = document.createElement('td');
- tdLeft.classList.add('td-left');
- tdLeft.innerText = option;
- tr.appendChild(tdLeft);
-
- const tdMiddle = document.createElement('td');
- tdMiddle.classList.add('td-middle');
- const range = document.createElement('input');
- range.setAttribute('type', 'range');
- range.setAttribute('id', `${this.name}-${settingName}-${option}-range`);
- range.setAttribute('data-game', this.name);
- range.setAttribute('data-setting', settingName);
- range.setAttribute('data-option', option);
- range.setAttribute('min', 0);
- range.setAttribute('max', 50);
- range.addEventListener('change', (evt) => this.#updateRangeSetting(evt));
- range.value = this.current[settingName][parseInt(option, 10)];
- tdMiddle.appendChild(range);
- tr.appendChild(tdMiddle);
-
- const tdRight = document.createElement('td');
- tdRight.setAttribute('id', `${this.name}-${settingName}-${option}`)
- tdRight.classList.add('td-right');
- tdRight.innerText = range.value;
- tr.appendChild(tdRight);
-
- const tdDelete = document.createElement('td');
- tdDelete.classList.add('td-delete');
- const deleteButton = document.createElement('span');
- deleteButton.classList.add('range-option-delete');
- deleteButton.innerText = '❌';
- deleteButton.addEventListener('click', () => {
- range.value = 0;
- range.dispatchEvent(new Event('change'));
- rangeTbody.removeChild(tr);
- });
- tdDelete.appendChild(deleteButton);
- tr.appendChild(tdDelete);
-
- rangeTbody.appendChild(tr);
-
- // Save new option to settings
- range.dispatchEvent(new Event('change'));
- });
-
- Object.keys(this.current[settingName]).forEach((option) => {
- // These options are statically generated below, and should always appear even if they are deleted
- // from localStorage
- if (['random-low', 'random', 'random-high'].includes(option)) { return; }
-
- const tr = document.createElement('tr');
- const tdLeft = document.createElement('td');
- tdLeft.classList.add('td-left');
- tdLeft.innerText = option;
- tr.appendChild(tdLeft);
-
- const tdMiddle = document.createElement('td');
- tdMiddle.classList.add('td-middle');
- const range = document.createElement('input');
- range.setAttribute('type', 'range');
- range.setAttribute('id', `${this.name}-${settingName}-${option}-range`);
- range.setAttribute('data-game', this.name);
- range.setAttribute('data-setting', settingName);
- range.setAttribute('data-option', option);
- range.setAttribute('min', 0);
- range.setAttribute('max', 50);
- range.addEventListener('change', (evt) => this.#updateRangeSetting(evt));
- range.value = this.current[settingName][parseInt(option, 10)];
- tdMiddle.appendChild(range);
- tr.appendChild(tdMiddle);
-
- const tdRight = document.createElement('td');
- tdRight.setAttribute('id', `${this.name}-${settingName}-${option}`)
- tdRight.classList.add('td-right');
- tdRight.innerText = range.value;
- tr.appendChild(tdRight);
-
- const tdDelete = document.createElement('td');
- tdDelete.classList.add('td-delete');
- const deleteButton = document.createElement('span');
- deleteButton.classList.add('range-option-delete');
- deleteButton.innerText = '❌';
- deleteButton.addEventListener('click', () => {
- range.value = 0;
- const changeEvent = new Event('change');
- changeEvent.action = 'rangeDelete';
- range.dispatchEvent(changeEvent);
- rangeTbody.removeChild(tr);
- });
- tdDelete.appendChild(deleteButton);
- tr.appendChild(tdDelete);
-
- rangeTbody.appendChild(tr);
+ hintText.innerHTML += '
Certain values have special meaning:';
+ Object.keys(setting.value_names).forEach((specialName) => {
+ hintText.innerHTML += `
${specialName}: ${setting.value_names[specialName]}`;
});
}
- ['random', 'random-low', 'random-high'].forEach((option) => {
+ settingWrapper.appendChild(hintText);
+
+ const addOptionDiv = document.createElement('div');
+ addOptionDiv.classList.add('add-option-div');
+ const optionInput = document.createElement('input');
+ optionInput.setAttribute('id', `${this.name}-${settingName}-option`);
+ let placeholderText = `${setting.min} - ${setting.max}`;
+ acceptedValuesOutsideRange.forEach((aVal) => placeholderText += `, ${aVal}`);
+ optionInput.setAttribute('placeholder', placeholderText);
+ addOptionDiv.appendChild(optionInput);
+ const addOptionButton = document.createElement('button');
+ addOptionButton.innerText = 'Add';
+ addOptionDiv.appendChild(addOptionButton);
+ settingWrapper.appendChild(addOptionDiv);
+ optionInput.addEventListener('keydown', (evt) => {
+ if (evt.key === 'Enter') { addOptionButton.dispatchEvent(new Event('click')); }
+ });
+
+ addOptionButton.addEventListener('click', () => {
+ const optionInput = document.getElementById(`${this.name}-${settingName}-option`);
+ let option = optionInput.value;
+ if (!option || !option.trim()) { return; }
+ option = parseInt(option, 10);
+
+ let optionAcceptable = false;
+ if ((option > setting.min) && (option < setting.max)) {
+ optionAcceptable = true;
+ }
+ if (setting.hasOwnProperty('value_names') && Object.values(setting.value_names).includes(option)){
+ optionAcceptable = true;
+ }
+ if (!optionAcceptable) { return; }
+
+ optionInput.value = '';
+ if (document.getElementById(`${this.name}-${settingName}-${option}-range`)) { return; }
+
+ const tr = document.createElement('tr');
+ const tdLeft = document.createElement('td');
+ tdLeft.classList.add('td-left');
+ tdLeft.innerText = option;
+ if (
+ setting.hasOwnProperty('value_names') &&
+ Object.values(setting.value_names).includes(parseInt(option, 10))
+ ) {
+ const optionName = Object.keys(setting.value_names).find(
+ (key) => setting.value_names[key] === parseInt(option, 10)
+ );
+ tdLeft.innerText += ` [${optionName}]`;
+ }
+ tr.appendChild(tdLeft);
+
+ const tdMiddle = document.createElement('td');
+ tdMiddle.classList.add('td-middle');
+ const range = document.createElement('input');
+ range.setAttribute('type', 'range');
+ range.setAttribute('id', `${this.name}-${settingName}-${option}-range`);
+ range.setAttribute('data-game', this.name);
+ range.setAttribute('data-setting', settingName);
+ range.setAttribute('data-option', option);
+ range.setAttribute('min', 0);
+ range.setAttribute('max', 50);
+ range.addEventListener('change', (evt) => this.#updateRangeSetting(evt));
+ range.value = this.current[settingName][parseInt(option, 10)];
+ tdMiddle.appendChild(range);
+ tr.appendChild(tdMiddle);
+
+ const tdRight = document.createElement('td');
+ tdRight.setAttribute('id', `${this.name}-${settingName}-${option}`)
+ tdRight.classList.add('td-right');
+ tdRight.innerText = range.value;
+ tr.appendChild(tdRight);
+
+ const tdDelete = document.createElement('td');
+ tdDelete.classList.add('td-delete');
+ const deleteButton = document.createElement('span');
+ deleteButton.classList.add('range-option-delete');
+ deleteButton.innerText = '❌';
+ deleteButton.addEventListener('click', () => {
+ range.value = 0;
+ range.dispatchEvent(new Event('change'));
+ rangeTbody.removeChild(tr);
+ });
+ tdDelete.appendChild(deleteButton);
+ tr.appendChild(tdDelete);
+
+ rangeTbody.appendChild(tr);
+
+ // Save new option to settings
+ range.dispatchEvent(new Event('change'));
+ });
+
+ Object.keys(this.current[settingName]).forEach((option) => {
+ // These options are statically generated below, and should always appear even if they are deleted
+ // from localStorage
+ if (['random', 'random-low', 'random-middle', 'random-high'].includes(option)) { return; }
+
+ const tr = document.createElement('tr');
+ const tdLeft = document.createElement('td');
+ tdLeft.classList.add('td-left');
+ tdLeft.innerText = option;
+ if (
+ setting.hasOwnProperty('value_names') &&
+ Object.values(setting.value_names).includes(parseInt(option, 10))
+ ) {
+ const optionName = Object.keys(setting.value_names).find(
+ (key) => setting.value_names[key] === parseInt(option, 10)
+ );
+ tdLeft.innerText += ` [${optionName}]`;
+ }
+ tr.appendChild(tdLeft);
+
+ const tdMiddle = document.createElement('td');
+ tdMiddle.classList.add('td-middle');
+ const range = document.createElement('input');
+ range.setAttribute('type', 'range');
+ range.setAttribute('id', `${this.name}-${settingName}-${option}-range`);
+ range.setAttribute('data-game', this.name);
+ range.setAttribute('data-setting', settingName);
+ range.setAttribute('data-option', option);
+ range.setAttribute('min', 0);
+ range.setAttribute('max', 50);
+ range.addEventListener('change', (evt) => this.#updateRangeSetting(evt));
+ range.value = this.current[settingName][parseInt(option, 10)];
+ tdMiddle.appendChild(range);
+ tr.appendChild(tdMiddle);
+
+ const tdRight = document.createElement('td');
+ tdRight.setAttribute('id', `${this.name}-${settingName}-${option}`)
+ tdRight.classList.add('td-right');
+ tdRight.innerText = range.value;
+ tr.appendChild(tdRight);
+
+ const tdDelete = document.createElement('td');
+ tdDelete.classList.add('td-delete');
+ const deleteButton = document.createElement('span');
+ deleteButton.classList.add('range-option-delete');
+ deleteButton.innerText = '❌';
+ deleteButton.addEventListener('click', () => {
+ range.value = 0;
+ const changeEvent = new Event('change');
+ changeEvent.action = 'rangeDelete';
+ range.dispatchEvent(changeEvent);
+ rangeTbody.removeChild(tr);
+ });
+ tdDelete.appendChild(deleteButton);
+ tr.appendChild(tdDelete);
+
+ rangeTbody.appendChild(tr);
+ });
+
+ ['random', 'random-low', 'random-middle', 'random-high'].forEach((option) => {
const tr = document.createElement('tr');
const tdLeft = document.createElement('td');
tdLeft.classList.add('td-left');
@@ -704,6 +712,9 @@ class GameSettings {
case 'random-low':
tdLeft.innerText = "Random (Low)";
break;
+ case 'random-middle':
+ tdLeft.innerText = 'Random (Middle)';
+ break;
case 'random-high':
tdLeft.innerText = "Random (High)";
break;
diff --git a/WebHostLib/static/styles/player-options.css b/WebHostLib/static/styles/player-options.css
index 7d6a1970..cc2d5e2d 100644
--- a/WebHostLib/static/styles/player-options.css
+++ b/WebHostLib/static/styles/player-options.css
@@ -160,18 +160,18 @@ html{
margin-left: 0.25rem;
}
-#player-options table .special-range-container{
+#player-options table .named-range-container{
display: flex;
flex-direction: column;
}
-#player-options table .special-range-wrapper{
+#player-options table .named-range-wrapper{
display: flex;
flex-direction: row;
margin-top: 0.25rem;
}
-#player-options table .special-range-wrapper input[type=range]{
+#player-options table .named-range-wrapper input[type=range]{
flex-grow: 1;
}
diff --git a/docs/options api.md b/docs/options api.md
index 622d0a7e..80d0737e 100644
--- a/docs/options api.md
+++ b/docs/options api.md
@@ -144,13 +144,20 @@ A numeric option allowing a variety of integers including the endpoints. Has a d
`range_end` of 1. Allows for negative values as well. This will always be an integer and has no methods for string
comparisons.
-### SpecialRange
+### NamedRange
Like range but also allows you to define a dictionary of special names the user can use to equate to a specific value.
+`special_range_names` can be used to
+- give descriptive names to certain values from within the range
+- add option values above or below the regular range, to be associated with a special meaning
+
For example:
```python
+range_start = 1
+range_end = 99
special_range_names: {
"normal": 20,
"extreme": 99,
+ "unlimited": -1,
}
```
diff --git a/docs/world api.md b/docs/world api.md
index 71710ac2..6393f245 100644
--- a/docs/world api.md
+++ b/docs/world api.md
@@ -79,9 +79,9 @@ the options and the values are the values to be set for that option. These prese
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`
+ - If you have a `Range`/`NamedRange` 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
+ - If you have a `NamedRange` 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_` values.
- If you have a `Toggle`/`DefaultOnToggle` option, the value should be a `bool`.
diff --git a/test/webhost/test_option_presets.py b/test/webhost/test_option_presets.py
index 8c6ebea2..0c88b6c2 100644
--- a/test/webhost/test_option_presets.py
+++ b/test/webhost/test_option_presets.py
@@ -1,7 +1,7 @@
import unittest
from worlds import AutoWorldRegister
-from Options import Choice, SpecialRange, Toggle, Range
+from Options import Choice, NamedRange, Toggle, Range
class TestOptionPresets(unittest.TestCase):
@@ -14,7 +14,7 @@ class TestOptionPresets(unittest.TestCase):
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]
+ supported_types = [Choice, Toggle, Range, NamedRange]
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. "
@@ -46,8 +46,8 @@ class TestOptionPresets(unittest.TestCase):
# 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):
+ # Allow special named values for NamedRange option presets.
+ if isinstance(option, NamedRange):
self.assertTrue(
option_value in option.special_range_names,
f"Invalid preset '{option_name}': '{option_value}' in preset '{preset_name}' "
diff --git a/worlds/dlcquest/Options.py b/worlds/dlcquest/Options.py
index ce728b4e..769acbec 100644
--- a/worlds/dlcquest/Options.py
+++ b/worlds/dlcquest/Options.py
@@ -1,6 +1,6 @@
from dataclasses import dataclass
-from Options import Choice, DeathLink, PerGameCommonOptions, SpecialRange
+from Options import Choice, DeathLink, NamedRange, PerGameCommonOptions
class DoubleJumpGlitch(Choice):
@@ -33,7 +33,7 @@ class CoinSanity(Choice):
default = 0
-class CoinSanityRange(SpecialRange):
+class CoinSanityRange(NamedRange):
"""This is the amount of coins in a coin bundle
You need to collect that number of coins to get a location check, and when receiving coin items, you will get bundles of this size
It is highly recommended to not set this value below 10, as it generates a very large number of boring locations and items.
diff --git a/worlds/dlcquest/test/TestOptionsLong.py b/worlds/dlcquest/test/TestOptionsLong.py
index d0a5c0ed..3e9acac7 100644
--- a/worlds/dlcquest/test/TestOptionsLong.py
+++ b/worlds/dlcquest/test/TestOptionsLong.py
@@ -1,7 +1,7 @@
from typing import Dict
from BaseClasses import MultiWorld
-from Options import SpecialRange
+from Options import NamedRange
from .option_names import options_to_include
from .checks.world_checks import assert_can_win, assert_same_number_items_locations
from . import DLCQuestTestBase, setup_dlc_quest_solo_multiworld
@@ -14,7 +14,7 @@ def basic_checks(tester: DLCQuestTestBase, multiworld: MultiWorld):
def get_option_choices(option) -> Dict[str, int]:
- if issubclass(option, SpecialRange):
+ if issubclass(option, NamedRange):
return option.special_range_names
elif option.options:
return option.options
diff --git a/worlds/hk/Options.py b/worlds/hk/Options.py
index 2a19ffd3..fcc93847 100644
--- a/worlds/hk/Options.py
+++ b/worlds/hk/Options.py
@@ -2,7 +2,7 @@ import typing
from .ExtractedData import logic_options, starts, pool_options
from .Rules import cost_terms
-from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, SpecialRange
+from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, NamedRange
from .Charms import vanilla_costs, names as charm_names
if typing.TYPE_CHECKING:
@@ -242,7 +242,7 @@ class MaximumGeoPrice(Range):
default = 400
-class RandomCharmCosts(SpecialRange):
+class RandomCharmCosts(NamedRange):
"""Total Notch Cost of all Charms together. Vanilla sums to 90.
This value is distributed among all charms in a random fashion.
Special Cases:
@@ -250,7 +250,7 @@ class RandomCharmCosts(SpecialRange):
Set to -2 or shuffle to shuffle around the vanilla costs to different charms."""
display_name = "Randomize Charm Notch Costs"
- range_start = -2
+ range_start = 0
range_end = 240
default = -1
vanilla_costs: typing.List[int] = vanilla_costs
diff --git a/worlds/lufia2ac/Options.py b/worlds/lufia2ac/Options.py
index 419532cd..5f33d0bd 100644
--- a/worlds/lufia2ac/Options.py
+++ b/worlds/lufia2ac/Options.py
@@ -7,8 +7,8 @@ from dataclasses import dataclass
from itertools import accumulate, chain, combinations
from typing import Any, cast, Dict, Iterator, List, Mapping, Optional, Set, Tuple, Type, TYPE_CHECKING, Union
-from Options import AssembleOptions, Choice, DeathLink, ItemDict, OptionDict, PerGameCommonOptions, Range, \
- SpecialRange, TextChoice, Toggle
+from Options import AssembleOptions, Choice, DeathLink, ItemDict, NamedRange, OptionDict, PerGameCommonOptions, Range, \
+ TextChoice, Toggle
from .Enemies import enemy_name_to_sprite
from .Items import ItemType, l2ac_item_table
@@ -255,7 +255,7 @@ class CapsuleCravingsJPStyle(Toggle):
display_name = "Capsule cravings JP style"
-class CapsuleStartingForm(SpecialRange):
+class CapsuleStartingForm(NamedRange):
"""The starting form of your capsule monsters.
Supported values: 1 – 4, m
@@ -266,7 +266,6 @@ class CapsuleStartingForm(SpecialRange):
range_start = 1
range_end = 5
default = 1
- special_range_cutoff = 1
special_range_names = {
"default": 1,
"m": 5,
@@ -280,7 +279,7 @@ class CapsuleStartingForm(SpecialRange):
return self.value - 1
-class CapsuleStartingLevel(LevelMixin, SpecialRange):
+class CapsuleStartingLevel(LevelMixin, NamedRange):
"""The starting level of your capsule monsters.
Can be set to the special value party_starting_level to make it the same value as the party_starting_level option.
@@ -289,10 +288,9 @@ class CapsuleStartingLevel(LevelMixin, SpecialRange):
"""
display_name = "Capsule monster starting level"
- range_start = 0
+ range_start = 1
range_end = 99
default = 1
- special_range_cutoff = 1
special_range_names = {
"default": 1,
"party_starting_level": 0,
@@ -685,7 +683,7 @@ class RunSpeed(Choice):
default = option_disabled
-class ShopInterval(SpecialRange):
+class ShopInterval(NamedRange):
"""Place shops after a certain number of floors.
E.g., if you set this to 5, then you will be given the opportunity to shop after completing B5, B10, B15, etc.,
@@ -698,10 +696,9 @@ class ShopInterval(SpecialRange):
"""
display_name = "Shop interval"
- range_start = 0
+ range_start = 1
range_end = 10
default = 0
- special_range_cutoff = 1
special_range_names = {
"disabled": 0,
}
diff --git a/worlds/pokemon_rb/options.py b/worlds/pokemon_rb/options.py
index 794977d3..8afe91b8 100644
--- a/worlds/pokemon_rb/options.py
+++ b/worlds/pokemon_rb/options.py
@@ -1,4 +1,4 @@
-from Options import Toggle, Choice, Range, SpecialRange, TextChoice, DeathLink
+from Options import Toggle, Choice, Range, NamedRange, TextChoice, DeathLink
class GameVersion(Choice):
@@ -285,7 +285,7 @@ class AllPokemonSeen(Toggle):
display_name = "All Pokemon Seen"
-class DexSanity(SpecialRange):
+class DexSanity(NamedRange):
"""Adds location checks for Pokemon flagged "owned" on your Pokedex. You may specify a percentage of Pokemon to
have checks added. If Accessibility is set to locations, this will be the percentage of all logically reachable
Pokemon that will get a location check added to it. With items or minimal Accessibility, it will be the percentage
@@ -412,7 +412,7 @@ class LevelScaling(Choice):
default = 1
-class ExpModifier(SpecialRange):
+class ExpModifier(NamedRange):
"""Modifier for EXP gained. When specifying a number, exp is multiplied by this amount and divided by 16."""
display_name = "Exp Modifier"
default = 16
@@ -607,8 +607,8 @@ class RandomizeTMMoves(Toggle):
display_name = "Randomize TM Moves"
-class TMHMCompatibility(SpecialRange):
- range_start = -1
+class TMHMCompatibility(NamedRange):
+ range_start = 0
range_end = 100
special_range_names = {
"vanilla": -1,
@@ -675,12 +675,12 @@ class RandomizeMoveTypes(Toggle):
default = 0
-class SecondaryTypeChance(SpecialRange):
+class SecondaryTypeChance(NamedRange):
"""If randomize_pokemon_types is on, this is the chance each Pokemon will have a secondary type. If follow_evolutions
is selected, it is the chance a second type will be added at each evolution stage. vanilla will give secondary types
to Pokemon that normally have a secondary type."""
display_name = "Secondary Type Chance"
- range_start = -1
+ range_start = 0
range_end = 100
default = -1
special_range_names = {
diff --git a/worlds/stardew_valley/options.py b/worlds/stardew_valley/options.py
index d85bbf06..267ebd7a 100644
--- a/worlds/stardew_valley/options.py
+++ b/worlds/stardew_valley/options.py
@@ -1,7 +1,7 @@
from dataclasses import dataclass
from typing import Dict
-from Options import Range, SpecialRange, Toggle, Choice, OptionSet, PerGameCommonOptions, DeathLink, Option
+from Options import Range, NamedRange, Toggle, Choice, OptionSet, PerGameCommonOptions, DeathLink, Option
from .mods.mod_data import ModNames
@@ -48,12 +48,12 @@ class Goal(Choice):
return super().get_option_name(value)
-class StartingMoney(SpecialRange):
+class StartingMoney(NamedRange):
"""Amount of gold when arriving at the farm.
Set to -1 or unlimited for infinite money"""
internal_name = "starting_money"
display_name = "Starting Gold"
- range_start = -1
+ range_start = 0
range_end = 50000
default = 5000
@@ -67,7 +67,7 @@ class StartingMoney(SpecialRange):
}
-class ProfitMargin(SpecialRange):
+class ProfitMargin(NamedRange):
"""Multiplier over all gold earned in-game by the player."""
internal_name = "profit_margin"
display_name = "Profit Margin"
@@ -283,7 +283,7 @@ class SpecialOrderLocations(Choice):
option_board_qi = 2
-class HelpWantedLocations(SpecialRange):
+class HelpWantedLocations(NamedRange):
"""Include location checks for Help Wanted quests
Out of every 7 quests, 4 will be item deliveries, and then 1 of each for: Fishing, Gathering and Slaying Monsters.
Choosing a multiple of 7 is recommended."""
@@ -429,7 +429,7 @@ class MultipleDaySleepEnabled(Toggle):
default = 1
-class MultipleDaySleepCost(SpecialRange):
+class MultipleDaySleepCost(NamedRange):
"""How much gold it will cost to use MultiSleep. You will have to pay that amount for each day skipped."""
internal_name = "multiple_day_sleep_cost"
display_name = "Multiple Day Sleep Cost"
@@ -446,7 +446,7 @@ class MultipleDaySleepCost(SpecialRange):
}
-class ExperienceMultiplier(SpecialRange):
+class ExperienceMultiplier(NamedRange):
"""How fast you want to earn skill experience.
A lower setting mean less experience.
A higher setting means more experience."""
@@ -466,7 +466,7 @@ class ExperienceMultiplier(SpecialRange):
}
-class FriendshipMultiplier(SpecialRange):
+class FriendshipMultiplier(NamedRange):
"""How fast you want to earn friendship points with villagers.
A lower setting mean less friendship per action.
A higher setting means more friendship per action."""
diff --git a/worlds/stardew_valley/test/TestOptions.py b/worlds/stardew_valley/test/TestOptions.py
index 02b1ebf6..ccffc284 100644
--- a/worlds/stardew_valley/test/TestOptions.py
+++ b/worlds/stardew_valley/test/TestOptions.py
@@ -4,7 +4,7 @@ from random import random
from typing import Dict
from BaseClasses import ItemClassification, MultiWorld
-from Options import SpecialRange
+from Options import NamedRange
from . import setup_solo_multiworld, SVTestBase, SVTestCase, allsanity_options_without_mods, allsanity_options_with_mods
from .. import StardewItem, items_by_group, Group, StardewValleyWorld
from ..locations import locations_by_tag, LocationTags, location_table
@@ -42,7 +42,7 @@ def check_no_ginger_island(tester: unittest.TestCase, multiworld: MultiWorld):
def get_option_choices(option) -> Dict[str, int]:
- if issubclass(option, SpecialRange):
+ if issubclass(option, NamedRange):
return option.special_range_names
elif option.options:
return option.options
@@ -53,7 +53,7 @@ class TestGenerateDynamicOptions(SVTestCase):
def test_given_special_range_when_generate_then_basic_checks(self):
options = StardewValleyWorld.options_dataclass.type_hints
for option_name, option in options.items():
- if not isinstance(option, SpecialRange):
+ if not isinstance(option, NamedRange):
continue
for value in option.special_range_names:
with self.subTest(f"{option_name}: {value}"):
@@ -152,7 +152,7 @@ class TestGenerateAllOptionsWithExcludeGingerIsland(SVTestCase):
def test_given_special_range_when_generate_exclude_ginger_island(self):
options = StardewValleyWorld.options_dataclass.type_hints
for option_name, option in options.items():
- if not isinstance(option, SpecialRange) or option_name == ExcludeGingerIsland.internal_name:
+ if not isinstance(option, NamedRange) or option_name == ExcludeGingerIsland.internal_name:
continue
for value in option.special_range_names:
with self.subTest(f"{option_name}: {value}"):
diff --git a/worlds/stardew_valley/test/long/TestOptionsLong.py b/worlds/stardew_valley/test/long/TestOptionsLong.py
index 3634dc5f..e3da6968 100644
--- a/worlds/stardew_valley/test/long/TestOptionsLong.py
+++ b/worlds/stardew_valley/test/long/TestOptionsLong.py
@@ -2,7 +2,7 @@ import unittest
from typing import Dict
from BaseClasses import MultiWorld
-from Options import SpecialRange
+from Options import NamedRange
from .option_names import options_to_include
from worlds.stardew_valley.test.checks.world_checks import assert_can_win, assert_same_number_items_locations
from .. import setup_solo_multiworld, SVTestCase
@@ -14,7 +14,7 @@ def basic_checks(tester: unittest.TestCase, multiworld: MultiWorld):
def get_option_choices(option) -> Dict[str, int]:
- if issubclass(option, SpecialRange):
+ if issubclass(option, NamedRange):
return option.special_range_names
elif option.options:
return option.options
diff --git a/worlds/stardew_valley/test/long/TestRandomWorlds.py b/worlds/stardew_valley/test/long/TestRandomWorlds.py
index e22c6c35..1f1d5965 100644
--- a/worlds/stardew_valley/test/long/TestRandomWorlds.py
+++ b/worlds/stardew_valley/test/long/TestRandomWorlds.py
@@ -2,7 +2,7 @@ from typing import Dict
import random
from BaseClasses import MultiWorld
-from Options import SpecialRange, Range
+from Options import NamedRange, Range
from .option_names import options_to_include
from .. import setup_solo_multiworld, SVTestCase
from ..checks.goal_checks import assert_perfection_world_is_valid, assert_goal_world_is_valid
@@ -12,7 +12,7 @@ from ..checks.world_checks import assert_same_number_items_locations, assert_vic
def get_option_choices(option) -> Dict[str, int]:
- if issubclass(option, SpecialRange):
+ if issubclass(option, NamedRange):
return option.special_range_names
if issubclass(option, Range):
return {f"{val}": val for val in range(option.range_start, option.range_end + 1)}
diff --git a/worlds/zillion/options.py b/worlds/zillion/options.py
index 80f9469e..cb861e96 100644
--- a/worlds/zillion/options.py
+++ b/worlds/zillion/options.py
@@ -3,7 +3,7 @@ from dataclasses import dataclass
from typing import Dict, Tuple
from typing_extensions import TypeGuard # remove when Python >= 3.10
-from Options import DefaultOnToggle, PerGameCommonOptions, Range, SpecialRange, Toggle, Choice
+from Options import DefaultOnToggle, NamedRange, PerGameCommonOptions, Range, Toggle, Choice
from zilliandomizer.options import \
Options as ZzOptions, char_to_gun, char_to_jump, ID, \
@@ -11,7 +11,7 @@ from zilliandomizer.options import \
from zilliandomizer.options.parsing import validate as zz_validate
-class ZillionContinues(SpecialRange):
+class ZillionContinues(NamedRange):
"""
number of continues before game over
@@ -218,7 +218,7 @@ class ZillionSkill(Range):
default = 2
-class ZillionStartingCards(SpecialRange):
+class ZillionStartingCards(NamedRange):
"""
how many ID Cards to start the game with