mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-17 09:41:24 +01:00
Implement hierarchical burg placement and route generation system
Major Changes: - Enhanced burg placement system with three-tier hierarchy: * Primary centers (capitals + large ports) connected by main roads * Regional centers (plaza burgs) connected by secondary roads * Local settlements connected by trails to existing network Burg Placement Improvements (burgs-and-states.js): - Added identifyLargePorts() function to mark coastal settlements as major population centers - Implemented placeRegionalCenters() function for strategic plaza burg placement - Enhanced placeTowns() with hierarchical scoring based on distance to major centers - Updated population calculations to respect settlement hierarchy - Modified defineBurgFeatures() to guarantee plazas for regional centers Route Generation Overhaul (routes-generator.js): - Created hierarchical route system eliminating overlapping routes: * Main roads connect primary population centers (capitals + large ports) * Secondary roads connect plaza burgs to main network and each other * Trails connect isolated settlements to nearest existing routes - Added filtered burg categorization to prevent duplicate connections - Implemented intelligent pathfinding that integrates with existing routes - Fixed getLength() function with fallback calculation for DOM timing issues CSV Export Enhancement (routes-overview.js): - Updated routes CSV export to include new "secondary" route type - Added documentation for supported route types in export function Technical Features: - Distance-based population gradients radiating from major centers - Urquhart graph algorithm for optimal route networks - Integration with existing pathfinding cost system - Proper route merging and connection tracking - Robust error handling for route length calculations Result: - Realistic settlement hierarchy with proper population distribution - Non-overlapping transportation network with clear purpose for each route type - Radial patterns from major centers through regional hubs to local settlements - Enhanced world-building with economically logical settlement placement
This commit is contained in:
parent
51572e34a8
commit
9c090894f2
3 changed files with 485 additions and 43 deletions
|
|
@ -10,6 +10,8 @@ window.BurgsAndStates = (() => {
|
|||
const burgs = (pack.burgs = placeCapitals());
|
||||
pack.states = createStates();
|
||||
|
||||
identifyLargePorts();
|
||||
placeRegionalCenters();
|
||||
placeTowns();
|
||||
expandStates();
|
||||
normalizeStates();
|
||||
|
|
@ -111,35 +113,248 @@ window.BurgsAndStates = (() => {
|
|||
return states;
|
||||
}
|
||||
|
||||
// place secondary settlements based on geo and economical evaluation
|
||||
// identify and mark large ports as primary population centers
|
||||
function identifyLargePorts() {
|
||||
TIME && console.time("identifyLargePorts");
|
||||
const {cells, features} = pack;
|
||||
const temp = grid.cells.temp;
|
||||
|
||||
// Track primary population centers (capitals + large ports)
|
||||
pack.primaryCenters = burgs.slice(1).map(b => b.i); // Start with capitals
|
||||
|
||||
const potentialPorts = [];
|
||||
|
||||
// Find potential large port locations
|
||||
for (const b of burgs) {
|
||||
if (!b.i || b.i === 0) continue; // Skip first element and undefined burgs
|
||||
|
||||
const i = b.cell;
|
||||
const haven = cells.haven[i];
|
||||
|
||||
if (haven && temp[cells.g[i]] > 0) {
|
||||
const f = cells.f[haven];
|
||||
const waterBodySize = features[f].cells;
|
||||
const harborQuality = cells.harbor[i];
|
||||
|
||||
// Large port criteria: large water body and good harbor
|
||||
if (waterBodySize > 10 && harborQuality === 1) {
|
||||
const portScore = waterBodySize * harborQuality + cells.s[i];
|
||||
potentialPorts.push({burg: b, score: portScore, waterBody: f});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by score and select best ports, ensuring spacing
|
||||
potentialPorts.sort((a, b) => b.score - a.score);
|
||||
const burgsTree = d3.quadtree();
|
||||
|
||||
// Add existing capitals to tree
|
||||
for (const b of burgs) {
|
||||
if (b.i && b.capital) {
|
||||
burgsTree.add([b.x, b.y]);
|
||||
}
|
||||
}
|
||||
|
||||
const minPortSpacing = (graphWidth + graphHeight) / 8; // Minimum spacing between major ports
|
||||
|
||||
for (const portData of potentialPorts) {
|
||||
const b = portData.burg;
|
||||
if (burgsTree.find(b.x, b.y, minPortSpacing) === undefined) {
|
||||
b.isLargePort = true;
|
||||
b.port = portData.waterBody;
|
||||
pack.primaryCenters.push(b.i);
|
||||
burgsTree.add([b.x, b.y]);
|
||||
}
|
||||
}
|
||||
|
||||
TIME && console.timeEnd("identifyLargePorts");
|
||||
}
|
||||
|
||||
// place regional centers (plaza burgs) between primary population centers
|
||||
function placeRegionalCenters() {
|
||||
TIME && console.time("placeRegionalCenters");
|
||||
const {cells} = pack;
|
||||
|
||||
if (!pack.primaryCenters || pack.primaryCenters.length < 2) {
|
||||
pack.regionalCenters = [];
|
||||
TIME && console.timeEnd("placeRegionalCenters");
|
||||
return;
|
||||
}
|
||||
|
||||
pack.regionalCenters = [];
|
||||
const primaryBurgs = pack.primaryCenters.map(id => burgs[id]);
|
||||
|
||||
// Calculate target number of regional centers
|
||||
const targetRegionalCenters = Math.max(1, Math.floor(pack.primaryCenters.length * 0.6));
|
||||
|
||||
const score = new Int16Array(cells.s.map(s => s * (0.7 + Math.random() * 0.6))); // Regional center score
|
||||
const candidates = cells.i
|
||||
.filter(i => !cells.burg[i] && score[i] > 0 && cells.culture[i])
|
||||
.sort((a, b) => score[b] - score[a]);
|
||||
|
||||
const burgsTree = d3.quadtree();
|
||||
|
||||
// Add primary centers to tree
|
||||
primaryBurgs.forEach(b => burgsTree.add([b.x, b.y]));
|
||||
|
||||
const minPrimaryDistance = (graphWidth + graphHeight) / 12; // Min distance from primary centers
|
||||
const minRegionalSpacing = (graphWidth + graphHeight) / 20; // Min distance between regional centers
|
||||
|
||||
let placedRegional = 0;
|
||||
|
||||
for (const cell of candidates) {
|
||||
if (placedRegional >= targetRegionalCenters) break;
|
||||
|
||||
const [x, y] = cells.p[cell];
|
||||
|
||||
// Check distance from primary centers (should be far enough to be useful)
|
||||
const nearestPrimary = burgsTree.find(x, y, minPrimaryDistance);
|
||||
if (nearestPrimary) continue;
|
||||
|
||||
// Check distance from other regional centers
|
||||
let tooClose = false;
|
||||
for (const regionalId of pack.regionalCenters) {
|
||||
const regional = burgs[regionalId];
|
||||
const distance = Math.sqrt((x - regional.x) ** 2 + (y - regional.y) ** 2);
|
||||
if (distance < minRegionalSpacing) {
|
||||
tooClose = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (tooClose) continue;
|
||||
|
||||
// Create regional center burg
|
||||
const burg = burgs.length;
|
||||
const culture = cells.culture[cell];
|
||||
const name = Names.getCulture(culture);
|
||||
|
||||
burgs.push({
|
||||
cell, x, y,
|
||||
state: 0,
|
||||
i: burg,
|
||||
culture,
|
||||
name,
|
||||
capital: 0,
|
||||
feature: cells.f[cell],
|
||||
isRegionalCenter: true,
|
||||
guaranteedPlaza: true
|
||||
});
|
||||
|
||||
cells.burg[cell] = burg;
|
||||
pack.regionalCenters.push(burg);
|
||||
placedRegional++;
|
||||
}
|
||||
|
||||
TIME && console.timeEnd("placeRegionalCenters");
|
||||
}
|
||||
|
||||
// place secondary settlements based on hierarchical population distribution
|
||||
function placeTowns() {
|
||||
TIME && console.time("placeTowns");
|
||||
const score = new Int16Array(cells.s.map(s => s * gauss(1, 3, 0, 20, 3))); // a bit randomized cell score for towns placement
|
||||
|
||||
// Helper function to calculate distance-based population multiplier
|
||||
const getPopulationMultiplier = (cellId) => {
|
||||
const [x, y] = cells.p[cellId];
|
||||
let minDistanceToPrimary = Infinity;
|
||||
let minDistanceToRegional = Infinity;
|
||||
|
||||
// Find distance to nearest primary center (capital or large port)
|
||||
if (pack.primaryCenters) {
|
||||
for (const primaryId of pack.primaryCenters) {
|
||||
const primary = burgs[primaryId];
|
||||
if (primary) {
|
||||
const distance = Math.sqrt((x - primary.x) ** 2 + (y - primary.y) ** 2);
|
||||
minDistanceToPrimary = Math.min(minDistanceToPrimary, distance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find distance to nearest regional center
|
||||
if (pack.regionalCenters) {
|
||||
for (const regionalId of pack.regionalCenters) {
|
||||
const regional = burgs[regionalId];
|
||||
if (regional) {
|
||||
const distance = Math.sqrt((x - regional.x) ** 2 + (y - regional.y) ** 2);
|
||||
minDistanceToRegional = Math.min(minDistanceToRegional, distance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate influence from primary centers (stronger influence)
|
||||
const primaryInfluence = minDistanceToPrimary === Infinity ? 0 :
|
||||
Math.max(0, 1 - (minDistanceToPrimary / ((graphWidth + graphHeight) / 4)));
|
||||
|
||||
// Calculate influence from regional centers (medium influence)
|
||||
const regionalInfluence = minDistanceToRegional === Infinity ? 0 :
|
||||
Math.max(0, 0.6 - (minDistanceToRegional / ((graphWidth + graphHeight) / 6)));
|
||||
|
||||
// Combine influences with primary having more weight
|
||||
const combinedInfluence = Math.max(primaryInfluence * 1.0, regionalInfluence * 0.7);
|
||||
|
||||
// Return multiplier between 0.3 and 1.5
|
||||
return 0.3 + combinedInfluence * 1.2;
|
||||
};
|
||||
|
||||
// Calculate hierarchical score for each cell
|
||||
const baseScore = cells.s.map(s => s * gauss(1, 3, 0, 20, 3));
|
||||
const hierarchicalScore = new Float32Array(cells.i.length);
|
||||
|
||||
for (const i of cells.i) {
|
||||
if (cells.burg[i] || !cells.culture[i] || baseScore[i] <= 0) {
|
||||
hierarchicalScore[i] = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
const populationMultiplier = getPopulationMultiplier(i);
|
||||
hierarchicalScore[i] = baseScore[i] * populationMultiplier;
|
||||
}
|
||||
|
||||
const sorted = cells.i
|
||||
.filter(i => !cells.burg[i] && score[i] > 0 && cells.culture[i])
|
||||
.sort((a, b) => score[b] - score[a]); // filtered and sorted array of indexes
|
||||
.filter(i => hierarchicalScore[i] > 0)
|
||||
.sort((a, b) => hierarchicalScore[b] - hierarchicalScore[a]);
|
||||
|
||||
const desiredNumber =
|
||||
manorsInput.value == 100000
|
||||
? rn(sorted.length / 5 / (grid.points.length / 10000) ** 0.8)
|
||||
: manorsInput.valueAsNumber;
|
||||
const burgsNumber = Math.min(desiredNumber, sorted.length); // towns to generate
|
||||
const burgsNumber = Math.min(desiredNumber, sorted.length);
|
||||
let burgsAdded = 0;
|
||||
|
||||
const burgsTree = burgs[0];
|
||||
let spacing = (graphWidth + graphHeight) / 150 / (burgsNumber ** 0.7 / 66); // min distance between towns
|
||||
let spacing = (graphWidth + graphHeight) / 150 / (burgsNumber ** 0.7 / 66);
|
||||
|
||||
// Add existing burgs to tree for spacing calculations
|
||||
for (let i = 1; i < burgs.length; i++) {
|
||||
if (burgs[i] && burgs[i].x !== undefined) {
|
||||
burgsTree.add([burgs[i].x, burgs[i].y]);
|
||||
}
|
||||
}
|
||||
|
||||
while (burgsAdded < burgsNumber && spacing > 1) {
|
||||
for (let i = 0; burgsAdded < burgsNumber && i < sorted.length; i++) {
|
||||
if (cells.burg[sorted[i]]) continue;
|
||||
const cell = sorted[i];
|
||||
const [x, y] = cells.p[cell];
|
||||
const s = spacing * gauss(1, 0.3, 0.2, 2, 2); // randomize to make placement not uniform
|
||||
if (burgsTree.find(x, y, s) !== undefined) continue; // to close to existing burg
|
||||
|
||||
// Adjust spacing based on hierarchy - closer spacing near population centers
|
||||
const populationMultiplier = getPopulationMultiplier(cell);
|
||||
const adjustedSpacing = spacing * gauss(1, 0.3, 0.2, 2, 2) * (2 - populationMultiplier);
|
||||
|
||||
if (burgsTree.find(x, y, adjustedSpacing) !== undefined) continue;
|
||||
|
||||
const burg = burgs.length;
|
||||
const culture = cells.culture[cell];
|
||||
const name = Names.getCulture(culture);
|
||||
burgs.push({cell, x, y, state: 0, i: burg, culture, name, capital: 0, feature: cells.f[cell]});
|
||||
burgs.push({
|
||||
cell, x, y,
|
||||
state: 0,
|
||||
i: burg,
|
||||
culture,
|
||||
name,
|
||||
capital: 0,
|
||||
feature: cells.f[cell],
|
||||
hierarchicalScore: hierarchicalScore[cell]
|
||||
});
|
||||
burgsTree.add([x, y]);
|
||||
cells.burg[cell] = burg;
|
||||
burgsAdded++;
|
||||
|
|
@ -151,7 +366,7 @@ window.BurgsAndStates = (() => {
|
|||
ERROR && console.error(`Cannot place all burgs. Requested ${desiredNumber}, placed ${burgsAdded}`);
|
||||
}
|
||||
|
||||
burgs[0] = {name: undefined}; // do not store burgsTree anymore
|
||||
burgs[0] = {name: undefined};
|
||||
TIME && console.timeEnd("placeTowns");
|
||||
}
|
||||
};
|
||||
|
|
@ -175,19 +390,38 @@ window.BurgsAndStates = (() => {
|
|||
b.port = port ? f : 0; // port is defined by water body id it lays on
|
||||
} else b.port = 0;
|
||||
|
||||
// define burg population (keep urbanization at about 10% rate)
|
||||
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
|
||||
// calculate hierarchical population based on burg type and position
|
||||
let basePopulation = Math.max(cells.s[i] / 8 + b.i / 1000 + (i % 100) / 1000, 0.1);
|
||||
|
||||
// Apply hierarchical multipliers
|
||||
if (b.capital) {
|
||||
basePopulation *= 1.8; // Capitals are major population centers
|
||||
} else if (b.isLargePort) {
|
||||
basePopulation *= 1.6; // Large ports are significant population centers
|
||||
} else if (b.isRegionalCenter) {
|
||||
basePopulation *= 1.3; // Regional centers have elevated population
|
||||
} else if (b.hierarchicalScore) {
|
||||
// Use the hierarchical score calculated during placement for population gradient
|
||||
const maxHierarchicalScore = Math.max(...pack.burgs.filter(burg => burg.hierarchicalScore).map(burg => burg.hierarchicalScore));
|
||||
if (maxHierarchicalScore > 0) {
|
||||
const hierarchicalMultiplier = 0.7 + (b.hierarchicalScore / maxHierarchicalScore) * 0.6;
|
||||
basePopulation *= hierarchicalMultiplier;
|
||||
}
|
||||
}
|
||||
|
||||
b.population = rn(basePopulation, 3);
|
||||
|
||||
if (b.port) {
|
||||
b.population = b.population * 1.3; // increase port population
|
||||
if (!b.isLargePort) {
|
||||
b.population = b.population * 1.2; // Moderate boost for regular ports
|
||||
}
|
||||
const [x, y] = getCloseToEdgePoint(i, haven);
|
||||
b.x = x;
|
||||
b.y = y;
|
||||
}
|
||||
|
||||
// add random factor
|
||||
b.population = rn(b.population * gauss(2, 3, 0.6, 20, 3), 3);
|
||||
// add random factor (reduced to maintain hierarchy)
|
||||
b.population = rn(b.population * gauss(1.8, 2.5, 0.7, 15, 2.5), 3);
|
||||
|
||||
// shift burgs on rivers semi-randomly and just a bit
|
||||
if (!b.port && cells.r[i]) {
|
||||
|
|
@ -268,15 +502,53 @@ window.BurgsAndStates = (() => {
|
|||
.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));
|
||||
b.plaza = Number(pop > 20 || (pop > 10 && P(0.8)) || (pop > 4 && P(0.7)) || P(0.6));
|
||||
b.walls = Number(b.capital || pop > 30 || (pop > 20 && P(0.75)) || (pop > 10 && P(0.5)) || P(0.1));
|
||||
|
||||
// Citadel assignment - capitals and major centers get priority
|
||||
b.citadel = Number(b.capital || b.isLargePort || (pop > 50 && P(0.75)) || (pop > 15 && P(0.5)) || P(0.1));
|
||||
|
||||
// Plaza assignment - ensure regional centers get plazas, scale with hierarchy
|
||||
if (b.guaranteedPlaza || b.isRegionalCenter) {
|
||||
b.plaza = 1; // Regional centers always get plazas
|
||||
} else if (b.capital || b.isLargePort) {
|
||||
b.plaza = Number(P(0.9)); // Primary centers very likely to have plazas
|
||||
} else {
|
||||
// Regular settlements based on population and proximity to centers
|
||||
let plazaChance = 0.6;
|
||||
if (pop > 20) plazaChance = 0.9;
|
||||
else if (pop > 10) plazaChance = 0.8;
|
||||
else if (pop > 4) plazaChance = 0.7;
|
||||
|
||||
// Reduce chance if far from any major center
|
||||
if (b.hierarchicalScore) {
|
||||
const maxScore = Math.max(...pack.burgs.filter(burg => burg.hierarchicalScore).map(burg => burg.hierarchicalScore));
|
||||
if (maxScore > 0) {
|
||||
const hierarchyFactor = b.hierarchicalScore / maxScore;
|
||||
plazaChance *= (0.5 + hierarchyFactor * 0.5); // Scale with hierarchy
|
||||
}
|
||||
}
|
||||
|
||||
b.plaza = Number(P(plazaChance));
|
||||
}
|
||||
|
||||
// Walls assignment - hierarchy-aware
|
||||
b.walls = Number(b.capital || b.isLargePort || pop > 30 || (pop > 20 && P(0.75)) || (pop > 10 && P(0.5)) || P(0.1));
|
||||
|
||||
// Shanty assignment - more common in larger population centers
|
||||
b.shanty = Number(pop > 60 || (pop > 40 && P(0.75)) || (pop > 20 && b.walls && P(0.4)));
|
||||
|
||||
// Temple assignment - influenced by hierarchy and theocracy
|
||||
const religion = cells.religion[b.cell];
|
||||
const theocracy = pack.states[b.state].form === "Theocracy";
|
||||
b.temple = Number(
|
||||
(religion && theocracy && P(0.5)) || pop > 50 || (pop > 35 && P(0.75)) || (pop > 20 && P(0.5))
|
||||
);
|
||||
let templeChance = 0;
|
||||
|
||||
if (religion && theocracy && P(0.5)) templeChance = 1;
|
||||
else if (b.capital || b.isLargePort) templeChance = 0.8;
|
||||
else if (b.isRegionalCenter) templeChance = 0.6;
|
||||
else if (pop > 50) templeChance = 0.7;
|
||||
else if (pop > 35) templeChance = 0.5;
|
||||
else if (pop > 20) templeChance = 0.3;
|
||||
|
||||
b.temple = Number(P(templeChance));
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -12,12 +12,13 @@ const ROUTE_TYPE_MODIFIERS = {
|
|||
|
||||
window.Routes = (function () {
|
||||
function generate(lockedRoutes = []) {
|
||||
const {capitalsByFeature, burgsByFeature, portsByFeature} = sortBurgsByFeature(pack.burgs);
|
||||
const {capitalsByFeature, burgsByFeature, portsByFeature, primaryByFeature, plazaByFeature, unconnectedBurgsByFeature} = sortBurgsByFeature(pack.burgs);
|
||||
|
||||
const connections = new Map();
|
||||
lockedRoutes.forEach(route => addConnections(route.points.map(p => p[2])));
|
||||
|
||||
const mainRoads = generateMainRoads();
|
||||
const secondaryRoads = generateSecondaryRoads();
|
||||
const trails = generateTrails();
|
||||
const seaRoutes = generateSeaRoutes();
|
||||
|
||||
|
|
@ -28,6 +29,9 @@ window.Routes = (function () {
|
|||
const burgsByFeature = {};
|
||||
const capitalsByFeature = {};
|
||||
const portsByFeature = {};
|
||||
const primaryByFeature = {}; // capitals + large ports
|
||||
const plazaByFeature = {}; // plaza burgs (excluding primary centers)
|
||||
const unconnectedBurgsByFeature = {}; // burgs not connected by main roads or secondary roads
|
||||
|
||||
const addBurg = (object, feature, burg) => {
|
||||
if (!object[feature]) object[feature] = [];
|
||||
|
|
@ -38,24 +42,40 @@ window.Routes = (function () {
|
|||
if (burg.i && !burg.removed) {
|
||||
const {feature, capital, port} = burg;
|
||||
addBurg(burgsByFeature, feature, burg);
|
||||
|
||||
if (capital) addBurg(capitalsByFeature, feature, burg);
|
||||
if (port) addBurg(portsByFeature, port, burg);
|
||||
|
||||
// Primary centers: capitals and large ports
|
||||
if (capital || burg.isLargePort) {
|
||||
addBurg(primaryByFeature, feature, burg);
|
||||
}
|
||||
|
||||
// Plaza burgs: those with plazas but not primary centers
|
||||
if ((burg.plaza || burg.isRegionalCenter || burg.guaranteedPlaza) && !capital && !burg.isLargePort) {
|
||||
addBurg(plazaByFeature, feature, burg);
|
||||
}
|
||||
|
||||
// Unconnected burgs: those not already connected by main roads or secondary roads
|
||||
if (!capital && !burg.isLargePort && !(burg.plaza || burg.isRegionalCenter || burg.guaranteedPlaza)) {
|
||||
addBurg(unconnectedBurgsByFeature, feature, burg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {burgsByFeature, capitalsByFeature, portsByFeature};
|
||||
return {burgsByFeature, capitalsByFeature, portsByFeature, primaryByFeature, plazaByFeature, unconnectedBurgsByFeature};
|
||||
}
|
||||
|
||||
function generateMainRoads() {
|
||||
TIME && console.time("generateMainRoads");
|
||||
const mainRoads = [];
|
||||
|
||||
for (const [key, featureCapitals] of Object.entries(capitalsByFeature)) {
|
||||
const points = featureCapitals.map(burg => [burg.x, burg.y]);
|
||||
for (const [key, featurePrimary] of Object.entries(primaryByFeature)) {
|
||||
const points = featurePrimary.map(burg => [burg.x, burg.y]);
|
||||
const urquhartEdges = calculateUrquhartEdges(points);
|
||||
urquhartEdges.forEach(([fromId, toId]) => {
|
||||
const start = featureCapitals[fromId].cell;
|
||||
const exit = featureCapitals[toId].cell;
|
||||
const start = featurePrimary[fromId].cell;
|
||||
const exit = featurePrimary[toId].cell;
|
||||
|
||||
const segments = findPathSegments({isWater: false, connections, start, exit});
|
||||
for (const segment of segments) {
|
||||
|
|
@ -69,16 +89,108 @@ window.Routes = (function () {
|
|||
return mainRoads;
|
||||
}
|
||||
|
||||
function generateSecondaryRoads() {
|
||||
TIME && console.time("generateSecondaryRoads");
|
||||
const secondaryRoads = [];
|
||||
|
||||
for (const [key, featurePlazas] of Object.entries(plazaByFeature)) {
|
||||
// Skip if no plaza burgs in this feature
|
||||
if (featurePlazas.length === 0) continue;
|
||||
|
||||
const featurePrimary = primaryByFeature[key] || [];
|
||||
|
||||
// Combine plaza burgs with primary centers for connection network
|
||||
const allConnectableBurgs = [...featurePlazas, ...featurePrimary];
|
||||
|
||||
// If we have primary centers in this feature, connect plazas to them
|
||||
if (featurePrimary.length > 0 && featurePlazas.length > 0) {
|
||||
// Connect each plaza to the nearest primary center
|
||||
for (const plazaBurg of featurePlazas) {
|
||||
let nearestPrimary = null;
|
||||
let minDistance = Infinity;
|
||||
|
||||
for (const primaryBurg of featurePrimary) {
|
||||
const distance = Math.sqrt(
|
||||
(plazaBurg.x - primaryBurg.x) ** 2 + (plazaBurg.y - primaryBurg.y) ** 2
|
||||
);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
nearestPrimary = primaryBurg;
|
||||
}
|
||||
}
|
||||
|
||||
if (nearestPrimary) {
|
||||
const segments = findPathSegments({
|
||||
isWater: false,
|
||||
connections,
|
||||
start: plazaBurg.cell,
|
||||
exit: nearestPrimary.cell
|
||||
});
|
||||
for (const segment of segments) {
|
||||
addConnections(segment);
|
||||
secondaryRoads.push({feature: Number(key), cells: segment});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Connect plaza burgs to each other if there are multiple
|
||||
if (featurePlazas.length >= 2) {
|
||||
const points = featurePlazas.map(burg => [burg.x, burg.y]);
|
||||
const urquhartEdges = calculateUrquhartEdges(points);
|
||||
urquhartEdges.forEach(([fromId, toId]) => {
|
||||
const start = featurePlazas[fromId].cell;
|
||||
const exit = featurePlazas[toId].cell;
|
||||
|
||||
const segments = findPathSegments({isWater: false, connections, start, exit});
|
||||
for (const segment of segments) {
|
||||
addConnections(segment);
|
||||
secondaryRoads.push({feature: Number(key), cells: segment});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
TIME && console.timeEnd("generateSecondaryRoads");
|
||||
return secondaryRoads;
|
||||
}
|
||||
|
||||
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]);
|
||||
for (const [key, unconnectedBurgs] of Object.entries(unconnectedBurgsByFeature)) {
|
||||
// Skip if no unconnected burgs in this feature
|
||||
if (unconnectedBurgs.length === 0) continue;
|
||||
|
||||
// Get all connected burgs in this feature (primary + plaza)
|
||||
const connectedBurgs = [...(primaryByFeature[key] || []), ...(plazaByFeature[key] || [])];
|
||||
|
||||
// Connect unconnected burgs to the network
|
||||
for (const unconnectedBurg of unconnectedBurgs) {
|
||||
if (connectedBurgs.length > 0) {
|
||||
// Find the best connection point (could be a burg or a point on an existing route)
|
||||
const bestConnection = findBestConnectionPoint(unconnectedBurg, connectedBurgs, connections);
|
||||
|
||||
if (bestConnection) {
|
||||
const segments = findPathSegments({
|
||||
isWater: false,
|
||||
connections,
|
||||
start: unconnectedBurg.cell,
|
||||
exit: bestConnection.cell
|
||||
});
|
||||
for (const segment of segments) {
|
||||
addConnections(segment);
|
||||
trails.push({feature: Number(key), cells: segment});
|
||||
}
|
||||
}
|
||||
} else if (unconnectedBurgs.length >= 2) {
|
||||
// If no connected burgs exist, create minimal trail network between unconnected burgs
|
||||
const points = unconnectedBurgs.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 start = unconnectedBurgs[fromId].cell;
|
||||
const exit = unconnectedBurgs[toId].cell;
|
||||
|
||||
const segments = findPathSegments({isWater: false, connections, start, exit});
|
||||
for (const segment of segments) {
|
||||
|
|
@ -86,12 +198,34 @@ window.Routes = (function () {
|
|||
trails.push({feature: Number(key), cells: segment});
|
||||
}
|
||||
});
|
||||
break; // Only do this once per feature
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TIME && console.timeEnd("generateTrails");
|
||||
return trails;
|
||||
}
|
||||
|
||||
// Helper function to find the best connection point for a trail
|
||||
function findBestConnectionPoint(unconnectedBurg, connectedBurgs, connections) {
|
||||
let bestConnection = null;
|
||||
let minDistance = Infinity;
|
||||
|
||||
// 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;
|
||||
bestConnection = connectedBurg;
|
||||
}
|
||||
}
|
||||
|
||||
return bestConnection;
|
||||
}
|
||||
|
||||
function generateSeaRoutes() {
|
||||
TIME && console.time("generateSeaRoutes");
|
||||
const seaRoutes = [];
|
||||
|
|
@ -143,6 +277,12 @@ window.Routes = (function () {
|
|||
routes.push({i: routes.length, group: "roads", feature, points});
|
||||
}
|
||||
|
||||
for (const {feature, cells, merged} of mergeRoutes(secondaryRoads)) {
|
||||
if (merged) continue;
|
||||
const points = getPoints("secondary", cells, pointsArray);
|
||||
routes.push({i: routes.length, group: "secondary", feature, points});
|
||||
}
|
||||
|
||||
for (const {feature, cells, merged} of mergeRoutes(trails)) {
|
||||
if (merged) continue;
|
||||
const points = getPoints("trails", cells, pointsArray);
|
||||
|
|
@ -421,20 +561,32 @@ window.Routes = (function () {
|
|||
});
|
||||
}
|
||||
|
||||
function hasSecondaryRoad(cellId) {
|
||||
const connections = pack.cells.routes[cellId];
|
||||
if (!connections) return false;
|
||||
|
||||
return Object.values(connections).some(routeId => {
|
||||
const route = pack.routes.find(route => route.i === routeId);
|
||||
if (!route) return false;
|
||||
return route.group === "secondary";
|
||||
});
|
||||
}
|
||||
|
||||
function isCrossroad(cellId) {
|
||||
const connections = pack.cells.routes[cellId];
|
||||
if (!connections) return false;
|
||||
if (Object.keys(connections).length > 3) return true;
|
||||
const roadConnections = Object.values(connections).filter(routeId => {
|
||||
const majorRoadConnections = Object.values(connections).filter(routeId => {
|
||||
const route = pack.routes.find(route => route.i === routeId);
|
||||
return route?.group === "roads";
|
||||
return route?.group === "roads" || route?.group === "secondary";
|
||||
});
|
||||
return roadConnections.length > 2;
|
||||
return majorRoadConnections.length > 2;
|
||||
}
|
||||
|
||||
// name generator data
|
||||
const models = {
|
||||
roads: {burg_suffix: 3, prefix_suffix: 6, the_descriptor_prefix_suffix: 2, the_descriptor_burg_suffix: 1},
|
||||
secondary: {burg_suffix: 5, prefix_suffix: 4, the_descriptor_prefix_suffix: 1, the_descriptor_burg_suffix: 2},
|
||||
trails: {burg_suffix: 8, prefix_suffix: 1, the_descriptor_burg_suffix: 1},
|
||||
searoutes: {burg_suffix: 4, prefix_suffix: 2, the_descriptor_prefix_suffix: 1}
|
||||
};
|
||||
|
|
@ -567,6 +719,7 @@ window.Routes = (function () {
|
|||
|
||||
const suffixes = {
|
||||
roads: {road: 7, route: 3, way: 2, highway: 1},
|
||||
secondary: {road: 4, route: 2, way: 3, avenue: 1, boulevard: 1},
|
||||
trails: {trail: 4, path: 1, track: 1, pass: 1},
|
||||
searoutes: {"sea route": 5, lane: 2, passage: 1, seaway: 1}
|
||||
};
|
||||
|
|
@ -596,6 +749,7 @@ window.Routes = (function () {
|
|||
|
||||
const ROUTE_CURVES = {
|
||||
roads: d3.curveCatmullRom.alpha(0.1),
|
||||
secondary: d3.curveCatmullRom.alpha(0.1),
|
||||
trails: d3.curveCatmullRom.alpha(0.1),
|
||||
searoutes: d3.curveCatmullRom.alpha(0.5),
|
||||
default: d3.curveCatmullRom.alpha(0.1)
|
||||
|
|
@ -610,6 +764,20 @@ window.Routes = (function () {
|
|||
|
||||
function getLength(routeId) {
|
||||
const path = routes.select("#route" + routeId).node();
|
||||
if (!path) {
|
||||
// Fallback: calculate length from route points if DOM element not available
|
||||
const route = pack.routes.find(r => r.i === routeId);
|
||||
if (route && route.points) {
|
||||
let length = 0;
|
||||
for (let i = 0; i < route.points.length - 1; i++) {
|
||||
const [x1, y1] = route.points[i];
|
||||
const [x2, y2] = route.points[i + 1];
|
||||
length += Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
|
||||
}
|
||||
return length;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return path.getTotalLength();
|
||||
}
|
||||
|
||||
|
|
@ -644,6 +812,7 @@ window.Routes = (function () {
|
|||
areConnected,
|
||||
getRoute,
|
||||
hasRoad,
|
||||
hasSecondaryRoad,
|
||||
isCrossroad,
|
||||
generateName,
|
||||
getPath,
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ function overviewRoutes() {
|
|||
}
|
||||
|
||||
function downloadRoutesData() {
|
||||
// Export all route types: roads (main), secondary (plaza connections), trails, searoutes
|
||||
let data = "Id,Route,Group,Length\n"; // headers
|
||||
|
||||
body.querySelectorAll(":scope > div").forEach(function (el) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue