feat: war alert

This commit is contained in:
Azgaar 2022-10-05 01:03:24 +03:00
parent 50edcb4e30
commit 364d22e6ad
11 changed files with 216 additions and 24 deletions

91
src/config/military.ts Normal file
View file

@ -0,0 +1,91 @@
export const getDefaultMilitaryOptions: () => IMilitaryOption[] = 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}
];
};
export const relationsAlertRate: {[key in TRelation]: number} = {
Vassal: -0.5,
Ally: -0.2,
Friendly: -0.1,
Neutral: 0,
Unknown: 0,
x: 0,
Suspicion: 0.1,
Suzerain: 0.3,
Rival: 0.5,
Enemy: 1
};
type TCulture = Exclude<TCultureType, "Generic">;
export const stateModifier: {[key in TMilitaryType]: {[key in TCulture]: number}} = {
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}
};
export const cellTypeModifier: {[key: string]: {[key in TMilitaryType]: number}} = {
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
}
};
export const burgTypeModifier: {[key: string]: {[key in TMilitaryType]: number}} = {
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}
};

View file

@ -6,6 +6,7 @@ import {findCell} from "utils/graphUtils";
import {rn} from "utils/numberUtils";
import {P, rand, rw} from "utils/probabilityUtils";
import {parseTransform} from "utils/stringUtils";
import {getDefaultMilitaryOptions} from "config/military";
// update old .map version to the current one
export function resolveVersionConflicts(version) {
@ -272,7 +273,7 @@ export function resolveVersionConflicts(version) {
const year = rand(100, 2000);
const era = Names.getBaseShort(P(0.7) ? 1 : rand(nameBases.length)) + " Era";
const eraShort = era[0] + "E";
const military = Military.getDefaultOptions();
const military = getDefaultMilitaryOptions();
options = {winds, year, era, eraShort, military};
// v1.3 added campaings data for all states

View file

@ -5,6 +5,7 @@ import {rn, minmax} from "utils/numberUtils";
import {rand, gauss, ra} from "utils/probabilityUtils";
import {si} from "utils/unitUtils";
import {nth} from "utils/languageUtils";
import {getDefaultMilitaryOptions} from "config/military";
window.Military = (function () {
const generate = function () {
@ -12,7 +13,7 @@ window.Military = (function () {
const {cells, states} = pack;
const {p} = cells;
const valid = states.filter(s => s.i && !s.removed); // valid states
if (!options.military) options.military = getDefaultOptions();
if (!options.military) options.military = getDefaultMilitaryOptions();
const expn = d3.sum(valid.map(s => s.expansionism)); // total expansion
const area = d3.sum(valid.map(s => s.area)); // total area
@ -331,16 +332,6 @@ window.Military = (function () {
validStates.forEach(s => drawRegiments(s.military, s.i));
}
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}
];
};
const drawRegiments = function (regiments, s) {
const size = +armies.attr("box-size");
const w = d => (d.n ? size * 4 : size * 6);
@ -512,7 +503,6 @@ window.Military = (function () {
return {
generate,
redraw,
getDefaultOptions,
getName,
generateNote,
drawRegiments,

View file

@ -4,20 +4,16 @@ import {NAMEBASE} from "config/namebases";
import {getInputNumber} from "utils/nodeUtils";
import {P, rand, rw} from "utils/probabilityUtils";
import {AreaTiers, culturalMonarchyFormsMap, culturalTheocracyFormsMap, StateForms} from "./config";
import type {TStateStatistics} from "./collectStatistics";
// create 5 area tiers, 4 is the biggest, 0 the smallest
export function createAreaTiers(statistics: TStateStatistics) {
const stateAreas = Object.entries(statistics)
.filter(([id]) => Number(id))
.map(([, {area}]) => area);
const medianArea = d3.median(stateAreas)!;
export function createAreaTiers(stateAreas: number[]) {
const meanArea = d3.mean(stateAreas)!;
const topTierIndex = Math.max(Math.ceil(stateAreas.length ** 0.4) - 2, 0);
const minTopTierArea = stateAreas.sort((a, b) => b - a)[topTierIndex];
return (area: number) => {
const tier = Math.min(Math.floor((area / medianArea) * 2.6), 4) as AreaTiers;
const tier = Math.min(Math.floor((area / meanArea) * 2.6), 4) as AreaTiers;
if (tier === AreaTiers.EMPIRE && area < minTopTierArea) return AreaTiers.KINGDOM;
return tier;
};

View file

@ -0,0 +1,45 @@
import {relationsAlertRate} from "config/military";
import {minmax, rn} from "utils/numberUtils";
export function defineWarAlert(
neighbors: number[],
relations: TRelation[],
areaRate: number,
expansionismRate: number
) {
const expansionismRealization = getRealization(areaRate, expansionismRate);
const peacefulness = getPeacefulness(relations);
const neighborliness = getNeighborliness(neighbors, relations);
const warAlert = getWarAlert(expansionismRealization, peacefulness, neighborliness);
return rn(warAlert, 2);
}
function getRealization(expansionismRate: number, areaRate: number) {
const [MIN, MAX] = [0.25, 4];
return minmax(expansionismRate / areaRate, MIN, MAX);
}
function getPeacefulness(relations: TRelation[]) {
if (relations.includes("Enemy")) return 1;
if (relations.includes("Rival")) return 0.8;
if (relations.includes("Suspicion")) return 0.5;
return 0.1;
}
function getNeighborliness(neighbors: number[], relations: TRelation[]) {
const neighborRelations = neighbors.map(neibStateId => relations[neibStateId]);
const initialRate = 0.5;
const rate = neighborRelations.reduce((acc, relation) => (acc += relationsAlertRate[relation]), initialRate);
const [MIN, MAX] = [0.3, 3];
return minmax(rate, MIN, MAX);
}
function getWarAlert(expansionismRealization: number, peacefulness: number, neighborliness: number) {
const alert = expansionismRealization * peacefulness * neighborliness;
const [MIN, MAX] = [0.1, 5];
return minmax(alert, MIN, MAX);
}

View file

@ -5,6 +5,7 @@ import {defineFullStateName, defineStateName} from "./defineStateName";
import {defineStateColors} from "./defineStateColors";
import {isBurg} from "utils/typeUtils";
import {generateConflicts} from "./generateConflicts";
import {defineWarAlert} from "./defineWarAlert";
import type {TStateStatistics} from "./collectStatistics";
import type {TStateData} from "./createStateData";
@ -21,11 +22,15 @@ export function specifyStates(
TIME && console.time("specifyStates");
const colors = defineStateColors(statistics);
const getAreaTier = createAreaTiers(statistics);
const getNameBase = (cultureId: number) => cultures[cultureId].base;
const states: IState[] = statesData.map(stateData => {
const {i, center, type, culture, capital} = stateData;
const stateAreas = getStateAreas(statistics);
const totalArea = stateAreas.reduce((a, b) => a + b);
const totalExpansionism = statesData.map(state => state.expansionism).reduce((a, b) => a + b);
const getAreaTier = createAreaTiers(stateAreas);
const states = statesData.map(stateData => {
const {i, center, type, culture, capital, expansionism} = stateData;
const {area, burgs: burgsNumber, neighbors, ...stats} = statistics[i];
const color = colors[i];
@ -35,6 +40,7 @@ export function specifyStates(
const relations = diplomacy[i];
const isVassal = relations.includes("Vassal");
const alert = defineWarAlert(neighbors, relations, area / totalArea, expansionism / totalExpansionism);
const nameBase = getNameBase(culture);
const areaTier = getAreaTier(area);
@ -44,7 +50,7 @@ export function specifyStates(
const pole = poles[i];
return {
const state: IState = {
name,
...stateData,
form,
@ -56,8 +62,10 @@ export function specifyStates(
...stats,
neighbors,
relations,
alert,
pole
};
return state;
});
const conflicts = generateConflicts(states, cultures); // mutates states
@ -65,3 +73,9 @@ export function specifyStates(
TIME && console.timeEnd("specifyStates");
return {states: [NEUTRALS, ...states], conflicts};
}
function getStateAreas(statistics: TStateStatistics) {
return Object.entries(statistics)
.filter(([id]) => Number(id))
.map(([, {area}]) => area);
}

View file

@ -0,0 +1,15 @@
import {TIME} from "config/logging";
import {getDefaultMilitaryOptions} from "config/military";
import {getUnitModifiers} from "./getUnitModifiers";
export function generateMilitary(states: TStates) {
TIME && console.time("generateMilitaryForces");
if (!options.military) options.military = getDefaultMilitaryOptions();
// const unitModifiers = getUnitModifiers(states);
console.log(states);
TIME && console.timeEnd("generateMilitaryForces");
}

View file

@ -0,0 +1,21 @@
import {stateModifier} from "config/military";
import {isState} from "utils/typeUtils";
// calculate overall state modifiers for unit types based on state features
export function getUnitModifiers(states: TStates) {
const validStates = states.filter(isState);
for (const state of validStates) {
const military = {platoons: []};
const {i: stateId, relations, expansionism, area, neighbors, alert} = state;
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;
military[unit.name] = modifier * alert;
}
}
}

View file

@ -1,10 +1,12 @@
import {markupPackFeatures} from "scripts/generation/markup";
import {rankCells} from "scripts/generation/pack/rankCells";
import {pick} from "utils/functionUtils";
import {generateBurgsAndStates} from "./burgsAndStates/generateBurgsAndStates";
import {expandCultures} from "./cultures/expandCultures";
import {generateCultures} from "./cultures/generateCultures";
import {generateLakeNames} from "./lakes/generateLakeNames";
import {generateMilitary} from "./military/generateMilitary";
import {generateProvinces} from "./provinces/generateProvinces";
import {generateReligions} from "./religions/generateReligions";
import {repackGrid} from "./repackGrid";
@ -162,6 +164,8 @@ export function createPack(grid: IGrid): IPack {
const rivers = specifyRivers(rawRivers, cultureIds, cultures);
const features = generateLakeNames(mergedFeatures, cultureIds, cultures);
generateMilitary(states);
// Military.generate();
// Markers.generate();
// addZones(); // add to pack data

View file

@ -26,6 +26,7 @@ interface IOptions {
winds: [number, number, number, number, number, number];
stateLabelsMode: "auto" | "short" | "full";
year: number;
military: IMilitaryOption[];
}
declare let populationRate: number;

View file

@ -19,6 +19,7 @@ interface IState {
urban: number;
neighbors: number[];
relations: TRelation[];
alert: number;
removed?: boolean;
}
@ -51,3 +52,16 @@ type TRelation =
| "Vassal"
| "Enemy"
| "x";
interface IMilitaryOption {
name: string;
icon: string;
crew: number;
power: number;
rural: number;
urban: number;
type: TMilitaryType;
separate: Logical;
}
type TMilitaryType = "melee" | "ranged" | "mounted" | "machinery" | "naval" | "armored" | "aviation" | "magical";