diff --git a/src/modules/burgs-and-states.js b/src/modules/burgs-and-states.js index be4b359e..be1dba67 100644 --- a/src/modules/burgs-and-states.js +++ b/src/modules/burgs-and-states.js @@ -1053,7 +1053,7 @@ window.BurgsAndStates = (function () { const isTheocracy = (religion && pack.religions[religion].expansion === "state") || (P(0.1) && ["Organized", "Cult"].includes(pack.religions[religion].type)); - const isAnarchy = P(0.01 - tier / 500); + const isAnarchy = P((1 - tier / 5) / 100); // 1% for smallest states, 0.2% for biggest if (isTheocracy) s.form = "Theocracy"; else if (isAnarchy) s.form = "Anarchy"; diff --git a/src/scripts/events/onhover.ts b/src/scripts/events/onhover.ts index ac1ce961..8eb166b0 100644 --- a/src/scripts/events/onhover.ts +++ b/src/scripts/events/onhover.ts @@ -16,6 +16,7 @@ import { } from "utils/unitUtils"; import {showMainTip, tip} from "scripts/tooltips"; import {defineEmblemData} from "./utils"; +import {isState} from "utils/typeUtils"; export const onMouseMove = debounce(handleMouseMove, 100); @@ -75,8 +76,9 @@ const getHoveredElement = (tagName: string, group: string, subgroup: string, isL if (layerIsOn("togglePopulation")) return "populationLayer"; if (layerIsOn("toggleTemp")) return "temperatureLayer"; if (layerIsOn("toggleBiomes") && biome[cellId]) return "biomesLayer"; - if (religion[cellId]) return "religionsLayer"; // layerIsOn("toggleReligions") && - if (layerIsOn("toggleProvinces") || (layerIsOn("toggleStates") && state[cellId])) return "statesLayer"; + if (layerIsOn("toggleReligions") && religion[cellId]) return "religionsLayer"; + // if (layerIsOn("toggleProvinces") || (layerIsOn("toggleStates") && state[cellId])) return "statesLayer"; + if (state[cellId]) return "statesLayer"; if (layerIsOn("toggleCultures") && culture[cellId]) return "culturesLayer"; if (layerIsOn("toggleHeight")) return "heightLayer"; @@ -193,16 +195,18 @@ const onHoverEventsMap: OnHoverEventMap = { }, statesLayer: ({packCellId}) => { - const state = pack.cells.state[packCellId]; - const stateName = pack.states[state].fullName; - const province = pack.cells.province[packCellId]; - const prov = province ? `${pack.provinces[province].fullName}, ` : ""; - tip(prov + stateName); + const stateId = pack.cells.state[packCellId]; + const state = pack.states[stateId]; + const stateName = isState(state) ? state.fullName || state.name : state.name; - highlightDialogLine("statesEditor", state); - highlightDialogLine("diplomacyEditor", state); - highlightDialogLine("militaryEditor", state); - highlightDialogLine("provincesEditor", province); + const provinceId = pack.cells.province[packCellId]; + const provinceName = provinceId ? `${pack.provinces[provinceId].fullName}, ` : ""; + tip(provinceName + stateName); + + highlightDialogLine("statesEditor", stateId); + highlightDialogLine("diplomacyEditor", stateId); + highlightDialogLine("militaryEditor", stateId); + highlightDialogLine("provincesEditor", provinceId); }, culturesLayer: ({packCellId}) => { diff --git a/src/scripts/generation/generation.ts b/src/scripts/generation/generation.ts index 614840b4..9091e41b 100644 --- a/src/scripts/generation/generation.ts +++ b/src/scripts/generation/generation.ts @@ -70,8 +70,8 @@ async function generate(options?: IGenerationOptions) { // renderLayer("biomes"); renderLayer("burgs"); renderLayer("routes"); - // renderLayer("states"); - renderLayer("religions"); + renderLayer("states"); + // renderLayer("religions"); // pack.cells.route.forEach((route, index) => { // if (route === 2) drawPoint(pack.cells.p[index], {color: "black"}); diff --git a/src/scripts/generation/pack/burgsAndStates/collectStatistics.ts b/src/scripts/generation/pack/burgsAndStates/collectStatistics.ts new file mode 100644 index 00000000..30f236c8 --- /dev/null +++ b/src/scripts/generation/pack/burgsAndStates/collectStatistics.ts @@ -0,0 +1,46 @@ +import {MIN_LAND_HEIGHT} from "config/generation"; +import {TIME} from "config/logging"; + +export type TStateStatistics = Record; +const initialData = {cells: 0, area: 0, burgs: 0, rural: 0, urban: 0, neighbors: [] as number[]}; + +// calculate states data like area, population, etc. +export function collectStatistics( + cells: Pick, + burgs: TBurgs +) { + TIME && console.time("collectStatistics"); + + const statesData: TStateStatistics = {}; + const initiate = (stateId: number) => { + statesData[stateId] = structuredClone(initialData); + }; + + // check for neighboring states + const checkNeib = (neibCellId: number, stateId: number) => { + const neibStateId = cells.state[neibCellId]; + if (!neibStateId || neibStateId === stateId) return; + if (!statesData[stateId].neighbors.includes(neibStateId)) statesData[stateId].neighbors.push(neibStateId); + }; + + for (const cellId of cells.i) { + if (cells.h[cellId] < MIN_LAND_HEIGHT) continue; + const stateId = cells.state[cellId]; + if (!statesData[stateId]) initiate(stateId); + + cells.c[cellId].forEach(neibCellId => checkNeib(neibCellId, stateId)); + + statesData[stateId].cells += 1; + statesData[stateId].area += cells.area[cellId]; + statesData[stateId].rural += cells.pop[cellId]; + + const burgId = cells.burg[cellId]; + if (burgId) { + statesData[stateId].burgs += 1; + statesData[stateId].urban += (burgs[burgId] as IBurg)?.population || 0; + } + } + + TIME && console.timeEnd("collectStatistics"); + return statesData; +} diff --git a/src/scripts/generation/pack/burgsAndStates/createStateData.ts b/src/scripts/generation/pack/burgsAndStates/createStateData.ts new file mode 100644 index 00000000..8e155d81 --- /dev/null +++ b/src/scripts/generation/pack/burgsAndStates/createStateData.ts @@ -0,0 +1,34 @@ +import {TIME} from "config/logging"; +import {getInputNumber} from "utils/nodeUtils"; +import {rn} from "utils/numberUtils"; +import type {createCapitals} from "./createCapitals"; +import {defineStateName} from "./defineStateName"; +import {generateStateEmblem} from "./generateStateEmblem"; + +type TCapitals = ReturnType; +export type TStateData = Pick< + IState, + "i" | "name" | "type" | "culture" | "center" | "expansionism" | "capital" | "coa" +>; + +export function createStateData(capitals: TCapitals, cultures: TCultures) { + TIME && console.time("createStates"); + + const powerInput = getInputNumber("powerInput"); + + const statesData: TStateData[] = capitals.map((capital, index) => { + const {cell: cellId, culture: cultureId, name: capitalName} = capital; + const id = index + 1; + const name = defineStateName(cellId, capitalName, cultureId, cultures); + + const {type, shield} = cultures[cultureId] as ICulture; + const expansionism = rn(Math.random() * powerInput + 1, 1); + + const coa = generateStateEmblem(type, shield); + + return {i: id, name, type, center: cellId, expansionism, capital: id, culture: cultureId, coa}; + }); + + TIME && console.timeEnd("createStates"); + return statesData; +} diff --git a/src/scripts/generation/pack/burgsAndStates/createStates.ts b/src/scripts/generation/pack/burgsAndStates/createStates.ts deleted file mode 100644 index 73c5591b..00000000 --- a/src/scripts/generation/pack/burgsAndStates/createStates.ts +++ /dev/null @@ -1,44 +0,0 @@ -import {TIME} from "config/logging"; -import {getColors} from "utils/colorUtils"; -import {getInputNumber} from "utils/nodeUtils"; -import {rn} from "utils/numberUtils"; -import {each} from "utils/probabilityUtils"; -import {NEUTRALS} from "./config"; -import type {createCapitals} from "./createCapitals"; - -const {Names, COA} = window; - -type TCapitals = ReturnType; - -export function createStates(capitals: TCapitals, cultures: TCultures): TStates { - TIME && console.time("createStates"); - - const colors = getColors(capitals.length); - const powerInput = getInputNumber("powerInput"); - - const states = capitals.map((capital, index) => { - const {cell: cellId, culture: cultureId, name: capitalName} = capital; - const id = index + 1; - const name = getStateName(cellId, capitalName, cultureId, cultures); - const color = colors[index]; - - const {type, shield: cultureShield} = cultures[cultureId] as ICulture; - const expansionism = rn(Math.random() * powerInput + 1, 1); - - const shield = COA.getShield(cultureShield, null); - const coa: ICoa = {...COA.generate(null, null, null, type), shield}; - - return {i: id, name, type, center: cellId, color, expansionism, capital: id, culture: cultureId, coa} as IState; - }); - - TIME && console.timeEnd("createStates"); - return [NEUTRALS, ...states]; -} - -function getStateName(cellId: number, capitalName: string, cultureId: number, cultures: TCultures): string { - const useCapitalName = capitalName.length < 9 && each(5)(cellId); - const nameBase = cultures[cultureId].base; - const basename: string = useCapitalName ? capitalName : Names.getBaseShort(nameBase); - - return Names.getState(basename, basename); -} diff --git a/src/scripts/generation/pack/burgsAndStates/defineStateForm.ts b/src/scripts/generation/pack/burgsAndStates/defineStateForm.ts new file mode 100644 index 00000000..cb7092fe --- /dev/null +++ b/src/scripts/generation/pack/burgsAndStates/defineStateForm.ts @@ -0,0 +1,64 @@ +import * as d3 from "d3"; + +import type {TStateStatistics} from "./collectStatistics"; + +const generic = {Monarchy: 25, Republic: 2, Union: 1}; +const naval = {Monarchy: 25, Republic: 8, Union: 3}; + +const republic = { + Republic: 75, + Federation: 4, + "Trade Company": 4, + "Most Serene Republic": 2, + Oligarchy: 2, + Tetrarchy: 1, + Triumvirate: 1, + Diarchy: 1, + Junta: 1 +}; + +const union = { + Union: 3, + League: 4, + Confederation: 1, + "United Kingdom": 1, + "United Republic": 1, + "United Provinces": 2, + Commonwealth: 1, + Heptarchy: 1 +}; + +const theocracy = {Theocracy: 20, Brotherhood: 1, Thearchy: 2, See: 1, "Holy State": 1}; + +const anarchy = {"Free Territory": 2, Council: 3, Commune: 1, Community: 1}; + +const monarchy = ["Duchy", "Grand Duchy", "Principality", "Kingdom", "Empire"]; // per area tier + +enum AreaTiers { + DUCHY = 0, + GRAND_DUCHY = 1, + PRINCIPALITY = 2, + KINGDOM = 3, + EMPIRE = 4 +} + +// create 5 area tiers, where 4 are the biggest, 0 the smallest +export function createAreaTiers(statistics: TStateStatistics) { + const stateAreas = Object.entries(statistics) + .filter(([id]) => Number(id)) + .map(([, {area}]) => area); + const medianArea = d3.median(stateAreas)!; + + const topTierIndex = Math.max(Math.ceil(stateAreas.length ** 0.4) - 2, 0); + const minTopTierArea = stateAreas.sort((a, b) => b - a)[topTierIndex]; + + return (area: number) => { + const tier = Math.min(Math.floor((area / medianArea) * 2.6), 4) as AreaTiers; + if (tier === AreaTiers.EMPIRE && area < minTopTierArea) return AreaTiers.KINGDOM; + return tier; + }; +} + +export function defineStateForm(type: TCultureType, areaTier: AreaTiers) { + return {form: "testForm", formName: "testFormName"}; +} diff --git a/src/scripts/generation/pack/burgsAndStates/defineStateName.ts b/src/scripts/generation/pack/burgsAndStates/defineStateName.ts new file mode 100644 index 00000000..7c40451c --- /dev/null +++ b/src/scripts/generation/pack/burgsAndStates/defineStateName.ts @@ -0,0 +1,15 @@ +import {each} from "utils/probabilityUtils"; + +const {Names} = window; + +export function defineStateName(cellId: number, capitalName: string, cultureId: number, cultures: TCultures): string { + const useCapitalName = capitalName.length < 9 && each(5)(cellId); + const nameBase = cultures[cultureId].base; + const basename: string = useCapitalName ? capitalName : Names.getBaseShort(nameBase); + + return Names.getState(basename, basename); +} + +export function defineFullStateName(name: string, form: string) { + return `${name} ${form}`; +} diff --git a/src/scripts/generation/pack/burgsAndStates/expandStates.ts b/src/scripts/generation/pack/burgsAndStates/expandStates.ts index 1de4dcf8..64bdc3aa 100644 --- a/src/scripts/generation/pack/burgsAndStates/expandStates.ts +++ b/src/scripts/generation/pack/burgsAndStates/expandStates.ts @@ -4,12 +4,12 @@ import {TIME} from "config/logging"; import {getInputNumber} from "utils/nodeUtils"; import {minmax} from "utils/numberUtils"; import {ELEVATION, FOREST_BIOMES, MIN_LAND_HEIGHT, DISTANCE_FIELD} from "config/generation"; -import {isNeutals} from "utils/typeUtils"; +import type {TStateData} from "./createStateData"; // growth algorithm to assign cells to states export function expandStates( capitalCells: Map, - states: TStates, + statesData: TStateData[], features: TPackFeatures, cells: Pick ) { @@ -24,10 +24,7 @@ export function expandStates( const neutralInput = getInputNumber("neutralInput"); const maxExpansionCost = (cellsNumber / 2) * neutralInput * statesNeutral; - for (const state of states) { - if (state.i === 0) continue; - - const {i: stateId, center: cellId} = state as IState; + for (const {i: stateId, center: cellId} of statesData) { stateIds[cellId] = stateId; cost[cellId] = 1; queue.push({cellId, stateId}, 0); @@ -66,11 +63,13 @@ export function expandStates( const GENERIC_LANDLOCKED_FEE = 0; const NAVAL_LANDLOCKED_FEE = 30; + const statesMap = new Map(statesData.map(stateData => [stateData.i, stateData])); + while (queue.length) { const priority = queue.peekValue()!; const {cellId, stateId} = queue.pop()!; - const {type, culture, center, expansionism} = getState(stateId); + const {type, culture, center, expansionism} = statesMap.get(stateId)!; const capitalBiome = cells.biome[center]; cells.c[cellId].forEach(neibCellId => { @@ -100,12 +99,6 @@ export function expandStates( return normalizeStates(stateIds, capitalCells, cells.c, cells.h); - function getState(stateId: number) { - const state = states[stateId]; - if (isNeutals(state)) throw new Error("Neutrals cannot expand"); - return state; - } - function getCultureCost(cellId: number, stateCulture: number) { return cells.culture[cellId] === stateCulture ? SAME_CULTURE_BONUS : DIFFERENT_CULTURES_FEE; } diff --git a/src/scripts/generation/pack/burgsAndStates/generateBurgsAndStates.ts b/src/scripts/generation/pack/burgsAndStates/generateBurgsAndStates.ts index eeefba07..b3750328 100644 --- a/src/scripts/generation/pack/burgsAndStates/generateBurgsAndStates.ts +++ b/src/scripts/generation/pack/burgsAndStates/generateBurgsAndStates.ts @@ -1,12 +1,14 @@ import {WARN} from "config/logging"; import {pick} from "utils/functionUtils"; import {getInputNumber} from "utils/nodeUtils"; +import {collectStatistics} from "./collectStatistics"; import {NEUTRALS, NO_BURG} from "./config"; import {createCapitals} from "./createCapitals"; -import {createStates} from "./createStates"; +import {createStateData} from "./createStateData"; import {createTowns} from "./createTowns"; import {expandStates} from "./expandStates"; import {specifyBurgs} from "./specifyBurgs"; +import {specifyStates} from "./specifyStates"; export function generateBurgsAndStates( cultures: TCultures, @@ -16,7 +18,24 @@ export function generateBurgsAndStates( vertices: IGraphVertices, cells: Pick< IPack["cells"], - "v" | "c" | "p" | "b" | "i" | "g" | "h" | "f" | "t" | "haven" | "harbor" | "r" | "fl" | "biome" | "s" | "culture" + | "v" + | "c" + | "p" + | "b" + | "i" + | "g" + | "area" + | "h" + | "f" + | "t" + | "haven" + | "harbor" + | "r" + | "fl" + | "biome" + | "s" + | "pop" + | "culture" > ): {burgIds: Uint16Array; stateIds: Uint16Array; burgs: TBurgs; states: TStates} { const cellsNumber = cells.i.length; @@ -34,7 +53,7 @@ export function generateBurgsAndStates( const capitals = createCapitals(statesNumber, scoredCellIds, cultures, pick(cells, "p", "f", "culture")); const capitalCells = new Map(capitals.map(({cell}) => [cell, true])); - const states = createStates(capitals, cultures); + const statesData = createStateData(capitals, cultures); const towns = createTowns( cultures, @@ -44,7 +63,7 @@ export function generateBurgsAndStates( const stateIds = expandStates( capitalCells, - states, + statesData, features, pick(cells, "c", "h", "f", "t", "r", "fl", "s", "biome", "culture") ); @@ -57,13 +76,16 @@ export function generateBurgsAndStates( temp, vertices, cultures, - states, + statesData, rivers, pick(cells, "v", "p", "g", "h", "f", "haven", "harbor", "s", "biome", "fl", "r") ); const burgIds = assignBurgIds(burgs); + const statistics = collectStatistics({...cells, state: stateIds, burg: burgIds}, burgs); + const states = specifyStates(statesData, statistics, stateIds, burgIds); + return {burgIds, stateIds, burgs, states}; function getScoredCellIds() { diff --git a/src/scripts/generation/pack/burgsAndStates/generateStateEmblem.ts b/src/scripts/generation/pack/burgsAndStates/generateStateEmblem.ts new file mode 100644 index 00000000..1785a61f --- /dev/null +++ b/src/scripts/generation/pack/burgsAndStates/generateStateEmblem.ts @@ -0,0 +1,8 @@ +const {COA} = window; + +export function generateStateEmblem(type: string, cultureShield: string) { + const shield = COA.getShield(cultureShield, null); + const coa: ICoa = {...COA.generate(null, null, null, type), shield}; + + return coa; +} diff --git a/src/scripts/generation/pack/burgsAndStates/specifyBurgs.ts b/src/scripts/generation/pack/burgsAndStates/specifyBurgs.ts index ba17bc79..ce7b0b44 100644 --- a/src/scripts/generation/pack/burgsAndStates/specifyBurgs.ts +++ b/src/scripts/generation/pack/burgsAndStates/specifyBurgs.ts @@ -6,14 +6,13 @@ import {gauss, P} from "utils/probabilityUtils"; import {NO_BURG} from "./config"; import type {createCapitals} from "./createCapitals"; -import type {createStates} from "./createStates"; +import type {TStateData} from "./createStateData"; import type {createTowns} from "./createTowns"; const {COA} = window; type TCapitals = ReturnType; type TTowns = ReturnType; -type TStatesReturn = ReturnType; export function specifyBurgs( capitals: TCapitals, @@ -23,12 +22,14 @@ export function specifyBurgs( temp: Int8Array, vertices: IGraphVertices, cultures: TCultures, - states: TStatesReturn, + statesData: TStateData[], rivers: Omit[], cells: Pick ): TBurgs { TIME && console.time("specifyBurgs"); + const stateDataMap = new Map(statesData.map(data => [data.i, data])); + const burgs = [...capitals, ...towns].map((burgData, index) => { const {cell, culture, capital} = burgData; const state = stateIds[cell]; @@ -38,7 +39,8 @@ export function specifyBurgs( const [x, y] = defineLocation(cell, port); const type = defineType(cell, port, population); - const coa: ICoa = defineEmblem(state, culture, port, capital, type, cultures, states); + const stateData = stateDataMap.get(state)!; + const coa: ICoa = defineEmblem(culture, port, capital, type, cultures, stateData); const burg: IBurg = {i: index + 1, ...burgData, state, port, population, x, y, type, coa}; return burg; @@ -119,28 +121,27 @@ export function specifyBurgs( } function defineEmblem( - stateId: number, cultureId: number, port: number, capital: Logical, type: TCultureType, cultures: TCultures, - states: TStatesReturn + stateData: TStateData ) { const coaType = capital && P(0.2) ? "Capital" : type === "Generic" ? "City" : type; const cultureShield = cultures[cultureId].shield; - const stateShield = ((states[stateId] as IState)?.coa as ICoa)?.shield; - if (stateId === 0) { + if (!stateData) { const baseCoa = COA.generate(null, 0, null, coaType); - const shield = COA.getShield(cultureShield, stateShield); + const shield = COA.getShield(cultureShield); return {...baseCoa, shield}; } - const {culture: stateCultureId, coa: stateCOA} = states[stateId] as IState; + const {culture: stateCultureId, coa: stateCOA} = stateData; const kinship = defineKinshipToStateEmblem(); const baseCoa = COA.generate(stateCOA, kinship, null, coaType); + const stateShield = (stateData.coa as ICoa)?.shield; const shield = COA.getShield(cultureShield, stateShield); return {...baseCoa, shield}; diff --git a/src/scripts/generation/pack/burgsAndStates/specifyStates.ts b/src/scripts/generation/pack/burgsAndStates/specifyStates.ts new file mode 100644 index 00000000..f7cb818f --- /dev/null +++ b/src/scripts/generation/pack/burgsAndStates/specifyStates.ts @@ -0,0 +1,37 @@ +import {TIME} from "config/logging"; +import {getColors} from "utils/colorUtils"; +import {NEUTRALS} from "./config"; +import {createAreaTiers, defineStateForm} from "./defineStateForm"; +import {defineFullStateName} from "./defineStateName"; + +import type {TStateStatistics} from "./collectStatistics"; +import type {TStateData} from "./createStateData"; + +export function specifyStates( + statesData: TStateData[], + statistics: TStateStatistics, + stateIds: Uint16Array, + burgIds: Uint16Array +): TStates { + TIME && console.time("specifyState"); + + const colors = getColors(statesData.length); + const getAreaTier = createAreaTiers(statistics); + + const states: IState[] = statesData.map((stateData, index) => { + const {i, type, name} = stateData; + const {area, ...stats} = statistics[i]; + + const areaTier = getAreaTier(area); + const {form, formName} = defineStateForm(type, areaTier); + const fullName = defineFullStateName(name, form); + + const color = colors[index]; + + const state: IState = {...stateData, form, formName, fullName, color, area, ...stats}; + return state; + }); + + TIME && console.timeEnd("specifyState"); + return [NEUTRALS, ...states]; +} diff --git a/src/scripts/generation/pack/pack.ts b/src/scripts/generation/pack/pack.ts index d110e75d..8ff96f6b 100644 --- a/src/scripts/generation/pack/pack.ts +++ b/src/scripts/generation/pack/pack.ts @@ -104,7 +104,7 @@ export function createPack(grid: IGrid): IPack { rawRivers, vertices, { - ...pick(cells, "v", "c", "p", "b", "i", "g"), + ...pick(cells, "v", "c", "p", "b", "i", "g", "area"), h: heights, f: featureIds, t: distanceField, @@ -114,6 +114,7 @@ export function createPack(grid: IGrid): IPack { fl: flux, biome, s: suitability, + pop: population, culture: cultureIds } ); @@ -148,7 +149,6 @@ export function createPack(grid: IGrid): IPack { } }); - // Religions.generate(); // BurgsAndStates.defineStateForms(); // BurgsAndStates.generateProvinces(); // BurgsAndStates.defineBurgFeatures(); diff --git a/src/types/pack/states.d.ts b/src/types/pack/states.d.ts index 1a104e4b..beeb7dd0 100644 --- a/src/types/pack/states.d.ts +++ b/src/types/pack/states.d.ts @@ -2,14 +2,22 @@ interface IState { i: number; name: string; center: number; + capital: number; color: Hex | CssUrls; type: TCultureType; culture: number; expansionism: number; + form: string; + formName: string; fullName: string; - capital: Logical; coa: ICoa | string; // pole: TPoint ? + area: number; + cells: number; + burgs: number; + rural: number; + urban: number; + neighbors: number[]; removed?: boolean; }