Merge pull request #866 from Azgaar/vite-diplomacy

Vite diplomacy
This commit is contained in:
Azgaar 2022-09-05 21:01:48 +03:00 committed by GitHub
commit 37826f1c2d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 466 additions and 61 deletions

View file

@ -4,13 +4,15 @@
let grid = {}; // initial graph based on jittered square grid and data let grid = {}; // initial graph based on jittered square grid and data
let pack = {}; // packed graph and data let pack = {}; // packed graph and data
let notes = [];
let events = {};
let seed; let seed;
let mapId; let mapId;
let mapHistory = []; let mapHistory = [];
let elSelected; let elSelected;
let notes = [];
let customization = 0; let customization = 0;
let rulers; let rulers;

View file

@ -559,6 +559,7 @@ window.Markers = (function () {
const {cells, states} = pack; const {cells, states} = pack;
const state = states[cells.state[cell]]; const state = states[cells.state[cell]];
// TODO: use events
if (!state.campaigns) state.campaigns = BurgsAndStates.generateCampaign(state); if (!state.campaigns) state.campaigns = BurgsAndStates.generateCampaign(state);
const campaign = ra(state.campaigns); const campaign = ra(state.campaigns);
const date = generateDate(campaign.start, campaign.end); const date = generateDate(campaign.start, campaign.end);

View file

@ -149,11 +149,8 @@ window.Names = (function () {
// generate short name for base // generate short name for base
const getBaseShort = function (base) { const getBaseShort = function (base) {
if (nameBases[base] === undefined) { if (nameBases[base] === undefined) {
tip( const message = `Namebase ${base} does not exist. Please upload custom namebases of change the base in Cultures Editor`;
`Namebase ${base} does not exist. Please upload custom namebases of change the base in Cultures Editor`, tip(message, false, "error");
false,
"error"
);
base = 1; base = 1;
} }
const min = nameBases[base].min - 1; const min = nameBases[base].min - 1;

View file

@ -673,7 +673,7 @@ function getFileName(dataType) {
return name + " " + type + dateString; return name + " " + type + dateString;
} }
function downloadFile(data, name, type = "text/plain") { export function downloadFile(data, name, type = "text/plain") {
const dataBlob = new Blob([data], {type}); const dataBlob = new Blob([data], {type});
const url = window.URL.createObjectURL(dataBlob); const url = window.URL.createObjectURL(dataBlob);
const link = document.createElement("a"); const link = document.createElement("a");

View file

@ -26,7 +26,6 @@ import {createGrid} from "./grid/grid";
import {createPack} from "./pack/pack"; import {createPack} from "./pack/pack";
import {getInputValue, setInputValue} from "utils/nodeUtils"; import {getInputValue, setInputValue} from "utils/nodeUtils";
import {calculateMapCoordinates} from "modules/coordinates"; import {calculateMapCoordinates} from "modules/coordinates";
import {drawPoint} from "utils/debugUtils";
const {Zoom, ThreeD} = window; const {Zoom, ThreeD} = window;
@ -54,13 +53,14 @@ async function generate(options?: IGenerationOptions) {
window.mapCoordinates = calculateMapCoordinates(); window.mapCoordinates = calculateMapCoordinates();
const newGrid = await createGrid(grid, precreatedGraph); const newGrid = await createGrid(grid, precreatedGraph);
const newPack = createPack(newGrid); const {pack: newPack, conflicts} = createPack(newGrid);
// TODO: draw default ruler // TODO: draw default ruler
// redefine global grid and pack // redefine global grid and pack
grid = newGrid; grid = newGrid;
pack = newPack; pack = newPack;
events = {conflicts};
// temp rendering for debug // temp rendering for debug
// renderLayer("cells"); // renderLayer("cells");

View file

@ -126,7 +126,7 @@ export function generatePrecipitation(heights: Uint8Array, temperatures: Int8Arr
return precipitation; return precipitation;
} }
// TODO: move to renderer // TODO: move to renderers
function drawWindDirection() { function drawWindDirection() {
const wind = prec.append("g").attr("id", "wind"); const wind = prec.append("g").attr("id", "wind");

View file

@ -106,3 +106,22 @@ export const adjectivalForms = [
"Horde", "Horde",
"Marches" "Marches"
]; ];
export const relations = {
neighbors: {Ally: 1, Friendly: 2, Neutral: 1, Suspicion: 10, Rival: 9},
neighborsOfNeighbors: {Ally: 10, Friendly: 8, Neutral: 5, Suspicion: 1},
farStates: {Friendly: 1, Neutral: 12, Suspicion: 2},
navalToNaval: {Neutral: 2, Suspicion: 2, Rival: 1}
};
export const conflictTypes = {
War: 6,
Conflict: 2,
Campaign: 4,
Invasion: 2,
Rebellion: 2,
Conquest: 2,
Intervention: 1,
Expedition: 1,
Crusade: 1
};

View file

@ -0,0 +1,26 @@
import * as d3 from "d3";
import {getMixedColor} from "utils/colorUtils";
import {ra} from "utils/probabilityUtils";
import type {TStateStatistics} from "./collectStatistics";
export function defineStateColors(statistics: TStateStatistics) {
const scheme: Hex[] = d3.shuffle(["#e78ac3", "#a6d854", "#ffd92f", "#66c2a5", "#fc8d62", "#8da0cb"]);
const colors: Record<number, Hex> = {};
// assign colors using greedy algorithm
for (const i in statistics) {
const {neighbors} = statistics[i];
const schemeColor = scheme.find(schemeColor => neighbors.every(neighbor => colors[neighbor] !== schemeColor));
colors[i] = schemeColor || ra(scheme);
scheme.push(scheme.shift()!);
}
// make each color unique
for (const i in colors) {
const isColorReused = Object.values(colors).some(color => color === colors[i]);
if (isColorReused) colors[i] = getMixedColor(colors[i], 0.3);
}
return colors;
}

View file

@ -1,10 +1,10 @@
import {NAMEBASE} from "config/namebases";
import * as d3 from "d3"; import * as d3 from "d3";
import {NAMEBASE} from "config/namebases";
import {getInputNumber} from "utils/nodeUtils"; import {getInputNumber} from "utils/nodeUtils";
import {P, rand, rw} from "utils/probabilityUtils"; import {P, rand, rw} from "utils/probabilityUtils";
import type {TStateStatistics} from "./collectStatistics";
import {AreaTiers, culturalMonarchyFormsMap, culturalTheocracyFormsMap, StateForms} from "./config"; import {AreaTiers, culturalMonarchyFormsMap, culturalTheocracyFormsMap, StateForms} from "./config";
import type {TStateStatistics} from "./collectStatistics";
// create 5 area tiers, 4 is the biggest, 0 the smallest // create 5 area tiers, 4 is the biggest, 0 the smallest
export function createAreaTiers(statistics: TStateStatistics) { export function createAreaTiers(statistics: TStateStatistics) {
@ -23,9 +23,16 @@ export function createAreaTiers(statistics: TStateStatistics) {
}; };
} }
export function defineStateForm(type: TCultureType, areaTier: AreaTiers, nameBase: number, burgsNumber: number) { export function defineStateForm(
type: TCultureType,
areaTier: AreaTiers,
nameBase: number,
burgsNumber: number,
neighbors: number[],
isVassal: boolean
) {
const form = defineForm(type, areaTier); const form = defineForm(type, areaTier);
const formName = defineFormName(form, nameBase, areaTier, burgsNumber); const formName = defineFormName(form, nameBase, areaTier, burgsNumber, neighbors, isVassal);
return {form, formName}; return {form, formName};
} }
@ -51,23 +58,23 @@ function defineFormName(
form: ReturnType<typeof defineForm>, form: ReturnType<typeof defineForm>,
nameBase: number, nameBase: number,
areaTier: AreaTiers, areaTier: AreaTiers,
burgsNumber: number burgsNumber: number,
neighbors: number[],
isVassal: boolean
) { ) {
if (form === "Monarchy") return defineMonarchyForm(nameBase, areaTier); if (form === "Monarchy") return defineMonarchyForm(nameBase, areaTier, neighbors, isVassal);
if (form === "Republic") return defineRepublicForm(areaTier, burgsNumber); if (form === "Republic") return defineRepublicForm(areaTier, burgsNumber);
if (form === "Union") return rw(StateForms.union); if (form === "Union") return rw(StateForms.union);
if (form === "Theocracy") return defineTheocracyForm(nameBase, areaTier); if (form === "Theocracy") return defineTheocracyForm(nameBase, areaTier);
if (form === "Anarchy") return rw(StateForms.anarchy); if (form === "Anarchy") return rw(StateForms.anarchy);
return "test"; throw new Error("Unknown state form: " + form);
} }
// Default name depends on area tier, some name bases have special names for tiers // Default name depends on area tier, some name bases have special names for tiers
function defineMonarchyForm(nameBase: number, areaTier: AreaTiers, diplomacy = [""], neighbors = []) { function defineMonarchyForm(nameBase: number, areaTier: AreaTiers, neighbors: number[], isVassal: boolean) {
const form = StateForms.monarchy[areaTier]; const form = StateForms.monarchy[areaTier];
// TODO: specific names for vassals
const isVassal = diplomacy.includes("Vassal");
if (isVassal) { if (isVassal) {
if (areaTier === AreaTiers.DUCHY && neighbors.length > 1 && rand(6) < neighbors.length) return "Marches"; if (areaTier === AreaTiers.DUCHY && neighbors.length > 1 && rand(6) < neighbors.length) return "Marches";
if (nameBase === NAMEBASE.English && P(0.3)) return "Dominion"; if (nameBase === NAMEBASE.English && P(0.3)) return "Dominion";

View file

@ -7,30 +7,12 @@ import {createCapitals} from "./createCapitals";
import {createStateData} from "./createStateData"; import {createStateData} from "./createStateData";
import {createTowns} from "./createTowns"; import {createTowns} from "./createTowns";
import {expandStates} from "./expandStates"; import {expandStates} from "./expandStates";
import {generateRelations} from "./generateRelations";
import {specifyBurgs} from "./specifyBurgs"; import {specifyBurgs} from "./specifyBurgs";
import {specifyStates} from "./specifyStates"; import {specifyStates} from "./specifyStates";
type TCellsData = Pick< // prettier-ignore
IPack["cells"], type TCellsData = Pick<IPack["cells"], | "v" | "c" | "p" | "b" | "i" | "g" | "area" | "h" | "f" | "t" | "haven" | "harbor" | "r" | "fl" | "biome" | "s" | "pop" | "culture">;
| "v"
| "c"
| "p"
| "b"
| "i"
| "g"
| "area"
| "h"
| "f"
| "t"
| "haven"
| "harbor"
| "r"
| "fl"
| "biome"
| "s"
| "pop"
| "culture"
>;
export function generateBurgsAndStates( export function generateBurgsAndStates(
cultures: TCultures, cultures: TCultures,
@ -39,7 +21,7 @@ export function generateBurgsAndStates(
rivers: Omit<IRiver, "name" | "basin" | "type">[], rivers: Omit<IRiver, "name" | "basin" | "type">[],
vertices: IGraphVertices, vertices: IGraphVertices,
cells: TCellsData cells: TCellsData
): {burgIds: Uint16Array; stateIds: Uint16Array; burgs: TBurgs; states: TStates} { ): {burgIds: Uint16Array; stateIds: Uint16Array; burgs: TBurgs; states: TStates; conflicts: IConflict[]} {
const cellsNumber = cells.i.length; const cellsNumber = cells.i.length;
const scoredCellIds = getScoredCellIds(); const scoredCellIds = getScoredCellIds();
@ -49,7 +31,8 @@ export function generateBurgsAndStates(
burgIds: new Uint16Array(cellsNumber), burgIds: new Uint16Array(cellsNumber),
stateIds: new Uint16Array(cellsNumber), stateIds: new Uint16Array(cellsNumber),
burgs: [NO_BURG], burgs: [NO_BURG],
states: [NEUTRALS] states: [NEUTRALS],
conflicts: []
}; };
} }
@ -86,9 +69,10 @@ export function generateBurgsAndStates(
const burgIds = assignBurgIds(burgs); const burgIds = assignBurgIds(burgs);
const statistics = collectStatistics({...cells, state: stateIds, burg: burgIds}, burgs); const statistics = collectStatistics({...cells, state: stateIds, burg: burgIds}, burgs);
const states = specifyStates(statesData, statistics, cultures, burgs); const diplomacy = generateRelations(statesData, statistics, pick(cells, "f"));
const {states, conflicts} = specifyStates(statesData, statistics, diplomacy, cultures, burgs);
return {burgIds, stateIds, burgs, states}; return {burgIds, stateIds, burgs, states, conflicts};
function getScoredCellIds() { function getScoredCellIds() {
const score = new Int16Array(cells.s.map(s => s * Math.random())); const score = new Int16Array(cells.s.map(s => s * Math.random()));

View file

@ -0,0 +1,195 @@
import * as d3 from "d3";
import {TIME} from "config/logging";
import {getAdjective, list, trimVowels} from "utils/languageUtils";
import {gauss, P, ra, rw} from "utils/probabilityUtils";
import {conflictTypes} from "./config";
const {Names} = window;
export function generateConflicts(states: IState[], cultures: TCultures): IConflict[] {
TIME && console.time("generateConflicts");
const historicalWars = generateHistoricalConflicts(states, cultures);
const ongoingWars = generateOngoingConflicts(states);
TIME && console.timeEnd("generateConflicts");
return [...historicalWars, ...ongoingWars].sort((a, b) => a.start - b.start);
}
function generateOngoingConflicts(states: IState[]): IConflict[] {
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);
}
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 {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";
}
}
function generateHistoricalConflicts(states: IState[], cultures: TCultures): IConflict[] {
const statesMap = new Map(states.map(state => [state.i, state]));
const isConflict = (conflict: IConflict | null): conflict is IConflict => conflict !== null;
const getNameBase = (cultureId: number) => cultures[cultureId].base;
return states.map(generateConflicts).flat();
function generateConflicts(state: IState): IConflict[] {
const conflicts = state.neighbors
.map((neighbor, index) => {
if (index && P(0.8)) return null;
const enemy = statesMap.get(neighbor);
if (!enemy) return null;
const properName = P(0.8) ? enemy.name : Names.getBaseShort(getNameBase(enemy.culture));
const name = getAdjective(properName) + " " + rw(conflictTypes);
const start = gauss(options.year - 100, 150, 1, options.year - 6);
const end = start + gauss(4, 5, 1, options.year - start - 1);
const parties = {attackers: [state.i], defenders: [enemy.i]};
const conflict: IConflict = {name, start, end, parties};
return conflict;
})
.filter(isConflict);
return conflicts;
}
}

View file

@ -0,0 +1,124 @@
import * as d3 from "d3";
import {TIME} from "config/logging";
import {P, rw} from "utils/probabilityUtils";
import {relations} from "./config";
import type {TStateStatistics} from "./collectStatistics";
import type {TStateData} from "./createStateData";
export type TDiplomacy = {[key: number]: TRelation[]};
interface IDiplomacyData {
i: number;
type: TCultureType;
center: number;
expansionism: number;
area: number;
neighbors: number[];
}
export function generateRelations(
statesData: TStateData[],
statistics: TStateStatistics,
cells: Pick<IPack["cells"], "f">
) {
TIME && console.time("generateRelations");
const diplomacy = getBlankDiplomacyMatrix(statesData);
if (statesData.length < 2) return diplomacy;
const stateAreas = Object.values(statistics).map(({area}) => area);
const averageStateArea = d3.mean(stateAreas)!;
for (let i = 0; i < statesData.length; i++) {
const from = getDiplomacyData(statesData[i], statistics);
if (diplomacy[from.i].includes("Vassal")) {
// Vassal copy relations from its Suzerain
const suzerain = diplomacy[from.i].indexOf("Vassal");
for (const to of statesData) {
if (from.i === to.i || to.i === suzerain) continue;
diplomacy[from.i][to.i] = diplomacy[suzerain][to.i];
// vassals are Ally to each other
if (diplomacy[suzerain][to.i] === "Suzerain") diplomacy[from.i][to.i] = "Ally";
for (let e = 0; e < statesData.length; e++) {
const nested = statesData[e];
if (nested.i === from.i || nested.i === suzerain) continue;
if (diplomacy[nested.i][suzerain] === "Suzerain" || diplomacy[nested.i][suzerain] === "Vassal") continue;
diplomacy[nested.i][from.i] = diplomacy[nested.i][suzerain];
}
}
continue;
}
for (let j = i + 1; j < statesData.length; j++) {
const to = getDiplomacyData(statesData[j], statistics);
if (diplomacy[to.i].includes("Vassal")) {
// relations to vassal is the same as to its Suzerain
const suzerain = diplomacy[to.i].indexOf("Vassal");
diplomacy[from.i][to.i] = diplomacy[from.i][suzerain];
continue;
}
const isVassal = detectVassalState(from, to, averageStateArea);
if (isVassal) {
diplomacy[from.i][to.i] = "Suzerain";
diplomacy[to.i][from.i] = "Vassal";
continue;
}
const relations = defineRelations(from, to, cells.f);
diplomacy[from.i][to.i] = relations;
diplomacy[to.i][from.i] = relations;
}
}
TIME && console.timeEnd("generateRelations");
return diplomacy;
}
function getBlankDiplomacyMatrix(statesData: TStateData[]) {
const length = statesData.length + 1;
return statesData.reduce((acc, {i}) => {
acc[i] = new Array(length).fill("x");
return acc;
}, {} as TDiplomacy);
}
function getDiplomacyData(stateData: TStateData, statistics: TStateStatistics): IDiplomacyData {
const {i, type, center, expansionism} = stateData;
const {neighbors, area} = statistics[i];
return {i, type, center, expansionism, neighbors, area};
}
function detectVassalState(from: IDiplomacyData, to: IDiplomacyData, averageStateArea: number) {
if (P(0.2)) return false;
const isNeighbor = from.neighbors.includes(to.i);
if (!isNeighbor) return false;
const isMuchSmaller = from.area * 2 < to.area && from.area < averageStateArea && to.area > averageStateArea;
if (isMuchSmaller) return true;
return false;
}
function defineRelations(from: IDiplomacyData, to: IDiplomacyData, featureIds: Uint16Array) {
const isNeighbor = from.neighbors.includes(to.i);
if (isNeighbor) return rw(relations.neighbors); // relations between neighboring states
const isNeighborOfNeighbor = from.neighbors.some(neighbor => to.neighbors.includes(neighbor));
if (isNeighborOfNeighbor) return rw(relations.neighborsOfNeighbors); // relations between neighbors of neighbors
const isNaval = from.type === "Naval" && to.type === "Naval" && featureIds[from.center] !== featureIds[to.center];
if (isNaval) return rw(relations.navalToNaval); // relations between naval states on different islands
return rw(relations.farStates); // relations between far states
}

View file

@ -1,45 +1,63 @@
import {TIME} from "config/logging"; import {TIME} from "config/logging";
import {getColors} from "utils/colorUtils";
import {NEUTRALS} from "./config"; import {NEUTRALS} from "./config";
import {createAreaTiers, defineStateForm} from "./defineStateForm"; import {createAreaTiers, defineStateForm} from "./defineStateForm";
import {defineFullStateName, defineStateName} from "./defineStateName"; import {defineFullStateName, defineStateName} from "./defineStateName";
import {defineStateColors} from "./defineStateColors";
import {isBurg} from "utils/typeUtils"; import {isBurg} from "utils/typeUtils";
import {generateConflicts} from "./generateConflicts";
import type {TStateStatistics} from "./collectStatistics"; import type {TStateStatistics} from "./collectStatistics";
import type {TStateData} from "./createStateData"; import type {TStateData} from "./createStateData";
import type {TDiplomacy} from "./generateRelations";
export function specifyStates( export function specifyStates(
statesData: TStateData[], statesData: TStateData[],
statistics: TStateStatistics, statistics: TStateStatistics,
diplomacy: TDiplomacy,
cultures: TCultures, cultures: TCultures,
burgs: TBurgs burgs: TBurgs
): TStates { ): {states: TStates; conflicts: IConflict[]} {
TIME && console.time("specifyStates"); TIME && console.time("specifyStates");
const colors = getColors(statesData.length); const colors = defineStateColors(statistics);
const getAreaTier = createAreaTiers(statistics); const getAreaTier = createAreaTiers(statistics);
const getNameBase = (cultureId: number) => cultures[cultureId].base; const getNameBase = (cultureId: number) => cultures[cultureId].base;
const states: IState[] = statesData.map((stateData, index) => { const states: IState[] = statesData.map(stateData => {
const {i, center, type, culture, capital} = stateData; const {i, center, type, culture, capital} = stateData;
const {area, burgs: burgsNumber, ...stats} = statistics[i]; const {area, burgs: burgsNumber, neighbors, ...stats} = statistics[i];
const color = colors[i];
const capitalBurg = burgs[capital]; const capitalBurg = burgs[capital];
const capitalName = isBurg(capitalBurg) ? capitalBurg.name : null; const capitalName = isBurg(capitalBurg) ? capitalBurg.name : null;
if (!capitalName) throw new Error("State capital is not a burg"); 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 nameBase = getNameBase(culture);
const areaTier = getAreaTier(area); const areaTier = getAreaTier(area);
const {form, formName} = defineStateForm(type, areaTier, nameBase, burgsNumber); const {form, formName} = defineStateForm(type, areaTier, nameBase, burgsNumber, neighbors, isVassal);
const name = defineStateName(center, capitalName, nameBase, formName); const name = defineStateName(center, capitalName, nameBase, formName);
const fullName = defineFullStateName(name, formName); const fullName = defineFullStateName(name, formName);
const color = colors[index]; return {
name,
const state: IState = {name, ...stateData, form, formName, fullName, color, area, burgs: burgsNumber, ...stats}; ...stateData,
return state; form,
formName,
fullName,
color,
area,
burgs: burgsNumber,
...stats,
neighbors,
relations
};
}); });
const conflicts = generateConflicts(states, cultures); // mutates states
TIME && console.timeEnd("specifyStates"); TIME && console.timeEnd("specifyStates");
return [NEUTRALS, ...states]; return {states: [NEUTRALS, ...states], conflicts};
} }

View file

@ -18,7 +18,7 @@ import {generateReligions} from "./religions/generateReligions";
const {LAND_COAST, WATER_COAST, DEEPER_WATER} = DISTANCE_FIELD; const {LAND_COAST, WATER_COAST, DEEPER_WATER} = DISTANCE_FIELD;
const {Biomes} = window; const {Biomes} = window;
export function createPack(grid: IGrid): IPack { export function createPack(grid: IGrid): {pack: IPack; conflicts: IConflict[]} {
const {temp, prec} = grid.cells; const {temp, prec} = grid.cells;
const {vertices, cells} = repackGrid(grid); const {vertices, cells} = repackGrid(grid);
@ -97,7 +97,7 @@ export function createPack(grid: IGrid): IPack {
pop: population pop: population
}); });
const {burgIds, stateIds, burgs, states} = generateBurgsAndStates( const {burgIds, stateIds, burgs, states, conflicts} = generateBurgsAndStates(
cultures, cultures,
mergedFeatures, mergedFeatures,
temp, temp,
@ -149,7 +149,6 @@ export function createPack(grid: IGrid): IPack {
} }
}); });
// BurgsAndStates.defineStateForms();
// BurgsAndStates.generateProvinces(); // BurgsAndStates.generateProvinces();
// BurgsAndStates.defineBurgFeatures(); // BurgsAndStates.defineBurgFeatures();
@ -200,7 +199,7 @@ export function createPack(grid: IGrid): IPack {
religions religions
}; };
return pack; return {pack, conflicts};
} }
// repack grid cells: discart deep water cells, add land cells along the coast // repack grid cells: discart deep water cells, add land cells along the coast

17
src/types/events.d.ts vendored Normal file
View file

@ -0,0 +1,17 @@
interface IEvents {
conflicts: IConflict[];
}
interface IEvent {
name: string;
start: number;
end?: number; // undefined for ongoing events
description?: string;
}
interface IConflict extends IEvent {
parties: {
attackers: number[];
defenders: number[];
};
}

View file

@ -1,11 +1,13 @@
declare let grid: IGrid; declare let grid: IGrid;
declare let pack: IPack; declare let pack: IPack;
declare let notes: INote[];
declare let events: IEvents;
declare let seed: string; declare let seed: string;
declare let mapId: number; declare let mapId: number;
declare let mapHistory: IMapHistoryEntry[]; declare let mapHistory: IMapHistoryEntry[];
declare let notes: INote[];
declare let customization: number; declare let customization: number;
declare let rulers: Rulers; declare let rulers: Rulers;
@ -24,6 +26,7 @@ interface IOptions {
showMFCGMap: boolean; showMFCGMap: boolean;
winds: [number, number, number, number, number, number]; winds: [number, number, number, number, number, number];
stateLabelsMode: "auto" | "short" | "full"; stateLabelsMode: "auto" | "short" | "full";
year: number;
} }
declare let populationRate: number; declare let populationRate: number;

View file

@ -18,6 +18,7 @@ interface IState {
rural: number; rural: number;
urban: number; urban: number;
neighbors: number[]; neighbors: number[];
relations: TRelation[];
removed?: boolean; removed?: boolean;
} }
@ -36,3 +37,15 @@ interface ICoa {
shield: string; shield: string;
t1: string; t1: string;
} }
type TRelation =
| "Ally"
| "Friendly"
| "Neutral"
| "Suspicion"
| "Rival"
| "Unknown"
| "Suzerain"
| "Vassal"
| "Enemy"
| "x";