mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-17 17:51:24 +01:00
158 lines
6.1 KiB
TypeScript
158 lines
6.1 KiB
TypeScript
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";
|
|
}
|
|
}
|