diff --git a/src/scripts/generation/pack/burgsAndStates/defineStateForm.ts b/src/scripts/generation/pack/burgsAndStates/defineStateForm.ts index 67f27d9e..0610edd0 100644 --- a/src/scripts/generation/pack/burgsAndStates/defineStateForm.ts +++ b/src/scripts/generation/pack/burgsAndStates/defineStateForm.ts @@ -28,11 +28,11 @@ export function defineStateForm( areaTier: AreaTiers, nameBase: number, burgsNumber: number, - relations: TRelation[], - neighbors: number[] + neighbors: number[], + isVassal: boolean ) { const form = defineForm(type, areaTier); - const formName = defineFormName(form, nameBase, areaTier, burgsNumber, relations, neighbors); + const formName = defineFormName(form, nameBase, areaTier, burgsNumber, neighbors, isVassal); return {form, formName}; } @@ -59,10 +59,10 @@ function defineFormName( nameBase: number, areaTier: AreaTiers, burgsNumber: number, - relations: TRelation[], - neighbors: number[] + neighbors: number[], + isVassal: boolean ) { - if (form === "Monarchy") return defineMonarchyForm(nameBase, areaTier, relations, neighbors); + if (form === "Monarchy") return defineMonarchyForm(nameBase, areaTier, neighbors, isVassal); if (form === "Republic") return defineRepublicForm(areaTier, burgsNumber); if (form === "Union") return rw(StateForms.union); if (form === "Theocracy") return defineTheocracyForm(nameBase, areaTier); @@ -72,10 +72,9 @@ function defineFormName( } // Default name depends on area tier, some name bases have special names for tiers -function defineMonarchyForm(nameBase: number, areaTier: AreaTiers, relations: TRelation[], neighbors: number[]) { +function defineMonarchyForm(nameBase: number, areaTier: AreaTiers, neighbors: number[], isVassal: boolean) { const form = StateForms.monarchy[areaTier]; - const isVassal = relations.includes("Vassal"); if (isVassal) { if (areaTier === AreaTiers.DUCHY && neighbors.length > 1 && rand(6) < neighbors.length) return "Marches"; if (nameBase === NAMEBASE.English && P(0.3)) return "Dominion"; diff --git a/src/scripts/generation/pack/burgsAndStates/generateConflicts.ts b/src/scripts/generation/pack/burgsAndStates/generateConflicts.ts new file mode 100644 index 00000000..46bfeba0 --- /dev/null +++ b/src/scripts/generation/pack/burgsAndStates/generateConflicts.ts @@ -0,0 +1,158 @@ +import * as d3 from "d3"; + +import {TIME} from "config/logging"; +import {list, trimVowels} from "utils/languageUtils"; +import {gauss, ra} from "utils/probabilityUtils"; + +export function generateConflicts(states: IState[]) { + TIME && console.time("generateConflicts"); + + const statesMap = new Map(states.map(state => [state.i, state])); + const wars: IConflict[] = []; + + for (const {i: stateId, relations} of states) { + if (!relations.includes("Rival")) continue; // no rivals to attack + if (relations.includes("Vassal")) continue; // not independent + if (relations.includes("Enemy")) continue; // already at war + + // select candidates to attack: rival independent states + const candidates = relations + .map((relation, stateId) => { + const state = statesMap.get(stateId); + const isVassal = state?.relations.includes("Vassal"); + return relation === "Rival" && state && !isVassal ? stateId : 0; + }) + .filter(index => index); + if (!candidates.length) continue; + + const attacker = statesMap.get(stateId); + const defender = statesMap.get(ra(candidates)); + if (!attacker || !defender) continue; + + const attackerPower = getStatePower(attacker); + const defenderPower = getStatePower(defender); + if (attackerPower < defenderPower * gauss(1.6, 0.8, 0, 10, 2)) continue; // defender is too strong + + const war = simulateWar(attacker, defender); + wars.push(war); + } + + TIME && console.timeEnd("generateConflicts"); + return wars; + + function simulateWar(attacker: IState, defender: IState): IConflict { + const history = [`${attacker.name} declared a war on its rival ${defender.name}`]; + + // vassals join the war + function addVassals(state: IState, side: "attackers" | "defenders") { + const vassals = getVassals(state); + if (vassals.length === 0) return []; + const names = list(vassals.map(({name}) => name)); + history.push(`${state.name}'s vassal${vassals.length > 1 ? "s" : ""} ${names} joined the war on ${side} side`); + return vassals; + } + + const attackers = [attacker, ...addVassals(attacker, "attackers")]; + const defenders = [defender, ...addVassals(defender, "defenders")]; + + let attackersPower = d3.sum(attackers.map(getStatePower)); + let defendersPower = d3.sum(defenders.map(getStatePower)); + + defender.relations.forEach((relation, stateId) => { + if (relation !== "Ally" || !stateId) return; + const ally = statesMap.get(stateId)!; + if (ally.relations.includes("Vassal")) return; + + const allyParty = [ally, ...getVassals(ally)]; + + const joinedPower = defendersPower + d3.sum(allyParty.map(getStatePower)); + const isWeak = joinedPower < attackersPower * gauss(1.6, 0.8, 0, 10, 2); + const isRival = ally.relations[attacker.i] !== "Rival"; + if (!isRival && isWeak) { + // defender's ally does't involve: break the pact + const reason = ally.relations.includes("Enemy") ? "Being already at war," : `Frightened by ${attacker.name},`; + history.push(`${reason} ${ally.name} severed the defense pact with ${defender.name}`); + + allyParty.forEach(ally => { + defender.relations[ally.i] = "Suspicion"; + ally.relations[defender.i] = "Suspicion"; + }); + + return; + } + + // defender's ally and its vassals join the war + defenders.push(...allyParty); + const withVassals = allyParty.length > 1 ? " and its vassals " : ""; + history.push(`Defender's ally ${ally.name}${withVassals}joined the war`); + + defendersPower = joinedPower; + }); + + attacker.relations.forEach((relation, stateId) => { + if (relation !== "Ally" || !stateId) return; + if (defenders.some(defender => defender.i === stateId)) return; + const ally = statesMap.get(stateId)!; + if (ally.relations.includes("Vassal")) return; + + const allyParty = [ally, ...getVassals(ally)]; + + const joinedPower = attackersPower + d3.sum(allyParty.map(getStatePower)); + const isWeak = joinedPower < defendersPower * 1.2; + const isRival = ally.relations[defender.i] !== "Rival"; + if (!isRival || isWeak) { + history.push(`Attacker's ally ${ally.name} avoided entering the war`); + return; + } + + const allies = ally.relations.map((relation, stateId) => (relation === "Ally" ? stateId : 0)); + if (defenders.some(({i}) => allies.includes(i))) { + history.push(`Attacker's ally ${ally.name} did not join the war as it has allies on both sides`); + return; + } + + // attacker's ally and its vassals join the war + attackers.push(...allyParty); + const withVassals = allyParty.length > 1 ? " and its vassals " : ""; + history.push(`Attacker's ally ${ally.name}${withVassals}joined the war`); + + attackersPower = joinedPower; + }); + + // change relations to Enemy for all participants + attackers.forEach(attacker => { + defenders.forEach(defender => { + defender.relations[attacker.i] = "Enemy"; + attacker.relations[defender.i] = "Enemy"; + }); + }); + + const advantage = getAdvantage(attackersPower, defendersPower); + const winning = attackersPower > defendersPower ? "attackers" : "defenders"; + history.push(`At the moment, the ${advantage} advantage is on the side of the ${winning}`); + + const name = `${attacker.name}-${trimVowels(defender.name)}ian War`; + const parties = {attackers: attackers.map(({i}) => i), defenders: defenders.map(({i}) => i)}; + const start = options.year - gauss(2, 2, 0, 5); + return {type: "conflict", name, start, parties, description: history.join(". ")}; + } + + function getStatePower(state: IState) { + return state.area * state.expansionism; + } + + function getVassals(state: IState) { + return state.relations + .map((relation, stateId) => (relation === "Suzerain" ? stateId : 0)) + .filter(stateId => stateId) + .map(stateId => statesMap.get(stateId)!); + } + + function getAdvantage(p1: number, p2: number) { + const advantage = p1 > p2 ? p1 / p2 : p2 / p1; + if (advantage > 3) return "overwhelming"; + if (advantage > 2) return "decisive"; + if (advantage > 1.3) return "significant"; + return "minor"; + } +} diff --git a/src/scripts/generation/pack/burgsAndStates/simulateWars.ts b/src/scripts/generation/pack/burgsAndStates/simulateWars.ts deleted file mode 100644 index abe3c2b7..00000000 --- a/src/scripts/generation/pack/burgsAndStates/simulateWars.ts +++ /dev/null @@ -1,35 +0,0 @@ -import {TIME} from "config/logging"; - -import type {TStateData} from "./createStateData"; -import type {TDiplomacy} from "./generateRelations"; - -export function simulateWars(statesData: TStateData[], diplomacy: TDiplomacy) { - TIME && console.time("simulateWars"); - - // declare wars - for (const {i} of statesData) { - const relations = diplomacy[i]; - if (!relations.includes("Rival")) continue; // no rivals to attack - if (relations.includes("Vassal")) continue; // not independent - if (relations.includes("Enemy")) continue; // already at war - - // select candidates to attack: rival independent states - const candidates = relations - .map((relation, index) => (relation === "Rival" && !diplomacy[index].includes("Vassal") ? index : 0)) - .filter(index => index); - if (!candidates.length) continue; - - const attacker = getDiplomacyData(statesData[i], statistics); - const defender = getDiplomacyData(statesData[ra(candidates)], statistics); - - const attackerPower = attacker.area * attacker.expansionism; - const defenderPower = defender.area * defender.expansionism; - if (attackerPower < defenderPower * gauss(1.6, 0.8, 0, 10, 2)) continue; // defender is too strong - - const attackers = [attacker]; - const defenders = [defender]; - } - - TIME && console.timeEnd("simulateWars"); - return null; -} diff --git a/src/scripts/generation/pack/burgsAndStates/specifyStates.ts b/src/scripts/generation/pack/burgsAndStates/specifyStates.ts index bdc718a3..cebb55db 100644 --- a/src/scripts/generation/pack/burgsAndStates/specifyStates.ts +++ b/src/scripts/generation/pack/burgsAndStates/specifyStates.ts @@ -4,6 +4,7 @@ import {createAreaTiers, defineStateForm} from "./defineStateForm"; import {defineFullStateName, defineStateName} from "./defineStateName"; import {defineStateColors} from "./defineStateColors"; import {isBurg} from "utils/typeUtils"; +import {generateConflicts} from "./generateConflicts"; import type {TStateStatistics} from "./collectStatistics"; import type {TStateData} from "./createStateData"; @@ -32,13 +33,15 @@ export function specifyStates( if (!capitalName) throw new Error("State capital is not a burg"); const relations = diplomacy[i]; + const isVassal = relations.includes("Vassal"); + const nameBase = getNameBase(culture); const areaTier = getAreaTier(area); - const {form, formName} = defineStateForm(type, areaTier, nameBase, burgsNumber, relations, neighbors); + const {form, formName} = defineStateForm(type, areaTier, nameBase, burgsNumber, neighbors, isVassal); const name = defineStateName(center, capitalName, nameBase, formName); const fullName = defineFullStateName(name, formName); - const state: IState = { + return { name, ...stateData, form, @@ -51,9 +54,12 @@ export function specifyStates( neighbors, relations }; - return state; }); + const wars = generateConflicts(states); // mutates states + console.log(wars); + console.log(states); + TIME && console.timeEnd("specifyStates"); return [NEUTRALS, ...states]; } diff --git a/src/scripts/generation/pack/pack.ts b/src/scripts/generation/pack/pack.ts index 8ff96f6b..85af9c85 100644 --- a/src/scripts/generation/pack/pack.ts +++ b/src/scripts/generation/pack/pack.ts @@ -149,7 +149,6 @@ export function createPack(grid: IGrid): IPack { } }); - // BurgsAndStates.defineStateForms(); // BurgsAndStates.generateProvinces(); // BurgsAndStates.defineBurgFeatures(); diff --git a/src/types/events.d.ts b/src/types/events.d.ts new file mode 100644 index 00000000..25ef3a08 --- /dev/null +++ b/src/types/events.d.ts @@ -0,0 +1,15 @@ +interface IEvent { + type: string; + name: string; + start: number; + end?: number; // undefined for ongoing events + description: string; +} + +interface IConflict extends IEvent { + type: "conflict"; + parties: { + attackers: number[]; + defenders: number[]; + }; +} diff --git a/src/types/globals.d.ts b/src/types/globals.d.ts index 005d054a..c1c18074 100644 --- a/src/types/globals.d.ts +++ b/src/types/globals.d.ts @@ -24,6 +24,7 @@ interface IOptions { showMFCGMap: boolean; winds: [number, number, number, number, number, number]; stateLabelsMode: "auto" | "short" | "full"; + year: number; } declare let populationRate: number;