mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-17 09:41:24 +01:00
commit
37826f1c2d
17 changed files with 466 additions and 61 deletions
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -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()));
|
||||||
|
|
|
||||||
195
src/scripts/generation/pack/burgsAndStates/generateConflicts.ts
Normal file
195
src/scripts/generation/pack/burgsAndStates/generateConflicts.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
124
src/scripts/generation/pack/burgsAndStates/generateRelations.ts
Normal file
124
src/scripts/generation/pack/burgsAndStates/generateRelations.ts
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -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};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
17
src/types/events.d.ts
vendored
Normal 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[];
|
||||||
|
};
|
||||||
|
}
|
||||||
5
src/types/globals.d.ts
vendored
5
src/types/globals.d.ts
vendored
|
|
@ -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;
|
||||||
|
|
|
||||||
13
src/types/pack/states.d.ts
vendored
13
src/types/pack/states.d.ts
vendored
|
|
@ -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";
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue