refactor: clean up global variable declarations and improve type definitions

This commit is contained in:
Marc Emmanuel 2026-01-25 19:13:02 +01:00
parent 613e826133
commit 3ed3d0dbd8
8 changed files with 105 additions and 177 deletions

View file

@ -1,24 +1,8 @@
import { range, mean } from "d3";
import { rn } from "../utils";
import { PackedGraph } from "./PackedGraph";
declare global {
var Biomes: BiomesModule;
var pack: PackedGraph;
var grid: any;
var TIME: boolean;
var biomesData: {
i: number[];
name: string[];
color: string[];
biomesMartix: Uint8Array[];
habitability: number[];
iconsDensity: number[];
icons: string[][];
cost: number[];
};
}
class BiomesModule {
@ -74,7 +58,7 @@ class BiomesModule {
{swamp: 1}
];
const cost: number[] = [10, 200, 150, 60, 50, 70, 70, 80, 90, 200, 1000, 5000, 150]; // biome movement cost
const biomesMartix: Uint8Array[] = [
const biomesMatrix: Uint8Array[] = [
// hot ↔ cold [>19°C; <-4°C]; dry ↕ wet
new Uint8Array([1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 10]),
new Uint8Array([3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 9, 9, 9, 9, 10, 10, 10]),
@ -95,7 +79,7 @@ class BiomesModule {
parsedIcons[i] = parsed;
}
return {i: range(0, name.length), name, color, biomesMartix, habitability, iconsDensity, icons: parsedIcons, cost};
return {i: range(0, name.length), name, color, biomesMatrix, habitability, iconsDensity, icons: parsedIcons, cost};
};
define() {
@ -135,7 +119,7 @@ class BiomesModule {
// in other cases use biome matrix
const moistureBand = Math.min((moisture / 5) | 0, 4); // [0-4]
const temperatureBand = Math.min(Math.max(20 - temperature, 0), 25); // [0-25]
return biomesData.biomesMartix[moistureBand][temperatureBand];
return biomesData.biomesMatrix[moistureBand][temperatureBand];
}
private isWetland(moisture: number, temperature: number, height: number) {

View file

@ -1,18 +1,9 @@
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: LakesModule;
var grid: any;
var pack: PackedGraph;
var seed: string;
var Features: FeatureModule;
}
type FeatureType = "ocean" | "lake" | "island";
@ -58,18 +49,6 @@ class FeatureModule {
private WATER_COAST = -1;
private DEEP_WATER = -2;
private get grid() {
return grid;
}
private get packedGraph() {
return pack;
}
private get seed() {
return seed;
}
/**
* calculate distance to coast for every cell
*/
@ -100,9 +79,9 @@ class FeatureModule {
*/
markupGrid() {
TIME && console.time("markupGrid");
Math.random = Alea(this.seed); // get the same result on heightmap edit in Erase mode
Math.random = Alea(seed); // get the same result on heightmap edit in Erase mode
const { h: heights, c: neighbors, b: borderCells, i } = this.grid.cells;
const { h: heights, c: neighbors, b: borderCells, i } = grid.cells;
const cellsNumber = i.length;
const distanceField = new Int8Array(cellsNumber); // gird.cells.t
const featureIds = new Uint16Array(cellsNumber); // gird.cells.f
@ -141,9 +120,9 @@ class FeatureModule {
// markup deep ocean cells
this.markup({ distanceField, neighbors, start: this.DEEP_WATER, increment: -1, limit: -10 });
this.grid.cells.t = distanceField;
this.grid.cells.f = featureIds;
this.grid.features = [0, ...features];
grid.cells.t = distanceField;
grid.cells.f = featureIds;
grid.features = [0, ...features];
TIME && console.timeEnd("markupGrid");
}
@ -153,7 +132,7 @@ class FeatureModule {
*/
markupPack() {
const defineHaven = (cellId: number) => {
const waterCells = neighbors[cellId].filter((index: number) => isWater(index, this.packedGraph));
const waterCells = neighbors[cellId].filter((index: number) => isWater(index, pack));
const distances = waterCells.map((neibCellId: number) => distanceSquared(cells.p[cellId], cells.p[neibCellId]));
const closest = distances.indexOf(Math.min.apply(Math, distances));
@ -215,7 +194,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.shoreline = unique((feature.vertices as number[]).map(vertex => vertices.c[vertex].filter((index: number) => isLand(index, pack))).flat() || []);
feature.height = Lakes.getHeight(feature as PackedGraphFeature);
}
@ -226,7 +205,7 @@ class FeatureModule {
TIME && console.time("markupPack");
const { cells, vertices } = this.packedGraph;
const { cells, vertices } = pack;
const { c: neighbors, b: borderCells, i } = cells;
const packCellsNumber = i.length;
if (!packCellsNumber) return; // no cells -> there is nothing to do
@ -242,7 +221,7 @@ class FeatureModule {
const firstCell = queue[0];
featureIds[firstCell] = featureId;
const land = isLand(firstCell, this.packedGraph);
const land = isLand(firstCell, pack);
let border = Boolean(borderCells[firstCell]); // true if feature touches map border
let totalCells = 1; // count cells in a feature
@ -252,7 +231,7 @@ class FeatureModule {
if (!border && borderCells[cellId]) border = true;
for (const neighborId of neighbors[cellId]) {
const isNeibLand = isLand(neighborId, this.packedGraph);
const isNeibLand = isLand(neighborId, pack);
if (land && !isNeibLand) {
distanceField[cellId] = this.LAND_COAST;
@ -280,12 +259,11 @@ class FeatureModule {
this.markup({ distanceField, neighbors, start: this.DEEPER_LAND, increment: 1 }); // markup pack land
this.markup({ distanceField, neighbors, start: this.DEEP_WATER, increment: -1, limit: -10 }); // markup pack water
this.packedGraph.cells.t = distanceField;
this.packedGraph.cells.f = featureIds;
this.packedGraph.cells.haven = haven;
this.packedGraph.cells.harbor = harbor;
this.packedGraph.features = [0 as unknown as PackedGraphFeature, ...features];
pack.cells.t = distanceField;
pack.cells.f = featureIds;
pack.cells.haven = haven;
pack.cells.harbor = harbor;
pack.features = [0 as unknown as PackedGraphFeature, ...features];
TIME && console.timeEnd("markupPack");
}
@ -293,14 +271,14 @@ class FeatureModule {
* define feature groups (ocean, sea, gulf, continent, island, isle, freshwater lake, salt lake, etc.)
*/
defineGroups() {
const gridCellsNumber = this.grid.cells.i.length;
const gridCellsNumber = grid.cells.i.length;
const OCEAN_MIN_SIZE = gridCellsNumber / 25;
const SEA_MIN_SIZE = gridCellsNumber / 1000;
const CONTINENT_MIN_SIZE = gridCellsNumber / 10;
const ISLAND_MIN_SIZE = gridCellsNumber / 1000;
const defineIslandGroup = (feature: PackedGraphFeature) => {
const prevFeature = this.packedGraph.features[this.packedGraph.cells.f[feature.firstCell - 1]];
const prevFeature = pack.features[pack.cells.f[feature.firstCell - 1]];
if (prevFeature && prevFeature.type === "lake") return "lake_island";
if (feature.cells > CONTINENT_MIN_SIZE) return "continent";
if (feature.cells > ISLAND_MIN_SIZE) return "island";
@ -334,7 +312,7 @@ class FeatureModule {
throw new Error(`Markup: unknown feature type ${feature.type}`);
}
for (const feature of this.packedGraph.features) {
for (const feature of pack.features) {
if (!feature || feature.type === "ocean") continue;
if (feature.type === "lake") feature.height = Lakes.getHeight(feature);

View file

@ -3,12 +3,7 @@ import { range as d3Range, leastIndex, mean } from "d3";
import { createTypedArray, byId, findGridCell, getNumberInRange, lim, minmax, P, rand } from "../utils";
declare global {
interface Window {
HeightmapGenerator: HeightmapGenerator;
}
var heightmapTemplates: any;
var TIME: boolean;
var ERROR: boolean;
var HeightmapGenerator: HeightmapGenerator;
}
type Tool = "Hill" | "Pit" | "Range" | "Trough" | "Strait" | "Mask" | "Invert" | "Add" | "Multiply" | "Smooth";
@ -19,21 +14,6 @@ class HeightmapGenerator {
blobPower: number = 0;
linePower: number = 0;
// TODO: remove after migration to TS and use param in constructor
get seed() {
return (window as any).seed;
}
get graphWidth() {
return (window as any).graphWidth;
}
get graphHeight() {
return (window as any).graphHeight;
}
constructor() {
}
private clearData() {
this.heights = null;
this.grid = null;
@ -107,8 +87,8 @@ class HeightmapGenerator {
let h = lim(getNumberInRange(height));
do {
const x = this.getPointInRange(rangeX, this.graphWidth);
const y = this.getPointInRange(rangeY, this.graphHeight);
const x = this.getPointInRange(rangeX, graphWidth);
const y = this.getPointInRange(rangeY, graphHeight);
if (x === undefined || y === undefined) return;
start = findGridCell(x, y, this.grid);
limit++;
@ -143,8 +123,8 @@ class HeightmapGenerator {
let h = lim(getNumberInRange(height));
do {
const x = this.getPointInRange(rangeX, this.graphWidth);
const y = this.getPointInRange(rangeY, this.graphHeight);
const x = this.getPointInRange(rangeX, graphWidth);
const y = this.getPointInRange(rangeY, graphHeight);
if (x === undefined || y === undefined) return;
start = findGridCell(x, y, this.grid);
limit++;
@ -207,8 +187,8 @@ class HeightmapGenerator {
if (rangeX && rangeY) {
// find start and end points
const startX = this.getPointInRange(rangeX, this.graphWidth) as number;
const startY = this.getPointInRange(rangeY, this.graphHeight) as number;
const startX = this.getPointInRange(rangeX, graphWidth) as number;
const startY = this.getPointInRange(rangeY, graphHeight) as number;
let dist = 0;
let limit = 0;
@ -216,11 +196,11 @@ class HeightmapGenerator {
let endX;
do {
endX = Math.random() * this.graphWidth * 0.8 + this.graphWidth * 0.1;
endY = Math.random() * this.graphHeight * 0.7 + this.graphHeight * 0.15;
endX = Math.random() * graphWidth * 0.8 + graphWidth * 0.1;
endY = Math.random() * graphHeight * 0.7 + graphHeight * 0.15;
dist = Math.abs(endY - startY) + Math.abs(endX - startX);
limit++;
} while ((dist < this.graphWidth / 8 || dist > this.graphWidth / 3) && limit < 50);
} while ((dist < graphWidth / 8 || dist > graphWidth / 3) && limit < 50);
startCellId = findGridCell(startX, startY, this.grid);
endCellId = findGridCell(endX, endY, this.grid);
@ -311,19 +291,19 @@ class HeightmapGenerator {
let endX: number;
let endY: number;
do {
startX = this.getPointInRange(rangeX, this.graphWidth) as number;
startY = this.getPointInRange(rangeY, this.graphHeight) as number;
startX = this.getPointInRange(rangeX, graphWidth) as number;
startY = this.getPointInRange(rangeY, graphHeight) as number;
startCellId = findGridCell(startX, startY, this.grid);
limit++;
} while (this.heights[startCellId] < 20 && limit < 50);
limit = 0;
do {
endX = Math.random() * this.graphWidth * 0.8 + this.graphWidth * 0.1;
endY = Math.random() * this.graphHeight * 0.7 + this.graphHeight * 0.15;
endX = Math.random() * graphWidth * 0.8 + graphWidth * 0.1;
endY = Math.random() * graphHeight * 0.7 + graphHeight * 0.15;
dist = Math.abs(endY - startY) + Math.abs(endX - startX);
limit++;
} while ((dist < this.graphWidth / 8 || dist > this.graphWidth / 2) && limit < 50);
} while ((dist < graphWidth / 8 || dist > graphWidth / 2) && limit < 50);
endCellId = findGridCell(endX, endY, this.grid);
}
@ -378,14 +358,14 @@ class HeightmapGenerator {
if (desiredWidth < 1 && P(desiredWidth)) return;
const used = new Uint8Array(this.heights.length);
const vert = direction === "vertical";
const startX = vert ? Math.floor(Math.random() * this.graphWidth * 0.4 + this.graphWidth * 0.3) : 5;
const startY = vert ? 5 : Math.floor(Math.random() * this.graphHeight * 0.4 + this.graphHeight * 0.3);
const startX = vert ? Math.floor(Math.random() * graphWidth * 0.4 + graphWidth * 0.3) : 5;
const startY = vert ? 5 : Math.floor(Math.random() * graphHeight * 0.4 + graphHeight * 0.3);
const endX = vert
? Math.floor(this.graphWidth - startX - this.graphWidth * 0.1 + Math.random() * this.graphWidth * 0.2)
: this.graphWidth - 5;
? Math.floor(graphWidth - startX - graphWidth * 0.1 + Math.random() * graphWidth * 0.2)
: graphWidth - 5;
const endY = vert
? this.graphHeight - 5
: Math.floor(this.graphHeight - startY - this.graphHeight * 0.1 + Math.random() * this.graphHeight * 0.2);
? graphHeight - 5
: Math.floor(graphHeight - startY - graphHeight * 0.1 + Math.random() * graphHeight * 0.2);
const start = findGridCell(startX, startY, this.grid);
const end = findGridCell(endX, endY, this.grid);
@ -462,8 +442,8 @@ class HeightmapGenerator {
this.heights = this.heights.map((h, i) => {
const [x, y] = this.grid.points[i];
const nx = (2 * x) / this.graphWidth - 1; // [-1, 1], 0 is center
const ny = (2 * y) / this.graphHeight - 1; // [-1, 1], 0 is center
const nx = (2 * x) / graphWidth - 1; // [-1, 1], 0 is center
const ny = (2 * y) / graphHeight - 1; // [-1, 1], 0 is center
let distance = (1 - nx ** 2) * (1 - ny ** 2); // 1 is center, 0 is edge
if (power < 0) distance = 1 - distance; // inverted, 0 is center, 1 is edge
const masked = h * distance;
@ -509,7 +489,7 @@ class HeightmapGenerator {
TIME && console.time("defineHeightmap");
const id = (byId("templateInput")! as HTMLInputElement).value;
Math.random = Alea(this.seed);
Math.random = Alea(seed);
const isTemplate = id in heightmapTemplates;
const heights = isTemplate ? this.fromTemplate(graph, id) : await this.fromPrecreated(graph, id);

View file

@ -2,14 +2,9 @@ import { PackedGraphFeature } from "./features";
import { min, mean } from "d3";
import { byId,
rn } from "../utils";
import { PackedGraph } from "./PackedGraph";
declare global {
var Lakes: LakesModule;
var pack: PackedGraph;
var Names: any;
var heightExponentInput: HTMLInputElement;
}
export class LakesModule {

View file

@ -3,14 +3,7 @@ import type { Selection } from 'd3';
import { clipPoly,P,rn,round } from '../utils';
declare global {
interface Window {
OceanLayers: any;
}
var TIME: boolean;
var ERROR: boolean;
var grid: any;
var oceanLayers: Selection<SVGGElement, unknown, null, undefined>;
var OceanLayers: typeof OceanModule.prototype.draw;
}
class OceanModule {
private cells: any;
@ -25,10 +18,6 @@ class OceanModule {
this.oceanLayers = oceanLayers;
}
get grid(): any {
return grid;
}
randomizeOutline() {
const limits = [];
let odd = 0.2;

View file

@ -1,32 +1,13 @@
import Alea from "alea";
import { curveBasis,
line,
mean, min, sum, curveCatmullRom, Selection } from "d3";
mean, min, sum, curveCatmullRom } from "d3";
import { each,
rn,round,
rw} from "../utils";
import { PackedGraphFeature } from "./features";
import { PackedGraph } from "./PackedGraph";
import { LakesModule } from "./lakes";
declare global {
interface Window {
Rivers: any;
}
var WARN: boolean;
var graphHeight: number;
var graphWidth: number;
var pack: PackedGraph;
var rivers: Selection<SVGElement, unknown, null, undefined>;
var pointsInput: HTMLInputElement;
var grid: any;
var seed: string;
var TIME: boolean;
var Names: any;
var Lakes: LakesModule;
var Rivers: RiverModule;
}
export interface River {
@ -66,22 +47,10 @@ class RiverModule {
smallLength: number | null = null;
get graphHeight() {
return graphHeight;
}
get graphWidth() {
return graphWidth;
}
get pack(): PackedGraph {
return pack;
}
generate(allowErosion = true) {
TIME && console.time("generateRivers");
Math.random = Alea(seed);
const {cells, features} = this.pack;
const {cells, features} = pack;
const riversData: {[riverId: number]: number[]} = {};
const riverParents: {[key: number]: number} = {};
@ -220,7 +189,7 @@ class RiverModule {
// re-initialize rivers and confluence arrays
cells.r = new Uint16Array(cells.i.length);
cells.conf = new Uint16Array(cells.i.length);
this.pack.rivers = [];
pack.rivers = [];
const defaultWidthFactor = rn(1 / (parseInt(pointsInput.dataset.cells || "10000") / 10000) ** 0.25, 2);
const mainStemWidthFactor = defaultWidthFactor * 1.2;
@ -256,7 +225,7 @@ class RiverModule {
})
);
this.pack.rivers.push({
pack.rivers.push({
i: riverId,
source,
mouth,
@ -274,7 +243,7 @@ class RiverModule {
const downcutRivers = () => {
const MAX_DOWNCUT = 5;
for (const i of this.pack.cells.i) {
for (const i of pack.cells.i) {
if (cells.h[i] < 35) continue; // don't donwcut lowlands
if (!cells.fl[i]) continue;
@ -322,7 +291,7 @@ class RiverModule {
};
alterHeights(): Uint8Array {
const {h, c, t} = this.pack.cells as {h: Uint8Array, c: number[][], t: Uint8Array};
const {h, c, t} = pack.cells as {h: Uint8Array, c: number[][], t: Uint8Array};
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;
@ -331,14 +300,14 @@ class RiverModule {
// depression filling algorithm (for a correct water flux modeling)
resolveDepressions(h: Uint8Array) {
const {cells, features} = this.pack;
const {cells, features} = pack;
const maxIterations = +(document.getElementById("resolveDepressionsStepsOutput") as HTMLInputElement)?.value;
const checkLakeMaxIteration = maxIterations * 0.85;
const elevateLakeMaxIteration = maxIterations * 0.75;
const height = (i: number) => features[cells.f[i]].height || h[i]; // height of lake or specific cell
const lakes = features.filter((feature: PackedGraphFeature) => feature.type === "lake");
const lakes = features.filter((feature) => feature.type === "lake");
const land = cells.i.filter((i: number) => h[i] >= 20 && !cells.b[i]); // exclude near-border cells
land.sort((a: number, b: number) => h[a] - h[b]); // lowest cells go first
@ -389,7 +358,7 @@ class RiverModule {
};
addMeandering(riverCells: number[], riverPoints = null, meandering = 0.5): [number, number, number][] {
const {fl, h} = this.pack.cells;
const {fl, h} = pack.cells;
const meandered = [];
const lastStep = riverCells.length - 1;
const points = this.getRiverPoints(riverCells, riverPoints);
@ -441,7 +410,7 @@ class RiverModule {
getRiverPoints(riverCells: number[], riverPoints: [number, number][] | null) {
if (riverPoints) return riverPoints;
const {p} = this.pack.cells;
const {p} = pack.cells;
return riverCells.map((cell, i) => {
if (cell === -1) return this.getBorderPoint(riverCells[i - 1]);
return p[cell];
@ -449,12 +418,12 @@ class RiverModule {
};
getBorderPoint(i: number) {
const [x, y] = this.pack.cells.p[i];
const min = Math.min(y, this.graphHeight - y, x, this.graphWidth - x);
const [x, y] = pack.cells.p[i];
const min = Math.min(y, graphHeight - y, x, graphWidth - x);
if (min === y) return [x, 0];
else if (min === this.graphHeight - y) return [x, this.graphHeight];
else if (min === graphHeight - y) return [x, graphHeight];
else if (min === x) return [0, y];
return [this.graphWidth, y];
return [graphWidth, y];
};
getOffset({flux, pointIndex, widthFactor, startingWidth}: {flux: number, pointIndex: number, widthFactor: number, startingWidth: number}) {
@ -499,7 +468,7 @@ class RiverModule {
};
specify() {
const rivers = this.pack.rivers;
const rivers = pack.rivers;
if (!rivers.length) return;
for (const river of rivers) {
@ -510,13 +479,13 @@ class RiverModule {
};
getName(cell: number) {
return Names.getCulture(this.pack.cells.culture[cell]);
return Names.getCulture(pack.cells.culture[cell]);
};
getType({i, length, parent}: River) {
if (this.smallLength === null) {
const threshold = Math.ceil(this.pack.rivers.length * 0.15);
this.smallLength = this.pack.rivers.map(r => r.length || 0).sort((a: number, b: number) => a - b)[threshold];
const threshold = Math.ceil(pack.rivers.length * 0.15);
this.smallLength = pack.rivers.map(r => r.length || 0).sort((a: number, b: number) => a - b)[threshold];
}
const isSmall: boolean = length < (this.smallLength as number);
@ -537,8 +506,8 @@ class RiverModule {
// remove river and all its tributaries
remove(id: number) {
const cells = this.pack.cells;
const riversToRemove = this.pack.rivers.filter(r => r.i === id || r.parent === id || r.basin === id).map(r => r.i);
const cells = pack.cells;
const riversToRemove = pack.rivers.filter(r => r.i === id || r.parent === id || r.basin === id).map(r => r.i);
riversToRemove.forEach(r => rivers.select("#river" + r).remove());
cells.r.forEach((r, i) => {
if (!r || !riversToRemove.includes(r)) return;
@ -546,11 +515,11 @@ class RiverModule {
cells.fl[i] = grid.cells.prec[cells.g[i]];
cells.conf[i] = 0;
});
this.pack.rivers = this.pack.rivers.filter(r => !riversToRemove.includes(r.i));
pack.rivers = pack.rivers.filter(r => !riversToRemove.includes(r.i));
};
getBasin(r: number): number {
const parent = this.pack.rivers.find(river => river.i === r)?.parent;
const parent = pack.rivers.find(river => river.i === r)?.parent;
if (!parent || r === parent) return r;
return this.getBasin(parent);
};

View file

@ -1,5 +1,5 @@
import { PackedGraphFeature } from "./features";
import { River } from "./river-generator";
import { PackedGraphFeature } from "../modules/features";
import { River } from "../modules/river-generator";
type TypedArray = Uint8Array | Uint16Array | Uint32Array | Int8Array | Int16Array | Float32Array | Float64Array;

33
src/types/global.ts Normal file
View file

@ -0,0 +1,33 @@
import type { Selection } from 'd3';
import { PackedGraph } from "./PackedGraph";
declare global {
var seed: string;
var pack: PackedGraph;
var grid: any;
var graphHeight: number;
var graphWidth: number;
var TIME: boolean;
var WARN: boolean;
var ERROR: boolean;
var heightmapTemplates: any;
var Names: any;
var pointsInput: HTMLInputElement;
var heightExponentInput: HTMLInputElement;
var rivers: Selection<SVGElement, unknown, null, undefined>;
var oceanLayers: Selection<SVGGElement, unknown, null, undefined>;
var biomesData: {
i: number[];
name: string[];
color: string[];
biomesMatrix: Uint8Array[];
habitability: number[];
iconsDensity: number[];
icons: string[][];
cost: number[];
};
}