mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-16 17:31:24 +01:00
feat: generateRegiments
This commit is contained in:
parent
3f7f0c4826
commit
c3298ade47
13 changed files with 283 additions and 36 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
113
src/scripts/generation/pack/military/generateRegiments.ts
Normal file
113
src/scripts/generation/pack/military/generateRegiments.ts
Normal 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;
|
||||
}
|
||||
92
src/scripts/generation/pack/military/specifyRegiments.ts
Normal file
92
src/scripts/generation/pack/military/specifyRegiments.ts
Normal 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}.`;
|
||||
}
|
||||
};
|
||||
|
|
@ -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
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
4
src/types/globals.d.ts
vendored
4
src/types/globals.d.ts
vendored
|
|
@ -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;
|
||||
|
|
|
|||
5
src/types/overrides.d.ts
vendored
5
src/types/overrides.d.ts
vendored
|
|
@ -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[];
|
||||
}
|
||||
|
|
|
|||
2
src/types/pack/pack.d.ts
vendored
2
src/types/pack/pack.d.ts
vendored
|
|
@ -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 {
|
||||
|
|
|
|||
26
src/types/pack/states.d.ts
vendored
26
src/types/pack/states.d.ts
vendored
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue