refactor: start files migration nightmare

This commit is contained in:
Azgaar 2022-06-25 00:47:48 +03:00
parent c4736cc640
commit bc65e0e207
64 changed files with 1990 additions and 816 deletions

View file

@ -7771,7 +7771,6 @@
<script src="utils/commonUtils.js"></script> <script src="utils/commonUtils.js"></script>
<script src="utils/arrayUtils.js"></script> <script src="utils/arrayUtils.js"></script>
<script src="utils/colorUtils.js"></script> <script src="utils/colorUtils.js"></script>
<script src="utils/graphUtils.js"></script>
<script src="utils/nodeUtils.js"></script> <script src="utils/nodeUtils.js"></script>
<script src="utils/numberUtils.js"></script> <script src="utils/numberUtils.js"></script>
<script src="utils/polyfills.js"></script> <script src="utils/polyfills.js"></script>
@ -7783,33 +7782,39 @@
<script src="modules/voronoi.js"></script> <script src="modules/voronoi.js"></script>
<script src="config/heightmap-templates.js"></script> <script src="config/heightmap-templates.js"></script>
<script src="config/precreated-heightmaps.js"></script> <script src="config/precreated-heightmaps.js"></script>
<script src="modules/heightmap-generator.js"></script> <script type="module" src="modules/heightmap-generator.js"></script>
<script src="modules/ocean-layers.js"></script> <script type="module" src="modules/ocean-layers.js"></script>
<script src="modules/river-generator.js"></script> <script type="module" src="modules/river-generator.js"></script>
<script src="modules/lakes.js"></script> <script type="module" src="modules/lakes.js"></script>
<script src="modules/names-generator.js"></script> <script type="module" src="modules/names-generator.js"></script>
<script src="modules/cultures-generator.js"></script> <script type="module" src="modules/biomes.js"></script>
<script src="modules/burgs-and-states.js?v=1.87.04"></script> <script type="module" src="modules/cultures-generator.js"></script>
<script src="modules/routes-generator.js"></script> <script type="module" src="modules/burgs-and-states.js?v=1.87.04"></script>
<script src="modules/religions-generator.js"></script> <script type="module" src="modules/routes-generator.js"></script>
<script src="modules/military-generator.js"></script> <script type="module" src="modules/religions-generator.js"></script>
<script src="modules/markers-generator.js"></script> <script type="module" src="modules/military-generator.js"></script>
<script src="modules/coa-generator.js"></script> <script type="module" src="modules/markers-generator.js"></script>
<script type="module" src="modules/coa-generator.js"></script>
<script src="modules/submap.js"></script> <script src="modules/submap.js"></script>
<script src="libs/polylabel.min.js"></script> <script src="libs/polylabel.min.js"></script>
<script src="libs/lineclip.min.js"></script> <script src="libs/lineclip.min.js"></script>
<script src="libs/alea.min.js"></script> <script src="libs/alea.min.js"></script>
<script src="modules/fonts.js"></script> <script src="modules/fonts.js"></script>
<script src="modules/ui/layers.js"></script> <script type="module" src="modules/ui/layers.js"></script>
<script src="modules/ui/measurers.js?v=1.87.02"></script> <script src="modules/ui/measurers.js?v=1.87.02"></script>
<script src="modules/ui/stylePresets.js"></script> <script src="modules/ui/stylePresets.js"></script>
<script type="module" src="modules/ui/general.js?v=1.87.00"></script>
<script type="module" src="modules/ui/options.js?v=1.87.00"></script>
<script src="modules/ui/general.js?v=1.87.00"></script> <script src="modules/define-globals.js"></script>
<script src="modules/ui/options.js?v=1.87.00"></script> <script src="modules/define-svg.js"></script>
<script src="main.js"></script> <script src="modules/zoom.js"></script>
<script type="module" src="/src/main.ts"></script>
<script type="module" src="modules/activeZooming.js"></script>
<script defer src="modules/relief-icons.js"></script> <script defer src="modules/relief-icons.js"></script>
<script defer src="modules/ui/style.js"></script> <script type="module" src="modules/ui/style.js"></script>
<script defer src="modules/ui/editors.js?v=1.87.01"></script> <script defer src="modules/ui/editors.js?v=1.87.01"></script>
<script defer src="modules/ui/tools.js?v=1.87.03"></script> <script defer src="modules/ui/tools.js?v=1.87.03"></script>
<script defer src="modules/ui/world-configurator.js"></script> <script defer src="modules/ui/world-configurator.js"></script>
@ -7829,7 +7834,7 @@
<script defer src="modules/ui/relief-editor.js"></script> <script defer src="modules/ui/relief-editor.js"></script>
<script defer src="modules/ui/burg-editor.js"></script> <script defer src="modules/ui/burg-editor.js"></script>
<script defer src="modules/ui/units-editor.js"></script> <script defer src="modules/ui/units-editor.js"></script>
<script defer src="modules/ui/notes-editor.js"></script> <script type="module" src="modules/ui/notes-editor.js"></script>
<script defer src="modules/ui/diplomacy-editor.js"></script> <script defer src="modules/ui/diplomacy-editor.js"></script>
<script defer src="modules/ui/zones-editor.js"></script> <script defer src="modules/ui/zones-editor.js"></script>
<script defer src="modules/ui/burgs-overview.js"></script> <script defer src="modules/ui/burgs-overview.js"></script>

92
modules/activeZooming.js Normal file
View file

@ -0,0 +1,92 @@
window.handleZoom = function (isScaleChanged, isPositionChanged) {
viewbox.attr("transform", `translate(${viewX} ${viewY}) scale(${scale})`);
if (isPositionChanged) drawCoordinates();
if (isScaleChanged) {
invokeActiveZooming();
drawScaleBar(scale);
}
// zoom image converter overlay
if (customization === 1) {
const canvas = document.getElementById("canvas");
if (!canvas || canvas.style.opacity === "0") return;
const img = document.getElementById("imageToConvert");
if (!img) return;
const ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.setTransform(scale, 0, 0, scale, viewX, viewY);
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
}
};
// active zooming feature
export function invokeActiveZooming() {
if (coastline.select("#sea_island").size() && +coastline.select("#sea_island").attr("auto-filter")) {
// toggle shade/blur filter for coatline on zoom
const filter = scale > 1.5 && scale <= 2.6 ? null : scale > 2.6 ? "url(#blurFilter)" : "url(#dropShadow)";
coastline.select("#sea_island").attr("filter", filter);
}
// rescale labels on zoom
if (labels.style("display") !== "none") {
labels.selectAll("g").each(function () {
if (this.id === "burgLabels") return;
const desired = +this.dataset.size;
const relative = Math.max(rn((desired + desired / scale) / 2, 2), 1);
if (rescaleLabels.checked) this.setAttribute("font-size", relative);
const hidden = hideLabels.checked && (relative * scale < 6 || relative * scale > 60);
if (hidden) this.classList.add("hidden");
else this.classList.remove("hidden");
});
}
// rescale emblems on zoom
if (emblems.style("display") !== "none") {
emblems.selectAll("g").each(function () {
const size = this.getAttribute("font-size") * scale;
const hidden = hideEmblems.checked && (size < 25 || size > 300);
if (hidden) this.classList.add("hidden");
else this.classList.remove("hidden");
if (!hidden && window.COArenderer && this.children.length && !this.children[0].getAttribute("href"))
renderGroupCOAs(this);
});
}
// turn off ocean pattern if scale is big (improves performance)
oceanPattern
.select("rect")
.attr("fill", scale > 10 ? "#fff" : "url(#oceanic)")
.attr("opacity", scale > 10 ? 0.2 : null);
// change states halo width
if (!customization) {
const desired = +statesHalo.attr("data-width");
const haloSize = rn(desired / scale ** 0.8, 2);
statesHalo.attr("stroke-width", haloSize).style("display", haloSize > 0.1 ? "block" : "none");
}
// rescale map markers
+markers.attr("rescale") &&
pack.markers?.forEach(marker => {
const {i, x, y, size = 30, hidden} = marker;
const el = !hidden && document.getElementById(`marker${i}`);
if (!el) return;
const zoomedSize = Math.max(rn(size / 5 + 24 / scale, 2), 1);
el.setAttribute("width", zoomedSize);
el.setAttribute("height", zoomedSize);
el.setAttribute("x", rn(x - zoomedSize / 2, 1));
el.setAttribute("y", rn(y - zoomedSize, 1));
});
// rescale rulers to have always the same size
if (ruler.style("display") !== "none") {
const size = rn((10 / scale ** 0.3) * 2, 2);
ruler.selectAll("text").attr("font-size", size);
}
}

76
modules/biomes.js Normal file
View file

@ -0,0 +1,76 @@
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};
};
return {getDefault};
})();

View file

