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:
el-u 2023-11-25 00:10:52 +01:00 committed by GitHub
parent e64c7b1cbb
commit c944ecf628
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 290 additions and 254 deletions

View File

@ -696,11 +696,19 @@ class Range(NumericOption):
return int(round(random.triangular(lower, end, tri), 0)) return int(round(random.triangular(lower, end, tri), 0))
class SpecialRange(Range): class NamedRange(Range):
special_range_cutoff = 0
special_range_names: typing.Dict[str, int] = {} special_range_names: typing.Dict[str, int] = {}
"""Special Range names have to be all lowercase as matching is done with text.lower()""" """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 @classmethod
def from_text(cls, text: str) -> Range: def from_text(cls, text: str) -> Range:
text = text.lower() text = text.lower()
@ -708,6 +716,19 @@ class SpecialRange(Range):
return cls(cls.special_range_names[text]) return cls(cls.special_range_names[text])
return super().from_text(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 @classmethod
def weighted_range(cls, text) -> Range: def weighted_range(cls, text) -> Range:
if text == "random-low": if text == "random-low":
@ -891,7 +912,7 @@ class Accessibility(Choice):
default = 1 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 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.""" A lower setting means more getting stuck. A higher setting means less getting stuck."""
default = 50 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"): if os.path.isfile(full_path) and full_path.endswith(".yaml"):
os.unlink(full_path) os.unlink(full_path)
def dictify_range(option: typing.Union[Range, SpecialRange]): def dictify_range(option: Range):
data = {option.default: 50} data = {option.default: 50}
for sub_option in ["random", "random-low", "random-high"]: for sub_option in ["random", "random-low", "random-high"]:
if sub_option != option.default: if sub_option != option.default:

View File

@ -81,8 +81,8 @@ def create():
"max": option.range_end, "max": option.range_end,
} }
if issubclass(option, Options.SpecialRange): if issubclass(option, Options.NamedRange):
game_options[option_name]["type"] = 'special_range' game_options[option_name]["type"] = 'named_range'
game_options[option_name]["value_names"] = {} game_options[option_name]["value_names"] = {}
for key, val in option.special_range_names.items(): for key, val in option.special_range_names.items():
game_options[option_name]["value_names"][key] = val game_options[option_name]["value_names"][key] = val
@ -133,7 +133,7 @@ def create():
continue continue
option = world.options_dataclass.type_hints[option_name].from_any(option_value) 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, \ assert option_value in option.special_range_names, \
f"Invalid preset value '{option_value}' for '{option_name}' in '{preset_name}'. " \ 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}." f"Expected {option.special_range_names.keys()} or {option.range_start}-{option.range_end}."

View File

@ -216,13 +216,13 @@ const buildOptionsTable = (options, romOpts = false) => {
element.appendChild(randomButton); element.appendChild(randomButton);
break; break;
case 'special_range': case 'named_range':
element = document.createElement('div'); element = document.createElement('div');
element.classList.add('special-range-container'); element.classList.add('named-range-container');
// Build the select element // Build the select element
let specialRangeSelect = document.createElement('select'); let namedRangeSelect = document.createElement('select');
specialRangeSelect.setAttribute('data-key', option); namedRangeSelect.setAttribute('data-key', option);
Object.keys(options[option].value_names).forEach((presetName) => { Object.keys(options[option].value_names).forEach((presetName) => {
let presetOption = document.createElement('option'); let presetOption = document.createElement('option');
presetOption.innerText = presetName; presetOption.innerText = presetName;
@ -232,58 +232,58 @@ const buildOptionsTable = (options, romOpts = false) => {
words[i] = words[i][0].toUpperCase() + words[i].substring(1); words[i] = words[i][0].toUpperCase() + words[i].substring(1);
} }
presetOption.innerText = words.join(' '); presetOption.innerText = words.join(' ');
specialRangeSelect.appendChild(presetOption); namedRangeSelect.appendChild(presetOption);
}); });
let customOption = document.createElement('option'); let customOption = document.createElement('option');
customOption.innerText = 'Custom'; customOption.innerText = 'Custom';
customOption.value = 'custom'; customOption.value = 'custom';
customOption.selected = true; customOption.selected = true;
specialRangeSelect.appendChild(customOption); namedRangeSelect.appendChild(customOption);
if (Object.values(options[option].value_names).includes(Number(currentOptions[gameName][option]))) { 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 // Build range element
let specialRangeWrapper = document.createElement('div'); let namedRangeWrapper = document.createElement('div');
specialRangeWrapper.classList.add('special-range-wrapper'); namedRangeWrapper.classList.add('named-range-wrapper');
let specialRange = document.createElement('input'); let namedRange = document.createElement('input');
specialRange.setAttribute('type', 'range'); namedRange.setAttribute('type', 'range');
specialRange.setAttribute('data-key', option); namedRange.setAttribute('data-key', option);
specialRange.setAttribute('min', options[option].min); namedRange.setAttribute('min', options[option].min);
specialRange.setAttribute('max', options[option].max); namedRange.setAttribute('max', options[option].max);
specialRange.value = currentOptions[gameName][option]; namedRange.value = currentOptions[gameName][option];
// Build rage value element // Build rage value element
let specialRangeVal = document.createElement('span'); let namedRangeVal = document.createElement('span');
specialRangeVal.classList.add('range-value'); namedRangeVal.classList.add('range-value');
specialRangeVal.setAttribute('id', `${option}-value`); namedRangeVal.setAttribute('id', `${option}-value`);
specialRangeVal.innerText = currentOptions[gameName][option] !== 'random' ? namedRangeVal.innerText = currentOptions[gameName][option] !== 'random' ?
currentOptions[gameName][option] : options[option].defaultValue; currentOptions[gameName][option] : options[option].defaultValue;
// Configure select event listener // Configure select event listener
specialRangeSelect.addEventListener('change', (event) => { namedRangeSelect.addEventListener('change', (event) => {
if (event.target.value === 'custom') { return; } if (event.target.value === 'custom') { return; }
// Update range slider // Update range slider
specialRange.value = event.target.value; namedRange.value = event.target.value;
document.getElementById(`${option}-value`).innerText = event.target.value; document.getElementById(`${option}-value`).innerText = event.target.value;
updateGameOption(event.target); updateGameOption(event.target);
}); });
// Configure range event handler // Configure range event handler
specialRange.addEventListener('change', (event) => { namedRange.addEventListener('change', (event) => {
// Update select element // Update select element
specialRangeSelect.value = namedRangeSelect.value =
(Object.values(options[option].value_names).includes(parseInt(event.target.value))) ? (Object.values(options[option].value_names).includes(parseInt(event.target.value))) ?
parseInt(event.target.value) : 'custom'; parseInt(event.target.value) : 'custom';
document.getElementById(`${option}-value`).innerText = event.target.value; document.getElementById(`${option}-value`).innerText = event.target.value;
updateGameOption(event.target); updateGameOption(event.target);
}); });
element.appendChild(specialRangeSelect); element.appendChild(namedRangeSelect);
specialRangeWrapper.appendChild(specialRange); namedRangeWrapper.appendChild(namedRange);
specialRangeWrapper.appendChild(specialRangeVal); namedRangeWrapper.appendChild(namedRangeVal);
element.appendChild(specialRangeWrapper); element.appendChild(namedRangeWrapper);
// Randomize button // Randomize button
randomButton.innerText = '🎲'; randomButton.innerText = '🎲';
@ -291,15 +291,15 @@ const buildOptionsTable = (options, romOpts = false) => {
randomButton.setAttribute('data-key', option); randomButton.setAttribute('data-key', option);
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!'); randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
randomButton.addEventListener('click', (event) => toggleRandomize( randomButton.addEventListener('click', (event) => toggleRandomize(
event, specialRange, specialRangeSelect) event, namedRange, namedRangeSelect)
); );
if (currentOptions[gameName][option] === 'random') { if (currentOptions[gameName][option] === 'random') {
randomButton.classList.add('active'); randomButton.classList.add('active');
specialRange.disabled = true; namedRange.disabled = true;
specialRangeSelect.disabled = true; namedRangeSelect.disabled = true;
} }
specialRangeWrapper.appendChild(randomButton); namedRangeWrapper.appendChild(randomButton);
break; break;
default: default:

View File

@ -93,9 +93,10 @@ class WeightedSettings {
}); });
break; break;
case 'range': case 'range':
case 'special_range': case 'named_range':
this.current[game][gameSetting]['random'] = 0; this.current[game][gameSetting]['random'] = 0;
this.current[game][gameSetting]['random-low'] = 0; this.current[game][gameSetting]['random-low'] = 0;
this.current[game][gameSetting]['random-middle'] = 0;
this.current[game][gameSetting]['random-high'] = 0; this.current[game][gameSetting]['random-high'] = 0;
if (setting.hasOwnProperty('defaultValue')) { if (setting.hasOwnProperty('defaultValue')) {
this.current[game][gameSetting][setting.defaultValue] = 25; this.current[game][gameSetting][setting.defaultValue] = 25;
@ -522,49 +523,28 @@ class GameSettings {
break; break;
case 'range': case 'range':
case 'special_range': case 'named_range':
const rangeTable = document.createElement('table'); const rangeTable = document.createElement('table');
const rangeTbody = document.createElement('tbody'); 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 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'); const hintText = document.createElement('p');
hintText.classList.add('hint-text'); hintText.classList.add('hint-text');
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 /><br />Accepted values:<br />` +
`Maximum value: ${setting.max}`; `Normal range: ${setting.min} - ${setting.max}`;
const acceptedValuesOutsideRange = [];
if (setting.hasOwnProperty('value_names')) { 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]}`;
acceptedValuesOutsideRange.push(setting.value_names[specialName]);
}
});
hintText.innerHTML += '<br /><br />Certain values have special meaning:'; hintText.innerHTML += '<br /><br />Certain values have special meaning:';
Object.keys(setting.value_names).forEach((specialName) => { Object.keys(setting.value_names).forEach((specialName) => {
hintText.innerHTML += `<br />${specialName}: ${setting.value_names[specialName]}`; hintText.innerHTML += `<br />${specialName}: ${setting.value_names[specialName]}`;
@ -577,7 +557,9 @@ class GameSettings {
addOptionDiv.classList.add('add-option-div'); addOptionDiv.classList.add('add-option-div');
const optionInput = document.createElement('input'); const optionInput = document.createElement('input');
optionInput.setAttribute('id', `${this.name}-${settingName}-option`); optionInput.setAttribute('id', `${this.name}-${settingName}-option`);
optionInput.setAttribute('placeholder', `${setting.min} - ${setting.max}`); let placeholderText = `${setting.min} - ${setting.max}`;
acceptedValuesOutsideRange.forEach((aVal) => placeholderText += `, ${aVal}`);
optionInput.setAttribute('placeholder', placeholderText);
addOptionDiv.appendChild(optionInput); addOptionDiv.appendChild(optionInput);
const addOptionButton = document.createElement('button'); const addOptionButton = document.createElement('button');
addOptionButton.innerText = 'Add'; addOptionButton.innerText = 'Add';
@ -592,7 +574,16 @@ class GameSettings {
let option = optionInput.value; let option = optionInput.value;
if (!option || !option.trim()) { return; } if (!option || !option.trim()) { return; }
option = parseInt(option, 10); option = parseInt(option, 10);
if ((option < setting.min) || (option > setting.max)) { return; }
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 = ''; optionInput.value = '';
if (document.getElementById(`${this.name}-${settingName}-${option}-range`)) { return; } if (document.getElementById(`${this.name}-${settingName}-${option}-range`)) { return; }
@ -600,6 +591,15 @@ class GameSettings {
const tdLeft = document.createElement('td'); const tdLeft = document.createElement('td');
tdLeft.classList.add('td-left'); tdLeft.classList.add('td-left');
tdLeft.innerText = option; 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); tr.appendChild(tdLeft);
const tdMiddle = document.createElement('td'); const tdMiddle = document.createElement('td');
@ -645,12 +645,21 @@ class GameSettings {
Object.keys(this.current[settingName]).forEach((option) => { Object.keys(this.current[settingName]).forEach((option) => {
// These options are statically generated below, and should always appear even if they are deleted // These options are statically generated below, and should always appear even if they are deleted
// from localStorage // from localStorage
if (['random-low', 'random', 'random-high'].includes(option)) { return; } if (['random', 'random-low', 'random-middle', 'random-high'].includes(option)) { return; }
const tr = document.createElement('tr'); const tr = document.createElement('tr');
const tdLeft = document.createElement('td'); const tdLeft = document.createElement('td');
tdLeft.classList.add('td-left'); tdLeft.classList.add('td-left');
tdLeft.innerText = option; 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); tr.appendChild(tdLeft);
const tdMiddle = document.createElement('td'); const tdMiddle = document.createElement('td');
@ -691,9 +700,8 @@ class GameSettings {
rangeTbody.appendChild(tr); rangeTbody.appendChild(tr);
}); });
}
['random', 'random-low', 'random-high'].forEach((option) => { ['random', 'random-low', 'random-middle', 'random-high'].forEach((option) => {
const tr = document.createElement('tr'); const tr = document.createElement('tr');
const tdLeft = document.createElement('td'); const tdLeft = document.createElement('td');
tdLeft.classList.add('td-left'); tdLeft.classList.add('td-left');
@ -704,6 +712,9 @@ class GameSettings {
case 'random-low': case 'random-low':
tdLeft.innerText = "Random (Low)"; tdLeft.innerText = "Random (Low)";
break; break;
case 'random-middle':
tdLeft.innerText = 'Random (Middle)';
break;
case 'random-high': case 'random-high':
tdLeft.innerText = "Random (High)"; tdLeft.innerText = "Random (High)";
break; break;

View File

@ -160,18 +160,18 @@ html{
margin-left: 0.25rem; margin-left: 0.25rem;
} }
#player-options table .special-range-container{ #player-options table .named-range-container{
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
#player-options table .special-range-wrapper{ #player-options table .named-range-wrapper{
display: flex; display: flex;
flex-direction: row; flex-direction: row;
margin-top: 0.25rem; 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; flex-grow: 1;
} }

View File

@ -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 `range_end` of 1. Allows for negative values as well. This will always be an integer and has no methods for string
comparisons. 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. 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: For example:
```python ```python
range_start = 1
range_end = 99
special_range_names: { special_range_names: {
"normal": 20, "normal": 20,
"extreme": 99, "extreme": 99,
"unlimited": -1,
} }
``` ```

View File

@ -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: 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. 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. `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 `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`. - If you have a `Toggle`/`DefaultOnToggle` option, the value should be a `bool`.

View File

@ -1,7 +1,7 @@
import unittest import unittest
from worlds import AutoWorldRegister from worlds import AutoWorldRegister
from Options import Choice, SpecialRange, Toggle, Range from Options import Choice, NamedRange, Toggle, Range
class TestOptionPresets(unittest.TestCase): class TestOptionPresets(unittest.TestCase):
@ -14,7 +14,7 @@ class TestOptionPresets(unittest.TestCase):
with self.subTest(game=game_name, preset=preset_name, option=option_name): with self.subTest(game=game_name, preset=preset_name, option=option_name):
try: try:
option = world_type.options_dataclass.type_hints[option_name].from_any(option_value) 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]): 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}' " self.fail(f"'{option_name}' in preset '{preset_name}' for game '{game_name}' "
f"is not a supported type for webhost. " 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.) # Check for from_text resolving to a different value. ("random" is allowed though.)
if option_value != "random" and isinstance(option_value, str): if option_value != "random" and isinstance(option_value, str):
# Allow special named values for SpecialRange option presets. # Allow special named values for NamedRange option presets.
if isinstance(option, SpecialRange): if isinstance(option, NamedRange):
self.assertTrue( self.assertTrue(
option_value in option.special_range_names, option_value in option.special_range_names,
f"Invalid preset '{option_name}': '{option_value}' in preset '{preset_name}' " f"Invalid preset '{option_name}': '{option_value}' in preset '{preset_name}' "

View File

@ -1,6 +1,6 @@
from dataclasses import dataclass from dataclasses import dataclass
from Options import Choice, DeathLink, PerGameCommonOptions, SpecialRange from Options import Choice, DeathLink, NamedRange, PerGameCommonOptions
class DoubleJumpGlitch(Choice): class DoubleJumpGlitch(Choice):
@ -33,7 +33,7 @@ class CoinSanity(Choice):
default = 0 default = 0
class CoinSanityRange(SpecialRange): class CoinSanityRange(NamedRange):
"""This is the amount of coins in a coin bundle """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 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. It is highly recommended to not set this value below 10, as it generates a very large number of boring locations and items.

View File

@ -1,7 +1,7 @@
from typing import Dict from typing import Dict
from BaseClasses import MultiWorld from BaseClasses import MultiWorld
from Options import SpecialRange from Options import NamedRange
from .option_names import options_to_include from .option_names import options_to_include
from .checks.world_checks import assert_can_win, assert_same_number_items_locations from .checks.world_checks import assert_can_win, assert_same_number_items_locations
from . import DLCQuestTestBase, setup_dlc_quest_solo_multiworld 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]: def get_option_choices(option) -> Dict[str, int]:
if issubclass(option, SpecialRange): if issubclass(option, NamedRange):
return option.special_range_names return option.special_range_names
elif option.options: elif option.options:
return option.options return option.options

View File

@ -2,7 +2,7 @@ import typing
from .ExtractedData import logic_options, starts, pool_options from .ExtractedData import logic_options, starts, pool_options
from .Rules import cost_terms 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 from .Charms import vanilla_costs, names as charm_names
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
@ -242,7 +242,7 @@ class MaximumGeoPrice(Range):
default = 400 default = 400
class RandomCharmCosts(SpecialRange): class RandomCharmCosts(NamedRange):
"""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:
@ -250,7 +250,7 @@ class RandomCharmCosts(SpecialRange):
Set to -2 or shuffle 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 = 0
range_end = 240 range_end = 240
default = -1 default = -1
vanilla_costs: typing.List[int] = vanilla_costs vanilla_costs: typing.List[int] = vanilla_costs

View File

@ -7,8 +7,8 @@ from dataclasses import dataclass
from itertools import accumulate, chain, combinations from itertools import accumulate, chain, combinations
from typing import Any, cast, Dict, Iterator, List, Mapping, Optional, Set, Tuple, Type, TYPE_CHECKING, Union 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, \ from Options import AssembleOptions, Choice, DeathLink, ItemDict, NamedRange, OptionDict, PerGameCommonOptions, Range, \
SpecialRange, TextChoice, Toggle TextChoice, Toggle
from .Enemies import enemy_name_to_sprite from .Enemies import enemy_name_to_sprite
from .Items import ItemType, l2ac_item_table from .Items import ItemType, l2ac_item_table
@ -255,7 +255,7 @@ class CapsuleCravingsJPStyle(Toggle):
display_name = "Capsule cravings JP style" display_name = "Capsule cravings JP style"
class CapsuleStartingForm(SpecialRange): class CapsuleStartingForm(NamedRange):
"""The starting form of your capsule monsters. """The starting form of your capsule monsters.
Supported values: 1 4, m Supported values: 1 4, m
@ -266,7 +266,6 @@ class CapsuleStartingForm(SpecialRange):
range_start = 1 range_start = 1
range_end = 5 range_end = 5
default = 1 default = 1
special_range_cutoff = 1
special_range_names = { special_range_names = {
"default": 1, "default": 1,
"m": 5, "m": 5,
@ -280,7 +279,7 @@ class CapsuleStartingForm(SpecialRange):
return self.value - 1 return self.value - 1
class CapsuleStartingLevel(LevelMixin, SpecialRange): class CapsuleStartingLevel(LevelMixin, NamedRange):
"""The starting level of your capsule monsters. """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. 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" display_name = "Capsule monster starting level"
range_start = 0 range_start = 1
range_end = 99 range_end = 99
default = 1 default = 1
special_range_cutoff = 1
special_range_names = { special_range_names = {
"default": 1, "default": 1,
"party_starting_level": 0, "party_starting_level": 0,
@ -685,7 +683,7 @@ class RunSpeed(Choice):
default = option_disabled default = option_disabled
class ShopInterval(SpecialRange): class ShopInterval(NamedRange):
"""Place shops after a certain number of floors. """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., 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" display_name = "Shop interval"
range_start = 0 range_start = 1
range_end = 10 range_end = 10
default = 0 default = 0
special_range_cutoff = 1
special_range_names = { special_range_names = {
"disabled": 0, "disabled": 0,
} }

View File

@ -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): class GameVersion(Choice):
@ -285,7 +285,7 @@ class AllPokemonSeen(Toggle):
display_name = "All Pokemon Seen" 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 """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 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 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 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.""" """Modifier for EXP gained. When specifying a number, exp is multiplied by this amount and divided by 16."""
display_name = "Exp Modifier" display_name = "Exp Modifier"
default = 16 default = 16
@ -607,8 +607,8 @@ class RandomizeTMMoves(Toggle):
display_name = "Randomize TM Moves" display_name = "Randomize TM Moves"
class TMHMCompatibility(SpecialRange): class TMHMCompatibility(NamedRange):
range_start = -1 range_start = 0
range_end = 100 range_end = 100
special_range_names = { special_range_names = {
"vanilla": -1, "vanilla": -1,
@ -675,12 +675,12 @@ class RandomizeMoveTypes(Toggle):
default = 0 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 """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 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.""" to Pokemon that normally have a secondary type."""
display_name = "Secondary Type Chance" display_name = "Secondary Type Chance"
range_start = -1 range_start = 0
range_end = 100 range_end = 100
default = -1 default = -1
special_range_names = { special_range_names = {

View File

@ -1,7 +1,7 @@
from dataclasses import dataclass from dataclasses import dataclass
from typing import Dict 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 from .mods.mod_data import ModNames
@ -48,12 +48,12 @@ class Goal(Choice):
return super().get_option_name(value) return super().get_option_name(value)
class StartingMoney(SpecialRange): class StartingMoney(NamedRange):
"""Amount of gold when arriving at the farm. """Amount of gold when arriving at the farm.
Set to -1 or unlimited for infinite money""" Set to -1 or unlimited for infinite money"""
internal_name = "starting_money" internal_name = "starting_money"
display_name = "Starting Gold" display_name = "Starting Gold"
range_start = -1 range_start = 0
range_end = 50000 range_end = 50000
default = 5000 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.""" """Multiplier over all gold earned in-game by the player."""
internal_name = "profit_margin" internal_name = "profit_margin"
display_name = "Profit Margin" display_name = "Profit Margin"
@ -283,7 +283,7 @@ class SpecialOrderLocations(Choice):
option_board_qi = 2 option_board_qi = 2
class HelpWantedLocations(SpecialRange): class HelpWantedLocations(NamedRange):
"""Include location checks for Help Wanted quests """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. 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.""" Choosing a multiple of 7 is recommended."""
@ -429,7 +429,7 @@ class MultipleDaySleepEnabled(Toggle):
default = 1 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.""" """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" internal_name = "multiple_day_sleep_cost"
display_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. """How fast you want to earn skill experience.
A lower setting mean less experience. A lower setting mean less experience.
A higher setting means more 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. """How fast you want to earn friendship points with villagers.
A lower setting mean less friendship per action. A lower setting mean less friendship per action.
A higher setting means more friendship per action.""" A higher setting means more friendship per action."""

View File

@ -4,7 +4,7 @@ from random import random
from typing import Dict from typing import Dict
from BaseClasses import ItemClassification, MultiWorld 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 setup_solo_multiworld, SVTestBase, SVTestCase, allsanity_options_without_mods, allsanity_options_with_mods
from .. import StardewItem, items_by_group, Group, StardewValleyWorld from .. import StardewItem, items_by_group, Group, StardewValleyWorld
from ..locations import locations_by_tag, LocationTags, location_table 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]: def get_option_choices(option) -> Dict[str, int]:
if issubclass(option, SpecialRange): if issubclass(option, NamedRange):
return option.special_range_names return option.special_range_names
elif option.options: elif option.options:
return option.options return option.options
@ -53,7 +53,7 @@ class TestGenerateDynamicOptions(SVTestCase):
def test_given_special_range_when_generate_then_basic_checks(self): def test_given_special_range_when_generate_then_basic_checks(self):
options = StardewValleyWorld.options_dataclass.type_hints options = StardewValleyWorld.options_dataclass.type_hints
for option_name, option in options.items(): for option_name, option in options.items():
if not isinstance(option, SpecialRange): if not isinstance(option, NamedRange):
continue continue
for value in option.special_range_names: for value in option.special_range_names:
with self.subTest(f"{option_name}: {value}"): 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): def test_given_special_range_when_generate_exclude_ginger_island(self):
options = StardewValleyWorld.options_dataclass.type_hints options = StardewValleyWorld.options_dataclass.type_hints
for option_name, option in options.items(): 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 continue
for value in option.special_range_names: for value in option.special_range_names:
with self.subTest(f"{option_name}: {value}"): with self.subTest(f"{option_name}: {value}"):

View File

@ -2,7 +2,7 @@ import unittest
from typing import Dict from typing import Dict
from BaseClasses import MultiWorld from BaseClasses import MultiWorld
from Options import SpecialRange from Options import NamedRange
from .option_names import options_to_include 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 worlds.stardew_valley.test.checks.world_checks import assert_can_win, assert_same_number_items_locations
from .. import setup_solo_multiworld, SVTestCase 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]: def get_option_choices(option) -> Dict[str, int]:
if issubclass(option, SpecialRange): if issubclass(option, NamedRange):
return option.special_range_names return option.special_range_names
elif option.options: elif option.options:
return option.options return option.options

View File

@ -2,7 +2,7 @@ from typing import Dict
import random import random
from BaseClasses import MultiWorld from BaseClasses import MultiWorld
from Options import SpecialRange, Range from Options import NamedRange, Range
from .option_names import options_to_include from .option_names import options_to_include
from .. import setup_solo_multiworld, SVTestCase from .. import setup_solo_multiworld, SVTestCase
from ..checks.goal_checks import assert_perfection_world_is_valid, assert_goal_world_is_valid 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]: def get_option_choices(option) -> Dict[str, int]:
if issubclass(option, SpecialRange): if issubclass(option, NamedRange):
return option.special_range_names return option.special_range_names
if issubclass(option, Range): if issubclass(option, Range):
return {f"{val}": val for val in range(option.range_start, option.range_end + 1)} return {f"{val}": val for val in range(option.range_start, option.range_end + 1)}

View File

@ -3,7 +3,7 @@ from dataclasses import dataclass
from typing import Dict, Tuple from typing import Dict, Tuple
from typing_extensions import TypeGuard # remove when Python >= 3.10 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 \ from zilliandomizer.options import \
Options as ZzOptions, char_to_gun, char_to_jump, ID, \ 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 from zilliandomizer.options.parsing import validate as zz_validate
class ZillionContinues(SpecialRange): class ZillionContinues(NamedRange):
""" """
number of continues before game over number of continues before game over
@ -218,7 +218,7 @@ class ZillionSkill(Range):
default = 2 default = 2
class ZillionStartingCards(SpecialRange): class ZillionStartingCards(NamedRange):
""" """
how many ID Cards to start the game with how many ID Cards to start the game with