mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-16 17:31:24 +01:00
241 lines
No EOL
10 KiB
JavaScript
241 lines
No EOL
10 KiB
JavaScript
(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};
|
|
|
|
}))); |