refactor: main roads

This commit is contained in:
max 2022-08-18 22:10:04 +03:00
parent aff29d9d71
commit 5c2d30c8f0
10 changed files with 169 additions and 34 deletions

View file

@ -58,3 +58,9 @@ export const FOREST_BIOMES = [
TEMPERATE_RAINFOREST,
TAIGA
];
export const ROUTES = {
MAIN_ROAD: 1,
SMALL_ROAD: 2,
SEA_ROUTE: 3
};

View file

@ -0,0 +1,23 @@
import * as d3 from "d3";
import {round} from "utils/stringUtils";
export function drawRoutes() {
routes.selectAll("path").remove();
const lineGen = d3.line().curve(d3.curveBasis);
const routePaths: Dict<string[]> = {};
for (const {i, type, cells: routeCells} of pack.routes) {
const points = routeCells.map(cellId => pack.cells.p[cellId]);
const path = round(lineGen(points)!);
if (!routePaths[type]) routePaths[type] = [];
routePaths[type].push(`<path id="${type}${i}" d="${path}"/>`);
}
for (const type in routePaths) {
routes.select(`[data-type=${type}]`).html(routePaths[type].join(""));
}
}

View file

@ -16,6 +16,7 @@ import {drawPrecipitation} from "./drawPrecipitation";
import {drawProvinces} from "./drawProvinces";
import {drawReligions} from "./drawReligions";
import {drawRivers} from "./drawRivers";
import {drawRoutes} from "./drawRoutes";
import {drawStates} from "./drawStates";
import {drawTemperature} from "./drawTemperature";
@ -37,6 +38,7 @@ const layerRenderersMap = {
provinces: drawProvinces,
religions: drawReligions,
rivers: drawRivers,
routes: drawRoutes,
states: drawStates,
temperature: drawTemperature
};

View file

@ -32,9 +32,6 @@ export function defineSvg(width, height) {
stateBorders = borders.append("g").attr("id", "stateBorders");
provinceBorders = borders.append("g").attr("id", "provinceBorders");
routes = viewbox.append("g").attr("id", "routes");
roads = routes.append("g").attr("id", "roads");
trails = routes.append("g").attr("id", "trails");
searoutes = routes.append("g").attr("id", "searoutes");
temperature = viewbox.append("g").attr("id", "temperature");
coastline = viewbox.append("g").attr("id", "coastline");
ice = viewbox.append("g").attr("id", "ice").style("display", "none");
@ -57,6 +54,11 @@ export function defineSvg(width, height) {
ruler = viewbox.append("g").attr("id", "ruler").style("display", "none");
debug = viewbox.append("g").attr("id", "debug");
// route groups
roads = routes.append("g").attr("id", "roads").attr("data-type", "road");
trails = routes.append("g").attr("id", "trails").attr("data-type", "trail");
searoutes = routes.append("g").attr("id", "searoutes").attr("data-type", "sea");
// lake and coast groups
lakes.append("g").attr("id", "freshwater");
lakes.append("g").attr("id", "salt");

View file

@ -67,6 +67,7 @@ async function generate(options?: IGenerationOptions) {
renderLayer("heightmap");
renderLayer("rivers");
// renderLayer("biomes");
renderLayer("routes");
WARN && console.warn(`TOTAL: ${rn((performance.now() - timeStart) / 1000, 2)}s`);
// showStatistics();

View file

@ -1,36 +1,124 @@
import {TIME} from "config/logging";
import FlatQueue from "flatqueue";
export function generateRoutes(burgs: TBurgs) {
const routeScores = new Uint8Array(n); // cell road power
getRoads(burgs);
import {TIME} from "config/logging";
import {ROUTES} from "config/generation";
const isBurg = (burg: TNoBurg | IBurg): burg is IBurg => burg.i > 0;
export function generateRoutes(burgs: TBurgs, cells: Pick<IPack["cells"], "c" | "h" | "biome" | "state" | "burg">) {
const cellRoutes = new Uint8Array(cells.h.length);
const mainRoads = generateMainRoads();
// const townRoutes = getTrails();
// const oceanRoutes = getSearoutes();
return routeScores;
}
const routes = combineRoutes();
const getRoads = function (burgs: TBurgs) {
console.log(routes);
return {cellRoutes, routes};
function generateMainRoads() {
TIME && console.time("generateMainRoads");
const cells = pack.cells;
const mainRoads: {feature: number; from: number; to: number; end: number; cells: number[]}[] = [];
const isBurg = (burg: TNoBurg | IBurg): burg is IBurg => burg.i > 0;
const capitals = burgs.filter(burg => isBurg(burg) && burg.capital && !burg.removed) as IBurg[];
capitals.sort((a, b) => a.population - b.population);
const capitalsByFeature = burgs.reduce((acc, burg) => {
if (!isBurg(burg)) return acc;
const {capital, removed, feature} = burg;
if (!capital || removed) return acc;
if (capitals.length < 2) return []; // not enough capitals to build main roads
if (!acc[feature]) acc[feature] = [];
acc[feature].push(burg);
return acc;
}, {} as {[feature: string]: IBurg[]});
const routes = []; // array to store path segments
for (const [key, featureCapitals] of Object.entries(capitalsByFeature)) {
for (let i = 0; i < featureCapitals.length; i++) {
const {cell: from} = featureCapitals[i];
for (const {i, feature, cell: fromCell} of capitals) {
const sameFeatureCapitals = capitals.filter(capital => i !== capital.i && feature === capital.feature);
for (const {cell: toCell} of sameFeatureCapitals) {
const [from, exit] = findLandPath(fromCell, toCell, true);
const segments = restorePath(fromCell, exit, "main", from);
segments.forEach(s => routes.push(s));
for (let j = i + 1; j < featureCapitals.length; j++) {
const {cell: to} = featureCapitals[j];
const {end, pathCells} = findLandPath({start: from, exit: to});
if (end !== null && pathCells.length) {
pathCells.forEach(cellId => {
cellRoutes[cellId] = ROUTES.MAIN_ROAD;
});
mainRoads.push({feature: Number(key), from, to, end, cells: pathCells});
}
}
}
}
cells.i.forEach(i => (cells.s[i] += cells.road[i] / 2)); // add roads to suitability score
TIME && console.timeEnd("generateMainRoads");
return mainRoads;
}
// find land path to a specific cell or to a closest road
function findLandPath({start, exit}: {start: number; exit: number}) {
const from: number[] = [];
const end = findPath();
if (end === null) return {end, pathCells: []};
const pathCells = restorePath(start, end, from);
return {end, pathCells};
function findPath() {
const cost: number[] = [];
const queue = new FlatQueue<number>();
queue.push(start, 0);
while (queue.length) {
const priority = queue.peekValue()!;
const next = queue.pop()!;
if (cellRoutes[next]) return next;
for (const neibCellId of cells.c[next]) {
if (cells.h[neibCellId] < 20) continue; // ignore water cells
const stateChangeCost = cells.state && cells.state[neibCellId] !== cells.state[next] ? 400 : 0; // trails tend to lay within the same state
const habitability = biomesData.habitability[cells.biome[neibCellId]];
if (!habitability) continue; // avoid inhabitable cells (eg. lava, glacier)
const habitedCost = habitability ? Math.max(100 - habitability, 0) : 400; // routes tend to lay within populated areas
const heightChangeCost = Math.abs(cells.h[neibCellId] - cells.h[next]) * 10; // routes tend to avoid elevation changes
const heightCost = cells.h[neibCellId] > 80 ? cells.h[neibCellId] : 0; // routes tend to avoid mountainous areas
const cellCoast = 10 + stateChangeCost + habitedCost + heightChangeCost + heightCost;
const totalCost = priority + (cellRoutes[neibCellId] || cells.burg[neibCellId] ? cellCoast / 3 : cellCoast);
if (from[neibCellId] || totalCost >= cost[neibCellId]) continue;
from[neibCellId] = next;
if (neibCellId === exit) return exit;
cost[neibCellId] = totalCost;
queue.push(neibCellId, totalCost);
}
}
return null;
}
}
function combineRoutes() {
const routes: TRoutes = [];
for (const {feature, from, to, end, cells} of mainRoads) {
routes.push({i: routes.length, type: "road", feature, from, to, end, cells});
}
return routes;
};
}
}
function restorePath(start: number, end: number, from: number[]) {
const cells: number[] = [];
let current = end;
let prev = end;
while (current !== start) {
prev = from[current];
cells.push(current);
current = prev;
}
return cells;
}

View file

@ -117,7 +117,7 @@ export function createPack(grid: IGrid): IPack {
}
);
const routeScores = generateRoutes();
const {cellRoutes, routes} = generateRoutes(burgs, {c: cells.c, h: heights, biome, state: stateIds, burg: burgIds});
// Religions.generate();
// BurgsAndStates.defineStateForms();
@ -158,14 +158,15 @@ export function createPack(grid: IGrid): IPack {
culture: cultureIds,
burg: burgIds,
state: stateIds,
road: routeScores
route: cellRoutes
// religion, province
},
features: mergedFeatures,
rivers: rawRivers, // "name" | "basin" | "type"
cultures,
states,
burgs
burgs,
routes
};
return pack;

View file

@ -66,9 +66,9 @@ let borders: Selection<SVGGElement>;
let stateBorders: Selection<SVGGElement>;
let provinceBorders: Selection<SVGGElement>;
let routes: Selection<SVGGElement>;
let roads: Selection<SVGGElement>;
let trails: Selection<SVGGElement>;
let searoutes: Selection<SVGGElement>;
// let roads: Selection<SVGGElement>;
// let trails: Selection<SVGGElement>;
// let searoutes: Selection<SVGGElement>;
let temperature: Selection<SVGGElement>;
let coastline: Selection<SVGGElement>;
let ice: Selection<SVGGElement>;

View file

@ -7,6 +7,7 @@ interface IPack extends IGraph {
burgs: TBurgs;
rivers: IRiver[];
religions: IReligion[];
routes: TRoutes;
}
interface IPackCells {
@ -29,7 +30,7 @@ interface IPackCells {
burg: UintArray;
haven: UintArray;
harbor: UintArray;
road: Uint8Array;
route: Uint8Array; // [0, 1, 2, 3], see ROUTES enum, defined by generateRoutes()
q: Quadtree;
}

11
src/types/pack/routes.d.ts vendored Normal file
View file

@ -0,0 +1,11 @@
interface IRoute {
i: number;
type: "road" | "trail" | "sea";
feature: number;
from: number;
to: number;
end: number;
cells: number[];
}
type TRoutes = IRoute[];