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 efc115cf5..7e740baae 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 f4f6463a8..c9408c62c 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 c8110b80b..1c403bd18 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 35a83b5ea..171778a45 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 ff6b27312..239507a44 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 efa1e89a0..45bf6018d 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 1ba6a365e..585a91538 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 88857a011..238fcddfe 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 000000000..3019763d5
--- /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 e4ec33748..de43511aa 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));
+ const ROUTES = {
+ MAIN_ROAD: 1,
+ TRAIL: 2,
+ SEA_ROUTE: 3
+ };
+
+ function generate() {
+ const {cells, burgs} = pack;
+
+ const cellRoutes = new Uint8Array(cells.h.length);
+
+ const {capitalsByFeature, burgsByFeature, portsByFeature} = sortBurgsByFeature(burgs);
+ const connections = new Map();
+
+ const mainRoads = generateMainRoads();
+ const trails = generateTrails();
+ const seaRoutes = generateSeaRoutes();
+
+ cells.route = cellRoutes;
+ pack.routes = combineRoutes();
+
+ function sortBurgsByFeature(burgs) {
+ const burgsByFeature = {};
+ const capitalsByFeature = {};
+ const portsByFeature = {};
+
+ const addBurg = (object, feature, burg) => {
+ if (!object[feature]) object[feature] = [];
+ object[feature].push(burg);
+ };
+
+ 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);
+ }
}
+
+ return {burgsByFeature, capitalsByFeature, portsByFeature};
}
- cells.i.forEach(i => (cells.s[i] += cells.road[i] / 2)); // add roads to suitability score
- TIME && console.timeEnd("generateMainRoads");
- return paths;
- };
+ function generateMainRoads() {
+ TIME && console.time("generateMainRoads");
+ const mainRoads = [];
+
+ 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});
+ }
+ });
+ }
- 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("generateMainRoads");
+ return mainRoads;
}
- TIME && console.timeEnd("generateTrails");
- return paths;
- };
+ function generateTrails() {
+ TIME && console.time("generateTrails");
- const getSearoutes = function () {
- TIME && console.time("generateSearoutes");
- const {cells, burgs, features} = pack;
- const allPorts = burgs.filter(b => b.port > 0 && !b.removed);
+ const trails = [];
- if (!allPorts.length) return [];
+ 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 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
+ 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});
+ }
+ });
+ }
- bodies.forEach(f => {
- const ports = allPorts.filter(b => b.port === f); // all ports on the same feature
- if (!ports.length) return;
+ TIME && console.timeEnd("generateTrails");
+ return trails;
+ }
- if (features[f]?.border) addOverseaRoute(f, ports[0]);
+ 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});
+ }
+ });
+ }
- // get inner-map routes
- for (let s = 0; s < ports.length; s++) {
- const source = ports[s].cell;
- if (connected[source]) continue;
+ TIME && console.timeEnd("generateSearoutes");
+ return mainRoads;
+ }
- for (let t = s + 1; t < ports.length; t++) {
- const target = ports[t].cell;
- if (connected[target]) continue;
+ 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;
+ }
+ }
- const [from, exit, passable] = findOceanPath(target, source, true);
- if (!passable) continue;
+ function findPathSegments({isWater, cellRoutes, connections, start, exit}) {
+ const from = findPath(isWater, cellRoutes, start, exit, connections);
+ if (!from) return [];
- const path = restorePath(target, exit, "ocean", from);
- paths = paths.concat(path);
+ const pathCells = restorePath(start, exit, from);
+ const segments = getRouteSegments(pathCells, connections);
+ return segments;
+ }
- connected[source] = 1;
- connected[target] = 1;
- }
+ 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});
}
- });
-
- 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]);
+
+ return routes;
+ }
+ }
+
+ function findPath(isWater, cellRoutes, start, exit, connections) {
+ const {temp} = grid.cells;
+ const {cells} = pack;
+
+ const from = [];
+ const cost = [];
+ const queue = new FlatQueue();
+ queue.push(start, 0);
+
+ return isWater ? findWaterPath() : findLandPath();
+
+ 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
}
- TIME && console.timeEnd("generateSearoutes");
- return paths;
- };
+ function findWaterPath() {
+ const MIN_PASSABLE_TEMP = -4;
- const draw = function (main, small, water) {
- TIME && console.time("drawRoutes");
- const {cells, burgs} = pack;
- const {burg, p} = cells;
+ while (queue.length) {
+ const priority = queue.peekValue();
+ const next = queue.pop();
- 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("");
+ for (const neibCellId of cells.c[next]) {
+ if (neibCellId === exit) {
+ from[neibCellId] = next;
+ return from;
+ }
- lineGen.curve(d3.curveCatmullRom.alpha(0.1));
- roads.html(getPathsHTML(main, "road"));
- trails.html(getPathsHTML(small, "trail"));
+ if (cells.h[neibCellId] >= 20) continue; // ignore land cells
+ if (temp[cells.g[neibCellId]] < MIN_PASSABLE_TEMP) continue; // ignore to cold cells
- lineGen.curve(d3.curveBundle.beta(1));
- searoutes.html(getPathsHTML(water, "searoute"));
+ 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;
- TIME && console.timeEnd("drawRoutes");
- };
+ const cellsCost = distanceCost * typeModifier * routeModifier * connectionModifier;
+ const totalCost = priority + cellsCost;
- 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);
- };
+ if (from[neibCellId] || totalCost >= cost[neibCellId]) continue;
+ from[neibCellId] = next;
- 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.road[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.road[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});
+ cost[neibCellId] = totalCost;
+ queue.push(neibCellId, totalCost);
+ }
}
+
+ return null; // path is not found
}
- 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
+ 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;
+ }
- if (type === "ocean" || !cells.road[prev]) segment.push(end);
- if (!cells.road[prev]) cells.road[prev] = score;
+ function getRouteSegments(pathCells, connections) {
+ const segments = [];
+ let segment = [];
- for (let i = 0, limit = 1000; i < limit; i++) {
- if (!from[current]) break;
- current = from[current];
+ 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 (cells.road[current]) {
+ 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});
-
- while (queue.length) {
- const next = queue.dequeue(),
- n = next.e,
- p = next.p;
- if (toRoute && n !== start && cells.road[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.road[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});
+ // 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]);
+
+ const {halfedges, triangles} = Delaunator.from(points);
+ const n = triangles.length;
+
+ const removed = new Uint8Array(n);
+ const edges = [];
+
+ 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 549a7a6fc..6dfa6049e 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 58c1c18bf..b304ce819 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 40bc55fb3..3a89fe891 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 8120fff13..d2d01c198 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 4d62305c9..d743246e5 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 97a3573ff..6f0332bf8 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 845673a8d..83813cdb3 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 1574e7291..bf275623a 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.
From b5a34afe93c0646baebbab833fe099fc5c5bf4e2 Mon Sep 17 00:00:00 2001
From: Azgaar
Date: Sun, 11 Aug 2024 22:11:55 +0200
Subject: [PATCH 30/37] chore - remove merge conflict markers
---
versioning.js | 4 ----
1 file changed, 4 deletions(-)
diff --git a/versioning.js b/versioning.js
index 487a0c923..9a22c7401 100644
--- a/versioning.js
+++ b/versioning.js
@@ -42,10 +42,6 @@ const version = "1.99.00"; // 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
-<<<<<<< HEAD
-=======
-
New 3D scene options and improvements
->>>>>>> 00abd5213b446922a60e2053eaca711a6d4067e2
Join our Discord server and Reddit community to ask questions, share maps, discuss the Generator and Worlbuilding, report bugs and propose new features.