@ -1,4 +1,6 @@
"use strict"; import {TIME} from "/src/config/logging";
import {findCell} from "/src/utils/graphUtils";
import {layerIsOn} from "./ui/layers";
window.BurgsAndStates = (function () { window.BurgsAndStates = (function () {
const generate = function () { const generate = function () {

View file

@ -1,5 +1,3 @@
"use strict";
window.COA = (function () { window.COA = (function () {
const tinctures = { const tinctures = {
field: {metals: 3, colours: 4, stains: +P(0.03), patterns: 1}, field: {metals: 3, colours: 4, stains: +P(0.03), patterns: 1},
@ -305,7 +303,19 @@ window.COA = (function () {
Highland: {tower: 1, raven: 1, wolfHeadErased: 1, wolfPassant: 1, goat: 1, axe: 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}, 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}, 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}, Nomadic: {
pot: 1,
buckle: 1,
wheel: 2,
sabre: 2,
sabresCrossed: 1,
bow: 2,
arrow: 1,
horseRampant: 1,
horseSalient: 1,
crescent: 1,
camel: 3
},
Hunting: { Hunting: {
bugleHorn: 2, bugleHorn: 2,
bugleHorn2: 1, bugleHorn2: 1,
@ -322,7 +332,19 @@ window.COA = (function () {
// selection based on type // selection based on type
City: {key: 3, bell: 2, lute: 1, tower: 1, castle: 1, mallet: 1, cannon: 1, anvil: 1}, 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}, 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}, С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 // specific cases
natural: {fountain: "azure", garb: "or", raven: "sable"}, // charges to mainly use predefined colours natural: {fountain: "azure", garb: "or", raven: "sable"}, // charges to mainly use predefined colours
sinister: [ sinister: [
@ -508,7 +530,22 @@ window.COA = (function () {
}, },
// charges // charges
inescutcheon: {e: 4, jln: 1}, 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}, 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}, 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}, wolfPassant: {e: 10, def: 1, abc: 1, bdefh: 1, jlh: 1, abcpqh: 1},
@ -681,18 +718,41 @@ window.COA = (function () {
const coa = {t1}; const coa = {t1};
let charge = P(usedPattern ? 0.5 : 0.93) ? true : false; // 80% for charge 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 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 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 divisioned = rareDivided
const division = divisioned ? (parent?.division && P(kinship - 0.1) ? parent.division.division : rw(divisions.variants)) : null; ? 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) 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(charges[type])
: selectCharge();
if (division) { if (division) {
const t = getTincture("division", usedTinctures, P(0.98) ? coa.t1 : null); const t = getTincture("division", usedTinctures, P(0.98) ? coa.t1 : null);
coa.division = {division, t}; 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) { if (ordinary) {
@ -768,7 +828,14 @@ window.COA = (function () {
// counterchanged, 40% // counterchanged, 40%
else if (["perPale", "perFess", "perBend", "perBendSinister"].includes(division) && P(0.8)) { else if (["perPale", "perFess", "perBend", "perBendSinister"].includes(division) && P(0.8)) {
// place 2 charges in division standard positions // 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; coa.charges[0].p = p1;
const charge = selectCharge(charges.single); const charge = selectCharge(charges.single);

View file

@ -1,4 +1,4 @@
"use strict"; import {TIME} from "/src/config/logging";
window.Cultures = (function () { window.Cultures = (function () {
let cells; let cells;

33
modules/define-globals.js Normal file
View file

@ -0,0 +1,33 @@
"use strict";
// define global vabiable, each to be refactored and de-globalized 1-by-1
let grid = {}; // initial graph based on jittered square grid and data
let pack = {}; // packed graph and data
let seed;
let mapId;
let mapHistory = [];
let elSelected;
let notes = [];
let customization = 0;
let rulers;
let biomesData;
let nameBases;
let color;
let lineGen;
// defined in main.js
let graphWidth;
let graphHeight;
let svgWidth;
let svgHeight;
let options = {};
let mapCoordinates = {};
let populationRate;
let distanceScale;
let urbanization;
let urbanDensity;
let statesNeutral;

177
modules/define-svg.js Normal file
View file

@ -0,0 +1,177 @@
"use strict";
// temporary define svg elements as globals
let svg,
defs,
viewbox,
scaleBar,
legend,
ocean,
oceanLayers,
oceanPattern,
lakes,
landmass,
texture,
terrs,
biomes,
cells,
gridOverlay,
coordinates,
compass,
rivers,
terrain,
relig,
cults,
regions,
statesBody,
statesHalo,
provs,
zones,
borders,
stateBorders,
provinceBorders,
routes,
roads,
trails,
searoutes,
temperature,
coastline,
ice,
prec,
population,
emblems,
labels,
icons,
burgLabels,
burgIcons,
anchors,
armies,
markers,
fogging,
ruler,
debug;
function defineSvg(width, height) {
// append svg layers (in default order)
svg = d3.select("#map");
defs = svg.select("#deftemp");
viewbox = svg.select("#viewbox");
scaleBar = svg.select("#scaleBar");
legend = svg.append("g").attr("id", "legend");
ocean = viewbox.append("g").attr("id", "ocean");
oceanLayers = ocean.append("g").attr("id", "oceanLayers");
oceanPattern = ocean.append("g").attr("id", "oceanPattern");
lakes = viewbox.append("g").attr("id", "lakes");
landmass = viewbox.append("g").attr("id", "landmass");
texture = viewbox.append("g").attr("id", "texture");
terrs = viewbox.append("g").attr("id", "terrs");
biomes = viewbox.append("g").attr("id", "biomes");
cells = viewbox.append("g").attr("id", "cells");
gridOverlay = viewbox.append("g").attr("id", "gridOverlay");
coordinates = viewbox.append("g").attr("id", "coordinates");
compass = viewbox.append("g").attr("id", "compass");
rivers = viewbox.append("g").attr("id", "rivers");
terrain = viewbox.append("g").attr("id", "terrain");
relig = viewbox.append("g").attr("id", "relig");
cults = viewbox.append("g").attr("id", "cults");
regions = viewbox.append("g").attr("id", "regions");
statesBody = regions.append("g").attr("id", "statesBody");
statesHalo = regions.append("g").attr("id", "statesHalo");
provs = viewbox.append("g").attr("id", "provs");
zones = viewbox.append("g").attr("id", "zones").style("display", "none");
borders = viewbox.append("g").attr("id", "borders");
stateBorders = borders.append("g").attr("id", "stateBorders");
provinceBorders = borders.append("g").attr("id", "provinceBorders");
routes = viewbox.append("g").attr("id", "routes");
roads = routes.append("g").attr("id", "roads");
trails = routes.append("g").attr("id", "trails");
searoutes = routes.append("g").attr("id", "searoutes");
temperature = viewbox.append("g").attr("id", "temperature");
coastline = viewbox.append("g").attr("id", "coastline");
ice = viewbox.append("g").attr("id", "ice").style("display", "none");
prec = viewbox.append("g").attr("id", "prec").style("display", "none");
population = viewbox.append("g").attr("id", "population");
emblems = viewbox.append("g").attr("id", "emblems").style("display", "none");
labels = viewbox.append("g").attr("id", "labels");
icons = viewbox.append("g").attr("id", "icons");
burgIcons = icons.append("g").attr("id", "burgIcons");
anchors = icons.append("g").attr("id", "anchors");
armies = viewbox.append("g").attr("id", "armies").style("display", "none");
markers = viewbox.append("g").attr("id", "markers");
fogging = viewbox
.append("g")
.attr("id", "fogging-cont")
.attr("mask", "url(#fog)")
.append("g")
.attr("id", "fogging")
.style("display", "none");
ruler = viewbox.append("g").attr("id", "ruler").style("display", "none");
debug = viewbox.append("g").attr("id", "debug");
// lake and coast groups
lakes.append("g").attr("id", "freshwater");
lakes.append("g").attr("id", "salt");
lakes.append("g").attr("id", "sinkhole");
lakes.append("g").attr("id", "frozen");
lakes.append("g").attr("id", "lava");
lakes.append("g").attr("id", "dry");
coastline.append("g").attr("id", "sea_island");
coastline.append("g").attr("id", "lake_island");
labels.append("g").attr("id", "states");
labels.append("g").attr("id", "addedLabels");
burgLabels = labels.append("g").attr("id", "burgLabels");
burgIcons.append("g").attr("id", "cities");
burgLabels.append("g").attr("id", "cities");
anchors.append("g").attr("id", "cities");
burgIcons.append("g").attr("id", "towns");
burgLabels.append("g").attr("id", "towns");
anchors.append("g").attr("id", "towns");
// population groups
population.append("g").attr("id", "rural");
population.append("g").attr("id", "urban");
// emblem groups
emblems.append("g").attr("id", "burgEmblems");
emblems.append("g").attr("id", "provinceEmblems");
emblems.append("g").attr("id", "stateEmblems");
// fogging
fogging.append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%");
fogging
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", "100%")
.attr("height", "100%")
.attr("fill", "#e8f0f6")
.attr("filter", "url(#splotch)");
// assign events separately as not a viewbox child
scaleBar.on("mousemove", () => tip("Click to open Units Editor")).on("click", () => editUnits());
legend
.on("mousemove", () => tip("Drag to change the position. Click to hide the legend"))
.on("click", () => clearLegend());
landmass.append("rect").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height);
oceanPattern
.append("rect")
.attr("fill", "url(#oceanic)")
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height);
oceanLayers
.append("rect")
.attr("id", "oceanBase")
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height);
}

View file

@ -422,8 +422,8 @@ function editStateName(state) {
position: {my: "center", at: "center", of: "svg"} position: {my: "center", at: "center", of: "svg"}
}); });
if (modules.editStateName) return; if (fmg.modules.editStateName) return;
modules.editStateName = true; fmg.modules.editStateName = true;
// add listeners // add listeners
byId("stateNameEditorShortCulture").on("click", regenerateShortNameCuture); byId("stateNameEditorShortCulture").on("click", regenerateShortNameCuture);

View file

@ -262,7 +262,7 @@ function getName(id) {
} }
function getGraph(currentGraph) { function getGraph(currentGraph) {
const newGraph = shouldRegenerateGrid(currentGraph) ? generateGrid() : deepCopy(currentGraph); const newGraph = shouldRegenerateGrid(currentGraph) ? generateGrid() : structuredClone(currentGraph);
delete newGraph.cells.h; delete newGraph.cells.h;
return newGraph; return newGraph;
} }

View file

@ -1,4 +1,6 @@
"use strict"; import {TIME} from "/src/config/logging";
import {createTypedArray} from "/src/utils";
import {findGridCell} from "/src/utils/graphUtils";
window.HeightmapGenerator = (function () { window.HeightmapGenerator = (function () {
let grid = null; let grid = null;
@ -388,8 +390,12 @@ window.HeightmapGenerator = (function () {
const vert = direction === "vertical"; const vert = direction === "vertical";
const startX = vert ? Math.floor(Math.random() * graphWidth * 0.4 + graphWidth * 0.3) : 5; const startX = vert ? Math.floor(Math.random() * graphWidth * 0.4 + graphWidth * 0.3) : 5;
const startY = vert ? 5 : Math.floor(Math.random() * graphHeight * 0.4 + graphHeight * 0.3); const startY = vert ? 5 : Math.floor(Math.random() * graphHeight * 0.4 + graphHeight * 0.3);
const endX = vert ? Math.floor(graphWidth - startX - graphWidth * 0.1 + Math.random() * graphWidth * 0.2) : graphWidth - 5; const endX = vert
const endY = vert ? graphHeight - 5 : Math.floor(graphHeight - startY - graphHeight * 0.1 + Math.random() * graphHeight * 0.2); ? Math.floor(graphWidth - startX - graphWidth * 0.1 + Math.random() * graphWidth * 0.2)
: graphWidth - 5;
const endY = vert
? graphHeight - 5
: Math.floor(graphHeight - startY - graphHeight * 0.1 + Math.random() * graphHeight * 0.2);
const start = findGridCell(startX, startY, grid); const start = findGridCell(startX, startY, grid);
const end = findGridCell(endX, endY, grid); const end = findGridCell(endX, endY, grid);

View file

@ -1,5 +1,3 @@
"use strict";
window.Lakes = (function () { window.Lakes = (function () {
const setClimateData = function (h) { const setClimateData = function (h) {
const cells = pack.cells; const cells = pack.cells;
@ -12,7 +10,10 @@ window.Lakes = (function () {
f.flux = f.shoreline.reduce((acc, c) => acc + grid.cells.prec[cells.g[c]], 0); f.flux = f.shoreline.reduce((acc, c) => acc + grid.cells.prec[cells.g[c]], 0);
// temperature and evaporation to detect closed lakes // temperature and evaporation to detect closed lakes
f.temp = f.cells < 6 ? grid.cells.temp[cells.g[f.firstCell]] : rn(d3.mean(f.shoreline.map(c => grid.cells.temp[cells.g[c]])), 1); f.temp =
f.cells < 6
? grid.cells.temp[cells.g[f.firstCell]]
: rn(d3.mean(f.shoreline.map(c => grid.cells.temp[cells.g[c]])), 1);
const height = (f.height - 18) ** heightExponentInput.value; // height in meters const height = (f.height - 18) ** heightExponentInput.value; // height in meters
const evaporation = ((700 * (f.temp + 0.006 * height)) / 50 + 75) / (80 - f.temp); // based on Penman formula, [1-11] const evaporation = ((700 * (f.temp + 0.006 * height)) / 50 + 75) / (80 - f.temp); // based on Penman formula, [1-11]
f.evaporation = rn(evaporation * f.cells); f.evaporation = rn(evaporation * f.cells);

View file

@ -1,4 +1,4 @@
"use strict"; import {TIME} from "/src/config/logging";
window.Markers = (function () { window.Markers = (function () {
let config = []; let config = [];
@ -20,6 +20,7 @@ window.Markers = (function () {
list: function to select candidates list: function to select candidates
add: function to add marker legend add: function to add marker legend
*/ */
// prettier-ignore
return [ return [
{type: "volcanoes", icon: "🌋", dx: 52, px: 13, min: 10, each: 500, multiplier: 1, list: listVolcanoes, add: addVolcano}, {type: "volcanoes", icon: "🌋", dx: 52, px: 13, min: 10, each: 500, multiplier: 1, list: listVolcanoes, add: addVolcano},
{type: "hot-springs", icon: "♨️", dy: 52, min: 30, each: 1200, multiplier: 1, list: listHotSprings, add: addHotSpring}, {type: "hot-springs", icon: "♨️", dy: 52, min: 30, each: 1200, multiplier: 1, list: listHotSprings, add: addHotSpring},
@ -199,7 +200,13 @@ window.Markers = (function () {
function listBridges({cells, burgs}) { function listBridges({cells, burgs}) {
const meanFlux = d3.mean(cells.fl.filter(fl => fl)); const meanFlux = d3.mean(cells.fl.filter(fl => fl));
return cells.i.filter( return cells.i.filter(
i => !occupied[i] && cells.burg[i] && cells.t[i] !== 1 && burgs[cells.burg[i]].population > 20 && cells.r[i] && cells.fl[i] > meanFlux i =>
!occupied[i] &&
cells.burg[i] &&
cells.t[i] !== 1 &&
burgs[cells.burg[i]].population > 20 &&
cells.r[i] &&
cells.fl[i] > meanFlux
); );
} }
@ -441,7 +448,21 @@ window.Markers = (function () {
"rat tails", "rat tails",
"pig ears" "pig ears"
]; ];
const types = ["hot", "cold", "fire", "ice", "smoky", "misty", "shiny", "sweet", "bitter", "salty", "sour", "sparkling", "smelly"]; const types = [
"hot",
"cold",
"fire",
"ice",
"smoky",
"misty",
"shiny",
"sweet",
"bitter",
"salty",
"sour",
"sparkling",
"smelly"
];
const drinks = [ const drinks = [
"wine", "wine",
"brandy", "brandy",
@ -469,7 +490,11 @@ window.Markers = (function () {
const typeName = P(0.3) ? "inn" : "tavern"; const typeName = P(0.3) ? "inn" : "tavern";
const isAnimalThemed = P(0.7); const isAnimalThemed = P(0.7);
const animal = ra(animals); const animal = ra(animals);
const name = isAnimalThemed ? (P(0.6) ? ra(colors) + " " + animal : ra(adjectives) + " " + animal) : ra(adjectives) + " " + capitalize(typeName); const name = isAnimalThemed
? P(0.6)
? ra(colors) + " " + animal
: ra(adjectives) + " " + animal
: ra(adjectives) + " " + capitalize(typeName);
const meal = isAnimalThemed && P(0.3) ? animal : ra(courses); const meal = isAnimalThemed && P(0.3) ? animal : ra(courses);
const course = `${ra(methods)} ${meal}`.toLowerCase(); const course = `${ra(methods)} ${meal}`.toLowerCase();
const drink = `${P(0.5) ? ra(types) : ra(colors)} ${ra(drinks)}`.toLowerCase(); const drink = `${P(0.5) ? ra(types) : ra(colors)} ${ra(drinks)}`.toLowerCase();
@ -478,18 +503,26 @@ window.Markers = (function () {
} }
function listLighthouses({cells}) { function listLighthouses({cells}) {
return cells.i.filter(i => !occupied[i] && cells.harbor[i] > 6 && cells.c[i].some(c => cells.h[c] < 20 && cells.road[c])); return cells.i.filter(
i => !occupied[i] && cells.harbor[i] > 6 && cells.c[i].some(c => cells.h[c] < 20 && cells.road[c])
);
} }
function addLighthouse(id, cell) { function addLighthouse(id, cell) {
const {cells} = pack; const {cells} = pack;
const proper = cells.burg[cell] ? pack.burgs[cells.burg[cell]].name : Names.getCulture(cells.culture[cell]); const proper = cells.burg[cell] ? pack.burgs[cells.burg[cell]].name : Names.getCulture(cells.culture[cell]);
notes.push({id, name: getAdjective(proper) + " Lighthouse" + name, legend: `A lighthouse to serve as a beacon for ships in the open sea`}); notes.push({
id,
name: getAdjective(proper) + " Lighthouse" + name,
legend: `A lighthouse to serve as a beacon for ships in the open sea`
});
} }
function listWaterfalls({cells}) { function listWaterfalls({cells}) {
return cells.i.filter(i => cells.r[i] && !occupied[i] && cells.h[i] >= 50 && cells.c[i].some(c => cells.h[c] < 40 && cells.r[c])); return cells.i.filter(
i => cells.r[i] && !occupied[i] && cells.h[i] >= 50 && cells.c[i].some(c => cells.h[c] < 40 && cells.r[c])
);
} }
function addWaterfall(id, cell) { function addWaterfall(id, cell) {
@ -509,7 +542,9 @@ window.Markers = (function () {
} }
function listBattlefields({cells}) { function listBattlefields({cells}) {
return cells.i.filter(i => !occupied[i] && cells.state[i] && cells.pop[i] > 2 && cells.h[i] < 50 && cells.h[i] > 25); return cells.i.filter(
i => !occupied[i] && cells.state[i] && cells.pop[i] > 2 && cells.h[i] < 50 && cells.h[i] > 25
);
} }
function addBattlefield(id, cell) { function addBattlefield(id, cell) {
@ -555,7 +590,9 @@ window.Markers = (function () {
} }
function listSeaMonsters({cells, features}) { function listSeaMonsters({cells, features}) {
return cells.i.filter(i => !occupied[i] && cells.h[i] < 20 && cells.road[i] && features[cells.f[i]].type === "ocean"); return cells.i.filter(
i => !occupied[i] && cells.h[i] < 20 && cells.road[i] && features[cells.f[i]].type === "ocean"
);
} }
function addSeaMonster(id, cell) { function addSeaMonster(id, cell) {
@ -589,7 +626,17 @@ window.Markers = (function () {
"horrifying", "horrifying",
"feared" "feared"
]; ];
const subjects = ["Locals", "Elders", "Inscriptions", "Tipplers", "Legends", "Whispers", "Rumors", "Journeying folk", "Tales"]; const subjects = [
"Locals",
"Elders",
"Inscriptions",
"Tipplers",
"Legends",
"Whispers",
"Rumors",
"Journeying folk",
"Tales"
];
const species = [ const species = [
"Ogre", "Ogre",
"Troll", "Troll",
@ -625,13 +672,21 @@ window.Markers = (function () {
const monster = ra(species); const monster = ra(species);
const toponym = Names.getCulture(cells.culture[cell]); const toponym = Names.getCulture(cells.culture[cell]);
const name = `${toponym} ${monster}`; const name = `${toponym} ${monster}`;
const legend = `${ra(subjects)} speak of a ${ra(adjectives)} ${monster} who inhabits ${toponym} hills and ${ra(modusOperandi)}`; const legend = `${ra(subjects)} speak of a ${ra(adjectives)} ${monster} who inhabits ${toponym} hills and ${ra(
modusOperandi
)}`;
notes.push({id, name, legend}); notes.push({id, name, legend});
} }
// Sacred mountains spawn on lonely mountains // Sacred mountains spawn on lonely mountains
function listSacredMountains({cells}) { function listSacredMountains({cells}) {
return cells.i.filter(i => !occupied[i] && cells.h[i] >= 70 && cells.c[i].some(c => cells.culture[c]) && cells.c[i].every(c => cells.h[c] < 60)); return cells.i.filter(
i =>
!occupied[i] &&
cells.h[i] >= 70 &&
cells.c[i].some(c => cells.culture[c]) &&
cells.c[i].every(c => cells.h[c] < 60)
);
} }
function addSacredMountain(id, cell) { function addSacredMountain(id, cell) {
@ -674,7 +729,9 @@ window.Markers = (function () {
// Sacred palm groves spawn on oasises // Sacred palm groves spawn on oasises
function listSacredPalmGroves({cells}) { function listSacredPalmGroves({cells}) {
return cells.i.filter(i => !occupied[i] && cells.culture[i] && cells.biome[i] === 1 && cells.pop[i] > 1 && cells.road[i]); return cells.i.filter(
i => !occupied[i] && cells.culture[i] && cells.biome[i] === 1 && cells.pop[i] > 1 && cells.road[i]
);
} }
function addSacredPalmGrove(id, cell) { function addSacredPalmGrove(id, cell) {
@ -765,7 +822,20 @@ window.Markers = (function () {
function addStatue(id, cell) { function addStatue(id, cell) {
const {cells} = pack; const {cells} = pack;
const variants = ["Statue", "Obelisk", "Monument", "Column", "Monolith", "Pillar", "Megalith", "Stele", "Runestone", "Sculpture", "Effigy", "Idol"]; const variants = [
"Statue",
"Obelisk",
"Monument",
"Column",
"Monolith",
"Pillar",
"Megalith",
"Stele",
"Runestone",
"Sculpture",
"Effigy",
"Idol"
];
const scripts = { const scripts = {
cypriot: "𐠁𐠂𐠃𐠄𐠅𐠈𐠊𐠋𐠌𐠍𐠎𐠏𐠐𐠑𐠒𐠓𐠔𐠕𐠖𐠗𐠘𐠙𐠚𐠛𐠜𐠝𐠞𐠟𐠠𐠡𐠢𐠣𐠤𐠥𐠦𐠧𐠨𐠩𐠪𐠫𐠬𐠭𐠮𐠯𐠰𐠱𐠲𐠳𐠴𐠵𐠷𐠸𐠼𐠿 ", cypriot: "𐠁𐠂𐠃𐠄𐠅𐠈𐠊𐠋𐠌𐠍𐠎𐠏𐠐𐠑𐠒𐠓𐠔𐠕𐠖𐠗𐠘𐠙𐠚𐠛𐠜𐠝𐠞𐠟𐠠𐠡𐠢𐠣𐠤𐠥𐠦𐠧𐠨𐠩𐠪𐠫𐠬𐠭𐠮𐠯𐠰𐠱𐠲𐠳𐠴𐠵𐠷𐠸𐠼𐠿 ",
geez: "ሀለሐመሠረሰቀበተኀነአከወዐዘየደገጠጰጸፀፈፐ ", geez: "ሀለሐመሠረሰቀበተኀነአከወዐዘየደገጠጰጸፀፈፐ ",
@ -820,7 +890,16 @@ window.Markers = (function () {
} }
function addCircuses(id, cell) { function addCircuses(id, cell) {
const adjectives = ["Fantastical", "Wonderous", "Incomprehensible", "Magical", "Extraordinary", "Unmissable", "World-famous", "Breathtaking"]; const adjectives = [
"Fantastical",
"Wonderous",
"Incomprehensible",
"Magical",
"Extraordinary",
"Unmissable",
"World-famous",
"Breathtaking"
];
const adjective = ra(adjectives); const adjective = ra(adjectives);
const name = `Travelling ${adjective} Circus`; const name = `Travelling ${adjective} Circus`;
@ -932,8 +1011,26 @@ window.Markers = (function () {
function addDances(id, cell) { function addDances(id, cell) {
const {cells, burgs} = pack; const {cells, burgs} = pack;
const burgName = burgs[cells.burg[cell]].name; const burgName = burgs[cells.burg[cell]].name;
const socialTypes = ["gala", "dance", "performance", "ball", "soiree", "jamboree", "exhibition", "carnival", "festival", "jubilee"]; const socialTypes = [
const people = ["great and the good", "nobility", "local elders", "foreign dignitaries", "spiritual leaders", "suspected revolutionaries"]; "gala",
"dance",
"performance",
"ball",
"soiree",
"jamboree",
"exhibition",
"carnival",
"festival",
"jubilee"
];
const people = [
"great and the good",
"nobility",
"local elders",
"foreign dignitaries",
"spiritual leaders",
"suspected revolutionaries"
];
const socialType = ra(socialTypes); const socialType = ra(socialTypes);
const name = `${burgName} ${socialType}`; const name = `${burgName} ${socialType}`;

View file

@ -1,4 +1,4 @@
"use strict"; import {TIME} from "/src/config/logging";
window.Military = (function () { window.Military = (function () {
const generate = function () { const generate = function () {
@ -10,7 +10,18 @@ window.Military = (function () {
const expn = d3.sum(valid.map(s => s.expansionism)); // total expansion const expn = d3.sum(valid.map(s => s.expansionism)); // total expansion
const area = d3.sum(valid.map(s => s.area)); // total area const area = d3.sum(valid.map(s => s.area)); // total area
const rate = {x: 0, Ally: -0.2, Friendly: -0.1, Neutral: 0, Suspicion: 0.1, Enemy: 1, Unknown: 0, Rival: 0.5, Vassal: 0.5, Suzerain: -0.5}; const rate = {
x: 0,
Ally: -0.2,
Friendly: -0.1,
Neutral: 0,
Suspicion: 0.1,
Enemy: 1,
Unknown: 0,
Rival: 0.5,
Vassal: 0.5,
Suzerain: -0.5
};
const stateModifier = { const stateModifier = {
melee: {Nomadic: 0.5, Highland: 1.2, Lake: 1, Naval: 0.7, Hunting: 1.2, River: 1.1}, melee: {Nomadic: 0.5, Highland: 1.2, Lake: 1, Naval: 0.7, Hunting: 1.2, River: 1.1},
@ -24,14 +35,59 @@ window.Military = (function () {
}; };
const cellTypeModifier = { const cellTypeModifier = {
nomadic: {melee: 0.2, ranged: 0.5, mounted: 3, machinery: 0.4, naval: 0.3, armored: 1.6, aviation: 1, magical: 0.5}, nomadic: {
wetland: {melee: 0.8, ranged: 2, mounted: 0.3, machinery: 1.2, naval: 1.0, armored: 0.2, aviation: 0.5, magical: 0.5}, melee: 0.2,
highland: {melee: 1.2, ranged: 1.6, mounted: 0.3, machinery: 3, naval: 1.0, armored: 0.8, aviation: 0.3, magical: 2} ranged: 0.5,
mounted: 3,
machinery: 0.4,
naval: 0.3,
armored: 1.6,
aviation: 1,
magical: 0.5
},
wetland: {
melee: 0.8,
ranged: 2,
mounted: 0.3,
machinery: 1.2,
naval: 1.0,
armored: 0.2,
aviation: 0.5,
magical: 0.5
},
highland: {
melee: 1.2,
ranged: 1.6,
mounted: 0.3,
machinery: 3,
naval: 1.0,
armored: 0.8,
aviation: 0.3,
magical: 2
}
}; };
const burgTypeModifier = { const burgTypeModifier = {
nomadic: {melee: 0.3, ranged: 0.8, mounted: 3, machinery: 0.4, naval: 1.0, armored: 1.6, aviation: 1, magical: 0.5}, nomadic: {
wetland: {melee: 1, ranged: 1.6, mounted: 0.2, machinery: 1.2, naval: 1.0, armored: 0.2, aviation: 0.5, magical: 0.5}, melee: 0.3,
ranged: 0.8,
mounted: 3,
machinery: 0.4,
naval: 1.0,
armored: 1.6,
aviation: 1,
magical: 0.5
},
wetland: {
melee: 1,
ranged: 1.6,
mounted: 0.2,
machinery: 1.2,
naval: 1.0,
armored: 0.2,
aviation: 0.5,
magical: 0.5
},
highland: {melee: 1.2, ranged: 2, mounted: 0.3, machinery: 3, naval: 1.0, armored: 0.8, aviation: 0.3, magical: 2} highland: {melee: 1.2, ranged: 2, mounted: 0.3, machinery: 3, naval: 1.0, armored: 0.8, aviation: 0.3, magical: 2}
}; };
@ -40,8 +96,16 @@ window.Military = (function () {
const d = s.diplomacy; const d = s.diplomacy;
const expansionRate = minmax(s.expansionism / expn / (s.area / area), 0.25, 4); // how much state expansionism is realized const expansionRate = minmax(s.expansionism / expn / (s.area / area), 0.25, 4); // how much state expansionism is realized
const diplomacyRate = d.some(d => d === "Enemy") ? 1 : d.some(d => d === "Rival") ? 0.8 : d.some(d => d === "Suspicion") ? 0.5 : 0.1; // peacefulness const diplomacyRate = d.some(d => d === "Enemy")
const neighborsRateRaw = s.neighbors.map(n => (n ? pack.states[n].diplomacy[s.i] : "Suspicion")).reduce((s, r) => (s += rate[r]), 0.5); ? 1
: d.some(d => d === "Rival")
? 0.8
: d.some(d => d === "Suspicion")
? 0.5
: 0.1; // peacefulness
const neighborsRateRaw = s.neighbors
.map(n => (n ? pack.states[n].diplomacy[s.i] : "Suspicion"))
.reduce((s, r) => (s += rate[r]), 0.5);
const neighborsRate = minmax(neighborsRateRaw, 0.3, 3); // neighbors rate const neighborsRate = minmax(neighborsRateRaw, 0.3, 3); // neighbors rate
s.alert = minmax(rn(expansionRate * diplomacyRate * neighborsRate, 2), 0.1, 5); // alert rate (area modifier) s.alert = minmax(rn(expansionRate * diplomacyRate * neighborsRate, 2), 0.1, 5); // alert rate (area modifier)
s.temp.platoons = []; s.temp.platoons = [];
@ -86,8 +150,10 @@ window.Military = (function () {
let modifier = cells.pop[i] / 100; // basic rural army in percentages let modifier = cells.pop[i] / 100; // basic rural army in percentages
if (culture !== stateObj.culture) modifier = stateObj.form === "Union" ? modifier / 1.2 : modifier / 2; // non-dominant culture if (culture !== stateObj.culture) modifier = stateObj.form === "Union" ? modifier / 1.2 : modifier / 2; // non-dominant culture
if (religion !== cells.religion[stateObj.center]) modifier = stateObj.form === "Theocracy" ? modifier / 2.2 : modifier / 1.4; // non-dominant religion if (religion !== cells.religion[stateObj.center])
if (cells.f[i] !== cells.f[stateObj.center]) modifier = stateObj.type === "Naval" ? modifier / 1.2 : modifier / 1.8; // different landmass modifier = stateObj.form === "Theocracy" ? modifier / 2.2 : modifier / 1.4; // non-dominant religion
if (cells.f[i] !== cells.f[stateObj.center])
modifier = stateObj.type === "Naval" ? modifier / 1.2 : modifier / 1.8; // different landmass
const type = getType(i); const type = getType(i);
for (const unit of options.military) { for (const unit of options.military) {
@ -111,7 +177,17 @@ window.Military = (function () {
n = 1; n = 1;
} }
stateObj.temp.platoons.push({cell: i, a: total, t: total, x, y, u: unit.name, n, s: unit.separate, type: unit.type}); stateObj.temp.platoons.push({
cell: i,
a: total,
t: total,
x,
y,
u: unit.name,
n,
s: unit.separate,
type: unit.type
});
} }
} }
@ -153,7 +229,17 @@ window.Military = (function () {
n = 1; n = 1;
} }
stateObj.temp.platoons.push({cell: b.cell, a: total, t: total, x, y, u: unit.name, n, s: unit.separate, type: unit.type}); stateObj.temp.platoons.push({
cell: b.cell,
a: total,
t: total,
x,
y,
u: unit.name,
n,
s: unit.separate,
type: unit.type
});
} }
} }
@ -379,7 +465,13 @@ window.Military = (function () {
// get default regiment emblem // get default regiment emblem
const getEmblem = function (r) { const getEmblem = function (r) {
if (!r.n && !Object.values(r.u).length) return "🔰"; // "Newbie" regiment without troops if (!r.n && !Object.values(r.u).length) return "🔰"; // "Newbie" regiment without troops
if (!r.n && pack.states[r.state].form === "Monarchy" && pack.cells.burg[r.cell] && pack.burgs[pack.cells.burg[r.cell]].capital) return "👑"; // "Royal" regiment based in capital if (
!r.n &&
pack.states[r.state].form === "Monarchy" &&
pack.cells.burg[r.cell] &&
pack.burgs[pack.cells.burg[r.cell]].capital
)
return "👑"; // "Royal" regiment based in capital
const mainUnit = Object.entries(r.u).sort((a, b) => b[1] - a[1])[0][0]; // unit with more troops in regiment const mainUnit = Object.entries(r.u).sort((a, b) => b[1] - a[1])[0][0]; // unit with more troops in regiment
const unit = options.military.find(u => u.name === mainUnit); const unit = options.military.find(u => u.name === mainUnit);
return unit.icon; return unit.icon;
@ -400,7 +492,9 @@ window.Military = (function () {
.map(t => `${t}: ${r.u[t]}`) .map(t => `${t}: ${r.u[t]}`)
.join("\r\n") .join("\r\n")
: null; : null;
const troops = composition ? `\r\n\r\nRegiment composition in ${options.year} ${options.eraShort}:\r\n${composition}.` : ""; const troops = composition
? `\r\n\r\nRegiment composition in ${options.year} ${options.eraShort}:\r\n${composition}.`
: "";
const campaign = s.campaigns ? ra(s.campaigns) : null; const campaign = s.campaigns ? ra(s.campaigns) : null;
const year = campaign ? rand(campaign.start, campaign.end) : gauss(options.year - 100, 150, 1, options.year - 6); const year = campaign ? rand(campaign.start, campaign.end) : gauss(options.year - 100, 150, 1, options.year - 6);
@ -409,5 +503,16 @@ window.Military = (function () {
notes.push({id: `regiment${s.i}-${r.i}`, name: `${r.icon} ${r.name}`, legend}); notes.push({id: `regiment${s.i}-${r.i}`, name: `${r.icon} ${r.name}`, legend});
}; };
return {generate, redraw, getDefaultOptions, getName, generateNote, drawRegiments, drawRegiment, moveRegiment, getTotal, getEmblem}; return {
generate,
redraw,
getDefaultOptions,
getName,
generateNote,
drawRegiments,
drawRegiment,
moveRegiment,
getTotal,
getEmblem
};
})(); })();

View file

@ -1,5 +1,3 @@
"use strict";
window.Names = (function () { window.Names = (function () {
let chains = []; let chains = [];
@ -142,7 +140,11 @@ window.Names = (function () {
// generate short name for base // generate short name for base
const getBaseShort = function (base) { const getBaseShort = function (base) {
if (nameBases[base] === undefined) { if (nameBases[base] === undefined) {
tip(`Namebase ${base} does not exist. Please upload custom namebases of change the base in Cultures Editor`, false, "error"); tip(
`Namebase ${base} does not exist. Please upload custom namebases of change the base in Cultures Editor`,
false,
"error"
);
base = 1; base = 1;
} }
const min = nameBases[base].min - 1; const min = nameBases[base].min - 1;
@ -165,7 +167,8 @@ window.Names = (function () {
// remove -sk/-ev/-ov for Ruthenian // remove -sk/-ev/-ov for Ruthenian
else if (base === 12) return vowel(name.slice(-1)) ? name : name + "u"; else if (base === 12) return vowel(name.slice(-1)) ? name : name + "u";
// Japanese ends on any vowel or -u // Japanese ends on any vowel or -u
else if (base === 18 && P(0.4)) name = vowel(name.slice(0, 1).toLowerCase()) ? "Al" + name.toLowerCase() : "Al " + name; // Arabic starts with -Al else if (base === 18 && P(0.4))
name = vowel(name.slice(0, 1).toLowerCase()) ? "Al" + name.toLowerCase() : "Al " + name; // Arabic starts with -Al
// no suffix for fantasy bases // no suffix for fantasy bases
if (base > 32 && base < 42) return name; if (base > 32 && base < 42) return name;
@ -304,5 +307,16 @@ window.Names = (function () {
]; ];
}; };
return {getBase, getCulture, getCultureShort, getBaseShort, getState, updateChain, clearChains, getNameBases, getMapName, calculateChain}; return {
getBase,
getCulture,
getCultureShort,
getBaseShort,
getState,
updateChain,
clearChains,
getNameBases,
getMapName,
calculateChain
};
})(); })();

View file

@ -1,4 +1,4 @@
"use strict"; import {TIME} from "/src/config/logging";
window.OceanLayers = (function () { window.OceanLayers = (function () {
let cells, vertices, pointsN, used; let cells, vertices, pointsN, used;

View file

@ -1,4 +1,5 @@
"use strict"; import {TIME} from "/src/config/logging";
import {findAll} from "/src/utils/graphUtils";
window.Religions = (function () { window.Religions = (function () {
// name generation approach and relative chance to be selected // name generation approach and relative chance to be selected

View file

@ -1,4 +1,4 @@
"use strict"; import {TIME} from "/src/config/logging";
window.Rivers = (function () { window.Rivers = (function () {
const generate = function (allowErosion = true) { const generate = function (allowErosion = true) {
@ -48,7 +48,9 @@ window.Rivers = (function () {
cells.fl[i] += prec[cells.g[i]] / cellsNumberModifier; // add flux from precipitation cells.fl[i] += prec[cells.g[i]] / cellsNumberModifier; // add flux from precipitation
// create lake outlet if lake is not in deep depression and flux > evaporation // create lake outlet if lake is not in deep depression and flux > evaporation
const lakes = lakeOutCells[i] ? features.filter(feature => i === feature.outCell && feature.flux > feature.evaporation) : []; const lakes = lakeOutCells[i]
? features.filter(feature => i === feature.outCell && feature.flux > feature.evaporation)
: [];
for (const lake of lakes) { for (const lake of lakes) {
const lakeCell = cells.c[i].find(c => h[c] < 20 && cells.f[c] === lake.i); const lakeCell = cells.c[i].find(c => h[c] < 20 && cells.f[c] === lake.i);
cells.fl[lakeCell] += Math.max(lake.flux - lake.evaporation, 0); // not evaporated lake water drains to outlet cells.fl[lakeCell] += Math.max(lake.flux - lake.evaporation, 0); // not evaporated lake water drains to outlet
@ -191,7 +193,18 @@ window.Rivers = (function () {
const length = getApproximateLength(meanderedPoints); const length = getApproximateLength(meanderedPoints);
const width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor, 0)); const width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor, 0));
pack.rivers.push({i: riverId, source, mouth, discharge, length, width, widthFactor, sourceWidth: 0, parent, cells: riverCells}); pack.rivers.push({
i: riverId,
source,
mouth,
discharge,
length,
width,
widthFactor,
sourceWidth: 0,
parent,
cells: riverCells
});
} }
} }

View file

@ -1,3 +1,6 @@
import {TIME} from "/src/config/logging";
import {findCell} from "/src/utils/graphUtils";
window.Routes = (function () { window.Routes = (function () {
const getRoads = function () { const getRoads = function () {
TIME && console.time("generateMainRoads"); TIME && console.time("generateMainRoads");
@ -39,7 +42,10 @@ window.Routes = (function () {
if (!i) { if (!i) {
// build trail from the first burg on island // build trail from the first burg on island
// to the farthest one on the same island or the closest road // to the farthest one on the same island or the closest road
const farthest = d3.scan(isle, (a, c) => (c.y - b.y) ** 2 + (c.x - b.x) ** 2 - ((a.y - b.y) ** 2 + (a.x - b.x) ** 2)); const farthest = d3.scan(
isle,
(a, c) => (c.y - b.y) ** 2 + (c.x - b.x) ** 2 - ((a.y - b.y) ** 2 + (a.x - b.x) ** 2)
);
const to = isle[farthest].cell; const to = isle[farthest].cell;
if (cells.road[to]) return; if (cells.road[to]) return;
const [from, exit] = findLandPath(b.cell, to, true); const [from, exit] = findLandPath(b.cell, to, true);
@ -131,7 +137,8 @@ window.Routes = (function () {
const getBurgCoords = b => [burgs[b].x, burgs[b].y]; const getBurgCoords = b => [burgs[b].x, burgs[b].y];
const getPathPoints = cells => cells.map(i => (Array.isArray(i) ? i : burg[i] ? getBurgCoords(burg[i]) : p[i])); const getPathPoints = cells => cells.map(i => (Array.isArray(i) ? i : burg[i] ? getBurgCoords(burg[i]) : p[i]));
const getPath = segment => round(lineGen(getPathPoints(segment)), 1); const getPath = segment => round(lineGen(getPathPoints(segment)), 1);
const getPathsHTML = (paths, type) => paths.map((path, i) => `<path id="${type}${i}" d="${getPath(path)}" />`).join(""); const getPathsHTML = (paths, type) =>
paths.map((path, i) => `<path id="${type}${i}" d="${getPath(path)}" />`).join("");
lineGen.curve(d3.curveCatmullRom.alpha(0.1)); lineGen.curve(d3.curveCatmullRom.alpha(0.1));
roads.html(getPathsHTML(main, "road")); roads.html(getPathsHTML(main, "road"));

View file

@ -32,17 +32,27 @@ class Battle {
close: () => Battle.prototype.context.cancelResults() close: () => Battle.prototype.context.cancelResults()
}); });
if (modules.Battle) return; if (fmg.modules.Battle) return;
modules.Battle = true; fmg.modules.Battle = true;
// add listeners // add listeners
document.getElementById("battleType").addEventListener("click", ev => this.toggleChange(ev)); document.getElementById("battleType").addEventListener("click", ev => this.toggleChange(ev));
document.getElementById("battleType").nextElementSibling.addEventListener("click", ev => Battle.prototype.context.changeType(ev)); document
document.getElementById("battleNameShow").addEventListener("click", () => Battle.prototype.context.showNameSection()); .getElementById("battleType")
document.getElementById("battleNamePlace").addEventListener("change", ev => (Battle.prototype.context.place = ev.target.value)); .nextElementSibling.addEventListener("click", ev => Battle.prototype.context.changeType(ev));
document
.getElementById("battleNameShow")
.addEventListener("click", () => Battle.prototype.context.showNameSection());
document
.getElementById("battleNamePlace")
.addEventListener("change", ev => (Battle.prototype.context.place = ev.target.value));
document.getElementById("battleNameFull").addEventListener("change", ev => Battle.prototype.context.changeName(ev)); document.getElementById("battleNameFull").addEventListener("change", ev => Battle.prototype.context.changeName(ev));
document.getElementById("battleNameCulture").addEventListener("click", () => Battle.prototype.context.generateName("culture")); document
document.getElementById("battleNameRandom").addEventListener("click", () => Battle.prototype.context.generateName("random")); .getElementById("battleNameCulture")
.addEventListener("click", () => Battle.prototype.context.generateName("culture"));
document
.getElementById("battleNameRandom")
.addEventListener("click", () => Battle.prototype.context.generateName("random"));
document.getElementById("battleNameHide").addEventListener("click", this.hideNameSection); document.getElementById("battleNameHide").addEventListener("click", this.hideNameSection);
document.getElementById("battleAddRegiment").addEventListener("click", this.addSide); document.getElementById("battleAddRegiment").addEventListener("click", this.addSide);
document.getElementById("battleRoll").addEventListener("click", () => Battle.prototype.context.randomize()); document.getElementById("battleRoll").addEventListener("click", () => Battle.prototype.context.randomize());
@ -52,11 +62,19 @@ class Battle {
document.getElementById("battleWiki").addEventListener("click", () => wiki("Battle-Simulator")); document.getElementById("battleWiki").addEventListener("click", () => wiki("Battle-Simulator"));
document.getElementById("battlePhase_attackers").addEventListener("click", ev => this.toggleChange(ev)); document.getElementById("battlePhase_attackers").addEventListener("click", ev => this.toggleChange(ev));
document.getElementById("battlePhase_attackers").nextElementSibling.addEventListener("click", ev => Battle.prototype.context.changePhase(ev, "attackers")); document
.getElementById("battlePhase_attackers")
.nextElementSibling.addEventListener("click", ev => Battle.prototype.context.changePhase(ev, "attackers"));
document.getElementById("battlePhase_defenders").addEventListener("click", ev => this.toggleChange(ev)); document.getElementById("battlePhase_defenders").addEventListener("click", ev => this.toggleChange(ev));
document.getElementById("battlePhase_defenders").nextElementSibling.addEventListener("click", ev => Battle.prototype.context.changePhase(ev, "defenders")); document
document.getElementById("battleDie_attackers").addEventListener("click", () => Battle.prototype.context.rollDie("attackers")); .getElementById("battlePhase_defenders")
document.getElementById("battleDie_defenders").addEventListener("click", () => Battle.prototype.context.rollDie("defenders")); .nextElementSibling.addEventListener("click", ev => Battle.prototype.context.changePhase(ev, "defenders"));
document
.getElementById("battleDie_attackers")
.addEventListener("click", () => Battle.prototype.context.rollDie("attackers"));
document
.getElementById("battleDie_defenders")
.addEventListener("click", () => Battle.prototype.context.rollDie("defenders"));
} }
defineType() { defineType() {
@ -82,8 +100,12 @@ class Battle {
document.getElementById("battleType").className = "icon-button-" + this.type; document.getElementById("battleType").className = "icon-button-" + this.type;
const sideSpecific = document.getElementById("battlePhases_" + this.type + "_attackers"); const sideSpecific = document.getElementById("battlePhases_" + this.type + "_attackers");
const attackers = sideSpecific ? sideSpecific.content : document.getElementById("battlePhases_" + this.type).content; const attackers = sideSpecific
const defenders = sideSpecific ? document.getElementById("battlePhases_" + this.type + "_defenders").content : attackers; ? sideSpecific.content
: document.getElementById("battlePhases_" + this.type).content;
const defenders = sideSpecific
? document.getElementById("battlePhases_" + this.type + "_defenders").content
: attackers;
document.getElementById("battlePhase_attackers").nextElementSibling.innerHTML = ""; document.getElementById("battlePhase_attackers").nextElementSibling.innerHTML = "";
document.getElementById("battlePhase_defenders").nextElementSibling.innerHTML = ""; document.getElementById("battlePhase_defenders").nextElementSibling.innerHTML = "";
@ -146,19 +168,30 @@ class Battle {
<text x="0" y="1.04em" style="">${regiment.icon}</text></svg>`; <text x="0" y="1.04em" style="">${regiment.icon}</text></svg>`;
const body = `<tbody id="battle${state.i}-${regiment.i}">`; const body = `<tbody id="battle${state.i}-${regiment.i}">`;
let initial = `<tr class="battleInitial"><td>${icon}</td><td class="regiment" data-tip="${regiment.name}">${regiment.name.slice(0, 24)}</td>`; let initial = `<tr class="battleInitial"><td>${icon}</td><td class="regiment" data-tip="${
let casualties = `<tr class="battleCasualties"><td></td><td data-tip="${state.fullName}">${state.fullName.slice(0, 26)}</td>`; regiment.name
}">${regiment.name.slice(0, 24)}</td>`;
let casualties = `<tr class="battleCasualties"><td></td><td data-tip="${state.fullName}">${state.fullName.slice(
0,
26
)}</td>`;
let survivors = `<tr class="battleSurvivors"><td></td><td data-tip="Supply line length, affects morale">Distance to base: ${distance} ${distanceUnitInput.value}</td>`; let survivors = `<tr class="battleSurvivors"><td></td><td data-tip="Supply line length, affects morale">Distance to base: ${distance} ${distanceUnitInput.value}</td>`;
for (const u of options.military) { for (const u of options.military) {
initial += `<td data-tip="Initial forces" style="width: 2.5em; text-align: center">${regiment.u[u.name] || 0}</td>`; initial += `<td data-tip="Initial forces" style="width: 2.5em; text-align: center">${
regiment.u[u.name] || 0
}</td>`;
casualties += `<td data-tip="Casualties" style="width: 2.5em; text-align: center; color: red">0</td>`; casualties += `<td data-tip="Casualties" style="width: 2.5em; text-align: center; color: red">0</td>`;
survivors += `<td data-tip="Survivors" style="width: 2.5em; text-align: center; color: green">${regiment.u[u.name] || 0}</td>`; survivors += `<td data-tip="Survivors" style="width: 2.5em; text-align: center; color: green">${
regiment.u[u.name] || 0
}</td>`;
} }
initial += `<td data-tip="Initial forces" style="width: 2.5em; text-align: center">${regiment.a || 0}</td></tr>`; initial += `<td data-tip="Initial forces" style="width: 2.5em; text-align: center">${regiment.a || 0}</td></tr>`;
casualties += `<td data-tip="Casualties" style="width: 2.5em; text-align: center; color: red">0</td></tr>`; casualties += `<td data-tip="Casualties" style="width: 2.5em; text-align: center; color: red">0</td></tr>`;
survivors += `<td data-tip="Survivors" style="width: 2.5em; text-align: center; color: green">${regiment.a || 0}</td></tr>`; survivors += `<td data-tip="Survivors" style="width: 2.5em; text-align: center; color: green">${
regiment.a || 0
}</td></tr>`;
const div = side === "attackers" ? battleAttackers : battleDefenders; const div = side === "attackers" ? battleAttackers : battleDefenders;
div.innerHTML += body + initial + casualties + survivors + "</tbody>"; div.innerHTML += body + initial + casualties + survivors + "</tbody>";
@ -173,17 +206,23 @@ class Battle {
.filter(s => s.military && !s.removed) .filter(s => s.military && !s.removed)
.map(s => s.military) .map(s => s.military)
.flat(); .flat();
const distance = reg => rn(Math.hypot(context.y - reg.y, context.x - reg.x) * distanceScaleInput.value) + " " + distanceUnitInput.value; const distance = reg =>
const isAdded = reg => context.defenders.regiments.some(r => r === reg) || context.attackers.regiments.some(r => r === reg); rn(Math.hypot(context.y - reg.y, context.x - reg.x) * distanceScaleInput.value) + " " + distanceUnitInput.value;
const isAdded = reg =>
context.defenders.regiments.some(r => r === reg) || context.attackers.regiments.some(r => r === reg);
body.innerHTML = regiments body.innerHTML = regiments
.map(r => { .map(r => {
const s = pack.states[r.state], const s = pack.states[r.state],
added = isAdded(r), added = isAdded(r),
dist = added ? "0 " + distanceUnitInput.value : distance(r); dist = added ? "0 " + distanceUnitInput.value : distance(r);
return `<div ${added ? "class='inactive'" : ""} data-s=${s.i} data-i=${r.i} data-state=${s.name} data-regiment=${r.name} return `<div ${added ? "class='inactive'" : ""} data-s=${s.i} data-i=${r.i} data-state=${
s.name
} data-regiment=${r.name}
data-total=${r.a} data-distance=${dist} data-tip="Click to select regiment"> data-total=${r.a} data-distance=${dist} data-tip="Click to select regiment">
<svg width=".9em" height=".9em" style="margin-bottom:-1px; stroke: #333"><rect x="0" y="0" width="100%" height="100%" fill="${s.color}" ></svg> <svg width=".9em" height=".9em" style="margin-bottom:-1px; stroke: #333"><rect x="0" y="0" width="100%" height="100%" fill="${
s.color
}" ></svg>
<div style="width:6em">${s.name.slice(0, 11)}</div> <div style="width:6em">${s.name.slice(0, 11)}</div>
<div style="width:1.2em">${r.icon}</div> <div style="width:1.2em">${r.icon}</div>
<div style="width:13em">${r.name.slice(0, 24)}</div> <div style="width:13em">${r.name.slice(0, 24)}</div>
@ -267,7 +306,10 @@ class Battle {
} }
generateName(type) { generateName(type) {
const place = type === "culture" ? Names.getCulture(pack.cells.culture[this.cell], null, null, "") : Names.getBase(rand(nameBases.length - 1)); const place =
type === "culture"
? Names.getCulture(pack.cells.culture[this.cell], null, null, "")
: Names.getBase(rand(nameBases.length - 1));
document.getElementById("battleNamePlace").value = this.place = place; document.getElementById("battleNamePlace").value = this.place = place;
document.getElementById("battleNameFull").value = this.name = this.defineName(); document.getElementById("battleNameFull").value = this.name = this.defineName();
$("#battleScreen").dialog({title: this.name}); $("#battleScreen").dialog({title: this.name});
@ -286,35 +328,161 @@ class Battle {
calculateStrength(side) { calculateStrength(side) {
const scheme = { const scheme = {
// field battle phases // field battle phases
skirmish: {melee: 0.2, ranged: 2.4, mounted: 0.1, machinery: 3, naval: 1, armored: 0.2, aviation: 1.8, magical: 1.8}, // ranged excel skirmish: {
melee: 0.2,
ranged: 2.4,
mounted: 0.1,
machinery: 3,
naval: 1,
armored: 0.2,
aviation: 1.8,
magical: 1.8
}, // ranged excel
melee: {melee: 2, ranged: 1.2, mounted: 1.5, machinery: 0.5, naval: 0.2, armored: 2, aviation: 0.8, magical: 0.8}, // melee excel melee: {melee: 2, ranged: 1.2, mounted: 1.5, machinery: 0.5, naval: 0.2, armored: 2, aviation: 0.8, magical: 0.8}, // melee excel
pursue: {melee: 1, ranged: 1, mounted: 4, machinery: 0.05, naval: 1, armored: 1, aviation: 1.5, magical: 0.6}, // mounted excel pursue: {melee: 1, ranged: 1, mounted: 4, machinery: 0.05, naval: 1, armored: 1, aviation: 1.5, magical: 0.6}, // mounted excel
retreat: {melee: 0.1, ranged: 0.01, mounted: 0.5, machinery: 0.01, naval: 0.2, armored: 0.1, aviation: 0.8, magical: 0.05}, // reduced retreat: {
melee: 0.1,
ranged: 0.01,
mounted: 0.5,
machinery: 0.01,
naval: 0.2,
armored: 0.1,
aviation: 0.8,
magical: 0.05
}, // reduced
// naval battle phases // naval battle phases
shelling: {melee: 0, ranged: 0.2, mounted: 0, machinery: 2, naval: 2, armored: 0, aviation: 0.1, magical: 0.5}, // naval and machinery excel shelling: {melee: 0, ranged: 0.2, mounted: 0, machinery: 2, naval: 2, armored: 0, aviation: 0.1, magical: 0.5}, // naval and machinery excel
boarding: {melee: 1, ranged: 0.5, mounted: 0.5, machinery: 0, naval: 0.5, armored: 0.4, aviation: 0, magical: 0.2}, // melee excel boarding: {
melee: 1,
ranged: 0.5,
mounted: 0.5,
machinery: 0,
naval: 0.5,
armored: 0.4,
aviation: 0,
magical: 0.2
}, // melee excel
chase: {melee: 0, ranged: 0.15, mounted: 0, machinery: 1, naval: 1, armored: 0, aviation: 0.15, magical: 0.5}, // reduced chase: {melee: 0, ranged: 0.15, mounted: 0, machinery: 1, naval: 1, armored: 0, aviation: 0.15, magical: 0.5}, // reduced
withdrawal: {melee: 0, ranged: 0.02, mounted: 0, machinery: 0.5, naval: 0.1, armored: 0, aviation: 0.1, magical: 0.3}, // reduced withdrawal: {
melee: 0,
ranged: 0.02,
mounted: 0,
machinery: 0.5,
naval: 0.1,
armored: 0,
aviation: 0.1,
magical: 0.3
}, // reduced
// siege phases // siege phases
blockade: {melee: 0.25, ranged: 0.25, mounted: 0.2, machinery: 0.5, naval: 0.2, armored: 0.1, aviation: 0.25, magical: 0.25}, // no active actions blockade: {
sheltering: {melee: 0.3, ranged: 0.5, mounted: 0.2, machinery: 0.5, naval: 0.2, armored: 0.1, aviation: 0.25, magical: 0.25}, // no active actions melee: 0.25,
ranged: 0.25,
mounted: 0.2,
machinery: 0.5,
naval: 0.2,
armored: 0.1,
aviation: 0.25,
magical: 0.25
}, // no active actions
sheltering: {
melee: 0.3,
ranged: 0.5,
mounted: 0.2,
machinery: 0.5,
naval: 0.2,
armored: 0.1,
aviation: 0.25,
magical: 0.25
}, // no active actions
sortie: {melee: 2, ranged: 0.5, mounted: 1.2, machinery: 0.2, naval: 0.1, armored: 0.5, aviation: 1, magical: 1}, // melee excel sortie: {melee: 2, ranged: 0.5, mounted: 1.2, machinery: 0.2, naval: 0.1, armored: 0.5, aviation: 1, magical: 1}, // melee excel
bombardment: {melee: 0.2, ranged: 0.5, mounted: 0.2, machinery: 3, naval: 1, armored: 0.5, aviation: 1, magical: 1}, // machinery excel bombardment: {
storming: {melee: 1, ranged: 0.6, mounted: 0.5, machinery: 1, naval: 0.1, armored: 0.1, aviation: 0.5, magical: 0.5}, // melee excel melee: 0.2,
ranged: 0.5,
mounted: 0.2,
machinery: 3,
naval: 1,
armored: 0.5,
aviation: 1,
magical: 1
}, // machinery excel
storming: {
melee: 1,
ranged: 0.6,
mounted: 0.5,
machinery: 1,
naval: 0.1,
armored: 0.1,
aviation: 0.5,
magical: 0.5
}, // melee excel
defense: {melee: 2, ranged: 3, mounted: 1, machinery: 1, naval: 0.1, armored: 1, aviation: 0.5, magical: 1}, // ranged excel defense: {melee: 2, ranged: 3, mounted: 1, machinery: 1, naval: 0.1, armored: 1, aviation: 0.5, magical: 1}, // ranged excel
looting: {melee: 1.6, ranged: 1.6, mounted: 0.5, machinery: 0.2, naval: 0.02, armored: 0.2, aviation: 0.1, magical: 0.3}, // melee excel looting: {
surrendering: {melee: 0.1, ranged: 0.1, mounted: 0.05, machinery: 0.01, naval: 0.01, armored: 0.02, aviation: 0.01, magical: 0.03}, // reduced melee: 1.6,
ranged: 1.6,
mounted: 0.5,
machinery: 0.2,
naval: 0.02,
armored: 0.2,
aviation: 0.1,
magical: 0.3
}, // melee excel
surrendering: {
melee: 0.1,
ranged: 0.1,
mounted: 0.05,
machinery: 0.01,
naval: 0.01,
armored: 0.02,
aviation: 0.01,
magical: 0.03
}, // reduced
// ambush phases // ambush phases
surprise: {melee: 2, ranged: 2.4, mounted: 1, machinery: 1, naval: 1, armored: 1, aviation: 0.8, magical: 1.2}, // increased surprise: {melee: 2, ranged: 2.4, mounted: 1, machinery: 1, naval: 1, armored: 1, aviation: 0.8, magical: 1.2}, // increased
shock: {melee: 0.5, ranged: 0.5, mounted: 0.5, machinery: 0.4, naval: 0.3, armored: 0.1, aviation: 0.4, magical: 0.5}, // reduced shock: {
melee: 0.5,
ranged: 0.5,
mounted: 0.5,
machinery: 0.4,
naval: 0.3,
armored: 0.1,
aviation: 0.4,
magical: 0.5
}, // reduced
// langing phases // langing phases
landing: {melee: 0.8, ranged: 0.6, mounted: 0.6, machinery: 0.5, naval: 0.5, armored: 0.5, aviation: 0.5, magical: 0.6}, // reduced landing: {
flee: {melee: 0.1, ranged: 0.01, mounted: 0.5, machinery: 0.01, naval: 0.5, armored: 0.1, aviation: 0.2, magical: 0.05}, // reduced melee: 0.8,
waiting: {melee: 0.05, ranged: 0.5, mounted: 0.05, machinery: 0.5, naval: 2, armored: 0.05, aviation: 0.5, magical: 0.5}, // reduced ranged: 0.6,
mounted: 0.6,
machinery: 0.5,
naval: 0.5,
armored: 0.5,
aviation: 0.5,
magical: 0.6
}, // reduced
flee: {
melee: 0.1,
ranged: 0.01,
mounted: 0.5,
machinery: 0.01,
naval: 0.5,
armored: 0.1,
aviation: 0.2,
magical: 0.05
}, // reduced
waiting: {
melee: 0.05,
ranged: 0.5,
mounted: 0.05,
machinery: 0.5,
naval: 2,
armored: 0.05,
aviation: 0.5,
magical: 0.5
}, // reduced
// air battle phases // air battle phases
maneuvering: {melee: 0, ranged: 0.1, mounted: 0, machinery: 0.2, naval: 0, armored: 0, aviation: 1, magical: 0.2}, // aviation maneuvering: {melee: 0, ranged: 0.1, mounted: 0, machinery: 0.2, naval: 0, armored: 0, aviation: 1, magical: 0.2}, // aviation
@ -324,7 +492,8 @@ class Battle {
const forces = this.getJoinedForces(this[side].regiments); const forces = this.getJoinedForces(this[side].regiments);
const phase = this[side].phase; const phase = this[side].phase;
const adjuster = Math.max(populationRate / 10, 10); // population adjuster, by default 100 const adjuster = Math.max(populationRate / 10, 10); // population adjuster, by default 100
this[side].power = d3.sum(options.military.map(u => (forces[u.name] || 0) * u.power * scheme[phase][u.type])) / adjuster; this[side].power =
d3.sum(options.military.map(u => (forces[u.name] || 0) * u.power * scheme[phase][u.type])) / adjuster;
const UIvalue = this[side].power ? Math.max(this[side].power | 0, 1) : 0; const UIvalue = this[side].power ? Math.max(this[side].power | 0, 1) : 0;
document.getElementById("battlePower_" + side).innerHTML = UIvalue; document.getElementById("battlePower_" + side).innerHTML = UIvalue;
} }
@ -723,11 +892,13 @@ class Battle {
const status = battleStatus[+P(0.7)]; const status = battleStatus[+P(0.7)];
const result = `The ${this.getTypeName(this.type)} ended in ${status}`; const result = `The ${this.getTypeName(this.type)} ended in ${status}`;
const legend = `${this.name} took place in ${options.year} ${options.eraShort}. It was fought between ${getSide(this.attackers.regiments, 1)} and ${getSide( const legend = `${this.name} took place in ${options.year} ${options.eraShort}. It was fought between ${getSide(
this.defenders.regiments, this.attackers.regiments,
0 1
)}. ${result}. )} and ${getSide(this.defenders.regiments, 0)}. ${result}.
\r\nAttackers losses: ${getLosses(this.attackers.casualties)}%, defenders losses: ${getLosses(this.defenders.casualties)}%`; \r\nAttackers losses: ${getLosses(this.attackers.casualties)}%, defenders losses: ${getLosses(
this.defenders.casualties
)}%`;
notes.push({id: `marker${i}`, name: this.name, legend}); notes.push({id: `marker${i}`, name: this.name, legend});
tip(`${this.name} is over. ${result}`, true, "success", 4000); tip(`${this.name} is over. ${result}`, true, "success", 4000);

View file

@ -12,8 +12,8 @@ function editBiomes() {
const animate = d3.transition().duration(2000).ease(d3.easeSinIn); const animate = d3.transition().duration(2000).ease(d3.easeSinIn);
refreshBiomesEditor(); refreshBiomesEditor();
if (modules.editBiomes) return; if (fmg.modules.editBiomes) return;
modules.editBiomes = true; fmg.modules.editBiomes = true;
$("#biomesEditor").dialog({ $("#biomesEditor").dialog({
title: "Biomes Editor", title: "Biomes Editor",
@ -88,7 +88,9 @@ function editBiomes() {
const rural = b.rural[i] * populationRate; const rural = b.rural[i] * populationRate;
const urban = b.urban[i] * populationRate * urbanization; const urban = b.urban[i] * populationRate * urbanization;
const population = rn(rural + urban); 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; totalArea += area;
totalPopulation += population; totalPopulation += population;
@ -104,7 +106,9 @@ function editBiomes() {
data-color=${b.color[i]} data-color=${b.color[i]}
> >
<fill-box fill="${b.color[i]}"></fill-box> <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> <span data-tip="Biome habitability percent" class="hide">%</span>
<input <input
data-tip="Biome habitability percent. Click and set new value to change" 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> <span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="biomePopulation hide">${si(population)}</div> <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> <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> </div>
`; `;
} }
@ -403,7 +411,14 @@ function editBiomes() {
// change of append new element // change of append new element
if (exists.size()) exists.attr("data-biome", biomeNew).attr("fill", color).attr("stroke", color); 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);
}); });
} }

View file

@ -17,8 +17,8 @@ function editBurg(id) {
position: {my: "left top", at: "left+10 top+10", of: "svg", collision: "fit"} position: {my: "left top", at: "left+10 top+10", of: "svg", collision: "fit"}
}); });
if (modules.editBurg) return; if (fmg.modules.editBurg) return;
modules.editBurg = true; fmg.modules.editBurg = true;
// add listeners // add listeners
document.getElementById("burgGroupShow").addEventListener("click", showGroupSection); document.getElementById("burgGroupShow").addEventListener("click", showGroupSection);
@ -284,7 +284,9 @@ function editBurg(id) {
alertMessage.innerHTML = /* html */ `Are you sure you want to remove ${ alertMessage.innerHTML = /* html */ `Are you sure you want to remove ${
basic || capital ? "all unlocked elements in the burg group" : "the entire burg group" basic || capital ? "all unlocked elements in the burg group" : "the entire burg group"
}? }?
<br />Please note that capital or locked burgs will not be deleted. <br /><br />Burgs to be removed: ${burgsToRemove.length}`; <br />Please note that capital or locked burgs will not be deleted. <br /><br />Burgs to be removed: ${
burgsToRemove.length
}`;
$("#alert").dialog({ $("#alert").dialog({
resizable: false, resizable: false,
title: "Remove burg group", title: "Remove burg group",
@ -433,7 +435,8 @@ function editBurg(id) {
function addCustomMfcgLink() { function addCustomMfcgLink() {
const id = +elSelected.attr("data-id"); const id = +elSelected.attr("data-id");
const burg = pack.burgs[id]; const burg = pack.burgs[id];
const message = "Enter custom link to the burg map. It can be a link to Medieval Fantasy City Generator or other tool. Keep empty to use MFCG seed"; const message =
"Enter custom link to the burg map. It can be a link to Medieval Fantasy City Generator or other tool. Keep empty to use MFCG seed";
prompt(message, {default: burg.link || "", required: false}, link => { prompt(message, {default: burg.link || "", required: false}, link => {
if (link) burg.link = link; if (link) burg.link = link;
else delete burg.link; else delete burg.link;

View file

@ -11,8 +11,8 @@ function overviewBurgs() {
burgsOverviewAddLines(); burgsOverviewAddLines();
$("#burgsOverview").dialog(); $("#burgsOverview").dialog();
if (modules.overviewBurgs) return; if (fmg.modules.overviewBurgs) return;
modules.overviewBurgs = true; fmg.modules.overviewBurgs = true;
$("#burgsOverview").dialog({ $("#burgsOverview").dialog({
title: "Burgs Overview", title: "Burgs Overview",
@ -93,7 +93,9 @@ function overviewBurgs() {
data-type="${type}" data-type="${type}"
> >
<span data-tip="Click to zoom into view" class="icon-dot-circled pointer"></span> <span data-tip="Click to zoom into view" class="icon-dot-circled pointer"></span>
<input data-tip="Burg name. Click and type to change" class="burgName" value="${b.name}" autocorrect="off" spellcheck="false" /> <input data-tip="Burg name. Click and type to change" class="burgName" value="${
b.name
}" autocorrect="off" spellcheck="false" />
<input data-tip="Burg province" class="burgState" value="${province}" disabled /> <input data-tip="Burg province" class="burgState" value="${province}" disabled />
<input data-tip="Burg state" class="burgState" value="${state}" disabled /> <input data-tip="Burg state" class="burgState" value="${state}" disabled />
<select data-tip="Dominant culture. Click to change burg culture (to change cell culture use Cultures Editor)" class="stateCulture"> <select data-tip="Dominant culture. Click to change burg culture (to change cell culture use Cultures Editor)" class="stateCulture">
@ -106,10 +108,14 @@ function overviewBurgs() {
data-tip="${b.capital ? " This burg is a state capital" : "Click to assign a capital status"}" data-tip="${b.capital ? " This burg is a state capital" : "Click to assign a capital status"}"
class="icon-star-empty${b.capital ? "" : " inactive pointer"}" class="icon-star-empty${b.capital ? "" : " inactive pointer"}"
></span> ></span>
<span data-tip="Click to toggle port status" class="icon-anchor pointer${b.port ? "" : " inactive"}" style="font-size:.9em"></span> <span data-tip="Click to toggle port status" class="icon-anchor pointer${
b.port ? "" : " inactive"
}" style="font-size:.9em"></span>
</div> </div>
<span data-tip="Edit burg" class="icon-pencil"></span> <span data-tip="Edit burg" class="icon-pencil"></span>
<span class="locks pointer ${b.lock ? "icon-lock" : "icon-lock-open inactive"}" onmouseover="showElementLockTip(event)"></span> <span class="locks pointer ${
b.lock ? "icon-lock" : "icon-lock-open inactive"
}" onmouseover="showElementLockTip(event)"></span>
<span data-tip="Remove burg" class="icon-trash-empty"></span> <span data-tip="Remove burg" class="icon-trash-empty"></span>
</div>`; </div>`;
} }
@ -125,8 +131,12 @@ function overviewBurgs() {
body.querySelectorAll("div > input.burgName").forEach(el => el.addEventListener("input", changeBurgName)); body.querySelectorAll("div > input.burgName").forEach(el => el.addEventListener("input", changeBurgName));
body.querySelectorAll("div > span.icon-dot-circled").forEach(el => el.addEventListener("click", zoomIntoBurg)); body.querySelectorAll("div > span.icon-dot-circled").forEach(el => el.addEventListener("click", zoomIntoBurg));
body.querySelectorAll("div > select.stateCulture").forEach(el => el.addEventListener("change", changeBurgCulture)); body.querySelectorAll("div > select.stateCulture").forEach(el => el.addEventListener("change", changeBurgCulture));
body.querySelectorAll("div > input.burgPopulation").forEach(el => el.addEventListener("change", changeBurgPopulation)); body
body.querySelectorAll("div > span.icon-star-empty").forEach(el => el.addEventListener("click", toggleCapitalStatus)); .querySelectorAll("div > input.burgPopulation")
.forEach(el => el.addEventListener("change", changeBurgPopulation));
body
.querySelectorAll("div > span.icon-star-empty")
.forEach(el => el.addEventListener("click", toggleCapitalStatus));
body.querySelectorAll("div > span.icon-anchor").forEach(el => el.addEventListener("click", togglePortStatus)); body.querySelectorAll("div > span.icon-anchor").forEach(el => el.addEventListener("click", togglePortStatus));
body.querySelectorAll("div > span.locks").forEach(el => el.addEventListener("click", toggleBurgLockStatus)); body.querySelectorAll("div > span.locks").forEach(el => el.addEventListener("click", toggleBurgLockStatus));
body.querySelectorAll("div > span.icon-pencil").forEach(el => el.addEventListener("click", openBurgEditor)); body.querySelectorAll("div > span.icon-pencil").forEach(el => el.addEventListener("click", openBurgEditor));
@ -137,7 +147,9 @@ function overviewBurgs() {
function getCultureOptions(culture) { function getCultureOptions(culture) {
let options = ""; let options = "";
pack.cultures.filter(c => !c.removed).forEach(c => (options += `<option ${c.i === culture ? "selected" : ""} value="${c.i}">${c.name}</option>`)); pack.cultures
.filter(c => !c.removed)
.forEach(c => (options += `<option ${c.i === culture ? "selected" : ""} value="${c.i}">${c.name}</option>`));
return options; return options;
} }
@ -228,7 +240,8 @@ function overviewBurgs() {
function triggerBurgRemove() { function triggerBurgRemove() {
const burg = +this.parentNode.dataset.id; const burg = +this.parentNode.dataset.id;
if (pack.burgs[burg].capital) return tip("You cannot remove the capital. Please change the capital first", false, "error"); if (pack.burgs[burg].capital)
return tip("You cannot remove the capital. Please change the capital first", false, "error");
confirmationDialog({ confirmationDialog({
title: "Remove burg", title: "Remove burg",
@ -266,8 +279,10 @@ function overviewBurgs() {
function addBurgOnClick() { function addBurgOnClick() {
const point = d3.mouse(this); const point = d3.mouse(this);
const cell = findCell(point[0], point[1]); const cell = findCell(point[0], point[1]);
if (pack.cells.h[cell] < 20) return tip("You cannot place state into the water. Please click on a land cell", false, "error"); if (pack.cells.h[cell] < 20)
if (pack.cells.burg[cell]) return tip("There is already a burg in this cell. Please select a free cell", false, "error"); return tip("You cannot place state into the water. Please click on a land cell", false, "error");
if (pack.cells.burg[cell])
return tip("There is already a burg in this cell. Please select a free cell", false, "error");
addBurg(point); // add new burg addBurg(point); // add new burg
@ -301,7 +316,19 @@ function overviewBurgs() {
const capital = b.capital; const capital = b.capital;
const province = pack.cells.province[b.cell]; const province = pack.cells.province[b.cell];
const parent = province ? province + states.length - 1 : b.state; const parent = province ? province + states.length - 1 : b.state;
return {id, i: b.i, state: b.state, culture: b.culture, province, parent, name: b.name, population, capital, x: b.x, y: b.y}; return {
id,
i: b.i,
state: b.state,
culture: b.culture,
province,
parent,
name: b.name,
population,
capital,
x: b.x,
y: b.y
};
}); });
const data = states.concat(burgs); const data = states.concat(burgs);
if (data.length < 2) return tip("No burgs to show", false, "error"); if (data.length < 2) return tip("No burgs to show", false, "error");

View file

@ -17,8 +17,8 @@ function editCoastline(node = d3.event.target) {
drawCoastlineVertices(); drawCoastlineVertices();
viewbox.on("touchmove mousemove", null); viewbox.on("touchmove mousemove", null);
if (modules.editCoastline) return; if (fmg.modules.editCoastline) return;
modules.editCoastline = true; fmg.modules.editCoastline = true;
// add listeners // add listeners
document.getElementById("coastlineGroupsShow").addEventListener("click", showGroupSection); document.getElementById("coastlineGroupsShow").addEventListener("click", showGroupSection);
@ -55,7 +55,9 @@ function editCoastline(node = d3.event.target) {
.attr("r", 0.4) .attr("r", 0.4)
.attr("data-v", d => d) .attr("data-v", d => d)
.call(d3.drag().on("drag", dragVertex)) .call(d3.drag().on("drag", dragVertex))
.on("mousemove", () => tip("Drag to move the vertex, please use for fine-tuning only. Edit heightmap to change actual cell heights")); .on("mousemove", () =>
tip("Drag to move the vertex, please use for fine-tuning only. Edit heightmap to change actual cell heights")
);
const area = pack.features[f].area; const area = pack.features[f].area;
coastlineArea.innerHTML = si(getArea(area)) + " " + getAreaUnit(); coastlineArea.innerHTML = si(getArea(area)) + " " + getAreaUnit();

View file

@ -57,8 +57,8 @@ function editDiplomacy() {
refreshDiplomacyEditor(); refreshDiplomacyEditor();
viewbox.style("cursor", "crosshair").on("click", selectStateOnMapClick); viewbox.style("cursor", "crosshair").on("click", selectStateOnMapClick);
if (modules.editDiplomacy) return; if (fmg.modules.editDiplomacy) return;
modules.editDiplomacy = true; fmg.modules.editDiplomacy = true;
$("#diplomacyEditor").dialog({ $("#diplomacyEditor").dialog({
title: "Diplomacy Editor", title: "Diplomacy Editor",

View file

@ -1,8 +1,6 @@
// module stub to store common functions for ui editors // module stub to store common functions for ui editors
"use strict"; "use strict";
modules.editors = true;
// restore default viewbox events // restore default viewbox events
function restoreDefaultEvents() { function restoreDefaultEvents() {
svg.call(zoom); svg.call(zoom);

View file

@ -194,7 +194,14 @@ function showElevationProfile(data, routeLen, isRiver) {
.attr("fill", "darkgray"); .attr("fill", "darkgray");
let colors = getColorScheme(terrs.attr("scheme")); let colors = getColorScheme(terrs.attr("scheme"));
const landdef = chart.select("defs").append("linearGradient").attr("id", "landdef").attr("x1", "0%").attr("y1", "0%").attr("x2", "0%").attr("y2", "100%"); const landdef = chart
.select("defs")
.append("linearGradient")
.attr("id", "landdef")
.attr("x1", "0%")
.attr("y1", "0%")
.attr("x2", "0%")
.attr("y2", "100%");
if (chartData.mah == chartData.mih) { if (chartData.mah == chartData.mih) {
landdef landdef
@ -247,7 +254,14 @@ function showElevationProfile(data, routeLen, isRiver) {
path += " L" + parseInt(xscale(extra.length) + +xOffset) + "," + parseInt(yscale(0) + +yOffset); path += " L" + parseInt(xscale(extra.length) + +xOffset) + "," + parseInt(yscale(0) + +yOffset);
path += " L" + parseInt(xscale(0) + +xOffset) + "," + parseInt(yscale(0) + +yOffset); path += " L" + parseInt(xscale(0) + +xOffset) + "," + parseInt(yscale(0) + +yOffset);
path += "Z"; path += "Z";
chart.append("g").attr("id", "epland").append("path").attr("d", path).attr("stroke", "purple").attr("stroke-width", "0").attr("fill", "url(#landdef)"); chart
.append("g")
.attr("id", "epland")
.append("path")
.attr("d", path)
.attr("stroke", "purple")
.attr("stroke-width", "0")
.attr("fill", "url(#landdef)");
// biome / heights // biome / heights
let g = chart.append("g").attr("id", "epbiomes"); let g = chart.append("g").attr("id", "epbiomes");
@ -289,7 +303,14 @@ function showElevationProfile(data, routeLen, isRiver) {
chartData.cell[k] + chartData.cell[k] +
")"; ")";
g.append("rect").attr("stroke", c).attr("fill", c).attr("x", x).attr("y", y).attr("width", xscale(1)).attr("height", 15).attr("data-tip", dataTip); g.append("rect")
.attr("stroke", c)
.attr("fill", c)
.attr("x", x)
.attr("y", y)
.attr("width", xscale(1))
.attr("height", 15)
.attr("data-tip", dataTip);
} }
const xAxis = d3 const xAxis = d3
@ -371,7 +392,17 @@ function showElevationProfile(data, routeLen, isRiver) {
// arrow from burg name to graph line // arrow from burg name to graph line
g.append("path") g.append("path")
.attr("id", "eparrow" + b) .attr("id", "eparrow" + b)
.attr("d", "M" + x1.toString() + "," + (y1 + 3).toString() + "L" + x1.toString() + "," + parseInt(chartData.points[k][1] - 3).toString()) .attr(
"d",
"M" +
x1.toString() +
"," +
(y1 + 3).toString() +
"L" +
x1.toString() +
"," +
parseInt(chartData.points[k][1] - 3).toString()
)
.attr("stroke", "darkgray") .attr("stroke", "darkgray")
.attr("fill", "lightgray") .attr("fill", "lightgray")
.attr("stroke-width", "1") .attr("stroke-width", "1")
@ -385,6 +416,6 @@ function showElevationProfile(data, routeLen, isRiver) {
document.getElementById("epCurve").removeEventListener("change", draw); document.getElementById("epCurve").removeEventListener("change", draw);
document.getElementById("epSave").removeEventListener("click", downloadCSV); document.getElementById("epSave").removeEventListener("click", downloadCSV);
document.getElementById("elevationGraph").innerHTML = ""; document.getElementById("elevationGraph").innerHTML = "";
modules.elevation = false; fmg.modules.elevation = false;
} }
} }

View file

@ -1,5 +1,5 @@
"use strict"; import {findCell} from "/src/utils/graphUtils";
// Module to store general UI functions import {MOBILE} from "/src/constants";
// fit full-screen map if window is resized // fit full-screen map if window is resized
window.addEventListener("resize", function (e) { window.addEventListener("resize", function (e) {
@ -431,7 +431,7 @@ document.querySelectorAll("[data-locked]").forEach(function (e) {
}); });
// lock option // lock option
function lock(id) { export function lock(id) {
const input = document.querySelector('[data-stored="' + id + '"]'); const input = document.querySelector('[data-stored="' + id + '"]');
if (input) store(id, input.value); if (input) store(id, input.value);
const el = document.getElementById("lock_" + id); const el = document.getElementById("lock_" + id);
@ -450,13 +450,13 @@ function unlock(id) {
} }
// check if option is locked // check if option is locked
function locked(id) { export function locked(id) {
const lockEl = document.getElementById("lock_" + id); const lockEl = document.getElementById("lock_" + id);
return lockEl.dataset.locked == 1; return lockEl.dataset.locked == 1;
} }
// return key value stored in localStorage or null // return key value stored in localStorage or null
function stored(key) { export function stored(key) {
return localStorage.getItem(key) || null; return localStorage.getItem(key) || null;
} }
@ -482,7 +482,7 @@ function speak(text) {
} }
// apply drop-down menu option. If the value is not in options, add it // apply drop-down menu option. If the value is not in options, add it
function applyOption($select, value, name = value) { export function applyOption($select, value, name = value) {
const isExisting = Array.from($select.options).some(o => o.value === value); const isExisting = Array.from($select.options).some(o => o.value === value);
if (!isExisting) $select.options.add(new Option(name, value)); if (!isExisting) $select.options.add(new Option(name, value));
$select.value = value; $select.value = value;

View file

@ -8,8 +8,8 @@ function editHeightmap(options) {
if (!mode) showModeDialog(); if (!mode) showModeDialog();
else enterHeightmapEditMode(mode); else enterHeightmapEditMode(mode);
if (modules.editHeightmap) return; if (fmg.modules.editHeightmap) return;
modules.editHeightmap = true; fmg.modules.editHeightmap = true;
// add listeners // add listeners
byId("paintBrushes").on("click", openBrushesPanel); byId("paintBrushes").on("click", openBrushesPanel);
@ -29,7 +29,10 @@ function editHeightmap(options) {
<p>You can <i>keep</i> the data, but you won't be able to change the coastline.</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>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="dowloadMap();">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")} for guidance.</p>`; <p style="margin-bottom: 0">Check out ${link(
"https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Heightmap-customization",
"wiki"
)} for guidance.</p>`;
$("#alert").dialog({ $("#alert").dialog({
resizable: false, resizable: false,
@ -148,7 +151,11 @@ function editHeightmap(options) {
// Exit customization mode // Exit customization mode
function finalizeHeightmap() { function finalizeHeightmap() {
if (viewbox.select("#heights").selectAll("*").size() < 200) if (viewbox.select("#heights").selectAll("*").size() < 200)
return tip("Insufficient land area! There should be at least 200 land cells to finalize the heightmap", null, "error"); return tip(
"Insufficient land area! There should be at least 200 land cells to finalize the heightmap",
null,
"error"
);
if (byId("imageConverter").offsetParent) return tip("Please exit the Image Conversion mode first", null, "error"); if (byId("imageConverter").offsetParent) return tip("Please exit the Image Conversion mode first", null, "error");
delete window.edits; // remove global variable delete window.edits; // remove global variable
@ -210,7 +217,8 @@ function editHeightmap(options) {
if (!erosionAllowed) { if (!erosionAllowed) {
for (const i of pack.cells.i) { for (const i of pack.cells.i) {
const g = pack.cells.g[i]; const g = pack.cells.g[i];
if (pack.cells.h[i] !== grid.cells.h[g] && pack.cells.h[i] >= 20 === grid.cells.h[g] >= 20) pack.cells.h[i] = grid.cells.h[g]; if (pack.cells.h[i] !== grid.cells.h[g] && pack.cells.h[i] >= 20 === grid.cells.h[g] >= 20)
pack.cells.h[i] = grid.cells.h[g];
} }
} }
@ -349,7 +357,8 @@ function editHeightmap(options) {
const isLand = pack.cells.h[i] >= 20; const isLand = pack.cells.h[i] >= 20;
// check biome // check biome
pack.cells.biome[i] = isLand && biome[g] ? biome[g] : getBiomeId(grid.cells.prec[g], grid.cells.temp[g], pack.cells.h[i]); pack.cells.biome[i] =
isLand && biome[g] ? biome[g] : getBiomeId(grid.cells.prec[g], grid.cells.temp[g], pack.cells.h[i]);
// rivers data // rivers data
if (!erosionAllowed) { if (!erosionAllowed) {
@ -373,7 +382,9 @@ function editHeightmap(options) {
const findBurgCell = function (x, y) { const findBurgCell = function (x, y) {
let i = findCell(x, y); let i = findCell(x, y);
if (pack.cells.h[i] >= 20) return i; if (pack.cells.h[i] >= 20) return i;
const dist = pack.cells.c[i].map(c => (pack.cells.h[c] < 20 ? Infinity : (pack.cells.p[c][0] - x) ** 2 + (pack.cells.p[c][1] - y) ** 2)); const dist = pack.cells.c[i].map(c =>
pack.cells.h[c] < 20 ? Infinity : (pack.cells.p[c][0] - x) ** 2 + (pack.cells.p[c][1] - y) ** 2
);
return pack.cells.c[i][d3.scan(dist)]; return pack.cells.c[i][d3.scan(dist)];
}; };
@ -551,8 +562,8 @@ function editHeightmap(options) {
}) })
.on("dialogclose", exitBrushMode); .on("dialogclose", exitBrushMode);
if (modules.openBrushesPanel) return; if (fmg.modules.openBrushesPanel) return;
modules.openBrushesPanel = true; fmg.modules.openBrushesPanel = true;
// add listeners // add listeners
byId("brushesButtons").on("click", e => toggleBrushMode(e)); byId("brushesButtons").on("click", e => toggleBrushMode(e));
@ -630,15 +641,25 @@ function editHeightmap(options) {
const brush = document.querySelector("#brushesButtons > button.pressed").id; const brush = document.querySelector("#brushesButtons > button.pressed").id;
if (brush === "brushRaise") s.forEach(i => (h[i] = h[i] < 20 ? 20 : lim(h[i] + power))); if (brush === "brushRaise") s.forEach(i => (h[i] = h[i] < 20 ? 20 : lim(h[i] + power)));
else if (brush === "brushElevate") s.forEach((i, d) => (h[i] = lim(h[i] + interpolate(d / Math.max(s.length - 1, 1))))); else if (brush === "brushElevate")
s.forEach((i, d) => (h[i] = lim(h[i] + interpolate(d / Math.max(s.length - 1, 1)))));
else if (brush === "brushLower") s.forEach(i => (h[i] = lim(h[i] - power))); else if (brush === "brushLower") s.forEach(i => (h[i] = lim(h[i] - power)));
else if (brush === "brushDepress") s.forEach((i, d) => (h[i] = lim(h[i] - interpolate(d / Math.max(s.length - 1, 1))))); else if (brush === "brushDepress")
s.forEach((i, d) => (h[i] = lim(h[i] - interpolate(d / Math.max(s.length - 1, 1)))));
else if (brush === "brushAlign") s.forEach(i => (h[i] = lim(h[start]))); else if (brush === "brushAlign") s.forEach(i => (h[i] = lim(h[start])));
else if (brush === "brushSmooth") else if (brush === "brushSmooth")
s.forEach( s.forEach(
i => (h[i] = rn((d3.mean(grid.cells.c[i].filter(i => (land ? h[i] >= 20 : 1)).map(c => h[c])) + h[i] * (10 - power) + 0.6) / (11 - power), 1)) i =>
(h[i] = rn(
(d3.mean(grid.cells.c[i].filter(i => (land ? h[i] >= 20 : 1)).map(c => h[c])) +
h[i] * (10 - power) +
0.6) /
(11 - power),
1
))
); );
else if (brush === "brushDisrupt") s.forEach(i => (h[i] = h[i] < 15 ? h[i] : lim(h[i] + power / 1.6 - Math.random() * power))); else if (brush === "brushDisrupt")
s.forEach(i => (h[i] = h[i] < 15 ? h[i] : lim(h[i] + power / 1.6 - Math.random() * power)));
mockHeightmapSelection(s); mockHeightmapSelection(s);
// updateHistory(); uncomment to update history every step // updateHistory(); uncomment to update history every step
@ -662,7 +683,8 @@ function editHeightmap(options) {
const operator = conditionSign.value; const operator = conditionSign.value;
const operand = rescaleModifier.valueAsNumber; const operand = rescaleModifier.valueAsNumber;
if (Number.isNaN(operand)) return tip("Operand should be a number", false, "error"); if (Number.isNaN(operand)) return tip("Operand should be a number", false, "error");
if ((operator === "add" || operator === "subtract") && !Number.isInteger(operand)) return tip("Operand should be an integer", false, "error"); if ((operator === "add" || operator === "subtract") && !Number.isInteger(operand))
return tip("Operand should be an integer", false, "error");
HeightmapGenerator.setGraph(grid); HeightmapGenerator.setGraph(grid);
@ -691,7 +713,8 @@ function editHeightmap(options) {
function startFromScratch() { function startFromScratch() {
if (changeOnlyLand.checked) return tip("Not allowed when 'Change only land cells' mode is set", false, "error"); if (changeOnlyLand.checked) return tip("Not allowed when 'Change only land cells' mode is set", false, "error");
const someHeights = grid.cells.h.some(h => h); const someHeights = grid.cells.h.some(h => h);
if (!someHeights) return tip("Heightmap is already cleared, please do not click twice if not required", false, "error"); if (!someHeights)
return tip("Heightmap is already cleared, please do not click twice if not required", false, "error");
grid.cells.h = new Uint8Array(grid.cells.i.length); grid.cells.h = new Uint8Array(grid.cells.i.length);
viewbox.select("#heights").selectAll("*").remove(); viewbox.select("#heights").selectAll("*").remove();
@ -711,10 +734,15 @@ function editHeightmap(options) {
position: {my: "right top", at: "right-10 top+10", of: "svg"} position: {my: "right top", at: "right-10 top+10", of: "svg"}
}); });
if (modules.openTemplateEditor) return; if (fmg.modules.openTemplateEditor) return;
modules.openTemplateEditor = true; fmg.modules.openTemplateEditor = true;
$("#templateBody").sortable({items: "> div", handle: ".icon-resize-vertical", containment: "#templateBody", axis: "y"}); $("#templateBody").sortable({
items: "> div",
handle: ".icon-resize-vertical",
containment: "#templateBody",
axis: "y"
});
// add listeners // add listeners
$body.on("click", function (ev) { $body.on("click", function (ev) {
@ -788,22 +816,31 @@ function editHeightmap(options) {
const common = /* html */ `<div data-type="${type}">${Hide}<div style="width:4em">${type}</div>${Trash}${Reorder}`; const common = /* html */ `<div data-type="${type}">${Hide}<div style="width:4em">${type}</div>${Trash}${Reorder}`;
const TempY = /* html */ `<span>y: const TempY = /* html */ `<span>y:
<input class="templateY" data-tip="Placement range percentage along Y axis (minY-maxY)" value=${arg5 || "20-80"} /> <input class="templateY" data-tip="Placement range percentage along Y axis (minY-maxY)" value=${
arg5 || "20-80"
} />
</span>`; </span>`;
const TempX = /* html */ `<span>x: const TempX = /* html */ `<span>x:
<input class="templateX" data-tip="Placement range percentage along X axis (minX-maxX)" value=${arg4 || "15-85"} /> <input class="templateX" data-tip="Placement range percentage along X axis (minX-maxX)" value=${
arg4 || "15-85"
} />
</span>`; </span>`;
const Height = /* html */ `<span>h: const Height = /* html */ `<span>h:
<input class="templateHeight" data-tip="Blob maximum height, use hyphen to get a random number in range" value=${arg3 || "40-50"} /> <input class="templateHeight" data-tip="Blob maximum height, use hyphen to get a random number in range" value=${
arg3 || "40-50"
} />
</span>`; </span>`;
const Count = /* html */ `<span>n: const Count = /* html */ `<span>n:
<input class="templateCount" data-tip="Blobs to add, use hyphen to get a random number in range" value=${count || "1-2"} /> <input class="templateCount" data-tip="Blobs to add, use hyphen to get a random number in range" value=${
count || "1-2"
} />
</span>`; </span>`;
if (type === "Hill" || type === "Pit" || type === "Range" || type === "Trough") return /* html */ `${common}${TempY}${TempX}${Height}${Count}</div>`; if (type === "Hill" || type === "Pit" || type === "Range" || type === "Trough")
return /* html */ `${common}${TempY}${TempX}${Height}${Count}</div>`;
if (type === "Strait") if (type === "Strait")
return /* html */ `${common} return /* html */ `${common}
@ -814,7 +851,9 @@ function editHeightmap(options) {
</select> </select>
</span> </span>
<span>w: <span>w:
<input class="templateCount" data-tip="Strait width, use hyphen to get a random number in range" value=${count || "2-7"} /> <input class="templateCount" data-tip="Strait width, use hyphen to get a random number in range" value=${
count || "2-7"
} />
</span> </span>
</div>`; </div>`;
@ -1042,8 +1081,8 @@ function editHeightmap(options) {
viewbox.select("#heights").selectAll("*").remove(); viewbox.select("#heights").selectAll("*").remove();
updateHistory(); updateHistory();
if (modules.openImageConverter) return; if (fmg.modules.openImageConverter) return;
modules.openImageConverter = true; fmg.modules.openImageConverter = true;
// add color pallete // add color pallete
void (function createColorPallete() { void (function createColorPallete() {
@ -1245,7 +1284,8 @@ function editHeightmap(options) {
const assinged = []; // store assigned heights const assinged = []; // store assigned heights
unassigned.forEach(el => { unassigned.forEach(el => {
const clr = el.dataset.color; const clr = el.dataset.color;
const height = type === "hue" ? getHeightByHue(clr) : type === "lum" ? getHeightByLum(clr) : getHeightByScheme(clr); const height =
type === "hue" ? getHeightByHue(clr) : type === "lum" ? getHeightByLum(clr) : getHeightByScheme(clr);
const colorTo = color(1 - (height < 20 ? (height - 5) / 100 : height / 100)); const colorTo = color(1 - (height < 20 ? (height - 5) / 100 : height / 100));
viewbox viewbox
.select("#heights") .select("#heights")

View file

@ -13,7 +13,6 @@ function handleKeydown(event) {
} }
function handleKeyup(event) { function handleKeyup(event) {
if (!modules.editors) return; // if editors are not loaded, do nothing
if (!allowHotkeys()) return; // in some cases (e.g. in a textarea) hotkeys are not allowed if (!allowHotkeys()) return; // in some cases (e.g. in a textarea) hotkeys are not allowed
event.stopPropagation(); event.stopPropagation();

View file

@ -18,8 +18,8 @@ function editIce() {
close: closeEditor close: closeEditor
}); });
if (modules.editIce) return; if (fmg.modules.editIce) return;
modules.editIce = true; fmg.modules.editIce = true;
// add listeners // add listeners
document.getElementById("iceEditStyle").addEventListener("click", () => editStyle("ice")); document.getElementById("iceEditStyle").addEventListener("click", () => editStyle("ice"));

View file

@ -22,8 +22,8 @@ function editLabel() {
selectLabelGroup(text); selectLabelGroup(text);
updateValues(textPath); updateValues(textPath);
if (modules.editLabel) return; if (fmg.modules.editLabel) return;
modules.editLabel = true; fmg.modules.editLabel = true;
// add listeners // add listeners
document.getElementById("labelGroupShow").addEventListener("click", showGroupSection); document.getElementById("labelGroupShow").addEventListener("click", showGroupSection);
@ -78,7 +78,9 @@ function editLabel() {
} }
function updateValues(textPath) { 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("labelStartOffset").value = parseFloat(textPath.getAttribute("startOffset"));
document.getElementById("labelRelativeSize").value = parseFloat(textPath.getAttribute("font-size")); document.getElementById("labelRelativeSize").value = parseFloat(textPath.getAttribute("font-size"));
} }
@ -298,7 +300,13 @@ function editLabel() {
function changeText() { function changeText() {
const input = document.getElementById("labelText").value; const input = document.getElementById("labelText").value;
const el = elSelected.select("textPath").node(); 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 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 lines = input.split("|");
const top = (lines.length - 1) / -2; // y offset const top = (lines.length - 1) / -2; // y offset
@ -313,7 +321,8 @@ function editLabel() {
el.innerHTML = inner; el.innerHTML = inner;
example.remove(); 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() { function generateRandomName() {

View file

@ -19,8 +19,8 @@ function editLake() {
drawLakeVertices(); drawLakeVertices();
viewbox.on("touchmove mousemove", null); viewbox.on("touchmove mousemove", null);
if (modules.editLake) return; if (fmg.modules.editLake) return;
modules.editLake = true; fmg.modules.editLake = true;
// add listeners // add listeners
document.getElementById("lakeName").addEventListener("input", changeName); document.getElementById("lakeName").addEventListener("input", changeName);
@ -48,7 +48,8 @@ function editLake() {
document.getElementById("lakeArea").value = si(getArea(l.area)) + " " + getAreaUnit(); document.getElementById("lakeArea").value = si(getArea(l.area)) + " " + getAreaUnit();
const length = d3.polygonLength(l.vertices.map(v => pack.vertices.p[v])); const length = d3.polygonLength(l.vertices.map(v => pack.vertices.p[v]));
document.getElementById("lakeShoreLength").value = si(length * distanceScaleInput.value) + " " + distanceUnitInput.value; document.getElementById("lakeShoreLength").value =
si(length * distanceScaleInput.value) + " " + distanceUnitInput.value;
const lakeCells = Array.from(cells.i.filter(i => cells.f[i] === l.i)); const lakeCells = Array.from(cells.i.filter(i => cells.f[i] === l.i));
const heights = lakeCells.map(i => cells.h[i]); const heights = lakeCells.map(i => cells.h[i]);
@ -91,7 +92,9 @@ function editLake() {
.attr("r", 0.4) .attr("r", 0.4)
.attr("data-v", d => d) .attr("data-v", d => d)
.call(d3.drag().on("drag", dragVertex)) .call(d3.drag().on("drag", dragVertex))
.on("mousemove", () => tip("Drag to move the vertex, please use for fine-tuning only. Edit heightmap to change actual cell heights")); .on("mousemove", () =>
tip("Drag to move the vertex, please use for fine-tuning only. Edit heightmap to change actual cell heights")
);
} }
function dragVertex() { function dragVertex() {

View file

@ -1,5 +1,5 @@
// UI module stub to control map layers import {TIME} from "/src/config/logging";
"use strict"; import {invokeActiveZooming} from "../activeZooming";
let presets = {}; // global object let presets = {}; // global object
restoreCustomPresets(); // run on-load restoreCustomPresets(); // run on-load
@ -946,7 +946,7 @@ function toggleStates(event) {
} }
} }
function drawStates() { export function drawStates() {
TIME && console.time("drawStates"); TIME && console.time("drawStates");
regions.selectAll("path").remove(); regions.selectAll("path").remove();
@ -1110,7 +1110,7 @@ function toggleBorders(event) {
} }
// draw state and province borders // draw state and province borders
function drawBorders() { export function drawBorders() {
TIME && console.time("drawBorders"); TIME && console.time("drawBorders");
borders.selectAll("path").remove(); borders.selectAll("path").remove();
@ -1554,7 +1554,7 @@ function toggleRivers(event) {
} }
} }
function drawRivers() { export function drawRivers() {
TIME && console.time("drawRivers"); TIME && console.time("drawRivers");
rivers.selectAll("*").remove(); rivers.selectAll("*").remove();
@ -1870,7 +1870,7 @@ function drawEmblems() {
TIME && console.timeEnd("drawEmblems"); TIME && console.timeEnd("drawEmblems");
} }
function layerIsOn(el) { export function layerIsOn(el) {
const buttonoff = document.getElementById(el).classList.contains("buttonoff"); const buttonoff = document.getElementById(el).classList.contains("buttonoff");
return !buttonoff; return !buttonoff;
} }

View file

@ -10,8 +10,8 @@ function overviewMilitary() {
addLines(); addLines();
$("#militaryOverview").dialog(); $("#militaryOverview").dialog();
if (modules.overviewMilitary) return; if (fmg.modules.overviewMilitary) return;
modules.overviewMilitary = true; fmg.modules.overviewMilitary = true;
updateHeaders(); updateHeaders();
$("#militaryOverview").dialog({ $("#militaryOverview").dialog({
@ -54,7 +54,9 @@ function overviewMilitary() {
const insert = html => document.getElementById("militaryTotal").insertAdjacentHTML("beforebegin", html); const insert = html => document.getElementById("militaryTotal").insertAdjacentHTML("beforebegin", html);
for (const u of options.military) { for (const u of options.military) {
const label = capitalize(u.name.replace(/_/g, " ")); const label = capitalize(u.name.replace(/_/g, " "));
insert(`<div data-tip="State ${u.name} units number. Click to sort" class="sortable removable" data-sortby="${u.name}">${label}&nbsp;</div>`); insert(
`<div data-tip="State ${u.name} units number. Click to sort" class="sortable removable" data-sortby="${u.name}">${label}&nbsp;</div>`
);
} }
header.querySelectorAll(".removable").forEach(function (e) { header.querySelectorAll(".removable").forEach(function (e) {
e.addEventListener("click", function () { e.addEventListener("click", function () {
@ -76,7 +78,9 @@ function overviewMilitary() {
const rate = (total / population) * 100; const rate = (total / population) * 100;
const sortData = options.military.map(u => `data-${u.name}="${getForces(u)}"`).join(" "); const sortData = options.military.map(u => `data-${u.name}="${getForces(u)}"`).join(" ");
const lineData = options.military.map(u => `<div data-type="${u.name}" data-tip="State ${u.name} units number">${getForces(u)}</div>`).join(" "); const lineData = options.military
.map(u => `<div data-type="${u.name}" data-tip="State ${u.name} units number">${getForces(u)}</div>`)
.join(" ");
lines += /* html */ `<div lines += /* html */ `<div
class="states" class="states"
@ -91,9 +95,14 @@ function overviewMilitary() {
<fill-box data-tip="${s.fullName}" fill="${s.color}" disabled></fill-box> <fill-box data-tip="${s.fullName}" fill="${s.color}" disabled></fill-box>
<input data-tip="${s.fullName}" style="width:6em" value="${s.name}" readonly /> <input data-tip="${s.fullName}" style="width:6em" value="${s.name}" readonly />
${lineData} ${lineData}
<div data-type="total" data-tip="Total state military personnel (considering crew)" style="font-weight: bold">${si(total)}</div> <div data-type="total" data-tip="Total state military personnel (considering crew)" style="font-weight: bold">${si(
total
)}</div>
<div data-type="population" data-tip="State population">${si(population)}</div> <div data-type="population" data-tip="State population">${si(population)}</div>
<div data-type="rate" data-tip="Military personnel rate (% of state population). Depends on war alert">${rn(rate, 2)}%</div> <div data-type="rate" data-tip="Military personnel rate (% of state population). Depends on war alert">${rn(
rate,
2
)}%</div>
<input <input
data-tip="War Alert. Editable modifier to military forces number, depends of political situation" data-tip="War Alert. Editable modifier to military forces number, depends of political situation"
style="width:4.1em" style="width:4.1em"
@ -131,7 +140,9 @@ function overviewMilitary() {
}); });
const getForces = u => s.military.reduce((s, r) => s + (r.u[u.name] || 0), 0); const getForces = u => s.military.reduce((s, r) => s + (r.u[u.name] || 0), 0);
options.military.forEach(u => (line.dataset[u.name] = line.querySelector(`div[data-type='${u.name}']`).innerHTML = getForces(u))); options.military.forEach(
u => (line.dataset[u.name] = line.querySelector(`div[data-type='${u.name}']`).innerHTML = getForces(u))
);
const population = rn((s.rural + s.urban * urbanization) * populationRate); const population = rn((s.rural + s.urban * urbanization) * populationRate);
const total = (line.dataset.total = options.military.reduce((s, u) => s + getForces(u) * u.crew, 0)); const total = (line.dataset.total = options.military.reduce((s, u) => s + getForces(u) * u.crew, 0));
@ -237,7 +248,16 @@ function overviewMilitary() {
position: {my: "center", at: "center", of: "svg"}, position: {my: "center", at: "center", of: "svg"},
buttons: { buttons: {
Apply: applyMilitaryOptions, Apply: applyMilitaryOptions,
Add: () => addUnitLine({icon: "🛡️", name: "custom" + militaryOptionsTable.rows.length, rural: 0.2, urban: 0.5, crew: 1, power: 1, type: "melee"}), Add: () =>
addUnitLine({
icon: "🛡️",
name: "custom" + militaryOptionsTable.rows.length,
rural: 0.2,
urban: 0.5,
crew: 1,
power: 1,
type: "melee"
}),
Restore: restoreDefaultUnits, Restore: restoreDefaultUnits,
Cancel: function () { Cancel: function () {
$(this).dialog("close"); $(this).dialog("close");
@ -254,8 +274,8 @@ function overviewMilitary() {
} }
}); });
if (modules.overviewMilitaryCustomize) return; if (fmg.modules.overviewMilitaryCustomize) return;
modules.overviewMilitaryCustomize = true; fmg.modules.overviewMilitaryCustomize = true;
tableBody.addEventListener("click", event => { tableBody.addEventListener("click", event => {
const el = event.target; const el = event.target;
@ -294,7 +314,9 @@ function overviewMilitary() {
function addUnitLine(unit) { function addUnitLine(unit) {
const {type, icon, name, rural, urban, power, crew, separate} = unit; const {type, icon, name, rural, urban, power, crew, separate} = unit;
const row = document.createElement("tr"); const row = document.createElement("tr");
const typeOptions = types.map(t => `<option ${type === t ? "selected" : ""} value="${t}">${t}</option>`).join(" "); const typeOptions = types
.map(t => `<option ${type === t ? "selected" : ""} value="${t}">${t}</option>`)
.join(" ");
const getLimitButton = attr => const getLimitButton = attr =>
`<button `<button
@ -305,7 +327,9 @@ function overviewMilitary() {
${getLimitText(unit[attr])} ${getLimitText(unit[attr])}
</button>`; </button>`;
row.innerHTML = /* html */ `<td><button data-type="icon" data-tip="Click to select unit icon">${icon || " "}</button></td> row.innerHTML = /* html */ `<td><button data-type="icon" data-tip="Click to select unit icon">${
icon || " "
}</button></td>
<td><input data-tip="Type unit name. If name is changed for existing unit, old unit will be replaced" value="${name}" /></td> <td><input data-tip="Type unit name. If name is changed for existing unit, old unit will be replaced" value="${name}" /></td>
<td>${getLimitButton("biomes")}</td> <td>${getLimitButton("biomes")}</td>
<td>${getLimitButton("states")}</td> <td>${getLimitButton("states")}</td>
@ -344,7 +368,9 @@ function overviewMilitary() {
const lines = filtered.map( const lines = filtered.map(
({i, name, fullName, color}) => ({i, name, fullName, color}) =>
`<tr data-tip="${name}"><td><span style="color:${color}">⬤</span></td> `<tr data-tip="${name}"><td><span style="color:${color}">⬤</span></td>
<td><input data-i="${i}" id="el${i}" type="checkbox" class="checkbox" ${!initial.length || initial.includes(i) ? "checked" : ""} > <td><input data-i="${i}" id="el${i}" type="checkbox" class="checkbox" ${
!initial.length || initial.includes(i) ? "checked" : ""
} >
<label for="el${i}" class="checkbox-label">${fullName || name}</label> <label for="el${i}" class="checkbox-label">${fullName || name}</label>
</td></tr>` </td></tr>`
); );
@ -395,7 +421,8 @@ function overviewMilitary() {
$("#militaryOptions").dialog("close"); $("#militaryOptions").dialog("close");
options.military = unitLines.map((r, i) => { options.military = unitLines.map((r, i) => {
const elements = Array.from(r.querySelectorAll("input, button, select")); const elements = Array.from(r.querySelectorAll("input, button, select"));
const [icon, name, biomes, states, cultures, religions, rural, urban, crew, power, type, separate] = elements.map(el => { const [icon, name, biomes, states, cultures, religions, rural, urban, crew, power, type, separate] =
elements.map(el => {
const {type, value} = el.dataset || {}; const {type, value} = el.dataset || {};
if (type === "icon") return el.innerHTML || ""; if (type === "icon") return el.innerHTML || "";
if (type) return value ? value.split(",").map(v => parseInt(v)) : null; if (type) return value ? value.split(",").map(v => parseInt(v)) : null;
@ -419,7 +446,8 @@ function overviewMilitary() {
} }
function militaryRecalculate() { function militaryRecalculate() {
alertMessage.innerHTML = "Are you sure you want to recalculate military forces for all states?<br>Regiments for all states will be regenerated"; alertMessage.innerHTML =
"Are you sure you want to recalculate military forces for all states?<br>Regiments for all states will be regenerated";
$("#alert").dialog({ $("#alert").dialog({
resizable: false, resizable: false,
title: "Remove regiment", title: "Remove regiment",

View file

@ -4,8 +4,8 @@ function editNamesbase() {
closeDialogs("#namesbaseEditor, .stable"); closeDialogs("#namesbaseEditor, .stable");
$("#namesbaseEditor").dialog(); $("#namesbaseEditor").dialog();
if (modules.editNamesbase) return; if (fmg.modules.editNamesbase) return;
modules.editNamesbase = true; fmg.modules.editNamesbase = true;
// add listeners // add listeners
document.getElementById("namesbaseSelect").addEventListener("change", updateInputs); document.getElementById("namesbaseSelect").addEventListener("change", updateInputs);
@ -23,15 +23,23 @@ function editNamesbase() {
const uploader = document.getElementById("namesbaseToLoad"); const uploader = document.getElementById("namesbaseToLoad");
document.getElementById("namesbaseUpload").addEventListener("click", () => { document.getElementById("namesbaseUpload").addEventListener("click", () => {
uploader.addEventListener("change", function (event) { uploader.addEventListener(
"change",
function (event) {
uploadFile(event.target, d => namesbaseUpload(d, true)); uploadFile(event.target, d => namesbaseUpload(d, true));
}, { once: true }); },
{once: true}
);
uploader.click(); uploader.click();
}); });
document.getElementById("namesbaseUploadExtend").addEventListener("click", () => { document.getElementById("namesbaseUploadExtend").addEventListener("click", () => {
uploader.addEventListener("change", function (event) { uploader.addEventListener(
"change",
function (event) {
uploadFile(event.target, d => namesbaseUpload(d, false)); uploadFile(event.target, d => namesbaseUpload(d, false));
}, { once: true }); },
{once: true}
);
uploader.click(); uploader.click();
}); });
@ -147,21 +155,28 @@ function editNamesbase() {
: "none"; : "none";
const geminate = namesArray.map(name => name.match(/[^\w\s]|(.)(?=\1)/g) || []).flat(); const geminate = namesArray.map(name => name.match(/[^\w\s]|(.)(?=\1)/g) || []).flat();
const doubled = unique(geminate).filter(char => geminate.filter(doudledChar => doudledChar === char).length > 3) || ["none"]; const doubled = unique(geminate).filter(
char => geminate.filter(doudledChar => doudledChar === char).length > 3
) || ["none"];
const duplicates = unique(namesArray.filter((e, i, a) => a.indexOf(e) !== i)).join(", ") || "none"; const duplicates = unique(namesArray.filter((e, i, a) => a.indexOf(e) !== i)).join(", ") || "none";
const multiwordRate = d3.mean(namesArray.map(n => +n.includes(" "))); const multiwordRate = d3.mean(namesArray.map(n => +n.includes(" ")));
const getLengthQuality = () => { const getLengthQuality = () => {
if (length < 30) return "<span data-tip='Namesbase contains < 30 names - not enough to generate reasonable data' style='color:red'>[not enough]</span>"; if (length < 30)
if (length < 100) return "<span data-tip='Namesbase contains < 100 names - not enough to generate good names' style='color:darkred'>[low]</span>"; return "<span data-tip='Namesbase contains < 30 names - not enough to generate reasonable data' style='color:red'>[not enough]</span>";
if (length <= 400) return "<span data-tip='Namesbase contains a reasonable number of samples' style='color:green'>[good]</span>"; if (length < 100)
return "<span data-tip='Namesbase contains < 100 names - not enough to generate good names' style='color:darkred'>[low]</span>";
if (length <= 400)
return "<span data-tip='Namesbase contains a reasonable number of samples' style='color:green'>[good]</span>";
return "<span data-tip='Namesbase contains > 400 names. That is too much, try to reduce it to ~300 names' style='color:darkred'>[overmuch]</span>"; return "<span data-tip='Namesbase contains > 400 names. That is too much, try to reduce it to ~300 names' style='color:darkred'>[overmuch]</span>";
}; };
const getVarietyLevel = () => { const getVarietyLevel = () => {
if (variety < 15) return "<span data-tip='Namesbase average variety < 15 - generated names will be too repetitive' style='color:red'>[low]</span>"; if (variety < 15)
if (variety < 30) return "<span data-tip='Namesbase average variety < 30 - names can be too repetitive' style='color:orange'>[mean]</span>"; return "<span data-tip='Namesbase average variety < 15 - generated names will be too repetitive' style='color:red'>[low]</span>";
if (variety < 30)
return "<span data-tip='Namesbase average variety < 30 - names can be too repetitive' style='color:orange'>[mean]</span>";
return "<span data-tip='Namesbase variety is good' style='color:green'>[good]</span>"; return "<span data-tip='Namesbase variety is good' style='color:green'>[good]</span>";
}; };
@ -175,9 +190,14 @@ function editNamesbase() {
<div data-tip="Common name length">Median name length: ${d3.median(wordsLength)}</div> <div data-tip="Common name length">Median name length: ${d3.median(wordsLength)}</div>
<hr /> <hr />
<div data-tip="Characters outside of Basic Latin have bad font support">Non-basic chars: ${nonBasicLatinChars}</div> <div data-tip="Characters outside of Basic Latin have bad font support">Non-basic chars: ${nonBasicLatinChars}</div>
<div data-tip="Characters that are frequently (more than 3 times) doubled">Doubled chars: ${doubled.join("")}</div> <div data-tip="Characters that are frequently (more than 3 times) doubled">Doubled chars: ${doubled.join(
""
)}</div>
<div data-tip="Names used more than one time">Duplicates: ${duplicates}</div> <div data-tip="Names used more than one time">Duplicates: ${duplicates}</div>
<div data-tip="Percentage of names containing space character">Multi-word names: ${rn(multiwordRate * 100, 2)}%</div> <div data-tip="Percentage of names containing space character">Multi-word names: ${rn(
multiwordRate * 100,
2
)}%</div>
</div>`; </div>`;
$("#alert").dialog({ $("#alert").dialog({
@ -194,7 +214,8 @@ function editNamesbase() {
function namesbaseAdd() { function namesbaseAdd() {
const base = nameBases.length; const base = nameBases.length;
const b = "This,is,an,example,of,name,base,showing,correct,format,It,should,have,at,least,one,hundred,names,separated,with,comma"; const b =
"This,is,an,example,of,name,base,showing,correct,format,It,should,have,at,least,one,hundred,names,separated,with,comma";
nameBases.push({name: "Base" + base, min: 5, max: 12, d: "", m: 0, b}); nameBases.push({name: "Base" + base, min: 5, max: 12, d: "", m: 0, b});
document.getElementById("namesbaseSelect").add(new Option("Base" + base, base)); document.getElementById("namesbaseSelect").add(new Option("Base" + base, base));
document.getElementById("namesbaseSelect").value = base; document.getElementById("namesbaseSelect").value = base;

View file

@ -69,12 +69,12 @@ function editNotes(id, name) {
if (!window.tinymce) { if (!window.tinymce) {
const url = "https://cdn.tiny.cloud/1/4i6a79ymt2y0cagke174jp3meoi28vyecrch12e5puyw3p9a/tinymce/5/tinymce.min.js"; const url = "https://cdn.tiny.cloud/1/4i6a79ymt2y0cagke174jp3meoi28vyecrch12e5puyw3p9a/tinymce/5/tinymce.min.js";
try { try {
await import(url); await import(/* @vite-ignore */ url);
} catch (error) { } catch (error) {
// error may be caused by failed request being cached, try again with random hash // error may be caused by failed request being cached, try again with random hash
try { try {
const hash = Math.random().toString(36).substring(2, 15); const hash = Math.random().toString(36).substring(2, 15);
await import(`${url}#${hash}`); await import(/* @vite-ignore */ `${url}#${hash}`);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }

View file

@ -1,5 +1,4 @@
// UI module to control the options (preferences) import {stored, lock, locked, applyOption} from "./general";
"use strict";
$("#optionsContainer").draggable({handle: ".drag-trigger", snap: "svg", snapMode: "both"}); $("#optionsContainer").draggable({handle: ".drag-trigger", snap: "svg", snapMode: "both"});
$("#exitCustomization").draggable({handle: "div"}); $("#exitCustomization").draggable({handle: "div"});
@ -170,10 +169,7 @@ function changeMapSize() {
const maxWidth = Math.max(+mapWidthInput.value, graphWidth); const maxWidth = Math.max(+mapWidthInput.value, graphWidth);
const maxHeight = Math.max(+mapHeightInput.value, graphHeight); const maxHeight = Math.max(+mapHeightInput.value, graphHeight);
zoom.translateExtent([ Zoom.translateExtent([0, 0, maxWidth, maxHeight]);
[0, 0],
[maxWidth, maxHeight]
]);
landmass.select("rect").attr("x", 0).attr("y", 0).attr("width", maxWidth).attr("height", maxHeight); landmass.select("rect").attr("x", 0).attr("y", 0).attr("width", maxWidth).attr("height", maxHeight);
oceanPattern.select("rect").attr("x", 0).attr("y", 0).attr("width", maxWidth).attr("height", maxHeight); oceanPattern.select("rect").attr("x", 0).attr("y", 0).attr("width", maxWidth).attr("height", maxHeight);
oceanLayers.select("rect").attr("x", 0).attr("y", 0).attr("width", maxWidth).attr("height", maxHeight); oceanLayers.select("rect").attr("x", 0).attr("y", 0).attr("width", maxWidth).attr("height", maxHeight);
@ -186,7 +182,7 @@ function changeMapSize() {
} }
// just apply canvas size that was already set // just apply canvas size that was already set
function applyMapSize() { export function applyMapSize() {
const zoomMin = +zoomExtentMin.value; const zoomMin = +zoomExtentMin.value;
const zoomMax = +zoomExtentMax.value; const zoomMax = +zoomExtentMax.value;
graphWidth = +mapWidthInput.value; graphWidth = +mapWidthInput.value;
@ -194,13 +190,10 @@ function applyMapSize() {
svgWidth = Math.min(graphWidth, window.innerWidth); svgWidth = Math.min(graphWidth, window.innerWidth);
svgHeight = Math.min(graphHeight, window.innerHeight); svgHeight = Math.min(graphHeight, window.innerHeight);
svg.attr("width", svgWidth).attr("height", svgHeight); svg.attr("width", svgWidth).attr("height", svgHeight);
zoom
.translateExtent([ Zoom.translateExtent([0, 0, graphWidth, graphHeight]);
[0, 0], Zoom.scaleExtent([zoomMin, zoomMax]);
[graphWidth, graphHeight] Zoom.scaleTo(svg, zoomMin);
])
.scaleExtent([zoomMin, zoomMax])
.scaleTo(svg, zoomMin);
} }
function toggleFullscreen() { function toggleFullscreen() {
@ -217,17 +210,13 @@ function toggleFullscreen() {
} }
function toggleTranslateExtent(el) { function toggleTranslateExtent(el) {
const on = (el.dataset.on = +!+el.dataset.on); const on = !Number(el.dataset.on);
if (on) const extent = on
zoom.translateExtent([ ? [-graphWidth / 2, -graphHeight / 2, graphWidth * 1.5, graphHeight * 1.5]
[-graphWidth / 2, -graphHeight / 2], : [0, 0, graphWidth, graphHeight];
[graphWidth * 1.5, graphHeight * 1.5] Zoom.translateExtent(extent);
]);
else el.dataset.on = Number(on);
zoom.translateExtent([
[0, 0],
[graphWidth, graphHeight]
]);
} }
// add voice options // add voice options
@ -294,7 +283,8 @@ function restoreSeed(id) {
function restoreDefaultZoomExtent() { function restoreDefaultZoomExtent() {
zoomExtentMin.value = 1; zoomExtentMin.value = 1;
zoomExtentMax.value = 20; zoomExtentMax.value = 20;
zoom.scaleExtent([1, 20]).scaleTo(svg, 1); Zoom.scaleExtent([1, 20]);
Zoom.scaleTo(svg, 1);
} }
function copyMapURL() { function copyMapURL() {
@ -461,15 +451,16 @@ function changeDialogsTheme(themeColor, transparency) {
} }
function changeZoomExtent(value) { function changeZoomExtent(value) {
const min = Math.max(+zoomExtentMin.value, 0.01); const min = Math.max(+byId("zoomExtentMin").value, 0.01);
const max = Math.min(+zoomExtentMax.value, 200); const max = Math.min(+byId("zoomExtentMax").value, 200);
zoom.scaleExtent([min, max]); Zoom.scaleExtent([min, max]);
const scale = minmax(+value, 0.01, 200); const scale = minmax(+value, 0.01, 200);
zoom.scaleTo(svg, scale); Zoom.scaleTo(svg, scale);
} }
// restore options stored in localStorage // restore options stored in localStorage
function applyStoredOptions() { export function applyStoredOptions() {
if (!stored("mapWidth") || !stored("mapHeight")) { if (!stored("mapWidth") || !stored("mapHeight")) {
mapWidthInput.value = window.innerWidth; mapWidthInput.value = window.innerWidth;
mapHeightInput.value = window.innerHeight; mapHeightInput.value = window.innerHeight;
@ -530,7 +521,7 @@ function applyStoredOptions() {
} }
// randomize options if randomization is allowed (not locked or options='default') // randomize options if randomization is allowed (not locked or options='default')
function randomizeOptions() { export function randomizeOptions() {
const randomize = new URL(window.location.href).searchParams.get("options") === "default"; // ignore stored options const randomize = new URL(window.location.href).searchParams.get("options") === "default"; // ignore stored options
// 'Options' settings // 'Options' settings
@ -595,7 +586,7 @@ function randomizeCultureSet() {
} }
function setRendering(value) { function setRendering(value) {
viewbox.attr("shape-rendering", value); fmg.viewbox?.attr("shape-rendering", value);
} }
// generate current year and era name // generate current year and era name
@ -652,7 +643,7 @@ document.getElementById("sticked").addEventListener("click", function (event) {
else if (id === "saveButton") showSavePane(); else if (id === "saveButton") showSavePane();
else if (id === "exportButton") showExportPane(); else if (id === "exportButton") showExportPane();
else if (id === "loadButton") showLoadPane(); else if (id === "loadButton") showLoadPane();
else if (id === "zoomReset") resetZoom(1000); else if (id === "zoomReset") Zoom.reset(1000);
}); });
function regeneratePrompt(options) { function regeneratePrompt(options) {
@ -975,8 +966,8 @@ function toggle3dOptions() {
updateValues(); updateValues();
if (modules.options3d) return; if (fmg.modules.options3d) return;
modules.options3d = true; fmg.modules.options3d = true;
document.getElementById("options3dUpdate").addEventListener("click", ThreeD.update); document.getElementById("options3dUpdate").addEventListener("click", ThreeD.update);
document.getElementById("options3dSave").addEventListener("click", ThreeD.saveScreenshot); document.getElementById("options3dSave").addEventListener("click", ThreeD.saveScreenshot);

View file

@ -11,8 +11,8 @@ function editProvinces() {
const body = document.getElementById("provincesBodySection"); const body = document.getElementById("provincesBodySection");
refreshProvincesEditor(); refreshProvincesEditor();
if (modules.editProvinces) return; if (fmg.modules.editProvinces) return;
modules.editProvinces = true; fmg.modules.editProvinces = true;
$("#provincesEditor").dialog({ $("#provincesEditor").dialog({
title: "Provinces Editor", title: "Provinces Editor",
@ -123,7 +123,9 @@ function editProvinces() {
const rural = p.rural * populationRate; const rural = p.rural * populationRate;
const urban = p.urban * populationRate * urbanization; const urban = p.urban * populationRate * urbanization;
const population = rn(rural + urban); 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; totalPopulation += population;
const stateName = pack.states[p.state].name; const stateName = pack.states[p.state].name;
@ -144,9 +146,15 @@ function editProvinces() {
> >
<fill-box fill="${p.color}"></fill-box> <fill-box fill="${p.color}"></fill-box>
<input data-tip="Province name. Click to change" class="name pointer" value="${p.name}" readonly /> <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> <svg data-tip="Click to show and edit province emblem" class="coaIcon pointer hide" viewBox="0 0 200 200"><use href="#provinceCOA${
<input data-tip="Province form name. Click to change" class="name pointer hide" value="${p.formName}" readonly /> p.i
<span data-tip="Province capital. Click to zoom into view" class="icon-star-empty pointer hide ${p.burg ? "" : "placeholder"}"></span> }"></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 <select
data-tip="Province capital. Click to select from burgs within the state. No capital means the province is governed from the state capital" 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"}" class="cultureBase hide ${p.burgs.length ? "" : "placeholder"}"
@ -191,7 +199,9 @@ function editProvinces() {
function getCapitalOptions(burgs, capital) { function getCapitalOptions(burgs, capital) {
let options = ""; 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; return options;
} }
@ -265,7 +275,11 @@ function editProvinces() {
const {name, burg: burgId, burgs: provinceBurgs} = province; const {name, burg: burgId, burgs: provinceBurgs} = province;
if (provinceBurgs.some(b => burgs[b].capital)) 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"); if (!burgId) return tip("Cannot declare independence of a province without burg", false, "error");
const oldStateId = province.state; const oldStateId = province.state;
@ -311,7 +325,10 @@ function editProvinces() {
return relations; return relations;
}); });
diplomacy.push("x"); 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 // create new state
states.push({ states.push({
@ -373,8 +390,12 @@ function editProvinces() {
const l = n => Number(n).toLocaleString(); 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: 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"} /> <input type="number" min="0" step="1" id="urbanPop" value=${urban} style="width:6em" ${
<p>Total population: ${l(total)} <span id="totalPop">${l(total)}</span> (<span id="totalPopPerc">100</span>%)</p>`; 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 update = function () {
const totalNew = ruralPop.valueAsNumber + urbanPop.valueAsNumber; const totalNew = ruralPop.valueAsNumber + urbanPop.valueAsNumber;
@ -493,8 +514,8 @@ function editProvinces() {
position: {my: "center", at: "center", of: "svg"} position: {my: "center", at: "center", of: "svg"}
}); });
if (modules.editProvinceName) return; if (fmg.modules.editProvinceName) return;
modules.editProvinceName = true; fmg.modules.editProvinceName = true;
// add listeners // add listeners
document.getElementById("provinceNameEditorShortCulture").addEventListener("click", regenerateShortNameCuture); document.getElementById("provinceNameEditorShortCulture").addEventListener("click", regenerateShortNameCuture);
@ -692,7 +713,13 @@ function editProvinces() {
function updateChart() { function updateChart() {
const value = 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); root.sum(value);
node.data(treeLayout(root).leaves()); node.data(treeLayout(root).leaves());
@ -774,7 +801,13 @@ function editProvinces() {
customization = 11; customization = 11;
provs.select("g#provincesBody").append("g").attr("id", "temp"); 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.querySelectorAll("#provincesBottom > *").forEach(el => (el.style.display = "none"));
document.getElementById("provincesManuallyButtons").style.display = "inline-block"; document.getElementById("provincesManuallyButtons").style.display = "inline-block";
@ -786,7 +819,11 @@ function editProvinces() {
$("#provincesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}}); $("#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); 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"); body.querySelector("div").classList.add("selected");
selectProvince(+body.querySelector("div").dataset.id); selectProvince(+body.querySelector("div").dataset.id);
@ -857,7 +894,11 @@ function editProvinces() {
if (i === pack.provinces[provinceOld].center) { if (i === pack.provinces[provinceOld].center) {
const center = centers.select("polygon[data-center='" + i + "']"); const center = centers.select("polygon[data-center='" + i + "']");
if (!center.size()) centers.append("polygon").attr("data-center", i).attr("points", getPackPolygon(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; return;
} }
@ -919,7 +960,8 @@ function editProvinces() {
provincesHeader.querySelector("div[data-sortby='state']").style.left = "22em"; provincesHeader.querySelector("div[data-sortby='state']").style.left = "22em";
provincesFooter.style.display = "block"; provincesFooter.style.display = "block";
body.querySelectorAll("div > input, select, span, svg").forEach(e => (e.style.pointerEvents = "all")); 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(); restoreDefaultEvents();
clearMainTip(); clearMainTip();
@ -941,14 +983,20 @@ function editProvinces() {
const {cells, provinces} = pack; const {cells, provinces} = pack;
const point = d3.mouse(this); const point = d3.mouse(this);
const center = findCell(point[0], point[1]); 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]; const oldProvince = cells.province[center];
if (oldProvince && provinces[oldProvince].center === center) if (oldProvince && provinces[oldProvince].center === center)
return tip("The cell is already a center of a different province. Select other cell", false, "error"); return tip("The cell is already a center of a different province. Select other cell", false, "error");
const state = cells.state[center]; 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(); if (d3.event.shiftKey === false) exitAddProvinceMode();
@ -1014,7 +1062,10 @@ function editProvinces() {
function downloadProvincesData() { function downloadProvincesData() {
const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value; 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) { body.querySelectorAll(":scope > div").forEach(function (el) {
const key = parseInt(el.dataset.id); const key = parseInt(el.dataset.id);

View file

@ -19,8 +19,8 @@ function editRegiment(selector) {
position: {my: "left top", at: "left+10 top+10", of: "#map"} position: {my: "left top", at: "left+10 top+10", of: "#map"}
}); });
if (modules.editRegiment) return; if (fmg.modules.editRegiment) return;
modules.editRegiment = true; fmg.modules.editRegiment = true;
// add listeners // add listeners
document.getElementById("regimentNameRestore").addEventListener("click", restoreName); document.getElementById("regimentNameRestore").addEventListener("click", restoreName);

View file

@ -9,8 +9,8 @@ function overviewRegiments(state) {
addLines(); addLines();
$("#regimentsOverview").dialog(); $("#regimentsOverview").dialog();
if (modules.overviewRegiments) return; if (fmg.modules.overviewRegiments) return;
modules.overviewRegiments = true; fmg.modules.overviewRegiments = true;
updateHeaders(); updateHeaders();
$("#regimentsOverview").dialog({ $("#regimentsOverview").dialog({
@ -37,7 +37,9 @@ function overviewRegiments(state) {
const insert = html => document.getElementById("regimentsTotal").insertAdjacentHTML("beforebegin", html); const insert = html => document.getElementById("regimentsTotal").insertAdjacentHTML("beforebegin", html);
for (const u of options.military) { for (const u of options.military) {
const label = capitalize(u.name.replace(/_/g, " ")); const label = capitalize(u.name.replace(/_/g, " "));
insert(`<div data-tip="Regiment ${u.name} units number. Click to sort" class="sortable removable" data-sortby="${u.name}">${label}&nbsp;</div>`); insert(
`<div data-tip="Regiment ${u.name} units number. Click to sort" class="sortable removable" data-sortby="${u.name}">${label}&nbsp;</div>`
);
} }
header.querySelectorAll(".removable").forEach(function (e) { header.querySelectorAll(".removable").forEach(function (e) {
e.addEventListener("click", function () { e.addEventListener("click", function () {
@ -60,7 +62,9 @@ function overviewRegiments(state) {
for (const r of s.military) { for (const r of s.military) {
const sortData = options.military.map(u => `data-${u.name}=${r.u[u.name] || 0}`).join(" "); const sortData = options.military.map(u => `data-${u.name}=${r.u[u.name] || 0}`).join(" ");
const lineData = options.military const lineData = options.military
.map(u => `<div data-type="${u.name}" data-tip="${capitalize(u.name)} units number">${r.u[u.name] || 0}</div>`) .map(
u => `<div data-type="${u.name}" data-tip="${capitalize(u.name)} units number">${r.u[u.name] || 0}</div>`
)
.join(" "); .join(" ");
lines += /* html */ `<div class="states" data-id=${r.i} data-s="${s.i}" data-state="${s.name}" data-name="${r.name}" ${sortData} data-total="${r.a}"> lines += /* html */ `<div class="states" data-id=${r.i} data-s="${s.i}" data-state="${s.name}" data-name="${r.name}" ${sortData} data-total="${r.a}">
@ -79,7 +83,9 @@ function overviewRegiments(state) {
lines += /* html */ `<div id="regimentsTotalLine" class="totalLine" data-tip="Total of all displayed regiments"> lines += /* html */ `<div id="regimentsTotalLine" class="totalLine" data-tip="Total of all displayed regiments">
<div style="width: 21em; margin-left: 1em">Regiments: ${regiments.length}</div> <div style="width: 21em; margin-left: 1em">Regiments: ${regiments.length}</div>
${options.military.map(u => `<div style="width:5em">${si(d3.sum(regiments.map(r => r.u[u.name] || 0)))}</div>`).join(" ")} ${options.military
.map(u => `<div style="width:5em">${si(d3.sum(regiments.map(r => r.u[u.name] || 0)))}</div>`)
.join(" ")}
<div style="width:5em">${si(d3.sum(regiments.map(r => r.a)))}</div> <div style="width:5em">${si(d3.sum(regiments.map(r => r.a)))}</div>
</div>`; </div>`;
@ -92,7 +98,9 @@ function overviewRegiments(state) {
// add listeners // add listeners
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => regimentHighlightOn(ev))); body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => regimentHighlightOn(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => regimentHighlightOff(ev))); body
.querySelectorAll("div.states")
.forEach(el => el.addEventListener("mouseleave", ev => regimentHighlightOff(ev)));
} }
function updateFilter(state) { function updateFilter(state) {

View file

@ -19,8 +19,8 @@ function editReliefIcon() {
close: closeReliefEditor close: closeReliefEditor
}); });
if (modules.editReliefIcon) return; if (fmg.modules.editReliefIcon) return;
modules.editReliefIcon = true; fmg.modules.editReliefIcon = true;
// add listeners // add listeners
document.getElementById("reliefIndividual").addEventListener("click", enterIndividualMode); document.getElementById("reliefIndividual").addEventListener("click", enterIndividualMode);
@ -260,7 +260,9 @@ function editReliefIcon() {
const type = reliefIconsDiv.querySelector("svg.pressed")?.dataset.type; const type = reliefIconsDiv.querySelector("svg.pressed")?.dataset.type;
selection = type ? terrain.selectAll("use[href='" + type + "']") : terrain.selectAll("use"); selection = type ? terrain.selectAll("use[href='" + type + "']") : terrain.selectAll("use");
const size = selection.size(); const size = selection.size();
alertMessage.innerHTML = type ? `Are you sure you want to remove all ${type} icons (${size})?` : `Are you sure you want to remove all icons (${size})?`; alertMessage.innerHTML = type
? `Are you sure you want to remove all ${type} icons (${size})?`
: `Are you sure you want to remove all icons (${size})?`;
} }
$("#alert").dialog({ $("#alert").dialog({

View file

@ -21,8 +21,8 @@ function createRiver() {
close: closeRiverCreator close: closeRiverCreator
}); });
if (modules.createRiver) return; if (fmg.modules.createRiver) return;
modules.createRiver = true; fmg.modules.createRiver = true;
// add listeners // add listeners
document.getElementById("riverCreatorComplete").addEventListener("click", addRiver); document.getElementById("riverCreatorComplete").addEventListener("click", addRiver);
@ -100,12 +100,30 @@ function createRiver() {
const name = getName(mouth); const name = getName(mouth);
const basin = getBasin(parent); const basin = getBasin(parent);
rivers.push({i: riverId, source, mouth, discharge, length, width, widthFactor, sourceWidth, parent, cells: riverCells, basin, name, type: "River"}); rivers.push({
i: riverId,
source,
mouth,
discharge,
length,
width,
widthFactor,
sourceWidth,
parent,
cells: riverCells,
basin,
name,
type: "River"
});
const id = "river" + riverId; const id = "river" + riverId;
// render river // render river
lineGen.curve(d3.curveCatmullRom.alpha(0.1)); lineGen.curve(d3.curveCatmullRom.alpha(0.1));
viewbox.select("#rivers").append("path").attr("id", id).attr("d", getRiverPath(meanderedPoints, widthFactor, sourceWidth)); viewbox
.select("#rivers")
.append("path")
.attr("id", id)
.attr("d", getRiverPath(meanderedPoints, widthFactor, sourceWidth));
editRiver(id); editRiver(id);
} }

View file

@ -10,7 +10,10 @@ function editRiver(id) {
elSelected = d3.select("#" + id).on("click", addControlPoint); elSelected = d3.select("#" + id).on("click", addControlPoint);
tip("Drag control points to change the river course. Click on point to remove it. Click on river to add additional control point. For major changes please create a new river instead", true); tip(
"Drag control points to change the river course. Click on point to remove it. Click on river to add additional control point. For major changes please create a new river instead",
true
);
debug.append("g").attr("id", "controlCells"); debug.append("g").attr("id", "controlCells");
debug.append("g").attr("id", "controlPoints"); debug.append("g").attr("id", "controlPoints");
@ -29,8 +32,8 @@ function editRiver(id) {
close: closeRiverEditor close: closeRiverEditor
}); });
if (modules.editRiver) return; if (fmg.modules.editRiver) return;
modules.editRiver = true; fmg.modules.editRiver = true;
// add listeners // add listeners
document.getElementById("riverCreateSelectingCells").addEventListener("click", createRiver); document.getElementById("riverCreateSelectingCells").addEventListener("click", createRiver);
@ -163,7 +166,7 @@ function editRiver(id) {
elSelected.attr("d", path); elSelected.attr("d", path);
updateRiverLength(river); updateRiverLength(river);
if (modules.elevation) showEPForRiver(elSelected.node()); if (fmg.modules.elevation) showEPForRiver(elSelected.node());
} }
function addControlPoint() { function addControlPoint() {
@ -227,7 +230,7 @@ function editRiver(id) {
} }
function showElevationProfile() { function showElevationProfile() {
modules.elevation = true; fmg.modules.elevation = true;
showEPForRiver(elSelected.node()); showEPForRiver(elSelected.node());
} }

View file

@ -8,8 +8,8 @@ function overviewRivers() {
riversOverviewAddLines(); riversOverviewAddLines();
$("#riversOverview").dialog(); $("#riversOverview").dialog();
if (modules.overviewRivers) return; if (fmg.modules.overviewRivers) return;
modules.overviewRivers = true; fmg.modules.overviewRivers = true;
$("#riversOverview").dialog({ $("#riversOverview").dialog({
title: "Rivers Overview", title: "Rivers Overview",
@ -75,7 +75,9 @@ function overviewRivers() {
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => riverHighlightOff(ev))); body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => riverHighlightOff(ev)));
body.querySelectorAll("div > span.icon-dot-circled").forEach(el => el.addEventListener("click", zoomToRiver)); body.querySelectorAll("div > span.icon-dot-circled").forEach(el => el.addEventListener("click", zoomToRiver));
body.querySelectorAll("div > span.icon-pencil").forEach(el => el.addEventListener("click", openRiverEditor)); body.querySelectorAll("div > span.icon-pencil").forEach(el => el.addEventListener("click", openRiverEditor));
body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.addEventListener("click", triggerRiverRemove)); body
.querySelectorAll("div > span.icon-trash-empty")
.forEach(el => el.addEventListener("click", triggerRiverRemove));
applySorting(riversHeader); applySorting(riversHeader);
} }
@ -110,7 +112,18 @@ function overviewRivers() {
} else { } else {
rivers.attr("data-basin", "hightlighted"); rivers.attr("data-basin", "hightlighted");
const basins = [...new Set(pack.rivers.map(r => r.basin))]; const basins = [...new Set(pack.rivers.map(r => r.basin))];
const colors = ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf"]; const colors = [
"#1f77b4",
"#ff7f0e",
"#2ca02c",
"#d62728",
"#9467bd",
"#8c564b",
"#e377c2",
"#7f7f7f",
"#bcbd22",
"#17becf"
];
basins.forEach((b, i) => { basins.forEach((b, i) => {
const color = colors[i % colors.length]; const color = colors[i % colors.length];

View file

@ -21,8 +21,8 @@ function editRoute(onClick) {
viewbox.on("touchmove mousemove", showEditorTips); viewbox.on("touchmove mousemove", showEditorTips);
if (onClick) toggleRouteCreationMode(); if (onClick) toggleRouteCreationMode();
if (modules.editRoute) return; if (fmg.modules.editRoute) return;
modules.editRoute = true; fmg.modules.editRoute = true;
// add listeners // add listeners
document.getElementById("routeGroupsShow").addEventListener("click", showGroupSection); document.getElementById("routeGroupsShow").addEventListener("click", showGroupSection);
@ -97,11 +97,11 @@ function editRoute(onClick) {
const l = elSelected.node().getTotalLength(); const l = elSelected.node().getTotalLength();
routeLength.innerHTML = rn(l * distanceScaleInput.value) + " " + distanceUnitInput.value; routeLength.innerHTML = rn(l * distanceScaleInput.value) + " " + distanceUnitInput.value;
if (modules.elevation) showEPForRoute(elSelected.node()); if (fmg.modules.elevation) showEPForRoute(elSelected.node());
} }
function showElevationProfile() { function showElevationProfile() {
modules.elevation = true; fmg.modules.elevation = true;
showEPForRoute(elSelected.node()); showEPForRoute(elSelected.node());
} }

View file

@ -76,9 +76,22 @@ function selectStyleElement() {
// stroke color and width // stroke color and width
if ( 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"; styleStroke.style.display = "block";
styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke"); styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke");
@ -87,14 +100,29 @@ function selectStyleElement() {
} }
// stroke dash // 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"; styleStrokeDash.style.display = "block";
styleStrokeDasharrayInput.value = el.attr("stroke-dasharray") || ""; styleStrokeDasharrayInput.value = el.attr("stroke-dasharray") || "";
styleStrokeLinecapInput.value = el.attr("stroke-linecap") || "inherit"; styleStrokeLinecapInput.value = el.attr("stroke-linecap") || "inherit";
} }
// clipping // 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"; styleClipping.style.display = "block";
styleClippingInput.value = el.attr("mask") || ""; styleClippingInput.value = el.attr("mask") || "";
} }
@ -142,8 +170,12 @@ function selectStyleElement() {
if (sel === "population") { if (sel === "population") {
stylePopulation.style.display = "block"; stylePopulation.style.display = "block";
stylePopulationRuralStrokeInput.value = stylePopulationRuralStrokeOutput.value = population.select("#rural").attr("stroke"); stylePopulationRuralStrokeInput.value = stylePopulationRuralStrokeOutput.value = population
stylePopulationUrbanStrokeInput.value = stylePopulationUrbanStrokeOutput.value = population.select("#urban").attr("stroke"); .select("#rural")
.attr("stroke");
stylePopulationUrbanStrokeInput.value = stylePopulationUrbanStrokeOutput.value = population
.select("#urban")
.attr("stroke");
styleStrokeWidth.style.display = "block"; styleStrokeWidth.style.display = "block";
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || ""; styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || "";
} }
@ -233,7 +265,8 @@ function selectStyleElement() {
styleOcean.style.display = "block"; styleOcean.style.display = "block";
styleOceanFill.value = styleOceanFillOutput.value = oceanLayers.select("#oceanBase").attr("fill"); styleOceanFill.value = styleOceanFillOutput.value = oceanLayers.select("#oceanBase").attr("fill");
styleOceanPattern.value = document.getElementById("oceanicPattern")?.getAttribute("href"); 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"); outlineLayers.value = oceanLayers.attr("layers");
} }
@ -551,7 +584,10 @@ styleFontAdd.addEventListener("click", function () {
if (!family) return tip("Please provide a font name", false, "error"); 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 (existingFont) return tip("The font is already added", false, "error");
if (method === "fontURL") addWebFont(family, src); if (method === "fontURL") addWebFont(family, src);
@ -710,9 +746,9 @@ styleArmiesSize.addEventListener("input", function () {
}); });
}); });
emblemsStateSizeInput.addEventListener("change", drawEmblems); emblemsStateSizeInput.addEventListener("change", () => drawEmblems());
emblemsProvinceSizeInput.addEventListener("change", drawEmblems); emblemsProvinceSizeInput.addEventListener("change", () => drawEmblems());
emblemsBurgSizeInput.addEventListener("change", drawEmblems); emblemsBurgSizeInput.addEventListener("change", () => drawEmblems());
// request a URL to image to be used as a texture // request a URL to image to be used as a texture
function textureProvideURL() { function textureProvideURL() {

View file

@ -1,14 +1,26 @@
// UI module to control the style presets
"use strict"; "use strict";
// UI module to control the style presets
const systemPresets = ["default", "ancient", "gloom", "light", "watercolor", "clean", "atlas", "cyberpunk", "monochrome"]; const systemPresets = [
"default",
"ancient",
"gloom",
"light",
"watercolor",
"clean",
"atlas",
"cyberpunk",
"monochrome"
];
const customPresetPrefix = "fmgStyle_"; const customPresetPrefix = "fmgStyle_";
// add style presets to list // add style presets to list
{ {
const systemOptions = systemPresets.map(styleName => `<option value="${styleName}">${styleName}</option>`); const systemOptions = systemPresets.map(styleName => `<option value="${styleName}">${styleName}</option>`);
const storedStyles = Object.keys(localStorage).filter(key => key.startsWith(customPresetPrefix)); const storedStyles = Object.keys(localStorage).filter(key => key.startsWith(customPresetPrefix));
const customOptions = storedStyles.map(styleName => `<option value="${styleName}">${styleName.replace(customPresetPrefix, "")} [custom]</option>`); const customOptions = storedStyles.map(
styleName => `<option value="${styleName}">${styleName.replace(customPresetPrefix, "")} [custom]</option>`
);
const options = systemOptions.join("") + customOptions.join(""); const options = systemOptions.join("") + customOptions.join("");
document.getElementById("stylePreset").innerHTML = options; document.getElementById("stylePreset").innerHTML = options;
} }
@ -37,7 +49,8 @@ async function getStylePreset(desiredPreset) {
const isValid = JSON.isValid(storedStyleJSON); const isValid = JSON.isValid(storedStyleJSON);
if (isValid) return [desiredPreset, JSON.parse(storedStyleJSON)]; if (isValid) return [desiredPreset, JSON.parse(storedStyleJSON)];
ERROR && console.error(`Custom style ${desiredPreset} stored in localStorage is not valid. Applying default style`); ERROR &&
console.error(`Custom style ${desiredPreset} stored in localStorage is not valid. Applying default style`);
presetToLoad = "default"; presetToLoad = "default";
} }
} }
@ -126,8 +139,8 @@ function addStylePreset() {
styleSaverJSON.value = JSON.stringify(collectStyleData(), null, 2); styleSaverJSON.value = JSON.stringify(collectStyleData(), null, 2);
checkName(); checkName();
if (modules.saveStyle) return; if (fmg.modules.saveStyle) return;
modules.saveStyle = true; fmg.modules.saveStyle = true;
// add listeners // add listeners
document.getElementById("styleSaverName").addEventListener("input", checkName); document.getElementById("styleSaverName").addEventListener("input", checkName);
@ -145,8 +158,31 @@ function addStylePreset() {
"#stateBorders": ["opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter"], "#stateBorders": ["opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter"],
"#provinceBorders": ["opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter"], "#provinceBorders": ["opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter"],
"#cells": ["opacity", "stroke", "stroke-width", "filter", "mask"], "#cells": ["opacity", "stroke", "stroke-width", "filter", "mask"],
"#gridOverlay": ["opacity", "scale", "dx", "dy", "type", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "transform", "filter", "mask"], "#gridOverlay": [
"#coordinates": ["opacity", "data-size", "font-size", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter", "mask"], "opacity",
"scale",
"dx",
"dy",
"type",
"stroke",
"stroke-width",
"stroke-dasharray",
"stroke-linecap",
"transform",
"filter",
"mask"
],
"#coordinates": [
"opacity",
"data-size",
"font-size",
"stroke",
"stroke-width",
"stroke-dasharray",
"stroke-linecap",
"filter",
"mask"
],
"#compass": ["opacity", "transform", "filter", "mask", "shape-rendering"], "#compass": ["opacity", "transform", "filter", "mask", "shape-rendering"],
"#rose": ["transform"], "#rose": ["transform"],
"#relig": ["opacity", "stroke", "stroke-width", "filter"], "#relig": ["opacity", "stroke", "stroke-width", "filter"],
@ -174,7 +210,17 @@ function addStylePreset() {
"#statesBody": ["opacity", "filter"], "#statesBody": ["opacity", "filter"],
"#statesHalo": ["opacity", "data-width", "stroke-width", "filter"], "#statesHalo": ["opacity", "data-width", "stroke-width", "filter"],
"#provs": ["opacity", "fill", "font-size", "font-family", "filter"], "#provs": ["opacity", "fill", "font-size", "font-family", "filter"],
"#temperature": ["opacity", "font-size", "fill", "fill-opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter"], "#temperature": [
"opacity",
"font-size",
"fill",
"fill-opacity",
"stroke",
"stroke-width",
"stroke-dasharray",
"stroke-linecap",
"filter"
],
"#ice": ["opacity", "fill", "stroke", "stroke-width", "filter"], "#ice": ["opacity", "fill", "stroke", "stroke-width", "filter"],
"#emblems": ["opacity", "stroke-width", "filter"], "#emblems": ["opacity", "stroke-width", "filter"],
"#texture": ["opacity", "filter", "mask"], "#texture": ["opacity", "filter", "mask"],
@ -184,16 +230,65 @@ function addStylePreset() {
"#oceanBase": ["fill"], "#oceanBase": ["fill"],
"#oceanicPattern": ["href", "opacity"], "#oceanicPattern": ["href", "opacity"],
"#terrs": ["opacity", "scheme", "terracing", "skip", "relax", "curve", "filter", "mask"], "#terrs": ["opacity", "scheme", "terracing", "skip", "relax", "curve", "filter", "mask"],
"#legend": ["data-size", "font-size", "font-family", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "data-x", "data-y", "data-columns"], "#legend": [
"data-size",
"font-size",
"font-family",
"stroke",
"stroke-width",
"stroke-dasharray",
"stroke-linecap",
"data-x",
"data-y",
"data-columns"
],
"#legendBox": ["fill", "fill-opacity"], "#legendBox": ["fill", "fill-opacity"],
"#burgLabels > #cities": ["opacity", "fill", "text-shadow", "data-size", "font-size", "font-family"], "#burgLabels > #cities": ["opacity", "fill", "text-shadow", "data-size", "font-size", "font-family"],
"#burgIcons > #cities": ["opacity", "fill", "fill-opacity", "size", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap"], "#burgIcons > #cities": [
"opacity",
"fill",
"fill-opacity",
"size",
"stroke",
"stroke-width",
"stroke-dasharray",
"stroke-linecap"
],
"#anchors > #cities": ["opacity", "fill", "size", "stroke", "stroke-width"], "#anchors > #cities": ["opacity", "fill", "size", "stroke", "stroke-width"],
"#burgLabels > #towns": ["opacity", "fill", "text-shadow", "data-size", "font-size", "font-family"], "#burgLabels > #towns": ["opacity", "fill", "text-shadow", "data-size", "font-size", "font-family"],
"#burgIcons > #towns": ["opacity", "fill", "fill-opacity", "size", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap"], "#burgIcons > #towns": [
"opacity",
"fill",
"fill-opacity",
"size",
"stroke",
"stroke-width",
"stroke-dasharray",
"stroke-linecap"
],
"#anchors > #towns": ["opacity", "fill", "size", "stroke", "stroke-width"], "#anchors > #towns": ["opacity", "fill", "size", "stroke", "stroke-width"],
"#labels > #states": ["opacity", "fill", "stroke", "stroke-width", "text-shadow", "data-size", "font-size", "font-family", "filter"], "#labels > #states": [
"#labels > #addedLabels": ["opacity", "fill", "stroke", "stroke-width", "text-shadow", "data-size", "font-size", "font-family", "filter"], "opacity",
"fill",
"stroke",
"stroke-width",
"text-shadow",
"data-size",
"font-size",
"font-family",
"filter"
],
"#labels > #addedLabels": [
"opacity",
"fill",
"stroke",
"stroke-width",
"text-shadow",
"data-size",
"font-size",
"font-family",
"filter"
],
"#fogging": ["opacity", "fill", "filter"] "#fogging": ["opacity", "fill", "filter"]
}; };
@ -238,7 +333,8 @@ function addStylePreset() {
if (!styleJSON) return tip("Please provide a style JSON", false, "error"); if (!styleJSON) return tip("Please provide a style JSON", false, "error");
if (!JSON.isValid(styleJSON)) return tip("JSON string is not valid, please check the format", false, "error"); if (!JSON.isValid(styleJSON)) return tip("JSON string is not valid, please check the format", false, "error");
if (!desiredName) return tip("Please provide a preset name", false, "error"); if (!desiredName) return tip("Please provide a preset name", false, "error");
if (styleSaverTip.innerHTML === "default") return tip("You cannot overwrite default preset, please change the name", false, "error"); if (styleSaverTip.innerHTML === "default")
return tip("You cannot overwrite default preset, please change the name", false, "error");
const presetName = customPresetPrefix + desiredName; const presetName = customPresetPrefix + desiredName;
applyOption(stylePreset, presetName, desiredName + " [custom]"); applyOption(stylePreset, presetName, desiredName + " [custom]");

View file

@ -136,7 +136,14 @@ window.UISubmap = (function () {
} }
async function loadPreview($container, w, h) { async function loadPreview($container, w, h) {
const url = await getMapURL("png", {globe: false, noWater: true, fullMap: true, noLabels: true, noScaleBar: true, noIce: true}); const url = await getMapURL("png", {
globe: false,
noWater: true,
fullMap: true,
noLabels: true,
noScaleBar: true,
noIce: true
});
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext("2d");
@ -173,7 +180,11 @@ window.UISubmap = (function () {
const {angle, shiftX, shiftY, ratio, mirrorH, mirrorV} = getTransformInput(); const {angle, shiftX, shiftY, ratio, mirrorH, mirrorV} = getTransformInput();
const [cx, cy] = [graphWidth / 2, graphHeight / 2]; const [cx, cy] = [graphWidth / 2, graphHeight / 2];
const rot = alfa => (x, y) => [(x - cx) * Math.cos(alfa) - (y - cy) * Math.sin(alfa) + cx, (y - cy) * Math.cos(alfa) + (x - cx) * Math.sin(alfa) + cy]; const rot = alfa => (x, y) =>
[
(x - cx) * Math.cos(alfa) - (y - cy) * Math.sin(alfa) + cx,
(y - cy) * Math.cos(alfa) + (x - cx) * Math.sin(alfa) + cy
];
const shift = (dx, dy) => (x, y) => [x + dx, y + dy]; const shift = (dx, dy) => (x, y) => [x + dx, y + dy];
const scale = r => (x, y) => [(x - cx) * r + cx, (y - cy) * r + cy]; const scale = r => (x, y) => [(x - cx) * r + cx, (y - cy) * r + cy];
const flipH = (x, y) => [-x + 2 * cx, y]; const flipH = (x, y) => [-x + 2 * cx, y];
@ -185,7 +196,11 @@ window.UISubmap = (function () {
let inverse = id; let inverse = id;
if (angle) [projection, inverse] = [rot(angle), rot(-angle)]; if (angle) [projection, inverse] = [rot(angle), rot(-angle)];
if (ratio) [projection, inverse] = [app(scale(Math.pow(1.1, ratio)), projection), app(inverse, scale(Math.pow(1.1, -ratio)))]; if (ratio)
[projection, inverse] = [
app(scale(Math.pow(1.1, ratio)), projection),
app(inverse, scale(Math.pow(1.1, -ratio)))
];
if (mirrorH) [projection, inverse] = [app(flipH, projection), app(inverse, flipH)]; if (mirrorH) [projection, inverse] = [app(flipH, projection), app(inverse, flipH)];
if (mirrorV) [projection, inverse] = [app(flipV, projection), app(inverse, flipV)]; if (mirrorV) [projection, inverse] = [app(flipV, projection), app(inverse, flipV)];
if (shiftX || shiftY) { if (shiftX || shiftY) {
@ -208,6 +223,14 @@ window.UISubmap = (function () {
}); });
}, 1000); }, 1000);
// calculate x y extreme points of viewBox
function getViewBoxExtent() {
return [
[Math.abs(viewX / scale), Math.abs(viewY / scale)],
[Math.abs(viewX / scale) + graphWidth / scale, Math.abs(viewY / scale) + graphHeight / scale]
];
}
// Create submap from the current map. Submap limits defined by the current window size (canvas viewport) // Create submap from the current map. Submap limits defined by the current window size (canvas viewport)
const generateSubmap = debounce(function () { const generateSubmap = debounce(function () {
WARN && console.warn("Resampling current map"); WARN && console.warn("Resampling current map");
@ -244,7 +267,10 @@ window.UISubmap = (function () {
// fix scale // fix scale
distanceScaleInput.value = distanceScaleOutput.value = rn((distanceScale = distanceScaleOutput.value / scale), 2); distanceScaleInput.value = distanceScaleOutput.value = rn((distanceScale = distanceScaleOutput.value / scale), 2);
populationRateInput.value = populationRateOutput.value = rn((populationRate = populationRateOutput.value / scale), 2); populationRateInput.value = populationRateOutput.value = rn(
(populationRate = populationRateOutput.value / scale),
2
);
customization = 0; customization = 0;
startResample(options); startResample(options);
}, 1000); }, 1000);
@ -253,9 +279,9 @@ window.UISubmap = (function () {
// Do model changes with Submap.resample then do view changes if needed // Do model changes with Submap.resample then do view changes if needed
resetZoom(0); resetZoom(0);
let oldstate = { let oldstate = {
grid: deepCopy(grid), grid: structuredClone(grid),
pack: deepCopy(pack), pack: structuredClone(pack),
notes: deepCopy(notes), notes: structuredClone(notes),
seed, seed,
graphWidth, graphWidth,
graphHeight graphHeight

View file

@ -3,8 +3,8 @@ function editUnits() {
closeDialogs("#unitsEditor, .stable"); closeDialogs("#unitsEditor, .stable");
$("#unitsEditor").dialog(); $("#unitsEditor").dialog();
if (modules.editUnits) return; if (fmg.modules.editUnits) return;
modules.editUnits = true; fmg.modules.editUnits = true;
$("#unitsEditor").dialog({ $("#unitsEditor").dialog({
title: "Units Editor", title: "Units Editor",

View file

@ -34,8 +34,8 @@ function editWorld() {
updateGlobeTemperature(); updateGlobeTemperature();
updateGlobePosition(); updateGlobePosition();
if (modules.editWorld) return; if (fmg.modules.editWorld) return;
modules.editWorld = true; fmg.modules.editWorld = true;
document.getElementById("worldControls").addEventListener("input", e => updateWorld(e.target)); document.getElementById("worldControls").addEventListener("input", e => updateWorld(e.target));
globe.select("#globeWindArrows").on("click", changeWind); globe.select("#globeWindArrows").on("click", changeWind);
@ -78,11 +78,15 @@ function editWorld() {
const unit = distanceUnitInput.value; const unit = distanceUnitInput.value;
const meridian = toKilometer(eqD * 2 * scale); const meridian = toKilometer(eqD * 2 * scale);
document.getElementById("mapSize").innerHTML = `${graphWidth}x${graphHeight}`; document.getElementById("mapSize").innerHTML = `${graphWidth}x${graphHeight}`;
document.getElementById("mapSizeFriendly").innerHTML = `${rn(graphWidth * scale)}x${rn(graphHeight * scale)} ${unit}`; document.getElementById("mapSizeFriendly").innerHTML = `${rn(graphWidth * scale)}x${rn(
graphHeight * scale
)} ${unit}`;
document.getElementById("meridianLength").innerHTML = rn(eqD * 2); document.getElementById("meridianLength").innerHTML = rn(eqD * 2);
document.getElementById("meridianLengthFriendly").innerHTML = `${rn(eqD * 2 * scale)} ${unit}`; document.getElementById("meridianLengthFriendly").innerHTML = `${rn(eqD * 2 * scale)} ${unit}`;
document.getElementById("meridianLengthEarth").innerHTML = meridian ? " = " + rn(meridian / 200) + "%🌏" : ""; 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`; document.getElementById("mapCoordinates").innerHTML = `${lat(mc.latN)} ${Math.abs(rn(mc.lonW))}°W; ${lat(
mc.latS
)} ${rn(mc.lonE)}°E`;
function toKilometer(v) { function toKilometer(v) {
if (unit === "km") return v; if (unit === "km") return v;
@ -110,8 +114,12 @@ function editWorld() {
const tPole = +document.getElementById("temperaturePoleOutput").value; const tPole = +document.getElementById("temperaturePoleOutput").value;
document.getElementById("temperaturePoleF").innerHTML = rn((tPole * 9) / 5 + 32); document.getElementById("temperaturePoleF").innerHTML = rn((tPole * 9) / 5 + 32);
globe.selectAll(".tempGradient90").attr("stop-color", clr(1 - (tPole - tMin) / (tMax - tMin))); 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
globe.selectAll(".tempGradient30").attr("stop-color", clr(1 - (tEq - ((tEq - tPole) * 1) / 3 - tMin) / (tMax - tMin))); .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))); globe.select(".tempGradient0").attr("stop-color", clr(1 - (tEq - tMin) / (tMax - tMin)));
} }

View file

@ -8,8 +8,8 @@ function editZones() {
updateFilters(); updateFilters();
zonesEditorAddLines(); zonesEditorAddLines();
if (modules.editZones) return; if (fmg.modules.editZones) return;
modules.editZones = true; fmg.modules.editZones = true;
$("#zonesEditor").dialog({ $("#zonesEditor").dialog({
title: "Zones Editor", title: "Zones Editor",
@ -61,7 +61,8 @@ function editZones() {
const filterSelect = document.getElementById("zonesFilterType"); const filterSelect = document.getElementById("zonesFilterType");
const typeToFilterBy = types.includes(zonesFilterType.value) ? zonesFilterType.value : "all"; const typeToFilterBy = types.includes(zonesFilterType.value) ? zonesFilterType.value : "all";
filterSelect.innerHTML = "<option value='all'>all</option>" + types.map(type => `<option value="${type}">${type}</option>`).join(""); filterSelect.innerHTML =
"<option value='all'>all</option>" + types.map(type => `<option value="${type}">${type}</option>`).join("");
filterSelect.value = typeToFilterBy; filterSelect.value = typeToFilterBy;
} }
@ -80,9 +81,12 @@ function editZones() {
const fill = zoneEl.getAttribute("fill"); const fill = zoneEl.getAttribute("fill");
const area = getArea(d3.sum(c.map(i => pack.cells.area[i]))); const area = getArea(d3.sum(c.map(i => pack.cells.area[i])));
const rural = d3.sum(c.map(i => pack.cells.pop[i])) * populationRate; const rural = d3.sum(c.map(i => pack.cells.pop[i])) * populationRate;
const urban = d3.sum(c.map(i => pack.cells.burg[i]).map(b => pack.burgs[b].population)) * populationRate * urbanization; const urban =
d3.sum(c.map(i => pack.cells.burg[i]).map(b => pack.burgs[b].population)) * populationRate * urbanization;
const population = rural + urban; const population = rural + urban;
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}. Click to change`; const populationTip = `Total population: ${si(population)}; Rural population: ${si(
rural
)}; Urban population: ${si(urban)}. Click to change`;
const inactive = zoneEl.style.display === "none"; const inactive = zoneEl.style.display === "none";
const focused = defs.select("#fog #focus" + zoneEl.id).size(); const focused = defs.select("#fog #focus" + zoneEl.id).size();
@ -98,8 +102,12 @@ function editZones() {
<span data-tip="${populationTip}" class="icon-male hide"></span> <span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div> <div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
<span data-tip="Drag to raise or lower the zone" class="icon-resize-vertical hide"></span> <span data-tip="Drag to raise or lower the zone" class="icon-resize-vertical hide"></span>
<span data-tip="Toggle zone focus" class="icon-pin ${focused ? "" : " inactive"} hide ${c.length ? "" : " placeholder"}"></span> <span data-tip="Toggle zone focus" class="icon-pin ${focused ? "" : " inactive"} hide ${
<span data-tip="Toggle zone visibility" class="icon-eye ${inactive ? " inactive" : ""} hide ${c.length ? "" : " placeholder"}"></span> c.length ? "" : " placeholder"
}"></span>
<span data-tip="Toggle zone visibility" class="icon-eye ${inactive ? " inactive" : ""} hide ${
c.length ? "" : " placeholder"
}"></span>
<span data-tip="Remove zone" class="icon-trash-empty hide"></span> <span data-tip="Remove zone" class="icon-trash-empty hide"></span>
</div>`; </div>`;
}); });
@ -109,7 +117,9 @@ function editZones() {
// update footer // update footer
const totalArea = getArea(graphWidth * graphHeight); const totalArea = getArea(graphWidth * graphHeight);
zonesFooterArea.dataset.area = totalArea; zonesFooterArea.dataset.area = totalArea;
const totalPop = (d3.sum(pack.cells.pop) + d3.sum(pack.burgs.filter(b => !b.removed).map(b => b.population)) * urbanization) * populationRate; const totalPop =
(d3.sum(pack.cells.pop) + d3.sum(pack.burgs.filter(b => !b.removed).map(b => b.population)) * urbanization) *
populationRate;
zonesFooterPopulation.dataset.population = totalPop; zonesFooterPopulation.dataset.population = totalPop;
zonesFooterNumber.innerHTML = /* html */ `${filteredZones.length} of ${zones.length}`; zonesFooterNumber.innerHTML = /* html */ `${filteredZones.length} of ${zones.length}`;
zonesFooterCells.innerHTML = pack.cells.i.length; zonesFooterCells.innerHTML = pack.cells.i.length;
@ -150,7 +160,13 @@ function editZones() {
zonesEditorAddLines(); zonesEditorAddLines();
} }
$(body).sortable({items: "div.states", handle: ".icon-resize-vertical", containment: "parent", axis: "y", update: movezone}); $(body).sortable({
items: "div.states",
handle: ".icon-resize-vertical",
containment: "parent",
axis: "y",
update: movezone
});
function movezone(ev, ui) { function movezone(ev, ui) {
const zone = $("#" + ui.item.attr("data-id")); const zone = $("#" + ui.item.attr("data-id"));
const prev = $("#" + ui.item.prev().attr("data-id")); const prev = $("#" + ui.item.prev().attr("data-id"));
@ -174,7 +190,11 @@ function editZones() {
$("#zonesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}}); $("#zonesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
tip("Click to select a zone, drag to paint a zone", true); tip("Click to select a zone, drag to paint a zone", true);
viewbox.style("cursor", "crosshair").on("click", selectZoneOnMapClick).call(d3.drag().on("start", dragZoneBrush)).on("touchmove mousemove", moveZoneBrush); viewbox
.style("cursor", "crosshair")
.on("click", selectZoneOnMapClick)
.call(d3.drag().on("start", dragZoneBrush))
.on("touchmove mousemove", moveZoneBrush);
body.querySelector("div").classList.add("selected"); body.querySelector("div").classList.add("selected");
zones.selectAll("g").each(function () { zones.selectAll("g").each(function () {
@ -285,7 +305,8 @@ function editZones() {
zonesEditor.querySelectorAll(".hide:not(.show)").forEach(el => el.classList.remove("hidden")); zonesEditor.querySelectorAll(".hide:not(.show)").forEach(el => el.classList.remove("hidden"));
zonesFooter.style.display = "block"; zonesFooter.style.display = "block";
body.querySelectorAll("div > input, select, svg").forEach(e => (e.style.pointerEvents = "all")); body.querySelectorAll("div > input, select, svg").forEach(e => (e.style.pointerEvents = "all"));
if (!close) $("#zonesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}}); if (!close)
$("#zonesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
restoreDefaultEvents(); restoreDefaultEvents();
clearMainTip(); clearMainTip();
@ -356,7 +377,8 @@ function editZones() {
body.querySelectorAll(":scope > div").forEach(function (el) { body.querySelectorAll(":scope > div").forEach(function (el) {
el.querySelector(".stateCells").innerHTML = rn((+el.dataset.cells / totalCells) * 100, 2) + "%"; el.querySelector(".stateCells").innerHTML = rn((+el.dataset.cells / totalCells) * 100, 2) + "%";
el.querySelector(".biomeArea").innerHTML = rn((+el.dataset.area / totalArea) * 100, 2) + "%"; el.querySelector(".biomeArea").innerHTML = rn((+el.dataset.area / totalArea) * 100, 2) + "%";
el.querySelector(".culturePopulation").innerHTML = rn((+el.dataset.population / totalPopulation) * 100, 2) + "%"; el.querySelector(".culturePopulation").innerHTML =
rn((+el.dataset.population / totalPopulation) * 100, 2) + "%";
}); });
} else { } else {
body.dataset.type = "absolute"; body.dataset.type = "absolute";
@ -369,7 +391,13 @@ function editZones() {
const description = "Unknown zone"; const description = "Unknown zone";
const type = "Unknown"; const type = "Unknown";
const fill = "url(#hatch" + (id.slice(4) % 42) + ")"; const fill = "url(#hatch" + (id.slice(4) % 42) + ")";
zones.append("g").attr("id", id).attr("data-description", description).attr("data-type", type).attr("data-cells", "").attr("fill", fill); zones
.append("g")
.attr("id", id)
.attr("data-description", description)
.attr("data-type", type)
.attr("data-cells", "")
.attr("fill", fill);
zonesEditorAddLines(); zonesEditorAddLines();
} }
@ -411,13 +439,19 @@ function editZones() {
const burgs = pack.burgs.filter(b => !b.removed && cells.includes(b.cell)); const burgs = pack.burgs.filter(b => !b.removed && cells.includes(b.cell));
const rural = rn(d3.sum(cells.map(i => pack.cells.pop[i])) * populationRate); const rural = rn(d3.sum(cells.map(i => pack.cells.pop[i])) * populationRate);
const urban = rn(d3.sum(cells.map(i => pack.cells.burg[i]).map(b => pack.burgs[b].population)) * populationRate * urbanization); const urban = rn(
d3.sum(cells.map(i => pack.cells.burg[i]).map(b => pack.burgs[b].population)) * populationRate * urbanization
);
const total = rural + urban; const total = rural + urban;
const l = n => Number(n).toLocaleString(); 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: 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" ${burgs.length ? "" : "disabled"} /> <input type="number" min="0" step="1" id="urbanPop" value=${urban} style="width:6em" ${
<p>Total population: ${l(total)} <span id="totalPop">${l(total)}</span> (<span id="totalPopPerc">100</span>%)</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 update = function () {
const totalNew = ruralPop.valueAsNumber + urbanPop.valueAsNumber; const totalNew = ruralPop.valueAsNumber + urbanPop.valueAsNumber;

52
modules/zoom.js Normal file
View file

@ -0,0 +1,52 @@
"use strict";
// temporary expose to global
let scale = 1;
let viewX = 0;
let viewY = 0;
window.Zoom = (function () {
function onZoom() {
const {k, x, y} = d3.event.transform;
const isScaleChanged = Boolean(scale - k);
const isPositionChanged = Boolean(viewX - x || viewY - y);
if (!isScaleChanged && !isPositionChanged) return;
scale = k;
viewX = x;
viewY = y;
handleZoom(isScaleChanged, isPositionChanged);
}
const onZoomDebouced = debounce(onZoom, 50);
const zoom = d3.zoom().scaleExtent([1, 20]).on("zoom", onZoomDebouced);
// zoom to a specific point
function to(x, y, z = 8, d = 2000) {
const transform = d3.zoomIdentity.translate(x * -z + graphWidth / 2, y * -z + graphHeight / 2).scale(z);
svg.transition().duration(d).call(zoom.transform, transform);
}
// reset zoom to initial
function reset(d = 1000) {
svg.transition().duration(d).call(zoom.transform, d3.zoomIdentity);
}
function scaleExtent([min, max]) {
zoom.scaleExtent([min, max]);
}
function translateExtent([x1, y1, x2, y2]) {
zoom.translateExtent([
[x1, y1],
[x2, y2]
]);
}
function scaleTo(element, scale) {
zoom.scaleTo(element, scale);
}
return {to, reset, scaleExtent, translateExtent, scaleTo};
})();

View file

@ -8,7 +8,7 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"devDependencies": { "devDependencies": {
"vite": "^2.9.12", "typescript": "^4.7.4",
"typescript": "^4.7.4" "vite": "^2.9.12"
} }
} }

7
src/config/logging.ts Normal file
View file

@ -0,0 +1,7 @@
import {PRODUCTION} from "../constants";
export const DEBUG = Boolean(localStorage.getItem("debug"));
export const INFO = DEBUG || !PRODUCTION;
export const TIME = DEBUG || !PRODUCTION;
export const WARN = true;
export const ERROR = true;

9
src/constants/index.ts Normal file
View file

@ -0,0 +1,9 @@
export const PRODUCTION = location.hostname && location.hostname !== "localhost" && location.hostname !== "127.0.0.1";
// detect device
export const MOBILE = window.innerWidth < 600 || window.navigator.userAgentData?.mobile;
// typed arrays max values
export const UINT8_MAX = 255;
export const UINT16_MAX = 65535;
export const UINT32_MAX = 4294967295;

View file

@ -1,26 +1,22 @@
// Azgaar (azgaar.fmg@yandex.com). Minsk, 2017-2022. MIT License // Azgaar (azgaar.fmg@yandex.com). Minsk, 2017-2022. MIT License
// https://github.com/Azgaar/Fantasy-Map-Generator // https://github.com/Azgaar/Fantasy-Map-Generator
"use strict"; import {PRODUCTION, UINT16_MAX} from "./constants";
// set debug options import {INFO, TIME, WARN, ERROR} from "./config/logging";
const PRODUCTION = location.hostname && location.hostname !== "localhost" && location.hostname !== "127.0.0.1"; import {createTypedArray} from "./utils";
const DEBUG = localStorage.getItem("debug"); import {shouldRegenerateGrid, generateGrid, calculateVoronoi, getPackPolygon, isLand} from "./utils/graphUtils";
const INFO = DEBUG || !PRODUCTION; import {drawRivers, drawStates, drawBorders} from "../modules/ui/layers";
const TIME = DEBUG || !PRODUCTION; import {invokeActiveZooming} from "../modules/activeZooming";
const WARN = true; import {applyStoredOptions, applyMapSize, randomizeOptions} from "../modules/ui/options";
const ERROR = true; import {locked} from "../modules/ui/general";
// detect device globalThis.fmg = {
const MOBILE = window.innerWidth < 600 || navigator.userAgentData?.mobile; modules: {}
};
// typed arrays max values
const UINT8_MAX = 255;
const UINT16_MAX = 65535;
const UINT32_MAX = 4294967295;
if (PRODUCTION && "serviceWorker" in navigator) { if (PRODUCTION && "serviceWorker" in navigator) {
window.addEventListener("load", () => { window.addEventListener("load", () => {
navigator.serviceWorker.register("./sw.js").catch(err => { navigator.serviceWorker.register("../sw.js").catch(err => {
console.error("ServiceWorker registration failed: ", err); console.error("ServiceWorker registration failed: ", err);
}); });
}); });
@ -29,170 +25,47 @@ if (PRODUCTION && "serviceWorker" in navigator) {
"beforeinstallprompt", "beforeinstallprompt",
async event => { async event => {
event.preventDefault(); event.preventDefault();
const Installation = await import("./modules/dynamic/installation.js"); const Installation = await import("../modules/dynamic/installation.js");
Installation.init(event); Installation.init(event);
}, },
{once: true} {once: true}
); );
} }
// append svg layers (in default order)
let svg = d3.select("#map");
let defs = svg.select("#deftemp");
let viewbox = svg.select("#viewbox");
let scaleBar = svg.select("#scaleBar");
let legend = svg.append("g").attr("id", "legend");
let ocean = viewbox.append("g").attr("id", "ocean");
let oceanLayers = ocean.append("g").attr("id", "oceanLayers");
let oceanPattern = ocean.append("g").attr("id", "oceanPattern");
let lakes = viewbox.append("g").attr("id", "lakes");
let landmass = viewbox.append("g").attr("id", "landmass");
let texture = viewbox.append("g").attr("id", "texture");
let terrs = viewbox.append("g").attr("id", "terrs");
let biomes = viewbox.append("g").attr("id", "biomes");
let cells = viewbox.append("g").attr("id", "cells");
let gridOverlay = viewbox.append("g").attr("id", "gridOverlay");
let coordinates = viewbox.append("g").attr("id", "coordinates");
let compass = viewbox.append("g").attr("id", "compass");
let rivers = viewbox.append("g").attr("id", "rivers");
let terrain = viewbox.append("g").attr("id", "terrain");
let relig = viewbox.append("g").attr("id", "relig");
let cults = viewbox.append("g").attr("id", "cults");
let regions = viewbox.append("g").attr("id", "regions");
let statesBody = regions.append("g").attr("id", "statesBody");
let statesHalo = regions.append("g").attr("id", "statesHalo");
let provs = viewbox.append("g").attr("id", "provs");
let zones = viewbox.append("g").attr("id", "zones").style("display", "none");
let borders = viewbox.append("g").attr("id", "borders");
let stateBorders = borders.append("g").attr("id", "stateBorders");
let provinceBorders = borders.append("g").attr("id", "provinceBorders");
let routes = viewbox.append("g").attr("id", "routes");
let roads = routes.append("g").attr("id", "roads");
let trails = routes.append("g").attr("id", "trails");
let searoutes = routes.append("g").attr("id", "searoutes");
let temperature = viewbox.append("g").attr("id", "temperature");
let coastline = viewbox.append("g").attr("id", "coastline");
let ice = viewbox.append("g").attr("id", "ice").style("display", "none");
let prec = viewbox.append("g").attr("id", "prec").style("display", "none");
let population = viewbox.append("g").attr("id", "population");
let emblems = viewbox.append("g").attr("id", "emblems").style("display", "none");
let labels = viewbox.append("g").attr("id", "labels");
let icons = viewbox.append("g").attr("id", "icons");
let burgIcons = icons.append("g").attr("id", "burgIcons");
let anchors = icons.append("g").attr("id", "anchors");
let armies = viewbox.append("g").attr("id", "armies").style("display", "none");
let markers = viewbox.append("g").attr("id", "markers");
let fogging = viewbox.append("g").attr("id", "fogging-cont").attr("mask", "url(#fog)").append("g").attr("id", "fogging").style("display", "none");
let ruler = viewbox.append("g").attr("id", "ruler").style("display", "none");
let debug = viewbox.append("g").attr("id", "debug");
// lake and coast groups
lakes.append("g").attr("id", "freshwater");
lakes.append("g").attr("id", "salt");
lakes.append("g").attr("id", "sinkhole");
lakes.append("g").attr("id", "frozen");
lakes.append("g").attr("id", "lava");
lakes.append("g").attr("id", "dry");
coastline.append("g").attr("id", "sea_island");
coastline.append("g").attr("id", "lake_island");
labels.append("g").attr("id", "states");
labels.append("g").attr("id", "addedLabels");
let burgLabels = labels.append("g").attr("id", "burgLabels");
burgIcons.append("g").attr("id", "cities");
burgLabels.append("g").attr("id", "cities");
anchors.append("g").attr("id", "cities");
burgIcons.append("g").attr("id", "towns");
burgLabels.append("g").attr("id", "towns");
anchors.append("g").attr("id", "towns");
// population groups
population.append("g").attr("id", "rural");
population.append("g").attr("id", "urban");
// emblem groups
emblems.append("g").attr("id", "burgEmblems");
emblems.append("g").attr("id", "provinceEmblems");
emblems.append("g").attr("id", "stateEmblems");
// fogging
fogging.append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%");
fogging.append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%").attr("fill", "#e8f0f6").attr("filter", "url(#splotch)");
// assign events separately as not a viewbox child
scaleBar.on("mousemove", () => tip("Click to open Units Editor")).on("click", () => editUnits());
legend.on("mousemove", () => tip("Drag to change the position. Click to hide the legend")).on("click", () => clearLegend());
// main data variables
let grid = {}; // initial graph based on jittered square grid and data
let pack = {}; // packed graph and data
let seed;
let mapId;
let mapHistory = [];
let elSelected;
let modules = {};
let notes = [];
let rulers = new Rulers();
let customization = 0;
let biomesData = applyDefaultBiomesSystem();
let nameBases = Names.getNameBases(); // cultures-related data
let color = d3.scaleSequential(d3.interpolateSpectral); // default color scheme
const lineGen = d3.line().curve(d3.curveBasis); // d3 line generator with default curve interpolation
// d3 zoom behavior
let scale = 1;
let viewX = 0;
let viewY = 0;
function onZoom() {
const {k, x, y} = d3.event.transform;
const isScaleChanged = Boolean(scale - k);
const isPositionChanged = Boolean(viewX - x || viewY - y);
if (!isScaleChanged && !isPositionChanged) return;
scale = k;
viewX = x;
viewY = y;
handleZoom(isScaleChanged, isPositionChanged);
}
const onZoomDebouced = debounce(onZoom, 50);
const zoom = d3.zoom().scaleExtent([1, 20]).on("zoom", onZoomDebouced);
// default options // default options
let options = { options = {
pinNotes: false, pinNotes: false,
showMFCGMap: true, showMFCGMap: true,
winds: [225, 45, 225, 315, 135, 315], winds: [225, 45, 225, 315, 135, 315],
stateLabelsMode: "auto" stateLabelsMode: "auto"
}; };
let mapCoordinates = {}; // map coordinates on globe mapCoordinates = {}; // map coordinates on globe
let populationRate = +document.getElementById("populationRateInput").value; populationRate = +byId("populationRateInput").value;
let distanceScale = +document.getElementById("distanceScaleInput").value; distanceScale = +byId("distanceScaleInput").value;
let urbanization = +document.getElementById("urbanizationInput").value; urbanization = +byId("urbanizationInput").value;
let urbanDensity = +document.getElementById("urbanDensityInput").value; urbanDensity = +byId("urbanDensityInput").value;
let statesNeutral = 1; // statesEditor growth parameter statesNeutral = 1; // statesEditor growth parameter
applyStoredOptions(); applyStoredOptions();
rulers = new Rulers();
biomesData = Biomes.getDefault();
nameBases = Names.getNameBases(); // cultures-related data
color = d3.scaleSequential(d3.interpolateSpectral); // default color scheme
lineGen = d3.line().curve(d3.curveBasis); // d3 line generator with default curve interpolation
// voronoi graph extension, cannot be changed after generation // voronoi graph extension, cannot be changed after generation
let graphWidth = +mapWidthInput.value; graphWidth = +byId("mapWidthInput").value;
let graphHeight = +mapHeightInput.value; graphHeight = +byId("mapHeightInput").value;
// svg canvas resolution, can be changed // svg canvas resolution, can be changed
let svgWidth = graphWidth; svgWidth = graphWidth;
let svgHeight = graphHeight; svgHeight = graphHeight;
landmass.append("rect").attr("x", 0).attr("y", 0).attr("width", graphWidth).attr("height", graphHeight); defineSvg(graphWidth, graphHeight);
oceanPattern.append("rect").attr("fill", "url(#oceanic)").attr("x", 0).attr("y", 0).attr("width", graphWidth).attr("height", graphHeight);
oceanLayers.append("rect").attr("id", "oceanBase").attr("x", 0).attr("y", 0).attr("width", graphWidth).attr("height", graphHeight);
document.addEventListener("DOMContentLoaded", async () => { document.on("DOMContentLoaded", async () => {
if (!location.hostname) { if (!location.hostname) {
const wiki = "https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Run-FMG-locally"; const wiki = "https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Run-FMG-locally";
alertMessage.innerHTML = /* html */ `Fantasy Map Generator cannot run serverless. Follow the <a href="${wiki}" target="_blank">instructions</a> on how you can alertMessage.innerHTML = /* html */ `Fantasy Map Generator cannot run serverless. Follow the <a href="${wiki}" target="_blank">instructions</a> on how you can
@ -321,7 +194,7 @@ function focusOn() {
if (cellParam) { if (cellParam) {
const cell = +params.get("cell"); const cell = +params.get("cell");
const [x, y] = pack.cells.p[cell]; const [x, y] = pack.cells.p[cell];
zoomTo(x, y, scale, 1600); Zoom.to(x, y, scale, 1600);
return; return;
} }
@ -330,13 +203,13 @@ function focusOn() {
if (!burg) return; if (!burg) return;
const {x, y} = burg; const {x, y} = burg;
zoomTo(x, y, scale, 1600); Zoom.to(x, y, scale, 1600);
return; return;
} }
const x = +params.get("x") || graphWidth / 2; const x = +params.get("x") || graphWidth / 2;
const y = +params.get("y") || graphHeight / 2; const y = +params.get("y") || graphHeight / 2;
zoomTo(x, y, scale, 1600); Zoom.to(x, y, scale, 1600);
} }
} }
@ -399,183 +272,18 @@ function findBurgForMFCG(params) {
}); });
} }
zoomTo(b.x, b.y, 8, 1600); Zoom.to(b.x, b.y, 8, 1600);
invokeActiveZooming(); invokeActiveZooming();
tip("Here stands the glorious city of " + b.name, true, "success", 15000); tip("Here stands the glorious city of " + b.name, true, "success", 15000);
} }
// apply default biomes data
function applyDefaultBiomesSystem() {
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};
}
function handleZoom(isScaleChanged, isPositionChanged) {
viewbox.attr("transform", `translate(${viewX} ${viewY}) scale(${scale})`);
if (isPositionChanged) drawCoordinates();
if (isScaleChanged) {
invokeActiveZooming();
drawScaleBar(scale);
}
// zoom image converter overlay
if (customization === 1) {
const canvas = document.getElementById("canvas");
if (!canvas || canvas.style.opacity === "0") return;
const img = document.getElementById("imageToConvert");
if (!img) return;
const ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.setTransform(scale, 0, 0, scale, viewX, viewY);
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
}
}
// Zoom to a specific point
function zoomTo(x, y, z = 8, d = 2000) {
const transform = d3.zoomIdentity.translate(x * -z + graphWidth / 2, y * -z + graphHeight / 2).scale(z);
svg.transition().duration(d).call(zoom.transform, transform);
}
// Reset zoom to initial
function resetZoom(d = 1000) {
svg.transition().duration(d).call(zoom.transform, d3.zoomIdentity);
}
// calculate x y extreme points of viewBox
function getViewBoxExtent() {
return [
[Math.abs(viewX / scale), Math.abs(viewY / scale)],
[Math.abs(viewX / scale) + graphWidth / scale, Math.abs(viewY / scale) + graphHeight / scale]
];
}
// active zooming feature
function invokeActiveZooming() {
if (coastline.select("#sea_island").size() && +coastline.select("#sea_island").attr("auto-filter")) {
// toggle shade/blur filter for coatline on zoom
const filter = scale > 1.5 && scale <= 2.6 ? null : scale > 2.6 ? "url(#blurFilter)" : "url(#dropShadow)";
coastline.select("#sea_island").attr("filter", filter);
}
// rescale labels on zoom
if (labels.style("display") !== "none") {
labels.selectAll("g").each(function () {
if (this.id === "burgLabels") return;
const desired = +this.dataset.size;
const relative = Math.max(rn((desired + desired / scale) / 2, 2), 1);
if (rescaleLabels.checked) this.setAttribute("font-size", relative);
const hidden = hideLabels.checked && (relative * scale < 6 || relative * scale > 60);
if (hidden) this.classList.add("hidden");
else this.classList.remove("hidden");
});
}
// rescale emblems on zoom
if (emblems.style("display") !== "none") {
emblems.selectAll("g").each(function () {
const size = this.getAttribute("font-size") * scale;
const hidden = hideEmblems.checked && (size < 25 || size > 300);
if (hidden) this.classList.add("hidden");
else this.classList.remove("hidden");
if (!hidden && window.COArenderer && this.children.length && !this.children[0].getAttribute("href")) renderGroupCOAs(this);
});
}
// turn off ocean pattern if scale is big (improves performance)
oceanPattern
.select("rect")
.attr("fill", scale > 10 ? "#fff" : "url(#oceanic)")
.attr("opacity", scale > 10 ? 0.2 : null);
// change states halo width
if (!customization) {
const desired = +statesHalo.attr("data-width");
const haloSize = rn(desired / scale ** 0.8, 2);
statesHalo.attr("stroke-width", haloSize).style("display", haloSize > 0.1 ? "block" : "none");
}
// rescale map markers
+markers.attr("rescale") &&
pack.markers?.forEach(marker => {
const {i, x, y, size = 30, hidden} = marker;
const el = !hidden && document.getElementById(`marker${i}`);
if (!el) return;
const zoomedSize = Math.max(rn(size / 5 + 24 / scale, 2), 1);
el.setAttribute("width", zoomedSize);
el.setAttribute("height", zoomedSize);
el.setAttribute("x", rn(x - zoomedSize / 2, 1));
el.setAttribute("y", rn(y - zoomedSize, 1));
});
// rescale rulers to have always the same size
if (ruler.style("display") !== "none") {
const size = rn((10 / scale ** 0.3) * 2, 2);
ruler.selectAll("text").attr("font-size", size);
}
}
async function renderGroupCOAs(g) { async function renderGroupCOAs(g) {
const [group, type] = g.id === "burgEmblems" ? [pack.burgs, "burg"] : g.id === "provinceEmblems" ? [pack.provinces, "province"] : [pack.states, "state"]; const [group, type] =
g.id === "burgEmblems"
? [pack.burgs, "burg"]
: g.id === "provinceEmblems"
? [pack.provinces, "province"]
: [pack.states, "state"];
for (let use of g.children) { for (let use of g.children) {
const i = +use.dataset.i; const i = +use.dataset.i;
const id = type + "COA" + i; const id = type + "COA" + i;
@ -1547,7 +1255,9 @@ function addZones(number = 1) {
const invader = ra(atWar); const invader = ra(atWar);
const target = invader.diplomacy.findIndex(d => d === "Enemy"); const target = invader.diplomacy.findIndex(d => d === "Enemy");
const cell = ra(cells.i.filter(i => cells.state[i] === target && cells.c[i].some(c => cells.state[c] === invader.i))); const cell = ra(
cells.i.filter(i => cells.state[i] === target && cells.c[i].some(c => cells.state[c] === invader.i))
);
if (!cell) return; if (!cell) return;
const cellsArray = [], const cellsArray = [],
@ -1589,7 +1299,9 @@ function addZones(number = 1) {
const neib = ra(state.neighbors.filter(n => n && !states[n].removed)); const neib = ra(state.neighbors.filter(n => n && !states[n].removed));
if (!neib) return; if (!neib) return;
const cell = cells.i.find(i => cells.state[i] === state.i && !state.removed && cells.c[i].some(c => cells.state[c] === neib)); const cell = cells.i.find(
i => cells.state[i] === state.i && !state.removed && cells.c[i].some(c => cells.state[c] === neib)
);
const cellsArray = []; const cellsArray = [];
const queue = []; const queue = [];
if (cell) queue.push(cell); if (cell) queue.push(cell);
@ -1610,7 +1322,17 @@ function addZones(number = 1) {
}); });
} }
const rebels = rw({Rebels: 5, Insurgents: 2, Mutineers: 1, Rioters: 1, Separatists: 1, Secessionists: 1, Insurrection: 2, Rebellion: 1, Conspiracy: 2}); const rebels = rw({
Rebels: 5,
Insurgents: 2,
Mutineers: 1,
Rioters: 1,
Separatists: 1,
Secessionists: 1,
Insurrection: 2,
Rebellion: 1,
Conspiracy: 2
});
const name = getAdjective(states[neib].name) + " " + rebels; const name = getAdjective(states[neib].name) + " " + rebels;
zonesData.push({name, type: "Rebels", cells: cellsArray, fill: "url(#hatch3)"}); zonesData.push({name, type: "Rebels", cells: cellsArray, fill: "url(#hatch3)"});
} }
@ -1619,7 +1341,14 @@ function addZones(number = 1) {
const organized = ra(pack.religions.filter(r => r.type === "Organized")); const organized = ra(pack.religions.filter(r => r.type === "Organized"));
if (!organized) return; if (!organized) return;
const cell = ra(cells.i.filter(i => cells.religion[i] && cells.religion[i] !== organized.i && cells.c[i].some(c => cells.religion[c] === organized.i))); const cell = ra(
cells.i.filter(
i =>
cells.religion[i] &&
cells.religion[i] !== organized.i &&
cells.c[i].some(c => cells.religion[c] === organized.i)
)
);
if (!cell) return; if (!cell) return;
const target = cells.religion[cell]; const target = cells.religion[cell];
const cellsArray = [], const cellsArray = [],
@ -1685,11 +1414,54 @@ function addZones(number = 1) {
}); });
} }
const adjective = () => ra(["Great", "Silent", "Severe", "Blind", "Unknown", "Loud", "Deadly", "Burning", "Bloody", "Brutal", "Fatal"]); const adjective = () =>
const animal = () => ra(["Ape", "Bear", "Boar", "Cat", "Cow", "Dog", "Pig", "Fox", "Bird", "Horse", "Rat", "Raven", "Sheep", "Spider", "Wolf"]); ra(["Great", "Silent", "Severe", "Blind", "Unknown", "Loud", "Deadly", "Burning", "Bloody", "Brutal", "Fatal"]);
const color = () => ra(["Golden", "White", "Black", "Red", "Pink", "Purple", "Blue", "Green", "Yellow", "Amber", "Orange", "Brown", "Grey"]); const animal = () =>
ra([
"Ape",
"Bear",
"Boar",
"Cat",
"Cow",
"Dog",
"Pig",
"Fox",
"Bird",
"Horse",
"Rat",
"Raven",
"Sheep",
"Spider",
"Wolf"
]);
const color = () =>
ra([
"Golden",
"White",
"Black",
"Red",
"Pink",
"Purple",
"Blue",
"Green",
"Yellow",
"Amber",
"Orange",
"Brown",
"Grey"
]);
const type = rw({Fever: 5, Pestilence: 2, Flu: 2, Pox: 2, Smallpox: 2, Plague: 4, Cholera: 2, Dropsy: 1, Leprosy: 2}); const type = rw({
Fever: 5,
Pestilence: 2,
Flu: 2,
Pox: 2,
Smallpox: 2,
Plague: 4,
Cholera: 2,
Dropsy: 1,
Leprosy: 2
});
const name = rw({[color()]: 4, [animal()]: 2, [adjective()]: 1}) + " " + type; const name = rw({[color()]: 4, [animal()]: 2, [adjective()]: 1}) + " " + type;
zonesData.push({name, type: "Disease", cells: cellsArray, fill: "url(#hatch12)"}); zonesData.push({name, type: "Disease", cells: cellsArray, fill: "url(#hatch12)"});
} }
@ -1812,7 +1584,9 @@ function addZones(number = 1) {
meanFlux = d3.mean(fl), meanFlux = d3.mean(fl),
maxFlux = d3.max(fl), maxFlux = d3.max(fl),
flux = (maxFlux - meanFlux) / 2 + meanFlux; flux = (maxFlux - meanFlux) / 2 + meanFlux;
const rivers = cells.i.filter(i => !used[i] && cells.h[i] < 50 && cells.r[i] && cells.fl[i] > flux && cells.burg[i]); const rivers = cells.i.filter(
i => !used[i] && cells.h[i] < 50 && cells.r[i] && cells.fl[i] > flux && cells.burg[i]
);
if (!rivers.length) return; if (!rivers.length) return;
const cell = +ra(rivers), const cell = +ra(rivers),
@ -1923,7 +1697,7 @@ const regenerateMap = debounce(async function (options) {
closeDialogs("#worldConfigurator, #options3d"); closeDialogs("#worldConfigurator, #options3d");
customization = 0; customization = 0;
resetZoom(1000); Zoom.reset(1000);
undraw(); undraw();
await generate(options); await generate(options);
restoreLayers(); restoreLayers();

7
src/types/global.d.ts vendored Normal file
View file

@ -0,0 +1,7 @@
interface Navigator {
userAgentData?: {
mobile: boolean;
};
}
type UnknownObject = {[key: string]: unknown};

33
src/utils/arrayUtils.ts Normal file
View file

@ -0,0 +1,33 @@
import {UINT16_MAX, UINT32_MAX, UINT8_MAX} from "../constants";
export function last<T>(array: T[]) {
return array[array.length - 1];
}
export function unique<T>(array: T[]) {
return [...new Set(array)];
}
function getTypedArray(maxValue: number) {
console.assert(
Number.isInteger(maxValue) && maxValue >= 0 && maxValue <= UINT32_MAX,
`Array maxValue must be an integer between 0 and ${UINT32_MAX}, got ${maxValue}`
);
if (maxValue <= UINT8_MAX) return Uint8Array;
if (maxValue <= UINT16_MAX) return Uint16Array;
if (maxValue <= UINT32_MAX) return Uint32Array;
return Uint32Array;
}
interface ICreateTypedArray {
maxValue: number;
length: number;
from: ArrayLike<number>;
}
export function createTypedArray({maxValue, length, from}: ICreateTypedArray) {
const typedArray = getTypedArray(maxValue);
if (!from) return new typedArray(length);
return typedArray.from(from);
}

View file

@ -1,8 +1,8 @@
"use strict"; import {TIME} from "../config/logging";
// FMG utils related to graph import {createTypedArray} from ".";
// check if new grid graph should be generated or we can use the existing one // check if new grid graph should be generated or we can use the existing one
function shouldRegenerateGrid(grid) { export function shouldRegenerateGrid(grid) {
const cellsDesired = +byId("pointsInput").dataset.cells; const cellsDesired = +byId("pointsInput").dataset.cells;
if (cellsDesired !== grid.cellsDesired) return true; if (cellsDesired !== grid.cellsDesired) return true;
@ -13,7 +13,7 @@ function shouldRegenerateGrid(grid) {
return grid.spacing !== newSpacing || grid.cellsX !== newCellsX || grid.cellsY !== newCellsY; return grid.spacing !== newSpacing || grid.cellsX !== newCellsX || grid.cellsY !== newCellsY;
} }
function generateGrid() { export function generateGrid() {
Math.random = aleaPRNG(seed); // reset PRNG Math.random = aleaPRNG(seed); // reset PRNG
const {spacing, cellsDesired, boundary, points, cellsX, cellsY} = placePoints(); const {spacing, cellsDesired, boundary, points, cellsX, cellsY} = placePoints();
const {cells, vertices} = calculateVoronoi(points, boundary); const {cells, vertices} = calculateVoronoi(points, boundary);
@ -36,7 +36,7 @@ function placePoints() {
} }
// calculate Delaunay and then Voronoi diagram // calculate Delaunay and then Voronoi diagram
function calculateVoronoi(points, boundary) { export function calculateVoronoi(points, boundary) {
TIME && console.time("calculateDelaunay"); TIME && console.time("calculateDelaunay");
const allPoints = points.concat(boundary); const allPoints = points.concat(boundary);
const delaunay = Delaunator.from(allPoints); const delaunay = Delaunator.from(allPoints);
@ -95,8 +95,11 @@ function getJitteredGrid(width, height, spacing) {
} }
// return cell index on a regular square grid // return cell index on a regular square grid
function findGridCell(x, y, grid) { export function findGridCell(x, y, grid) {
return Math.floor(Math.min(y / grid.spacing, grid.cellsY - 1)) * grid.cellsX + Math.floor(Math.min(x / grid.spacing, grid.cellsX - 1)); return (
Math.floor(Math.min(y / grid.spacing, grid.cellsY - 1)) * grid.cellsX +
Math.floor(Math.min(x / grid.spacing, grid.cellsX - 1))
);
} }
// return array of cell indexes in radius on a regular square grid // return array of cell indexes in radius on a regular square grid
@ -130,23 +133,23 @@ function find(x, y, radius = Infinity) {
return pack.cells.q.find(x, y, radius); return pack.cells.q.find(x, y, radius);
} }
// return closest cell index
function findCell(x, y, radius = Infinity) {
const found = pack.cells.q.find(x, y, radius);
return found ? found[2] : undefined;
}
// return array of cell indexes in radius // return array of cell indexes in radius
function findAll(x, y, radius) { export function findAll(x, y, radius) {
const found = pack.cells.q.findAll(x, y, radius); const found = pack.cells.q.findAll(x, y, radius);
return found.map(r => r[2]); return found.map(r => r[2]);
} }
// get polygon points for packed cells knowing cell id // get polygon points for packed cells knowing cell id
function getPackPolygon(i) { export function getPackPolygon(i) {
return pack.cells.v[i].map(v => pack.vertices.p[v]); return pack.cells.v[i].map(v => pack.vertices.p[v]);
} }
// return closest cell index
export function findCell(x, y, radius = Infinity) {
const found = pack.cells.q.find(x, y, radius);
return found ? found[2] : undefined;
}
// get polygon points for initial cells knowing cell id // get polygon points for initial cells knowing cell id
function getGridPolygon(i) { function getGridPolygon(i) {
return grid.cells.v[i].map(v => grid.vertices.p[v]); return grid.cells.v[i].map(v => grid.vertices.p[v]);
@ -215,12 +218,12 @@ function* poissonDiscSampler(x0, y0, x1, y1, r, k = 3) {
} }
// filter land cells // filter land cells
function isLand(i) { export function isLand(i) {
return pack.cells.h[i] >= 20; return pack.cells.h[i] >= 20;
} }
// filter water cells // filter water cells
function isWater(i) { export function isWater(i) {
return pack.cells.h[i] < 20; return pack.cells.h[i] < 20;
} }
@ -246,7 +249,14 @@ void (function addFindAll() {
i++; i++;
// Stop searching if this quadrant cant contain a closer node. // Stop searching if this quadrant cant contain a closer node.
if (!(t.node = t.q.node) || (t.x1 = t.q.x0) > t.x3 || (t.y1 = t.q.y0) > t.y3 || (t.x2 = t.q.x1) < t.x0 || (t.y2 = t.q.y1) < t.y0) continue; if (
!(t.node = t.q.node) ||
(t.x1 = t.q.x0) > t.x3 ||
(t.y1 = t.q.y0) > t.y3 ||
(t.x2 = t.q.x1) < t.x0 ||
(t.y2 = t.q.y1) < t.y0
)
continue;
// Bisect the current quadrant. // Bisect the current quadrant.
if (t.node.length) { if (t.node.length) {

1
src/utils/index.ts Normal file
View file

@ -0,0 +1 @@
export {last, unique, createTypedArray} from "./arrayUtils";

View file

@ -16,5 +16,5 @@
"noImplicitReturns": true, "noImplicitReturns": true,
"skipLibCheck": true "skipLibCheck": true
}, },
"include": ["src"] "include": ["src", "utils", "modules"]
} }

View file

@ -7,54 +7,3 @@ function last(array) {
function unique(array) { function unique(array) {
return [...new Set(array)]; return [...new Set(array)];
} }
// deep copy for Arrays (and other objects)
function deepCopy(obj) {
const id = x => x;
const dcTArray = a => a.map(id);
const dcObject = x => Object.fromEntries(Object.entries(x).map(([k, d]) => [k, dcAny(d)]));
const dcAny = x => (x instanceof Object ? (cf.get(x.constructor) || id)(x) : x);
// don't map keys, probably this is what we would expect
const dcMapCore = m => [...m.entries()].map(([k, v]) => [k, dcAny(v)]);
const cf = new Map([
[Int8Array, dcTArray],
[Uint8Array, dcTArray],
[Uint8ClampedArray, dcTArray],
[Int16Array, dcTArray],
[Uint16Array, dcTArray],
[Int32Array, dcTArray],
[Uint32Array, dcTArray],
[Float32Array, dcTArray],
[Float64Array, dcTArray],
[BigInt64Array, dcTArray],
[BigUint64Array, dcTArray],
[Map, m => new Map(dcMapCore(m))],
[WeakMap, m => new WeakMap(dcMapCore(m))],
[Array, a => a.map(dcAny)],
[Set, s => [...s.values()].map(dcAny)],
[Date, d => new Date(d.getTime())],
[Object, dcObject]
// ... extend here to implement their custom deep copy
]);
return dcAny(obj);
}
function getTypedArray(maxValue) {
console.assert(
Number.isInteger(maxValue) && maxValue >= 0 && maxValue <= UINT32_MAX,
`Array maxValue must be an integer between 0 and ${UINT32_MAX}, got ${maxValue}`
);
if (maxValue <= UINT8_MAX) return Uint8Array;
if (maxValue <= UINT16_MAX) return Uint16Array;
if (maxValue <= UINT32_MAX) return Uint32Array;
return Uint32Array;
}
function createTypedArray({maxValue, length, from}) {
const typedArray = getTypedArray(maxValue);
if (!from) return new typedArray(length);
return typedArray.from(from);
}

3
vite.config.js Normal file
View file

@ -0,0 +1,3 @@
import {defineConfig} from "vite";
export default defineConfig({});