mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-17 17:51:24 +01:00
refactor: generate wild provicnes continue
This commit is contained in:
parent
859d20546a
commit
2c35122bb8
5 changed files with 201 additions and 111 deletions
|
|
@ -143,11 +143,13 @@ export function createPack(grid: IGrid): IPack {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const {provinceIds, provinces} = generateProvinces(states, burgs, cultures, {
|
const {provinceIds, provinces} = generateProvinces(states, burgs, cultures, mergedFeatures, {
|
||||||
i: cells.i,
|
i: cells.i,
|
||||||
c: cells.c,
|
c: cells.c,
|
||||||
h: heights,
|
h: heights,
|
||||||
t: distanceField,
|
t: distanceField,
|
||||||
|
f: featureIds,
|
||||||
|
culture: cultureIds,
|
||||||
state: stateIds,
|
state: stateIds,
|
||||||
burg: burgIds
|
burg: burgIds
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import {DISTANCE_FIELD, ELEVATION, MIN_LAND_HEIGHT} from "config/generation";
|
|
||||||
import FlatQueue from "flatqueue";
|
import FlatQueue from "flatqueue";
|
||||||
|
|
||||||
|
import {DISTANCE_FIELD, ELEVATION, MIN_LAND_HEIGHT} from "config/generation";
|
||||||
import {gauss} from "utils/probabilityUtils";
|
import {gauss} from "utils/probabilityUtils";
|
||||||
|
|
||||||
const {WATER_COAST} = DISTANCE_FIELD;
|
const {WATER_COAST} = DISTANCE_FIELD;
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import {brighter, getMixedColor} from "utils/colorUtils";
|
||||||
import {gauss, P, rw} from "utils/probabilityUtils";
|
import {gauss, P, rw} from "utils/probabilityUtils";
|
||||||
import {isBurg, isState} from "utils/typeUtils";
|
import {isBurg, isState} from "utils/typeUtils";
|
||||||
import {provinceForms} from "./config";
|
import {provinceForms} from "./config";
|
||||||
|
import {generateProvinceName, generateProvinceEmblem} from "./utils";
|
||||||
|
|
||||||
const {COA, Names} = window;
|
const {COA, Names} = window;
|
||||||
|
|
||||||
|
|
@ -26,9 +27,9 @@ export function generateCoreProvinces(states: TStates, burgs: TBurgs, cultures:
|
||||||
|
|
||||||
for (let i = 0; i < provincesNumber; i++) {
|
for (let i = 0; i < provincesNumber; i++) {
|
||||||
const {i: burg, cell: center, culture: cultureId, coa: burgEmblem, name: burgName, type} = stateBurgs[i];
|
const {i: burg, cell: center, culture: cultureId, coa: burgEmblem, name: burgName, type} = stateBurgs[i];
|
||||||
|
|
||||||
const nameByBurg = P(0.5);
|
const nameByBurg = P(0.5);
|
||||||
const name = nameByBurg ? burgName : generateName(cultureId, cultures);
|
const name = generateName(nameByBurg, burgName, cultureId, cultures);
|
||||||
3;
|
|
||||||
const formName = rw(formsPool);
|
const formName = rw(formsPool);
|
||||||
formsPool[formName] += 10; // increase chance to get the same form again
|
formsPool[formName] += 10; // increase chance to get the same form again
|
||||||
|
|
||||||
|
|
@ -43,7 +44,9 @@ export function generateCoreProvinces(states: TStates, burgs: TBurgs, cultures:
|
||||||
return provinces;
|
return provinces;
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateName(cultureId: number, cultures: TCultures) {
|
function generateName(nameByBurg: boolean, burgName: string, cultureId: number, cultures: TCultures) {
|
||||||
|
if (nameByBurg) return burgName;
|
||||||
|
|
||||||
const base = cultures[cultureId].base;
|
const base = cultures[cultureId].base;
|
||||||
return Names.getState(Names.getBaseShort(base), base);
|
return Names.getState(Names.getBaseShort(base), base);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@ export function generateProvinces(
|
||||||
states: TStates,
|
states: TStates,
|
||||||
burgs: TBurgs,
|
burgs: TBurgs,
|
||||||
cultures: TCultures,
|
cultures: TCultures,
|
||||||
cells: Pick<IPack["cells"], "i" | "c" | "h" | "t" | "state" | "burg">
|
features: TPackFeatures,
|
||||||
|
cells: Pick<IPack["cells"], "i" | "c" | "h" | "t" | "f" | "culture" | "state" | "burg">
|
||||||
) {
|
) {
|
||||||
TIME && console.time("generateProvinces");
|
TIME && console.time("generateProvinces");
|
||||||
|
|
||||||
|
|
@ -18,7 +19,17 @@ export function generateProvinces(
|
||||||
|
|
||||||
const coreProvinces = generateCoreProvinces(states, burgs, cultures, percentage);
|
const coreProvinces = generateCoreProvinces(states, burgs, cultures, percentage);
|
||||||
const provinceIds = expandProvinces(percentage, coreProvinces, cells);
|
const provinceIds = expandProvinces(percentage, coreProvinces, cells);
|
||||||
const wildProvinces = generateWildProvinces(states, burgs, cultures, coreProvinces, provinceIds, cells); // mutates provinceIds
|
|
||||||
|
const wildProvinces = generateWildProvinces({
|
||||||
|
states,
|
||||||
|
burgs,
|
||||||
|
cultures,
|
||||||
|
features,
|
||||||
|
coreProvinces,
|
||||||
|
provinceIds,
|
||||||
|
percentage,
|
||||||
|
cells
|
||||||
|
}); // mutates provinceIds
|
||||||
|
|
||||||
const provinces = [...coreProvinces, ...wildProvinces];
|
const provinces = [...coreProvinces, ...wildProvinces];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,126 +1,200 @@
|
||||||
import {group} from "d3-array";
|
import {group} from "d3-array";
|
||||||
import {rand} from "utils/probabilityUtils";
|
import FlatQueue from "flatqueue";
|
||||||
|
|
||||||
|
import {DISTANCE_FIELD, MIN_LAND_HEIGHT} from "config/generation";
|
||||||
|
import {unique} from "utils/arrayUtils";
|
||||||
|
import {brighter, getMixedColor} from "utils/colorUtils";
|
||||||
|
import {gauss, P, ra, rw} from "utils/probabilityUtils";
|
||||||
|
import {isBurg, isState} from "utils/typeUtils";
|
||||||
|
import {provinceForms} from "./config";
|
||||||
|
|
||||||
|
const {COA, Names} = window;
|
||||||
|
|
||||||
// add "wild" provinces if some cells don't have a province assigned
|
// add "wild" provinces if some cells don't have a province assigned
|
||||||
export function generateWildProvinces(
|
export function generateWildProvinces({
|
||||||
states: TStates,
|
states,
|
||||||
burgs: TBurgs,
|
burgs,
|
||||||
cultures: TCultures,
|
cultures,
|
||||||
coreProvinces: IProvince[],
|
features,
|
||||||
provinceIds: Uint16Array,
|
coreProvinces,
|
||||||
cells: Pick<IPack["cells"], "i" | "state" | "burg">
|
provinceIds,
|
||||||
) {
|
percentage,
|
||||||
const stateProvincesMap = group(coreProvinces, (province: IProvince) => province.state);
|
cells
|
||||||
|
}: {
|
||||||
|
states: TStates;
|
||||||
|
burgs: TBurgs;
|
||||||
|
cultures: TCultures;
|
||||||
|
features: TPackFeatures;
|
||||||
|
coreProvinces: IProvince[];
|
||||||
|
provinceIds: Uint16Array;
|
||||||
|
percentage: number;
|
||||||
|
cells: Pick<IPack["cells"], "i" | "c" | "h" | "t" | "f" | "culture" | "state" | "burg">;
|
||||||
|
}) {
|
||||||
const noProvinceCells = Array.from(cells.i.filter(i => cells.state[i] && !provinceIds[i]));
|
const noProvinceCells = Array.from(cells.i.filter(i => cells.state[i] && !provinceIds[i]));
|
||||||
const wildProvinces = [] as IProvince[];
|
const wildProvinces = [] as IProvince[];
|
||||||
|
const colonyNamesMap = createColonyNamesMap();
|
||||||
|
|
||||||
for (const {i: stateId, name: stateName} of states) {
|
for (const state of states) {
|
||||||
const stateProvinces = stateProvincesMap.get(stateId);
|
if (!isState(state)) continue;
|
||||||
if (!stateProvinces || !stateProvinces.length) continue;
|
|
||||||
|
|
||||||
const coreProvinceNames = stateProvinces.map(({name}) => name);
|
let noProvinceCellsInState = noProvinceCells.filter(i => cells.state[i] === state.i);
|
||||||
const colonyNamePool = [stateName, ...coreProvinceNames].filter(name => name && !/new/i.test(name));
|
|
||||||
const getColonyName = () => {
|
|
||||||
if (colonyNamePool.length < 1) return null;
|
|
||||||
|
|
||||||
const index = rand(colonyNamePool.length - 1);
|
|
||||||
const spliced = colonyNamePool.splice(index, 1);
|
|
||||||
return spliced[0] ? `New ${spliced[0]}` : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
let noProvinceCellsInState = noProvinceCells.filter(i => cells.state[i] === stateId);
|
|
||||||
while (noProvinceCellsInState.length) {
|
while (noProvinceCellsInState.length) {
|
||||||
// add new province
|
|
||||||
const provinceId = coreProvinces.length + wildProvinces.length;
|
const provinceId = coreProvinces.length + wildProvinces.length;
|
||||||
const burgCell = noProvinceCellsInState.find(i => cells.burg[i]);
|
const burgCell = noProvinceCellsInState.find(i => cells.burg[i]);
|
||||||
const center = burgCell || noProvinceCellsInState[0];
|
const center = burgCell || noProvinceCellsInState[0];
|
||||||
const burg = burgCell ? cells.burg[burgCell] : 0;
|
const cultureId = cells.culture[center];
|
||||||
|
|
||||||
|
const burgId = burgCell ? cells.burg[burgCell] : 0;
|
||||||
|
const burg = burgs[burgId];
|
||||||
|
|
||||||
|
const provinceCells = expandWildProvince(center, provinceId, state.i); // mutates provinceIds
|
||||||
|
const formName = getProvinceForm(center, provinceCells, state.center);
|
||||||
|
const name = getProvinceName(state.i, formName, burg, cultureId);
|
||||||
|
const fullName = name + " " + formName;
|
||||||
|
|
||||||
|
const coa = generateEmblem(formName, state, burg, cultureId);
|
||||||
|
const color = brighter(getMixedColor(state.color, 0.2), 0.3);
|
||||||
|
|
||||||
|
wildProvinces.push({i: provinceId, name, formName, center, burg: burgId, state: state.i, fullName, color, coa});
|
||||||
|
|
||||||
|
// re-check
|
||||||
|
noProvinceCellsInState = noProvinceCells.filter(i => cells.state[i] === state.i && !provinceIds[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return wildProvinces;
|
||||||
|
|
||||||
|
function createColonyNamesMap() {
|
||||||
|
const stateProvincesMap = group(coreProvinces, (province: IProvince) => province.state);
|
||||||
|
|
||||||
|
const colonyNamesMap = new Map<number, string[]>(
|
||||||
|
states.map(state => {
|
||||||
|
const stateProvinces = stateProvincesMap.get(state.i) || [];
|
||||||
|
const coreProvinceNames = stateProvinces.map(province => province.name);
|
||||||
|
const colonyNamePool = unique([state.name, ...coreProvinceNames].filter(name => name && !/new/i.test(name)));
|
||||||
|
return [state.i, colonyNamePool];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return colonyNamesMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getColonyName(stateId: number) {
|
||||||
|
const namesPool = colonyNamesMap.get(stateId) || [];
|
||||||
|
if (namesPool.length < 1) return null;
|
||||||
|
|
||||||
|
const name = ra(namesPool);
|
||||||
|
colonyNamesMap.set(
|
||||||
|
stateId,
|
||||||
|
namesPool.filter(n => n !== name)
|
||||||
|
);
|
||||||
|
|
||||||
|
return `New ${name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProvinceName(stateId: number, formName: string, burg: TNoBurg | IBurg, cultureId: number) {
|
||||||
|
const colonyName = formName === "Colony" && P(0.8) && getColonyName(stateId);
|
||||||
|
if (colonyName) return colonyName;
|
||||||
|
|
||||||
|
if (burg?.name && P(0.5)) return burg.name;
|
||||||
|
|
||||||
|
const base = cultures[cultureId].base;
|
||||||
|
return Names.getState(Names.getBaseShort(base), base);
|
||||||
|
}
|
||||||
|
|
||||||
|
function expandWildProvince(center: number, provinceId: number, stateId: number) {
|
||||||
|
const maxExpansionCost = percentage === 100 ? 1000 : gauss(20, 5, 5, 100) * percentage ** 0.5;
|
||||||
|
|
||||||
|
const provinceCells = [center];
|
||||||
provinceIds[center] = provinceId;
|
provinceIds[center] = provinceId;
|
||||||
|
|
||||||
// expand province
|
const queue = new FlatQueue<number>();
|
||||||
const costs = [];
|
const cost: number[] = [];
|
||||||
costs[center] = 1;
|
cost[center] = 1;
|
||||||
queue.push(center, 0);
|
queue.push(center, 0);
|
||||||
|
|
||||||
while (queue.length) {
|
while (queue.length) {
|
||||||
const priority = queue.peekValue();
|
const priority = queue.peekValue()!;
|
||||||
const next = queue.pop();
|
const next = queue.pop()!;
|
||||||
|
|
||||||
cells.c[next].forEach(neibCellId => {
|
cells.c[next].forEach(neibCellId => {
|
||||||
if (cells.province[neibCellId]) return;
|
if (provinceIds[neibCellId]) return;
|
||||||
const land = cells.h[neibCellId] >= 20;
|
if (cells.state[neibCellId] !== stateId) return;
|
||||||
if (cells.state[neibCellId] && cells.state[neibCellId] !== s.i) return;
|
|
||||||
const cost = land ? (cells.state[neibCellId] === s.i ? 3 : 20) : cells.t[neibCellId] ? 10 : 30;
|
|
||||||
const totalCost = priority + cost;
|
|
||||||
|
|
||||||
if (totalCost > max) return;
|
const isLand = cells.h[neibCellId] >= MIN_LAND_HEIGHT;
|
||||||
if (!costs[neibCellId] || totalCost < costs[neibCellId]) {
|
const cellCost = isLand ? 3 : cells.t[neibCellId] === DISTANCE_FIELD.WATER_COAST ? 10 : 30;
|
||||||
if (land && cells.state[neibCellId] === s.i) cells.province[neibCellId] = provinceId; // assign province to a cell
|
const totalCost = priority + cellCost;
|
||||||
costs[neibCellId] = totalCost;
|
if (totalCost > maxExpansionCost) return;
|
||||||
|
|
||||||
|
if (!cost[neibCellId] || totalCost < cost[neibCellId]) {
|
||||||
|
if (isLand && cells.state[neibCellId] === stateId) {
|
||||||
|
// assign province to a cell
|
||||||
|
provinceCells.push(neibCellId);
|
||||||
|
provinceIds[neibCellId] = provinceId;
|
||||||
|
}
|
||||||
|
cost[neibCellId] = totalCost;
|
||||||
queue.push(neibCellId, totalCost);
|
queue.push(neibCellId, totalCost);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// generate "wild" province name
|
return provinceCells;
|
||||||
const cultureId = cells.culture[center];
|
}
|
||||||
const f = pack.features[cells.f[center]];
|
|
||||||
const color = brighter(getMixedColor(s.color, 0.2), 0.3);
|
|
||||||
|
|
||||||
const provCells = noProvinceCellsInState.filter(i => cells.province[i] === provinceId);
|
function getProvinceForm(center: number, provinceCells: number[], stateCenter: number) {
|
||||||
const singleIsle = provCells.length === f.cells && !provCells.find(i => cells.f[i] !== f.i);
|
const feature = features[cells.f[center]];
|
||||||
const isleGroup = !singleIsle && !provCells.find(i => pack.features[cells.f[i]].group !== "isle");
|
if (feature === 0) throw new Error("Feature is not defined");
|
||||||
const colony = !singleIsle && !isleGroup && P(0.5) && !isPassable(s.center, center);
|
|
||||||
|
|
||||||
const name = (function () {
|
const provinceFeatures = unique(provinceCells.map(i => cells.f[i]));
|
||||||
const colonyName = colony && P(0.8) && getColonyName();
|
const isWholeIsle = provinceCells.length === feature.cells && provinceFeatures.length === 1;
|
||||||
if (colonyName) return colonyName;
|
if (isWholeIsle) return "Island";
|
||||||
if (burgCell && P(0.5)) return burgs[burg].name;
|
|
||||||
const base = pack.cultures[cultureId].base;
|
|
||||||
|
|
||||||
return Names.getState(Names.getBaseShort(base), base);
|
const isIsleGroup = provinceFeatures.every(featureId => (features[featureId] as TPackFeature)?.group === "isle");
|
||||||
})();
|
if (isIsleGroup) return "Islands";
|
||||||
|
|
||||||
const formName = (function () {
|
const isColony = P(0.5) && !isConnected(stateCenter, center);
|
||||||
if (singleIsle) return "Island";
|
if (isColony) return "Colony";
|
||||||
if (isleGroup) return "Islands";
|
|
||||||
if (colony) return "Colony";
|
|
||||||
return rw(forms["Wild"]);
|
|
||||||
})();
|
|
||||||
|
|
||||||
const fullName = name + " " + formName;
|
return rw(provinceForms["Wild"]);
|
||||||
|
|
||||||
const dominion = colony ? P(0.95) : singleIsle || isleGroup ? P(0.7) : P(0.3);
|
// check if two cells are connected by land withing same state
|
||||||
const kinship = dominion ? 0 : 0.4;
|
function isConnected(from: number, to: number) {
|
||||||
const type = getType(center, burgs[burg]?.port);
|
|
||||||
const coa = COA.generate(s.coa, kinship, dominion, type);
|
|
||||||
coa.shield = COA.getPackShield(cultureId, s.i);
|
|
||||||
|
|
||||||
provinces.push({i: provinceId, state: s.i, center, burg, name, formName, fullName, color, coa});
|
|
||||||
s.provinces.push(provinceId);
|
|
||||||
|
|
||||||
// check if there is a land way within the same state between two cells
|
|
||||||
function isPassable(from, to) {
|
|
||||||
if (cells.f[from] !== cells.f[to]) return false; // on different islands
|
if (cells.f[from] !== cells.f[to]) return false; // on different islands
|
||||||
const queue = [from];
|
const queue = [from];
|
||||||
|
const checked: Dict<boolean> = {[from]: true};
|
||||||
const used = new Uint8Array(cells.i.length);
|
const stateId = cells.state[from];
|
||||||
const state = cells.state[from];
|
|
||||||
|
|
||||||
while (queue.length) {
|
while (queue.length) {
|
||||||
const current = queue.pop();
|
const current = queue.pop()!;
|
||||||
if (current === to) return true; // way is found
|
if (current === to) return true;
|
||||||
cells.c[current].forEach(c => {
|
|
||||||
if (used[c] || cells.h[c] < 20 || cells.state[c] !== state) return;
|
for (const neibId of cells.c[current]) {
|
||||||
queue.push(c);
|
if (checked[neibId] || cells.state[neibId] !== stateId) continue;
|
||||||
used[c] = 1;
|
queue.push(neibId);
|
||||||
});
|
checked[neibId] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return false; // way is not found
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// re-check
|
function generateEmblem(formName: string, state: IState, burg: TNoBurg | IBurg, cultureId: number) {
|
||||||
noProvinceCellsInState = noProvinceCells.filter(i => cells.state[i] === stateId && !provinceIds[i]);
|
const dominion = P(getDominionChance(formName));
|
||||||
|
const kinship = dominion ? 0 : 0.4;
|
||||||
|
const coaType = isBurg(burg) ? burg.type : "Generic";
|
||||||
|
const coa = COA.generate(state.coa, kinship, dominion, coaType);
|
||||||
|
|
||||||
|
const cultureShield = cultures[cultureId].shield;
|
||||||
|
const stateShield = (state.coa as ICoa)?.shield;
|
||||||
|
coa.shield = COA.getShield(cultureShield, stateShield);
|
||||||
|
|
||||||
|
return coa;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDominionChance(formName: string) {
|
||||||
|
if (formName === "Colony") return 0.95;
|
||||||
|
if (formName === "Island") return 0.7;
|
||||||
|
if (formName === "Islands") return 0.7;
|
||||||
|
return 0.3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue