diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 00000000..6afdcc7d
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,11 @@
+{
+ "version": "0.1.0",
+ "configurations": [
+ {
+ "name": "Debug",
+ "type": "chrome",
+ "request": "launch",
+ "file": "${workspaceFolder}/index.html"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/index.css b/index.css
index 7e4ba024..ba0f6bf1 100644
--- a/index.css
+++ b/index.css
@@ -1189,7 +1189,8 @@ div.states:hover {
}
div.states > *,
-div.states sup {
+div.states sup,
+div.totalLine > div {
display: inline-block;
}
@@ -1415,6 +1416,7 @@ div.states.Self {
border-color: #858b8e;
background-image: linear-gradient(to right, #f2f2f2 0%, #b0c6d9 100%);
font-style: italic;
+ font-weight: bold;
margin-bottom: .2em;
cursor: default !important;
}
@@ -1437,7 +1439,8 @@ rect.fillRect {
stroke-width: 2;
}
-#militaryHeader > div {
+#militaryHeader > div,
+#regimentsHeader > div {
width: 5.2em;
}
@@ -1446,7 +1449,8 @@ rect.fillRect {
}
#militaryBody div.states > input,
-#militaryBody div.states > div {
+#militaryBody div.states > div,
+#regimentsBody div.states > div {
width: 5em;
}
diff --git a/index.html b/index.html
index c9676d1d..3b5fb392 100644
--- a/index.html
+++ b/index.html
@@ -1960,7 +1960,7 @@
+
+
Ally
Friendly
@@ -2844,8 +2846,6 @@
Suzerain
-
-
@@ -3280,19 +3280,39 @@
-
+
+
+
+
@@ -3519,6 +3539,7 @@
+
diff --git a/main.js b/main.js
index 49f345d5..9c247a75 100644
--- a/main.js
+++ b/main.js
@@ -308,9 +308,8 @@ function applyDefaultBiomesSystem() {
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,80,100,255,150]; // biome movement cost
- const biomesMartix = [
- // hot ↔ cold; dry ↕ wet
+ const cost = [10,200,150,60,50,70,70,80,90,200,1000,5000,150]; // biome movement cost
+ const biomesMartix = [ // hot ↔ cold; 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,2]),
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,9,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]),
@@ -742,14 +741,14 @@ function calculateTemperatures() {
const tEq = +temperatureEquatorInput.value;
const tPole = +temperaturePoleInput.value;
const tDelta = tEq - tPole;
+ const int = d3.easePolyInOut.exponent(.5); // interpolation function
d3.range(0, cells.i.length, grid.cellsX).forEach(function(r) {
const y = grid.points[r][1];
- const lat = Math.abs(mapCoordinates.latN - y / graphHeight * mapCoordinates.latT);
- const initTemp = tEq - lat / 90 * tDelta;
+ const lat = Math.abs(mapCoordinates.latN - y / graphHeight * mapCoordinates.latT); // [0; 90]
+ const initTemp = tEq - int(lat / 90) * tDelta;
for (let i = r; i < r+grid.cellsX; i++) {
- const temp = initTemp - convertToFriendly(cells.h[i]);
- cells.temp[i] = Math.max(Math.min(temp, 127), -128);
+ cells.temp[i] = Math.max(Math.min(initTemp - convertToFriendly(cells.h[i]), 127), -128);
}
});
@@ -1036,7 +1035,7 @@ function drawCoastline() {
// Re-mark features (ocean, lakes, islands)
function reMarkFeatures() {
console.time("reMarkFeatures");
- const cells = pack.cells, features = pack.features = [0];
+ const cells = pack.cells, features = pack.features = [0], temp = grid.cells.temp;
cells.f = new Uint16Array(cells.i.length); // cell feature number
cells.t = new Int16Array(cells.i.length); // cell type: 1 = land along coast; -1 = water along coast;
cells.haven = cells.i.length < 65535 ? new Uint16Array(cells.i.length) : new Uint32Array(cells.i.length);// cell haven (opposite water cell);
@@ -1046,6 +1045,7 @@ function reMarkFeatures() {
const start = queue[0]; // first cell
cells.f[start] = i; // assign feature number
const land = cells.h[start] >= 20;
+ //const frozen = !land && temp[cells.g[start]] < -5; // check if water is frozen
let border = false; // true if feature touches map border
let cellNumber = 1; // to count cells number in a feature
@@ -1063,9 +1063,10 @@ function reMarkFeatures() {
if (!cells.t[e] && cells.t[q] === 1) cells.t[e] = 2;
else if (!cells.t[q] && cells.t[e] === 1) cells.t[q] = 2;
}
- if (land === eLand && cells.f[e] === 0) {
- cells.f[e] = i;
+ if (!cells.f[e] && land === eLand) {
+ //if (!land && frozen !== temp[cells.g[e]] < -5) return;
queue.push(e);
+ cells.f[e] = i;
cellNumber++;
}
});
@@ -1073,15 +1074,14 @@ function reMarkFeatures() {
const type = land ? "island" : border ? "ocean" : "lake";
let group;
- if (type === "lake") group = defineLakeGroup(start, cellNumber);
- else if (type === "ocean") group = "ocean";
+ if (type === "lake") group = defineLakeGroup(start, cellNumber, temp[cells.g[start]]);
+ else if (type === "ocean") group = defineOceanGroup(cellNumber);
else if (type === "island") group = defineIslandGroup(start, cellNumber);
features.push({i, land, border, type, cells: cellNumber, firstCell: start, group, ports:0});
queue[0] = cells.f.findIndex(f => !f); // find unmarked cell
}
- function defineLakeGroup(cell, number) {
- const temp = grid.cells.temp[cells.g[cell]];
+ function defineLakeGroup(cell, number, temp) {
if (temp > 24) return "salt";
if (temp < -3) return "frozen";
const height = d3.max(cells.c[cell].map(c => cells.h[c]));
@@ -1090,6 +1090,12 @@ function reMarkFeatures() {
return "freshwater";
}
+ function defineOceanGroup(number) {
+ if (number > grid.cells.i.length / 25) return "ocean";
+ if (number > grid.cells.i.length / 100) return "sea";
+ return "gulf";
+ }
+
function defineIslandGroup(cell, number) {
if (cell && features[cells.f[cell-1]].type === "lake") return "lake_island";
if (number > grid.cells.i.length / 10) return "continent";
@@ -1124,12 +1130,13 @@ function defineBiomes() {
for (const i of cells.i) {
if (f[cells.f[i]].group === "freshwater") cells.h[i] = 19; // de-elevate lakes
- if (cells.h[i] < 20) continue; // water cells have biome 0
+ const temp = grid.cells.temp[cells.g[i]]; // temperature
+
+ if (cells.h[i] < 20 && temp > -6) continue; // liquid water cells have biome 0
let moist = grid.cells.prec[cells.g[i]];
if (cells.r[i]) moist += Math.max(cells.fl[i] / 20, 2);
const n = cells.c[i].filter(isLand).map(c => grid.cells.prec[cells.g[c]]).concat([moist]);
moist = rn(4 + d3.mean(n));
- const temp = grid.cells.temp[cells.g[i]]; // flux from precipitation
cells.biome[i] = getBiomeId(moist, temp, cells.h[i]);
}
@@ -1138,6 +1145,7 @@ function defineBiomes() {
function getBiomeId(moisture, temperature, height) {
if (temperature < -5) return 11; // permafrost biome
+ if (height < 20) return 0; // liquid water cells have marine biome
if (moisture > 40 && height < 25 || moisture > 24 && height > 24) return 12; // wetland biome
const m = Math.min(moisture / 5 | 0, 4); // moisture band from 0 to 4
const t = Math.min(Math.max(20 - temperature, 0), 25); // temparature band from 0 to 25
diff --git a/modules/burgs-and-states.js b/modules/burgs-and-states.js
index 34fa3bee..1b86950d 100644
--- a/modules/burgs-and-states.js
+++ b/modules/burgs-and-states.js
@@ -137,22 +137,24 @@
}
}
- // define burg coordinates and define details
+ // define burg coordinates, port status and define details
const specifyBurgs = function() {
console.time("specifyBurgs");
- const cells = pack.cells, vertices = pack.vertices;
+ const cells = pack.cells, vertices = pack.vertices, features = pack.features;
+ checkAccessibility();
for (const b of pack.burgs) {
if (!b.i) continue;
const i = b.cell;
// asign port status
- if (cells.haven[i]) {
- const f = cells.f[cells.haven[i]]; // water body id
+ const haven = cells.haven[i];
+ if (haven && cells.biome[haven] === 0) {
+ const f = cells.f[haven]; // water body id
// port is a capital with any harbor OR town with good harbor
- const port = pack.features[f].cells > 1 && ((b.capital && cells.harbor[i]) || cells.harbor[i] === 1);
+ const port = features[f].cells > 1 && ((b.capital && cells.harbor[i]) || cells.harbor[i] === 1);
b.port = port ? f : 0; // port is defined by water body id it lays on
- if (port) {pack.features[f].ports += 1; pack.features[b.feature].ports += 1;}
+ if (port) {features[f].ports += 1; features[b.feature].ports += 1;}
} else b.port = 0;
// define burg population (keep urbanization at about 10% rate)
@@ -178,12 +180,43 @@
}
// de-assign port status if it's the only one on feature
- for (const f of pack.features) {
+ for (const f of features) {
if (!f.i || f.land || f.ports !== 1) continue;
const port = pack.burgs.find(b => b.port === f.i);
port.port = 0;
f.port = 0;
- pack.features[port.feature].ports -= 1;
+ features[port.feature].ports -= 1;
+ }
+
+ // separate arctic seas for correct searoutes generation
+ function checkAccessibility() {
+ const oceanCells = cells.i.filter(i => cells.h[i] < 20 && features[cells.f[i]].type === "ocean");
+ const marked = [];
+ let firstCell = oceanCells.find(i => !marked[i]);
+
+ while (firstCell !== undefined) {
+ const queue = [firstCell];
+ const f = features[cells.f[firstCell]]; // old feature
+ const i = last(features).i+1; // new feature id to assign
+ const biome = cells.biome[firstCell];
+ marked[firstCell] = 1;
+ let cellNumber = 1;
+
+ while (queue.length) {
+ for (const c of cells.c[queue.pop()]) {
+ if (cells.biome[c] !== biome || cells.h[c] >= 20) continue;
+ if (marked[c]) continue;
+ queue.push(c);
+ cells.f[c] = i;
+ marked[c] = 1;
+ cellNumber++;
+ }
+ }
+
+ const group = biome ? "frozen " + f.group : f.group;
+ features.push({i, parent:f.i, land:false, border:true, type:"ocean", cells: cellNumber, firstCell, group, ports:0});
+ firstCell = oceanCells.find(i => !marked[i]);
+ }
}
console.timeEnd("specifyBurgs");
@@ -274,16 +307,19 @@
while (queue.length) {
const next = queue.dequeue(), n = next.e, p = next.p, s = next.s, b = next.b;
const type = states[s].type;
+ const culture = states[s].culture;
cells.c[n].forEach(function(e) {
if (cells.state[e] && e === states[cells.state[e]].center) return; // do not overwrite capital cells
- const cultureCost = states[s].culture === cells.culture[e] ? -9 : 700;
+ const cultureCost = culture === cells.culture[e] ? -9 : 100;
+ const populationCost = cells.s[e] ? 20 - cells.s[e] : 2500;
const biomeCost = getBiomeCost(b, cells.biome[e], type);
const heightCost = getHeightCost(pack.features[cells.f[e]], cells.h[e], type);
const riverCost = getRiverCost(cells.r[e], e, type);
const typeCost = getTypeCost(cells.t[e], type);
- const totalCost = p + (10 + cultureCost + biomeCost + heightCost + riverCost + typeCost) / states[s].expansionism;
+ const cellCost = Math.max(cultureCost + populationCost + biomeCost + heightCost + riverCost + typeCost, 0);
+ const totalCost = p + 10 + cellCost / states[s].expansionism;
if (totalCost > neutral) return;
@@ -325,7 +361,7 @@
function getTypeCost(t, type) {
if (t === 1) return type === "Naval" || type === "Lake" ? 0 : type === "Nomadic" ? 60 : 20; // penalty for coastline
if (t === 2) return type === "Naval" || type === "Nomadic" ? 30 : 0; // low penalty for land level 2 for Navals and nomads
- if (t !== -1) return type === "Naval" || type === "Lake" ? 100 : 0; // penalty for mainland for navals
+ if (t !== -1) return type === "Naval" || type === "Lake" ? 100 : 0; // penalty for mainland for navals
return 0;
}
diff --git a/modules/military-generator.js b/modules/military-generator.js
index 99b91bad..8063d17d 100644
--- a/modules/military-generator.js
+++ b/modules/military-generator.js
@@ -153,6 +153,7 @@
});
function createRegiments(nodes, s) {
+ if (!nodes.length) return [];
nodes.sort((a,b) => a.a - b.a);
const tree = d3.quadtree(nodes, d => d.x, d => d.y);
nodes.forEach(n => {
diff --git a/modules/names-generator.js b/modules/names-generator.js
index 2a8c66c0..84b3a860 100644
--- a/modules/names-generator.js
+++ b/modules/names-generator.js
@@ -76,6 +76,9 @@
}
// parse word to get a final name
+ const l = last(w); // last letter
+ if (l === "'" || l === " ") w = w.slice(0,-1); // not allow apostrophe and space at the end
+
let name = [...w].reduce(function(r, c, i, d) {
if (c === d[i+1] && !dupl.includes(c)) return r; // duplication is not allowed
if (!r.length) return c.toUpperCase();
@@ -83,8 +86,7 @@
if (r.slice(-1) === " ") return r + c.toUpperCase(); // capitalize letter after space
if (r.slice(-1) === "-") return r + c.toUpperCase(); // capitalize letter after hyphen
if (c === "a" && d[i+1] === "e") return r; // "ae" => "e"
- if (c === " " && i+1 === d.length) return r;
- if (i+2 < d.length && !vowel(c) && !vowel(d[i+1]) && !vowel(d[i+2])) return r; // remove consonant before 2 consonants
+ if (i+1 < d.length && !vowel(c) && !vowel(d[i-1]) && !vowel(d[i+1])) return r; // remove consonant between 2 consonants
if (i+2 < d.length && c === d[i+1] && c === d[i+2]) return r; // remove tree same letters in a row
return r + c;
}, "");
@@ -96,6 +98,7 @@
console.error("Name is too short! Random name to be selected");
name = ra(nameBases[base].b.split(","));
}
+
return name;
}
@@ -115,8 +118,7 @@
// generate short name for base
const getBaseShort = function(base) {
if (nameBases[base] === undefined) {
- tip(`Namebase for culture ${pack.cultures[culture].name} 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;
diff --git a/modules/routes-generator.js b/modules/routes-generator.js
index 816c3a6f..e331ce8b 100644
--- a/modules/routes-generator.js
+++ b/modules/routes-generator.js
@@ -64,43 +64,49 @@
const cells = pack.cells, allPorts = pack.burgs.filter(b => b.port > 0 && !b.removed);
if (allPorts.length < 2) return [];
const bodies = new Set(allPorts.map(b => b.port)); // features with ports
- let from = [], exit = null, path = [], paths = []; // array to store path segments
+ let paths = []; // array to store path segments
bodies.forEach(function(f) {
const ports = allPorts.filter(b => b.port === f);
if (ports.length < 2) return;
const first = ports[0].cell;
+ const farthest = ports[d3.scan(ports, (a, b) => ((b.y - ports[0].y) ** 2 + (b.x - ports[0].x) ** 2) - ((a.y - ports[0].y) ** 2 + (a.x - ports[0].x) ** 2))].cell;
// directly connect first port with the farthest one on the same island to remove gap
- if (pack.features[f].type !== "lake") {
+ void function() {
+ if (pack.features[f].type === "lake") return;
const portsOnIsland = ports.filter(b => cells.f[b.cell] === cells.f[first]);
- if (portsOnIsland.length > 3) {
- const opposite = ports[d3.scan(portsOnIsland, (a, b) => ((b.y - ports[0].y) ** 2 + (b.x - ports[0].x) ** 2) - ((a.y - ports[0].y) ** 2 + (a.x - ports[0].x) ** 2))].cell;
- //debug.append("circle").attr("r", 1).attr("fill", "blue").attr("cx", pack.cells.p[first][0]).attr("cy", pack.cells.p[first][1])
- //debug.append("circle").attr("r", 1).attr("fill", "green").attr("cx", pack.cells.p[opposite][0]).attr("cy", pack.cells.p[opposite][1])
- [from, exit] = findOceanPath(opposite, first);
- from[first] = cells.haven[first];
- path = restorePath(opposite, first, "ocean", from);
- paths = paths.concat(path);
- }
- }
+ if (portsOnIsland.length < 4) return;
+ const opposite = ports[d3.scan(portsOnIsland, (a, b) => ((b.y - ports[0].y) ** 2 + (b.x - ports[0].x) ** 2) - ((a.y - ports[0].y) ** 2 + (a.x - ports[0].x) ** 2))].cell;
+ //debug.append("circle").attr("cx", pack.cells.p[opposite][0]).attr("cy", pack.cells.p[opposite][1]).attr("r", 1);
+ //debug.append("circle").attr("cx", pack.cells.p[first][0]).attr("cy", pack.cells.p[first][1]).attr("fill", "red").attr("r", 1);
+ const [from, exit, passable] = findOceanPath(opposite, first);
+ if (!passable) return;
+ from[first] = cells.haven[first];
+ const path = restorePath(opposite, first, "ocean", from);
+ paths = paths.concat(path);
+ }()
// directly connect first port with the farthest one
- const farthest = ports[d3.scan(ports, (a, b) => ((b.y - ports[0].y) ** 2 + (b.x - ports[0].x) ** 2) - ((a.y - ports[0].y) ** 2 + (a.x - ports[0].x) ** 2))].cell;
- [from, exit] = findOceanPath(farthest, first);
- from[first] = cells.haven[first];
- path = restorePath(farthest, first, "ocean", from);
- paths = paths.concat(path);
+ void function() {
+ const [from, exit, passable] = findOceanPath(farthest, first);
+ if (!passable) return;
+ from[first] = cells.haven[first];
+ const path = restorePath(farthest, first, "ocean", from);
+ paths = paths.concat(path);
+ }()
// indirectly connect first port with all other ports
- if (ports.length < 3) return;
- for (const p of ports) {
- if (p.cell === first || p.cell === farthest) continue;
- [from, exit] = findOceanPath(p.cell, first, true);
- //from[exit] = cells.haven[exit];
- const path = restorePath(p.cell, exit, "ocean", from);
- paths = paths.concat(path);
- }
+ void function() {
+ if (ports.length < 3) return;
+ for (const p of ports) {
+ if (p.cell === first || p.cell === farthest) continue;
+ const [from, exit, passable] = findOceanPath(p.cell, first, true);
+ if (!passable) continue;
+ const path = restorePath(p.cell, exit, "ocean", from);
+ paths = paths.concat(path);
+ }
+ }()
});
@@ -173,9 +179,11 @@
for (const c of cells.c[n]) {
if (cells.h[c] < 20) continue; // ignore water cells
const stateChangeCost = cells.state && cells.state[c] !== cells.state[n] ? 400 : 0; // trails tend to lay within the same state
- const habitedCost = Math.max(100 - biomesData.habitability[cells.biome[c]], 0); // routes tend to lay within populated areas
+ const habitability = biomesData.habitability[cells.biome[c]];
+ const habitedCost = habitability ? Math.max(100 - habitability, 0) : 400; // routes tend to lay within populated areas
const heightChangeCost = Math.abs(cells.h[c] - cells.h[n]) * 10; // routes tend to avoid elevation changes
- const cellCoast = 10 + stateChangeCost + habitedCost + heightChangeCost;
+ const heightCost = cells.h[c] > 80 ? cells.h[c] : 0; // routes tend to avoid mountainous areas
+ const cellCoast = 10 + stateChangeCost + habitedCost + heightChangeCost + heightCost;
const totalCost = p + (cells.road[c] || cells.burg[c] ? cellCoast / 3 : cellCoast);
if (from[c] || totalCost >= cost[c]) continue;
@@ -234,22 +242,21 @@
while (queue.length) {
const next = queue.dequeue(), n = next.e, p = next.p;
- if (toRoute && n !== start && cells.road[n]) return [from, n];
+ if (toRoute && n !== start && cells.road[n]) return [from, n, true];
for (const c of cells.c[n]) {
+ if (c === exit) {from[c] = n; return [from, exit, true];}
if (cells.h[c] >= 20) continue; // ignore land cells
const dist2 = (cells.p[c][1] - cells.p[n][1]) ** 2 + (cells.p[c][0] - cells.p[n][0]) ** 2;
const totalCost = p + (cells.road[c] ? 1 + dist2 / 2 : dist2 + (cells.t[c] ? 1 : 100));
if (from[c] || totalCost >= cost[c]) continue;
- from[c] = n;
- if (c === exit) return [from, exit];
- cost[c] = totalCost;
+ from[c] = n, cost[c] = totalCost;
queue.queue({e: c, p: totalCost});
}
}
- return [from, exit];
+ return [from, exit, false];
}
})));
\ No newline at end of file
diff --git a/modules/ui/diplomacy-editor.js b/modules/ui/diplomacy-editor.js
index 69df11d8..4a109089 100644
--- a/modules/ui/diplomacy-editor.js
+++ b/modules/ui/diplomacy-editor.js
@@ -68,6 +68,7 @@ function editDiplomacy() {
const tipChange = `${tip}. Click to change relations to ${selName}`;
lines += `
+
${s.fullName}
`;
}
body.insertAdjacentHTML("beforeend", lines);
@@ -74,6 +81,8 @@ function overviewMilitary() {
// add listeners
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => stateHighlightOn(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => stateHighlightOff(ev)));
+
+ if (body.dataset.type === "percentage") {body.dataset.type = "absolute"; togglePercentageMode();}
applySorting(militaryHeader);
}
@@ -103,7 +112,9 @@ function overviewMilitary() {
function updateFooter() {
const lines = Array.from(body.querySelectorAll(":scope > div"));
const statesNumber = militaryFooterStates.innerHTML = pack.states.filter(s => s.i && !s.removed).length;
- militaryFooterForces.innerHTML = si(d3.sum(lines.map(el => el.dataset.total)) / statesNumber);
+ const total = d3.sum(lines.map(el => el.dataset.total));
+ militaryFooterForcesTotal.innerHTML = si(total);
+ militaryFooterForces.innerHTML = si(total / statesNumber);
militaryFooterRate.innerHTML = rn(d3.sum(lines.map(el => el.dataset.rate)) / statesNumber, 2) + "%";
militaryFooterAlert.innerHTML = rn(d3.sum(lines.map(el => el.dataset.alert)) / statesNumber, 2);
}
@@ -112,31 +123,51 @@ function overviewMilitary() {
if (!layerIsOn("toggleStates")) return;
const state = +event.target.dataset.id;
if (customization || !state) return;
- const path = regions.select("#state"+state).attr("d");
- debug.append("path").attr("class", "highlight").attr("d", path)
+ const d = regions.select("#state"+state).attr("d");
+
+ const path = debug.append("path").attr("class", "highlight").attr("d", d)
.attr("fill", "none").attr("stroke", "red").attr("stroke-width", 1).attr("opacity", 1)
- .attr("filter", "url(#blur1)").call(transition);
- }
+ .attr("filter", "url(#blur1)");
- function transition(path) {
- const duration = (path.node().getTotalLength() + 5000) / 2;
- path.transition().duration(duration).attrTween("stroke-dasharray", tweenDash);
- }
-
- function tweenDash() {
- const l = this.getTotalLength();
+ const l = path.node().getTotalLength(), dur = (l + 5000) / 2;
const i = d3.interpolateString("0," + l, l + "," + l);
- return t => i(t);
+ path.transition().duration(dur).attrTween("stroke-dasharray", function() {return t => i(t)});
+
+ armies.select("#army"+state).transition().duration(dur).style("fill", "#ff0000");
}
- function removePath(path) {
- path.transition().duration(1000).attr("opacity", 0).remove();
- }
-
- function stateHighlightOff() {
+ function stateHighlightOff(event) {
debug.selectAll(".highlight").each(function() {
- d3.select(this).call(removePath);
+ d3.select(this).transition().duration(1000).attr("opacity", 0).remove();
});
+
+ const state = +event.target.dataset.id;
+ armies.select("#army"+state).transition().duration(1000).style("fill", null);
+ }
+
+ function togglePercentageMode() {
+ if (body.dataset.type === "absolute") {
+ body.dataset.type = "percentage";
+ const lines = body.querySelectorAll(":scope > div");
+ const array = Array.from(lines), cache = [];
+
+ const total = function(type) {
+ if (cache[type]) cache[type];
+ cache[type] = d3.sum(array.map(el => +el.dataset[type]));
+ return cache[type];
+ }
+
+ lines.forEach(function(el) {
+ el.querySelectorAll("div").forEach(function(div) {
+ const type = div.dataset.type;
+ if (type === "rate") return;
+ div.textContent = rn(+el.dataset[type] / total(type) * 100) + "%";
+ });
+ });
+ } else {
+ body.dataset.type = "absolute";
+ addLines();
+ }
}
function militaryCustomize() {
diff --git a/modules/ui/options.js b/modules/ui/options.js
index bf9ae5c2..53a5a1ec 100644
--- a/modules/ui/options.js
+++ b/modules/ui/options.js
@@ -336,10 +336,10 @@ function randomizeOptions() {
// 'Options' settings
if (randomize || !locked("template")) randomizeHeightmapTemplate();
if (randomize || !locked("regions")) regionsInput.value = regionsOutput.value = gauss(15, 3, 2, 30);
- if (randomize || !locked("provinces")) provincesInput.value = provincesOutput.value = gauss(40, 20, 20, 100);
+ if (randomize || !locked("provinces")) provincesInput.value = provincesOutput.value = gauss(20, 10, 20, 100);
if (randomize || !locked("manors")) {manorsInput.value = 1000; manorsOutput.value = "auto";}
if (randomize || !locked("religions")) religionsInput.value = religionsOutput.value = gauss(5, 2, 2, 10);
- if (randomize || !locked("power")) powerInput.value = powerOutput.value = gauss(3, 2, 0, 10);
+ if (randomize || !locked("power")) powerInput.value = powerOutput.value = gauss(4, 2, 0, 10, 2);
if (randomize || !locked("neutral")) neutralInput.value = neutralOutput.value = rn(1 + Math.random(), 1);
if (randomize || !locked("cultures")) culturesInput.value = culturesOutput.value = gauss(12, 3, 5, 30);
if (randomize || !locked("culturesSet")) randomizeCultureSet();
diff --git a/modules/ui/regiments-overview.js b/modules/ui/regiments-overview.js
new file mode 100644
index 00000000..176bf642
--- /dev/null
+++ b/modules/ui/regiments-overview.js
@@ -0,0 +1,160 @@
+"use strict";
+function overviewRegiments(state) {
+ if (customization) return;
+ closeDialogs(".stable");
+
+ const body = document.getElementById("regimentsBody");
+ updateFilter();
+ addLines();
+ $("#regimentsOverview").dialog();
+
+ if (modules.overviewRegiments) return;
+ modules.overviewRegiments = true;
+ updateHeaders();
+
+ $("#regimentsOverview").dialog({
+ title: "Regiments Overview", resizable: false, width: fitContent(),
+ position: {my: "center", at: "center", of: "svg"}
+ });
+
+ // add listeners
+ document.getElementById("regimentsOverviewRefresh").addEventListener("click", addLines);
+ document.getElementById("regimentsPercentage").addEventListener("click", togglePercentageMode);
+ document.getElementById("regimentsAddNew").addEventListener("click", toggleAddRegiment);
+ document.getElementById("regimentsExport").addEventListener("click", downloadRegimentsData);
+ document.getElementById("regimentsFilter").addEventListener("change", filterRegiments);
+
+ body.addEventListener("click", function(ev) {
+ const el = ev.target, line = el.parentNode, state = +line.dataset.id;
+ //if (el.tagName === "SPAN") showRegimentList(state);
+ });
+
+ // update military types in header and tooltips
+ function updateHeaders() {
+ const header = document.getElementById("regimentsHeader");
+ header.querySelectorAll(".removable").forEach(el => el.remove());
+ const insert = html => document.getElementById("regimentsTotal").insertAdjacentHTML("beforebegin", html);
+ for (const u of options.military) {
+ const label = capitalize(u.name.replace(/_/g, ' '));
+ insert(`${label}
`);
+ }
+ header.querySelectorAll(".removable").forEach(function(e) {
+ e.addEventListener("click", function() {sortLines(this);});
+ });
+ }
+
+ // add line for each state
+ function addLines() {
+ body.innerHTML = "";
+ let lines = "";
+ const regiments = [];
+
+ for (const s of pack.states) {
+ if (!s.i || s.removed || !s.military.length) continue;
+ if (state !== -1 && s.i !== state) continue; // specific state is selected
+
+ 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 => `${r.u[u.name]||0}
`).join(" ");
+
+ lines += `
+
+
+
${r.icon}
+
+ ${lineData}
+
${r.a}
+
+
`;
+
+ regiments.push(r);
+ }
+ }
+
+ lines += `
+
Regiments: ${regiments.length}
+ ${options.military.map(u => `
${si(d3.sum(regiments.map(r => r.u[u.name]||0)))}
`).join(" ")}
+
${si(d3.sum(regiments.map(r => r.a)))}
+
`;
+
+ body.insertAdjacentHTML("beforeend", lines);
+ if (body.dataset.type === "percentage") {body.dataset.type = "absolute"; togglePercentageMode();}
+ applySorting(regimentsHeader);
+
+ // 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)));
+ }
+
+ function updateFilter() {
+ const filter = document.getElementById("regimentsFilter");
+ filter.options.length = 0; // remove all options
+ filter.options.add(new Option(`all`, -1, false, state === -1));
+ const statesSorted = pack.states.filter(s => s.i && !s.removed).sort((a, b) => (a.name > b.name) ? 1 : -1);
+ statesSorted.forEach(s => filter.options.add(new Option(s.name, s.i, false, s.i == state)));
+ }
+
+ function filterRegiments() {
+ state = +this.value;
+ addLines();
+ }
+
+ function regimentHighlightOn(event) {
+ const state = +event.target.dataset.s;
+ const id = +event.target.dataset.id;
+ if (customization || !state) return;
+ armies.select(`g > g#regiment${state}-${id}`).transition().duration(2000).style("fill", "#ff0000");
+ }
+
+ function regimentHighlightOff(event) {
+ const state = +event.target.dataset.s;
+ const id = +event.target.dataset.id;
+ armies.select(`g > g#regiment${state}-${id}`).transition().duration(1000).style("fill", null);
+ }
+
+ function togglePercentageMode() {
+ if (body.dataset.type === "absolute") {
+ body.dataset.type = "percentage";
+ const lines = body.querySelectorAll(":scope > div:not(.totalLine)");
+ const array = Array.from(lines), cache = [];
+
+ const total = function(type) {
+ if (cache[type]) cache[type];
+ cache[type] = d3.sum(array.map(el => +el.dataset[type]));
+ return cache[type];
+ }
+
+ lines.forEach(function(el) {
+ el.querySelectorAll("div").forEach(function(div) {
+ const type = div.dataset.type;
+ if (type === "rate") return;
+ div.textContent = rn(+el.dataset[type] / total(type) * 100) + "%";
+ });
+ });
+ } else {
+ body.dataset.type = "absolute";
+ addLines();
+ }
+ }
+
+ function toggleAddRegiment() {
+
+ }
+
+ function downloadRegimentsData() {
+ const units = options.military.map(u => u.name);
+ let data = "State,Id,Name,"+units.map(u => capitalize(u)).join(",")+",Total\n"; // headers
+
+ body.querySelectorAll(":scope > div:not(.totalLine)").forEach(function(el) {
+ data += el.dataset.state + ",";
+ data += el.dataset.id + ",";
+ data += el.dataset.name + ",";
+ data += units.map(u => el.dataset[u]).join(",") + ",";
+ data += el.dataset.total + "\n";
+ });
+
+ const name = getFileName("Regiments") + ".csv";
+ downloadFile(data, name);
+ }
+
+}
\ No newline at end of file
diff --git a/modules/ui/states-editor.js b/modules/ui/states-editor.js
index 61a53035..eb29adb3 100644
--- a/modules/ui/states-editor.js
+++ b/modules/ui/states-editor.js
@@ -173,30 +173,20 @@ function editStates() {
if (!layerIsOn("toggleStates")) return;
const state = +event.target.dataset.id;
if (customization || !state) return;
- const path = statesBody.select("#state"+state).attr("d");
- debug.append("path").attr("class", "highlight").attr("d", path)
+ const d = regions.select("#state"+state).attr("d");
+
+ const path = debug.append("path").attr("class", "highlight").attr("d", d)
.attr("fill", "none").attr("stroke", "red").attr("stroke-width", 1).attr("opacity", 1)
- .attr("filter", "url(#blur1)").call(transition);
- }
+ .attr("filter", "url(#blur1)");
- function transition(path) {
- const duration = (path.node().getTotalLength() + 5000) / 2;
- path.transition().duration(duration).attrTween("stroke-dasharray", tweenDash);
- }
-
- function tweenDash() {
- const l = this.getTotalLength();
+ const l = path.node().getTotalLength(), dur = (l + 5000) / 2;
const i = d3.interpolateString("0," + l, l + "," + l);
- return t => i(t);
- }
-
- function removePath(path) {
- path.transition().duration(1000).attr("opacity", 0).remove();
+ path.transition().duration(dur).attrTween("stroke-dasharray", function() {return t => i(t)});
}
- function stateHighlightOff() {
- debug.selectAll(".highlight").each(function(el) {
- d3.select(this).call(removePath);
+ function stateHighlightOff(event) {
+ debug.selectAll(".highlight").each(function() {
+ d3.select(this).transition().duration(1000).attr("opacity", 0).remove();
});
}