v1.5.86 - separate lakes object

This commit is contained in:
Azgaar 2021-03-01 23:24:36 +03:00
parent cb88ab9229
commit 5f5a67d436
11 changed files with 175 additions and 109 deletions

View file

@ -1441,7 +1441,10 @@ div.states > .coaIcon > use {
#riverBody div.label, #riverBody div.label,
#riverBody input, #riverBody input,
#riverBody select { #riverBody select,
#lakeBody div.label,
#lakeBody input,
#lakeBody select {
display: inline-block; display: inline-block;
width: 7em; width: 7em;
} }

View file

@ -1624,6 +1624,37 @@
</div> </div>
</div> </div>
<div id="lakeEditor" class="dialog" style="display: none">
<div id="lakeBody" style="padding-bottom: .3em">
<div style="padding: .1em">
<div class="label" style="width: 4.8em">Name:</div>
<span id="lakeNameCulture" data-tip="Generate culture-specific name for the lake" class="icon-book pointer"></span>
<span id="lakeNameRandom" data-tip="Generate random name for the lake" class="icon-globe pointer"></span>
<input id="lakeName" data-tip="Type to rename the lake" autocorrect="off" spellcheck="false">
<span data-tip="Speak the name. You can change voice and language in options" class="speaker">🔊</span>
</div>
<div style="padding: .1em" data-tip="Type to change lake type (group)">
<div class="label">Type:</div>
<select id="lakeGroup" data-tip="Select lake type (group)" style="width:9em"></select>
<input id="lakeGroupName" placeholder="new group name" data-tip="Provide a name for the new group" style="display:none; width:9em"/>
<span id="lakeGroupAdd" data-tip="Create new type (group) for the lake" class="icon-plus pointer"></span>
<span id="lakeGroupRemove" data-tip="Remove the group" class="icon-trash-empty pointer"></span>
</div>
<div style="padding: .1em" data-tip="Lake area in selected units">
<div class="label">Area:</div>
<input id="lakeArea" disabled/>
</div>
</div>
<div id="lakeBottom">
<button id="lakeEditStyle" data-tip="Edit lake group style in Style Editor" class="icon-brush"></button>
<button id="lakeLegend" data-tip="Edit free text notes (legend) for the lake" class="icon-edit"></button>
</div>
</div>
<div id="elevationProfile" class="dialog" style="display: none" width="100%"> <div id="elevationProfile" class="dialog" style="display: none" width="100%">
<div id="elevationGraph" data-tip="Elevation profile"></div> <div id="elevationGraph" data-tip="Elevation profile"></div>
<div style="text-align: center"> <div style="text-align: center">
@ -1663,21 +1694,6 @@
<button id="routeRemove" data-tip="Remove route. Shortcut: Delete" class="icon-trash fastDelete"></button> <button id="routeRemove" data-tip="Remove route. Shortcut: Delete" class="icon-trash fastDelete"></button>
</div> </div>
<div id="lakeEditor" class="dialog" style="display: none">
<button id="lakeGroupsShow" data-tip="Show the group selection" class="icon-tags"></button>
<div id="lakeGroupsSelection" style="display: none">
<button id="lakeGroupsHide" data-tip="Hide the group section" class="icon-tags"></button>
<select id="lakeGroup" data-tip="Select a group for this lake" style="width:9em"></select>
<input id="lakeGroupName" placeholder="new group name" data-tip="Provide a name for the new group" style="display:none; width:9em"/>
<span id="lakeGroupAdd" data-tip="Create new group for this lake" class="icon-plus pointer"></span>
<span id="lakeGroupRemove" data-tip="Remove the group" class="icon-trash-empty pointer"></span>
</div>
<button id="lakeEditStyle" data-tip="Edit lake group style in Style Editor" class="icon-brush"></button>
<button id="lakeArea" data-tip="Lake area in selected units">0</button>
<button id="lakeLegend" data-tip="Edit free text notes (legend) for the lake" class="icon-edit"></button>
</div>
<div id="iceEditor" class="dialog" style="display: none"> <div id="iceEditor" class="dialog" style="display: none">
<button id="iceEditStyle" data-tip="Edit style in Style Editor" class="icon-brush"></button> <button id="iceEditStyle" data-tip="Edit style in Style Editor" class="icon-brush"></button>
<button id="iceRandomize" data-tip="Randomize Iceberd shape" class="icon-shuffle"></button> <button id="iceRandomize" data-tip="Randomize Iceberd shape" class="icon-shuffle"></button>
@ -4037,6 +4053,7 @@
<script src="modules/heightmap-generator.js"></script> <script src="modules/heightmap-generator.js"></script>
<script src="modules/ocean-layers.js"></script> <script src="modules/ocean-layers.js"></script>
<script src="modules/river-generator.js"></script> <script src="modules/river-generator.js"></script>
<script src="modules/lakes.js"></script>
<script src="modules/names-generator.js"></script> <script src="modules/names-generator.js"></script>
<script src="modules/cultures-generator.js"></script> <script src="modules/cultures-generator.js"></script>
<script src="modules/burgs-and-states.js"></script> <script src="modules/burgs-and-states.js"></script>

28
main.js
View file

@ -547,7 +547,7 @@ function generate() {
drawCoastline(); drawCoastline();
Rivers.generate(); Rivers.generate();
defineLakesGroup(); Lakes.defineGroup();
defineBiomes(); defineBiomes();
rankCells(); rankCells();
@ -564,6 +564,7 @@ function generate() {
BurgsAndStates.drawStateLabels(); BurgsAndStates.drawStateLabels();
Rivers.specify(); Rivers.specify();
Lakes.generateName();
Military.generate(); Military.generate();
addMarkers(); addMarkers();
@ -1120,31 +1121,6 @@ function reMarkFeatures() {
TIME && console.timeEnd("reMarkFeatures"); TIME && console.timeEnd("reMarkFeatures");
} }
function defineLakesGroup() {
for (const feature of pack.features) {
if (feature.type !== "lake") continue;
const lakeEl = lakes.select(`[data-f="${feature.i}"]`).node();
if (!lakeEl) continue;
feature.group = defineGroup(feature);
document.getElementById(feature.group).appendChild(lakeEl);
}
function defineGroup(feature) {
if (feature.temp < -3) return "frozen";
if (feature.height > 60 && feature.cells < 10 && feature.firstCell % 5 === 0) return "lava";
if (!feature.inlets && !feature.outlet) {
if (feature.evaporation / 2 > feature.flux) return "dry";
if (feature.cells < 3 && feature.firstCell % 5 === 0) return "sinkhole";
}
if (!feature.outlet && feature.evaporation > feature.flux) return "salt";
return "freshwater";
}
}
// assign biome id for each cell // assign biome id for each cell
function defineBiomes() { function defineBiomes() {
TIME && console.time("defineBiomes"); TIME && console.time("defineBiomes");

89
modules/lakes.js Normal file
View file

@ -0,0 +1,89 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.Lakes = factory());
}(this, (function () {'use strict';
const setClimateData = function(h) {
const cells = pack.cells;
const lakeOutCells = new Uint16Array(cells.i.length);
pack.features.forEach(f => {
if (f.type !== "lake") return;
// default flux: sum of precipition around lake first cell
f.flux = rn(d3.sum(f.shoreline.map(c => grid.cells.prec[cells.g[c]])) / 2);
// temperature and evaporation to detect closed lakes
f.temp = f.cells < 6 ? grid.cells.temp[cells.g[f.firstCell]] : rn(d3.mean(f.shoreline.map(c => grid.cells.temp[cells.g[c]])), 1);
const height = (f.height - 18) ** heightExponentInput.value; // height in meters
const evaporation = (700 * (f.temp + .006 * height) / 50 + 75) / (80 - f.temp); // based on Penman formula, [1-11]
f.evaporation = rn(evaporation * f.cells);
// lake outlet cell
f.outCell = f.shoreline[d3.scan(f.shoreline, (a,b) => h[a] - h[b])];
lakeOutCells[f.outCell] = f.i;
});
return lakeOutCells;
}
const cleanupLakeData = function() {
for (const feature of pack.features) {
if (feature.type !== "lake") continue;
delete feature.river;
delete feature.enteringFlux;
delete feature.shoreline;
delete feature.outCell;
feature.height = rn(feature.height);
const inlets = feature.inlets?.filter(r => pack.rivers.find(river => river.i === r));
if (!inlets || !inlets.length) delete feature.inlets;
else feature.inlets = inlets;
const outlet = feature.outlet && pack.rivers.find(river => river.i === feature.outlet);
if (!outlet) delete feature.outlet;
}
}
const defineGroup = function() {
for (const feature of pack.features) {
if (feature.type !== "lake") continue;
const lakeEl = lakes.select(`[data-f="${feature.i}"]`).node();
if (!lakeEl) continue;
feature.group = getGroup(feature);
document.getElementById(feature.group).appendChild(lakeEl);
}
}
const generateName = function() {
for (const feature of pack.features) {
if (feature.type !== "lake") continue;
feature.name = getName(feature);
}
}
const getName = function(feature) {
const landCell = pack.cells.c[feature.firstCell].find(c => pack.cells.h[c] >= 20);
const culture = pack.cells.culture[landCell];
return Names.getCulture(culture);
}
function getGroup(feature) {
if (feature.temp < -3) return "frozen";
if (feature.height > 60 && feature.cells < 10 && feature.firstCell % 5 === 0) return "lava";
if (!feature.inlets && !feature.outlet) {
if (feature.evaporation / 2 > feature.flux) return "dry";
if (feature.cells < 3 && feature.firstCell % 5 === 0) return "sinkhole";
}
if (!feature.outlet && feature.evaporation > feature.flux) return "salt";
return "freshwater";
}
return {setClimateData, cleanupLakeData, defineGroup, generateName, getName};
})));

View file

@ -21,7 +21,7 @@ const generate = function(changeHeights = true) {
resolveDepressions(h); resolveDepressions(h);
drainWater(); drainWater();
defineRivers(); defineRivers();
cleanupLakeData(); Lakes.cleanupLakeData();
if (changeHeights) cells.h = Uint8Array.from(h); // apply changed heights as basic one if (changeHeights) cells.h = Uint8Array.from(h); // apply changed heights as basic one
@ -57,25 +57,7 @@ const generate = function(changeHeights = true) {
function drainWater() { function drainWater() {
const MIN_FLUX_TO_FORM_RIVER = 30; const MIN_FLUX_TO_FORM_RIVER = 30;
const land = cells.i.filter(i => h[i] >= 20).sort((a,b) => h[b] - h[a]); const land = cells.i.filter(i => h[i] >= 20).sort((a,b) => h[b] - h[a]);
const lakeOutCells = Lakes.setClimateData(h);
const lakeOutCells = new Uint16Array(cells.i.length); // to enumerate lake outlet positions
features.forEach(f => {
if (f.type !== "lake") return;
const gridCell = cells.g[f.firstCell];
// lake possible outlet: cell around with min height
f.outCell = f.shoreline[d3.scan(f.shoreline, (a,b) => h[a] - h[b])];
lakeOutCells[f.outCell] = f.i;
// default flux: sum of precipition around lake first cell
f.flux = rn(d3.sum(f.shoreline.map(c => grid.cells.prec[cells.g[c]])) / 2);
// temperature and evaporation to detect closed lakes
f.temp = f.cells < 6 ? grid.cells.temp[gridCell] : rn(d3.mean(f.shoreline.map(c => grid.cells.temp[cells.g[c]])), 1);
const height = (f.height - 18) ** heightExponentInput.value; // height in meters
const evaporation = (700 * (f.temp + .006 * height) / 50 + 75) / (80 - f.temp); // based on Penman formula, [1-11]
f.evaporation = rn(evaporation * f.cells);
});
land.forEach(function(i) { land.forEach(function(i) {
cells.fl[i] += grid.cells.prec[cells.g[i]]; // flux from precipitation cells.fl[i] += grid.cells.prec[cells.g[i]]; // flux from precipitation
@ -208,24 +190,6 @@ const generate = function(changeHeights = true) {
// draw rivers // draw rivers
rivers.html(riverPaths.map(d => `<path id="river${d[1]}" d="${d[0]}"/>`).join("")); rivers.html(riverPaths.map(d => `<path id="river${d[1]}" d="${d[0]}"/>`).join(""));
} }
function cleanupLakeData() {
for (const feature of features) {
if (feature.type !== "lake") continue;
delete feature.river;
delete feature.enteringFlux;
delete feature.shoreline;
delete feature.outCell;
feature.height = rn(feature.height);
const inlets = feature.inlets?.filter(r => pack.rivers.find(river => river.i === r));
if (!inlets || !inlets.length) delete feature.inlets;
else feature.inlets = inlets;
const outlet = feature.outlet && pack.rivers.find(river => river.i === feature.outlet);
if (!outlet) delete feature.outlet;
}
}
} }
// depression filling algorithm (for a correct water flux modeling) // depression filling algorithm (for a correct water flux modeling)
@ -238,7 +202,6 @@ const resolveDepressions = function(h) {
const uniqueCells = new Set(); const uniqueCells = new Set();
l.vertices.forEach(v => pack.vertices.c[v].forEach(c => cells.h[c] >= 20 && uniqueCells.add(c))); l.vertices.forEach(v => pack.vertices.c[v].forEach(c => cells.h[c] >= 20 && uniqueCells.add(c)));
l.shoreline = [...uniqueCells]; l.shoreline = [...uniqueCells];
l.height = 21;
}); });
const land = cells.i.filter(i => h[i] >= 20 && !cells.b[i]); // exclude near-border cells const land = cells.i.filter(i => h[i] >= 20 && !cells.b[i]); // exclude near-border cells

View file

@ -145,7 +145,12 @@ function showMapTooltip(point, e, i, g) {
} }
if (subgroup === "burgIcons") {tip("Click to edit the Burg"); return;} if (subgroup === "burgIcons") {tip("Click to edit the Burg"); return;}
if (subgroup === "burgLabels") {tip("Click to edit the Burg"); return;} if (subgroup === "burgLabels") {tip("Click to edit the Burg"); return;}
if (group === "lakes" && !land) {tip(`${capitalize(subgroup)} lake. Click to edit`); return;} if (group === "lakes" && !land) {
const lakeId = +e.target.dataset.f;
const name = pack.features[lakeId]?.name;
const fullName = subgroup === "freshwater" ? name : name + " " + subgroup;
tip(`${fullName} lake. Click to edit`); return;
}
if (group === "coastline") {tip("Click to edit the coastline"); return;} if (group === "coastline") {tip("Click to edit the coastline"); return;}
if (group === "zones") { if (group === "zones") {
const zone = path[path.length-8]; const zone = path[path.length-8];

View file

@ -185,7 +185,7 @@ function editHeightmap() {
} }
} }
defineLakesGroup(); Lakes.defineGroup();
defineBiomes(); defineBiomes();
rankCells(); rankCells();
Cultures.generate(); Cultures.generate();
@ -201,6 +201,8 @@ function editHeightmap() {
BurgsAndStates.drawStateLabels(); BurgsAndStates.drawStateLabels();
Rivers.specify(); Rivers.specify();
Lakes.generateName();
Military.generate(); Military.generate();
addMarkers(); addMarkers();
addZones(); addZones();

View file

@ -13,6 +13,7 @@ function editLake() {
const node = d3.event.target; const node = d3.event.target;
debug.append("g").attr("id", "vertices"); debug.append("g").attr("id", "vertices");
elSelected = d3.select(node); elSelected = d3.select(node);
updateLakeValues();
selectLakeGroup(node); selectLakeGroup(node);
drawLakeVertices(); drawLakeVertices();
viewbox.on("touchmove mousemove", null); viewbox.on("touchmove mousemove", null);
@ -21,19 +22,34 @@ function editLake() {
modules.editLake = true; modules.editLake = true;
// add listeners // add listeners
document.getElementById("lakeGroupsShow").addEventListener("click", showGroupSection); document.getElementById("lakeName").addEventListener("input", changeName);
document.getElementById("lakeNameCulture").addEventListener("click", generateNameCulture);
document.getElementById("lakeNameRandom").addEventListener("click", generateNameRandom);
document.getElementById("lakeGroup").addEventListener("change", changeLakeGroup); document.getElementById("lakeGroup").addEventListener("change", changeLakeGroup);
document.getElementById("lakeGroupAdd").addEventListener("click", toggleNewGroupInput); document.getElementById("lakeGroupAdd").addEventListener("click", toggleNewGroupInput);
document.getElementById("lakeGroupName").addEventListener("change", createNewGroup); document.getElementById("lakeGroupName").addEventListener("change", createNewGroup);
document.getElementById("lakeGroupRemove").addEventListener("click", removeLakeGroup); document.getElementById("lakeGroupRemove").addEventListener("click", removeLakeGroup);
document.getElementById("lakeGroupsHide").addEventListener("click", hideGroupSection);
document.getElementById("lakeEditStyle").addEventListener("click", editGroupStyle); document.getElementById("lakeEditStyle").addEventListener("click", editGroupStyle);
document.getElementById("lakeLegend").addEventListener("click", editLakeLegend); document.getElementById("lakeLegend").addEventListener("click", editLakeLegend);
function getLake() {
const lakeId = +elSelected.attr("data-f");
return pack.features.find(feature => feature.i === lakeId);
}
function updateLakeValues() {
const l = getLake();
document.getElementById("lakeName").value = l.name;
document.getElementById("lakeGroup").value = l.type;
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
document.getElementById("lakeArea").value = si(l.area * distanceScaleInput.value ** 2) + unit;
}
function drawLakeVertices() { function drawLakeVertices() {
const f = +elSelected.attr("data-f"); // feature id const v = getLake().vertices; // lake outer vertices
const v = pack.features[f].vertices; // lake outer vertices
const c = [... new Set(v.map(v => pack.vertices.c[v]).flat())]; const c = [... new Set(v.map(v => pack.vertices.c[v]).flat())];
debug.select("#vertices").selectAll("polygon").data(c).enter().append("polygon") debug.select("#vertices").selectAll("polygon").data(c).enter().append("polygon")
@ -43,10 +59,6 @@ function editLake() {
.attr("cx", d => pack.vertices.p[d][0]).attr("cy", d => pack.vertices.p[d][1]) .attr("cx", d => pack.vertices.p[d][0]).attr("cy", d => pack.vertices.p[d][1])
.attr("r", .4).attr("data-v", d => d).call(d3.drag().on("drag", dragVertex)) .attr("r", .4).attr("data-v", d => d).call(d3.drag().on("drag", dragVertex))
.on("mousemove", () => tip("Drag to move the vertex, please use for fine-tuning only. Edit heightmap to change actual cell heights")); .on("mousemove", () => tip("Drag to move the vertex, please use for fine-tuning only. Edit heightmap to change actual cell heights"));
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
const area = pack.features[f].area;
lakeArea.innerHTML = si(area * distanceScaleInput.value ** 2) + unit;
} }
function dragVertex() { function dragVertex() {
@ -61,29 +73,29 @@ function editLake() {
function redrawLake() { function redrawLake() {
lineGen.curve(d3.curveBasisClosed); lineGen.curve(d3.curveBasisClosed);
const f = +elSelected.attr("data-f"); const feature = getLake();
const vertices = pack.features[f].vertices; const points = feature.vertices.map(v => pack.vertices.p[v]);
const points = vertices.map(v => pack.vertices.p[v]);
const d = round(lineGen(points)); const d = round(lineGen(points));
elSelected.attr("d", d); elSelected.attr("d", d);
defs.select("mask#land > path#land_"+f).attr("d", d); // update land mask defs.select("mask#land > path#land_"+feature.i).attr("d", d); // update land mask
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value; const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
const area = Math.abs(d3.polygonArea(points)); feature.area = Math.abs(d3.polygonArea(points));
lakeArea.innerHTML = si(area * distanceScaleInput.value ** 2) + unit; document.getElementById("lakeArea").value = si(feature.area * distanceScaleInput.value ** 2) + unit;
} }
function showGroupSection() { function changeName() {
document.querySelectorAll("#lakeEditor > button").forEach(el => el.style.display = "none"); getLake().name = this.value;
document.getElementById("lakeGroupsSelection").style.display = "inline-block";
} }
function hideGroupSection() { function generateNameCulture() {
document.querySelectorAll("#lakeEditor > button").forEach(el => el.style.display = "inline-block"); const lake = getLake();
document.getElementById("lakeGroupsSelection").style.display = "none"; lake.name = lakeName.value = Lakes.getName(lake);
document.getElementById("lakeGroupName").style.display = "none"; }
document.getElementById("lakeGroupName").value = "";
document.getElementById("lakeGroup").style.display = "inline-block"; function generateNameRandom() {
const lake = getLake();
lake.name = lakeName.value = Names.getBase(rand(nameBases.length-1));
} }
function selectLakeGroup(node) { function selectLakeGroup(node) {

View file

@ -421,7 +421,6 @@ function applyStoredOptions() {
const height = +params.get("height"); const height = +params.get("height");
if (width) mapWidthInput.value = width; if (width) mapWidthInput.value = width;
if (height) mapHeightInput.value = height; if (height) mapHeightInput.value = height;
//window.history.pushState({}, null, "?");
} }
// randomize options if randomization is allowed (not locked or options='default') // randomize options if randomization is allowed (not locked or options='default')

View file

@ -95,7 +95,7 @@ async function openEmblemEditor() {
function regenerateRivers() { function regenerateRivers() {
Rivers.generate(); Rivers.generate();
defineLakesGroup(); Lakes.defineGroup();
Rivers.specify(); Rivers.specify();
if (!layerIsOn("toggleRivers")) toggleRivers(); if (!layerIsOn("toggleRivers")) toggleRivers();
} }

View file

@ -47,7 +47,7 @@ function editWorld() {
generatePrecipitation(); generatePrecipitation();
const heights = new Uint8Array(pack.cells.h); const heights = new Uint8Array(pack.cells.h);
Rivers.generate(); Rivers.generate();
defineLakesGroup(); Lakes.defineGroup();
Rivers.specify(); Rivers.specify();
pack.cells.h = new Float32Array(heights); pack.cells.h = new Float32Array(heights);
defineBiomes(); defineBiomes();