From 5c2d30c8f0b07c306f7b371e6ad64a124992cc79 Mon Sep 17 00:00:00 2001 From: max Date: Thu, 18 Aug 2022 22:10:04 +0300 Subject: [PATCH] refactor: main roads --- src/config/generation.ts | 6 + src/layers/renderers/drawRoutes.ts | 23 +++ src/layers/renderers/index.ts | 2 + src/modules/define-svg.js | 8 +- src/scripts/generation/generation.ts | 1 + src/scripts/generation/pack/generateRoutes.ts | 136 ++++++++++++++---- src/scripts/generation/pack/pack.ts | 7 +- src/types/globals.d.ts | 6 +- src/types/pack/pack.d.ts | 3 +- src/types/pack/routes.d.ts | 11 ++ 10 files changed, 169 insertions(+), 34 deletions(-) create mode 100644 src/layers/renderers/drawRoutes.ts create mode 100644 src/types/pack/routes.d.ts diff --git a/src/config/generation.ts b/src/config/generation.ts index bd2d1c4b..caf99966 100644 --- a/src/config/generation.ts +++ b/src/config/generation.ts @@ -58,3 +58,9 @@ export const FOREST_BIOMES = [ TEMPERATE_RAINFOREST, TAIGA ]; + +export const ROUTES = { + MAIN_ROAD: 1, + SMALL_ROAD: 2, + SEA_ROUTE: 3 +}; diff --git a/src/layers/renderers/drawRoutes.ts b/src/layers/renderers/drawRoutes.ts new file mode 100644 index 00000000..158501b0 --- /dev/null +++ b/src/layers/renderers/drawRoutes.ts @@ -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 = {}; + + 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(``); + } + + for (const type in routePaths) { + routes.select(`[data-type=${type}]`).html(routePaths[type].join("")); + } +} diff --git a/src/layers/renderers/index.ts b/src/layers/renderers/index.ts index 750b6e69..b2abbc20 100644 --- a/src/layers/renderers/index.ts +++ b/src/layers/renderers/index.ts @@ -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 }; diff --git a/src/modules/define-svg.js b/src/modules/define-svg.js index 29dc267b..9006b702 100644 --- a/src/modules/define-svg.js +++ b/src/modules/define-svg.js @@ -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"); diff --git a/src/scripts/generation/generation.ts b/src/scripts/generation/generation.ts index 33cbb513..ffbfb2cf 100644 --- a/src/scripts/generation/generation.ts +++ b/src/scripts/generation/generation.ts @@ -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(); diff --git a/src/scripts/generation/pack/generateRoutes.ts b/src/scripts/generation/pack/generateRoutes.ts index 9e7388ee..f0175ad1 100644 --- a/src/scripts/generation/pack/generateRoutes.ts +++ b/src/scripts/generation/pack/generateRoutes.ts @@ -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) { + 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) { - TIME && console.time("generateMainRoads"); - const cells = pack.cells; + console.log(routes); + return {cellRoutes, routes}; - 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); + function generateMainRoads() { + TIME && console.time("generateMainRoads"); + const mainRoads: {feature: number; from: number; to: number; end: number; cells: number[]}[] = []; - if (capitals.length < 2) return []; // not enough capitals to build main roads + const capitalsByFeature = burgs.reduce((acc, burg) => { + if (!isBurg(burg)) return acc; + const {capital, removed, feature} = burg; + if (!capital || removed) return acc; - const routes = []; // array to store path segments + if (!acc[feature]) acc[feature] = []; + acc[feature].push(burg); + return acc; + }, {} as {[feature: string]: IBurg[]}); - 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 (const [key, featureCapitals] of Object.entries(capitalsByFeature)) { + for (let i = 0; i < featureCapitals.length; i++) { + const {cell: from} = featureCapitals[i]; + + 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}); + } + } + } + } + + 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(); + 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; } } - cells.i.forEach(i => (cells.s[i] += cells.road[i] / 2)); // add roads to suitability score - TIME && console.timeEnd("generateMainRoads"); - return routes; -}; + 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; +} diff --git a/src/scripts/generation/pack/pack.ts b/src/scripts/generation/pack/pack.ts index b5ec0a7e..986478a1 100644 --- a/src/scripts/generation/pack/pack.ts +++ b/src/scripts/generation/pack/pack.ts @@ -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; diff --git a/src/types/globals.d.ts b/src/types/globals.d.ts index 17a1e3f2..a2efe7ec 100644 --- a/src/types/globals.d.ts +++ b/src/types/globals.d.ts @@ -66,9 +66,9 @@ let borders: Selection; let stateBorders: Selection; let provinceBorders: Selection; let routes: Selection; -let roads: Selection; -let trails: Selection; -let searoutes: Selection; +// let roads: Selection; +// let trails: Selection; +// let searoutes: Selection; let temperature: Selection; let coastline: Selection; let ice: Selection; diff --git a/src/types/pack/pack.d.ts b/src/types/pack/pack.d.ts index 1e261486..d1a3302a 100644 --- a/src/types/pack/pack.d.ts +++ b/src/types/pack/pack.d.ts @@ -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; } diff --git a/src/types/pack/routes.d.ts b/src/types/pack/routes.d.ts new file mode 100644 index 00000000..0577f92c --- /dev/null +++ b/src/types/pack/routes.d.ts @@ -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[];