diff --git a/public/libs/define-globals.js b/public/libs/define-globals.js index 1535bbea..d1cf0020 100644 --- a/public/libs/define-globals.js +++ b/public/libs/define-globals.js @@ -17,7 +17,7 @@ let rulers; let biomesData; let nameBases; -// defined in main.js +// defined in main.ts let graphWidth; let graphHeight; let svgWidth; @@ -44,7 +44,7 @@ let svg, texture, terrs, biomes, - cells, + // cells, gridOverlay, coordinates, compass, diff --git a/src/config/generation.ts b/src/config/generation.ts index 751db86f..bd2d1c4b 100644 --- a/src/config/generation.ts +++ b/src/config/generation.ts @@ -12,6 +12,7 @@ export enum DISTANCE_FIELD { export enum ELEVATION { MOUNTAINS = 70, + FOOTHILLS = 60, HILLS = 50, LOWLANDS = 30 } @@ -46,7 +47,7 @@ const { WETLAND } = BIOME; -export const NOMADIC_BIOMES = [HOT_DESERT, COLD_DESERT, GRASSLAND]; +export const NOMADIC_BIOMES = [HOT_DESERT, COLD_DESERT, SAVANNA, GRASSLAND]; export const HUNTING_BIOMES = [SAVANNA, TROPICAL_RAINFOREST, TEMPERATE_RAINFOREST, TAIGA, TUNDRA, WETLAND]; diff --git a/src/modules/burgs-and-states.js b/src/modules/burgs-and-states.js index ec7f2c22..be862dec 100644 --- a/src/modules/burgs-and-states.js +++ b/src/modules/burgs-and-states.js @@ -8,7 +8,6 @@ import {Voronoi} from "/src/modules/voronoi"; import {getColors, getMixedColor, getRandomColor} from "utils/colorUtils"; import {findCell} from "utils/graphUtils"; import {getAdjective, trimVowels} from "utils/languageUtils"; -import {getMiddlePoint} from "utils/lineUtils"; import {minmax, rn} from "utils/numberUtils"; import {each, gauss, generateSeed, P, ra, rand, rw} from "utils/probabilityUtils"; import {round, splitInTwo} from "utils/stringUtils"; diff --git a/src/scripts/generation/pack/burgsAndStates.ts b/src/scripts/generation/pack/burgsAndStates.ts deleted file mode 100644 index 3e6a1b75..00000000 --- a/src/scripts/generation/pack/burgsAndStates.ts +++ /dev/null @@ -1,294 +0,0 @@ -import * as d3 from "d3"; - -import {TIME, WARN} from "config/logging"; -import {getColors} from "utils/colorUtils"; -import {getInputNumber} from "utils/nodeUtils"; -import {rn} from "utils/numberUtils"; -import {each, gauss} from "utils/probabilityUtils"; -import {getCommonEdgePoint} from "utils/lineUtils"; - -const {Names, COA} = window; - -export function generateBurgsAndStates( - cells: Pick, - vertices: IGraphVertices, - cultures: TCultures, - features: TPackFeatures, - temp: Int8Array -) { - const cellsNumber = cells.i.length; - const burgIds = new Uint16Array(cellsNumber); - - const noBurg: TNoBurg = {name: undefined}; - const neutrals: TNeutrals = {i: 0, name: "Neutrals"}; - - const scoredCellIds = getScoredCellIds(); - const statesNumber = getStatesNumber(scoredCellIds.length); - if (statesNumber === 0) return {burgIds, burgs: [noBurg], states: [neutrals]}; - - const capitals = createCapitals(); - const states = createStates(); - const towns = createTowns(); - - const roadScores = new Uint16Array(cellsNumber); // TODO: define roads - const burgs = specifyBurgs(); - - return {burgIds, states, burgs}; - - function getScoredCellIds() { - // cell score for capitals placement - const score = new Int16Array(cells.s.map(s => s * Math.random())); - - // filtered and sorted array of indexes - const sorted = cells.i.filter(i => score[i] > 0 && cells.culture[i]).sort((a, b) => score[b] - score[a]); - - return sorted; - } - - function getStatesNumber(populatedCells: number) { - const requestedStatesNumber = getInputNumber("regionsOutput"); - - if (populatedCells < requestedStatesNumber * 10) { - const maxAllowed = Math.floor(populatedCells / 10); - if (maxAllowed === 0) { - WARN && console.warn("There is no populated cells. Cannot generate states"); - return 0; - } - - WARN && console.warn(`Not enough populated cells (${populatedCells}). Will generate only ${maxAllowed} states`); - return maxAllowed; - } - - return requestedStatesNumber; - } - - function createCapitals() { - TIME && console.time("createCapitals"); - - const capitals = placeCapitals().map((cellId, index) => { - const id = index + 1; - const cultureId = cells.culture[cellId]; - const name: string = Names.getCultureShort(cultureId); - const featureId = cells.f[cellId]; - - return {i: id, cell: cellId, culture: cultureId, name, feature: featureId, capital: 1 as Logical}; - }); - - for (const {cell, i} of capitals) { - burgIds[cell] = i; - } - - TIME && console.timeEnd("createCapitals"); - return capitals; - - function placeCapitals() { - function attemptToPlaceCapitals(spacing: number): number[] { - const capitalCells: number[] = []; - const capitalsQuadtree = d3.quadtree(); - - for (const cellId of scoredCellIds) { - const [x, y] = cells.p[cellId]; - - if (capitalsQuadtree.find(x, y, spacing) === undefined) { - capitalCells.push(cellId); - capitalsQuadtree.add([x, y]); - - if (capitalCells.length === statesNumber) return capitalCells; - } - } - - WARN && console.warn("Cannot place capitals, trying again with reduced spacing"); - return attemptToPlaceCapitals(spacing / 1.2); - } - - // initial min distance between capitals, reduced by 1.2 each iteration if not enough space - const initialSpacing = (graphWidth + graphHeight) / 2 / statesNumber; - return attemptToPlaceCapitals(initialSpacing); - } - } - - function createStates() { - TIME && console.time("createStates"); - - const colors = getColors(capitals.length); - const each5th = each(5); // select each 5th element - const powerInput = getInputNumber("powerInput"); - - const states = capitals.map((capital, index) => { - const {cell: cellId, culture: cultureId, name: capitalName, i: capitalId} = capital; - const id = index + 1; - - const useCapitalName = capitalName.length < 9 && each5th(cellId); - const basename = useCapitalName ? capitalName : Names.getCultureShort(cultureId); - const name: string = Names.getState(basename, cultureId); - const color = colors[index]; - - const type = (cultures[cultureId] as ICulture).type; - const expansionism = rn(Math.random() * powerInput + 1, 1); - - const shield = COA.getShield(cultureId, null); - const coa = {...COA.generate(null, null, null, type), shield}; - - return {i: id, center: cellId, type, name, color, expansionism, capital: capitalId, culture: cultureId, coa}; - }); - - TIME && console.timeEnd("createStates"); - return [neutrals, ...states]; - } - - function createTowns() { - TIME && console.time("createTowns"); - - const townsNumber = getTownsNumber(); - if (townsNumber === 0) return []; - - // randomize cells score a bit for more natural towns placement - const randomizeScore = (suitability: number) => suitability * gauss(1, 3, 0, 20, 3); - const scores = new Int16Array(cells.s.map(randomizeScore)); - - // take populated cells without capitals - const scoredCellsIds = cells.i.filter(i => scores[i] > 0 && cells.culture[i] && !burgIds[i]); - scoredCellsIds.sort((a, b) => scores[b] - scores[a]); // sort by randomized suitability score - - const towns = placeTowns().map((cellId, index) => { - const id = index + 1; - const cultureId = cells.culture[cellId]; - const name: string = Names.getCulture(cultureId); - const featureId = cells.f[cellId]; - - return {i: id, cell: cellId, culture: cultureId, name, feature: featureId, capital: 0 as Logical}; - }); - - for (const {cell, i} of towns) { - burgIds[cell] = i; - } - - TIME && console.timeEnd("createTowns"); - return towns; - - function getTownsNumber() { - const inputTownsNumber = getInputNumber("manorsInput"); - const shouldAutoDefine = inputTownsNumber === 1000; - const desiredTownsNumber = shouldAutoDefine ? rn(scoredCellsIds.length / 5 ** 0.8) : inputTownsNumber; - - return Math.min(desiredTownsNumber, scoredCellsIds.length); - } - - function placeTowns() { - function attemptToPlaceTowns(spacing: number): number[] { - const townCells: number[] = []; - const townsQuadtree = d3.quadtree(); - - const randomizeScaping = (spacing: number) => spacing * gauss(1, 0.3, 0.2, 2, 2); - - for (const cellId of scoredCellsIds) { - const [x, y] = cells.p[cellId]; - - // randomize min spacing a bit to make placement not that uniform - const currentSpacing = randomizeScaping(spacing); - - if (townsQuadtree.find(x, y, currentSpacing) === undefined) { - townCells.push(cellId); - townsQuadtree.add([x, y]); - - if (townCells.length === townsNumber) return townCells; - } - } - - WARN && console.warn("Cannot place towns, trying again with reduced spacing"); - return attemptToPlaceTowns(spacing / 2); - } - - // initial min distance between towns, reduced by 2 each iteration if not enough space - const initialSpacing = (graphWidth + graphHeight) / 150 / (townsNumber ** 0.7 / 66); - return attemptToPlaceTowns(initialSpacing); - } - } - - function specifyBurgs(): TBurgs { - TIME && console.time("specifyBurgs"); - - const burgs = [...capitals, ...towns].map(burgData => { - const {cell, capital} = burgData; - - const port = definePort(cell, capital); - const population = definePopulation(cell, capital, port); - const [x, y] = defineLocation(cell, port); - - const type = defineType(cell, port); - const coa = defineEmblem(); - - return {...burgData, port, population, x, y, type, coa}; - }); - - TIME && console.timeEnd("specifyBurgs"); - return [noBurg, ...burgs]; - - function definePort(cellId: number, capital: Logical) { - if (!cells.haven[cellId]) return 0; // must be a coastal cell - if (temp[cells.g[cellId]] <= 0) return 0; // temperature must be above zero °C - - const havenCellId = cells.haven[cellId]; - const havenFeatureId = cells.f[havenCellId]; - const feature = features[havenFeatureId] as IPackFeatureOcean | IPackFeatureLake; - if (feature.cells < 2) return 0; // water body must have at least 2 cells - - const isSafeHarbor = cells.harbor[cellId] === 1; - if (!capital && !isSafeHarbor) return 0; // must be a capital or safe harbor - - return havenFeatureId; - } - - function definePopulation(cellId: number, capital: Logical, port: number) { - const basePopulation = (cells.s[cellId] + roadScores[cellId] / 2) / 4; - const decimalPart = (cellId % 1000) / 1000; - - const capitalMultiplier = capital ? 1.3 : 1; - const portMultiplier = port ? 1.3 : 1; - const randomMultiplier = gauss(1, 1.5, 0.3, 10, 3); - - const total = (basePopulation + decimalPart) * capitalMultiplier * portMultiplier * randomMultiplier; - return rn(Math.max(0.1, total), 3); - } - - function defineLocation(cellId: number, port: number) { - const [cellX, cellY] = cells.p[cellId]; - - if (port) { - // place ports on the coast - const [x, y] = getCommonEdgePoint(cells.v, vertices, cellId, cells.haven[cellId]); - return [rn(x, 2), rn(y, 2)]; - } - - if (cells.r[cellId]) { - // place river burgs a bit off of the cell center - const offset = Math.min(cells.fl[cellId] / 150, 1); - const x = cellId % 2 ? cellX + offset : cellX - offset; - const y = cells.r[cellId] % 2 ? cellY + offset : cellY - offset; - return [rn(x, 2), rn(y, 2)]; - } - - return [cellX, cellY]; - } - - function defineType(cellId: number, port: number) { - const cells = pack.cells; - if (port) return "Naval"; - if (cells.haven[cellId] && pack.features[cells.f[cells.haven[cellId]]].type === "lake") return "Lake"; - if (cells.h[cellId] > 60) return "Highland"; - if (cells.r[cellId] && cells.r[cellId].length > 100 && cells.r[cellId].length >= pack.rivers[0].length) - return "River"; - - if (!cells.burg[cellId] || pack.burgs[cells.burg[cellId]].population < 6) { - if (population < 5 && [1, 2, 3, 4].includes(cells.biome[cellId])) return "Nomadic"; - if (cells.biome[cellId] > 4 && cells.biome[cellId] < 10) return "Hunting"; - } - - return "Generic"; - } - - function defineEmblem() { - return "emblem"; - } - } -} diff --git a/src/scripts/generation/pack/burgsAndStates/config.ts b/src/scripts/generation/pack/burgsAndStates/config.ts new file mode 100644 index 00000000..2e16c1da --- /dev/null +++ b/src/scripts/generation/pack/burgsAndStates/config.ts @@ -0,0 +1,2 @@ +export const NO_BURG: TNoBurg = {name: undefined}; +export const NEUTRALS: TNeutrals = {i: 0, name: "Neutrals"}; diff --git a/src/scripts/generation/pack/burgsAndStates/createCapitals.ts b/src/scripts/generation/pack/burgsAndStates/createCapitals.ts new file mode 100644 index 00000000..9688a478 --- /dev/null +++ b/src/scripts/generation/pack/burgsAndStates/createCapitals.ts @@ -0,0 +1,50 @@ +import * as d3 from "d3"; + +import {TIME, WARN} from "config/logging"; + +const {Names} = window; + +export function createCapitals(statesNumber: number, scoredCellIds: UintArray, burgIds: Uint16Array) { + TIME && console.time("createCapitals"); + + const capitals = placeCapitals(statesNumber, scoredCellIds).map((cellId, index) => { + const id = index + 1; + const cultureId = cells.culture[cellId]; + const name: string = Names.getCultureShort(cultureId); + const featureId = cells.f[cellId]; + + return {i: id, cell: cellId, culture: cultureId, name, feature: featureId, capital: 1 as Logical}; + }); + + for (const {cell, i} of capitals) { + burgIds[cell] = i; + } + + TIME && console.timeEnd("createCapitals"); + return capitals; +} + +function placeCapitals(statesNumber: number, scoredCellIds: UintArray) { + function attemptToPlaceCapitals(spacing: number): number[] { + const capitalCells: number[] = []; + const capitalsQuadtree = d3.quadtree(); + + for (const cellId of scoredCellIds) { + const [x, y] = cells.p[cellId]; + + if (capitalsQuadtree.find(x, y, spacing) === undefined) { + capitalCells.push(cellId); + capitalsQuadtree.add([x, y]); + + if (capitalCells.length === statesNumber) return capitalCells; + } + } + + WARN && console.warn("Cannot place capitals, trying again with reduced spacing"); + return attemptToPlaceCapitals(spacing / 1.2); + } + + // initial min distance between capitals, reduced by 1.2 each iteration if not enough space + const initialSpacing = (graphWidth + graphHeight) / 2 / statesNumber; + return attemptToPlaceCapitals(initialSpacing); +} diff --git a/src/scripts/generation/pack/burgsAndStates/createStates.ts b/src/scripts/generation/pack/burgsAndStates/createStates.ts new file mode 100644 index 00000000..b96c3dcf --- /dev/null +++ b/src/scripts/generation/pack/burgsAndStates/createStates.ts @@ -0,0 +1,40 @@ +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) { + TIME && console.time("createStates"); + + const colors = getColors(capitals.length); + const each5th = each(5); // select each 5th element + const powerInput = getInputNumber("powerInput"); + + const states = capitals.map((capital, index) => { + const {cell: cellId, culture: cultureId, name: capitalName, i: capitalId} = capital; + const id = index + 1; + + const useCapitalName = capitalName.length < 9 && each5th(cellId); + const basename = useCapitalName ? capitalName : Names.getCultureShort(cultureId); + const name: string = Names.getState(basename, cultureId); + const color = colors[index]; + + const type = (cultures[cultureId] as ICulture).type; + const expansionism = rn(Math.random() * powerInput + 1, 1); + + const shield = COA.getShield(cultureId, null); + const coa = {...COA.generate(null, null, null, type), shield}; + + return {i: id, center: cellId, type, name, color, expansionism, capital: capitalId, culture: cultureId, coa}; + }); + + TIME && console.timeEnd("createStates"); + return [NEUTRALS, ...states]; +} diff --git a/src/scripts/generation/pack/burgsAndStates/createTowns.ts b/src/scripts/generation/pack/burgsAndStates/createTowns.ts new file mode 100644 index 00000000..0e33a34e --- /dev/null +++ b/src/scripts/generation/pack/burgsAndStates/createTowns.ts @@ -0,0 +1,77 @@ +import * as d3 from "d3"; + +import {TIME, WARN} from "config/logging"; +import {getInputNumber} from "utils/nodeUtils"; +import {rn} from "utils/numberUtils"; +import {gauss} from "utils/probabilityUtils"; + +const {Names} = window; + +export function createTowns(scoredCellIds: UintArray, burgIds: Uint16Array) { + TIME && console.time("createTowns"); + + const townsNumber = getTownsNumber(); + if (townsNumber === 0) return []; + + // randomize cells score a bit for more natural towns placement + const randomizeScore = (suitability: number) => suitability * gauss(1, 3, 0, 20, 3); + const scores = new Int16Array(cells.s.map(randomizeScore)); + + // take populated cells without capitals + const scoredCellsIds = cells.i.filter(i => scores[i] > 0 && cells.culture[i] && !burgIds[i]); + scoredCellsIds.sort((a, b) => scores[b] - scores[a]); // sort by randomized suitability score + + const towns = placeTowns(townsNumber, scoredCellIds).map((cellId, index) => { + const id = index + 1; + const cultureId = cells.culture[cellId]; + const name: string = Names.getCulture(cultureId); + const featureId = cells.f[cellId]; + + return {i: id, cell: cellId, culture: cultureId, name, feature: featureId, capital: 0 as Logical}; + }); + + for (const {cell, i} of towns) { + burgIds[cell] = i; + } + + TIME && console.timeEnd("createTowns"); + return towns; + + function getTownsNumber() { + const inputTownsNumber = getInputNumber("manorsInput"); + const shouldAutoDefine = inputTownsNumber === 1000; + const desiredTownsNumber = shouldAutoDefine ? rn(scoredCellsIds.length / 5 ** 0.8) : inputTownsNumber; + + return Math.min(desiredTownsNumber, scoredCellsIds.length); + } +} + +function placeTowns(townsNumber: number, scoredCellIds: UintArray) { + function attemptToPlaceTowns(spacing: number): number[] { + const townCells: number[] = []; + const townsQuadtree = d3.quadtree(); + + const randomizeScaping = (spacing: number) => spacing * gauss(1, 0.3, 0.2, 2, 2); + + for (const cellId of scoredCellIds) { + const [x, y] = cells.p[cellId]; + + // randomize min spacing a bit to make placement not that uniform + const currentSpacing = randomizeScaping(spacing); + + if (townsQuadtree.find(x, y, currentSpacing) === undefined) { + townCells.push(cellId); + townsQuadtree.add([x, y]); + + if (townCells.length === townsNumber) return townCells; + } + } + + WARN && console.warn("Cannot place towns, trying again with reduced spacing"); + return attemptToPlaceTowns(spacing / 2); + } + + // initial min distance between towns, reduced by 2 each iteration if not enough space + const initialSpacing = (graphWidth + graphHeight) / 150 / (townsNumber ** 0.7 / 66); + return attemptToPlaceTowns(initialSpacing); +} diff --git a/src/scripts/generation/pack/burgsAndStates/expandStates.ts b/src/scripts/generation/pack/burgsAndStates/expandStates.ts new file mode 100644 index 00000000..abdb4444 --- /dev/null +++ b/src/scripts/generation/pack/burgsAndStates/expandStates.ts @@ -0,0 +1,90 @@ +import {TIME} from "config/logging"; +import FlatQueue from "flatqueue"; +import {minmax} from "utils/numberUtils"; + +// growth algorithm to assign cells to states +export function expandStates() { + TIME && console.time("expandStates"); + const {cells, states, cultures, burgs} = pack; + + cells.state = new Uint16Array(cells.i.length); + const queue = new FlatQueue(); + const cost = []; + const neutral = (cells.i.length / 5000) * 2500 * neutralInput.value * statesNeutral; // limit cost for state growth + + states + .filter(s => s.i && !s.removed) + .forEach(state => { + const capitalCell = burgs[state.capital].cell; + cells.state[capitalCell] = state.i; + const cultureCenter = cultures[state.culture].center; + const biome = cells.biome[cultureCenter]; // state native biome + queue.push({cellId: state.center, stateId: state.i, b: biome}, 0); + cost[state.center] = 1; + }); + + while (queue.length) { + const priority = queue.peekValue(); + const {cellId, stateId, biome} = queue.pop(); + const {type, culture} = states[stateId]; + + cells.c[cellId].forEach(neibCellId => { + if (cells.state[neibCellId] && neibCellId === states[cells.state[neibCellId]].center) return; // do not overwrite capital cells + + const cultureCost = culture === cells.culture[neibCellId] ? -9 : 100; + const populationCost = + cells.h[neibCellId] < 20 ? 0 : cells.s[neibCellId] ? Math.max(20 - cells.s[neibCellId], 0) : 5000; + const biomeCost = getBiomeCost(biome, cells.biome[neibCellId], type); + const heightCost = getHeightCost(pack.features[cells.f[neibCellId]], cells.h[neibCellId], type); + const riverCost = getRiverCost(cells.r[neibCellId], neibCellId, type); + const typeCost = getTypeCost(cells.t[neibCellId], type); + const cellCost = Math.max(cultureCost + populationCost + biomeCost + heightCost + riverCost + typeCost, 0); + const totalCost = priority + 10 + cellCost / states[stateId].expansionism; + + if (totalCost > neutral) return; + + if (!cost[neibCellId] || totalCost < cost[neibCellId]) { + if (cells.h[neibCellId] >= 20) cells.state[neibCellId] = stateId; // assign state to cell + cost[neibCellId] = totalCost; + + queue.push({cellId: neibCellId, stateId, biome}, totalCost); + } + }); + } + + burgs.filter(b => b.i && !b.removed).forEach(b => (b.state = cells.state[b.cell])); // assign state to burgs + + function getBiomeCost(b, biome, type) { + if (b === biome) return 10; // tiny penalty for native biome + if (type === "Hunting") return biomesData.cost[biome] * 2; // non-native biome penalty for hunters + if (type === "Nomadic" && biome > 4 && biome < 10) return biomesData.cost[biome] * 3; // forest biome penalty for nomads + return biomesData.cost[biome]; // general non-native biome penalty + } + + function getHeightCost(f, h, type) { + if (type === "Lake" && f.type === "lake") return 10; // low lake crossing penalty for Lake cultures + if (type === "Naval" && h < 20) return 300; // low sea crossing penalty for Navals + if (type === "Nomadic" && h < 20) return 10000; // giant sea crossing penalty for Nomads + if (h < 20) return 1000; // general sea crossing penalty + if (type === "Highland" && h < 62) return 1100; // penalty for highlanders on lowlands + if (type === "Highland") return 0; // no penalty for highlanders on highlands + if (h >= 67) return 2200; // general mountains crossing penalty + if (h >= 44) return 300; // general hills crossing penalty + return 0; + } + + function getRiverCost(r, i, type) { + if (type === "River") return r ? 0 : 100; // penalty for river cultures + if (!r) return 0; // no penalty for others if there is no river + return minmax(cells.fl[i] / 10, 20, 100); // river penalty from 20 to 100 based on flux + } + + function getTypeCost(t, type) { + if (t === 1) return type === "Naval" || type === "Lake" ? 0 : type === "Nomadic" ? 60 : 20; // penalty for coastline + if (t === 2) return type === "Naval" || type === "Nomadic" ? 30 : 0; // low penalty for land level 2 for Navals and nomads + if (t !== -1) return type === "Naval" || type === "Lake" ? 100 : 0; // penalty for mainland for navals + return 0; + } + + TIME && console.timeEnd("expandStates"); +} diff --git a/src/scripts/generation/pack/burgsAndStates/generateBurgsAndStates.ts b/src/scripts/generation/pack/burgsAndStates/generateBurgsAndStates.ts new file mode 100644 index 00000000..2976cd73 --- /dev/null +++ b/src/scripts/generation/pack/burgsAndStates/generateBurgsAndStates.ts @@ -0,0 +1,66 @@ +import {WARN} from "config/logging"; +import {getInputNumber} from "utils/nodeUtils"; +import {NEUTRALS, NO_BURG} from "./config"; +import {createCapitals} from "./createCapitals"; +import {createStates} from "./createStates"; +import {createTowns} from "./createTowns"; +import {expandStates} from "./expandStates"; +import {specifyBurgs} from "./specifyBurgs"; + +export function generateBurgsAndStates( + cells: Pick< + IPack["cells"], + "v" | "p" | "i" | "g" | "h" | "f" | "haven" | "harbor" | "r" | "fl" | "biome" | "s" | "culture" + >, + vertices: IGraphVertices, + cultures: TCultures, + features: TPackFeatures, + temp: Int8Array, + rivers: Omit[] +) { + const cellsNumber = cells.i.length; + const burgIds = new Uint16Array(cellsNumber); + + const scoredCellIds = getScoredCellIds(); + const statesNumber = getStatesNumber(scoredCellIds.length); + if (statesNumber === 0) return {burgIds, burgs: [NO_BURG], states: [NEUTRALS]}; + + const capitals = createCapitals(statesNumber, scoredCellIds, burgIds); + const states = createStates(capitals, cultures); + const towns = createTowns(scoredCellIds, burgIds); + + expandStates(); + // normalizeStates(); + const roadScores = new Uint16Array(cellsNumber); // TODO: define roads + + const burgs = specifyBurgs(capitals, towns, roadScores); + + return {burgIds, states, burgs}; + + function getScoredCellIds() { + // cell score for capitals placement + const score = new Int16Array(cells.s.map(s => s * Math.random())); + + // filtered and sorted array of indexes + const sorted = cells.i.filter(i => score[i] > 0 && cells.culture[i]).sort((a, b) => score[b] - score[a]); + + return sorted; + } + + function getStatesNumber(populatedCells: number) { + const requestedStatesNumber = getInputNumber("regionsOutput"); + + if (populatedCells < requestedStatesNumber * 10) { + const maxAllowed = Math.floor(populatedCells / 10); + if (maxAllowed === 0) { + WARN && console.warn("There is no populated cells. Cannot generate states"); + return 0; + } + + WARN && console.warn(`Not enough populated cells (${populatedCells}). Will generate only ${maxAllowed} states`); + return maxAllowed; + } + + return requestedStatesNumber; + } +} diff --git a/src/scripts/generation/pack/burgsAndStates/specifyBurgs.ts b/src/scripts/generation/pack/burgsAndStates/specifyBurgs.ts new file mode 100644 index 00000000..5882f331 --- /dev/null +++ b/src/scripts/generation/pack/burgsAndStates/specifyBurgs.ts @@ -0,0 +1,126 @@ +import {ELEVATION, NOMADIC_BIOMES, HUNTING_BIOMES} from "config/generation"; +import {TIME} from "config/logging"; +import {getCommonEdgePoint} from "utils/lineUtils"; +import {rn} from "utils/numberUtils"; +import {gauss, P} from "utils/probabilityUtils"; +import {NO_BURG} from "./config"; +import type {createCapitals} from "./createCapitals"; +import type {createTowns} from "./createTowns"; + +const {COA} = window; + +type TCapitals = ReturnType; +type TTowns = ReturnType; + +export function specifyBurgs(capitals: TCapitals, towns: TTowns, roadScores: Uint16Array): TBurgs { + TIME && console.time("specifyBurgs"); + + const burgs = [...capitals, ...towns].map(burgData => { + const {cell, culture, capital, state} = burgData; + + const port = definePort(cell, capital); + const population = definePopulation(cell, capital, port); + const [x, y] = defineLocation(cell, port); + + const type = defineType(cell, port, population); + const coa = defineEmblem(state, culture, port, capital, type); + + return {...burgData, port, population, x, y, type, coa}; + }); + + TIME && console.timeEnd("specifyBurgs"); + return [NO_BURG, ...burgs]; + + function definePort(cellId: number, capital: Logical) { + if (!cells.haven[cellId]) return 0; // must be a coastal cell + if (temp[cells.g[cellId]] <= 0) return 0; // temperature must be above zero °C + + const havenCellId = cells.haven[cellId]; + const havenFeatureId = cells.f[havenCellId]; + const feature = features[havenFeatureId] as IPackFeatureOcean | IPackFeatureLake; + if (feature.cells < 2) return 0; // water body must have at least 2 cells + + const isSafeHarbor = cells.harbor[cellId] === 1; + if (!capital && !isSafeHarbor) return 0; // must be a capital or safe harbor + + return havenFeatureId; + } + + // get population in points, where 1 point = 1000 people by default + function definePopulation(cellId: number, capital: Logical, port: number) { + const basePopulation = (cells.s[cellId] + roadScores[cellId] / 2) / 4; + const decimalPart = (cellId % 1000) / 1000; + + const capitalMultiplier = capital ? 1.3 : 1; + const portMultiplier = port ? 1.3 : 1; + const randomMultiplier = gauss(1, 1.5, 0.3, 10, 3); + + const total = (basePopulation + decimalPart) * capitalMultiplier * portMultiplier * randomMultiplier; + return rn(Math.max(0.1, total), 3); + } + + function defineLocation(cellId: number, port: number) { + const [cellX, cellY] = cells.p[cellId]; + + if (port) { + // place ports on the coast + const [x, y] = getCommonEdgePoint(cells.v, vertices, cellId, cells.haven[cellId]); + return [rn(x, 2), rn(y, 2)]; + } + + if (cells.r[cellId]) { + // place river burgs a bit off of the cell center + const offset = Math.min(cells.fl[cellId] / 150, 1); + const x = cellId % 2 ? cellX + offset : cellX - offset; + const y = cells.r[cellId] % 2 ? cellY + offset : cellY - offset; + return [rn(x, 2), rn(y, 2)]; + } + + return [cellX, cellY]; + } + + function defineType(cellId: number, port: number, population: number): TCultureType { + if (port) return "Naval"; + + const haven = cells.haven[cellId]; + const waterBody = features[cells.f[haven]]; + if (haven && (waterBody as TPackFeature).type === "lake") return "Lake"; + + if (cells.h[cellId] > ELEVATION.FOOTHILLS) return "Highland"; + if (cells.r[cellId] && rivers[cellId].length > 100) return "River"; + + if (population < 6) { + const biome = cells.biome[cellId]; + if (population < 5 && NOMADIC_BIOMES.includes(biome)) return "Nomadic"; + if (HUNTING_BIOMES.includes(biome)) return "Hunting"; + } + + return "Generic"; + } + + function defineEmblem(stateId: number, cultureId: number, port: number, capital: Logical, type: TCultureType) { + const coaType = capital && P(0.2) ? "Capital" : type === "Generic" ? "City" : type; + + if (stateId === 0) { + const baseCoa = COA.generate(null, 0, null, coaType); + const shield = COA.getShield(cultureId, stateId); + return {...baseCoa, shield}; + } + + const {culture: stateCultureId, coa: stateCOA} = states[stateId] as IState; + const kinship = defineKinshipToStateEmblem(); + + const baseCoa = COA.generate(stateCOA, kinship, null, coaType); + const shield = COA.getShield(cultureId, stateId); + return {...baseCoa, shield}; + + function defineKinshipToStateEmblem() { + const baseKinship = 0.25; + const capitalModifier = capital ? 0.1 : 0; + const portModifier = port ? -0.1 : 0; + const cultureModifier = cultureId === stateCultureId ? 0 : -0.25; + + return baseKinship + capitalModifier + portModifier + cultureModifier; + } + } +} diff --git a/src/scripts/generation/pack/pack.ts b/src/scripts/generation/pack/pack.ts index 50f644ac..c19f8cbd 100644 --- a/src/scripts/generation/pack/pack.ts +++ b/src/scripts/generation/pack/pack.ts @@ -11,7 +11,7 @@ import {pick} from "utils/functionUtils"; import {rn} from "utils/numberUtils"; import {generateCultures, expandCultures} from "./cultures"; import {generateRivers} from "./rivers"; -import {generateBurgsAndStates} from "./burgsAndStates"; +import {generateBurgsAndStates} from "./burgsAndStates/generateBurgsAndStates"; const {LAND_COAST, WATER_COAST, DEEPER_WATER} = DISTANCE_FIELD; const {Biomes} = window; @@ -98,18 +98,21 @@ export function createPack(grid: IGrid): IPack { const {burgIds, states, burgs} = generateBurgsAndStates( { ...pick(cells, "v", "p", "i", "g"), + h: heights, f: featureIds, haven, harbor, r: riverIds, fl: flux, + biome, s: suitability, culture: cultureIds }, vertices, cultures, mergedFeatures, - temp + temp, + rawRivers ); // Religions.generate(); diff --git a/src/types/pack/burgs.d.ts b/src/types/pack/burgs.d.ts index f031d457..380cb2f8 100644 --- a/src/types/pack/burgs.d.ts +++ b/src/types/pack/burgs.d.ts @@ -5,6 +5,7 @@ interface IBurg { x: number; y: number; population: number; + type: TCultureType; capital: Logical; // 1 - capital, 0 - burg port: number; // port feature id, 0 - not a port shanty?: number; diff --git a/src/types/pack/features.d.ts b/src/types/pack/features.d.ts index b1642cc9..2ae8f95a 100644 --- a/src/types/pack/features.d.ts +++ b/src/types/pack/features.d.ts @@ -6,7 +6,6 @@ interface IPackFeatureBase { vertices: number[]; // indexes of perimetric vertices area: number; // area of the feature perimetric polygon } -3; interface IPackFeatureOcean extends IPackFeatureBase { land: false; diff --git a/src/types/pack/states.d.ts b/src/types/pack/states.d.ts index 73f139ae..d972a7ce 100644 --- a/src/types/pack/states.d.ts +++ b/src/types/pack/states.d.ts @@ -1,8 +1,10 @@ interface IState { i: number; name: string; + culture: number; fullName: string; removed?: boolean; + coa: ICoa | string; } type TNeutrals = { @@ -11,3 +13,12 @@ type TNeutrals = { }; type TStates = [TNeutrals, ...IState[]]; + +interface ICoa { + t1: string; + division: {}; + ordinaries: {}[]; + charges: {}[]; + shield: "heater"; + t1: "purpure"; +}