mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-17 09:41:24 +01:00
refactor: generate conflicts
This commit is contained in:
parent
136e71eae9
commit
a2192fb984
7 changed files with 190 additions and 47 deletions
|
|
@ -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";
|
||||
|
|
|
|||
158
src/scripts/generation/pack/burgsAndStates/generateConflicts.ts
Normal file
158
src/scripts/generation/pack/burgsAndStates/generateConflicts.ts
Normal file
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -149,7 +149,6 @@ export function createPack(grid: IGrid): IPack {
|
|||
}
|
||||
});
|
||||
|
||||
// BurgsAndStates.defineStateForms();
|
||||
// BurgsAndStates.generateProvinces();
|
||||
// BurgsAndStates.defineBurgFeatures();
|
||||
|
||||
|
|
|
|||
15
src/types/events.d.ts
vendored
Normal file
15
src/types/events.d.ts
vendored
Normal file
|
|
@ -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[];
|
||||
};
|
||||
}
|
||||
1
src/types/globals.d.ts
vendored
1
src/types/globals.d.ts
vendored
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue