From c3298ade47c4fbb938237b24fdf7988379ba89c0 Mon Sep 17 00:00:00 2001 From: Azgaar Date: Fri, 4 Nov 2022 01:18:37 +0300 Subject: [PATCH] feat: generateRegiments --- src/config/military.ts | 8 +- src/modules/military-generator.js | 4 +- .../pack/military/generateMilitary.ts | 33 +++-- .../pack/military/generatePlatoons.ts | 25 +++- .../pack/military/generateRegiments.ts | 113 ++++++++++++++++++ .../pack/military/specifyRegiments.ts | 92 ++++++++++++++ src/scripts/generation/pack/pack.ts | 3 +- src/scripts/generation/pack/repackGrid.ts | 2 +- src/types/globals.d.ts | 4 +- src/types/overrides.d.ts | 5 +- src/types/pack/pack.d.ts | 2 +- src/types/pack/states.d.ts | 26 +++- src/utils/graphUtils.ts | 2 +- 13 files changed, 283 insertions(+), 36 deletions(-) create mode 100644 src/scripts/generation/pack/military/generateRegiments.ts create mode 100644 src/scripts/generation/pack/military/specifyRegiments.ts diff --git a/src/config/military.ts b/src/config/military.ts index c9e6e804..b5d21af6 100644 --- a/src/config/military.ts +++ b/src/config/military.ts @@ -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, diff --git a/src/modules/military-generator.js b/src/modules/military-generator.js index 69a4d38d..e6ca9204 100644 --- a/src/modules/military-generator.js +++ b/src/modules/military-generator.js @@ -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 diff --git a/src/scripts/generation/pack/military/generateMilitary.ts b/src/scripts/generation/pack/military/generateMilitary.ts index 25480f55..b42698ec 100644 --- a/src/scripts/generation/pack/military/generateMilitary.ts +++ b/src/scripts/generation/pack/military/generateMilitary.ts @@ -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"); } diff --git a/src/scripts/generation/pack/military/generatePlatoons.ts b/src/scripts/generation/pack/military/generatePlatoons.ts index 4e3e343d..d8796571 100644 --- a/src/scripts/generation/pack/military/generatePlatoons.ts +++ b/src/scripts/generation/pack/military/generatePlatoons.ts @@ -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[], 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; diff --git a/src/scripts/generation/pack/military/generateRegiments.ts b/src/scripts/generation/pack/military/generateRegiments.ts new file mode 100644 index 00000000..2af02bbd --- /dev/null +++ b/src/scripts/generation/pack/military/generateRegiments.ts @@ -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; + + const removed = new Set(); + 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; +} diff --git a/src/scripts/generation/pack/military/specifyRegiments.ts b/src/scripts/generation/pack/military/specifyRegiments.ts new file mode 100644 index 00000000..060026e0 --- /dev/null +++ b/src/scripts/generation/pack/military/specifyRegiments.ts @@ -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}.`; + } +}; diff --git a/src/scripts/generation/pack/pack.ts b/src/scripts/generation/pack/pack.ts index 8eb0e372..a52f9302 100644 --- a/src/scripts/generation/pack/pack.ts +++ b/src/scripts/generation/pack/pack.ts @@ -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 }); diff --git a/src/scripts/generation/pack/repackGrid.ts b/src/scripts/generation/pack/repackGrid.ts index 48a32b1e..16a9f926 100644 --- a/src/scripts/generation/pack/repackGrid.ts +++ b/src/scripts/generation/pack/repackGrid.ts @@ -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) } diff --git a/src/types/globals.d.ts b/src/types/globals.d.ts index a1d42c50..b9bd08d3 100644 --- a/src/types/globals.d.ts +++ b/src/types/globals.d.ts @@ -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; diff --git a/src/types/overrides.d.ts b/src/types/overrides.d.ts index 713029bc..fbaf0772 100644 --- a/src/types/overrides.d.ts +++ b/src/types/overrides.d.ts @@ -36,7 +36,6 @@ interface Node { off: (name: string, fn: EventListenerOrEventListenerObject) => void; } -interface Quadtree extends d3.Quadtree { - 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 extends d3.Quadtree { + findAll: (x: number, y: number, radius: number) => T[]; } diff --git a/src/types/pack/pack.d.ts b/src/types/pack/pack.d.ts index 824431c1..9108c984 100644 --- a/src/types/pack/pack.d.ts +++ b/src/types/pack/pack.d.ts @@ -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; } interface IPackBase extends IGraph { diff --git a/src/types/pack/states.d.ts b/src/types/pack/states.d.ts index 403271ba..d4f310a6 100644 --- a/src/types/pack/states.d.ts +++ b/src/types/pack/states.d.ts @@ -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"; diff --git a/src/utils/graphUtils.ts b/src/utils/graphUtils.ts index 963fcb1b..071076ac 100644 --- a/src/utils/graphUtils.ts +++ b/src/utils/graphUtils.ts @@ -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