feat: generateRegiments

This commit is contained in:
Azgaar 2022-11-04 01:18:37 +03:00
parent 3f7f0c4826
commit c3298ade47
13 changed files with 283 additions and 36 deletions

View file

@ -1,4 +1,4 @@
export const getDefaultMilitaryOptions: () => IMilitaryUnit[] = function () {
export const getDefaultMilitaryOptions: () => IMilitaryUnitConfig[] = 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},
@ -32,7 +32,9 @@ export const stateModifier: {[key in TMilitaryUnitType]: {[key in TCultureType]:
magical: {Generic: 1, Nomadic: 1, Highland: 2, Lake: 1, Naval: 1, Hunting: 1, River: 1}
};
export const cellTypeModifier: {[key: string]: {[key in TMilitaryUnitType]: number}} = {
type TCellType = "nomadic" | "wetland" | "highland";
export const cellTypeModifier: {[key in TCellType]: {[key in TMilitaryUnitType]: number}} = {
nomadic: {
melee: 0.2,
ranged: 0.5,
@ -65,7 +67,7 @@ export const cellTypeModifier: {[key: string]: {[key in TMilitaryUnitType]: numb
}
};
export const burgTypeModifier: {[key: string]: {[key in TMilitaryUnitType]: number}} = {
export const burgTypeModifier: {[key in TCellType]: {[key in TMilitaryUnitType]: number}} = {
nomadic: {
melee: 0.3,
ranged: 0.8,

View file

@ -305,8 +305,8 @@ window.Military = (function () {
.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};
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, state: s.i};
});
// generate name for regiments

View file

@ -1,31 +1,38 @@
import {TIME} from "config/logging";
import {getDefaultMilitaryOptions} from "config/military";
import {isState} from "utils/typeUtils";
import {generatePlatoons} from "./generatePlatoons";
import {generateRegiments} from "./generateRegiments";
import {getUnitModifiers} from "./getUnitModifiers";
export type TCellsData = Pick<
IPack["cells"],
"i" | "p" | "h" | "f" | "haven" | "pop" | "biome" | "culture" | "state" | "burg" | "religion"
"i" | "p" | "h" | "f" | "haven" | "pop" | "biome" | "culture" | "state" | "burg" | "province" | "religion"
>;
export interface IPlatoon {
unit: IMilitaryUnit;
cell: number;
a: number;
t: number;
x: number;
y: number;
}
export function generateMilitary(states: TStates, burgs: TBurgs, cells: TCellsData) {
export function generateMilitary(states: TStates, burgs: TBurgs, provinces: TProvinces, cells: TCellsData) {
TIME && console.time("generateMilitaryForces");
if (!options.military) options.military = getDefaultMilitaryOptions();
const unitModifiers = getUnitModifiers(states);
const platoons = generatePlatoons(states, unitModifiers, cells);
const platoons = generatePlatoons(states, burgs, unitModifiers, cells);
console.log({states, unitModifiers, platoons});
for (const state of states) {
if (!isState(state)) continue;
state.regiments = generateRegiments({
stateId: state.i,
platoons: platoons[state.i],
states,
provinceIds: cells.province,
provinces,
burgIds: cells.burg,
burgs
});
}
console.log({states});
TIME && console.timeEnd("generateMilitaryForces");
}

View file

@ -2,7 +2,15 @@ import {ELEVATION, NOMADIC_BIOMES, WETLAND_BIOMES} from "config/generation";
import {burgTypeModifier, cellTypeModifier} from "config/military";
import {rn} from "utils/numberUtils";
import {isBurg, isState} from "utils/typeUtils";
import {IPlatoon, TCellsData} from "./generateMilitary";
import {TCellsData} from "./generateMilitary";
export interface IPlatoon {
unit: IMilitaryUnitConfig;
cell: number;
total: number;
x: number;
y: number;
}
export function generatePlatoons(states: TStates, burgs: TBurgs, unitModifiers: Dict<number>[], cells: TCellsData) {
const platoons: {[key: number]: IPlatoon[]} = {};
@ -32,13 +40,14 @@ export function generatePlatoons(states: TStates, burgs: TBurgs, unitModifiers:
const stateModifiers = unitModifiers[stateId];
const cellType = getCellType(biomeId, cells.h[i]);
const isGeneric = cellType === "generic";
for (const unit of options.military) {
if (!checkUnitConstrains(unit, biomeId, stateId, cultureId, religionId)) continue;
if (unit.type === "naval" && !isNavyProducer(cells.haven[i], burg)) continue;
const ruralUnitModifier = cellTypeModifier[cellType][unit.type];
const urbanUnitModifier = burgTypeModifier[cellType][unit.type];
const ruralUnitModifier = isGeneric ? 1 : cellTypeModifier[cellType][unit.type];
const urbanUnitModifier = isGeneric ? 1 : burgTypeModifier[cellType][unit.type];
const ruralArmy = ruralBase * unit.rural * ruralUnitModifier * cellModifier * stateModifiers[unit.name];
const urbanArmy = urbanBase * unit.urban * urbanUnitModifier * cellModifier * stateModifiers[unit.name];
@ -50,7 +59,7 @@ export function generatePlatoons(states: TStates, burgs: TBurgs, unitModifiers:
const [x, y] = cells.p[placeCell];
if (!platoons[stateId]) platoons[stateId] = [];
platoons[stateId].push({unit, cell: i, a: total, t: total, x, y});
platoons[stateId].push({unit, cell: i, total, x, y});
}
}
@ -92,7 +101,13 @@ function getCellType(biomeId: number, cellHeight: number) {
return "generic";
}
function checkUnitConstrains(unit: IMilitaryUnit, biome: number, state: number, culture: number, religion: number) {
function checkUnitConstrains(
unit: IMilitaryUnitConfig,
biome: number,
state: number,
culture: number,
religion: number
) {
if (unit.biomes?.length && !unit.biomes.includes(biome)) return false;
if (unit.states?.length && !unit.states.includes(state)) return false;
if (unit.cultures?.length && !unit.cultures.includes(culture)) return false;

View file

@ -0,0 +1,113 @@
import * as d3 from "d3";
import {getName, getEmblem, generateNote} from "./specifyRegiments";
import type {IPlatoon} from "./generatePlatoons";
const MIN_DISTANCE = 20;
export function generateRegiments({
stateId,
platoons,
states,
provinceIds,
provinces,
burgIds,
burgs
}: {
stateId: number;
platoons: IPlatoon[];
states: TStates;
provinceIds: Uint16Array;
provinces: TProvinces;
burgIds: Uint16Array;
burgs: TBurgs;
}): IRegiment[] {
const regiments: IRegiment[] = [];
if (!platoons.length) return regiments;
platoons.sort((a, b) => a.total - b.total);
const tree = d3.quadtree(
platoons,
d => d.x,
d => d.y
) as Quadtree<IPlatoon>;
const removed = new Set<IPlatoon>();
const remove = (platoon: IPlatoon) => {
tree.remove(platoon);
removed.add(platoon);
};
const expectedSize = 3 * populationRate; // expected regiment size is about 3k
for (const platoon of platoons) {
if (removed.has(platoon)) continue;
remove(platoon);
const regimentPlatoons = [platoon];
let regimentForce = platoon.total;
// join all overlapping mergeable platoons
const overlapping = tree.findAll(platoon.x, platoon.y, MIN_DISTANCE);
for (const overlappingPlatoon of overlapping) {
if (!isMergeable(platoon, overlappingPlatoon)) continue;
regimentPlatoons.push(overlappingPlatoon);
regimentForce += overlappingPlatoon.total;
remove(overlappingPlatoon);
}
if (regimentForce >= expectedSize) continue;
// if joined force is still too small, check platoons in further range
const radius = (expectedSize - platoon.total) / MIN_DISTANCE;
const candidates = tree.findAll(platoon.x, platoon.y, radius);
for (const candidatePlatoon of candidates) {
if (candidatePlatoon.total >= expectedSize) break;
if (!isMergeable(platoon, candidatePlatoon)) continue;
regimentPlatoons.push(candidatePlatoon);
regimentForce += candidatePlatoon.total;
remove(candidatePlatoon);
break;
}
regiments.push({
i: regiments.length,
icon: "", // define below
name: "", // define below
state: stateId,
cell: platoon.cell,
x: platoon.x,
y: platoon.y,
bx: platoon.x,
by: platoon.y,
total: regimentForce,
units: getRegimentUnits(regimentPlatoons),
isNaval: platoon.unit.type === "naval"
});
}
for (const regiment of regiments) {
regiment.name = getName(regiment, regiments, provinceIds, burgIds, provinces, burgs);
regiment.icon = getEmblem(regiment, states, burgs, burgIds);
generateNote(regiment, provinceIds, burgIds, provinces, burgs); // TODO: move out of military generation
}
return regiments;
}
// check if 2 plattons can be merged
function isMergeable(platoon1: IPlatoon, platoon2: IPlatoon) {
return platoon1.unit.name === platoon2.unit.name || (!platoon1.unit.separate && !platoon2.unit.separate);
}
function getRegimentUnits(platoons: IPlatoon[]) {
const units: {[key: string]: number} = {};
for (const platoon of platoons) {
if (!units[platoon.unit.name]) units[platoon.unit.name] = 0;
units[platoon.unit.name] += platoon.total;
}
return units;
}

View file

@ -0,0 +1,92 @@
import {nth} from "utils/languageUtils";
import {gauss} from "utils/probabilityUtils";
import {isBurg, isProvince, isState} from "utils/typeUtils";
export const getName = (
regiment: IRegiment,
regiments: IRegiment[],
provinceIds: Uint16Array,
burgIds: Uint16Array,
provinces: TProvinces,
burgs: TBurgs
) => {
const proper = getProperName();
const number = nth(regiments.filter(reg => reg.isNaval === regiment.isNaval && reg.i < regiment.i).length + 1);
const form = regiment.isNaval ? "Fleet" : "Regiment";
return `${number}${proper ? ` (${proper}) ` : ` `}${form}`;
function getProperName() {
if (regiment.isNaval) return null;
const province = provinces[provinceIds[regiment.cell]];
if (isProvince(province)) return province.name;
const burg = burgs[burgIds[regiment.cell]];
if (isBurg(burg)) return burg.name;
return null;
}
};
export const getEmblem = (regiment: IRegiment, states: TStates, burgs: TBurgs, burgIds: Uint16Array) => {
if (regiment.isNaval) return "🌊"; //
if (!regiment.isNaval && !regiment.total) return "🔰"; // "Newbie": regiment without troops
const state = states[regiment.state];
const isMonarchy = isState(state) && state.form === "Monarchy";
const burg = burgs[burgIds[regiment.cell]];
const isCapital = isBurg(burg) && burg.capital;
if (isMonarchy && isCapital) return "👑"; // "Royal" regiment based in capital
// unit with more troops in regiment
const largestUnitName = Object.entries(regiment.units).sort((a, b) => b[1] - a[1])[0][0];
const unit = options.military.find(unit => unit.name === largestUnitName);
return unit?.icon || "🎖️";
};
export const generateNote = (
regiment: IRegiment,
provinceIds: Uint16Array,
burgIds: Uint16Array,
provinces: TProvinces,
burgs: TBurgs
) => {
const baseName = getBaseName();
const station = baseName ? `${regiment.name} is ${regiment.isNaval ? "based" : "stationed"} in ${baseName}. ` : "";
const troops = getTroopsComposition() || "";
// TODO: add campaigns
// const campaign = state.campaigns ? ra(state.campaigns) : null;
// const year = campaign ? rand(campaign.start, campaign.end) : 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 year = gauss(options.year - 100, 150, 1, options.year - 6);
const legend = `Regiment was formed in ${year} ${options.era}. ${station}${troops}`;
const id = `regiment${regiment.state}-${regiment.i}`;
const name = `${regiment.icon} ${regiment.name}`;
notes.push({id, name, legend});
function getBaseName() {
const burg = burgs[burgIds[regiment.cell]];
if (isBurg(burg)) return burg.name;
const province = provinces[provinceIds[regiment.cell]];
if (isProvince(province)) return province.fullName;
return null;
}
function getTroopsComposition() {
if (regiment.total) return null;
const composition = Object.keys(regiment.units)
.map(t => `${t}: ${regiment.units[t]}`)
.join("\r\n");
return `\r\n\r\nRegiment composition in ${options.year} ${options.eraShort}:\r\n${composition}.`;
}
};

View file

@ -164,7 +164,7 @@ export function createPack(grid: IGrid): IPack {
const rivers = specifyRivers(rawRivers, cultureIds, cultures);
const features = generateLakeNames(mergedFeatures, cultureIds, cultures);
generateMilitary(states, burgs, {
generateMilitary(states, burgs, provinces, {
i: cells.i,
p: cells.p,
h: heights,
@ -175,6 +175,7 @@ export function createPack(grid: IGrid): IPack {
culture: cultureIds,
state: stateIds,
burg: burgIds,
province: provinceIds,
religion: religionIds
});

View file

@ -68,7 +68,7 @@ export function repackGrid(grid: IGrid) {
...cells,
p: newCells.p,
g: createTypedArray({maxValue: grid.points.length, from: newCells.g}),
q: d3.quadtree(newCells.p.map(([x, y], i) => [x, y, i])) as unknown as Quadtree,
q: d3.quadtree(newCells.p.map(([x, y], i) => [x, y, i])),
h: new Uint8Array(newCells.h),
area: createTypedArray({maxValue: UINT16_MAX, from: cells.i}).map(getCellArea)
}

View file

@ -26,7 +26,9 @@ interface IOptions {
winds: [number, number, number, number, number, number];
stateLabelsMode: "auto" | "short" | "full";
year: number;
military: IMilitaryUnit[];
era: string;
eraShort: string;
military: IMilitaryUnitConfig[];
}
declare let populationRate: number;

View file

@ -36,7 +36,6 @@ interface Node {
off: (name: string, fn: EventListenerOrEventListenerObject) => void;
}
interface Quadtree extends d3.Quadtree<Number> {
find: (x: number, y: number, radius: number) => [x: number, y: number, cellId: number];
findAll: (x: number, y: number, radius: number) => [x: number, y: number, cellId: number][];
interface Quadtree<T> extends d3.Quadtree<T> {
findAll: (x: number, y: number, radius: number) => T[];
}

View file

@ -32,7 +32,7 @@ interface IPackCells {
haven: UintArray;
harbor: UintArray;
route: Uint8Array; // [0, 1, 2, 3], see ROUTES enum, defined by generateRoutes()
q: Quadtree;
q: Quadtree<number[]>;
}
interface IPackBase extends IGraph {

View file

@ -20,6 +20,7 @@ interface IState {
neighbors: number[];
relations: TRelation[];
alert: number;
regiments: IRegiment[];
removed?: boolean;
}
@ -53,7 +54,7 @@ type TRelation =
| "Enemy"
| "x";
interface IMilitaryUnit {
interface IMilitaryUnitConfig {
name: string;
icon: string;
crew: number;
@ -62,10 +63,25 @@ interface IMilitaryUnit {
urban: number;
type: TMilitaryUnitType;
separate: Logical;
biomes?: number[];
states?: number[];
cultures?: number[];
religions?: number[];
biomes?: number[]; // allowed biomes
states?: number[]; // allowed states
cultures?: number[]; // allowed cultures
religions?: number[]; // allowed religions
}
interface IRegiment {
i: number;
icon: string;
name: string;
state: number; // stateId
cell: number; // base cell
x: number; // current position x
y: number; // current position y
bx: number; // base position x
by: number; // base position y
total: number;
units: {[key: string]: number};
isNaval: boolean;
}
type TMilitaryUnitType = "melee" | "ranged" | "mounted" | "machinery" | "naval" | "armored" | "aviation" | "magical";

View file

@ -48,7 +48,7 @@ export function findCell(x: number, y: number): number;
export function findCell(x: number, y: number, radius: number): number | undefined;
export function findCell(x: number, y: number, radius = Infinity): number | undefined {
const found = pack.cells.q.find(x, y, radius);
return found ? found[2] : undefined;
return found?.[2];
}
// get polygon points for initial cells knowing cell id