diff --git a/index.html b/index.html
index cba2c7d9..098295ae 100644
--- a/index.html
+++ b/index.html
@@ -3409,6 +3409,11 @@
+
+
Temperature:
@@ -3443,6 +3448,18 @@
data-feature="port"
class="burgFeature icon-anchor"
>
+
+
b.capital && !b.removed);
+ // capitals (exclude sky burgs)
+ const capitals = pack.burgs.filter(b => b.capital && !b.removed && !(b.flying || b.skyPort));
const capitalIcons = burgIcons.select("#cities");
const capitalSize = capitalIcons.attr("size") || 1;
const capitalAnchors = anchors.selectAll("#cities");
@@ -35,8 +35,8 @@ function drawBurgIcons() {
.attr("width", capitalAnchorsSize)
.attr("height", capitalAnchorsSize);
- // towns
- const towns = pack.burgs.filter(b => b.i && !b.capital && !b.removed);
+ // towns (exclude sky burgs)
+ const towns = pack.burgs.filter(b => b.i && !b.capital && !b.removed && !(b.flying || b.skyPort));
const townIcons = burgIcons.select("#towns");
const townSize = townIcons.attr("size") || 0.5;
const townsAnchors = anchors.selectAll("#towns");
@@ -66,4 +66,20 @@ function drawBurgIcons() {
.attr("height", townsAnchorsSize);
TIME && console.timeEnd("drawBurgIcons");
+
+ // Sky burgs (flying or sky port) — styled separately
+ const sky = pack.burgs.filter(b => b.i && !b.removed && (b.flying || b.skyPort));
+ const skyIcons = burgIcons.select("#skyburgs");
+ const skySize = skyIcons.attr("size") || 0.6;
+
+ skyIcons
+ .selectAll("circle")
+ .data(sky)
+ .enter()
+ .append("circle")
+ .attr("id", d => "burg" + d.i)
+ .attr("data-id", d => d.i)
+ .attr("cx", d => d.x)
+ .attr("cy", d => d.y)
+ .attr("r", skySize);
}
diff --git a/modules/routes-generator.js b/modules/routes-generator.js
index 8627a992..de7ec3d1 100644
--- a/modules/routes-generator.js
+++ b/modules/routes-generator.js
@@ -37,7 +37,8 @@ window.Routes = (function () {
// PHASE 1: IMMEDIATE PROCESSING (blocking - critical routes for trade and diplomacy)
TIME && console.time("generateCriticalRoutes");
const majorSeaRoutes = generateMajorSeaRoutes(); // Tier 1: Long-distance maritime trade
- const royalRoads = generateRoyalRoads(); // Tier 2: Capital-to-capital connections
+ const royalRoads = generateRoyalRoads(); // Tier 2: Capital-to-capital roads
+ const airRoutes = generateAirRoutes(); // Sky trade between sky ports
TIME && console.timeEnd("generateCriticalRoutes");
// Create initial routes with critical paths only
@@ -831,8 +832,41 @@ window.Routes = (function () {
routes.push({i: routes.length, group: "roads", feature, points, type: type || "royal"});
}
+ // Air routes
+ for (const {feature, cells, merged, type} of mergeRoutes(airRoutes)) {
+ if (merged) continue;
+ const points = getPoints("airroutes", cells, pointsArray);
+ routes.push({i: routes.length, group: "airroutes", feature, points, type: type || "air"});
+ }
+
return routes;
}
+
+ // Connect sky ports (flying burgs) using sparse graph
+ function generateAirRoutes() {
+ TIME && console.time("generateAirRoutes");
+ const air = [];
+
+ const skyPorts = pack.burgs.filter(b => b && b.i && !b.removed && (b.skyPort || b.flying));
+ if (skyPorts.length < 2) {
+ TIME && console.timeEnd("generateAirRoutes");
+ return air;
+ }
+
+ // Use Urquhart edges to avoid a complete graph
+ const points = skyPorts.map(b => [b.x, b.y]);
+ const edges = calculateUrquhartEdges(points);
+ edges.forEach(([ai, bi]) => {
+ const a = skyPorts[ai];
+ const b = skyPorts[bi];
+ if (!a || !b) return;
+ // For air routes, we can connect directly between burg cells
+ air.push({feature: -1, cells: [a.cell, b.cell], type: "air"});
+ });
+
+ TIME && console.timeEnd("generateAirRoutes");
+ return air;
+ }
// Function to append background-generated routes to pack
function appendRoutesToPack(marketRoads, localRoads, footpaths, regionalSeaRoutes) {
@@ -1258,7 +1292,8 @@ window.Routes = (function () {
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}
+ searoutes: {burg_suffix: 4, prefix_suffix: 2, the_descriptor_prefix_suffix: 1},
+ airroutes: {burg_suffix: 3, prefix_suffix: 5, the_descriptor_prefix_suffix: 2}
};
const prefixes = [
@@ -1391,7 +1426,8 @@ window.Routes = (function () {
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}
+ searoutes: {"sea route": 5, lane: 2, passage: 1, seaway: 1},
+ airroutes: {"sky route": 5, "air lane": 3, skyway: 2, airway: 2, "aerial path": 1}
};
function generateName({group, points}) {
@@ -1402,7 +1438,13 @@ window.Routes = (function () {
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";
+ const base = group === "searoutes"
+ ? "Sea route"
+ : group === "airroutes"
+ ? "Sky 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}`;
@@ -1417,7 +1459,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 group === "searoutes" ? "Sea route" : "Route";
+ return group === "searoutes" ? "Sea route" : group === "airroutes" ? "Sky route" : "Route";
function getBurgName() {
const priority = [points.at(-1), points.at(0), points.slice(1, -1).reverse()];
@@ -1434,6 +1476,7 @@ window.Routes = (function () {
secondary: d3.curveCatmullRom.alpha(0.1),
trails: d3.curveCatmullRom.alpha(0.1),
searoutes: d3.curveCatmullRom.alpha(0.5),
+ airroutes: d3.curveCatmullRom.alpha(0.5),
default: d3.curveCatmullRom.alpha(0.1)
};
diff --git a/modules/ui/burg-editor.js b/modules/ui/burg-editor.js
index 13e32850..4c7cffc7 100644
--- a/modules/ui/burg-editor.js
+++ b/modules/ui/burg-editor.js
@@ -34,6 +34,7 @@ function editBurg(id) {
byId("burgCulture").addEventListener("input", changeCulture);
byId("burgNameReCulture").addEventListener("click", generateNameCulture);
byId("burgPopulation").addEventListener("change", changePopulation);
+ byId("burgAltitude")?.addEventListener("change", changeAltitude);
burgBody.querySelectorAll(".burgFeature").forEach(el => el.addEventListener("click", toggleFeature));
byId("burgLinkOpen").addEventListener("click", openBurgLink);
byId("burgLinkEdit").addEventListener("click", changeBurgLink);
@@ -77,13 +78,25 @@ function editBurg(id) {
byId("burgTemperature").innerHTML = convertTemperature(temperature);
byId("burgTemperatureLikeIn").dataset.tip =
"Average yearly temperature is like in " + getTemperatureLikeness(temperature);
- byId("burgElevation").innerHTML = getHeight(pack.cells.h[b.cell]);
+ const altitudeRow = byId("burgAltitudeRow");
+ if (b.flying) {
+ altitudeRow.style.display = "flex";
+ byId("burgAltitude").value = b.altitude ?? 1000;
+ byId("burgElevation").innerHTML = `${b.altitude ?? 1000} m (sky altitude)`;
+ } else {
+ altitudeRow.style.display = "none";
+ byId("burgElevation").innerHTML = getHeight(pack.cells.h[b.cell]);
+ }
// toggle features
if (b.capital) byId("burgCapital").classList.remove("inactive");
else byId("burgCapital").classList.add("inactive");
if (b.port) byId("burgPort").classList.remove("inactive");
else byId("burgPort").classList.add("inactive");
+ if (b.skyPort) byId("burgSkyPort").classList.remove("inactive");
+ else byId("burgSkyPort").classList.add("inactive");
+ if (b.flying) byId("burgFlying").classList.remove("inactive");
+ else byId("burgFlying").classList.add("inactive");
if (b.citadel) byId("burgCitadel").classList.remove("inactive");
else byId("burgCitadel").classList.add("inactive");
if (b.walls) byId("burgWalls").classList.remove("inactive");
@@ -292,12 +305,50 @@ function editBurg(id) {
updateBurgPreview(burg);
}
+ function changeAltitude() {
+ const id = +elSelected.attr("data-id");
+ const burg = pack.burgs[id];
+ burg.altitude = Math.max(0, Math.round(+byId("burgAltitude").value));
+ }
+
function toggleFeature() {
const id = +elSelected.attr("data-id");
const burg = pack.burgs[id];
const feature = this.dataset.feature;
const turnOn = this.classList.contains("inactive");
if (feature === "port") togglePort(id);
+ else if (feature === "skyPort") {
+ burg.skyPort = +turnOn;
+ // Assign to Sky State when turning on (if not a state capital)
+ if (turnOn) {
+ try {
+ if (!burg.capital) {
+ const skyId = ensureSkyState(id);
+ if (burg.state !== skyId) {
+ // Reassign cell ownership
+ pack.cells.state[burg.cell] = skyId;
+ burg.state = skyId;
+ }
+ }
+ } catch (e) { ERROR && console.error(e); }
+ }
+ // Regenerate routes to reflect air network
+ regenerateRoutes();
+ }
+ else if (feature === "flying") {
+ burg.flying = +turnOn;
+ if (turnOn) {
+ try {
+ const skyId = ensureSkyState(id);
+ if (burg.state !== skyId) {
+ pack.cells.state[burg.cell] = skyId;
+ burg.state = skyId;
+ }
+ if (burg.altitude == null) burg.altitude = 1000;
+ } catch (e) { ERROR && console.error(e); }
+ }
+ regenerateRoutes();
+ }
else if (feature === "capital") toggleCapital(id);
else burg[feature] = +turnOn;
if (burg[feature]) this.classList.remove("inactive");
@@ -434,8 +485,10 @@ function editBurg(id) {
const id = +elSelected.attr("data-id");
const burg = pack.burgs[id];
- if (cells.h[cell] < 20) {
- tip("Cannot place burg into the water! Select a land cell", false, "error");
+ const isWater = cells.h[cell] < 20;
+ const allowWater = pack.burgs[id]?.flying || d3.event.altKey;
+ if (isWater && !allowWater) {
+ tip("Hold Alt or mark as Flying to place over water", false, "error");
return;
}
@@ -447,7 +500,7 @@ function editBurg(id) {
const newState = cells.state[cell];
const oldState = burg.state;
- if (newState !== oldState && burg.capital) {
+ if (newState !== oldState && burg.capital && !isWater) {
tip("Capital cannot be relocated into another state!", false, "error");
return;
}
@@ -477,7 +530,15 @@ function editBurg(id) {
cells.burg[burg.cell] = 0;
cells.burg[cell] = id;
burg.cell = cell;
- burg.state = newState;
+
+ // Set target state based on terrain and sky features
+ if (isWater || burg.flying) {
+ const skyId = ensureSkyState(id);
+ cells.state[cell] = skyId;
+ burg.state = skyId;
+ } else {
+ burg.state = newState;
+ }
burg.x = x;
burg.y = y;
if (burg.capital) pack.states[newState].center = burg.cell;
diff --git a/modules/ui/burgs-overview.js b/modules/ui/burgs-overview.js
index 53a093d4..21309df1 100644
--- a/modules/ui/burgs-overview.js
+++ b/modules/ui/burgs-overview.js
@@ -281,12 +281,25 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) {
const point = d3.mouse(this);
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");
+ // Allow placing over water as a flying burg when Alt is held
+ if (pack.cells.h[cell] < 20 && !d3.event.altKey)
+ return tip("Hold Alt to place a flying burg over water", false, "error");
if (pack.cells.burg[cell])
return tip("There is already a burg in this cell. Please select a free cell", false, "error");
- addBurg(point); // add new burg
+ const id = addBurg(point); // add new burg
+
+ // Mark flying burgs and assign to Sky State, make them sky ports
+ if (pack.cells.h[cell] < 20) {
+ const burg = pack.burgs[id];
+ burg.flying = 1;
+ burg.skyPort = 1;
+ if (burg.altitude == null) burg.altitude = 1000;
+ const skyStateId = ensureSkyState(id);
+ if (burg.state !== skyStateId) burg.state = skyStateId;
+ // Keep as non-sea port
+ burg.port = 0;
+ }
if (d3.event.shiftKey === false) {
exitAddBurgMode();
@@ -520,7 +533,7 @@ function downloadBurgsData() {
let data = `Id,Burg,Province,Province Full Name,State,State Full Name,Culture,Religion,Population,`;
data += `X_World (m),Y_World (m),X_Pixel,Y_Pixel,`; // New world coords + renamed pixel coords
data += `Latitude,Longitude,`; // Keep for compatibility
- data += `Elevation (${heightUnit.value}),Temperature,Temperature likeness,`;
+ data += `Elevation (${heightUnit.value}),Sky Altitude (m),Temperature,Temperature likeness,`;
data += `Capital,Port,Citadel,Walls,Plaza,Temple,Shanty Town,Emblem,City Generator Link\n`;
const valid = pack.burgs.filter(b => b.i && !b.removed); // all valid burgs
@@ -553,6 +566,8 @@ function downloadBurgsData() {
// Continue with elevation and other data
data += parseInt(getHeight(pack.cells.h[b.cell])) + ",";
+ // Sky altitude in meters (only for flying burgs), else blank
+ data += (b.flying ? (b.altitude ?? 1000) : "") + ",";
const temperature = grid.cells.temp[pack.cells.g[b.cell]];
data += convertTemperature(temperature) + ",";
data += getTemperatureLikeness(temperature) + ",";
diff --git a/modules/ui/editors.js b/modules/ui/editors.js
index 3ebff350..b6f10739 100644
--- a/modules/ui/editors.js
+++ b/modules/ui/editors.js
@@ -227,6 +227,52 @@ function moveBurgToGroup(id, g) {
}
}
+// Ensure a dedicated locked Sky State exists; create if missing and return its id
+function ensureSkyState(anchorBurgId) {
+ const {states, burgs, cultures, cells} = pack;
+
+ // Reuse existing sky state if present
+ let sky = states.find(s => s && s.i && !s.removed && s.skyRealm);
+ if (sky) return sky.i;
+
+ // Create a new locked Sky State
+ const b = burgs[anchorBurgId];
+ const i = states.length;
+ const culture = (b && b.culture) || cells.culture?.[b?.cell] || 1;
+ const type = "Generic";
+ const name = "Sky Realm";
+ const color = "#5b8bd4";
+ const coa = COA.generate(null, null, null, cultures[culture]?.type || "Generic");
+ coa.shield = COA.getShield(culture, null);
+
+ const newState = {
+ i,
+ name,
+ type,
+ color,
+ capital: anchorBurgId,
+ center: b.cell,
+ culture,
+ expansionism: 0.1,
+ coa,
+ lock: 1,
+ skyRealm: 1
+ };
+
+ states.push(newState);
+
+ // Assign the burg and its cell to the Sky State
+ if (cells && typeof b.cell === "number") cells.state[b.cell] = i;
+ if (b) {
+ b.state = i;
+ b.capital = 1;
+ // Move to cities layer for capitals
+ moveBurgToGroup(anchorBurgId, "cities");
+ }
+
+ return i;
+}
+
function moveAllBurgsToGroup(fromGroup, toGroup) {
const groupToMove = document.querySelector(`#burgIcons #${fromGroup}`);
const burgsToMove = Array.from(groupToMove.children).map(x => x.dataset.id);
@@ -316,6 +362,9 @@ function togglePort(burg) {
function getBurgLink(burg) {
if (burg.link) return burg.link;
+ // Sky burgs: force MFCG with sky-friendly parameters
+ if (burg.flying || burg.skyPort) return createMfcgLink(burg, true);
+
const population = burg.population * populationRate * urbanization;
if (population >= options.villageMaxPopulation || burg.citadel || burg.walls || burg.temple || burg.shanty)
return createMfcgLink(burg);
@@ -323,42 +372,42 @@ function getBurgLink(burg) {
return createVillageGeneratorLink(burg);
}
-function createMfcgLink(burg) {
+function createMfcgLink(burg, isSky = false) {
const {cells} = pack;
const {i, name, population: burgPopulation, cell} = burg;
const burgSeed = burg.MFCG || seed + String(burg.i).padStart(4, 0);
const sizeRaw = 2.13 * Math.pow((burgPopulation * populationRate) / urbanDensity, 0.385);
- const size = minmax(Math.ceil(sizeRaw), 6, 100);
+ const size = isSky ? 25 : minmax(Math.ceil(sizeRaw), 6, 100);
const population = rn(burgPopulation * populationRate * urbanization);
- const river = cells.r[cell] ? 1 : 0;
- const coast = Number(burg.port > 0);
- const sea = (() => {
- if (!coast || !cells.haven[cell]) return null;
+ const river = isSky ? 0 : (cells.r[cell] ? 1 : 0);
+ const coast = isSky ? 0 : Number(burg.port > 0);
- // calculate see direction: 0 = south, 0.5 = west, 1 = north, 1.5 = east
- const p1 = cells.p[cell];
- const p2 = cells.p[cells.haven[cell]];
- let deg = (Math.atan2(p2[1] - p1[1], p2[0] - p1[0]) * 180) / Math.PI - 90;
- if (deg < 0) deg += 360;
- return rn(normalize(deg, 0, 360) * 2, 2);
- })();
+ const sea = !isSky && coast && cells.haven[cell]
+ ? (() => {
+ const p1 = cells.p[cell];
+ const p2 = cells.p[cells.haven[cell]];
+ let deg = (Math.atan2(p2[1] - p1[1], p2[0] - p1[0]) * 180) / Math.PI - 90;
+ if (deg < 0) deg += 360;
+ return rn(normalize(deg, 0, 360) * 2, 2);
+ })()
+ : null;
const arableBiomes = river ? [1, 2, 3, 4, 5, 6, 7, 8] : [5, 6, 7, 8];
- const farms = +arableBiomes.includes(cells.biome[cell]);
+ const farms = isSky ? 0 : +arableBiomes.includes(cells.biome[cell]);
- const citadel = +burg.citadel;
- const urban_castle = +(citadel && each(2)(i));
+ const citadel = isSky ? 1 : +burg.citadel;
+ const urban_castle = isSky ? 1 : +(citadel && each(2)(i));
+ const hub = isSky ? 0 : Routes.isCrossroad(cell);
+ const walls = isSky ? 1 : +burg.walls;
+ const plaza = isSky ? 1 : +burg.plaza;
+ const temple = isSky ? 1 : +burg.temple;
+ const shantytown = isSky ? 0 : +burg.shanty;
+ const greens = isSky ? 1 : undefined;
+ const gates = isSky ? 0 : -1;
- const hub = Routes.isCrossroad(cell);
- const walls = +burg.walls;
- const plaza = +burg.plaza;
- const temple = +burg.temple;
- const shantytown = +burg.shanty;
-
- const url = new URL("https://watabou.github.io/city-generator/");
- url.search = new URLSearchParams({
+ const params = {
name,
population,
size,
@@ -372,9 +421,13 @@ function createMfcgLink(burg) {
plaza,
temple,
walls,
- shantytown,
- gates: -1
- });
+ shantytown
+ };
+ if (greens !== undefined) params.greens = greens;
+ if (gates !== undefined) params.gates = gates;
+
+ const url = new URL("https://watabou.github.io/city-generator/");
+ url.search = new URLSearchParams(params);
if (sea) url.searchParams.append("sea", sea);
return url.toString();
diff --git a/modules/ui/heightmap-editor.js b/modules/ui/heightmap-editor.js
index 20fa201c..51bcd684 100644
--- a/modules/ui/heightmap-editor.js
+++ b/modules/ui/heightmap-editor.js
@@ -401,11 +401,14 @@ function editHeightmap(options) {
// find best cell for burgs
for (const b of pack.burgs) {
if (!b.i || b.removed) continue;
- b.cell = findBurgCell(b.x, b.y);
+ // Keep flying burgs at their current (possibly water) cell
+ if (!b.flying) {
+ b.cell = findBurgCell(b.x, b.y);
+ }
b.feature = pack.cells.f[b.cell];
pack.cells.burg[b.cell] = b.i;
- if (!b.capital && pack.cells.h[b.cell] < 20) removeBurg(b.i);
+ if (!b.capital && pack.cells.h[b.cell] < 20 && !b.flying) removeBurg(b.i);
if (b.capital) pack.states[b.state].center = b.cell;
}
diff --git a/modules/ui/style-presets.js b/modules/ui/style-presets.js
index 01626205..7d408c86 100644
--- a/modules/ui/style-presets.js
+++ b/modules/ui/style-presets.js
@@ -222,6 +222,7 @@ function addStylePreset() {
"#roads": ["opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter", "mask"],
"#trails": ["opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter", "mask"],
"#searoutes": ["opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter", "mask"],
+ "#airroutes": ["opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter", "mask"],
"#statesBody": ["opacity", "filter"],
"#statesHalo": ["opacity", "data-width", "stroke-width", "filter"],
"#provs": ["opacity", "fill", "font-size", "font-family", "filter"],
@@ -290,6 +291,16 @@ function addStylePreset() {
"stroke-dasharray",
"stroke-linecap"
],
+ "#burgIcons > #skyburgs": [
+ "opacity",
+ "fill",
+ "fill-opacity",
+ "size",
+ "stroke",
+ "stroke-width",
+ "stroke-dasharray",
+ "stroke-linecap"
+ ],
"#anchors > #cities": ["opacity", "fill", "size", "stroke", "stroke-width"],
"#burgLabels > #towns": [
"opacity",
diff --git a/skyports.md b/skyports.md
new file mode 100644
index 00000000..d1b0900b
--- /dev/null
+++ b/skyports.md
@@ -0,0 +1,538 @@
+Sky Burgs and Air Routes — Implementation Guide
+
+Overview
+- Add “Sky State” for flying cities.
+- Allow creating and relocating flying burgs over water.
+- Add “Sky Port” and “Flying” toggles in the Burg Editor.
+- Generate air routes between sky ports and show them on a distinct SVG group.
+- Add style controls and preset styles for the new `#airroutes` group.
+- Protect flying burgs during heightmap reapply.
+
+Notes
+- The instructions below are surgical and limited to sky burgs/air routes only.
+- When exact lines are uncertain, use the shown anchors to locate code.
+- After changes, regenerate routes to see air routes: Tools → Regenerate → Routes.
+
+1) Add air routes group to the SVG scene
+File: main.js
+Anchor: near other route groups
+
+Find:
+ let routes = viewbox.append("g").attr("id", "routes");
+ let roads = routes.append("g").attr("id", "roads");
+ let trails = routes.append("g").attr("id", "trails");
+ let searoutes = routes.append("g").attr("id", "searoutes");
+
+Add immediately after:
+ let airroutes = routes.append("g").attr("id", "airroutes");
+
+2) Create a Sky State helper
+File: modules/ui/editors.js
+Anchor: place after function moveBurgToGroup or near other global helpers.
+
+Add:
+ // Ensure a dedicated locked Sky State exists; create if missing and return its id
+ function ensureSkyState(anchorBurgId) {
+ const {states, burgs, cultures, cells} = pack;
+ let sky = states.find(s => s && s.i && !s.removed && s.skyRealm);
+ if (sky) return sky.i;
+
+ const b = burgs[anchorBurgId];
+ const i = states.length;
+ const culture = (b && b.culture) || cells.culture?.[b?.cell] || 1;
+ const type = "Generic";
+ const name = "Sky Realm";
+ const color = "#5b8bd4";
+ const coa = COA.generate(null, null, null, cultures[culture]?.type || "Generic");
+ coa.shield = COA.getShield(culture, null);
+
+ const newState = {
+ i, name, type, color,
+ capital: anchorBurgId,
+ center: b.cell,
+ culture,
+ expansionism: 0.1,
+ coa,
+ lock: 1,
+ skyRealm: 1
+ };
+ states.push(newState);
+
+ if (cells && typeof b.cell === "number") cells.state[b.cell] = i;
+ if (b) {
+ b.state = i;
+ b.capital = 1;
+ moveBurgToGroup(anchorBurgId, "cities");
+ }
+ return i;
+ }
+
+3) Allow adding flying burgs over water (Alt to place)
+File: modules/ui/burgs-overview.js
+Anchor: function addBurgOnClick()
+
+Change water check:
+ if (pack.cells.h[cell] < 20 && !d3.event.altKey)
+ return tip("Hold Alt to place a flying burg over water", false, "error");
+
+After calling addBurg(point), mark and assign sky:
+ const id = addBurg(point);
+ if (pack.cells.h[cell] < 20) {
+ const burg = pack.burgs[id];
+ burg.flying = 1;
+ burg.skyPort = 1;
+ const skyStateId = ensureSkyState(id);
+ if (burg.state !== skyStateId) burg.state = skyStateId;
+ burg.port = 0; // not a sea port
+ }
+
+4) Generate air routes between sky ports
+File: modules/routes-generator.js
+Anchors: inside generate(lockedRoutes), within the critical phase and in createRoutesData
+
+Add to critical phase (just after generating majorSeaRoutes and royalRoads):
+ const airRoutes = generateAirRoutes();
+
+Add to createRoutesData to push air routes first pass:
+ for (const {feature, cells, merged, type} of mergeRoutes(airRoutes)) {
+ if (merged) continue;
+ const points = getPoints("airroutes", cells, pointsArray);
+ routes.push({i: routes.length, group: "airroutes", feature, points, type: type || "air"});
+ }
+
+Define generateAirRoutes() below createRoutesData helpers:
+ function generateAirRoutes() {
+ TIME && console.time("generateAirRoutes");
+ const air = [];
+ const skyPorts = pack.burgs.filter(b => b && b.i && !b.removed && (b.skyPort || b.flying));
+ if (skyPorts.length < 2) { TIME && console.timeEnd("generateAirRoutes"); return air; }
+ const points = skyPorts.map(b => [b.x, b.y]);
+ const edges = calculateUrquhartEdges(points);
+ edges.forEach(([ai, bi]) => {
+ const a = skyPorts[ai];
+ const b = skyPorts[bi];
+ air.push({feature: -1, cells: [a.cell, b.cell], type: "air"});
+ });
+ TIME && console.timeEnd("generateAirRoutes");
+ return air;
+ }
+
+Notes:
+- drawRoutes() and getPath already support arbitrary route groups. No extra change needed.
+- Air routes use the default curve; this is acceptable. Add a custom curve if desired.
+
+5) Style editor support and preset styles
+File: modules/ui/style-presets.js
+Anchor: in the attributes map within collectStyleData (search for "#searoutes")
+
+Add a sibling line:
+ "#airroutes": ["opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter", "mask"],
+
+Preset styles: add minimal entries to each preset JSON under styles/ to visibly render air routes. Example for styles/default.json:
+ "#airroutes": {
+ "opacity": 0.95,
+ "stroke": "#8a2be2",
+ "stroke-width": 0.6,
+ "stroke-dasharray": "2 3",
+ "stroke-linecap": "round",
+ "filter": null,
+ "mask": null
+ }
+
+Repeat similarly for other presets you use (light, pale, ancient, atlas, clean, darkSeas, cyberpunk, gloom, monochrome, night, watercolor). Colors can be tuned per theme, but a purple dashed route is a good default.
+
+6) Add Burg Editor toggles for Sky Port and Flying
+File: index.html
+Anchor: Burg Editor → Features group (look for id="burgPort")
+
+Insert two features after Port:
+
+
+
+File: modules/ui/burg-editor.js
+Anchor 1: updateBurgValues() — toggle icons active state
+
+Add:
+ if (b.skyPort) byId("burgSkyPort").classList.remove("inactive"); else byId("burgSkyPort").classList.add("inactive");
+ if (b.flying) byId("burgFlying").classList.remove("inactive"); else byId("burgFlying").classList.add("inactive");
+
+Anchor 2: toggleFeature() — handle clicks
+
+Add cases before the generic assignment:
+ if (feature === "port") togglePort(id);
+ else if (feature === "skyPort") {
+ burg.skyPort = +turnOn;
+ if (turnOn && !burg.capital) {
+ const skyId = ensureSkyState(id);
+ if (burg.state !== skyId) {
+ pack.cells.state[burg.cell] = skyId; // own the cell
+ burg.state = skyId;
+ }
+ }
+ regenerateRoutes();
+ } else if (feature === "flying") {
+ burg.flying = +turnOn;
+ regenerateRoutes();
+ } else if (feature === "capital") toggleCapital(id);
+ else burg[feature] = +turnOn;
+
+Keep the existing UI updates following this block.
+
+7) Protect flying burgs during heightmap reapply
+File: modules/ui/heightmap-editor.js
+Anchor: function that reassigns burg cells when applying heightmap (search for "findBurgCell(b.x, b.y)")
+
+Modify so flying burgs stay where they are (even if over water) and are not removed:
+ // Keep flying burgs at their current (possibly water) cell
+ if (!b.flying) { b.cell = findBurgCell(b.x, b.y); }
+ b.feature = pack.cells.f[b.cell];
+ pack.cells.burg[b.cell] = b.i;
+ if (!b.capital && pack.cells.h[b.cell] < 20 && !b.flying) removeBurg(b.i);
+
+8) Allow relocating flying burgs over water
+File: modules/ui/burg-editor.js
+Anchor: relocateBurgOnClick()
+
+Replace water restriction with:
+ const isWater = cells.h[cell] < 20;
+ const allowWater = pack.burgs[id]?.flying || d3.event.altKey;
+ if (isWater && !allowWater) {
+ tip("Hold Alt or mark as Flying to place over water", false, "error");
+ return;
+ }
+
+Adjust state assignment on relocation:
+ if (isWater) {
+ if (burg.skyPort) {
+ const skyId = ensureSkyState(id);
+ cells.state[cell] = skyId;
+ burg.state = skyId;
+ } else {
+ burg.state = oldState; // keep previous state when only flying
+ }
+ } else {
+ burg.state = newState;
+ }
+
+Usage
+- Add flying sky port over water: enable Add Burg, hold Alt over water and click. The first flying burg creates a locked Sky Realm and becomes its capital.
+- Convert an existing burg: open Burg Editor → click Cloud (Flying). Optional: click Rocket (Sky Port) to join Sky Realm and participate in air routes.
+- Relocate: use Relocate in Burg Editor. Flying or Alt allows dropping onto water. Sky Port burgs on water are assigned to Sky Realm automatically.
+- Style air routes: Style → element "routes" → group "airroutes".
+
+Verification Checklist
+- Routes layer shows air routes in the chosen style.
+- Creating Alt-click water burg marks it as flying + sky port in the Sky Realm.
+- Burg Editor shows Cloud and Rocket toggles; they update state and routes when toggled.
+- Heightmap reapply keeps flying burgs; non-flying water burgs (non-capitals) are still removed.
+
+Optional Enhancements
+- Add custom curve for air routes in ROUTE_CURVES, e.g., like searoutes.
+- Add a legend entry for air routes using drawLegend in modules/ui/editors.js.
+
+Appendix — Name sky routes and render them curved
+
+Goal: ensure air routes get meaningful names (e.g., “Sky route Raven–Star”) and are drawn as smooth curves.
+
+Files and anchors
+- File: modules/routes-generator.js
+- Anchors: search for each block shown below (models, suffixes, generateName, ROUTE_CURVES)
+
+1) Extend naming models for air routes
+Find the models object (near the comment “// name generator data”) and add an entry for airroutes:
+
+ 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},
+ airroutes: {burg_suffix: 3, prefix_suffix: 5, the_descriptor_prefix_suffix: 2}
+ };
+
+2) Add suffixes for air routes
+Find the suffixes object and add a key for airroutes with sky-themed suffixes:
+
+ 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},
+ airroutes: {"sky route": 5, "air lane": 3, skyway: 2, airway: 2, "aerial path": 1}
+ };
+
+3) Update generateName to recognize airroutes
+Inside function generateName({group, points}), adjust two return paths to include a “Sky route” base when group === "airroutes":
+
+- For short segments (when points.length < 4):
+
+ const base = group === "searoutes"
+ ? "Sea route"
+ : group === "airroutes"
+ ? "Sky route"
+ : group === "secondary" || group === "roads"
+ ? "Road"
+ : "Trail";
+
+- For the final generic fallback:
+
+ return group === "searoutes" ? "Sea route" : group === "airroutes" ? "Sky route" : "Route";
+
+This ensures the generator can emit: “Sky route –” or “The skyway”, etc.
+
+4) Make air routes curved
+Find the ROUTE_CURVES object and add a curve for airroutes similar to searoutes (Catmull-Rom spline with higher alpha):
+
+ 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),
+ airroutes: d3.curveCatmullRom.alpha(0.5),
+ default: d3.curveCatmullRom.alpha(0.1)
+ };
+
+No other code changes are required: draw and UI already call Routes.generateName and use the group’s curve when building the SVG path.
+
+Appendix — Ensure sky burgs are never Wildlands (auto Sky Realm state)
+
+Goal: make sure sky burgs do not end up in state 0 (Wildlands/Neutrals). The first sky burg should create a dedicated Sky Realm state and become its capital; subsequent sky burgs should join the Sky Realm automatically, even if placed on land.
+
+Files and anchors
+- File: modules/ui/editors.js (Sky State helper)
+- File: modules/ui/burgs-overview.js (Alt-place over water)
+- File: modules/ui/burg-editor.js (feature toggles + relocation)
+
+1) Sky State helper (create-on-first-use)
+Already covered in step 2 above (ensureSkyState). This creates a locked state with the first sky burg as capital and assigns the burg/cell to this state.
+
+2) When adding a sky burg over water (Alt), assign Sky Realm immediately
+Already covered in step 3 above. The code sets `burg.flying = 1`, `burg.skyPort = 1`, and assigns to Sky Realm via `ensureSkyState(id)`.
+
+3) When toggling Flying on any burg, assign to Sky Realm
+File: modules/ui/burg-editor.js
+Anchor: toggleFeature()
+
+Replace the current Flying branch so it also assigns the burg to Sky Realm, regardless of terrain:
+
+ else if (feature === "flying") {
+ burg.flying = +turnOn;
+ if (turnOn) {
+ try {
+ const skyId = ensureSkyState(id);
+ if (burg.state !== skyId) {
+ pack.cells.state[burg.cell] = skyId;
+ burg.state = skyId;
+ }
+ } catch (e) { ERROR && console.error(e); }
+ }
+ regenerateRoutes();
+ }
+
+4) On relocation, keep sky burgs in the Sky Realm
+File: modules/ui/burg-editor.js
+Anchor: relocateBurgOnClick()
+
+Adjust the state assignment so Flying burgs also move into/retain the Sky Realm (not Wildlands) when placed on water or land:
+
+ if (isWater || burg.flying) {
+ const skyId = ensureSkyState(id);
+ cells.state[cell] = skyId;
+ burg.state = skyId;
+ } else {
+ burg.state = newState;
+ }
+
+Result
+- The first sky burg creates a dedicated locked Sky Realm and becomes its capital.
+- Any burg marked as Flying joins Sky Realm immediately and will not belong to Wildlands.
+- Relocating a sky burg keeps it in Sky Realm regardless of terrain (water or land).
+
+Appendix — Configure altitude (elevation) for sky burgs
+
+Goal: allow setting a custom altitude for Flying burgs and show it in the Burg Editor. Do not change the map cell height; store altitude on the burg itself.
+
+Files and anchors
+- File: index.html (Burg Editor markup)
+- File: modules/ui/burg-editor.js (UI bindings)
+- File: modules/ui/burgs-overview.js (default altitude on Alt-place)
+
+1) Add an Altitude input in Burg Editor
+File: index.html
+Anchor: inside the Burg Editor body, near the Population row (search for id="burgPopulation"). Insert after Population:
+
+
+
+2) Bind and show/hide altitude for Flying burgs
+File: modules/ui/burg-editor.js
+Anchor A: in editBurg(), after existing listeners are attached, add listener to persist altitude changes:
+
+ byId("burgAltitude").addEventListener("change", changeAltitude);
+
+Anchor B: in updateBurgValues(), show altitude when b.flying and set value:
+
+ const altitudeRow = byId("burgAltitudeRow");
+ if (b.flying) {
+ altitudeRow.style.display = "flex";
+ byId("burgAltitude").value = b.altitude ?? 1000; // default 1000 m if unset
+ } else {
+ altitudeRow.style.display = "none";
+ }
+
+Also tweak the Elevation display for flying burgs (optional but recommended):
+
+ if (b.flying) byId("burgElevation").innerHTML = `${b.altitude ?? 1000} m (sky altitude)`;
+ else byId("burgElevation").innerHTML = getHeight(pack.cells.h[b.cell]);
+
+Anchor C: add the changeAltitude() handler alongside other change handlers:
+
+ function changeAltitude() {
+ const id = +elSelected.attr("data-id");
+ const burg = pack.burgs[id];
+ burg.altitude = Math.max(0, Math.round(+byId("burgAltitude").value));
+ }
+
+3) Set a default altitude when creating/toggling sky burgs
+File: modules/ui/burgs-overview.js
+Anchor: in addBurgOnClick(), when marking a water-placed burg as flying/skyPort, also initialize altitude:
+
+ burg.altitude = burg.altitude ?? 1000;
+
+File: modules/ui/burg-editor.js
+Anchor: in toggleFeature(), in the Flying branch when turning on, also set default altitude if missing:
+
+ if (turnOn && (burg.altitude == null)) burg.altitude = 1000;
+
+Notes
+- Altitude is stored per-burg (e.g., 1000 = 1000 meters). It does not alter `cells.h` and won’t affect terrain/temperature automatically.
+- CSV/GeoJSON exports may not include `altitude` by default; add it to exporters if you need it in data outputs.
+
+Appendix — Force Watabou (MFCG) city links for sky burgs
+
+Goal: for any sky burg (Flying/Sky Port), open Watabou’s city generator with fixed options that suit floating cities, e.g.
+
+ https://watabou.github.io/city-generator/?size=25&seed=588489202&citadel=1&urban_castle=1&plaza=1&temple=1&walls=1&shantytown=0&coast=0&river=0&greens=1&gates=0
+
+Files and anchors
+- File: modules/ui/editors.js
+- Anchors: getBurgLink(burg), createMfcgLink(burg)
+
+1) Prefer MFCG link for sky burgs
+In getBurgLink(burg), before existing logic, short‑circuit for Flying/Sky Port burgs:
+
+ function getBurgLink(burg) {
+ if (burg.link) return burg.link;
+ if (burg.flying || burg.skyPort) return createMfcgLink(burg, /*isSky*/ true);
+ ... existing logic ...
+ }
+
+2) Add sky mode to createMfcgLink and override parameters
+Change the function signature and branch overrides:
+
+ function createMfcgLink(burg, isSky = false) {
+ const {cells} = pack;
+ const {i, name, population: burgPopulation, cell} = burg;
+ const burgSeed = burg.MFCG || seed + String(burg.i).padStart(4, 0);
+
+ const sizeRaw = 2.13 * Math.pow((burgPopulation * populationRate) / urbanDensity, 0.385);
+ const size = isSky ? 25 : minmax(Math.ceil(sizeRaw), 6, 100);
+ const population = rn(burgPopulation * populationRate * urbanization);
+
+ const river = isSky ? 0 : (cells.r[cell] ? 1 : 0);
+ const coast = isSky ? 0 : Number(burg.port > 0);
+
+ // Only compute sea direction for non-sky cities
+ const sea = !isSky && coast && cells.haven[cell]
+ ? (() => { const p1 = cells.p[cell], p2 = cells.p[cells.haven[cell]];
+ let deg = (Math.atan2(p2[1] - p1[1], p2[0] - p1[0]) * 180) / Math.PI - 90;
+ if (deg < 0) deg += 360; return rn(normalize(deg, 0, 360) * 2, 2); })()
+ : null;
+
+ const farms = isSky ? 0 : +([5,6,7,8].includes(cells.biome[cell]));
+
+ // Force features for sky cities
+ const citadel = isSky ? 1 : +burg.citadel;
+ const urban_castle = isSky ? 1 : +(citadel && each(2)(i));
+ const hub = isSky ? 0 : Routes.isCrossroad(cell);
+ const walls = isSky ? 1 : +burg.walls;
+ const plaza = isSky ? 1 : +burg.plaza;
+ const temple = isSky ? 1 : +burg.temple;
+ const shantytown = isSky ? 0 : +burg.shanty;
+ const greens = isSky ? 1 : undefined; // optional MFCG param for parks/greens
+ const gates = isSky ? 0 : -1; // per request: no city gates for sky cities
+
+ const params = {
+ name, population, size, seed: burgSeed,
+ river, coast, farms, citadel, urban_castle, hub, plaza, temple, walls, shantytown,
+ // Watabou accepts extra flags; only append when defined
+ };
+ if (greens !== undefined) params.greens = greens;
+ if (gates !== undefined) params.gates = gates;
+
+ const url = new URL("https://watabou.github.io/city-generator/");
+ url.search = new URLSearchParams(params);
+ if (sea) url.searchParams.append("sea", sea);
+ return url.toString();
+ }
+
+Appendix — Style sky burg icons as a separate group
+
+Goal: render Flying/Sky Port burg icons in their own SVG subgroup so you can size/color them independently via the Style editor.
+
+Files and anchors
+- File: main.js (add a group under `#burgIcons`)
+- File: modules/renderers/draw-burg-icons.js (render sky burg icons into the new group)
+- File: modules/ui/style-presets.js (expose style controls for the group)
+
+1) Add the `#skyburgs` icon group in the DOM
+File: main.js
+Anchor: where `#burgIcons` is created (near `let burgIcons = icons.append("g").attr("id", "burgIcons");`).
+
+Add:
+ burgIcons.append("g").attr("id", "skyburgs");
+
+2) Render sky burgs into `#skyburgs`
+File: modules/renderers/draw-burg-icons.js
+Anchors: top-level filters that compute `capitals` and `towns`, and the draw pass where circles are appended.
+
+- Exclude sky burgs from capitals/towns:
+ const capitals = pack.burgs.filter(b => b.capital && !b.removed && !(b.flying || b.skyPort));
+ const towns = pack.burgs.filter(b => b.i && !b.capital && !b.removed && !(b.flying || b.skyPort));
+
+- After drawing towns, add a new pass:
+
+ // Sky burgs (flying or sky port)
+ const sky = pack.burgs.filter(b => b.i && !b.removed && (b.flying || b.skyPort));
+ const skyIcons = burgIcons.select("#skyburgs");
+ const skySize = skyIcons.attr("size") || 0.6;
+ skyIcons
+ .selectAll("circle")
+ .data(sky)
+ .enter()
+ .append("circle")
+ .attr("id", d => "burg" + d.i)
+ .attr("data-id", d => d.i)
+ .attr("cx", d => d.x)
+ .attr("cy", d => d.y)
+ .attr("r", skySize);
+
+3) Expose `#skyburgs` in the Style editor
+File: modules/ui/style-presets.js
+Anchor: in the attributes map alongside `#burgIcons > #cities` and `#burgIcons > #towns`.
+
+Add an entry:
+ "#burgIcons > #skyburgs": [
+ "opacity","fill","fill-opacity","size","stroke","stroke-width","stroke-dasharray","stroke-linecap"
+ ],
+
+Usage
+- Open Style → element `burgIcons` → group `skyburgs` and set `size`, `fill`, `stroke`, etc.
+- Defaults: if no size is set, `draw-burg-icons` uses `0.6` as a fallback.
+- Optional: add preset defaults for `#burgIcons > #skyburgs` in your styles/*.json if you want consistent theme styling.
+
+Notes
+- This keeps existing behavior for non‑sky burgs.
+- If you want a different default size for sky cities, adjust `size = isSky ? 25 : ...`.
+- The “roads” control in MFCG is indirect; setting `hub=0` and `gates=0` reduces road/gate features. There is no direct `roads=0` flag.
diff --git a/styles/ancient.json b/styles/ancient.json
index d57aa524..e8504b78 100644
--- a/styles/ancient.json
+++ b/styles/ancient.json
@@ -218,6 +218,15 @@
"filter": null,
"mask": null
},
+ "#airroutes": {
+ "opacity": 0.95,
+ "stroke": "#8a2be2",
+ "stroke-width": 0.6,
+ "stroke-dasharray": "2 3",
+ "stroke-linecap": "round",
+ "filter": null,
+ "mask": null
+ },
"#statesBody": {
"opacity": 0.2,
"filter": "url(#filter-sepia)"
diff --git a/styles/atlas.json b/styles/atlas.json
index ef7d7f8a..5170d6c1 100644
--- a/styles/atlas.json
+++ b/styles/atlas.json
@@ -218,6 +218,15 @@
"filter": null,
"mask": null
},
+ "#airroutes": {
+ "opacity": 0.95,
+ "stroke": "#8a2be2",
+ "stroke-width": 0.6,
+ "stroke-dasharray": "2 3",
+ "stroke-linecap": "round",
+ "filter": null,
+ "mask": null
+ },
"#statesBody": {
"opacity": 0.49,
"filter": null
diff --git a/styles/clean.json b/styles/clean.json
index c5aad094..a985c46b 100644
--- a/styles/clean.json
+++ b/styles/clean.json
@@ -219,6 +219,15 @@
"filter": null,
"mask": "url(#water)"
},
+ "#airroutes": {
+ "opacity": 0.95,
+ "stroke": "#8a2be2",
+ "stroke-width": 0.6,
+ "stroke-dasharray": "2 3",
+ "stroke-linecap": "round",
+ "filter": null,
+ "mask": null
+ },
"#statesBody": {
"opacity": 0.3,
"filter": null
diff --git a/styles/cyberpunk.json b/styles/cyberpunk.json
index 93f22284..326c9b86 100644
--- a/styles/cyberpunk.json
+++ b/styles/cyberpunk.json
@@ -218,6 +218,15 @@
"filter": null,
"mask": null
},
+ "#airroutes": {
+ "opacity": 0.95,
+ "stroke": "#ff00ff",
+ "stroke-width": 0.8,
+ "stroke-dasharray": "2 3",
+ "stroke-linecap": "round",
+ "filter": null,
+ "mask": null
+ },
"#statesBody": {
"opacity": 0,
"filter": null
diff --git a/styles/darkSeas.json b/styles/darkSeas.json
index 2bc90fa6..5f0274a4 100644
--- a/styles/darkSeas.json
+++ b/styles/darkSeas.json
@@ -207,6 +207,15 @@
"filter": null,
"mask": null
},
+ "#airroutes": {
+ "opacity": 0.95,
+ "stroke": "#c084ff",
+ "stroke-width": 0.8,
+ "stroke-dasharray": "2 3",
+ "stroke-linecap": "round",
+ "filter": null,
+ "mask": null
+ },
"#statesBody": {
"opacity": 0.5,
"filter": null
diff --git a/styles/default.json b/styles/default.json
index 23b06487..61bcf446 100644
--- a/styles/default.json
+++ b/styles/default.json
@@ -218,6 +218,15 @@
"filter": null,
"mask": null
},
+ "#airroutes": {
+ "opacity": 0.95,
+ "stroke": "#8a2be2",
+ "stroke-width": 0.6,
+ "stroke-dasharray": "2 3",
+ "stroke-linecap": "round",
+ "filter": null,
+ "mask": null
+ },
"#statesBody": {
"opacity": 0.4,
"filter": null
diff --git a/styles/gloom.json b/styles/gloom.json
index 19318882..b4f8955e 100644
--- a/styles/gloom.json
+++ b/styles/gloom.json
@@ -219,6 +219,15 @@
"filter": null,
"mask": null
},
+ "#airroutes": {
+ "opacity": 0.95,
+ "stroke": "#b088f9",
+ "stroke-width": 0.6,
+ "stroke-dasharray": "2 3",
+ "stroke-linecap": "round",
+ "filter": null,
+ "mask": null
+ },
"#statesBody": {
"opacity": 0.4,
"filter": null
diff --git a/styles/light.json b/styles/light.json
index de539872..fd32d606 100644
--- a/styles/light.json
+++ b/styles/light.json
@@ -218,6 +218,15 @@
"filter": null,
"mask": null
},
+ "#airroutes": {
+ "opacity": 0.95,
+ "stroke": "#8a2be2",
+ "stroke-width": 0.6,
+ "stroke-dasharray": "2 3",
+ "stroke-linecap": "round",
+ "filter": null,
+ "mask": null
+ },
"#statesBody": {
"opacity": 0.2,
"filter": null
diff --git a/styles/monochrome.json b/styles/monochrome.json
index 1ee17c43..9fb4b956 100644
--- a/styles/monochrome.json
+++ b/styles/monochrome.json
@@ -212,6 +212,15 @@
"filter": null,
"mask": null
},
+ "#airroutes": {
+ "opacity": 0.9,
+ "stroke": "#000000",
+ "stroke-width": 0.6,
+ "stroke-dasharray": "2 3",
+ "stroke-linecap": "round",
+ "filter": null,
+ "mask": null
+ },
"#statesBody": {
"opacity": 0.4,
"filter": null
diff --git a/styles/night.json b/styles/night.json
index 67a5e799..05685e4f 100644
--- a/styles/night.json
+++ b/styles/night.json
@@ -218,6 +218,15 @@
"filter": "",
"mask": ""
},
+ "#airroutes": {
+ "opacity": 0.95,
+ "stroke": "#d8b4fe",
+ "stroke-width": 0.6,
+ "stroke-dasharray": "2 3",
+ "stroke-linecap": "round",
+ "filter": null,
+ "mask": null
+ },
"#statesBody": {
"opacity": 0.07,
"filter": ""
diff --git a/styles/pale.json b/styles/pale.json
index 8e839600..9c73bae4 100644
--- a/styles/pale.json
+++ b/styles/pale.json
@@ -218,6 +218,15 @@
"filter": null,
"mask": null
},
+ "#airroutes": {
+ "opacity": 0.95,
+ "stroke": "#7b68ee",
+ "stroke-width": 0.6,
+ "stroke-dasharray": "2 3",
+ "stroke-linecap": "round",
+ "filter": null,
+ "mask": null
+ },
"#statesBody": {
"opacity": 0.15,
"filter": null
diff --git a/styles/watercolor.json b/styles/watercolor.json
index 982c1b49..a5c4bf3f 100644
--- a/styles/watercolor.json
+++ b/styles/watercolor.json
@@ -218,6 +218,15 @@
"filter": null,
"mask": null
},
+ "#airroutes": {
+ "opacity": 0.95,
+ "stroke": "#8a2be2",
+ "stroke-width": 0.6,
+ "stroke-dasharray": "2 3",
+ "stroke-linecap": "round",
+ "filter": null,
+ "mask": null
+ },
"#statesBody": {
"opacity": 0.05,
"filter": null