mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2026-04-04 06:27:24 +02:00
refactor: migrate military generator (#1330)
* refactor: migrate military gnerator * fix: build + lint * Update src/modules/military-generator.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/modules/military-generator.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/modules/military-generator.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: clean up regiment notes and improve emblem retrieval logic * Update src/modules/military-generator.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
parent
43e35cdecb
commit
32227518d2
6 changed files with 658 additions and 436 deletions
|
|
@ -1,406 +0,0 @@
|
||||||
"use strict";
|
|
||||||
|
|
||||||
window.Military = (function () {
|
|
||||||
const generate = function () {
|
|
||||||
TIME && console.time("generateMilitary");
|
|
||||||
const {cells, states} = pack;
|
|
||||||
const {p} = cells;
|
|
||||||
const valid = states.filter(s => s.i && !s.removed); // valid states
|
|
||||||
if (!options.military) options.military = getDefaultOptions();
|
|
||||||
|
|
||||||
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: -0.2,
|
|
||||||
Friendly: -0.1,
|
|
||||||
Neutral: 0,
|
|
||||||
Suspicion: 0.1,
|
|
||||||
Enemy: 1,
|
|
||||||
Unknown: 0,
|
|
||||||
Rival: 0.5,
|
|
||||||
Vassal: 0.5,
|
|
||||||
Suzerain: -0.5
|
|
||||||
};
|
|
||||||
|
|
||||||
const stateModifier = {
|
|
||||||
melee: {Nomadic: 0.5, Highland: 1.2, Lake: 1, Naval: 0.7, Hunting: 1.2, River: 1.1},
|
|
||||||
ranged: {Nomadic: 0.9, Highland: 1.3, Lake: 1, Naval: 0.8, Hunting: 2, River: 0.8},
|
|
||||||
mounted: {Nomadic: 2.3, Highland: 0.6, Lake: 0.7, Naval: 0.3, Hunting: 0.7, River: 0.8},
|
|
||||||
machinery: {Nomadic: 0.8, Highland: 1.4, Lake: 1.1, Naval: 1.4, Hunting: 0.4, River: 1.1},
|
|
||||||
naval: {Nomadic: 0.5, Highland: 0.5, Lake: 1.2, Naval: 1.8, Hunting: 0.7, River: 1.2},
|
|
||||||
armored: {Nomadic: 1, Highland: 0.5, Lake: 1, Naval: 1, Hunting: 0.7, River: 1.1},
|
|
||||||
aviation: {Nomadic: 0.5, Highland: 0.5, Lake: 1.2, Naval: 1.2, Hunting: 0.6, River: 1.2},
|
|
||||||
magical: {Nomadic: 1, Highland: 2, Lake: 1, Naval: 1, Hunting: 1, River: 1}
|
|
||||||
};
|
|
||||||
|
|
||||||
const cellTypeModifier = {
|
|
||||||
nomadic: {
|
|
||||||
melee: 0.2,
|
|
||||||
ranged: 0.5,
|
|
||||||
mounted: 3,
|
|
||||||
machinery: 0.4,
|
|
||||||
naval: 0.3,
|
|
||||||
armored: 1.6,
|
|
||||||
aviation: 1,
|
|
||||||
magical: 0.5
|
|
||||||
},
|
|
||||||
wetland: {
|
|
||||||
melee: 0.8,
|
|
||||||
ranged: 2,
|
|
||||||
mounted: 0.3,
|
|
||||||
machinery: 1.2,
|
|
||||||
naval: 1.0,
|
|
||||||
armored: 0.2,
|
|
||||||
aviation: 0.5,
|
|
||||||
magical: 0.5
|
|
||||||
},
|
|
||||||
highland: {
|
|
||||||
melee: 1.2,
|
|
||||||
ranged: 1.6,
|
|
||||||
mounted: 0.3,
|
|
||||||
machinery: 3,
|
|
||||||
naval: 1.0,
|
|
||||||
armored: 0.8,
|
|
||||||
aviation: 0.3,
|
|
||||||
magical: 2
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const burgTypeModifier = {
|
|
||||||
nomadic: {
|
|
||||||
melee: 0.3,
|
|
||||||
ranged: 0.8,
|
|
||||||
mounted: 3,
|
|
||||||
machinery: 0.4,
|
|
||||||
naval: 1.0,
|
|
||||||
armored: 1.6,
|
|
||||||
aviation: 1,
|
|
||||||
magical: 0.5
|
|
||||||
},
|
|
||||||
wetland: {
|
|
||||||
melee: 1,
|
|
||||||
ranged: 1.6,
|
|
||||||
mounted: 0.2,
|
|
||||||
machinery: 1.2,
|
|
||||||
naval: 1.0,
|
|
||||||
armored: 0.2,
|
|
||||||
aviation: 0.5,
|
|
||||||
magical: 0.5
|
|
||||||
},
|
|
||||||
highland: {melee: 1.2, ranged: 2, mounted: 0.3, machinery: 3, naval: 1.0, armored: 0.8, aviation: 0.3, magical: 2}
|
|
||||||
};
|
|
||||||
|
|
||||||
valid.forEach(s => {
|
|
||||||
s.temp = {};
|
|
||||||
const d = s.diplomacy;
|
|
||||||
|
|
||||||
const expansionRate = minmax(s.expansionism / expn / (s.area / area), 0.25, 4); // how much state expansionism is realized
|
|
||||||
const diplomacyRate = d.some(d => d === "Enemy")
|
|
||||||
? 1
|
|
||||||
: d.some(d => d === "Rival")
|
|
||||||
? 0.8
|
|
||||||
: d.some(d => d === "Suspicion")
|
|
||||||
? 0.5
|
|
||||||
: 0.1; // peacefulness
|
|
||||||
const neighborsRateRaw = s.neighbors
|
|
||||||
.map(n => (n ? pack.states[n].diplomacy[s.i] : "Suspicion"))
|
|
||||||
.reduce((s, r) => (s += rate[r]), 0.5);
|
|
||||||
const neighborsRate = minmax(neighborsRateRaw, 0.3, 3); // neighbors rate
|
|
||||||
s.alert = minmax(rn(expansionRate * diplomacyRate * neighborsRate, 2), 0.1, 5); // alert rate (area modifier)
|
|
||||||
s.temp.platoons = [];
|
|
||||||
|
|
||||||
// apply overall state modifiers for unit types based on state features
|
|
||||||
for (const unit of options.military) {
|
|
||||||
if (!stateModifier[unit.type]) continue;
|
|
||||||
|
|
||||||
let modifier = stateModifier[unit.type][s.type] || 1;
|
|
||||||
if (unit.type === "mounted" && s.formName.includes("Horde")) modifier *= 2;
|
|
||||||
else if (unit.type === "naval" && s.form === "Republic") modifier *= 1.2;
|
|
||||||
s.temp[unit.name] = modifier * s.alert;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const getType = cell => {
|
|
||||||
if ([1, 2, 3, 4].includes(cells.biome[cell])) return "nomadic";
|
|
||||||
if ([7, 8, 9, 12].includes(cells.biome[cell])) return "wetland";
|
|
||||||
if (cells.h[cell] >= 70) return "highland";
|
|
||||||
return "generic";
|
|
||||||
};
|
|
||||||
|
|
||||||
function passUnitLimits(unit, biome, state, culture, religion) {
|
|
||||||
if (unit.biomes && !unit.biomes.includes(biome)) return false;
|
|
||||||
if (unit.states && !unit.states.includes(state)) return false;
|
|
||||||
if (unit.cultures && !unit.cultures.includes(culture)) return false;
|
|
||||||
if (unit.religions && !unit.religions.includes(religion)) return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// rural cells
|
|
||||||
for (const i of cells.i) {
|
|
||||||
if (!cells.pop[i]) continue;
|
|
||||||
|
|
||||||
const biome = cells.biome[i];
|
|
||||||
const state = cells.state[i];
|
|
||||||
const culture = cells.culture[i];
|
|
||||||
const religion = cells.religion[i];
|
|
||||||
|
|
||||||
const stateObj = states[state];
|
|
||||||
if (!state || stateObj.removed) continue;
|
|
||||||
|
|
||||||
let modifier = cells.pop[i] / 100; // basic rural army in percentages
|
|
||||||
if (culture !== stateObj.culture) modifier = stateObj.form === "Union" ? modifier / 1.2 : modifier / 2; // non-dominant culture
|
|
||||||
if (religion !== cells.religion[stateObj.center])
|
|
||||||
modifier = stateObj.form === "Theocracy" ? modifier / 2.2 : modifier / 1.4; // non-dominant religion
|
|
||||||
if (cells.f[i] !== cells.f[stateObj.center])
|
|
||||||
modifier = stateObj.type === "Naval" ? modifier / 1.2 : modifier / 1.8; // different landmass
|
|
||||||
const type = getType(i);
|
|
||||||
|
|
||||||
for (const unit of options.military) {
|
|
||||||
const perc = +unit.rural;
|
|
||||||
if (isNaN(perc) || perc <= 0 || !stateObj.temp[unit.name]) continue;
|
|
||||||
if (!passUnitLimits(unit, biome, state, culture, religion)) continue;
|
|
||||||
if (unit.type === "naval" && !cells.haven[i]) continue; // only near-ocean cells create naval units
|
|
||||||
|
|
||||||
const cellTypeMod = type === "generic" ? 1 : cellTypeModifier[type][unit.type]; // cell specific modifier
|
|
||||||
const army = modifier * perc * cellTypeMod; // rural cell army
|
|
||||||
const total = rn(army * stateObj.temp[unit.name] * populationRate); // total troops
|
|
||||||
if (!total) continue;
|
|
||||||
|
|
||||||
let [x, y] = p[i];
|
|
||||||
let n = 0;
|
|
||||||
|
|
||||||
// place naval units to sea
|
|
||||||
if (unit.type === "naval") {
|
|
||||||
const haven = cells.haven[i];
|
|
||||||
[x, y] = p[haven];
|
|
||||||
n = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
stateObj.temp.platoons.push({
|
|
||||||
cell: i,
|
|
||||||
a: total,
|
|
||||||
t: total,
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
u: unit.name,
|
|
||||||
n,
|
|
||||||
s: unit.separate,
|
|
||||||
type: unit.type
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// burgs
|
|
||||||
for (const b of pack.burgs) {
|
|
||||||
if (!b.i || b.removed || !b.state || !b.population) continue;
|
|
||||||
|
|
||||||
const biome = cells.biome[b.cell];
|
|
||||||
const state = b.state;
|
|
||||||
const culture = b.culture;
|
|
||||||
const religion = cells.religion[b.cell];
|
|
||||||
|
|
||||||
const stateObj = states[state];
|
|
||||||
let m = (b.population * urbanization) / 100; // basic urban army in percentages
|
|
||||||
if (b.capital) m *= 1.2; // capital has household troops
|
|
||||||
if (culture !== stateObj.culture) m = stateObj.form === "Union" ? m / 1.2 : m / 2; // non-dominant culture
|
|
||||||
if (religion !== cells.religion[stateObj.center]) m = stateObj.form === "Theocracy" ? m / 2.2 : m / 1.4; // non-dominant religion
|
|
||||||
if (cells.f[b.cell] !== cells.f[stateObj.center]) m = stateObj.type === "Naval" ? m / 1.2 : m / 1.8; // different landmass
|
|
||||||
const type = getType(b.cell);
|
|
||||||
|
|
||||||
for (const unit of options.military) {
|
|
||||||
const perc = +unit.urban;
|
|
||||||
if (isNaN(perc) || perc <= 0 || !stateObj.temp[unit.name]) continue;
|
|
||||||
if (!passUnitLimits(unit, biome, state, culture, religion)) continue;
|
|
||||||
if (unit.type === "naval" && (!b.port || !cells.haven[b.cell])) continue; // only ports create naval units
|
|
||||||
|
|
||||||
const mod = type === "generic" ? 1 : burgTypeModifier[type][unit.type]; // cell specific modifier
|
|
||||||
const army = m * perc * mod; // urban cell army
|
|
||||||
const total = rn(army * stateObj.temp[unit.name] * populationRate); // total troops
|
|
||||||
if (!total) continue;
|
|
||||||
|
|
||||||
let [x, y] = p[b.cell];
|
|
||||||
let n = 0;
|
|
||||||
|
|
||||||
// place naval to sea
|
|
||||||
if (unit.type === "naval") {
|
|
||||||
const haven = cells.haven[b.cell];
|
|
||||||
[x, y] = p[haven];
|
|
||||||
n = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
stateObj.temp.platoons.push({
|
|
||||||
cell: b.cell,
|
|
||||||
a: total,
|
|
||||||
t: total,
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
u: unit.name,
|
|
||||||
n,
|
|
||||||
s: unit.separate,
|
|
||||||
type: unit.type
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const expected = 3 * populationRate; // expected regiment size
|
|
||||||
const mergeable = (n0, n1) => (!n0.s && !n1.s) || n0.u === n1.u; // check if regiments can be merged
|
|
||||||
|
|
||||||
// remove all existing regiment notes before regenerating
|
|
||||||
for (let i = notes.length - 1; i >= 0; i--) {
|
|
||||||
if (notes[i].id.startsWith("regiment")) notes.splice(i, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// get regiments for each state
|
|
||||||
valid.forEach(s => {
|
|
||||||
s.military = createRegiments(s.temp.platoons, s);
|
|
||||||
delete s.temp; // do not store temp data
|
|
||||||
});
|
|
||||||
|
|
||||||
function createRegiments(nodes, s) {
|
|
||||||
if (!nodes.length) return [];
|
|
||||||
|
|
||||||
nodes.sort((a, b) => a.a - b.a); // form regiments in cells with most troops
|
|
||||||
const tree = d3.quadtree(
|
|
||||||
nodes,
|
|
||||||
d => d.x,
|
|
||||||
d => d.y
|
|
||||||
);
|
|
||||||
|
|
||||||
nodes.forEach(node => {
|
|
||||||
tree.remove(node);
|
|
||||||
const overlap = tree.find(node.x, node.y, 20);
|
|
||||||
if (overlap && overlap.t && mergeable(node, overlap)) {
|
|
||||||
merge(node, overlap);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (node.t > expected) return;
|
|
||||||
const r = (expected - node.t) / (node.s ? 40 : 20); // search radius
|
|
||||||
const candidates = findAllInQuadtree(node.x, node.y, r, tree);
|
|
||||||
for (const c of candidates) {
|
|
||||||
if (c.t < expected && mergeable(node, c)) {
|
|
||||||
merge(node, 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
|
|
||||||
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, bx: r.x, by: r.y, u, n: r.n, name, state: s.i};
|
|
||||||
});
|
|
||||||
|
|
||||||
// generate name for regiments
|
|
||||||
regiments.forEach(r => {
|
|
||||||
r.name = getName(r, regiments);
|
|
||||||
r.icon = getEmblem(r);
|
|
||||||
generateNote(r, s);
|
|
||||||
});
|
|
||||||
|
|
||||||
return regiments;
|
|
||||||
}
|
|
||||||
|
|
||||||
TIME && console.timeEnd("generateMilitary");
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDefaultOptions = function () {
|
|
||||||
return [
|
|
||||||
{icon: "⚔️", name: "infantry", rural: 0.25, urban: 0.2, crew: 1, power: 1, type: "melee", separate: 0},
|
|
||||||
{icon: "🏹", name: "archers", rural: 0.12, urban: 0.2, crew: 1, power: 1, type: "ranged", separate: 0},
|
|
||||||
{icon: "🐴", name: "cavalry", rural: 0.12, urban: 0.03, crew: 2, power: 2, type: "mounted", separate: 0},
|
|
||||||
{icon: "💣", name: "artillery", rural: 0, urban: 0.03, crew: 8, power: 12, type: "machinery", separate: 0},
|
|
||||||
{icon: "🌊", name: "fleet", rural: 0, urban: 0.015, crew: 100, power: 50, type: "naval", separate: 1}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
// utilize si function to make regiment total text fit regiment box
|
|
||||||
const getTotal = reg => (reg.a > (reg.n ? 999 : 99999) ? si(reg.a) : reg.a);
|
|
||||||
|
|
||||||
const getName = function (r, regiments) {
|
|
||||||
const cells = pack.cells;
|
|
||||||
const proper = r.n
|
|
||||||
? null
|
|
||||||
: cells.province[r.cell] && pack.provinces[cells.province[r.cell]]
|
|
||||||
? pack.provinces[cells.province[r.cell]].name
|
|
||||||
: cells.burg[r.cell] && pack.burgs[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}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// get default regiment emblem
|
|
||||||
const getEmblem = function (r) {
|
|
||||||
if (!r.n && !Object.values(r.u).length) return "🔰"; // "Newbie" regiment without troops
|
|
||||||
if (
|
|
||||||
!r.n &&
|
|
||||||
pack.states[r.state].form === "Monarchy" &&
|
|
||||||
pack.cells.burg[r.cell] &&
|
|
||||||
pack.burgs[pack.cells.burg[r.cell]].capital
|
|
||||||
)
|
|
||||||
return "👑"; // "Royal" regiment based in capital
|
|
||||||
const mainUnit = Object.entries(r.u).sort((a, b) => b[1] - a[1])[0][0]; // unit with more troops in regiment
|
|
||||||
const unit = options.military.find(u => u.name === mainUnit);
|
|
||||||
return unit.icon;
|
|
||||||
};
|
|
||||||
|
|
||||||
const generateNote = function (r, s) {
|
|
||||||
const cells = pack.cells;
|
|
||||||
const base =
|
|
||||||
cells.burg[r.cell] && pack.burgs[cells.burg[r.cell]]
|
|
||||||
? pack.burgs[cells.burg[r.cell]].name
|
|
||||||
: cells.province[r.cell] && pack.provinces[cells.province[r.cell]]
|
|
||||||
? pack.provinces[cells.province[r.cell]].fullName
|
|
||||||
: null;
|
|
||||||
const station = base ? `${r.name} is ${r.n ? "based" : "stationed"} in ${base}. ` : "";
|
|
||||||
|
|
||||||
const composition = r.a
|
|
||||||
? Object.keys(r.u)
|
|
||||||
.map(t => `— ${t}: ${r.u[t]}`)
|
|
||||||
.join("\r\n")
|
|
||||||
: null;
|
|
||||||
const troops = composition
|
|
||||||
? `\r\n\r\nRegiment composition in ${options.year} ${options.eraShort}:\r\n${composition}.`
|
|
||||||
: "";
|
|
||||||
|
|
||||||
const campaign = s.campaigns ? ra(s.campaigns) : null;
|
|
||||||
const year = campaign
|
|
||||||
? rand(campaign.start, campaign.end || options.year)
|
|
||||||
: gauss(options.year - 100, 150, 1, options.year - 6);
|
|
||||||
const conflict = campaign ? ` during the ${campaign.name}` : "";
|
|
||||||
const legend = `Regiment was formed in ${year} ${options.era}${conflict}. ${station}${troops}`;
|
|
||||||
const id = `regiment${s.i}-${r.i}`;
|
|
||||||
const existing = notes.find(n => n.id === id);
|
|
||||||
if (existing) {
|
|
||||||
existing.name = r.name;
|
|
||||||
existing.legend = legend;
|
|
||||||
} else {
|
|
||||||
notes.push({id, name: r.name, legend});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
generate,
|
|
||||||
getDefaultOptions,
|
|
||||||
getName,
|
|
||||||
generateNote,
|
|
||||||
getTotal,
|
|
||||||
getEmblem
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
@ -8534,7 +8534,6 @@
|
||||||
|
|
||||||
<script defer src="config/heightmap-templates.js"></script>
|
<script defer src="config/heightmap-templates.js"></script>
|
||||||
<script defer src="config/precreated-heightmaps.js"></script>
|
<script defer src="config/precreated-heightmaps.js"></script>
|
||||||
<script defer src="modules/military-generator.js?v=1.112.3"></script>
|
|
||||||
<script defer src="modules/resample.js?v=1.112.1"></script>
|
<script defer src="modules/resample.js?v=1.112.1"></script>
|
||||||
<script defer src="libs/alea.min.js?v1.105.0"></script>
|
<script defer src="libs/alea.min.js?v1.105.0"></script>
|
||||||
<script defer src="libs/polylabel.min.js?v1.105.0"></script>
|
<script defer src="libs/polylabel.min.js?v1.105.0"></script>
|
||||||
|
|
|
||||||
|
|
@ -15,5 +15,6 @@ import "./religions-generator";
|
||||||
import "./provinces-generator";
|
import "./provinces-generator";
|
||||||
import "./emblem";
|
import "./emblem";
|
||||||
import "./ice";
|
import "./ice";
|
||||||
|
import "./military-generator";
|
||||||
import "./markers-generator";
|
import "./markers-generator";
|
||||||
import "./fonts";
|
import "./fonts";
|
||||||
|
|
|
||||||
630
src/modules/military-generator.ts
Normal file
630
src/modules/military-generator.ts
Normal file
|
|
@ -0,0 +1,630 @@
|
||||||
|
import { quadtree, sum } from "d3";
|
||||||
|
import {
|
||||||
|
findAllInQuadtree,
|
||||||
|
gauss,
|
||||||
|
minmax,
|
||||||
|
nth,
|
||||||
|
ra,
|
||||||
|
rand,
|
||||||
|
rn,
|
||||||
|
si,
|
||||||
|
} from "../utils";
|
||||||
|
import type { State } from "./states-generator";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
var Military: MilitaryModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MilitaryRegiment {
|
||||||
|
i: number;
|
||||||
|
t: number; // total troops
|
||||||
|
name: string;
|
||||||
|
a: number; // regiment army
|
||||||
|
s: number; // separate flag (for fleets)
|
||||||
|
cell: number;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
bx: number; // base x (for movement)
|
||||||
|
by: number; // base y (for movement)
|
||||||
|
u: Record<string, number>; // units composition
|
||||||
|
n: number; // naval unit flag
|
||||||
|
type: string; // unit type
|
||||||
|
icon?: string;
|
||||||
|
children?: MilitaryRegiment[]; // merged regiments
|
||||||
|
state: number;
|
||||||
|
angle?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Platoon {
|
||||||
|
cell: number;
|
||||||
|
a: number; // platoon army
|
||||||
|
t: number; // total troops in platoon
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
u: string; // unit type
|
||||||
|
n: number; // naval unit flag
|
||||||
|
s: number; // separate flag (for fleets)
|
||||||
|
type: string; // unit type
|
||||||
|
children?: Platoon[]; // merged platoons
|
||||||
|
}
|
||||||
|
|
||||||
|
class MilitaryModule {
|
||||||
|
generate() {
|
||||||
|
TIME && console.time("generateMilitary");
|
||||||
|
const { cells, states } = pack;
|
||||||
|
const { p } = cells;
|
||||||
|
const valid = states.filter((s) => s.i && !s.removed); // valid states
|
||||||
|
if (!options.military) options.military = this.getDefaultOptions();
|
||||||
|
|
||||||
|
const expn = sum(valid.map((s) => s.expansionism)); // total expansion
|
||||||
|
const area = sum(valid.map((s) => s.area)); // total area
|
||||||
|
const rate = {
|
||||||
|
x: 0,
|
||||||
|
Ally: -0.2,
|
||||||
|
Friendly: -0.1,
|
||||||
|
Neutral: 0,
|
||||||
|
Suspicion: 0.1,
|
||||||
|
Enemy: 1,
|
||||||
|
Unknown: 0,
|
||||||
|
Rival: 0.5,
|
||||||
|
Vassal: 0.5,
|
||||||
|
Suzerain: -0.5,
|
||||||
|
};
|
||||||
|
|
||||||
|
const stateModifier = {
|
||||||
|
melee: {
|
||||||
|
Nomadic: 0.5,
|
||||||
|
Highland: 1.2,
|
||||||
|
Lake: 1,
|
||||||
|
Naval: 0.7,
|
||||||
|
Hunting: 1.2,
|
||||||
|
River: 1.1,
|
||||||
|
},
|
||||||
|
ranged: {
|
||||||
|
Nomadic: 0.9,
|
||||||
|
Highland: 1.3,
|
||||||
|
Lake: 1,
|
||||||
|
Naval: 0.8,
|
||||||
|
Hunting: 2,
|
||||||
|
River: 0.8,
|
||||||
|
},
|
||||||
|
mounted: {
|
||||||
|
Nomadic: 2.3,
|
||||||
|
Highland: 0.6,
|
||||||
|
Lake: 0.7,
|
||||||
|
Naval: 0.3,
|
||||||
|
Hunting: 0.7,
|
||||||
|
River: 0.8,
|
||||||
|
},
|
||||||
|
machinery: {
|
||||||
|
Nomadic: 0.8,
|
||||||
|
Highland: 1.4,
|
||||||
|
Lake: 1.1,
|
||||||
|
Naval: 1.4,
|
||||||
|
Hunting: 0.4,
|
||||||
|
River: 1.1,
|
||||||
|
},
|
||||||
|
naval: {
|
||||||
|
Nomadic: 0.5,
|
||||||
|
Highland: 0.5,
|
||||||
|
Lake: 1.2,
|
||||||
|
Naval: 1.8,
|
||||||
|
Hunting: 0.7,
|
||||||
|
River: 1.2,
|
||||||
|
},
|
||||||
|
armored: {
|
||||||
|
Nomadic: 1,
|
||||||
|
Highland: 0.5,
|
||||||
|
Lake: 1,
|
||||||
|
Naval: 1,
|
||||||
|
Hunting: 0.7,
|
||||||
|
River: 1.1,
|
||||||
|
},
|
||||||
|
aviation: {
|
||||||
|
Nomadic: 0.5,
|
||||||
|
Highland: 0.5,
|
||||||
|
Lake: 1.2,
|
||||||
|
Naval: 1.2,
|
||||||
|
Hunting: 0.6,
|
||||||
|
River: 1.2,
|
||||||
|
},
|
||||||
|
magical: {
|
||||||
|
Nomadic: 1,
|
||||||
|
Highland: 2,
|
||||||
|
Lake: 1,
|
||||||
|
Naval: 1,
|
||||||
|
Hunting: 1,
|
||||||
|
River: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const cellTypeModifier = {
|
||||||
|
nomadic: {
|
||||||
|
melee: 0.2,
|
||||||
|
ranged: 0.5,
|
||||||
|
mounted: 3,
|
||||||
|
machinery: 0.4,
|
||||||
|
naval: 0.3,
|
||||||
|
armored: 1.6,
|
||||||
|
aviation: 1,
|
||||||
|
magical: 0.5,
|
||||||
|
},
|
||||||
|
wetland: {
|
||||||
|
melee: 0.8,
|
||||||
|
ranged: 2,
|
||||||
|
mounted: 0.3,
|
||||||
|
machinery: 1.2,
|
||||||
|
naval: 1.0,
|
||||||
|
armored: 0.2,
|
||||||
|
aviation: 0.5,
|
||||||
|
magical: 0.5,
|
||||||
|
},
|
||||||
|
highland: {
|
||||||
|
melee: 1.2,
|
||||||
|
ranged: 1.6,
|
||||||
|
mounted: 0.3,
|
||||||
|
machinery: 3,
|
||||||
|
naval: 1.0,
|
||||||
|
armored: 0.8,
|
||||||
|
aviation: 0.3,
|
||||||
|
magical: 2,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const burgTypeModifier = {
|
||||||
|
nomadic: {
|
||||||
|
melee: 0.3,
|
||||||
|
ranged: 0.8,
|
||||||
|
mounted: 3,
|
||||||
|
machinery: 0.4,
|
||||||
|
naval: 1.0,
|
||||||
|
armored: 1.6,
|
||||||
|
aviation: 1,
|
||||||
|
magical: 0.5,
|
||||||
|
},
|
||||||
|
wetland: {
|
||||||
|
melee: 1,
|
||||||
|
ranged: 1.6,
|
||||||
|
mounted: 0.2,
|
||||||
|
machinery: 1.2,
|
||||||
|
naval: 1.0,
|
||||||
|
armored: 0.2,
|
||||||
|
aviation: 0.5,
|
||||||
|
magical: 0.5,
|
||||||
|
},
|
||||||
|
highland: {
|
||||||
|
melee: 1.2,
|
||||||
|
ranged: 2,
|
||||||
|
mounted: 0.3,
|
||||||
|
machinery: 3,
|
||||||
|
naval: 1.0,
|
||||||
|
armored: 0.8,
|
||||||
|
aviation: 0.3,
|
||||||
|
magical: 2,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
valid.forEach((s) => {
|
||||||
|
s.temp = {};
|
||||||
|
const d = s.diplomacy!;
|
||||||
|
|
||||||
|
const expansionRate = minmax(
|
||||||
|
s.expansionism / expn / (s.area! / area),
|
||||||
|
0.25,
|
||||||
|
4,
|
||||||
|
); // how much state expansionism is realized
|
||||||
|
const diplomacyRate = d.some((d) => d === "Enemy")
|
||||||
|
? 1
|
||||||
|
: d.some((d) => d === "Rival")
|
||||||
|
? 0.8
|
||||||
|
: d.some((d) => d === "Suspicion")
|
||||||
|
? 0.5
|
||||||
|
: 0.1; // peacefulness
|
||||||
|
const neighborsRateRaw = s
|
||||||
|
.neighbors!.map((n) =>
|
||||||
|
n ? pack.states[n].diplomacy![s.i] : "Suspicion",
|
||||||
|
)
|
||||||
|
.reduce((s, r) => s + rate[r as keyof typeof rate], 0.5);
|
||||||
|
const neighborsRate = minmax(neighborsRateRaw, 0.3, 3); // neighbors rate
|
||||||
|
s.alert = minmax(
|
||||||
|
rn(expansionRate * diplomacyRate * neighborsRate, 2),
|
||||||
|
0.1,
|
||||||
|
5,
|
||||||
|
); // alert rate (area modifier)
|
||||||
|
s.temp.platoons = [];
|
||||||
|
|
||||||
|
// apply overall state modifiers for unit types based on state features
|
||||||
|
for (const unit of options.military) {
|
||||||
|
if (!stateModifier[unit.type as keyof typeof stateModifier]) continue;
|
||||||
|
|
||||||
|
let modifier =
|
||||||
|
stateModifier[unit.type as keyof typeof stateModifier][
|
||||||
|
s.type as keyof (typeof stateModifier)[keyof typeof stateModifier]
|
||||||
|
] || 1;
|
||||||
|
if (unit.type === "mounted" && s.formName!.includes("Horde"))
|
||||||
|
modifier *= 2;
|
||||||
|
else if (unit.type === "naval" && s.form === "Republic")
|
||||||
|
modifier *= 1.2;
|
||||||
|
s.temp[unit.name] = modifier * s.alert;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const getType = (cell: number) => {
|
||||||
|
if ([1, 2, 3, 4].includes(cells.biome[cell])) return "nomadic";
|
||||||
|
if ([7, 8, 9, 12].includes(cells.biome[cell])) return "wetland";
|
||||||
|
if (cells.h[cell] >= 70) return "highland";
|
||||||
|
return "generic";
|
||||||
|
};
|
||||||
|
|
||||||
|
function passUnitLimits(
|
||||||
|
unit: any,
|
||||||
|
biome: number,
|
||||||
|
state: number,
|
||||||
|
culture: number,
|
||||||
|
religion: number,
|
||||||
|
) {
|
||||||
|
if (unit.biomes && !unit.biomes.includes(biome)) return false;
|
||||||
|
if (unit.states && !unit.states.includes(state)) return false;
|
||||||
|
if (unit.cultures && !unit.cultures.includes(culture)) return false;
|
||||||
|
if (unit.religions && !unit.religions.includes(religion)) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// rural cells
|
||||||
|
for (const i of cells.i) {
|
||||||
|
if (!cells.pop[i]) continue;
|
||||||
|
|
||||||
|
const biome = cells.biome[i];
|
||||||
|
const state = cells.state[i];
|
||||||
|
const culture = cells.culture[i];
|
||||||
|
const religion = cells.religion[i];
|
||||||
|
|
||||||
|
const stateObj = states[state];
|
||||||
|
if (!state || stateObj.removed) continue;
|
||||||
|
|
||||||
|
let modifier = cells.pop[i] / 100; // basic rural army in percentages
|
||||||
|
if (culture !== stateObj.culture)
|
||||||
|
modifier = stateObj.form === "Union" ? modifier / 1.2 : modifier / 2; // non-dominant culture
|
||||||
|
if (religion !== cells.religion[stateObj.center])
|
||||||
|
modifier =
|
||||||
|
stateObj.form === "Theocracy" ? modifier / 2.2 : modifier / 1.4; // non-dominant religion
|
||||||
|
if (cells.f[i] !== cells.f[stateObj.center])
|
||||||
|
modifier = stateObj.type === "Naval" ? modifier / 1.2 : modifier / 1.8; // different landmass
|
||||||
|
const type = getType(i);
|
||||||
|
|
||||||
|
for (const unit of options.military) {
|
||||||
|
const perc = +unit.rural;
|
||||||
|
if (Number.isNaN(perc) || perc <= 0 || !stateObj.temp[unit.name])
|
||||||
|
continue;
|
||||||
|
if (!passUnitLimits(unit, biome, state, culture, religion)) continue;
|
||||||
|
if (unit.type === "naval" && !cells.haven[i]) continue; // only near-ocean cells create naval units
|
||||||
|
|
||||||
|
const cellTypeMod =
|
||||||
|
type === "generic"
|
||||||
|
? 1
|
||||||
|
: cellTypeModifier[type as keyof typeof cellTypeModifier][
|
||||||
|
unit.type as keyof (typeof cellTypeModifier)[keyof typeof cellTypeModifier]
|
||||||
|
]; // cell specific modifier
|
||||||
|
const army = modifier * perc * cellTypeMod; // rural cell army
|
||||||
|
const total = rn(army * stateObj.temp[unit.name] * populationRate); // total troops
|
||||||
|
if (!total) continue;
|
||||||
|
|
||||||
|
let [x, y] = p[i];
|
||||||
|
let n = 0;
|
||||||
|
|
||||||
|
// place naval units to sea
|
||||||
|
if (unit.type === "naval") {
|
||||||
|
const haven = cells.haven[i];
|
||||||
|
[x, y] = p[haven];
|
||||||
|
n = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
stateObj.temp.platoons.push({
|
||||||
|
cell: i,
|
||||||
|
a: total,
|
||||||
|
t: total,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
u: unit.name,
|
||||||
|
n,
|
||||||
|
s: unit.separate,
|
||||||
|
type: unit.type,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// burgs
|
||||||
|
for (const b of pack.burgs) {
|
||||||
|
if (!b.i || b.removed || !b.state || !b.population) continue;
|
||||||
|
|
||||||
|
const biome = cells.biome[b.cell];
|
||||||
|
const state = b.state;
|
||||||
|
const culture = b.culture;
|
||||||
|
const religion = cells.religion[b.cell];
|
||||||
|
|
||||||
|
const stateObj = states[state];
|
||||||
|
let m = (b.population * urbanization) / 100; // basic urban army in percentages
|
||||||
|
if (b.capital) m *= 1.2; // capital has household troops
|
||||||
|
if (culture !== stateObj.culture)
|
||||||
|
m = stateObj.form === "Union" ? m / 1.2 : m / 2; // non-dominant culture
|
||||||
|
if (religion !== cells.religion[stateObj.center])
|
||||||
|
m = stateObj.form === "Theocracy" ? m / 2.2 : m / 1.4; // non-dominant religion
|
||||||
|
if (cells.f[b.cell] !== cells.f[stateObj.center])
|
||||||
|
m = stateObj.type === "Naval" ? m / 1.2 : m / 1.8; // different landmass
|
||||||
|
const type = getType(b.cell);
|
||||||
|
|
||||||
|
for (const unit of options.military) {
|
||||||
|
const perc = +unit.urban;
|
||||||
|
if (Number.isNaN(perc) || perc <= 0 || !stateObj.temp[unit.name])
|
||||||
|
continue;
|
||||||
|
if (!passUnitLimits(unit, biome, state, culture!, religion)) continue;
|
||||||
|
if (unit.type === "naval" && (!b.port || !cells.haven[b.cell]))
|
||||||
|
continue; // only ports create naval units
|
||||||
|
|
||||||
|
const mod =
|
||||||
|
type === "generic"
|
||||||
|
? 1
|
||||||
|
: burgTypeModifier[type as keyof typeof burgTypeModifier][
|
||||||
|
unit.type as keyof (typeof burgTypeModifier)[keyof typeof burgTypeModifier]
|
||||||
|
]; // cell specific modifier
|
||||||
|
const army = m * perc * mod; // urban cell army
|
||||||
|
const total = rn(army * stateObj.temp[unit.name] * populationRate); // total troops
|
||||||
|
if (!total) continue;
|
||||||
|
|
||||||
|
let [x, y] = p[b.cell];
|
||||||
|
let n = 0;
|
||||||
|
|
||||||
|
// place naval to sea
|
||||||
|
if (unit.type === "naval") {
|
||||||
|
const haven = cells.haven[b.cell];
|
||||||
|
[x, y] = p[haven];
|
||||||
|
n = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
stateObj.temp.platoons.push({
|
||||||
|
cell: b.cell,
|
||||||
|
a: total,
|
||||||
|
t: total,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
u: unit.name,
|
||||||
|
n,
|
||||||
|
s: unit.separate,
|
||||||
|
type: unit.type,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const expected = 3 * populationRate; // expected regiment size
|
||||||
|
const mergeable = (
|
||||||
|
n0: { s: number; u: string },
|
||||||
|
n1: { s: number; u: string },
|
||||||
|
) => (!n0.s && !n1.s) || n0.u === n1.u; // check if regiments can be merged
|
||||||
|
const createRegiments = (
|
||||||
|
nodes: Platoon[],
|
||||||
|
s: State,
|
||||||
|
): MilitaryRegiment[] => {
|
||||||
|
if (!nodes.length) return [];
|
||||||
|
nodes.sort((a, b) => a.a - b.a); // form regiments starting from cells with fewest troops
|
||||||
|
const tree = quadtree(
|
||||||
|
nodes,
|
||||||
|
(d) => d.x,
|
||||||
|
(d) => d.y,
|
||||||
|
);
|
||||||
|
|
||||||
|
// add n0 to n1's ultimate parent
|
||||||
|
const merge = (
|
||||||
|
n0: { s: number; u: string; t: number; children?: any[] },
|
||||||
|
n1: { s: number; u: string; t: number; children?: any[] },
|
||||||
|
) => {
|
||||||
|
if (!n1.children) n1.children = [n0];
|
||||||
|
else n1.children.push(n0);
|
||||||
|
if (n0.children)
|
||||||
|
n0.children.forEach((n) => {
|
||||||
|
n1.children!.push(n);
|
||||||
|
});
|
||||||
|
n1.t += n0.t;
|
||||||
|
n0.t = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
tree.remove(node);
|
||||||
|
const overlap = tree.find(node.x, node.y, 20);
|
||||||
|
if (overlap && overlap.t && mergeable(node, overlap)) {
|
||||||
|
merge(node, overlap);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (node.t > expected) return;
|
||||||
|
const r = (expected - node.t) / (node.s ? 40 : 20); // search radius
|
||||||
|
const candidates = findAllInQuadtree(node.x, node.y, r, tree);
|
||||||
|
for (const c of candidates) {
|
||||||
|
if (c.t < expected && mergeable(node, c)) {
|
||||||
|
merge(node, c);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// parse regiments data
|
||||||
|
const regiments: Omit<MilitaryRegiment, "s" | "t" | "type">[] = nodes
|
||||||
|
.filter((n) => n.t)
|
||||||
|
.sort((a, b) => b.t - a.t)
|
||||||
|
.map((r, i) => {
|
||||||
|
const u: Record<string, number> = {};
|
||||||
|
u[r.u] = r.a;
|
||||||
|
(r.children ?? []).forEach((n) => {
|
||||||
|
u[n.u] = (u[n.u] ?? 0) + n.a;
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
i,
|
||||||
|
a: r.t,
|
||||||
|
cell: r.cell,
|
||||||
|
x: r.x,
|
||||||
|
y: r.y,
|
||||||
|
bx: r.x,
|
||||||
|
by: r.y,
|
||||||
|
u,
|
||||||
|
n: r.n,
|
||||||
|
name: "",
|
||||||
|
state: s.i,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// generate name for regiments
|
||||||
|
regiments.forEach((r: Omit<MilitaryRegiment, "s" | "t" | "type">) => {
|
||||||
|
r.name = this.getName(
|
||||||
|
r as MilitaryRegiment,
|
||||||
|
regiments as MilitaryRegiment[],
|
||||||
|
);
|
||||||
|
r.icon = this.getEmblem(r as MilitaryRegiment);
|
||||||
|
this.generateNote(r as MilitaryRegiment, s);
|
||||||
|
});
|
||||||
|
|
||||||
|
return regiments as MilitaryRegiment[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// remove all existing regiment notes before regenerating
|
||||||
|
for (let i = notes.length - 1; i >= 0; i--) {
|
||||||
|
if (notes[i].id.startsWith("regiment")) notes.splice(i, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// get regiments for each state
|
||||||
|
valid.forEach((s) => {
|
||||||
|
s.military = createRegiments(s.temp.platoons, s);
|
||||||
|
delete s.temp; // do not store temp data
|
||||||
|
});
|
||||||
|
|
||||||
|
TIME && console.timeEnd("generateMilitary");
|
||||||
|
}
|
||||||
|
|
||||||
|
getDefaultOptions() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
icon: "⚔️",
|
||||||
|
name: "infantry",
|
||||||
|
rural: 0.25,
|
||||||
|
urban: 0.2,
|
||||||
|
crew: 1,
|
||||||
|
power: 1,
|
||||||
|
type: "melee",
|
||||||
|
separate: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "🏹",
|
||||||
|
name: "archers",
|
||||||
|
rural: 0.12,
|
||||||
|
urban: 0.2,
|
||||||
|
crew: 1,
|
||||||
|
power: 1,
|
||||||
|
type: "ranged",
|
||||||
|
separate: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "🐴",
|
||||||
|
name: "cavalry",
|
||||||
|
rural: 0.12,
|
||||||
|
urban: 0.03,
|
||||||
|
crew: 2,
|
||||||
|
power: 2,
|
||||||
|
type: "mounted",
|
||||||
|
separate: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "💣",
|
||||||
|
name: "artillery",
|
||||||
|
rural: 0,
|
||||||
|
urban: 0.03,
|
||||||
|
crew: 8,
|
||||||
|
power: 12,
|
||||||
|
type: "machinery",
|
||||||
|
separate: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "🌊",
|
||||||
|
name: "fleet",
|
||||||
|
rural: 0,
|
||||||
|
urban: 0.015,
|
||||||
|
crew: 100,
|
||||||
|
power: 50,
|
||||||
|
type: "naval",
|
||||||
|
separate: 1,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
getName(r: MilitaryRegiment, regiments: MilitaryRegiment[]) {
|
||||||
|
const cells = pack.cells;
|
||||||
|
const proper = r.n
|
||||||
|
? null
|
||||||
|
: cells.province[r.cell] && pack.provinces[cells.province[r.cell]]
|
||||||
|
? pack.provinces[cells.province[r.cell]].name
|
||||||
|
: cells.burg[r.cell] && pack.burgs[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}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// utilize si function to make regiment total text fit regiment box
|
||||||
|
getTotal(reg: MilitaryRegiment) {
|
||||||
|
return reg.a > (reg.n ? 999 : 99999) ? si(reg.a) : reg.a;
|
||||||
|
}
|
||||||
|
|
||||||
|
generateNote(r: MilitaryRegiment, s: State) {
|
||||||
|
const cells = pack.cells;
|
||||||
|
const base =
|
||||||
|
cells.burg[r.cell] && pack.burgs[cells.burg[r.cell]]
|
||||||
|
? pack.burgs[cells.burg[r.cell]].name
|
||||||
|
: cells.province[r.cell] && pack.provinces[cells.province[r.cell]]
|
||||||
|
? pack.provinces[cells.province[r.cell]].fullName
|
||||||
|
: null;
|
||||||
|
const station = base
|
||||||
|
? `${r.name} is ${r.n ? "based" : "stationed"} in ${base}. `
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const composition = r.a
|
||||||
|
? Object.keys(r.u)
|
||||||
|
.map((t) => `— ${t}: ${r.u[t as keyof typeof r.u]}`)
|
||||||
|
.join("\r\n")
|
||||||
|
: null;
|
||||||
|
const troops = composition
|
||||||
|
? `\r\n\r\nRegiment composition in ${options.year} ${options.eraShort}:\r\n${composition}.`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const campaign = s.campaigns ? ra(s.campaigns) : null;
|
||||||
|
const year = campaign
|
||||||
|
? rand(campaign.start, campaign.end || options.year)
|
||||||
|
: gauss(options.year - 100, 150, 1, options.year - 6);
|
||||||
|
const conflict = campaign ? ` during the ${campaign.name}` : "";
|
||||||
|
const legend = `Regiment was formed in ${year} ${options.era}${conflict}. ${station}${troops}`;
|
||||||
|
const id = `regiment${s.i}-${r.i}`;
|
||||||
|
const existing = notes.find((n) => n.id === id);
|
||||||
|
if (existing) {
|
||||||
|
existing.name = r.name;
|
||||||
|
existing.legend = legend;
|
||||||
|
} else {
|
||||||
|
notes.push({ id, name: r.name, legend });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get default regiment emblem
|
||||||
|
getEmblem(r: MilitaryRegiment) {
|
||||||
|
if (!r.n && !Object.values(r.u).length) return "🔰"; // "Newbie" regiment without troops
|
||||||
|
if (
|
||||||
|
!r.n &&
|
||||||
|
pack.states[r.state].form === "Monarchy" &&
|
||||||
|
pack.cells.burg[r.cell] &&
|
||||||
|
pack.burgs[pack.cells.burg[r.cell]].capital
|
||||||
|
)
|
||||||
|
return "👑"; // "Royal" regiment based in capital
|
||||||
|
const mainUnit = Object.entries(r.u).sort((a, b) => b[1] - a[1])[0][0]; // unit with more troops in regiment
|
||||||
|
const unit = options.military.find(
|
||||||
|
(u: { name: string; icon: string }) => u.name === mainUnit,
|
||||||
|
);
|
||||||
|
return unit ? unit.icon : "⚔️";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.Military = new MilitaryModule();
|
||||||
|
|
@ -52,6 +52,8 @@ export interface State {
|
||||||
form?: string;
|
form?: string;
|
||||||
military?: any[];
|
military?: any[];
|
||||||
provinces?: number[];
|
provinces?: number[];
|
||||||
|
temp?: any;
|
||||||
|
alert?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
class StatesModule {
|
class StatesModule {
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,13 @@
|
||||||
import { color, easeSinInOut, transition } from "d3";
|
import { color, easeSinInOut, transition } from "d3";
|
||||||
|
import type { MilitaryRegiment } from "../modules/military-generator";
|
||||||
import { rn } from "../utils";
|
import { rn } from "../utils";
|
||||||
|
|
||||||
interface Regiment {
|
|
||||||
i: number;
|
|
||||||
name: string;
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
n?: number;
|
|
||||||
angle?: number;
|
|
||||||
icon: string;
|
|
||||||
state: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
var drawMilitary: () => void;
|
var drawMilitary: () => void;
|
||||||
var drawRegiments: (regiments: Regiment[], stateId: number) => void;
|
var drawRegiments: (regiments: MilitaryRegiment[], stateId: number) => void;
|
||||||
var drawRegiment: (reg: Regiment, stateId: number) => void;
|
var drawRegiment: (reg: MilitaryRegiment, stateId: number) => void;
|
||||||
var moveRegiment: (reg: Regiment, x: number, y: number) => void;
|
var moveRegiment: (reg: MilitaryRegiment, x: number, y: number) => void;
|
||||||
var armies: import("d3").Selection<SVGGElement, unknown, null, undefined>;
|
var armies: import("d3").Selection<SVGGElement, unknown, null, undefined>;
|
||||||
var Military: { getTotal: (reg: Regiment) => number };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const militaryRenderer = (): void => {
|
const militaryRenderer = (): void => {
|
||||||
|
|
@ -34,12 +23,15 @@ const militaryRenderer = (): void => {
|
||||||
TIME && console.timeEnd("drawMilitary");
|
TIME && console.timeEnd("drawMilitary");
|
||||||
};
|
};
|
||||||
|
|
||||||
const drawRegimentsRenderer = (regiments: Regiment[], s: number): void => {
|
const drawRegimentsRenderer = (
|
||||||
|
regiments: MilitaryRegiment[],
|
||||||
|
s: number,
|
||||||
|
): void => {
|
||||||
const size = +armies.attr("box-size");
|
const size = +armies.attr("box-size");
|
||||||
const w = (d: Regiment) => (d.n ? size * 4 : size * 6);
|
const w = (d: MilitaryRegiment) => (d.n ? size * 4 : size * 6);
|
||||||
const h = size * 2;
|
const h = size * 2;
|
||||||
const x = (d: Regiment) => rn(d.x - w(d) / 2, 2);
|
const x = (d: MilitaryRegiment) => rn(d.x - w(d) / 2, 2);
|
||||||
const y = (d: Regiment) => rn(d.y - size, 2);
|
const y = (d: MilitaryRegiment) => rn(d.y - size, 2);
|
||||||
|
|
||||||
const stateColor = pack.states[s]?.color;
|
const stateColor = pack.states[s]?.color;
|
||||||
const baseColor = stateColor && stateColor[0] === "#" ? stateColor : "#999";
|
const baseColor = stateColor && stateColor[0] === "#" ? stateColor : "#999";
|
||||||
|
|
@ -83,9 +75,9 @@ const drawRegimentsRenderer = (regiments: Regiment[], s: number): void => {
|
||||||
.attr("x", (d) => x(d) - size)
|
.attr("x", (d) => x(d) - size)
|
||||||
.attr("y", (d) => d.y)
|
.attr("y", (d) => d.y)
|
||||||
.text((d) =>
|
.text((d) =>
|
||||||
d.icon.startsWith("http") || d.icon.startsWith("data:image")
|
d.icon!.startsWith("http") || d.icon!.startsWith("data:image")
|
||||||
? ""
|
? ""
|
||||||
: d.icon,
|
: d.icon!,
|
||||||
);
|
);
|
||||||
g.append("image")
|
g.append("image")
|
||||||
.attr("class", "regimentImage")
|
.attr("class", "regimentImage")
|
||||||
|
|
@ -94,13 +86,13 @@ const drawRegimentsRenderer = (regiments: Regiment[], s: number): void => {
|
||||||
.attr("height", h)
|
.attr("height", h)
|
||||||
.attr("width", h)
|
.attr("width", h)
|
||||||
.attr("href", (d) =>
|
.attr("href", (d) =>
|
||||||
d.icon.startsWith("http") || d.icon.startsWith("data:image")
|
d.icon!.startsWith("http") || d.icon!.startsWith("data:image")
|
||||||
? d.icon
|
? d.icon!
|
||||||
: "",
|
: "",
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const drawRegimentRenderer = (reg: Regiment, stateId: number): void => {
|
const drawRegimentRenderer = (reg: MilitaryRegiment, stateId: number): void => {
|
||||||
const size = +armies.attr("box-size");
|
const size = +armies.attr("box-size");
|
||||||
const w = reg.n ? size * 4 : size * 6;
|
const w = reg.n ? size * 4 : size * 6;
|
||||||
const h = size * 2;
|
const h = size * 2;
|
||||||
|
|
@ -149,9 +141,9 @@ const drawRegimentRenderer = (reg: Regiment, stateId: number): void => {
|
||||||
.attr("x", x1 - size)
|
.attr("x", x1 - size)
|
||||||
.attr("y", reg.y)
|
.attr("y", reg.y)
|
||||||
.text(
|
.text(
|
||||||
reg.icon.startsWith("http") || reg.icon.startsWith("data:image")
|
reg.icon!.startsWith("http") || reg.icon!.startsWith("data:image")
|
||||||
? ""
|
? ""
|
||||||
: reg.icon,
|
: reg.icon!,
|
||||||
);
|
);
|
||||||
g.append("image")
|
g.append("image")
|
||||||
.attr("class", "regimentImage")
|
.attr("class", "regimentImage")
|
||||||
|
|
@ -161,14 +153,18 @@ const drawRegimentRenderer = (reg: Regiment, stateId: number): void => {
|
||||||
.attr("width", h)
|
.attr("width", h)
|
||||||
.attr(
|
.attr(
|
||||||
"href",
|
"href",
|
||||||
reg.icon.startsWith("http") || reg.icon.startsWith("data:image")
|
reg.icon!.startsWith("http") || reg.icon!.startsWith("data:image")
|
||||||
? reg.icon
|
? reg.icon!
|
||||||
: "",
|
: "",
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// move one regiment to another
|
// move one regiment to another
|
||||||
const moveRegimentRenderer = (reg: Regiment, x: number, y: number): void => {
|
const moveRegimentRenderer = (
|
||||||
|
reg: MilitaryRegiment,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
): void => {
|
||||||
const el = armies
|
const el = armies
|
||||||
.select(`g#army${reg.state}`)
|
.select(`g#army${reg.state}`)
|
||||||
.select(`g#regiment${reg.state}-${reg.i}`);
|
.select(`g#regiment${reg.state}-${reg.i}`);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue