diff --git a/public/libs/define-globals.js b/public/libs/define-globals.js index d1cf0020..fb1890a0 100644 --- a/public/libs/define-globals.js +++ b/public/libs/define-globals.js @@ -4,13 +4,15 @@ let grid = {}; // initial graph based on jittered square grid and data let pack = {}; // packed graph and data +let notes = []; +let events = {}; + let seed; let mapId; let mapHistory = []; let elSelected; -let notes = []; let customization = 0; let rulers; diff --git a/src/modules/markers-generator.js b/src/modules/markers-generator.js index bcd12577..fdef557f 100644 --- a/src/modules/markers-generator.js +++ b/src/modules/markers-generator.js @@ -559,6 +559,7 @@ window.Markers = (function () { const {cells, states} = pack; const state = states[cells.state[cell]]; + // TODO: use events if (!state.campaigns) state.campaigns = BurgsAndStates.generateCampaign(state); const campaign = ra(state.campaigns); const date = generateDate(campaign.start, campaign.end); diff --git a/src/modules/names-generator.js b/src/modules/names-generator.js index 21e8e062..a480ac77 100644 --- a/src/modules/names-generator.js +++ b/src/modules/names-generator.js @@ -149,11 +149,8 @@ window.Names = (function () { // generate short name for base const getBaseShort = function (base) { if (nameBases[base] === undefined) { - tip( - `Namebase ${base} does not exist. Please upload custom namebases of change the base in Cultures Editor`, - false, - "error" - ); + const message = `Namebase ${base} does not exist. Please upload custom namebases of change the base in Cultures Editor`; + tip(message, false, "error"); base = 1; } const min = nameBases[base].min - 1; diff --git a/src/modules/ui/editors.js b/src/modules/ui/editors.js index 7435974a..5c40ddb9 100644 --- a/src/modules/ui/editors.js +++ b/src/modules/ui/editors.js @@ -673,7 +673,7 @@ function getFileName(dataType) { 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 url = window.URL.createObjectURL(dataBlob); const link = document.createElement("a"); diff --git a/src/scripts/generation/generation.ts b/src/scripts/generation/generation.ts index 9091e41b..209afeb2 100644 --- a/src/scripts/generation/generation.ts +++ b/src/scripts/generation/generation.ts @@ -26,7 +26,6 @@ import {createGrid} from "./grid/grid"; import {createPack} from "./pack/pack"; import {getInputValue, setInputValue} from "utils/nodeUtils"; import {calculateMapCoordinates} from "modules/coordinates"; -import {drawPoint} from "utils/debugUtils"; const {Zoom, ThreeD} = window; @@ -54,13 +53,14 @@ async function generate(options?: IGenerationOptions) { window.mapCoordinates = calculateMapCoordinates(); const newGrid = await createGrid(grid, precreatedGraph); - const newPack = createPack(newGrid); + const {pack: newPack, conflicts} = createPack(newGrid); // TODO: draw default ruler // redefine global grid and pack grid = newGrid; pack = newPack; + events = {conflicts}; // temp rendering for debug // renderLayer("cells"); diff --git a/src/scripts/generation/grid/precipitation.ts b/src/scripts/generation/grid/precipitation.ts index 4b9bfda8..b13ac894 100644 --- a/src/scripts/generation/grid/precipitation.ts +++ b/src/scripts/generation/grid/precipitation.ts @@ -126,7 +126,7 @@ export function generatePrecipitation(heights: Uint8Array, temperatures: Int8Arr return precipitation; } -// TODO: move to renderer +// TODO: move to renderers function drawWindDirection() { const wind = prec.append("g").attr("id", "wind"); diff --git a/src/scripts/generation/pack/burgsAndStates/config.ts b/src/scripts/generation/pack/burgsAndStates/config.ts index 8461ab76..8d038625 100644 --- a/src/scripts/generation/pack/burgsAndStates/config.ts +++ b/src/scripts/generation/pack/burgsAndStates/config.ts @@ -106,3 +106,22 @@ export const adjectivalForms = [ "Horde", "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 +}; diff --git a/src/scripts/generation/pack/burgsAndStates/defineStateColors.ts b/src/scripts/generation/pack/burgsAndStates/defineStateColors.ts new file mode 100644 index 00000000..0216234b --- /dev/null +++ b/src/scripts/generation/pack/burgsAndStates/defineStateColors.ts @@ -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 = {}; + + // 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; +} diff --git a/src/scripts/generation/pack/burgsAndStates/defineStateForm.ts b/src/scripts/generation/pack/burgsAndStates/defineStateForm.ts index e6847c61..0610edd0 100644 --- a/src/scripts/generation/pack/burgsAndStates/defineStateForm.ts +++ b/src/scripts/generation/pack/burgsAndStates/defineStateForm.ts @@ -1,10 +1,10 @@ -import {NAMEBASE} from "config/namebases"; import * as d3 from "d3"; +import {NAMEBASE} from "config/namebases"; import {getInputNumber} from "utils/nodeUtils"; import {P, rand, rw} from "utils/probabilityUtils"; -import type {TStateStatistics} from "./collectStatistics"; import {AreaTiers, culturalMonarchyFormsMap, culturalTheocracyFormsMap, StateForms} from "./config"; +import type {TStateStatistics} from "./collectStatistics"; // create 5 area tiers, 4 is the biggest, 0 the smallest 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 formName = defineFormName(form, nameBase, areaTier, burgsNumber); + const formName = defineFormName(form, nameBase, areaTier, burgsNumber, neighbors, isVassal); return {form, formName}; } @@ -51,23 +58,23 @@ function defineFormName( form: ReturnType, nameBase: number, 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 === "Union") return rw(StateForms.union); if (form === "Theocracy") return defineTheocracyForm(nameBase, areaTier); 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 -function defineMonarchyForm(nameBase: number, areaTier: AreaTiers, diplomacy = [""], neighbors = []) { +function defineMonarchyForm(nameBase: number, areaTier: AreaTiers, neighbors: number[], isVassal: boolean) { const form = StateForms.monarchy[areaTier]; - // TODO: specific names for vassals - const isVassal = diplomacy.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/generateBurgsAndStates.ts b/src/scripts/generation/pack/burgsAndStates/generateBurgsAndStates.ts index fc370ff3..d2d2dc8f 100644 --- a/src/scripts/generation/pack/burgsAndStates/generateBurgsAndStates.ts +++ b/src/scripts/generation/pack/burgsAndStates/generateBurgsAndStates.ts @@ -7,30 +7,12 @@ import {createCapitals} from "./createCapitals"; import {createStateData} from "./createStateData"; import {createTowns} from "./createTowns"; import {expandStates} from "./expandStates"; +import {generateRelations} from "./generateRelations"; import {specifyBurgs} from "./specifyBurgs"; import {specifyStates} from "./specifyStates"; -type TCellsData = Pick< - IPack["cells"], - | "v" - | "c" - | "p" - | "b" - | "i" - | "g" - | "area" - | "h" - | "f" - | "t" - | "haven" - | "harbor" - | "r" - | "fl" - | "biome" - | "s" - | "pop" - | "culture" ->; +// prettier-ignore +type TCellsData = Pick; export function generateBurgsAndStates( cultures: TCultures, @@ -39,7 +21,7 @@ export function generateBurgsAndStates( rivers: Omit[], vertices: IGraphVertices, 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 scoredCellIds = getScoredCellIds(); @@ -49,7 +31,8 @@ export function generateBurgsAndStates( burgIds: new Uint16Array(cellsNumber), stateIds: new Uint16Array(cellsNumber), burgs: [NO_BURG], - states: [NEUTRALS] + states: [NEUTRALS], + conflicts: [] }; } @@ -86,9 +69,10 @@ export function generateBurgsAndStates( const burgIds = assignBurgIds(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() { const score = new Int16Array(cells.s.map(s => s * Math.random())); diff --git a/src/scripts/generation/pack/burgsAndStates/generateConflicts.ts b/src/scripts/generation/pack/burgsAndStates/generateConflicts.ts new file mode 100644 index 00000000..30f7f8c8 --- /dev/null +++ b/src/scripts/generation/pack/burgsAndStates/generateConflicts.ts @@ -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; + } +} diff --git a/src/scripts/generation/pack/burgsAndStates/generateRelations.ts b/src/scripts/generation/pack/burgsAndStates/generateRelations.ts new file mode 100644 index 00000000..2f84eae4 --- /dev/null +++ b/src/scripts/generation/pack/burgsAndStates/generateRelations.ts @@ -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 +) { + 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 +} diff --git a/src/scripts/generation/pack/burgsAndStates/specifyStates.ts b/src/scripts/generation/pack/burgsAndStates/specifyStates.ts index 3bc1fbbc..ba9be9ee 100644 --- a/src/scripts/generation/pack/burgsAndStates/specifyStates.ts +++ b/src/scripts/generation/pack/burgsAndStates/specifyStates.ts @@ -1,45 +1,63 @@ import {TIME} from "config/logging"; -import {getColors} from "utils/colorUtils"; import {NEUTRALS} from "./config"; 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"; +import type {TDiplomacy} from "./generateRelations"; export function specifyStates( statesData: TStateData[], statistics: TStateStatistics, + diplomacy: TDiplomacy, cultures: TCultures, burgs: TBurgs -): TStates { +): {states: TStates; conflicts: IConflict[]} { TIME && console.time("specifyStates"); - const colors = getColors(statesData.length); + const colors = defineStateColors(statistics); const getAreaTier = createAreaTiers(statistics); 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 {area, burgs: burgsNumber, ...stats} = statistics[i]; + const {area, burgs: burgsNumber, neighbors, ...stats} = statistics[i]; + const color = colors[i]; const capitalBurg = burgs[capital]; const capitalName = isBurg(capitalBurg) ? capitalBurg.name : null; 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); + const {form, formName} = defineStateForm(type, areaTier, nameBase, burgsNumber, neighbors, isVassal); const name = defineStateName(center, capitalName, nameBase, formName); const fullName = defineFullStateName(name, formName); - const color = colors[index]; - - const state: IState = {name, ...stateData, form, formName, fullName, color, area, burgs: burgsNumber, ...stats}; - return state; + return { + name, + ...stateData, + form, + formName, + fullName, + color, + area, + burgs: burgsNumber, + ...stats, + neighbors, + relations + }; }); + const conflicts = generateConflicts(states, cultures); // mutates states + TIME && console.timeEnd("specifyStates"); - return [NEUTRALS, ...states]; + return {states: [NEUTRALS, ...states], conflicts}; } diff --git a/src/scripts/generation/pack/pack.ts b/src/scripts/generation/pack/pack.ts index 8ff96f6b..f5be5c3c 100644 --- a/src/scripts/generation/pack/pack.ts +++ b/src/scripts/generation/pack/pack.ts @@ -18,7 +18,7 @@ import {generateReligions} from "./religions/generateReligions"; const {LAND_COAST, WATER_COAST, DEEPER_WATER} = DISTANCE_FIELD; const {Biomes} = window; -export function createPack(grid: IGrid): IPack { +export function createPack(grid: IGrid): {pack: IPack; conflicts: IConflict[]} { const {temp, prec} = grid.cells; const {vertices, cells} = repackGrid(grid); @@ -97,7 +97,7 @@ export function createPack(grid: IGrid): IPack { pop: population }); - const {burgIds, stateIds, burgs, states} = generateBurgsAndStates( + const {burgIds, stateIds, burgs, states, conflicts} = generateBurgsAndStates( cultures, mergedFeatures, temp, @@ -149,7 +149,6 @@ export function createPack(grid: IGrid): IPack { } }); - // BurgsAndStates.defineStateForms(); // BurgsAndStates.generateProvinces(); // BurgsAndStates.defineBurgFeatures(); @@ -200,7 +199,7 @@ export function createPack(grid: IGrid): IPack { religions }; - return pack; + return {pack, conflicts}; } // repack grid cells: discart deep water cells, add land cells along the coast diff --git a/src/types/events.d.ts b/src/types/events.d.ts new file mode 100644 index 00000000..814ba4af --- /dev/null +++ b/src/types/events.d.ts @@ -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[]; + }; +} diff --git a/src/types/globals.d.ts b/src/types/globals.d.ts index 005d054a..15f81616 100644 --- a/src/types/globals.d.ts +++ b/src/types/globals.d.ts @@ -1,11 +1,13 @@ declare let grid: IGrid; declare let pack: IPack; +declare let notes: INote[]; +declare let events: IEvents; + declare let seed: string; declare let mapId: number; declare let mapHistory: IMapHistoryEntry[]; -declare let notes: INote[]; declare let customization: number; declare let rulers: Rulers; @@ -24,6 +26,7 @@ interface IOptions { showMFCGMap: boolean; winds: [number, number, number, number, number, number]; stateLabelsMode: "auto" | "short" | "full"; + year: number; } declare let populationRate: number; diff --git a/src/types/pack/states.d.ts b/src/types/pack/states.d.ts index beeb7dd0..8136e6ce 100644 --- a/src/types/pack/states.d.ts +++ b/src/types/pack/states.d.ts @@ -18,6 +18,7 @@ interface IState { rural: number; urban: number; neighbors: number[]; + relations: TRelation[]; removed?: boolean; } @@ -36,3 +37,15 @@ interface ICoa { shield: string; t1: string; } + +type TRelation = + | "Ally" + | "Friendly" + | "Neutral" + | "Suspicion" + | "Rival" + | "Unknown" + | "Suzerain" + | "Vassal" + | "Enemy" + | "x";