This commit is contained in:
Azgaar 2020-03-27 17:52:23 +03:00
parent cba011282d
commit c8f758ab3c
15 changed files with 397 additions and 135 deletions

11
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,11 @@
{
"version": "0.1.0",
"configurations": [
{
"name": "Debug",
"type": "chrome",
"request": "launch",
"file": "${workspaceFolder}/index.html"
}
]
}

View file

@ -1189,7 +1189,8 @@ div.states:hover {
} }
div.states > *, div.states > *,
div.states sup { div.states sup,
div.totalLine > div {
display: inline-block; display: inline-block;
} }
@ -1415,6 +1416,7 @@ div.states.Self {
border-color: #858b8e; border-color: #858b8e;
background-image: linear-gradient(to right, #f2f2f2 0%, #b0c6d9 100%); background-image: linear-gradient(to right, #f2f2f2 0%, #b0c6d9 100%);
font-style: italic; font-style: italic;
font-weight: bold;
margin-bottom: .2em; margin-bottom: .2em;
cursor: default !important; cursor: default !important;
} }
@ -1437,7 +1439,8 @@ rect.fillRect {
stroke-width: 2; stroke-width: 2;
} }
#militaryHeader > div { #militaryHeader > div,
#regimentsHeader > div {
width: 5.2em; width: 5.2em;
} }
@ -1446,7 +1449,8 @@ rect.fillRect {
} }
#militaryBody div.states > input, #militaryBody div.states > input,
#militaryBody div.states > div { #militaryBody div.states > div,
#regimentsBody div.states > div {
width: 5em; width: 5em;
} }

View file

@ -1960,7 +1960,7 @@
</div> </div>
<div> <div>
<i data-locked=0 id="lock_temperaturePole" class="icon-lock-open"></i> <i data-locked=0 id="lock_temperaturePole" class="icon-lock-open"></i>
<label data-tip="Set temperature at poles"> <label data-tip="Set temperature near poles">
<i>Poles:</i> <i>Poles:</i>
<input id="temperaturePoleInput" data-stored="temperaturePole" type="number" min="-30" max="30">°C = <input id="temperaturePoleInput" data-stored="temperaturePole" type="number" min="-30" max="30">°C =
<span id="temperaturePoleF"></span>°F <span id="temperaturePoleF"></span>°F
@ -2832,6 +2832,8 @@
<div style="left:12.4em" data-tip="Click to sort by diplomatical relations" class="sortable alphabetically" data-sortby="relations">Relations&nbsp;</div> <div style="left:12.4em" data-tip="Click to sort by diplomatical relations" class="sortable alphabetically" data-sortby="relations">Relations&nbsp;</div>
</div> </div>
<div id="diplomacyBodySection" class="table"></div>
<div id="diplomacySelect"> <div id="diplomacySelect">
<div data-tip="Ally means states formed a defensive pact and will protect each other in case of third party aggression">Ally</div> <div data-tip="Ally means states formed a defensive pact and will protect each other in case of third party aggression">Ally</div>
<div data-tip="State is friendly to anouther state when they share some common interests">Friendly</div> <div data-tip="State is friendly to anouther state when they share some common interests">Friendly</div>
@ -2844,8 +2846,6 @@
<div data-tip="Suzerain is a state having some control over its vassals">Suzerain</div> <div data-tip="Suzerain is a state having some control over its vassals">Suzerain</div>
</div> </div>
<div id="diplomacyBodySection" class="table"></div>
<div id="diplomacyBottom" style="margin-top: .1em"> <div id="diplomacyBottom" style="margin-top: .1em">
<button id="diplomacyEditorRefresh" data-tip="Refresh the Editor" class="icon-cw"></button> <button id="diplomacyEditorRefresh" data-tip="Refresh the Editor" class="icon-cw"></button>
<button id="diplomacyEditStyle" data-tip="Edit states (including diplomacy view) style in Style Editor" class="icon-adjust"></button> <button id="diplomacyEditStyle" data-tip="Edit states (including diplomacy view) style in Style Editor" class="icon-adjust"></button>
@ -3280,19 +3280,39 @@
<div id="militaryFooter" class="totalLine"> <div id="militaryFooter" class="totalLine">
<div data-tip="States number" style="margin-left: 4px">States:&nbsp;<span id="militaryFooterStates">0</span></div> <div data-tip="States number" style="margin-left: 4px">States:&nbsp;<span id="militaryFooterStates">0</span></div>
<div data-tip="Total military forces" style="margin-left: 14px">Total forces:&nbsp;<span id="militaryFooterForcesTotal">0</span></div>
<div data-tip="Average military forces per state" style="margin-left: 14px">Average forces:&nbsp;<span id="militaryFooterForces">0</span></div> <div data-tip="Average military forces per state" style="margin-left: 14px">Average forces:&nbsp;<span id="militaryFooterForces">0</span></div>
<div data-tip="Average forces rate per state" style="margin-left: 14px">Average rate:&nbsp;<span id="militaryFooterRate">0%</span></div> <div data-tip="Average forces rate per state" style="margin-left: 14px">Average rate:&nbsp;<span id="militaryFooterRate">0%</span></div>
<div data-tip="Average War Alert" style="margin-left: 14px">Average alert:&nbsp;<span id="militaryFooterAlert">0</span></div> <div data-tip="Average War Alert" style="margin-left: 14px">Average alert:&nbsp;<span id="militaryFooterAlert">0</span></div>
</div> </div>
<div id="militaryBottom"> <div id="militaryBottom">
<button id="militaryOverviewRefresh" data-tip="Refresh the Editor" class="icon-cw"></button> <button id="militaryOverviewRefresh" data-tip="Refresh the overview screen" class="icon-cw"></button>
<button id="militaryOptionsButton" data-tip="Edit Military units" class="icon-cog"></button> <button id="militaryOptionsButton" data-tip="Edit Military units" class="icon-cog"></button>
<button id="militaryPercentage" data-tip="Toggle percentage / absolute values views" class="icon-percent"></button>
<button id="militaryOverviewRecalculate" data-tip="Recalculate military forces based on current options" class="icon-retweet"></button> <button id="militaryOverviewRecalculate" data-tip="Recalculate military forces based on current options" class="icon-retweet"></button>
<button id="militaryExport" data-tip="Save military-related data as a text file (.csv)" class="icon-download"></button> <button id="militaryExport" data-tip="Save military-related data as a text file (.csv)" class="icon-download"></button>
</div> </div>
</div> </div>
<div id="regimentsOverview" class="dialog stable" style="display: none">
<div id="regimentsHeader" class="header">
<div data-tip="State name. Click to sort" style="left:1.8em; width: 9em" class="sortable alphabetically" data-sortby="state">State&nbsp;</div>
<div data-tip="Regiment emblem and name. Click to sort by name" style="width: 12em" class="sortable alphabetically" data-sortby="name">Name&nbsp;</div>
<div data-tip="Total military personnel (not considering crew). Click to sort" style="margin-left: .8em" id="regimentsTotal" class="sortable icon-sort-number-down" data-sortby="total">Total&nbsp;</div>
</div>
<div id="regimentsBody" class="table" data-type="absolute"></div>
<div id="regimentsBottom">
<button id="regimentsOverviewRefresh" data-tip="Refresh the overview screen" class="icon-cw"></button>
<button id="regimentsPercentage" data-tip="Toggle percentage / absolute values views" class="icon-percent"></button>
<button id="regimentsAddNew" data-tip="Add new Regiment" class="icon-user-plus"></button>
<button id="regimentsExport" data-tip="Save military-related data as a text file (.csv)" class="icon-download"></button>
<div data-tip="Select state" style="display:inline-block"><span>State: </span><select id="regimentsFilter"></select></div>
</div>
</div>
<div id="militaryOptions" class="dialog stable" style="display: none"> <div id="militaryOptions" class="dialog stable" style="display: none">
<table id="militaryOptionsTable"> <table id="militaryOptionsTable">
<thead> <thead>
@ -3519,6 +3539,7 @@
<script defer src="modules/ui/burgs-overview.js"></script> <script defer src="modules/ui/burgs-overview.js"></script>
<script defer src="modules/ui/rivers-overview.js"></script> <script defer src="modules/ui/rivers-overview.js"></script>
<script defer src="modules/ui/military-overview.js"></script> <script defer src="modules/ui/military-overview.js"></script>
<script defer src="modules/ui/regiments-overview.js"></script>
<script defer src="modules/ui/regiment-editor.js"></script> <script defer src="modules/ui/regiment-editor.js"></script>
<script defer src="modules/ui/editors.js"></script> <script defer src="modules/ui/editors.js"></script>
<script defer src="modules/ui/3d.js"></script> <script defer src="modules/ui/3d.js"></script>

40
main.js
View file

@ -308,9 +308,8 @@ function applyDefaultBiomesSystem() {
const habitability = [0,4,10,22,30,50,100,80,90,12,4,0,12]; 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 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 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 cost = [10,200,150,60,50,70,70,80,90,200,1000,5000,150]; // biome movement cost
const biomesMartix = [ const biomesMartix = [ // hot ↔ cold; dry ↕ wet
// 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([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([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]), 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 tEq = +temperatureEquatorInput.value;
const tPole = +temperaturePoleInput.value; const tPole = +temperaturePoleInput.value;
const tDelta = tEq - tPole; const tDelta = tEq - tPole;
const int = d3.easePolyInOut.exponent(.5); // interpolation function
d3.range(0, cells.i.length, grid.cellsX).forEach(function(r) { d3.range(0, cells.i.length, grid.cellsX).forEach(function(r) {
const y = grid.points[r][1]; const y = grid.points[r][1];
const lat = Math.abs(mapCoordinates.latN - y / graphHeight * mapCoordinates.latT); const lat = Math.abs(mapCoordinates.latN - y / graphHeight * mapCoordinates.latT); // [0; 90]
const initTemp = tEq - lat / 90 * tDelta; const initTemp = tEq - int(lat / 90) * tDelta;
for (let i = r; i < r+grid.cellsX; i++) { for (let i = r; i < r+grid.cellsX; i++) {
const temp = initTemp - convertToFriendly(cells.h[i]); cells.temp[i] = Math.max(Math.min(initTemp - convertToFriendly(cells.h[i]), 127), -128);
cells.temp[i] = Math.max(Math.min(temp, 127), -128);
} }
}); });
@ -1036,7 +1035,7 @@ function drawCoastline() {
// Re-mark features (ocean, lakes, islands) // Re-mark features (ocean, lakes, islands)
function reMarkFeatures() { function reMarkFeatures() {
console.time("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.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.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); 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 const start = queue[0]; // first cell
cells.f[start] = i; // assign feature number cells.f[start] = i; // assign feature number
const land = cells.h[start] >= 20; 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 border = false; // true if feature touches map border
let cellNumber = 1; // to count cells number in a feature 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; 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; else if (!cells.t[q] && cells.t[e] === 1) cells.t[q] = 2;
} }
if (land === eLand && cells.f[e] === 0) { if (!cells.f[e] && land === eLand) {
cells.f[e] = i; //if (!land && frozen !== temp[cells.g[e]] < -5) return;
queue.push(e); queue.push(e);
cells.f[e] = i;
cellNumber++; cellNumber++;
} }
}); });
@ -1073,15 +1074,14 @@ function reMarkFeatures() {
const type = land ? "island" : border ? "ocean" : "lake"; const type = land ? "island" : border ? "ocean" : "lake";
let group; let group;
if (type === "lake") group = defineLakeGroup(start, cellNumber); if (type === "lake") group = defineLakeGroup(start, cellNumber, temp[cells.g[start]]);
else if (type === "ocean") group = "ocean"; else if (type === "ocean") group = defineOceanGroup(cellNumber);
else if (type === "island") group = defineIslandGroup(start, cellNumber); else if (type === "island") group = defineIslandGroup(start, cellNumber);
features.push({i, land, border, type, cells: cellNumber, firstCell: start, group, ports:0}); features.push({i, land, border, type, cells: cellNumber, firstCell: start, group, ports:0});
queue[0] = cells.f.findIndex(f => !f); // find unmarked cell queue[0] = cells.f.findIndex(f => !f); // find unmarked cell
} }
function defineLakeGroup(cell, number) { function defineLakeGroup(cell, number, temp) {
const temp = grid.cells.temp[cells.g[cell]];
if (temp > 24) return "salt"; if (temp > 24) return "salt";
if (temp < -3) return "frozen"; if (temp < -3) return "frozen";
const height = d3.max(cells.c[cell].map(c => cells.h[c])); const height = d3.max(cells.c[cell].map(c => cells.h[c]));
@ -1090,6 +1090,12 @@ function reMarkFeatures() {
return "freshwater"; 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) { function defineIslandGroup(cell, number) {
if (cell && features[cells.f[cell-1]].type === "lake") return "lake_island"; if (cell && features[cells.f[cell-1]].type === "lake") return "lake_island";
if (number > grid.cells.i.length / 10) return "continent"; if (number > grid.cells.i.length / 10) return "continent";
@ -1124,12 +1130,13 @@ function defineBiomes() {
for (const i of cells.i) { for (const i of cells.i) {
if (f[cells.f[i]].group === "freshwater") cells.h[i] = 19; // de-elevate lakes 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]]; let moist = grid.cells.prec[cells.g[i]];
if (cells.r[i]) moist += Math.max(cells.fl[i] / 20, 2); 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]); const n = cells.c[i].filter(isLand).map(c => grid.cells.prec[cells.g[c]]).concat([moist]);
moist = rn(4 + d3.mean(n)); 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]); cells.biome[i] = getBiomeId(moist, temp, cells.h[i]);
} }
@ -1138,6 +1145,7 @@ function defineBiomes() {
function getBiomeId(moisture, temperature, height) { function getBiomeId(moisture, temperature, height) {
if (temperature < -5) return 11; // permafrost biome 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 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 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 const t = Math.min(Math.max(20 - temperature, 0), 25); // temparature band from 0 to 25

View file

@ -137,22 +137,24 @@
} }
} }
// define burg coordinates and define details // define burg coordinates, port status and define details
const specifyBurgs = function() { const specifyBurgs = function() {
console.time("specifyBurgs"); 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) { for (const b of pack.burgs) {
if (!b.i) continue; if (!b.i) continue;
const i = b.cell; const i = b.cell;
// asign port status // asign port status
if (cells.haven[i]) { const haven = cells.haven[i];
const f = cells.f[cells.haven[i]]; // water body id 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 // 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 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; } else b.port = 0;
// define burg population (keep urbanization at about 10% rate) // 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 // 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; if (!f.i || f.land || f.ports !== 1) continue;
const port = pack.burgs.find(b => b.port === f.i); const port = pack.burgs.find(b => b.port === f.i);
port.port = 0; port.port = 0;
f.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"); console.timeEnd("specifyBurgs");
@ -274,16 +307,19 @@
while (queue.length) { while (queue.length) {
const next = queue.dequeue(), n = next.e, p = next.p, s = next.s, b = next.b; const next = queue.dequeue(), n = next.e, p = next.p, s = next.s, b = next.b;
const type = states[s].type; const type = states[s].type;
const culture = states[s].culture;
cells.c[n].forEach(function(e) { cells.c[n].forEach(function(e) {
if (cells.state[e] && e === states[cells.state[e]].center) return; // do not overwrite capital cells 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 biomeCost = getBiomeCost(b, cells.biome[e], type);
const heightCost = getHeightCost(pack.features[cells.f[e]], cells.h[e], type); const heightCost = getHeightCost(pack.features[cells.f[e]], cells.h[e], type);
const riverCost = getRiverCost(cells.r[e], e, type); const riverCost = getRiverCost(cells.r[e], e, type);
const typeCost = getTypeCost(cells.t[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; if (totalCost > neutral) return;
@ -325,7 +361,7 @@
function getTypeCost(t, type) { function getTypeCost(t, type) {
if (t === 1) return type === "Naval" || type === "Lake" ? 0 : type === "Nomadic" ? 60 : 20; // penalty for coastline 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 === 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; return 0;
} }

View file

@ -153,6 +153,7 @@
}); });
function createRegiments(nodes, s) { function createRegiments(nodes, s) {
if (!nodes.length) return [];
nodes.sort((a,b) => a.a - b.a); nodes.sort((a,b) => a.a - b.a);
const tree = d3.quadtree(nodes, d => d.x, d => d.y); const tree = d3.quadtree(nodes, d => d.x, d => d.y);
nodes.forEach(n => { nodes.forEach(n => {

View file

@ -76,6 +76,9 @@
} }
// parse word to get a final name // 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) { let name = [...w].reduce(function(r, c, i, d) {
if (c === d[i+1] && !dupl.includes(c)) return r; // duplication is not allowed if (c === d[i+1] && !dupl.includes(c)) return r; // duplication is not allowed
if (!r.length) return c.toUpperCase(); 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 space
if (r.slice(-1) === "-") return r + c.toUpperCase(); // capitalize letter after hyphen 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 === "a" && d[i+1] === "e") return r; // "ae" => "e"
if (c === " " && i+1 === d.length) return r; 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 && !vowel(c) && !vowel(d[i+1]) && !vowel(d[i+2])) return r; // remove consonant before 2 consonants
if (i+2 < d.length && c === d[i+1] && c === d[i+2]) return r; // remove tree same letters in a row 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; return r + c;
}, ""); }, "");
@ -96,6 +98,7 @@
console.error("Name is too short! Random name to be selected"); console.error("Name is too short! Random name to be selected");
name = ra(nameBases[base].b.split(",")); name = ra(nameBases[base].b.split(","));
} }
return name; return name;
} }
@ -115,8 +118,7 @@
// generate short name for base // generate short name for base
const getBaseShort = function(base) { const getBaseShort = function(base) {
if (nameBases[base] === undefined) { if (nameBases[base] === undefined) {
tip(`Namebase for culture ${pack.cultures[culture].name} does not exist. tip(`Namebase ${base} does not exist. Please upload custom namebases of change the base in Cultures Editor`, false, "error");
Please upload custom namebases of change the base in Cultures Editor`, false, "error");
base = 1; base = 1;
} }
const min = nameBases[base].min-1; const min = nameBases[base].min-1;

View file

@ -64,43 +64,49 @@
const cells = pack.cells, allPorts = pack.burgs.filter(b => b.port > 0 && !b.removed); const cells = pack.cells, allPorts = pack.burgs.filter(b => b.port > 0 && !b.removed);
if (allPorts.length < 2) return []; if (allPorts.length < 2) return [];
const bodies = new Set(allPorts.map(b => b.port)); // features with ports 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) { bodies.forEach(function(f) {
const ports = allPorts.filter(b => b.port === f); const ports = allPorts.filter(b => b.port === f);
if (ports.length < 2) return; if (ports.length < 2) return;
const first = ports[0].cell; 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 // 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]); const portsOnIsland = ports.filter(b => cells.f[b.cell] === cells.f[first]);
if (portsOnIsland.length > 3) { 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; 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("cx", pack.cells.p[opposite][0]).attr("cy", pack.cells.p[opposite][1]).attr("r", 1);
//debug.append("circle").attr("r", 1).attr("fill", "green").attr("cx", pack.cells.p[opposite][0]).attr("cy", pack.cells.p[opposite][1]) //debug.append("circle").attr("cx", pack.cells.p[first][0]).attr("cy", pack.cells.p[first][1]).attr("fill", "red").attr("r", 1);
[from, exit] = findOceanPath(opposite, first); const [from, exit, passable] = findOceanPath(opposite, first);
from[first] = cells.haven[first]; if (!passable) return;
path = restorePath(opposite, first, "ocean", from); from[first] = cells.haven[first];
paths = paths.concat(path); const path = restorePath(opposite, first, "ocean", from);
} paths = paths.concat(path);
} }()
// directly connect first port with the farthest one // 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; void function() {
[from, exit] = findOceanPath(farthest, first); const [from, exit, passable] = findOceanPath(farthest, first);
from[first] = cells.haven[first]; if (!passable) return;
path = restorePath(farthest, first, "ocean", from); from[first] = cells.haven[first];
paths = paths.concat(path); const path = restorePath(farthest, first, "ocean", from);
paths = paths.concat(path);
}()
// indirectly connect first port with all other ports // indirectly connect first port with all other ports
if (ports.length < 3) return; void function() {
for (const p of ports) { if (ports.length < 3) return;
if (p.cell === first || p.cell === farthest) continue; for (const p of ports) {
[from, exit] = findOceanPath(p.cell, first, true); if (p.cell === first || p.cell === farthest) continue;
//from[exit] = cells.haven[exit]; const [from, exit, passable] = findOceanPath(p.cell, first, true);
const path = restorePath(p.cell, exit, "ocean", from); if (!passable) continue;
paths = paths.concat(path); const path = restorePath(p.cell, exit, "ocean", from);
} paths = paths.concat(path);
}
}()
}); });
@ -173,9 +179,11 @@
for (const c of cells.c[n]) { for (const c of cells.c[n]) {
if (cells.h[c] < 20) continue; // ignore water cells 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 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 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); const totalCost = p + (cells.road[c] || cells.burg[c] ? cellCoast / 3 : cellCoast);
if (from[c] || totalCost >= cost[c]) continue; if (from[c] || totalCost >= cost[c]) continue;
@ -234,22 +242,21 @@
while (queue.length) { while (queue.length) {
const next = queue.dequeue(), n = next.e, p = next.p; 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]) { 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 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 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)); const totalCost = p + (cells.road[c] ? 1 + dist2 / 2 : dist2 + (cells.t[c] ? 1 : 100));
if (from[c] || totalCost >= cost[c]) continue; if (from[c] || totalCost >= cost[c]) continue;
from[c] = n; from[c] = n, cost[c] = totalCost;
if (c === exit) return [from, exit];
cost[c] = totalCost;
queue.queue({e: c, p: totalCost}); queue.queue({e: c, p: totalCost});
} }
} }
return [from, exit]; return [from, exit, false];
} }
}))); })));

View file

@ -68,6 +68,7 @@ function editDiplomacy() {
const tipChange = `${tip}. Click to change relations to ${selName}`; const tipChange = `${tip}. Click to change relations to ${selName}`;
lines += `<div class="states" data-id=${s.i} data-name="${s.fullName}" data-relations="${relation}"> lines += `<div class="states" data-id=${s.i} data-name="${s.fullName}" data-relations="${relation}">
<span data-tip="${tipSelect}" class="icon-right-open"></span>
<div data-tip="${tipSelect}" style="width:12em">${s.fullName}</div> <div data-tip="${tipSelect}" style="width:12em">${s.fullName}</div>
<svg data-tip="${tipChange}" width=".9em" height=".9em" style="margin-bottom:-1px" class="changeRelations"> <svg data-tip="${tipChange}" width=".9em" height=".9em" style="margin-bottom:-1px" class="changeRelations">
<rect x="0" y="0" width="100%" height="100%" fill="${color}" class="fillRect pointer" style="pointer-events: none"></rect> <rect x="0" y="0" width="100%" height="100%" fill="${color}" class="fillRect pointer" style="pointer-events: none"></rect>
@ -91,30 +92,20 @@ function editDiplomacy() {
if (!layerIsOn("toggleStates")) return; if (!layerIsOn("toggleStates")) return;
const state = +event.target.dataset.id; const state = +event.target.dataset.id;
if (customization || !state) return; if (customization || !state) return;
const path = regions.select("#state"+state).attr("d"); const d = regions.select("#state"+state).attr("d");
debug.append("path").attr("class", "highlight").attr("d", path)
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("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 l = path.node().getTotalLength(), dur = (l + 5000) / 2;
const duration = (path.node().getTotalLength() + 5000) / 2;
path.transition().duration(duration).attrTween("stroke-dasharray", tweenDash);
}
function tweenDash() {
const l = this.getTotalLength();
const i = d3.interpolateString("0," + l, l + "," + l); const i = d3.interpolateString("0," + l, l + "," + l);
return t => i(t); path.transition().duration(dur).attrTween("stroke-dasharray", function() {return t => i(t)});
}
function removePath(path) {
path.transition().duration(1000).attr("opacity", 0).remove();
} }
function stateHighlightOff() { function stateHighlightOff(event) {
debug.selectAll(".highlight").each(function(el) { debug.selectAll(".highlight").each(function() {
d3.select(this).call(removePath); d3.select(this).transition().duration(1000).attr("opacity", 0).remove();
}); });
} }

View file

@ -311,7 +311,7 @@ function editHeightmap() {
const land = pack.cells.h[i] >= 20; const land = pack.cells.h[i] >= 20;
// check biome // check biome
if (land && !biome[g]) pack.cells.biome[i] = getBiomeId(grid.cells.prec[g], grid.cells.temp[g]); if (!biome[g]) pack.cells.biome[i] = getBiomeId(grid.cells.prec[g], grid.cells.temp[g]);
else if (!land && biome[g]) pack.cells.biome[i] = 0; else if (!land && biome[g]) pack.cells.biome[i] = 0;
else pack.cells.biome[i] = biome[g]; else pack.cells.biome[i] = biome[g];

View file

@ -334,9 +334,9 @@ function drawBiomes() {
const cells = pack.cells, vertices = pack.vertices, n = cells.i.length; const cells = pack.cells, vertices = pack.vertices, n = cells.i.length;
const used = new Uint8Array(cells.i.length); const used = new Uint8Array(cells.i.length);
const paths = new Array(biomesData.i.length).fill(""); const paths = new Array(biomesData.i.length).fill("");
for (const i of cells.i) { for (const i of cells.i) {
if (!cells.biome[i]) continue; // no need to mark water if (!cells.biome[i]) continue; // no need to mark marine biome (liquid water)
if (used[i]) continue; // already marked if (used[i]) continue; // already marked
const b = cells.biome[i]; const b = cells.biome[i];
const onborder = cells.c[i].some(n => cells.biome[n] !== b); const onborder = cells.c[i].some(n => cells.biome[n] !== b);

View file

@ -20,6 +20,7 @@ function overviewMilitary() {
// add listeners // add listeners
document.getElementById("militaryOverviewRefresh").addEventListener("click", addLines); document.getElementById("militaryOverviewRefresh").addEventListener("click", addLines);
document.getElementById("militaryPercentage").addEventListener("click", togglePercentageMode);
document.getElementById("militaryOptionsButton").addEventListener("click", militaryCustomize); document.getElementById("militaryOptionsButton").addEventListener("click", militaryCustomize);
document.getElementById("militaryOverviewRecalculate").addEventListener("click", militaryRecalculate); document.getElementById("militaryOverviewRecalculate").addEventListener("click", militaryRecalculate);
document.getElementById("militaryExport").addEventListener("click", downloadMilitaryData); document.getElementById("militaryExport").addEventListener("click", downloadMilitaryData);
@ -29,6 +30,11 @@ function overviewMilitary() {
changeAlert(state, line, +el.value); changeAlert(state, line, +el.value);
}); });
body.addEventListener("click", function(ev) {
const el = ev.target, line = el.parentNode, state = +line.dataset.id;
if (el.tagName === "SPAN") overviewRegiments(state);
});
// update military types in header and tooltips // update military types in header and tooltips
function updateHeaders() { function updateHeaders() {
const header = document.getElementById("militaryHeader"); const header = document.getElementById("militaryHeader");
@ -62,10 +68,11 @@ function overviewMilitary() {
<svg data-tip="${s.fullName}" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${s.color}" class="fillRect"></svg> <svg data-tip="${s.fullName}" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${s.color}" class="fillRect"></svg>
<input data-tip="${s.fullName}" style="width:6em" value="${s.name}" readonly> <input data-tip="${s.fullName}" style="width:6em" value="${s.name}" readonly>
${lineData} ${lineData}
<div data-type="total" data-tip="Total state military personnel (considering crew)"><b>${si(total)}</b></div> <div data-type="total" data-tip="Total state military personnel (considering crew)" style="font-weight: bold">${si(total)}</div>
<div data-tip="State population">${si(population)}</div> <div data-type="population" data-tip="State population">${si(population)}</div>
<div data-type="rate" data-tip="Military personnel rate (% of state population). Depends on war alert">${rn(rate, 2)}%</div> <div data-type="rate" data-tip="Military personnel rate (% of state population). Depends on war alert">${rn(rate, 2)}%</div>
<input data-type="alert" data-tip="War Alert. Editable modifier to military forces number, depends of political situation" type="number" min=0 step=.01 value="${rn(s.alert, 2)}"> <input data-tip="War Alert. Editable modifier to military forces number, depends of political situation" style="width:4.1em" type="number" min=0 step=.01 value="${rn(s.alert, 2)}">
<span data-tip="Show regiments list" class="icon-list-bullet pointer"></span>
</div>`; </div>`;
} }
body.insertAdjacentHTML("beforeend", lines); body.insertAdjacentHTML("beforeend", lines);
@ -74,6 +81,8 @@ function overviewMilitary() {
// add listeners // add listeners
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => stateHighlightOn(ev))); body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => stateHighlightOn(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => stateHighlightOff(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); applySorting(militaryHeader);
} }
@ -103,7 +112,9 @@ function overviewMilitary() {
function updateFooter() { function updateFooter() {
const lines = Array.from(body.querySelectorAll(":scope > div")); const lines = Array.from(body.querySelectorAll(":scope > div"));
const statesNumber = militaryFooterStates.innerHTML = pack.states.filter(s => s.i && !s.removed).length; 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) + "%"; 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); militaryFooterAlert.innerHTML = rn(d3.sum(lines.map(el => el.dataset.alert)) / statesNumber, 2);
} }
@ -112,31 +123,51 @@ function overviewMilitary() {
if (!layerIsOn("toggleStates")) return; if (!layerIsOn("toggleStates")) return;
const state = +event.target.dataset.id; const state = +event.target.dataset.id;
if (customization || !state) return; if (customization || !state) return;
const path = regions.select("#state"+state).attr("d"); const d = regions.select("#state"+state).attr("d");
debug.append("path").attr("class", "highlight").attr("d", path)
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("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 l = path.node().getTotalLength(), dur = (l + 5000) / 2;
const duration = (path.node().getTotalLength() + 5000) / 2;
path.transition().duration(duration).attrTween("stroke-dasharray", tweenDash);
}
function tweenDash() {
const l = this.getTotalLength();
const i = d3.interpolateString("0," + l, l + "," + l); 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) { function stateHighlightOff(event) {
path.transition().duration(1000).attr("opacity", 0).remove();
}
function stateHighlightOff() {
debug.selectAll(".highlight").each(function() { 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() { function militaryCustomize() {

View file

@ -336,10 +336,10 @@ function randomizeOptions() {
// 'Options' settings // 'Options' settings
if (randomize || !locked("template")) randomizeHeightmapTemplate(); if (randomize || !locked("template")) randomizeHeightmapTemplate();
if (randomize || !locked("regions")) regionsInput.value = regionsOutput.value = gauss(15, 3, 2, 30); 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("manors")) {manorsInput.value = 1000; manorsOutput.value = "auto";}
if (randomize || !locked("religions")) religionsInput.value = religionsOutput.value = gauss(5, 2, 2, 10); 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("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("cultures")) culturesInput.value = culturesOutput.value = gauss(12, 3, 5, 30);
if (randomize || !locked("culturesSet")) randomizeCultureSet(); if (randomize || !locked("culturesSet")) randomizeCultureSet();

View file

@ -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(`<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() {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 => `<div data-type="${u.name}" data-tip="${capitalize(u.name)} units number">${r.u[u.name]||0}</div>`).join(" ");
lines += `<div class="states" data-id=${r.i} data-s="${s.i}" data-state="${s.name}" data-name="${r.name}" ${sortData} data-total="${r.a}">
<svg data-tip="${s.fullName}" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${s.color}" class="fillRect"></svg>
<input data-tip="${s.fullName}" style="width:6em" value="${s.name}" readonly>
<span data-tip="Regiment's emblem" style="width:1em">${r.icon}</span>
<input data-tip="Regiment's name" style="width:13em" value="${r.name}" readonly>
${lineData}
<div data-type="total" data-tip="Total military personnel (not considering crew)" style="font-weight: bold">${r.a}</div>
<span data-tip="Edit regiment" class="icon-pencil pointer"></span>
</div>`;
regiments.push(r);
}
}
lines += `<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(" ")}
<div style="width:5em">${si(d3.sum(regiments.map(r => r.a)))}</div>
</div>`;
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);
}
}

View file

@ -173,30 +173,20 @@ function editStates() {
if (!layerIsOn("toggleStates")) return; if (!layerIsOn("toggleStates")) return;
const state = +event.target.dataset.id; const state = +event.target.dataset.id;
if (customization || !state) return; if (customization || !state) return;
const path = statesBody.select("#state"+state).attr("d"); const d = regions.select("#state"+state).attr("d");
debug.append("path").attr("class", "highlight").attr("d", path)
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("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 l = path.node().getTotalLength(), dur = (l + 5000) / 2;
const duration = (path.node().getTotalLength() + 5000) / 2;
path.transition().duration(duration).attrTween("stroke-dasharray", tweenDash);
}
function tweenDash() {
const l = this.getTotalLength();
const i = d3.interpolateString("0," + l, l + "," + l); const i = d3.interpolateString("0," + l, l + "," + l);
return t => i(t); path.transition().duration(dur).attrTween("stroke-dasharray", function() {return t => i(t)});
}
function removePath(path) {
path.transition().duration(1000).attr("opacity", 0).remove();
} }
function stateHighlightOff() { function stateHighlightOff(event) {
debug.selectAll(".highlight").each(function(el) { debug.selectAll(".highlight").each(function() {
d3.select(this).call(removePath); d3.select(this).transition().duration(1000).attr("opacity", 0).remove();
}); });
} }