mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-17 09:41:24 +01:00
v1.3.03a
This commit is contained in:
parent
cba011282d
commit
c8f758ab3c
15 changed files with 397 additions and 135 deletions
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
|
||||
})));
|
||||
|
|
@ -68,6 +68,7 @@ function editDiplomacy() {
|
|||
const tipChange = `${tip}. Click to change relations to ${selName}`;
|
||||
|
||||
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>
|
||||
<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>
|
||||
|
|
@ -91,30 +92,20 @@ function editDiplomacy() {
|
|||
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);
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -311,7 +311,7 @@ function editHeightmap() {
|
|||
const land = pack.cells.h[i] >= 20;
|
||||
|
||||
// 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 pack.cells.biome[i] = biome[g];
|
||||
|
||||
|
|
|
|||
|
|
@ -334,9 +334,9 @@ function drawBiomes() {
|
|||
const cells = pack.cells, vertices = pack.vertices, n = cells.i.length;
|
||||
const used = new Uint8Array(cells.i.length);
|
||||
const paths = new Array(biomesData.i.length).fill("");
|
||||
|
||||
|
||||
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
|
||||
const b = cells.biome[i];
|
||||
const onborder = cells.c[i].some(n => cells.biome[n] !== b);
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ function overviewMilitary() {
|
|||
|
||||
// add listeners
|
||||
document.getElementById("militaryOverviewRefresh").addEventListener("click", addLines);
|
||||
document.getElementById("militaryPercentage").addEventListener("click", togglePercentageMode);
|
||||
document.getElementById("militaryOptionsButton").addEventListener("click", militaryCustomize);
|
||||
document.getElementById("militaryOverviewRecalculate").addEventListener("click", militaryRecalculate);
|
||||
document.getElementById("militaryExport").addEventListener("click", downloadMilitaryData);
|
||||
|
|
@ -29,6 +30,11 @@ function overviewMilitary() {
|
|||
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
|
||||
function updateHeaders() {
|
||||
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>
|
||||
<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)"><b>${si(total)}</b></div>
|
||||
<div data-tip="State population">${si(population)}</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>
|
||||
<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>`;
|
||||
}
|
||||
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() {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
160
modules/ui/regiments-overview.js
Normal file
160
modules/ui/regiments-overview.js
Normal 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} </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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue