This commit is contained in:
Azgaar 2020-03-15 17:35:07 +03:00
parent 7c74c3d29f
commit f4a84fc6d6
22 changed files with 1678 additions and 325 deletions

View file

@ -26,6 +26,7 @@
collectStatistics();
assignColors();
generateCampaigns();
generateDiplomacy();
Routes.draw(capitalRoutes, townRoutes, oceanRoutes);
drawBurgs();
@ -145,15 +146,20 @@
if (!b.i) continue;
const i = b.cell;
// asign port status: capital with any harbor and towns with good harbors
const port = (b.capital && cells.harbor[i]) || cells.harbor[i] === 1;
b.port = port ? cells.f[cells.haven[i]] : 0; // port is defined by feature id it lays on
// asign port status
if (cells.haven[i]) {
const f = cells.f[cells.haven[i]]; // 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);
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;}
} else b.port = 0;
// define burg population (keep urbanization at about 10% rate)
b.population = rn(Math.max((cells.s[i] + cells.road[i]) / 8 + b.i / 1000 + i % 100 / 1000, .1), 3);
if (b.capital) b.population = rn(b.population * 1.3, 3); // increase capital population
if (port) {
if (b.port) {
b.population = b.population * 1.3; // increase port population
const e = cells.v[i].filter(v => vertices.c[v].some(c => c === cells.haven[i])); // vertices of common edge
b.x = rn((vertices.p[e[0]][0] + vertices.p[e[1]][0]) / 2, 2);
@ -164,7 +170,7 @@
b.population = rn(b.population * gauss(2,3,.6,20,3), 3);
// shift burgs on rivers semi-randomly and just a bit
if (!port && cells.r[i]) {
if (!b.port && cells.r[i]) {
const shift = Math.min(cells.fl[i]/150, 1);
if (i%2) b.x = rn(b.x + shift, 2); else b.x = rn(b.x - shift, 2);
if (cells.r[i]%2) b.y = rn(b.y + shift, 2); else b.y = rn(b.y - shift, 2);
@ -173,11 +179,11 @@
// de-assign port status if it's the only one on feature
for (const f of pack.features) {
if (!f.i || f.land) continue;
const onFeature = pack.burgs.filter(b => b.port === f.i);
if (onFeature.length === 1) {
onFeature[0].port = 0;
}
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;
}
console.timeEnd("specifyBurgs");
@ -589,6 +595,20 @@
console.timeEnd("assignColors");
}
// generate historical wars
const generateCampaigns = function() {
const wars = {"War":4, "Conflict":2, "Campaign":4, "Invasion":2, "Rebellion":2, "Conquest":2, "Intervention":1, "Expedition":1, "Crusade":1};
pack.states.forEach(s => {
if (!s.i || s.removed) return;
s.campaigns = (s.neighbors||[0]).map(i => {
const name = i && P(.8) ? pack.states[i].name : Names.getCultureShort(s.culture);
const start = gauss(options.year-100, 150, 1, options.year-6), end = start + gauss(4, 5, 1, options.year - start - 1);
return {name:getAdjective(name) + " " + rw(wars), start, end};
}).sort((a, b) => a.start - b.start);
});
}
// generate Diplomatic Relationships
const generateDiplomacy = function() {
console.time("generateDiplomacy");
@ -666,6 +686,9 @@
// start a war
const war = [`${an}-${trimVowels(dn)}ian War`,`${an} declared a war on its rival ${dn}`];
const start = options.year - gauss(2, 2, 0, 5);
states[attacker].campaigns.push({name: `${trimVowels(dn)}ian War`, start, end:options.year});
states[defender].campaigns.push({name: `${trimVowels(an)}ian War`, start, end:options.year});
// attacker vassals join the war
ad.forEach((r, d) => {if (r === "Suzerain") {
@ -997,6 +1020,6 @@
return {generate, expandStates, normalizeStates, assignColors,
drawBurgs, specifyBurgs, defineBurgFeatures, drawStateLabels, collectStatistics,
generateDiplomacy, defineStateForms, getFullName, generateProvinces};
generateCampaigns, generateDiplomacy, defineStateForms, getFullName, generateProvinces};
})));

View file

@ -0,0 +1,241 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.Military = factory());
}(this, (function () {'use strict';
let cells, p, states;
const generate = function() {
console.time("calculateMilitaryForces");
cells = pack.cells, p = cells.p, states = pack.states;
const valid = states.filter(s => s.i && !s.removed); // valid states
const expn = d3.sum(valid.map(s => s.expansionism)); // total expansion
const area = d3.sum(valid.map(s => s.area)); // total area
const rate = {x:0, Ally:-.2, Friendly:-.1, Neutral:0, Suspicion:.1, Enemy:1, Unknown:0, Rival:.5, Vassal:.5, Suzerain:-.5};
valid.forEach(s => {
const temp = s.temp = {}, d = s.diplomacy;
const expansionRate = Math.min(Math.max((s.expansionism / expn) / (s.area / area), .25), 4); // how much state expansionism is realized
const diplomacyRate = d.some(d => d === "Enemy") ? 1 : d.some(d => d === "Rival") ? .8 : d.some(d => d === "Suspicion") ? .5 : .1; // peacefulness
const neighborsRate = Math.min(Math.max(s.neighbors.map(n => n ? pack.states[n].diplomacy[s.i] : "Suspicion").reduce((s, r) => s += rate[r], .5), .3), 3); // neighbors rate
s.alert = rn(expansionRate * diplomacyRate * neighborsRate, 2); // war alert rate (army modifier)
temp.platoons = [];
// apply overall state modifiers for unit types based on state features
for (const unit of options.military) {
let modifier = 1;
if (unit.type === "mounted") {
if (s.type === "Naval") modifier /= 1.4;
if (s.form === "Horde") modifier *= 2;
} else if (unit.type === "ranged") {
if (s.type === "Hunting") modifier *= 1.4;
} else if (unit.type === "naval") {
if (s.type === "Naval") modifier *= 2; else
if (s.type === "River") modifier *= 1.2; else
if (s.type === "Nomadic") modifier /= 1.4;
if (s.form === "Republic") modifier *= 1.2;
}
temp[unit.name] = modifier * s.alert;
}
});
const portsMod = d3.max(pack.features.map(f => f.land ? 0 : f.ports)) * .75;
const normalizeNaval = ports => normalize(ports, 0, portsMod);
for (const i of cells.i) {
if (!cells.pop[i]) continue;
const s = states[cells.state[i]]; // cell state
if (!s.i || s.removed) continue;
let m = cells.pop[i] / 100; // basic rural army in percentages
if (cells.culture[i] !== s.culture) m = s.form === "Union" ? m / 1.2 : m / 2; // non-dominant culture
if (cells.religion[i] !== cells.religion[s.center]) m = s.form === "Theocracy" ? m / 2.2 : m / 1.4; // non-dominant religion
if (cells.f[i] !== cells.f[s.center]) m = s.type === "Naval" ? m / 1.2 : m / 1.8; // different landmass
const nomadic = [1, 2, 3, 4].includes(cells.biome[i]);
const wetland = [7, 8, 9, 12].includes(cells.biome[i]);
const highland = cells.h[i] >= 70;
for (const u of options.military) {
const perc = +u.rural;
if (isNaN(perc) || perc <= 0) continue;
let army = m * perc; // basic army for rural cell
if (nomadic) { // "nomadic" biomes special rules
if (u.type === "melee") army /= 5; else
if (u.type === "ranged") army /= 2; else
if (u.type === "mounted") army *= 3;
}
if (wetland) { // "wet" biomes special rules
if (u.type === "melee") army *= 1.2; else
if (u.type === "ranged") army *= 1.4; else
if (u.type === "mounted") army /= 3;
}
if (highland) { // highlands special rules
if (u.type === "ranged") army *= 2; else
if (u.type === "mounted") army /= 3;
}
const t = rn(army * s.temp[u.name] * populationRate.value);
if (!t) continue;
let x = p[i][0], y = p[i][1], n = 0;
if (u.type === "naval") {let haven = cells.haven[i]; x = p[haven][0], y = p[haven][1]; n = 1}; // place naval to sea
s.temp.platoons.push({cell: i, a:t, t, x, y, u:u.name, n, s:u.separate});
}
}
for (const b of pack.burgs) {
if (!b.i || b.removed || !b.state || !b.population) continue;
const s = states[b.state]; // burg state
let m = b.population * urbanization.value / 100; // basic urban army in percentages
if (b.capital) m *= 1.2; // capital has household troops
if (b.culture !== s.culture) m = s.form === "Union" ? m / 1.2 : m / 2; // non-dominant culture
if (cells.religion[b.cell] !== cells.religion[s.center]) m = s.form === "Theocracy" ? m / 2.2 : m / 1.4; // non-dominant religion
if (cells.f[b.cell] !== cells.f[s.center]) m = s.type === "Naval" ? m / 1.2 : m / 1.8; // different landmass
const biome = cells.biome[b.cell]; // burg biome
const nomadic = [1, 2, 3, 4].includes(biome);
const wetland = [7, 8, 9, 12].includes(biome);
const highland = cells.h[b.cell] >= 70;
for (const u of options.military) {
const perc = +u.urban;
if (isNaN(perc) || perc <= 0) continue;
let army = m * perc; // basic army for rural cell
if (u.type === "naval") {
if (!b.port) continue; // only ports have naval units
army *= normalizeNaval(pack.features[b.port].ports);
}
if (nomadic) { // "nomadic" biomes special rules
if (u.type === "melee") army /= 3; else
if (u.type === "machinery") army /= 2; else
if (u.type === "mounted") army *= 3;
}
if (wetland) { // "wet" biomes special rules
if (u.type === "melee") army *= 1.2; else
if (u.type === "ranged") army *= 1.4; else
if (u.type === "machinery") army *= 1.2; else
if (u.type === "mounted") army /= 4;
}
if (highland) { // highlands special rules
if (u.type === "ranged") army *= 2; else
if (u.type === "naval") army /= 3; else
if (u.type === "mounted") army /= 3;
}
const t = rn(army * s.temp[u.name] * populationRate.value);
if (!t) continue;
let x = p[b.cell][0], y = p[b.cell][1], n = 0;
if (u.type === "naval") {let haven = cells.haven[b.cell]; x = p[haven][0], y = p[haven][1]; n = 1}; // place naval to sea
s.temp.platoons.push({cell: b.cell, a:t, t, x, y, u:u.name, n, s:u.separate});
}
}
const expected = 3 * populationRate.value; // expected regiment size
const mergeable = (n, s) => (!n.s && !s.s) || n.u === s.u;
// get regiments for each state
valid.forEach(s => {
s.military = createRegiments(s.temp.platoons, s);
delete s.temp; // do not store temp data
drawRegiments(s.military, s.i, s.color);
});
function createRegiments(nodes, s) {
nodes.sort((a,b) => a.a - b.a);
const tree = d3.quadtree(nodes, d => d.x, d => d.y);
nodes.forEach(n => {
tree.remove(n);
const overlap = tree.find(n.x, n.y, 20);
if (overlap && overlap.t && mergeable(n, overlap)) {merge(n, overlap); return;}
if (n.t > expected) return;
const r = (expected - n.t) / (n.s?40:20); // search radius
const candidates = tree.findAll(n.x, n.y, r);
for (const c of candidates) {
if (c.t < expected && mergeable(n, c)) {merge(n, c); break;}
}
});
// add n0 to n1's ultimate parent
function merge(n0, n1) {
if (!n1.childen) n1.childen = [n0]; else n1.childen.push(n0);
if (n0.childen) n0.childen.forEach(n => n1.childen.push(n));
n1.t += n0.t;
n0.t = 0;
}
// parse regiments data to easy-readable json
const regiments = nodes.filter(n => n.t).sort((a,b) => b.t - a.t).map((r, i) => {
const u = {}; u[r.u] = r.a;
(r.childen||[]).forEach(n => u[n.u] = u[n.u] ? u[n.u] += n.a : n.a);
return {i, a:r.t, cell:r.cell, x:r.x, y:r.y, u, n:r.n, name};
});
// generate name for regiments
regiments.forEach(r => {
r.name = getName(r, regiments);
generateNote(r, s);
});
return regiments;
}
console.timeEnd("calculateMilitaryForces");
}
function drawRegiments(regiments, s, color) {
const size = 3;
const army = armies.append("g").attr("id", "army"+s).attr("fill", color);
const g = army.selectAll("g").data(regiments).enter().append("g").attr("id", d => "regiment"+s+"-"+d.i);
g.append("rect").attr("data-name", d => d.name).attr("data-state", s).attr("data-id", d => d.i)
.attr("x", d => d.n ? d.x-size*2 : d.x-size*3).attr("y", d => d.y-size)
.attr("width", d => d.n ? size*4 : size*6).attr("height", size*2);
g.append("text").attr("x", d => d.x).attr("y", d => d.y).text(d => d.a);
}
const drawRegiment = function(reg, s, x = reg.x, y = reg.y) {
const size = 3;
const g = armies.select("g#army"+s).append("g").attr("id", "regiment"+s+"-"+reg.i);
g.append("rect").attr("data-name", reg.name).attr("data-state", s).attr("data-id", reg.i)
.attr("x", reg.n ? x-size*2 : x-size*3).attr("y", y-size)
.attr("width", reg.n ? size*4 : size*6).attr("height", size*2);
g.append("text").attr("x", x).attr("y", y).text(reg.a);
}
const getName = function(r, regiments) {
const proper = r.n ? null :
cells.province[r.cell] ? pack.provinces[cells.province[r.cell]].name :
cells.burg[r.cell] ? pack.burgs[cells.burg[r.cell]].name : null
const number = nth(regiments.filter(reg => reg.n === r.n && reg.i < r.i).length+1);
const form = r.n ? "Fleet" : "Regiment";
return `${number}${proper?` (${proper}) `:` `}${form}`;
}
const generateNote = function(r, s) {
const base = cells.burg[r.cell] ? pack.burgs[cells.burg[r.cell]].name :
cells.province[r.cell] ? pack.provinces[cells.province[r.cell]].fullName : null;
const station = base ? `${r.name} is ${r.n ? "based" : "stationed"} in ${base}. ` : null;
const composition = Object.keys(r.u).map(t => `${t}: ${r.u[t]}`).join("\r\n");
const troops = `\r\n\r\nRegiment composition:\r\n${composition}.`;
const campaign = ra(s.campaigns);
const year = rand(campaign.start, campaign.end);
const legend = `Regiment was formed in ${year} ${options.era} during the ${campaign.name}. ${station}${troops}`;
notes.push({id:`regiment${s.i}-${r.i}`, name:r.name, legend});
}
return {generate, getName, generateNote, drawRegiment};
})));

View file

@ -117,8 +117,9 @@
const increment = rn(.8 + Math.random() * .6, 1); // river bed widening modifier
const [path, length] = getPath(riverEnhanced, width, increment);
riverPaths.push([r, path, width, increment]);
const parent = riverSegments[0].parent || 0;
pack.rivers.push({i:r, parent, length, source:riverSegments[0].cell, mouth:last(riverSegments).cell});
const source = riverSegments[0], mouth = riverSegments[riverSegments.length-2];
const parent = source.parent || 0;
pack.rivers.push({i:r, parent, length, source:source.cell, mouth:mouth.cell});
} else {
// remove too short rivers
riverSegments.filter(s => cells.r[s.cell] === r).forEach(s => cells.r[s.cell] = 0);
@ -258,6 +259,7 @@
for (const r of pack.rivers) {
r.basin = getBasin(r.i, r.parent);
r.name = getName(r.mouth);
//debug.append("circle").attr("cx", pack.cells.p[r.mouth][0]).attr("cy", pack.cells.p[r.mouth][1]).attr("r", 2);
const small = r.length < smallLength;
r.type = r.parent && !(r.i%6) ? small ? "Branch" : "Fork" : small ? rw(smallType) : "River";
}

View file

@ -226,12 +226,12 @@ function getMapData() {
const dateString = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate();
const license = "File can be loaded in azgaar.github.io/Fantasy-Map-Generator";
const params = [version, license, dateString, seed, graphWidth, graphHeight].join("|");
const options = [distanceUnitInput.value, distanceScaleInput.value, areaUnit.value,
const settings = [distanceUnitInput.value, distanceScaleInput.value, areaUnit.value,
heightUnit.value, heightExponentInput.value, temperatureScale.value,
barSize.value, barLabel.value, barBackOpacity.value, barBackColor.value,
barPosX.value, barPosY.value, populationRate.value, urbanization.value,
mapSizeOutput.value, latitudeOutput.value, temperatureEquatorOutput.value,
temperaturePoleOutput.value, precOutput.value, JSON.stringify(winds),
temperaturePoleOutput.value, precOutput.value, JSON.stringify(options.winds),
mapName.value].join("|");
const coords = JSON.stringify(mapCoordinates);
const biomes = [biomesData.color, biomesData.habitability, biomesData.name].join("|");
@ -265,7 +265,7 @@ function getMapData() {
const pop = Array.from(pack.cells.pop).map(p => rn(p, 4));
// data format as below
const data = [params, options, coords, biomes, notesData, svg_xml,
const data = [params, settings, coords, biomes, notesData, svg_xml,
gridGeneral, grid.cells.h, grid.cells.prec, grid.cells.f, grid.cells.t, grid.cells.temp,
features, cultures, states, burgs,
pack.cells.biome, pack.cells.burg, pack.cells.conf, pack.cells.culture, pack.cells.fl,
@ -554,29 +554,29 @@ function parseLoadedData(data) {
console.group("Loaded Map " + seed);
void function parseOptions() {
const options = data[1].split("|");
if (options[0]) applyOption(distanceUnitInput, options[0]);
if (options[1]) distanceScaleInput.value = distanceScaleOutput.value = options[1];
if (options[2]) areaUnit.value = options[2];
if (options[3]) applyOption(heightUnit, options[3]);
if (options[4]) heightExponentInput.value = heightExponentOutput.value = options[4];
if (options[5]) temperatureScale.value = options[5];
if (options[6]) barSize.value = barSizeOutput.value = options[6];
if (options[7] !== undefined) barLabel.value = options[7];
if (options[8] !== undefined) barBackOpacity.value = options[8];
if (options[9]) barBackColor.value = options[9];
if (options[10]) barPosX.value = options[10];
if (options[11]) barPosY.value = options[11];
if (options[12]) populationRate.value = populationRateOutput.value = options[12];
if (options[13]) urbanization.value = urbanizationOutput.value = options[13];
if (options[14]) mapSizeInput.value = mapSizeOutput.value = Math.max(Math.min(options[14], 100), 1);
if (options[15]) latitudeInput.value = latitudeOutput.value = Math.max(Math.min(options[15], 100), 0);
if (options[16]) temperatureEquatorInput.value = temperatureEquatorOutput.value = options[16];
if (options[17]) temperaturePoleInput.value = temperaturePoleOutput.value = options[17];
if (options[18]) precInput.value = precOutput.value = options[18];
if (options[19]) winds = JSON.parse(options[19]);
if (options[20]) mapName.value = options[20];
void function parseSettings() {
const settings = data[1].split("|");
if (settings[0]) applyOption(distanceUnitInput, settings[0]);
if (settings[1]) distanceScaleInput.value = distanceScaleOutput.value = settings[1];
if (settings[2]) areaUnit.value = settings[2];
if (settings[3]) applyOption(heightUnit, settings[3]);
if (settings[4]) heightExponentInput.value = heightExponentOutput.value = settings[4];
if (settings[5]) temperatureScale.value = settings[5];
if (settings[6]) barSize.value = barSizeOutput.value = settings[6];
if (settings[7] !== undefined) barLabel.value = settings[7];
if (settings[8] !== undefined) barBackOpacity.value = settings[8];
if (settings[9]) barBackColor.value = settings[9];
if (settings[10]) barPosX.value = settings[10];
if (settings[11]) barPosY.value = settings[11];
if (settings[12]) populationRate.value = populationRateOutput.value = settings[12];
if (settings[13]) urbanization.value = urbanizationOutput.value = settings[13];
if (settings[14]) mapSizeInput.value = mapSizeOutput.value = Math.max(Math.min(settings[14], 100), 1);
if (settings[15]) latitudeInput.value = latitudeOutput.value = Math.max(Math.min(settings[15], 100), 0);
if (settings[16]) temperatureEquatorInput.value = temperatureEquatorOutput.value = settings[16];
if (settings[17]) temperaturePoleInput.value = temperaturePoleOutput.value = settings[17];
if (settings[18]) precInput.value = precOutput.value = settings[18];
if (settings[19]) options.winds = JSON.parse(settings[19]);
if (settings[20]) mapName.value = settings[20];
}()
void function parseConfiguration() {
@ -931,6 +931,14 @@ function parseLoadedData(data) {
BurgsAndStates.collectStatistics();
}
if (version < 1.3) {
// v 1.3 added ports attribute to pack.features
for (const f of pack.features) {
if (!f.i) continue;
f.ports = pack.burgs.filter(b => !b.removed && b.port === f.i).length;
}
}
}()
changeMapSize();

View file

@ -17,7 +17,7 @@ function restoreDefaultEvents() {
function clicked() {
const el = d3.event.target;
if (!el || !el.parentElement || !el.parentElement.parentElement) return;
const parent = el.parentElement, grand = parent.parentElement;
const parent = el.parentElement, grand = parent.parentElement, great = grand.parentElement;
const p = d3.mouse(this);
const i = findCell(p[0], p[1]);
@ -27,8 +27,9 @@ function clicked() {
else if (grand.id === "burgLabels") editBurg();
else if (grand.id === "burgIcons") editBurg();
else if (parent.id === "terrain") editReliefIcon();
else if (parent.id === "markers") editMarker();
else if (parent.id === "markers") editMarker();
else if (grand.id === "coastline") editCoastline();
else if (great.id === "armies") editRegiment();
else if (pack.cells.t[i] === 1) {
const node = document.getElementById("island_"+pack.cells.f[i]);
editCoastline(node);

View file

@ -39,10 +39,14 @@ function clearMainTip() {
tooltip.innerHTML = "";
}
// show tip at the bottom of the screen, consider possible translation
function showDataTip(e) {
if (!e.target) return;
if (e.target.dataset.tip) {tip(e.target.dataset.tip); return;};
if (e.target.parentNode.dataset.tip) tip(e.target.parentNode.dataset.tip);
let dataTip = e.target.dataset.tip;
if (!dataTip && e.target.parentNode.dataset.tip) dataTip = e.target.parentNode.dataset.tip;
if (!dataTip) return;
const tooltip = lang === "en" ? dataTip : translate(e.target.dataset.t || e.target.parentNode.dataset.t, dataTip);
tip(tooltip);
}
function moved() {
@ -84,6 +88,7 @@ function showMapTooltip(point, e, i, g) {
const land = pack.cells.h[i] >= 20;
// specific elements
if (group === "armies") {tip(e.target.dataset.name + ". Click to edit"); return;}
if (group === "rivers") {tip(getRiverName(e.target.id) + "Click to edit"); return;}
if (group === "routes") {tip("Click to edit the Route"); return;}
if (group === "terrain") {tip("Click to edit the Relief Icon"); return;}
@ -132,14 +137,15 @@ function updateCellInfo(point, i, g) {
const cells = pack.cells;
const x = infoX.innerHTML = rn(point[0]);
const y = infoY.innerHTML = rn(point[1]);
const f = cells.f[i];
infoLat.innerHTML = toDMS(mapCoordinates.latN - (y / graphHeight) * mapCoordinates.latT, "lat");
infoLon.innerHTML = toDMS(mapCoordinates.lonW + (x / graphWidth) * mapCoordinates.lonT, "lon");
infoCell.innerHTML = i;
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
infoArea.innerHTML = cells.area[i] ? si(cells.area[i] * distanceScaleInput.value ** 2) + unit : "n/a";
const h = pack.cells.h[i] < 20 ? grid.cells.h[pack.cells.g[i]] : pack.cells.h[i];
infoHeight.innerHTML = getFriendlyHeight(point) + " (" + h + ")";
infoEvelation.innerHTML = getElevation(pack.features[f], pack.cells.h[i]);
infoDepth.innerHTML = getDepth(pack.features[f], pack.cells.h[i], point);
infoTemp.innerHTML = convertTemperature(grid.cells.temp[g]);
infoPrec.innerHTML = cells.h[i] >= 20 ? getFriendlyPrecipitation(i) : "n/a";
infoRiver.innerHTML = cells.h[i] >= 20 && cells.r[i] ? getRiverInfo(cells.r[i]) : "no";
@ -149,7 +155,6 @@ function updateCellInfo(point, i, g) {
infoReligion.innerHTML = cells.religion[i] ? `${pack.religions[cells.religion[i]].name} (${cells.religion[i]})` : "no";
infoPopulation.innerHTML = getFriendlyPopulation(i);
infoBurg.innerHTML = cells.burg[i] ? pack.burgs[cells.burg[i]].name + " (" + cells.burg[i] + ")" : "no";
const f = cells.f[i];
infoFeature.innerHTML = f ? pack.features[f].group + " (" + f + ")" : "n/a";
infoBiome.innerHTML = biomesData.name[cells.biome[i]];
}
@ -164,6 +169,26 @@ function toDMS(coord, c) {
return degrees + "° " + minutes + " " + seconds + "″ " + cardinal;
}
// get surface elevation
function getElevation(f, h) {
if (f.land) return getHeight(h) + " (" + h + ")"; // land: usual height
if (f.border) return "0 " + heightUnit.value; // ocean: 0
// lake: lowest coast height - 1
const lakeCells = Array.from(pack.cells.i.filter(i => pack.cells.f[i] === f.i));
const heights = lakeCells.map(i => pack.cells.c[i].map(c => pack.cells.h[c])).flat().filter(h => h > 19);
const elevation = (d3.min(heights)||20) - 1;
return getHeight(elevation) + " (" + elevation + ")";
}
// get water depth
function getDepth(f, h, p) {
if (f.land) return "0 " + heightUnit.value; // land: 0
if (!f.border) return getHeight(h, "abs"); // lake: pack abs height
const gridH = grid.cells.h[findGridCell(p[0], p[1])];
return getHeight(gridH, "abs"); // ocean: grig height
}
// get user-friendly (real-world) height value from map data
function getFriendlyHeight(p) {
const packH = pack.cells.h[findCell(p[0], p[1])];
@ -172,7 +197,7 @@ function getFriendlyHeight(p) {
return getHeight(h);
}
function getHeight(h) {
function getHeight(h, abs) {
const unit = heightUnit.value;
let unitRatio = 3.281; // default calculations are in feet
if (unit === "m") unitRatio = 1; // if meter
@ -182,6 +207,7 @@ function getHeight(h) {
if (h >= 20) height = Math.pow(h - 18, +heightExponentInput.value);
else if (h < 20 && h > 0) height = (h - 20) / h * 50;
if (abs) height = Math.abs(height);
return rn(height * unitRatio) + " " + unit;
}
@ -216,7 +242,7 @@ document.querySelectorAll("[data-locked]").forEach(function(e) {
else tip("Click to lock the option and always use the current value on new map generation");
event.stopPropagation();
});
e.addEventListener("click", function(event) {
const id = (this.id).slice(5);
if (this.className === "icon-lock") unlock(id);
@ -341,7 +367,7 @@ document.addEventListener("keyup", event => {
else if (shift && key === 79) editNotes(); // Shift + "O" to edit Notes
else if (shift && key === 84) overviewBurgs(); // Shift + "T" to open Burgs overview
else if (shift && key === 86) overviewRivers(); // Shift + "V" to open Rivers overview
//else if (shift && key === 77) overviewMilitary(); // Shift + "M" to open Military overview
else if (shift && key === 77) overviewMilitary(); // Shift + "M" to open Military overview
else if (shift && key === 69) viewCellDetails(); // Shift + "E" to open Cell Details
else if (shift && key === 49) toggleAddBurg(); // Shift + "1" to click to add Burg

View file

@ -3,8 +3,8 @@
function editHeightmap() {
void function selectEditMode() {
alertMessage.innerHTML = `<p>Heightmap is a core element on which all other data (rivers, burgs, states etc) is based.
So the best edit approach is to <i>erase</i> the secondary data and let the system automatically regenerate it on edit completion.</p>
alertMessage.innerHTML = `<span>Heightmap is a core element on which all other data (rivers, burgs, states etc) is based.
So the best edit approach is to <i>erase</i> the secondary data and let the system automatically regenerate it on edit completion.</span>
<p>You can also <i>keep</i> all the data, but you won't be able to change the coastline.</p>
<p>If you need to change the coastline and keep the data, you may try the <i>risk</i> edit option.
The data will be restored as much as possible, but the coastline change can cause unexpected fluctuations and errors.</p>
@ -128,8 +128,7 @@ function editHeightmap() {
customization = 0;
customizationMenu.style.display = "none";
if (options.querySelector(".tab > button.active").id === "toolsTab")
toolsContent.style.display = "block";
if (document.getElementById("options").querySelector(".tab > button.active").id === "toolsTab") toolsContent.style.display = "block";
layersPreset.disabled = false;
exitCustomization.style.display = "none"; // hide finalize button
restoreDefaultEvents();
@ -195,6 +194,7 @@ function editHeightmap() {
BurgsAndStates.drawStateLabels();
Rivers.specify();
Military.generate();
addMarkers();
addZones();
console.timeEnd("regenerateErasedData");
@ -307,6 +307,7 @@ function editHeightmap() {
for (const i of pack.cells.i) {
const g = pack.cells.g[i];
if (pack.features[pack.cells.f[i]].group === "freshwater") pack.cells.h[i] = 19; // de-elevate lakes
const land = pack.cells.h[i] >= 20;
// check biome

View file

@ -399,7 +399,7 @@ function drawPrec() {
const data = cells.i.filter(i => cells.h[i] >= 20 && cells.prec[i]);
prec.selectAll("circle").data(data).enter().append("circle")
.attr("cx", d => p[d][0]).attr("cy", d => p[d][1]).attr("r", 0)
.transition(show).attr("r", d => rn(Math.max(Math.sqrt(cells.prec[d] * .5), .8),2));
.transition(show).attr("r", d => rn(Math.max(Math.sqrt(cells.prec[d] * .5), .8),2));
}
function togglePopulation(event) {

View file

@ -6,11 +6,12 @@ function overviewMilitary() {
if (!layerIsOn("toggleBorders")) toggleBorders();
const body = document.getElementById("militaryBody");
militaryOverviewAddLines();
addLines();
$("#militaryOverview").dialog();
if (modules.overviewMilitary) return;
modules.overviewMilitary = true;
updateHeaders();
$("#militaryOverview").dialog({
title: "Military Overview", resizable: false, width: fitContent(),
@ -18,43 +19,58 @@ function overviewMilitary() {
});
// add listeners
document.getElementById("militaryOverviewRefresh").addEventListener("click", militaryOverviewAddLines);
document.getElementById("militaryOverviewRefresh").addEventListener("click", addLines);
document.getElementById("militaryOptionsButton").addEventListener("click", militaryCustomize);
document.getElementById("militaryOverviewRecalculate").addEventListener("click", militaryRecalculate);
document.getElementById("militaryExport").addEventListener("click", downloadMilitaryData);
// add line for each river
function militaryOverviewAddLines() {
body.innerHTML = "";
let lines = "", militaryTotal = 0;
body.addEventListener("change", function(ev) {
const el = ev.target, line = el.parentNode, state = +line.dataset.id, type = el.dataset.type;
if (type && type !== "alert") changeForces(state, line, type, +el.value); else
if (type === "alert") changeAlert(state, line, +el.value);
});
// update military types in header and tooltips
function updateHeaders() {
const header = document.getElementById("militaryHeader");
header.querySelectorAll(".removable").forEach(el => el.remove());
const insert = html => document.getElementById("militaryTotal").insertAdjacentHTML("beforebegin", html);
for (const u of options.military) {
const label = capitalize(u.name.replace(/_/g, ' '));
insert(`<div data-tip="State ${u.name} units number. Click to sort" class="sortable removable" data-sortby="${u.name}">${label}&nbsp;</div>`);
}
header.querySelectorAll(".removable").forEach(function(e) {
e.addEventListener("click", function() {sortLines(this);});
});
}
// add line for each state
function addLines() {
body.innerHTML = "";
let lines = "";
const states = pack.states.filter(s => s.i && !s.removed);
const popRate = +populationRate.value;
for (const s of states) {
const total = (s.military.infantry + s.military.cavalry + s.military.archers + s.military.fleet / 10);
const rate = total / (s.rural + s.urban * urbanization.value) * 100;
militaryTotal += total;
const population = rn((s.rural + s.urban * urbanization.value) * populationRate.value);
const getForces = u => s.military.reduce((s, r) => s+(r.u[u.name]||0), 0);
const total = options.military.reduce((s, u) => s + getForces(u) * u.crew, 0);
const rate = total / population * 100;
lines += `<div class="states" data-id=${s.i} data-state="${s.name}" data-infantry="${s.military.infantry}"
data-archers="${s.military.archers}" data-cavalry="${s.military.cavalry}" data-reserve="${s.military.reserve}"
data-fleet="${s.military.fleet}" data-rate="${rate}" data-total="${total}">
<svg data-tip="State color" 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="State name" class="stateName" value="${s.name}" readonly>
const sortData = options.military.map(u => `data-${u.name}="${getForces(u)}"`).join(" ");
const lineData = options.military.map(u => `<input data-type="${u.name}" data-tip="State ${u.name} units number" type="number" min=0 step=1 value="${getForces(u)}">`).join(" ");
<input data-tip="State infantry number" type="number" class="militaryArmy" min=0 step=1 value="${rn(s.military.infantry * popRate)}">
<input data-tip="State archers number" type="number" class="militaryArmy" min=0 step=1 value="${rn(s.military.archers * popRate)}">
<input data-tip="State cavalry number" type="number" class="militaryArmy" min=0 step=1 value="${rn(s.military.cavalry * popRate)}">
<input data-tip="Number of ships in state navy" class="militaryFleet" type="number" min=0 step=1 value="${s.military.fleet}">
<div data-tip="Total military personnel (including ships crew)">${si(total * popRate)}</div>
<div data-tip="Armed forces personnel (% of state population). Depends on diplomatic situation">${rn(rate, 2)}%</div>
<div data-tip="State manpower (reserve)">${si(s.military.reserve * popRate)}</div>
lines += `<div class="states" data-id=${s.i} data-state="${s.name}" ${sortData} data-total="${total}" data-population="${population}" data-rate="${rate}" data-alert="${s.alert}">
<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="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. Modifier to military forces number, depends of political situation" type="number" min=0 step=.01 value="${rn(s.alert, 2)}">
</div>`;
}
body.insertAdjacentHTML("beforeend", lines);
// update footer
militaryFooterStates.innerHTML = states.length;
militaryFooterAverage.innerHTML = si(militaryTotal / states.length * popRate);
updateFooter();
// add listeners
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => stateHighlightOn(ev)));
@ -62,6 +78,40 @@ function overviewMilitary() {
applySorting(militaryHeader);
}
function changeForces(state, line, type, value) {
const s = pack.states[state];
if (!s.military.alert) {tip("Value won't be applied as War Alert is 0. Change Alert value to positive first", false, "error"); return;}
line.dataset[type] = value;
s.military[type] = value / populationRate.value / s.military.alert;
updateTotal(s.military, line);
updateFooter();
}
function changeAlert(state, line, alert) {
const s = pack.states[state];
s.military.alert = line.dataset.alert = alert;
const getForces = u => rn(s.military[u.name] * alert * populationRate.value)||0;
options.military.forEach(u => line.dataset[u.name] = line.querySelector(`input[data-type='${u.name}']`).value = getForces(u));
updateTotal(s.military, line);
updateFooter();
}
function updateTotal(m, line) {
line.dataset.total = rn(d3.sum(options.military.map(u => (m[u.name]||0) * u.crew)) * m.alert * populationRate.value);
line.dataset.rate = line.dataset.total / line.dataset.population * 100;
line.querySelector("div[data-type='total']>b").innerHTML = si(line.dataset.total);
line.querySelector("div[data-type='rate']").innerHTML = rn(line.dataset.rate, 2) + "%";
}
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);
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);
}
function stateHighlightOn(event) {
if (!layerIsOn("toggleStates")) return;
const state = +event.target.dataset.id;
@ -82,7 +132,7 @@ function overviewMilitary() {
const i = d3.interpolateString("0," + l, l + "," + l);
return t => i(t);
}
function removePath(path) {
path.transition().duration(1000).attr("opacity", 0).remove();
}
@ -93,19 +143,92 @@ function overviewMilitary() {
});
}
function militaryCustomize() {
const types = ["default", "melee", "ranged", "mounted", "machinery", "naval"];
const table = document.getElementById("militaryOptions").querySelector("tbody");
removeUnitLines();
options.military.map(u => addUnitLine(u));
$("#militaryOptions").dialog({
title: "Edit Military Units", resizable: false, width: fitContent(),
position: {my: "center", at: "center", of: "svg"},
buttons: {
Apply: function() {applyMilitaryOptions(); $(this).dialog("close");},
Add: () => addUnitLine({name: "custom", rural: 0.2, urban: 0.5, crew: 1, type: "default"}),
Restore: restoreDefaultUnits,
Cancel: function() {$(this).dialog("close");}
}, open: function() {
const buttons = $(this).dialog("widget").find(".ui-dialog-buttonset > button");
buttons[0].addEventListener("mousemove", () => tip("Apply military units settings. All forces will be recalculated!"));
buttons[1].addEventListener("mousemove", () => tip("Add new military unit to the table"));
buttons[2].addEventListener("mousemove", () => tip("Restore default military units and settings"));
buttons[3].addEventListener("mousemove", () => tip("Close the window without saving the changes"));
}
});
function removeUnitLines() {
table.querySelectorAll("tr").forEach(el => el.remove());
}
function addUnitLine(u) {
const row = `<tr>
<td><input data-tip="Type unit name. If name is changed for existing unit, old unit will be replaced" value="${u.name}"></td>
<td><input data-tip="Enter conscription percentage for rural population" type="number" min=0 max=100 step=.01 value="${u.rural}"></td>
<td><input data-tip="Enter conscription percentage for urban population" type="number" min=0 max=100 step=.01 value="${u.urban}"></td>
<td><input data-tip="Enter average number of people in crew" type="number" min=1 step=1 value="${u.crew}"></td>
<td><select data-tip="Select unit type to apply special rules on forces recalculation">${types.map(t => `<option ${u.type === t ? "selected" : ""} value="${t}">${t}</option>`).join(" ")}</select></td>
<td data-tip="Check if unit is separate and can be stacked only with units of the same type">
<input id="${u.name}Separate" type="checkbox" class="checkbox" checked=${u.separate}>
<label for="${u.name}Separate" class="checkbox-label"></label>
</td>
<td data-tip="Remove the unit"><span data-tip="Remove unit type" class="icon-trash-empty pointer" onclick="this.parentElement.parentElement.remove();"></span></td>
</tr>`;
table.insertAdjacentHTML("beforeend", row);
}
function restoreDefaultUnits() {
removeUnitLines();
[{name:"infantry", rural:.25, urban:.2, crew:1, type:"melee", separate:0},
{name:"archers", rural:.12, urban:.2, crew:1, type:"ranged", separate:0},
{name:"cavalry", rural:.12, urban:.03, crew:3, type:"mounted", separate:0},
{name:"artillery", rural:0, urban:.03, crew:8, type:"machinery", separate:0},
{name:"fleet", rural:0, urban:.015, crew:100, type:"naval", separate:1}].map(u => addUnitLine(u));
}
function applyMilitaryOptions() {
options.military = Array.from(table.querySelectorAll("tr")).map(r => {
const [name, rural, urban, crew, type, separate] = Array.from(r.querySelectorAll("input, select")).map(d => d.value||d.checked);
return {name:name.replace(/[&\/\\#, +()$~%.'":*?<>{}]/g, '_'), rural:+rural||0, urban:+urban||0, crew:+crew||0, type, separate:+separate||0};
});
localStorage.setItem("military", JSON.stringify(options.military));
calculateMilitaryForces();
updateHeaders();
addLines();
}
}
function militaryRecalculate() {
calculateMilitaryForces();
addLines();
}
function downloadMilitaryData() {
let data = "Id,River,Type,Length,Basin\n"; // headers
const units = options.military.map(u => u.name);
let data = "Id,State,"+units.map(u => capitalize(u)).join(",")+",Total,Population,Rate,War Alert\n"; // headers
body.querySelectorAll(":scope > div").forEach(function(el) {
data += el.dataset.id + ",";
data += el.dataset.name + ",";
data += el.dataset.type + ",";
data += el.querySelector(".biomeArea").innerHTML + ",";
data += el.dataset.basin + "\n";
data += el.dataset.state + ",";
data += units.map(u => el.dataset[u]).join(",") + ",";
data += el.dataset.total + ",";
data += el.dataset.population + ",";
data += el.dataset.rate + ",";
data += el.dataset.alert + "\n";
});
const name = getFileName("Military") + ".csv";
downloadFile(data, name);
}
}
}

View file

@ -20,7 +20,7 @@ function showOptions(event) {
}
regenerate.style.display = "none";
options.style.display = "block";
document.getElementById("options").style.display = "block";
optionsTrigger.style.display = "none";
if (event) event.stopPropagation();
@ -28,21 +28,21 @@ function showOptions(event) {
// Hide options pane on trigger click
function hideOptions(event) {
options.style.display = "none";
document.getElementById("options").style.display = "none";
optionsTrigger.style.display = "block";
if (event) event.stopPropagation();
}
// To toggle options on hotkey press
function toggleOptions(event) {
if (options.style.display === "none") showOptions(event);
if (document.getElementById("options").style.display === "none") showOptions(event);
else hideOptions(event);
}
// Toggle "New Map!" pane on hover
optionsTrigger.addEventListener("mouseenter", function() {
if (optionsTrigger.classList.contains("glow")) return;
if (options.style.display === "none") regenerate.style.display = "block";
if (document.getElementById("options").style.display === "none") regenerate.style.display = "block";
});
collapsible.addEventListener("mouseleave", function() {
@ -50,15 +50,15 @@ collapsible.addEventListener("mouseleave", function() {
});
// Activate options tab on click
options.querySelector("div.tab").addEventListener("click", function(event) {
document.getElementById("options").querySelector("div.tab").addEventListener("click", function(event) {
if (event.target.tagName !== "BUTTON") return;
const id = event.target.id;
const active = options.querySelector(".tab > button.active");
const active = document.getElementById("options").querySelector(".tab > button.active");
if (active && id === active.id) return; // already active tab is clicked
if (active) active.classList.remove("active");
document.getElementById(id).classList.add("active");
options.querySelectorAll(".tabcontent").forEach(e => e.style.display = "none");
document.getElementById("options").querySelectorAll(".tabcontent").forEach(e => e.style.display = "none");
if (id === "layersTab") layersContent.style.display = "block"; else
if (id === "styleTab") styleContent.style.display = "block"; else
@ -69,7 +69,7 @@ options.querySelector("div.tab").addEventListener("click", function(event) {
if (id === "aboutTab") aboutContent.style.display = "block";
});
options.querySelectorAll("i.collapsible").forEach(el => el.addEventListener("click", collapse));
document.getElementById("options").querySelectorAll("i.collapsible").forEach(el => el.addEventListener("click", collapse));
function collapse(e) {
const trigger = e.target;
const section = trigger.parentElement.nextElementSibling;
@ -309,7 +309,8 @@ function applyStoredOptions() {
if(stored.slice(0,5) === "style") applyOption(stylePreset, stored, stored.slice(5));
}
if (localStorage.getItem("winds")) winds = localStorage.getItem("winds").split(",").map(w => +w);
if (localStorage.getItem("winds")) options.winds = localStorage.getItem("winds").split(",").map(w => +w);
if (localStorage.getItem("military")) options.military = JSON.parse(localStorage.getItem("military"));
changeDialogsTransparency(localStorage.getItem("transparency") || 5);
if (localStorage.getItem("tooltipSize")) changeTooltipSize(localStorage.getItem("tooltipSize"));

View file

@ -37,7 +37,7 @@ function editProvinces() {
const el = ev.target, cl = el.classList, line = el.parentNode, p = +line.dataset.id;
if (cl.contains("fillRect")) changeFill(el); else
if (cl.contains("name")) editProvinceName(p); else
if (cl.contains("icon-fleur")) provinceOpenCOA(ev, p); else
if (cl.contains("icon-coa")) provinceOpenCOA(ev, p); else
if (cl.contains("icon-star-empty")) capitalZoomIn(p); else
if (cl.contains("icon-flag-empty")) declareProvinceIndependence(p); else
if (cl.contains("culturePopulation")) changePopulation(p); else
@ -116,7 +116,7 @@ function editProvinces() {
lines += `<div class="states" data-id=${p.i} data-name=${p.name} data-form=${p.formName} data-color="${p.color}" data-capital="${capital}" data-state="${stateName}" data-area=${area} data-population=${population}>
<svg data-tip="Province fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${p.color}" class="fillRect pointer"></svg>
<input data-tip="Province name. Click to change" class="name pointer" value="${p.name}" readonly>
<span data-tip="Click to open province COA in the Iron Arachne Heraldry Generator. Ctrl + click to change the seed" class="icon-fleur pointer hide"></span>
<span data-tip="Click to open province COA in the Iron Arachne Heraldry Generator. Ctrl + click to change the seed" class="icon-coa pointer hide"></span>
<input data-tip="Province form name. Click to change" class="name pointer hide" value="${p.formName}" readonly>
<span data-tip="Province capital. Click to zoom into view" class="icon-star-empty pointer hide ${p.burg?'':'placeholder'}"></span>
<select data-tip="Province capital. Click to select from burgs within the state. No capital means the province is governed from the state capital" class="cultureBase hide ${p.burgs.length?'':'placeholder'}">${p.burgs.length ? getCapitalOptions(p.burgs, p.burg) : ''}</select>

View file

@ -0,0 +1,277 @@
"use strict";
function editRegiment() {
if (customization) return;
closeDialogs(".stable");
// if (!layerIsOn("toggleArmies")) toggleArmies();
armies.selectAll(":scope > g").classed("draggable", true);
armies.selectAll(":scope > g > g").call(d3.drag().on("drag", dragRegiment));
elSelected = d3.event.target;
if (!pack.states[elSelected.dataset.state]) return;
if (!regiment()) return;
updateRegimentData(regiment());
drawBase();
$("#regimentEditor").dialog({
title: "Edit Regiment", resizable: false, close: closeEditor,
position: {my: "left top", at: "left+10 top+10", of: "#map"},
close: closeEditor
});
if (modules.editRegiment) return;
modules.editRegiment = true;
// add listeners
document.getElementById("regimentNameRestore").addEventListener("click", restoreName);
document.getElementById("regimentType").addEventListener("click", changeType);
document.getElementById("regimentName").addEventListener("change", changeName);
document.getElementById("regimentRegenerateLegend").addEventListener("click", regenerateLegend);
document.getElementById("regimentLegend").addEventListener("click", editLegend);
document.getElementById("regimentSplit").addEventListener("click", splitRegiment);
document.getElementById("regimentAdd").addEventListener("click", toggleAdd);
document.getElementById("regimentAttach").addEventListener("click", toggleAttach);
document.getElementById("regimentRemove").addEventListener("click", removeRegiment);
// get regiment data element
function regiment() {
return pack.states[elSelected.dataset.state].military.find(r => r.i == elSelected.dataset.id);
}
function updateRegimentData(regiment) {
document.getElementById("regimentType").className = regiment.n ? "icon-anchor" :"icon-users";
document.getElementById("regimentName").value = regiment.name;
const composition = document.getElementById("regimentComposition");
composition.innerHTML = options.military.map(u => {
return `<div data-tip="${capitalize(u.name)} number. Input to change">
<div class="label">${capitalize(u.name)}:</div>
<input data-u="${u.name}" type="number" min=0 step=1 value="${(regiment.u[u.name]||0)}">
<i>${u.type}</i></div>`
}).join("");
composition.querySelectorAll("input").forEach(el => el.addEventListener("change", changeUnit));
}
function drawBase() {
const reg = regiment();
const tr = parseTransform(elSelected.parentNode.getAttribute("transform"));
const tx = +tr[0], ty = +tr[1];
const x2 = +elSelected.nextSibling.getAttribute("x"), y2 = +elSelected.nextSibling.getAttribute("y");
const clr = pack.states[elSelected.dataset.state].color;
const base = viewbox.insert("g", "g#armies").attr("id", "regimentBase");
base.on("mouseenter", d => {tip("Regiment base. Drag to re-base the regiment", true);}).on("mouseleave", d => {tip('', true);});
base.append("line").attr("x1", reg.x).attr("y1", reg.y).attr("x2", x2+tx).attr("y2", y2+ty).attr("class", "dragLine");
base.append("circle").attr("cx", reg.x).attr("cy", reg.y).attr("r", 2).attr("fill", clr).call(d3.drag().on("drag", dragBase));
}
function changeType() {
const reg = regiment();
reg.n = +!reg.n;
document.getElementById("regimentType").className = reg.n ? "icon-anchor" :"icon-users";
const size = 3;
elSelected.setAttribute("x", reg.n ? reg.x-size*2 : reg.x-size*3);
elSelected.setAttribute("width", reg.n ? size*4 : size*6);
}
function changeName() {
elSelected.dataset.name = regiment().name = this.value;
}
function restoreName() {
const reg = regiment(), regs = pack.states[elSelected.dataset.state].military;
const name = Military.getName(reg, regs);
elSelected.dataset.name = reg.name = document.getElementById("regimentName").value = name;
}
function changeUnit() {
const u = this.dataset.u;
const reg = regiment();
reg.u[u] = (+this.value)||0;
reg.a = d3.sum(Object.values(reg.u));
elSelected.nextSibling.innerHTML = reg.a;
if (militaryOverviewRefresh.offsetParent) militaryOverviewRefresh.click();
}
function splitRegiment() {
const reg = regiment(), u1 = reg.u;
const state = elSelected.dataset.state, military = pack.states[state].military;
const i = last(military).i + 1, u2 = Object.assign({}, u1); // u clone
Object.keys(u1).forEach(u => u1[u] = Math.ceil(u1[u]/2)); // halved old reg
Object.keys(u2).forEach(u => u2[u] = Math.floor(u2[u]/2)); // halved new reg
reg.a = d3.sum(Object.values(u1)); // old reg total
const a = d3.sum(Object.values(u2)); // new reg total
const newReg = {a, cell:reg.cell, i, n:reg.n, u:u2, x:reg.x, y:reg.y};
newReg.name = Military.getName(newReg, military);
military.push(newReg);
elSelected.parentNode.remove(); // undraw old reg
Military.drawRegiment(reg, state, reg.x, reg.y-6); // draw old reg above
Military.drawRegiment(newReg, state, reg.x, reg.y+6); // draw new reg below
$("#regimentEditor").dialog("close");
}
function toggleAdd() {
document.getElementById("regimentAdd").classList.toggle("pressed");
if (document.getElementById("regimentAdd").classList.contains("pressed")) {
viewbox.style("cursor", "crosshair").on("click", addRegimentOnClick);
tip("Click on map to create new regiment or fleet", true);
} else {
clearMainTip();
viewbox.on("click", clicked).style("cursor", "default");
}
}
function addRegimentOnClick() {
const point = d3.mouse(this);
const cell = findCell(point[0], point[1]);
const x = pack.cells.p[cell][0], y = pack.cells.p[cell][1];
const state = elSelected.dataset.state, military = pack.states[state].military;
const i = military.length ? last(military).i + 1 : 0;
const n = +(pack.cells.h[cell] < 20); // naval or land
const reg = {a:0, cell, i, n, u:{}, x, y};
reg.name = Military.getName(reg, military);
military.push(reg);
Military.drawRegiment(reg, state);
toggleAdd();
}
function toggleAttach() {
document.getElementById("regimentAttach").classList.toggle("pressed");
if (document.getElementById("regimentAttach").classList.contains("pressed")) {
viewbox.style("cursor", "crosshair").on("click", attachRegimentOnClick);
tip("Click on another regiment to unite both regiments. The current regiment will be removed", true);
armies.selectAll(":scope > g").classed("draggable", false);
} else {
clearMainTip();
armies.selectAll(":scope > g").classed("draggable", true);
viewbox.on("click", clicked).style("cursor", "default");
}
}
function attachRegimentOnClick() {
const target = d3.event.target, army = target.parentElement.parentElement;
if (army.parentElement.id !== "armies") {
tip("Please click on a regiment", false, "error");
return;
}
if (target === elSelected) {
tip("Cannot attach regiment to itself. Please click on another regiment", false, "error");
return;
}
if (army !== elSelected.parentElement.parentElement) {
tip("Cannot attach this regiment to regiment of other state", false, "error");
return;
};
const reg = regiment(); // reg to be attached
const sel = pack.states[target.dataset.state].military.find(r => r.i == target.dataset.id); // reg to attach to
for (const unit of options.military) {
const u = unit.name;
if (reg.u[u]) sel.u[u] ? sel.u[u] += reg.u[u] : sel.u[u] = reg.u[u];
}
sel.a = d3.sum(Object.values(sel.u)); // reg total
target.nextSibling.innerHTML = sel.a; // update selected reg total text
// remove attached regiment
const military = pack.states[elSelected.dataset.state].military;
military.splice(military.indexOf(reg), 1);
const index = notes.findIndex(n => n.id === elSelected.parentNode.id);
if (index != -1) notes.splice(index, 1);
elSelected.parentNode.remove();
$("#regimentEditor").dialog("close");
}
function regenerateLegend() {
const index = notes.findIndex(n => n.id === elSelected.parentNode.id);
if (index != -1) notes.splice(index, 1);
const s = pack.states[elSelected.dataset.state];
Military.generateNote(regiment(), s);
}
function editLegend() {
editNotes(elSelected.parentNode.id, regiment().name);
}
function removeRegiment() {
alertMessage.innerHTML = "Are you sure you want to remove the regiment?";
$("#alert").dialog({resizable: false, title: "Remove regiment",
buttons: {
Remove: function() {
$(this).dialog("close");
const military = pack.states[elSelected.dataset.state].military;
const regIndex = military.indexOf(regiment());
if (regIndex === -1) return;
military.splice(regIndex, 1);
const index = notes.findIndex(n => n.id === elSelected.parentNode.id);
if (index != -1) notes.splice(index, 1);
elSelected.parentNode.remove();
if (militaryOverviewRefresh.offsetParent) militaryOverviewRefresh.click();
$("#regimentEditor").dialog("close");
},
Cancel: function() {$(this).dialog("close");}
}
});
}
function dragRegiment() {
const tr = parseTransform(this.getAttribute("transform"));
const dx = +tr[0] - d3.event.x, dy = +tr[1] - d3.event.y;
d3.select(this).raise();
d3.select(this.parentNode).raise();
const self = elSelected.parentNode === this;
const baseLine = viewbox.select("g#regimentBase > line");
const x2 = +elSelected.nextSibling.getAttribute("x");
const y2 = +elSelected.nextSibling.getAttribute("y");
d3.event.on("drag", function() {
const x = dx + d3.event.x, y = dy + d3.event.y;
this.setAttribute("transform", `translate(${(x)},${(y)})`);
if (self) baseLine.attr("x2", x2+x).attr("y2", y2+y);
});
}
function dragBase() {
const tr = parseTransform(this.getAttribute("transform"));
const dx = +tr[0] - d3.event.x, dy = +tr[1] - d3.event.y;
const baseLine = viewbox.select("g#regimentBase > line");
d3.event.on("drag", function() {
const x = dx + d3.event.x, y = dy + d3.event.y;
this.setAttribute("transform", `translate(${(x)},${(y)})`);
baseLine.attr("x1", d3.event.x).attr("y1", d3.event.y);
});
d3.event.on("end", function() {
const reg = regiment();
const x = d3.event.x, y = d3.event.y, cell = findCell(x, y);
reg.cell = cell, reg.x = x, reg.y = y;
});
}
function closeEditor() {
armies.selectAll(":scope > g").classed("draggable", false);
armies.selectAll("g>g").call(d3.drag().on("drag", null));
viewbox.select("g#regimentBase").remove();
document.getElementById("regimentAdd").classList.remove("pressed");
document.getElementById("regimentAttach").classList.remove("pressed");
restoreDefaultEvents();
elSelected = null;
}
}

View file

@ -42,7 +42,7 @@ function editStates() {
const el = ev.target, cl = el.classList, line = el.parentNode, state = +line.dataset.id;
if (cl.contains("fillRect")) stateChangeFill(el); else
if (cl.contains("name")) editStateName(state); else
if (cl.contains("icon-fleur")) stateOpenCOA(ev, state); else
if (cl.contains("icon-coa")) stateOpenCOA(ev, state); else
if (cl.contains("icon-star-empty")) stateCapitalZoomIn(state); else
if (cl.contains("culturePopulation")) changePopulation(state); else
if (cl.contains("icon-pin")) focusOnState(state, cl); else
@ -90,7 +90,7 @@ function editStates() {
data-population=${population} data-burgs=${s.burgs} data-color="" data-form="" data-capital="" data-culture="" data-type="" data-expansionism="">
<svg width="9" height="9" class="placeholder"></svg>
<input data-tip="Neutral lands name. Click to change" class="stateName name pointer italic" value="${s.name}" readonly>
<span class="icon-fleur placeholder hide"></span>
<span class="icon-coa placeholder hide"></span>
<input class="stateForm placeholder" value="none">
<span class="icon-star-empty placeholder hide"></span>
<input class="stateCapital placeholder hide">
@ -114,7 +114,7 @@ function editStates() {
data-area=${area} data-population=${population} data-burgs=${s.burgs} data-culture=${pack.cultures[s.culture].name} data-type=${s.type} data-expansionism=${s.expansionism}>
<svg data-tip="State fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${s.color}" class="fillRect pointer"></svg>
<input data-tip="State name. Click to change" class="stateName name pointer" value="${s.name}" readonly>
<span data-tip="Click to open state COA in the Iron Arachne Heraldry Generator. Ctrl + click to change the seed" class="icon-fleur pointer hide"></span>
<span data-tip="Click to open state COA in the Iron Arachne Heraldry Generator. Ctrl + click to change the seed" class="icon-coa pointer hide"></span>
<input data-tip="State form name. Click to change" class="stateForm name pointer" value="${s.formName}" readonly>
<span data-tip="State capital. Click to zoom into view" class="icon-star-empty pointer hide"></span>
<input data-tip="Capital name. Click and type to rename" class="stateCapital hide" value="${capital}" autocorrect="off" spellcheck="false"/>

View file

@ -8,13 +8,13 @@ function editWorld() {
"Southern": () => applyPreset(33, 75),
"Restore Winds": restoreDefaultWinds
}, open: function() {
const buttons = $(this).dialog("widget").find(".ui-dialog-buttonset > button")
const buttons = $(this).dialog("widget").find(".ui-dialog-buttonset > button");
buttons[0].addEventListener("mousemove", () => tip("Click to set map size to cover the whole World"));
buttons[1].addEventListener("mousemove", () => tip("Click to set map size to cover the Northern latitudes"));
buttons[2].addEventListener("mousemove", () => tip("Click to set map size to cover the Tropical latitudes"));
buttons[3].addEventListener("mousemove", () => tip("Click to set map size to cover the Southern latitudes"));
buttons[4].addEventListener("mousemove", () => tip("Click to restore default wind directions"));
},
}
});
const globe = d3.select("#globe");
@ -56,6 +56,7 @@ function editWorld() {
if (layerIsOn("togglePrec")) drawPrec();
if (layerIsOn("toggleBiomes")) drawBiomes();
if (layerIsOn("toggleCoordinates")) drawCoordinates();
if (document.getElementById("canvas3d")) setTimeout(ThreeD.update(), 500);
}
function updateGlobePosition() {
@ -100,26 +101,26 @@ function editWorld() {
function updateWindDirections() {
globe.select("#globeWindArrows").selectAll("path").each(function(d, i) {
const tr = parseTransform(this.getAttribute("transform"));
this.setAttribute("transform", `rotate(${winds[i]} ${tr[1]} ${tr[2]})`);
this.setAttribute("transform", `rotate(${options.winds[i]} ${tr[1]} ${tr[2]})`);
});
}
function changeWind() {
const arrow = d3.event.target.nextElementSibling;
const tier = +arrow.dataset.tier;
winds[tier] = (winds[tier] + 45) % 360;
options.winds[tier] = (options.winds[tier] + 45) % 360;
const tr = parseTransform(arrow.getAttribute("transform"));
arrow.setAttribute("transform", `rotate(${winds[tier]} ${tr[1]} ${tr[2]})`);
localStorage.setItem("winds", winds);
arrow.setAttribute("transform", `rotate(${options.winds[tier]} ${tr[1]} ${tr[2]})`);
localStorage.setItem("winds", options.winds);
const mapTiers = d3.range(mapCoordinates.latN, mapCoordinates.latS, -30).map(c => (90-c) / 30 | 0);
if (mapTiers.includes(tier)) updateWorld();
}
function restoreDefaultWinds() {
const defaultWinds = [225, 45, 225, 315, 135, 315];
const mapTiers = d3.range(mapCoordinates.latN, mapCoordinates.latS, -30).map(c => (90-c) / 30 | 0);
const update = mapTiers.some(t => winds[t] != defaultWinds[t]);
winds = defaultWinds;
const update = mapTiers.some(t => options.winds[t] != defaultWinds[t]);
options.winds = defaultWinds;
updateWindDirections();
if (update) updateWorld();
}

View file

@ -232,6 +232,11 @@ function gauss(expected = 100, deviation = 30, min = 0, max = 300, round = 0) {
return rn(Math.max(Math.min(d3.randomNormal(expected, deviation)(), max), min), round);
}
// get integer from float as floor + P(fractional)
function Pint(float) {
return ~~float + +P(float % 1);
}
// round value to d decimals
function rn(v, d = 0) {
const m = Math.pow(10, d);
@ -403,6 +408,9 @@ function getAdjective(string) {
return trimVowels(string) + "ian";
}
// get ordinal out of integer: 1 => 1st
const nth = n => n+(["st","nd","rd"][((n+90)%100-10)%10-1]||"th");
// split string into 2 almost equal parts not breaking words
function splitInTwo(str) {
const half = str.length / 2;
@ -596,5 +604,9 @@ function isCtrlClick(event) {
return event.ctrlKey || event.metaKey;
}
function generateDate(from = 100, to = 1000) {
return new Date(rand(from, to),rand(12),rand(31)).toLocaleDateString("en", {year:'numeric', month:'long', day:'numeric'});
}
// localStorageDB
!function(){function e(t,o){return n?void(n.transaction("s").objectStore("s").get(t).onsuccess=function(e){var t=e.target.result&&e.target.result.v||null;o(t)}):void setTimeout(function(){e(t,o)},100)}var t=window.indexedDB||window.mozIndexedDB||window.webkitIndexedDB||window.msIndexedDB;if(!t)return void console.error("indexDB not supported");var n,o={k:"",v:""},r=t.open("d2",1);r.onsuccess=function(e){n=this.result},r.onerror=function(e){console.error("indexedDB request error"),console.log(e)},r.onupgradeneeded=function(e){n=null;var t=e.target.result.createObjectStore("s",{keyPath:"k"});t.transaction.oncomplete=function(e){n=e.target.db}},window.ldb={get:e,set:function(e,t){o.k=e,o.v=t,n.transaction("s","readwrite").objectStore("s").put(o)}}}();