mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-17 17:51:24 +01:00
refactor: generate states
This commit is contained in:
parent
e62d6ec74c
commit
c2fc0679ad
15 changed files with 472 additions and 301 deletions
|
|
@ -17,7 +17,7 @@ let rulers;
|
||||||
let biomesData;
|
let biomesData;
|
||||||
let nameBases;
|
let nameBases;
|
||||||
|
|
||||||
// defined in main.js
|
// defined in main.ts
|
||||||
let graphWidth;
|
let graphWidth;
|
||||||
let graphHeight;
|
let graphHeight;
|
||||||
let svgWidth;
|
let svgWidth;
|
||||||
|
|
@ -44,7 +44,7 @@ let svg,
|
||||||
texture,
|
texture,
|
||||||
terrs,
|
terrs,
|
||||||
biomes,
|
biomes,
|
||||||
cells,
|
// cells,
|
||||||
gridOverlay,
|
gridOverlay,
|
||||||
coordinates,
|
coordinates,
|
||||||
compass,
|
compass,
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ export enum DISTANCE_FIELD {
|
||||||
|
|
||||||
export enum ELEVATION {
|
export enum ELEVATION {
|
||||||
MOUNTAINS = 70,
|
MOUNTAINS = 70,
|
||||||
|
FOOTHILLS = 60,
|
||||||
HILLS = 50,
|
HILLS = 50,
|
||||||
LOWLANDS = 30
|
LOWLANDS = 30
|
||||||
}
|
}
|
||||||
|
|
@ -46,7 +47,7 @@ const {
|
||||||
WETLAND
|
WETLAND
|
||||||
} = BIOME;
|
} = 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];
|
export const HUNTING_BIOMES = [SAVANNA, TROPICAL_RAINFOREST, TEMPERATE_RAINFOREST, TAIGA, TUNDRA, WETLAND];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import {Voronoi} from "/src/modules/voronoi";
|
||||||
import {getColors, getMixedColor, getRandomColor} from "utils/colorUtils";
|
import {getColors, getMixedColor, getRandomColor} from "utils/colorUtils";
|
||||||
import {findCell} from "utils/graphUtils";
|
import {findCell} from "utils/graphUtils";
|
||||||
import {getAdjective, trimVowels} from "utils/languageUtils";
|
import {getAdjective, trimVowels} from "utils/languageUtils";
|
||||||
import {getMiddlePoint} from "utils/lineUtils";
|
|
||||||
import {minmax, rn} from "utils/numberUtils";
|
import {minmax, rn} from "utils/numberUtils";
|
||||||
import {each, gauss, generateSeed, P, ra, rand, rw} from "utils/probabilityUtils";
|
import {each, gauss, generateSeed, P, ra, rand, rw} from "utils/probabilityUtils";
|
||||||
import {round, splitInTwo} from "utils/stringUtils";
|
import {round, splitInTwo} from "utils/stringUtils";
|
||||||
|
|
|
||||||
|
|
@ -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<IPack["cells"], "v" | "p" | "i" | "g" | "f" | "haven" | "harbor" | "r" | "fl" | "s" | "culture">,
|
|
||||||
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";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
2
src/scripts/generation/pack/burgsAndStates/config.ts
Normal file
2
src/scripts/generation/pack/burgsAndStates/config.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export const NO_BURG: TNoBurg = {name: undefined};
|
||||||
|
export const NEUTRALS: TNeutrals = {i: 0, name: "Neutrals"};
|
||||||
50
src/scripts/generation/pack/burgsAndStates/createCapitals.ts
Normal file
50
src/scripts/generation/pack/burgsAndStates/createCapitals.ts
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
40
src/scripts/generation/pack/burgsAndStates/createStates.ts
Normal file
40
src/scripts/generation/pack/burgsAndStates/createStates.ts
Normal file
|
|
@ -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<typeof createCapitals>;
|
||||||
|
|
||||||
|
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];
|
||||||
|
}
|
||||||
77
src/scripts/generation/pack/burgsAndStates/createTowns.ts
Normal file
77
src/scripts/generation/pack/burgsAndStates/createTowns.ts
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
90
src/scripts/generation/pack/burgsAndStates/expandStates.ts
Normal file
90
src/scripts/generation/pack/burgsAndStates/expandStates.ts
Normal file
|
|
@ -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");
|
||||||
|
}
|
||||||
|
|
@ -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<IRiver, "name" | "basin" | "type">[]
|
||||||
|
) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
126
src/scripts/generation/pack/burgsAndStates/specifyBurgs.ts
Normal file
126
src/scripts/generation/pack/burgsAndStates/specifyBurgs.ts
Normal file
|
|
@ -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<typeof createCapitals>;
|
||||||
|
type TTowns = ReturnType<typeof createTowns>;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,7 +11,7 @@ import {pick} from "utils/functionUtils";
|
||||||
import {rn} from "utils/numberUtils";
|
import {rn} from "utils/numberUtils";
|
||||||
import {generateCultures, expandCultures} from "./cultures";
|
import {generateCultures, expandCultures} from "./cultures";
|
||||||
import {generateRivers} from "./rivers";
|
import {generateRivers} from "./rivers";
|
||||||
import {generateBurgsAndStates} from "./burgsAndStates";
|
import {generateBurgsAndStates} from "./burgsAndStates/generateBurgsAndStates";
|
||||||
|
|
||||||
const {LAND_COAST, WATER_COAST, DEEPER_WATER} = DISTANCE_FIELD;
|
const {LAND_COAST, WATER_COAST, DEEPER_WATER} = DISTANCE_FIELD;
|
||||||
const {Biomes} = window;
|
const {Biomes} = window;
|
||||||
|
|
@ -98,18 +98,21 @@ export function createPack(grid: IGrid): IPack {
|
||||||
const {burgIds, states, burgs} = generateBurgsAndStates(
|
const {burgIds, states, burgs} = generateBurgsAndStates(
|
||||||
{
|
{
|
||||||
...pick(cells, "v", "p", "i", "g"),
|
...pick(cells, "v", "p", "i", "g"),
|
||||||
|
h: heights,
|
||||||
f: featureIds,
|
f: featureIds,
|
||||||
haven,
|
haven,
|
||||||
harbor,
|
harbor,
|
||||||
r: riverIds,
|
r: riverIds,
|
||||||
fl: flux,
|
fl: flux,
|
||||||
|
biome,
|
||||||
s: suitability,
|
s: suitability,
|
||||||
culture: cultureIds
|
culture: cultureIds
|
||||||
},
|
},
|
||||||
vertices,
|
vertices,
|
||||||
cultures,
|
cultures,
|
||||||
mergedFeatures,
|
mergedFeatures,
|
||||||
temp
|
temp,
|
||||||
|
rawRivers
|
||||||
);
|
);
|
||||||
|
|
||||||
// Religions.generate();
|
// Religions.generate();
|
||||||
|
|
|
||||||
1
src/types/pack/burgs.d.ts
vendored
1
src/types/pack/burgs.d.ts
vendored
|
|
@ -5,6 +5,7 @@ interface IBurg {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
population: number;
|
population: number;
|
||||||
|
type: TCultureType;
|
||||||
capital: Logical; // 1 - capital, 0 - burg
|
capital: Logical; // 1 - capital, 0 - burg
|
||||||
port: number; // port feature id, 0 - not a port
|
port: number; // port feature id, 0 - not a port
|
||||||
shanty?: number;
|
shanty?: number;
|
||||||
|
|
|
||||||
1
src/types/pack/features.d.ts
vendored
1
src/types/pack/features.d.ts
vendored
|
|
@ -6,7 +6,6 @@ interface IPackFeatureBase {
|
||||||
vertices: number[]; // indexes of perimetric vertices
|
vertices: number[]; // indexes of perimetric vertices
|
||||||
area: number; // area of the feature perimetric polygon
|
area: number; // area of the feature perimetric polygon
|
||||||
}
|
}
|
||||||
3;
|
|
||||||
|
|
||||||
interface IPackFeatureOcean extends IPackFeatureBase {
|
interface IPackFeatureOcean extends IPackFeatureBase {
|
||||||
land: false;
|
land: false;
|
||||||
|
|
|
||||||
11
src/types/pack/states.d.ts
vendored
11
src/types/pack/states.d.ts
vendored
|
|
@ -1,8 +1,10 @@
|
||||||
interface IState {
|
interface IState {
|
||||||
i: number;
|
i: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
culture: number;
|
||||||
fullName: string;
|
fullName: string;
|
||||||
removed?: boolean;
|
removed?: boolean;
|
||||||
|
coa: ICoa | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type TNeutrals = {
|
type TNeutrals = {
|
||||||
|
|
@ -11,3 +13,12 @@ type TNeutrals = {
|
||||||
};
|
};
|
||||||
|
|
||||||
type TStates = [TNeutrals, ...IState[]];
|
type TStates = [TNeutrals, ...IState[]];
|
||||||
|
|
||||||
|
interface ICoa {
|
||||||
|
t1: string;
|
||||||
|
division: {};
|
||||||
|
ordinaries: {}[];
|
||||||
|
charges: {}[];
|
||||||
|
shield: "heater";
|
||||||
|
t1: "purpure";
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue