Skip to content

Commit e2e9f1f

Browse files
committed
perf: use typed arrays and inline loops across generators
Replace sparse JS cost/checked arrays with Float32Array/Uint8Array in cultures, states, provinces, religions, zones, lakes, and pathUtils. In river-generator, precompute outCell-to-lake map, replace sort-based min-finding with inline loops, and remove dead debug code. In ice.js, use Set for ID lookup and sequential IDs during generate().
1 parent be36069 commit e2e9f1f

9 files changed

Lines changed: 75 additions & 55 deletions

File tree

public/modules/ice.js

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,13 @@
33
// Ice layer data model - separates ice data from SVG rendering
44
window.Ice = (function () {
55

6-
// Find next available id for new ice element idealy filling gaps
6+
// Find next available id for new ice element ideally filling gaps
77
function getNextId() {
88
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;
9+
const existingIds = new Set(pack.ice.map(e => e.i));
10+
for (let id = 0; ; id++) {
11+
if (!existingIds.has(id)) return id;
1312
}
14-
return existingIds[existingIds.length - 1] + 1;
1513
}
1614

1715
// Generate glaciers and icebergs based on temperature and height
@@ -24,6 +22,7 @@ window.Ice = (function () {
2422
const ICEBERG_MAX_TEMP = 0;
2523
const GLACIER_MAX_TEMP = -8;
2624
const minMaxTemp = d3.min(temp);
25+
let nextId = 0; // sequential IDs since we start from clear()
2726

2827
// Generate glaciers on cold land
2928
{
@@ -36,7 +35,7 @@ window.Ice = (function () {
3635
isolines[type].polygons.forEach(points => {
3736
const clipped = clipPoly(points);
3837
pack.ice.push({
39-
i: getNextId(),
38+
i: nextId++,
4039
points: clipped,
4140
type: "glacier"
4241
});
@@ -64,7 +63,7 @@ window.Ice = (function () {
6463
]);
6564

6665
pack.ice.push({
67-
i: getNextId(),
66+
i: nextId++,
6867
points,
6968
type: "iceberg",
7069
cellId,

src/modules/cultures-generator.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1289,7 +1289,7 @@ class CulturesModule {
12891289
const { cells, cultures } = pack;
12901290

12911291
const queue = new FlatQueue();
1292-
const cost: number[] = [];
1292+
const cost = new Float32Array(cells.i.length);
12931293

12941294
const neutralRate =
12951295
(byId("neutralRate") as HTMLInputElement)?.valueAsNumber || 1;
@@ -1313,6 +1313,7 @@ class CulturesModule {
13131313
{ cellId: culture.center, cultureId: culture.i, priority: 0 },
13141314
0,
13151315
);
1316+
cost[culture.center as number] = 1; // mark as seeded
13161317
}
13171318

13181319
const getBiomeCost = (c: number, biome: number, type: string) => {

src/modules/lakes.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ export class LakesModule {
101101
const ELEVATION_LIMIT = +(
102102
byId("lakeElevationLimitOutput") as HTMLInputElement
103103
)?.value;
104+
const checked = new Uint8Array(cells.i.length);
104105

105106
pack.features.forEach((feature) => {
106107
if (feature.type !== "lake") return;
@@ -117,8 +118,8 @@ export class LakesModule {
117118
(a, b) => h[a] - h[b],
118119
)[0];
119120
const queue = [lowestShorelineCell];
120-
const checked = [];
121-
checked[lowestShorelineCell] = true;
121+
const visited = [lowestShorelineCell];
122+
checked[lowestShorelineCell] = 1;
122123

123124
while (queue.length && isDeep) {
124125
const cellId: number = queue.pop() as number;
@@ -133,11 +134,15 @@ export class LakesModule {
133134
isDeep = false;
134135
}
135136

136-
checked[neibCellId] = true;
137+
checked[neibCellId] = 1;
138+
visited.push(neibCellId);
137139
queue.push(neibCellId);
138140
}
139141
}
140142

143+
// reset checked for visited cells only
144+
for (let j = 0; j < visited.length; j++) checked[visited[j]] = 0;
145+
141146
feature.closed = isDeep;
142147
});
143148
}

src/modules/provinces-generator.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ class ProvinceModule {
174174

175175
// expand generated provinces
176176
const queue = new FlatQueue();
177-
const cost: number[] = [];
177+
const cost = new Float32Array(cells.i.length);
178178

179179
provinces.forEach((p) => {
180180
if (!p.i || p.removed || isProvinceLocked(p)) return;
@@ -269,7 +269,7 @@ class ProvinceModule {
269269
provinceIds[center] = provinceId;
270270

271271
// expand province
272-
const cost: number[] = [];
272+
const cost = new Float32Array(cells.i.length);
273273
cost[center] = 1;
274274
queue.push({ e: center, p: 0 }, 0);
275275
while (queue.length) {

src/modules/religions-generator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -894,7 +894,7 @@ class ReligionsModule {
894894
const religionIds = this.spreadFolkReligions(religions);
895895

896896
const queue = new FlatQueue();
897-
const cost: number[] = [];
897+
const cost = new Float32Array(cells.i.length);
898898

899899
// limit cost for organized religions growth
900900
const maxExpansionCost =

src/modules/river-generator.ts

Lines changed: 45 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Alea from "alea";
2-
import { curveBasis, curveCatmullRom, line, mean, min, sum } from "d3";
2+
import { curveBasis, curveCatmullRom, line, mean, sum } from "d3";
33
import { each, rn, round, rw } from "../utils";
44

55
declare global {
@@ -69,16 +69,21 @@ class RiverModule {
6969
.sort((a: number, b: number) => h[b] - h[a]);
7070
const lakeOutCells = Lakes.defineClimateData(h);
7171

72+
// pre-compute map from outCell to qualifying lake features
73+
const outCellToLakes = new Map<number, any[]>();
74+
for (const feature of features) {
75+
if (feature.type !== "lake" || !feature.outCell) continue;
76+
if (!(feature.flux > feature.evaporation)) continue;
77+
const list = outCellToLakes.get(feature.outCell);
78+
if (list) list.push(feature);
79+
else outCellToLakes.set(feature.outCell, [feature]);
80+
}
81+
7282
for (const i of land) {
7383
cells.fl[i] += prec[cells.g[i]] / cellsNumberModifier; // add flux from precipitation
7484

7585
// create lake outlet if lake is not in deep depression and flux > evaporation
76-
const lakes = lakeOutCells[i]
77-
? features.filter(
78-
(feature: any) =>
79-
i === feature.outCell && feature.flux > feature.evaporation,
80-
)
81-
: [];
86+
const lakes = (lakeOutCells[i] && outCellToLakes.get(i)) || [];
8287
for (const lake of lakes) {
8388
const lakeCell = cells.c[i].find(
8489
(c: number) => h[c] < 20 && cells.f[c] === lake.i,
@@ -121,34 +126,29 @@ class RiverModule {
121126
}
122127

123128
// downhill cell (make sure it's not in the source lake)
124-
let min = null;
129+
let minCell = -1;
125130
if (lakeOutCells[i]) {
126-
const filtered = cells.c[i].filter(
127-
(c: number) =>
128-
!lakes.map((lake: any) => lake.i).includes(cells.f[c]),
129-
);
130-
min = filtered.sort((a: number, b: number) => h[a] - h[b])[0];
131+
const lakeIds = new Set(lakes.map((lake: any) => lake.i));
132+
let minH = Infinity;
133+
for (const c of cells.c[i]) {
134+
if (lakeIds.has(cells.f[c])) continue;
135+
if (h[c] < minH) { minH = h[c]; minCell = c; }
136+
}
131137
} else if (cells.haven[i]) {
132-
min = cells.haven[i];
138+
minCell = cells.haven[i];
133139
} else {
134-
min = cells.c[i].sort((a: number, b: number) => h[a] - h[b])[0];
140+
let minH = Infinity;
141+
for (const c of cells.c[i]) {
142+
if (h[c] < minH) { minH = h[c]; minCell = c; }
143+
}
135144
}
136145

137146
// cells is depressed
138-
if (h[i] <= h[min]) continue;
139-
140-
// debug
141-
// .append("line")
142-
// .attr("x1", pack.cells.p[i][0])
143-
// .attr("y1", pack.cells.p[i][1])
144-
// .attr("x2", pack.cells.p[min][0])
145-
// .attr("y2", pack.cells.p[min][1])
146-
// .attr("stroke", "#333")
147-
// .attr("stroke-width", 0.2);
147+
if (minCell < 0 || h[i] <= h[minCell]) continue;
148148

149149
if (cells.fl[i] < MIN_FLUX_TO_FORM_RIVER) {
150150
// flux is too small to operate as a river
151-
if (h[min] >= 20) cells.fl[min] += cells.fl[i];
151+
if (h[minCell] >= 20) cells.fl[minCell] += cells.fl[i];
152152
continue;
153153
}
154154

@@ -159,7 +159,7 @@ class RiverModule {
159159
riverNext++;
160160
}
161161

162-
flowDown(min, cells.fl[i], cells.r[i]);
162+
flowDown(minCell, cells.fl[i], cells.r[i]);
163163
}
164164
};
165165

@@ -370,28 +370,39 @@ class RiverModule {
370370
if (iteration < checkLakeMaxIteration) {
371371
for (const l of lakes) {
372372
if (l.closed) continue;
373-
const minHeight = min(l.shoreline.map((s: number) => h[s])) as number;
373+
let minHeight = Infinity;
374+
for (let si = 0; si < l.shoreline.length; si++) {
375+
const sh = h[l.shoreline[si]];
376+
if (sh < minHeight) minHeight = sh;
377+
}
374378
if (minHeight >= 100 || l.height > minHeight) continue;
375379

376380
if (iteration > elevateLakeMaxIteration) {
377381
l.shoreline.forEach((i: number) => {
378382
h[i] = cells.h[i];
379383
});
380-
l.height =
381-
(min(l.shoreline.map((s: number) => h[s])) as number) - 1;
384+
let resetMin = Infinity;
385+
for (let si = 0; si < l.shoreline.length; si++) {
386+
const sh = h[l.shoreline[si]];
387+
if (sh < resetMin) resetMin = sh;
388+
}
389+
l.height = resetMin - 1;
382390
l.closed = true;
383391
continue;
384392
}
385393

386394
depressions++;
387-
l.height = (minHeight as number) + 0.2;
395+
l.height = minHeight + 0.2;
388396
}
389397
}
390398

391399
for (const i of land) {
392-
const minHeight = min(
393-
cells.c[i].map((c: number) => height(c)),
394-
) as number;
400+
const neighbors = cells.c[i];
401+
let minHeight = Infinity;
402+
for (let ni = 0; ni < neighbors.length; ni++) {
403+
const nh = height(neighbors[ni]);
404+
if (nh < minHeight) minHeight = nh;
405+
}
395406
if (minHeight >= 100 || h[i] > minHeight) continue;
396407

397408
depressions++;

src/modules/states-generator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ class StatesModule {
146146
cells.state = cells.state || new Uint16Array(cells.i.length);
147147

148148
const queue = new FlatQueue();
149-
const cost: number[] = [];
149+
const cost = new Float32Array(cells.i.length);
150150

151151
const globalGrowthRate =
152152
(byId("growthRate") as HTMLInputElement)?.valueAsNumber || 1;

src/modules/zones-generator.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ class ZonesModule {
265265
if (!burg) return;
266266

267267
const cellsArray: number[] = [];
268-
const cost: number[] = [];
268+
const cost = new Float32Array(pack.cells.i.length);
269269
const maxCells = rand(20, 40);
270270

271271
const queue = new FlatQueue();
@@ -409,7 +409,7 @@ class ZonesModule {
409409
usedCells[burg.cell] = 1;
410410

411411
const cellsArray: number[] = [];
412-
const cost: number[] = [];
412+
const cost = new Float32Array(pack.cells.i.length);
413413
const maxCells = rand(5, 25);
414414

415415
const queue = new FlatQueue();

src/utils/pathUtils.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -341,10 +341,14 @@ export const findPath = (
341341
): number[] | null => {
342342
if (isExit(start)) return null;
343343

344-
const from = [];
345-
const cost = [];
344+
const cellCount = packedGraph.cells.i.length;
345+
const from = new Int32Array(cellCount).fill(-1);
346+
const cost = new Float32Array(cellCount);
347+
cost.fill(Infinity);
348+
346349
const queue = new window.FlatQueue();
347350
queue.push(start, 0);
351+
cost[start] = 0;
348352

349353
while (queue.length) {
350354
const currentCost = queue.peekValue();
@@ -353,7 +357,7 @@ export const findPath = (
353357
for (const next of packedGraph.cells.c[current]) {
354358
if (isExit(next)) {
355359
from[next] = current;
356-
return restorePath(next, start, from);
360+
return restorePath(next, start, from as any);
357361
}
358362

359363
const nextCost = getCost(current, next);

0 commit comments

Comments
 (0)