This commit is contained in:
Azgaar 2019-11-17 17:52:39 +03:00
parent 4a0c62edf7
commit 6fa693b562
12 changed files with 111 additions and 60 deletions

View file

@ -1520,14 +1520,14 @@
<p data-tip="Map generation settings. Generate a new map to apply the settings">Map settings (new map to apply):</p>
<table>
<tr data-tip="Canvas height and width in pixels, please keep at your screen size or less to improve performance">
<tr data-tip="Canvas size in pixels, keep equal to screen size or less to improve performance. Please note the best aspect ratio for maps is 2:1">
<td></td>
<td>Canvas size</td>
<td>
<span data-tip="Map width in pixels">width</span>
<input data-tip="Map width in pixels" id="mapWidthInput" class="paired" type="number" min=240 value=960>
<span data-tip="Map height in pixels">height</span>
<input data-tip="Map height in pixels" id="mapHeightInput" class="paired" type="number" min=135 value=540>
<span>width</span>
<input id="mapWidthInput" class="paired" type="number" min=240 value=960>
<span>height</span>
<input id="mapHeightInput" class="paired" type="number" min=135 value=540>
</td>
<td>
<i data-tip="Toggle between screen size and initial canvas size" id="toggleFullscreen" class="icon-resize-full-alt"></i>

50
main.js
View file

@ -499,6 +499,7 @@ function generate() {
markFeatures();
openNearSeaLakes();
OceanLayers();
defineMapSize();
calculateMapCoordinates();
calculateTemperatures();
generatePrecipitation();
@ -676,6 +677,38 @@ function openNearSeaLakes() {
console.timeEnd("openLakes");
}
// define map size and position based on template and random factor
function defineMapSize() {
const [size, latitude] = getSizeAndLatitude();
if (!locked("mapSize")) mapSizeOutput.value = mapSizeInput.value = size;
if (!locked("latitude")) latitudeOutput.value = latitudeInput.value = latitude;
function getSizeAndLatitude() {
const template = document.getElementById("templateInput").value; // heightmap template
const part = grid.features.some(f => f.land && f.border); // if land goes over map borders
const max = part ? 85 : 100; // max size
const lat = part ? gauss(P(.5) ? 30 : 70, 15, 15, 85) : gauss(50, 20, 0, 100); // latiture shift
if (!part) {
if (template === "Pangea") return [100, 50];
if (template === "Shattered" && P(.8)) return [100, 50];
if (template === "Continents" && P(.7)) return [100, 50];
if (template === "Archipelago" && P(.35)) return [100, 50];
if (template === "High Island" && P(.3)) return [100, 50];
if (template === "Low Island" && P(.1)) return [100, 50];
}
if (template === "Pangea") return [gauss(75, 20, 30, max), lat];
if (template === "Volcano") return [gauss(40, 25, 10, max), lat];
if (template === "Mediterranean") return [gauss(40, 30, 15, 80), lat];
if (template === "Peninsula") return [gauss(15, 15, 5, 80), lat];
if (template === "Isthmus") return [gauss(20, 20, 3, 80), lat];
if (template === "Atoll") return [gauss(10, 10, 2, max), lat];
return [gauss(50, 20, 15, max), lat]; // Continents, Archipelago, High Island, Low Island
}
}
// calculate map position on globe
function calculateMapCoordinates() {
const size = +document.getElementById("mapSizeOutput").value;
@ -1158,7 +1191,7 @@ function addMarkers(number = 1) {
.attr("data-size", 1).attr("width", 30).attr("height", 30);
const height = getFriendlyHeight([x, y]);
const proper = Names.getCulture(cells.culture[cell]);
const name = Math.random() < .3 ? "Mount " + proper : Math.random() > .3 ? proper + " Volcano" : proper;
const name = P(.3) ? "Mount " + proper : Math.random() > .3 ? proper + " Volcano" : proper;
notes.push({id, name, legend:`Active volcano. Height: ${height}`});
count--;
}
@ -1234,7 +1267,7 @@ function addMarkers(number = 1) {
const burg = pack.burgs[cells.burg[cell]];
const river = pack.rivers.find(r => r.i === pack.cells.r[cell]);
const riverName = river ? `${river.name} ${river.type}` : "river";
const name = river && Math.random() < .2 ? river.name : burg.name;
const name = river && P(.2) ? river.name : burg.name;
notes.push({id, name:`${name} Bridge`, legend:`A stone bridge over the ${riverName} near ${burg.name}`});
count--;
}
@ -1261,8 +1294,8 @@ function addMarkers(number = 1) {
.attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30)
.attr("data-size", 1).attr("width", 30).attr("height", 30);
const type = Math.random() > .7 ? "inn" : "tavern";
const name = Math.random() < .5 ? ra(color) + " " + ra(animal) : Math.random() < .6 ? ra(adj) + " " + ra(animal) : ra(adj) + " " + capitalize(type);
const type = P(.3) ? "inn" : "tavern";
const name = P(.5) ? ra(color) + " " + ra(animal) : P(.6) ? ra(adj) + " " + ra(animal) : ra(adj) + " " + capitalize(type);
notes.push({id, name: "The " + name, legend:`A big and famous roadside ${type}`});
}
}()
@ -1375,7 +1408,7 @@ function addZones(number = 1) {
const cellsArray = [], queue = [cell], power = rand(5, 30);
while (queue.length) {
const q = Math.random() < .4 ? queue.shift() : queue.pop();
const q = P(.4) ? queue.shift() : queue.pop();
cellsArray.push(q);
if (cellsArray.length > power) break;
@ -1543,7 +1576,7 @@ function addZones(number = 1) {
const cellsArray = [], queue = [cell], power = rand(10, 30);
while (queue.length) {
const q = Math.random() < .5 ? queue.shift() : queue.pop();
const q = P(.5) ? queue.shift() : queue.pop();
cellsArray.push(q);
if (cellsArray.length > power) break;
cells.c[q].forEach(e => {
@ -1564,7 +1597,7 @@ function addZones(number = 1) {
const cellsArray = [], queue = [cell], power = rand(3, 15);
while (queue.length) {
const q = Math.random() < .3 ? queue.shift() : queue.pop();
const q = P(.3) ? queue.shift() : queue.pop();
cellsArray.push(q);
if (cellsArray.length > power) break;
cells.c[q].forEach(e => {
@ -1668,10 +1701,11 @@ function showStatistics() {
const template = templateInput.value;
const templateRandom = locked("template") ? "" : "(random)";
const stats = ` Seed: ${seed}
Size: ${graphWidth}x${graphHeight}
Canvas size: ${graphWidth}x${graphHeight}
Template: ${template} ${templateRandom}
Points: ${grid.points.length}
Cells: ${pack.cells.i.length}
Map size: ${mapSizeOutput.value}%
States: ${pack.states.length-1}
Provinces: ${pack.provinces.length-1}
Burgs: ${pack.burgs.length-1}

View file

@ -186,13 +186,13 @@
const defineBurgFeatures = function() {
pack.burgs.filter(b => b.i && !b.removed).forEach(b => {
const pop = b.population;
b.citadel = b.capital || pop > 50 && Math.random() < .75 || Math.random() < .5 ? 1 : 0;
b.plaza = pop > 50 || pop > 30 && Math.random() < .75 || pop > 10 && Math.random() < .5 || Math.random() < .25 ? 1 : 0;
b.walls = b.capital || pop > 30 || pop > 20 && Math.random() < .75 || pop > 10 && Math.random() < .5 || Math.random() < .2 ? 1 : 0;
b.shanty = pop > 30 || pop > 20 && Math.random() < .75 || b.walls && Math.random() < .75 ? 1 : 0;
b.citadel = b.capital || pop > 50 && P(.75) || P(.5) ? 1 : 0;
b.plaza = pop > 50 || pop > 30 && P(.75) || pop > 10 && P(.5) || P(.25) ? 1 : 0;
b.walls = b.capital || pop > 30 || pop > 20 && P(.75) || pop > 10 && P(.5) || P(.2) ? 1 : 0;
b.shanty = pop > 30 || pop > 20 && P(.75) || b.walls && P(.75) ? 1 : 0;
const religion = pack.cells.religion[b.cell];
const theocracy = pack.states[b.state].form === "Theocracy";
b.temple = religion && theocracy || pop > 50 || pop > 35 && Math.random() < .75 || pop > 20 && Math.random() < .5 ? 1 : 0;
b.temple = religion && theocracy || pop > 50 || pop > 35 && P(.75) || pop > 20 && P(.5) ? 1 : 0;
});
}
@ -642,7 +642,7 @@
let status = naval ? rw(navals) : neib ? rw(neibs) : neibOfNeib ? rw(neibsOfNeibs) : rw(far);
// add Vassal
if (neib && Math.random() < .8 && states[f].area > areaMean && states[t].area < areaMean && states[f].area / states[t].area > 2) status = "Vassal";
if (neib && P(.8) && states[f].area > areaMean && states[t].area < areaMean && states[f].area / states[t].area > 2) status = "Vassal";
states[f].diplomacy[t] = status === "Vassal" ? "Suzerain" : status;
states[t].diplomacy[f] = status;
}
@ -707,7 +707,7 @@
ad.forEach((r, d) => {
if (r !== "Ally" || states[d].diplomacy.includes("Vassal") || defenders.includes(d)) return;
const name = states[d].name;
if (states[d].diplomacy[defender] !== "Rival" && (Math.random() < .2 || ap <= dp * 1.2)) {war.push(`${an}'s ally ${name} avoided entering the war`); return;}
if (states[d].diplomacy[defender] !== "Rival" && (P(.2) || ap <= dp * 1.2)) {war.push(`${an}'s ally ${name} avoided entering the war`); return;}
const allies = states[d].diplomacy.map((r, d) => r === "Ally" ? d : 0).filter(d => d);
if (allies.some(ally => defenders.includes(ally))) {war.push(`${an}'s ally ${name} did not join the war as its allies are in war on both sides`); return;};
@ -759,7 +759,7 @@
for (const s of states) {
if (list && !list.includes(s.i)) continue;
const religion = pack.cells.religion[s.center];
const theocracy = religion && pack.religions[religion].expansion === "state" || (Math.random() < .1 && pack.religions[religion].type === "Organized");
const theocracy = religion && pack.religions[religion].expansion === "state" || (P(.1) && pack.religions[religion].type === "Organized");
s.form = theocracy ? "Theocracy" : s.type === "Naval" ? ra(navalArray) : ra(genericArray);
s.formName = selectForm(s);
s.fullName = getFullName(s);
@ -767,14 +767,14 @@
function selectForm(s) {
const base = pack.cultures[s.culture].base;
if (s.type === "Nomadic" && Math.random() < .3) return "Horde"; // some nomadic states
if (s.type === "Nomadic" && P(.3)) return "Horde"; // some nomadic states
if (s.form === "Monarchy") {
const form = monarchy[expTiers[s.i]];
// Default name depends on exponent tier, some culture bases have special names for tiers
if (s.diplomacy) {
if (form === "Duchy" && s.neighbors.length > 1 && rand(6) < s.neighbors.length && s.diplomacy.includes("Vassal")) return "Marches"; // some vassal dutchies on borderland
if (Math.random() < .3 && s.diplomacy.includes("Vassal")) return "Protectorate"; // some vassals
if (P(.3) && s.diplomacy.includes("Vassal")) return "Protectorate"; // some vassals
}
if (base === 16 && (form === "Empire" || form === "Kingdom")) return "Sultanate"; // Turkic
@ -797,7 +797,7 @@
s.name = pack.burgs[s.capital].name;
return "Free City";
}
if (Math.random() < .3) return "City-state";
if (P(.3)) return "City-state";
}
return rw(republic);
}
@ -861,7 +861,7 @@
const center = stateBurgs[i].cell;
const burg = stateBurgs[i].i;
const c = stateBurgs[i].culture;
const name = Math.random() < .5 ? Names.getState(Names.getCultureShort(c), c) : stateBurgs[i].name;
const name = P(.5) ? Names.getState(Names.getCultureShort(c), c) : stateBurgs[i].name;
const formName = rw(form);
form[formName] += 5;
const fullName = name + " " + formName;
@ -950,12 +950,12 @@
// generate "wild" province name
const c = cells.culture[center];
const name = burgCell && Math.random() < .5 ? burgs[burg].name : Names.getState(Names.getCultureShort(c), c);
const name = burgCell && P(.5) ? burgs[burg].name : Names.getState(Names.getCultureShort(c), c);
const f = pack.features[cells.f[center]];
const provCells = stateNoProvince.filter(i => cells.province[i] === province);
const singleIsle = provCells.length === f.cells && !provCells.find(i => cells.f[i] !== f.i);
const isleGroup = !singleIsle && !provCells.find(i => pack.features[cells.f[i]].group !== "isle");
const colony = !singleIsle && !isleGroup && Math.random() < .5 && !isPassable(s.center, center);
const colony = !singleIsle && !isleGroup && P(.5) && !isPassable(s.center, center);
const formName = singleIsle ? "Island" : isleGroup ? "Islands" : colony ? "Colony" : rw(forms["Wild"]);
const fullName = name + " " + formName;
const color = getMixedColor(s.color);

View file

@ -92,7 +92,7 @@
if (cells.h[i] > 50) return "Highland"; // no penalty for hills and moutains, high for other elevations
const f = pack.features[cells.f[cells.haven[i]]]; // feature
if (f.type === "lake" && f.cells > 5) return "Lake" // low water cross penalty and high for non-along-coastline growth
if ((f.cells < 10 && cells.harbor[i]) || (cells.harbor[i] === 1 && Math.random() < .5)) return "Naval"; // low water cross penalty and high for non-along-coastline growth
if ((f.cells < 10 && cells.harbor[i]) || (cells.harbor[i] === 1 && P(.5))) return "Naval"; // low water cross penalty and high for non-along-coastline growth
if (cells.r[i] && cells.fl[i] > 100) return "River"; // no River cross penalty, penalty for non-River growth
const b = cells.biome[i];
if (b === 4 || b === 1 || b === 2) return "Nomadic"; // high penalty in forest biomes and near coastline

View file

@ -14,17 +14,17 @@
const input = document.getElementById("templateInput");
if (!locked("template")) {
const templates = {
"Volcano": 5,
"Volcano": 3,
"High Island": 22,
"Low Island": 10,
"Low Island": 9,
"Continents": 20,
"Archipelago": 30,
"Archipelago": 25,
"Mediterranean":3,
"Peninsula": 3,
"Pangea": 2,
"Pangea": 5,
"Isthmus": 2,
"Atoll": 1,
"Shattered": 2};
"Shattered": 7};
input.value = rw(templates);
}
@ -235,7 +235,7 @@
const addHill = function(count, height, rangeX, rangeY) {
count = getNumberInRange(count);
const power = getBlobPower();
while (count >= 1 || Math.random() < count) {addOneHill(); count--;}
while (count > 0) {addOneHill(); count--;}
function addOneHill() {
const change = new Uint8Array( cells.h.length);
@ -268,7 +268,7 @@
const addPit = function(count, height, rangeX, rangeY) {
count = getNumberInRange(count);
while (count >= 1 || Math.random() < count) {addOnePit(); count--;}
while (count > 0) {addOnePit(); count--;}
function addOnePit() {
const used = new Uint8Array(cells.h.length);
@ -301,7 +301,7 @@
const addRange = function(count, height, rangeX, rangeY) {
count = getNumberInRange(count);
const power = getLinePower();
while (count >= 1 || Math.random() < count) {addOneRange(); count--;}
while (count > 0) {addOneRange(); count--;}
function addOneRange() {
const used = new Uint8Array(cells.h.length);
@ -376,7 +376,7 @@
const addTrough = function(count, height, rangeX, rangeY) {
count = getNumberInRange(count);
const power = getLinePower();
while (count >= 1 || Math.random() < count) {addOneTrough(); count--;}
while (count > 0) {addOneTrough(); count--;}
function addOneTrough() {
const used = new Uint8Array(cells.h.length);
@ -455,7 +455,7 @@
const addStrait = function(width, direction = "vertical") {
width = Math.min(getNumberInRange(width), grid.cellsX/3);
if (width < 1 && Math.random() < width) return;
if (width < 1 && P(width)) return;
const used = new Uint8Array(cells.h.length);
const vert = direction === "vertical";
const startX = vert ? Math.floor(Math.random() * graphWidth * .4 + graphWidth * .3) : 5;

View file

@ -137,17 +137,17 @@
if (base === 5 && ["sk", "ev", "ov"].includes(name.slice(-2))) name = name.slice(0,-2); // remove -sk/-ev/-ov for Ruthenian
else if (base === 12) return vowel(name.slice(-1)) ? name : name + "u"; // Japanese ends on any vowel or -u
else if (base === 18 && Math.random() < .4) name = vowel(name.slice(0,1).toLowerCase()) ? "Al" + name.toLowerCase() : "Al " + name; // Arabic starts with -Al
else if (base === 18 && P(.4)) name = vowel(name.slice(0,1).toLowerCase()) ? "Al" + name.toLowerCase() : "Al " + name; // Arabic starts with -Al
// no suffix for fantasy bases
if (base > 32 && base < 42) return name;
// define if suffix should be used
if (name.length > 3 && vowel(name.slice(-1))) {
if (vowel(name.slice(-2,-1)) && Math.random() < .85) name = name.slice(0,-2); // 85% for vv
else if (Math.random() < .7) name = name.slice(0,-1); // ~60% for cv
if (vowel(name.slice(-2,-1)) && P(.85)) name = name.slice(0,-2); // 85% for vv
else if (P(.7)) name = name.slice(0,-1); // ~60% for cv
else return name;
} else if (Math.random() < .4) return name; // 60% for cc and vc
} else if (P(.4)) return name; // 60% for cc and vc
// define suffix
let suffix = "ia"; // standard suffix
@ -187,17 +187,17 @@
const getMapName = function(force) {
if (!force && locked("mapName")) return;
if (force && locked("mapName")) unlock("mapName");
const base = Math.random() < .7 ? 2 : Math.random() < .5 ? rand(0, 6) : rand(0, 31);
const base = P(.7) ? 2 : P(.5) ? rand(0, 6) : rand(0, 31);
if (!nameBases[base]) {tip("Namebase is not found", false, "error"); return ""};
const min = nameBases[base].min-1;
const max = Math.max(nameBases[base].max-3, min);
const baseName = getBase(base, min, max, "", 0);
const name = Math.random() < .7 ? addSuffix(baseName) : baseName;
const name = P(.7) ? addSuffix(baseName) : baseName;
mapName.value = name;
}
function addSuffix(name) {
const suffix = Math.random() < .8 ? "ia" : "land";
const suffix = P(.8) ? "ia" : "land";
if (suffix === "ia" && name.length > 6) name = name.slice(0, -(name.length-3)); else
if (suffix === "land" && name.length > 6) name = name.slice(0, -(name.length-5));
return validateSuffix(name, suffix);

View file

@ -49,9 +49,9 @@
function randomizeOutline() {
const limits = [];
let odd = 0.2
let odd = .2
for (let l = -9; l < 0; l++) {
if (Math.random() < odd) {odd = 0.2; limits.push(l);}
if (P(odd)) {odd = .2; limits.push(l);}
else {odd *= 2;}
}
return limits;

View file

@ -178,13 +178,13 @@
const sin = Math.sin(angle), cos = Math.cos(angle);
const serpentine = 1 / (s + 1) + 0.3;
const meandr = serpentine + Math.random() * rndFactor;
if (Math.random() < 0.5) side *= -1; // change meandring direction in 50%
if (P(.5)) side *= -1; // change meandring direction in 50%
const dist2 = (eX - sX) ** 2 + (eY - sY) ** 2;
// if dist2 is big or river is small add extra points at 1/3 and 2/3 of segment
if (dist2 > 64 || (dist2 > 16 && segments.length < 6)) {
const p1x = (sX * 2 + eX) / 3 + side * -sin * meandr;
const p1y = (sY * 2 + eY) / 3 + side * cos * meandr;
if (Math.random() < 0.2) side *= -1; // change 2nd extra point meandring direction in 20%
if (P(.2)) side *= -1; // change 2nd extra point meandring direction in 20%
const p2x = (sX + eX * 2) / 3 + side * sin * meandr;
const p2y = (sY + eY * 2) / 3 + side * cos * meandr;
riverEnhanced.push([p1x, p1y], [p2x, p2y]);

View file

@ -947,7 +947,7 @@ function drawCoordinates() {
const desired = +coordinates.attr("data-size"); // desired label size
coordinates.attr("font-size", Math.max(rn(desired / scale ** .8, 2), .1)); // actual label size
const graticule = d3.geoGraticule().extent([[mapCoordinates.lonW, mapCoordinates.latN], [mapCoordinates.lonE, mapCoordinates.latS]])
const graticule = d3.geoGraticule().extent([[mapCoordinates.lonW, mapCoordinates.latN], [mapCoordinates.lonE+.1, mapCoordinates.latS+.1]])
.stepMajor([400, 400]).stepMinor([step, step]);
const projection = d3.geoEquirectangular().fitSize([graphWidth, graphHeight], graticule());

View file

@ -318,16 +318,13 @@ function randomizeOptions() {
if (!locked("power")) powerInput.value = powerOutput.value = gauss(3, 2, 0, 10);
if (!locked("neutral")) neutralInput.value = neutralOutput.value = rn(1 + Math.random(), 1);
if (!locked("cultures")) culturesInput.value = culturesOutput.value = gauss(12, 3, 5, 30);
if (!locked("culturesSet")) culturesSet.value = ra(Array.from(culturesSet.options)).value;
changeCultureSet();
if (!locked("culturesSet")) randomizeCultureSet();
// 'Configure World' settings
if (!locked("prec")) precInput.value = precOutput.value = gauss(120, 20, 5, 500);
const tMax = +temperatureEquatorOutput.max, tMin = +temperatureEquatorOutput.min; // temperature extremes
if (!locked("temperatureEquator")) temperatureEquatorOutput.value = temperatureEquatorInput.value = rand(tMax-6, tMax);
if (!locked("temperaturePole")) temperaturePoleOutput.value = temperaturePoleInput.value = rand(tMin, tMin+10);
if (!locked("mapSize")) mapSizeOutput.value = mapSizeInput.value = gauss(50, 20, 15, 100);
if (!locked("latitude")) latitudeOutput.value = latitudeInput.value = gauss(50, 20, 15, 100);
// 'Units Editor' settings
const US = navigator.language === "en-US";
@ -338,6 +335,21 @@ function randomizeOptions() {
if (!stored("temperatureScale")) temperatureScale.value = US ? "°F" : "°C";
}
// select culture set pseudo-randomly
function randomizeCultureSet() {
const sets = {
"world": 25,
"european": 20,
"oriental": 10,
"english": 10,
"antique": 5,
"highFantasy": 22,
"darkFantasy": 6,
"random": 2};
culturesSet.value = rw(sets);
changeCultureSet();
}
// remove all saved data from LocalStorage and reload the page
function restoreDefaultOptions() {
localStorage.clear();

View file

@ -749,8 +749,7 @@ function editStates() {
const center = burgCell ? burgCell : provCells[0];
const burg = burgCell ? cells.burg[burgCell] : 0;
const name = burgCell && Math.random() < .7
? getAdjective(pack.burgs[burg].name)
const name = burgCell && P(.7) ? getAdjective(pack.burgs[burg].name)
: getAdjective(states[state].name) + " " + provinces[initProv].name.split(" ").slice(-1)[0];
const formName = name.split(" ").length > 1 ? provinces[initProv].formName : rw(form);
const fullName = name + " " + formName;

View file

@ -217,11 +217,17 @@ function convertTemperature(c) {
// random number in a range
function rand(min, max) {
if (min === undefined && !max === undefined) return Math.random();
if (min === undefined && max === undefined) return Math.random();
if (max === undefined) {max = min; min = 0;}
return Math.floor(Math.random() * (max - min + 1)) + min;
}
// probability shorthand
function P(probability) {
return Math.random() < probability;
}
// random number (normal or gaussian distribution)
function gauss(expected = 100, deviation = 30, min = 0, max = 300, round = 0) {
return rn(Math.max(Math.min(d3.randomNormal(expected, deviation)(), max), min), round);
}
@ -380,7 +386,7 @@ function getAdjective(string) {
// special cases for some suffixes
if (string.length > 8 && string.slice(-6) === "orszag") return string.slice(0, -6);
if (string.length > 6 && string.slice(-4) === "stan") return string.slice(0, -4);
if (Math.random() < .5 && string.slice(-4) === "land") return string + "ic";
if (P(.5) && string.slice(-4) === "land") return string + "ic";
if (string.slice(-4) === " Guo") string = string.slice(0, -4);
// don't change is name ends on suffix
@ -447,7 +453,7 @@ function lim(v) {
// get number from string in format "1-3" or "2" or "0.5"
function getNumberInRange(r) {
if (typeof r !== "string") {console.error("The value should be a string", r); return 0;}
if (!isNaN(+r)) return +r;
if (!isNaN(+r)) return ~~r + +P(r - ~~r);
const sign = r[0] === "-" ? -1 : 1;
if (isNaN(+r[0])) r = r.slice(1);
const range = r.includes("-") ? r.split("-") : null;