Skip to content

Commit 4b341a6

Browse files
StempunkDevCopilot
andauthored
Data model ice (#1279)
* prototype for ice seperation * feat: migrate ice data to new data model and update version to 1.110.0 * refactor: update ice data handling and rendering for improved performance * feat: integrate ice generation and recalculation in heightmap editing * fix ice selection(hopefully) * fix ice selection better(pls) * refactor: remove redundant element selection in ice editing functions * fix: clear ice data before generating glaciers and icebergs * sparse array implementation with reduced updates * fix logic chech in modules/dynamic/auto-update.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: migrate ice data to new data model structure * refactor: streamline ice generation process and clean up rendering functions * refactor: simplify ice rendering logic by removing redundant clearing of old SVG * fix: update editIce function to accept element parameter and improve logic for glacier handling * ice drawing with only type on less occuring glaciers * feat: add compactPackData function to filter out undefined glaciers and icebergs * fix: clear existing ice elements before redrawing in editHeightmap function * fix compact problems on autosave * refactor: unify ice data structure and streamline ice element handling * refactor: improve getNextId function to fill gaps in ice element IDs(optional commit) * just to be sure * bump version in html * fix index.html script import * feat: add ice module script to index.html * fix migration check --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 81c1ba2 commit 4b341a6

File tree

13 files changed

+403
-118
lines changed

13 files changed

+403
-118
lines changed

public/main.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,8 @@ async function generate(options) {
632632
Biomes.define();
633633
Features.defineGroups();
634634

635+
Ice.generate();
636+
635637
rankCells();
636638
Cultures.generate();
637639
Cultures.expand();

public/modules/dynamic/auto-update.js

Lines changed: 86 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -253,8 +253,8 @@ export function resolveVersionConflicts(mapVersion) {
253253
const source = findCell(s.x, s.y);
254254
const mouth = findCell(e.x, e.y);
255255
const name = Rivers.getName(mouth);
256-
const type = length < 25 ? rw({Creek: 9, River: 3, Brook: 3, Stream: 1}) : "River";
257-
pack.rivers.push({i, parent: 0, length, source, mouth, basin: i, name, type});
256+
const type = length < 25 ? rw({ Creek: 9, River: 3, Brook: 3, Stream: 1 }) : "River";
257+
pack.rivers.push({ i, parent: 0, length, source, mouth, basin: i, name, type });
258258
});
259259
}
260260

@@ -270,7 +270,7 @@ export function resolveVersionConflicts(mapVersion) {
270270
const era = Names.getBaseShort(P(0.7) ? 1 : rand(nameBases.length)) + " Era";
271271
const eraShort = era[0] + "E";
272272
const military = Military.getDefaultOptions();
273-
options = {winds, year, era, eraShort, military};
273+
options = { winds, year, era, eraShort, military };
274274

275275
// v1.3 added campaings data for all states
276276
States.generateCampaigns();
@@ -481,7 +481,7 @@ export function resolveVersionConflicts(mapVersion) {
481481
if (isOlderThan("1.65.0")) {
482482
// v1.65 changed rivers data
483483
d3.select("#rivers").attr("style", null); // remove style to unhide layer
484-
const {cells, rivers} = pack;
484+
const { cells, rivers } = pack;
485485
const defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2);
486486

487487
for (const river of rivers) {
@@ -497,8 +497,8 @@ export function resolveVersionConflicts(mapVersion) {
497497

498498
for (let i = 0; i <= segments; i++) {
499499
const shift = increment * i;
500-
const {x: x1, y: y1} = node.getPointAtLength(length + shift);
501-
const {x: x2, y: y2} = node.getPointAtLength(length - shift);
500+
const { x: x1, y: y1 } = node.getPointAtLength(length + shift);
501+
const { x: x2, y: y2 } = node.getPointAtLength(length - shift);
502502
const x = rn((x1 + x2) / 2, 1);
503503
const y = rn((y1 + y2) / 2, 1);
504504

@@ -565,7 +565,7 @@ export function resolveVersionConflicts(mapVersion) {
565565
const fill = circle && circle.getAttribute("fill");
566566
const stroke = circle && circle.getAttribute("stroke");
567567

568-
const marker = {i, icon, type, x, y, size, cell};
568+
const marker = { i, icon, type, x, y, size, cell };
569569
if (size && size !== 30) marker.size = size;
570570
if (!isNaN(px) && px !== 12) marker.px = px;
571571
if (!isNaN(dx) && dx !== 50) marker.dx = dx;
@@ -631,21 +631,21 @@ export function resolveVersionConflicts(mapVersion) {
631631

632632
if (isOlderThan("1.88.0")) {
633633
// v1.87 may have incorrect shield for some reason
634-
pack.states.forEach(({coa}) => {
634+
pack.states.forEach(({ coa }) => {
635635
if (coa?.shield === "state") delete coa.shield;
636636
});
637637
}
638638

639639
if (isOlderThan("1.91.0")) {
640640
// from 1.91.00 custom coa is moved to coa object
641641
pack.states.forEach(state => {
642-
if (state.coa === "custom") state.coa = {custom: true};
642+
if (state.coa === "custom") state.coa = { custom: true };
643643
});
644644
pack.provinces.forEach(province => {
645-
if (province.coa === "custom") province.coa = {custom: true};
645+
if (province.coa === "custom") province.coa = { custom: true };
646646
});
647647
pack.burgs.forEach(burg => {
648-
if (burg.coa === "custom") burg.coa = {custom: true};
648+
if (burg.coa === "custom") burg.coa = { custom: true };
649649
});
650650

651651
// from 1.91.00 emblems don't have transform attribute
@@ -747,7 +747,7 @@ export function resolveVersionConflicts(mapVersion) {
747747
const skip = terrs.attr("skip");
748748
const relax = terrs.attr("relax");
749749

750-
const curveTypes = {0: "curveBasisClosed", 1: "curveLinear", 2: "curveStep"};
750+
const curveTypes = { 0: "curveBasisClosed", 1: "curveLinear", 2: "curveStep" };
751751
const curve = curveTypes[terrs.attr("curve")] || "curveBasisClosed";
752752

753753
terrs
@@ -882,7 +882,7 @@ export function resolveVersionConflicts(mapVersion) {
882882
const secondCellId = points[1][2];
883883
const feature = pack.cells.f[secondCellId];
884884

885-
pack.routes.push({i: pack.routes.length, group, feature, points});
885+
pack.routes.push({ i: pack.routes.length, group, feature, points });
886886
}
887887
}
888888
routes.selectAll("path").remove();
@@ -914,7 +914,7 @@ export function resolveVersionConflicts(mapVersion) {
914914
const type = this.dataset.type;
915915
const color = this.getAttribute("fill");
916916
const cells = this.dataset.cells.split(",").map(Number);
917-
pack.zones.push({i, name, type, cells, color});
917+
pack.zones.push({ i, name, type, cells, color });
918918
});
919919
zones.style("display", null).selectAll("*").remove();
920920
if (layerIsOn("toggleZones")) drawZones();
@@ -975,7 +975,7 @@ export function resolveVersionConflicts(mapVersion) {
975975

976976
if (isOlderThan("1.109.0")) {
977977
// v1.109.0 added customizable burg groups and icons
978-
options.burgs = {groups: []};
978+
options.burgs = { groups: [] };
979979

980980
burgIcons.selectAll("circle, use").each(function () {
981981
const group = this.parentNode.id;
@@ -987,7 +987,7 @@ export function resolveVersionConflicts(mapVersion) {
987987
burgIcons.selectAll("g").each(function (_el, index) {
988988
const name = this.id;
989989
const isDefault = name === "towns";
990-
options.burgs.groups.push({name, active: true, order: index + 1, isDefault, preview: "watabou-city"});
990+
options.burgs.groups.push({ name, active: true, order: index + 1, isDefault, preview: "watabou-city" });
991991
if (!this.dataset.icon) this.dataset.icon = "#icon-circle";
992992

993993
const size = Number(this.getAttribute("size") || 2) * 2;
@@ -1036,4 +1036,74 @@ export function resolveVersionConflicts(mapVersion) {
10361036
delete options.showMFCGMap;
10371037
delete options.villageMaxPopulation;
10381038
}
1039+
1040+
if (isOlderThan("1.111.0")) {
1041+
// v1.111.0 moved ice data from SVG to data model
1042+
// Migrate old ice SVG elements to new pack.ice structure
1043+
if (!pack.ice.length) {
1044+
pack.ice = [];
1045+
let iceId = 0;
1046+
1047+
const iceLayer = document.getElementById("ice");
1048+
if (iceLayer) {
1049+
// Migrate glaciers (type="iceShield")
1050+
iceLayer.querySelectorAll("polygon[type='iceShield']").forEach(polygon => {
1051+
// Parse points string "x1,y1 x2,y2 x3,y3 ..." into array [[x1,y1], [x2,y2], ...]
1052+
const points = [...polygon.points].map(svgPoint => [svgPoint.x, svgPoint.y]);
1053+
1054+
const transform = polygon.getAttribute("transform");
1055+
const iceElement = {
1056+
i: iceId++,
1057+
points,
1058+
type: "glacier"
1059+
};
1060+
if (transform) {
1061+
iceElement.offset = parseTransform(transform);
1062+
}
1063+
pack.ice.push(iceElement);
1064+
});
1065+
1066+
// Migrate icebergs
1067+
iceLayer.querySelectorAll("polygon:not([type])").forEach(polygon => {
1068+
const cellId = +polygon.getAttribute("cell");
1069+
const size = +polygon.getAttribute("size");
1070+
1071+
// points string must exist, cell attribute must be present, and size must be non-zero
1072+
if (polygon.getAttribute("cell") === null || !size) return;
1073+
1074+
// Parse points string "x1,y1 x2,y2 x3,y3 ..." into array [[x1,y1], [x2,y2], ...]
1075+
const points = [...polygon.points].map(svgPoint => [svgPoint.x, svgPoint.y]);
1076+
1077+
const transform = polygon.getAttribute("transform");
1078+
const iceElement = {
1079+
i: iceId++,
1080+
points,
1081+
type: "iceberg",
1082+
cellId,
1083+
size
1084+
};
1085+
if (transform) {
1086+
iceElement.offset = parseTransform(transform);
1087+
}
1088+
pack.ice.push(iceElement);
1089+
});
1090+
1091+
// Clear old SVG elements
1092+
iceLayer.querySelectorAll("*").forEach(el => el.remove());
1093+
} else {
1094+
// If ice layer element doesn't exist, create it
1095+
ice = viewbox.insert("g", "#coastline").attr("id", "ice");
1096+
ice
1097+
.attr("opacity", null)
1098+
.attr("fill", "#e8f0f6")
1099+
.attr("stroke", "#e8f0f6")
1100+
.attr("stroke-width", 1)
1101+
.attr("filter", "url(#dropShadow05)");
1102+
}
1103+
1104+
// Re-render ice from migrated data
1105+
if (layerIsOn("toggleIce")) drawIce();
1106+
}
1107+
1108+
}
10391109
}

public/modules/ice.js

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
"use strict";
2+
3+
// Ice layer data model - separates ice data from SVG rendering
4+
window.Ice = (function () {
5+
6+
// Find next available id for new ice element idealy filling gaps
7+
function getNextId() {
8+
if (pack.ice.length === 0) return 0;
9+
// find gaps in existing ids
10+
const existingIds = pack.ice.map(e => e.i).sort((a, b) => a - b);
11+
for (let id = 0; id < existingIds[existingIds.length - 1]; id++) {
12+
if (!existingIds.includes(id)) return id;
13+
}
14+
return existingIds[existingIds.length - 1] + 1;
15+
}
16+
17+
// Generate glaciers and icebergs based on temperature and height
18+
function generate() {
19+
clear();
20+
const { cells, features } = grid;
21+
const { temp, h } = cells;
22+
Math.random = aleaPRNG(seed);
23+
24+
const ICEBERG_MAX_TEMP = 0;
25+
const GLACIER_MAX_TEMP = -8;
26+
const minMaxTemp = d3.min(temp);
27+
28+
// Generate glaciers on cold land
29+
{
30+
const type = "iceShield";
31+
const getType = cellId =>
32+
h[cellId] >= 20 && temp[cellId] <= GLACIER_MAX_TEMP ? type : null;
33+
const isolines = getIsolines(grid, getType, { polygons: true });
34+
35+
if (isolines[type]?.polygons) {
36+
isolines[type].polygons.forEach(points => {
37+
const clipped = clipPoly(points);
38+
pack.ice.push({
39+
i: getNextId(),
40+
points: clipped,
41+
type: "glacier"
42+
});
43+
});
44+
}
45+
}
46+
47+
// Generate icebergs on cold water
48+
for (const cellId of grid.cells.i) {
49+
const t = temp[cellId];
50+
if (h[cellId] >= 20) continue; // no icebergs on land
51+
if (t > ICEBERG_MAX_TEMP) continue; // too warm: no icebergs
52+
if (features[cells.f[cellId]].type === "lake") continue; // no icebergs on lakes
53+
if (P(0.8)) continue; // skip most of eligible cells
54+
55+
const randomFactor = 0.8 + rand() * 0.4; // random size factor
56+
let baseSize = (1 - normalize(t, minMaxTemp, 1)) * 0.8; // size: 0 = zero, 1 = full
57+
if (cells.t[cellId] === -1) baseSize /= 1.3; // coastline: smaller icebergs
58+
const size = minmax(rn(baseSize * randomFactor, 2), 0.1, 1);
59+
60+
const [cx, cy] = grid.points[cellId];
61+
const points = getGridPolygon(cellId).map(([x, y]) => [
62+
rn(lerp(cx, x, size), 2),
63+
rn(lerp(cy, y, size), 2)
64+
]);
65+
66+
pack.ice.push({
67+
i: getNextId(),
68+
points,
69+
type: "iceberg",
70+
cellId,
71+
size
72+
});
73+
}
74+
}
75+
76+
function addIceberg(cellId, size) {
77+
const [cx, cy] = grid.points[cellId];
78+
const points = getGridPolygon(cellId).map(([x, y]) => [
79+
rn(lerp(cx, x, size), 2),
80+
rn(lerp(cy, y, size), 2)
81+
]);
82+
const id = getNextId();
83+
pack.ice.push({
84+
i: id,
85+
points,
86+
type: "iceberg",
87+
cellId,
88+
size
89+
});
90+
redrawIceberg(id);
91+
}
92+
93+
function removeIce(id) {
94+
const index = pack.ice.findIndex(element => element.i === id);
95+
if (index !== -1) {
96+
const type = pack.ice.find(element => element.i === id).type;
97+
pack.ice.splice(index, 1);
98+
if (type === "glacier") {
99+
redrawGlacier(id);
100+
} else {
101+
redrawIceberg(id);
102+
}
103+
104+
}
105+
}
106+
107+
function updateIceberg(id, points, size) {
108+
const iceberg = pack.ice.find(element => element.i === id);
109+
if (iceberg) {
110+
iceberg.points = points;
111+
iceberg.size = size;
112+
}
113+
}
114+
115+
function randomizeIcebergShape(id) {
116+
const iceberg = pack.ice.find(element => element.i === id);
117+
if (!iceberg) return;
118+
119+
const cellId = iceberg.cellId;
120+
const size = iceberg.size;
121+
const [cx, cy] = grid.points[cellId];
122+
123+
// Get a different random cell for the polygon template
124+
const i = ra(grid.cells.i);
125+
const cn = grid.points[i];
126+
const poly = getGridPolygon(i).map(p => [p[0] - cn[0], p[1] - cn[1]]);
127+
const points = poly.map(p => [
128+
rn(cx + p[0] * size, 2),
129+
rn(cy + p[1] * size, 2)
130+
]);
131+
132+
iceberg.points = points;
133+
}
134+
135+
function changeIcebergSize(id, newSize) {
136+
const iceberg = pack.ice.find(element => element.i === id);
137+
if (!iceberg) return;
138+
139+
const cellId = iceberg.cellId;
140+
const [cx, cy] = grid.points[cellId];
141+
const oldSize = iceberg.size;
142+
143+
const flat = iceberg.points.flat();
144+
const pairs = [];
145+
while (flat.length) pairs.push(flat.splice(0, 2));
146+
const poly = pairs.map(p => [(p[0] - cx) / oldSize, (p[1] - cy) / oldSize]);
147+
const points = poly.map(p => [
148+
rn(cx + p[0] * newSize, 2),
149+
rn(cy + p[1] * newSize, 2)
150+
]);
151+
152+
iceberg.points = points;
153+
iceberg.size = newSize;
154+
}
155+
156+
// Clear all ice
157+
function clear() {
158+
pack.ice = [];
159+
}
160+
161+
return {
162+
generate,
163+
addIceberg,
164+
removeIce,
165+
updateIceberg,
166+
randomizeIcebergShape,
167+
changeIcebergSize,
168+
clear
169+
};
170+
})();

public/modules/io/load.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,7 @@ async function parseLoadedData(data, mapVersion) {
406406
pack.cells.province = data[27] ? Uint16Array.from(data[27].split(",")) : new Uint16Array(pack.cells.i.length);
407407
// data[28] had deprecated cells.crossroad
408408
pack.cells.routes = data[36] ? JSON.parse(data[36]) : {};
409+
pack.ice = data[39] ? JSON.parse(data[39]) : [];
409410

410411
if (data[31]) {
411412
const namesDL = data[31].split("/");
@@ -449,7 +450,7 @@ async function parseLoadedData(data, mapVersion) {
449450
if (isVisible(routes) && hasChild(routes, "path")) turnOn("toggleRoutes");
450451
if (hasChildren(temperature)) turnOn("toggleTemperature");
451452
if (hasChild(population, "line")) turnOn("togglePopulation");
452-
if (hasChildren(ice)) turnOn("toggleIce");
453+
if (isVisible(ice)) turnOn("toggleIce");
453454
if (hasChild(prec, "circle")) turnOn("togglePrecipitation");
454455
if (isVisible(emblems) && hasChild(emblems, "use")) turnOn("toggleEmblems");
455456
if (isVisible(labels)) turnOn("toggleLabels");

0 commit comments

Comments
 (0)