diff --git a/src/scripts/generation/pack/burgsAndStates/expandStates.ts b/src/scripts/generation/pack/burgsAndStates/expandStates.ts index abdb4444..28b68147 100644 --- a/src/scripts/generation/pack/burgsAndStates/expandStates.ts +++ b/src/scripts/generation/pack/burgsAndStates/expandStates.ts @@ -1,90 +1,195 @@ -import {TIME} from "config/logging"; import FlatQueue from "flatqueue"; + +import {TIME} from "config/logging"; +import {getInputNumber} from "utils/nodeUtils"; import {minmax} from "utils/numberUtils"; +import type {createCapitals} from "./createCapitals"; +import type {createStates} from "./createStates"; +import {ELEVATION, FOREST_BIOMES, MIN_LAND_HEIGHT, DISTANCE_FIELD} from "config/generation"; + +type TCapitals = ReturnType; +type TStates = ReturnType; // growth algorithm to assign cells to states -export function expandStates() { +export function expandStates( + capitals: TCapitals, + states: TStates, + features: TPackFeatures, + cells: Pick +) { 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 + const cellsNumber = cells.s.length; + const stateIds = new Uint16Array(cellsNumber); - 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; - }); + const queue = new FlatQueue<{cellId: number; stateId: number}>(); + const cost: number[] = []; + + const neutralInput = getInputNumber("neutralInput"); + const maxExpansionCost = (cellsNumber / 2) * neutralInput * statesNeutral; + + for (const {i: stateId, cell: cellId} of capitals) { + stateIds[cellId] = stateId; + cost[cellId] = 1; + queue.push({cellId, stateId}, 0); + } + + // expansion costs (less is better) + const SAME_CULTURE_BONUS = -9; + const DIFFERENT_CULTURES_FEE = 100; + + const MAX_SUITABILITY_COST = 20; + const UNINHABITED_LAND_FEE = 5000; + + const NATIVE_BIOME_FIXED_COST = 10; + const HUNTERS_NON_NATIVE_BIOME_FEE_MULTIPLIER = 2; + const NOMADS_FOREST_BIOMES_FEE_MULTIPLIER = 3; + const GENERIC_NON_NATIVE_BIOME_FEE_MULTIPLIER = 1; + + const GENERIC_DEEP_WATER_FEE_MULTIPLIER = 2; + const GENERIC_WATER_CROSSING_FEE = 1000; + const NOMADS_WATER_CROSSING_FEE = 10000; + const NAVAL_WATER_CROSSING_FEE = 300; + const LAKE_STATES_LAKE_CROSSING_FEE = 10; + const GENERIC_MOUNTAINS_CROSSING_FEE = 2200; + const GENERIC_HILLS_CROSSING_FEE = 300; + const HIGHLAND_STATE_LOWLANDS_FEE = 1100; + const HIGHLAND_STATE_HIGHTLAND_COST = 0; + + const RIVER_STATE_RIVER_CROSSING_COST = 0; + const RIVER_STATE_NO_RIVER_COST = 100; + const RIVER_CROSSING_MIN_COST = 20; + const RIVER_CROSSING_MAX_COST = 100; + + const GENERIC_LAND_COAST_FEE = 20; + const MARITIME_LAND_COAST_FEE = 0; + const NOMADS_LAND_COAST_FEE = 60; + const GENERIC_LANDLOCKED_FEE = 0; + const NAVAL_LANDLOCKED_FEE = 30; while (queue.length) { - const priority = queue.peekValue(); - const {cellId, stateId, biome} = queue.pop(); - const {type, culture} = states[stateId]; + const priority = queue.peekValue()!; + const {cellId, stateId} = queue.pop()!; + + const {type, culture, center, expansionism} = getState(stateId); + const capitalBiome = cells.biome[center]; cells.c[cellId].forEach(neibCellId => { - if (cells.state[neibCellId] && neibCellId === states[cells.state[neibCellId]].center) return; // do not overwrite capital cells + if (neibCellId === center && stateIds[neibCellId]) return; // do not overwrite capital cells + + const cultureCost = getCultureCost(culture, neibCellId); + const populationCost = getPopulationCost(neibCellId); + const biomeCost = getBiomeCost(neibCellId, capitalBiome, type); + const heightCost = getHeightCost(neibCellId, type); + const riverCost = getRiverCost(neibCellId, type); + const typeCost = getTypeCost(neibCellId, type); - 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; + const totalCost = priority + 10 + cellCost / expansionism; + if (totalCost > maxExpansionCost) return; if (!cost[neibCellId] || totalCost < cost[neibCellId]) { - if (cells.h[neibCellId] >= 20) cells.state[neibCellId] = stateId; // assign state to cell + if (cells.h[neibCellId] >= MIN_LAND_HEIGHT) stateIds[neibCellId] = stateId; // assign state to cell cost[neibCellId] = totalCost; - queue.push({cellId: neibCellId, stateId, biome}, totalCost); + queue.push({cellId: neibCellId, stateId}, 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"); + return stateIds; + + function isNeutrals(state: Entry): state is TNeutrals { + return state.i === 0; + } + + function getState(stateId: number) { + const state = states[stateId]; + if (isNeutrals(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; + } + + function getPopulationCost(cellId: number) { + const isWater = cells.h[cellId] < MIN_LAND_HEIGHT; + if (isWater) return 0; + + const suitability = cells.s[cellId]; + if (suitability) return Math.max(MAX_SUITABILITY_COST - suitability, 0); + + return UNINHABITED_LAND_FEE; + } + + function getBiomeCost(cellId: number, capitalBiome: number, type: TCultureType) { + const biome = cells.biome[cellId]; + if (biome === capitalBiome) return NATIVE_BIOME_FIXED_COST; + + const defaultCost = biomesData.cost[biome]; + if (type === "Hunting") return defaultCost * HUNTERS_NON_NATIVE_BIOME_FEE_MULTIPLIER; + if (type === "Nomadic" && FOREST_BIOMES.includes(biome)) return defaultCost * NOMADS_FOREST_BIOMES_FEE_MULTIPLIER; + return defaultCost * GENERIC_NON_NATIVE_BIOME_FEE_MULTIPLIER; + } + + function getHeightCost(cellId: number, type: TCultureType) { + const height = cells.h[cellId]; + const isWater = height < MIN_LAND_HEIGHT; + + if (isWater) { + const feature = features[cells.f[cellId]]; + if (feature === 0) throw new Error(`No feature for cell ${cellId}`); + const isDeepWater = cells.t[cellId] > DISTANCE_FIELD.WATER_COAST; + const multiplier = isDeepWater ? GENERIC_DEEP_WATER_FEE_MULTIPLIER : 1; + + if (type === "Lake" && feature.type === "lake") return LAKE_STATES_LAKE_CROSSING_FEE * multiplier; + if (type === "Naval") return NAVAL_WATER_CROSSING_FEE * multiplier; + if (type === "Nomadic") return NOMADS_WATER_CROSSING_FEE * multiplier; + return GENERIC_WATER_CROSSING_FEE * multiplier; + } + + const isLowlands = height <= ELEVATION.FOOTHILLS; + const isHills = height >= ELEVATION.HILLS; + const isMountains = height >= ELEVATION.MOUNTAINS; + + if (type === "Highland") { + if (isLowlands) return HIGHLAND_STATE_LOWLANDS_FEE; + return HIGHLAND_STATE_HIGHTLAND_COST; + } + + if (isMountains) return GENERIC_MOUNTAINS_CROSSING_FEE; + if (isHills) return GENERIC_HILLS_CROSSING_FEE; + return 0; + } + + function getRiverCost(cellId: number, type: TCultureType) { + const isRiver = cells.r[cellId] !== 0; + if (type === "River") return isRiver ? RIVER_STATE_RIVER_CROSSING_COST : RIVER_STATE_NO_RIVER_COST; + if (!isRiver) return 0; + + const flux = cells.fl[cellId]; + return minmax(flux / 10, RIVER_CROSSING_MIN_COST, RIVER_CROSSING_MAX_COST); + } + + function getTypeCost(cellId: number, type: TCultureType) { + const isMaritime = type === "Naval" || type === "Lake"; + const t = cells.t[cellId]; + + const isLandCoast = t === DISTANCE_FIELD.LAND_COAST; + if (isLandCoast) { + if (isMaritime) return MARITIME_LAND_COAST_FEE; + if (type === "Nomadic") return NOMADS_LAND_COAST_FEE; + return GENERIC_LAND_COAST_FEE; + } + + const isLandlocked = t === DISTANCE_FIELD.LANDLOCKED; + if (isLandlocked) { + if (type === "Naval") return NAVAL_LANDLOCKED_FEE; + return GENERIC_LANDLOCKED_FEE; + } + + return 0; + } } diff --git a/src/scripts/generation/pack/burgsAndStates/generateBurgsAndStates.ts b/src/scripts/generation/pack/burgsAndStates/generateBurgsAndStates.ts index 157eb705..fb3e9c2f 100644 --- a/src/scripts/generation/pack/burgsAndStates/generateBurgsAndStates.ts +++ b/src/scripts/generation/pack/burgsAndStates/generateBurgsAndStates.ts @@ -9,15 +9,12 @@ 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[] + cells: Pick< + IPack["cells"], + "v" | "c" | "p" | "i" | "g" | "h" | "f" | "t" | "haven" | "harbor" | "r" | "fl" | "biome" | "s" | "culture" + > ) { const cellsNumber = cells.i.length; const burgIds = new Uint16Array(cellsNumber); @@ -30,13 +27,19 @@ export function generateBurgsAndStates( const states = createStates(capitals, cultures); const towns = createTowns(burgIds, cultures, pick(cells, "p", "i", "f", "s", "culture")); - expandStates(); + const stateIds = expandStates( + capitals, + states, + features, + pick(cells, "c", "h", "f", "t", "r", "fl", "s", "biome", "culture") + ); // normalizeStates(); + // burgs.filter(b => b.i && !b.removed).forEach(b => (b.state = stateIds[b.cell])); // assign state to burgs const roadScores = new Uint16Array(cellsNumber); // TODO: define roads const burgs = specifyBurgs(capitals, towns, roadScores); - return {burgIds, states, burgs}; + return {burgIds, stateIds, burgs, states}; function getScoredCellIds() { // cell score for capitals placement diff --git a/src/scripts/generation/pack/cultures.ts b/src/scripts/generation/pack/cultures.ts index e3a16249..d26c197f 100644 --- a/src/scripts/generation/pack/cultures.ts +++ b/src/scripts/generation/pack/cultures.ts @@ -282,35 +282,33 @@ export const expandCultures = function ( TIME && console.time("expandCultures"); const cultureIds = new Uint16Array(cells.h.length); // cell cultures - const isWilderness = (culture: ICulture | TWilderness): culture is TWilderness => culture.i === 0; - const queue = new FlatQueue<{cellId: number; cultureId: number}>(); + + const isWilderness = (culture: ICulture | TWilderness): culture is TWilderness => culture.i === 0; cultures.forEach(culture => { if (isWilderness(culture) || culture.removed) return; queue.push({cellId: culture.center, cultureId: culture.i}, 0); }); const cellsNumberFactor = cells.h.length / 1.6; - const neutral = cellsNumberFactor * getInputNumber("neutralInput"); // limit cost for culture growth + const maxExpansionCost = cellsNumberFactor * getInputNumber("neutralInput"); // limit cost for culture growth const cost: number[] = []; while (queue.length) { const priority = queue.peekValue()!; const {cellId, cultureId} = queue.pop()!; - if (cultureId === 0) throw new Error("Wilderness culture should not expand"); - const {type, expansionism} = cultures[cultureId] as ICulture; - const cultureBiome = cells.biome[cellId]; + const {type, expansionism, center} = getCulture(cultureId); + const cultureBiome = cells.biome[center]; cells.c[cellId].forEach(neibCellId => { - const biome = cells.biome[neibCellId]; - const biomeCost = getBiomeCost(biome, cultureBiome, type); + const biomeCost = getBiomeCost(neibCellId, cultureBiome, type); const heightCost = getHeightCost(neibCellId, cells.h[neibCellId], type); const riverCost = getRiverCost(cells.r[neibCellId], neibCellId, type); const typeCost = getTypeCost(cells.t[neibCellId], type); - const totalCost = priority + (biomeCost + heightCost + riverCost + typeCost) / expansionism; - if (totalCost > neutral) return; + const totalCost = priority + (biomeCost + heightCost + riverCost + typeCost) / expansionism; + if (totalCost > maxExpansionCost) return; if (!cost[neibCellId] || totalCost < cost[neibCellId]) { if (cells.pop[neibCellId] > 0) cultureIds[neibCellId] = cultureId; // assign culture to populated cell @@ -323,7 +321,14 @@ export const expandCultures = function ( TIME && console.timeEnd("expandCultures"); return cultureIds; - function getBiomeCost(biome: number, cultureBiome: number, type: TCultureType) { + function getCulture(cultureId: number) { + const culture = cultures[cultureId]; + if (isWilderness(culture)) throw new Error("Wilderness culture cannot expand"); + return culture; + } + + function getBiomeCost(cellId: number, cultureBiome: number, type: TCultureType) { + const biome = cells.biome[cellId]; if (cultureBiome === biome) return 10; // tiny penalty for native biome if (type === "Hunting") return biomesData.cost[biome] * 5; // non-native biome penalty for hunters if (type === "Nomadic" && FOREST_BIOMES.includes(biome)) return biomesData.cost[biome] * 10; // forest biome penalty for nomads diff --git a/src/scripts/generation/pack/pack.ts b/src/scripts/generation/pack/pack.ts index c19f8cbd..2f06d2e7 100644 --- a/src/scripts/generation/pack/pack.ts +++ b/src/scripts/generation/pack/pack.ts @@ -95,25 +95,19 @@ export function createPack(grid: IGrid): IPack { pop: population }); - 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, - rawRivers - ); + const {burgIds, stateIds, burgs, states} = generateBurgsAndStates(cultures, mergedFeatures, { + ...pick(cells, "v", "c", "p", "i", "g"), + h: heights, + f: featureIds, + t: distanceField, + haven, + harbor, + r: riverIds, + fl: flux, + biome, + s: suitability, + culture: cultureIds + }); // Religions.generate(); // BurgsAndStates.defineStateForms(); @@ -152,8 +146,9 @@ export function createPack(grid: IGrid): IPack { s: suitability, pop: population, culture: cultureIds, - burg: burgIds - // state, religion, province + burg: burgIds, + state: stateIds + // religion, province }, features: mergedFeatures, rivers: rawRivers, // "name" | "basin" | "type" diff --git a/src/types/common.d.ts b/src/types/common.d.ts index ff342bdb..8148e129 100644 --- a/src/types/common.d.ts +++ b/src/types/common.d.ts @@ -2,6 +2,9 @@ type Logical = number & (1 | 0); // data type for logical numbers type UnknownObject = {[key: string]: unknown}; +// extract element from array +type Entry = T[number]; + type noop = () => void; interface Dict { diff --git a/src/types/pack/states.d.ts b/src/types/pack/states.d.ts index d972a7ce..1021334e 100644 --- a/src/types/pack/states.d.ts +++ b/src/types/pack/states.d.ts @@ -2,6 +2,7 @@ interface IState { i: number; name: string; culture: number; + type: TCultureType; fullName: string; removed?: boolean; coa: ICoa | string;