hoi4-agent-tools 4.3.0 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -1
- package/dist/hoi4_agent_tools/core/bounded-source.js +31 -9
- package/dist/hoi4_agent_tools/core/configuration.js +11 -0
- package/dist/hoi4_agent_tools/core/operation-context.js +17 -1
- package/dist/hoi4_agent_tools/core/temporary-directories.js +7 -2
- package/dist/hoi4_agent_tools/core/transaction-limits.js +18 -4
- package/dist/hoi4_agent_tools/core/transactions.js +23 -4
- package/dist/hoi4_agent_tools/core/workspace-json.js +54 -0
- package/dist/hoi4_agent_tools/core/workspace.js +42 -6
- package/dist/hoi4_agent_tools/map/appearance-validation.js +1110 -0
- package/dist/hoi4_agent_tools/map/bmp.js +9 -1
- package/dist/hoi4_agent_tools/map/border-quality-input.js +209 -0
- package/dist/hoi4_agent_tools/map/border-quality.js +1180 -0
- package/dist/hoi4_agent_tools/map/colormap.js +128 -0
- package/dist/hoi4_agent_tools/map/geography/geometry.js +284 -0
- package/dist/hoi4_agent_tools/map/geography/index.js +6 -0
- package/dist/hoi4_agent_tools/map/geography/model.js +196 -0
- package/dist/hoi4_agent_tools/map/geography/preparation.js +120 -0
- package/dist/hoi4_agent_tools/map/geography/preview.js +647 -0
- package/dist/hoi4_agent_tools/map/geography/raster.js +253 -0
- package/dist/hoi4_agent_tools/map/geography/resolve.js +1467 -0
- package/dist/hoi4_agent_tools/map/model.js +15 -2
- package/dist/hoi4_agent_tools/map/operations.js +111 -8
- package/dist/hoi4_agent_tools/map/partition-fields.js +398 -0
- package/dist/hoi4_agent_tools/map/partition-geography.js +113 -0
- package/dist/hoi4_agent_tools/map/partition-graph.js +165 -0
- package/dist/hoi4_agent_tools/map/partition-groups.js +164 -0
- package/dist/hoi4_agent_tools/map/partition-provinces.js +727 -0
- package/dist/hoi4_agent_tools/map/partition-tuning.js +101 -0
- package/dist/hoi4_agent_tools/map/partition-watershed.js +252 -0
- package/dist/hoi4_agent_tools/map/partition.js +163 -0
- package/dist/hoi4_agent_tools/map/reference-import.js +333 -0
- package/dist/hoi4_agent_tools/map/reference-preparation.js +138 -5
- package/dist/hoi4_agent_tools/map/reference-sheets.js +231 -0
- package/dist/hoi4_agent_tools/map/reference-world.js +104 -13
- package/dist/hoi4_agent_tools/map/relief/checks.js +281 -0
- package/dist/hoi4_agent_tools/map/relief/fields.js +15 -0
- package/dist/hoi4_agent_tools/map/relief/full-resolution.js +133 -0
- package/dist/hoi4_agent_tools/map/relief/grid.js +190 -0
- package/dist/hoi4_agent_tools/map/relief/hydrology.js +1315 -0
- package/dist/hoi4_agent_tools/map/relief/index.js +27 -0
- package/dist/hoi4_agent_tools/map/relief/land.js +110 -0
- package/dist/hoi4_agent_tools/map/relief/macro-relief.js +535 -0
- package/dist/hoi4_agent_tools/map/relief/math.js +93 -0
- package/dist/hoi4_agent_tools/map/relief/river-raster.js +680 -0
- package/dist/hoi4_agent_tools/map/relief/terrain-synthesis.js +304 -0
- package/dist/hoi4_agent_tools/map/render.js +36 -0
- package/dist/hoi4_agent_tools/map/service.js +191 -16
- package/dist/hoi4_agent_tools/map/tool-operations.js +208 -2
- package/dist/hoi4_agent_tools/map/validation.js +9 -0
- package/dist/hoi4_agent_tools/map/water.js +227 -0
- package/dist/hoi4_agent_tools/map/world-context.js +24 -1
- package/dist/hoi4_agent_tools/map/world-edge.js +335 -0
- package/dist/hoi4_agent_tools/map/world-noise.js +34 -0
- package/dist/hoi4_agent_tools/map/world-partition.js +119 -0
- package/dist/hoi4_agent_tools/map/world-provinces.js +131 -56
- package/dist/hoi4_agent_tools/map/world.js +462 -124
- package/dist/hoi4_agent_tools/mcp/tools/map.js +69 -1
- package/dist/hoi4_agent_tools/reference/operations.js +3 -2
- package/dist/hoi4_agent_tools/schemas/geography.js +325 -0
- package/dist/hoi4_agent_tools/schemas/map-requests.js +20 -2
- package/dist/hoi4_agent_tools/schemas/map.js +209 -2
- package/dist/hoi4_agent_tools/schemas/reference-sheets.js +92 -0
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/configuration.md +17 -14
- package/docs/map.md +258 -15
- package/docs/tools.md +4 -4
- package/docs/troubleshooting.md +11 -11
- package/npm-shrinkwrap.json +109 -91
- package/package.json +3 -3
- package/schemas/configuration.schema.json +65 -58
- package/schemas/continuous-focus-palette.schema.json +1 -1
- package/schemas/custom-weighted-pool.schema.json +1 -1
- package/schemas/decision-inspect-input.schema.json +1 -1
- package/schemas/error-log-input.schema.json +1 -1
- package/schemas/event-feature-manifest.schema.json +1 -1
- package/schemas/focus-plan.schema.json +1 -1
- package/schemas/focus-planning-sidecar.schema.json +1 -1
- package/schemas/geography-model.schema.json +1148 -0
- package/schemas/gui-animation-source.schema.json +1 -1
- package/schemas/gui-helper.schema.json +5 -11
- package/schemas/gui-scenario.schema.json +1 -1
- package/schemas/helper-expansion-request.schema.json +1 -1
- package/schemas/helper-expansion-summary.schema.json +1 -1
- package/schemas/impact-inspect-input.schema.json +1 -1
- package/schemas/map-operation.schema.json +2156 -259
- package/schemas/mechanic-test-input.schema.json +1 -1
- package/schemas/mod-index-input.schema.json +1 -1
- package/schemas/operation-result.schema.json +1 -1
- package/schemas/package-check-input.schema.json +1 -1
- package/schemas/package-manifest.schema.json +1 -1
- package/schemas/probability-analysis-result.schema.json +25 -7
- package/schemas/probability-compare-input.schema.json +1 -1
- package/schemas/probability-evaluate-input.schema.json +1 -1
- package/schemas/probability-inspect-input.schema.json +1 -1
- package/schemas/probability-render-input.schema.json +1 -1
- package/schemas/probability-scenario-set.schema.json +1 -1
- package/schemas/probability-sequence-input.schema.json +1 -1
- package/schemas/probability-simulate-input.schema.json +1 -1
- package/schemas/probability-sweep-input.schema.json +1 -1
- package/schemas/reference-context-input.schema.json +1 -1
- package/schemas/reference-read-input.schema.json +1 -1
- package/schemas/reference-search-input.schema.json +1 -1
- package/schemas/reference-sheets.schema.json +267 -0
- package/schemas/scenario-suite.schema.json +1 -1
- package/schemas/scenario-test-input.schema.json +1 -1
- package/schemas/script-validate-input.schema.json +1 -1
- package/schemas/shared-scenario.schema.json +1 -1
- package/schemas/source-lookup-input.schema.json +1 -1
- package/server.json +2 -2
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* River pixels (ADR 0056): the macro rivers of `hydrology.ts` become the one-pixel, orthogonally
|
|
3
|
+
* connected paths of `rivers.bmp`. Each river is routed in short chunks by A* over the
|
|
4
|
+
* full-resolution heights inside a corridor around its macro route; a river never touches another
|
|
5
|
+
* river except where it joins one (a tributary ends beside its parent in a red pixel, a
|
|
6
|
+
* distributary starts beside it in a yellow one), so no 2 x 2 block can form. Exactly one green
|
|
7
|
+
* source marks the main branch of every connected river.
|
|
8
|
+
*/
|
|
9
|
+
import { LAND, MinHeap } from './grid.js';
|
|
10
|
+
import { noise2, seedFromString, smoothstep } from './math.js';
|
|
11
|
+
/** Index values of `rivers.bmp`. */
|
|
12
|
+
export const RIVER_SOURCE_INDEX = 0;
|
|
13
|
+
export const RIVER_TRIBUTARY_END_INDEX = 1;
|
|
14
|
+
export const RIVER_DISTRIBUTARY_START_INDEX = 2;
|
|
15
|
+
export const RIVER_LAND_INDEX = 255;
|
|
16
|
+
export const RIVER_WATER_INDEX = 254;
|
|
17
|
+
const WINDOW_LIMIT = 288;
|
|
18
|
+
class Scratch {
|
|
19
|
+
cost = new Float32Array(WINDOW_LIMIT * WINDOW_LIMIT);
|
|
20
|
+
previous = new Int32Array(WINDOW_LIMIT * WINDOW_LIMIT);
|
|
21
|
+
stamp = new Int32Array(WINDOW_LIMIT * WINDOW_LIMIT);
|
|
22
|
+
heap = new MinHeap(1 << 15);
|
|
23
|
+
run = 0;
|
|
24
|
+
}
|
|
25
|
+
/** True when the pixel or any of its eight neighbours belongs to a river. */
|
|
26
|
+
function nearRiver(context, x, y) {
|
|
27
|
+
const { width, height, owner } = context;
|
|
28
|
+
for (let dy = -1; dy <= 1; dy += 1) {
|
|
29
|
+
const ny = y + dy;
|
|
30
|
+
if (ny < 0 || ny >= height)
|
|
31
|
+
continue;
|
|
32
|
+
for (let dx = -1; dx <= 1; dx += 1) {
|
|
33
|
+
const nx = x + dx;
|
|
34
|
+
if (nx < 0 || nx >= width)
|
|
35
|
+
continue;
|
|
36
|
+
if (owner[ny * width + nx] !== 0)
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
/** Owners other than `allowed` among the eight neighbours of a pixel (and the pixel itself). */
|
|
43
|
+
function foreignNeighbour(context, x, y, allowed) {
|
|
44
|
+
const { width, height, owner } = context;
|
|
45
|
+
for (let dy = -1; dy <= 1; dy += 1) {
|
|
46
|
+
const ny = y + dy;
|
|
47
|
+
if (ny < 0 || ny >= height)
|
|
48
|
+
continue;
|
|
49
|
+
for (let dx = -1; dx <= 1; dx += 1) {
|
|
50
|
+
const nx = x + dx;
|
|
51
|
+
if (nx < 0 || nx >= width)
|
|
52
|
+
continue;
|
|
53
|
+
const value = owner[ny * width + nx];
|
|
54
|
+
if (value !== 0 && value !== allowed)
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
/** True when a land pixel touches water orthogonally, or lies on the map border. */
|
|
61
|
+
function atMouth(context, x, y) {
|
|
62
|
+
const { width, height, kind } = context;
|
|
63
|
+
if (x === 0 || y === 0 || x === width - 1 || y === height - 1)
|
|
64
|
+
return true;
|
|
65
|
+
const index = y * width + x;
|
|
66
|
+
return (kind[index - 1] !== LAND ||
|
|
67
|
+
kind[index + 1] !== LAND ||
|
|
68
|
+
kind[index - width] !== LAND ||
|
|
69
|
+
kind[index + width] !== LAND);
|
|
70
|
+
}
|
|
71
|
+
/** One A* run over a window; returns pixel indices from the start to the goal, or undefined. */
|
|
72
|
+
function search(context, scratch, spec) {
|
|
73
|
+
const { width, kind, heights, seed } = context;
|
|
74
|
+
const win = spec.window;
|
|
75
|
+
scratch.run += 1;
|
|
76
|
+
const run = scratch.run;
|
|
77
|
+
const heap = scratch.heap;
|
|
78
|
+
heap.clear();
|
|
79
|
+
const startX = spec.start % width;
|
|
80
|
+
const startY = (spec.start - startX) / width;
|
|
81
|
+
const goalX = spec.goal === undefined ? spec.aim.x : spec.goal % width;
|
|
82
|
+
const goalY = spec.goal === undefined ? spec.aim.y : (spec.goal - (spec.goal % width)) / width;
|
|
83
|
+
const wanderSeed = seed;
|
|
84
|
+
const segX = spec.guide.bx - spec.guide.ax;
|
|
85
|
+
const segY = spec.guide.by - spec.guide.ay;
|
|
86
|
+
const segLength = segX * segX + segY * segY;
|
|
87
|
+
const local = (x, y) => (y - win.y0) * win.width + (x - win.x0);
|
|
88
|
+
const startLocal = local(startX, startY);
|
|
89
|
+
scratch.stamp[startLocal] = run;
|
|
90
|
+
scratch.cost[startLocal] = 0;
|
|
91
|
+
scratch.previous[startLocal] = -1;
|
|
92
|
+
heap.push(Math.abs(goalX - startX) + Math.abs(goalY - startY), startLocal);
|
|
93
|
+
const tolerance = spec.tolerance ?? 0;
|
|
94
|
+
let expanded = 0;
|
|
95
|
+
const limit = win.width * win.height * 6;
|
|
96
|
+
while (heap.length > 0) {
|
|
97
|
+
const current = heap.pop();
|
|
98
|
+
const g = scratch.cost[current];
|
|
99
|
+
const x = win.x0 + (current % win.width);
|
|
100
|
+
const y = win.y0 + (current - (current % win.width)) / win.width;
|
|
101
|
+
if (heap.popKey > (g + Math.abs(goalX - x) + Math.abs(goalY - y)) * 1.0001 + 0.01)
|
|
102
|
+
continue;
|
|
103
|
+
const goalReached = spec.goalTest === undefined
|
|
104
|
+
? Math.abs(x - goalX) + Math.abs(y - goalY) <= tolerance
|
|
105
|
+
: spec.goalTest(x, y);
|
|
106
|
+
if (goalReached && current !== startLocal) {
|
|
107
|
+
const path = [];
|
|
108
|
+
for (let at = current; at >= 0; at = scratch.previous[at]) {
|
|
109
|
+
const ax = win.x0 + (at % win.width);
|
|
110
|
+
const ay = win.y0 + (at - (at % win.width)) / win.width;
|
|
111
|
+
path.push(ay * width + ax);
|
|
112
|
+
}
|
|
113
|
+
return path.reverse();
|
|
114
|
+
}
|
|
115
|
+
if (++expanded > limit)
|
|
116
|
+
return undefined;
|
|
117
|
+
const here = y * width + x;
|
|
118
|
+
for (let n = 0; n < 4; n += 1) {
|
|
119
|
+
const nx = x + (n === 0 ? 1 : n === 1 ? -1 : 0);
|
|
120
|
+
const ny = y + (n === 2 ? 1 : n === 3 ? -1 : 0);
|
|
121
|
+
if (nx < win.x0 || ny < win.y0 || nx >= win.x0 + win.width || ny >= win.y0 + win.height)
|
|
122
|
+
continue;
|
|
123
|
+
const next = ny * width + nx;
|
|
124
|
+
if (kind[next] !== LAND)
|
|
125
|
+
continue;
|
|
126
|
+
const isGoal = spec.goalTest !== undefined
|
|
127
|
+
? spec.goalTest(nx, ny)
|
|
128
|
+
: spec.goal !== undefined && Math.abs(nx - goalX) + Math.abs(ny - goalY) <= tolerance;
|
|
129
|
+
// Cells beside another river are off limits, except where a tributary joins its parent.
|
|
130
|
+
if (!(spec.joinGoal === true && isGoal) && nearRiver(context, nx, ny))
|
|
131
|
+
continue;
|
|
132
|
+
let step = 1;
|
|
133
|
+
const rise = heights[next] - heights[here];
|
|
134
|
+
if (rise > 0)
|
|
135
|
+
step += 0.8 * rise;
|
|
136
|
+
step +=
|
|
137
|
+
2.2 *
|
|
138
|
+
(0.6 * noise2(wanderSeed, nx / 10, ny / 10) + 0.4 * noise2(wanderSeed + 3, nx / 4, ny / 4));
|
|
139
|
+
if (segLength > 0) {
|
|
140
|
+
const t = Math.max(0, Math.min(1, ((nx - spec.guide.ax) * segX + (ny - spec.guide.ay) * segY) / segLength));
|
|
141
|
+
const dx = nx - (spec.guide.ax + t * segX);
|
|
142
|
+
const dy = ny - (spec.guide.ay + t * segY);
|
|
143
|
+
step += 0.012 * Math.sqrt(dx * dx + dy * dy);
|
|
144
|
+
}
|
|
145
|
+
const candidate = Math.fround(g + step);
|
|
146
|
+
const target = local(nx, ny);
|
|
147
|
+
if (scratch.stamp[target] === run && scratch.cost[target] <= candidate)
|
|
148
|
+
continue;
|
|
149
|
+
scratch.stamp[target] = run;
|
|
150
|
+
scratch.cost[target] = candidate;
|
|
151
|
+
scratch.previous[target] = current;
|
|
152
|
+
heap.push(Math.fround(candidate + Math.abs(goalX - nx) + Math.abs(goalY - ny)), target);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
/** True when the pixel would complete a 2 x 2 block of river pixels with the three around it. */
|
|
158
|
+
function formsBlock(context, x, y) {
|
|
159
|
+
const { width, height, owner } = context;
|
|
160
|
+
for (const [ox, oy] of [
|
|
161
|
+
[-1, -1],
|
|
162
|
+
[0, -1],
|
|
163
|
+
[-1, 0],
|
|
164
|
+
[0, 0],
|
|
165
|
+
]) {
|
|
166
|
+
const bx = x + ox;
|
|
167
|
+
const by = y + oy;
|
|
168
|
+
if (bx < 0 || by < 0 || bx + 1 >= width || by + 1 >= height)
|
|
169
|
+
continue;
|
|
170
|
+
let others = 0;
|
|
171
|
+
for (const [cx, cy] of [
|
|
172
|
+
[0, 0],
|
|
173
|
+
[1, 0],
|
|
174
|
+
[0, 1],
|
|
175
|
+
[1, 1],
|
|
176
|
+
]) {
|
|
177
|
+
if (bx + cx === x && by + cy === y)
|
|
178
|
+
continue;
|
|
179
|
+
if (owner[(by + cy) * width + bx + cx] !== 0)
|
|
180
|
+
others += 1;
|
|
181
|
+
}
|
|
182
|
+
if (others === 3)
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* A pixel that touches the parent orthogonally at exactly one pixel, touches no other river and
|
|
189
|
+
* closes no 2 x 2 block: where a branch may start or a tributary may end.
|
|
190
|
+
*/
|
|
191
|
+
function singleContact(context, x, y, parent) {
|
|
192
|
+
if (parent <= 0)
|
|
193
|
+
return false;
|
|
194
|
+
const { width, height, owner } = context;
|
|
195
|
+
if (foreignNeighbour(context, x, y, parent))
|
|
196
|
+
return false;
|
|
197
|
+
let beside = 0;
|
|
198
|
+
for (const [dx, dy] of [
|
|
199
|
+
[1, 0],
|
|
200
|
+
[-1, 0],
|
|
201
|
+
[0, 1],
|
|
202
|
+
[0, -1],
|
|
203
|
+
]) {
|
|
204
|
+
const nx = x + dx;
|
|
205
|
+
const ny = y + dy;
|
|
206
|
+
if (nx < 0 || ny < 0 || nx >= width || ny >= height)
|
|
207
|
+
continue;
|
|
208
|
+
if (owner[ny * width + nx] === parent)
|
|
209
|
+
beside += 1;
|
|
210
|
+
}
|
|
211
|
+
return beside === 1 && !formsBlock(context, x, y);
|
|
212
|
+
}
|
|
213
|
+
/** Remove loops: a pixel that touches an earlier pixel of the path (other than its predecessor) cuts the path back. */
|
|
214
|
+
function removeChords(path, width) {
|
|
215
|
+
const kept = [];
|
|
216
|
+
const position = new Map();
|
|
217
|
+
for (const pixel of path) {
|
|
218
|
+
const x = pixel % width;
|
|
219
|
+
let cut = kept.length - 1;
|
|
220
|
+
for (const neighbour of [pixel - 1, pixel + 1, pixel - width, pixel + width]) {
|
|
221
|
+
if (neighbour === pixel - 1 && x === 0)
|
|
222
|
+
continue;
|
|
223
|
+
if (neighbour === pixel + 1 && x === width - 1)
|
|
224
|
+
continue;
|
|
225
|
+
const at = position.get(neighbour);
|
|
226
|
+
if (at !== undefined && at < cut)
|
|
227
|
+
cut = at;
|
|
228
|
+
}
|
|
229
|
+
while (kept.length > cut + 1)
|
|
230
|
+
position.delete(kept.pop());
|
|
231
|
+
if (position.has(pixel))
|
|
232
|
+
continue;
|
|
233
|
+
position.set(pixel, kept.length);
|
|
234
|
+
kept.push(pixel);
|
|
235
|
+
}
|
|
236
|
+
return kept;
|
|
237
|
+
}
|
|
238
|
+
function windowFor(points, width, height, margin) {
|
|
239
|
+
let x0 = Infinity;
|
|
240
|
+
let y0 = Infinity;
|
|
241
|
+
let x1 = -Infinity;
|
|
242
|
+
let y1 = -Infinity;
|
|
243
|
+
for (const pixel of points) {
|
|
244
|
+
const x = pixel % width;
|
|
245
|
+
const y = (pixel - x) / width;
|
|
246
|
+
if (x < x0)
|
|
247
|
+
x0 = x;
|
|
248
|
+
if (x > x1)
|
|
249
|
+
x1 = x;
|
|
250
|
+
if (y < y0)
|
|
251
|
+
y0 = y;
|
|
252
|
+
if (y > y1)
|
|
253
|
+
y1 = y;
|
|
254
|
+
}
|
|
255
|
+
const wx0 = Math.max(0, x0 - margin);
|
|
256
|
+
const wy0 = Math.max(0, y0 - margin);
|
|
257
|
+
const wx1 = Math.min(width - 1, x1 + margin);
|
|
258
|
+
const wy1 = Math.min(height - 1, y1 + margin);
|
|
259
|
+
return {
|
|
260
|
+
x0: wx0,
|
|
261
|
+
y0: wy0,
|
|
262
|
+
width: Math.min(WINDOW_LIMIT, wx1 - wx0 + 1),
|
|
263
|
+
height: Math.min(WINDOW_LIMIT, wy1 - wy0 + 1),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
/** A free land pixel near the centre of a macro cell. */
|
|
267
|
+
function cellPixel(context, cell, macroWidth) {
|
|
268
|
+
const mx = cell % macroWidth;
|
|
269
|
+
const my = (cell - mx) / macroWidth;
|
|
270
|
+
const { width, height, kind } = context;
|
|
271
|
+
const cx = Math.min(width - 1, 2 * mx + 1);
|
|
272
|
+
const cy = Math.min(height - 1, 2 * my + 1);
|
|
273
|
+
let best = -1;
|
|
274
|
+
let bestScore = Infinity;
|
|
275
|
+
for (let dy = -2; dy <= 2; dy += 1)
|
|
276
|
+
for (let dx = -2; dx <= 2; dx += 1) {
|
|
277
|
+
const x = cx + dx;
|
|
278
|
+
const y = cy + dy;
|
|
279
|
+
if (x < 0 || y < 0 || x >= width || y >= height)
|
|
280
|
+
continue;
|
|
281
|
+
const pixel = y * width + x;
|
|
282
|
+
if (kind[pixel] !== LAND || nearRiver(context, x, y))
|
|
283
|
+
continue;
|
|
284
|
+
const score = dx * dx + dy * dy;
|
|
285
|
+
if (score < bestScore) {
|
|
286
|
+
bestScore = score;
|
|
287
|
+
best = pixel;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return best;
|
|
291
|
+
}
|
|
292
|
+
/** The pixel of a river nearest to a point, within pixels. */
|
|
293
|
+
function nearestOwned(context, cx, cy, value, radius) {
|
|
294
|
+
const { width, height, owner } = context;
|
|
295
|
+
let best;
|
|
296
|
+
let bestDistance = Infinity;
|
|
297
|
+
for (let dy = -radius; dy <= radius; dy += 1) {
|
|
298
|
+
const y = cy + dy;
|
|
299
|
+
if (y < 0 || y >= height)
|
|
300
|
+
continue;
|
|
301
|
+
for (let dx = -radius; dx <= radius; dx += 1) {
|
|
302
|
+
const x = cx + dx;
|
|
303
|
+
if (x < 0 || x >= width || owner[y * width + x] !== value)
|
|
304
|
+
continue;
|
|
305
|
+
const d = dx * dx + dy * dy;
|
|
306
|
+
if (d < bestDistance) {
|
|
307
|
+
bestDistance = d;
|
|
308
|
+
best = { x, y };
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return best;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Smoothed pixel-space polyline of a macro river, one point per macro cell, ends fixed, with small
|
|
316
|
+
* bends added: a river on a macro cell grid would otherwise run in the straight 45 degree and axis
|
|
317
|
+
* lines the grid favours.
|
|
318
|
+
*/
|
|
319
|
+
function smoothedPoints(cells, macroWidth, seed) {
|
|
320
|
+
const n = cells.length;
|
|
321
|
+
const xs = new Float64Array(n);
|
|
322
|
+
const ys = new Float64Array(n);
|
|
323
|
+
for (let i = 0; i < n; i += 1) {
|
|
324
|
+
xs[i] = 2 * (cells[i] % macroWidth) + 0.5;
|
|
325
|
+
ys[i] = 2 * Math.floor(cells[i] / macroWidth) + 0.5;
|
|
326
|
+
}
|
|
327
|
+
const points = [];
|
|
328
|
+
for (let i = 0; i < n; i += 1) {
|
|
329
|
+
const r = Math.min(3, i, n - 1 - i);
|
|
330
|
+
let x = 0;
|
|
331
|
+
let y = 0;
|
|
332
|
+
for (let j = i - r; j <= i + r; j += 1) {
|
|
333
|
+
x += xs[j];
|
|
334
|
+
y += ys[j];
|
|
335
|
+
}
|
|
336
|
+
points.push([x / (2 * r + 1), y / (2 * r + 1)]);
|
|
337
|
+
}
|
|
338
|
+
if (n < 6)
|
|
339
|
+
return points;
|
|
340
|
+
let length = 0;
|
|
341
|
+
const arc = new Float64Array(n);
|
|
342
|
+
for (let i = 1; i < n; i += 1) {
|
|
343
|
+
const ex = points[i][0] - points[i - 1][0];
|
|
344
|
+
const ey = points[i][1] - points[i - 1][1];
|
|
345
|
+
length += Math.sqrt(ex * ex + ey * ey);
|
|
346
|
+
arc[i] = length;
|
|
347
|
+
}
|
|
348
|
+
const amplitude = Math.min(9, 3 + 0.02 * length);
|
|
349
|
+
const bent = [];
|
|
350
|
+
for (let i = 0; i < n; i += 1) {
|
|
351
|
+
const a = points[Math.max(0, i - 2)];
|
|
352
|
+
const b = points[Math.min(n - 1, i + 2)];
|
|
353
|
+
let tx = b[0] - a[0];
|
|
354
|
+
let ty = b[1] - a[1];
|
|
355
|
+
const norm = Math.sqrt(tx * tx + ty * ty);
|
|
356
|
+
if (norm === 0) {
|
|
357
|
+
bent.push(points[i]);
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
tx /= norm;
|
|
361
|
+
ty /= norm;
|
|
362
|
+
const taper = smoothstep(Math.min(i, n - 1 - i) / 6);
|
|
363
|
+
const offset = Math.max(-1, Math.min(1, (noise2(seed, arc[i] / 17, 0.5) * 2 - 1) * 1.8)) *
|
|
364
|
+
amplitude *
|
|
365
|
+
taper;
|
|
366
|
+
bent.push([points[i][0] - ty * offset, points[i][1] + tx * offset]);
|
|
367
|
+
}
|
|
368
|
+
return bent;
|
|
369
|
+
}
|
|
370
|
+
/** Four-connected pixels along a polyline: short steps along the larger axis give natural staircases. */
|
|
371
|
+
function orthogonalTrace(points, from, width, height) {
|
|
372
|
+
let cx = from % width;
|
|
373
|
+
let cy = (from - cx) / width;
|
|
374
|
+
const path = [from];
|
|
375
|
+
for (const [px, py] of points) {
|
|
376
|
+
const tx = Math.min(width - 1, Math.max(0, Math.round(px)));
|
|
377
|
+
const ty = Math.min(height - 1, Math.max(0, Math.round(py)));
|
|
378
|
+
while (cx !== tx || cy !== ty) {
|
|
379
|
+
const dx = tx - cx;
|
|
380
|
+
const dy = ty - cy;
|
|
381
|
+
if (dx !== 0 && Math.abs(dx) >= Math.abs(dy))
|
|
382
|
+
cx += dx > 0 ? 1 : -1;
|
|
383
|
+
else
|
|
384
|
+
cy += dy > 0 ? 1 : -1;
|
|
385
|
+
path.push(cy * width + cx);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return path;
|
|
389
|
+
}
|
|
390
|
+
/** Turn the macro rivers into river pixels. */
|
|
391
|
+
export function rasterizeRivers(input) {
|
|
392
|
+
const { rivers, width, height, kind, heights } = input;
|
|
393
|
+
const total = width * height;
|
|
394
|
+
const owner = new Int16Array(total);
|
|
395
|
+
const context = {
|
|
396
|
+
width,
|
|
397
|
+
height,
|
|
398
|
+
kind,
|
|
399
|
+
heights,
|
|
400
|
+
owner,
|
|
401
|
+
seed: seedFromString(input.seed, 'river-pixels'),
|
|
402
|
+
};
|
|
403
|
+
const scratch = new Scratch();
|
|
404
|
+
const notes = [];
|
|
405
|
+
const out = [];
|
|
406
|
+
const failed = [];
|
|
407
|
+
let skipped = 0;
|
|
408
|
+
const emptyRiver = (river) => ({
|
|
409
|
+
id: river.id,
|
|
410
|
+
named: river.named,
|
|
411
|
+
pixels: new Int32Array(0),
|
|
412
|
+
complete: false,
|
|
413
|
+
});
|
|
414
|
+
const giveUp = (river) => {
|
|
415
|
+
out.push(emptyRiver(river));
|
|
416
|
+
if (river.named)
|
|
417
|
+
failed.push(river.id);
|
|
418
|
+
else
|
|
419
|
+
skipped += 1;
|
|
420
|
+
};
|
|
421
|
+
for (let index = 0; index < rivers.length; index += 1) {
|
|
422
|
+
if (index % 16 === 0)
|
|
423
|
+
input.signal?.throwIfAborted();
|
|
424
|
+
const river = rivers[index];
|
|
425
|
+
const self = index + 1;
|
|
426
|
+
const parentRiver = river.parent >= 0 ? out[river.parent] : undefined;
|
|
427
|
+
if (river.parent >= 0 && (parentRiver === undefined || parentRiver.pixels.length === 0)) {
|
|
428
|
+
giveUp(river);
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
const parent = river.parent >= 0 ? river.parent + 1 : 0;
|
|
432
|
+
const cells = river.cells;
|
|
433
|
+
// Where the parent river's pixels really run, near the macro cell it joins.
|
|
434
|
+
const anchor = river.joinCell < 0
|
|
435
|
+
? undefined
|
|
436
|
+
: nearestOwned(context, Math.min(width - 1, 2 * (river.joinCell % input.macroWidth) + 1), Math.min(height - 1, 2 * Math.floor(river.joinCell / input.macroWidth) + 1), parent, 40);
|
|
437
|
+
const points = smoothedPoints(cells, input.macroWidth, seedFromString(input.seed, `bend:${river.id}`));
|
|
438
|
+
// The first pixel.
|
|
439
|
+
let start = -1;
|
|
440
|
+
let first = 0;
|
|
441
|
+
if (river.junction === 'distributary' && anchor !== undefined) {
|
|
442
|
+
let best = Infinity;
|
|
443
|
+
const aim = points[Math.min(points.length - 1, 3)];
|
|
444
|
+
for (let dy = -8; dy <= 8; dy += 1)
|
|
445
|
+
for (let dx = -8; dx <= 8; dx += 1) {
|
|
446
|
+
const px = anchor.x + dx;
|
|
447
|
+
const py = anchor.y + dy;
|
|
448
|
+
if (px < 1 || py < 1 || px >= width - 1 || py >= height - 1)
|
|
449
|
+
continue;
|
|
450
|
+
if (owner[py * width + px] !== parent)
|
|
451
|
+
continue;
|
|
452
|
+
for (const [ox, oy] of [
|
|
453
|
+
[1, 0],
|
|
454
|
+
[-1, 0],
|
|
455
|
+
[0, 1],
|
|
456
|
+
[0, -1],
|
|
457
|
+
]) {
|
|
458
|
+
const sx = px + ox;
|
|
459
|
+
const sy = py + oy;
|
|
460
|
+
const pixel = sy * width + sx;
|
|
461
|
+
if (kind[pixel] !== LAND || owner[pixel] !== 0)
|
|
462
|
+
continue;
|
|
463
|
+
if (!singleContact(context, sx, sy, parent))
|
|
464
|
+
continue;
|
|
465
|
+
const score = (sx - aim[0]) * (sx - aim[0]) + (sy - aim[1]) * (sy - aim[1]);
|
|
466
|
+
if (score < best) {
|
|
467
|
+
best = score;
|
|
468
|
+
start = pixel;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
first = 1;
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
// A source beside another river moves a little downstream.
|
|
476
|
+
for (let k = 0; k < Math.min(8, cells.length - 3) && start < 0; k += 1) {
|
|
477
|
+
start = cellPixel(context, cells[k], input.macroWidth);
|
|
478
|
+
if (start >= 0)
|
|
479
|
+
first = k + 1;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (start < 0 || (river.endsIn === 'river' && anchor === undefined)) {
|
|
483
|
+
giveUp(river);
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
const traced = orthogonalTrace(points.slice(first), start, width, height);
|
|
487
|
+
const valid = (pixel) => kind[pixel] === LAND && !nearRiver(context, pixel % width, Math.floor(pixel / width));
|
|
488
|
+
let path = [start];
|
|
489
|
+
const lookahead = 48;
|
|
490
|
+
for (let i = 1; i < traced.length; i += 1) {
|
|
491
|
+
const pixel = traced[i];
|
|
492
|
+
if (valid(pixel)) {
|
|
493
|
+
path.push(pixel);
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
// Blocked: join the trace again at the next free pixel by a short search.
|
|
497
|
+
let next = -1;
|
|
498
|
+
for (let j = i + 1; j < Math.min(traced.length, i + lookahead); j += 1)
|
|
499
|
+
if (valid(traced[j])) {
|
|
500
|
+
next = j;
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
const here = path[path.length - 1];
|
|
504
|
+
let found;
|
|
505
|
+
if (next >= 0)
|
|
506
|
+
for (const margin of [8, 20, 40]) {
|
|
507
|
+
const target = traced[next];
|
|
508
|
+
const hx = here % width;
|
|
509
|
+
const hy = (here - hx) / width;
|
|
510
|
+
const tx = target % width;
|
|
511
|
+
const ty = (target - tx) / width;
|
|
512
|
+
found = search(context, scratch, {
|
|
513
|
+
start: here,
|
|
514
|
+
window: windowFor([here, target], width, height, margin),
|
|
515
|
+
goal: target,
|
|
516
|
+
tolerance: 0,
|
|
517
|
+
aim: { x: tx, y: ty },
|
|
518
|
+
guide: { ax: hx, ay: hy, bx: tx, by: ty },
|
|
519
|
+
self,
|
|
520
|
+
parent,
|
|
521
|
+
});
|
|
522
|
+
// A detour much longer than the gap it closes would draw a loop around the obstacle.
|
|
523
|
+
if (found !== undefined &&
|
|
524
|
+
found.length > 2.2 * (Math.abs(tx - hx) + Math.abs(ty - hy)) + 14)
|
|
525
|
+
found = undefined;
|
|
526
|
+
if (found !== undefined)
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
// Nothing to rejoin: the trace ran into water or another river, and the end search takes over.
|
|
530
|
+
if (found === undefined)
|
|
531
|
+
break;
|
|
532
|
+
for (let k = 1; k < found.length; k += 1)
|
|
533
|
+
path.push(found[k]);
|
|
534
|
+
i = next;
|
|
535
|
+
}
|
|
536
|
+
// The end: the mouth for most rivers, the parent for tributaries.
|
|
537
|
+
let complete = false;
|
|
538
|
+
const here = path[path.length - 1];
|
|
539
|
+
const hx = here % width;
|
|
540
|
+
const hy = (here - hx) / width;
|
|
541
|
+
const endRiver = river.endsIn === 'river';
|
|
542
|
+
if (!endRiver && atMouth(context, hx, hy))
|
|
543
|
+
complete = true;
|
|
544
|
+
else {
|
|
545
|
+
const last = points[points.length - 1];
|
|
546
|
+
const jx = anchor?.x ?? Math.round(last[0]);
|
|
547
|
+
const jy = anchor?.y ?? Math.round(last[1]);
|
|
548
|
+
for (const margin of endRiver ? [10, 24, 48, 90] : [10, 24, 48]) {
|
|
549
|
+
const target = endRiver ? jy * width + jx : here;
|
|
550
|
+
const found = search(context, scratch, {
|
|
551
|
+
start: here,
|
|
552
|
+
window: windowFor([here, target], width, height, margin + (endRiver ? 16 : 0)),
|
|
553
|
+
goalTest: endRiver
|
|
554
|
+
? (x, y) => kind[y * width + x] === LAND &&
|
|
555
|
+
Math.abs(x - jx) + Math.abs(y - jy) <= (margin > 60 ? 120 : 30) &&
|
|
556
|
+
owner[y * width + x] === 0 &&
|
|
557
|
+
singleContact(context, x, y, parent)
|
|
558
|
+
: (x, y) => owner[y * width + x] === 0 &&
|
|
559
|
+
kind[y * width + x] === LAND &&
|
|
560
|
+
atMouth(context, x, y),
|
|
561
|
+
joinGoal: endRiver,
|
|
562
|
+
aim: { x: jx, y: jy },
|
|
563
|
+
guide: { ax: hx, ay: hy, bx: jx, by: jy },
|
|
564
|
+
self,
|
|
565
|
+
parent,
|
|
566
|
+
});
|
|
567
|
+
if (found !== undefined) {
|
|
568
|
+
for (let k = 1; k < found.length; k += 1)
|
|
569
|
+
path.push(found[k]);
|
|
570
|
+
complete = true;
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
path = removeChords(path, width);
|
|
576
|
+
for (const pixel of path)
|
|
577
|
+
owner[pixel] = self;
|
|
578
|
+
if (!complete && river.named)
|
|
579
|
+
failed.push(river.id);
|
|
580
|
+
else if (!complete)
|
|
581
|
+
skipped += 1;
|
|
582
|
+
if ((path.length < 4 || !complete) && !river.named) {
|
|
583
|
+
for (const pixel of path)
|
|
584
|
+
owner[pixel] = 0;
|
|
585
|
+
out.push(emptyRiver(river));
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
out.push({ id: river.id, named: river.named, pixels: Int32Array.from(path), complete });
|
|
589
|
+
}
|
|
590
|
+
if (failed.length > 0)
|
|
591
|
+
notes.push({
|
|
592
|
+
code: 'MAP_RIVER_PIXEL_ROUTE_INCOMPLETE',
|
|
593
|
+
severity: 'warning',
|
|
594
|
+
message: `${failed.length} river(s) could not be routed to the end at full resolution: ${failed
|
|
595
|
+
.slice(0, 8)
|
|
596
|
+
.join(', ')}`,
|
|
597
|
+
details: { rivers: failed.slice(0, 50), count: failed.length },
|
|
598
|
+
});
|
|
599
|
+
if (skipped > 0)
|
|
600
|
+
notes.push({
|
|
601
|
+
code: 'MAP_RIVER_STREAMS_DROPPED',
|
|
602
|
+
severity: 'info',
|
|
603
|
+
message: `${skipped} short stream(s) of the accumulation network found no room at full resolution and were dropped`,
|
|
604
|
+
details: { streams: skipped },
|
|
605
|
+
});
|
|
606
|
+
// The pixel budget: drop the smallest unnamed headwaters (rivers nothing else joins) first.
|
|
607
|
+
if (input.pixelCap !== undefined) {
|
|
608
|
+
let used = 0;
|
|
609
|
+
for (const river of out)
|
|
610
|
+
used += river.pixels.length;
|
|
611
|
+
if (used > input.pixelCap) {
|
|
612
|
+
const children = new Int32Array(rivers.length);
|
|
613
|
+
rivers.forEach((river, index) => {
|
|
614
|
+
if (out[index].pixels.length > 0 && river.parent >= 0)
|
|
615
|
+
children[river.parent] = children[river.parent] + 1;
|
|
616
|
+
});
|
|
617
|
+
const order = rivers
|
|
618
|
+
.map((river, index) => index)
|
|
619
|
+
.filter((index) => !rivers[index].named && out[index].pixels.length > 0)
|
|
620
|
+
.sort((a, b) => rivers[a].order - rivers[b].order ||
|
|
621
|
+
rivers[a].catchment[rivers[a].catchment.length - 1] -
|
|
622
|
+
rivers[b].catchment[rivers[b].catchment.length - 1] ||
|
|
623
|
+
a - b);
|
|
624
|
+
let dropped = 0;
|
|
625
|
+
for (let pass = 0; pass < 6 && used > input.pixelCap; pass += 1)
|
|
626
|
+
for (const index of order) {
|
|
627
|
+
if (used <= input.pixelCap)
|
|
628
|
+
break;
|
|
629
|
+
const river = out[index];
|
|
630
|
+
if (river.pixels.length === 0 || children[index] > 0)
|
|
631
|
+
continue;
|
|
632
|
+
used -= river.pixels.length;
|
|
633
|
+
for (const pixel of river.pixels)
|
|
634
|
+
owner[pixel] = 0;
|
|
635
|
+
out[index] = emptyRiver(rivers[index]);
|
|
636
|
+
const parent = rivers[index].parent;
|
|
637
|
+
if (parent >= 0)
|
|
638
|
+
children[parent] = children[parent] - 1;
|
|
639
|
+
dropped += 1;
|
|
640
|
+
}
|
|
641
|
+
notes.push({
|
|
642
|
+
code: 'MAP_RIVER_DENSITY',
|
|
643
|
+
severity: used > input.pixelCap ? 'warning' : 'info',
|
|
644
|
+
message: `The rivers came to more than ${input.pixelCap} pixels, the most a map should carry, so ${dropped} small headwater stream(s) were dropped${used > input.pixelCap ? `; the named rivers alone use ${used}` : ''}`,
|
|
645
|
+
details: { cap: input.pixelCap, dropped, pixels: used },
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
// Beds: heights never rise downstream, then the bitmap.
|
|
650
|
+
const bitmap = new Uint8Array(total);
|
|
651
|
+
for (let pixel = 0; pixel < total; pixel += 1)
|
|
652
|
+
bitmap[pixel] = kind[pixel] === LAND ? RIVER_LAND_INDEX : RIVER_WATER_INDEX;
|
|
653
|
+
const order = new Uint8Array(total);
|
|
654
|
+
let pixels = 0;
|
|
655
|
+
for (let index = 0; index < rivers.length; index += 1) {
|
|
656
|
+
const river = rivers[index];
|
|
657
|
+
const pixelRiver = out[index];
|
|
658
|
+
const path = pixelRiver.pixels;
|
|
659
|
+
if (path.length === 0)
|
|
660
|
+
continue;
|
|
661
|
+
let level = Infinity;
|
|
662
|
+
for (let k = 0; k < path.length; k += 1) {
|
|
663
|
+
const pixel = path[k];
|
|
664
|
+
const cell = Math.min(river.cells.length - 1, Math.floor((k * river.cells.length) / path.length));
|
|
665
|
+
const w = river.width[cell];
|
|
666
|
+
level = Math.min(level, heights[pixel] - (0.25 + 0.06 * (w - 3)));
|
|
667
|
+
heights[pixel] = Math.max(96, level);
|
|
668
|
+
bitmap[pixel] = w;
|
|
669
|
+
order[pixel] = Math.min(255, river.order);
|
|
670
|
+
}
|
|
671
|
+
if (river.parent < 0)
|
|
672
|
+
bitmap[path[0]] = RIVER_SOURCE_INDEX;
|
|
673
|
+
else if (river.junction === 'tributary' && pixelRiver.complete)
|
|
674
|
+
bitmap[path[path.length - 1]] = RIVER_TRIBUTARY_END_INDEX;
|
|
675
|
+
else if (river.junction === 'distributary')
|
|
676
|
+
bitmap[path[0]] = RIVER_DISTRIBUTARY_START_INDEX;
|
|
677
|
+
pixels += path.length;
|
|
678
|
+
}
|
|
679
|
+
return { bitmap, order, rivers: out, pixels, notes };
|
|
680
|
+
}
|