From c075e704fdeaeede2d05d4fdcc46c3988037366a Mon Sep 17 00:00:00 2001 From: barrulus Date: Fri, 5 Sep 2025 17:18:21 +0100 Subject: [PATCH] Sky burgs + air routes: layer, editor toggles, styling, altitude, icons, and generators --- main.js | 1 + modules/renderers/draw-burg-icons.js | 15 +++++++++++++++ modules/routes-generator.js | 4 ++-- modules/ui/burg-editor.js | 13 +++++++++++-- modules/ui/burgs-overview.js | 2 +- modules/ui/editors.js | 22 +++++++++++++++------- 6 files changed, 45 insertions(+), 12 deletions(-) diff --git a/main.js b/main.js index 0d5d3c87..1a93189f 100644 --- a/main.js +++ b/main.js @@ -114,6 +114,7 @@ labels.append("g").attr("id", "states"); labels.append("g").attr("id", "addedLabels"); burgIcons.append("g").attr("id", "cities"); +burgIcons.append("g").attr("id", "skyburgs"); burgLabels.append("g").attr("id", "cities"); anchors.append("g").attr("id", "cities"); diff --git a/modules/renderers/draw-burg-icons.js b/modules/renderers/draw-burg-icons.js index cd737d33..9668f492 100644 --- a/modules/renderers/draw-burg-icons.js +++ b/modules/renderers/draw-burg-icons.js @@ -65,6 +65,21 @@ function drawBurgIcons() { .attr("width", townsAnchorsSize) .attr("height", townsAnchorsSize); + // 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); + TIME && console.timeEnd("drawBurgIcons"); // Sky burgs (flying or sky port) — styled separately diff --git a/modules/routes-generator.js b/modules/routes-generator.js index de7ec3d1..14dbd8f8 100644 --- a/modules/routes-generator.js +++ b/modules/routes-generator.js @@ -75,6 +75,8 @@ window.Routes = (function () { for (const burg of burgs) { if (burg.i && !burg.removed) { + // Exclude flying / sky port burgs from land road/trail graphs + if (burg.flying || burg.skyPort) continue; const {feature, capital, port} = burg; addBurg(burgsByFeature, feature, burg); @@ -846,13 +848,11 @@ window.Routes = (function () { 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); diff --git a/modules/ui/burg-editor.js b/modules/ui/burg-editor.js index 4c7cffc7..52a01535 100644 --- a/modules/ui/burg-editor.js +++ b/modules/ui/burg-editor.js @@ -334,6 +334,7 @@ function editBurg(id) { } // Regenerate routes to reflect air network regenerateRoutes(); + if (layerIsOn("toggleBurgIcons")) drawBurgIcons(); } else if (feature === "flying") { burg.flying = +turnOn; @@ -348,6 +349,7 @@ function editBurg(id) { } catch (e) { ERROR && console.error(e); } } regenerateRoutes(); + if (layerIsOn("toggleBurgIcons")) drawBurgIcons(); } else if (feature === "capital") toggleCapital(id); else burg[feature] = +turnOn; @@ -541,11 +543,18 @@ function editBurg(id) { } burg.x = x; burg.y = y; - if (burg.capital) pack.states[newState].center = burg.cell; - + if (burg.capital) pack.states[burg.state].center = burg.cell; + if (d3.event.shiftKey === false) toggleRelocateBurg(); } + function changeAltitude() { + const id = +elSelected.attr("data-id"); + const burg = pack.burgs[id]; + burg.altitude = Math.max(0, Math.round(+byId("burgAltitude").value)); + if (burg.flying) byId("burgElevation").innerHTML = `${burg.altitude} m (sky altitude)`; + } + function editBurgLegend() { const id = elSelected.attr("data-id"); const name = elSelected.text(); diff --git a/modules/ui/burgs-overview.js b/modules/ui/burgs-overview.js index 21309df1..c33ef695 100644 --- a/modules/ui/burgs-overview.js +++ b/modules/ui/burgs-overview.js @@ -288,7 +288,6 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) { return tip("There is already a burg in this cell. Please select a free cell", false, "error"); 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]; @@ -299,6 +298,7 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) { if (burg.state !== skyStateId) burg.state = skyStateId; // Keep as non-sea port burg.port = 0; + if (layerIsOn("toggleBurgIcons")) drawBurgIcons(); } if (d3.event.shiftKey === false) { diff --git a/modules/ui/editors.js b/modules/ui/editors.js index b6f10739..69c52a3d 100644 --- a/modules/ui/editors.js +++ b/modules/ui/editors.js @@ -189,7 +189,9 @@ function addBurg(point) { BurgsAndStates.defineBurgFeatures(burg); - const newRoute = Routes.connect(cellId); + // Do not auto-connect routes for water-placed (flying) burgs + const isWater = cells.h[cellId] < 20; + const newRoute = isWater ? null : Routes.connect(cellId); if (newRoute && layerIsOn("toggleRoutes")) { routes .select("#" + newRoute.group) @@ -230,7 +232,6 @@ 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; @@ -258,7 +259,6 @@ function ensureSkyState(anchorBurgId) { lock: 1, skyRealm: 1 }; - states.push(newState); // Assign the burg and its cell to the Sky State @@ -269,7 +269,6 @@ function ensureSkyState(anchorBurgId) { // Move to cities layer for capitals moveBurgToGroup(anchorBurgId, "cities"); } - return i; } @@ -367,7 +366,7 @@ function getBurgLink(burg) { const population = burg.population * populationRate * urbanization; if (population >= options.villageMaxPopulation || burg.citadel || burg.walls || burg.temple || burg.shanty) - return createMfcgLink(burg); + return createMfcgLink(burg, false); return createVillageGeneratorLink(burg); } @@ -386,6 +385,7 @@ function createMfcgLink(burg, isSky = false) { const sea = !isSky && coast && cells.haven[cell] ? (() => { + // calculate sea 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; @@ -407,6 +407,7 @@ function createMfcgLink(burg, isSky = false) { const greens = isSky ? 1 : undefined; const gates = isSky ? 0 : -1; + const url = new URL("https://watabou.github.io/city-generator/"); const params = { name, population, @@ -425,8 +426,6 @@ function createMfcgLink(burg, isSky = false) { }; 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); @@ -483,6 +482,15 @@ function createVillageGeneratorLink(burg) { return url.toString(); } +// helper: draw legend entry for Air routes +function drawAirRoutesLegend() { + const group = document.querySelector("#airroutes"); + if (!group) return tip("Air routes group not found", false, "error"); + const stroke = group.getAttribute("stroke") || "#8a2be2"; + const data = [["airroutes", stroke, "Air routes"]]; + drawLegend("Routes", data); +} + // draw legend box function drawLegend(name, data) { legend.selectAll("*").remove(); // fully redraw every time