mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-22 12:01:23 +01:00
Merge branch 'master' of https://github.com/mosuzi/Fantasy-Map-Generator into localization-dev
This commit is contained in:
commit
b3dc77578c
303 changed files with 11387 additions and 2609 deletions
144
modules/biomes.js
Normal file
144
modules/biomes.js
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
"use strict";
|
||||
|
||||
const MIN_LAND_HEIGHT = 20;
|
||||
|
||||
const names = [
|
||||
"Marine",
|
||||
"Hot desert",
|
||||
"Cold desert",
|
||||
"Savanna",
|
||||
"Grassland",
|
||||
"Tropical seasonal forest",
|
||||
"Temperate deciduous forest",
|
||||
"Tropical rainforest",
|
||||
"Temperate rainforest",
|
||||
"Taiga",
|
||||
"Tundra",
|
||||
"Glacier",
|
||||
"Wetland"
|
||||
];
|
||||
|
||||
window.Biomes = (function () {
|
||||
const getDefault = () => {
|
||||
const name = [
|
||||
"Marine",
|
||||
"Hot desert",
|
||||
"Cold desert",
|
||||
"Savanna",
|
||||
"Grassland",
|
||||
"Tropical seasonal forest",
|
||||
"Temperate deciduous forest",
|
||||
"Tropical rainforest",
|
||||
"Temperate rainforest",
|
||||
"Taiga",
|
||||
"Tundra",
|
||||
"Glacier",
|
||||
"Wetland"
|
||||
];
|
||||
|
||||
const color = [
|
||||
"#466eab",
|
||||
"#fbe79f",
|
||||
"#b5b887",
|
||||
"#d2d082",
|
||||
"#c8d68f",
|
||||
"#b6d95d",
|
||||
"#29bc56",
|
||||
"#7dcb35",
|
||||
"#409c43",
|
||||
"#4b6b32",
|
||||
"#96784b",
|
||||
"#d5e7eb",
|
||||
"#0b9131"
|
||||
];
|
||||
const habitability = [0, 4, 10, 22, 30, 50, 100, 80, 90, 12, 4, 0, 12];
|
||||
const iconsDensity = [0, 3, 2, 120, 120, 120, 120, 150, 150, 100, 5, 0, 150];
|
||||
const icons = [
|
||||
{},
|
||||
{dune: 3, cactus: 6, deadTree: 1},
|
||||
{dune: 9, deadTree: 1},
|
||||
{acacia: 1, grass: 9},
|
||||
{grass: 1},
|
||||
{acacia: 8, palm: 1},
|
||||
{deciduous: 1},
|
||||
{acacia: 5, palm: 3, deciduous: 1, swamp: 1},
|
||||
{deciduous: 6, swamp: 1},
|
||||
{conifer: 1},
|
||||
{grass: 1},
|
||||
{},
|
||||
{swamp: 1}
|
||||
];
|
||||
const cost = [10, 200, 150, 60, 50, 70, 70, 80, 90, 200, 1000, 5000, 150]; // biome movement cost
|
||||
const biomesMartix = [
|
||||
// hot ↔ cold [>19°C; <-4°C]; dry ↕ wet
|
||||
new Uint8Array([1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 10]),
|
||||
new Uint8Array([3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 9, 9, 9, 9, 10, 10, 10]),
|
||||
new Uint8Array([5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 9, 9, 9, 9, 9, 10, 10, 10]),
|
||||
new Uint8Array([5, 6, 6, 6, 6, 6, 6, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 10, 10, 10]),
|
||||
new Uint8Array([7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 10, 10])
|
||||
];
|
||||
|
||||
// parse icons weighted array into a simple array
|
||||
for (let i = 0; i < icons.length; i++) {
|
||||
const parsed = [];
|
||||
for (const icon in icons[i]) {
|
||||
for (let j = 0; j < icons[i][icon]; j++) {
|
||||
parsed.push(icon);
|
||||
}
|
||||
}
|
||||
icons[i] = parsed;
|
||||
}
|
||||
|
||||
return {i: d3.range(0, name.length), name, color, biomesMartix, habitability, iconsDensity, icons, cost};
|
||||
};
|
||||
|
||||
// assign biome id for each cell
|
||||
function define() {
|
||||
TIME && console.time("defineBiomes");
|
||||
|
||||
const {fl: flux, r: riverIds, h: heights, c: neighbors, g: gridReference} = pack.cells;
|
||||
const {temp, prec} = grid.cells;
|
||||
pack.cells.biome = new Uint8Array(pack.cells.i.length); // biomes array
|
||||
|
||||
for (let cellId = 0; cellId < heights.length; cellId++) {
|
||||
const height = heights[cellId];
|
||||
const moisture = height < MIN_LAND_HEIGHT ? 0 : calculateMoisture(cellId);
|
||||
const temperature = temp[gridReference[cellId]];
|
||||
pack.cells.biome[cellId] = getId(moisture, temperature, height, Boolean(riverIds[cellId]));
|
||||
}
|
||||
|
||||
function calculateMoisture(cellId) {
|
||||
let moisture = prec[gridReference[cellId]];
|
||||
if (riverIds[cellId]) moisture += Math.max(flux[cellId] / 10, 2);
|
||||
|
||||
const moistAround = neighbors[cellId]
|
||||
.filter(neibCellId => heights[neibCellId] >= MIN_LAND_HEIGHT)
|
||||
.map(c => prec[gridReference[c]])
|
||||
.concat([moisture]);
|
||||
return rn(4 + d3.mean(moistAround));
|
||||
}
|
||||
|
||||
TIME && console.timeEnd("defineBiomes");
|
||||
}
|
||||
|
||||
function getId(moisture, temperature, height, hasRiver) {
|
||||
if (height < 20) return 0; // all water cells: marine biome
|
||||
if (temperature < -5) return 11; // too cold: permafrost biome
|
||||
if (temperature >= 25 && !hasRiver && moisture < 8) return 1; // too hot and dry: hot desert biome
|
||||
if (isWetland(moisture, temperature, height)) return 12; // too wet: wetland biome
|
||||
|
||||
// in other cases use biome matrix
|
||||
const moistureBand = Math.min((moisture / 5) | 0, 4); // [0-4]
|
||||
const temperatureBand = Math.min(Math.max(20 - temperature, 0), 25); // [0-25]
|
||||
return biomesData.biomesMartix[moistureBand][temperatureBand];
|
||||
}
|
||||
|
||||
function isWetland(moisture, temperature, height) {
|
||||
if (temperature <= -2) return false; // too cold
|
||||
if (moisture > 40 && height < 25) return true; // near coast
|
||||
if (moisture > 24 && height > 24 && height < 60) return true; // off coast
|
||||
return false;
|
||||
}
|
||||
|
||||
return {getDefault, define, getId};
|
||||
})();
|
||||
|
|
@ -502,223 +502,6 @@ window.BurgsAndStates = (function () {
|
|||
TIME && console.timeEnd("updateCulturesForBurgsAndStates");
|
||||
};
|
||||
|
||||
// calculate and draw curved state labels for a list of states
|
||||
const drawStateLabels = function (list) {
|
||||
TIME && console.time("drawStateLabels");
|
||||
const {cells, features, states} = pack;
|
||||
const paths = []; // text paths
|
||||
lineGen.curve(d3.curveBundle.beta(1));
|
||||
const mode = options.stateLabelsMode || "auto";
|
||||
|
||||
for (const s of states) {
|
||||
if (!s.i || s.removed || s.lock || !s.cells || (list && !list.includes(s.i))) continue;
|
||||
|
||||
const used = [];
|
||||
const visualCenter = findCell(s.pole[0], s.pole[1]);
|
||||
const start = cells.state[visualCenter] === s.i ? visualCenter : s.center;
|
||||
const hull = getHull(start, s.i, s.cells / 10);
|
||||
const points = [...hull].map(v => pack.vertices.p[v]);
|
||||
const delaunay = Delaunator.from(points);
|
||||
const voronoi = new Voronoi(delaunay, points, points.length);
|
||||
const chain = connectCenters(voronoi.vertices, s.pole[1]);
|
||||
const relaxed = chain.map(i => voronoi.vertices.p[i]).filter((p, i) => i % 15 === 0 || i + 1 === chain.length);
|
||||
paths.push([s.i, relaxed]);
|
||||
|
||||
function getHull(start, state, maxLake) {
|
||||
const queue = [start];
|
||||
const hull = new Set();
|
||||
|
||||
while (queue.length) {
|
||||
const q = queue.pop();
|
||||
const sameStateNeibs = cells.c[q].filter(c => cells.state[c] === state);
|
||||
|
||||
cells.c[q].forEach(function (c, d) {
|
||||
const passableLake = features[cells.f[c]].type === "lake" && features[cells.f[c]].cells < maxLake;
|
||||
if (cells.b[c] || (cells.state[c] !== state && !passableLake)) return hull.add(cells.v[q][d]);
|
||||
|
||||
const hasCoadjacentSameStateCells = sameStateNeibs.some(neib => cells.c[c].includes(neib));
|
||||
if (hull.size > 20 && !hasCoadjacentSameStateCells && !passableLake) return hull.add(cells.v[q][d]);
|
||||
|
||||
if (used[c]) return;
|
||||
used[c] = 1;
|
||||
queue.push(c);
|
||||
});
|
||||
}
|
||||
|
||||
return hull;
|
||||
}
|
||||
|
||||
function connectCenters(c, y) {
|
||||
// check if vertex is inside the area
|
||||
const inside = c.p.map(function (p) {
|
||||
if (p[0] <= 0 || p[1] <= 0 || p[0] >= graphWidth || p[1] >= graphHeight) return false; // out of the screen
|
||||
return used[findCell(p[0], p[1])];
|
||||
});
|
||||
|
||||
const pointsInside = d3.range(c.p.length).filter(i => inside[i]);
|
||||
if (!pointsInside.length) return [0];
|
||||
const h = c.p.length < 200 ? 0 : c.p.length < 600 ? 0.5 : 1; // power of horyzontality shift
|
||||
const end =
|
||||
pointsInside[
|
||||
d3.scan(
|
||||
pointsInside,
|
||||
(a, b) => c.p[a][0] - c.p[b][0] + (Math.abs(c.p[a][1] - y) - Math.abs(c.p[b][1] - y)) * h
|
||||
)
|
||||
]; // left point
|
||||
const start =
|
||||
pointsInside[
|
||||
d3.scan(
|
||||
pointsInside,
|
||||
(a, b) => c.p[b][0] - c.p[a][0] - (Math.abs(c.p[b][1] - y) - Math.abs(c.p[a][1] - y)) * h
|
||||
)
|
||||
]; // right point
|
||||
|
||||
// connect leftmost and rightmost points with shortest path
|
||||
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
||||
const cost = [],
|
||||
from = [];
|
||||
queue.queue({e: start, p: 0});
|
||||
|
||||
while (queue.length) {
|
||||
const next = queue.dequeue(),
|
||||
n = next.e,
|
||||
p = next.p;
|
||||
if (n === end) break;
|
||||
|
||||
for (const v of c.v[n]) {
|
||||
if (v === -1) continue;
|
||||
const totalCost = p + (inside[v] ? 1 : 100);
|
||||
if (from[v] || totalCost >= cost[v]) continue;
|
||||
cost[v] = totalCost;
|
||||
from[v] = n;
|
||||
queue.queue({e: v, p: totalCost});
|
||||
}
|
||||
}
|
||||
|
||||
// restore path
|
||||
const chain = [end];
|
||||
let cur = end;
|
||||
while (cur !== start) {
|
||||
cur = from[cur];
|
||||
if (inside[cur]) chain.push(cur);
|
||||
}
|
||||
return chain;
|
||||
}
|
||||
}
|
||||
|
||||
void (function drawLabels() {
|
||||
const g = labels.select("#states");
|
||||
const t = defs.select("#textPaths");
|
||||
const displayed = layerIsOn("toggleLabels");
|
||||
if (!displayed) toggleLabels();
|
||||
|
||||
// remove state labels to be redrawn
|
||||
for (const state of pack.states) {
|
||||
if (!state.i || state.removed || state.lock) continue;
|
||||
if (list && !list.includes(state.i)) continue;
|
||||
|
||||
byId(`stateLabel${state.i}`)?.remove();
|
||||
byId(`textPath_stateLabel${state.i}`)?.remove();
|
||||
}
|
||||
|
||||
const example = g.append("text").attr("x", 0).attr("x", 0).text("Average");
|
||||
const letterLength = example.node().getComputedTextLength() / 7; // average length of 1 letter
|
||||
|
||||
paths.forEach(p => {
|
||||
const id = p[0];
|
||||
const state = states[p[0]];
|
||||
const {name, fullName} = state;
|
||||
|
||||
const path = p[1].length > 1 ? round(lineGen(p[1])) : `M${p[1][0][0] - 50},${p[1][0][1]}h${100}`;
|
||||
const textPath = t
|
||||
.append("path")
|
||||
.attr("d", path)
|
||||
.attr("id", "textPath_stateLabel" + id);
|
||||
const pathLength = p[1].length > 1 ? textPath.node().getTotalLength() / letterLength : 0; // path length in letters
|
||||
|
||||
const [lines, ratio] = getLines(mode, name, fullName, pathLength);
|
||||
|
||||
// prolongate path if it's too short
|
||||
if (pathLength && pathLength < lines[0].length) {
|
||||
const points = p[1];
|
||||
const f = points[0];
|
||||
const l = points[points.length - 1];
|
||||
const [dx, dy] = [l[0] - f[0], l[1] - f[1]];
|
||||
const mod = Math.abs((letterLength * lines[0].length) / dx) / 2;
|
||||
points[0] = [rn(f[0] - dx * mod), rn(f[1] - dy * mod)];
|
||||
points[points.length - 1] = [rn(l[0] + dx * mod), rn(l[1] + dy * mod)];
|
||||
textPath.attr("d", round(lineGen(points)));
|
||||
}
|
||||
|
||||
example.attr("font-size", ratio + "%");
|
||||
const top = (lines.length - 1) / -2; // y offset
|
||||
const spans = lines.map((l, d) => {
|
||||
example.text(l);
|
||||
const left = example.node().getBBox().width / -2; // x offset
|
||||
return `<tspan x=${rn(left, 1)} dy="${d ? 1 : top}em">${l}</tspan>`;
|
||||
});
|
||||
|
||||
const el = g
|
||||
.append("text")
|
||||
.attr("id", "stateLabel" + id)
|
||||
.append("textPath")
|
||||
.attr("xlink:href", "#textPath_stateLabel" + id)
|
||||
.attr("startOffset", "50%")
|
||||
.attr("font-size", ratio + "%")
|
||||
.node();
|
||||
|
||||
el.insertAdjacentHTML("afterbegin", spans.join(""));
|
||||
if (mode === "full" || lines.length === 1) return;
|
||||
|
||||
// check whether multilined label is generally inside the state. If no, replace with short name label
|
||||
const cs = pack.cells.state;
|
||||
const b = el.parentNode.getBBox();
|
||||
const c1 = () => +cs[findCell(b.x, b.y)] === id;
|
||||
const c2 = () => +cs[findCell(b.x + b.width / 2, b.y)] === id;
|
||||
const c3 = () => +cs[findCell(b.x + b.width, b.y)] === id;
|
||||
const c4 = () => +cs[findCell(b.x + b.width, b.y + b.height)] === id;
|
||||
const c5 = () => +cs[findCell(b.x + b.width / 2, b.y + b.height)] === id;
|
||||
const c6 = () => +cs[findCell(b.x, b.y + b.height)] === id;
|
||||
if (c1() + c2() + c3() + c4() + c5() + c6() > 3) return; // generally inside => exit
|
||||
|
||||
// move to one-line name
|
||||
const text = pathLength > fullName.length * 1.8 ? fullName : name;
|
||||
example.text(text);
|
||||
const left = example.node().getBBox().width / -2; // x offset
|
||||
el.innerHTML = `<tspan x="${left}px">${text}</tspan>`;
|
||||
|
||||
const correctedRatio = minmax(rn((pathLength / text.length) * 60), 40, 130);
|
||||
el.setAttribute("font-size", correctedRatio + "%");
|
||||
});
|
||||
|
||||
example.remove();
|
||||
if (!displayed) toggleLabels();
|
||||
})();
|
||||
|
||||
function getLines(mode, name, fullName, pathLength) {
|
||||
// short name
|
||||
if (mode === "short" || (mode === "auto" && pathLength < name.length)) {
|
||||
const lines = splitInTwo(name);
|
||||
const ratio = pathLength / lines[0].length;
|
||||
return [lines, minmax(rn(ratio * 60), 50, 150)];
|
||||
}
|
||||
|
||||
// full name: one line
|
||||
if (pathLength > fullName.length * 2.5) {
|
||||
const lines = [fullName];
|
||||
const ratio = pathLength / lines[0].length;
|
||||
return [lines, minmax(rn(ratio * 70), 70, 170)];
|
||||
}
|
||||
|
||||
// full name: two lines
|
||||
const lines = splitInTwo(fullName);
|
||||
const ratio = pathLength / lines[0].length;
|
||||
return [lines, minmax(rn(ratio * 60), 70, 150)];
|
||||
}
|
||||
|
||||
TIME && console.timeEnd("drawStateLabels");
|
||||
};
|
||||
|
||||
// calculate states data like area, population etc.
|
||||
const collectStatistics = function () {
|
||||
TIME && console.time("collectStatistics");
|
||||
|
|
@ -1076,7 +859,7 @@ window.BurgsAndStates = (function () {
|
|||
if (P(0.3) && s.diplomacy.includes("Vassal")) return "Protectorate"; // some vassals
|
||||
}
|
||||
|
||||
if (base === 16 && (form === "Empire" || form === "Kingdom")) return "Sultanate"; // Turkic
|
||||
if (base === 16 && (form === "Empire" || form === "Kingdom")) return "Khaganate"; // Turkic
|
||||
if (base === 5 && (form === "Empire" || form === "Kingdom")) return "Tsardom"; // Ruthenian
|
||||
if ([16, 31].includes(base) && (form === "Empire" || form === "Kingdom")) return "Khaganate"; // Turkic, Mongolian
|
||||
if (base === 12 && (form === "Kingdom" || form === "Grand Duchy")) return "Shogunate"; // Japanese
|
||||
|
|
@ -1405,7 +1188,6 @@ window.BurgsAndStates = (function () {
|
|||
specifyBurgs,
|
||||
defineBurgFeatures,
|
||||
getType,
|
||||
drawStateLabels,
|
||||
collectStatistics,
|
||||
generateCampaign,
|
||||
generateCampaigns,
|
||||
|
|
|
|||
|
|
@ -60,8 +60,7 @@ window.COA = (function () {
|
|||
people: 1,
|
||||
architecture: 1,
|
||||
miscellaneous: 3,
|
||||
inescutcheon: 3,
|
||||
uploaded: 0
|
||||
inescutcheon: 3
|
||||
},
|
||||
single: {
|
||||
conventional: 12,
|
||||
|
|
@ -79,8 +78,7 @@ window.COA = (function () {
|
|||
people: 2,
|
||||
architecture: 1,
|
||||
miscellaneous: 10,
|
||||
inescutcheon: 5,
|
||||
uploaded: 0
|
||||
inescutcheon: 5
|
||||
},
|
||||
semy: {conventional: 4, crosses: 1},
|
||||
conventional: {
|
||||
|
|
@ -117,6 +115,8 @@ window.COA = (function () {
|
|||
fleurDeLis: 6,
|
||||
sun: 3,
|
||||
sunInSplendour: 1,
|
||||
sunInSplendour2: 1,
|
||||
moonInCrescent: 1,
|
||||
crescent: 5,
|
||||
fountain: 1
|
||||
},
|
||||
|
|
@ -211,29 +211,63 @@ window.COA = (function () {
|
|||
lionRampant: 6,
|
||||
lionPassant: 2,
|
||||
lionPassantGuardant: 1,
|
||||
lionSejant: 1,
|
||||
wolfRampant: 1,
|
||||
wolfPassant: 1,
|
||||
wolfStatant: 1,
|
||||
greyhoundCourant: 1,
|
||||
greyhoundRampant: 1,
|
||||
greyhoundSejant: 1,
|
||||
mastiffStatant: 1,
|
||||
talbotPassant: 1,
|
||||
talbotSejant: 1,
|
||||
martenCourant: 1,
|
||||
boarRampant: 1,
|
||||
stagPassant: 1,
|
||||
hindStatant: 1,
|
||||
horseRampant: 2,
|
||||
horseSalient: 1,
|
||||
horsePassant: 1,
|
||||
bearRampant: 2,
|
||||
bearPassant: 1,
|
||||
bullPassant: 1,
|
||||
cowStatant: 1,
|
||||
goat: 1,
|
||||
lamb: 1,
|
||||
lambPassantReguardant: 1,
|
||||
agnusDei: 1,
|
||||
ramPassant: 1,
|
||||
badgerStatant: 1,
|
||||
elephant: 1,
|
||||
rhinoceros: 1,
|
||||
camel: 1,
|
||||
porcupine: 1,
|
||||
snake: 1
|
||||
hedgehog: 1,
|
||||
catPassantGuardant: 1,
|
||||
rabbitSejant: 1,
|
||||
ratRampant: 1,
|
||||
squirrel: 1,
|
||||
frog: 1,
|
||||
snake: 1,
|
||||
crocodile: 1,
|
||||
lizard: 1,
|
||||
scorpion: 1,
|
||||
butterfly: 1,
|
||||
bee: 1,
|
||||
fly: 1
|
||||
},
|
||||
animalHeads: {
|
||||
wolfHeadErased: 2,
|
||||
bullHeadCaboshed: 1,
|
||||
deerHeadCaboshed: 1,
|
||||
donkeyHeadCaboshed: 1,
|
||||
lionHeadCaboshed: 2,
|
||||
lionHeadErased: 2,
|
||||
boarHeadErased: 1,
|
||||
horseHeadCouped: 1,
|
||||
ramHeadErased: 1,
|
||||
elephantHeadErased: 1
|
||||
},
|
||||
animalHeads: {wolfHeadErased: 2, bullHeadCaboshed: 1, deerHeadCaboshed: 1, lionHeadCaboshed: 2},
|
||||
fantastic: {
|
||||
dragonPassant: 2,
|
||||
dragonRampant: 2,
|
||||
|
|
@ -241,17 +275,51 @@ window.COA = (function () {
|
|||
wyvernWithWingsDisplayed: 1,
|
||||
griffinPassant: 1,
|
||||
griffinRampant: 1,
|
||||
eagleTwoHeards: 2,
|
||||
eagleTwoHeads: 2,
|
||||
unicornRampant: 1,
|
||||
pegasus: 1,
|
||||
serpent: 1,
|
||||
basilisk: 1
|
||||
basilisk: 1,
|
||||
sagittarius: 1
|
||||
},
|
||||
birds: {eagle: 9, raven: 2, cock: 3, parrot: 1, swan: 2, swanErased: 1, heron: 1, owl: 1},
|
||||
plants: {tree: 1, oak: 1, cinquefoil: 1, rose: 1, apple: 1},
|
||||
aquatic: {escallop: 5, pike: 1, cancer: 1, dolphin: 1},
|
||||
seafaring: {anchor: 6, boat: 2, boat2: 1, lymphad: 2, armillarySphere: 1},
|
||||
agriculture: {garb: 2, rake: 1, plough: 2},
|
||||
birds: {
|
||||
eagle: 9,
|
||||
falcon: 2,
|
||||
raven: 2,
|
||||
cock: 3,
|
||||
parrot: 1,
|
||||
swan: 2,
|
||||
swanErased: 1,
|
||||
heron: 1,
|
||||
owl: 1,
|
||||
owlDisplayed: 1,
|
||||
dove: 2,
|
||||
doveDisplayed: 1,
|
||||
duck: 1,
|
||||
peacock: 1,
|
||||
peacockInPride: 1,
|
||||
swallow: 1
|
||||
},
|
||||
plants: {
|
||||
tree: 1,
|
||||
oak: 1,
|
||||
pineTree: 1,
|
||||
palmTree: 1,
|
||||
trefoil: 1,
|
||||
quatrefoil: 1,
|
||||
cinquefoil: 1,
|
||||
sextifoil: 1,
|
||||
mapleLeaf: 1,
|
||||
rose: 1,
|
||||
apple: 1,
|
||||
pear: 1,
|
||||
grapeBunch: 1,
|
||||
wheatStalk: 1,
|
||||
pineCone: 1
|
||||
},
|
||||
aquatic: {escallop: 5, pike: 1, plaice: 1, salmon: 1, cancer: 1, dolphin: 1},
|
||||
seafaring: {anchor: 6, boat: 2, boat2: 1, lymphad: 2, caravel: 1, armillarySphere: 1},
|
||||
agriculture: {garb: 2, sickle: 1, scythe: 1, rake: 1, plough: 2},
|
||||
arms: {
|
||||
sword: 4,
|
||||
falchion: 1,
|
||||
|
|
@ -261,20 +329,26 @@ window.COA = (function () {
|
|||
hatchet: 3,
|
||||
axe: 3,
|
||||
lochaberAxe: 1,
|
||||
spear: 1,
|
||||
mallet: 1,
|
||||
bowWithArrow: 3,
|
||||
bow: 1,
|
||||
arrow: 1,
|
||||
arrowsSheaf: 1,
|
||||
arbalest: 1,
|
||||
helmet: 2,
|
||||
gauntlet: 1,
|
||||
shield: 1,
|
||||
cannon: 1
|
||||
},
|
||||
bodyparts: {hand: 4, head: 1, headWreathed: 1, foot: 1},
|
||||
bodyparts: {hand: 4, head: 1, headWreathed: 1, foot: 1, skull: 1},
|
||||
people: {cavalier: 3, monk: 1, angel: 2},
|
||||
architecture: {tower: 1, castle: 1},
|
||||
architecture: {tower: 1, castle: 1, bridge: 1, column: 1},
|
||||
miscellaneous: {
|
||||
crown: 2,
|
||||
crown2: 1,
|
||||
laurelWreath: 1,
|
||||
mitre: 1,
|
||||
orb: 1,
|
||||
chalice: 1,
|
||||
key: 1,
|
||||
|
|
@ -285,6 +359,7 @@ window.COA = (function () {
|
|||
pot: 1,
|
||||
bucket: 1,
|
||||
horseshoe: 3,
|
||||
stirrup: 1,
|
||||
attire: 1,
|
||||
stagsAttires: 1,
|
||||
ramsHorn: 1,
|
||||
|
|
@ -293,63 +368,220 @@ window.COA = (function () {
|
|||
wingSword: 1,
|
||||
lute: 1,
|
||||
harp: 1,
|
||||
drum: 1,
|
||||
wheel: 2,
|
||||
crosier: 1,
|
||||
sceptre: 1,
|
||||
fasces: 1,
|
||||
log: 1,
|
||||
chain: 1,
|
||||
anvil: 1
|
||||
anvil: 1,
|
||||
ladder: 1,
|
||||
banner: 1,
|
||||
bookClosed: 1,
|
||||
bookOpen: 1,
|
||||
scissors: 1
|
||||
},
|
||||
// selection based on culture type:
|
||||
Naval: {anchor: 3, boat: 1, lymphad: 2, armillarySphere: 1, escallop: 1, dolphin: 1},
|
||||
Highland: {tower: 1, raven: 1, wolfHeadErased: 1, wolfPassant: 1, goat: 1, axe: 1},
|
||||
River: {tower: 1, garb: 1, rake: 1, boat: 1, pike: 2, bullHeadCaboshed: 1, apple: 1, plough: 1},
|
||||
Lake: {cancer: 2, escallop: 1, pike: 2, heron: 1, boat: 1, boat2: 2},
|
||||
Nomadic: {pot: 1, buckle: 1, wheel: 2, sabre: 2, sabresCrossed: 1, bow: 2, arrow: 1, horseRampant: 1, horseSalient: 1, crescent: 1, camel: 3},
|
||||
Hunting: {
|
||||
natural: {
|
||||
fountain: "azure",
|
||||
garb: "or",
|
||||
raven: "sable",
|
||||
dove: "argent",
|
||||
doveDisplayed: "argent",
|
||||
fly: "sable"
|
||||
}, // charges to mainly use predefined colours
|
||||
multicolor: {
|
||||
// charges that can have several tinctures
|
||||
agnusDei: 2,
|
||||
angel: 2,
|
||||
apple: 2,
|
||||
arbalest: 3,
|
||||
arrow: 3,
|
||||
arrowsSheaf: 3,
|
||||
axe: 2,
|
||||
badgerStatant: 2,
|
||||
banner: 2,
|
||||
basilisk: 3,
|
||||
bearPassant: 3,
|
||||
bearRampant: 3,
|
||||
bee: 3,
|
||||
bell: 2,
|
||||
boarHeadErased: 3,
|
||||
boarRampant: 3,
|
||||
boat: 2,
|
||||
bookClosed: 3,
|
||||
bookOpen: 3,
|
||||
bowWithArrow: 3,
|
||||
bucket: 2,
|
||||
bugleHorn: 2,
|
||||
bugleHorn2: 1,
|
||||
stagsAttires: 2,
|
||||
attire: 2,
|
||||
hatchet: 1,
|
||||
bowWithArrow: 1,
|
||||
arrowsSheaf: 1,
|
||||
deerHeadCaboshed: 1,
|
||||
wolfStatant: 1,
|
||||
oak: 1,
|
||||
greyhoundSejant: 1
|
||||
bugleHorn2: 2,
|
||||
bullHeadCaboshed: 2,
|
||||
bullPassant: 3,
|
||||
butterfly: 3,
|
||||
camel: 2,
|
||||
cannon: 2,
|
||||
caravel: 3,
|
||||
castle: 2,
|
||||
catPassantGuardant: 2,
|
||||
chalice: 2,
|
||||
cock: 3,
|
||||
cowStatant: 3,
|
||||
crocodile: 2,
|
||||
crown: 2,
|
||||
crown2: 3,
|
||||
deerHeadCaboshed: 2,
|
||||
dolphin: 2,
|
||||
donkeyHeadCaboshed: 2,
|
||||
dove: 2,
|
||||
doveDisplayed: 2,
|
||||
dragonPassant: 3,
|
||||
dragonRampant: 3,
|
||||
drum: 3,
|
||||
duck: 3,
|
||||
eagle: 3,
|
||||
eagleTwoHeads: 3,
|
||||
elephant: 2,
|
||||
elephantHeadErased: 2,
|
||||
falchion: 2,
|
||||
falcon: 3,
|
||||
fasces: 3,
|
||||
fly: 3,
|
||||
garb: 2,
|
||||
goat: 3,
|
||||
grapeBunch: 3,
|
||||
greyhoundCourant: 3,
|
||||
greyhoundRampant: 2,
|
||||
greyhoundSejant: 3,
|
||||
griffinPassant: 3,
|
||||
griffinRampant: 3,
|
||||
harp: 2,
|
||||
hatchet: 2,
|
||||
head: 2,
|
||||
headWreathed: 3,
|
||||
hedgehog: 3,
|
||||
heron: 2,
|
||||
hindStatant: 2,
|
||||
horsePassant: 2,
|
||||
horseRampant: 3,
|
||||
horseSalient: 2,
|
||||
lamb: 2,
|
||||
lambPassantReguardant: 2,
|
||||
laurelWreath: 2,
|
||||
lionHeadCaboshed: 2,
|
||||
lionHeadErased: 2,
|
||||
lionPassant: 3,
|
||||
lionPassantGuardant: 3,
|
||||
lionRampant: 3,
|
||||
lionSejant: 3,
|
||||
lochaberAxe: 2,
|
||||
lute: 2,
|
||||
lymphad: 3,
|
||||
mallet: 2,
|
||||
martenCourant: 3,
|
||||
mastiffStatant: 3,
|
||||
mitre: 3,
|
||||
oak: 3,
|
||||
orb: 3,
|
||||
owl: 2,
|
||||
owlDisplayed: 2,
|
||||
palmTree: 3,
|
||||
parrot: 2,
|
||||
peacock: 3,
|
||||
peacockInPride: 3,
|
||||
pear: 2,
|
||||
pegasus: 3,
|
||||
pike: 2,
|
||||
pineTree: 2,
|
||||
plaice: 2,
|
||||
plough: 2,
|
||||
porcupine: 2,
|
||||
rabbitSejant: 2,
|
||||
ramHeadErased: 3,
|
||||
ramPassant: 3,
|
||||
ratRampant: 2,
|
||||
raven: 2,
|
||||
rhinoceros: 2,
|
||||
rose: 3,
|
||||
sabre: 2,
|
||||
sabre2: 2,
|
||||
sabresCrossed: 2,
|
||||
sagittarius: 3,
|
||||
salmon: 2,
|
||||
scythe: 2,
|
||||
serpent: 2,
|
||||
shield: 2,
|
||||
sickle: 2,
|
||||
snake: 2,
|
||||
spear: 2,
|
||||
squirrel: 2,
|
||||
stagPassant: 2,
|
||||
stirrup: 2,
|
||||
swallow: 2,
|
||||
swan: 3,
|
||||
swanErased: 3,
|
||||
sword: 2,
|
||||
talbotPassant: 3,
|
||||
talbotSejant: 3,
|
||||
tower: 2,
|
||||
unicornRampant: 3,
|
||||
wheatStalk: 2,
|
||||
wingSword: 3,
|
||||
wolfHeadErased: 2,
|
||||
wolfPassant: 3,
|
||||
wolfRampant: 3,
|
||||
wolfStatant: 3,
|
||||
wyvern: 3,
|
||||
wyvernWithWingsDisplayed: 3
|
||||
},
|
||||
// selection based on type
|
||||
City: {key: 3, bell: 2, lute: 1, tower: 1, castle: 1, mallet: 1, cannon: 1, anvil: 1},
|
||||
Capital: {crown: 2, orb: 1, lute: 1, castle: 3, tower: 1, crown2: 2},
|
||||
Сathedra: {chalice: 1, orb: 1, crosier: 2, lamb: 1, monk: 2, angel: 3, crossLatin: 2, crossPatriarchal: 1, crossOrthodox: 1, crossCalvary: 1, agnusDei: 3},
|
||||
// specific cases
|
||||
natural: {fountain: "azure", garb: "or", raven: "sable"}, // charges to mainly use predefined colours
|
||||
sinister: [
|
||||
// charges that can be sinister
|
||||
"moonInCrescent",
|
||||
"crossGamma",
|
||||
"lionRampant",
|
||||
"lionPassant",
|
||||
"lionSejant",
|
||||
"wolfRampant",
|
||||
"wolfPassant",
|
||||
"wolfStatant",
|
||||
"wolfHeadErased",
|
||||
"greyhoundСourant",
|
||||
"greyhoundRampant",
|
||||
"greyhoundSejant",
|
||||
"mastiffStatant",
|
||||
"talbotPassant",
|
||||
"talbotSejant",
|
||||
"martenCourant",
|
||||
"boarRampant",
|
||||
"badgerStatant",
|
||||
"stagPassant",
|
||||
"hindStatant",
|
||||
"horseRampant",
|
||||
"horseSalient",
|
||||
"horsePassant",
|
||||
"bullPassant",
|
||||
"bearRampant",
|
||||
"bearPassant",
|
||||
"cowStatant",
|
||||
"boarHeadErased",
|
||||
"horseHeadCouped",
|
||||
"lionHeadErased",
|
||||
"ramHeadErased",
|
||||
"elephantHeadErased",
|
||||
"ramPassant",
|
||||
"goat",
|
||||
"lamb",
|
||||
"lambPassantReguardant",
|
||||
"agnusDei",
|
||||
"dove",
|
||||
"doveDisplayed",
|
||||
"duck",
|
||||
"peacock",
|
||||
"peacockInPride",
|
||||
"swallow",
|
||||
"elephant",
|
||||
"rhinoceros",
|
||||
"eagle",
|
||||
"falcon",
|
||||
"raven",
|
||||
"cock",
|
||||
"parrot",
|
||||
|
|
@ -357,6 +589,8 @@ window.COA = (function () {
|
|||
"swanErased",
|
||||
"heron",
|
||||
"pike",
|
||||
"plaice",
|
||||
"salmon",
|
||||
"dragonPassant",
|
||||
"dragonRampant",
|
||||
"wyvern",
|
||||
|
|
@ -366,6 +600,7 @@ window.COA = (function () {
|
|||
"unicornRampant",
|
||||
"pegasus",
|
||||
"serpent",
|
||||
"sagittarius",
|
||||
"hatchet",
|
||||
"lochaberAxe",
|
||||
"hand",
|
||||
|
|
@ -378,6 +613,7 @@ window.COA = (function () {
|
|||
"headWreathed",
|
||||
"knight",
|
||||
"lymphad",
|
||||
"caravel",
|
||||
"log",
|
||||
"crosier",
|
||||
"dolphin",
|
||||
|
|
@ -389,13 +625,23 @@ window.COA = (function () {
|
|||
"fasces",
|
||||
"lionPassantGuardant",
|
||||
"helmet",
|
||||
"gauntlet",
|
||||
"shield",
|
||||
"foot",
|
||||
"sickle",
|
||||
"scythe",
|
||||
"plough",
|
||||
"sabre2",
|
||||
"cannon",
|
||||
"porcupine",
|
||||
"hedgehog",
|
||||
"catPassantGuardant",
|
||||
"rabbitSejant",
|
||||
"ratRampant",
|
||||
"squirrel",
|
||||
"basilisk",
|
||||
"snake",
|
||||
"crocodile",
|
||||
"anvil"
|
||||
],
|
||||
reversed: [
|
||||
|
|
@ -404,24 +650,222 @@ window.COA = (function () {
|
|||
"mullet",
|
||||
"mullet7",
|
||||
"crescent",
|
||||
"crossTau",
|
||||
"cancer",
|
||||
"frog",
|
||||
"lizard",
|
||||
"scorpion",
|
||||
"butterfly",
|
||||
"bee",
|
||||
"fly",
|
||||
"trefoil",
|
||||
"cinquefoil",
|
||||
"sword",
|
||||
"falchion",
|
||||
"sabresCrossed",
|
||||
"spear",
|
||||
"gauntlet",
|
||||
"hand",
|
||||
"horseshoe",
|
||||
"bowWithArrow",
|
||||
"arrow",
|
||||
"arrowsSheaf",
|
||||
"arbalest",
|
||||
"rake",
|
||||
"sickle",
|
||||
"scythe",
|
||||
"scissors",
|
||||
"crossTriquetra",
|
||||
"crossLatin",
|
||||
"crossTau",
|
||||
"sabre2"
|
||||
],
|
||||
patternable: [
|
||||
// charges that can have pattern tincture when counterchanged
|
||||
"lozengePloye",
|
||||
"roundel",
|
||||
"annulet",
|
||||
"mullet4",
|
||||
"mullet8",
|
||||
"delf",
|
||||
"triangle",
|
||||
"trianglePierced",
|
||||
"sun",
|
||||
"fountain",
|
||||
"inescutcheonRound",
|
||||
"inescutcheonSquare",
|
||||
"inescutcheonNo",
|
||||
"crossHummetty",
|
||||
"crossVoided",
|
||||
"crossPattee",
|
||||
"crossPatteeAlisee",
|
||||
"crossFormee",
|
||||
"crossFormee2",
|
||||
"crossPotent",
|
||||
"crossJerusalem",
|
||||
"crosslet",
|
||||
"crossClechy",
|
||||
"crossBottony",
|
||||
"crossFleury",
|
||||
"crossPatonce",
|
||||
"crossPommy",
|
||||
"crossGamma",
|
||||
"crossArrowed",
|
||||
"crossFitchy",
|
||||
"crossCercelee",
|
||||
"crossMoline",
|
||||
"crossAvellane",
|
||||
"crossErminee",
|
||||
"crossBiparted",
|
||||
"crossMaltese",
|
||||
"crossTemplar",
|
||||
"crossCeltic",
|
||||
"crossCeltic2",
|
||||
"crossTau"
|
||||
]
|
||||
};
|
||||
|
||||
// charges specific to culture or burg type (FMG-only config, not coming from Armoria)
|
||||
const typeMapping = {
|
||||
Naval: {anchor: 3, boat: 1, lymphad: 2, armillarySphere: 1, escallop: 1, dolphin: 1, plaice: 1, caravel: 1},
|
||||
Highland: {tower: 1, raven: 1, wolfHeadErased: 1, wolfPassant: 1, goat: 1, axe: 1},
|
||||
River: {
|
||||
tower: 1,
|
||||
garb: 1,
|
||||
rake: 1,
|
||||
boat: 1,
|
||||
pike: 2,
|
||||
bullHeadCaboshed: 1,
|
||||
apple: 1,
|
||||
pear: 1,
|
||||
plough: 1,
|
||||
salmon: 1,
|
||||
cancer: 1,
|
||||
bridge: 2,
|
||||
sickle: 1,
|
||||
scythe: 1,
|
||||
grapeBunch: 1,
|
||||
wheatStalk: 1,
|
||||
crocodile: 1
|
||||
},
|
||||
Lake: {
|
||||
cancer: 2,
|
||||
escallop: 1,
|
||||
pike: 2,
|
||||
heron: 1,
|
||||
boat: 1,
|
||||
boat2: 2,
|
||||
salmon: 1,
|
||||
cancer: 1,
|
||||
sickle: 1,
|
||||
swanErased: 1,
|
||||
swan: 1,
|
||||
frog: 1
|
||||
},
|
||||
Nomadic: {
|
||||
pot: 1,
|
||||
buckle: 1,
|
||||
wheel: 2,
|
||||
sabre: 2,
|
||||
sabresCrossed: 1,
|
||||
bow: 2,
|
||||
arrow: 1,
|
||||
horseRampant: 1,
|
||||
horseSalient: 1,
|
||||
crescent: 1,
|
||||
camel: 3,
|
||||
falcon: 1
|
||||
},
|
||||
Hunting: {
|
||||
bugleHorn: 2,
|
||||
bugleHorn2: 1,
|
||||
stagsAttires: 2,
|
||||
attire: 2,
|
||||
hatchet: 1,
|
||||
bowWithArrow: 1,
|
||||
arrowsSheaf: 1,
|
||||
deerHeadCaboshed: 1,
|
||||
wolfStatant: 1,
|
||||
oak: 1,
|
||||
pineCone: 1,
|
||||
pineTree: 1,
|
||||
oak: 1,
|
||||
owl: 1,
|
||||
falcon: 1,
|
||||
peacock: 1,
|
||||
boarHeadErased: 2,
|
||||
horseHeadCouped: 1,
|
||||
rabbitSejant: 1,
|
||||
wolfRampant: 1,
|
||||
wolfPassant: 1,
|
||||
wolfStatant: 1,
|
||||
greyhoundCourant: 1,
|
||||
greyhoundRampant: 1,
|
||||
greyhoundSejant: 1,
|
||||
mastiffStatant: 1,
|
||||
talbotPassant: 1,
|
||||
talbotSejant: 1,
|
||||
stagPassant: 1
|
||||
},
|
||||
// selection based on type
|
||||
City: {
|
||||
key: 4,
|
||||
bell: 3,
|
||||
lute: 1,
|
||||
tower: 1,
|
||||
castle: 1,
|
||||
mallet: 1,
|
||||
cannon: 1,
|
||||
anvil: 1,
|
||||
buckle: 1,
|
||||
horseshoe: 1,
|
||||
stirrup: 1,
|
||||
banner: 1,
|
||||
bookClosed: 1,
|
||||
scissors: 1,
|
||||
bridge: 2,
|
||||
cannon: 1,
|
||||
shield: 1,
|
||||
arbalest: 1,
|
||||
bowWithArrow: 1,
|
||||
spear: 1,
|
||||
lochaberAxe: 1,
|
||||
grapeBunch: 1,
|
||||
cock: 1,
|
||||
ramHeadErased: 1,
|
||||
ratRampant: 1
|
||||
},
|
||||
Capital: {
|
||||
crown: 2,
|
||||
crown2: 1,
|
||||
laurelWreath: 1,
|
||||
orb: 1,
|
||||
lute: 1,
|
||||
castle: 3,
|
||||
tower: 1,
|
||||
crown2: 2,
|
||||
column: 1,
|
||||
lionRampant: 1,
|
||||
stagPassant: 1
|
||||
},
|
||||
Сathedra: {
|
||||
crossHummetty: 3,
|
||||
mitre: 3,
|
||||
chalice: 1,
|
||||
orb: 1,
|
||||
crosier: 2,
|
||||
lamb: 1,
|
||||
monk: 2,
|
||||
angel: 3,
|
||||
crossLatin: 2,
|
||||
crossPatriarchal: 1,
|
||||
crossOrthodox: 1,
|
||||
crossCalvary: 1,
|
||||
agnusDei: 3,
|
||||
bookOpen: 1,
|
||||
sceptre: 1
|
||||
}
|
||||
};
|
||||
|
||||
const positions = {
|
||||
conventional: {
|
||||
e: 20,
|
||||
|
|
@ -508,7 +952,22 @@ window.COA = (function () {
|
|||
},
|
||||
// charges
|
||||
inescutcheon: {e: 4, jln: 1},
|
||||
mascle: {e: 15, abcdefgzi: 3, beh: 3, bdefh: 4, acegi: 1, kn: 3, joe: 2, abc: 3, jlh: 8, jleh: 1, df: 3, abcpqh: 4, pqe: 3, eknpq: 3},
|
||||
mascle: {
|
||||
e: 15,
|
||||
abcdefgzi: 3,
|
||||
beh: 3,
|
||||
bdefh: 4,
|
||||
acegi: 1,
|
||||
kn: 3,
|
||||
joe: 2,
|
||||
abc: 3,
|
||||
jlh: 8,
|
||||
jleh: 1,
|
||||
df: 3,
|
||||
abcpqh: 4,
|
||||
pqe: 3,
|
||||
eknpq: 3
|
||||
},
|
||||
lionRampant: {e: 10, def: 2, abc: 2, bdefh: 1, kn: 1, jlh: 2, abcpqh: 1},
|
||||
lionPassant: {e: 10, def: 1, abc: 1, bdefh: 1, jlh: 1, abcpqh: 1},
|
||||
wolfPassant: {e: 10, def: 1, abc: 1, bdefh: 1, jlh: 1, abcpqh: 1},
|
||||
|
|
@ -667,7 +1126,7 @@ window.COA = (function () {
|
|||
};
|
||||
|
||||
const generate = function (parent, kinship, dominion, type) {
|
||||
if (!parent || parent === "custom") {
|
||||
if (!parent || parent.custom) {
|
||||
parent = null;
|
||||
kinship = 0;
|
||||
dominion = 0;
|
||||
|
|
@ -681,18 +1140,45 @@ window.COA = (function () {
|
|||
const coa = {t1};
|
||||
|
||||
let charge = P(usedPattern ? 0.5 : 0.93) ? true : false; // 80% for charge
|
||||
const linedOrdinary = (charge && P(0.3)) || P(0.5) ? (parent?.ordinaries && P(kinship) ? parent.ordinaries[0].ordinary : rw(ordinaries.lined)) : null;
|
||||
const linedOrdinary =
|
||||
(charge && P(0.3)) || P(0.5)
|
||||
? parent?.ordinaries && P(kinship)
|
||||
? parent.ordinaries[0].ordinary
|
||||
: rw(ordinaries.lined)
|
||||
: null;
|
||||
|
||||
const ordinary = (!charge && P(0.65)) || P(0.3) ? (linedOrdinary ? linedOrdinary : rw(ordinaries.straight)) : null; // 36% for ordinary
|
||||
const rareDivided = ["chief", "terrace", "chevron", "quarter", "flaunches"].includes(ordinary);
|
||||
const divisioned = rareDivided ? P(0.03) : charge && ordinary ? P(0.03) : charge ? P(0.3) : ordinary ? P(0.7) : P(0.995); // 33% for division
|
||||
const division = divisioned ? (parent?.division && P(kinship - 0.1) ? parent.division.division : rw(divisions.variants)) : null;
|
||||
|
||||
const divisioned = rareDivided
|
||||
? P(0.03)
|
||||
: charge && ordinary
|
||||
? P(0.03)
|
||||
: charge
|
||||
? P(0.3)
|
||||
: ordinary
|
||||
? P(0.7)
|
||||
: P(0.995); // 33% for division
|
||||
|
||||
const division = divisioned
|
||||
? parent?.division && P(kinship - 0.1)
|
||||
? parent.division.division
|
||||
: rw(divisions.variants)
|
||||
: null;
|
||||
|
||||
if (charge)
|
||||
charge = parent?.charges && P(kinship - 0.1) ? parent.charges[0].charge : type && type !== "Generic" && P(0.2) ? rw(charges[type]) : selectCharge();
|
||||
charge =
|
||||
parent?.charges && P(kinship - 0.1)
|
||||
? parent.charges[0].charge
|
||||
: type && type !== "Generic" && P(0.2)
|
||||
? rw(typeMapping[type])
|
||||
: selectCharge();
|
||||
|
||||
if (division) {
|
||||
const t = getTincture("division", usedTinctures, P(0.98) ? coa.t1 : null);
|
||||
coa.division = {division, t};
|
||||
if (divisions[division]) coa.division.line = usedPattern || (ordinary && P(0.7)) ? "straight" : rw(divisions[division]);
|
||||
if (divisions[division])
|
||||
coa.division.line = usedPattern || (ordinary && P(0.7)) ? "straight" : rw(divisions[division]);
|
||||
}
|
||||
|
||||
if (ordinary) {
|
||||
|
|
@ -708,8 +1194,9 @@ window.COA = (function () {
|
|||
}
|
||||
|
||||
if (charge) {
|
||||
let p = "e",
|
||||
t = "gules";
|
||||
let p = "e";
|
||||
let t = "gules";
|
||||
|
||||
const ordinaryT = coa.ordinaries ? coa.ordinaries[0].t : null;
|
||||
if (positions.ordinariesOn[ordinary] && P(0.8)) {
|
||||
// place charge over ordinary (use tincture of field type)
|
||||
|
|
@ -739,7 +1226,12 @@ window.COA = (function () {
|
|||
}
|
||||
|
||||
if (charges.natural[charge]) t = charges.natural[charge]; // natural tincture
|
||||
coa.charges = [{charge, t, p}];
|
||||
const item = {charge, t, p};
|
||||
|
||||
const multicolor = charges.multicolor[charge];
|
||||
if (multicolor > 1) item.t2 = P(0.25) ? getTincture("charge", usedTinctures, coa.t1) : t;
|
||||
if (multicolor > 2) item.t3 = P(0.5) ? getTincture("charge", usedTinctures, coa.t1) : t;
|
||||
coa.charges = [item];
|
||||
|
||||
if (p === "ABCDEFGHIKL" && P(0.95)) {
|
||||
// add central charge if charge is in bordure
|
||||
|
|
@ -768,7 +1260,14 @@ window.COA = (function () {
|
|||
// counterchanged, 40%
|
||||
else if (["perPale", "perFess", "perBend", "perBendSinister"].includes(division) && P(0.8)) {
|
||||
// place 2 charges in division standard positions
|
||||
const [p1, p2] = division === "perPale" ? ["p", "q"] : division === "perFess" ? ["k", "n"] : division === "perBend" ? ["l", "m"] : ["j", "o"]; // perBendSinister
|
||||
const [p1, p2] =
|
||||
division === "perPale"
|
||||
? ["p", "q"]
|
||||
: division === "perFess"
|
||||
? ["k", "n"]
|
||||
: division === "perBend"
|
||||
? ["l", "m"]
|
||||
: ["j", "o"]; // perBendSinister
|
||||
coa.charges[0].p = p1;
|
||||
|
||||
const charge = selectCharge(charges.single);
|
||||
|
|
|
|||
|
|
@ -1814,11 +1814,16 @@ window.COArenderer = (function () {
|
|||
const loadedPatterns = getPatterns(coa, id);
|
||||
const blacklight = `<radialGradient id="backlight_${id}" cx="100%" cy="100%" r="150%"><stop stop-color="#fff" stop-opacity=".3" offset="0"/><stop stop-color="#fff" stop-opacity=".15" offset=".25"/><stop stop-color="#000" stop-opacity="0" offset="1"/></radialGradient>`;
|
||||
const field = `<rect x="0" y="0" width="200" height="200" fill="${clr(coa.t1)}"/>`;
|
||||
const style = `<style>
|
||||
g.secondary,path.secondary {fill: var(--secondary);}
|
||||
g.tertiary,path.tertiary {fill: var(--tertiary);}
|
||||
</style>`;
|
||||
|
||||
const divisionGroup = division ? templateDivision() : "";
|
||||
const overlay = `<path d="${shieldPath}" fill="url(#backlight_${id})" stroke="#333"/>`;
|
||||
|
||||
const svg = `<svg id="${id}" width="200" height="200" viewBox="${viewBox}">
|
||||
<defs>${shieldClip}${divisionClip}${loadedCharges}${loadedPatterns}${blacklight}</defs>
|
||||
<defs>${shieldClip}${divisionClip}${loadedCharges}${loadedPatterns}${blacklight}${style}</defs>
|
||||
<g clip-path="url(#${shield}_${id})">${field}${divisionGroup}${templateAboveAll()}</g>
|
||||
${overlay}</svg>`;
|
||||
|
||||
|
|
@ -1903,17 +1908,20 @@ window.COArenderer = (function () {
|
|||
return svg + `</g>`;
|
||||
}
|
||||
|
||||
function templateCharge(charge, tincture) {
|
||||
const fill = clr(tincture);
|
||||
function templateCharge(charge, tincture, secondaryTincture, tertiaryTincture) {
|
||||
const primary = clr(tincture);
|
||||
const secondary = clr(secondaryTincture || tincture);
|
||||
const tertiary = clr(tertiaryTincture || tincture);
|
||||
const stroke = charge.stroke || "#000";
|
||||
|
||||
const chargePositions = [...new Set(charge.p)].filter(position => positions[position]);
|
||||
|
||||
let svg = "";
|
||||
svg += `<g fill="${fill}" stroke="#000">`;
|
||||
let svg = `<g fill="${primary}" style="--secondary: ${secondary}; --tertiary: ${tertiary}" stroke="${stroke}">`;
|
||||
for (const p of chargePositions) {
|
||||
const transform = getElTransform(charge, p);
|
||||
svg += `<use href="#${charge.charge}_${id}" transform="${transform}"></use>`;
|
||||
}
|
||||
return svg + `</g>`;
|
||||
return svg + "</g>";
|
||||
|
||||
function getElTransform(c, p) {
|
||||
const s = (c.size || 1) * sizeModifier;
|
||||
|
|
@ -2015,14 +2023,8 @@ window.COArenderer = (function () {
|
|||
|
||||
// render coa if does not exist
|
||||
const trigger = async function (id, coa) {
|
||||
if (coa === "custom") {
|
||||
console.warn("Cannot render custom emblem", coa);
|
||||
return;
|
||||
}
|
||||
if (!coa) {
|
||||
console.warn(`Emblem ${id} is undefined`);
|
||||
return;
|
||||
}
|
||||
if (!coa) return console.warn(`Emblem ${id} is undefined`);
|
||||
if (coa.custom) return console.warn("Cannot render custom emblem", coa);
|
||||
if (!document.getElementById(id)) return draw(id, coa);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use strict";
|
||||
|
||||
// update old .map version to the current one
|
||||
// update old map file to the current version
|
||||
export function resolveVersionConflicts(version) {
|
||||
if (version < 1) {
|
||||
// v1.0 added a new religions layer
|
||||
|
|
@ -636,4 +636,87 @@ export function resolveVersionConflicts(version) {
|
|||
if (coa?.shield === "state") delete coa.shield;
|
||||
});
|
||||
}
|
||||
|
||||
if (version < 1.91) {
|
||||
// from v1.90.02 texture image is always there
|
||||
if (!texture.select("#textureImage").size()) {
|
||||
// cleanup old texture if it has no id and add new one
|
||||
texture.selectAll("*").remove();
|
||||
texture
|
||||
.append("image")
|
||||
.attr("id", "textureImage")
|
||||
.attr("width", "100%")
|
||||
.attr("height", "100%")
|
||||
.attr("preserveAspectRatio", "xMidYMid slice")
|
||||
.attr("src", "https://i2.wp.com/azgaar.files.wordpress.com/2021/10/marble-big.jpg");
|
||||
}
|
||||
|
||||
// from 1.91.00 custom coa is moved to coa object
|
||||
pack.states.forEach(state => {
|
||||
if (state.coa === "custom") state.coa = {custom: true};
|
||||
});
|
||||
pack.provinces.forEach(province => {
|
||||
if (province.coa === "custom") province.coa = {custom: true};
|
||||
});
|
||||
pack.burgs.forEach(burg => {
|
||||
if (burg.coa === "custom") burg.coa = {custom: true};
|
||||
});
|
||||
|
||||
// from 1.91.00 emblems don't have transform attribute
|
||||
emblems.selectAll("use").each(function () {
|
||||
const transform = this.getAttribute("transform");
|
||||
if (!transform) return;
|
||||
|
||||
const [dx, dy] = parseTransform(transform);
|
||||
const x = Number(this.getAttribute("x")) + Number(dx);
|
||||
const y = Number(this.getAttribute("y")) + Number(dy);
|
||||
|
||||
this.setAttribute("x", x);
|
||||
this.setAttribute("y", y);
|
||||
this.removeAttribute("transform");
|
||||
});
|
||||
|
||||
// from 1.91.00 coaSize is moved to coa object
|
||||
pack.states.forEach(state => {
|
||||
if (state.coaSize && state.coa) {
|
||||
state.coa.size = state.coaSize;
|
||||
delete state.coaSize;
|
||||
}
|
||||
});
|
||||
|
||||
pack.provinces.forEach(province => {
|
||||
if (province.coaSize && province.coa) {
|
||||
province.coa.size = province.coaSize;
|
||||
delete province.coaSize;
|
||||
}
|
||||
});
|
||||
|
||||
pack.burgs.forEach(burg => {
|
||||
if (burg.coaSize && burg.coa) {
|
||||
burg.coa.size = burg.coaSize;
|
||||
delete burg.coaSize;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (version < 1.92) {
|
||||
// v1.92 change labels text-anchor from 'start' to 'middle'
|
||||
labels.selectAll("tspan").each(function () {
|
||||
this.setAttribute("x", 0);
|
||||
});
|
||||
|
||||
// leftover from v1.90.02
|
||||
texture.style("display", null);
|
||||
const textureImage = texture.select("#textureImage");
|
||||
if (textureImage.size()) {
|
||||
const xlink = textureImage.attr("xlink:href");
|
||||
const href = textureImage.attr("href");
|
||||
const src = xlink || href;
|
||||
|
||||
if (src) {
|
||||
textureImage.attr("src", src);
|
||||
textureImage.attr("xlink:href", null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -401,7 +401,7 @@ function cultureChangeEmblemsShape() {
|
|||
};
|
||||
|
||||
pack.states.forEach(state => {
|
||||
if (state.culture !== culture || !state.i || state.removed || !state.coa || state.coa === "custom") return;
|
||||
if (state.culture !== culture || !state.i || state.removed || !state.coa || state.coa.custom) return;
|
||||
if (shape === state.coa.shield) return;
|
||||
state.coa.shield = shape;
|
||||
rerenderCOA("stateCOA" + state.i, state.coa);
|
||||
|
|
@ -413,7 +413,7 @@ function cultureChangeEmblemsShape() {
|
|||
!province.i ||
|
||||
province.removed ||
|
||||
!province.coa ||
|
||||
province.coa === "custom"
|
||||
province.coa.custom
|
||||
)
|
||||
return;
|
||||
if (shape === province.coa.shield) return;
|
||||
|
|
@ -422,7 +422,7 @@ function cultureChangeEmblemsShape() {
|
|||
});
|
||||
|
||||
pack.burgs.forEach(burg => {
|
||||
if (burg.culture !== culture || !burg.i || burg.removed || !burg.coa || burg.coa === "custom") return;
|
||||
if (burg.culture !== culture || !burg.i || burg.removed || !burg.coa || burg.coa.custom) return;
|
||||
if (shape === burg.coa.shield) return;
|
||||
burg.coa.shield = shape;
|
||||
rerenderCOA("burgCOA" + burg.i, burg.coa);
|
||||
|
|
|
|||
|
|
@ -494,7 +494,7 @@ function editStateName(state) {
|
|||
s.name = nameInput.value;
|
||||
s.formName = formSelect.value;
|
||||
s.fullName = fullNameInput.value;
|
||||
if (changed && stateNameEditorUpdateLabel.checked) BurgsAndStates.drawStateLabels([s.i]);
|
||||
if (changed && stateNameEditorUpdateLabel.checked) drawStateLabels([s.i]);
|
||||
refreshStatesEditor();
|
||||
}
|
||||
}
|
||||
|
|
@ -877,7 +877,7 @@ function recalculateStates(must) {
|
|||
if (!layerIsOn("toggleBorders")) toggleBorders();
|
||||
else drawBorders();
|
||||
if (layerIsOn("toggleProvinces")) drawProvinces();
|
||||
if (adjustLabels.checked) BurgsAndStates.drawStateLabels();
|
||||
if (adjustLabels.checked) drawStateLabels();
|
||||
refreshStatesEditor();
|
||||
}
|
||||
|
||||
|
|
@ -1022,7 +1022,7 @@ function applyStatesManualAssignent() {
|
|||
if (affectedStates.length) {
|
||||
refreshStatesEditor();
|
||||
layerIsOn("toggleStates") ? drawStates() : toggleStates();
|
||||
if (adjustLabels.checked) BurgsAndStates.drawStateLabels([...new Set(affectedStates)]);
|
||||
if (adjustLabels.checked) drawStateLabels([...new Set(affectedStates)]);
|
||||
adjustProvinces([...new Set(affectedProvinces)]);
|
||||
layerIsOn("toggleBorders") ? drawBorders() : toggleBorders();
|
||||
if (layerIsOn("toggleProvinces")) drawProvinces();
|
||||
|
|
@ -1459,7 +1459,7 @@ function openStateMergeDialog() {
|
|||
layerIsOn("toggleStates") ? drawStates() : toggleStates();
|
||||
layerIsOn("toggleBorders") ? drawBorders() : toggleBorders();
|
||||
layerIsOn("toggleProvinces") && drawProvinces();
|
||||
BurgsAndStates.drawStateLabels([rulingStateId]);
|
||||
drawStateLabels([rulingStateId]);
|
||||
|
||||
refreshStatesEditor();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,11 +3,12 @@ export function exportToJson(type) {
|
|||
return tip("Data cannot be exported when edit mode is active, please exit the mode and retry", false, "error");
|
||||
closeDialogs("#alert");
|
||||
|
||||
TIME && console.time("exportToJson");
|
||||
const typeMap = {
|
||||
Full: getFullDataJson,
|
||||
Minimal: getMinimalDataJson,
|
||||
PackCells: getPackCellsDataJson,
|
||||
GridCells: getGridCellsDataJson
|
||||
PackCells: getPackDataJson,
|
||||
GridCells: getGridDataJson
|
||||
};
|
||||
|
||||
const mapData = typeMap[type]();
|
||||
|
|
@ -19,24 +20,28 @@ export function exportToJson(type) {
|
|||
link.click();
|
||||
tip(`${link.download} is saved. Open "Downloads" screen (CTRL + J) to check`, true, "success", 7000);
|
||||
window.URL.revokeObjectURL(URL);
|
||||
TIME && console.timeEnd("exportToJson");
|
||||
}
|
||||
|
||||
function getFullDataJson() {
|
||||
TIME && console.time("getFullDataJson");
|
||||
|
||||
const info = getMapInfo();
|
||||
const settings = getSettings();
|
||||
const cells = getPackCellsData();
|
||||
const vertices = getPackVerticesData();
|
||||
const exportData = {info, settings, coords: mapCoordinates, cells, vertices, biomes: biomesData, notes, nameBases};
|
||||
const pack = getPackCellsData();
|
||||
const grid = getGridCellsData();
|
||||
|
||||
TIME && console.timeEnd("getFullDataJson");
|
||||
return JSON.stringify(exportData);
|
||||
return JSON.stringify({
|
||||
info,
|
||||
settings,
|
||||
mapCoordinates,
|
||||
pack,
|
||||
grid,
|
||||
biomesData,
|
||||
notes,
|
||||
nameBases
|
||||
});
|
||||
}
|
||||
|
||||
function getMinimalDataJson() {
|
||||
TIME && console.time("getMinimalDataJson");
|
||||
|
||||
const info = getMapInfo();
|
||||
const settings = getSettings();
|
||||
const packData = {
|
||||
|
|
@ -49,36 +54,23 @@ function getMinimalDataJson() {
|
|||
rivers: pack.rivers,
|
||||
markers: pack.markers
|
||||
};
|
||||
const exportData = {info, settings, coords: mapCoordinates, pack: packData, biomes: biomesData, notes, nameBases};
|
||||
|
||||
TIME && console.timeEnd("getMinimalDataJson");
|
||||
return JSON.stringify(exportData);
|
||||
return JSON.stringify({info, settings, mapCoordinates, pack: packData, biomesData, notes, nameBases});
|
||||
}
|
||||
|
||||
function getPackCellsDataJson() {
|
||||
TIME && console.time("getCellsDataJson");
|
||||
|
||||
function getPackDataJson() {
|
||||
const info = getMapInfo();
|
||||
const cells = getPackCellsData();
|
||||
const exportData = {info, cells};
|
||||
|
||||
TIME && console.timeEnd("getCellsDataJson");
|
||||
return JSON.stringify(exportData);
|
||||
return JSON.stringify({info, cells});
|
||||
}
|
||||
|
||||
function getGridCellsDataJson() {
|
||||
TIME && console.time("getGridCellsDataJson");
|
||||
|
||||
function getGridDataJson() {
|
||||
const info = getMapInfo();
|
||||
const gridCells = getGridCellsData();
|
||||
const exportData = {info, gridCells};
|
||||
|
||||
TIME && console.log("getGridCellsDataJson");
|
||||
return JSON.stringify(exportData);
|
||||
const cells = getGridCellsData();
|
||||
return JSON.stringify({info, cells});
|
||||
}
|
||||
|
||||
function getMapInfo() {
|
||||
const info = {
|
||||
return {
|
||||
version,
|
||||
description: "Azgaar's Fantasy Map Generator output: azgaar.github.io/Fantasy-map-generator",
|
||||
exportedAt: new Date().toISOString(),
|
||||
|
|
@ -86,12 +78,10 @@ function getMapInfo() {
|
|||
seed,
|
||||
mapId
|
||||
};
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
function getSettings() {
|
||||
const settings = {
|
||||
return {
|
||||
distanceUnit: distanceUnitInput.value,
|
||||
distanceScale: distanceScaleInput.value,
|
||||
areaUnit: areaUnit.value,
|
||||
|
|
@ -108,8 +98,6 @@ function getSettings() {
|
|||
urbanization: urbanization,
|
||||
mapSize: mapSizeOutput.value,
|
||||
latitudeO: latitudeOutput.value,
|
||||
temperatureEquator: temperatureEquatorOutput.value,
|
||||
temperaturePole: temperaturePoleOutput.value,
|
||||
prec: precOutput.value,
|
||||
options: options,
|
||||
mapName: mapName.value,
|
||||
|
|
@ -118,13 +106,10 @@ function getSettings() {
|
|||
rescaleLabels: rescaleLabels.checked,
|
||||
urbanDensity: urbanDensity
|
||||
};
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
function getPackCellsData() {
|
||||
const cellConverted = {
|
||||
i: Array.from(pack.cells.i),
|
||||
const dataArrays = {
|
||||
v: pack.cells.v,
|
||||
c: pack.cells.c,
|
||||
p: pack.cells.p,
|
||||
|
|
@ -150,41 +135,39 @@ function getPackCellsData() {
|
|||
province: Array.from(pack.cells.province)
|
||||
};
|
||||
|
||||
const cellObjArr = [];
|
||||
{
|
||||
cellConverted.i.forEach(value => {
|
||||
const cellobj = {
|
||||
i: value,
|
||||
v: cellConverted.v[value],
|
||||
c: cellConverted.c[value],
|
||||
p: cellConverted.p[value],
|
||||
g: cellConverted.g[value],
|
||||
h: cellConverted.h[value],
|
||||
area: cellConverted.area[value],
|
||||
f: cellConverted.f[value],
|
||||
t: cellConverted.t[value],
|
||||
haven: cellConverted.haven[value],
|
||||
harbor: cellConverted.harbor[value],
|
||||
fl: cellConverted.fl[value],
|
||||
r: cellConverted.r[value],
|
||||
conf: cellConverted.conf[value],
|
||||
biome: cellConverted.biome[value],
|
||||
s: cellConverted.s[value],
|
||||
pop: cellConverted.pop[value],
|
||||
culture: cellConverted.culture[value],
|
||||
burg: cellConverted.burg[value],
|
||||
road: cellConverted.road[value],
|
||||
crossroad: cellConverted.crossroad[value],
|
||||
state: cellConverted.state[value],
|
||||
religion: cellConverted.religion[value],
|
||||
province: cellConverted.province[value]
|
||||
};
|
||||
cellObjArr.push(cellobj);
|
||||
});
|
||||
}
|
||||
|
||||
const cellsData = {
|
||||
cells: cellObjArr,
|
||||
return {
|
||||
cells: Array.from(pack.cells.i).map(cellId => ({
|
||||
i: cellId,
|
||||
v: dataArrays.v[cellId],
|
||||
c: dataArrays.c[cellId],
|
||||
p: dataArrays.p[cellId],
|
||||
g: dataArrays.g[cellId],
|
||||
h: dataArrays.h[cellId],
|
||||
area: dataArrays.area[cellId],
|
||||
f: dataArrays.f[cellId],
|
||||
t: dataArrays.t[cellId],
|
||||
haven: dataArrays.haven[cellId],
|
||||
harbor: dataArrays.harbor[cellId],
|
||||
fl: dataArrays.fl[cellId],
|
||||
r: dataArrays.r[cellId],
|
||||
conf: dataArrays.conf[cellId],
|
||||
biome: dataArrays.biome[cellId],
|
||||
s: dataArrays.s[cellId],
|
||||
pop: dataArrays.pop[cellId],
|
||||
culture: dataArrays.culture[cellId],
|
||||
burg: dataArrays.burg[cellId],
|
||||
road: dataArrays.road[cellId],
|
||||
crossroad: dataArrays.crossroad[cellId],
|
||||
state: dataArrays.state[cellId],
|
||||
religion: dataArrays.religion[cellId],
|
||||
province: dataArrays.province[cellId]
|
||||
})),
|
||||
vertices: pack.vertices.c.map(vertexId => ({
|
||||
i: vertexId,
|
||||
p: pack.vertices.p[vertexId],
|
||||
v: pack.vertices.v[vertexId],
|
||||
c: pack.vertices.c[vertexId]
|
||||
})),
|
||||
features: pack.features,
|
||||
cultures: pack.cultures,
|
||||
burgs: pack.burgs,
|
||||
|
|
@ -194,32 +177,46 @@ function getPackCellsData() {
|
|||
rivers: pack.rivers,
|
||||
markers: pack.markers
|
||||
};
|
||||
|
||||
return cellsData;
|
||||
}
|
||||
|
||||
function getGridCellsData() {
|
||||
const dataArrays = {
|
||||
v: grid.cells.v,
|
||||
c: grid.cells.c,
|
||||
b: grid.cells.b,
|
||||
f: Array.from(grid.cells.f),
|
||||
t: Array.from(grid.cells.t),
|
||||
h: Array.from(grid.cells.h),
|
||||
temp: Array.from(grid.cells.temp),
|
||||
prec: Array.from(grid.cells.prec)
|
||||
};
|
||||
|
||||
const gridData = {
|
||||
cells: Array.from(grid.cells.i).map(cellId => ({
|
||||
i: cellId,
|
||||
v: dataArrays.v[cellId],
|
||||
c: dataArrays.c[cellId],
|
||||
b: dataArrays.b[cellId],
|
||||
f: dataArrays.f[cellId],
|
||||
t: dataArrays.t[cellId],
|
||||
h: dataArrays.h[cellId],
|
||||
temp: dataArrays.temp[cellId],
|
||||
prec: dataArrays.prec[cellId]
|
||||
})),
|
||||
vertices: grid.vertices.c.map(vertexId => ({
|
||||
i: vertexId,
|
||||
p: pack.vertices.p[vertexId],
|
||||
v: pack.vertices.v[vertexId],
|
||||
c: pack.vertices.c[vertexId]
|
||||
})),
|
||||
cellsDesired: grid.cellsDesired,
|
||||
spacing: grid.spacing,
|
||||
cellsY: grid.cellsY,
|
||||
cellsX: grid.cellsX,
|
||||
points: grid.points,
|
||||
boundary: grid.boundary
|
||||
boundary: grid.boundary,
|
||||
seed: grid.seed,
|
||||
features: pack.features
|
||||
};
|
||||
return gridData;
|
||||
}
|
||||
|
||||
function getPackVerticesData() {
|
||||
const {vertices} = pack;
|
||||
const verticesNumber = vertices.p.length;
|
||||
const verticesArray = new Array(verticesNumber);
|
||||
for (let i = 0; i < verticesNumber; i++) {
|
||||
verticesArray[i] = {
|
||||
p: vertices.p[i],
|
||||
v: vertices.v[i],
|
||||
c: vertices.c[i]
|
||||
};
|
||||
}
|
||||
return verticesArray;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -518,4 +518,21 @@ Jack Dawson
|
|||
Queso y Libertad
|
||||
RadioJay21H
|
||||
NEO
|
||||
Crecs`;
|
||||
Crecs
|
||||
A AASD
|
||||
Mikhail Ushakov
|
||||
NoFun
|
||||
AmbiguousCake
|
||||
Madeline Naiman
|
||||
2FingerzDown
|
||||
Josiah Lulf
|
||||
Vector Tragedy
|
||||
yann
|
||||
Blarghle Hargle
|
||||
Jelke Boonstra
|
||||
afistupmabum
|
||||
Rob Frantz
|
||||
Driver
|
||||
Tr4v3l3r
|
||||
Cooper Cantrell
|
||||
Maximilien Bouillot`;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ async function saveSVG() {
|
|||
link.click();
|
||||
|
||||
tip(
|
||||
`${link.download} is saved. Open "Downloads" screen (crtl + J) to check. You can set image scale in options`,
|
||||
`${link.download} is saved. Open "Downloads" screen (ctrl + J) to check. You can set image scale in options`,
|
||||
true,
|
||||
"success",
|
||||
5000
|
||||
|
|
@ -237,9 +237,23 @@ async function getMapURL(type, options = {}) {
|
|||
}
|
||||
|
||||
// replace ocean pattern href to base64
|
||||
if (location.hostname && cloneEl.getElementById("oceanicPattern")) {
|
||||
if (location.hostname) {
|
||||
const el = cloneEl.getElementById("oceanicPattern");
|
||||
const url = el.getAttribute("href");
|
||||
const url = el?.getAttribute("href");
|
||||
if (url) {
|
||||
await new Promise(resolve => {
|
||||
getBase64(url, base64 => {
|
||||
el.setAttribute("href", base64);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// replace texture href to base64
|
||||
if (location.hostname) {
|
||||
const el = cloneEl.getElementById("textureImage");
|
||||
const url = el?.getAttribute("href");
|
||||
if (url) {
|
||||
await new Promise(resolve => {
|
||||
getBase64(url, base64 => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
"use strict";
|
||||
// Functions to load and parse .map files
|
||||
|
||||
// Functions to load and parse .map/.gz files
|
||||
async function quickLoad() {
|
||||
const blob = await ldb.get("lastMap");
|
||||
if (blob) loadMapPrompt(blob);
|
||||
|
|
@ -112,11 +111,11 @@ function uploadMap(file, callback) {
|
|||
const currentVersion = parseFloat(version);
|
||||
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = function (fileLoadedEvent) {
|
||||
fileReader.onloadend = async function (fileLoadedEvent) {
|
||||
if (callback) callback();
|
||||
document.getElementById("coas").innerHTML = ""; // remove auto-generated emblems
|
||||
const result = fileLoadedEvent.target.result;
|
||||
const [mapData, mapVersion] = parseLoadedResult(result);
|
||||
const [mapData, mapVersion] = await parseLoadedResult(result);
|
||||
|
||||
const isInvalid = !mapData || isNaN(mapVersion) || mapData.length < 26 || !mapData[5];
|
||||
const isUpdated = mapVersion === currentVersion;
|
||||
|
|
@ -131,18 +130,40 @@ function uploadMap(file, callback) {
|
|||
if (isOutdated) return showUploadMessage("outdated", mapData, mapVersion);
|
||||
};
|
||||
|
||||
fileReader.readAsText(file, "UTF-8");
|
||||
fileReader.readAsArrayBuffer(file);
|
||||
}
|
||||
|
||||
function parseLoadedResult(result) {
|
||||
async function uncompress(compressedData) {
|
||||
try {
|
||||
const uncompressedStream = new Blob([compressedData]).stream().pipeThrough(new DecompressionStream("gzip"));
|
||||
|
||||
let uncompressedData = [];
|
||||
for await (const chunk of uncompressedStream) {
|
||||
uncompressedData = uncompressedData.concat(Array.from(chunk));
|
||||
}
|
||||
|
||||
return new Uint8Array(uncompressedData);
|
||||
} catch (error) {
|
||||
ERROR && console.error(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function parseLoadedResult(result) {
|
||||
try {
|
||||
const resultAsString = new TextDecoder().decode(result);
|
||||
// data can be in FMG internal format or base64 encoded
|
||||
const isDelimited = result.substr(0, 10).includes("|");
|
||||
const decoded = isDelimited ? result : decodeURIComponent(atob(result));
|
||||
const isDelimited = resultAsString.substring(0, 10).includes("|");
|
||||
const decoded = isDelimited ? resultAsString : decodeURIComponent(atob(resultAsString));
|
||||
|
||||
const mapData = decoded.split("\r\n");
|
||||
const mapVersion = parseFloat(mapData[0].split("|")[0] || mapData[0]);
|
||||
return [mapData, mapVersion];
|
||||
} catch (error) {
|
||||
// map file can be compressed with gzip
|
||||
const uncompressedData = await uncompress(result);
|
||||
if (uncompressedData) return parseLoadedResult(uncompressedData);
|
||||
|
||||
ERROR && console.error(error);
|
||||
return [null, null];
|
||||
}
|
||||
|
|
@ -153,7 +174,7 @@ function showUploadMessage(type, mapData, mapVersion) {
|
|||
let message, title, canBeLoaded;
|
||||
|
||||
if (type === "invalid") {
|
||||
message = `The file does not look like a valid <i>.map</i> file.<br>Please check the data format`;
|
||||
message = `The file does not look like a valid save file.<br>Please check the data format`;
|
||||
title = "Invalid file";
|
||||
canBeLoaded = false;
|
||||
} else if (type === "ancient") {
|
||||
|
|
@ -165,7 +186,7 @@ function showUploadMessage(type, mapData, mapVersion) {
|
|||
title = "Newer file";
|
||||
canBeLoaded = false;
|
||||
} else if (type === "outdated") {
|
||||
message = `The map version (${mapVersion}) does not match the Generator version (${version}).<br>Click OK to get map <b>auto-updated</b>.<br>In case of issues please keep using an ${archive} of the Generator`;
|
||||
message = `The map version (${mapVersion}) does not match the Generator version (${version}).<br>That is fine, click OK to the get map <b style="color: #005000">auto-updated</b>.<br>In case of issues please keep using an ${archive} of the Generator`;
|
||||
title = "Outdated file";
|
||||
canBeLoaded = true;
|
||||
}
|
||||
|
|
@ -218,10 +239,13 @@ async function parseLoadedData(data) {
|
|||
if (settings[13]) urbanization = urbanizationInput.value = urbanizationOutput.value = settings[13];
|
||||
if (settings[14]) mapSizeInput.value = mapSizeOutput.value = minmax(settings[14], 1, 100);
|
||||
if (settings[15]) latitudeInput.value = latitudeOutput.value = minmax(settings[15], 0, 100);
|
||||
if (settings[16]) temperatureEquatorInput.value = temperatureEquatorOutput.value = settings[16];
|
||||
if (settings[17]) temperaturePoleInput.value = temperaturePoleOutput.value = settings[17];
|
||||
if (settings[18]) precInput.value = precOutput.value = settings[18];
|
||||
|
||||
if (settings[19]) options = JSON.parse(settings[19]);
|
||||
// setting 16 and 17 (temperature) are part of options now, kept as "" in newer versions for compatibility
|
||||
if (settings[16]) options.temperatureEquator = +settings[16];
|
||||
if (settings[17]) options.temperatureNorthPole = options.temperatureSouthPole = +settings[17];
|
||||
|
||||
if (settings[20]) mapName.value = settings[20];
|
||||
if (settings[21]) hideLabels.checked = +settings[21];
|
||||
if (settings[22]) stylePreset.value = settings[22];
|
||||
|
|
@ -231,6 +255,9 @@ async function parseLoadedData(data) {
|
|||
|
||||
void (function applyOptionsToUI() {
|
||||
stateLabelsModeInput.value = options.stateLabelsMode;
|
||||
yearInput.value = options.year;
|
||||
eraInput.value = options.era;
|
||||
shapeRendering.value = viewbox.attr("shape-rendering") || "geometricPrecision";
|
||||
})();
|
||||
|
||||
void (function parseConfiguration() {
|
||||
|
|
@ -251,7 +278,7 @@ async function parseLoadedData(data) {
|
|||
}
|
||||
|
||||
const biomes = data[3].split("|");
|
||||
biomesData = applyDefaultBiomesSystem();
|
||||
biomesData = Biomes.getDefault();
|
||||
biomesData.color = biomes[0].split(",");
|
||||
biomesData.habitability = biomes[1].split(",").map(h => +h);
|
||||
biomesData.name = biomes[2].split(",");
|
||||
|
|
@ -404,7 +431,7 @@ async function parseLoadedData(data) {
|
|||
if (hasChildren(statesBody)) turnOn("toggleStates");
|
||||
if (hasChildren(provs)) turnOn("toggleProvinces");
|
||||
if (hasChildren(zones) && notHidden(zones)) turnOn("toggleZones");
|
||||
if (notHidden(borders) && hasChild(compass, "use")) turnOn("toggleBorders");
|
||||
if (notHidden(borders) && hasChild(borders, "path")) turnOn("toggleBorders");
|
||||
if (notHidden(routes) && hasChild(routes, "path")) turnOn("toggleRoutes");
|
||||
if (hasChildren(temperature)) turnOn("toggleTemp");
|
||||
if (hasChild(population, "line")) turnOn("togglePopulation");
|
||||
|
|
@ -431,7 +458,7 @@ async function parseLoadedData(data) {
|
|||
{
|
||||
// dynamically import and run auto-udpdate script
|
||||
const versionNumber = parseFloat(params[0]);
|
||||
const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.87.08");
|
||||
const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.93.00");
|
||||
resolveVersionConflicts(versionNumber);
|
||||
}
|
||||
|
||||
|
|
@ -588,11 +615,6 @@ async function parseLoadedData(data) {
|
|||
if (rulers && layerIsOn("toggleRulers")) rulers.draw();
|
||||
if (layerIsOn("toggleGrid")) drawGrid();
|
||||
|
||||
// set options
|
||||
yearInput.value = options.year;
|
||||
eraInput.value = options.era;
|
||||
shapeRendering.value = viewbox.attr("shape-rendering") || "geometricPrecision";
|
||||
|
||||
if (window.restoreDefaultEvents) restoreDefaultEvents();
|
||||
focusOn(); // based on searchParams focus on point, cell or burg
|
||||
invokeActiveZooming();
|
||||
|
|
|
|||
|
|
@ -1,8 +1,43 @@
|
|||
"use strict";
|
||||
// functions to save project as .map file
|
||||
|
||||
// prepare map data for saving
|
||||
function getMapData() {
|
||||
// functions to save the project to a file
|
||||
async function saveMap(method) {
|
||||
if (customization) return tip("Map cannot be saved in EDIT mode, please complete the edit and retry", false, "error");
|
||||
closeDialogs("#alert");
|
||||
|
||||
try {
|
||||
const mapData = prepareMapData();
|
||||
const filename = getFileName() + ".map";
|
||||
|
||||
saveToStorage(mapData, method === "storage"); // any method saves to indexedDB
|
||||
if (method === "machine") saveToMachine(mapData, filename);
|
||||
if (method === "dropbox") saveToDropbox(mapData, filename);
|
||||
} catch (error) {
|
||||
ERROR && console.error(error);
|
||||
alertMessage.innerHTML = /* html */ `An error is occured on map saving. If the issue persists, please copy the message below and report it on ${link(
|
||||
"https://github.com/Azgaar/Fantasy-Map-Generator/issues",
|
||||
"GitHub"
|
||||
)}. <p id="errorBox">${parseError(error)}</p>`;
|
||||
|
||||
$("#alert").dialog({
|
||||
resizable: false,
|
||||
title: "Saving error",
|
||||
width: "28em",
|
||||
buttons: {
|
||||
Retry: function () {
|
||||
$(this).dialog("close");
|
||||
saveMap(method);
|
||||
},
|
||||
Close: function () {
|
||||
$(this).dialog("close");
|
||||
}
|
||||
},
|
||||
position: {my: "center", at: "center", of: "svg"}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function prepareMapData() {
|
||||
const date = new Date();
|
||||
const dateString = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate();
|
||||
const license = "File can be loaded in azgaar.github.io/Fantasy-Map-Generator";
|
||||
|
|
@ -24,8 +59,8 @@ function getMapData() {
|
|||
urbanization,
|
||||
mapSizeOutput.value,
|
||||
latitudeOutput.value,
|
||||
temperatureEquatorOutput.value,
|
||||
temperaturePoleOutput.value,
|
||||
"", // previously used for temperatureEquatorOutput.value
|
||||
"", // previously used for tempNorthOutput.value
|
||||
precOutput.value,
|
||||
JSON.stringify(options),
|
||||
mapName.value,
|
||||
|
|
@ -117,36 +152,30 @@ function getMapData() {
|
|||
return mapData;
|
||||
}
|
||||
|
||||
// Download .map file
|
||||
function dowloadMap() {
|
||||
if (customization)
|
||||
return tip("Map cannot be saved when edit mode is active, please exit the mode and retry", false, "error");
|
||||
closeDialogs("#alert");
|
||||
// save map file to indexedDB
|
||||
async function saveToStorage(mapData, showTip = false) {
|
||||
const blob = new Blob([mapData], {type: "text/plain"});
|
||||
await ldb.set("lastMap", blob);
|
||||
showTip && tip("Map is saved to the browser storage", false, "success");
|
||||
}
|
||||
|
||||
const mapData = getMapData();
|
||||
// download map file
|
||||
function saveToMachine(mapData, filename) {
|
||||
const blob = new Blob([mapData], {type: "text/plain"});
|
||||
const URL = window.URL.createObjectURL(blob);
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.download = getFileName() + ".map";
|
||||
link.download = filename;
|
||||
link.href = URL;
|
||||
link.click();
|
||||
tip(`${link.download} is saved. Open "Downloads" screen (CTRL + J) to check`, true, "success", 7000);
|
||||
|
||||
tip('Map is saved to the "Downloads" folder (CTRL + J to open)', true, "success", 8000);
|
||||
window.URL.revokeObjectURL(URL);
|
||||
}
|
||||
|
||||
async function saveToDropbox() {
|
||||
if (customization)
|
||||
return tip("Map cannot be saved when edit mode is active, please exit the mode and retry", false, "error");
|
||||
closeDialogs("#alert");
|
||||
const mapData = getMapData();
|
||||
const filename = getFileName() + ".map";
|
||||
try {
|
||||
await Cloud.providers.dropbox.save(filename, mapData);
|
||||
tip("Map is saved to your Dropbox", true, "success", 8000);
|
||||
} catch (msg) {
|
||||
ERROR && console.error(msg);
|
||||
tip("Cannot save .map to your Dropbox", true, "error", 8000);
|
||||
}
|
||||
async function saveToDropbox(mapData, filename) {
|
||||
await Cloud.providers.dropbox.save(filename, mapData);
|
||||
tip("Map is saved to your Dropbox", true, "success", 8000);
|
||||
}
|
||||
|
||||
async function initiateAutosave() {
|
||||
|
|
@ -161,38 +190,44 @@ async function initiateAutosave() {
|
|||
if (diffInMinutes < timeoutMinutes) return;
|
||||
if (customization) return tip("Autosave: map cannot be saved in edit mode", false, "warning", 2000);
|
||||
|
||||
tip("Autosave: saving map...", false, "warning", 3000);
|
||||
const mapData = getMapData();
|
||||
const blob = new Blob([mapData], {type: "text/plain"});
|
||||
await ldb.set("lastMap", blob);
|
||||
console.log("Autosaved at", new Date().toLocaleTimeString());
|
||||
lastSavedAt = Date.now();
|
||||
try {
|
||||
tip("Autosave: saving map...", false, "warning", 3000);
|
||||
const mapData = prepareMapData();
|
||||
await saveToStorage(mapData);
|
||||
tip("Autosave: map is saved", false, "success", 2000);
|
||||
|
||||
lastSavedAt = Date.now();
|
||||
} catch (error) {
|
||||
ERROR && console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
setInterval(autosave, MINUTE / 2);
|
||||
}
|
||||
|
||||
async function quickSave() {
|
||||
if (customization)
|
||||
return tip("Map cannot be saved when edit mode is active, please exit the mode first", false, "error");
|
||||
// TODO: unused code
|
||||
async function compressData(uncompressedData) {
|
||||
const compressedStream = new Blob([uncompressedData]).stream().pipeThrough(new CompressionStream("gzip"));
|
||||
|
||||
const mapData = getMapData();
|
||||
const blob = new Blob([mapData], {type: "text/plain"});
|
||||
await ldb.set("lastMap", blob); // auto-save map
|
||||
tip("Map is saved to browser memory. Please also save as .map file to secure progress", true, "success", 2000);
|
||||
let compressedData = [];
|
||||
for await (const chunk of compressedStream) {
|
||||
compressedData = compressedData.concat(Array.from(chunk));
|
||||
}
|
||||
|
||||
return new Uint8Array(compressedData);
|
||||
}
|
||||
|
||||
const saveReminder = function () {
|
||||
if (localStorage.getItem("noReminder")) return;
|
||||
const message = [
|
||||
"Please don't forget to save your work as a .map file",
|
||||
"Please remember to save work as a .map file",
|
||||
"Saving in .map format will ensure your data won't be lost in case of issues",
|
||||
"Please don't forget to save the project to desktop from time to time",
|
||||
"Please remember to save the map to your desktop",
|
||||
"Saving will ensure your data won't be lost in case of issues",
|
||||
"Safety is number one priority. Please save the map",
|
||||
"Don't forget to save your map on a regular basis!",
|
||||
"Just a gentle reminder for you to save the map",
|
||||
"Please don't forget to save your progress (saving as .map is the best option)",
|
||||
"Don't want to be reminded about need to save? Press CTRL+Q"
|
||||
"Please don't forget to save your progress (saving to desktop is the best option)",
|
||||
"Don't want to get reminded about need to save? Press CTRL+Q"
|
||||
];
|
||||
const interval = 15 * 60 * 1000; // remind every 15 minutes
|
||||
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ window.Markers = (function () {
|
|||
{type: "rifts", icon: "🎆", min: 5, each: 3000, multiplier: +isFantasy, list: listRifts, add: addRift},
|
||||
{type: "disturbed-burials", icon: "💀", min: 20, each: 3000, multiplier: +isFantasy, list: listDisturbedBurial, add: addDisturbedBurial},
|
||||
{type: "necropolises", icon: "🪦", min: 20, each: 1000, multiplier: 1, list: listNecropolis, add: addNecropolis},
|
||||
{type: "encounters", icon: "🧙", min: 10, each: 600, multiplier: 1, list: listEncounters, add: addEncounter},
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -603,7 +604,7 @@ window.Markers = (function () {
|
|||
function addDungeon(id, cell) {
|
||||
const dungeonSeed = `${seed}${cell}`;
|
||||
const name = "Dungeon";
|
||||
const legend = `<div>Undiscovered dungeon. See <a href="https://watabou.github.io/one-page-dungeon/?seed=${dungeonSeed}" target="_blank">One page dungeon</a></div><iframe src="https://watabou.github.io/one-page-dungeon/?seed=${dungeonSeed}" sandbox="allow-scripts allow-same-origin"></iframe>`;
|
||||
const legend = `<div>Undiscovered dungeon. See <a href="https://watabou.github.io/one-page-dungeon/?seed=${dungeonSeed}" target="_blank">One page dungeon</a></div><iframe style="pointer-events: none;" src="https://watabou.github.io/one-page-dungeon/?seed=${dungeonSeed}" sandbox="allow-scripts allow-same-origin"></iframe>`;
|
||||
notes.push({id, name, legend});
|
||||
}
|
||||
|
||||
|
|
@ -849,18 +850,16 @@ window.Markers = (function () {
|
|||
const culture = cells.culture[cell];
|
||||
const biome = cells.biome[cell];
|
||||
const height = cells.p[cell];
|
||||
const locality =
|
||||
height >= 70
|
||||
? "highlander"
|
||||
: [1, 2].includes(biome)
|
||||
? "desert"
|
||||
: [3, 4].includes(biome)
|
||||
? "mounted"
|
||||
: [5, 6, 7, 8, 9].includes(biome)
|
||||
? "forest"
|
||||
: biome === 12
|
||||
? "swamp"
|
||||
: "angry";
|
||||
|
||||
const locality = ((height, biome) => {
|
||||
if (height >= 70) return "highlander";
|
||||
if ([1, 2].includes(biome)) return "desert";
|
||||
if ([3, 4].includes(biome)) return "mounted";
|
||||
if ([5, 6, 7, 8, 9].includes(biome)) return "forest";
|
||||
if (biome === 12) return "swamp";
|
||||
return "angry";
|
||||
})(height, biome);
|
||||
|
||||
const name = `${Names.getCulture(culture)} ${ra(animals)}`;
|
||||
const legend = `A gang of ${locality} ${rw(types)}.`;
|
||||
notes.push({id, name, legend});
|
||||
|
|
@ -1270,5 +1269,16 @@ window.Markers = (function () {
|
|||
notes.push({id, name, legend});
|
||||
}
|
||||
|
||||
function listEncounters({cells}) {
|
||||
return cells.i.filter(i => !occupied[i] && cells.h[i] >= 20 && cells.pop[i] > 1);
|
||||
}
|
||||
|
||||
function addEncounter(id, cell) {
|
||||
const name = "Random encounter";
|
||||
const encounterSeed = cell; // use just cell Id to not overwhelm the Vercel KV database
|
||||
const legend = `<div>You have encountered a character.</div><iframe src="https://deorum.vercel.app/encounter/${encounterSeed}" width="375" height="600" sandbox="allow-scripts allow-same-origin allow-popups"></iframe>`;
|
||||
notes.push({id, name, legend});
|
||||
}
|
||||
|
||||
return {add, generate, regenerate, getConfig, setConfig, deleteMarker};
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -211,7 +211,7 @@ window.Names = (function () {
|
|||
// Finnic
|
||||
else if (base === 15 && rnd < 0.4 && l < 6) suffix = "orszag";
|
||||
// Hungarian
|
||||
else if (base === 16) suffix = rnd < 0.6 ? "stan" : "ya";
|
||||
else if (base === 16) suffix = rnd < 0.6 ? "yurt" : "eli";
|
||||
// Turkish
|
||||
else if (base === 10) suffix = "guk";
|
||||
// Korean
|
||||
|
|
@ -279,7 +279,7 @@ window.Names = (function () {
|
|||
{name: "Portuguese", i: 13, min: 5, max: 11, d: "", m: .1, b: "Abrigada,Afonsoeiro,Agueda,Aguilada,Alagoas,Alagoinhas,Albufeira,Alcanhoes,Alcobaca,Alcoutim,Aldoar,Alenquer,Alfeizerao,Algarve,Almada,Almagreira,Almeirim,Alpalhao,Alpedrinha,Alvorada,Amieira,Anapolis,Apelacao,Aranhas,Arganil,Armacao,Assenceira,Aveiro,Avelar,Balsas,Barcarena,Barreiras,Barretos,Batalha,Beira,Benavente,Betim,Braga,Braganca,Brasilia,Brejo,Cabeceiras,Cabedelo,Cachoeiras,Cadafais,Calhandriz,Calheta,Caminha,Campinas,Canidelo,Canoas,Capinha,Carmoes,Cartaxo,Carvalhal,Carvoeiro,Cascavel,Castanhal,Caxias,Chapadinha,Chaves,Cocais,Coentral,Coimbra,Comporta,Conde,Coqueirinho,Coruche,Damaia,Dourados,Enxames,Ericeira,Ervidel,Escalhao,Esmoriz,Espinhal,Estela,Estoril,Eunapolis,Evora,Famalicao,Fanhoes,Faro,Fatima,Felgueiras,Ferreira,Figueira,Flecheiras,Florianopolis,Fornalhas,Fortaleza,Freiria,Freixeira,Fronteira,Fundao,Gracas,Gradil,Grainho,Gralheira,Guimaraes,Horta,Ilhavo,Ilheus,Lages,Lagos,Laranjeiras,Lavacolhos,Leiria,Limoeiro,Linhares,Lisboa,Lomba,Lorvao,Lourical,Lourinha,Luziania,Macedo,Machava,Malveira,Marinhais,Maxial,Mealhada,Milharado,Mira,Mirandela,Mogadouro,Montalegre,Mourao,Nespereira,Nilopolis,Obidos,Odemira,Odivelas,Oeiras,Oleiros,Olhalvo,Olinda,Olival,Oliveira,Oliveirinha,Palheiros,Palmeira,Palmital,Pampilhosa,Pantanal,Paradinha,Parelheiros,Pedrosinho,Pegoes,Penafiel,Peniche,Pinhao,Pinheiro,Pombal,Pontal,Pontinha,Portel,Portimao,Quarteira,Queluz,Ramalhal,Reboleira,Recife,Redinha,Ribadouro,Ribeira,Ribeirao,Rosais,Sabugal,Sacavem,Sagres,Sandim,Sangalhos,Santarem,Santos,Sarilhos,Seixas,Seixezelo,Seixo,Silvares,Silveira,Sinhaem,Sintra,Sobral,Sobralinho,Tabuaco,Tabuleiro,Taveiro,Teixoso,Telhado,Telheiro,Tomar,Torreira,Trancoso,Troviscal,Vagos,Varzea,Velas,Viamao,Viana,Vidigal,Vidigueira,Vidual,Vilamar,Vimeiro,Vinhais,Vitoria"},
|
||||
{name: "Nahuatl", i: 14, min: 6, max: 13, d: "l", m: 0, b: "Acapulco,Acatepec,Acatlan,Acaxochitlan,Acolman,Actopan,Acuamanala,Ahuacatlan,Almoloya,Amacuzac,Amanalco,Amaxac,Apaxco,Apetatitlan,Apizaco,Atenco,Atizapan,Atlacomulco,Atlapexco,Atotonilco,Axapusco,Axochiapan,Axocomanitla,Axutla,Azcapotzalco,Aztahuacan,Calimaya,Calnali,Calpulalpan,Camotlan,Capulhuac,Chalco,Chapulhuacan,Chapultepec,Chiapan,Chiautempan,Chiconautla,Chihuahua,Chilcuautla,Chimalhuacan,Cholollan,Cihuatlan,Coahuila,Coatepec,Coatetelco,Coatlan,Coatlinchan,Coatzacoalcos,Cocotitlan,Cohetzala,Colima,Colotlan,Coyoacan,Coyohuacan,Cuapiaxtla,Cuauhnahuac,Cuauhtemoc,Cuauhtitlan,Cuautepec,Cuautla,Cuaxomulco,Culhuacan,Ecatepec,Eloxochitlan,Epatlan,Epazoyucan,Huamantla,Huascazaloya,Huatlatlauca,Huautla,Huehuetlan,Huehuetoca,Huexotla,Hueyapan,Hueyotlipan,Hueypoxtla,Huichapan,Huimilpan,Huitzilac,Ixtapallocan,Iztacalco,Iztaccihuatl,Iztapalapa,Lolotla,Malinalco,Mapachtlan,Mazatepec,Mazatlan,Metepec,Metztitlan,Mexico,Miacatlan,Michoacan,Minatitlan,Mixcoac,Mixtla,Molcaxac,Nanacamilpa,Naucalpan,Naupan,Nextlalpan,Nezahualcoyotl,Nopalucan,Oaxaca,Ocotepec,Ocotitlan,Ocotlan,Ocoyoacac,Ocuilan,Ocuituco,Omitlan,Otompan,Otzoloapan,Pacula,Pahuatlan,Panotla,Papalotla,Patlachican,Piaztla,Popocatepetl,Sultepec,Tecamac,Tecolotlan,Tecozautla,Temamatla,Temascalapa,Temixco,Temoac,Temoaya,Tenayuca,Tenochtitlan,Teocuitlatlan,Teotihuacan,Teotlalco,Tepeacac,Tepeapulco,Tepehuacan,Tepetitlan,Tepeyanco,Tepotzotlan,Tepoztlan,Tetecala,Tetlatlahuca,Texcalyacac,Texcoco,Tezontepec,Tezoyuca,Timilpan,Tizapan,Tizayuca,Tlacopan,Tlacotenco,Tlahuac,Tlahuelilpan,Tlahuiltepa,Tlalmanalco,Tlalnepantla,Tlalpan,Tlanchinol,Tlatelolco,Tlaxcala,Tlaxcoapan,Tlayacapan,Tocatlan,Tolcayuca,Toluca,Tonanitla,Tonantzintla,Tonatico,Totolac,Totolapan,Tototlan,Tuchtlan,Tulantepec,Tultepec,Tzompantepec,Xalatlaco,Xaloztoc,Xaltocan,Xiloxoxtla,Xochiatipan,Xochicoatlan,Xochimilco,Xochitepec,Xolotlan,Xonacatlan,Yahualica,Yautepec,Yecapixtla,Yehaultepec,Zacatecas,Zacazonapan,Zacoalco,Zacualpan,Zacualtipan,Zapotlan,Zimapan,Zinacantepec,Zoyaltepec,Zumpahuacan"},
|
||||
{name: "Hungarian", i: 15, min: 6, max: 13, d: "", m: 0.1, b: "Aba,Abadszalok,Adony,Ajak,Albertirsa,Alsozsolca,Aszod,Babolna,Bacsalmas,Baktaloranthaza,Balassagyarmat,Balatonalmadi,Balatonboglar,Balkany,Balmazujvaros,Barcs,Bataszek,Batonyterenye,Battonya,Bekes,Berettyoujfalu,Berhida,Biatorbagy,Bicske,Biharkeresztes,Bodajk,Boly,Bonyhad,Budakalasz,Budakeszi,Celldomolk,Csakvar,Csenger,Csongrad,Csorna,Csorvas,Csurgo,Dabas,Demecser,Derecske,Devavanya,Devecser,Dombovar,Dombrad,Dunafoldvar,Dunaharaszti,Dunavarsany,Dunavecse,Edeleny,Elek,Emod,Encs,Enying,Ercsi,Fegyvernek,Fehergyarmat,Felsozsolca,Fertoszentmiklos,Fonyod,Fot,Fuzesabony,Fuzesgyarmat,Gardony,God,Gyal,Gyomaendrod,Gyomro,Hajdudorog,Hajduhadhaz,Hajdusamson,Hajduszoboszlo,Halasztelek,Harkany,Hatvan,Heves,Heviz,Ibrany,Isaszeg,Izsak,Janoshalma,Janossomorja,Jaszapati,Jaszarokszallas,Jaszfenyszaru,Jaszkiser,Kaba,Kalocsa,Kapuvar,Karcag,Kecel,Kemecse,Kenderes,Kerekegyhaza,Keszthely,Kisber,Kiskunmajsa,Kistarcsa,Kistelek,Kisujszallas,Kisvarda,Komadi,Komarom,Komlo,Kormend,Korosladany,Koszeg,Kozarmisleny,Kunhegyes,Kunszentmarton,Kunszentmiklos,Labatlan,Lajosmizse,Lenti,Letavertes,Letenye,Lorinci,Maglod,Mako,Mandok,Marcali,Martonvasar,Mateszalka,Melykut,Mezobereny,Mezocsat,Mezohegyes,Mezokeresztes,Mezokovesd,Mezotur,Mindszent,Mohacs,Monor,Mor,Morahalom,Nadudvar,Nagyatad,Nagyecsed,Nagyhalasz,Nagykallo,Nagykoros,Nagymaros,Nyekladhaza,Nyergesujfalu,Nyirbator,Nyirmada,Nyirtelek,Ocsa,Orkeny,Oroszlany,Paks,Pannonhalma,Paszto,Pecel,Pecsvarad,Pilisvorosvar,Polgar,Polgardi,Pomaz,Puspokladany,Pusztaszabolcs,Putnok,Racalmas,Rackeve,Rakamaz,Rakoczifalva,Sajoszent,Sandorfalva,Sarbogard,Sarkad,Sarospatak,Sarvar,Satoraljaujhely,Siklos,Simontornya,Soltvadkert,Sumeg,Szabadszallas,Szarvas,Szazhalombatta,Szecseny,Szeghalom,Szentgotthard,Szentlorinc,Szerencs,Szigethalom,Szigetvar,Szikszo,Tab,Tamasi,Tapioszele,Tapolca,Teglas,Tet,Tiszafoldvar,Tiszafured,Tiszakecske,Tiszalok,Tiszaujvaros,Tiszavasvari,Tokaj,Tokol,Tompa,Torokbalint,Torokszentmiklos,Totkomlos,Tura,Turkeve,Ujkigyos,ujszasz,Vamospercs,Varpalota,Vasarosnameny,Vasvar,Vecses,Veresegyhaz,Verpelet,Veszto,Zahony,Zalaszentgrot,Zirc,Zsambek"},
|
||||
{name: "Turkish", i: 16, min: 4, max: 10, d: "", m: 0, b: "Adapazari,Adiyaman,Afshin,Afyon,Akchaabat,Akchakoca,Akdamadeni,Akhisar,Aksaray,Akshehir,Amasya,Anamur,Antakya,Ardeshen,Ari,Artvin,Aydin,Babaeski,Bafra,Balikesir,Bandirma,Bartin,Bashiskele,Belen,Bergama,Besni,Biga,Bilecik,Bingul,Bitlis,Bodrum,Bolu,Bostanichi,Boyabat,Bozuyuk,Bucak,Bulancak,Bulanik,Burdur,Burhaniye,Ceyhan,Chanakkale,Chankiri,Chayeli,Cherkezkuy,Cheshme,Chivril,Chorlu,Cizre,Dalaman,Darica,Denizli,Derik,Develi,Devrek,Didim,Dilovasi,Dinar,Diyadin,Diyarbakir,Doubayazit,Durtyol,Duzce,Duzichi,Edirne,Elbistan,Emirda,Erbaa,Ercish,Erdek,Ereli,Ergani,Erzin,Erzincan,Erzurum,Eskishehir,Fethiye,Gazipasha,Gebze,Gelibolu,Gerede,Geyve,Giresun,Gulbashi,Gulcuk,Gumushhane,Gurnen,Guroymak,Hakkari,Harbiye,Havza,Hayrabolu,Hilvan,Imamolu,Inegul,Iskenderun,Islahiye,Kadirli,Kahta,Kaman,Kapakli,Karabuk,Karacabey,Karakupru,Karaman,Karamursel,Kars,Kartepe,Kastamonu,Kemer,Keshan,Kilimli,Kirklareli,Kirshehir,Kiziltepe,Korkuteli,Kovancilar,Kozan,Kozluk,Kulu,Kumluca,Kurfez,Kurtalan,Kutahya,Luleburgaz,Malatya,Malazgirt,Malkara,Manavgat,Manisa,Marmaris,Mersin,Merzifon,Midyat,Milas,Mula,Muratli,Mush,Nevshehir,Nide,Nizip,Nusaybin,Oltu,Ordu,Orhangazi,Ortaca,Osmancik,Osmaniye,Patnos,Payas,Pazarcik,Reyhanli,Rize,Safranbolu,Salihli,Samanda,Sandikli,Saray,Sarikamish,Sarikaya,Serik,Seydishehir,sharkishla,shirnak,Siirt,Silifke,Silvan,Simav,Sinop,Sivas,Siverek,Sorgun,Sungurlu,Surke,Suruch,Susurluk,Tarsus,Tatvan,Tekirda,Terme,Tire,Tokat,Tosya,Trabzon,Tunceli,Turgutlu,Turhal,udemish,Unye,Ushak,Uzunkurpru,Van,Vezirkurpru,Viranshehir,Yahyali,Yenishehir,Yerkury,Yozgat,Zile,Zonguldak"},
|
||||
{name: "Turkish", i: 16, min: 4, max: 10, d: "", m: 0, b: "Yelkaya,Buyrukkaya,Erdemtepe,Alakesen,Baharbeyli,Bozbay,Karaoklu,Altunbey,Yalkale,Yalkut,Akardere,Altayburnu,Esentepe,Okbelen,Derinsu,Alaoba,Yamanbeyli,Aykor,Ekinova,Saztepe,Baharkale,Devrekdibi,Alpseki,Ormanseki,Erkale,Yalbelen,Aytay,Yamanyaka,Altaydelen,Esen,Yedieli,Alpkor,Demirkor,Yediyol,Erdemkaya,Yayburnu,Ganiler,Bayatyurt,Kopuzteke,Aytepe,Deniz,Ayan,Ayazdere,Tepe,Kayra,Ayyaka,Deren,Adatepe,Kalkaneli,Bozkale,Yedidelen,Kocayolu,Sazdere,Bozkesen,Oguzeli,Yayladibi,Uluyol,Altay,Ayvar,Alazyaka,Yaloba,Suyaka,Baltaberi,Poyrazdelen,Eymir,Yediyuva,Kurt,Yeltepe,Oktar,Kara Ok,Ekinberi,Er Yurdu,Eren,Erenler,Ser,Oguz,Asay,Bozokeli,Aykut,Ormanyol,Yazkaya,Kalkanova,Yazbeyli,Dokuz Teke,Bilge,Ertensuyu,Kopuzyuva,Buyrukkut,Akardiken,Aybaray,Aslanbeyli,Altun Kaynak,Atikobasi,Yayla Eli,Kor Tepe,Salureli,Kor Kaya,Aybarberi,Kemerev,Yanaray,Beydileli,Buyrukoba,Yolduman,Tengri Tepe,Dokuzsu,Uzunkor,Erdem Yurdu,Kemer,Korteke,Bozokev,Bozoba,Ormankale,Askale,Oguztoprak,Yolberi,Kumseki,Esenobasi,Turkbelen,Ayazseki,Cereneli,Taykut,Bayramdelen,Beydilyaka,Boztepe,Uluoba,Yelyaka,Ulgardiken,Esensu,Baykale,Cerenkor,Bozyol,Duranoba,Aladuman,Denizli,Bahar,Yarkesen,Dokuzer,Yamankaya,Kocatarla,Alayaka,Toprakeli,Sarptarla,Sarpkoy,Serkaynak,Adayaka,Ayazkaynak,Kopuz,Turk,Kart,Kum,Erten,Buyruk,Yel,Ada,Alazova,Ayvarduman,Buyrukok,Ayvartoprak,Uzuntepe,Binseki,Yedibey,Durankale,Alaztoprak,Sarp Ok,Yaparobasi,Yaytepe,Asberi,Kalkankor,Beydiltepe,Adaberi,Bilgeyolu,Ganiyurt,Alkanteke,Esenerler,Asbey,Erdemkale,Erenkaynak,Oguzkoyu,Ayazoba,Boynuztoprak,Okova,Yaloklu,Sivriberi,Yuladiken,Sazbey,Karakaynak,Kopuzkoyu,Buyrukay,Kocakaya,Tepeduman,Yanarseki,Atikyurt,Esenev,Akarbeyli,Yayteke,Devreksungur,Akseki,Baykut,Kalkandere,Ulgarova,Devrekev,Yulabey,Bayatev,Yazsu,Vuraleli,Sivribeyli,Alaova,Alpobasi,Yalyurt,Elmatoprak,Alazkaynak,Esenay,Ertenev,Salurkor,Ekinok,Yalbey,Yeldere,Ganibay,Altaykut,Baltaboy,Ereli,Ayvarsu,Uzunsaz,Bayeli,Erenyol,Kocabay,Derintay,Ayazyol,Aslanoba,Esenkaynak,Ekinlik,Alpyolu,Alayunt,Bozeski,Erkil,Duransuyu,Yulak,Kut,Dodurga,Kutlubey,Kutluyurt,Boynuz,Alayol,Aybar,Aslaneli,Kemerseki,Baltasuyu,Akarer,Ayvarburnu,Boynuzbeyli,Adasungur,Esenkor,Yamanoba,Toprakkor,Uzunyurt,Sungur,Bozok,Kemerli,Alaz,Demirci,Kartepe"},
|
||||
{name: "Berber", i: 17, min: 4, max: 10, d: "s", m: .2, b: "Abkhouch,Adrar,Aeraysh,Afrag,Agadir,Agelmam,Aghmat,Agrakal,Agulmam,Ahaggar,Ait Baha,Ajdir,Akka,Almou,Amegdul,Amizmiz,Amknas,Amlil,Amurakush,Anfa,Annaba,Aousja,Arbat,Arfud,Argoub,Arif,Asfi,Asfru,Ashawen,Assamer,Assif,Awlluz,Ayt Melel,Azaghar,Azila,Azilal,Azmour,Azro,Azrou,Beccar,Beja,Bennour,Benslimane,Berkane,Berrechid,Bizerte,Bjaed,Bouayach,Boudenib,Boufrah,Bouskoura,Boutferda,Darallouch,Dar Bouazza,Darchaabane,Dcheira,Demnat,Denden,Djebel,Djedeida,Drargua,Elhusima,Essaouira,Ezzahra,Fas,Fnideq,Ghezeze,Goubellat,Grisaffen,Guelmim,Guercif,Hammamet,Harrouda,Hdifa,Hoceima,Houara,Idhan,Idurar,Ifendassen,Ifoghas,Ifrane,Ighoud,Ikbir,Imilchil,Imzuren,Inezgane,Irherm,Izoughar,Jendouba,Kacem,Kelibia,Kenitra,Kerrando,Khalidia,Khemisset,Khenifra,Khouribga,Khourigba,Kidal,Korba,Korbous,Lahraouyine,Larache,Leyun,Lqliaa,Manouba,Martil,Mazagan,Mcherga,Mdiq,Megrine,Mellal,Melloul,Midelt,Misur,Mohammedia,Mornag,Mrirt,Nabeul,Nadhour,Nador,Nawaksut,Nefza,Ouarzazate,Ouazzane,Oued Zem,Oujda,Ouladteima,Qsentina,Rades,Rafraf,Safi,Sefrou,Sejnane,Settat,Sijilmassa,Skhirat,Slimane,Somaa,Sraghna,Susa,Tabarka,Tadrart,Taferka,Tafilalt,Tafrawt,Tafza,Tagbalut,Tagerdayt,Taghzut,Takelsa,Taliouine,Tanja,Tantan,Taourirt,Targuist,Taroudant,Tarudant,Tasfelalayt,Tassort,Tata,Tattiwin,Tawnat,Taza,Tazagurt,Tazerka,Tazizawt,Taznakht,Tebourba,Teboursouk,Temara,Testour,Tetouan,Tibeskert,Tifelt,Tijdit,Tinariwen,Tinduf,Tinja,Tittawan,Tiznit,Toubkal,Trables,Tubqal,Tunes,Ultasila,Urup,Wagguten,Wararni,Warzazat,Watlas,Wehran,Wejda,Xamida,Yedder,Youssoufia,Zaghouan,Zahret,Zemmour,Zriba"},
|
||||
{name: "Arabic", i: 18, min: 4, max: 9, d: "ae", m: .2, b: "Abha,Ajman,Alabar,Alarjam,Alashraf,Alawali,Albawadi,Albirk,Aldhabiyah,Alduwaid,Alfareeq,Algayed,Alhazim,Alhrateem,Alhudaydah,Alhuwaya,Aljahra,Aljubail,Alkhafah,Alkhalas,Alkhawaneej,Alkhen,Alkhobar,Alkhuznah,Allisafah,Almshaykh,Almurjan,Almuwayh,Almuzaylif,Alnaheem,Alnashifah,Alqah,Alqouz,Alqurayyat,Alradha,Alraqmiah,Alsadyah,Alsafa,Alshagab,Alshuqaiq,Alsilaa,Althafeer,Alwasqah,Amaq,Amran,Annaseem,Aqbiyah,Arafat,Arar,Ardah,Asfan,Ashayrah,Askar,Ayaar,Aziziyah,Baesh,Bahrah,Balhaf,Banizayd,Bidiyah,Bisha,Biyatah,Buqhayq,Burayda,Dafiyat,Damad,Dammam,Dariyah,Dhafar,Dhahran,Dhalkut,Dhurma,Dibab,Doha,Dukhan,Duwaibah,Enaker,Fadhla,Fahaheel,Fanateer,Farasan,Fardah,Fujairah,Ghalilah,Ghar,Ghizlan,Ghomgyah,Ghran,Hadiyah,Haffah,Hajanbah,Hajrah,Haqqaq,Haradh,Hasar,Hawiyah,Hebaa,Hefar,Hijal,Husnah,Huwailat,Huwaitah,Irqah,Isharah,Ithrah,Jamalah,Jarab,Jareef,Jazan,Jeddah,Jiblah,Jihanah,Jilah,Jizan,Joraibah,Juban,Jumeirah,Kamaran,Keyad,Khab,Khaiybar,Khasab,Khathirah,Khawarah,Khulais,Kumzar,Limah,Linah,Madrak,Mahab,Mahalah,Makhtar,Mashwar,Masirah,Masliyah,Mastabah,Mazhar,Medina,Meeqat,Mirbah,Mokhtara,Muharraq,Muladdah,Musaykah,Mushayrif,Musrah,Mussafah,Nafhan,Najran,Nakhab,Nizwa,Oman,Qadah,Qalhat,Qamrah,Qasam,Qosmah,Qurain,Quriyat,Qurwa,Radaa,Rafha,Rahlah,Rakamah,Rasheedah,Rasmadrakah,Risabah,Rustaq,Ryadh,Sabtaljarah,Sadah,Safinah,Saham,Saihat,Salalah,Salmiya,Shabwah,Shalim,Shaqra,Sharjah,Sharurah,Shatifiyah,Shidah,Shihar,Shoqra,Shuwaq,Sibah,Sihmah,Sinaw,Sirwah,Sohar,Suhailah,Sulaibiya,Sunbah,Tabuk,Taif,Taqah,Tarif,Tharban,Thuqbah,Thuwal,Tubarjal,Turaif,Turbah,Tuwaiq,Ubar,Umaljerem,Urayarah,Urwah,Wabrah,Warbah,Yabreen,Yadamah,Yafur,Yarim,Yemen,Yiyallah,Zabid,Zahwah,Zallaq,Zinjibar,Zulumah"},
|
||||
{name: "Inuit", i: 19, min: 5, max: 15, d: "alutsn", m: 0, b: "Aaluik,Aappilattoq,Aasiaat,Agissat,Agssaussat,Akuliarutsip,Akunnaaq,Alluitsup,Alluttoq,Amitsorsuaq,Ammassalik,Anarusuk,Anguniartarfik,Annertussoq,Annikitsoq,Apparsuit,Apusiaajik,Arsivik,Arsuk,Atammik,Ateqanaq,Atilissuaq,Attu,Augpalugtoq,Aukarnersuaq,Aumat,Auvilkikavsaup,Avadtlek,Avallersuaq,Bjornesk,Blabaerdalen,Blomsterdalen,Brattalhid,Bredebrae,Brededal,Claushavn,Edderfulegoer,Egger,Eqalugalinnguit,Eqalugarssuit,Eqaluit,Eqqua,Etah,Graah,Hakluyt,Haredalen,Hareoen,Hundeo,Igaliku,Igdlorssuit,Igdluluarssuk,Iginniafik,Ikamiut,Ikarissat,Ikateq,Ikermiut,Ikermoissuaq,Ikorfarssuit,Ilimanaq,Illorsuit,Illunnguit,Iluileq,Ilulissat,Imaarsivik,Imartunarssuk,Immikkoortukajik,Innaarsuit,Inneruulalik,Inussullissuaq,Iperaq,Ippik,Iqek,Isortok,Isungartussoq,Itileq,Itissaalik,Itivdleq,Ittit,Ittoqqortoormiit,Ivingmiut,Ivittuut,Kanajoorartuut,Kangaamiut,Kangeq,Kangerluk,Kangerlussuaq,Kanglinnguit,Kapisillit,Kekertamiut,Kiatak,Kiataussaq,Kigatak,Kinaussak,Kingittorsuaq,Kitak,Kitsissuarsuit,Kitsissut,Klenczner,Kook,Kraulshavn,Kujalleq,Kullorsuaq,Kulusuk,Kuurmiit,Kuusuaq,Laksedalen,Maniitsoq,Marrakajik,Mattaangassut,Mernoq,Mittivakkat,Moriusaq,Myggbukta,Naajaat,Nangissat,Nanuuseq,Nappassoq,Narsarmijt,Narsarsuaq,Narssaq,Nasiffik,Natsiarsiorfik,Naujanguit,Niaqornaarsuk,Niaqornat,Nordfjordspasset,Nugatsiaq,Nunarssit,Nunarsuaq,Nunataaq,Nunatakavsaup,Nutaarmiut,Nuugaatsiaq,Nuuk,Nuukullak,Olonkinbyen,Oodaaq,Oqaatsut,Oqaitsunguit,Oqonermiut,Paagussat,Paamiut,Paatuut,Palungataq,Pamialluk,Perserajoq,Pituffik,Puugutaa,Puulkuip,Qaanaq,Qaasuitsup,Qaersut,Qajartalik,Qallunaat,Qaneq,Qaqortok,Qasigiannguit,Qassimiut,Qeertartivaq,Qeqertaq,Qeqertasussuk,Qeqqata,Qernertoq,Qernertunnguit,Qianarreq,Qingagssat,Qoornuup,Qorlortorsuaq,Qullikorsuit,Qunnerit,Qutdleq,Ravnedalen,Ritenbenk,Rypedalen,Saarloq,Saatorsuaq,Saattut,Salliaruseq,Sammeqqat,Sammisoq,Sanningassoq,Saqqaq,Saqqarlersuaq,Saqqarliit,Sarfannguit,Sattiaatteq,Savissivik,Serfanguaq,Sermersooq,Sermiligaaq,Sermilik,Sermitsiaq,Simitakaja,Simiutaq,Singamaq,Siorapaluk,Sisimiut,Sisuarsuit,Sullorsuaq,Suunikajik,Sverdrup,Taartoq,Takiseeq,Tasirliaq,Tasiusak,Tiilerilaaq,Timilersua,Timmiarmiut,Tukingassoq,Tussaaq,Tuttulissuup,Tuujuk,Uiivaq,Uilortussoq,Ujuaakajiip,Ukkusissat,Upernavik,Uttorsiutit,Uumannaq,Uunartoq,Uvkusigssat,Ymer"},
|
||||
|
|
|
|||
|
|
@ -363,23 +363,20 @@ window.Religions = (function () {
|
|||
Shamanism: 4,
|
||||
Animism: 4,
|
||||
Polytheism: 4,
|
||||
Totemism: 2,
|
||||
Druidism: 1,
|
||||
"Ancestor Worship": 1,
|
||||
"Nature Worship": 1
|
||||
"Ancestor Worship": 2,
|
||||
"Nature Worship": 1,
|
||||
Totemism: 1
|
||||
},
|
||||
Organized: {
|
||||
Polytheism: 14,
|
||||
Monotheism: 12,
|
||||
Dualism: 6,
|
||||
Pantheism: 6,
|
||||
"Non-theism": 4,
|
||||
Henotheism: 1,
|
||||
Panentheism: 1
|
||||
"Non-theism": 4
|
||||
},
|
||||
Cult: {
|
||||
Cult: 2,
|
||||
"Dark Cult": 2,
|
||||
Cult: 5,
|
||||
"Dark Cult": 5,
|
||||
Sect: 1
|
||||
},
|
||||
Heresy: {
|
||||
|
|
@ -418,17 +415,20 @@ window.Religions = (function () {
|
|||
};
|
||||
|
||||
const types = {
|
||||
Shamanism: {Beliefs: 3, Shamanism: 2, Spirits: 1},
|
||||
Animism: {Spirits: 1, Beliefs: 1},
|
||||
"Ancestor worship": {Beliefs: 1, Forefathers: 2, Ancestors: 2},
|
||||
Shamanism: {Beliefs: 3, Shamanism: 2, Druidism: 1, Spirits: 1},
|
||||
Animism: {Spirits: 3, Beliefs: 1},
|
||||
Polytheism: {Deities: 3, Faith: 1, Gods: 1, Pantheon: 1},
|
||||
"Ancestor worship": {Beliefs: 1, Forefathers: 2, Ancestors: 2},
|
||||
"Nature Worship": {Beliefs: 3, Druids: 1},
|
||||
Totemism: {Beliefs: 2, Totems: 2, Idols: 1},
|
||||
|
||||
Monotheism: {Religion: 2, Church: 3, Faith: 1},
|
||||
Dualism: {Religion: 3, Faith: 1, Cult: 1},
|
||||
Monotheism: {Religion: 1, Church: 1},
|
||||
"Non-theism": {Beliefs: 3, Spirits: 1},
|
||||
|
||||
Cult: {Cult: 4, Sect: 4, Arcanum: 1, Coterie: 1, Order: 1, Worship: 1},
|
||||
"Dark Cult": {Cult: 2, Sect: 2, Blasphemy: 1, Circle: 1, Coven: 1, Idols: 1, Occultism: 1},
|
||||
Cult: {Cult: 4, Sect: 2, Arcanum: 1, Order: 1, Worship: 1},
|
||||
"Dark Cult": {Cult: 2, Blasphemy: 1, Circle: 1, Coven: 1, Idols: 1, Occultism: 1},
|
||||
Sect: {Sect: 3, Society: 1},
|
||||
|
||||
Heresy: {
|
||||
Heresy: 3,
|
||||
|
|
@ -893,9 +893,10 @@ window.Religions = (function () {
|
|||
const {cells, cultures, burgs, states} = pack;
|
||||
|
||||
const random = () => Names.getCulture(cells.culture[center], null, null, "", 0);
|
||||
const type = () => rw(types[form]);
|
||||
const supreme = () => deity.split(/[ ,]+/)[0];
|
||||
const culture = () => cultures[cells.culture[center]].name;
|
||||
const type = rw(types[form]);
|
||||
const supreme = deity.split(/[ ,]+/)[0];
|
||||
const culture = cultures[cells.culture[center]].name;
|
||||
|
||||
const place = adj => {
|
||||
const burgId = cells.burg[center];
|
||||
const stateId = cells.state[center];
|
||||
|
|
@ -906,18 +907,18 @@ window.Religions = (function () {
|
|||
};
|
||||
|
||||
const m = rw(namingMethods[variety]);
|
||||
if (m === "Random + type") return [random() + " " + type(), "global"];
|
||||
if (m === "Random + type") return [random() + " " + type, "global"];
|
||||
if (m === "Random + ism") return [trimVowels(random()) + "ism", "global"];
|
||||
if (m === "Supreme + ism" && deity) return [trimVowels(supreme()) + "ism", "global"];
|
||||
if (m === "Supreme + ism" && deity) return [trimVowels(supreme) + "ism", "global"];
|
||||
if (m === "Faith of + Supreme" && deity)
|
||||
return [ra(["Faith", "Way", "Path", "Word", "Witnesses"]) + " of " + supreme(), "global"];
|
||||
return [ra(["Faith", "Way", "Path", "Word", "Witnesses"]) + " of " + supreme, "global"];
|
||||
if (m === "Place + ism") return [place() + "ism", "state"];
|
||||
if (m === "Culture + ism") return [trimVowels(culture()) + "ism", "culture"];
|
||||
if (m === "Place + ian + type") return [place("adj") + " " + type(), "state"];
|
||||
if (m === "Culture + type") return [culture() + " " + type(), "culture"];
|
||||
if (m === "Burg + ian + type") return [`${place("adj")} ${type()}`, "global"];
|
||||
if (m === "Random + ian + type") return [`${getAdjective(random())} ${type()}`, "global"];
|
||||
if (m === "Type + of the + meaning") return [`${type()} of the ${generateMeaning()}`, "global"];
|
||||
if (m === "Culture + ism") return [trimVowels(culture) + "ism", "culture"];
|
||||
if (m === "Place + ian + type") return [place("adj") + " " + type, "state"];
|
||||
if (m === "Culture + type") return [culture + " " + type, "culture"];
|
||||
if (m === "Burg + ian + type") return [`${place("adj")} ${type}`, "global"];
|
||||
if (m === "Random + ian + type") return [`${getAdjective(random())} ${type}`, "global"];
|
||||
if (m === "Type + of the + meaning") return [`${type} of the ${generateMeaning()}`, "global"];
|
||||
return [trimVowels(random()) + "ism", "global"]; // else
|
||||
}
|
||||
|
||||
|
|
|
|||
280
modules/renderers/state-labels.js
Normal file
280
modules/renderers/state-labels.js
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
"use strict";
|
||||
|
||||
// list - an optional array of stateIds to regenerate
|
||||
function drawStateLabels(list) {
|
||||
console.time("drawStateLabels");
|
||||
|
||||
const {cells, states, features} = pack;
|
||||
const stateIds = cells.state;
|
||||
|
||||
// increase step to 15 or 30 to make it faster and more horyzontal
|
||||
// decrease step to 5 to improve accuracy
|
||||
const ANGLE_STEP = 9;
|
||||
const raycast = precalculateAngles(ANGLE_STEP);
|
||||
|
||||
const INITIAL_DISTANCE = 10;
|
||||
const DISTANCE_STEP = 15;
|
||||
const MAX_ITERATIONS = 100;
|
||||
|
||||
const labelPaths = getLabelPaths();
|
||||
drawLabelPath();
|
||||
|
||||
function getLabelPaths() {
|
||||
const labelPaths = [];
|
||||
|
||||
for (const state of states) {
|
||||
if (!state.i || state.removed || state.lock) continue;
|
||||
if (list && !list.includes(state.i)) continue;
|
||||
|
||||
const offset = getOffsetWidth(state.cells);
|
||||
const maxLakeSize = state.cells / 50;
|
||||
const [x0, y0] = state.pole;
|
||||
|
||||
const offsetPoints = new Map(
|
||||
(offset ? raycast : []).map(({angle, x: x1, y: y1}) => {
|
||||
const [x, y] = [x0 + offset * x1, y0 + offset * y1];
|
||||
return [angle, {x, y}];
|
||||
})
|
||||
);
|
||||
|
||||
const distances = raycast.map(({angle, x: dx, y: dy, modifier}) => {
|
||||
let distanceMin;
|
||||
const distance1 = getMaxDistance(state.i, {x: x0, y: y0}, dx, dy, maxLakeSize);
|
||||
|
||||
if (offset) {
|
||||
const point2 = offsetPoints.get(angle - 90 < 0 ? angle + 270 : angle - 90);
|
||||
const distance2 = getMaxDistance(state.i, point2, dx, dy, maxLakeSize);
|
||||
|
||||
const point3 = offsetPoints.get(angle + 90 >= 360 ? angle - 270 : angle + 90);
|
||||
const distance3 = getMaxDistance(state.i, point3, dx, dy, maxLakeSize);
|
||||
|
||||
distanceMin = Math.min(distance1, distance2, distance3);
|
||||
} else {
|
||||
distanceMin = distance1;
|
||||
}
|
||||
|
||||
const [x, y] = [x0 + distanceMin * dx, y0 + distanceMin * dy];
|
||||
return {angle, distance: distanceMin * modifier, x, y};
|
||||
});
|
||||
|
||||
const {
|
||||
angle,
|
||||
x: x1,
|
||||
y: y1
|
||||
} = distances.reduce(
|
||||
(acc, {angle, distance, x, y}) => {
|
||||
if (distance > acc.distance) return {angle, distance, x, y};
|
||||
return acc;
|
||||
},
|
||||
{angle: 0, distance: 0, x: 0, y: 0}
|
||||
);
|
||||
|
||||
const oppositeAngle = angle >= 180 ? angle - 180 : angle + 180;
|
||||
const {x: x2, y: y2} = distances.reduce(
|
||||
(acc, {angle, distance, x, y}) => {
|
||||
const angleDif = getAnglesDif(angle, oppositeAngle);
|
||||
const score = distance * getAngleModifier(angleDif);
|
||||
if (score > acc.score) return {angle, score, x, y};
|
||||
return acc;
|
||||
},
|
||||
{angle: 0, score: 0, x: 0, y: 0}
|
||||
);
|
||||
|
||||
const pathPoints = [[x1, y1], state.pole, [x2, y2]];
|
||||
if (x1 > x2) pathPoints.reverse();
|
||||
labelPaths.push([state.i, pathPoints]);
|
||||
}
|
||||
|
||||
return labelPaths;
|
||||
|
||||
function getMaxDistance(stateId, point, dx, dy, maxLakeSize) {
|
||||
let distance = INITIAL_DISTANCE;
|
||||
|
||||
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
||||
const [x, y] = [point.x + distance * dx, point.y + distance * dy];
|
||||
const cellId = findCell(x, y, DISTANCE_STEP);
|
||||
|
||||
// drawPoint([x, y], {color: cellId && isPassable(cellId) ? "blue" : "red", radius: 0.8});
|
||||
|
||||
if (!cellId || !isPassable(cellId)) break;
|
||||
distance += DISTANCE_STEP;
|
||||
}
|
||||
|
||||
return distance;
|
||||
|
||||
function isPassable(cellId) {
|
||||
const feature = features[cells.f[cellId]];
|
||||
if (feature.type === "lake") return feature.cells <= maxLakeSize;
|
||||
return stateIds[cellId] === stateId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawLabelPath() {
|
||||
const mode = options.stateLabelsMode || "auto";
|
||||
const lineGen = d3.line().curve(d3.curveBundle.beta(1));
|
||||
|
||||
const textGroup = d3.select("g#labels > g#states");
|
||||
const pathGroup = d3.select("defs > g#deftemp > g#textPaths");
|
||||
|
||||
const testLabel = textGroup.append("text").attr("x", 0).attr("y", 0).text("Example");
|
||||
const letterLength = testLabel.node().getComputedTextLength() / 7; // approximate length of 1 letter
|
||||
testLabel.remove();
|
||||
|
||||
for (const [stateId, pathPoints] of labelPaths) {
|
||||
const state = states[stateId];
|
||||
if (!state.i || state.removed) throw new Error("State must not be neutral or removed");
|
||||
if (pathPoints.length < 2) throw new Error("Label path must have at least 2 points");
|
||||
|
||||
textGroup.select("#stateLabel" + stateId).remove();
|
||||
pathGroup.select("#textPath_stateLabel" + stateId).remove();
|
||||
|
||||
const textPath = pathGroup
|
||||
.append("path")
|
||||
.attr("d", round(lineGen(pathPoints)))
|
||||
.attr("id", "textPath_stateLabel" + stateId);
|
||||
|
||||
const pathLength = textPath.node().getTotalLength() / letterLength; // path length in letters
|
||||
const [lines, ratio] = getLinesAndRatio(mode, state.name, state.fullName, pathLength);
|
||||
|
||||
// prolongate path if it's too short
|
||||
const longestLineLength = d3.max(lines.map(({length}) => length));
|
||||
if (pathLength && pathLength < longestLineLength) {
|
||||
const [x1, y1] = pathPoints.at(0);
|
||||
const [x2, y2] = pathPoints.at(-1);
|
||||
const [dx, dy] = [(x2 - x1) / 2, (y2 - y1) / 2];
|
||||
|
||||
const mod = longestLineLength / pathLength;
|
||||
pathPoints[0] = [x1 + dx - dx * mod, y1 + dy - dy * mod];
|
||||
pathPoints[pathPoints.length - 1] = [x2 - dx + dx * mod, y2 - dy + dy * mod];
|
||||
|
||||
textPath.attr("d", round(lineGen(pathPoints)));
|
||||
}
|
||||
|
||||
const textElement = textGroup
|
||||
.append("text")
|
||||
.attr("id", "stateLabel" + stateId)
|
||||
.append("textPath")
|
||||
.attr("startOffset", "50%")
|
||||
.attr("font-size", ratio + "%")
|
||||
.node();
|
||||
|
||||
const top = (lines.length - 1) / -2; // y offset
|
||||
const spans = lines.map((line, index) => `<tspan x="0" dy="${index ? 1 : top}em">${line}</tspan>`);
|
||||
textElement.insertAdjacentHTML("afterbegin", spans.join(""));
|
||||
|
||||
const {width, height} = textElement.getBBox();
|
||||
textElement.setAttribute("href", "#textPath_stateLabel" + stateId);
|
||||
|
||||
if (mode === "full" || lines.length === 1) continue;
|
||||
|
||||
// check if label fits state boundaries. If no, replace it with short name
|
||||
const [[x1, y1], [x2, y2]] = [pathPoints.at(0), pathPoints.at(-1)];
|
||||
const angleRad = Math.atan2(y2 - y1, x2 - x1);
|
||||
|
||||
const isInsideState = checkIfInsideState(textElement, angleRad, width / 2, height / 2, stateIds, stateId);
|
||||
if (isInsideState) continue;
|
||||
|
||||
// replace name to one-liner
|
||||
const text = pathLength > state.fullName.length * 1.8 ? state.fullName : state.name;
|
||||
textElement.innerHTML = `<tspan x="0">${text}</tspan>`;
|
||||
|
||||
const correctedRatio = minmax(rn((pathLength / text.length) * 50), 40, 130);
|
||||
textElement.setAttribute("font-size", correctedRatio + "%");
|
||||
}
|
||||
}
|
||||
|
||||
// point offset to reduce label overlap with state borders
|
||||
function getOffsetWidth(cellsNumber) {
|
||||
if (cellsNumber < 80) return 0;
|
||||
if (cellsNumber < 140) return 5;
|
||||
if (cellsNumber < 200) return 15;
|
||||
if (cellsNumber < 300) return 20;
|
||||
if (cellsNumber < 500) return 25;
|
||||
return 30;
|
||||
}
|
||||
|
||||
// difference between two angles in range [0, 180]
|
||||
function getAnglesDif(angle1, angle2) {
|
||||
return 180 - Math.abs(Math.abs(angle1 - angle2) - 180);
|
||||
}
|
||||
|
||||
// score multiplier based on angle difference betwee left and right sides
|
||||
function getAngleModifier(angleDif) {
|
||||
if (angleDif === 0) return 1;
|
||||
if (angleDif <= 15) return 0.95;
|
||||
if (angleDif <= 30) return 0.9;
|
||||
if (angleDif <= 45) return 0.6;
|
||||
if (angleDif <= 60) return 0.3;
|
||||
if (angleDif <= 90) return 0.1;
|
||||
return 0; // >90
|
||||
}
|
||||
|
||||
function precalculateAngles(step) {
|
||||
const angles = [];
|
||||
const RAD = Math.PI / 180;
|
||||
|
||||
for (let angle = 0; angle < 360; angle += step) {
|
||||
const x = Math.cos(angle * RAD);
|
||||
const y = Math.sin(angle * RAD);
|
||||
const angleDif = 90 - Math.abs((angle % 180) - 90);
|
||||
const modifier = 1 - angleDif / 120; // [0.25, 1]
|
||||
angles.push({angle, modifier, x, y});
|
||||
}
|
||||
|
||||
return angles;
|
||||
}
|
||||
|
||||
function getLinesAndRatio(mode, name, fullName, pathLength) {
|
||||
// short name
|
||||
if (mode === "short" || (mode === "auto" && pathLength <= name.length)) {
|
||||
const lines = splitInTwo(name);
|
||||
const longestLineLength = d3.max(lines.map(({length}) => length));
|
||||
const ratio = pathLength / longestLineLength;
|
||||
return [lines, minmax(rn(ratio * 60), 50, 150)];
|
||||
}
|
||||
|
||||
// full name: one line
|
||||
if (pathLength > fullName.length * 2) {
|
||||
const lines = [fullName];
|
||||
const ratio = pathLength / lines[0].length;
|
||||
return [lines, minmax(rn(ratio * 70), 70, 170)];
|
||||
}
|
||||
|
||||
// full name: two lines
|
||||
const lines = splitInTwo(fullName);
|
||||
const longestLineLength = d3.max(lines.map(({length}) => length));
|
||||
const ratio = pathLength / longestLineLength;
|
||||
return [lines, minmax(rn(ratio * 60), 70, 150)];
|
||||
}
|
||||
|
||||
// check whether multi-lined label is mostly inside the state. If no, replace it with short name label
|
||||
function checkIfInsideState(textElement, angleRad, halfwidth, halfheight, stateIds, stateId) {
|
||||
const bbox = textElement.getBBox();
|
||||
const [cx, cy] = [bbox.x + bbox.width / 2, bbox.y + bbox.height / 2];
|
||||
|
||||
const points = [
|
||||
[-halfwidth, -halfheight],
|
||||
[+halfwidth, -halfheight],
|
||||
[+halfwidth, halfheight],
|
||||
[-halfwidth, halfheight],
|
||||
[0, halfheight],
|
||||
[0, -halfheight]
|
||||
];
|
||||
|
||||
const sin = Math.sin(angleRad);
|
||||
const cos = Math.cos(angleRad);
|
||||
const rotatedPoints = points.map(([x, y]) => [cx + x * cos - y * sin, cy + x * sin + y * cos]);
|
||||
|
||||
let pointsInside = 0;
|
||||
for (const [x, y] of rotatedPoints) {
|
||||
const isInside = stateIds[findCell(x, y)] === stateId;
|
||||
if (isInside) pointsInside++;
|
||||
if (pointsInside > 4) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
console.timeEnd("drawStateLabels");
|
||||
}
|
||||
|
|
@ -215,7 +215,7 @@ window.Submap = (function () {
|
|||
// biome calculation based on (resampled) grid.cells.temp and prec
|
||||
// it's safe to recalculate.
|
||||
stage("Regenerating Biome.");
|
||||
defineBiomes();
|
||||
Biomes.define();
|
||||
// recalculate suitability and population
|
||||
// TODO: normalize according to the base-map
|
||||
rankCells();
|
||||
|
|
@ -276,7 +276,7 @@ window.Submap = (function () {
|
|||
|
||||
drawStates();
|
||||
drawBorders();
|
||||
BurgsAndStates.drawStateLabels();
|
||||
drawStateLabels();
|
||||
|
||||
Rivers.specify();
|
||||
Lakes.generateName();
|
||||
|
|
|
|||
167
modules/ui/3d.js
167
modules/ui/3d.js
|
|
@ -12,7 +12,11 @@ window.ThreeD = (function () {
|
|||
waterColor: "#466eab",
|
||||
extendedWater: 0,
|
||||
labels3d: 0,
|
||||
resolution: 2
|
||||
wireframe: 0,
|
||||
resolution: 2,
|
||||
resolutionScale: 2048,
|
||||
sunColor: "#cccccc",
|
||||
subdivide: 0
|
||||
};
|
||||
|
||||
// set variables
|
||||
|
|
@ -92,7 +96,11 @@ window.ThreeD = (function () {
|
|||
|
||||
const setScale = function (scale) {
|
||||
options.scale = scale;
|
||||
geometry.vertices.forEach((v, i) => (v.z = getMeshHeight(i)));
|
||||
let vertices = geometry.getAttribute("position");
|
||||
for (let i = 0; i < vertices.count; i++) {
|
||||
vertices.setZ(i, getMeshHeight(i));
|
||||
}
|
||||
geometry.setAttribute("position", vertices);
|
||||
geometry.verticesNeedUpdate = true;
|
||||
geometry.computeVertexNormals();
|
||||
geometry.verticesNeedUpdate = false;
|
||||
|
|
@ -100,6 +108,17 @@ window.ThreeD = (function () {
|
|||
redraw();
|
||||
};
|
||||
|
||||
const setSunColor = function (color) {
|
||||
options.sunColor = color;
|
||||
spotLight.color = new THREE.Color(color);
|
||||
render();
|
||||
};
|
||||
|
||||
const setResolutionScale = function (scale) {
|
||||
options.resolutionScale = scale;
|
||||
redraw();
|
||||
};
|
||||
|
||||
const setLightness = function (intensity) {
|
||||
options.lightness = intensity;
|
||||
ambientLight.intensity = intensity;
|
||||
|
|
@ -148,6 +167,16 @@ window.ThreeD = (function () {
|
|||
}
|
||||
};
|
||||
|
||||
const toggle3dSubdivision = function () {
|
||||
options.subdivide = !options.subdivide;
|
||||
redraw();
|
||||
};
|
||||
|
||||
const toggleWireframe = function () {
|
||||
options.wireframe = !options.wireframe;
|
||||
redraw();
|
||||
};
|
||||
|
||||
const setColors = function (sky, water) {
|
||||
options.skyColor = sky;
|
||||
scene.background = scene.fog.color = new THREE.Color(sky);
|
||||
|
|
@ -189,16 +218,20 @@ window.ThreeD = (function () {
|
|||
// light
|
||||
ambientLight = new THREE.AmbientLight(0xcccccc, options.lightness);
|
||||
scene.add(ambientLight);
|
||||
spotLight = new THREE.SpotLight(0xcccccc, 0.8, 2000, 0.8, 0, 0);
|
||||
spotLight = new THREE.SpotLight(options.sunColor, 0.8, 2000, 0.8, 0, 0);
|
||||
spotLight.position.set(options.sun.x, options.sun.y, options.sun.z);
|
||||
spotLight.castShadow = true;
|
||||
//maybe add a option for this. But changing the option will require to reinstance the spotLight.
|
||||
spotLight.shadow.mapSize.width = 2048;
|
||||
spotLight.shadow.mapSize.height = 2048;
|
||||
scene.add(spotLight);
|
||||
//scene.add(new THREE.SpotLightHelper(spotLight));
|
||||
|
||||
// Rendered
|
||||
// Renderer
|
||||
Renderer = new THREE.WebGLRenderer({canvas, antialias: true, preserveDrawingBuffer: true});
|
||||
Renderer.setSize(canvas.width, canvas.height);
|
||||
Renderer.shadowMap.enabled = true;
|
||||
// Renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||||
if (options.extendedWater) extendWater(graphWidth, graphHeight);
|
||||
createMesh(graphWidth, graphHeight, grid.cellsX, grid.cellsY);
|
||||
|
||||
|
|
@ -223,7 +256,7 @@ window.ThreeD = (function () {
|
|||
|
||||
function textureToSprite(texture, width, height) {
|
||||
const map = new THREE.TextureLoader().load(texture);
|
||||
map.anisotropy = Renderer.getMaxAnisotropy();
|
||||
map.anisotropy = Renderer.capabilities.getMaxAnisotropy();
|
||||
const material = new THREE.SpriteMaterial({map});
|
||||
|
||||
const sprite = new THREE.Sprite(material);
|
||||
|
|
@ -242,7 +275,11 @@ window.ThreeD = (function () {
|
|||
context2d.fillStyle = color;
|
||||
context2d.fillText(text, 0, size * quality);
|
||||
|
||||
return textureToSprite(context2d.canvas.toDataURL(), context2d.canvas.width / quality, context2d.canvas.height / quality);
|
||||
return textureToSprite(
|
||||
context2d.canvas.toDataURL(),
|
||||
context2d.canvas.width / quality,
|
||||
context2d.canvas.height / quality
|
||||
);
|
||||
}
|
||||
|
||||
function get3dCoords(baseX, baseY) {
|
||||
|
|
@ -296,9 +333,23 @@ window.ThreeD = (function () {
|
|||
};
|
||||
|
||||
const city_icon_material = new THREE.MeshPhongMaterial({color: cityOptions.iconColor});
|
||||
city_icon_material.wireframe = options.wireframe;
|
||||
const town_icon_material = new THREE.MeshPhongMaterial({color: townOptions.iconColor});
|
||||
const city_icon_geometry = new THREE.CylinderGeometry(cityOptions.iconSize * 2, cityOptions.iconSize * 2, cityOptions.iconSize, 16, 1);
|
||||
const town_icon_geometry = new THREE.CylinderGeometry(townOptions.iconSize * 2, townOptions.iconSize * 2, townOptions.iconSize, 16, 1);
|
||||
town_icon_material.wireframe = options.wireframe;
|
||||
const city_icon_geometry = new THREE.CylinderGeometry(
|
||||
cityOptions.iconSize * 2,
|
||||
cityOptions.iconSize * 2,
|
||||
cityOptions.iconSize,
|
||||
16,
|
||||
1
|
||||
);
|
||||
const town_icon_geometry = new THREE.CylinderGeometry(
|
||||
townOptions.iconSize * 2,
|
||||
townOptions.iconSize * 2,
|
||||
townOptions.iconSize,
|
||||
16,
|
||||
1
|
||||
);
|
||||
const line_material = new THREE.LineBasicMaterial({color: cityOptions.iconColor});
|
||||
|
||||
// burg labels
|
||||
|
|
@ -387,32 +438,78 @@ window.ThreeD = (function () {
|
|||
lines = [];
|
||||
}
|
||||
|
||||
async function createMeshTextureUrl() {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const mapOptions = {
|
||||
noLabels: options.labels3d,
|
||||
noWater: options.extendedWater,
|
||||
fullMap: true
|
||||
};
|
||||
const url = await getMapURL("mesh", mapOptions);
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
canvas.width = options.resolutionScale;
|
||||
canvas.height = options.resolutionScale;
|
||||
const img = new Image();
|
||||
img.src = url;
|
||||
|
||||
img.onload = function () {
|
||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||
canvas.toBlob(blob => {
|
||||
const blobObj = window.URL.createObjectURL(blob);
|
||||
window.setTimeout(() => {
|
||||
canvas.remove();
|
||||
window.URL.revokeObjectURL(blobObj);
|
||||
}, 100);
|
||||
resolve(blobObj);
|
||||
});
|
||||
};
|
||||
});
|
||||
}
|
||||
// create a mesh from pixel data
|
||||
async function createMesh(width, height, segmentsX, segmentsY) {
|
||||
const mapOptions = {
|
||||
noLabels: options.labels3d,
|
||||
noWater: options.extendedWater,
|
||||
fullMap: true
|
||||
};
|
||||
const url = await getMapURL("mesh", mapOptions);
|
||||
window.setTimeout(() => window.URL.revokeObjectURL(url), 5000);
|
||||
|
||||
if (texture) texture.dispose();
|
||||
texture = new THREE.TextureLoader().load(url, render);
|
||||
texture.needsUpdate = true;
|
||||
if (!options.wireframe) {
|
||||
texture = new THREE.TextureLoader().load(await createMeshTextureUrl(), render);
|
||||
texture.needsUpdate = true;
|
||||
texture.anisotropy = Renderer.capabilities.getMaxAnisotropy();
|
||||
}
|
||||
|
||||
if (material) material.dispose();
|
||||
material = new THREE.MeshLambertMaterial();
|
||||
material.map = texture;
|
||||
material.transparent = true;
|
||||
|
||||
if (options.wireframe) {
|
||||
material.wireframe = true;
|
||||
} else {
|
||||
material.map = texture;
|
||||
material.transparent = true;
|
||||
}
|
||||
|
||||
if (geometry) geometry.dispose();
|
||||
geometry = new THREE.PlaneGeometry(width, height, segmentsX - 1, segmentsY - 1);
|
||||
geometry.vertices.forEach((v, i) => (v.z = getMeshHeight(i)));
|
||||
geometry.computeVertexNormals();
|
||||
|
||||
let vertices = geometry.getAttribute("position");
|
||||
for (let i = 0; i < vertices.count; i++) {
|
||||
vertices.setZ(i, getMeshHeight(i));
|
||||
}
|
||||
|
||||
geometry.setAttribute("position", vertices);
|
||||
geometry.computeVertexNormals();
|
||||
if (mesh) scene.remove(mesh);
|
||||
mesh = new THREE.Mesh(geometry, material);
|
||||
if (options.subdivide) {
|
||||
await loadLoopSubdivision();
|
||||
const subdivideParams = {
|
||||
split: true,
|
||||
uvSmooth: false,
|
||||
preserveEdges: true,
|
||||
flatOnly: false,
|
||||
maxTriangles: Infinity
|
||||
};
|
||||
const smoothGeometry = loopSubdivision.modify(geometry, 1, subdivideParams);
|
||||
mesh = new THREE.Mesh(smoothGeometry, material);
|
||||
} else {
|
||||
mesh = new THREE.Mesh(geometry, material);
|
||||
}
|
||||
mesh.rotation.x = -Math.PI / 2;
|
||||
mesh.castShadow = true;
|
||||
mesh.receiveShadow = true;
|
||||
|
|
@ -449,7 +546,7 @@ window.ThreeD = (function () {
|
|||
noWater: options.extendedWater,
|
||||
fullMap: true
|
||||
};
|
||||
const url = await getMapURL("mesh", mapOptions);
|
||||
const url = await createMeshTextureUrl();
|
||||
window.setTimeout(() => window.URL.revokeObjectURL(url), 4000);
|
||||
texture = new THREE.TextureLoader().load(url, render);
|
||||
material.map = texture;
|
||||
|
|
@ -464,7 +561,10 @@ window.ThreeD = (function () {
|
|||
|
||||
// scene
|
||||
scene = new THREE.Scene();
|
||||
scene.background = new THREE.TextureLoader().load("https://i0.wp.com/azgaar.files.wordpress.com/2019/10/stars-1.png", render);
|
||||
scene.background = new THREE.TextureLoader().load(
|
||||
"https://i0.wp.com/azgaar.files.wordpress.com/2019/10/stars-1.png",
|
||||
render
|
||||
);
|
||||
|
||||
// Renderer
|
||||
Renderer = new THREE.WebGLRenderer({canvas, antialias: true, preserveDrawingBuffer: true});
|
||||
|
|
@ -579,6 +679,17 @@ window.ThreeD = (function () {
|
|||
});
|
||||
}
|
||||
|
||||
function loadLoopSubdivision() {
|
||||
if (window.loopSubdivision) return Promise.resolve(true);
|
||||
|
||||
return new Promise(resolve => {
|
||||
const script = document.createElement("script");
|
||||
script.src = "libs/loopsubdivison.min.js";
|
||||
document.head.append(script);
|
||||
script.onload = () => resolve(true);
|
||||
script.onerror = () => resolve(false);
|
||||
});
|
||||
}
|
||||
function OrbitControls(camera, domElement) {
|
||||
if (THREE.OrbitControls) return new THREE.OrbitControls(camera, domElement);
|
||||
|
||||
|
|
@ -596,7 +707,7 @@ window.ThreeD = (function () {
|
|||
|
||||
return new Promise(resolve => {
|
||||
const script = document.createElement("script");
|
||||
script.src = "libs/objexporter.min.js";
|
||||
script.src = "libs/objexporter.min.js?v=1.89.35";
|
||||
document.head.append(script);
|
||||
script.onload = () => resolve(new THREE.OBJExporter());
|
||||
script.onerror = () => resolve(false);
|
||||
|
|
@ -609,11 +720,15 @@ window.ThreeD = (function () {
|
|||
update,
|
||||
stop,
|
||||
options,
|
||||
setSunColor,
|
||||
setScale,
|
||||
setResolutionScale,
|
||||
setLightness,
|
||||
setSun,
|
||||
setRotation,
|
||||
toggleLabels,
|
||||
toggle3dSubdivision,
|
||||
toggleWireframe,
|
||||
toggleSky,
|
||||
setResolution,
|
||||
setColors,
|
||||
|
|
|
|||
|
|
@ -88,7 +88,9 @@ function editBiomes() {
|
|||
const rural = b.rural[i] * populationRate;
|
||||
const urban = b.urban[i] * populationRate * urbanization;
|
||||
const population = rn(rural + urban);
|
||||
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}`;
|
||||
const populationTip = `Total population: ${si(population)}; Rural population: ${si(
|
||||
rural
|
||||
)}; Urban population: ${si(urban)}`;
|
||||
totalArea += area;
|
||||
totalPopulation += population;
|
||||
|
||||
|
|
@ -104,7 +106,9 @@ function editBiomes() {
|
|||
data-color=${b.color[i]}
|
||||
>
|
||||
<fill-box fill="${b.color[i]}"></fill-box>
|
||||
<input data-tip="Biome name. Click and type to change" class="biomeName" value="${b.name[i]}" autocorrect="off" spellcheck="false" />
|
||||
<input data-tip="Biome name. Click and type to change" class="biomeName" value="${
|
||||
b.name[i]
|
||||
}" autocorrect="off" spellcheck="false" />
|
||||
<span data-tip="Biome habitability percent" class="hide">%</span>
|
||||
<input
|
||||
data-tip="Biome habitability percent. Click and set new value to change"
|
||||
|
|
@ -121,7 +125,11 @@ function editBiomes() {
|
|||
<span data-tip="${populationTip}" class="icon-male hide"></span>
|
||||
<div data-tip="${populationTip}" class="biomePopulation hide">${si(population)}</div>
|
||||
<span data-tip="Open Wikipedia article about the biome" class="icon-info-circled pointer hide"></span>
|
||||
${i > 12 && !b.cells[i] ? '<span data-tip="Remove the custom biome" class="icon-trash-empty hide"></span>' : ""}
|
||||
${
|
||||
i > 12 && !b.cells[i]
|
||||
? '<span data-tip="Remove the custom biome" class="icon-trash-empty hide"></span>'
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
@ -403,7 +411,14 @@ function editBiomes() {
|
|||
|
||||
// change of append new element
|
||||
if (exists.size()) exists.attr("data-biome", biomeNew).attr("fill", color).attr("stroke", color);
|
||||
else temp.append("polygon").attr("data-cell", i).attr("data-biome", biomeNew).attr("points", getPackPolygon(i)).attr("fill", color).attr("stroke", color);
|
||||
else
|
||||
temp
|
||||
.append("polygon")
|
||||
.attr("data-cell", i)
|
||||
.attr("data-biome", biomeNew)
|
||||
.attr("points", getPackPolygon(i))
|
||||
.attr("fill", color)
|
||||
.attr("stroke", color);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -449,8 +464,8 @@ function editBiomes() {
|
|||
}
|
||||
|
||||
function restoreInitialBiomes() {
|
||||
biomesData = applyDefaultBiomesSystem();
|
||||
defineBiomes();
|
||||
biomesData = Biomes.getDefault();
|
||||
Biomes.define();
|
||||
drawBiomes();
|
||||
recalculatePopulation();
|
||||
refreshBiomesEditor();
|
||||
|
|
|
|||
|
|
@ -1176,13 +1176,13 @@ function refreshAllEditors() {
|
|||
// dynamically loaded editors
|
||||
async function editStates() {
|
||||
if (customization) return;
|
||||
const Editor = await import("../dynamic/editors/states-editor.js?v=1.89.35");
|
||||
const Editor = await import("../dynamic/editors/states-editor.js?v=1.92.00");
|
||||
Editor.open();
|
||||
}
|
||||
|
||||
async function editCultures() {
|
||||
if (customization) return;
|
||||
const Editor = await import("../dynamic/editors/cultures-editor.js?v=1.89.09");
|
||||
const Editor = await import("../dynamic/editors/cultures-editor.js?v=1.91.00");
|
||||
Editor.open();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -112,13 +112,13 @@ function editEmblem(type, id, el) {
|
|||
if (type === "burg") name = "Burg of " + name;
|
||||
document.getElementById("emblemArmiger").innerText = name;
|
||||
|
||||
if (el.coa === "custom") emblemShapeSelector.disabled = true;
|
||||
if (el.coa.custom) emblemShapeSelector.disabled = true;
|
||||
else {
|
||||
emblemShapeSelector.disabled = false;
|
||||
emblemShapeSelector.value = el.coa.shield;
|
||||
}
|
||||
|
||||
const size = el.coaSize || 1;
|
||||
const size = el.coa.size || 1;
|
||||
document.getElementById("emblemSizeSlider").value = size;
|
||||
document.getElementById("emblemSizeNumber").value = size;
|
||||
}
|
||||
|
|
@ -178,7 +178,9 @@ function editEmblem(type, id, el) {
|
|||
}
|
||||
|
||||
function changeSize() {
|
||||
const size = (el.coaSize = +this.value);
|
||||
const size = +this.value;
|
||||
el.coa.size = size;
|
||||
|
||||
document.getElementById("emblemSizeSlider").value = size;
|
||||
document.getElementById("emblemSizeNumber").value = size;
|
||||
|
||||
|
|
@ -189,8 +191,9 @@ function editEmblem(type, id, el) {
|
|||
// re-append use element
|
||||
const categotySize = +g.attr("font-size");
|
||||
const shift = (categotySize * size) / 2;
|
||||
const x = el.x || el.pole[0];
|
||||
const y = el.y || el.pole[1];
|
||||
const x = el.coa.x || el.x || el.pole[0];
|
||||
const y = el.coa.y || el.y || el.pole[1];
|
||||
|
||||
g.append("use")
|
||||
.attr("data-i", el.i)
|
||||
.attr("x", rn(x - shift), 2)
|
||||
|
|
@ -220,7 +223,7 @@ function editEmblem(type, id, el) {
|
|||
}
|
||||
|
||||
function openInArmoria() {
|
||||
const coa = el.coa && el.coa !== "custom" ? el.coa : {t1: "sable"};
|
||||
const coa = el.coa && !el.coa.custom ? el.coa : {t1: "sable"};
|
||||
const json = JSON.stringify(coa).replaceAll("#", "%23");
|
||||
const url = `https://azgaar.github.io/Armoria/?coa=${json}&from=FMG`;
|
||||
openURL(url);
|
||||
|
|
@ -281,7 +284,13 @@ function editEmblem(type, id, el) {
|
|||
defs.insertAdjacentHTML("beforeend", svg);
|
||||
|
||||
if (oldEmblem) oldEmblem.remove();
|
||||
el.coa = "custom";
|
||||
|
||||
const customCoa = {custom: true};
|
||||
if (el.coa.size) customCoa.size = el.coa.size;
|
||||
if (el.coa.x) customCoa.x = el.coa.x;
|
||||
if (el.coa.y) customCoa.y = el.coa.y;
|
||||
el.coa = customCoa;
|
||||
|
||||
emblemShapeSelector.disabled = true;
|
||||
};
|
||||
|
||||
|
|
@ -509,13 +518,21 @@ function editEmblem(type, id, el) {
|
|||
}
|
||||
|
||||
function dragEmblem() {
|
||||
const tr = parseTransform(this.getAttribute("transform"));
|
||||
const x = +tr[0] - d3.event.x,
|
||||
y = +tr[1] - d3.event.y;
|
||||
const x = Number(this.getAttribute("x")) - d3.event.x;
|
||||
const y = Number(this.getAttribute("y")) - d3.event.y;
|
||||
|
||||
d3.event.on("drag", function () {
|
||||
const transform = `translate(${x + d3.event.x},${y + d3.event.y})`;
|
||||
this.setAttribute("transform", transform);
|
||||
this.setAttribute("x", x + d3.event.x);
|
||||
this.setAttribute("y", y + d3.event.y);
|
||||
});
|
||||
|
||||
d3.event.on("end", function () {
|
||||
const categotySize = Number(this.parentNode.getAttribute("font-size"));
|
||||
const size = el.coa.size || 1;
|
||||
const shift = (categotySize * size) / 2;
|
||||
|
||||
el.coa.x = rn(x + d3.event.x + shift, 2);
|
||||
el.coa.y = rn(y + d3.event.y + shift, 2);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -87,6 +87,8 @@ function handleMouseMove() {
|
|||
if (cellInfo?.offsetParent) updateCellInfo(point, i, gridCell);
|
||||
}
|
||||
|
||||
let currentNoteId = null; // store currently displayed node to not rerender to often
|
||||
|
||||
// show note box on hover (if any)
|
||||
function showNotes(e) {
|
||||
if (notesEditor?.offsetParent) return;
|
||||
|
|
@ -96,13 +98,17 @@ function showNotes(e) {
|
|||
|
||||
const note = notes.find(note => note.id === id);
|
||||
if (note !== undefined && note.legend !== "") {
|
||||
if (currentNoteId === id) return;
|
||||
currentNoteId = id;
|
||||
|
||||
document.getElementById("notes").style.display = "block";
|
||||
document.getElementById("notesHeader").innerHTML = note.name;
|
||||
document.getElementById("notesBody").innerHTML = note.legend;
|
||||
} else if (!options.pinNotes && !markerEditor?.offsetParent) {
|
||||
} else if (!options.pinNotes && !markerEditor?.offsetParent && !e.shiftKey) {
|
||||
document.getElementById("notes").style.display = "none";
|
||||
document.getElementById("notesHeader").innerHTML = "";
|
||||
document.getElementById("notesBody").innerHTML = "";
|
||||
currentNoteId = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -160,7 +166,7 @@ function showMapTooltip(point, e, i, g) {
|
|||
}
|
||||
if (group === "labels") return tip("Click to edit the Label");
|
||||
|
||||
if (group === "markers") return tip("Click to edit the Marker and pin the marker note");
|
||||
if (group === "markers") return tip("Click to edit the Marker. Hold Shift to not close the assosiated note");
|
||||
|
||||
if (group === "ruler") {
|
||||
const tag = e.target.tagName;
|
||||
|
|
@ -494,6 +500,7 @@ function showInfo() {
|
|||
const Reddit = link("https://www.reddit.com/r/FantasyMapGenerator", "Reddit");
|
||||
const Patreon = link("https://www.patreon.com/azgaar", "Patreon");
|
||||
const Armoria = link("https://azgaar.github.io/Armoria", "Armoria");
|
||||
const Deorum = link("https://deorum.vercel.app", "Deorum");
|
||||
|
||||
const QuickStart = link(
|
||||
"https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Quick-Start-Tutorial",
|
||||
|
|
@ -515,8 +522,6 @@ function showInfo() {
|
|||
and ${VideoTutorial}.
|
||||
</p>
|
||||
|
||||
<p>Check out our another project: ${Armoria} — heraldry generator and editor.</p>
|
||||
|
||||
<ul style="columns:2">
|
||||
<li>${link("https://github.com/Azgaar/Fantasy-Map-Generator", "GitHub repository")}</li>
|
||||
<li>${link("https://github.com/Azgaar/Fantasy-Map-Generator/blob/master/LICENSE", "License")}</li>
|
||||
|
|
@ -524,7 +529,14 @@ function showInfo() {
|
|||
<li>${link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Hotkeys", "Hotkeys")}</li>
|
||||
<li>${link("https://trello.com/b/7x832DG4/fantasy-map-generator", "Devboard")}</li>
|
||||
<li><a href="mailto:azgaar.fmg@yandex.by" target="_blank">Contact Azgaar</a></li>
|
||||
</ul>`;
|
||||
</ul>
|
||||
|
||||
<p>Check out our other projects:
|
||||
<ul>
|
||||
<li>${Armoria}: a tool for creating heraldic coats of arms</li>
|
||||
<li>${Deorum}: a vast gallery of customizable fantasy characters</li>
|
||||
</ul>
|
||||
</p>`;
|
||||
|
||||
$("#alert").dialog({
|
||||
resizable: false,
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ function editHeightmap(options) {
|
|||
<p><i>Erase</i> mode also allows you Convert an Image into a heightmap or use Template Editor.</p>
|
||||
<p>You can <i>keep</i> the data, but you won't be able to change the coastline.</p>
|
||||
<p>Try <i>risk</i> mode to change the coastline and keep the data. The data will be restored as much as possible, but it can cause unpredictable errors.</p>
|
||||
<p>Please <span class="pseudoLink" onclick="dowloadMap();">save the map</span> before editing the heightmap!</p>
|
||||
<p>Please <span class="pseudoLink" onclick="saveMap('machine')">save the map</span> before editing the heightmap!</p>
|
||||
<p style="margin-bottom: 0">Check out ${link(
|
||||
"https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Heightmap-customization",
|
||||
"wiki"
|
||||
|
|
@ -192,11 +192,14 @@ function editHeightmap(options) {
|
|||
document
|
||||
.getElementById("mapLayers")
|
||||
.querySelectorAll("li")
|
||||
.forEach(function (e) {
|
||||
if (editHeightmap.layers.includes(e.id) && !layerIsOn(e.id)) e.click();
|
||||
// turn on
|
||||
else if (!editHeightmap.layers.includes(e.id) && layerIsOn(e.id)) e.click(); // turn off
|
||||
.forEach(e => {
|
||||
const wasOn = editHeightmap.layers.includes(e.id);
|
||||
if ((wasOn && !layerIsOn(e.id)) || (!wasOn && layerIsOn(e.id))) e.click();
|
||||
});
|
||||
if (!layerIsOn("toggleBorders")) borders.selectAll("path").remove();
|
||||
if (!layerIsOn("toggleStates")) regions.selectAll("path").remove();
|
||||
if (!layerIsOn("toggleRivers")) rivers.selectAll("*").remove();
|
||||
|
||||
getCurrentPreset();
|
||||
}
|
||||
|
||||
|
|
@ -236,7 +239,7 @@ function editHeightmap(options) {
|
|||
|
||||
drawRivers();
|
||||
Lakes.defineGroup();
|
||||
defineBiomes();
|
||||
Biomes.define();
|
||||
rankCells();
|
||||
|
||||
Cultures.generate();
|
||||
|
|
@ -250,7 +253,7 @@ function editHeightmap(options) {
|
|||
|
||||
drawStates();
|
||||
drawBorders();
|
||||
BurgsAndStates.drawStateLabels();
|
||||
drawStateLabels();
|
||||
|
||||
Rivers.specify();
|
||||
Lakes.generateName();
|
||||
|
|
@ -370,10 +373,6 @@ function editHeightmap(options) {
|
|||
const g = pack.cells.g[i];
|
||||
const isLand = pack.cells.h[i] >= 20;
|
||||
|
||||
// check biome
|
||||
pack.cells.biome[i] =
|
||||
isLand && biome[g] ? biome[g] : getBiomeId(grid.cells.prec[g], grid.cells.temp[g], pack.cells.h[i]);
|
||||
|
||||
// rivers data
|
||||
if (!erosionAllowed) {
|
||||
pack.cells.r[i] = r[g];
|
||||
|
|
@ -381,6 +380,12 @@ function editHeightmap(options) {
|
|||
pack.cells.fl[i] = fl[g];
|
||||
}
|
||||
|
||||
// check biome
|
||||
pack.cells.biome[i] =
|
||||
isLand && biome[g]
|
||||
? biome[g]
|
||||
: Biomes.getId(grid.cells.prec[g], grid.cells.temp[g], pack.cells.h[i], Boolean(pack.cells.r[i]));
|
||||
|
||||
if (!isLand) continue;
|
||||
pack.cells.culture[i] = culture[g];
|
||||
pack.cells.pop[i] = pop[g];
|
||||
|
|
@ -437,7 +442,7 @@ function editHeightmap(options) {
|
|||
c.center = findCell(c.x, c.y);
|
||||
}
|
||||
|
||||
BurgsAndStates.drawStateLabels();
|
||||
drawStateLabels();
|
||||
drawStates();
|
||||
drawBorders();
|
||||
|
||||
|
|
|
|||
|
|
@ -25,15 +25,15 @@ function handleKeyup(event) {
|
|||
|
||||
if (code === "F1") showInfo();
|
||||
else if (code === "F2") regeneratePrompt();
|
||||
else if (code === "F6") quickSave();
|
||||
else if (code === "F6") saveMap("storage");
|
||||
else if (code === "F9") quickLoad();
|
||||
else if (code === "Tab") toggleOptions(event);
|
||||
else if (code === "Escape") closeAllDialogs();
|
||||
else if (code === "Delete") removeElementOnKey();
|
||||
else if (code === "KeyO" && document.getElementById("canvas3d")) toggle3dOptions();
|
||||
else if (ctrl && code === "KeyQ") toggleSaveReminder();
|
||||
else if (ctrl && code === "KeyS") dowloadMap();
|
||||
else if (ctrl && code === "KeyC") saveToDropbox();
|
||||
else if (ctrl && code === "KeyS") saveMap("machine");
|
||||
else if (ctrl && code === "KeyC") saveMap("dropbox");
|
||||
else if (ctrl && code === "KeyZ" && undo?.offsetParent) undo.click();
|
||||
else if (ctrl && code === "KeyY" && redo?.offsetParent) redo.click();
|
||||
else if (shift && code === "KeyH") editHeightmap();
|
||||
|
|
|
|||
|
|
@ -78,7 +78,9 @@ function editLabel() {
|
|||
}
|
||||
|
||||
function updateValues(textPath) {
|
||||
document.getElementById("labelText").value = [...textPath.querySelectorAll("tspan")].map(tspan => tspan.textContent).join("|");
|
||||
document.getElementById("labelText").value = [...textPath.querySelectorAll("tspan")]
|
||||
.map(tspan => tspan.textContent)
|
||||
.join("|");
|
||||
document.getElementById("labelStartOffset").value = parseFloat(textPath.getAttribute("startOffset"));
|
||||
document.getElementById("labelRelativeSize").value = parseFloat(textPath.getAttribute("font-size"));
|
||||
}
|
||||
|
|
@ -298,22 +300,15 @@ function editLabel() {
|
|||
function changeText() {
|
||||
const input = document.getElementById("labelText").value;
|
||||
const el = elSelected.select("textPath").node();
|
||||
const example = d3.select(elSelected.node().parentNode).append("text").attr("x", 0).attr("x", 0).attr("font-size", el.getAttribute("font-size")).node();
|
||||
|
||||
const lines = input.split("|");
|
||||
const top = (lines.length - 1) / -2; // y offset
|
||||
const inner = lines
|
||||
.map((l, d) => {
|
||||
example.innerHTML = l;
|
||||
const left = example.getBBox().width / -2; // x offset
|
||||
return `<tspan x="${left}px" dy="${d ? 1 : top}em">${l}</tspan>`;
|
||||
})
|
||||
.join("");
|
||||
if (lines.length > 1) {
|
||||
const top = (lines.length - 1) / -2; // y offset
|
||||
el.innerHTML = lines.map((line, index) => `<tspan x="0" dy="${index ? 1 : top}em">${line}</tspan>`).join("");
|
||||
} else el.innerHTML = `<tspan x="0">${lines}</tspan>`;
|
||||
|
||||
el.innerHTML = inner;
|
||||
example.remove();
|
||||
|
||||
if (elSelected.attr("id").slice(0, 10) === "stateLabel") tip("Use States Editor to change an actual state name, not just a label", false, "warning");
|
||||
if (elSelected.attr("id").slice(0, 10) === "stateLabel")
|
||||
tip("Use States Editor to change an actual state name, not just a label", false, "warning");
|
||||
}
|
||||
|
||||
function generateRandomName() {
|
||||
|
|
|
|||
|
|
@ -1517,27 +1517,15 @@ function toggleRelief(event) {
|
|||
function toggleTexture(event) {
|
||||
if (!layerIsOn("toggleTexture")) {
|
||||
turnButtonOn("toggleTexture");
|
||||
// append default texture image selected by default. Don't append on load to not harm performance
|
||||
if (!texture.selectAll("*").size()) {
|
||||
const x = +styleTextureShiftX.value;
|
||||
const y = +styleTextureShiftY.value;
|
||||
const image = texture
|
||||
.append("image")
|
||||
.attr("id", "textureImage")
|
||||
.attr("x", x)
|
||||
.attr("y", y)
|
||||
.attr("width", graphWidth - x)
|
||||
.attr("height", graphHeight - y)
|
||||
.attr("preserveAspectRatio", "xMidYMid slice");
|
||||
getBase64(styleTextureInput.value, base64 => image.attr("xlink:href", base64));
|
||||
}
|
||||
$("#texture").fadeIn();
|
||||
zoom.scaleBy(svg, 1.00001); // enforce browser re-draw
|
||||
// href is not set directly to avoid image loading when layer is off
|
||||
const textureImage = byId("textureImage");
|
||||
if (textureImage) textureImage.setAttribute("href", textureImage.getAttribute("src"));
|
||||
|
||||
if (event && isCtrlClick(event)) editStyle("texture");
|
||||
} else {
|
||||
if (event && isCtrlClick(event)) return editStyle("texture");
|
||||
$("#texture").fadeOut();
|
||||
turnButtonOff("toggleTexture");
|
||||
texture.select("image").attr("href", null);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1762,9 +1750,9 @@ function drawEmblems() {
|
|||
TIME && console.time("drawEmblems");
|
||||
const {states, provinces, burgs} = pack;
|
||||
|
||||
const validStates = states.filter(s => s.i && !s.removed && s.coa && s.coaSize != 0);
|
||||
const validProvinces = provinces.filter(p => p.i && !p.removed && p.coa && p.coaSize != 0);
|
||||
const validBurgs = burgs.filter(b => b.i && !b.removed && b.coa && b.coaSize != 0);
|
||||
const validStates = states.filter(s => s.i && !s.removed && s.coa && s.coa.size !== 0);
|
||||
const validProvinces = provinces.filter(p => p.i && !p.removed && p.coa && p.coa.size !== 0);
|
||||
const validBurgs = burgs.filter(b => b.i && !b.removed && b.coa && b.coa.size !== 0);
|
||||
|
||||
const getStateEmblemsSize = () => {
|
||||
const startSize = minmax((graphHeight + graphWidth) / 40, 10, 100);
|
||||
|
|
@ -1790,26 +1778,26 @@ function drawEmblems() {
|
|||
const sizeBurgs = getBurgEmblemSize();
|
||||
const burgCOAs = validBurgs.map(burg => {
|
||||
const {x, y} = burg;
|
||||
const size = burg.coaSize || 1;
|
||||
const size = burg.coa.size || 1;
|
||||
const shift = (sizeBurgs * size) / 2;
|
||||
return {type: "burg", i: burg.i, x, y, size, shift};
|
||||
return {type: "burg", i: burg.i, x: burg.coa.x || x, y: burg.coa.y || y, size, shift};
|
||||
});
|
||||
|
||||
const sizeProvinces = getProvinceEmblemsSize();
|
||||
const provinceCOAs = validProvinces.map(province => {
|
||||
if (!province.pole) getProvincesVertices();
|
||||
const [x, y] = province.pole || pack.cells.p[province.center];
|
||||
const size = province.coaSize || 1;
|
||||
const size = province.coa.size || 1;
|
||||
const shift = (sizeProvinces * size) / 2;
|
||||
return {type: "province", i: province.i, x, y, size, shift};
|
||||
return {type: "province", i: province.i, x: province.coa.x || x, y: province.coa.y || y, size, shift};
|
||||
});
|
||||
|
||||
const sizeStates = getStateEmblemsSize();
|
||||
const stateCOAs = validStates.map(state => {
|
||||
const [x, y] = state.pole || pack.cells.p[state.center];
|
||||
const size = state.coaSize || 1;
|
||||
const size = state.coa.size || 1;
|
||||
const shift = (sizeStates * size) / 2;
|
||||
return {type: "state", i: state.i, x, y, size, shift};
|
||||
return {type: "state", i: state.i, x: state.coa.x || x, y: state.coa.y || y, size, shift};
|
||||
});
|
||||
|
||||
const nodes = burgCOAs.concat(provinceCOAs).concat(stateCOAs);
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ function overviewMarkers() {
|
|||
const markersGenerationConfig = document.getElementById("markersGenerationConfig");
|
||||
const markersRemoveAll = document.getElementById("markersRemoveAll");
|
||||
const markersExport = document.getElementById("markersExport");
|
||||
const markerTypeInput = document.getElementById("addedMarkerType");
|
||||
const markerTypeSelector = document.getElementById("markerTypeSelector");
|
||||
|
||||
addLines();
|
||||
|
||||
|
|
@ -33,9 +35,26 @@ function overviewMarkers() {
|
|||
listen(markersAddFromOverview, "click", toggleAddMarker),
|
||||
listen(markersGenerationConfig, "click", configMarkersGeneration),
|
||||
listen(markersRemoveAll, "click", triggerRemoveAll),
|
||||
listen(markersExport, "click", exportMarkers)
|
||||
listen(markersExport, "click", exportMarkers),
|
||||
listen(markerTypeSelector, "click", toggleMarkerTypeMenu)
|
||||
];
|
||||
|
||||
const types = [{type: "empty", icon: "❓"}, ...Markers.getConfig()];
|
||||
types.forEach(({icon, type}) => {
|
||||
const option = document.createElement("button");
|
||||
option.textContent = `${icon} ${type}`;
|
||||
markerTypeSelectMenu.appendChild(option);
|
||||
|
||||
listeners.push(
|
||||
listen(option, "click", () => {
|
||||
markerTypeSelector.textContent = icon;
|
||||
markerTypeInput.value = type;
|
||||
changeMarkerType();
|
||||
toggleMarkerTypeMenu();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
function handleLineClick(ev) {
|
||||
const el = ev.target;
|
||||
const i = +el.parentNode.dataset.i;
|
||||
|
|
@ -139,11 +158,21 @@ function overviewMarkers() {
|
|||
});
|
||||
}
|
||||
|
||||
function toggleMarkerTypeMenu() {
|
||||
document.getElementById("markerTypeSelectMenu").classList.toggle("visible");
|
||||
}
|
||||
|
||||
function toggleAddMarker() {
|
||||
markersAddFromOverview.classList.toggle("pressed");
|
||||
addMarker.click();
|
||||
}
|
||||
|
||||
function changeMarkerType() {
|
||||
if (!markersAddFromOverview.classList.contains("pressed")) {
|
||||
toggleAddMarker();
|
||||
}
|
||||
}
|
||||
|
||||
function removeMarker(i) {
|
||||
notes = notes.filter(note => note.id !== `marker${i}`);
|
||||
pack.markers = pack.markers.filter(marker => marker.i !== i);
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ document
|
|||
|
||||
// show popup with a list of Patreon supportes (updated manually)
|
||||
async function showSupporters() {
|
||||
const {supporters} = await import("../dynamic/supporters.js?v=1.89.15");
|
||||
const {supporters} = await import("../dynamic/supporters.js?v=1.93.03");
|
||||
const list = supporters.split("\n").sort();
|
||||
const columns = window.innerWidth < 800 ? 2 : 5;
|
||||
|
||||
|
|
@ -381,7 +381,7 @@ function changeEmblemShape(emblemShape) {
|
|||
};
|
||||
|
||||
pack.states.forEach(state => {
|
||||
if (!state.i || state.removed || !state.coa || state.coa === "custom") return;
|
||||
if (!state.i || state.removed || !state.coa || state.coa.custom) return;
|
||||
const newShield = specificShape || COA.getShield(state.culture, null);
|
||||
if (newShield === state.coa.shield) return;
|
||||
state.coa.shield = newShield;
|
||||
|
|
@ -389,7 +389,7 @@ function changeEmblemShape(emblemShape) {
|
|||
});
|
||||
|
||||
pack.provinces.forEach(province => {
|
||||
if (!province.i || province.removed || !province.coa || province.coa === "custom") return;
|
||||
if (!province.i || province.removed || !province.coa || province.coa.custom) return;
|
||||
const culture = pack.cells.culture[province.center];
|
||||
const newShield = specificShape || COA.getShield(culture, province.state);
|
||||
if (newShield === province.coa.shield) return;
|
||||
|
|
@ -398,7 +398,7 @@ function changeEmblemShape(emblemShape) {
|
|||
});
|
||||
|
||||
pack.burgs.forEach(burg => {
|
||||
if (!burg.i || burg.removed || !burg.coa || burg.coa === "custom") return;
|
||||
if (!burg.i || burg.removed || !burg.coa || burg.coa.custom) return;
|
||||
const newShield = specificShape || COA.getShield(burg.culture, burg.state);
|
||||
if (newShield === burg.coa.shield) return;
|
||||
burg.coa.shield = newShield;
|
||||
|
|
@ -559,11 +559,10 @@ function applyStoredOptions() {
|
|||
if (key.slice(0, 5) === "style") applyOption(stylePreset, key, key.slice(5));
|
||||
}
|
||||
|
||||
if (stored("winds"))
|
||||
options.winds = localStorage
|
||||
.getItem("winds")
|
||||
.split(",")
|
||||
.map(w => +w);
|
||||
if (stored("winds")) options.winds = localStorage.getItem("winds").split(",").map(Number);
|
||||
if (stored("temperatureEquator")) options.temperatureEquator = +localStorage.getItem("temperatureEquator");
|
||||
if (stored("temperatureNorthPole")) options.temperatureNorthPole = +localStorage.getItem("temperatureNorthPole");
|
||||
if (stored("temperatureSouthPole")) options.temperatureSouthPole = +localStorage.getItem("temperatureSouthPole");
|
||||
if (stored("military")) options.military = JSON.parse(stored("military"));
|
||||
|
||||
if (stored("tooltipSize")) changeTooltipSize(stored("tooltipSize"));
|
||||
|
|
@ -607,13 +606,10 @@ function randomizeOptions() {
|
|||
if (randomize || !locked("culturesSet")) randomizeCultureSet();
|
||||
|
||||
// 'Configure World' settings
|
||||
if (randomize || !locked("temperatureEquator")) options.temperatureEquator = gauss(25, 7, 20, 35, 0);
|
||||
if (randomize || !locked("temperatureNorthPole")) options.temperatureNorthPole = gauss(-25, 7, -40, 10, 0);
|
||||
if (randomize || !locked("temperatureSouthPole")) options.temperatureSouthPole = gauss(-15, 7, -40, 10, 0);
|
||||
if (randomize || !locked("prec")) precInput.value = precOutput.value = gauss(100, 40, 5, 500);
|
||||
const tMax = 30,
|
||||
tMin = -30; // temperature extremes
|
||||
if (randomize || !locked("temperatureEquator"))
|
||||
temperatureEquatorOutput.value = temperatureEquatorInput.value = rand(tMax - 10, tMax);
|
||||
if (randomize || !locked("temperaturePole"))
|
||||
temperaturePoleOutput.value = temperaturePoleInput.value = rand(tMin, tMin + 30);
|
||||
|
||||
// 'Units Editor' settings
|
||||
const US = navigator.language === "en-US";
|
||||
|
|
@ -789,7 +785,7 @@ function showExportPane() {
|
|||
}
|
||||
|
||||
async function exportToJson(type) {
|
||||
const {exportToJson} = await import("../dynamic/export-json.js");
|
||||
const {exportToJson} = await import("../dynamic/export-json.js?v=1.93.03");
|
||||
exportToJson(type);
|
||||
}
|
||||
|
||||
|
|
@ -797,7 +793,7 @@ async function showLoadPane() {
|
|||
$("#loadMapData").dialog({
|
||||
title: "Load map",
|
||||
resizable: false,
|
||||
width: "24em",
|
||||
width: "auto",
|
||||
position: {my: "center", at: "center", of: "svg"},
|
||||
buttons: {
|
||||
Close: function () {
|
||||
|
|
@ -848,7 +844,7 @@ async function connectToDropbox() {
|
|||
|
||||
function loadURL() {
|
||||
const pattern = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/;
|
||||
const inner = `Provide URL to a .map file:
|
||||
const inner = `Provide URL to map file:
|
||||
<input id="mapURL" type="url" style="width: 24em" placeholder="https://e-cloud.com/test.map">
|
||||
<br><i>Please note server should allow CORS for file to be loaded. If CORS is not allowed, save file to Dropbox and provide a direct link</i>`;
|
||||
alertMessage.innerHTML = inner;
|
||||
|
|
@ -1060,6 +1056,7 @@ function toggle3dOptions() {
|
|||
document.getElementById("options3dSunX").addEventListener("change", changeSunPosition);
|
||||
document.getElementById("options3dSunY").addEventListener("change", changeSunPosition);
|
||||
document.getElementById("options3dSunZ").addEventListener("change", changeSunPosition);
|
||||
document.getElementById("options3dMeshSkinResolution").addEventListener("change", changeResolutionScale);
|
||||
document.getElementById("options3dMeshRotationRange").addEventListener("input", changeRotation);
|
||||
document.getElementById("options3dMeshRotationNumber").addEventListener("change", changeRotation);
|
||||
document.getElementById("options3dGlobeRotationRange").addEventListener("input", changeRotation);
|
||||
|
|
@ -1069,6 +1066,9 @@ function toggle3dOptions() {
|
|||
document.getElementById("options3dMeshSky").addEventListener("input", changeColors);
|
||||
document.getElementById("options3dMeshWater").addEventListener("input", changeColors);
|
||||
document.getElementById("options3dGlobeResolution").addEventListener("change", changeResolution);
|
||||
// document.getElementById("options3dMeshWireframeMode").addEventListener("change",toggleWireframe3d);
|
||||
document.getElementById("options3dSunColor").addEventListener("input", changeSunColor);
|
||||
document.getElementById("options3dSubdivide").addEventListener("change", toggle3dSubdivision);
|
||||
|
||||
function updateValues() {
|
||||
const globe = document.getElementById("canvas3d").dataset.type === "viewGlobe";
|
||||
|
|
@ -1081,6 +1081,7 @@ function toggle3dOptions() {
|
|||
options3dSunY.value = ThreeD.options.sun.y;
|
||||
options3dSunZ.value = ThreeD.options.sun.z;
|
||||
options3dMeshRotationRange.value = options3dMeshRotationNumber.value = ThreeD.options.rotateMesh;
|
||||
options3dMeshSkinResolution.value = ThreeD.options.resolutionScale;
|
||||
options3dGlobeRotationRange.value = options3dGlobeRotationNumber.value = ThreeD.options.rotateGlobe;
|
||||
options3dMeshLabels3d.value = ThreeD.options.labels3d;
|
||||
options3dMeshSkyMode.value = ThreeD.options.extendedWater;
|
||||
|
|
@ -1088,6 +1089,8 @@ function toggle3dOptions() {
|
|||
options3dMeshSky.value = ThreeD.options.skyColor;
|
||||
options3dMeshWater.value = ThreeD.options.waterColor;
|
||||
options3dGlobeResolution.value = ThreeD.options.resolution;
|
||||
options3dSunColor.value = ThreeD.options.sunColor;
|
||||
options3dSubdivide.value = ThreeD.options.subdivide;
|
||||
}
|
||||
|
||||
function changeHeightScale() {
|
||||
|
|
@ -1095,11 +1098,20 @@ function toggle3dOptions() {
|
|||
ThreeD.setScale(+this.value);
|
||||
}
|
||||
|
||||
function changeResolutionScale() {
|
||||
options3dMeshSkinResolution.value = this.value;
|
||||
ThreeD.setResolutionScale(+this.value);
|
||||
}
|
||||
|
||||
function changeLightness() {
|
||||
options3dLightnessRange.value = options3dLightnessNumber.value = this.value;
|
||||
ThreeD.setLightness(this.value / 100);
|
||||
}
|
||||
|
||||
function changeSunColor() {
|
||||
ThreeD.setSunColor(options3dSunColor.value);
|
||||
}
|
||||
|
||||
function changeSunPosition() {
|
||||
const x = +options3dSunX.value;
|
||||
const y = +options3dSunY.value;
|
||||
|
|
@ -1117,6 +1129,14 @@ function toggle3dOptions() {
|
|||
ThreeD.toggleLabels();
|
||||
}
|
||||
|
||||
function toggle3dSubdivision() {
|
||||
ThreeD.toggle3dSubdivision();
|
||||
}
|
||||
|
||||
// function toggleWireframe3d() {
|
||||
// ThreeD.toggleWireframe();
|
||||
// }
|
||||
|
||||
function toggleSkyMode() {
|
||||
const hide = ThreeD.options.extendedWater;
|
||||
options3dColorSection.style.display = hide ? "none" : "block";
|
||||
|
|
|
|||
|
|
@ -124,7 +124,9 @@ function editProvinces() {
|
|||
const rural = p.rural * populationRate;
|
||||
const urban = p.urban * populationRate * urbanization;
|
||||
const population = rn(rural + urban);
|
||||
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}`;
|
||||
const populationTip = `Total population: ${si(population)}; Rural population: ${si(
|
||||
rural
|
||||
)}; Urban population: ${si(urban)}`;
|
||||
totalPopulation += population;
|
||||
|
||||
const stateName = pack.states[p.state].name;
|
||||
|
|
@ -145,9 +147,15 @@ function editProvinces() {
|
|||
>
|
||||
<fill-box fill="${p.color}"></fill-box>
|
||||
<input data-tip="Province name. Click to change" class="name pointer" value="${p.name}" readonly />
|
||||
<svg data-tip="Click to show and edit province emblem" class="coaIcon pointer hide" viewBox="0 0 200 200"><use href="#provinceCOA${p.i}"></use></svg>
|
||||
<input data-tip="Province form name. Click to change" class="name pointer hide" value="${p.formName}" readonly />
|
||||
<span data-tip="Province capital. Click to zoom into view" class="icon-star-empty pointer hide ${p.burg ? "" : "placeholder"}"></span>
|
||||
<svg data-tip="Click to show and edit province emblem" class="coaIcon pointer hide" viewBox="0 0 200 200"><use href="#provinceCOA${
|
||||
p.i
|
||||
}"></use></svg>
|
||||
<input data-tip="Province form name. Click to change" class="name pointer hide" value="${
|
||||
p.formName
|
||||
}" readonly />
|
||||
<span data-tip="Province capital. Click to zoom into view" class="icon-star-empty pointer hide ${
|
||||
p.burg ? "" : "placeholder"
|
||||
}"></span>
|
||||
<select
|
||||
data-tip="Province capital. Click to select from burgs within the state. No capital means the province is governed from the state capital"
|
||||
class="cultureBase hide ${p.burgs.length ? "" : "placeholder"}"
|
||||
|
|
@ -164,7 +172,7 @@ function editProvinces() {
|
|||
class="icon-flag-empty ${separable ? "" : "placeholder"} hide"
|
||||
></span>
|
||||
<span data-tip="Toggle province focus" class="icon-pin ${focused ? "" : " inactive"} hide"></span>
|
||||
<span data-tip="Lock the province" class="icon-lock${p.lock ? '' : '-open'} hide"></span>
|
||||
<span data-tip="Lock the province" class="icon-lock${p.lock ? "" : "-open"} hide"></span>
|
||||
<span data-tip="Remove the province" class="icon-trash-empty hide"></span>
|
||||
</div>`;
|
||||
}
|
||||
|
|
@ -193,7 +201,9 @@ function editProvinces() {
|
|||
|
||||
function getCapitalOptions(burgs, capital) {
|
||||
let options = "";
|
||||
burgs.forEach(b => (options += `<option ${b === capital ? "selected" : ""} value="${b}">${pack.burgs[b].name}</option>`));
|
||||
burgs.forEach(
|
||||
b => (options += `<option ${b === capital ? "selected" : ""} value="${b}">${pack.burgs[b].name}</option>`)
|
||||
);
|
||||
return options;
|
||||
}
|
||||
|
||||
|
|
@ -267,7 +277,11 @@ function editProvinces() {
|
|||
const {name, burg: burgId, burgs: provinceBurgs} = province;
|
||||
|
||||
if (provinceBurgs.some(b => burgs[b].capital))
|
||||
return tip("Cannot declare independence of a province having capital burg. Please change capital first", false, "error");
|
||||
return tip(
|
||||
"Cannot declare independence of a province having capital burg. Please change capital first",
|
||||
false,
|
||||
"error"
|
||||
);
|
||||
if (!burgId) return tip("Cannot declare independence of a province without burg", false, "error");
|
||||
|
||||
const oldStateId = province.state;
|
||||
|
|
@ -313,7 +327,10 @@ function editProvinces() {
|
|||
return relations;
|
||||
});
|
||||
diplomacy.push("x");
|
||||
states[0].diplomacy.push([`Independance declaration`, `${name} declared its independance from ${states[oldStateId].name}`]);
|
||||
states[0].diplomacy.push([
|
||||
`Independance declaration`,
|
||||
`${name} declared its independance from ${states[oldStateId].name}`
|
||||
]);
|
||||
|
||||
// create new state
|
||||
states.push({
|
||||
|
|
@ -348,7 +365,7 @@ function editProvinces() {
|
|||
|
||||
BurgsAndStates.collectStatistics();
|
||||
BurgsAndStates.defineStateForms(newStates);
|
||||
BurgsAndStates.drawStateLabels(allStates);
|
||||
drawStateLabels(allStates);
|
||||
|
||||
// redraw emblems
|
||||
allStates.forEach(stateId => {
|
||||
|
|
@ -375,8 +392,12 @@ function editProvinces() {
|
|||
const l = n => Number(n).toLocaleString();
|
||||
|
||||
alertMessage.innerHTML = /* html */ ` Rural: <input type="number" min="0" step="1" id="ruralPop" value=${rural} style="width:6em" /> Urban:
|
||||
<input type="number" min="0" step="1" id="urbanPop" value=${urban} style="width:6em" ${p.burgs.length ? "" : "disabled"} />
|
||||
<p>Total population: ${l(total)} ⇒ <span id="totalPop">${l(total)}</span> (<span id="totalPopPerc">100</span>%)</p>`;
|
||||
<input type="number" min="0" step="1" id="urbanPop" value=${urban} style="width:6em" ${
|
||||
p.burgs.length ? "" : "disabled"
|
||||
} />
|
||||
<p>Total population: ${l(total)} ⇒ <span id="totalPop">${l(
|
||||
total
|
||||
)}</span> (<span id="totalPopPerc">100</span>%)</p>`;
|
||||
|
||||
const update = function () {
|
||||
const totalNew = ruralPop.valueAsNumber + urbanPop.valueAsNumber;
|
||||
|
|
@ -694,7 +715,13 @@ function editProvinces() {
|
|||
|
||||
function updateChart() {
|
||||
const value =
|
||||
this.value === "area" ? d => d.area : this.value === "rural" ? d => d.rural : this.value === "urban" ? d => d.urban : d => d.rural + d.urban;
|
||||
this.value === "area"
|
||||
? d => d.area
|
||||
: this.value === "rural"
|
||||
? d => d.rural
|
||||
: this.value === "urban"
|
||||
? d => d.urban
|
||||
: d => d.rural + d.urban;
|
||||
|
||||
root.sum(value);
|
||||
node.data(treeLayout(root).leaves());
|
||||
|
|
@ -776,7 +803,13 @@ function editProvinces() {
|
|||
|
||||
customization = 11;
|
||||
provs.select("g#provincesBody").append("g").attr("id", "temp");
|
||||
provs.select("g#provincesBody").append("g").attr("id", "centers").attr("fill", "none").attr("stroke", "#ff0000").attr("stroke-width", 1);
|
||||
provs
|
||||
.select("g#provincesBody")
|
||||
.append("g")
|
||||
.attr("id", "centers")
|
||||
.attr("fill", "none")
|
||||
.attr("stroke", "#ff0000")
|
||||
.attr("stroke-width", 1);
|
||||
|
||||
document.querySelectorAll("#provincesBottom > *").forEach(el => (el.style.display = "none"));
|
||||
document.getElementById("provincesManuallyButtons").style.display = "inline-block";
|
||||
|
|
@ -788,7 +821,11 @@ function editProvinces() {
|
|||
$("#provincesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
|
||||
|
||||
tip("Click on a province to select, drag the circle to change province", true);
|
||||
viewbox.style("cursor", "crosshair").on("click", selectProvinceOnMapClick).call(d3.drag().on("start", dragBrush)).on("touchmove mousemove", moveBrush);
|
||||
viewbox
|
||||
.style("cursor", "crosshair")
|
||||
.on("click", selectProvinceOnMapClick)
|
||||
.call(d3.drag().on("start", dragBrush))
|
||||
.on("touchmove mousemove", moveBrush);
|
||||
|
||||
body.querySelector("div").classList.add("selected");
|
||||
selectProvince(+body.querySelector("div").dataset.id);
|
||||
|
|
@ -859,7 +896,11 @@ function editProvinces() {
|
|||
if (i === pack.provinces[provinceOld].center) {
|
||||
const center = centers.select("polygon[data-center='" + i + "']");
|
||||
if (!center.size()) centers.append("polygon").attr("data-center", i).attr("points", getPackPolygon(i));
|
||||
tip("Province center cannot be assigned to a different region. Please remove the province first", false, "error");
|
||||
tip(
|
||||
"Province center cannot be assigned to a different region. Please remove the province first",
|
||||
false,
|
||||
"error"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -921,7 +962,8 @@ function editProvinces() {
|
|||
provincesHeader.querySelector("div[data-sortby='state']").style.left = "22em";
|
||||
provincesFooter.style.display = "block";
|
||||
body.querySelectorAll("div > input, select, span, svg").forEach(e => (e.style.pointerEvents = "all"));
|
||||
if (!close) $("#provincesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
|
||||
if (!close)
|
||||
$("#provincesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
|
||||
|
||||
restoreDefaultEvents();
|
||||
clearMainTip();
|
||||
|
|
@ -943,14 +985,20 @@ function editProvinces() {
|
|||
const {cells, provinces} = pack;
|
||||
const point = d3.mouse(this);
|
||||
const center = findCell(point[0], point[1]);
|
||||
if (cells.h[center] < 20) return tip("You cannot place province into the water. Please click on a land cell", false, "error");
|
||||
if (cells.h[center] < 20)
|
||||
return tip("You cannot place province into the water. Please click on a land cell", false, "error");
|
||||
|
||||
const oldProvince = cells.province[center];
|
||||
if (oldProvince && provinces[oldProvince].center === center)
|
||||
return tip("The cell is already a center of a different province. Select other cell", false, "error");
|
||||
|
||||
const state = cells.state[center];
|
||||
if (!state) return tip("You cannot create a province in neutral lands. Please assign this land to a state first", false, "error");
|
||||
if (!state)
|
||||
return tip(
|
||||
"You cannot create a province in neutral lands. Please assign this land to a state first",
|
||||
false,
|
||||
"error"
|
||||
);
|
||||
|
||||
if (d3.event.shiftKey === false) exitAddProvinceMode();
|
||||
|
||||
|
|
@ -1016,7 +1064,10 @@ function editProvinces() {
|
|||
|
||||
function downloadProvincesData() {
|
||||
const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value;
|
||||
let data = "Id,Province,Full Name,Form,State,Color,Capital,Area " + unit + ",Total Population,Rural Population,Urban Population\n"; // headers
|
||||
let data =
|
||||
"Id,Province,Full Name,Form,State,Color,Capital,Area " +
|
||||
unit +
|
||||
",Total Population,Rural Population,Urban Population\n"; // headers
|
||||
|
||||
body.querySelectorAll(":scope > div").forEach(function (el) {
|
||||
const key = parseInt(el.dataset.id);
|
||||
|
|
|
|||
|
|
@ -76,9 +76,22 @@ function selectStyleElement() {
|
|||
|
||||
// stroke color and width
|
||||
if (
|
||||
["armies", "routes", "lakes", "borders", "cults", "relig", "cells", "coastline", "prec", "ice", "icons", "coordinates", "zones", "gridOverlay"].includes(
|
||||
sel
|
||||
)
|
||||
[
|
||||
"armies",
|
||||
"routes",
|
||||
"lakes",
|
||||
"borders",
|
||||
"cults",
|
||||
"relig",
|
||||
"cells",
|
||||
"coastline",
|
||||
"prec",
|
||||
"ice",
|
||||
"icons",
|
||||
"coordinates",
|
||||
"zones",
|
||||
"gridOverlay"
|
||||
].includes(sel)
|
||||
) {
|
||||
styleStroke.style.display = "block";
|
||||
styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke");
|
||||
|
|
@ -87,14 +100,29 @@ function selectStyleElement() {
|
|||
}
|
||||
|
||||
// stroke dash
|
||||
if (["routes", "borders", "temperature", "legend", "population", "coordinates", "zones", "gridOverlay"].includes(sel)) {
|
||||
if (
|
||||
["routes", "borders", "temperature", "legend", "population", "coordinates", "zones", "gridOverlay"].includes(sel)
|
||||
) {
|
||||
styleStrokeDash.style.display = "block";
|
||||
styleStrokeDasharrayInput.value = el.attr("stroke-dasharray") || "";
|
||||
styleStrokeLinecapInput.value = el.attr("stroke-linecap") || "inherit";
|
||||
}
|
||||
|
||||
// clipping
|
||||
if (["cells", "gridOverlay", "coordinates", "compass", "terrain", "temperature", "routes", "texture", "biomes", "zones"].includes(sel)) {
|
||||
if (
|
||||
[
|
||||
"cells",
|
||||
"gridOverlay",
|
||||
"coordinates",
|
||||
"compass",
|
||||
"terrain",
|
||||
"temperature",
|
||||
"routes",
|
||||
"texture",
|
||||
"biomes",
|
||||
"zones"
|
||||
].includes(sel)
|
||||
) {
|
||||
styleClipping.style.display = "block";
|
||||
styleClippingInput.value = el.attr("mask") || "";
|
||||
}
|
||||
|
|
@ -142,8 +170,12 @@ function selectStyleElement() {
|
|||
|
||||
if (sel === "population") {
|
||||
stylePopulation.style.display = "block";
|
||||
stylePopulationRuralStrokeInput.value = stylePopulationRuralStrokeOutput.value = population.select("#rural").attr("stroke");
|
||||
stylePopulationUrbanStrokeInput.value = stylePopulationUrbanStrokeOutput.value = population.select("#urban").attr("stroke");
|
||||
stylePopulationRuralStrokeInput.value = stylePopulationRuralStrokeOutput.value = population
|
||||
.select("#rural")
|
||||
.attr("stroke");
|
||||
stylePopulationUrbanStrokeInput.value = stylePopulationUrbanStrokeOutput.value = population
|
||||
.select("#urban")
|
||||
.attr("stroke");
|
||||
styleStrokeWidth.style.display = "block";
|
||||
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || "";
|
||||
}
|
||||
|
|
@ -233,7 +265,8 @@ function selectStyleElement() {
|
|||
styleOcean.style.display = "block";
|
||||
styleOceanFill.value = styleOceanFillOutput.value = oceanLayers.select("#oceanBase").attr("fill");
|
||||
styleOceanPattern.value = document.getElementById("oceanicPattern")?.getAttribute("href");
|
||||
styleOceanPatternOpacity.value = styleOceanPatternOpacityOutput.value = document.getElementById("oceanicPattern").getAttribute("opacity") || 1;
|
||||
styleOceanPatternOpacity.value = styleOceanPatternOpacityOutput.value =
|
||||
document.getElementById("oceanicPattern").getAttribute("opacity") || 1;
|
||||
outlineLayers.value = oceanLayers.attr("layers");
|
||||
}
|
||||
|
||||
|
|
@ -334,8 +367,9 @@ styleFilterInput.addEventListener("change", function () {
|
|||
});
|
||||
|
||||
styleTextureInput.addEventListener("change", function () {
|
||||
if (this.value === "none") texture.select("image").attr("xlink:href", "");
|
||||
else getBase64(this.value, base64 => texture.select("image").attr("xlink:href", base64));
|
||||
texture.select("image").attr("src", this.value);
|
||||
if (layerIsOn("toggleTexture")) texture.select("image").attr("href", this.value);
|
||||
zoom.scaleBy(svg, 1.00001);
|
||||
});
|
||||
|
||||
styleTextureShiftX.addEventListener("input", function () {
|
||||
|
|
@ -551,7 +585,10 @@ styleFontAdd.addEventListener("click", function () {
|
|||
|
||||
if (!family) return tip("Please provide a font name", false, "error");
|
||||
|
||||
const existingFont = method === "fontURL" ? fonts.find(font => font.family === family && font.src === src) : fonts.find(font => font.family === family);
|
||||
const existingFont =
|
||||
method === "fontURL"
|
||||
? fonts.find(font => font.family === family && font.src === src)
|
||||
: fonts.find(font => font.family === family);
|
||||
if (existingFont) return tip("The font is already added", false, "error");
|
||||
|
||||
if (method === "fontURL") addWebFont(family, src);
|
||||
|
|
@ -726,17 +763,17 @@ function textureProvideURL() {
|
|||
buttons: {
|
||||
Apply: function () {
|
||||
const name = textureURL.value.split("/").pop();
|
||||
if (!name || name === "") {
|
||||
tip("Please provide a valid URL", false, "error");
|
||||
return;
|
||||
}
|
||||
if (!name || name === "") return tip("Please provide a valid URL", false, "error");
|
||||
|
||||
const opt = document.createElement("option");
|
||||
opt.value = textureURL.value;
|
||||
opt.text = name.slice(0, 20);
|
||||
styleTextureInput.add(opt);
|
||||
styleTextureInput.value = textureURL.value;
|
||||
getBase64(textureURL.value, base64 => texture.select("image").attr("xlink:href", base64));
|
||||
zoom.scaleBy(svg, 1.00001); // enforce browser re-draw
|
||||
|
||||
const image = texture.select("image");
|
||||
image.attr("src", textureURL.value);
|
||||
if (layerIsOn("toggleTexture")) image.attr("href", textureURL.value);
|
||||
$(this).dialog("close");
|
||||
},
|
||||
Cancel: function () {
|
||||
|
|
|
|||
|
|
@ -89,6 +89,10 @@ function applyStyle(style) {
|
|||
} else {
|
||||
el.setAttribute(attribute, value);
|
||||
}
|
||||
|
||||
if (layerIsOn("toggleTexture") && selector === "#textureImage" && attribute === "src") {
|
||||
el.setAttribute("href", value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -225,7 +229,7 @@ function addStylePreset() {
|
|||
"#ice": ["opacity", "fill", "stroke", "stroke-width", "filter"],
|
||||
"#emblems": ["opacity", "stroke-width", "filter"],
|
||||
"#texture": ["opacity", "filter", "mask"],
|
||||
"#textureImage": ["x", "y"],
|
||||
"#textureImage": ["x", "y", "src"],
|
||||
"#zones": ["opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter", "mask"],
|
||||
"#oceanLayers": ["filter", "layers"],
|
||||
"#oceanBase": ["fill"],
|
||||
|
|
|
|||
|
|
@ -48,7 +48,10 @@ function showBurgTemperatureGraph(id) {
|
|||
// Standard deviation for average temperature for the year from [0, 1] to [min, max]
|
||||
const yearSig = lstOut[0] * 62.9466411977018 + 0.28613807855649165;
|
||||
// Standard deviation for the difference between the minimum and maximum temperatures for the year
|
||||
const yearDelTmpSig = lstOut[1] * 13.541688670361175 + 0.1414213562373084 > yearSig ? yearSig : lstOut[1] * 13.541688670361175 + 0.1414213562373084;
|
||||
const yearDelTmpSig =
|
||||
lstOut[1] * 13.541688670361175 + 0.1414213562373084 > yearSig
|
||||
? yearSig
|
||||
: lstOut[1] * 13.541688670361175 + 0.1414213562373084;
|
||||
// Expected value for the difference between the minimum and maximum temperatures for the year
|
||||
const yearDelTmpMu = lstOut[2] * 15.266666666666667 + 0.6416666666666663;
|
||||
|
||||
|
|
@ -67,7 +70,20 @@ function showBurgTemperatureGraph(id) {
|
|||
const year = new Date().getFullYear(); // use current year
|
||||
const startDate = new Date(year, 0, 1);
|
||||
const endDate = new Date(year, 11, 31);
|
||||
const months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
||||
const months = [
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December"
|
||||
];
|
||||
|
||||
const xscale = d3.scaleTime().domain([startDate, endDate]).range([0, chartWidth]);
|
||||
const yscale = d3.scaleLinear().domain([minT, maxT]).range([chartHeight, 0]);
|
||||
|
|
@ -91,7 +107,11 @@ function showBurgTemperatureGraph(id) {
|
|||
});
|
||||
|
||||
drawGraph();
|
||||
$("#alert").dialog({title: "Annual temperature in " + b.name, width: "auto", position: {my: "center", at: "center", of: "svg"}});
|
||||
$("#alert").dialog({
|
||||
title: "Annual temperature in " + b.name,
|
||||
width: "auto",
|
||||
position: {my: "center", at: "center", of: "svg"}
|
||||
});
|
||||
|
||||
function drawGraph() {
|
||||
alertMessage.innerHTML = "";
|
||||
|
|
@ -109,11 +129,26 @@ function showBurgTemperatureGraph(id) {
|
|||
const legendX = n => (chartWidth * n) / 4;
|
||||
const legendTextX = n => legendX(n) + 10;
|
||||
legend.append("circle").attr("cx", legendX(1)).attr("cy", legendY).attr("r", 4).style("fill", "red");
|
||||
legend.append("text").attr("x", legendTextX(1)).attr("y", legendY).attr("alignment-baseline", "central").text("Day temperature");
|
||||
legend
|
||||
.append("text")
|
||||
.attr("x", legendTextX(1))
|
||||
.attr("y", legendY)
|
||||
.attr("alignment-baseline", "central")
|
||||
.text("Day temperature");
|
||||
legend.append("circle").attr("cx", legendX(2)).attr("cy", legendY).attr("r", 4).style("fill", "orange");
|
||||
legend.append("text").attr("x", legendTextX(2)).attr("y", legendY).attr("alignment-baseline", "central").text("Mean temperature");
|
||||
legend
|
||||
.append("text")
|
||||
.attr("x", legendTextX(2))
|
||||
.attr("y", legendY)
|
||||
.attr("alignment-baseline", "central")
|
||||
.text("Mean temperature");
|
||||
legend.append("circle").attr("cx", legendX(3)).attr("cy", legendY).attr("r", 4).style("fill", "blue");
|
||||
legend.append("text").attr("x", legendTextX(3)).attr("y", legendY).attr("alignment-baseline", "central").text("Night temperature");
|
||||
legend
|
||||
.append("text")
|
||||
.attr("x", legendTextX(3))
|
||||
.attr("y", legendY)
|
||||
.attr("alignment-baseline", "central")
|
||||
.text("Night temperature");
|
||||
|
||||
const xGrid = d3.axisBottom(xscale).ticks().tickSize(-chartHeight);
|
||||
const yGrid = d3.axisLeft(yscale).ticks(5).tickSize(-chartWidth);
|
||||
|
|
@ -135,7 +170,10 @@ function showBurgTemperatureGraph(id) {
|
|||
}
|
||||
|
||||
const xAxis = d3.axisBottom(xscale).ticks().tickFormat(d3.timeFormat("%B"));
|
||||
const yAxis = d3.axisLeft(yscale).ticks(5).tickFormat(convertTemperature);
|
||||
const yAxis = d3
|
||||
.axisLeft(yscale)
|
||||
.ticks(5)
|
||||
.tickFormat(v => convertTemperature(v));
|
||||
|
||||
const axis = chart.append("g");
|
||||
axis
|
||||
|
|
@ -146,9 +184,24 @@ function showBurgTemperatureGraph(id) {
|
|||
axis.select("path.domain").attr("d", `M0.5,0.5 H${chartWidth + 0.5}`);
|
||||
|
||||
const curves = chart.append("g").attr("fill", "none").style("stroke-width", 2.5);
|
||||
curves.append("path").attr("d", getCurve(tempMean)).attr("data-type", "daily").attr("stroke", "orange").on("mousemove", printVal);
|
||||
curves.append("path").attr("d", getCurve(tempMin)).attr("data-type", "night").attr("stroke", "blue").on("mousemove", printVal);
|
||||
curves.append("path").attr("d", getCurve(tempMax)).attr("data-type", "day").attr("stroke", "red").on("mousemove", printVal);
|
||||
curves
|
||||
.append("path")
|
||||
.attr("d", getCurve(tempMean))
|
||||
.attr("data-type", "daily")
|
||||
.attr("stroke", "orange")
|
||||
.on("mousemove", printVal);
|
||||
curves
|
||||
.append("path")
|
||||
.attr("d", getCurve(tempMin))
|
||||
.attr("data-type", "night")
|
||||
.attr("stroke", "blue")
|
||||
.on("mousemove", printVal);
|
||||
curves
|
||||
.append("path")
|
||||
.attr("d", getCurve(tempMax))
|
||||
.attr("data-type", "day")
|
||||
.attr("stroke", "red")
|
||||
.on("mousemove", printVal);
|
||||
|
||||
function printVal() {
|
||||
const [x, y] = d3.mouse(this);
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ toolsContent.addEventListener("click", function (event) {
|
|||
});
|
||||
|
||||
function processFeatureRegeneration(event, button) {
|
||||
if (button === "regenerateStateLabels") BurgsAndStates.drawStateLabels();
|
||||
if (button === "regenerateStateLabels") drawStateLabels();
|
||||
else if (button === "regenerateReliefIcons") {
|
||||
ReliefIcons();
|
||||
if (!layerIsOn("toggleRelief")) toggleRelief();
|
||||
|
|
@ -154,7 +154,7 @@ function regenerateStates() {
|
|||
layerIsOn("toggleBorders") ? drawBorders() : toggleBorders();
|
||||
if (layerIsOn("toggleProvinces")) drawProvinces();
|
||||
|
||||
BurgsAndStates.drawStateLabels();
|
||||
drawStateLabels();
|
||||
Military.generate();
|
||||
if (layerIsOn("toggleEmblems")) drawEmblems();
|
||||
|
||||
|
|
@ -570,9 +570,8 @@ function addLabelOnClick() {
|
|||
.attr("data-size", 18)
|
||||
.attr("filter", null);
|
||||
|
||||
const example = group.append("text").attr("x", 0).attr("x", 0).text(name);
|
||||
const example = group.append("text").attr("x", 0).attr("y", 0).text(name);
|
||||
const width = example.node().getBBox().width;
|
||||
const x = width / -2; // x offset;
|
||||
example.remove();
|
||||
|
||||
group.classed("hidden", false);
|
||||
|
|
@ -584,7 +583,7 @@ function addLabelOnClick() {
|
|||
.attr("startOffset", "50%")
|
||||
.attr("font-size", "100%")
|
||||
.append("tspan")
|
||||
.attr("x", x)
|
||||
.attr("x", 0)
|
||||
.text(name);
|
||||
|
||||
defs
|
||||
|
|
@ -828,9 +827,17 @@ function addMarkerOnClick() {
|
|||
// Find the currently selected marker to use as a base
|
||||
const isMarkerSelected = markers.length && elSelected?.node()?.parentElement?.id === "markers";
|
||||
const selectedMarker = isMarkerSelected ? markers.find(marker => marker.i === +elSelected.attr("id").slice(6)) : null;
|
||||
const baseMarker = selectedMarker || {icon: "❓"};
|
||||
|
||||
const selectedType = document.getElementById("addedMarkerType").value;
|
||||
const selectedConfig = Markers.getConfig().find(({type}) => type === selectedType);
|
||||
|
||||
const baseMarker = selectedMarker || selectedConfig || {icon: "❓"};
|
||||
const marker = Markers.add({...baseMarker, x, y, cell});
|
||||
|
||||
if (selectedConfig && selectedConfig.add) {
|
||||
selectedConfig.add("marker" + marker.i, cell);
|
||||
}
|
||||
|
||||
const markersElement = document.getElementById("markers");
|
||||
const rescale = +markersElement.getAttribute("rescale");
|
||||
markersElement.insertAdjacentHTML("beforeend", drawMarker(marker, rescale));
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
function editWorld() {
|
||||
if (customization) return;
|
||||
|
||||
$("#worldConfigurator").dialog({
|
||||
title: "Configure World",
|
||||
resizable: false,
|
||||
|
|
@ -8,8 +9,7 @@ function editWorld() {
|
|||
"Whole World": () => applyWorldPreset(100, 50),
|
||||
Northern: () => applyWorldPreset(33, 25),
|
||||
Tropical: () => applyWorldPreset(33, 50),
|
||||
Southern: () => applyWorldPreset(33, 75),
|
||||
"Restore Winds": restoreDefaultWinds
|
||||
Southern: () => applyWorldPreset(33, 75)
|
||||
},
|
||||
open: function () {
|
||||
const buttons = $(this).dialog("widget").find(".ui-dialog-buttonset > button");
|
||||
|
|
@ -17,7 +17,6 @@ function editWorld() {
|
|||
buttons[1].addEventListener("mousemove", () => tip("Click to set map size to cover the Northern latitudes"));
|
||||
buttons[2].addEventListener("mousemove", () => tip("Click to set map size to cover the Tropical latitudes"));
|
||||
buttons[3].addEventListener("mousemove", () => tip("Click to set map size to cover the Southern latitudes"));
|
||||
buttons[4].addEventListener("mousemove", () => tip("Click to restore default wind directions"));
|
||||
},
|
||||
close: function () {
|
||||
$(this).dialog("destroy");
|
||||
|
|
@ -25,28 +24,56 @@ function editWorld() {
|
|||
});
|
||||
|
||||
const globe = d3.select("#globe");
|
||||
const clr = d3.scaleSequential(d3.interpolateSpectral);
|
||||
const tMax = 30,
|
||||
tMin = -25; // temperature extremes
|
||||
const projection = d3.geoOrthographic().translate([100, 100]).scale(100);
|
||||
const path = d3.geoPath(projection);
|
||||
|
||||
updateInputValues();
|
||||
updateGlobeTemperature();
|
||||
updateGlobePosition();
|
||||
|
||||
if (modules.editWorld) return;
|
||||
modules.editWorld = true;
|
||||
|
||||
document.getElementById("worldControls").addEventListener("input", e => updateWorld(e.target));
|
||||
byId("worldControls").addEventListener("input", e => updateWorld(e.target));
|
||||
globe.select("#globeWindArrows").on("click", changeWind);
|
||||
globe.select("#globeGraticule").attr("d", round(path(d3.geoGraticule()()))); // globe graticule
|
||||
updateWindDirections();
|
||||
|
||||
byId("restoreWinds").addEventListener("click", restoreDefaultWinds);
|
||||
|
||||
function updateInputValues() {
|
||||
byId("temperatureEquatorInput").value = options.temperatureEquator;
|
||||
byId("temperatureEquatorOutput").value = options.temperatureEquator;
|
||||
byId("temperatureEquatorF").innerText = convertTemperature(options.temperatureEquator, "°F");
|
||||
|
||||
byId("temperatureNorthPoleInput").value = options.temperatureNorthPole;
|
||||
byId("temperatureNorthPoleOutput").value = options.temperatureNorthPole;
|
||||
byId("temperatureNorthPoleF").innerText = convertTemperature(options.temperatureNorthPole, "°F");
|
||||
|
||||
byId("temperatureSouthPoleInput").value = options.temperatureSouthPole;
|
||||
byId("temperatureSouthPoleOutput").value = options.temperatureSouthPole;
|
||||
byId("temperatureSouthPoleF").innerText = convertTemperature(options.temperatureSouthPole, "°F");
|
||||
}
|
||||
|
||||
function updateWorld(el) {
|
||||
if (el) {
|
||||
document.getElementById(el.dataset.stored + "Input").value = el.value;
|
||||
document.getElementById(el.dataset.stored + "Output").value = el.value;
|
||||
if (el.dataset.stored) lock(el.dataset.stored);
|
||||
if (el?.dataset.stored) {
|
||||
const stored = el.dataset.stored;
|
||||
byId(stored + "Input").value = el.value;
|
||||
byId(stored + "Output").value = el.value;
|
||||
lock(el.dataset.stored);
|
||||
|
||||
if (stored === "temperatureEquator") {
|
||||
options.temperatureEquator = Number(el.value);
|
||||
byId("temperatureEquatorF").innerText = convertTemperature(options.temperatureEquator, "°F");
|
||||
}
|
||||
if (stored === "temperatureNorthPole") {
|
||||
options.temperatureNorthPole = Number(el.value);
|
||||
byId("temperatureNorthPoleF").innerText = convertTemperature(options.temperatureNorthPole, "°F");
|
||||
}
|
||||
if (stored === "temperatureSouthPole") {
|
||||
options.temperatureSouthPole = Number(el.value);
|
||||
byId("temperatureSouthPoleF").innerText = convertTemperature(options.temperatureSouthPole, "°F");
|
||||
}
|
||||
}
|
||||
|
||||
updateGlobeTemperature();
|
||||
|
|
@ -58,18 +85,18 @@ function editWorld() {
|
|||
Lakes.defineGroup();
|
||||
Rivers.specify();
|
||||
pack.cells.h = new Float32Array(heights);
|
||||
defineBiomes();
|
||||
Biomes.define();
|
||||
|
||||
if (layerIsOn("toggleTemp")) drawTemp();
|
||||
if (layerIsOn("togglePrec")) drawPrec();
|
||||
if (layerIsOn("toggleBiomes")) drawBiomes();
|
||||
if (layerIsOn("toggleCoordinates")) drawCoordinates();
|
||||
if (layerIsOn("toggleRivers")) drawRivers();
|
||||
if (document.getElementById("canvas3d")) setTimeout(ThreeD.update(), 500);
|
||||
if (byId("canvas3d")) setTimeout(ThreeD.update(), 500);
|
||||
}
|
||||
|
||||
function updateGlobePosition() {
|
||||
const size = +document.getElementById("mapSizeOutput").value;
|
||||
const size = +byId("mapSizeOutput").value;
|
||||
const eqD = ((graphHeight / 2) * 100) / size;
|
||||
|
||||
calculateMapCoordinates();
|
||||
|
|
@ -77,12 +104,12 @@ function editWorld() {
|
|||
const scale = +distanceScaleInput.value;
|
||||
const unit = distanceUnitInput.value;
|
||||
const meridian = toKilometer(eqD * 2 * scale);
|
||||
document.getElementById("mapSize").innerHTML = `${graphWidth}x${graphHeight}`;
|
||||
document.getElementById("mapSizeFriendly").innerHTML = `${rn(graphWidth * scale)}x${rn(graphHeight * scale)} ${unit}`;
|
||||
document.getElementById("meridianLength").innerHTML = rn(eqD * 2);
|
||||
document.getElementById("meridianLengthFriendly").innerHTML = `${rn(eqD * 2 * scale)} ${unit}`;
|
||||
document.getElementById("meridianLengthEarth").innerHTML = meridian ? " = " + rn(meridian / 200) + "%🌏" : "";
|
||||
document.getElementById("mapCoordinates").innerHTML = `${lat(mc.latN)} ${Math.abs(rn(mc.lonW))}°W; ${lat(mc.latS)} ${rn(mc.lonE)}°E`;
|
||||
byId("mapSize").innerHTML = `${graphWidth}x${graphHeight}`;
|
||||
byId("mapSizeFriendly").innerHTML = `${rn(graphWidth * scale)}x${rn(graphHeight * scale)} ${unit}`;
|
||||
byId("meridianLength").innerHTML = rn(eqD * 2);
|
||||
byId("meridianLengthFriendly").innerHTML = `${rn(eqD * 2 * scale)} ${unit}`;
|
||||
byId("meridianLengthEarth").innerHTML = meridian ? " = " + rn(meridian / 200) + "%🌏" : "";
|
||||
byId("mapCoordinates").innerHTML = `${lat(mc.latN)} ${Math.abs(rn(mc.lonW))}°W; ${lat(mc.latS)} ${rn(mc.lonE)}°E`;
|
||||
|
||||
function toKilometer(v) {
|
||||
if (unit === "km") return v;
|
||||
|
|
@ -104,15 +131,24 @@ function editWorld() {
|
|||
globe.select("#globeArea").attr("d", round(path(area.outline()))); // map area
|
||||
}
|
||||
|
||||
// update temperatures on globe (visual-only)
|
||||
function updateGlobeTemperature() {
|
||||
const tEq = +document.getElementById("temperatureEquatorOutput").value;
|
||||
document.getElementById("temperatureEquatorF").innerHTML = rn((tEq * 9) / 5 + 32);
|
||||
const tPole = +document.getElementById("temperaturePoleOutput").value;
|
||||
document.getElementById("temperaturePoleF").innerHTML = rn((tPole * 9) / 5 + 32);
|
||||
globe.selectAll(".tempGradient90").attr("stop-color", clr(1 - (tPole - tMin) / (tMax - tMin)));
|
||||
globe.selectAll(".tempGradient60").attr("stop-color", clr(1 - (tEq - ((tEq - tPole) * 2) / 3 - tMin) / (tMax - tMin)));
|
||||
globe.selectAll(".tempGradient30").attr("stop-color", clr(1 - (tEq - ((tEq - tPole) * 1) / 3 - tMin) / (tMax - tMin)));
|
||||
globe.select(".tempGradient0").attr("stop-color", clr(1 - (tEq - tMin) / (tMax - tMin)));
|
||||
const tEq = options.temperatureEquator;
|
||||
const tNP = options.temperatureNorthPole;
|
||||
const tSP = options.temperatureSouthPole;
|
||||
|
||||
const scale = d3.scaleSequential(d3.interpolateSpectral);
|
||||
const getColor = value => scale(1 - value);
|
||||
const [tMin, tMax] = [-25, 30]; // temperature extremes
|
||||
const tDelta = tMax - tMin;
|
||||
|
||||
globe.select("#grad90").attr("stop-color", getColor((tNP - tMin) / tDelta));
|
||||
globe.select("#grad60").attr("stop-color", getColor((tEq - ((tEq - tNP) * 2) / 3 - tMin) / tDelta));
|
||||
globe.select("#grad30").attr("stop-color", getColor((tEq - ((tEq - tNP) * 1) / 4 - tMin) / tDelta));
|
||||
globe.select("#grad0").attr("stop-color", getColor((tEq - tMin) / tDelta));
|
||||
globe.select("#grad-30").attr("stop-color", getColor((tEq - ((tEq - tSP) * 1) / 4 - tMin) / tDelta));
|
||||
globe.select("#grad-60").attr("stop-color", getColor((tEq - ((tEq - tSP) * 2) / 3 - tMin) / tDelta));
|
||||
globe.select("#grad-90").attr("stop-color", getColor((tSP - tMin) / tDelta));
|
||||
}
|
||||
|
||||
function updateWindDirections() {
|
||||
|
|
@ -146,8 +182,8 @@ function editWorld() {
|
|||
}
|
||||
|
||||
function applyWorldPreset(size, lat) {
|
||||
document.getElementById("mapSizeInput").value = document.getElementById("mapSizeOutput").value = size;
|
||||
document.getElementById("latitudeInput").value = document.getElementById("latitudeOutput").value = lat;
|
||||
byId("mapSizeInput").value = byId("mapSizeOutput").value = size;
|
||||
byId("latitudeInput").value = byId("latitudeOutput").value = lat;
|
||||
lock("mapSize");
|
||||
lock("latitude");
|
||||
updateWorld();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue