diff --git a/public/main.js b/public/main.js
index 6da462d5..16f0b302 100644
--- a/public/main.js
+++ b/public/main.js
@@ -632,6 +632,9 @@ async function generate(options) {
Biomes.define();
Features.defineGroups();
+ Ice.initialize();
+ Ice.generate();
+
rankCells();
Cultures.generate();
Cultures.expand();
diff --git a/public/modules/ice.js b/public/modules/ice.js
new file mode 100644
index 00000000..18b535b1
--- /dev/null
+++ b/public/modules/ice.js
@@ -0,0 +1,170 @@
+"use strict";
+
+// Ice layer data model - separates ice data from SVG rendering
+const Ice = (() => {
+ // Initialize ice data structure
+ function initialize() {
+ pack.ice = {
+ glaciers: [], // auto-generated glaciers on cold land
+ icebergs: [] // manually edited and auto-generated icebergs on cold water
+ };
+ }
+
+ // Generate glaciers and icebergs based on temperature and height
+ function generate() {
+ const {cells, features} = grid;
+ const {temp, h} = cells;
+ Math.random = aleaPRNG(seed);
+
+ const ICEBERG_MAX_TEMP = 0;
+ const GLACIER_MAX_TEMP = -8;
+ const minMaxTemp = d3.min(temp);
+
+ // Generate glaciers on cold land
+ {
+ const type = "iceShield";
+ const getType = cellId =>
+ h[cellId] >= 20 && temp[cellId] <= GLACIER_MAX_TEMP ? type : null;
+ const isolines = getIsolines(grid, getType, {polygons: true});
+
+ if (isolines[type]?.polygons) {
+ isolines[type].polygons.forEach(points => {
+ const clipped = clipPoly(points);
+ pack.ice.glaciers.push({
+ points: clipped,
+ offset: null
+ });
+ });
+ }
+ }
+
+ // Generate icebergs on cold water
+ for (const cellId of grid.cells.i) {
+ const t = temp[cellId];
+ if (h[cellId] >= 20) continue; // no icebergs on land
+ if (t > ICEBERG_MAX_TEMP) continue; // too warm: no icebergs
+ if (features[cells.f[cellId]].type === "lake") continue; // no icebergs on lakes
+ if (P(0.8)) continue; // skip most of eligible cells
+
+ const randomFactor = 0.8 + rand() * 0.4; // random size factor
+ let baseSize = (1 - normalize(t, minMaxTemp, 1)) * 0.8; // size: 0 = zero, 1 = full
+ if (cells.t[cellId] === -1) baseSize /= 1.3; // coastline: smaller icebergs
+ const size = minmax(rn(baseSize * randomFactor, 2), 0.1, 1);
+
+ const [cx, cy] = grid.points[cellId];
+ const points = getGridPolygon(cellId).map(([x, y]) => [
+ rn(lerp(cx, x, size), 2),
+ rn(lerp(cy, y, size), 2)
+ ]);
+
+ pack.ice.icebergs.push({
+ cellId,
+ size,
+ points,
+ offset: null
+ });
+ }
+ }
+
+ // Add a new iceberg (manual editing)
+ function addIceberg(cellId, size) {
+ const [cx, cy] = grid.points[cellId];
+ const points = getGridPolygon(cellId).map(([x, y]) => [
+ rn(lerp(cx, x, size), 2),
+ rn(lerp(cy, y, size), 2)
+ ]);
+
+ pack.ice.icebergs.push({
+ cellId,
+ size,
+ points,
+ offset: null
+ });
+
+ return pack.ice.icebergs.length - 1; // return index
+ }
+
+ // Remove ice element by index
+ function removeIce(type, index) {
+ if (type === "glacier" && pack.ice.glaciers[index]) {
+ pack.ice.glaciers.splice(index, 1);
+ } else if (type === "iceberg" && pack.ice.icebergs[index]) {
+ pack.ice.icebergs.splice(index, 1);
+ }
+ }
+
+ // Update iceberg points and size
+ function updateIceberg(index, points, size) {
+ if (pack.ice.icebergs[index]) {
+ pack.ice.icebergs[index].points = points;
+ pack.ice.icebergs[index].size = size;
+ }
+ }
+
+ // Randomize iceberg shape
+ function randomizeIcebergShape(index) {
+ const iceberg = pack.ice.icebergs[index];
+ if (!iceberg) return;
+
+ const cellId = iceberg.cellId;
+ const size = iceberg.size;
+ const [cx, cy] = grid.points[cellId];
+
+ // Get a different random cell for the polygon template
+ const i = ra(grid.cells.i);
+ const cn = grid.points[i];
+ const poly = getGridPolygon(i).map(p => [p[0] - cn[0], p[1] - cn[1]]);
+ const points = poly.map(p => [
+ rn(cx + p[0] * size, 2),
+ rn(cy + p[1] * size, 2)
+ ]);
+
+ iceberg.points = points;
+ }
+
+ // Change iceberg size and recalculate points
+ function changeIcebergSize(index, newSize) {
+ const iceberg = pack.ice.icebergs[index];
+ if (!iceberg) return;
+
+ const cellId = iceberg.cellId;
+ const [cx, cy] = grid.points[cellId];
+ const oldSize = iceberg.size;
+
+ // Recalculate points based on new size
+ const flat = iceberg.points.flat();
+ const pairs = [];
+ while (flat.length) pairs.push(flat.splice(0, 2));
+ const poly = pairs.map(p => [(p[0] - cx) / oldSize, (p[1] - cy) / oldSize]);
+ const points = poly.map(p => [
+ rn(cx + p[0] * newSize, 2),
+ rn(cy + p[1] * newSize, 2)
+ ]);
+
+ iceberg.points = points;
+ iceberg.size = newSize;
+ }
+
+ // Get all ice data
+ function getData() {
+ return pack.ice;
+ }
+
+ // Clear all ice
+ function clear() {
+ pack.ice.glaciers = [];
+ pack.ice.icebergs = [];
+ }
+
+ return {
+ initialize,
+ generate,
+ addIceberg,
+ removeIce,
+ updateIceberg,
+ randomizeIcebergShape,
+ changeIcebergSize,
+ getData,
+ clear
+ };
+})();
diff --git a/public/modules/io/load.js b/public/modules/io/load.js
index 689757b2..5414f8dd 100644
--- a/public/modules/io/load.js
+++ b/public/modules/io/load.js
@@ -406,6 +406,7 @@ async function parseLoadedData(data, mapVersion) {
pack.cells.province = data[27] ? Uint16Array.from(data[27].split(",")) : new Uint16Array(pack.cells.i.length);
// data[28] had deprecated cells.crossroad
pack.cells.routes = data[36] ? JSON.parse(data[36]) : {};
+ pack.ice = data[39] ? JSON.parse(data[39]) : {glaciers: [], icebergs: []};
if (data[31]) {
const namesDL = data[31].split("/");
@@ -449,7 +450,11 @@ async function parseLoadedData(data, mapVersion) {
if (isVisible(routes) && hasChild(routes, "path")) turnOn("toggleRoutes");
if (hasChildren(temperature)) turnOn("toggleTemperature");
if (hasChild(population, "line")) turnOn("togglePopulation");
- if (hasChildren(ice)) turnOn("toggleIce");
+ if (pack.ice?.glaciers?.length || pack.ice?.icebergs?.length) {
+ ice.selectAll("*").remove(); // clear old SVG
+ drawIce(); // re-render ice from data
+ turnOn("toggleIce");
+ }
if (hasChild(prec, "circle")) turnOn("togglePrecipitation");
if (isVisible(emblems) && hasChild(emblems, "use")) turnOn("toggleEmblems");
if (isVisible(labels)) turnOn("toggleLabels");
diff --git a/public/modules/io/save.js b/public/modules/io/save.js
index 304fef59..4b60fa71 100644
--- a/public/modules/io/save.js
+++ b/public/modules/io/save.js
@@ -102,6 +102,7 @@ function prepareMapData() {
const cellRoutes = JSON.stringify(pack.cells.routes);
const routes = JSON.stringify(pack.routes);
const zones = JSON.stringify(pack.zones);
+ const ice = JSON.stringify(pack.ice);
// store name array only if not the same as default
const defaultNB = Names.getNameBases();
@@ -155,7 +156,8 @@ function prepareMapData() {
markers,
cellRoutes,
routes,
- zones
+ zones,
+ ice
].join("\r\n");
return mapData;
}
diff --git a/public/modules/renderers/draw-ice.js b/public/modules/renderers/draw-ice.js
new file mode 100644
index 00000000..37075275
--- /dev/null
+++ b/public/modules/renderers/draw-ice.js
@@ -0,0 +1,37 @@
+"use strict";
+
+// Ice layer renderer - renders ice from data model to SVG
+function drawIce() {
+ TIME && console.time("drawIce");
+
+ // Clear existing ice SVG
+ ice.selectAll("*").remove();
+
+ // Draw glaciers
+ pack.ice.glaciers.forEach((glacier, index) => {
+ ice
+ .append("polygon")
+ .attr("points", glacier.points)
+ .attr("type", "iceShield")
+ .attr("data-index", index)
+ .attr("class", "glacier");
+ });
+
+ // Draw icebergs
+ pack.ice.icebergs.forEach((iceberg, index) => {
+ ice
+ .append("polygon")
+ .attr("points", iceberg.points)
+ .attr("cell", iceberg.cellId)
+ .attr("size", iceberg.size)
+ .attr("data-index", index)
+ .attr("class", "iceberg");
+ });
+
+ TIME && console.timeEnd("drawIce");
+}
+
+// Re-render ice layer from data model
+function redrawIce() {
+ drawIce();
+}
diff --git a/public/modules/ui/ice-editor.js b/public/modules/ui/ice-editor.js
index a9e6ff28..0919fcf6 100644
--- a/public/modules/ui/ice-editor.js
+++ b/public/modules/ui/ice-editor.js
@@ -5,10 +5,14 @@ function editIce() {
if (!layerIsOn("toggleIce")) toggleIce();
elSelected = d3.select(d3.event.target);
- const type = elSelected.attr("type") ? "Glacier" : "Iceberg";
- document.getElementById("iceRandomize").style.display = type === "Glacier" ? "none" : "inline-block";
- document.getElementById("iceSize").style.display = type === "Glacier" ? "none" : "inline-block";
- if (type === "Iceberg") document.getElementById("iceSize").value = +elSelected.attr("size");
+ const index = +elSelected.attr("data-index");
+ const isGlacier = elSelected.attr("type") === "iceShield";
+ const type = isGlacier ? "Glacier" : "Iceberg";
+
+ document.getElementById("iceRandomize").style.display = isGlacier ? "none" : "inline-block";
+ document.getElementById("iceSize").style.display = isGlacier ? "none" : "inline-block";
+ if (!isGlacier) document.getElementById("iceSize").value = +elSelected.attr("size");
+
ice.selectAll("*").classed("draggable", true).call(d3.drag().on("drag", dragElement));
$("#iceEditor").dialog({
@@ -29,28 +33,18 @@ function editIce() {
document.getElementById("iceRemove").addEventListener("click", removeIce);
function randomizeShape() {
- const c = grid.points[+elSelected.attr("cell")];
- const s = +elSelected.attr("size");
- const i = ra(grid.cells.i),
- cn = grid.points[i];
- const poly = getGridPolygon(i).map(p => [p[0] - cn[0], p[1] - cn[1]]);
- const points = poly.map(p => [rn(c[0] + p[0] * s, 2), rn(c[1] + p[1] * s, 2)]);
- elSelected.attr("points", points);
+ Ice.randomizeIcebergShape(index);
+ redrawIce();
+ elSelected = ice.selectAll(`[data-index="${index}"]`).node();
+ elSelected = d3.select(elSelected);
}
function changeSize() {
- const c = grid.points[+elSelected.attr("cell")];
- const s = +elSelected.attr("size");
- const flat = elSelected
- .attr("points")
- .split(",")
- .map(el => +el);
- const pairs = [];
- while (flat.length) pairs.push(flat.splice(0, 2));
- const poly = pairs.map(p => [(p[0] - c[0]) / s, (p[1] - c[1]) / s]);
- const size = +this.value;
- const points = poly.map(p => [rn(c[0] + p[0] * size, 2), rn(c[1] + p[1] * size, 2)]);
- elSelected.attr("points", points).attr("size", size);
+ const newSize = +this.value;
+ Ice.changeIcebergSize(index, newSize);
+ redrawIce();
+ elSelected = ice.selectAll(`[data-index="${index}"]`).node();
+ elSelected = d3.select(elSelected);
}
function toggleAdd() {
@@ -67,17 +61,16 @@ function editIce() {
function addIcebergOnClick() {
const [x, y] = d3.mouse(this);
const i = findGridCell(x, y, grid);
- const [cx, cy] = grid.points[i];
const size = +document.getElementById("iceSize")?.value || 1;
- const points = getGridPolygon(i).map(([x, y]) => [rn(lerp(cx, x, size), 2), rn(lerp(cy, y, size), 2)]);
- const iceberg = ice.append("polygon").attr("points", points).attr("cell", i).attr("size", size);
- iceberg.call(d3.drag().on("drag", dragElement));
+ Ice.addIceberg(i, size);
+ redrawIce();
+
if (d3.event.shiftKey === false) toggleAdd();
}
function removeIce() {
- const type = elSelected.attr("type") ? "Glacier" : "Iceberg";
+ const type = isGlacier ? "Glacier" : "Iceberg";
alertMessage.innerHTML = /* html */ `Are you sure you want to remove the ${type}?`;
$("#alert").dialog({
resizable: false,
@@ -85,7 +78,8 @@ function editIce() {
buttons: {
Remove: function () {
$(this).dialog("close");
- elSelected.remove();
+ Ice.removeIce(isGlacier ? "glacier" : "iceberg", index);
+ redrawIce();
$("#iceEditor").dialog("close");
},
Cancel: function () {
@@ -96,14 +90,25 @@ function editIce() {
}
function dragElement() {
- const tr = parseTransform(this.getAttribute("transform"));
- const dx = +tr[0] - d3.event.x,
- dy = +tr[1] - d3.event.y;
+ const isGlacier = elSelected.attr("type") === "iceShield";
+ const idx = +elSelected.attr("data-index");
+ const initialTransform = parseTransform(this.getAttribute("transform"));
+ const dx = initialTransform[0] - d3.event.x;
+ const dy = initialTransform[1] - d3.event.y;
d3.event.on("drag", function () {
- const x = d3.event.x,
- y = d3.event.y;
- this.setAttribute("transform", `translate(${dx + x},${dy + y})`);
+ const x = d3.event.x;
+ const y = d3.event.y;
+ const transform = `translate(${dx + x},${dy + y})`;
+ this.setAttribute("transform", transform);
+
+ // Update data model with new position
+ const offset = [dx + x, dy + y];
+ const iceData = isGlacier ? pack.ice.glaciers[idx] : pack.ice.icebergs[idx];
+ if (iceData) {
+ // Store offset for visual positioning, actual geometry stays in points
+ iceData.offset = offset;
+ }
});
}
@@ -114,3 +119,4 @@ function editIce() {
unselect();
}
}
+
diff --git a/public/modules/ui/layers.js b/public/modules/ui/layers.js
index 5037a5ee..ce619937 100644
--- a/public/modules/ui/layers.js
+++ b/public/modules/ui/layers.js
@@ -420,42 +420,31 @@ function toggleIce(event) {
function drawIce() {
TIME && console.time("drawIce");
- const {cells, features} = grid;
- const {temp, h} = cells;
- Math.random = aleaPRNG(seed);
+ // Clear existing ice SVG
+ ice.selectAll("*").remove();
- const ICEBERG_MAX_TEMP = 0;
- const GLACIER_MAX_TEMP = -8;
- const minMaxTemp = d3.min(temp);
+ // Draw glaciers
+ pack.ice.glaciers.forEach((glacier, index) => {
+ ice
+ .append("polygon")
+ .attr("points", glacier.points)
+ .attr("type", "iceShield")
+ .attr("data-index", index)
+ .attr("class", "glacier")
+ .attr("transform", glacier.offset ? `translate(${glacier.offset[0]},${glacier.offset[1]})` : null);
+ });
- // cold land: draw glaciers
- {
- const type = "iceShield";
- const getType = cellId => (h[cellId] >= 20 && temp[cellId] <= GLACIER_MAX_TEMP ? type : null);
- const isolines = getIsolines(grid, getType, {polygons: true});
- isolines[type]?.polygons?.forEach(points => {
- const clipped = clipPoly(points);
- ice.append("polygon").attr("points", clipped).attr("type", type);
- });
- }
-
- // cold water: draw icebergs
- for (const cellId of grid.cells.i) {
- const t = temp[cellId];
- if (h[cellId] >= 20) continue; // no icebergs on land
- if (t > ICEBERG_MAX_TEMP) continue; // too warm: no icebergs
- if (features[cells.f[cellId]].type === "lake") continue; // no icebers on lakes
- if (P(0.8)) continue; // skip most of eligible cells
-
- const randomFactor = 0.8 + rand() * 0.4; // random size factor
- let baseSize = (1 - normalize(t, minMaxTemp, 1)) * 0.8; // size: 0 = zero size, 1 = full size
- if (cells.t[cellId] === -1) baseSize /= 1.3; // coasline: smaller icebergs
- const size = minmax(rn(baseSize * randomFactor, 2), 0.1, 1);
-
- const [cx, cy] = grid.points[cellId];
- const points = getGridPolygon(cellId).map(([x, y]) => [rn(lerp(cx, x, size), 2), rn(lerp(cy, y, size), 2)]);
- ice.append("polygon").attr("points", points).attr("cell", cellId).attr("size", size);
- }
+ // Draw icebergs
+ pack.ice.icebergs.forEach((iceberg, index) => {
+ ice
+ .append("polygon")
+ .attr("points", iceberg.points)
+ .attr("cell", iceberg.cellId)
+ .attr("size", iceberg.size)
+ .attr("data-index", index)
+ .attr("class", "iceberg")
+ .attr("transform", iceberg.offset ? `translate(${iceberg.offset[0]},${iceberg.offset[1]})` : null);
+ });
TIME && console.timeEnd("drawIce");
}
diff --git a/public/modules/ui/tools.js b/public/modules/ui/tools.js
index a3df5c00..eade993f 100644
--- a/public/modules/ui/tools.js
+++ b/public/modules/ui/tools.js
@@ -555,7 +555,7 @@ function regenerateMilitary() {
function regenerateIce() {
if (!layerIsOn("toggleIce")) toggleIce();
- ice.selectAll("*").remove();
+ Ice.generate();
drawIce();
}
diff --git a/src/index.html b/src/index.html
index 21d84187..8a5c4e6b 100644
--- a/src/index.html
+++ b/src/index.html
@@ -8491,40 +8491,43 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
@@ -8535,7 +8538,7 @@
-
+
@@ -8566,8 +8569,8 @@
-
-
+
+
@@ -8583,5 +8586,6 @@
+