mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-17 17:51:24 +01:00
refactor: main roads
This commit is contained in:
parent
aff29d9d71
commit
5c2d30c8f0
10 changed files with 169 additions and 34 deletions
|
|
@ -58,3 +58,9 @@ export const FOREST_BIOMES = [
|
||||||
TEMPERATE_RAINFOREST,
|
TEMPERATE_RAINFOREST,
|
||||||
TAIGA
|
TAIGA
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const ROUTES = {
|
||||||
|
MAIN_ROAD: 1,
|
||||||
|
SMALL_ROAD: 2,
|
||||||
|
SEA_ROUTE: 3
|
||||||
|
};
|
||||||
|
|
|
||||||
23
src/layers/renderers/drawRoutes.ts
Normal file
23
src/layers/renderers/drawRoutes.ts
Normal 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(""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,7 @@ import {drawPrecipitation} from "./drawPrecipitation";
|
||||||
import {drawProvinces} from "./drawProvinces";
|
import {drawProvinces} from "./drawProvinces";
|
||||||
import {drawReligions} from "./drawReligions";
|
import {drawReligions} from "./drawReligions";
|
||||||
import {drawRivers} from "./drawRivers";
|
import {drawRivers} from "./drawRivers";
|
||||||
|
import {drawRoutes} from "./drawRoutes";
|
||||||
import {drawStates} from "./drawStates";
|
import {drawStates} from "./drawStates";
|
||||||
import {drawTemperature} from "./drawTemperature";
|
import {drawTemperature} from "./drawTemperature";
|
||||||
|
|
||||||
|
|
@ -37,6 +38,7 @@ const layerRenderersMap = {
|
||||||
provinces: drawProvinces,
|
provinces: drawProvinces,
|
||||||
religions: drawReligions,
|
religions: drawReligions,
|
||||||
rivers: drawRivers,
|
rivers: drawRivers,
|
||||||
|
routes: drawRoutes,
|
||||||
states: drawStates,
|
states: drawStates,
|
||||||
temperature: drawTemperature
|
temperature: drawTemperature
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -32,9 +32,6 @@ export function defineSvg(width, height) {
|
||||||
stateBorders = borders.append("g").attr("id", "stateBorders");
|
stateBorders = borders.append("g").attr("id", "stateBorders");
|
||||||
provinceBorders = borders.append("g").attr("id", "provinceBorders");
|
provinceBorders = borders.append("g").attr("id", "provinceBorders");
|
||||||
routes = viewbox.append("g").attr("id", "routes");
|
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");
|
temperature = viewbox.append("g").attr("id", "temperature");
|
||||||
coastline = viewbox.append("g").attr("id", "coastline");
|
coastline = viewbox.append("g").attr("id", "coastline");
|
||||||
ice = viewbox.append("g").attr("id", "ice").style("display", "none");
|
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");
|
ruler = viewbox.append("g").attr("id", "ruler").style("display", "none");
|
||||||
debug = viewbox.append("g").attr("id", "debug");
|
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
|
// lake and coast groups
|
||||||
lakes.append("g").attr("id", "freshwater");
|
lakes.append("g").attr("id", "freshwater");
|
||||||
lakes.append("g").attr("id", "salt");
|
lakes.append("g").attr("id", "salt");
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ async function generate(options?: IGenerationOptions) {
|
||||||
renderLayer("heightmap");
|
renderLayer("heightmap");
|
||||||
renderLayer("rivers");
|
renderLayer("rivers");
|
||||||
// renderLayer("biomes");
|
// renderLayer("biomes");
|
||||||
|
renderLayer("routes");
|
||||||
|
|
||||||
WARN && console.warn(`TOTAL: ${rn((performance.now() - timeStart) / 1000, 2)}s`);
|
WARN && console.warn(`TOTAL: ${rn((performance.now() - timeStart) / 1000, 2)}s`);
|
||||||
// showStatistics();
|
// showStatistics();
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,124 @@
|
||||||
import {TIME} from "config/logging";
|
import FlatQueue from "flatqueue";
|
||||||
|
|
||||||
export function generateRoutes(burgs: TBurgs) {
|
import {TIME} from "config/logging";
|
||||||
const routeScores = new Uint8Array(n); // cell road power
|
import {ROUTES} from "config/generation";
|
||||||
getRoads(burgs);
|
|
||||||
|
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 townRoutes = getTrails();
|
||||||
// const oceanRoutes = getSearoutes();
|
// const oceanRoutes = getSearoutes();
|
||||||
|
|
||||||
return routeScores;
|
const routes = combineRoutes();
|
||||||
}
|
|
||||||
|
|
||||||
const getRoads = function (burgs: TBurgs) {
|
console.log(routes);
|
||||||
TIME && console.time("generateMainRoads");
|
return {cellRoutes, routes};
|
||||||
const cells = pack.cells;
|
|
||||||
|
|
||||||
const isBurg = (burg: TNoBurg | IBurg): burg is IBurg => burg.i > 0;
|
function generateMainRoads() {
|
||||||
const capitals = burgs.filter(burg => isBurg(burg) && burg.capital && !burg.removed) as IBurg[];
|
TIME && console.time("generateMainRoads");
|
||||||
capitals.sort((a, b) => a.population - b.population);
|
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) {
|
for (const [key, featureCapitals] of Object.entries(capitalsByFeature)) {
|
||||||
const sameFeatureCapitals = capitals.filter(capital => i !== capital.i && feature === capital.feature);
|
for (let i = 0; i < featureCapitals.length; i++) {
|
||||||
for (const {cell: toCell} of sameFeatureCapitals) {
|
const {cell: from} = featureCapitals[i];
|
||||||
const [from, exit] = findLandPath(fromCell, toCell, true);
|
|
||||||
const segments = restorePath(fromCell, exit, "main", from);
|
for (let j = i + 1; j < featureCapitals.length; j++) {
|
||||||
segments.forEach(s => routes.push(s));
|
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<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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cells.i.forEach(i => (cells.s[i] += cells.road[i] / 2)); // add roads to suitability score
|
function combineRoutes() {
|
||||||
TIME && console.timeEnd("generateMainRoads");
|
const routes: TRoutes = [];
|
||||||
return routes;
|
|
||||||
};
|
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
// Religions.generate();
|
||||||
// BurgsAndStates.defineStateForms();
|
// BurgsAndStates.defineStateForms();
|
||||||
|
|
@ -158,14 +158,15 @@ export function createPack(grid: IGrid): IPack {
|
||||||
culture: cultureIds,
|
culture: cultureIds,
|
||||||
burg: burgIds,
|
burg: burgIds,
|
||||||
state: stateIds,
|
state: stateIds,
|
||||||
road: routeScores
|
route: cellRoutes
|
||||||
// religion, province
|
// religion, province
|
||||||
},
|
},
|
||||||
features: mergedFeatures,
|
features: mergedFeatures,
|
||||||
rivers: rawRivers, // "name" | "basin" | "type"
|
rivers: rawRivers, // "name" | "basin" | "type"
|
||||||
cultures,
|
cultures,
|
||||||
states,
|
states,
|
||||||
burgs
|
burgs,
|
||||||
|
routes
|
||||||
};
|
};
|
||||||
|
|
||||||
return pack;
|
return pack;
|
||||||
|
|
|
||||||
6
src/types/globals.d.ts
vendored
6
src/types/globals.d.ts
vendored
|
|
@ -66,9 +66,9 @@ let borders: Selection<SVGGElement>;
|
||||||
let stateBorders: Selection<SVGGElement>;
|
let stateBorders: Selection<SVGGElement>;
|
||||||
let provinceBorders: Selection<SVGGElement>;
|
let provinceBorders: Selection<SVGGElement>;
|
||||||
let routes: Selection<SVGGElement>;
|
let routes: Selection<SVGGElement>;
|
||||||
let roads: Selection<SVGGElement>;
|
// let roads: Selection<SVGGElement>;
|
||||||
let trails: Selection<SVGGElement>;
|
// let trails: Selection<SVGGElement>;
|
||||||
let searoutes: Selection<SVGGElement>;
|
// let searoutes: Selection<SVGGElement>;
|
||||||
let temperature: Selection<SVGGElement>;
|
let temperature: Selection<SVGGElement>;
|
||||||
let coastline: Selection<SVGGElement>;
|
let coastline: Selection<SVGGElement>;
|
||||||
let ice: Selection<SVGGElement>;
|
let ice: Selection<SVGGElement>;
|
||||||
|
|
|
||||||
3
src/types/pack/pack.d.ts
vendored
3
src/types/pack/pack.d.ts
vendored
|
|
@ -7,6 +7,7 @@ interface IPack extends IGraph {
|
||||||
burgs: TBurgs;
|
burgs: TBurgs;
|
||||||
rivers: IRiver[];
|
rivers: IRiver[];
|
||||||
religions: IReligion[];
|
religions: IReligion[];
|
||||||
|
routes: TRoutes;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IPackCells {
|
interface IPackCells {
|
||||||
|
|
@ -29,7 +30,7 @@ interface IPackCells {
|
||||||
burg: UintArray;
|
burg: UintArray;
|
||||||
haven: UintArray;
|
haven: UintArray;
|
||||||
harbor: UintArray;
|
harbor: UintArray;
|
||||||
road: Uint8Array;
|
route: Uint8Array; // [0, 1, 2, 3], see ROUTES enum, defined by generateRoutes()
|
||||||
q: Quadtree;
|
q: Quadtree;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
11
src/types/pack/routes.d.ts
vendored
Normal file
11
src/types/pack/routes.d.ts
vendored
Normal 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[];
|
||||||
Loading…
Add table
Add a link
Reference in a new issue