mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-16 17:31:24 +01:00
efficiency
This commit is contained in:
parent
20dfb7cfcb
commit
21df872ca2
7 changed files with 466 additions and 210 deletions
43
main.js
43
main.js
|
|
@ -1219,25 +1219,42 @@ function updateSuitabilityFromHubs() {
|
|||
const hubs = burgs.filter(b => b.capital || b.port).map(b => b.cell);
|
||||
if (!hubs.length) return;
|
||||
|
||||
const cellCoords = cells.p;
|
||||
// Multi-source Dijkstra to compute min distance (in pixels) from any hub to every cell
|
||||
const n = cells.i.length;
|
||||
const distArr = new Float32Array(n);
|
||||
for (let i = 0; i < n; i++) distArr[i] = Infinity;
|
||||
const q = new FlatQueue();
|
||||
for (const hub of hubs) {
|
||||
distArr[hub] = 0;
|
||||
q.push(hub, 0);
|
||||
}
|
||||
while (q.length) {
|
||||
const d0 = q.peekValue();
|
||||
const u = q.pop();
|
||||
if (d0 !== distArr[u]) continue;
|
||||
const [ux, uy] = cells.p[u];
|
||||
const neigh = cells.c[u];
|
||||
for (let k = 0; k < neigh.length; k++) {
|
||||
const v = neigh[k];
|
||||
// skip water for suitability
|
||||
if (cells.h[v] < 20) continue;
|
||||
const [vx, vy] = cells.p[v];
|
||||
const w = Math.hypot(vx - ux, vy - uy);
|
||||
const nd = d0 + w;
|
||||
if (nd < distArr[v]) {
|
||||
distArr[v] = nd;
|
||||
q.push(v, nd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const maxSuitability = 100;
|
||||
const decayRate = 0.02;
|
||||
const areaMean = d3.mean(cells.area);
|
||||
|
||||
function minDistToHub(cellId) {
|
||||
const [x, y] = cellCoords[cellId];
|
||||
let minDist = Infinity;
|
||||
for (const hubCell of hubs) {
|
||||
const [hx, hy] = cellCoords[hubCell];
|
||||
const dist = Math.hypot(x - hx, y - hy);
|
||||
if (dist < minDist) minDist = dist;
|
||||
}
|
||||
return minDist;
|
||||
}
|
||||
|
||||
for (const i of cells.i) {
|
||||
if (cells.h[i] < 20) continue;
|
||||
const dist = minDistToHub(i);
|
||||
const dist = distArr[i];
|
||||
let s = maxSuitability * Math.exp(-decayRate * dist);
|
||||
s *= +biomesData.habitability[cells.biome[i]] / 100;
|
||||
cells.s[i] = s;
|
||||
|
|
|
|||
|
|
@ -862,7 +862,8 @@ window.BurgsAndStates = (() => {
|
|||
cells.state = cells.state || new Uint16Array(cells.i.length);
|
||||
|
||||
const queue = new FlatQueue();
|
||||
const cost = [];
|
||||
const cost = new Float32Array(cells.i.length);
|
||||
cost.fill(Infinity);
|
||||
|
||||
const globalGrowthRate = byId("growthRate").valueAsNumber || 1;
|
||||
const statesGrowthRate = byId("statesGrowthRate")?.valueAsNumber || 1;
|
||||
|
|
@ -889,31 +890,39 @@ window.BurgsAndStates = (() => {
|
|||
while (queue.length) {
|
||||
const next = queue.pop();
|
||||
|
||||
const {e, p, s, b} = next;
|
||||
const {type, culture} = states[s];
|
||||
const e0 = next.e;
|
||||
const p0 = next.p;
|
||||
const s0 = next.s;
|
||||
const b0 = next.b;
|
||||
const st = states[s0];
|
||||
const type = st.type;
|
||||
const culture = st.culture;
|
||||
|
||||
cells.c[e].forEach(e => {
|
||||
const state = states[cells.state[e]];
|
||||
if (state.lock) return; // do not overwrite cell of locked states
|
||||
if (cells.state[e] && e === state.center) return; // do not overwrite capital cells
|
||||
const neigh = cells.c[e0];
|
||||
for (let idx = 0; idx < neigh.length; idx++) {
|
||||
const e = neigh[idx];
|
||||
const stateOnCell = states[cells.state[e]];
|
||||
if (stateOnCell.lock) continue; // do not overwrite cell of locked states
|
||||
if (cells.state[e] && e === stateOnCell.center) continue; // do not overwrite capital cells
|
||||
|
||||
const cultureCost = culture === cells.culture[e] ? -9 : 100;
|
||||
const populationCost = cells.h[e] < 20 ? 0 : cells.s[e] ? Math.max(20 - cells.s[e], 0) : 5000;
|
||||
const biomeCost = getBiomeCost(b, cells.biome[e], type);
|
||||
const sVal = cells.s[e];
|
||||
const populationCost = cells.h[e] < 20 ? 0 : sVal ? Math.max(20 - sVal, 0) : 5000;
|
||||
const biomeCost = getBiomeCost(b0, cells.biome[e], type);
|
||||
const heightCost = getHeightCost(pack.features[cells.f[e]], cells.h[e], type);
|
||||
const riverCost = getRiverCost(cells.r[e], e, type);
|
||||
const typeCost = getTypeCost(cells.t[e], type);
|
||||
const cellCost = Math.max(cultureCost + populationCost + biomeCost + heightCost + riverCost + typeCost, 0);
|
||||
const totalCost = p + 10 + cellCost / states[s].expansionism;
|
||||
const totalCost = p0 + 10 + cellCost / st.expansionism;
|
||||
|
||||
if (totalCost > growthRate) return;
|
||||
if (totalCost > growthRate) continue;
|
||||
|
||||
if (!cost[e] || totalCost < cost[e]) {
|
||||
if (cells.h[e] >= 20) cells.state[e] = s; // assign state to cell
|
||||
if (totalCost < cost[e]) {
|
||||
if (cells.h[e] >= 20) cells.state[e] = s0; // assign state to cell
|
||||
cost[e] = totalCost;
|
||||
queue.push({e, p: totalCost, s, b}, totalCost);
|
||||
queue.push({e, p: totalCost, s: s0, b: b0}, totalCost);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
burgs.filter(b => b.i && !b.removed).forEach(b => (b.state = cells.state[b.cell])); // assign state to burgs
|
||||
|
|
|
|||
|
|
@ -134,7 +134,10 @@ window.HeightmapGenerator = (function () {
|
|||
}
|
||||
|
||||
function addOneHill() {
|
||||
const change = new Uint8Array(heights.length);
|
||||
// reuse a scratch buffer to avoid reallocations
|
||||
if (!addHill._change || addHill._change.length !== heights.length) addHill._change = new Uint8Array(heights.length);
|
||||
const change = addHill._change;
|
||||
change.fill(0);
|
||||
let limit = 0;
|
||||
let start;
|
||||
let h = lim(getNumberInRange(height));
|
||||
|
|
@ -148,17 +151,18 @@ window.HeightmapGenerator = (function () {
|
|||
|
||||
change[start] = h;
|
||||
const queue = [start];
|
||||
while (queue.length) {
|
||||
const q = queue.shift();
|
||||
|
||||
for (const c of grid.cells.c[q]) {
|
||||
for (let qi = 0; qi < queue.length; qi++) {
|
||||
const q = queue[qi];
|
||||
const neibs = grid.cells.c[q];
|
||||
for (let k = 0; k < neibs.length; k++) {
|
||||
const c = neibs[k];
|
||||
if (change[c]) continue;
|
||||
change[c] = change[q] ** blobPower * (Math.random() * 0.2 + 0.9);
|
||||
if (change[c] > 1) queue.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
heights = heights.map((h, i) => lim(h + change[i]));
|
||||
for (let i = 0; i < heights.length; i++) heights[i] = lim(heights[i] + change[i]);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -170,7 +174,9 @@ window.HeightmapGenerator = (function () {
|
|||
}
|
||||
|
||||
function addOnePit() {
|
||||
const used = new Uint8Array(heights.length);
|
||||
if (!addPit._used || addPit._used.length !== heights.length) addPit._used = new Uint8Array(heights.length);
|
||||
const used = addPit._used;
|
||||
used.fill(0);
|
||||
let limit = 0,
|
||||
start;
|
||||
let h = lim(getNumberInRange(height));
|
||||
|
|
@ -183,17 +189,18 @@ window.HeightmapGenerator = (function () {
|
|||
} while (heights[start] < 20 && limit < 50);
|
||||
|
||||
const queue = [start];
|
||||
while (queue.length) {
|
||||
const q = queue.shift();
|
||||
for (let qi = 0; qi < queue.length; qi++) {
|
||||
const q = queue[qi];
|
||||
h = h ** blobPower * (Math.random() * 0.2 + 0.9);
|
||||
if (h < 1) return;
|
||||
|
||||
grid.cells.c[q].forEach(function (c, i) {
|
||||
if (used[c]) return;
|
||||
const neibs = grid.cells.c[q];
|
||||
for (let k = 0; k < neibs.length; k++) {
|
||||
const c = neibs[k];
|
||||
if (used[c]) continue;
|
||||
heights[c] = lim(heights[c] - h * (Math.random() * 0.2 + 0.9));
|
||||
used[c] = 1;
|
||||
queue.push(c);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -207,7 +214,9 @@ window.HeightmapGenerator = (function () {
|
|||
}
|
||||
|
||||
function addOneRange() {
|
||||
const used = new Uint8Array(heights.length);
|
||||
if (!addRange._used || addRange._used.length !== heights.length) addRange._used = new Uint8Array(heights.length);
|
||||
const used = addRange._used;
|
||||
used.fill(0);
|
||||
let h = lim(getNumberInRange(height));
|
||||
|
||||
if (rangeX && rangeY) {
|
||||
|
|
@ -259,35 +268,44 @@ window.HeightmapGenerator = (function () {
|
|||
}
|
||||
|
||||
// add height to ridge and cells around
|
||||
let queue = range.slice(),
|
||||
i = 0;
|
||||
let queue = range.slice();
|
||||
let i = 0;
|
||||
while (queue.length) {
|
||||
const frontier = queue.slice();
|
||||
(queue = []), i++;
|
||||
frontier.forEach(i => {
|
||||
heights[i] = lim(heights[i] + h * (Math.random() * 0.3 + 0.85));
|
||||
});
|
||||
queue.length = 0; i++;
|
||||
for (let fi = 0; fi < frontier.length; fi++) {
|
||||
const idx = frontier[fi];
|
||||
heights[idx] = lim(heights[idx] + h * (Math.random() * 0.3 + 0.85));
|
||||
}
|
||||
h = h ** linePower - 1;
|
||||
if (h < 2) break;
|
||||
frontier.forEach(f => {
|
||||
grid.cells.c[f].forEach(i => {
|
||||
if (!used[i]) {
|
||||
queue.push(i);
|
||||
used[i] = 1;
|
||||
}
|
||||
});
|
||||
});
|
||||
for (let fi = 0; fi < frontier.length; fi++) {
|
||||
const f = frontier[fi];
|
||||
const neibs = grid.cells.c[f];
|
||||
for (let k = 0; k < neibs.length; k++) {
|
||||
const idx = neibs[k];
|
||||
if (!used[idx]) { queue.push(idx); used[idx] = 1; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// generate prominences
|
||||
range.forEach((cur, d) => {
|
||||
if (d % 6 !== 0) return;
|
||||
for (const l of d3.range(i)) {
|
||||
const min = grid.cells.c[cur][d3.scan(grid.cells.c[cur], (a, b) => heights[a] - heights[b])]; // downhill cell
|
||||
heights[min] = (heights[cur] * 2 + heights[min]) / 3;
|
||||
cur = min;
|
||||
for (let d = 0; d < range.length; d++) {
|
||||
if (d % 6 !== 0) continue;
|
||||
let cur = range[d];
|
||||
for (let l = 0; l < i; l++) {
|
||||
const nbrs = grid.cells.c[cur];
|
||||
let minIdx = nbrs[0];
|
||||
let minVal = heights[minIdx];
|
||||
for (let k = 1; k < nbrs.length; k++) {
|
||||
const idk = nbrs[k];
|
||||
const hv = heights[idk];
|
||||
if (hv < minVal) { minVal = hv; minIdx = idk; }
|
||||
}
|
||||
heights[minIdx] = (heights[cur] * 2 + heights[minIdx]) / 3;
|
||||
cur = minIdx;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -299,7 +317,9 @@ window.HeightmapGenerator = (function () {
|
|||
}
|
||||
|
||||
function addOneTrough() {
|
||||
const used = new Uint8Array(heights.length);
|
||||
if (!addTrough._used || addTrough._used.length !== heights.length) addTrough._used = new Uint8Array(heights.length);
|
||||
const used = addTrough._used;
|
||||
used.fill(0);
|
||||
let h = lim(getNumberInRange(height));
|
||||
|
||||
if (rangeX && rangeY) {
|
||||
|
|
@ -356,36 +376,44 @@ window.HeightmapGenerator = (function () {
|
|||
}
|
||||
|
||||
// add height to ridge and cells around
|
||||
let queue = range.slice(),
|
||||
i = 0;
|
||||
let queue = range.slice();
|
||||
let i = 0;
|
||||
while (queue.length) {
|
||||
const frontier = queue.slice();
|
||||
(queue = []), i++;
|
||||
frontier.forEach(i => {
|
||||
heights[i] = lim(heights[i] - h * (Math.random() * 0.3 + 0.85));
|
||||
});
|
||||
queue.length = 0; i++;
|
||||
for (let fi = 0; fi < frontier.length; fi++) {
|
||||
const idx = frontier[fi];
|
||||
heights[idx] = lim(heights[idx] - h * (Math.random() * 0.3 + 0.85));
|
||||
}
|
||||
h = h ** linePower - 1;
|
||||
if (h < 2) break;
|
||||
frontier.forEach(f => {
|
||||
grid.cells.c[f].forEach(i => {
|
||||
if (!used[i]) {
|
||||
queue.push(i);
|
||||
used[i] = 1;
|
||||
}
|
||||
});
|
||||
});
|
||||
for (let fi = 0; fi < frontier.length; fi++) {
|
||||
const f = frontier[fi];
|
||||
const neibs = grid.cells.c[f];
|
||||
for (let k = 0; k < neibs.length; k++) {
|
||||
const idx = neibs[k];
|
||||
if (!used[idx]) { queue.push(idx); used[idx] = 1; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// generate prominences
|
||||
range.forEach((cur, d) => {
|
||||
if (d % 6 !== 0) return;
|
||||
for (const l of d3.range(i)) {
|
||||
const min = grid.cells.c[cur][d3.scan(grid.cells.c[cur], (a, b) => heights[a] - heights[b])]; // downhill cell
|
||||
//debug.append("circle").attr("cx", p[min][0]).attr("cy", p[min][1]).attr("r", 1);
|
||||
heights[min] = (heights[cur] * 2 + heights[min]) / 3;
|
||||
cur = min;
|
||||
for (let d = 0; d < range.length; d++) {
|
||||
if (d % 6 !== 0) continue;
|
||||
let cur = range[d];
|
||||
for (let l = 0; l < i; l++) {
|
||||
const nbrs = grid.cells.c[cur];
|
||||
let minIdx = nbrs[0];
|
||||
let minVal = heights[minIdx];
|
||||
for (let k = 1; k < nbrs.length; k++) {
|
||||
const idk = nbrs[k];
|
||||
const hv = heights[idk];
|
||||
if (hv < minVal) { minVal = hv; minIdx = idk; }
|
||||
}
|
||||
heights[minIdx] = (heights[cur] * 2 + heights[minIdx]) / 3;
|
||||
cur = minIdx;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -452,37 +480,39 @@ window.HeightmapGenerator = (function () {
|
|||
const max = range === "land" || range === "all" ? 100 : +range.split("-")[1];
|
||||
const isLand = min === 20;
|
||||
|
||||
heights = heights.map(h => {
|
||||
if (h < min || h > max) return h;
|
||||
|
||||
for (let i = 0; i < heights.length; i++) {
|
||||
let h = heights[i];
|
||||
if (h < min || h > max) continue;
|
||||
if (add) h = isLand ? Math.max(h + add, 20) : h + add;
|
||||
if (mult !== 1) h = isLand ? (h - 20) * mult + 20 : h * mult;
|
||||
if (power) h = isLand ? (h - 20) ** power + 20 : h ** power;
|
||||
return lim(h);
|
||||
});
|
||||
heights[i] = lim(h);
|
||||
}
|
||||
};
|
||||
|
||||
const smooth = (fr = 2, add = 0) => {
|
||||
heights = heights.map((h, i) => {
|
||||
const a = [h];
|
||||
grid.cells.c[i].forEach(c => a.push(heights[c]));
|
||||
if (fr === 1) return d3.mean(a) + add;
|
||||
return lim((h * (fr - 1) + d3.mean(a) + add) / fr);
|
||||
});
|
||||
for (let i = 0; i < heights.length; i++) {
|
||||
const h = heights[i];
|
||||
const nbrs = grid.cells.c[i];
|
||||
let sum = h;
|
||||
for (let k = 0; k < nbrs.length; k++) sum += heights[nbrs[k]];
|
||||
const mean = sum / (nbrs.length + 1);
|
||||
heights[i] = fr === 1 ? mean + add : lim((h * (fr - 1) + mean + add) / fr);
|
||||
}
|
||||
};
|
||||
|
||||
const mask = (power = 1) => {
|
||||
const fr = power ? Math.abs(power) : 1;
|
||||
|
||||
heights = heights.map((h, i) => {
|
||||
for (let i = 0; i < heights.length; i++) {
|
||||
const h = heights[i];
|
||||
const [x, y] = grid.points[i];
|
||||
const nx = (2 * x) / graphWidth - 1; // [-1, 1], 0 is center
|
||||
const ny = (2 * y) / graphHeight - 1; // [-1, 1], 0 is center
|
||||
let distance = (1 - nx ** 2) * (1 - ny ** 2); // 1 is center, 0 is edge
|
||||
if (power < 0) distance = 1 - distance; // inverted, 0 is center, 1 is edge
|
||||
const nx = (2 * x) / graphWidth - 1; // [-1, 1]
|
||||
const ny = (2 * y) / graphHeight - 1; // [-1, 1]
|
||||
let distance = (1 - nx * nx) * (1 - ny * ny);
|
||||
if (power < 0) distance = 1 - distance;
|
||||
const masked = h * distance;
|
||||
return lim((h * (fr - 1) + masked) / fr);
|
||||
});
|
||||
heights[i] = lim((h * (fr - 1) + masked) / fr);
|
||||
}
|
||||
};
|
||||
|
||||
const invert = (count, axes) => {
|
||||
|
|
|
|||
|
|
@ -21,10 +21,16 @@ const ROUTE_TIER_MODIFIERS = {
|
|||
};
|
||||
|
||||
window.Routes = (function () {
|
||||
// Per-cell cost cache for fast path evaluations
|
||||
let RC = null;
|
||||
function generate(lockedRoutes = []) {
|
||||
TIME && console.time("generateRoutes");
|
||||
const {capitalsByFeature, burgsByFeature, portsByFeature, primaryByFeature, plazaByFeature, unconnectedBurgsByFeature} = sortBurgsByFeature(pack.burgs);
|
||||
|
||||
// Build per-cell route cost factors once
|
||||
RC = buildRouteCostCache();
|
||||
|
||||
// connections: Map<cellId, Set<neighborCellId>> for O(1) adjacency checks without string keys
|
||||
const connections = new Map();
|
||||
lockedRoutes.forEach(route => addConnections(route.points.map(p => p[2])));
|
||||
|
||||
|
|
@ -94,6 +100,36 @@ window.Routes = (function () {
|
|||
return {burgsByFeature, capitalsByFeature, portsByFeature, primaryByFeature, plazaByFeature, unconnectedBurgsByFeature};
|
||||
}
|
||||
|
||||
// Precompute per-cell route modifiers to avoid hot-path branching
|
||||
function buildRouteCostCache() {
|
||||
const {cells} = pack;
|
||||
const n = cells.i.length;
|
||||
const landHabitability = new Float32Array(n);
|
||||
const landHeight = new Float32Array(n);
|
||||
const isPassableLand = new Uint8Array(n);
|
||||
const waterType = new Float32Array(n);
|
||||
const isPassableWater = new Uint8Array(n);
|
||||
const stateId = new Uint32Array(n);
|
||||
const burgFactor = new Float32Array(n);
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const h = pack.cells.h[i];
|
||||
const hab = biomesData.habitability[pack.cells.biome[i]];
|
||||
isPassableLand[i] = h >= 20 && hab > 0 ? 1 : 0;
|
||||
landHabitability[i] = 1 + Math.max(100 - hab, 0) / 1000;
|
||||
landHeight[i] = 1 + Math.max(h - 25, 25) / 25;
|
||||
stateId[i] = pack.cells.state[i] >>> 0;
|
||||
burgFactor[i] = pack.cells.burg[i] ? 1 : 3;
|
||||
|
||||
const t = pack.cells.t[i];
|
||||
waterType[i] = ROUTE_TYPE_MODIFIERS[t] || ROUTE_TYPE_MODIFIERS.default;
|
||||
const temp = grid.cells.temp[pack.cells.g[i]];
|
||||
isPassableWater[i] = h < 20 && temp >= MIN_PASSABLE_SEA_TEMP ? 1 : 0;
|
||||
}
|
||||
|
||||
return {landHabitability, landHeight, isPassableLand, waterType, isPassableWater, stateId, burgFactor};
|
||||
}
|
||||
|
||||
// Tier 1: Major Sea Routes - Connect capitals and major ports across ALL water bodies
|
||||
// Simulates long-distance maritime trade like Hanseatic League routes
|
||||
function generateMajorSeaRoutes() {
|
||||
|
|
@ -101,18 +137,30 @@ window.Routes = (function () {
|
|||
const majorSeaRoutes = [];
|
||||
|
||||
// Get all significant ports for major trade routes
|
||||
const allMajorPorts = [];
|
||||
let allMajorPorts = [];
|
||||
pack.burgs.forEach(b => {
|
||||
if (b.i && !b.removed && b.port) {
|
||||
// Include more ports in major routes: capitals, large ports, and wealthy market towns
|
||||
if (b.capital ||
|
||||
b.isLargePort ||
|
||||
(b.population >= 5 && b.plaza) || // Major market towns (5000+ pop with plaza)
|
||||
(b.population >= 10)) { // Large cities regardless of status
|
||||
if (!b.i || b.removed) return;
|
||||
if (b.port) {
|
||||
if (b.capital || b.isLargePort || (b.population >= 5 && b.plaza) || (b.population >= 10)) {
|
||||
allMajorPorts.push(b);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Fallback: if there are <2 declared ports (e.g., single-port water bodies),
|
||||
// consider coastal capitals/markets as pseudo-ports to ensure some sea routes.
|
||||
if (allMajorPorts.length < 2) {
|
||||
const coastalCandidates = pack.burgs.filter(b => {
|
||||
if (!b.i || b.removed) return false;
|
||||
const cell = b.cell;
|
||||
const coastal = pack.cells.t[cell] === 1; // coastline
|
||||
const tempOK = grid.cells.temp[pack.cells.g[cell]] >= MIN_PASSABLE_SEA_TEMP;
|
||||
return coastal && tempOK && (b.capital || b.plaza || b.isLargePort || b.population >= 5);
|
||||
});
|
||||
// take up to 12 best by importance
|
||||
coastalCandidates.sort((a, b) => (b.capital - a.capital) || (b.population - a.population));
|
||||
allMajorPorts = coastalCandidates.slice(0, Math.min(12, coastalCandidates.length));
|
||||
}
|
||||
|
||||
if (allMajorPorts.length < 2) {
|
||||
TIME && console.timeEnd("generateMajorSeaRoutes");
|
||||
|
|
@ -133,39 +181,38 @@ window.Routes = (function () {
|
|||
const mediumPorts = allMajorPorts.filter(p => !p.capital && !p.isLargePort && p.population < 10);
|
||||
|
||||
// Use all capitals and top large ports as primary hubs
|
||||
const hubs = [...capitalPorts, ...largePorts.slice(0, Math.max(10, Math.floor(largePorts.length * 0.5)))];
|
||||
let hubs = [...capitalPorts, ...largePorts.slice(0, Math.max(10, Math.floor(largePorts.length * 0.5)))];
|
||||
if (hubs.length < 2) hubs = [...largePorts.slice(0, Math.min(20, largePorts.length))];
|
||||
const secondaryHubs = [...largePorts.slice(Math.max(10, Math.floor(largePorts.length * 0.5))), ...mediumPorts.slice(0, 20)];
|
||||
|
||||
// Connect primary hubs strategically (not all-to-all to avoid too many routes)
|
||||
// Connect capitals to each other
|
||||
for (let i = 0; i < capitalPorts.length; i++) {
|
||||
for (let j = i + 1; j < capitalPorts.length; j++) {
|
||||
const start = capitalPorts[i].cell;
|
||||
const exit = capitalPorts[j].cell;
|
||||
const distance = Math.sqrt((capitalPorts[i].x - capitalPorts[j].x) ** 2 + (capitalPorts[i].y - capitalPorts[j].y) ** 2);
|
||||
|
||||
// Connect if reasonably distant (long-distance trade) or same cultural sphere
|
||||
if (distance > 50 || capitalPorts[i].culture === capitalPorts[j].culture) {
|
||||
const segments = findPathSegments({isWater: true, connections, start, exit, routeType: "majorSea"});
|
||||
for (const segment of segments) {
|
||||
addConnections(segment);
|
||||
majorSeaRoutes.push({feature: -1, cells: segment, type: "majorSea"});
|
||||
}
|
||||
// Connect primary hubs strategically using sparse graph (Urquhart edges)
|
||||
if (hubs.length >= 2) {
|
||||
const points = hubs.map(p => [p.x, p.y]);
|
||||
const edges = calculateUrquhartEdges(points);
|
||||
edges.forEach(([ai, bi]) => {
|
||||
const a = hubs[ai];
|
||||
const b = hubs[bi];
|
||||
const start = a.cell;
|
||||
const exit = b.cell;
|
||||
const segments = findPathSegments({isWater: true, connections, start, exit, routeType: "majorSea"});
|
||||
for (const segment of segments) {
|
||||
addConnections(segment);
|
||||
majorSeaRoutes.push({feature: -1, cells: segment, type: "majorSea"});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Connect large ports to nearest 2-3 capitals for trade network
|
||||
// Connect large ports to nearest 1-2 hubs for trade network
|
||||
largePorts.slice(0, 15).forEach(port => {
|
||||
const nearestCapitals = capitalPorts
|
||||
const nearestHubs = hubs
|
||||
.map(cap => ({
|
||||
cap,
|
||||
distance: Math.sqrt((port.x - cap.x) ** 2 + (port.y - cap.y) ** 2)
|
||||
distance2: (port.x - cap.x) ** 2 + (port.y - cap.y) ** 2
|
||||
}))
|
||||
.sort((a, b) => a.distance - b.distance)
|
||||
.slice(0, Math.min(3, capitalPorts.length)); // Connect to up to 3 nearest capitals
|
||||
.sort((a, b) => a.distance2 - b.distance2)
|
||||
.slice(0, Math.min(2, hubs.length)); // Connect to up to 2 nearest hubs
|
||||
|
||||
nearestCapitals.forEach(({cap}) => {
|
||||
nearestHubs.forEach(({cap}) => {
|
||||
const segments = findPathSegments({
|
||||
isWater: true,
|
||||
connections,
|
||||
|
|
@ -186,9 +233,9 @@ window.Routes = (function () {
|
|||
let minDistance = Infinity;
|
||||
|
||||
hubs.forEach(hub => {
|
||||
const distance = Math.sqrt((port.x - hub.x) ** 2 + (port.y - hub.y) ** 2);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
const dx = port.x - hub.x; const dy = port.y - hub.y; const d2 = dx*dx + dy*dy;
|
||||
if (d2 < minDistance) {
|
||||
minDistance = d2;
|
||||
nearestHub = hub;
|
||||
}
|
||||
});
|
||||
|
|
@ -238,10 +285,9 @@ window.Routes = (function () {
|
|||
const edges = [];
|
||||
for (let i = 0; i < capitals.length; i++) {
|
||||
for (let j = i + 1; j < capitals.length; j++) {
|
||||
const distance = Math.sqrt(
|
||||
(capitals[i].x - capitals[j].x) ** 2 +
|
||||
(capitals[i].y - capitals[j].y) ** 2
|
||||
);
|
||||
const dx = (capitals[i].x - capitals[j].x);
|
||||
const dy = (capitals[i].y - capitals[j].y);
|
||||
const distance = dx*dx + dy*dy; // squared distance is sufficient for sorting
|
||||
edges.push({
|
||||
from: i,
|
||||
to: j,
|
||||
|
|
@ -382,30 +428,30 @@ window.Routes = (function () {
|
|||
);
|
||||
|
||||
// Connect each village to nearest market center
|
||||
const mapScaleLocal = Math.sqrt(graphWidth * graphHeight / 1000000);
|
||||
const maxLocalKm = 60; // cap local road reach
|
||||
const maxLocalDist2 = (maxLocalKm * mapScaleLocal) ** 2;
|
||||
const radiusPx = maxLocalKm * mapScaleLocal;
|
||||
const hashMarkets = makeSpatialHash(marketCenters, b => [b.x, b.y], radiusPx);
|
||||
|
||||
villages.forEach(village => {
|
||||
let nearestMarket = null;
|
||||
let minDistance = Infinity;
|
||||
|
||||
marketCenters.forEach(market => {
|
||||
const distance = Math.sqrt(
|
||||
(village.x - market.x) ** 2 +
|
||||
(village.y - market.y) ** 2
|
||||
);
|
||||
|
||||
const candidates = queryCandidatesWithinRadius(hashMarkets, village.x, village.y, radiusPx);
|
||||
for (const market of candidates) {
|
||||
const dx = village.x - market.x;
|
||||
const dy = village.y - market.y;
|
||||
const d2 = dx*dx + dy*dy;
|
||||
// Prefer markets in same state/culture
|
||||
let culturalModifier = 1;
|
||||
if (village.state === market.state) culturalModifier = 0.8;
|
||||
if (village.culture === market.culture) culturalModifier *= 0.9;
|
||||
|
||||
const adjustedDistance = distance * culturalModifier;
|
||||
|
||||
if (adjustedDistance < minDistance) {
|
||||
minDistance = adjustedDistance;
|
||||
nearestMarket = market;
|
||||
}
|
||||
});
|
||||
const adjustedDistance = d2 * culturalModifier;
|
||||
if (adjustedDistance < minDistance) { minDistance = adjustedDistance; nearestMarket = market; }
|
||||
}
|
||||
|
||||
if (nearestMarket) {
|
||||
if (nearestMarket && minDistance <= maxLocalDist2) {
|
||||
const segments = findPathSegments({
|
||||
isWater: false,
|
||||
connections,
|
||||
|
|
@ -415,6 +461,10 @@ window.Routes = (function () {
|
|||
});
|
||||
|
||||
for (const segment of segments) {
|
||||
// Skip excessively long local segments
|
||||
const pxLen = pathCellsLengthPx(segment);
|
||||
const kmLen = pxLen / mapScaleLocal;
|
||||
if (kmLen > maxLocalKm) continue;
|
||||
addConnections(segment);
|
||||
localRoads.push({
|
||||
feature: village.feature,
|
||||
|
|
@ -450,33 +500,27 @@ window.Routes = (function () {
|
|||
)
|
||||
);
|
||||
|
||||
// Connect each hamlet to nearest village (3-6 km as per research)
|
||||
// Connect each hamlet to nearest village (limit to <= 8 km)
|
||||
const mapScaleFoot = Math.sqrt(graphWidth * graphHeight / 1000000);
|
||||
const maxFootKm = 8;
|
||||
const maxFootDist2 = (maxFootKm * mapScaleFoot) ** 2;
|
||||
const radiusFootPx = maxFootKm * mapScaleFoot;
|
||||
const hashSettlements = makeSpatialHash(largerSettlements, b => [b.x, b.y], radiusFootPx);
|
||||
hamlets.forEach(hamlet => {
|
||||
let nearestVillage = null;
|
||||
let minDistance = Infinity;
|
||||
|
||||
largerSettlements.forEach(village => {
|
||||
const distance = Math.sqrt(
|
||||
(hamlet.x - village.x) ** 2 +
|
||||
(hamlet.y - village.y) ** 2
|
||||
);
|
||||
|
||||
// Strong preference for same culture/state
|
||||
const candidates = queryCandidatesWithinRadius(hashSettlements, hamlet.x, hamlet.y, radiusFootPx);
|
||||
for (const village of candidates) {
|
||||
const dx = hamlet.x - village.x;
|
||||
const dy = hamlet.y - village.y;
|
||||
const d2 = dx*dx + dy*dy;
|
||||
let modifier = 1;
|
||||
if (hamlet.state === village.state) modifier = 0.7;
|
||||
if (hamlet.culture === village.culture) modifier *= 0.8;
|
||||
|
||||
const adjustedDistance = distance * modifier;
|
||||
|
||||
// Only connect to nearby settlements (6 km max range)
|
||||
const mapScale = Math.sqrt(graphWidth * graphHeight / 1000000);
|
||||
const kmDistance = distance / mapScale;
|
||||
|
||||
if (kmDistance <= 8 && adjustedDistance < minDistance) {
|
||||
minDistance = adjustedDistance;
|
||||
nearestVillage = village;
|
||||
}
|
||||
});
|
||||
const adjustedDistance = d2 * modifier;
|
||||
if (d2 <= maxFootDist2 && adjustedDistance < minDistance) { minDistance = adjustedDistance; nearestVillage = village; }
|
||||
}
|
||||
|
||||
if (nearestVillage) {
|
||||
const segments = findPathSegments({
|
||||
|
|
@ -488,6 +532,9 @@ window.Routes = (function () {
|
|||
});
|
||||
|
||||
for (const segment of segments) {
|
||||
const pxLen = pathCellsLengthPx(segment);
|
||||
const kmLen = pxLen / mapScaleFoot;
|
||||
if (kmLen > maxFootKm) continue;
|
||||
addConnections(segment);
|
||||
footpaths.push({
|
||||
feature: hamlet.feature,
|
||||
|
|
@ -694,11 +741,11 @@ window.Routes = (function () {
|
|||
|
||||
// First, try connecting to the nearest connected burg
|
||||
for (const connectedBurg of connectedBurgs) {
|
||||
const distance = Math.sqrt(
|
||||
(unconnectedBurg.x - connectedBurg.x) ** 2 + (unconnectedBurg.y - connectedBurg.y) ** 2
|
||||
);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
const dx = unconnectedBurg.x - connectedBurg.x;
|
||||
const dy = unconnectedBurg.y - connectedBurg.y;
|
||||
const d2 = dx * dx + dy * dy;
|
||||
if (d2 < minDistance) {
|
||||
minDistance = d2;
|
||||
bestConnection = connectedBurg;
|
||||
}
|
||||
}
|
||||
|
|
@ -730,19 +777,38 @@ window.Routes = (function () {
|
|||
}
|
||||
|
||||
function addConnections(segment) {
|
||||
for (let i = 0; i < segment.length; i++) {
|
||||
const cellId = segment[i];
|
||||
const nextCellId = segment[i + 1];
|
||||
if (nextCellId) {
|
||||
connections.set(`${cellId}-${nextCellId}`, true);
|
||||
connections.set(`${nextCellId}-${cellId}`, true);
|
||||
}
|
||||
for (let i = 0; i < segment.length - 1; i++) {
|
||||
const a = segment[i];
|
||||
const b = segment[i + 1];
|
||||
let setA = connections.get(a);
|
||||
if (!setA) { setA = new Set(); connections.set(a, setA); }
|
||||
setA.add(b);
|
||||
let setB = connections.get(b);
|
||||
if (!setB) { setB = new Set(); connections.set(b, setB); }
|
||||
setB.add(a);
|
||||
}
|
||||
}
|
||||
|
||||
function hasConnection(a, b) {
|
||||
const s = connections.get(a);
|
||||
return s ? s.has(b) : false;
|
||||
}
|
||||
|
||||
function findPathSegments({isWater, connections, start, exit, routeType}) {
|
||||
const getCost = createCostEvaluator({isWater, connections, routeType});
|
||||
const pathCells = findPath(start, current => current === exit, getCost);
|
||||
const heuristicScale = getHeuristicScale(routeType, isWater);
|
||||
const heuristic = (node) => {
|
||||
const [ax, ay] = pack.cells.p[node];
|
||||
const [bx, by] = pack.cells.p[exit];
|
||||
const dx = ax - bx, dy = ay - by;
|
||||
const d = Math.hypot(dx, dy);
|
||||
return d * heuristicScale;
|
||||
};
|
||||
let pathCells = findPathAStar(start, exit, getCost, heuristic);
|
||||
if (!pathCells) {
|
||||
// Fallback to Dijkstra if A* fails or exceeds caps
|
||||
pathCells = findPath(start, current => current === exit, getCost);
|
||||
}
|
||||
if (!pathCells) return [];
|
||||
const segments = getRouteSegments(pathCells, connections);
|
||||
return segments;
|
||||
|
|
@ -859,20 +925,53 @@ window.Routes = (function () {
|
|||
}
|
||||
}
|
||||
|
||||
// Simple spatial hash for fast radius queries
|
||||
function makeSpatialHash(items, getXY, binSize) {
|
||||
const bins = new Map();
|
||||
for (const it of items) {
|
||||
const [x, y] = getXY(it);
|
||||
const ix = Math.floor(x / binSize);
|
||||
const iy = Math.floor(y / binSize);
|
||||
const key = ix + "," + iy;
|
||||
let arr = bins.get(key);
|
||||
if (!arr) { arr = []; bins.set(key, arr); }
|
||||
arr.push(it);
|
||||
}
|
||||
return {bins, binSize};
|
||||
}
|
||||
|
||||
function queryCandidatesWithinRadius(hash, x, y, radius) {
|
||||
const out = [];
|
||||
const s = hash.binSize;
|
||||
const r = Math.ceil(radius / s);
|
||||
const ix0 = Math.floor((x - radius) / s);
|
||||
const iy0 = Math.floor((y - radius) / s);
|
||||
const ix1 = Math.floor((x + radius) / s);
|
||||
const iy1 = Math.floor((y + radius) / s);
|
||||
for (let ix = ix0; ix <= ix1; ix++) {
|
||||
for (let iy = iy0; iy <= iy1; iy++) {
|
||||
const arr = hash.bins.get(ix + "," + iy);
|
||||
if (!arr) continue;
|
||||
for (const it of arr) out.push(it);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function createCostEvaluator({isWater, connections, routeType = "market"}) {
|
||||
return isWater ? getWaterPathCost : getLandPathCost;
|
||||
|
||||
function getLandPathCost(current, next) {
|
||||
if (pack.cells.h[next] < 20) return Infinity; // ignore water cells
|
||||
if (!RC || !RC.isPassableLand[next]) return Infinity;
|
||||
|
||||
const habitability = biomesData.habitability[pack.cells.biome[next]];
|
||||
if (!habitability) return Infinity; // inhabitable cells are not passable (e.g. glacier)
|
||||
|
||||
const distanceCost = dist2(pack.cells.p[current], pack.cells.p[next]);
|
||||
const habitabilityModifier = 1 + Math.max(100 - habitability, 0) / 1000; // [1, 1.1];
|
||||
const heightModifier = 1 + Math.max(pack.cells.h[next] - 25, 25) / 25; // [1, 3];
|
||||
const connectionModifier = connections.has(`${current}-${next}`) ? 0.5 : 1;
|
||||
const burgModifier = pack.cells.burg[next] ? 1 : 3;
|
||||
const [ax, ay] = pack.cells.p[current];
|
||||
const [bx, by] = pack.cells.p[next];
|
||||
const distanceCost = Math.hypot(ax - bx, ay - by);
|
||||
const habitabilityModifier = RC.landHabitability[next];
|
||||
const heightModifier = RC.landHeight[next];
|
||||
const setA = connections.get(current);
|
||||
const connectionModifier = setA && setA.has(next) ? 0.5 : 1;
|
||||
const burgModifier = RC.burgFactor[next];
|
||||
|
||||
// Medieval travel constraints
|
||||
const riverCrossingPenalty = pack.cells.r[next] && !pack.cells.burg[next] ? 1.5 : 1; // Bridges rare except at settlements
|
||||
|
|
@ -887,12 +986,14 @@ window.Routes = (function () {
|
|||
}
|
||||
|
||||
function getWaterPathCost(current, next) {
|
||||
if (pack.cells.h[next] >= 20) return Infinity; // ignore land cells
|
||||
if (grid.cells.temp[pack.cells.g[next]] < MIN_PASSABLE_SEA_TEMP) return Infinity; // ignore too cold cells
|
||||
if (!RC || !RC.isPassableWater[next]) return Infinity;
|
||||
|
||||
const distanceCost = dist2(pack.cells.p[current], pack.cells.p[next]);
|
||||
const typeModifier = ROUTE_TYPE_MODIFIERS[pack.cells.t[next]] || ROUTE_TYPE_MODIFIERS.default;
|
||||
const connectionModifier = connections.has(`${current}-${next}`) ? 0.5 : 1;
|
||||
const [ax, ay] = pack.cells.p[current];
|
||||
const [bx, by] = pack.cells.p[next];
|
||||
const distanceCost = Math.hypot(ax - bx, ay - by);
|
||||
const typeModifier = RC.waterType[next];
|
||||
const setA = connections.get(current);
|
||||
const connectionModifier = setA && setA.has(next) ? 0.5 : 1;
|
||||
|
||||
// Apply route tier modifier for sea routes
|
||||
const tierModifier = ROUTE_TIER_MODIFIERS[routeType]?.cost || 1;
|
||||
|
|
@ -917,6 +1018,12 @@ window.Routes = (function () {
|
|||
}
|
||||
}
|
||||
|
||||
function getHeuristicScale(routeType, isWater) {
|
||||
// Conservative lower bound for per-edge modifier to keep heuristic admissible
|
||||
const tier = ROUTE_TIER_MODIFIERS[routeType]?.cost || 1;
|
||||
return 0.5 * tier;
|
||||
}
|
||||
|
||||
function buildLinks(routes) {
|
||||
const links = {};
|
||||
|
||||
|
|
@ -999,7 +1106,7 @@ window.Routes = (function () {
|
|||
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}`);
|
||||
const isConnected = nextCellId !== undefined && ((connections.get(cellId)?.has(nextCellId)) || (connections.get(nextCellId)?.has(cellId)));
|
||||
|
||||
if (isConnected) {
|
||||
if (segment.length) {
|
||||
|
|
@ -1288,7 +1395,19 @@ window.Routes = (function () {
|
|||
};
|
||||
|
||||
function generateName({group, points}) {
|
||||
if (points.length < 4) return "Unnamed route segment";
|
||||
if (points.length < 4) {
|
||||
const start = points[0]?.[2];
|
||||
const end = points.at(-1)?.[2];
|
||||
const startB = start != null ? pack.cells.burg[start] : 0;
|
||||
const endB = end != null ? pack.cells.burg[end] : 0;
|
||||
const startName = startB ? getAdjective(pack.burgs[startB].name) : null;
|
||||
const endName = endB ? getAdjective(pack.burgs[endB].name) : null;
|
||||
const base = group === "searoutes" ? "Sea route" : group === "secondary" || group === "roads" ? "Road" : "Trail";
|
||||
if (startName && endName) return `${base} ${startName}–${endName}`;
|
||||
if (startName) return `${base} from ${startName}`;
|
||||
if (endName) return `${base} to ${endName}`;
|
||||
return `${base} segment`;
|
||||
}
|
||||
|
||||
const model = rw(models[group]);
|
||||
const suffix = rw(suffixes[group]);
|
||||
|
|
@ -1298,7 +1417,7 @@ window.Routes = (function () {
|
|||
if (model === "prefix_suffix") return `${ra(prefixes)} ${suffix}`;
|
||||
if (model === "the_descriptor_prefix_suffix") return `The ${ra(descriptors)} ${ra(prefixes)} ${suffix}`;
|
||||
if (model === "the_descriptor_burg_suffix" && burgName) return `The ${ra(descriptors)} ${burgName} ${suffix}`;
|
||||
return "Unnamed route";
|
||||
return group === "searoutes" ? "Sea route" : "Route";
|
||||
|
||||
function getBurgName() {
|
||||
const priority = [points.at(-1), points.at(0), points.slice(1, -1).reverse()];
|
||||
|
|
@ -1325,6 +1444,17 @@ window.Routes = (function () {
|
|||
return path;
|
||||
}
|
||||
|
||||
// Compute path length in pixels from list of cell ids
|
||||
function pathCellsLengthPx(cells) {
|
||||
let len = 0;
|
||||
for (let i = 0; i < cells.length - 1; i++) {
|
||||
const [x1, y1] = pack.cells.p[cells[i]];
|
||||
const [x2, y2] = pack.cells.p[cells[i + 1]];
|
||||
len += Math.hypot(x2 - x1, y2 - y1);
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
function getLength(routeId) {
|
||||
const path = routes.select("#route" + routeId).node();
|
||||
if (!path) {
|
||||
|
|
|
|||
|
|
@ -98,7 +98,21 @@ function overviewRoutes() {
|
|||
function zoomToRoute() {
|
||||
const routeId = +this.parentNode.dataset.id;
|
||||
const route = routes.select("#route" + routeId).node();
|
||||
highlightElement(route, 3);
|
||||
if (route) {
|
||||
highlightElement(route, 3);
|
||||
return;
|
||||
}
|
||||
// Fallback if SVG element not yet in DOM: center on route points
|
||||
const r = pack.routes.find(r => r.i === routeId);
|
||||
if (!r || !r.points || r.points.length === 0) return;
|
||||
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||||
for (const [x, y] of r.points) {
|
||||
if (x < minX) minX = x; if (x > maxX) maxX = x;
|
||||
if (y < minY) minY = y; if (y > maxY) maxY = y;
|
||||
}
|
||||
const cx = (minX + maxX) / 2;
|
||||
const cy = (minY + maxY) / 2;
|
||||
zoomTo(cx, cy, 3, 1200);
|
||||
}
|
||||
|
||||
function downloadRoutesData() {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,6 @@ else
|
|||
exit 1
|
||||
fi
|
||||
|
||||
chromium http://localhost:8000
|
||||
chromium http://localhost:9001
|
||||
|
||||
$PYTHON -m http.server 8000
|
||||
$PYTHON -m http.server 9001
|
||||
|
|
|
|||
|
|
@ -216,6 +216,62 @@ function findPath(start, isExit, getCost) {
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A* shortest path between two cells using an admissible heuristic.
|
||||
* Uses a reusable FlatQueue and typed arrays to minimize allocations.
|
||||
* @param {number} start - start cell id
|
||||
* @param {number} goal - goal cell id
|
||||
* @param {(current:number,next:number)=>number} getCost - edge cost; return Infinity for impassable
|
||||
* @param {(id:number)=>number} heuristic - estimated remaining cost from id to goal; must be admissible
|
||||
* @returns {number[]|null}
|
||||
*/
|
||||
function findPathAStar(start, goal, getCost, heuristic) {
|
||||
if (start === goal) return null;
|
||||
|
||||
// reusable arrays sized to pack.cells.c.length
|
||||
const n = pack.cells.c.length;
|
||||
if (!findPathAStar._g || findPathAStar._g.length !== n) {
|
||||
findPathAStar._g = new Float64Array(n);
|
||||
findPathAStar._f = new Float64Array(n);
|
||||
findPathAStar._from = new Int32Array(n);
|
||||
}
|
||||
const g = findPathAStar._g;
|
||||
const f = findPathAStar._f;
|
||||
const from = findPathAStar._from;
|
||||
for (let i = 0; i < n; i++) { g[i] = Infinity; f[i] = Infinity; from[i] = -1; }
|
||||
|
||||
const open = new FlatQueue();
|
||||
g[start] = 0;
|
||||
f[start] = heuristic(start);
|
||||
open.push(start, f[start]);
|
||||
|
||||
// simple safety cap to avoid pathological searches
|
||||
const maxPops = n * 4;
|
||||
let pops = 0;
|
||||
while (open.length) {
|
||||
if (pops++ > maxPops) return null;
|
||||
const currentF = open.peekValue();
|
||||
const current = open.pop();
|
||||
if (current === goal) return restorePath(goal, start, from);
|
||||
if (currentF > f[current]) continue; // stale entry
|
||||
|
||||
const neigh = pack.cells.c[current];
|
||||
for (let k = 0; k < neigh.length; k++) {
|
||||
const next = neigh[k];
|
||||
const edge = getCost(current, next);
|
||||
if (edge === Infinity) continue;
|
||||
const tentativeG = g[current] + edge;
|
||||
if (tentativeG >= g[next]) continue;
|
||||
from[next] = current;
|
||||
g[next] = tentativeG;
|
||||
f[next] = tentativeG + heuristic(next);
|
||||
open.push(next, f[next]);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// supplementary function for findPath
|
||||
function restorePath(exit, start, from) {
|
||||
const pathCells = [];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue