From 73ab86b9575597fc11d00bb83c626b3c4370ac48 Mon Sep 17 00:00:00 2001 From: barrulus Date: Fri, 5 Sep 2025 21:07:42 +0100 Subject: [PATCH] sky-burgs-sky-routes --- index.html | 17 + main.js | 6 +- modules/io/export.js | 3 + modules/renderers/draw-burg-icons.js | 24 +- modules/routes-generator.js | 53 ++- modules/ui/burg-editor.js | 71 +++- modules/ui/burgs-overview.js | 23 +- modules/ui/editors.js | 107 ++++-- modules/ui/heightmap-editor.js | 7 +- modules/ui/style-presets.js | 11 + skyports.md | 538 +++++++++++++++++++++++++++ styles/ancient.json | 9 + styles/atlas.json | 9 + styles/clean.json | 9 + styles/cyberpunk.json | 9 + styles/darkSeas.json | 9 + styles/default.json | 9 + styles/gloom.json | 9 + styles/light.json | 9 + styles/monochrome.json | 9 + styles/night.json | 9 + styles/pale.json | 9 + styles/watercolor.json | 9 + 23 files changed, 919 insertions(+), 49 deletions(-) create mode 100644 skyports.md 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