mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-22 03:51:23 +01:00
feat: routes generation
This commit is contained in:
parent
af927ed345
commit
6776e5b867
19 changed files with 607 additions and 295 deletions
|
|
@ -6146,7 +6146,7 @@
|
||||||
Data to be copied: heightmap, biomes, religions, population, precipitation, cultures, states, provinces,
|
Data to be copied: heightmap, biomes, religions, population, precipitation, cultures, states, provinces,
|
||||||
military regiments
|
military regiments
|
||||||
</p>
|
</p>
|
||||||
<p>Data to be regenerated: zones, roads, rivers</p>
|
<p>Data to be regenerated: zones, routes, rivers</p>
|
||||||
<p>Burgs may be remapped incorrectly, manual change is required</p>
|
<p>Burgs may be remapped incorrectly, manual change is required</p>
|
||||||
|
|
||||||
<p>Keep data for:</p>
|
<p>Keep data for:</p>
|
||||||
|
|
@ -8009,10 +8009,12 @@
|
||||||
<script src="libs/priority-queue.min.js"></script>
|
<script src="libs/priority-queue.min.js"></script>
|
||||||
<script src="libs/delaunator.min.js"></script>
|
<script src="libs/delaunator.min.js"></script>
|
||||||
<script src="libs/indexedDB.js?v=1.91.01"></script>
|
<script src="libs/indexedDB.js?v=1.91.01"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/flatqueue"></script>
|
||||||
|
|
||||||
<script src="utils/shorthands.js"></script>
|
<script src="utils/shorthands.js"></script>
|
||||||
<script src="utils/commonUtils.js?v=1.89.29"></script>
|
<script src="utils/commonUtils.js?v=1.89.29"></script>
|
||||||
<script src="utils/arrayUtils.js"></script>
|
<script src="utils/arrayUtils.js"></script>
|
||||||
|
<script src="utils/functionUtils.js"></script>
|
||||||
<script src="utils/colorUtils.js"></script>
|
<script src="utils/colorUtils.js"></script>
|
||||||
<script src="utils/graphUtils.js?v=1.96.00"></script>
|
<script src="utils/graphUtils.js?v=1.96.00"></script>
|
||||||
<script src="utils/nodeUtils.js"></script>
|
<script src="utils/nodeUtils.js"></script>
|
||||||
|
|
@ -8035,7 +8037,7 @@
|
||||||
<script src="modules/cultures-generator.js?v=1.96.05"></script>
|
<script src="modules/cultures-generator.js?v=1.96.05"></script>
|
||||||
<script src="modules/renderers/state-labels.js?v=1.96.04"></script>
|
<script src="modules/renderers/state-labels.js?v=1.96.04"></script>
|
||||||
<script src="modules/burgs-and-states.js?v=1.97.00"></script>
|
<script src="modules/burgs-and-states.js?v=1.97.00"></script>
|
||||||
<script src="modules/routes-generator.js"></script>
|
<script src="modules/routes-generator.js?v=1.98.00"></script>
|
||||||
<script src="modules/religions-generator.js?v=1.93.08"></script>
|
<script src="modules/religions-generator.js?v=1.93.08"></script>
|
||||||
<script src="modules/military-generator.js?v=1.96.01"></script>
|
<script src="modules/military-generator.js?v=1.96.01"></script>
|
||||||
<script src="modules/markers-generator.js?v=1.93.04"></script>
|
<script src="modules/markers-generator.js?v=1.93.04"></script>
|
||||||
|
|
|
||||||
11
main.js
11
main.js
|
|
@ -644,6 +644,7 @@ async function generate(options) {
|
||||||
Cultures.generate();
|
Cultures.generate();
|
||||||
Cultures.expand();
|
Cultures.expand();
|
||||||
BurgsAndStates.generate();
|
BurgsAndStates.generate();
|
||||||
|
Routes.generate();
|
||||||
Religions.generate();
|
Religions.generate();
|
||||||
BurgsAndStates.defineStateForms();
|
BurgsAndStates.defineStateForms();
|
||||||
BurgsAndStates.generateProvinces();
|
BurgsAndStates.generateProvinces();
|
||||||
|
|
@ -1652,8 +1653,8 @@ function addZones(number = 1) {
|
||||||
used[next.e] = 1;
|
used[next.e] = 1;
|
||||||
|
|
||||||
cells.c[next.e].forEach(function (e) {
|
cells.c[next.e].forEach(function (e) {
|
||||||
const r = cells.road[next.e];
|
const r = cells.route[next.e];
|
||||||
const c = r ? Math.max(10 - r, 1) : 100;
|
const c = r ? 5 : 100;
|
||||||
const p = next.p + c;
|
const p = next.p + c;
|
||||||
if (p > power) return;
|
if (p > power) return;
|
||||||
|
|
||||||
|
|
@ -1780,10 +1781,10 @@ function addZones(number = 1) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function addAvalanche() {
|
function addAvalanche() {
|
||||||
const roads = cells.i.filter(i => !used[i] && cells.road[i] && cells.h[i] >= 70);
|
const routes = cells.i.filter(i => !used[i] && cells.route[i] && cells.h[i] >= 70);
|
||||||
if (!roads.length) return;
|
if (!routes.length) return;
|
||||||
|
|
||||||
const cell = +ra(roads);
|
const cell = +ra(routes);
|
||||||
const cellsArray = [],
|
const cellsArray = [],
|
||||||
queue = [cell],
|
queue = [cell],
|
||||||
power = rand(3, 15);
|
power = rand(3, 15);
|
||||||
|
|
|
||||||
|
|
@ -6,27 +6,20 @@ window.BurgsAndStates = (function () {
|
||||||
const n = cells.i.length;
|
const n = cells.i.length;
|
||||||
|
|
||||||
cells.burg = new Uint16Array(n); // cell burg
|
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());
|
const burgs = (pack.burgs = placeCapitals());
|
||||||
pack.states = createStates();
|
pack.states = createStates();
|
||||||
const capitalRoutes = Routes.getRoads();
|
|
||||||
|
|
||||||
placeTowns();
|
placeTowns();
|
||||||
expandStates();
|
expandStates();
|
||||||
normalizeStates();
|
normalizeStates();
|
||||||
const townRoutes = Routes.getTrails();
|
|
||||||
specifyBurgs();
|
specifyBurgs();
|
||||||
|
|
||||||
const oceanRoutes = Routes.getSearoutes();
|
|
||||||
|
|
||||||
collectStatistics();
|
collectStatistics();
|
||||||
assignColors();
|
assignColors();
|
||||||
|
|
||||||
generateCampaigns();
|
generateCampaigns();
|
||||||
generateDiplomacy();
|
generateDiplomacy();
|
||||||
Routes.draw(capitalRoutes, townRoutes, oceanRoutes);
|
|
||||||
drawBurgs();
|
drawBurgs();
|
||||||
|
|
||||||
function placeCapitals() {
|
function placeCapitals() {
|
||||||
|
|
@ -167,7 +160,6 @@ window.BurgsAndStates = (function () {
|
||||||
const specifyBurgs = function () {
|
const specifyBurgs = function () {
|
||||||
TIME && console.time("specifyBurgs");
|
TIME && console.time("specifyBurgs");
|
||||||
const cells = pack.cells,
|
const cells = pack.cells,
|
||||||
vertices = pack.vertices,
|
|
||||||
features = pack.features,
|
features = pack.features,
|
||||||
temp = grid.cells.temp;
|
temp = grid.cells.temp;
|
||||||
|
|
||||||
|
|
@ -185,7 +177,7 @@ window.BurgsAndStates = (function () {
|
||||||
} else b.port = 0;
|
} else b.port = 0;
|
||||||
|
|
||||||
// define burg population (keep urbanization at about 10% rate)
|
// 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.capital) b.population = rn(b.population * 1.3, 3); // increase capital population
|
||||||
|
|
||||||
if (b.port) {
|
if (b.port) {
|
||||||
|
|
|
||||||
|
|
@ -122,8 +122,7 @@ function getPackCellsData() {
|
||||||
pop: Array.from(pack.cells.pop),
|
pop: Array.from(pack.cells.pop),
|
||||||
culture: Array.from(pack.cells.culture),
|
culture: Array.from(pack.cells.culture),
|
||||||
burg: Array.from(pack.cells.burg),
|
burg: Array.from(pack.cells.burg),
|
||||||
road: Array.from(pack.cells.road),
|
route: Array.from(pack.cells.route),
|
||||||
crossroad: Array.from(pack.cells.crossroad),
|
|
||||||
state: Array.from(pack.cells.state),
|
state: Array.from(pack.cells.state),
|
||||||
religion: Array.from(pack.cells.religion),
|
religion: Array.from(pack.cells.religion),
|
||||||
province: Array.from(pack.cells.province)
|
province: Array.from(pack.cells.province)
|
||||||
|
|
@ -150,8 +149,7 @@ function getPackCellsData() {
|
||||||
pop: dataArrays.pop[cellId],
|
pop: dataArrays.pop[cellId],
|
||||||
culture: dataArrays.culture[cellId],
|
culture: dataArrays.culture[cellId],
|
||||||
burg: dataArrays.burg[cellId],
|
burg: dataArrays.burg[cellId],
|
||||||
road: dataArrays.road[cellId],
|
route: dataArrays.route[cellId],
|
||||||
crossroad: dataArrays.crossroad[cellId],
|
|
||||||
state: dataArrays.state[cellId],
|
state: dataArrays.state[cellId],
|
||||||
religion: dataArrays.religion[cellId],
|
religion: dataArrays.religion[cellId],
|
||||||
province: dataArrays.province[cellId]
|
province: dataArrays.province[cellId]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import {rollups} from "../../../utils/functionUtils.js";
|
|
||||||
|
|
||||||
const entitiesMap = {
|
const entitiesMap = {
|
||||||
states: {
|
states: {
|
||||||
label: "State",
|
label: "State",
|
||||||
|
|
|
||||||
|
|
@ -383,12 +383,12 @@ async function parseLoadedData(data, mapVersion) {
|
||||||
cells.fl = Uint16Array.from(data[20].split(","));
|
cells.fl = Uint16Array.from(data[20].split(","));
|
||||||
cells.pop = Float32Array.from(data[21].split(","));
|
cells.pop = Float32Array.from(data[21].split(","));
|
||||||
cells.r = Uint16Array.from(data[22].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.s = Uint16Array.from(data[24].split(","));
|
||||||
cells.state = Uint16Array.from(data[25].split(","));
|
cells.state = Uint16Array.from(data[25].split(","));
|
||||||
cells.religion = data[26] ? Uint16Array.from(data[26].split(",")) : new Uint16Array(cells.i.length);
|
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.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]) {
|
if (data[31]) {
|
||||||
const namesDL = data[31].split("/");
|
const namesDL = data[31].split("/");
|
||||||
|
|
|
||||||
|
|
@ -135,12 +135,12 @@ function prepareMapData() {
|
||||||
pack.cells.fl,
|
pack.cells.fl,
|
||||||
pop,
|
pop,
|
||||||
pack.cells.r,
|
pack.cells.r,
|
||||||
pack.cells.road,
|
pack.cells.route,
|
||||||
pack.cells.s,
|
pack.cells.s,
|
||||||
pack.cells.state,
|
pack.cells.state,
|
||||||
pack.cells.religion,
|
pack.cells.religion,
|
||||||
pack.cells.province,
|
pack.cells.province,
|
||||||
pack.cells.crossroad,
|
[], // deprecated pack.cells.crossroad
|
||||||
religions,
|
religions,
|
||||||
provinces,
|
provinces,
|
||||||
namesData,
|
namesData,
|
||||||
|
|
|
||||||
|
|
@ -279,7 +279,7 @@ window.Markers = (function () {
|
||||||
}
|
}
|
||||||
|
|
||||||
function listInns({cells}) {
|
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) {
|
function addInn(id, cell) {
|
||||||
|
|
@ -542,7 +542,7 @@ window.Markers = (function () {
|
||||||
|
|
||||||
function listLighthouses({cells}) {
|
function listLighthouses({cells}) {
|
||||||
return cells.i.filter(
|
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}) {
|
function listSeaMonsters({cells, features}) {
|
||||||
return cells.i.filter(
|
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.religion[i] &&
|
||||||
cells.biome[i] === 1 &&
|
cells.biome[i] === 1 &&
|
||||||
cells.pop[i] > 1 &&
|
cells.pop[i] > 1 &&
|
||||||
cells.road[i]
|
cells.route[i]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -807,7 +807,7 @@ window.Markers = (function () {
|
||||||
}
|
}
|
||||||
|
|
||||||
function listBrigands({cells}) {
|
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) {
|
function addBrigands(id, cell) {
|
||||||
|
|
@ -867,7 +867,7 @@ window.Markers = (function () {
|
||||||
|
|
||||||
// Pirates spawn on sea routes
|
// Pirates spawn on sea routes
|
||||||
function listPirates({cells}) {
|
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) {
|
function addPirates(id, cell) {
|
||||||
|
|
@ -961,7 +961,7 @@ window.Markers = (function () {
|
||||||
}
|
}
|
||||||
|
|
||||||
function listCircuses({cells}) {
|
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) {
|
function addCircuse(id, cell) {
|
||||||
|
|
|
||||||
|
|
@ -712,9 +712,9 @@ window.Religions = (function () {
|
||||||
|
|
||||||
const religionsMap = new Map(religions.map(r => [r.i, r]));
|
const religionsMap = new Map(religions.map(r => [r.i, r]));
|
||||||
|
|
||||||
const isMainRoad = cellId => cells.road[cellId] - cells.crossroad[cellId] > 4;
|
const isMainRoad = cellId => cells.route[cellId] === 1;
|
||||||
const isTrail = cellId => cells.h[cellId] > 19 && cells.road[cellId] - cells.crossroad[cellId] === 1;
|
const isTrail = cellId => cells.route[cellId] === 2;
|
||||||
const isSeaRoute = cellId => cells.h[cellId] < 20 && cells.road[cellId];
|
const isSeaRoute = cellId => cells.route[cellId] === 3;
|
||||||
const isWater = cellId => cells.h[cellId] < 20;
|
const isWater = cellId => cells.h[cellId] < 20;
|
||||||
|
|
||||||
while (queue.length) {
|
while (queue.length) {
|
||||||
|
|
|
||||||
273
modules/routes-generator-old.js
Normal file
273
modules/routes-generator-old.js
Normal file
|
|
@ -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) => `<path id="${type}${i}" d="${getPath(path)}" />`).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];
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
@ -1,269 +1,320 @@
|
||||||
window.Routes = (function () {
|
window.Routes = (function () {
|
||||||
const getRoads = function () {
|
const ROUTES = {
|
||||||
TIME && console.time("generateMainRoads");
|
MAIN_ROAD: 1,
|
||||||
const cells = pack.cells;
|
TRAIL: 2,
|
||||||
const burgs = pack.burgs.filter(b => b.i && !b.removed);
|
SEA_ROUTE: 3
|
||||||
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 getTrails = function () {
|
function generate() {
|
||||||
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");
|
|
||||||
const {cells, burgs} = pack;
|
const {cells, burgs} = pack;
|
||||||
const {burg, p} = cells;
|
|
||||||
|
|
||||||
const getBurgCoords = b => [burgs[b].x, burgs[b].y];
|
const cellRoutes = new Uint8Array(cells.h.length);
|
||||||
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) => `<path id="${type}${i}" d="${getPath(path)}" />`).join("");
|
|
||||||
|
|
||||||
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
|
const {capitalsByFeature, burgsByFeature, portsByFeature} = sortBurgsByFeature(burgs);
|
||||||
roads.html(getPathsHTML(main, "road"));
|
const connections = new Map();
|
||||||
trails.html(getPathsHTML(small, "trail"));
|
|
||||||
|
|
||||||
lineGen.curve(d3.curveBundle.beta(1));
|
const mainRoads = generateMainRoads();
|
||||||
searoutes.html(getPathsHTML(water, "searoute"));
|
const trails = generateTrails();
|
||||||
|
const seaRoutes = generateSeaRoutes();
|
||||||
|
|
||||||
TIME && console.timeEnd("drawRoutes");
|
cells.route = cellRoutes;
|
||||||
};
|
pack.routes = combineRoutes();
|
||||||
|
|
||||||
const regenerate = function () {
|
function sortBurgsByFeature(burgs) {
|
||||||
routes.selectAll("path").remove();
|
const burgsByFeature = {};
|
||||||
pack.cells.road = new Uint16Array(pack.cells.i.length);
|
const capitalsByFeature = {};
|
||||||
pack.cells.crossroad = new Uint16Array(pack.cells.i.length);
|
const portsByFeature = {};
|
||||||
const main = getRoads();
|
|
||||||
const small = getTrails();
|
|
||||||
const water = getSearoutes();
|
|
||||||
draw(main, small, water);
|
|
||||||
};
|
|
||||||
|
|
||||||
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)
|
for (const burg of burgs) {
|
||||||
function findLandPath(start, exit = null, toRoad = null) {
|
if (burg.i && !burg.removed) {
|
||||||
const cells = pack.cells;
|
const {feature, capital, port} = burg;
|
||||||
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
addBurg(burgsByFeature, feature, burg);
|
||||||
const cost = [],
|
if (capital) addBurg(capitalsByFeature, feature, burg);
|
||||||
from = [];
|
if (port) addBurg(portsByFeature, port, burg);
|
||||||
queue.queue({e: start, p: 0});
|
}
|
||||||
|
}
|
||||||
|
|
||||||
while (queue.length) {
|
return {burgsByFeature, capitalsByFeature, portsByFeature};
|
||||||
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]) {
|
function generateMainRoads() {
|
||||||
if (cells.h[c] < 20) continue; // ignore water cells
|
TIME && console.time("generateMainRoads");
|
||||||
const stateChangeCost = cells.state && cells.state[c] !== cells.state[n] ? 400 : 0; // trails tend to lay within the same state
|
const mainRoads = [];
|
||||||
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;
|
for (const [key, featureCapitals] of Object.entries(capitalsByFeature)) {
|
||||||
from[c] = n;
|
const points = featureCapitals.map(burg => [burg.x, burg.y]);
|
||||||
if (c === exit) return [from, exit];
|
const urquhartEdges = calculateUrquhartEdges(points);
|
||||||
cost[c] = totalCost;
|
urquhartEdges.forEach(([fromId, toId]) => {
|
||||||
queue.queue({e: c, p: totalCost});
|
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) {
|
function findPath(isWater, cellRoutes, start, exit, connections) {
|
||||||
const cells = pack.cells;
|
const {temp} = grid.cells;
|
||||||
const path = []; // to store all segments;
|
const {cells} = pack;
|
||||||
let segment = [],
|
|
||||||
current = end,
|
|
||||||
prev = end;
|
|
||||||
const score = type === "main" ? 5 : 1; // to increase road score at cell
|
|
||||||
|
|
||||||
if (type === "ocean" || !cells.road[prev]) segment.push(end);
|
const from = [];
|
||||||
if (!cells.road[prev]) cells.road[prev] = score;
|
const cost = [];
|
||||||
|
const queue = new FlatQueue();
|
||||||
|
queue.push(start, 0);
|
||||||
|
|
||||||
for (let i = 0, limit = 1000; i < limit; i++) {
|
return isWater ? findWaterPath() : findLandPath();
|
||||||
if (!from[current]) break;
|
|
||||||
current = from[current];
|
|
||||||
|
|
||||||
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) {
|
if (segment.length) {
|
||||||
segment.push(current);
|
// segment stepped into existing segment
|
||||||
path.push(segment);
|
segment.push(pathCells[i]);
|
||||||
if (segment[0] !== end) {
|
segments.push(segment);
|
||||||
cells.road[segment[0]] += score;
|
segment = [];
|
||||||
cells.crossroad[segment[0]] += score;
|
|
||||||
}
|
|
||||||
if (current !== start) {
|
|
||||||
cells.road[current] += score;
|
|
||||||
cells.crossroad[current] += score;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
segment = [];
|
continue;
|
||||||
prev = current;
|
|
||||||
} else {
|
|
||||||
if (prev) segment.push(prev);
|
|
||||||
prev = null;
|
|
||||||
segment.push(current);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cells.road[current] += score;
|
segment.push(pathCells[i]);
|
||||||
if (current === start) break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (segment.length > 1) path.push(segment);
|
if (segment.length > 1) segments.push(segment);
|
||||||
return path;
|
|
||||||
|
return segments;
|
||||||
}
|
}
|
||||||
|
|
||||||
// find water paths
|
// Urquhart graph is obtained by removing the longest edge from each triangle in the Delaunay triangulation
|
||||||
function findOceanPath(start, exit = null, toRoute = null) {
|
// this gives us an aproximation of a desired road network, i.e. connections between burgs
|
||||||
const cells = pack.cells,
|
// code from https://observablehq.com/@mbostock/urquhart-graph
|
||||||
temp = grid.cells.temp;
|
function calculateUrquhartEdges(points) {
|
||||||
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
const score = (p0, p1) => dist2(points[p0], points[p1]);
|
||||||
const cost = [],
|
|
||||||
from = [];
|
|
||||||
queue.queue({e: start, p: 0});
|
|
||||||
|
|
||||||
while (queue.length) {
|
const {halfedges, triangles} = Delaunator.from(points);
|
||||||
const next = queue.dequeue(),
|
const n = triangles.length;
|
||||||
n = next.e,
|
|
||||||
p = next.p;
|
|
||||||
if (toRoute && n !== start && cells.road[n]) return [from, n, true];
|
|
||||||
|
|
||||||
for (const c of cells.c[n]) {
|
const removed = new Uint8Array(n);
|
||||||
if (c === exit) {
|
const edges = [];
|
||||||
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;
|
for (let e = 0; e < n; e += 3) {
|
||||||
(from[c] = n), (cost[c] = totalCost);
|
const p0 = triangles[e],
|
||||||
queue.queue({e: c, p: totalCost});
|
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};
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -145,8 +145,7 @@ window.Submap = (function () {
|
||||||
cells.state = new Uint16Array(pn);
|
cells.state = new Uint16Array(pn);
|
||||||
cells.burg = new Uint16Array(pn);
|
cells.burg = new Uint16Array(pn);
|
||||||
cells.religion = new Uint16Array(pn);
|
cells.religion = new Uint16Array(pn);
|
||||||
cells.road = new Uint16Array(pn);
|
cells.route = new Uint8Array(pn);
|
||||||
cells.crossroad = new Uint16Array(pn);
|
|
||||||
cells.province = new Uint16Array(pn);
|
cells.province = new Uint16Array(pn);
|
||||||
|
|
||||||
stage("Resampling culture, state and religion map.");
|
stage("Resampling culture, state and religion map.");
|
||||||
|
|
@ -272,7 +271,7 @@ window.Submap = (function () {
|
||||||
|
|
||||||
BurgsAndStates.drawBurgs();
|
BurgsAndStates.drawBurgs();
|
||||||
|
|
||||||
stage("Regenerating road network.");
|
stage("Regenerating routes network.");
|
||||||
Routes.regenerate();
|
Routes.regenerate();
|
||||||
|
|
||||||
drawStates();
|
drawStates();
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,7 @@ function addBurg(point) {
|
||||||
const feature = cells.f[cell];
|
const feature = cells.f[cell];
|
||||||
|
|
||||||
const temple = pack.states[state].form === "Theocracy";
|
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);
|
const type = BurgsAndStates.getType(cell, false);
|
||||||
|
|
||||||
// generate emblem
|
// generate emblem
|
||||||
|
|
@ -326,7 +326,7 @@ function createMfcgLink(burg) {
|
||||||
const citadel = +burg.citadel;
|
const citadel = +burg.citadel;
|
||||||
const urban_castle = +(citadel && each(2)(i));
|
const urban_castle = +(citadel && each(2)(i));
|
||||||
|
|
||||||
const hub = +cells.road[cell] > 50;
|
const hub = +cells.route[cell] === 1;
|
||||||
|
|
||||||
const walls = +burg.walls;
|
const walls = +burg.walls;
|
||||||
const plaza = +burg.plaza;
|
const plaza = +burg.plaza;
|
||||||
|
|
@ -371,7 +371,7 @@ function createVillageGeneratorLink(burg) {
|
||||||
else if (cells.r[cell]) tags.push("river");
|
else if (cells.r[cell]) tags.push("river");
|
||||||
else if (pop < 200 && each(4)(cell)) tags.push("pond");
|
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");
|
if (roadsAround > 1) tags.push("highway");
|
||||||
else if (roadsAround === 1) tags.push("dead end");
|
else if (roadsAround === 1) tags.push("dead end");
|
||||||
else tags.push("isolated");
|
else tags.push("isolated");
|
||||||
|
|
|
||||||
|
|
@ -281,8 +281,7 @@ function editHeightmap(options) {
|
||||||
const l = grid.cells.i.length;
|
const l = grid.cells.i.length;
|
||||||
const biome = new Uint8Array(l);
|
const biome = new Uint8Array(l);
|
||||||
const pop = new Uint16Array(l);
|
const pop = new Uint16Array(l);
|
||||||
const road = new Uint16Array(l);
|
const route = new Uint8Array(l);
|
||||||
const crossroad = new Uint16Array(l);
|
|
||||||
const s = new Uint16Array(l);
|
const s = new Uint16Array(l);
|
||||||
const burg = new Uint16Array(l);
|
const burg = new Uint16Array(l);
|
||||||
const state = new Uint16Array(l);
|
const state = new Uint16Array(l);
|
||||||
|
|
@ -300,8 +299,7 @@ function editHeightmap(options) {
|
||||||
biome[g] = pack.cells.biome[i];
|
biome[g] = pack.cells.biome[i];
|
||||||
culture[g] = pack.cells.culture[i];
|
culture[g] = pack.cells.culture[i];
|
||||||
pop[g] = pack.cells.pop[i];
|
pop[g] = pack.cells.pop[i];
|
||||||
road[g] = pack.cells.road[i];
|
route[g] = pack.cells.route[i];
|
||||||
crossroad[g] = pack.cells.crossroad[i];
|
|
||||||
s[g] = pack.cells.s[i];
|
s[g] = pack.cells.s[i];
|
||||||
state[g] = pack.cells.state[i];
|
state[g] = pack.cells.state[i];
|
||||||
province[g] = pack.cells.province[i];
|
province[g] = pack.cells.province[i];
|
||||||
|
|
@ -353,8 +351,7 @@ function editHeightmap(options) {
|
||||||
// assign saved pack data from grid back to pack
|
// assign saved pack data from grid back to pack
|
||||||
const n = pack.cells.i.length;
|
const n = pack.cells.i.length;
|
||||||
pack.cells.pop = new Float32Array(n);
|
pack.cells.pop = new Float32Array(n);
|
||||||
pack.cells.road = new Uint16Array(n);
|
pack.cells.route = new Uint8Array(n);
|
||||||
pack.cells.crossroad = new Uint16Array(n);
|
|
||||||
pack.cells.s = new Uint16Array(n);
|
pack.cells.s = new Uint16Array(n);
|
||||||
pack.cells.burg = new Uint16Array(n);
|
pack.cells.burg = new Uint16Array(n);
|
||||||
pack.cells.state = new Uint16Array(n);
|
pack.cells.state = new Uint16Array(n);
|
||||||
|
|
@ -389,8 +386,7 @@ function editHeightmap(options) {
|
||||||
if (!isLand) continue;
|
if (!isLand) continue;
|
||||||
pack.cells.culture[i] = culture[g];
|
pack.cells.culture[i] = culture[g];
|
||||||
pack.cells.pop[i] = pop[g];
|
pack.cells.pop[i] = pop[g];
|
||||||
pack.cells.road[i] = road[g];
|
pack.cells.route[i] = route[g];
|
||||||
pack.cells.crossroad[i] = crossroad[g];
|
|
||||||
pack.cells.s[i] = s[g];
|
pack.cells.s[i] = s[g];
|
||||||
pack.cells.state[i] = state[g];
|
pack.cells.state[i] = state[g];
|
||||||
pack.cells.province[i] = province[g];
|
pack.cells.province[i] = province[g];
|
||||||
|
|
|
||||||
|
|
@ -486,9 +486,7 @@ class RouteOpisometer extends Measurer {
|
||||||
const cells = pack.cells;
|
const cells = pack.cells;
|
||||||
|
|
||||||
const c = findCell(mousePoint[0], mousePoint[1]);
|
const c = findCell(mousePoint[0], mousePoint[1]);
|
||||||
if (!cells.road[c] && !d3.event.sourceEvent.shiftKey) {
|
if (!cells.route[c] && !d3.event.sourceEvent.shiftKey) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
context.trackCell(c, rigth);
|
context.trackCell(c, rigth);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,7 @@ function recalculatePopulation() {
|
||||||
if (!b.i || b.removed || b.lock) return;
|
if (!b.i || b.removed || b.lock) return;
|
||||||
const i = b.cell;
|
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.capital) b.population = b.population * 1.3; // increase capital population
|
||||||
if (b.port) b.population = b.population * 1.3; // increase port 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);
|
b.population = rn(b.population * gauss(2, 3, 0.6, 20, 3), 3);
|
||||||
|
|
|
||||||
|
|
@ -185,7 +185,7 @@ function editUnits() {
|
||||||
const burgs = pack.burgs;
|
const burgs = pack.burgs;
|
||||||
const point = d3.mouse(this);
|
const point = d3.mouse(this);
|
||||||
const c = findCell(point[0], point[1]);
|
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 b = cells.burg[c];
|
||||||
const x = b ? burgs[b].x : cells.p[c][0];
|
const x = b ? burgs[b].x : cells.p[c][0];
|
||||||
const y = b ? burgs[b].y : cells.p[c][1];
|
const y = b ? burgs[b].y : cells.p[c][1];
|
||||||
|
|
@ -194,7 +194,7 @@ function editUnits() {
|
||||||
d3.event.on("drag", function () {
|
d3.event.on("drag", function () {
|
||||||
const point = d3.mouse(this);
|
const point = d3.mouse(this);
|
||||||
const c = findCell(point[0], point[1]);
|
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);
|
routeOpisometer.trackCell(c, true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
|
"use strict";
|
||||||
|
// FMG helper functions
|
||||||
|
|
||||||
// extracted d3 code to bypass version conflicts
|
// extracted d3 code to bypass version conflicts
|
||||||
// https://github.com/d3/d3-array/blob/main/src/group.js
|
// https://github.com/d3/d3-array/blob/main/src/group.js
|
||||||
|
function rollups(values, reduce, ...keys) {
|
||||||
export function rollups(values, reduce, ...keys) {
|
|
||||||
return nest(values, Array.from, reduce, keys);
|
return nest(values, Array.from, reduce, keys);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -23,3 +25,7 @@ function nest(values, map, reduce, keys) {
|
||||||
return map(groups);
|
return map(groups);
|
||||||
})(values, 0);
|
})(values, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function dist2([x1, y1], [x2, y2]) {
|
||||||
|
return (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
// version and caching control
|
// 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;
|
document.title += " v" + version;
|
||||||
|
|
@ -28,6 +28,7 @@ const version = "1.97.04"; // generator version, update each time
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<strong>Latest changes:</strong>
|
<strong>Latest changes:</strong>
|
||||||
|
<li>New routes generatation algorithm</li>
|
||||||
<li>Preview villages map</li>
|
<li>Preview villages map</li>
|
||||||
<li>Ability to render ocean heightmap</li>
|
<li>Ability to render ocean heightmap</li>
|
||||||
<li>Scale bar styling features</li>
|
<li>Scale bar styling features</li>
|
||||||
|
|
@ -40,9 +41,6 @@ const version = "1.97.04"; // generator version, update each time
|
||||||
<li>North and South Poles temperature can be set independently</li>
|
<li>North and South Poles temperature can be set independently</li>
|
||||||
<li>More than 70 new heraldic charges</li>
|
<li>More than 70 new heraldic charges</li>
|
||||||
<li>Multi-color heraldic charges support</li>
|
<li>Multi-color heraldic charges support</li>
|
||||||
<li>New 3D scene options and improvements</li>
|
|
||||||
<li>Autosave feature (in Options)</li>
|
|
||||||
<li>Google translation support (in Options)</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p>Join our <a href="${discord}" target="_blank">Discord server</a> and <a href="${reddit}" target="_blank">Reddit community</a> to ask questions, share maps, discuss the Generator and Worlbuilding, report bugs and propose new features.</p>
|
<p>Join our <a href="${discord}" target="_blank">Discord server</a> and <a href="${reddit}" target="_blank">Reddit community</a> to ask questions, share maps, discuss the Generator and Worlbuilding, report bugs and propose new features.</p>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue