From c4663e7b9b7486f730fe7e617e9fc21c822852f0 Mon Sep 17 00:00:00 2001 From: barrulus Date: Sat, 6 Sep 2025 01:02:21 +0100 Subject: [PATCH] fix: sky port behavior, default sky altitude, Sky Realm robustness, and unfly state restore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary Ground burgs with sky ports keep land capabilities and land routes. Only truly flying burgs join the Sky Realm; turning off “flying” restores the ground state. Replace magic altitude 1000 with a DOM-configurable default. Guard state hover/pin logic for geometry-less Sky Realm to prevent console errors. Watabou link uses sky settings only for flying burgs. Details Land routing with sky ports: Change: include sky ports in land road/trail networks; exclude only flying burgs. Why: sky ports should not impose flying/land restrictions on a ground burg. File: modules/routes-generator.js Sky port toggle does not move state: Change: toggling skyPort no longer assigns the burg’s state to the Sky Realm or reassigns cell ownership. Why: sky port ≠ flying city; it’s just air connectivity. File: modules/ui/burg-editor.js Flying toggle and state restore: Change: enabling “flying” assigns the burg to the Sky Realm but does not overwrite the cell’s ground state; disabling “flying” restores the burg’s state to the underlying ground state (pack.cells.state[burg.cell]). Change: when relocating, assign Sky Realm only if the burg is placed over water; ground placements keep ground state. Why: fixes bug where reverting to ground left burg in Sky Realm and preserves ground sovereignty. File: modules/ui/burg-editor.js Default sky altitude via DOM (no more magic 1000): Change: add hidden input #burgDefaultSkyAltitude (default 1000 m) and read it everywhere altitude is defaulted (editor and “add burg on water” flow). Why: centralizes default, documents units, and makes it configurable without touching code. Files: index.html, modules/ui/burg-editor.js, modules/ui/burgs-overview.js Sky State creation robustness: Change: initialize a neutral diplomacy row for the new Sky State and extend existing states’ diplomacy arrays. Change: do not forcibly set the anchor cell ownership to Sky state when creating the Sky Realm. Why: prevents UI/editor crashes that assume diplomacy is rectangular; avoids accidental land transfer to Sky Realm. File: modules/ui/editors.js State hover/pin guards for Sky Realm: Change: skip fog/hover/highlight when the target state has no geometry (e.g., Sky Realm) or the path is missing. Why: fixes console errors in State hover and Pin feature. Files: modules/dynamic/editors/states-editor.js, modules/ui/diplomacy-editor.js, modules/ui/military-overview.js Watabou integration: Change: only treat burgs as “sky” for city links when flying is true (not when only skyPort is set). Why: ground burgs with sky ports should still have gates/roads/hubs/shanty, etc. File: modules/ui/editors.js User-visible Effects Ground cities can have sea port + sky port + roads/trails simultaneously. Toggling flying on shows altitude row; default altitude is read from the DOM. Toggling flying off restores the burg to its ground state affiliation. No more console errors when interacting with state hover/pin around the Sky Realm. Potential Impacts Land route generation may produce additional roads because sky-ported (but ground) burgs are now included. Diplomacy arrays gain a neutral column for the Sky Realm when first created (non-breaking, UI-aligned). Test Plan Set sky port on a ground burg: verify roads/trails remain; sea port and other features unaffected. Toggle flying on: burg state switches to Sky Realm; cell remains with ground state; altitude defaults from #burgDefaultSkyAltitude; air routes generate. Toggle flying off: burg state returns to the ground state; altitude row hides; routes regenerate. Hover/fog/pin states with the editors open: no errors with Sky Realm selected or hovered. Open Watabou (city generator) links: Ground + sky port: uses ground settings (gates/roads/hubs/shanty allowed). Flying: uses sky settings (no farms/gates/hub etc. as appropriate). Files Changed modules/routes-generator.js modules/ui/burg-editor.js modules/ui/burgs-overview.js modules/ui/editors.js modules/dynamic/editors/states-editor.js modules/ui/diplomacy-editor.js modules/ui/military-overview.js index.html --- index.html | 2 ++ modules/dynamic/editors/states-editor.js | 3 ++ modules/routes-generator.js | 4 +-- modules/ui/burg-editor.js | 39 ++++++++++++------------ modules/ui/burgs-overview.js | 8 ++++- modules/ui/diplomacy-editor.js | 1 + modules/ui/editors.js | 20 +++++++++--- modules/ui/military-overview.js | 1 + versioning.js | 12 ++++---- 9 files changed, 58 insertions(+), 32 deletions(-) diff --git a/index.html b/index.html index 64023f4f..0a24a113 100644 --- a/index.html +++ b/index.html @@ -3413,6 +3413,8 @@
Altitude:
m + +
diff --git a/modules/dynamic/editors/states-editor.js b/modules/dynamic/editors/states-editor.js index 69d2618b..1636f0da 100644 --- a/modules/dynamic/editors/states-editor.js +++ b/modules/dynamic/editors/states-editor.js @@ -572,8 +572,11 @@ function stateChangeExpansionism(state, line, value) { function toggleFog(state, cl) { if (customization) return; + // Skip virtual/sky states that have no geometry + if (pack.states[state]?.skyRealm) return; const path = statesBody.select("#state" + state).attr("d"), id = "focusState" + state; + if (!path) return; // nothing to fog/unfog cl.contains("inactive") ? fog(id, path) : unfog(id); cl.toggle("inactive"); } diff --git a/modules/routes-generator.js b/modules/routes-generator.js index a47d044f..5ca969ac 100644 --- a/modules/routes-generator.js +++ b/modules/routes-generator.js @@ -74,8 +74,8 @@ window.Routes = (function () { }; for (const burg of burgs) { - if (burg.i && !burg.removed && !burg.flying && !burg.skyPort) { - // Exclude flying / sky port burgs from land road/trail graphs + if (burg.i && !burg.removed && !burg.flying) { + // Exclude only flying burgs from land road/trail graphs const {feature, capital, port} = burg; addBurg(burgsByFeature, feature, burg); diff --git a/modules/ui/burg-editor.js b/modules/ui/burg-editor.js index 52a01535..ecaf3ad1 100644 --- a/modules/ui/burg-editor.js +++ b/modules/ui/burg-editor.js @@ -81,8 +81,10 @@ function editBurg(id) { 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)`; + const defAlt = getDefaultSkyAltitude(); + const altitude = b.altitude ?? defAlt; + byId("burgAltitude").value = altitude; + byId("burgElevation").innerHTML = `${altitude} m (sky altitude)`; } else { altitudeRow.style.display = "none"; byId("burgElevation").innerHTML = getHeight(pack.cells.h[b.cell]); @@ -318,20 +320,8 @@ function editBurg(id) { const turnOn = this.classList.contains("inactive"); if (feature === "port") togglePort(id); else if (feature === "skyPort") { + // Sky port should not change land restrictions or state 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(); if (layerIsOn("toggleBurgIcons")) drawBurgIcons(); @@ -342,11 +332,15 @@ function editBurg(id) { 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; + if (burg.altitude == null) burg.altitude = getDefaultSkyAltitude(); } catch (e) { ERROR && console.error(e); } + } else { + // Turning flying off: restore ground state ownership for the burg + // Do not change cells.state here; use the underlying cell state + const groundState = pack.cells.state[burg.cell] || 0; + burg.state = groundState; } regenerateRoutes(); if (layerIsOn("toggleBurgIcons")) drawBurgIcons(); @@ -532,9 +526,10 @@ function editBurg(id) { cells.burg[burg.cell] = 0; cells.burg[cell] = id; burg.cell = cell; - + if (isWater) { + // Set target state based on terrain and sky features - if (isWater || burg.flying) { + if (isWater) { const skyId = ensureSkyState(id); cells.state[cell] = skyId; burg.state = skyId; @@ -555,6 +550,12 @@ function editBurg(id) { if (burg.flying) byId("burgElevation").innerHTML = `${burg.altitude} m (sky altitude)`; } + function getDefaultSkyAltitude() { + const el = byId("burgDefaultSkyAltitude"); + const v = el ? +el.value : NaN; + return Number.isFinite(v) && v >= 0 ? v : 1000; + } + 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 c33ef695..095d8271 100644 --- a/modules/ui/burgs-overview.js +++ b/modules/ui/burgs-overview.js @@ -293,7 +293,13 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) { const burg = pack.burgs[id]; burg.flying = 1; burg.skyPort = 1; - if (burg.altitude == null) burg.altitude = 1000; + try { + const defAltEl = byId("burgDefaultSkyAltitude"); + const defAlt = defAltEl ? +defAltEl.value : NaN; + burg.altitude = burg.altitude ?? (Number.isFinite(defAlt) && defAlt >= 0 ? defAlt : 1000); + } catch (e) { + burg.altitude = burg.altitude ?? 1000; + } const skyStateId = ensureSkyState(id); if (burg.state !== skyStateId) burg.state = skyStateId; // Keep as non-sea port diff --git a/modules/ui/diplomacy-editor.js b/modules/ui/diplomacy-editor.js index 0f363b7b..b77860a8 100644 --- a/modules/ui/diplomacy-editor.js +++ b/modules/ui/diplomacy-editor.js @@ -151,6 +151,7 @@ function editDiplomacy() { const state = +event.target.dataset.id; if (customization || !state) return; const d = regions.select("#state" + state).attr("d"); + if (!d) return; // no geometry to highlight (e.g., Sky Realm) const path = debug .append("path") diff --git a/modules/ui/editors.js b/modules/ui/editors.js index b0188edc..87b64521 100644 --- a/modules/ui/editors.js +++ b/modules/ui/editors.js @@ -259,10 +259,22 @@ function ensureSkyState(anchorBurgId) { lock: 1, skyRealm: 1 }; + // initialize minimal diplomacy for the new Sky State + try { + const diplomacy = states.map(s => (s && s.i && !s.removed ? "Neutral" : "x")); + diplomacy[i] = "x"; + newState.diplomacy = diplomacy; + // extend existing states' diplomacy; skip state 0 (chronicle holder) + for (const s of states) { + if (s && s.i && !s.removed && Array.isArray(s.diplomacy)) s.diplomacy.push("Neutral"); + } + } catch (err) { + // fail-safe: do nothing if diplomacy cannot be set up + } + states.push(newState); - // Assign the burg and its cell to the Sky State - if (cells && typeof b.cell === "number") cells.state[b.cell] = i; + // Do not assign the cell itself to Sky state; keep underlying ground state mapping if (b) { b.state = i; b.capital = 1; @@ -361,8 +373,8 @@ 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); + // Sky-only: use sky generator options only for truly flying burgs + if (burg.flying) return createMfcgLink(burg, true); const population = burg.population * populationRate * urbanization; if (population >= options.villageMaxPopulation || burg.citadel || burg.walls || burg.temple || burg.shanty) diff --git a/modules/ui/military-overview.js b/modules/ui/military-overview.js index 46a86138..1b229fe1 100644 --- a/modules/ui/military-overview.js +++ b/modules/ui/military-overview.js @@ -176,6 +176,7 @@ function overviewMilitary() { if (!layerIsOn("toggleStates")) return; const d = regions.select("#state" + state).attr("d"); + if (!d) return; // no geometry to highlight (e.g., Sky Realm) const path = debug .append("path") diff --git a/versioning.js b/versioning.js index 05152dd6..b618c103 100644 --- a/versioning.js +++ b/versioning.js @@ -22,7 +22,7 @@ if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format o if (loadingScreenVersion) loadingScreenVersion.innerText = `v${VERSION}`; const storedVersion = localStorage.getItem("version"); - if (compareVersions(storedVersion, VERSION, {major: true, minor: true, patch: false}).isOlder) { + if (compareVersions(storedVersion, VERSION, { major: true, minor: true, patch: false }).isOlder) { setTimeout(showUpdateWindow, 6000); } @@ -45,14 +45,14 @@ if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format o
  • Styling: separate #burgIcons > #skyburgs group for sky burg icons
  • -

    Join our Discord server and Reddit community to ask questions, share maps, discuss the Generator and Worlbuilding, report bugs and propose new features.

    +

    Join our Discord server and Reddit community to ask questions, share maps, discuss the Generator and Worldbuilding, report bugs and propose new features.

    Thanks for all supporters on Patreon!`; $("#alert").dialog({ resizable: false, title: "Fantasy Map Generator update", width: "28em", - position: {my: "center center-4em", at: "center", of: "svg"}, + position: { my: "center center-4em", at: "center", of: "svg" }, buttons: { "Clear cache": () => cleanupData(), "Don't show again": function () { @@ -100,8 +100,8 @@ function isValidVersion(versionString) { return !isNaN(major) && !isNaN(minor) && !isNaN(patch); } -function compareVersions(version1, version2, options = {major: true, minor: true, patch: true}) { - if (!isValidVersion(version1) || !isValidVersion(version2)) return {isEqual: false, isNewer: false, isOlder: false}; +function compareVersions(version1, version2, options = { major: true, minor: true, patch: true }) { + if (!isValidVersion(version1) || !isValidVersion(version2)) return { isEqual: false, isNewer: false, isOlder: false }; let [major1, minor1, patch1] = version1.split(".").map(Number); let [major2, minor2, patch2] = version2.split(".").map(Number); @@ -114,5 +114,5 @@ function compareVersions(version1, version2, options = {major: true, minor: true const isNewer = major1 > major2 || (major1 === major2 && (minor1 > minor2 || (minor1 === minor2 && patch1 > patch2))); const isOlder = major1 < major2 || (major1 === major2 && (minor1 < minor2 || (minor1 === minor2 && patch1 < patch2))); - return {isEqual, isNewer, isOlder}; + return { isEqual, isNewer, isOlder }; }