From 6776e5b867c20dc9fa9fae6d1b671fe6aeb46277 Mon Sep 17 00:00:00 2001
From: Azgaar
Date: Sun, 24 Mar 2024 20:10:11 +0100
Subject: [PATCH] feat: routes generation
---
index.html | 6 +-
main.js | 11 +-
modules/burgs-and-states.js | 10 +-
modules/dynamic/export-json.js | 6 +-
modules/dynamic/overview/charts-overview.js | 2 -
modules/io/load.js | 4 +-
modules/io/save.js | 4 +-
modules/markers-generator.js | 14 +-
modules/religions-generator.js | 6 +-
modules/routes-generator-old.js | 273 +++++++++++
modules/routes-generator.js | 517 +++++++++++---------
modules/submap.js | 5 +-
modules/ui/editors.js | 6 +-
modules/ui/heightmap-editor.js | 12 +-
modules/ui/measurers.js | 4 +-
modules/ui/tools.js | 2 +-
modules/ui/units-editor.js | 4 +-
utils/functionUtils.js | 10 +-
versioning.js | 6 +-
19 files changed, 607 insertions(+), 295 deletions(-)
create mode 100644 modules/routes-generator-old.js
diff --git a/index.html b/index.html
index 9fa9dbc7..7f80d5cc 100644
--- a/index.html
+++ b/index.html
@@ -6146,7 +6146,7 @@
Data to be copied: heightmap, biomes, religions, population, precipitation, cultures, states, provinces,
military regiments
- Data to be regenerated: zones, roads, rivers
+ Data to be regenerated: zones, routes, rivers
Burgs may be remapped incorrectly, manual change is required
Keep data for:
@@ -8009,10 +8009,12 @@
+
+
@@ -8035,7 +8037,7 @@
-
+
diff --git a/main.js b/main.js
index efc115cf..7e740baa 100644
--- a/main.js
+++ b/main.js
@@ -644,6 +644,7 @@ async function generate(options) {
Cultures.generate();
Cultures.expand();
BurgsAndStates.generate();
+ Routes.generate();
Religions.generate();
BurgsAndStates.defineStateForms();
BurgsAndStates.generateProvinces();
@@ -1652,8 +1653,8 @@ function addZones(number = 1) {
used[next.e] = 1;
cells.c[next.e].forEach(function (e) {
- const r = cells.road[next.e];
- const c = r ? Math.max(10 - r, 1) : 100;
+ const r = cells.route[next.e];
+ const c = r ? 5 : 100;
const p = next.p + c;
if (p > power) return;
@@ -1780,10 +1781,10 @@ function addZones(number = 1) {
}
function addAvalanche() {
- const roads = cells.i.filter(i => !used[i] && cells.road[i] && cells.h[i] >= 70);
- if (!roads.length) return;
+ const routes = cells.i.filter(i => !used[i] && cells.route[i] && cells.h[i] >= 70);
+ if (!routes.length) return;
- const cell = +ra(roads);
+ const cell = +ra(routes);
const cellsArray = [],
queue = [cell],
power = rand(3, 15);
diff --git a/modules/burgs-and-states.js b/modules/burgs-and-states.js
index f4f6463a..c9408c62 100644
--- a/modules/burgs-and-states.js
+++ b/modules/burgs-and-states.js
@@ -6,27 +6,20 @@ window.BurgsAndStates = (function () {
const n = cells.i.length;
cells.burg = new Uint16Array(n); // cell burg
- cells.road = new Uint16Array(n); // cell road power
- cells.crossroad = new Uint16Array(n); // cell crossroad power
const burgs = (pack.burgs = placeCapitals());
pack.states = createStates();
- const capitalRoutes = Routes.getRoads();
placeTowns();
expandStates();
normalizeStates();
- const townRoutes = Routes.getTrails();
specifyBurgs();
- const oceanRoutes = Routes.getSearoutes();
-
collectStatistics();
assignColors();
generateCampaigns();
generateDiplomacy();
- Routes.draw(capitalRoutes, townRoutes, oceanRoutes);
drawBurgs();
function placeCapitals() {
@@ -167,7 +160,6 @@ window.BurgsAndStates = (function () {
const specifyBurgs = function () {
TIME && console.time("specifyBurgs");
const cells = pack.cells,
- vertices = pack.vertices,
features = pack.features,
temp = grid.cells.temp;
@@ -185,7 +177,7 @@ window.BurgsAndStates = (function () {
} else b.port = 0;
// define burg population (keep urbanization at about 10% rate)
- b.population = rn(Math.max((cells.s[i] + cells.road[i] / 2) / 8 + b.i / 1000 + (i % 100) / 1000, 0.1), 3);
+ b.population = rn(Math.max(cells.s[i] / 8 + b.i / 1000 + (i % 100) / 1000, 0.1), 3);
if (b.capital) b.population = rn(b.population * 1.3, 3); // increase capital population
if (b.port) {
diff --git a/modules/dynamic/export-json.js b/modules/dynamic/export-json.js
index c8110b80..1c403bd1 100644
--- a/modules/dynamic/export-json.js
+++ b/modules/dynamic/export-json.js
@@ -122,8 +122,7 @@ function getPackCellsData() {
pop: Array.from(pack.cells.pop),
culture: Array.from(pack.cells.culture),
burg: Array.from(pack.cells.burg),
- road: Array.from(pack.cells.road),
- crossroad: Array.from(pack.cells.crossroad),
+ route: Array.from(pack.cells.route),
state: Array.from(pack.cells.state),
religion: Array.from(pack.cells.religion),
province: Array.from(pack.cells.province)
@@ -150,8 +149,7 @@ function getPackCellsData() {
pop: dataArrays.pop[cellId],
culture: dataArrays.culture[cellId],
burg: dataArrays.burg[cellId],
- road: dataArrays.road[cellId],
- crossroad: dataArrays.crossroad[cellId],
+ route: dataArrays.route[cellId],
state: dataArrays.state[cellId],
religion: dataArrays.religion[cellId],
province: dataArrays.province[cellId]
diff --git a/modules/dynamic/overview/charts-overview.js b/modules/dynamic/overview/charts-overview.js
index 35a83b5e..171778a4 100644
--- a/modules/dynamic/overview/charts-overview.js
+++ b/modules/dynamic/overview/charts-overview.js
@@ -1,5 +1,3 @@
-import {rollups} from "../../../utils/functionUtils.js";
-
const entitiesMap = {
states: {
label: "State",
diff --git a/modules/io/load.js b/modules/io/load.js
index ff6b2731..239507a4 100644
--- a/modules/io/load.js
+++ b/modules/io/load.js
@@ -383,12 +383,12 @@ async function parseLoadedData(data, mapVersion) {
cells.fl = Uint16Array.from(data[20].split(","));
cells.pop = Float32Array.from(data[21].split(","));
cells.r = Uint16Array.from(data[22].split(","));
- cells.road = Uint16Array.from(data[23].split(","));
+ cells.route = Uint8Array.from(data[23].split(","));
cells.s = Uint16Array.from(data[24].split(","));
cells.state = Uint16Array.from(data[25].split(","));
cells.religion = data[26] ? Uint16Array.from(data[26].split(",")) : new Uint16Array(cells.i.length);
cells.province = data[27] ? Uint16Array.from(data[27].split(",")) : new Uint16Array(cells.i.length);
- cells.crossroad = data[28] ? Uint16Array.from(data[28].split(",")) : new Uint16Array(cells.i.length);
+ // data[28] for deprecated cells.crossroad
if (data[31]) {
const namesDL = data[31].split("/");
diff --git a/modules/io/save.js b/modules/io/save.js
index efa1e89a..45bf6018 100644
--- a/modules/io/save.js
+++ b/modules/io/save.js
@@ -135,12 +135,12 @@ function prepareMapData() {
pack.cells.fl,
pop,
pack.cells.r,
- pack.cells.road,
+ pack.cells.route,
pack.cells.s,
pack.cells.state,
pack.cells.religion,
pack.cells.province,
- pack.cells.crossroad,
+ [], // deprecated pack.cells.crossroad
religions,
provinces,
namesData,
diff --git a/modules/markers-generator.js b/modules/markers-generator.js
index 1ba6a365..585a9153 100644
--- a/modules/markers-generator.js
+++ b/modules/markers-generator.js
@@ -279,7 +279,7 @@ window.Markers = (function () {
}
function listInns({cells}) {
- return cells.i.filter(i => !occupied[i] && cells.h[i] >= 20 && cells.road[i] > 4 && cells.pop[i] > 10);
+ return cells.i.filter(i => !occupied[i] && cells.h[i] >= 20 && cells.route[i] === 1 && cells.pop[i] > 10);
}
function addInn(id, cell) {
@@ -542,7 +542,7 @@ window.Markers = (function () {
function listLighthouses({cells}) {
return cells.i.filter(
- i => !occupied[i] && cells.harbor[i] > 6 && cells.c[i].some(c => cells.h[c] < 20 && cells.road[c])
+ i => !occupied[i] && cells.harbor[i] > 6 && cells.c[i].some(c => cells.h[c] < 20 && cells.route[c])
);
}
@@ -642,7 +642,7 @@ window.Markers = (function () {
function listSeaMonsters({cells, features}) {
return cells.i.filter(
- i => !occupied[i] && cells.h[i] < 20 && cells.road[i] && features[cells.f[i]].type === "ocean"
+ i => !occupied[i] && cells.h[i] < 20 && cells.route[i] && features[cells.f[i]].type === "ocean"
);
}
@@ -792,7 +792,7 @@ window.Markers = (function () {
cells.religion[i] &&
cells.biome[i] === 1 &&
cells.pop[i] > 1 &&
- cells.road[i]
+ cells.route[i]
);
}
@@ -807,7 +807,7 @@ window.Markers = (function () {
}
function listBrigands({cells}) {
- return cells.i.filter(i => !occupied[i] && cells.culture[i] && cells.road[i] > 4);
+ return cells.i.filter(i => !occupied[i] && cells.culture[i] && cells.route[i] === 1);
}
function addBrigands(id, cell) {
@@ -867,7 +867,7 @@ window.Markers = (function () {
// Pirates spawn on sea routes
function listPirates({cells}) {
- return cells.i.filter(i => !occupied[i] && cells.h[i] < 20 && cells.road[i]);
+ return cells.i.filter(i => !occupied[i] && cells.h[i] < 20 && cells.route[i]);
}
function addPirates(id, cell) {
@@ -961,7 +961,7 @@ window.Markers = (function () {
}
function listCircuses({cells}) {
- return cells.i.filter(i => !occupied[i] && cells.culture[i] && cells.h[i] >= 20 && pack.cells.road[i]);
+ return cells.i.filter(i => !occupied[i] && cells.culture[i] && cells.h[i] >= 20 && pack.cells.route[i]);
}
function addCircuse(id, cell) {
diff --git a/modules/religions-generator.js b/modules/religions-generator.js
index 88857a01..238fcddf 100644
--- a/modules/religions-generator.js
+++ b/modules/religions-generator.js
@@ -712,9 +712,9 @@ window.Religions = (function () {
const religionsMap = new Map(religions.map(r => [r.i, r]));
- const isMainRoad = cellId => cells.road[cellId] - cells.crossroad[cellId] > 4;
- const isTrail = cellId => cells.h[cellId] > 19 && cells.road[cellId] - cells.crossroad[cellId] === 1;
- const isSeaRoute = cellId => cells.h[cellId] < 20 && cells.road[cellId];
+ const isMainRoad = cellId => cells.route[cellId] === 1;
+ const isTrail = cellId => cells.route[cellId] === 2;
+ const isSeaRoute = cellId => cells.route[cellId] === 3;
const isWater = cellId => cells.h[cellId] < 20;
while (queue.length) {
diff --git a/modules/routes-generator-old.js b/modules/routes-generator-old.js
new file mode 100644
index 00000000..3019763d
--- /dev/null
+++ b/modules/routes-generator-old.js
@@ -0,0 +1,273 @@
+window.RoutesOld = (function () {
+ const getRoads = function () {
+ TIME && console.time("generateMainRoads");
+ const cells = pack.cells;
+ const burgs = pack.burgs.filter(b => b.i && !b.removed);
+ const capitals = burgs.filter(b => b.capital).sort((a, b) => a.population - b.population);
+
+ if (capitals.length < 2) return []; // not enough capitals to build main roads
+ const paths = []; // array to store path segments
+
+ for (const b of capitals) {
+ const connect = capitals.filter(c => c.feature === b.feature && c !== b);
+ for (const t of connect) {
+ const [from, exit] = findLandPath(b.cell, t.cell, true);
+ const segments = restorePath(b.cell, exit, "main", from);
+ segments.forEach(s => paths.push(s));
+ }
+ }
+
+ cells.i.forEach(i => (cells.s[i] += cells.route[i] / 2)); // add roads to suitability score
+ TIME && console.timeEnd("generateMainRoads");
+ return paths;
+ };
+
+ const getTrails = function () {
+ TIME && console.time("generateTrails");
+ const cells = pack.cells;
+ const burgs = pack.burgs.filter(b => b.i && !b.removed);
+
+ if (burgs.length < 2) return []; // not enough burgs to build trails
+
+ let paths = []; // array to store path segments
+ for (const f of pack.features.filter(f => f.land)) {
+ const isle = burgs.filter(b => b.feature === f.i); // burgs on island
+ if (isle.length < 2) continue;
+
+ isle.forEach(function (b, i) {
+ let path = [];
+ if (!i) {
+ // build trail from the first burg on island
+ // to the farthest one on the same island or the closest road
+ const farthest = d3.scan(
+ isle,
+ (a, c) => (c.y - b.y) ** 2 + (c.x - b.x) ** 2 - ((a.y - b.y) ** 2 + (a.x - b.x) ** 2)
+ );
+ const to = isle[farthest].cell;
+ if (cells.route[to]) return;
+ const [from, exit] = findLandPath(b.cell, to, true);
+ path = restorePath(b.cell, exit, "small", from);
+ } else {
+ // build trail from all other burgs to the closest road on the same island
+ if (cells.route[b.cell]) return;
+ const [from, exit] = findLandPath(b.cell, null, true);
+ if (exit === null) return;
+ path = restorePath(b.cell, exit, "small", from);
+ }
+ if (path) paths = paths.concat(path);
+ });
+ }
+
+ TIME && console.timeEnd("generateTrails");
+ return paths;
+ };
+
+ const getSearoutes = function () {
+ TIME && console.time("generateSearoutes");
+ const {cells, burgs, features} = pack;
+ const allPorts = burgs.filter(b => b.port > 0 && !b.removed);
+
+ if (!allPorts.length) return [];
+
+ const bodies = new Set(allPorts.map(b => b.port)); // water features with ports
+ let paths = []; // array to store path segments
+ const connected = []; // store cell id of connected burgs
+
+ bodies.forEach(f => {
+ const ports = allPorts.filter(b => b.port === f); // all ports on the same feature
+ if (!ports.length) return;
+
+ if (features[f]?.border) addOverseaRoute(f, ports[0]);
+
+ // get inner-map routes
+ for (let s = 0; s < ports.length; s++) {
+ const source = ports[s].cell;
+ if (connected[source]) continue;
+
+ for (let t = s + 1; t < ports.length; t++) {
+ const target = ports[t].cell;
+ if (connected[target]) continue;
+
+ const [from, exit, passable] = findOceanPath(target, source, true);
+ if (!passable) continue;
+
+ const path = restorePath(target, exit, "ocean", from);
+ paths = paths.concat(path);
+
+ connected[source] = 1;
+ connected[target] = 1;
+ }
+ }
+ });
+
+ function addOverseaRoute(f, port) {
+ const {x, y, cell: source} = port;
+ const dist = p => Math.abs(p[0] - x) + Math.abs(p[1] - y);
+ const [x1, y1] = [
+ [0, y],
+ [x, 0],
+ [graphWidth, y],
+ [x, graphHeight]
+ ].sort((a, b) => dist(a) - dist(b))[0];
+ const target = findCell(x1, y1);
+
+ if (cells.f[target] === f && cells.h[target] < 20) {
+ const [from, exit, passable] = findOceanPath(target, source, true);
+
+ if (passable) {
+ const path = restorePath(target, exit, "ocean", from);
+ paths = paths.concat(path);
+ last(path).push([x1, y1]);
+ }
+ }
+ }
+
+ TIME && console.timeEnd("generateSearoutes");
+ return paths;
+ };
+
+ const draw = function (main, small, water) {
+ TIME && console.time("drawRoutes");
+ const {cells, burgs} = pack;
+ const {burg, p} = cells;
+
+ const getBurgCoords = b => [burgs[b].x, burgs[b].y];
+ const getPathPoints = cells => cells.map(i => (Array.isArray(i) ? i : burg[i] ? getBurgCoords(burg[i]) : p[i]));
+ const getPath = segment => round(lineGen(getPathPoints(segment)), 1);
+ const getPathsHTML = (paths, type) =>
+ paths.map((path, i) => ``).join("");
+
+ lineGen.curve(d3.curveCatmullRom.alpha(0.1));
+ roads.html(getPathsHTML(main, "road"));
+ trails.html(getPathsHTML(small, "trail"));
+
+ lineGen.curve(d3.curveBundle.beta(1));
+ searoutes.html(getPathsHTML(water, "searoute"));
+
+ TIME && console.timeEnd("drawRoutes");
+ };
+
+ const regenerate = function () {
+ routes.selectAll("path").remove();
+ pack.cells.route = new Uint16Array(pack.cells.i.length);
+ pack.cells.crossroad = new Uint16Array(pack.cells.i.length);
+ const main = getRoads();
+ const small = getTrails();
+ const water = getSearoutes();
+ draw(main, small, water);
+ };
+
+ return {getRoads, getTrails, getSearoutes, draw, regenerate};
+
+ // Find a land path to a specific cell (exit), to a closest road (toRoad), or to all reachable cells (null, null)
+ function findLandPath(start, exit = null, toRoad = null) {
+ const cells = pack.cells;
+ const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
+ const cost = [],
+ from = [];
+ queue.queue({e: start, p: 0});
+
+ while (queue.length) {
+ const next = queue.dequeue(),
+ n = next.e,
+ p = next.p;
+ if (toRoad && cells.route[n]) return [from, n];
+
+ for (const c of cells.c[n]) {
+ if (cells.h[c] < 20) continue; // ignore water cells
+ const stateChangeCost = cells.state && cells.state[c] !== cells.state[n] ? 400 : 0; // trails tend to lay within the same state
+ const habitability = biomesData.habitability[cells.biome[c]];
+ 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[c] - cells.h[n]) * 10; // routes tend to avoid elevation changes
+ const heightCost = cells.h[c] > 80 ? cells.h[c] : 0; // routes tend to avoid mountainous areas
+ const cellCoast = 10 + stateChangeCost + habitedCost + heightChangeCost + heightCost;
+ const totalCost = p + (cells.route[c] || cells.burg[c] ? cellCoast / 3 : cellCoast);
+
+ if (from[c] || totalCost >= cost[c]) continue;
+ from[c] = n;
+ if (c === exit) return [from, exit];
+ cost[c] = totalCost;
+ queue.queue({e: c, p: totalCost});
+ }
+ }
+ return [from, exit];
+ }
+
+ function restorePath(start, end, type, from) {
+ const cells = pack.cells;
+ const path = []; // to store all segments;
+ let segment = [],
+ current = end,
+ prev = end;
+ const score = type === "main" ? 5 : 1; // to increase road score at cell
+
+ if (type === "ocean" || !cells.route[prev]) segment.push(end);
+ if (!cells.route[prev]) cells.route[prev] = score;
+
+ for (let i = 0, limit = 1000; i < limit; i++) {
+ if (!from[current]) break;
+ current = from[current];
+
+ if (cells.route[current]) {
+ if (segment.length) {
+ segment.push(current);
+ path.push(segment);
+ if (segment[0] !== end) {
+ cells.route[segment[0]] += score;
+ cells.crossroad[segment[0]] += score;
+ }
+ if (current !== start) {
+ cells.route[current] += score;
+ cells.crossroad[current] += score;
+ }
+ }
+ segment = [];
+ prev = current;
+ } else {
+ if (prev) segment.push(prev);
+ prev = null;
+ segment.push(current);
+ }
+
+ cells.route[current] += score;
+ if (current === start) break;
+ }
+
+ if (segment.length > 1) path.push(segment);
+ return path;
+ }
+
+ // find water paths
+ function findOceanPath(start, exit = null, toRoute = null) {
+ const cells = pack.cells,
+ temp = grid.cells.temp;
+ const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
+ const cost = [],
+ from = [];
+ queue.queue({e: start, p: 0});
+
+ while (queue.length) {
+ const next = queue.dequeue(),
+ n = next.e,
+ p = next.p;
+ if (toRoute && n !== start && cells.route[n]) return [from, n, true];
+
+ for (const c of cells.c[n]) {
+ if (c === exit) {
+ from[c] = n;
+ return [from, exit, true];
+ }
+ if (cells.h[c] >= 20) continue; // ignore land cells
+ if (temp[cells.g[c]] <= -5) continue; // ignore cells with term <= -5
+ const dist2 = (cells.p[c][1] - cells.p[n][1]) ** 2 + (cells.p[c][0] - cells.p[n][0]) ** 2;
+ const totalCost = p + (cells.route[c] ? 1 + dist2 / 2 : dist2 + (cells.t[c] ? 1 : 100));
+
+ if (from[c] || totalCost >= cost[c]) continue;
+ (from[c] = n), (cost[c] = totalCost);
+ queue.queue({e: c, p: totalCost});
+ }
+ }
+ return [from, exit, false];
+ }
+})();
diff --git a/modules/routes-generator.js b/modules/routes-generator.js
index e4ec3374..de43511a 100644
--- a/modules/routes-generator.js
+++ b/modules/routes-generator.js
@@ -1,269 +1,320 @@
window.Routes = (function () {
- const getRoads = function () {
- TIME && console.time("generateMainRoads");
- const cells = pack.cells;
- const burgs = pack.burgs.filter(b => b.i && !b.removed);
- const capitals = burgs.filter(b => b.capital).sort((a, b) => a.population - b.population);
-
- if (capitals.length < 2) return []; // not enough capitals to build main roads
- const paths = []; // array to store path segments
-
- for (const b of capitals) {
- const connect = capitals.filter(c => c.feature === b.feature && c !== b);
- for (const t of connect) {
- const [from, exit] = findLandPath(b.cell, t.cell, true);
- const segments = restorePath(b.cell, exit, "main", from);
- segments.forEach(s => paths.push(s));
- }
- }
-
- cells.i.forEach(i => (cells.s[i] += cells.road[i] / 2)); // add roads to suitability score
- TIME && console.timeEnd("generateMainRoads");
- return paths;
+ const ROUTES = {
+ MAIN_ROAD: 1,
+ TRAIL: 2,
+ SEA_ROUTE: 3
};
- const getTrails = function () {
- TIME && console.time("generateTrails");
- const cells = pack.cells;
- const burgs = pack.burgs.filter(b => b.i && !b.removed);
-
- if (burgs.length < 2) return []; // not enough burgs to build trails
-
- let paths = []; // array to store path segments
- for (const f of pack.features.filter(f => f.land)) {
- const isle = burgs.filter(b => b.feature === f.i); // burgs on island
- if (isle.length < 2) continue;
-
- isle.forEach(function (b, i) {
- let path = [];
- if (!i) {
- // build trail from the first burg on island
- // to the farthest one on the same island or the closest road
- const farthest = d3.scan(isle, (a, c) => (c.y - b.y) ** 2 + (c.x - b.x) ** 2 - ((a.y - b.y) ** 2 + (a.x - b.x) ** 2));
- const to = isle[farthest].cell;
- if (cells.road[to]) return;
- const [from, exit] = findLandPath(b.cell, to, true);
- path = restorePath(b.cell, exit, "small", from);
- } else {
- // build trail from all other burgs to the closest road on the same island
- if (cells.road[b.cell]) return;
- const [from, exit] = findLandPath(b.cell, null, true);
- if (exit === null) return;
- path = restorePath(b.cell, exit, "small", from);
- }
- if (path) paths = paths.concat(path);
- });
- }
-
- TIME && console.timeEnd("generateTrails");
- return paths;
- };
-
- const getSearoutes = function () {
- TIME && console.time("generateSearoutes");
- const {cells, burgs, features} = pack;
- const allPorts = burgs.filter(b => b.port > 0 && !b.removed);
-
- if (!allPorts.length) return [];
-
- const bodies = new Set(allPorts.map(b => b.port)); // water features with ports
- let paths = []; // array to store path segments
- const connected = []; // store cell id of connected burgs
-
- bodies.forEach(f => {
- const ports = allPorts.filter(b => b.port === f); // all ports on the same feature
- if (!ports.length) return;
-
- if (features[f]?.border) addOverseaRoute(f, ports[0]);
-
- // get inner-map routes
- for (let s = 0; s < ports.length; s++) {
- const source = ports[s].cell;
- if (connected[source]) continue;
-
- for (let t = s + 1; t < ports.length; t++) {
- const target = ports[t].cell;
- if (connected[target]) continue;
-
- const [from, exit, passable] = findOceanPath(target, source, true);
- if (!passable) continue;
-
- const path = restorePath(target, exit, "ocean", from);
- paths = paths.concat(path);
-
- connected[source] = 1;
- connected[target] = 1;
- }
- }
- });
-
- function addOverseaRoute(f, port) {
- const {x, y, cell: source} = port;
- const dist = p => Math.abs(p[0] - x) + Math.abs(p[1] - y);
- const [x1, y1] = [
- [0, y],
- [x, 0],
- [graphWidth, y],
- [x, graphHeight]
- ].sort((a, b) => dist(a) - dist(b))[0];
- const target = findCell(x1, y1);
-
- if (cells.f[target] === f && cells.h[target] < 20) {
- const [from, exit, passable] = findOceanPath(target, source, true);
-
- if (passable) {
- const path = restorePath(target, exit, "ocean", from);
- paths = paths.concat(path);
- last(path).push([x1, y1]);
- }
- }
- }
-
- TIME && console.timeEnd("generateSearoutes");
- return paths;
- };
-
- const draw = function (main, small, water) {
- TIME && console.time("drawRoutes");
+ function generate() {
const {cells, burgs} = pack;
- const {burg, p} = cells;
- const getBurgCoords = b => [burgs[b].x, burgs[b].y];
- const getPathPoints = cells => cells.map(i => (Array.isArray(i) ? i : burg[i] ? getBurgCoords(burg[i]) : p[i]));
- const getPath = segment => round(lineGen(getPathPoints(segment)), 1);
- const getPathsHTML = (paths, type) => paths.map((path, i) => ``).join("");
+ const cellRoutes = new Uint8Array(cells.h.length);
- lineGen.curve(d3.curveCatmullRom.alpha(0.1));
- roads.html(getPathsHTML(main, "road"));
- trails.html(getPathsHTML(small, "trail"));
+ const {capitalsByFeature, burgsByFeature, portsByFeature} = sortBurgsByFeature(burgs);
+ const connections = new Map();
- lineGen.curve(d3.curveBundle.beta(1));
- searoutes.html(getPathsHTML(water, "searoute"));
+ const mainRoads = generateMainRoads();
+ const trails = generateTrails();
+ const seaRoutes = generateSeaRoutes();
- TIME && console.timeEnd("drawRoutes");
- };
+ cells.route = cellRoutes;
+ pack.routes = combineRoutes();
- const regenerate = function () {
- routes.selectAll("path").remove();
- pack.cells.road = new Uint16Array(pack.cells.i.length);
- pack.cells.crossroad = new Uint16Array(pack.cells.i.length);
- const main = getRoads();
- const small = getTrails();
- const water = getSearoutes();
- draw(main, small, water);
- };
+ function sortBurgsByFeature(burgs) {
+ const burgsByFeature = {};
+ const capitalsByFeature = {};
+ const portsByFeature = {};
- return {getRoads, getTrails, getSearoutes, draw, regenerate};
+ const addBurg = (object, feature, burg) => {
+ if (!object[feature]) object[feature] = [];
+ object[feature].push(burg);
+ };
- // Find a land path to a specific cell (exit), to a closest road (toRoad), or to all reachable cells (null, null)
- function findLandPath(start, exit = null, toRoad = null) {
- const cells = pack.cells;
- const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
- const cost = [],
- from = [];
- queue.queue({e: start, p: 0});
+ for (const burg of burgs) {
+ if (burg.i && !burg.removed) {
+ const {feature, capital, port} = burg;
+ addBurg(burgsByFeature, feature, burg);
+ if (capital) addBurg(capitalsByFeature, feature, burg);
+ if (port) addBurg(portsByFeature, port, burg);
+ }
+ }
- while (queue.length) {
- const next = queue.dequeue(),
- n = next.e,
- p = next.p;
- if (toRoad && cells.road[n]) return [from, n];
+ return {burgsByFeature, capitalsByFeature, portsByFeature};
+ }
- for (const c of cells.c[n]) {
- if (cells.h[c] < 20) continue; // ignore water cells
- const stateChangeCost = cells.state && cells.state[c] !== cells.state[n] ? 400 : 0; // trails tend to lay within the same state
- const habitability = biomesData.habitability[cells.biome[c]];
- 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[c] - cells.h[n]) * 10; // routes tend to avoid elevation changes
- const heightCost = cells.h[c] > 80 ? cells.h[c] : 0; // routes tend to avoid mountainous areas
- const cellCoast = 10 + stateChangeCost + habitedCost + heightChangeCost + heightCost;
- const totalCost = p + (cells.road[c] || cells.burg[c] ? cellCoast / 3 : cellCoast);
+ function generateMainRoads() {
+ TIME && console.time("generateMainRoads");
+ const mainRoads = [];
- if (from[c] || totalCost >= cost[c]) continue;
- from[c] = n;
- if (c === exit) return [from, exit];
- cost[c] = totalCost;
- queue.queue({e: c, p: totalCost});
+ for (const [key, featureCapitals] of Object.entries(capitalsByFeature)) {
+ const points = featureCapitals.map(burg => [burg.x, burg.y]);
+ const urquhartEdges = calculateUrquhartEdges(points);
+ urquhartEdges.forEach(([fromId, toId]) => {
+ const start = featureCapitals[fromId].cell;
+ const exit = featureCapitals[toId].cell;
+
+ const segments = findPathSegments({isWater: false, cellRoutes, connections, start, exit});
+ for (const segment of segments) {
+ addConnections(segment, ROUTES.MAIN_ROAD);
+ mainRoads.push({feature: Number(key), cells: segment});
+ }
+ });
+ }
+
+ TIME && console.timeEnd("generateMainRoads");
+ return mainRoads;
+ }
+
+ function generateTrails() {
+ TIME && console.time("generateTrails");
+
+ const trails = [];
+
+ for (const [key, featureBurgs] of Object.entries(burgsByFeature)) {
+ const points = featureBurgs.map(burg => [burg.x, burg.y]);
+ const urquhartEdges = calculateUrquhartEdges(points);
+ urquhartEdges.forEach(([fromId, toId]) => {
+ const start = featureBurgs[fromId].cell;
+ const exit = featureBurgs[toId].cell;
+
+ const segments = findPathSegments({isWater: false, cellRoutes, connections, start, exit});
+ for (const segment of segments) {
+ addConnections(segment, ROUTES.TRAIL);
+ trails.push({feature: Number(key), cells: segment});
+ }
+ });
+ }
+
+ TIME && console.timeEnd("generateTrails");
+ return trails;
+ }
+
+ function generateSeaRoutes() {
+ TIME && console.time("generateSearoutes");
+ const mainRoads = [];
+
+ for (const [key, featurePorts] of Object.entries(portsByFeature)) {
+ const points = featurePorts.map(burg => [burg.x, burg.y]);
+ const urquhartEdges = calculateUrquhartEdges(points);
+ urquhartEdges.forEach(([fromId, toId]) => {
+ const start = featurePorts[fromId].cell;
+ const exit = featurePorts[toId].cell;
+
+ const segments = findPathSegments({isWater: true, cellRoutes, connections, start, exit});
+ for (const segment of segments) {
+ addConnections(segment, ROUTES.SEA_ROUTE);
+ mainRoads.push({feature: Number(key), cells: segment});
+ }
+ });
+ }
+
+ TIME && console.timeEnd("generateSearoutes");
+ return mainRoads;
+ }
+
+ function addConnections(segment, roadTypeId) {
+ for (let i = 0; i < segment.length; i++) {
+ const cellId = segment[i];
+ const nextCellId = segment[i + 1];
+ if (nextCellId) connections.set(`${cellId}-${nextCellId}`, true);
+ if (!cellRoutes[cellId]) cellRoutes[cellId] = roadTypeId;
}
}
- return [from, exit];
+
+ function findPathSegments({isWater, cellRoutes, connections, start, exit}) {
+ const from = findPath(isWater, cellRoutes, start, exit, connections);
+ if (!from) return [];
+
+ const pathCells = restorePath(start, exit, from);
+ const segments = getRouteSegments(pathCells, connections);
+ return segments;
+ }
+
+ function combineRoutes() {
+ const routes = [];
+
+ for (const {feature, cells} of mainRoads) {
+ routes.push({i: routes.length, type: "road", feature, cells});
+ }
+
+ for (const {feature, cells} of trails) {
+ routes.push({i: routes.length, type: "trail", feature, cells});
+ }
+
+ for (const {feature, cells} of seaRoutes) {
+ routes.push({i: routes.length, type: "sea", feature, cells});
+ }
+
+ return routes;
+ }
}
- function restorePath(start, end, type, from) {
- const cells = pack.cells;
- const path = []; // to store all segments;
- let segment = [],
- current = end,
- prev = end;
- const score = type === "main" ? 5 : 1; // to increase road score at cell
+ function findPath(isWater, cellRoutes, start, exit, connections) {
+ const {temp} = grid.cells;
+ const {cells} = pack;
- if (type === "ocean" || !cells.road[prev]) segment.push(end);
- if (!cells.road[prev]) cells.road[prev] = score;
+ const from = [];
+ const cost = [];
+ const queue = new FlatQueue();
+ queue.push(start, 0);
- for (let i = 0, limit = 1000; i < limit; i++) {
- if (!from[current]) break;
- current = from[current];
+ return isWater ? findWaterPath() : findLandPath();
- if (cells.road[current]) {
+ function findLandPath() {
+ while (queue.length) {
+ const priority = queue.peekValue();
+ const next = queue.pop();
+
+ for (const neibCellId of cells.c[next]) {
+ if (cells.h[neibCellId] < 20) continue; // ignore water cells
+
+ const habitability = biomesData.habitability[cells.biome[neibCellId]];
+ if (!habitability) continue; // inhabitable cells are not passable (eg. lava, glacier)
+
+ const distanceCost = dist2(cells.p[next], cells.p[neibCellId]);
+
+ const habitabilityModifier = 1 + Math.max(100 - habitability, 0) / 1000; // [1, 1.1];
+ const heightModifier = 1 + Math.max(cells.h[neibCellId] - 50, 0) / 50; // [1, 2];
+ const roadModifier = cellRoutes[neibCellId] ? 1 : 2;
+ const burgModifier = cells.burg[neibCellId] ? 1 : 2;
+
+ const cellsCost = distanceCost * habitabilityModifier * heightModifier * roadModifier * burgModifier;
+ const totalCost = priority + cellsCost;
+
+ if (from[neibCellId] || totalCost >= cost[neibCellId]) continue;
+ from[neibCellId] = next;
+
+ if (neibCellId === exit) return from;
+
+ cost[neibCellId] = totalCost;
+ queue.push(neibCellId, totalCost);
+ }
+ }
+
+ return null; // path is not found
+ }
+
+ function findWaterPath() {
+ const MIN_PASSABLE_TEMP = -4;
+
+ while (queue.length) {
+ const priority = queue.peekValue();
+ const next = queue.pop();
+
+ for (const neibCellId of cells.c[next]) {
+ if (neibCellId === exit) {
+ from[neibCellId] = next;
+ return from;
+ }
+
+ if (cells.h[neibCellId] >= 20) continue; // ignore land cells
+ if (temp[cells.g[neibCellId]] < MIN_PASSABLE_TEMP) continue; // ignore to cold cells
+
+ const distanceCost = dist2(cells.p[next], cells.p[neibCellId]);
+ const typeModifier = Math.abs(cells.t[neibCellId]); // 1 for coastline, 2 for deep ocean, 3 for deeper ocean
+ const routeModifier = cellRoutes[neibCellId] ? 1 : 2;
+ const connectionModifier =
+ connections.has(`${next}-${neibCellId}`) || connections.has(`${neibCellId}-${next}`) ? 1 : 3;
+
+ const cellsCost = distanceCost * typeModifier * routeModifier * connectionModifier;
+ const totalCost = priority + cellsCost;
+
+ if (from[neibCellId] || totalCost >= cost[neibCellId]) continue;
+ from[neibCellId] = next;
+
+ cost[neibCellId] = totalCost;
+ queue.push(neibCellId, totalCost);
+ }
+ }
+
+ return null; // path is not found
+ }
+ }
+
+ function restorePath(start, end, from) {
+ const cells = [];
+
+ let current = end;
+ let prev = end;
+
+ while (current !== start) {
+ cells.push(current);
+ prev = from[current];
+ current = prev;
+ }
+
+ cells.push(current);
+
+ return cells;
+ }
+
+ function getRouteSegments(pathCells, connections) {
+ const segments = [];
+ let segment = [];
+
+ for (let i = 0; i < pathCells.length; i++) {
+ const cellId = pathCells[i];
+ const nextCellId = pathCells[i + 1];
+ const isConnected = connections.has(`${cellId}-${nextCellId}`) || connections.has(`${nextCellId}-${cellId}`);
+
+ if (isConnected) {
if (segment.length) {
- segment.push(current);
- path.push(segment);
- if (segment[0] !== end) {
- cells.road[segment[0]] += score;
- cells.crossroad[segment[0]] += score;
- }
- if (current !== start) {
- cells.road[current] += score;
- cells.crossroad[current] += score;
- }
+ // segment stepped into existing segment
+ segment.push(pathCells[i]);
+ segments.push(segment);
+ segment = [];
}
- segment = [];
- prev = current;
- } else {
- if (prev) segment.push(prev);
- prev = null;
- segment.push(current);
+ continue;
}
- cells.road[current] += score;
- if (current === start) break;
+ segment.push(pathCells[i]);
}
- if (segment.length > 1) path.push(segment);
- return path;
+ if (segment.length > 1) segments.push(segment);
+
+ return segments;
}
- // find water paths
- function findOceanPath(start, exit = null, toRoute = null) {
- const cells = pack.cells,
- temp = grid.cells.temp;
- const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
- const cost = [],
- from = [];
- queue.queue({e: start, p: 0});
+ // Urquhart graph is obtained by removing the longest edge from each triangle in the Delaunay triangulation
+ // this gives us an aproximation of a desired road network, i.e. connections between burgs
+ // code from https://observablehq.com/@mbostock/urquhart-graph
+ function calculateUrquhartEdges(points) {
+ const score = (p0, p1) => dist2(points[p0], points[p1]);
- while (queue.length) {
- const next = queue.dequeue(),
- n = next.e,
- p = next.p;
- if (toRoute && n !== start && cells.road[n]) return [from, n, true];
+ const {halfedges, triangles} = Delaunator.from(points);
+ const n = triangles.length;
- for (const c of cells.c[n]) {
- if (c === exit) {
- from[c] = n;
- return [from, exit, true];
- }
- if (cells.h[c] >= 20) continue; // ignore land cells
- if (temp[cells.g[c]] <= -5) continue; // ignore cells with term <= -5
- const dist2 = (cells.p[c][1] - cells.p[n][1]) ** 2 + (cells.p[c][0] - cells.p[n][0]) ** 2;
- const totalCost = p + (cells.road[c] ? 1 + dist2 / 2 : dist2 + (cells.t[c] ? 1 : 100));
+ const removed = new Uint8Array(n);
+ const edges = [];
- if (from[c] || totalCost >= cost[c]) continue;
- (from[c] = n), (cost[c] = totalCost);
- queue.queue({e: c, p: totalCost});
+ for (let e = 0; e < n; e += 3) {
+ const p0 = triangles[e],
+ p1 = triangles[e + 1],
+ p2 = triangles[e + 2];
+
+ const p01 = score(p0, p1),
+ p12 = score(p1, p2),
+ p20 = score(p2, p0);
+
+ removed[
+ p20 > p01 && p20 > p12
+ ? Math.max(e + 2, halfedges[e + 2])
+ : p12 > p01 && p12 > p20
+ ? Math.max(e + 1, halfedges[e + 1])
+ : Math.max(e, halfedges[e])
+ ] = 1;
+ }
+
+ for (let e = 0; e < n; ++e) {
+ if (e > halfedges[e] && !removed[e]) {
+ const t0 = triangles[e];
+ const t1 = triangles[e % 3 === 2 ? e - 2 : e + 1];
+ edges.push([t0, t1]);
}
}
- return [from, exit, false];
+
+ return edges;
}
+
+ return {generate};
})();
diff --git a/modules/submap.js b/modules/submap.js
index 549a7a6f..6dfa6049 100644
--- a/modules/submap.js
+++ b/modules/submap.js
@@ -145,8 +145,7 @@ window.Submap = (function () {
cells.state = new Uint16Array(pn);
cells.burg = new Uint16Array(pn);
cells.religion = new Uint16Array(pn);
- cells.road = new Uint16Array(pn);
- cells.crossroad = new Uint16Array(pn);
+ cells.route = new Uint8Array(pn);
cells.province = new Uint16Array(pn);
stage("Resampling culture, state and religion map.");
@@ -272,7 +271,7 @@ window.Submap = (function () {
BurgsAndStates.drawBurgs();
- stage("Regenerating road network.");
+ stage("Regenerating routes network.");
Routes.regenerate();
drawStates();
diff --git a/modules/ui/editors.js b/modules/ui/editors.js
index 58c1c18b..b304ce81 100644
--- a/modules/ui/editors.js
+++ b/modules/ui/editors.js
@@ -143,7 +143,7 @@ function addBurg(point) {
const feature = cells.f[cell];
const temple = pack.states[state].form === "Theocracy";
- const population = Math.max((cells.s[cell] + cells.road[cell]) / 3 + i / 1000 + (cell % 100) / 1000, 0.1);
+ const population = Math.max(cells.s[cell] / 3 + i / 1000 + (cell % 100) / 1000, 0.1);
const type = BurgsAndStates.getType(cell, false);
// generate emblem
@@ -326,7 +326,7 @@ function createMfcgLink(burg) {
const citadel = +burg.citadel;
const urban_castle = +(citadel && each(2)(i));
- const hub = +cells.road[cell] > 50;
+ const hub = +cells.route[cell] === 1;
const walls = +burg.walls;
const plaza = +burg.plaza;
@@ -371,7 +371,7 @@ function createVillageGeneratorLink(burg) {
else if (cells.r[cell]) tags.push("river");
else if (pop < 200 && each(4)(cell)) tags.push("pond");
- const roadsAround = cells.c[cell].filter(c => cells.h[c] >= 20 && cells.road[c]).length;
+ const roadsAround = cells.c[cell].filter(c => cells.h[c] >= 20 && cells.route[c]).length;
if (roadsAround > 1) tags.push("highway");
else if (roadsAround === 1) tags.push("dead end");
else tags.push("isolated");
diff --git a/modules/ui/heightmap-editor.js b/modules/ui/heightmap-editor.js
index 40bc55fb..3a89fe89 100644
--- a/modules/ui/heightmap-editor.js
+++ b/modules/ui/heightmap-editor.js
@@ -281,8 +281,7 @@ function editHeightmap(options) {
const l = grid.cells.i.length;
const biome = new Uint8Array(l);
const pop = new Uint16Array(l);
- const road = new Uint16Array(l);
- const crossroad = new Uint16Array(l);
+ const route = new Uint8Array(l);
const s = new Uint16Array(l);
const burg = new Uint16Array(l);
const state = new Uint16Array(l);
@@ -300,8 +299,7 @@ function editHeightmap(options) {
biome[g] = pack.cells.biome[i];
culture[g] = pack.cells.culture[i];
pop[g] = pack.cells.pop[i];
- road[g] = pack.cells.road[i];
- crossroad[g] = pack.cells.crossroad[i];
+ route[g] = pack.cells.route[i];
s[g] = pack.cells.s[i];
state[g] = pack.cells.state[i];
province[g] = pack.cells.province[i];
@@ -353,8 +351,7 @@ function editHeightmap(options) {
// assign saved pack data from grid back to pack
const n = pack.cells.i.length;
pack.cells.pop = new Float32Array(n);
- pack.cells.road = new Uint16Array(n);
- pack.cells.crossroad = new Uint16Array(n);
+ pack.cells.route = new Uint8Array(n);
pack.cells.s = new Uint16Array(n);
pack.cells.burg = new Uint16Array(n);
pack.cells.state = new Uint16Array(n);
@@ -389,8 +386,7 @@ function editHeightmap(options) {
if (!isLand) continue;
pack.cells.culture[i] = culture[g];
pack.cells.pop[i] = pop[g];
- pack.cells.road[i] = road[g];
- pack.cells.crossroad[i] = crossroad[g];
+ pack.cells.route[i] = route[g];
pack.cells.s[i] = s[g];
pack.cells.state[i] = state[g];
pack.cells.province[i] = province[g];
diff --git a/modules/ui/measurers.js b/modules/ui/measurers.js
index 8120fff1..d2d01c19 100644
--- a/modules/ui/measurers.js
+++ b/modules/ui/measurers.js
@@ -486,9 +486,7 @@ class RouteOpisometer extends Measurer {
const cells = pack.cells;
const c = findCell(mousePoint[0], mousePoint[1]);
- if (!cells.road[c] && !d3.event.sourceEvent.shiftKey) {
- return;
- }
+ if (!cells.route[c] && !d3.event.sourceEvent.shiftKey) return;
context.trackCell(c, rigth);
});
diff --git a/modules/ui/tools.js b/modules/ui/tools.js
index 4d62305c..d743246e 100644
--- a/modules/ui/tools.js
+++ b/modules/ui/tools.js
@@ -129,7 +129,7 @@ function recalculatePopulation() {
if (!b.i || b.removed || b.lock) return;
const i = b.cell;
- b.population = rn(Math.max((pack.cells.s[i] + pack.cells.road[i] / 2) / 8 + b.i / 1000 + (i % 100) / 1000, 0.1), 3);
+ b.population = rn(Math.max(pack.cells.s[i] / 8 + b.i / 1000 + (i % 100) / 1000, 0.1), 3);
if (b.capital) b.population = b.population * 1.3; // increase capital population
if (b.port) b.population = b.population * 1.3; // increase port population
b.population = rn(b.population * gauss(2, 3, 0.6, 20, 3), 3);
diff --git a/modules/ui/units-editor.js b/modules/ui/units-editor.js
index 97a3573f..6f0332bf 100644
--- a/modules/ui/units-editor.js
+++ b/modules/ui/units-editor.js
@@ -185,7 +185,7 @@ function editUnits() {
const burgs = pack.burgs;
const point = d3.mouse(this);
const c = findCell(point[0], point[1]);
- if (cells.road[c] || d3.event.sourceEvent.shiftKey) {
+ if (cells.route[c] || d3.event.sourceEvent.shiftKey) {
const b = cells.burg[c];
const x = b ? burgs[b].x : cells.p[c][0];
const y = b ? burgs[b].y : cells.p[c][1];
@@ -194,7 +194,7 @@ function editUnits() {
d3.event.on("drag", function () {
const point = d3.mouse(this);
const c = findCell(point[0], point[1]);
- if (cells.road[c] || d3.event.sourceEvent.shiftKey) {
+ if (cells.route[c] || d3.event.sourceEvent.shiftKey) {
routeOpisometer.trackCell(c, true);
}
});
diff --git a/utils/functionUtils.js b/utils/functionUtils.js
index 845673a8..83813cdb 100644
--- a/utils/functionUtils.js
+++ b/utils/functionUtils.js
@@ -1,7 +1,9 @@
+"use strict";
+// FMG helper functions
+
// extracted d3 code to bypass version conflicts
// https://github.com/d3/d3-array/blob/main/src/group.js
-
-export function rollups(values, reduce, ...keys) {
+function rollups(values, reduce, ...keys) {
return nest(values, Array.from, reduce, keys);
}
@@ -23,3 +25,7 @@ function nest(values, map, reduce, keys) {
return map(groups);
})(values, 0);
}
+
+function dist2([x1, y1], [x2, y2]) {
+ return (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2);
+}
diff --git a/versioning.js b/versioning.js
index 1574e729..bf275623 100644
--- a/versioning.js
+++ b/versioning.js
@@ -1,7 +1,7 @@
"use strict";
// version and caching control
-const version = "1.97.04"; // generator version, update each time
+const version = "1.98.00"; // generator version, update each time
{
document.title += " v" + version;
@@ -28,6 +28,7 @@ const version = "1.97.04"; // generator version, update each time
Latest changes:
+ - New routes generatation algorithm
- Preview villages map
- Ability to render ocean heightmap
- Scale bar styling features
@@ -40,9 +41,6 @@ const version = "1.97.04"; // generator version, update each time
- North and South Poles temperature can be set independently
- More than 70 new heraldic charges
- Multi-color heraldic charges support
- - New 3D scene options and improvements
- - Autosave feature (in Options)
- - Google translation support (in Options)
Join our Discord server and Reddit community to ask questions, share maps, discuss the Generator and Worlbuilding, report bugs and propose new features.