Merge branch 'upstream' into bugfix

This commit is contained in:
Mészáros Gergely 2022-04-17 00:26:47 +02:00
commit 3253bfdac3
19 changed files with 7095 additions and 4459 deletions

10459
index.html

File diff suppressed because one or more lines are too long

12
main.js
View file

@ -155,6 +155,7 @@ let options = {
};
let mapCoordinates = {}; // map coordinates on globe
let populationRate = +document.getElementById("populationRateInput").value;
let distanceScale = +document.getElementById("distanceScaleInput").value;
let urbanization = +document.getElementById("urbanizationInput").value;
let urbanDensity = +document.getElementById("urbanDensityInput").value;
@ -826,6 +827,7 @@ function markupGridOcean() {
TIME && console.timeEnd("markupGridOcean");
}
// Calculate cell-distance to coast for every cell
function markup(cells, start, increment, limit) {
for (let t = start, count = Infinity; count > 0 && t > limit; t += increment) {
count = 0;
@ -1617,14 +1619,16 @@ function addZones(number = 1) {
}
function addRebels() {
const state = ra(states.filter(s => s.i && s.neighbors.some(n => n)));
const state = ra(states.filter(s => s.i && !s.removed && s.neighbors.some(n => n)));
if (!state) return;
const neib = ra(state.neighbors.filter(n => n));
const cell = cells.i.find(i => cells.state[i] === state.i && cells.c[i].some(c => cells.state[c] === neib));
const neib = ra(state.neighbors.filter(n => n && !states[n].removed));
if (!neib) return;
const cell = cells.i.find(i => cells.state[i] === state.i && !state.removed && cells.c[i].some(c => cells.state[c] === neib));
const cellsArray = [],
queue = [cell],
queue = [],
power = rand(10, 30);
if (cell) queue.push.cell;
while (queue.length) {
const q = queue.shift();

View file

@ -256,7 +256,7 @@ window.BurgsAndStates = (function () {
icons.selectAll("use").remove();
// capitals
const capitals = pack.burgs.filter(b => b.capital);
const capitals = pack.burgs.filter(b => b.capital && !b.removed);
const capitalIcons = burgIcons.select("#cities");
const capitalLabels = burgLabels.select("#cities");
const capitalSize = capitalIcons.attr("size") || 1;
@ -299,7 +299,7 @@ window.BurgsAndStates = (function () {
.attr("height", caSize);
// towns
const towns = pack.burgs.filter(b => b.i && !b.capital);
const towns = pack.burgs.filter(b => b.i && !b.capital && !b.removed);
const townIcons = burgIcons.select("#towns");
const townLabels = burgLabels.select("#towns");
const townSize = townIcons.attr("size") || 0.5;

24
modules/io/formats.js Normal file
View file

@ -0,0 +1,24 @@
"use strict";
window.Formats = (function () {
async function csvParser(file, separator = ",") {
const txt = await file.text();
const rows = txt.split("\n");
const headers = rows
.shift()
.split(separator)
.map(x => x.toLowerCase());
const data = rows.filter(a => a.trim() !== "").map(r => r.split(separator));
return {
headers,
data,
iterator: function* (sortf) {
const dataset = sortf ? this.data.sort(sortf) : this.data;
for (const d of dataset) yield Object.fromEntries(d.map((a, i) => [this.headers[i], a]));
}
};
}
return {csvParser};
})();

View file

@ -205,7 +205,7 @@ async function parseLoadedData(data) {
void (function parseSettings() {
const settings = data[1].split("|");
if (settings[0]) applyOption(distanceUnitInput, settings[0]);
if (settings[1]) distanceScaleInput.value = distanceScaleOutput.value = settings[1];
if (settings[1]) distanceScale = distanceScaleInput.value = distanceScaleOutput.value = settings[1];
if (settings[2]) areaUnit.value = settings[2];
if (settings[3]) applyOption(heightUnit, settings[3]);
if (settings[4]) heightExponentInput.value = heightExponentOutput.value = settings[4];
@ -459,7 +459,7 @@ async function parseLoadedData(data) {
invalidReligions.forEach(r => {
const invalidCells = cells.i.filter(i => cells.religion[i] === r);
invalidCells.forEach(i => (cells.religion[i] = 0));
ERROR && console.error("Data Integrity Check. Invalid religion", c, "is assigned to cells", invalidCells);
ERROR && console.error("Data Integrity Check. Invalid religion", r, "is assigned to cells", invalidCells);
});
const invalidFeatures = [...new Set(cells.f)].filter(f => f && !pack.features[f]);

View file

@ -8,6 +8,18 @@ window.Markers = (function () {
const culturesSet = document.getElementById("culturesSet").value;
const isFantasy = culturesSet.includes("Fantasy");
/*
Default markers config:
type - short description (snake-case)
icon - unicode character, make sure it's supported by most of the browsers. Source: emojipedia.org
dx: icon offset in x direction, in pixels
dy: icon offset in y direction, in pixels
min: minimum number of candidates to add at least 1 marker
each: how many of the candidates should be added as markers
multiplier: multiply markers quantity to add
list: function to select candidates
add: function to add marker legend
*/
return [
{type: "volcanoes", icon: "🌋", dx: 52, px: 13, min: 10, each: 500, multiplier: 1, list: listVolcanoes, add: addVolcano},
{type: "hot-springs", icon: "♨️", dy: 52, min: 30, each: 1200, multiplier: 1, list: listHotSprings, add: addHotSpring},
@ -28,8 +40,15 @@ window.Markers = (function () {
{type: "brigands", icon: "💰", px: 13, min: 50, each: 100, multiplier: 1, list: listBrigands, add: addBrigands},
{type: "pirates", icon: "🏴‍☠️", dx: 51, min: 40, each: 300, multiplier: 1, list: listPirates, add: addPirates},
{type: "statues", icon: "🗿", min: 80, each: 1200, multiplier: 1, list: listStatues, add: addStatue},
{type: "ruines", icon: "🏺", min: 80, each: 1200, multiplier: 1, list: listRuins, add: addRuins},
{type: "portals", icon: "🌀", px: 14, min: 16, each: 8, multiplier: +isFantasy, list: listPortals, add: addPortal}
{type: "ruins", icon: "🏺", min: 80, each: 1200, multiplier: 1, list: listRuins, add: addRuins},
{type: "circuses", icon: "🎪", min: 80, each: 1000, multiplier: 1, list: listCircuses, add: addCircuses},
{type: "jousts", icon: "🤺", dx: 48, min: 5, each: 500, multiplier: 1, list: listJousts, add: addJousts},
{type: "canoes", icon: "🛶", min: 1000, each: 2000, multiplier: 1, list: listCanoes, add: addCanoes},
{type: "migration", icon: "🐗", min: 20, each: 1000, multiplier: 1, list: listMigrations, add: addMigrations},
{type: "dances", icon: "💃🏽", min: 5, each: 60, multiplier: 1, list: listDances, add: addDances},
{type: "mirage", icon: "💦", min: 10, each: 400, multiplier: 1, list: listMirage, add: addMirage},
{type: "portals", icon: "🌀", px: 14, min: 16, each: 8, multiplier: +isFantasy, list: listPortals, add: addPortal},
{type: "rifts", icon: "🎆", min: 1, each: 3000, multiplier: +isFantasy, list: listRifts, add: addRifts}
];
}
@ -84,6 +103,8 @@ window.Markers = (function () {
let candidates = Array.from(list(pack));
let quantity = getQuantity(candidates, min, each, multiplier);
// uncomment for debugging:
// console.log(`${icon} ${type}: each ${each} of ${candidates.length}, min ${min} candidates. Got ${quantity}`);
while (quantity && candidates.length) {
const [cell] = extractAnyElement(candidates);
@ -129,6 +150,12 @@ window.Markers = (function () {
return marker;
}
function deleteMarker(markerId) {
const noteId = "marker" + markerId;
notes = notes.filter(note => note.id !== noteId);
pack.markers = pack.markers.filter(m => m.i !== markerId);
}
function listVolcanoes({cells}) {
return cells.i.filter(i => !occupied[i] && cells.h[i] >= 70);
}
@ -182,7 +209,7 @@ window.Markers = (function () {
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 && P(0.2) ? river.name : burg.name;
const name = river && P(0.2) ? `${river.name} Bridge` : `${burg.name} Bridge`;
const weightedAdjectives = {
stone: 10,
wooden: 1,
@ -192,7 +219,18 @@ window.Markers = (function () {
beaten: 1,
weathered: 1
};
notes.push({id, name: `${name} Bridge`, legend: `A ${rw(weightedAdjectives)} bridge spans over the ${riverName} near ${burg.name}`});
const barriers = [
"collapse during the flood",
"being rumoured to attract trolls",
"the drying up of local trade",
"banditry infested the area",
"the old waypoints crumbled"
];
const legend = P(0.7)
? `A ${rw(weightedAdjectives)} bridge spans over the ${riverName} near ${burg.name}`
: `An old crossing of the ${riverName}, rarely used since ${ra(barriers)}`;
notes.push({id, name, legend});
}
function listInns({cells}) {
@ -407,14 +445,14 @@ window.Markers = (function () {
const drinks = [
"wine",
"brandy",
"jinn",
"gin",
"whisky",
"rom",
"beer",
"cider",
"mead",
"liquor",
"spirit",
"spirits",
"vodka",
"tequila",
"absinthe",
@ -773,10 +811,150 @@ window.Markers = (function () {
const ruinType = ra(types);
const name = `Ruined ${ruinType}`;
const legend = `Ruins of an ancient ${ruinType.toLowerCase()}. Untold riches may lie within.`;
const legend = `Ruins of an ancient ${ruinType.toLowerCase()}. Untold riches may lie within`;
notes.push({id, name, legend});
}
function listCircuses({cells}) {
return cells.i.filter(i => !occupied[i] && cells.culture[i] && cells.h[i] >= 20 && pack.cells.road[i]);
}
function addCircuses(id, cell) {
const adjectives = ["Fantastical", "Wonderous", "Incomprehensible", "Magical", "Extraordinary", "Unmissable", "World-famous", "Breathtaking"];
const adjective = ra(adjectives);
const name = `Travelling ${adjective} Circus`;
const legend = `Roll up, roll up, this ${adjective.toLowerCase()} circus is here for a limited time only`;
notes.push({id, name, legend});
}
function listJousts({cells, burgs}) {
return cells.i.filter(i => !occupied[i] && cells.burg[i] && burgs[cells.burg[i]].population > 20);
}
function addJousts(id, cell) {
const {cells, burgs} = pack;
const types = ["Joust", "Competition", "Melee", "Tournament", "Contest"];
const virtues = ["cunning", "might", "speed", "the greats", "acumen", "brutality"];
if (!cells.burg[cell]) return;
const burgName = burgs[cells.burg[cell]].name;
const type = ra(types);
const virtue = ra(virtues);
const name = `${burgName} ${type}`;
const legend = `Warriors from around the land gather for a ${type.toLowerCase()} of ${virtue} in ${burgName}, with fame, fortune and favour on offer to the victor`;
notes.push({id, name, legend});
}
function listCanoes({cells}) {
return cells.i.filter(i => !occupied[i] && cells.r[i]);
}
function addCanoes(id, cell) {
const river = pack.rivers.find(r => r.i === pack.cells.r[cell]);
const name = `Minor Jetty`;
const riverName = river ? `${river.name} ${river.type}` : "river";
const legend = `A small location along the ${riverName} to launch boats from sits here, along with a weary looking owner, willing to sell passage along the river`;
notes.push({id, name, legend});
}
function listMigrations({cells}) {
return cells.i.filter(i => !occupied[i] && cells.h[i] >= 20 && cells.pop[i] <= 2);
}
function addMigrations(id, cell) {
const animals = [
"Antelopes",
"Apes",
"Badgers",
"Bears",
"Beavers",
"Bisons",
"Boars",
"Buffalo",
"Cats",
"Cranes",
"Crocodiles",
"Crows",
"Deers",
"Dogs",
"Eagles",
"Elks",
"Foxs",
"Goats",
"Geese",
"Hares",
"Hawks",
"Herons",
"Horses",
"Hyenas",
"Ibises",
"Jackals",
"Jaguars",
"Larks",
"Leopards",
"Lions",
"Mantises",
"Martens",
"Mooses",
"Mules",
"Owls",
"Panthers",
"Rats",
"Ravens",
"Rooks",
"Scorpions",
"Sharks",
"Sheeps",
"Snakes",
"Spiders",
"Tigers",
"Wolfs",
"Wolverines",
"Camels",
"Falcons",
"Hounds",
"Oxen"
];
const animalChoice = ra(animals);
const name = `${animalChoice} migration`;
const legend = `A huge group of ${animalChoice.toLowerCase()} is migrating, though whether part of their annual routine, or something more extraordinary`;
notes.push({id, name, legend});
}
function listDances({cells, burgs}) {
return cells.i.filter(i => !occupied[i] && cells.burg[i] && burgs[cells.burg[i]].population > 15);
}
function addDances(id, cell) {
const {cells, burgs} = pack;
const burgName = burgs[cells.burg[cell]].name;
const socialTypes = ["gala", "dance", "performance", "ball", "soiree", "jamboree", "exhibition", "carnival", "festival", "jubilee"];
const people = ["great and the good", "nobility", "local elders", "foreign dignitaries", "spiritual leaders", "suspected revolutionaries"];
const socialType = ra(socialTypes);
const name = `${burgName} ${socialType}`;
const legend = `A ${socialType} has been organised at ${burgName} as a chance to gather the ${ra(
people
)} of the area together to be merry, make alliances and scheme around the crisis`;
notes.push({id, name, legend});
}
function listMirage({cells}) {
return cells.i.filter(i => !occupied[i] && cells.biome[i] === 1);
}
function addMirage(id, cell) {
const adjectives = ["Entrancing", "Diaphanous", "Illusory", "Distant", "Perculiar"];
const mirageAdjective = ra(adjectives);
const name = `${mirageAdjective} mirage`;
const legend = `This ${mirageAdjective.toLowerCase()} mirage has been luring travellers out of their way for eons`;
notes.push({id, name, legend});
}
function listPortals({burgs}) {
return burgs
.slice(1, Math.ceil(burgs.length / 10) + 1)
@ -787,14 +965,34 @@ window.Markers = (function () {
function addPortal(id, cell) {
const {cells, burgs} = pack;
// Portals can only be added to burgs
if (cells.burg[cell]) return;
if (!cells.burg[cell]) return;
const burgName = burgs[cells.burg[cell]].name;
const name = `${burgName} Portal`;
const legend = `An element of the magic portal system connecting major cities. Portals installed centuries ago, but still work fine`;
const legend = `An element of the magic portal system connecting major cities. The portals were installed centuries ago, but still work fine`;
notes.push({id, name, legend});
}
return {add, generate, regenerate, getConfig, setConfig};
function listRifts({cells}) {
return cells.i.filter(i => !occupied[i] && pack.cells.pop[i] <= 3 && biomesData.habitability[pack.cells.biome[i]]);
}
function addRifts(id, cell) {
const types = ["Demonic", "Interdimensional", "Abyssal", "Cosmic", "Cataclysmic", "Subterranean", "Ancient"];
const descriptions = [
"all known nearby beings to flee in terror",
"cracks in reality itself to form",
"swarms of foes to spill forth",
"the life of nearby plants to wither and decay",
"an emmissary to step through with an all-powerful relic"
];
const riftType = ra(types);
const name = `${riftType} Rift`;
const legend = `A rumoured ${riftType.toLowerCase()} rift in this area is causing ${ra(descriptions)}.`;
notes.push({id, name, legend});
}
return {add, generate, regenerate, getConfig, setConfig, deleteMarker};
})();

View file

@ -157,14 +157,6 @@ window.Military = (function () {
}
}
void (function removeExistingRegiments() {
armies.selectAll("g > g").each(function () {
const index = notes.findIndex(n => n.id === this.id);
if (index != -1) notes.splice(index, 1);
});
armies.selectAll("g").remove();
})();
const expected = 3 * populationRate; // expected regiment size
const mergeable = (n0, n1) => (!n0.s && !n1.s) || n0.u === n1.u; // check if regiments can be merged
@ -172,9 +164,10 @@ window.Military = (function () {
valid.forEach(s => {
s.military = createRegiments(s.temp.platoons, s);
delete s.temp; // do not store temp data
drawRegiments(s.military, s.i);
});
redraw();
function createRegiments(nodes, s) {
if (!nodes.length) return [];
@ -236,6 +229,16 @@ window.Military = (function () {
TIME && console.timeEnd("generateMilitaryForces");
};
function redraw() {
const validStates = pack.states.filter(s => s.i && !s.removed);
armies.selectAll("g > g").each(function () {
const index = notes.findIndex(n => n.id === this.id);
if (index != -1) notes.splice(index, 1);
});
armies.selectAll("g").remove();
validStates.forEach(s => drawRegiments(s.military, s.i));
}
const getDefaultOptions = function () {
return [
{icon: "⚔️", name: "infantry", rural: 0.25, urban: 0.2, crew: 1, power: 1, type: "melee", separate: 0},
@ -406,5 +409,5 @@ window.Military = (function () {
notes.push({id: `regiment${s.i}-${r.i}`, name: `${r.icon} ${r.name}`, legend});
};
return {generate, getDefaultOptions, getName, generateNote, drawRegiments, drawRegiment, moveRegiment, getTotal, getEmblem};
return {generate, redraw, getDefaultOptions, getName, generateNote, drawRegiments, drawRegiment, moveRegiment, getTotal, getEmblem};
})();

View file

@ -35,10 +35,12 @@ window.Rivers = (function () {
TIME && console.timeEnd("generateRivers");
function drainWater() {
//const MIN_FLUX_TO_FORM_RIVER = 10 * distanceScale;
const MIN_FLUX_TO_FORM_RIVER = 30;
const cellsNumberModifier = (pointsInput.dataset.cells / 10000) ** 0.25;
const prec = grid.cells.prec;
const area = pack.cells.area;
const land = cells.i.filter(i => h[i] >= 20).sort((a, b) => h[b] - h[a]);
const lakeOutCells = Lakes.setClimateData(h);

View file

@ -74,7 +74,7 @@ window.Routes = (function () {
const ports = allPorts.filter(b => b.port === f); // all ports on the same feature
if (!ports.length) return;
if (features[f].border) addOverseaRoute(f, ports[0]);
if (features[f]?.border) addOverseaRoute(f, ports[0]);
// get inner-map routes
for (let s = 0; s < ports.length; s++) {

411
modules/submap.js Normal file
View file

@ -0,0 +1,411 @@
"use strict";
/*
Cell resampler module used by submapper and resampler (transform)
main function: resample(options);
*/
window.Submap = (function () {
const isWater = (map, id) => (map.grid.cells.h[map.pack.cells.g[id]] < 20 ? true : false);
const inMap = (x, y) => x > 0 && x < graphWidth && y > 0 && y < graphHeight;
function resample(parentMap, options) {
/*
generate new map based on an existing one (resampling parentMap)
parentMap: {seed, grid, pack} from original map
options = {
projection: f(Number,Number)->[Number, Number]
function to calculate new coordinates
inverse: g(Number,Number)->[Number, Number]
inverse of f
depressRivers: Bool carve out riverbeds?
smoothHeightMap: Bool run smooth filter on heights
addLakesInDepressions: call FMG original funtion on heightmap
lockMarkers: Bool Auto lock all copied markers
lockBurgs: Bool Auto lock all copied burgs
}
*/
const projection = options.projection;
const inverse = options.inverse;
const stage = s => INFO && console.log("SUBMAP:", s);
const timeStart = performance.now();
const childMap = {grid, pack};
invokeActiveZooming();
// copy seed
seed = parentMap.seed;
Math.random = aleaPRNG(seed);
INFO && console.group("SubMap with seed: " + seed);
DEBUG && console.log("Using Options:", options);
// create new grid
applyMapSize();
placePoints();
calculateVoronoi(grid, grid.points);
drawScaleBar(scale);
const resampler = (points, qtree, f) => {
for (const [i, [x, y]] of points.entries()) {
const [tx, ty] = inverse(x, y);
const oldid = qtree.find(tx, ty, Infinity)[2];
f(i, oldid);
}
};
stage("Resampling heightmap, temperature and precipitation.");
// resample heightmap from old WorldState
const n = grid.points.length;
grid.cells.h = new Uint8Array(n); // heightmap
grid.cells.temp = new Int8Array(n); // temperature
grid.cells.prec = new Int8Array(n); // precipitation
const reverseGridMap = new Uint32Array(n); // cellmap from new -> oldcell
const oldGrid = parentMap.grid;
// build cache old -> [newcelllist]
const forwardGridMap = parentMap.grid.points.map(_ => []);
resampler(grid.points, parentMap.pack.cells.q, (id, oldid) => {
const cid = parentMap.pack.cells.g[oldid];
grid.cells.h[id] = oldGrid.cells.h[cid];
grid.cells.temp[id] = oldGrid.cells.temp[cid];
grid.cells.prec[id] = oldGrid.cells.prec[cid];
if (options.depressRivers) forwardGridMap[cid].push(id);
reverseGridMap[id] = cid;
});
// TODO: add smooth/noise function for h, temp, prec n times
// smooth heightmap
// smoothing should never change cell type (land->water or water->land)
if (options.smoothHeightMap) {
const gcells = grid.cells;
gcells.h.forEach((h, i) => {
const hs = gcells.c[i].map(c => gcells.h[c]);
hs.push(h);
gcells.h[i] = h >= 20 ? Math.max(d3.mean(hs), 20) : Math.min(d3.mean(hs), 19);
});
}
if (options.depressRivers) {
stage("Generating riverbeds.");
const rbeds = new Uint16Array(grid.cells.i.length);
// and erode riverbeds
parentMap.pack.rivers.forEach(r =>
r.cells.forEach(oldpc => {
if (oldpc < 0) return; // ignore out-of-map marker (-1)
const oldc = parentMap.pack.cells.g[oldpc];
const targetCells = forwardGridMap[oldc];
if (!targetCells) throw "TargetCell shouldn't be empty.";
targetCells.forEach(c => {
if (grid.cells.h[c] < 20) return;
rbeds[c] = 1;
});
})
);
// raise every land cell a bit except riverbeds
grid.cells.h.forEach((h, i) => {
if (rbeds[i] || h < 20) return;
grid.cells.h[i] = Math.min(h + 2, 100);
});
}
stage("Detect features, ocean and generating lakes.");
markFeatures();
markupGridOcean();
// Warning: addLakesInDeepDepressions can be very slow!
if (options.addLakesInDepressions) {
addLakesInDeepDepressions();
openNearSeaLakes();
}
OceanLayers();
calculateMapCoordinates();
// calculateTemperatures();
// generatePrecipitation();
stage("Cell cleanup.");
reGraph();
// remove misclassified cells
stage("Define coastline.");
drawCoastline();
/****************************************************/
/* Packed Graph */
/****************************************************/
const oldCells = parentMap.pack.cells;
// const reverseMap = new Map(); // cellmap from new -> oldcell
const forwardMap = parentMap.pack.cells.p.map(_ => []); // old -> [newcelllist]
const pn = pack.cells.i.length;
const cells = pack.cells;
cells.culture = new Uint16Array(pn);
cells.state = new Uint16Array(pn);
cells.burg = new Uint16Array(pn);
cells.religion = new Uint16Array(pn);
cells.road = new Uint16Array(pn);
cells.crossroad = new Uint16Array(pn);
cells.province = new Uint16Array(pn);
stage("Resampling culture, state and religion map.");
for (const [id, gridCellId] of cells.g.entries()) {
const oldGridId = reverseGridMap[gridCellId];
if (oldGridId === undefined) {
console.error("Can not find old cell id", reverseGridMap, "in", gridCellId);
continue;
}
// find old parent's children
const oldChildren = oldCells.i.filter(oid => oldCells.g[oid] == oldGridId);
let oldid; // matching cell on the original map
if (!oldChildren.length) {
// it *must* be a (deleted) deep ocean cell
if (!oldGrid.cells.h[oldGridId] < 20) {
console.error(`Warning, ${gridCellId} should be water cell, not ${oldGrid.cells.h[oldGridId]}`);
continue;
}
// find replacement: closest water cell
const [ox, oy] = cells.p[id];
const [tx, ty] = inverse(x, y);
oldid = oldCells.q.find(tx, ty, Infinity)[2];
if (!oldid) {
console.warn("Warning, no id found in quad", id, "parent", gridCellId);
continue;
}
} else {
// find closest children (packcell) on the parent map
const distance = x => (x[0] - cells.p[id][0]) ** 2 + (x[1] - cells.p[id][1]) ** 2;
let d = Infinity;
oldChildren.forEach(oid => {
// this should be always true, unless some algo modded the height!
if (isWater(parentMap, oid) !== isWater(childMap, id)) {
console.warn(`cell sank because of addLakesInDepressions: ${oid}`);
return;
}
const [oldpx, oldpy] = oldCells.p[oid];
const nd = distance(projection(oldpx, oldpy));
if (isNaN(nd)) {
console.error("Distance is not a number!", "Old point:", oldpx, oldpy);
}
if (nd < d) [d, oldid] = [nd, oid];
});
if (oldid === undefined) {
console.warn("Warning, no match for", id, "(parent:", gridCellId, ")");
continue;
}
}
if (isWater(childMap, id) !== isWater(parentMap, oldid)) {
WARN && console.warn("Type discrepancy detected:", id, oldid, `${pack.cells.t[id]} != ${oldCells.t[oldid]}`);
}
cells.culture[id] = oldCells.culture[oldid];
cells.state[id] = oldCells.state[oldid];
cells.religion[id] = oldCells.religion[oldid];
cells.province[id] = oldCells.province[oldid];
// reverseMap.set(id, oldid)
forwardMap[oldid].push(id);
}
stage("Regenerating river network.");
Rivers.generate();
drawRivers();
Lakes.defineGroup();
// biome calculation based on (resampled) grid.cells.temp and prec
// it's safe to recalculate.
stage("Regenerating Biome.");
defineBiomes();
// recalculate suitability and population
// TODO: normalize according to the base-map
rankCells();
stage("Porting Cultures");
pack.cultures = parentMap.pack.cultures;
// fix culture centers
const validCultures = new Set(pack.cells.culture);
pack.cultures.forEach((c, i) => {
if (!i) return; // ignore wildlands
if (!validCultures.has(i)) {
c.removed = true;
c.center = null;
return;
}
const newCenters = forwardMap[c.center];
c.center = newCenters.length ? newCenters[0] : pack.cells.culture.findIndex(x => x === i);
});
stage("Porting and locking burgs.");
copyBurgs(parentMap, projection, options);
// transfer states, mark states without land as removed.
stage("Porting states.");
const validStates = new Set(pack.cells.state);
pack.states = parentMap.pack.states;
// keep valid states and neighbors only
pack.states.forEach((s, i) => {
if (!s.i || s.removed) return; // ignore removed and neutrals
if (!validStates.has(i)) s.removed = true;
s.neighbors = s.neighbors.filter(n => validStates.has(n));
// find center
s.center = pack.burgs[s.capital].cell
? pack.burgs[s.capital].cell // capital is the best bet
: pack.cells.state.findIndex(x => x === i); // otherwise use the first valid cell
});
// transfer provinces, mark provinces without land as removed.
stage("Porting provinces.");
const validProvinces = new Set(pack.cells.province);
pack.provinces = parentMap.pack.provinces;
// mark uneccesary provinces
pack.provinces.forEach((p, i) => {
if (!p || p.removed) return;
if (!validProvinces.has(i)) {
p.removed = true;
return;
}
const newCenters = forwardMap[p.center];
p.center = newCenters.length ? newCenters[0] : pack.cells.province.findIndex(x => x === i);
});
BurgsAndStates.drawBurgs();
stage("Regenerating road network.");
Routes.regenerate();
drawStates();
drawBorders();
BurgsAndStates.drawStateLabels();
Rivers.specify();
Lakes.generateName();
stage("Porting military.");
for (const s of pack.states) {
if (!s.military) continue;
for (const m of s.military) {
[m.x, m.y] = projection(m.x, m.y);
[m.bx, m.by] = projection(m.bx, m.by);
const cc = forwardMap[m.cell];
m.cell = cc && cc.length ? cc[0] : null;
}
s.military = s.military.filter(m => m.cell).map((m, i) => ({...m, i}));
}
Military.redraw();
stage("Copying markers.");
for (const m of pack.markers) {
const [x, y] = projection(m.x, m.y);
if (!inMap(x, y)) {
Markers.deleteMarker(m.i);
} else {
m.x = x;
m.y = y;
m.cell = findCell(x, y);
if (options.lockMarkers) m.lock = true;
}
}
drawMarkers();
stage("Regenerating Zones.");
addZones();
Names.getMapName();
stage("Submap done.");
WARN && console.warn(`TOTAL: ${rn((performance.now() - timeStart) / 1000, 2)}s`);
showStatistics();
INFO && console.groupEnd("Generated Map " + seed);
}
/* find the nearest cell accepted by filter f *and* having at
* least one *neighbor* fulfilling filter g, up to cell-distance `max`
* returns [cellid, neighbor] tuple or undefined if no such cell.
* accepts coordinates (x, y)
*/
const findNearest =
(f, g, max = 3) =>
(px, py) => {
const d2 = c => (px - pack.cells.p[c][0]) ** 2 + (py - pack.cells.p[c][0]) ** 2;
const startCell = findCell(px, py);
const tested = new Set([startCell]); // ignore analyzed cells
const kernel = (cs, level) => {
const [bestf, bestg] = cs.filter(f).reduce(
([cf, cg], c) => {
const neighbors = pack.cells.c[c];
const betterg = neighbors.filter(g).reduce((u, x) => (d2(x) < d2(u) ? x : u));
if (cf === undefined) return [c, betterg];
return betterg && d2(cf) < d2(c) ? [c, betterg] : [cf, cg];
},
[undefined, undefined]
);
if (bestf && bestg) return [bestf, bestg];
// no suitable pair found, retry with next ring
const targets = new Set(cs.map(c => pack.cells.c[c]).flat());
const ring = Array.from(targets).filter(nc => !tested.has(nc));
if (level >= max || !ring.length) return [undefined, undefined];
ring.forEach(c => tested.add(c));
return kernel(ring, level + 1);
};
const pair = kernel([startCell], 1);
return pair;
};
function copyBurgs(parentMap, projection, options) {
const cells = pack.cells;
const childMap = {grid, pack};
pack.burgs = parentMap.pack.burgs;
// remap burgs to the best new cell
pack.burgs.forEach((b, id) => {
if (id == 0) return; // skip empty city of neturals
[b.x, b.y] = projection(b.x, b.y);
// disable out-of-map (removed) burgs
if (!inMap(b.x, b.y)) {
b.removed = true;
b.cell = null;
return;
}
const cityCell = findCell(b.x, b.y);
let searchFunc;
const isFreeLand = c => cells.t[c] === 1 && !cells.burg[c];
const nearCoast = c => cells.t[c] === -1;
// check if we need to relocate the burg
if (cells.burg[cityCell])
// already occupied
searchFunc = findNearest(isFreeLand, _ => true, 3);
if (isWater(childMap, cityCell) || b.port)
// burg is in water or port
searchFunc = findNearest(isFreeLand, nearCoast, 6);
if (searchFunc) {
const [newCell, neighbor] = searchFunc(b.x, b.y);
if (!newCell) {
WARN && console.warn(`Can not relocate Burg: ${b.name} sunk and destroyed. :-(`);
b.cell = null;
b.removed = true;
return;
}
DEBUG && console.log(`Moving ${b.name} from ${cityCell} to ${newCell} near ${neighbor}.`);
[b.x, b.y] = b.port ? getMiddlePoint(newCell, neighbor) : cells.p[newCell];
if (b.port) b.port = cells.f[neighbor]; // copy feature number
b.cell = newCell;
if (b.port && !isWater(childMap, neighbor)) console.error("betrayal! negihbor must be water!", b);
} else {
b.cell = cityCell;
}
if (!b.lock) b.lock = options.lockBurgs;
cells.burg[b.cell] = id;
});
}
// export
return {resample, findNearest};
})();

View file

@ -266,14 +266,7 @@ function editBurg(id) {
toggleNewGroupInput();
document.getElementById("burgInputGroup").value = "";
const newLabelG = document.querySelector("#burgLabels").appendChild(labelG.cloneNode(false));
newLabelG.id = group;
const newIconG = document.querySelector("#burgIcons").appendChild(iconG.cloneNode(false));
newIconG.id = group;
if (anchor) {
const newAnchorG = document.querySelector("#anchors").appendChild(anchorG.cloneNode(false));
newAnchorG.id = group;
}
addBurgsGroup(group);
moveBurgToGroup(id, group);
}

View file

@ -1,5 +1,6 @@
"use strict";
function editCultures() {
const cultureTypes = ["Generic", "River", "Lake", "Naval", "Nomadic", "Hunting", "Highland"];
if (customization) return;
closeDialogs("#culturesEditor, .stable");
if (!layerIsOn("toggleCultures")) toggleCultures();
@ -37,6 +38,8 @@ function editCultures() {
document.getElementById("culturesEditNamesBase").addEventListener("click", editNamesbase);
document.getElementById("culturesAdd").addEventListener("click", enterAddCulturesMode);
document.getElementById("culturesExport").addEventListener("click", downloadCulturesData);
document.getElementById("culturesImport").addEventListener("click", () => document.getElementById("culturesCSVToLoad").click());
document.getElementById("culturesCSVToLoad").addEventListener("change", uploadCulturesData);
function refreshCulturesEditor() {
culturesCollectStatistics();
@ -169,8 +172,7 @@ function editCultures() {
function getTypeOptions(type) {
let options = "";
const types = ["Generic", "River", "Lake", "Naval", "Nomadic", "Hunting", "Highland"];
types.forEach(t => (options += `<option ${type === t ? "selected" : ""} value="${t}">${t}</option>`));
cultureTypes.forEach(t => (options += `<option ${type === t ? "selected" : ""} value="${t}">${t}</option>`));
return options;
}
@ -366,7 +368,7 @@ function editCultures() {
width: "24em",
buttons: {
Apply: function () {
applyPopulationChange();
applyPopulationChange(rural, urban, ruralPop.value, urbanPop.value, culture);
$(this).dialog("close");
},
Cancel: function () {
@ -375,32 +377,33 @@ function editCultures() {
},
position: {my: "center", at: "center", of: "svg"}
});
}
function applyPopulationChange() {
const ruralChange = ruralPop.value / rural;
if (isFinite(ruralChange) && ruralChange !== 1) {
const cells = pack.cells.i.filter(i => pack.cells.culture[i] === culture);
cells.forEach(i => (pack.cells.pop[i] *= ruralChange));
}
if (!isFinite(ruralChange) && +ruralPop.value > 0) {
const points = ruralPop.value / populationRate;
const cells = pack.cells.i.filter(i => pack.cells.culture[i] === culture);
const pop = rn(points / cells.length);
cells.forEach(i => (pack.cells.pop[i] = pop));
}
const urbanChange = urbanPop.value / urban;
if (isFinite(urbanChange) && urbanChange !== 1) {
burgs.forEach(b => (b.population = rn(b.population * urbanChange, 4)));
}
if (!isFinite(urbanChange) && +urbanPop.value > 0) {
const points = urbanPop.value / populationRate / urbanization;
const population = rn(points / burgs.length, 4);
burgs.forEach(b => (b.population = population));
}
refreshCulturesEditor();
function applyPopulationChange(oldRural, oldUrban, newRural, newUrban, culture) {
const ruralChange = newRural / oldRural;
if (isFinite(ruralChange) && ruralChange !== 1) {
const cells = pack.cells.i.filter(i => pack.cells.culture[i] === culture);
cells.forEach(i => (pack.cells.pop[i] *= ruralChange));
}
if (!isFinite(ruralChange) && +newRural > 0) {
const points = newRural / populationRate;
const cells = pack.cells.i.filter(i => pack.cells.culture[i] === culture);
const pop = rn(points / cells.length);
cells.forEach(i => (pack.cells.pop[i] = pop));
}
const burgs = pack.burgs.filter(b => !b.removed && b.culture === culture);
const urbanChange = newUrban / oldUrban;
if (isFinite(urbanChange) && urbanChange !== 1) {
burgs.forEach(b => (b.population = rn(b.population * urbanChange, 4)));
}
if (!isFinite(urbanChange) && +newUrban > 0) {
const points = newUrban / populationRate / urbanization;
const population = rn(points / burgs.length, 4);
burgs.forEach(b => (b.population = population));
}
refreshCulturesEditor();
}
function cultureRegenerateBurgs() {
@ -856,7 +859,7 @@ function editCultures() {
function downloadCulturesData() {
const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value;
let data = "Id,Culture,Color,Cells,Expansionism,Type,Area " + unit + ",Population,Namesbase,Emblems Shape\n"; // headers
let data = "Id,Culture,Color,Cells,Expansionism,Type,Area " + unit + ",Population,Namesbase,Emblems Shape,Origin\n"; // headers
body.querySelectorAll(":scope > div").forEach(function (el) {
data += el.dataset.id + ",";
@ -869,7 +872,8 @@ function editCultures() {
data += el.dataset.population + ",";
const base = +el.dataset.base;
data += nameBases[base].name + ",";
data += el.dataset.emblems + "\n";
data += el.dataset.emblems + ",";
data += pack.cultures[+el.dataset.id].origin + "\n";
});
const name = getFileName("Cultures") + ".csv";
@ -881,4 +885,56 @@ function editCultures() {
exitCulturesManualAssignment("close");
exitAddCultureMode();
}
async function uploadCulturesData() {
const csv = await Formats.csvParser(this.files[0]);
this.value = "";
const cultures = pack.cultures;
const shapes = Object.keys(COA.shields.types)
.map(type => Object.keys(COA.shields[type]))
.flat();
const populated = pack.cells.pop.map((c, i) => (c ? i : null)).filter(c => c);
for (const c of csv.iterator((a, b) => +a[0] > +b[0])) {
let current;
if (+c.id < cultures.length) {
current = cultures[c.id];
current.removed = false;
const ratio = current.urban / (current.rural + current.urban);
applyPopulationChange(current.rural, current.urban, c.population * (1 - ratio), c.population * ratio, +c.id);
} else {
current = {i: cultures.length, center: ra(populated), area: 0, cells: 0, origin: 0, rural: 0, urban: 0};
cultures.push(current);
}
current.name = c.culture;
current.code = abbreviate(
current.name,
cultures.map(c => c.code)
);
current.color = c.color;
current.expansionism = +c.expansionism;
current.origin = +c.origin;
if (cultureTypes.includes(c.type)) current.type = c.type;
else current.type = "Generic";
const shieldShape = c["emblems shape"].toLowerCase();
if (shapes.includes(shieldShape)) current.shield = shieldShape;
else current.shield = "heater";
const nameBaseIndex = nameBases.findIndex(n => n.name == c.namesbase);
current.base = nameBaseIndex === -1 ? 0 : nameBaseIndex;
}
const validId = cultures.filter(c => !c.removed).map(c => c.i);
cultures.forEach(item => (item.origin = validId.includes(item.origin) ? item.origin : 0));
cultures[0].origin = null;
drawCultures();
refreshCulturesEditor();
}
}

View file

@ -169,7 +169,7 @@ function moveBurgToGroup(id, g) {
const icon = document.querySelector("#burgIcons [data-id='" + id + "']");
const anchor = document.querySelector("#anchors [data-id='" + id + "']");
if (!label || !icon) {
ERROR && console.error("Cannot find label or icon elements");
ERROR && console.error(`Cannot find label or icon elements for id ${id}`);
return;
}
@ -190,6 +190,25 @@ function moveBurgToGroup(id, g) {
}
}
function moveAllBurgsToGroup(fromGroup, toGroup) {
const groupToMove = document.querySelector(`#burgIcons #${fromGroup}`);
const burgsToMove = Array.from(groupToMove.children).map(x=>x.dataset.id);
addBurgsGroup(toGroup)
burgsToMove.forEach(x=>moveBurgToGroup(x, toGroup));
}
function addBurgsGroup(group) {
if (document.querySelector(`#burgLabels > #${group}`)) return;
const labelCopy = document.querySelector("#burgLabels > #towns").cloneNode(false);
const iconCopy = document.querySelector("#burgIcons > #towns").cloneNode(false);
const anchorCopy = document.querySelector("#anchors > #towns").cloneNode(false);
// FIXME: using the same id is against the spec!
document.querySelector("#burgLabels").appendChild(labelCopy).id = group;
document.querySelector("#burgIcons").appendChild(iconCopy).id = group;
document.querySelector("#anchors").appendChild(anchorCopy).id = group;
}
function removeBurg(id) {
const label = document.querySelector("#burgLabels [data-id='" + id + "']");
const icon = document.querySelector("#burgIcons [data-id='" + id + "']");

View file

@ -241,8 +241,7 @@ function editMarker(markerI) {
}
function deleteMarker() {
notes = notes.filter(note => note.id !== element.id);
pack.markers = pack.markers.filter(m => m.i !== marker.i);
Markers.deleteMarker(marker.i)
element.remove();
$("#markerEditor").dialog("close");
if (document.getElementById("markersOverviewRefresh").offsetParent) markersOverviewRefresh.click();

View file

@ -105,7 +105,8 @@ function showSupporters() {
Jonathan Williams,ojacid .,Brian Wilson,A Patreon of the Ahts,Shubham Jakhotiya,www15o,Jan Bundesmann,Angelique Badger,Joshua Xiong,Moist mongol,
Frank Fewkes,jason baldrick,Game Master Pro,Andrew Kircher,Preston Mitchell,Chris Kohut,Emarandzeb,Trentin Bergeron,Damon Gallaty,Pleaseworkforonce,
Jordan,William Markus,Sidr Dim,Alexander Whittaker,The Next Level,Patrick Valverde,Markus Peham,Daniel Cooper,the Beagles of Neorbus,Marley Moule,
Maximilian Schielke,Johnathan Xavier Hutchinson,Ele,Rita,Randy Ross,John Wick,RedSpaz,cameron cannon,Ian Grau-Fay,Kyle Barrett,Charlotte Wiland`;
Maximilian Schielke,Johnathan Xavier Hutchinson,Ele,Rita,Randy Ross,John Wick,RedSpaz,cameron cannon,Ian Grau-Fay,Kyle Barrett,Charlotte Wiland,
David Kaul,E. Jason Davis,Cyberate,Atenfox,Sea Wolf,Holly Loveless`;
const array = supporters
.replace(/(?:\r\n|\r|\n)/g, "")
@ -334,25 +335,25 @@ function copyMapURL() {
.catch(err => tip("Could not copy URL: " + err, false, "error", 5000));
}
function changeCellsDensity(value) {
const convert = v => {
if (v == 1) return 1000;
if (v == 2) return 2000;
if (v == 3) return 5000;
if (v == 4) return 10000;
if (v == 5) return 20000;
if (v == 6) return 30000;
if (v == 7) return 40000;
if (v == 8) return 50000;
if (v == 9) return 60000;
if (v == 10) return 70000;
if (v == 11) return 80000;
if (v == 12) return 90000;
if (v == 13) return 100000;
};
const cells = convert(value);
const cellsDensityConstants = {
1: 1000,
2: 2000,
3: 5000,
4: 10000,
5: 20000,
6: 30000,
7: 40000,
8: 50000,
9: 60000,
10: 70000,
11: 80000,
12: 90000,
13: 100000,
};
pointsInput.setAttribute("data-cells", cells);
function changeCellsDensity(value) {
const cells = value in cellsDensityConstants? cellsDensityConstants[value]: 1000;
pointsInput.dataset.cells = cells;
pointsOutput_formatted.value = cells / 1000 + "K";
pointsOutput_formatted.style.color = cells > 50000 ? "#b12117" : cells !== 10000 ? "#dfdf12" : "#053305";
}

View file

@ -574,20 +574,20 @@ addFontMethod.addEventListener("change", function () {
});
styleFontSize.addEventListener("change", function () {
changeFontSize(+this.value);
changeFontSize(getEl(), +this.value);
});
styleFontPlus.addEventListener("click", function () {
const size = +getEl().attr("data-size") + 1;
changeFontSize(Math.min(size, 999));
changeFontSize(getEl(), Math.min(size, 999));
});
styleFontMinus.addEventListener("click", function () {
const size = +getEl().attr("data-size") - 1;
changeFontSize(Math.max(size, 1));
changeFontSize(getEl(), Math.max(size, 1));
});
function changeFontSize(size) {
function changeFontSize(el, size) {
styleFontSize.value = size;
const getSizeOnScale = element => {
@ -600,7 +600,7 @@ function changeFontSize(size) {
};
const scaleSize = getSizeOnScale(styleElementSelect.value);
getEl().attr("data-size", size).attr("font-size", scaleSize);
el.attr("data-size", size).attr("font-size", scaleSize);
if (styleElementSelect.value === "legend") redrawLegend();
}

160
modules/ui/submap.js Normal file
View file

@ -0,0 +1,160 @@
"use strict";
/*
UI elements for submap generation
*/
function openSubmapOptions() {
$("#submapOptionsDialog").dialog({
title: "Submap options",
resizable: false,
width: fitContent(),
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"},
buttons: {
Submap: function () {
$(this).dialog("close");
generateSubmap();
},
Cancel: function () {
$(this).dialog("close");
}
}
});
}
function openRemapOptions() {
resetZoom(0);
$("#remapOptionsDialog").dialog({
title: "Resampler options",
resizable: false,
width: fitContent(),
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"},
buttons: {
Resample: function () {
const cellNumId = Number(document.getElementById("submapPointsInput").value);
const cells = cellsDensityConstants[cellNumId];
$(this).dialog("close");
if (!cells) {
console.error("Unknown cell number!");
return;
}
changeCellsDensity(cellNumId);
resampleCurrentMap();
},
Cancel: function () {
$(this).dialog("close");
}
}
});
}
/* callbacks */
const resampleCurrentMap = debounce(function () {
// Resample the whole map to different cell resolution or shape
WARN && console.warn("Resampling current map");
const options = {
lockMarkers: false,
lockBurgs: false,
depressRivers: false,
addLakesInDepressions: false,
promoteTowns: false,
smoothHeightMap: false,
projection: (x, y) => [x, y],
inverse: (x, y) => [x, y]
};
startResample(options);
}, 1000);
const generateSubmap = debounce(function () {
// Create submap from the current map
// submap limits defined by the current window size (canvas viewport)
WARN && console.warn("Resampling current map");
closeDialogs("#worldConfigurator, #options3d");
const checked = id => Boolean(document.getElementById(id).checked);
// Create projection func from current zoom extents
const [[x0, y0], [x1, y1]] = getViewBoxExtent();
const options = {
lockMarkers: checked("submapLockMarkers"),
lockBurgs: checked("submapLockBurgs"),
depressRivers: checked("submapDepressRivers"),
addLakesInDepressions: checked("submapAddLakeInDepression"),
promoteTowns: checked("submapPromoteTowns"),
smoothHeightMap: scale > 2,
inverse: (x, y) => [(x * (x1 - x0)) / graphWidth + x0, (y * (y1 - y0)) / graphHeight + y0],
projection: (x, y) => [((x - x0) * graphWidth) / (x1 - x0), ((y - y0) * graphHeight) / (y1 - y0)]
};
// converting map position on the planet
const mapSizeOutput = document.getElementById("mapSizeOutput");
const latitudeOutput = document.getElementById("latitudeOutput");
const latN = 90 - ((180 - (mapSizeInput.value / 100) * 180) * latitudeOutput.value) / 100;
const newLatN = latN - ((y0 / graphHeight) * mapSizeOutput.value * 180) / 100;
mapSizeOutput.value /= scale;
latitudeOutput.value = ((90 - newLatN) / (180 - (mapSizeOutput.value / 100) * 180)) * 100;
document.getElementById("mapSizeInput").value = mapSizeOutput.value;
document.getElementById("latitudeInput").value = latitudeOutput.value;
// fix scale
distanceScaleInput.value = distanceScaleOutput.value = rn((distanceScale = distanceScaleOutput.value / scale), 2);
populationRateInput.value = populationRateOutput.value = rn((populationRate = populationRateOutput.value / scale), 2);
customization = 0;
startResample(options);
}, 1000);
async function startResample(options) {
undraw();
resetZoom(0);
let oldstate = {
grid: deepCopy(grid),
pack: deepCopy(pack),
seed,
graphWidth,
graphHeight
};
try {
const oldScale = scale;
await Submap.resample(oldstate, options);
if (options.promoteTowns) {
const groupName = "largetowns";
moveAllBurgsToGroup("towns", groupName);
changeRadius(rn(oldScale * 0.8, 2), groupName);
changeFontSize(svg.select(`#labels #${groupName}`), rn(oldScale * 2, 2));
invokeActiveZooming();
}
} catch (error) {
showSubmapErrorHandler(error);
}
oldstate = null; // destroy old state to free memory
restoreLayers();
turnButtonOn("toggleMarkers");
if (ThreeD.options.isOn) ThreeD.redraw();
if ($("#worldConfigurator").is(":visible")) editWorld();
}
function showSubmapErrorHandler(error) {
ERROR && console.error(error);
clearMainTip();
alertMessage.innerHTML = `Map resampling failed :_(.
<br>You may retry after clearing stored data or contact us at discord.
<p id="errorBox">${parseError(error)}</p>`;
$("#alert").dialog({
resizable: false,
title: "Generation error",
width: "32em",
buttons: {
Ok: function () {
$(this).dialog("close");
}
},
position: {my: "center", at: "center", of: "svg"}
});
}

View file

@ -103,6 +103,7 @@ function editUnits() {
function restoreDefaultUnits() {
// distanceScale
distanceScale = 3;
document.getElementById("distanceScaleOutput").value = 3;
document.getElementById("distanceScaleInput").value = 3;
unlock("distanceScale");

View file

@ -15,3 +15,37 @@ function common(a, b) {
function unique(array) {
return [...new Set(array)];
}
// deep copy for Arrays (and other objects)
function deepCopy(obj) {
const id = x=>x;
const dcTArray = a => a.map(id);
const dcObject = x => Object.fromEntries(Object.entries(x).map(([k,d])=>[k,dcAny(d)]));
const dcAny = x => x instanceof Object ? (cf.get(x.constructor)||id)(x) : x;
// don't map keys, probably this is what we would expect
const dcMapCore = m => [...m.entries()].map(([k,v])=>[k, dcAny(v)]);
const cf = new Map([
[Int8Array, dcTArray],
[Uint8Array, dcTArray],
[Uint8ClampedArray, dcTArray],
[Int16Array, dcTArray],
[Uint16Array, dcTArray],
[Int32Array, dcTArray],
[Uint32Array, dcTArray],
[Float32Array, dcTArray],
[Float64Array, dcTArray],
[BigInt64Array, dcTArray],
[BigUint64Array, dcTArray],
[Map, m => new Map(dcMapCore(m))],
[WeakMap, m => new WeakMap(dcMapCore(m))],
[Array, a => a.map(dcAny)],
[Set, s => [...s.values()].map(dcAny)],
[Date, d => new Date(d.getTime())],
[Object, dcObject],
// other types will be referenced
// ... extend here to implement their custom deep copy
]);
return dcAny(obj);
}