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/arrayUtils.js"></script>
<script src="utils/colorUtils.js"></script>
<script src="utils/graphUtils.js"></script>
<script src="utils/nodeUtils.js"></script>
<script src="utils/numberUtils.js"></script>
<script src="utils/polyfills.js"></script>
@ -7783,33 +7782,39 @@
<script src="modules/voronoi.js"></script>
<script src="config/heightmap-templates.js"></script>
<script src="config/precreated-heightmaps.js"></script>
<script src="modules/heightmap-generator.js"></script>
<script src="modules/ocean-layers.js"></script>
<script src="modules/river-generator.js"></script>
<script src="modules/lakes.js"></script>
<script src="modules/names-generator.js"></script>
<script src="modules/cultures-generator.js"></script>
<script src="modules/burgs-and-states.js?v=1.87.04"></script>
<script src="modules/routes-generator.js"></script>
<script src="modules/religions-generator.js"></script>
<script src="modules/military-generator.js"></script>
<script src="modules/markers-generator.js"></script>
<script src="modules/coa-generator.js"></script>
<script type="module" src="modules/heightmap-generator.js"></script>
<script type="module" src="modules/ocean-layers.js"></script>
<script type="module" src="modules/river-generator.js"></script>
<script type="module" src="modules/lakes.js"></script>
<script type="module" src="modules/names-generator.js"></script>
<script type="module" src="modules/biomes.js"></script>
<script type="module" src="modules/cultures-generator.js"></script>
<script type="module" src="modules/burgs-and-states.js?v=1.87.04"></script>
<script type="module" src="modules/routes-generator.js"></script>
<script type="module" src="modules/religions-generator.js"></script>
<script type="module" src="modules/military-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="libs/polylabel.min.js"></script>
<script src="libs/lineclip.min.js"></script>
<script src="libs/alea.min.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/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/ui/options.js?v=1.87.00"></script>
<script src="main.js"></script>
<script src="modules/define-globals.js"></script>
<script src="modules/define-svg.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/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/tools.js?v=1.87.03"></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/burg-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/zones-editor.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 () {
const generate = function () {

View file

@ -1,5 +1,3 @@
"use strict";
window.COA = (function () {
const tinctures = {
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},
River: {tower: 1, garb: 1, rake: 1, boat: 1, pike: 2, bullHeadCaboshed: 1, apple: 1, plough: 1},
Lake: {cancer: 2, escallop: 1, pike: 2, heron: 1, boat: 1, boat2: 2},
Nomadic: {pot: 1, buckle: 1, wheel: 2, sabre: 2, sabresCrossed: 1, bow: 2, arrow: 1, horseRampant: 1, horseSalient: 1, crescent: 1, camel: 3},
Nomadic: {
pot: 1,
buckle: 1,
wheel: 2,
sabre: 2,
sabresCrossed: 1,
bow: 2,
arrow: 1,
horseRampant: 1,
horseSalient: 1,
crescent: 1,
camel: 3
},
Hunting: {
bugleHorn: 2,
bugleHorn2: 1,
@ -322,7 +332,19 @@ window.COA = (function () {
// selection based on type
City: {key: 3, bell: 2, lute: 1, tower: 1, castle: 1, mallet: 1, cannon: 1, anvil: 1},
Capital: {crown: 2, orb: 1, lute: 1, castle: 3, tower: 1, crown2: 2},
Сathedra: {chalice: 1, orb: 1, crosier: 2, lamb: 1, monk: 2, angel: 3, crossLatin: 2, crossPatriarchal: 1, crossOrthodox: 1, crossCalvary: 1, agnusDei: 3},
Сathedra: {
chalice: 1,
orb: 1,
crosier: 2,
lamb: 1,
monk: 2,
angel: 3,
crossLatin: 2,
crossPatriarchal: 1,
crossOrthodox: 1,
crossCalvary: 1,
agnusDei: 3
},
// specific cases
natural: {fountain: "azure", garb: "or", raven: "sable"}, // charges to mainly use predefined colours
sinister: [
@ -508,7 +530,22 @@ window.COA = (function () {
},
// charges
inescutcheon: {e: 4, jln: 1},
mascle: {e: 15, abcdefgzi: 3, beh: 3, bdefh: 4, acegi: 1, kn: 3, joe: 2, abc: 3, jlh: 8, jleh: 1, df: 3, abcpqh: 4, pqe: 3, eknpq: 3},
mascle: {
e: 15,
abcdefgzi: 3,
beh: 3,
bdefh: 4,
acegi: 1,
kn: 3,
joe: 2,
abc: 3,
jlh: 8,
jleh: 1,
df: 3,
abcpqh: 4,
pqe: 3,
eknpq: 3
},
lionRampant: {e: 10, def: 2, abc: 2, bdefh: 1, kn: 1, jlh: 2, abcpqh: 1},
lionPassant: {e: 10, def: 1, abc: 1, bdefh: 1, jlh: 1, abcpqh: 1},
wolfPassant: {e: 10, def: 1, abc: 1, bdefh: 1, jlh: 1, abcpqh: 1},
@ -681,18 +718,41 @@ window.COA = (function () {
const coa = {t1};
let charge = P(usedPattern ? 0.5 : 0.93) ? true : false; // 80% for charge
const linedOrdinary = (charge && P(0.3)) || P(0.5) ? (parent?.ordinaries && P(kinship) ? parent.ordinaries[0].ordinary : rw(ordinaries.lined)) : null;
const linedOrdinary =
(charge && P(0.3)) || P(0.5)
? parent?.ordinaries && P(kinship)
? parent.ordinaries[0].ordinary
: rw(ordinaries.lined)
: null;
const ordinary = (!charge && P(0.65)) || P(0.3) ? (linedOrdinary ? linedOrdinary : rw(ordinaries.straight)) : null; // 36% for ordinary
const rareDivided = ["chief", "terrace", "chevron", "quarter", "flaunches"].includes(ordinary);
const divisioned = rareDivided ? P(0.03) : charge && ordinary ? P(0.03) : charge ? P(0.3) : ordinary ? P(0.7) : P(0.995); // 33% for division
const division = divisioned ? (parent?.division && P(kinship - 0.1) ? parent.division.division : rw(divisions.variants)) : null;
const divisioned = rareDivided
? P(0.03)
: charge && ordinary
? P(0.03)
: charge
? P(0.3)
: ordinary
? P(0.7)
: P(0.995); // 33% for division
const division = divisioned
? parent?.division && P(kinship - 0.1)
? parent.division.division
: rw(divisions.variants)
: null;
if (charge)
charge = parent?.charges && P(kinship - 0.1) ? parent.charges[0].charge : type && type !== "Generic" && P(0.2) ? rw(charges[type]) : selectCharge();
charge =
parent?.charges && P(kinship - 0.1)
? parent.charges[0].charge
: type && type !== "Generic" && P(0.2)
? rw(charges[type])
: selectCharge();
if (division) {
const t = getTincture("division", usedTinctures, P(0.98) ? coa.t1 : null);
coa.division = {division, t};
if (divisions[division]) coa.division.line = usedPattern || (ordinary && P(0.7)) ? "straight" : rw(divisions[division]);
if (divisions[division])
coa.division.line = usedPattern || (ordinary && P(0.7)) ? "straight" : rw(divisions[division]);
}
if (ordinary) {
@ -768,7 +828,14 @@ window.COA = (function () {
// counterchanged, 40%
else if (["perPale", "perFess", "perBend", "perBendSinister"].includes(division) && P(0.8)) {
// place 2 charges in division standard positions
const [p1, p2] = division === "perPale" ? ["p", "q"] : division === "perFess" ? ["k", "n"] : division === "perBend" ? ["l", "m"] : ["j", "o"]; // perBendSinister
const [p1, p2] =
division === "perPale"
? ["p", "q"]
: division === "perFess"
? ["k", "n"]
: division === "perBend"
? ["l", "m"]
: ["j", "o"]; // perBendSinister
coa.charges[0].p = p1;
const charge = selectCharge(charges.single);

View file

@ -1,4 +1,4 @@
"use strict";
import {TIME} from "/src/config/logging";
window.Cultures = (function () {
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"}
});
if (modules.editStateName) return;
modules.editStateName = true;
if (fmg.modules.editStateName) return;
fmg.modules.editStateName = true;
// add listeners
byId("stateNameEditorShortCulture").on("click", regenerateShortNameCuture);

View file

@ -262,7 +262,7 @@ function getName(id) {
}
function getGraph(currentGraph) {
const newGraph = shouldRegenerateGrid(currentGraph) ? generateGrid() : deepCopy(currentGraph);
const newGraph = shouldRegenerateGrid(currentGraph) ? generateGrid() : structuredClone(currentGraph);
delete newGraph.cells.h;
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 () {
let grid = null;
@ -388,8 +390,12 @@ window.HeightmapGenerator = (function () {
const vert = direction === "vertical";
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 endX = vert ? 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 endX = vert
? 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 end = findGridCell(endX, endY, grid);

View file

@ -1,5 +1,3 @@
"use strict";
window.Lakes = (function () {
const setClimateData = function (h) {
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);
// 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 evaporation = ((700 * (f.temp + 0.006 * height)) / 50 + 75) / (80 - f.temp); // based on Penman formula, [1-11]
f.evaporation = rn(evaporation * f.cells);

View file

@ -1,4 +1,4 @@
"use strict";
import {TIME} from "/src/config/logging";
window.Markers = (function () {
let config = [];
@ -20,6 +20,7 @@ window.Markers = (function () {
list: function to select candidates
add: function to add marker legend
*/
// prettier-ignore
return [
{type: "volcanoes", icon: "🌋", dx: 52, px: 13, min: 10, each: 500, multiplier: 1, list: listVolcanoes, add: addVolcano},
{type: "hot-springs", icon: "♨️", dy: 52, min: 30, each: 1200, multiplier: 1, list: listHotSprings, add: addHotSpring},
@ -199,7 +200,13 @@ window.Markers = (function () {
function listBridges({cells, burgs}) {
const meanFlux = d3.mean(cells.fl.filter(fl => fl));
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",
"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 = [
"wine",
"brandy",
@ -469,7 +490,11 @@ window.Markers = (function () {
const typeName = P(0.3) ? "inn" : "tavern";
const isAnimalThemed = P(0.7);
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 course = `${ra(methods)} ${meal}`.toLowerCase();
const drink = `${P(0.5) ? ra(types) : ra(colors)} ${ra(drinks)}`.toLowerCase();
@ -478,18 +503,26 @@ window.Markers = (function () {
}
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) {
const {cells} = pack;
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}) {
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) {
@ -509,7 +542,9 @@ window.Markers = (function () {
}
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) {
@ -555,7 +590,9 @@ window.Markers = (function () {
}
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) {
@ -589,7 +626,17 @@ window.Markers = (function () {
"horrifying",
"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 = [
"Ogre",
"Troll",
@ -625,13 +672,21 @@ window.Markers = (function () {
const monster = ra(species);
const toponym = Names.getCulture(cells.culture[cell]);
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});
}
// Sacred mountains spawn on lonely mountains
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) {
@ -674,7 +729,9 @@ window.Markers = (function () {
// Sacred palm groves spawn on oasises
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) {
@ -765,7 +822,20 @@ window.Markers = (function () {
function addStatue(id, cell) {
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 = {
cypriot: "𐠁𐠂𐠃𐠄𐠅𐠈𐠊𐠋𐠌𐠍𐠎𐠏𐠐𐠑𐠒𐠓𐠔𐠕𐠖𐠗𐠘𐠙𐠚𐠛𐠜𐠝𐠞𐠟𐠠𐠡𐠢𐠣𐠤𐠥𐠦𐠧𐠨𐠩𐠪𐠫𐠬𐠭𐠮𐠯𐠰𐠱𐠲𐠳𐠴𐠵𐠷𐠸𐠼𐠿 ",
geez: "ሀለሐመሠረሰቀበተኀነአከወዐዘየደገጠጰጸፀፈፐ ",
@ -820,7 +890,16 @@ window.Markers = (function () {
}
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 name = `Travelling ${adjective} Circus`;
@ -932,8 +1011,26 @@ window.Markers = (function () {
function addDances(id, cell) {
const {cells, burgs} = pack;
const burgName = burgs[cells.burg[cell]].name;
const socialTypes = ["gala", "dance", "performance", "ball", "soiree", "jamboree", "exhibition", "carnival", "festival", "jubilee"];
const people = ["great and the good", "nobility", "local elders", "foreign dignitaries", "spiritual leaders", "suspected revolutionaries"];
const socialTypes = [
"gala",
"dance",
"performance",
"ball",
"soiree",
"jamboree",
"exhibition",
"carnival",
"festival",
"jubilee"
];
const people = [
"great and the good",
"nobility",
"local elders",
"foreign dignitaries",
"spiritual leaders",
"suspected revolutionaries"
];
const socialType = ra(socialTypes);
const name = `${burgName} ${socialType}`;

View file

@ -1,4 +1,4 @@
"use strict";
import {TIME} from "/src/config/logging";
window.Military = (function () {
const generate = function () {
@ -10,7 +10,18 @@ window.Military = (function () {
const expn = d3.sum(valid.map(s => s.expansionism)); // total expansion
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 = {
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 = {
nomadic: {melee: 0.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}
nomadic: {
melee: 0.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 = {
nomadic: {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},
nomadic: {
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}
};
@ -40,8 +96,16 @@ window.Military = (function () {
const d = s.diplomacy;
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 neighborsRateRaw = s.neighbors.map(n => (n ? pack.states[n].diplomacy[s.i] : "Suspicion")).reduce((s, r) => (s += rate[r]), 0.5);
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 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
s.alert = minmax(rn(expansionRate * diplomacyRate * neighborsRate, 2), 0.1, 5); // alert rate (area modifier)
s.temp.platoons = [];
@ -86,8 +150,10 @@ window.Military = (function () {
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 (religion !== cells.religion[stateObj.center]) 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
if (religion !== cells.religion[stateObj.center])
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);
for (const unit of options.military) {
@ -111,7 +177,17 @@ window.Military = (function () {
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;
}
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
const getEmblem = function (r) {
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 unit = options.military.find(u => u.name === mainUnit);
return unit.icon;
@ -400,7 +492,9 @@ window.Military = (function () {
.map(t => `${t}: ${r.u[t]}`)
.join("\r\n")
: 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 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});
};
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 () {
let chains = [];
@ -142,7 +140,11 @@ window.Names = (function () {
// generate short name for base
const getBaseShort = function (base) {
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;
}
const min = nameBases[base].min - 1;
@ -165,7 +167,8 @@ window.Names = (function () {
// remove -sk/-ev/-ov for Ruthenian
else if (base === 12) return vowel(name.slice(-1)) ? name : name + "u";
// Japanese ends on any vowel or -u
else if (base === 18 && 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
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 () {
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 () {
// 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 () {
const generate = function (allowErosion = true) {
@ -48,7 +48,9 @@ window.Rivers = (function () {
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
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) {
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
@ -191,7 +193,18 @@ window.Rivers = (function () {
const length = getApproximateLength(meanderedPoints);
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 () {
const getRoads = function () {
TIME && console.time("generateMainRoads");
@ -39,7 +42,10 @@ window.Routes = (function () {
if (!i) {
// build trail from the first burg on island
// 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;
if (cells.road[to]) return;
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 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 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));
roads.html(getPathsHTML(main, "road"));

View file

@ -32,17 +32,27 @@ class Battle {
close: () => Battle.prototype.context.cancelResults()
});
if (modules.Battle) return;
modules.Battle = true;
if (fmg.modules.Battle) return;
fmg.modules.Battle = true;
// add listeners
document.getElementById("battleType").addEventListener("click", ev => this.toggleChange(ev));
document.getElementById("battleType").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("battleType")
.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("battleNameCulture").addEventListener("click", () => Battle.prototype.context.generateName("culture"));
document.getElementById("battleNameRandom").addEventListener("click", () => Battle.prototype.context.generateName("random"));
document
.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("battleAddRegiment").addEventListener("click", this.addSide);
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("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").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"));
document
.getElementById("battlePhase_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() {
@ -82,8 +100,12 @@ class Battle {
document.getElementById("battleType").className = "icon-button-" + this.type;
const sideSpecific = document.getElementById("battlePhases_" + this.type + "_attackers");
const attackers = sideSpecific ? sideSpecific.content : document.getElementById("battlePhases_" + this.type).content;
const defenders = sideSpecific ? document.getElementById("battlePhases_" + this.type + "_defenders").content : attackers;
const attackers = sideSpecific
? 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_defenders").nextElementSibling.innerHTML = "";
@ -146,19 +168,30 @@ class Battle {
<text x="0" y="1.04em" style="">${regiment.icon}</text></svg>`;
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 casualties = `<tr class="battleCasualties"><td></td><td data-tip="${state.fullName}">${state.fullName.slice(0, 26)}</td>`;
let initial = `<tr class="battleInitial"><td>${icon}</td><td class="regiment" data-tip="${
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>`;
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>`;
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>`;
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;
div.innerHTML += body + initial + casualties + survivors + "</tbody>";
@ -173,17 +206,23 @@ class Battle {
.filter(s => s.military && !s.removed)
.map(s => s.military)
.flat();
const distance = 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);
const distance = 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
.map(r => {
const s = pack.states[r.state],
added = isAdded(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">
<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:1.2em">${r.icon}</div>
<div style="width:13em">${r.name.slice(0, 24)}</div>
@ -267,7 +306,10 @@ class Battle {
}
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("battleNameFull").value = this.name = this.defineName();
$("#battleScreen").dialog({title: this.name});
@ -286,35 +328,161 @@ class Battle {
calculateStrength(side) {
const scheme = {
// 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
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
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
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
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
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
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
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
bombardment: {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
bombardment: {
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
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
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
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
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
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
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
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
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
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
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 phase = this[side].phase;
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;
document.getElementById("battlePower_" + side).innerHTML = UIvalue;
}
@ -723,11 +892,13 @@ class Battle {
const status = battleStatus[+P(0.7)];
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(
this.defenders.regiments,
0
)}. ${result}.
\r\nAttackers losses: ${getLosses(this.attackers.casualties)}%, defenders losses: ${getLosses(this.defenders.casualties)}%`;
const legend = `${this.name} took place in ${options.year} ${options.eraShort}. It was fought between ${getSide(
this.attackers.regiments,
1
)} and ${getSide(this.defenders.regiments, 0)}. ${result}.
\r\nAttackers losses: ${getLosses(this.attackers.casualties)}%, defenders losses: ${getLosses(
this.defenders.casualties
)}%`;
notes.push({id: `marker${i}`, name: this.name, legend});
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);
refreshBiomesEditor();
if (modules.editBiomes) return;
modules.editBiomes = true;
if (fmg.modules.editBiomes) return;
fmg.modules.editBiomes = true;
$("#biomesEditor").dialog({
title: "Biomes Editor",
@ -88,7 +88,9 @@ function editBiomes() {
const rural = b.rural[i] * populationRate;
const urban = b.urban[i] * populationRate * urbanization;
const population = rn(rural + urban);
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}`;
const populationTip = `Total population: ${si(population)}; Rural population: ${si(
rural
)}; Urban population: ${si(urban)}`;
totalArea += area;
totalPopulation += population;
@ -104,7 +106,9 @@ function editBiomes() {
data-color=${b.color[i]}
>
<fill-box fill="${b.color[i]}"></fill-box>
<input data-tip="Biome name. Click and type to change" class="biomeName" value="${b.name[i]}" autocorrect="off" spellcheck="false" />
<input data-tip="Biome name. Click and type to change" class="biomeName" value="${
b.name[i]
}" autocorrect="off" spellcheck="false" />
<span data-tip="Biome habitability percent" class="hide">%</span>
<input
data-tip="Biome habitability percent. Click and set new value to change"
@ -121,7 +125,11 @@ function editBiomes() {
<span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="biomePopulation hide">${si(population)}</div>
<span data-tip="Open Wikipedia article about the biome" class="icon-info-circled pointer hide"></span>
${i > 12 && !b.cells[i] ? '<span data-tip="Remove the custom biome" class="icon-trash-empty hide"></span>' : ""}
${
i > 12 && !b.cells[i]
? '<span data-tip="Remove the custom biome" class="icon-trash-empty hide"></span>'
: ""
}
</div>
`;
}
@ -403,7 +411,14 @@ function editBiomes() {
// change of append new element
if (exists.size()) exists.attr("data-biome", biomeNew).attr("fill", color).attr("stroke", color);
else temp.append("polygon").attr("data-cell", i).attr("data-biome", biomeNew).attr("points", getPackPolygon(i)).attr("fill", color).attr("stroke", color);
else
temp
.append("polygon")
.attr("data-cell", i)
.attr("data-biome", biomeNew)
.attr("points", getPackPolygon(i))
.attr("fill", color)
.attr("stroke", color);
});
}

View file

@ -17,8 +17,8 @@ function editBurg(id) {
position: {my: "left top", at: "left+10 top+10", of: "svg", collision: "fit"}
});
if (modules.editBurg) return;
modules.editBurg = true;
if (fmg.modules.editBurg) return;
fmg.modules.editBurg = true;
// add listeners
document.getElementById("burgGroupShow").addEventListener("click", showGroupSection);
@ -284,7 +284,9 @@ function editBurg(id) {
alertMessage.innerHTML = /* html */ `Are you sure you want to remove ${
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({
resizable: false,
title: "Remove burg group",
@ -433,7 +435,8 @@ function editBurg(id) {
function addCustomMfcgLink() {
const id = +elSelected.attr("data-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 => {
if (link) burg.link = link;
else delete burg.link;

View file

@ -11,8 +11,8 @@ function overviewBurgs() {
burgsOverviewAddLines();
$("#burgsOverview").dialog();
if (modules.overviewBurgs) return;
modules.overviewBurgs = true;
if (fmg.modules.overviewBurgs) return;
fmg.modules.overviewBurgs = true;
$("#burgsOverview").dialog({
title: "Burgs Overview",
@ -93,7 +93,9 @@ function overviewBurgs() {
data-type="${type}"
>
<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 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">
@ -106,10 +108,14 @@ function overviewBurgs() {
data-tip="${b.capital ? " This burg is a state capital" : "Click to assign a capital status"}"
class="icon-star-empty${b.capital ? "" : " inactive pointer"}"
></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>
<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>
</div>`;
}
@ -125,8 +131,12 @@ function overviewBurgs() {
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 > select.stateCulture").forEach(el => el.addEventListener("change", changeBurgCulture));
body.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 > 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.locks").forEach(el => el.addEventListener("click", toggleBurgLockStatus));
body.querySelectorAll("div > span.icon-pencil").forEach(el => el.addEventListener("click", openBurgEditor));
@ -137,7 +147,9 @@ function overviewBurgs() {
function getCultureOptions(culture) {
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;
}
@ -228,7 +240,8 @@ function overviewBurgs() {
function triggerBurgRemove() {
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({
title: "Remove burg",
@ -266,8 +279,10 @@ function overviewBurgs() {
function addBurgOnClick() {
const point = d3.mouse(this);
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.burg[cell]) return tip("There is already a burg in this cell. Please select a free cell", false, "error");
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.burg[cell])
return tip("There is already a burg in this cell. Please select a free cell", false, "error");
addBurg(point); // add new burg
@ -301,7 +316,19 @@ function overviewBurgs() {
const capital = b.capital;
const province = pack.cells.province[b.cell];
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);
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();
viewbox.on("touchmove mousemove", null);
if (modules.editCoastline) return;
modules.editCoastline = true;
if (fmg.modules.editCoastline) return;
fmg.modules.editCoastline = true;
// add listeners
document.getElementById("coastlineGroupsShow").addEventListener("click", showGroupSection);
@ -55,7 +55,9 @@ function editCoastline(node = d3.event.target) {
.attr("r", 0.4)
.attr("data-v", d => d)
.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;
coastlineArea.innerHTML = si(getArea(area)) + " " + getAreaUnit();

View file

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

View file

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

View file

@ -194,7 +194,14 @@ function showElevationProfile(data, routeLen, isRiver) {
.attr("fill", "darkgray");
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) {
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(0) + +xOffset) + "," + parseInt(yscale(0) + +yOffset);
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
let g = chart.append("g").attr("id", "epbiomes");
@ -289,7 +303,14 @@ function showElevationProfile(data, routeLen, isRiver) {
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
@ -371,7 +392,17 @@ function showElevationProfile(data, routeLen, isRiver) {
// arrow from burg name to graph line
g.append("path")
.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("fill", "lightgray")
.attr("stroke-width", "1")
@ -385,6 +416,6 @@ function showElevationProfile(data, routeLen, isRiver) {
document.getElementById("epCurve").removeEventListener("change", draw);
document.getElementById("epSave").removeEventListener("click", downloadCSV);
document.getElementById("elevationGraph").innerHTML = "";
modules.elevation = false;
fmg.modules.elevation = false;
}
}

View file

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

View file

@ -8,8 +8,8 @@ function editHeightmap(options) {
if (!mode) showModeDialog();
else enterHeightmapEditMode(mode);
if (modules.editHeightmap) return;
modules.editHeightmap = true;
if (fmg.modules.editHeightmap) return;
fmg.modules.editHeightmap = true;
// add listeners
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>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 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({
resizable: false,
@ -148,7 +151,11 @@ function editHeightmap(options) {
// Exit customization mode
function finalizeHeightmap() {
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");
delete window.edits; // remove global variable
@ -210,7 +217,8 @@ function editHeightmap(options) {
if (!erosionAllowed) {
for (const i of pack.cells.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;
// 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
if (!erosionAllowed) {
@ -373,7 +382,9 @@ function editHeightmap(options) {
const findBurgCell = function (x, y) {
let i = findCell(x, y);
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)];
};
@ -551,8 +562,8 @@ function editHeightmap(options) {
})
.on("dialogclose", exitBrushMode);
if (modules.openBrushesPanel) return;
modules.openBrushesPanel = true;
if (fmg.modules.openBrushesPanel) return;
fmg.modules.openBrushesPanel = true;
// add listeners
byId("brushesButtons").on("click", e => toggleBrushMode(e));
@ -630,15 +641,25 @@ function editHeightmap(options) {
const brush = document.querySelector("#brushesButtons > button.pressed").id;
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 === "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 === "brushSmooth")
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);
// updateHistory(); uncomment to update history every step
@ -662,7 +683,8 @@ function editHeightmap(options) {
const operator = conditionSign.value;
const operand = rescaleModifier.valueAsNumber;
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);
@ -691,7 +713,8 @@ function editHeightmap(options) {
function startFromScratch() {
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);
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);
viewbox.select("#heights").selectAll("*").remove();
@ -711,10 +734,15 @@ function editHeightmap(options) {
position: {my: "right top", at: "right-10 top+10", of: "svg"}
});
if (modules.openTemplateEditor) return;
modules.openTemplateEditor = true;
if (fmg.modules.openTemplateEditor) return;
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
$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 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>`;
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>`;
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>`;
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>`;
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")
return /* html */ `${common}
@ -814,7 +851,9 @@ function editHeightmap(options) {
</select>
</span>
<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>
</div>`;
@ -1042,8 +1081,8 @@ function editHeightmap(options) {
viewbox.select("#heights").selectAll("*").remove();
updateHistory();
if (modules.openImageConverter) return;
modules.openImageConverter = true;
if (fmg.modules.openImageConverter) return;
fmg.modules.openImageConverter = true;
// add color pallete
void (function createColorPallete() {
@ -1245,7 +1284,8 @@ function editHeightmap(options) {
const assinged = []; // store assigned heights
unassigned.forEach(el => {
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));
viewbox
.select("#heights")

View file

@ -13,7 +13,6 @@ function handleKeydown(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
event.stopPropagation();

View file

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

View file

@ -22,8 +22,8 @@ function editLabel() {
selectLabelGroup(text);
updateValues(textPath);
if (modules.editLabel) return;
modules.editLabel = true;
if (fmg.modules.editLabel) return;
fmg.modules.editLabel = true;
// add listeners
document.getElementById("labelGroupShow").addEventListener("click", showGroupSection);
@ -78,7 +78,9 @@ function editLabel() {
}
function updateValues(textPath) {
document.getElementById("labelText").value = [...textPath.querySelectorAll("tspan")].map(tspan => tspan.textContent).join("|");
document.getElementById("labelText").value = [...textPath.querySelectorAll("tspan")]
.map(tspan => tspan.textContent)
.join("|");
document.getElementById("labelStartOffset").value = parseFloat(textPath.getAttribute("startOffset"));
document.getElementById("labelRelativeSize").value = parseFloat(textPath.getAttribute("font-size"));
}
@ -298,7 +300,13 @@ function editLabel() {
function changeText() {
const input = document.getElementById("labelText").value;
const el = elSelected.select("textPath").node();
const example = d3.select(elSelected.node().parentNode).append("text").attr("x", 0).attr("x", 0).attr("font-size", el.getAttribute("font-size")).node();
const example = d3
.select(elSelected.node().parentNode)
.append("text")
.attr("x", 0)
.attr("x", 0)
.attr("font-size", el.getAttribute("font-size"))
.node();
const lines = input.split("|");
const top = (lines.length - 1) / -2; // y offset
@ -313,7 +321,8 @@ function editLabel() {
el.innerHTML = inner;
example.remove();
if (elSelected.attr("id").slice(0, 10) === "stateLabel") tip("Use States Editor to change an actual state name, not just a label", false, "warning");
if (elSelected.attr("id").slice(0, 10) === "stateLabel")
tip("Use States Editor to change an actual state name, not just a label", false, "warning");
}
function generateRandomName() {

View file

@ -19,8 +19,8 @@ function editLake() {
drawLakeVertices();
viewbox.on("touchmove mousemove", null);
if (modules.editLake) return;
modules.editLake = true;
if (fmg.modules.editLake) return;
fmg.modules.editLake = true;
// add listeners
document.getElementById("lakeName").addEventListener("input", changeName);
@ -48,7 +48,8 @@ function editLake() {
document.getElementById("lakeArea").value = si(getArea(l.area)) + " " + getAreaUnit();
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 heights = lakeCells.map(i => cells.h[i]);
@ -91,7 +92,9 @@ function editLake() {
.attr("r", 0.4)
.attr("data-v", d => d)
.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() {

View file

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

View file

@ -10,8 +10,8 @@ function overviewMilitary() {
addLines();
$("#militaryOverview").dialog();
if (modules.overviewMilitary) return;
modules.overviewMilitary = true;
if (fmg.modules.overviewMilitary) return;
fmg.modules.overviewMilitary = true;
updateHeaders();
$("#militaryOverview").dialog({
@ -54,7 +54,9 @@ function overviewMilitary() {
const insert = html => document.getElementById("militaryTotal").insertAdjacentHTML("beforebegin", html);
for (const u of options.military) {
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) {
e.addEventListener("click", function () {
@ -76,7 +78,9 @@ function overviewMilitary() {
const rate = (total / population) * 100;
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
class="states"
@ -91,9 +95,14 @@ function overviewMilitary() {
<fill-box data-tip="${s.fullName}" fill="${s.color}" disabled></fill-box>
<input data-tip="${s.fullName}" style="width:6em" value="${s.name}" readonly />
${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="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
data-tip="War Alert. Editable modifier to military forces number, depends of political situation"
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);
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 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"},
buttons: {
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,
Cancel: function () {
$(this).dialog("close");
@ -254,8 +274,8 @@ function overviewMilitary() {
}
});
if (modules.overviewMilitaryCustomize) return;
modules.overviewMilitaryCustomize = true;
if (fmg.modules.overviewMilitaryCustomize) return;
fmg.modules.overviewMilitaryCustomize = true;
tableBody.addEventListener("click", event => {
const el = event.target;
@ -294,7 +314,9 @@ function overviewMilitary() {
function addUnitLine(unit) {
const {type, icon, name, rural, urban, power, crew, separate} = unit;
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 =>
`<button
@ -305,7 +327,9 @@ function overviewMilitary() {
${getLimitText(unit[attr])}
</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>${getLimitButton("biomes")}</td>
<td>${getLimitButton("states")}</td>
@ -344,7 +368,9 @@ function overviewMilitary() {
const lines = filtered.map(
({i, name, fullName, color}) =>
`<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>
</td></tr>`
);
@ -395,7 +421,8 @@ function overviewMilitary() {
$("#militaryOptions").dialog("close");
options.military = unitLines.map((r, i) => {
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 || {};
if (type === "icon") return el.innerHTML || "";
if (type) return value ? value.split(",").map(v => parseInt(v)) : null;
@ -419,7 +446,8 @@ function overviewMilitary() {
}
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({
resizable: false,
title: "Remove regiment",

View file

@ -4,8 +4,8 @@ function editNamesbase() {
closeDialogs("#namesbaseEditor, .stable");
$("#namesbaseEditor").dialog();
if (modules.editNamesbase) return;
modules.editNamesbase = true;
if (fmg.modules.editNamesbase) return;
fmg.modules.editNamesbase = true;
// add listeners
document.getElementById("namesbaseSelect").addEventListener("change", updateInputs);
@ -23,15 +23,23 @@ function editNamesbase() {
const uploader = document.getElementById("namesbaseToLoad");
document.getElementById("namesbaseUpload").addEventListener("click", () => {
uploader.addEventListener("change", function (event) {
uploader.addEventListener(
"change",
function (event) {
uploadFile(event.target, d => namesbaseUpload(d, true));
}, { once: true });
},
{once: true}
);
uploader.click();
});
document.getElementById("namesbaseUploadExtend").addEventListener("click", () => {
uploader.addEventListener("change", function (event) {
uploader.addEventListener(
"change",
function (event) {
uploadFile(event.target, d => namesbaseUpload(d, false));
}, { once: true });
},
{once: true}
);
uploader.click();
});
@ -147,21 +155,28 @@ function editNamesbase() {
: "none";
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 multiwordRate = d3.mean(namesArray.map(n => +n.includes(" ")));
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 < 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>";
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 < 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>";
};
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 < 30) return "<span data-tip='Namesbase average variety < 30 - names can be too repetitive' style='color:orange'>[mean]</span>";
if (variety < 15)
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>";
};
@ -175,9 +190,14 @@ function editNamesbase() {
<div data-tip="Common name length">Median name length: ${d3.median(wordsLength)}</div>
<hr />
<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="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>`;
$("#alert").dialog({
@ -194,7 +214,8 @@ function editNamesbase() {
function namesbaseAdd() {
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});
document.getElementById("namesbaseSelect").add(new Option("Base" + base, base));
document.getElementById("namesbaseSelect").value = base;

View file

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

View file

@ -1,5 +1,4 @@
// UI module to control the options (preferences)
"use strict";
import {stored, lock, locked, applyOption} from "./general";
$("#optionsContainer").draggable({handle: ".drag-trigger", snap: "svg", snapMode: "both"});
$("#exitCustomization").draggable({handle: "div"});
@ -170,10 +169,7 @@ function changeMapSize() {
const maxWidth = Math.max(+mapWidthInput.value, graphWidth);
const maxHeight = Math.max(+mapHeightInput.value, graphHeight);
zoom.translateExtent([
[0, 0],
[maxWidth, maxHeight]
]);
Zoom.translateExtent([0, 0, maxWidth, 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);
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
function applyMapSize() {
export function applyMapSize() {
const zoomMin = +zoomExtentMin.value;
const zoomMax = +zoomExtentMax.value;
graphWidth = +mapWidthInput.value;
@ -194,13 +190,10 @@ function applyMapSize() {
svgWidth = Math.min(graphWidth, window.innerWidth);
svgHeight = Math.min(graphHeight, window.innerHeight);
svg.attr("width", svgWidth).attr("height", svgHeight);
zoom
.translateExtent([
[0, 0],
[graphWidth, graphHeight]
])
.scaleExtent([zoomMin, zoomMax])
.scaleTo(svg, zoomMin);
Zoom.translateExtent([0, 0, graphWidth, graphHeight]);
Zoom.scaleExtent([zoomMin, zoomMax]);
Zoom.scaleTo(svg, zoomMin);
}
function toggleFullscreen() {
@ -217,17 +210,13 @@ function toggleFullscreen() {
}
function toggleTranslateExtent(el) {
const on = (el.dataset.on = +!+el.dataset.on);
if (on)
zoom.translateExtent([
[-graphWidth / 2, -graphHeight / 2],
[graphWidth * 1.5, graphHeight * 1.5]
]);
else
zoom.translateExtent([
[0, 0],
[graphWidth, graphHeight]
]);
const on = !Number(el.dataset.on);
const extent = on
? [-graphWidth / 2, -graphHeight / 2, graphWidth * 1.5, graphHeight * 1.5]
: [0, 0, graphWidth, graphHeight];
Zoom.translateExtent(extent);
el.dataset.on = Number(on);
}
// add voice options
@ -294,7 +283,8 @@ function restoreSeed(id) {
function restoreDefaultZoomExtent() {
zoomExtentMin.value = 1;
zoomExtentMax.value = 20;
zoom.scaleExtent([1, 20]).scaleTo(svg, 1);
Zoom.scaleExtent([1, 20]);
Zoom.scaleTo(svg, 1);
}
function copyMapURL() {
@ -461,15 +451,16 @@ function changeDialogsTheme(themeColor, transparency) {
}
function changeZoomExtent(value) {
const min = Math.max(+zoomExtentMin.value, 0.01);
const max = Math.min(+zoomExtentMax.value, 200);
zoom.scaleExtent([min, max]);
const min = Math.max(+byId("zoomExtentMin").value, 0.01);
const max = Math.min(+byId("zoomExtentMax").value, 200);
Zoom.scaleExtent([min, max]);
const scale = minmax(+value, 0.01, 200);
zoom.scaleTo(svg, scale);
Zoom.scaleTo(svg, scale);
}
// restore options stored in localStorage
function applyStoredOptions() {
export function applyStoredOptions() {
if (!stored("mapWidth") || !stored("mapHeight")) {
mapWidthInput.value = window.innerWidth;
mapHeightInput.value = window.innerHeight;
@ -530,7 +521,7 @@ function applyStoredOptions() {
}
// 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
// 'Options' settings
@ -595,7 +586,7 @@ function randomizeCultureSet() {
}
function setRendering(value) {
viewbox.attr("shape-rendering", value);
fmg.viewbox?.attr("shape-rendering", value);
}
// 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 === "exportButton") showExportPane();
else if (id === "loadButton") showLoadPane();
else if (id === "zoomReset") resetZoom(1000);
else if (id === "zoomReset") Zoom.reset(1000);
});
function regeneratePrompt(options) {
@ -975,8 +966,8 @@ function toggle3dOptions() {
updateValues();
if (modules.options3d) return;
modules.options3d = true;
if (fmg.modules.options3d) return;
fmg.modules.options3d = true;
document.getElementById("options3dUpdate").addEventListener("click", ThreeD.update);
document.getElementById("options3dSave").addEventListener("click", ThreeD.saveScreenshot);

View file

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

View file

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

View file

@ -9,8 +9,8 @@ function overviewRegiments(state) {
addLines();
$("#regimentsOverview").dialog();
if (modules.overviewRegiments) return;
modules.overviewRegiments = true;
if (fmg.modules.overviewRegiments) return;
fmg.modules.overviewRegiments = true;
updateHeaders();
$("#regimentsOverview").dialog({
@ -37,7 +37,9 @@ function overviewRegiments(state) {
const insert = html => document.getElementById("regimentsTotal").insertAdjacentHTML("beforebegin", html);
for (const u of options.military) {
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) {
e.addEventListener("click", function () {
@ -60,7 +62,9 @@ function overviewRegiments(state) {
for (const r of s.military) {
const sortData = options.military.map(u => `data-${u.name}=${r.u[u.name] || 0}`).join(" ");
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(" ");
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">
<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>`;
@ -92,7 +98,9 @@ function overviewRegiments(state) {
// add listeners
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) {

View file

@ -19,8 +19,8 @@ function editReliefIcon() {
close: closeReliefEditor
});
if (modules.editReliefIcon) return;
modules.editReliefIcon = true;
if (fmg.modules.editReliefIcon) return;
fmg.modules.editReliefIcon = true;
// add listeners
document.getElementById("reliefIndividual").addEventListener("click", enterIndividualMode);
@ -260,7 +260,9 @@ function editReliefIcon() {
const type = reliefIconsDiv.querySelector("svg.pressed")?.dataset.type;
selection = type ? terrain.selectAll("use[href='" + type + "']") : terrain.selectAll("use");
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({

View file

@ -21,8 +21,8 @@ function createRiver() {
close: closeRiverCreator
});
if (modules.createRiver) return;
modules.createRiver = true;
if (fmg.modules.createRiver) return;
fmg.modules.createRiver = true;
// add listeners
document.getElementById("riverCreatorComplete").addEventListener("click", addRiver);
@ -100,12 +100,30 @@ function createRiver() {
const name = getName(mouth);
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;
// render river
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);
}

View file

@ -10,7 +10,10 @@ function editRiver(id) {
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", "controlPoints");
@ -29,8 +32,8 @@ function editRiver(id) {
close: closeRiverEditor
});
if (modules.editRiver) return;
modules.editRiver = true;
if (fmg.modules.editRiver) return;
fmg.modules.editRiver = true;
// add listeners
document.getElementById("riverCreateSelectingCells").addEventListener("click", createRiver);
@ -163,7 +166,7 @@ function editRiver(id) {
elSelected.attr("d", path);
updateRiverLength(river);
if (modules.elevation) showEPForRiver(elSelected.node());
if (fmg.modules.elevation) showEPForRiver(elSelected.node());
}
function addControlPoint() {
@ -227,7 +230,7 @@ function editRiver(id) {
}
function showElevationProfile() {
modules.elevation = true;
fmg.modules.elevation = true;
showEPForRiver(elSelected.node());
}

View file

@ -8,8 +8,8 @@ function overviewRivers() {
riversOverviewAddLines();
$("#riversOverview").dialog();
if (modules.overviewRivers) return;
modules.overviewRivers = true;
if (fmg.modules.overviewRivers) return;
fmg.modules.overviewRivers = true;
$("#riversOverview").dialog({
title: "Rivers Overview",
@ -75,7 +75,9 @@ function overviewRivers() {
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-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);
}
@ -110,7 +112,18 @@ function overviewRivers() {
} else {
rivers.attr("data-basin", "hightlighted");
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) => {
const color = colors[i % colors.length];

View file

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

View file

@ -76,9 +76,22 @@ function selectStyleElement() {
// stroke color and width
if (
["armies", "routes", "lakes", "borders", "cults", "relig", "cells", "coastline", "prec", "ice", "icons", "coordinates", "zones", "gridOverlay"].includes(
sel
)
[
"armies",
"routes",
"lakes",
"borders",
"cults",
"relig",
"cells",
"coastline",
"prec",
"ice",
"icons",
"coordinates",
"zones",
"gridOverlay"
].includes(sel)
) {
styleStroke.style.display = "block";
styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke");
@ -87,14 +100,29 @@ function selectStyleElement() {
}
// stroke dash
if (["routes", "borders", "temperature", "legend", "population", "coordinates", "zones", "gridOverlay"].includes(sel)) {
if (
["routes", "borders", "temperature", "legend", "population", "coordinates", "zones", "gridOverlay"].includes(sel)
) {
styleStrokeDash.style.display = "block";
styleStrokeDasharrayInput.value = el.attr("stroke-dasharray") || "";
styleStrokeLinecapInput.value = el.attr("stroke-linecap") || "inherit";
}
// clipping
if (["cells", "gridOverlay", "coordinates", "compass", "terrain", "temperature", "routes", "texture", "biomes", "zones"].includes(sel)) {
if (
[
"cells",
"gridOverlay",
"coordinates",
"compass",
"terrain",
"temperature",
"routes",
"texture",
"biomes",
"zones"
].includes(sel)
) {
styleClipping.style.display = "block";
styleClippingInput.value = el.attr("mask") || "";
}
@ -142,8 +170,12 @@ function selectStyleElement() {
if (sel === "population") {
stylePopulation.style.display = "block";
stylePopulationRuralStrokeInput.value = stylePopulationRuralStrokeOutput.value = population.select("#rural").attr("stroke");
stylePopulationUrbanStrokeInput.value = stylePopulationUrbanStrokeOutput.value = population.select("#urban").attr("stroke");
stylePopulationRuralStrokeInput.value = stylePopulationRuralStrokeOutput.value = population
.select("#rural")
.attr("stroke");
stylePopulationUrbanStrokeInput.value = stylePopulationUrbanStrokeOutput.value = population
.select("#urban")
.attr("stroke");
styleStrokeWidth.style.display = "block";
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || "";
}
@ -233,7 +265,8 @@ function selectStyleElement() {
styleOcean.style.display = "block";
styleOceanFill.value = styleOceanFillOutput.value = oceanLayers.select("#oceanBase").attr("fill");
styleOceanPattern.value = document.getElementById("oceanicPattern")?.getAttribute("href");
styleOceanPatternOpacity.value = styleOceanPatternOpacityOutput.value = document.getElementById("oceanicPattern").getAttribute("opacity") || 1;
styleOceanPatternOpacity.value = styleOceanPatternOpacityOutput.value =
document.getElementById("oceanicPattern").getAttribute("opacity") || 1;
outlineLayers.value = oceanLayers.attr("layers");
}
@ -551,7 +584,10 @@ styleFontAdd.addEventListener("click", function () {
if (!family) return tip("Please provide a font name", false, "error");
const existingFont = method === "fontURL" ? fonts.find(font => font.family === family && font.src === src) : fonts.find(font => font.family === family);
const existingFont =
method === "fontURL"
? fonts.find(font => font.family === family && font.src === src)
: fonts.find(font => font.family === family);
if (existingFont) return tip("The font is already added", false, "error");
if (method === "fontURL") addWebFont(family, src);
@ -710,9 +746,9 @@ styleArmiesSize.addEventListener("input", function () {
});
});
emblemsStateSizeInput.addEventListener("change", drawEmblems);
emblemsProvinceSizeInput.addEventListener("change", drawEmblems);
emblemsBurgSizeInput.addEventListener("change", drawEmblems);
emblemsStateSizeInput.addEventListener("change", () => drawEmblems());
emblemsProvinceSizeInput.addEventListener("change", () => drawEmblems());
emblemsBurgSizeInput.addEventListener("change", () => drawEmblems());
// request a URL to image to be used as a texture
function textureProvideURL() {

View file

@ -1,14 +1,26 @@
// UI module to control the style presets
"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_";
// add style presets to list
{
const systemOptions = systemPresets.map(styleName => `<option value="${styleName}">${styleName}</option>`);
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("");
document.getElementById("stylePreset").innerHTML = options;
}
@ -37,7 +49,8 @@ async function getStylePreset(desiredPreset) {
const isValid = JSON.isValid(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";
}
}
@ -126,8 +139,8 @@ function addStylePreset() {
styleSaverJSON.value = JSON.stringify(collectStyleData(), null, 2);
checkName();
if (modules.saveStyle) return;
modules.saveStyle = true;
if (fmg.modules.saveStyle) return;
fmg.modules.saveStyle = true;
// add listeners
document.getElementById("styleSaverName").addEventListener("input", checkName);
@ -145,8 +158,31 @@ function addStylePreset() {
"#stateBorders": ["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"],
"#gridOverlay": ["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"],
"#gridOverlay": [
"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"],
"#rose": ["transform"],
"#relig": ["opacity", "stroke", "stroke-width", "filter"],
@ -174,7 +210,17 @@ function addStylePreset() {
"#statesBody": ["opacity", "filter"],
"#statesHalo": ["opacity", "data-width", "stroke-width", "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"],
"#emblems": ["opacity", "stroke-width", "filter"],
"#texture": ["opacity", "filter", "mask"],
@ -184,16 +230,65 @@ function addStylePreset() {
"#oceanBase": ["fill"],
"#oceanicPattern": ["href", "opacity"],
"#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"],
"#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"],
"#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"],
"#labels > #states": ["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"],
"#labels > #states": [
"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"]
};
@ -238,7 +333,8 @@ function addStylePreset() {
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 (!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;
applyOption(stylePreset, presetName, desiredName + " [custom]");

View file

@ -136,7 +136,14 @@ window.UISubmap = (function () {
}
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 ctx = canvas.getContext("2d");
@ -173,7 +180,11 @@ window.UISubmap = (function () {
const {angle, shiftX, shiftY, ratio, mirrorH, mirrorV} = getTransformInput();
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 scale = r => (x, y) => [(x - cx) * r + cx, (y - cy) * r + cy];
const flipH = (x, y) => [-x + 2 * cx, y];
@ -185,7 +196,11 @@ window.UISubmap = (function () {
let inverse = id;
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 (mirrorV) [projection, inverse] = [app(flipV, projection), app(inverse, flipV)];
if (shiftX || shiftY) {
@ -208,6 +223,14 @@ window.UISubmap = (function () {
});
}, 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)
const generateSubmap = debounce(function () {
WARN && console.warn("Resampling current map");
@ -244,7 +267,10 @@ window.UISubmap = (function () {
// fix scale
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;
startResample(options);
}, 1000);
@ -253,9 +279,9 @@ window.UISubmap = (function () {
// Do model changes with Submap.resample then do view changes if needed
resetZoom(0);
let oldstate = {
grid: deepCopy(grid),
pack: deepCopy(pack),
notes: deepCopy(notes),
grid: structuredClone(grid),
pack: structuredClone(pack),
notes: structuredClone(notes),
seed,
graphWidth,
graphHeight

View file

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

View file

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

View file

@ -8,8 +8,8 @@ function editZones() {
updateFilters();
zonesEditorAddLines();
if (modules.editZones) return;
modules.editZones = true;
if (fmg.modules.editZones) return;
fmg.modules.editZones = true;
$("#zonesEditor").dialog({
title: "Zones Editor",
@ -61,7 +61,8 @@ function editZones() {
const filterSelect = document.getElementById("zonesFilterType");
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;
}
@ -80,9 +81,12 @@ function editZones() {
const fill = zoneEl.getAttribute("fill");
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 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 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 focused = defs.select("#fog #focus" + zoneEl.id).size();
@ -98,8 +102,12 @@ function editZones() {
<span data-tip="${populationTip}" class="icon-male hide"></span>
<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="Toggle zone focus" class="icon-pin ${focused ? "" : " inactive"} hide ${c.length ? "" : " placeholder"}"></span>
<span data-tip="Toggle zone visibility" class="icon-eye ${inactive ? " inactive" : ""} hide ${c.length ? "" : " placeholder"}"></span>
<span data-tip="Toggle zone focus" class="icon-pin ${focused ? "" : " inactive"} hide ${
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>
</div>`;
});
@ -109,7 +117,9 @@ function editZones() {
// update footer
const totalArea = getArea(graphWidth * graphHeight);
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;
zonesFooterNumber.innerHTML = /* html */ `${filteredZones.length} of ${zones.length}`;
zonesFooterCells.innerHTML = pack.cells.i.length;
@ -150,7 +160,13 @@ function editZones() {
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) {
const zone = $("#" + ui.item.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"}});
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");
zones.selectAll("g").each(function () {
@ -285,7 +305,8 @@ function editZones() {
zonesEditor.querySelectorAll(".hide:not(.show)").forEach(el => el.classList.remove("hidden"));
zonesFooter.style.display = "block";
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();
clearMainTip();
@ -356,7 +377,8 @@ function editZones() {
body.querySelectorAll(":scope > div").forEach(function (el) {
el.querySelector(".stateCells").innerHTML = rn((+el.dataset.cells / totalCells) * 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 {
body.dataset.type = "absolute";
@ -369,7 +391,13 @@ function editZones() {
const description = "Unknown zone";
const type = "Unknown";
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();
}
@ -411,13 +439,19 @@ function editZones() {
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 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 l = n => Number(n).toLocaleString();
alertMessage.innerHTML = /* html */ `Rural: <input type="number" min="0" step="1" id="ruralPop" value=${rural} style="width:6em" /> Urban:
<input type="number" min="0" step="1" id="urbanPop" value=${urban} style="width:6em" ${burgs.length ? "" : "disabled"} />
<p>Total population: ${l(total)} <span id="totalPop">${l(total)}</span> (<span id="totalPopPerc">100</span>%)</p>`;
<input type="number" min="0" step="1" id="urbanPop" value=${urban} style="width:6em" ${
burgs.length ? "" : "disabled"
} />
<p>Total population: ${l(total)} <span id="totalPop">${l(
total
)}</span> (<span id="totalPopPerc">100</span>%)</p>`;
const update = function () {
const totalNew = ruralPop.valueAsNumber + urbanPop.valueAsNumber;

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"
},
"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
// https://github.com/Azgaar/Fantasy-Map-Generator
"use strict";
// set debug options
const PRODUCTION = location.hostname && location.hostname !== "localhost" && location.hostname !== "127.0.0.1";
const DEBUG = localStorage.getItem("debug");
const INFO = DEBUG || !PRODUCTION;
const TIME = DEBUG || !PRODUCTION;
const WARN = true;
const ERROR = true;
import {PRODUCTION, UINT16_MAX} from "./constants";
import {INFO, TIME, WARN, ERROR} from "./config/logging";
import {createTypedArray} from "./utils";
import {shouldRegenerateGrid, generateGrid, calculateVoronoi, getPackPolygon, isLand} from "./utils/graphUtils";
import {drawRivers, drawStates, drawBorders} from "../modules/ui/layers";
import {invokeActiveZooming} from "../modules/activeZooming";
import {applyStoredOptions, applyMapSize, randomizeOptions} from "../modules/ui/options";
import {locked} from "../modules/ui/general";
// detect device
const MOBILE = window.innerWidth < 600 || navigator.userAgentData?.mobile;
// typed arrays max values
const UINT8_MAX = 255;
const UINT16_MAX = 65535;
const UINT32_MAX = 4294967295;
globalThis.fmg = {
modules: {}
};
if (PRODUCTION && "serviceWorker" in navigator) {
window.addEventListener("load", () => {
navigator.serviceWorker.register("./sw.js").catch(err => {
navigator.serviceWorker.register("../sw.js").catch(err => {
console.error("ServiceWorker registration failed: ", err);
});
});
@ -29,170 +25,47 @@ if (PRODUCTION && "serviceWorker" in navigator) {
"beforeinstallprompt",
async event => {
event.preventDefault();
const Installation = await import("./modules/dynamic/installation.js");
const Installation = await import("../modules/dynamic/installation.js");
Installation.init(event);
},
{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
let options = {
options = {
pinNotes: false,
showMFCGMap: true,
winds: [225, 45, 225, 315, 135, 315],
stateLabelsMode: "auto"
};
let mapCoordinates = {}; // map coordinates on globe
let populationRate = +document.getElementById("populationRateInput").value;
let distanceScale = +document.getElementById("distanceScaleInput").value;
let urbanization = +document.getElementById("urbanizationInput").value;
let urbanDensity = +document.getElementById("urbanDensityInput").value;
let statesNeutral = 1; // statesEditor growth parameter
mapCoordinates = {}; // map coordinates on globe
populationRate = +byId("populationRateInput").value;
distanceScale = +byId("distanceScaleInput").value;
urbanization = +byId("urbanizationInput").value;
urbanDensity = +byId("urbanDensityInput").value;
statesNeutral = 1; // statesEditor growth parameter
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
let graphWidth = +mapWidthInput.value;
let graphHeight = +mapHeightInput.value;
graphWidth = +byId("mapWidthInput").value;
graphHeight = +byId("mapHeightInput").value;
// svg canvas resolution, can be changed
let svgWidth = graphWidth;
let svgHeight = graphHeight;
svgWidth = graphWidth;
svgHeight = graphHeight;
landmass.append("rect").attr("x", 0).attr("y", 0).attr("width", graphWidth).attr("height", 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);
defineSvg(graphWidth, graphHeight);
document.addEventListener("DOMContentLoaded", async () => {
document.on("DOMContentLoaded", async () => {
if (!location.hostname) {
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
@ -321,7 +194,7 @@ function focusOn() {
if (cellParam) {
const cell = +params.get("cell");
const [x, y] = pack.cells.p[cell];
zoomTo(x, y, scale, 1600);
Zoom.to(x, y, scale, 1600);
return;
}
@ -330,13 +203,13 @@ function focusOn() {
if (!burg) return;
const {x, y} = burg;
zoomTo(x, y, scale, 1600);
Zoom.to(x, y, scale, 1600);
return;
}
const x = +params.get("x") || graphWidth / 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();
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) {
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) {
const i = +use.dataset.i;
const id = type + "COA" + i;
@ -1547,7 +1255,9 @@ function addZones(number = 1) {
const invader = ra(atWar);
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;
const cellsArray = [],
@ -1589,7 +1299,9 @@ function addZones(number = 1) {
const neib = ra(state.neighbors.filter(n => n && !states[n].removed));
if (!neib) return;
const cell = cells.i.find(i => cells.state[i] === state.i && !state.removed && cells.c[i].some(c => cells.state[c] === neib));
const cell = cells.i.find(
i => cells.state[i] === state.i && !state.removed && cells.c[i].some(c => cells.state[c] === neib)
);
const cellsArray = [];
const queue = [];
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;
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"));
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;
const target = cells.religion[cell];
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 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 adjective = () =>
ra(["Great", "Silent", "Severe", "Blind", "Unknown", "Loud", "Deadly", "Burning", "Bloody", "Brutal", "Fatal"]);
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;
zonesData.push({name, type: "Disease", cells: cellsArray, fill: "url(#hatch12)"});
}
@ -1812,7 +1584,9 @@ function addZones(number = 1) {
meanFlux = d3.mean(fl),
maxFlux = d3.max(fl),
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;
const cell = +ra(rivers),
@ -1923,7 +1697,7 @@ const regenerateMap = debounce(async function (options) {
closeDialogs("#worldConfigurator, #options3d");
customization = 0;
resetZoom(1000);
Zoom.reset(1000);
undraw();
await generate(options);
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";
// FMG utils related to graph
import {TIME} from "../config/logging";
import {createTypedArray} from ".";
// 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;
if (cellsDesired !== grid.cellsDesired) return true;
@ -13,7 +13,7 @@ function shouldRegenerateGrid(grid) {
return grid.spacing !== newSpacing || grid.cellsX !== newCellsX || grid.cellsY !== newCellsY;
}
function generateGrid() {
export function generateGrid() {
Math.random = aleaPRNG(seed); // reset PRNG
const {spacing, cellsDesired, boundary, points, cellsX, cellsY} = placePoints();
const {cells, vertices} = calculateVoronoi(points, boundary);
@ -36,7 +36,7 @@ function placePoints() {
}
// calculate Delaunay and then Voronoi diagram
function calculateVoronoi(points, boundary) {
export function calculateVoronoi(points, boundary) {
TIME && console.time("calculateDelaunay");
const allPoints = points.concat(boundary);
const delaunay = Delaunator.from(allPoints);
@ -95,8 +95,11 @@ function getJitteredGrid(width, height, spacing) {
}
// return cell index on a regular square grid
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));
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 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 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
function findAll(x, y, radius) {
export function findAll(x, y, radius) {
const found = pack.cells.q.findAll(x, y, radius);
return found.map(r => r[2]);
}
// 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 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
function getGridPolygon(i) {
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
function isLand(i) {
export function isLand(i) {
return pack.cells.h[i] >= 20;
}
// filter water cells
function isWater(i) {
export function isWater(i) {
return pack.cells.h[i] < 20;
}
@ -246,7 +249,14 @@ void (function addFindAll() {
i++;
// 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.
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,
"skipLibCheck": true
},
"include": ["src"]
"include": ["src", "utils", "modules"]
}

View file

@ -7,54 +7,3 @@ function last(array) {
function unique(array) {
return [...new Set(array)];
}
// deep copy for Arrays (and other objects)
function deepCopy(obj) {
const id = x => x;
const dcTArray = a => a.map(id);
const dcObject = x => Object.fromEntries(Object.entries(x).map(([k, d]) => [k, dcAny(d)]));
const dcAny = x => (x instanceof Object ? (cf.get(x.constructor) || id)(x) : x);
// don't map keys, probably this is what we would expect
const dcMapCore = m => [...m.entries()].map(([k, v]) => [k, dcAny(v)]);
const cf = new Map([
[Int8Array, dcTArray],
[Uint8Array, dcTArray],
[Uint8ClampedArray, dcTArray],
[Int16Array, dcTArray],
[Uint16Array, dcTArray],
[Int32Array, dcTArray],
[Uint32Array, dcTArray],
[Float32Array, dcTArray],
[Float64Array, dcTArray],
[BigInt64Array, dcTArray],
[BigUint64Array, dcTArray],
[Map, m => new Map(dcMapCore(m))],
[WeakMap, m => new WeakMap(dcMapCore(m))],
[Array, a => a.map(dcAny)],
[Set, s => [...s.values()].map(dcAny)],
[Date, d => new Date(d.getTime())],
[Object, dcObject]
// ... 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({});