refactor: Migrate lakes functionality to lakes.ts and update related interfaces

This commit is contained in:
Marc Emmanuel 2026-01-21 23:04:23 +01:00
parent 49e7e5c533
commit 371c775578
8 changed files with 136 additions and 110 deletions

View file

@ -8469,7 +8469,6 @@
<script defer src="config/heightmap-templates.js"></script>
<script defer src="config/precreated-heightmaps.js"></script>
<script defer src="modules/lakes.js?v=1.99.00"></script>
<script defer src="modules/names-generator.js?v=1.106.0"></script>
<script defer src="modules/cultures-generator.js?v=1.106.0"></script>
<script defer src="modules/burgs-generator.js?v=1.109.5"></script>

View file

@ -1,22 +1,27 @@
import { PackedGraphFeature } from "./features";
import { River } from "./river-generator";
type TypedArray = Uint8Array | Uint16Array | Uint32Array | Int8Array | Int16Array | Float32Array | Float64Array;
export interface PackedGraph {
cells: {
i: number[]; // cell indices
c: number[][]; // neighboring cells
v: number[][]; // neighboring vertices
p: [number, number][]; // cell polygon points
b: boolean[]; // cell is on border
h: Uint8Array; // cell heights
t: Uint8Array; // cell terrain types
h: TypedArray; // cell heights
t: TypedArray; // cell terrain types
r: Uint16Array; // river id passing through cell
f: Uint16Array; // feature id occupying cell
fl: Uint16Array | Uint8Array; // flux presence in cell
conf: Uint16Array | Uint8Array; // cell water confidence
haven: Uint8Array; // cell is a haven
fl: TypedArray; // flux presence in cell
conf: TypedArray; // cell water confidence
haven: TypedArray; // cell is a haven
g: number[]; // cell ground type
culture: number[]; // cell culture id
p: [number, number][]; // cell polygon points
biome: TypedArray; // cell biome id
harbor: TypedArray; // cell harbour presence
};
vertices: {
i: number[]; // vertex indices
@ -24,6 +29,7 @@ export interface PackedGraph {
v: number[][]; // neighboring vertices
x: number[]; // x coordinates
y: number[]; // y coordinates
p: [number, number][]; // vertex points
};
rivers: River[];
features: PackedGraphFeature[];

View file

@ -1,10 +1,11 @@
import { range, mean } from "d3";
import { rn } from "../utils";
import { PackedGraph } from "./PackedGraph";
declare global {
var Biomes: BiomesModule;
var pack: any;
var pack: PackedGraph;
var grid: any;
var TIME: boolean;

View file

@ -1,15 +1,17 @@
import { clipPoly, connectVertices, createTypedArray, distanceSquared, isLand, isWater, rn, TYPED_ARRAY_MAX_VALUES,unique } from "../utils";
import Alea from "alea";
import { polygonArea } from "d3";
import { LakesModule } from "./lakes";
import { PackedGraph } from "./PackedGraph";
declare global {
interface Window {
Features: any;
}
var TIME: boolean;
var Lakes: any;
var Lakes: LakesModule;
var grid: any;
var pack: any;
var pack: PackedGraph;
var seed: string;
}
@ -30,11 +32,15 @@ export interface PackedGraphFeature {
temp: number;
flux: number;
evaporation: number;
inlets: number[];
outlet: number;
river: number;
enteringFlux: number;
closed: boolean;
name: string;
// River related
inlets?: number[];
outlet?: number;
river?: number;
enteringFlux?: number;
closed?: boolean;
outCell?: number;
}
export interface GridFeature {
@ -210,7 +216,7 @@ class FeatureModule {
if (type === "lake") {
if (area > 0) feature.vertices = (feature.vertices as number[]).reverse();
feature.shoreline = unique((feature.vertices as number[]).map(vertex => vertices.c[vertex].filter((index: number) => isLand(index, this.packedGraph))).flat() || []);
feature.height = Lakes.getHeight(feature);
feature.height = Lakes.getHeight(feature as PackedGraphFeature);
}
return {
@ -278,7 +284,7 @@ class FeatureModule {
this.packedGraph.cells.f = featureIds;
this.packedGraph.cells.haven = haven;
this.packedGraph.cells.harbor = harbor;
this.packedGraph.features = [0, ...features];
this.packedGraph.features = [0 as unknown as PackedGraphFeature, ...features];
TIME && console.timeEnd("markupPack");
}

View file

@ -1,6 +1,7 @@
import "./voronoi";
import "./heightmap-generator";
import "./features";
import "./lakes";
import "./ocean-layers";
import "./river-generator";
import "./biomes"

View file

@ -1,12 +1,98 @@
"use strict";
import { PackedGraphFeature } from "./features";
import { min, mean } from "d3";
import { byId,
rn } from "../utils";
import { PackedGraph } from "./PackedGraph";
window.Lakes = (function () {
const LAKE_ELEVATION_DELTA = 0.1;
declare global {
var Lakes: LakesModule;
var pack: PackedGraph;
var Names: any;
var heightExponentInput: HTMLInputElement;
}
export class LakesModule {
private LAKE_ELEVATION_DELTA = 0.1;
getHeight(feature: PackedGraphFeature) {
const heights = pack.cells.h;
const minShoreHeight = min(feature.shoreline.map(cellId => heights[cellId])) || 20;
return rn(minShoreHeight - this.LAKE_ELEVATION_DELTA, 2);
};
defineNames() {
pack.features.forEach((feature: PackedGraphFeature) => {
if (feature.type !== "lake") return;
feature.name = this.getName(feature);
});
};
getName(feature: PackedGraphFeature): string {
const landCell = feature.shoreline[0];
const culture = pack.cells.culture[landCell];
return Names.getCulture(culture);
};
cleanupLakeData = function () {
for (const feature of pack.features) {
if (feature.type !== "lake") continue;
delete feature.river;
delete feature.enteringFlux;
delete feature.outCell;
delete feature.closed;
feature.height = rn(feature.height, 3);
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;
}
};
defineClimateData(heights: Uint8Array) {
const {cells, features} = pack;
const lakeOutCells = new Uint16Array(cells.i.length);
const getFlux = (lake: PackedGraphFeature) => {
return lake.shoreline.reduce((acc, c) => acc + grid.cells.prec[cells.g[c]], 0);
}
const getLakeTemp = (lake: PackedGraphFeature) => {
if (lake.cells < 6) return grid.cells.temp[cells.g[lake.firstCell]];
return rn(mean(lake.shoreline.map(c => grid.cells.temp[cells.g[c]])) as number, 1);
}
const getLakeEvaporation = (lake: PackedGraphFeature) => {
const height = (lake.height - 18) ** Number(heightExponentInput.value); // height in meters
const evaporation = ((700 * (lake.temp + 0.006 * height)) / 50 + 75) / (80 - lake.temp); // based on Penman formula, [1-11]
return rn(evaporation * lake.cells);
}
const getLowestShoreCell = (lake: PackedGraphFeature) => {
return lake.shoreline.sort((a, b) => heights[a] - heights[b])[0];
}
features.forEach(feature => {
if (feature.type !== "lake") return;
feature.flux = getFlux(feature);
feature.temp = getLakeTemp(feature);
feature.evaporation = getLakeEvaporation(feature);
if (feature.closed) return; // no outlet for lakes in depressed areas
feature.outCell = getLowestShoreCell(feature);
lakeOutCells[feature.outCell as number] = feature.i;
});
return lakeOutCells;
};
// check if lake can be potentially open (not in deep depression)
const detectCloseLakes = h => {
detectCloseLakes(h: Uint8Array) {
const {cells} = pack;
const ELEVATION_LIMIT = +byId("lakeElevationLimitOutput").value;
const ELEVATION_LIMIT = +(byId("lakeElevationLimitOutput") as HTMLInputElement)?.value;
pack.features.forEach(feature => {
if (feature.type !== "lake") return;
@ -25,7 +111,7 @@ window.Lakes = (function () {
checked[lowestShorelineCell] = true;
while (queue.length && isDeep) {
const cellId = queue.pop();
const cellId: number = queue.pop() as number;
for (const neibCellId of cells.c[cellId]) {
if (checked[neibCellId]) continue;
@ -44,80 +130,6 @@ window.Lakes = (function () {
feature.closed = isDeep;
});
};
const defineClimateData = function (heights) {
const {cells, features} = pack;
const lakeOutCells = new Uint16Array(cells.i.length);
features.forEach(feature => {
if (feature.type !== "lake") return;
feature.flux = getFlux(feature);
feature.temp = getLakeTemp(feature);
feature.evaporation = getLakeEvaporation(feature);
if (feature.closed) return; // no outlet for lakes in depressed areas
feature.outCell = getLowestShoreCell(feature);
lakeOutCells[feature.outCell] = feature.i;
});
return lakeOutCells;
function getFlux(lake) {
return lake.shoreline.reduce((acc, c) => acc + grid.cells.prec[cells.g[c]], 0);
}
function getLakeTemp(lake) {
if (lake.cells < 6) return grid.cells.temp[cells.g[lake.firstCell]];
return rn(d3.mean(lake.shoreline.map(c => grid.cells.temp[cells.g[c]])), 1);
}
function getLakeEvaporation(lake) {
const height = (lake.height - 18) ** heightExponentInput.value; // height in meters
const evaporation = ((700 * (lake.temp + 0.006 * height)) / 50 + 75) / (80 - lake.temp); // based on Penman formula, [1-11]
return rn(evaporation * lake.cells);
}
function getLowestShoreCell(lake) {
return lake.shoreline.sort((a, b) => heights[a] - heights[b])[0];
}
};
const cleanupLakeData = function () {
for (const feature of pack.features) {
if (feature.type !== "lake") continue;
delete feature.river;
delete feature.enteringFlux;
delete feature.outCell;
delete feature.closed;
feature.height = rn(feature.height, 3);
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 getHeight = function (feature) {
const heights = pack.cells.h;
const minShoreHeight = d3.min(feature.shoreline.map(cellId => heights[cellId])) || 20;
return rn(minShoreHeight - LAKE_ELEVATION_DELTA, 2);
};
const defineNames = function () {
pack.features.forEach(feature => {
if (feature.type !== "lake") return;
feature.name = getName(feature);
});
};
const getName = function (feature) {
const landCell = feature.shoreline[0];
const culture = pack.cells.culture[landCell];
return Names.getCulture(culture);
};
return {defineClimateData, cleanupLakeData, detectCloseLakes, getHeight, defineNames, getName};
})();
window.Lakes = new LakesModule();

View file

@ -7,6 +7,7 @@ rn,round,
rw} from "../utils";
import { PackedGraphFeature } from "./features";
import { PackedGraph } from "./PackedGraph";
import { LakesModule } from "./lakes";
declare global {
interface Window {
@ -16,7 +17,7 @@ declare global {
var WARN: boolean;
var graphHeight: number;
var graphWidth: number;
var pack: any;
var pack: PackedGraph;
var rivers: Selection<SVGElement, unknown, null, undefined>;
var pointsInput: HTMLInputElement;
@ -25,7 +26,7 @@ declare global {
var TIME: boolean;
var Names: any;
var Lakes: any;
var Lakes: LakesModule;
}
export interface River {
@ -114,8 +115,8 @@ class RiverModule {
const sameRiver = cells.c[lakeCell].some((c: number) => cells.r[c] === lake.river);
if (sameRiver) {
cells.r[lakeCell] = lake.river;
addCellToRiver(lakeCell, lake.river);
cells.r[lakeCell] = lake.river as number;
addCellToRiver(lakeCell, lake.river as number);
} else {
cells.r[lakeCell] = riverNext;
addCellToRiver(lakeCell, riverNext);
@ -132,7 +133,7 @@ class RiverModule {
for (const lake of lakes) {
if (!Array.isArray(lake.inlets)) continue;
for (const inlet of lake.inlets) {
riverParents[inlet] = outlet;
riverParents[inlet] = outlet as number;
}
}
@ -199,7 +200,7 @@ class RiverModule {
// pour water to the water body
const waterBody = features[cells.f[toCell]];
if (waterBody.type === "lake") {
if (!waterBody.river || fromFlux > waterBody.enteringFlux) {
if (!waterBody.river || fromFlux > (waterBody.enteringFlux as number)) {
waterBody.river = river;
waterBody.enteringFlux = fromFlux;
}
@ -320,16 +321,16 @@ class RiverModule {
TIME && console.timeEnd("generateRivers");
};
alterHeights() {
alterHeights(): Uint8Array {
const {h, c, t} = this.pack.cells as {h: Uint8Array, c: number[][], t: Uint8Array};
return Array.from(h).map((h, i) => {
return Uint8Array.from(Array.from(h).map((h, i) => {
if (h < 20 || t[i] < 1) return h;
return h + t[i] / 100 + (mean(c[i].map(c => t[c])) || 0) / 10000;
});
}));
};
// depression filling algorithm (for a correct water flux modeling)
resolveDepressions(h: number[]) {
resolveDepressions(h: Uint8Array) {
const {cells, features} = this.pack;
const maxIterations = +(document.getElementById("resolveDepressionsStepsOutput") as HTMLInputElement)?.value;
const checkLakeMaxIteration = maxIterations * 0.85;

View file

@ -44,7 +44,7 @@ declare global {
}
interface Array<T> {
flat(depth?: number): T[];
flat(depth?: number): T;
at(index: number): T | undefined;
}