diff --git a/public/main.js b/public/main.js index 6da462d5..7dbb9585 100644 --- a/public/main.js +++ b/public/main.js @@ -632,6 +632,8 @@ async function generate(options) { Biomes.define(); Features.defineGroups(); + Ice.generate(); + rankCells(); Cultures.generate(); Cultures.expand(); diff --git a/public/modules/dynamic/auto-update.js b/public/modules/dynamic/auto-update.js index a3190e3b..0b1cd227 100644 --- a/public/modules/dynamic/auto-update.js +++ b/public/modules/dynamic/auto-update.js @@ -253,8 +253,8 @@ export function resolveVersionConflicts(mapVersion) { const source = findCell(s.x, s.y); const mouth = findCell(e.x, e.y); const name = Rivers.getName(mouth); - const type = length < 25 ? rw({Creek: 9, River: 3, Brook: 3, Stream: 1}) : "River"; - pack.rivers.push({i, parent: 0, length, source, mouth, basin: i, name, type}); + const type = length < 25 ? rw({ Creek: 9, River: 3, Brook: 3, Stream: 1 }) : "River"; + pack.rivers.push({ i, parent: 0, length, source, mouth, basin: i, name, type }); }); } @@ -270,7 +270,7 @@ export function resolveVersionConflicts(mapVersion) { const era = Names.getBaseShort(P(0.7) ? 1 : rand(nameBases.length)) + " Era"; const eraShort = era[0] + "E"; const military = Military.getDefaultOptions(); - options = {winds, year, era, eraShort, military}; + options = { winds, year, era, eraShort, military }; // v1.3 added campaings data for all states States.generateCampaigns(); @@ -481,7 +481,7 @@ export function resolveVersionConflicts(mapVersion) { if (isOlderThan("1.65.0")) { // v1.65 changed rivers data d3.select("#rivers").attr("style", null); // remove style to unhide layer - const {cells, rivers} = pack; + const { cells, rivers } = pack; const defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2); for (const river of rivers) { @@ -497,8 +497,8 @@ export function resolveVersionConflicts(mapVersion) { for (let i = 0; i <= segments; i++) { const shift = increment * i; - const {x: x1, y: y1} = node.getPointAtLength(length + shift); - const {x: x2, y: y2} = node.getPointAtLength(length - shift); + const { x: x1, y: y1 } = node.getPointAtLength(length + shift); + const { x: x2, y: y2 } = node.getPointAtLength(length - shift); const x = rn((x1 + x2) / 2, 1); const y = rn((y1 + y2) / 2, 1); @@ -565,7 +565,7 @@ export function resolveVersionConflicts(mapVersion) { const fill = circle && circle.getAttribute("fill"); const stroke = circle && circle.getAttribute("stroke"); - const marker = {i, icon, type, x, y, size, cell}; + const marker = { i, icon, type, x, y, size, cell }; if (size && size !== 30) marker.size = size; if (!isNaN(px) && px !== 12) marker.px = px; if (!isNaN(dx) && dx !== 50) marker.dx = dx; @@ -631,7 +631,7 @@ export function resolveVersionConflicts(mapVersion) { if (isOlderThan("1.88.0")) { // v1.87 may have incorrect shield for some reason - pack.states.forEach(({coa}) => { + pack.states.forEach(({ coa }) => { if (coa?.shield === "state") delete coa.shield; }); } @@ -639,13 +639,13 @@ export function resolveVersionConflicts(mapVersion) { if (isOlderThan("1.91.0")) { // from 1.91.00 custom coa is moved to coa object pack.states.forEach(state => { - if (state.coa === "custom") state.coa = {custom: true}; + if (state.coa === "custom") state.coa = { custom: true }; }); pack.provinces.forEach(province => { - if (province.coa === "custom") province.coa = {custom: true}; + if (province.coa === "custom") province.coa = { custom: true }; }); pack.burgs.forEach(burg => { - if (burg.coa === "custom") burg.coa = {custom: true}; + if (burg.coa === "custom") burg.coa = { custom: true }; }); // from 1.91.00 emblems don't have transform attribute @@ -747,7 +747,7 @@ export function resolveVersionConflicts(mapVersion) { const skip = terrs.attr("skip"); const relax = terrs.attr("relax"); - const curveTypes = {0: "curveBasisClosed", 1: "curveLinear", 2: "curveStep"}; + const curveTypes = { 0: "curveBasisClosed", 1: "curveLinear", 2: "curveStep" }; const curve = curveTypes[terrs.attr("curve")] || "curveBasisClosed"; terrs @@ -882,7 +882,7 @@ export function resolveVersionConflicts(mapVersion) { const secondCellId = points[1][2]; const feature = pack.cells.f[secondCellId]; - pack.routes.push({i: pack.routes.length, group, feature, points}); + pack.routes.push({ i: pack.routes.length, group, feature, points }); } } routes.selectAll("path").remove(); @@ -914,7 +914,7 @@ export function resolveVersionConflicts(mapVersion) { const type = this.dataset.type; const color = this.getAttribute("fill"); const cells = this.dataset.cells.split(",").map(Number); - pack.zones.push({i, name, type, cells, color}); + pack.zones.push({ i, name, type, cells, color }); }); zones.style("display", null).selectAll("*").remove(); if (layerIsOn("toggleZones")) drawZones(); @@ -975,7 +975,7 @@ export function resolveVersionConflicts(mapVersion) { if (isOlderThan("1.109.0")) { // v1.109.0 added customizable burg groups and icons - options.burgs = {groups: []}; + options.burgs = { groups: [] }; burgIcons.selectAll("circle, use").each(function () { const group = this.parentNode.id; @@ -987,7 +987,7 @@ export function resolveVersionConflicts(mapVersion) { burgIcons.selectAll("g").each(function (_el, index) { const name = this.id; const isDefault = name === "towns"; - options.burgs.groups.push({name, active: true, order: index + 1, isDefault, preview: "watabou-city"}); + options.burgs.groups.push({ name, active: true, order: index + 1, isDefault, preview: "watabou-city" }); if (!this.dataset.icon) this.dataset.icon = "#icon-circle"; const size = Number(this.getAttribute("size") || 2) * 2; @@ -1036,4 +1036,74 @@ export function resolveVersionConflicts(mapVersion) { delete options.showMFCGMap; delete options.villageMaxPopulation; } + + if (isOlderThan("1.111.0")) { + // v1.111.0 moved ice data from SVG to data model + // Migrate old ice SVG elements to new pack.ice structure + if (!pack.ice.length) { + pack.ice = []; + let iceId = 0; + + const iceLayer = document.getElementById("ice"); + if (iceLayer) { + // Migrate glaciers (type="iceShield") + iceLayer.querySelectorAll("polygon[type='iceShield']").forEach(polygon => { + // Parse points string "x1,y1 x2,y2 x3,y3 ..." into array [[x1,y1], [x2,y2], ...] + const points = [...polygon.points].map(svgPoint => [svgPoint.x, svgPoint.y]); + + const transform = polygon.getAttribute("transform"); + const iceElement = { + i: iceId++, + points, + type: "glacier" + }; + if (transform) { + iceElement.offset = parseTransform(transform); + } + pack.ice.push(iceElement); + }); + + // Migrate icebergs + iceLayer.querySelectorAll("polygon:not([type])").forEach(polygon => { + const cellId = +polygon.getAttribute("cell"); + const size = +polygon.getAttribute("size"); + + // points string must exist, cell attribute must be present, and size must be non-zero + if (polygon.getAttribute("cell") === null || !size) return; + + // Parse points string "x1,y1 x2,y2 x3,y3 ..." into array [[x1,y1], [x2,y2], ...] + const points = [...polygon.points].map(svgPoint => [svgPoint.x, svgPoint.y]); + + const transform = polygon.getAttribute("transform"); + const iceElement = { + i: iceId++, + points, + type: "iceberg", + cellId, + size + }; + if (transform) { + iceElement.offset = parseTransform(transform); + } + pack.ice.push(iceElement); + }); + + // Clear old SVG elements + iceLayer.querySelectorAll("*").forEach(el => el.remove()); + } else { + // If ice layer element doesn't exist, create it + ice = viewbox.insert("g", "#coastline").attr("id", "ice"); + ice + .attr("opacity", null) + .attr("fill", "#e8f0f6") + .attr("stroke", "#e8f0f6") + .attr("stroke-width", 1) + .attr("filter", "url(#dropShadow05)"); + } + + // Re-render ice from migrated data + if (layerIsOn("toggleIce")) drawIce(); + } + + } } diff --git a/public/modules/ice.js b/public/modules/ice.js new file mode 100644 index 00000000..90c7c3e6 --- /dev/null +++ b/public/modules/ice.js @@ -0,0 +1,170 @@ +"use strict"; + +// Ice layer data model - separates ice data from SVG rendering +window.Ice = (function () { + + // Find next available id for new ice element idealy filling gaps + function getNextId() { + if (pack.ice.length === 0) return 0; + // find gaps in existing ids + const existingIds = pack.ice.map(e => e.i).sort((a, b) => a - b); + for (let id = 0; id < existingIds[existingIds.length - 1]; id++) { + if (!existingIds.includes(id)) return id; + } + return existingIds[existingIds.length - 1] + 1; + } + + // Generate glaciers and icebergs based on temperature and height + function generate() { + clear(); + 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.push({ + i: getNextId(), + points: clipped, + type: "glacier" + }); + }); + } + } + + // 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.push({ + i: getNextId(), + points, + type: "iceberg", + cellId, + size + }); + } + } + + 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) + ]); + const id = getNextId(); + pack.ice.push({ + i: id, + points, + type: "iceberg", + cellId, + size + }); + redrawIceberg(id); + } + + function removeIce(id) { + const index = pack.ice.findIndex(element => element.i === id); + if (index !== -1) { + const type = pack.ice.find(element => element.i === id).type; + pack.ice.splice(index, 1); + if (type === "glacier") { + redrawGlacier(id); + } else { + redrawIceberg(id); + } + + } + } + + function updateIceberg(id, points, size) { + const iceberg = pack.ice.find(element => element.i === id); + if (iceberg) { + iceberg.points = points; + iceberg.size = size; + } + } + + function randomizeIcebergShape(id) { + const iceberg = pack.ice.find(element => element.i === id); + 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; + } + + function changeIcebergSize(id, newSize) { + const iceberg = pack.ice.find(element => element.i === id); + if (!iceberg) return; + + const cellId = iceberg.cellId; + const [cx, cy] = grid.points[cellId]; + const oldSize = iceberg.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; + } + + // Clear all ice + function clear() { + pack.ice = []; + } + + return { + generate, + addIceberg, + removeIce, + updateIceberg, + randomizeIcebergShape, + changeIcebergSize, + clear + }; +})(); diff --git a/public/modules/io/load.js b/public/modules/io/load.js index 689757b2..9b401733 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]) : []; if (data[31]) { const namesDL = data[31].split("/"); @@ -449,7 +450,7 @@ 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 (isVisible(ice)) 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..25cd7493 100644 --- a/public/modules/io/save.js +++ b/public/modules/io/save.js @@ -32,12 +32,13 @@ async function saveMap(method) { $(this).dialog("close"); } }, - position: {my: "center", at: "center", of: "svg"} + position: { my: "center", at: "center", of: "svg" } }); } } function prepareMapData() { + const date = new Date(); const dateString = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate(); const license = "File can be loaded in azgaar.github.io/Fantasy-Map-Generator"; @@ -89,8 +90,8 @@ function prepareMapData() { const serializedSVG = new XMLSerializer().serializeToString(cloneEl); - const {spacing, cellsX, cellsY, boundary, points, features, cellsDesired} = grid; - const gridGeneral = JSON.stringify({spacing, cellsX, cellsY, boundary, points, features, cellsDesired}); + const { spacing, cellsX, cellsY, boundary, points, features, cellsDesired } = grid; + const gridGeneral = JSON.stringify({ spacing, cellsX, cellsY, boundary, points, features, cellsDesired }); const packFeatures = JSON.stringify(pack.features); const cultures = JSON.stringify(pack.cultures); const states = JSON.stringify(pack.states); @@ -102,6 +103,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,21 +157,22 @@ function prepareMapData() { markers, cellRoutes, routes, - zones + zones, + ice ].join("\r\n"); return mapData; } // save map file to indexedDB async function saveToStorage(mapData, showTip = false) { - const blob = new Blob([mapData], {type: "text/plain"}); + const blob = new Blob([mapData], { type: "text/plain" }); await ldb.set("lastMap", blob); showTip && tip("Map is saved to the browser storage", false, "success"); } // download map file function saveToMachine(mapData, filename) { - const blob = new Blob([mapData], {type: "text/plain"}); + const blob = new Blob([mapData], { type: "text/plain" }); const URL = window.URL.createObjectURL(blob); const link = document.createElement("a"); diff --git a/public/modules/renderers/draw-ice.js b/public/modules/renderers/draw-ice.js new file mode 100644 index 00000000..4b35f75c --- /dev/null +++ b/public/modules/renderers/draw-ice.js @@ -0,0 +1,70 @@ +"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(); + + let html = ""; + + // Draw all ice elements + pack.ice.forEach(iceElement => { + if (iceElement.type === "glacier") { + html += getGlacierHtml(iceElement); + } else if (iceElement.type === "iceberg") { + html += getIcebergHtml(iceElement); + } + }); + + ice.html(html); + + TIME && console.timeEnd("drawIce"); +} + +function redrawIceberg(id) { + TIME && console.time("redrawIceberg"); + const iceberg = pack.ice.find(element => element.i === id); + let el = ice.selectAll(`polygon[data-id="${id}"]:not([type="glacier"])`); + if (!iceberg && !el.empty()) { + el.remove(); + } else { + if (el.empty()) { + // Create new element if it doesn't exist + const polygon = getIcebergHtml(iceberg); + ice.node().insertAdjacentHTML("beforeend", polygon); + el = ice.selectAll(`polygon[data-id="${id}"]:not([type="glacier"])`); + } + el.attr("points", iceberg.points); + el.attr("transform", iceberg.offset ? `translate(${iceberg.offset[0]},${iceberg.offset[1]})` : null); + } + TIME && console.timeEnd("redrawIceberg"); +} + +function redrawGlacier(id) { + TIME && console.time("redrawGlacier"); + const glacier = pack.ice.find(element => element.i === id); + let el = ice.selectAll(`polygon[data-id="${id}"][type="glacier"]`); + if (!glacier && !el.empty()) { + el.remove(); + } else { + if (el.empty()) { + // Create new element if it doesn't exist + const polygon = getGlacierHtml(glacier); + ice.node().insertAdjacentHTML("beforeend", polygon); + el = ice.selectAll(`polygon[data-id="${id}"][type="glacier"]`); + } + el.attr("points", glacier.points); + el.attr("transform", glacier.offset ? `translate(${glacier.offset[0]},${glacier.offset[1]})` : null); + } + TIME && console.timeEnd("redrawGlacier"); +} + +function getGlacierHtml(glacier) { + return ``; +} + +function getIcebergHtml(iceberg) { + return ``; +} \ No newline at end of file diff --git a/public/modules/ui/editors.js b/public/modules/ui/editors.js index 77c391ee..50eaf1c7 100644 --- a/public/modules/ui/editors.js +++ b/public/modules/ui/editors.js @@ -26,7 +26,7 @@ function clicked() { else if (ancestor.id === "labels" && el.tagName === "tspan") editLabel(); else if (grand.id === "burgLabels") editBurg(); else if (grand.id === "burgIcons") editBurg(); - else if (parent.id === "ice") editIce(); + else if (parent.id === "ice") editIce(el); else if (parent.id === "terrain") editReliefIcon(); else if (grand.id === "markers" || great.id === "markers") editMarker(); else if (grand.id === "coastline") editCoastline(); diff --git a/public/modules/ui/heightmap-editor.js b/public/modules/ui/heightmap-editor.js index d655e39d..5c3f1fc3 100644 --- a/public/modules/ui/heightmap-editor.js +++ b/public/modules/ui/heightmap-editor.js @@ -259,6 +259,8 @@ function editHeightmap(options) { Rivers.specify(); Lakes.defineNames(); + Ice.generate(); + Military.generate(); Markers.generate(); Zones.generate(); @@ -465,6 +467,10 @@ function editHeightmap(options) { .attr("id", d => base + d); }); + // recalculate ice + Ice.generate(); + ice.selectAll("*").remove(); + TIME && console.timeEnd("restoreRiskedData"); INFO && console.groupEnd("Edit Heightmap"); } diff --git a/public/modules/ui/ice-editor.js b/public/modules/ui/ice-editor.js index a9e6ff28..16818b4c 100644 --- a/public/modules/ui/ice-editor.js +++ b/public/modules/ui/ice-editor.js @@ -1,26 +1,32 @@ "use strict"; -function editIce() { +function editIce(element) { if (customization) return; + if (elSelected && element === elSelected.node()) return; + closeDialogs(".stable"); 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 id = +elSelected.attr("data-id"); + const iceElement = pack.ice.find(el => el.i === id); + const isGlacier = elSelected.attr("type") === "glacier"; + 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 = iceElement?.size || ""; + ice.selectAll("*").classed("draggable", true).call(d3.drag().on("drag", dragElement)); $("#iceEditor").dialog({ title: "Edit " + type, resizable: false, - position: {my: "center top+60", at: "top", of: d3.event, collision: "fit"}, + position: { my: "center top+60", at: "top", of: d3.event, collision: "fit" }, close: closeEditor }); if (modules.editIce) return; modules.editIce = true; - // add listeners document.getElementById("iceEditStyle").addEventListener("click", () => editStyle("ice")); document.getElementById("iceRandomize").addEventListener("click", randomizeShape); @@ -28,29 +34,18 @@ function editIce() { document.getElementById("iceNew").addEventListener("click", toggleAdd); 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); + const selectedId = +elSelected.attr("data-id"); + Ice.randomizeIcebergShape(selectedId); + redrawIceberg(selectedId); } 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; + const selectedId = +elSelected.attr("data-id"); + Ice.changeIcebergSize(selectedId, newSize); + redrawIceberg(selectedId); } function toggleAdd() { @@ -67,17 +62,15 @@ 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); + if (d3.event.shiftKey === false) toggleAdd(); } function removeIce() { - const type = elSelected.attr("type") ? "Glacier" : "Iceberg"; + const type = elSelected.attr("type") === "glacier" ? "Glacier" : "Iceberg"; alertMessage.innerHTML = /* html */ `Are you sure you want to remove the ${type}?`; $("#alert").dialog({ resizable: false, @@ -85,7 +78,7 @@ function editIce() { buttons: { Remove: function () { $(this).dialog("close"); - elSelected.remove(); + Ice.removeIce(+elSelected.attr("data-id")); $("#iceEditor").dialog("close"); }, Cancel: function () { @@ -96,14 +89,24 @@ function editIce() { } function dragElement() { - const tr = parseTransform(this.getAttribute("transform")); - const dx = +tr[0] - d3.event.x, - dy = +tr[1] - d3.event.y; + const selectedId = +elSelected.attr("data-id"); + 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 = pack.ice.find(element => element.i === selectedId); + if (iceData) { + // Store offset for visual positioning, actual geometry stays in points + iceData.offset = offset; + } }); } @@ -114,3 +117,4 @@ function editIce() { unselect(); } } + diff --git a/public/modules/ui/layers.js b/public/modules/ui/layers.js index 5037a5ee..f2f04a4b 100644 --- a/public/modules/ui/layers.js +++ b/public/modules/ui/layers.js @@ -417,49 +417,6 @@ function toggleIce(event) { } } -function drawIce() { - TIME && console.time("drawIce"); - - 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); - - // 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); - } - - TIME && console.timeEnd("drawIce"); -} - function toggleCultures(event) { const cultures = pack.cultures.filter(c => c.i && !c.removed); const empty = !cults.selectAll("path").size(); 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/public/versioning.js b/public/versioning.js index 11fcde66..8069c818 100644 --- a/public/versioning.js +++ b/public/versioning.js @@ -13,7 +13,7 @@ * Example: 1.102.2 -> Major version 1, Minor version 102, Patch version 2 */ -const VERSION = "1.110.0"; +const VERSION = "1.111.0"; if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format or parsing function"); { diff --git a/src/index.html b/src/index.html index 21d84187..3fd7ba17 100644 --- a/src/index.html +++ b/src/index.html @@ -8498,6 +8498,7 @@ + @@ -8515,16 +8516,16 @@ - + - + - - + + @@ -8535,7 +8536,7 @@ - + @@ -8549,12 +8550,12 @@ - - - + + + - + @@ -8566,8 +8567,8 @@ - - + + @@ -8583,5 +8584,6 @@ +