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))
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:

View File

@ -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}."

View File

@ -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:

View File

@ -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,49 +523,28 @@ 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 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}`;
`below, then press the "Add" button to add a weight for it.<br /><br />Accepted values:<br />` +
`Normal range: ${setting.min} - ${setting.max}`;
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]}`;
acceptedValuesOutsideRange.push(setting.value_names[specialName]);
}
});
hintText.innerHTML += '<br /><br />Certain values have special meaning:';
Object.keys(setting.value_names).forEach((specialName) => {
hintText.innerHTML += `<br />${specialName}: ${setting.value_names[specialName]}`;
@ -577,7 +557,9 @@ class GameSettings {
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}`);
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';
@ -592,7 +574,16 @@ class GameSettings {
let option = optionInput.value;
if (!option || !option.trim()) { return; }
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 = '';
if (document.getElementById(`${this.name}-${settingName}-${option}-range`)) { return; }
@ -600,6 +591,15 @@ class GameSettings {
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');
@ -645,12 +645,21 @@ class GameSettings {
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; }
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');
@ -691,9 +700,8 @@ class GameSettings {
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 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;

View File

@ -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;
}

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
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,
}
```

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:
- 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`.

View File

@ -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}' "

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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,
}

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):
@ -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 = {

View File

@ -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."""

View File

@ -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}"):

View File

@ -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

View File

@ -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)}

View File

@ -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