mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-17 09:41:24 +01:00
v1.3a
This commit is contained in:
parent
7c74c3d29f
commit
f4a84fc6d6
22 changed files with 1678 additions and 325 deletions
|
|
@ -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};
|
||||
|
||||
})));
|
||||
|
|
|
|||
241
modules/military-generator.js
Normal file
241
modules/military-generator.js
Normal 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};
|
||||
|
||||
})));
|
||||
|
|
@ -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";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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} </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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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"));
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
277
modules/ui/regiment-editor.js
Normal file
277
modules/ui/regiment-editor.js
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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"/>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)}}}();
|
||||
Loading…
Add table
Add a link
Reference in a new issue