Core: Introduce new Option class NamedRange (#2330)
Co-authored-by: Chris Wilson <chris@legendserver.info> Co-authored-by: Zach Parks <zach@alliware.com>
This commit is contained in:
parent
e64c7b1cbb
commit
c944ecf628
29
Options.py
29
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:
|
||||
|
|
|
@ -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}."
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.<br /><br />Accepted values:<br />` +
|
||||
`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.<br />Minimum value: ${setting.min}<br />` +
|
||||
`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) => {
|
||||
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 += `<br />${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 += '<br /><br />Certain values have special meaning:';
|
||||
Object.keys(setting.value_names).forEach((specialName) => {
|
||||
hintText.innerHTML += `<br />${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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -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_<name>` values.
|
||||
- If you have a `Toggle`/`DefaultOnToggle` option, the value should be a `bool`.
|
||||
|
|
|
@ -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}' "
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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}"):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue