diff --git a/modules/burgs-and-states.js b/modules/burgs-and-states.js index 6facd2d9..dfd734cb 100644 --- a/modules/burgs-and-states.js +++ b/modules/burgs-and-states.js @@ -253,11 +253,11 @@ window.BurgsAndStates = (() => { return "Generic"; }; - const defineBurgFeatures = newburg => { + const defineBurgFeatures = burg => { const {cells} = pack; pack.burgs - .filter(b => (newburg ? b.i == newburg.i : b.i && !b.removed && !b.lock)) + .filter(b => (burg ? b.i == burg.i : b.i && !b.removed && !b.lock)) .forEach(b => { const pop = b.population; b.citadel = Number(b.capital || (pop > 50 && P(0.75)) || (pop > 15 && P(0.5)) || P(0.1)); diff --git a/modules/routes-generator.js b/modules/routes-generator.js index 96315e11..62f6a8c7 100644 --- a/modules/routes-generator.js +++ b/modules/routes-generator.js @@ -172,58 +172,6 @@ window.Routes = (function () { return routesMerged > 1 ? mergeRoutes(routes) : routes; } - function preparePointsArray() { - const {cells, burgs} = pack; - return cells.p.map(([x, y], cellId) => { - const burgId = cells.burg[cellId]; - if (burgId) return [burgs[burgId].x, burgs[burgId].y]; - return [x, y]; - }); - } - - function getPoints(group, cells, points) { - const data = cells.map(cellId => [...points[cellId], cellId]); - - // resolve sharp angles - if (group !== "searoutes") { - for (let i = 1; i < cells.length - 1; i++) { - const cellId = cells[i]; - if (pack.cells.burg[cellId]) continue; - - const [prevX, prevY] = data[i - 1]; - const [currX, currY] = data[i]; - const [nextX, nextY] = data[i + 1]; - - const dAx = prevX - currX; - const dAy = prevY - currY; - const dBx = nextX - currX; - const dBy = nextY - currY; - const angle = Math.abs((Math.atan2(dAx * dBy - dAy * dBx, dAx * dBx + dAy * dBy) * 180) / Math.PI); - - if (angle < ROUTES_SHARP_ANGLE) { - const middleX = (prevX + nextX) / 2; - const middleY = (prevY + nextY) / 2; - let newX, newY; - - if (angle < ROUTES_VERY_SHARP_ANGLE) { - newX = rn((currX + middleX * 2) / 3, 2); - newY = rn((currY + middleY * 2) / 3, 2); - } else { - newX = rn((currX + middleX) / 2, 2); - newY = rn((currY + middleY) / 2, 2); - } - - if (findCell(newX, newY) === cellId) { - data[i] = [newX, newY, cellId]; - points[cellId] = [data[i][0], data[i][1]]; // change cell coordinate for all routes - } - } - } - } - - return data; // [[x, y, cell], [x, y, cell]]; - } - function buildLinks(routes) { const links = {}; @@ -247,6 +195,58 @@ window.Routes = (function () { } } + function preparePointsArray() { + const {cells, burgs} = pack; + return cells.p.map(([x, y], cellId) => { + const burgId = cells.burg[cellId]; + if (burgId) return [burgs[burgId].x, burgs[burgId].y]; + return [x, y]; + }); + } + + function getPoints(group, cells, points) { + const data = cells.map(cellId => [...points[cellId], cellId]); + + // resolve sharp angles + if (group !== "searoutes") { + for (let i = 1; i < cells.length - 1; i++) { + const cellId = cells[i]; + if (pack.cells.burg[cellId]) continue; + + const [prevX, prevY] = data[i - 1]; + const [currX, currY] = data[i]; + const [nextX, nextY] = data[i + 1]; + + const dAx = prevX - currX; + const dAy = prevY - currY; + const dBx = nextX - currX; + const dBy = nextY - currY; + const angle = Math.abs((Math.atan2(dAx * dBy - dAy * dBx, dAx * dBx + dAy * dBy) * 180) / Math.PI); + + if (angle < ROUTES_SHARP_ANGLE) { + const middleX = (prevX + nextX) / 2; + const middleY = (prevY + nextY) / 2; + let newX, newY; + + if (angle < ROUTES_VERY_SHARP_ANGLE) { + newX = rn((currX + middleX * 2) / 3, 2); + newY = rn((currY + middleY * 2) / 3, 2); + } else { + newX = rn((currX + middleX) / 2, 2); + newY = rn((currY + middleY) / 2, 2); + } + + if (findCell(newX, newY) === cellId) { + data[i] = [newX, newY, cellId]; + points[cellId] = [data[i][0], data[i][1]]; // change cell coordinate for all routes + } + } + } + } + + return data; // [[x, y, cell], [x, y, cell]]; + } + const MIN_PASSABLE_SEA_TEMP = -4; const TYPE_MODIFIERS = { "-1": 1, // coastline @@ -418,6 +418,80 @@ window.Routes = (function () { return edges; } + // connect cell with routes system by land + function connect(cellId) { + if (isConnected(cellId)) return; + + const {cells, routes} = pack; + + const path = findConnectionPath(cellId); + if (!path) return; + + const pathCells = restorePath(...path); + const pointsArray = preparePointsArray(); + const points = getPoints("trails", pathCells, pointsArray); + const feature = cells.f[cellId]; + + const routeId = Math.max(...routes.map(route => route.i)) + 1; + const newRoute = {i: routeId, group: "trails", feature, points}; + routes.push(newRoute); + + for (let i = 0; i < pathCells.length; i++) { + const cellId = pathCells[i]; + const nextCellId = pathCells[i + 1]; + if (nextCellId) addConnection(cellId, nextCellId, routeId); + } + + return newRoute; + + function findConnectionPath(start) { + const from = []; + const cost = []; + const queue = new FlatQueue(); + queue.push(start, 0); + + while (queue.length) { + const priority = queue.peekValue(); + const next = queue.pop(); + + for (const neibCellId of cells.c[next]) { + if (isConnected(neibCellId)) { + from[neibCellId] = next; + return [start, neibCellId, from]; + } + + 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] - 25, 25) / 25; // [1, 3]; + + const cellsCost = distanceCost * habitabilityModifier * heightModifier; + const totalCost = priority + cellsCost; + + if (totalCost >= cost[neibCellId]) continue; + from[neibCellId] = next; + cost[neibCellId] = totalCost; + queue.push(neibCellId, totalCost); + } + } + + return null; // path is not found + } + + function addConnection(from, to, routeId) { + const routes = pack.cells.routes; + + if (!routes[from]) routes[from] = {}; + routes[from][to] = routeId; + + if (!routes[to]) routes[to] = {}; + routes[to][from] = routeId; + } + } + // utility functions function isConnected(cellId) { const {routes} = pack.cells; @@ -610,6 +684,20 @@ window.Routes = (function () { } } + const ROUTE_CURVES = { + roads: d3.curveCatmullRom.alpha(0.1), + trails: d3.curveCatmullRom.alpha(0.1), + searoutes: d3.curveCatmullRom.alpha(0.5), + default: d3.curveCatmullRom.alpha(0.1) + }; + + function getPath({group, points}) { + const lineGen = d3.line(); + lineGen.curve(ROUTE_CURVES[group] || ROUTE_CURVES.default); + const path = round(lineGen(points.map(p => [p[0], p[1]])), 1); + return path; + } + function getLength(routeId) { const path = routes.select("#route" + routeId).node(); return path.getTotalLength(); @@ -638,12 +726,14 @@ window.Routes = (function () { return { generate, + connect, isConnected, areConnected, getRoute, hasRoad, isCrossroad, generateName, + getPath, getLength, remove }; diff --git a/modules/ui/burgs-overview.js b/modules/ui/burgs-overview.js index 561722ce..8f68cbee 100644 --- a/modules/ui/burgs-overview.js +++ b/modules/ui/burgs-overview.js @@ -279,7 +279,8 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) { function addBurgOnClick() { const point = d3.mouse(this); - const cell = findCell(point[0], point[1]); + const cell = findCell(...point); + if (pack.cells.h[cell] < 20) return tip("You cannot place state into the water. Please click on a land cell", false, "error"); if (pack.cells.burg[cell]) diff --git a/modules/ui/editors.js b/modules/ui/editors.js index 79dcef4e..dfaeb9a2 100644 --- a/modules/ui/editors.js +++ b/modules/ui/editors.js @@ -132,27 +132,43 @@ function applySorting(headers) { } function addBurg(point) { - const cells = pack.cells; - const x = rn(point[0], 2), - y = rn(point[1], 2); - const cell = findCell(x, point[1]); - const i = pack.burgs.length; - const culture = cells.culture[cell]; - const name = Names.getCulture(culture); - const state = cells.state[cell]; - const feature = cells.f[cell]; + const {cells, states} = pack; + const x = rn(point[0], 2); + const y = rn(point[1], 2); - const temple = pack.states[state].form === "Theocracy"; - const population = Math.max(cells.s[cell] / 3 + i / 1000 + (cell % 100) / 1000, 0.1); - const type = BurgsAndStates.getType(cell, false); + const cellId = findCell(x, y); + const i = pack.burgs.length; + const culture = cells.culture[cellId]; + const name = Names.getCulture(culture); + const state = cells.state[cellId]; + const feature = cells.f[cellId]; + + const population = Math.max(cells.s[cellId] / 3 + i / 1000 + (cellId % 100) / 1000, 0.1); + const type = BurgsAndStates.getType(cellId, false); // generate emblem - const coa = COA.generate(pack.states[state].coa, 0.25, null, type); + const coa = COA.generate(states[state].coa, 0.25, null, type); coa.shield = COA.getShield(culture, state); COArenderer.add("burg", i, coa, x, y); - pack.burgs.push({name, cell, x, y, state, i, culture, feature, capital: 0, port: 0, temple, population, coa, type}); - cells.burg[cell] = i; + const burg = { + name, + cell: cellId, + x, + y, + state, + i, + culture, + feature, + capital: 0, + port: 0, + temple: 0, + population, + coa, + type + }; + pack.burgs.push(burg); + cells.burg[cellId] = i; const townSize = burgIcons.select("#towns").attr("size") || 0.5; burgIcons @@ -173,7 +189,17 @@ function addBurg(point) { .attr("dy", `${townSize * -1.5}px`) .text(name); - BurgsAndStates.defineBurgFeatures(pack.burgs[i]); + BurgsAndStates.defineBurgFeatures(burg); + + const newRoute = Routes.connect(cellId); + if (newRoute && layerIsOn("toggleRoutes")) { + routes + .select("#" + newRoute.group) + .append("path") + .attr("d", Routes.getPath(newRoute)) + .attr("id", "route" + newRoute.i); + } + return i; } diff --git a/modules/ui/layers.js b/modules/ui/layers.js index 4bae98f7..0b85aa0c 100644 --- a/modules/ui/layers.js +++ b/modules/ui/layers.js @@ -1631,26 +1631,14 @@ function toggleRoutes(event) { } } -const ROUTE_CURVES = { - roads: d3.curveCatmullRom.alpha(0.1), - trails: d3.curveCatmullRom.alpha(0.1), - searoutes: d3.curveCatmullRom.alpha(0.5), - default: d3.curveCatmullRom.alpha(0.1) -}; - function drawRoutes() { TIME && console.time("drawRoutes"); const routePaths = {}; - const lineGen = d3.line(); for (const route of pack.routes) { const {i, group} = route; - lineGen.curve(ROUTE_CURVES[group] || ROUTE_CURVES.default); - const points = route.points.map(p => [p[0], p[1]]); - const path = round(lineGen(points), 1); - if (!routePaths[group]) routePaths[group] = []; - routePaths[group].push(``); + routePaths[group].push(``); } routes.selectAll("path").remove(); diff --git a/modules/ui/routes-creator.js b/modules/ui/routes-creator.js index 0be3f277..cd9d853f 100644 --- a/modules/ui/routes-creator.js +++ b/modules/ui/routes-creator.js @@ -84,15 +84,12 @@ function createRoute(defaultGroup) { .attr("r", 0.6); const group = byId("routeCreatorGroupSelect").value; - const lineGen = d3.line(); - lineGen.curve(ROUTE_CURVES[group] || ROUTE_CURVES.default); - const path = round(lineGen(points), 1); routes.select("#routeTemp").remove(); routes .select("#" + group) .append("path") - .attr("d", path) + .attr("d", Routes.getPath({group, points})) .attr("id", "routeTemp"); } diff --git a/modules/ui/routes-editor.js b/modules/ui/routes-editor.js index 9e05242a..067213e8 100644 --- a/modules/ui/routes-editor.js +++ b/modules/ui/routes-editor.js @@ -134,13 +134,7 @@ function editRoute(id) { } function redrawRoute(route) { - const lineGen = d3.line(); - lineGen.curve(ROUTE_CURVES[route.group] || ROUTE_CURVES.default); - - const points = route.points.map(p => [p[0], p[1]]); - const path = round(lineGen(points), 1); - elSelected.attr("d", path); - + elSelected.attr("d", Routes.getPath(route)); updateRouteLength(route); if (byId("elevationProfile").offsetParent) showRouteElevationProfile(); }