hoi4-agent-tools 4.3.0 → 4.5.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 +33 -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/managed-outputs.js +26 -6
- package/dist/hoi4_agent_tools/core/operation-context.js +17 -1
- package/dist/hoi4_agent_tools/core/source/encoding.js +56 -2
- package/dist/hoi4_agent_tools/core/source/parser.js +16 -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/focus/drift.js +34 -0
- package/dist/hoi4_agent_tools/focus/service.js +9 -3
- package/dist/hoi4_agent_tools/focus/tool-operations.js +2 -0
- 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 +17 -3
- package/dist/hoi4_agent_tools/map/operations.js +284 -14
- 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 +142 -6
- 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 +206 -16
- package/dist/hoi4_agent_tools/map/tool-operations.js +249 -3
- 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 +47 -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-outputs.js +22 -10
- 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-realms.js +152 -0
- package/dist/hoi4_agent_tools/map/world.js +562 -163
- 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 +280 -15
- 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/focus.md +6 -0
- package/docs/map.md +292 -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 +2325 -292
- 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,304 @@
|
|
|
1
|
+
import { LAKE, LAND, chamferDistance } from './grid.js';
|
|
2
|
+
import { landDistanceField, lakeHeight } from '../water.js';
|
|
3
|
+
import { BARRIER_CREST, BARRIER_FORT_LINE, BARRIER_LAKE_SHORE, BARRIER_RIVER, BARRIER_STEEP, BARRIER_WALL, } from './fields.js';
|
|
4
|
+
import { checkRangesAndFeatures, reliefStatistics, straightRiverShare, } from './checks.js';
|
|
5
|
+
import { buildFullResolutionHeights } from './full-resolution.js';
|
|
6
|
+
import { RIVER_PIXEL_CAP, synthesizeHydrology } from './hydrology.js';
|
|
7
|
+
import { DEFAULT_RELIEF, LAND_FLOOR, HEIGHT_CEILING, peakHeight, synthesizeMacroRelief, } from './macro-relief.js';
|
|
8
|
+
import { rasterizeRivers } from './river-raster.js';
|
|
9
|
+
/** Red channel response of the normal map per height step (base game: about 2.6). */
|
|
10
|
+
export const NORMAL_RESPONSE = 2.6;
|
|
11
|
+
function normalMap(floats, width, height, signal) {
|
|
12
|
+
const halfWidth = width / 2;
|
|
13
|
+
const halfHeight = height / 2;
|
|
14
|
+
const out = new Uint8Array(halfWidth * halfHeight * 3);
|
|
15
|
+
const gain = NORMAL_RESPONSE / 127;
|
|
16
|
+
const at = (x, y) => floats[(y < 0 ? 0 : y >= height ? height - 1 : y) * width + (x < 0 ? 0 : x >= width ? width - 1 : x)];
|
|
17
|
+
for (let y = 0; y < halfHeight; y += 1) {
|
|
18
|
+
if (y % 64 === 0)
|
|
19
|
+
signal?.throwIfAborted();
|
|
20
|
+
for (let x = 0; x < halfWidth; x += 1) {
|
|
21
|
+
const dx = at(2 * x + 2, 2 * y) - at(2 * x - 2, 2 * y);
|
|
22
|
+
const dy = at(2 * x, 2 * y - 2) - at(2 * x, 2 * y + 2);
|
|
23
|
+
const nx = -gain * dx;
|
|
24
|
+
const ny = gain * dy;
|
|
25
|
+
const length = Math.sqrt(nx * nx + ny * ny + 1);
|
|
26
|
+
const o = (y * halfWidth + x) * 3;
|
|
27
|
+
out[o] = Math.round(128 + (nx / length) * 127);
|
|
28
|
+
out[o + 1] = Math.round(128 + (ny / length) * 127);
|
|
29
|
+
out[o + 2] = Math.round(128 + (1 / length) * 127);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
function barrierField(width, height, kind, heights, riverBitmap, macro, signal) {
|
|
35
|
+
const strength = new Uint8Array(width * height);
|
|
36
|
+
const sources = new Uint8Array(width * height);
|
|
37
|
+
for (let y = 0; y < height; y += 1) {
|
|
38
|
+
if (y % 64 === 0)
|
|
39
|
+
signal?.throwIfAborted();
|
|
40
|
+
const my = y >> 1;
|
|
41
|
+
for (let x = 0; x < width; x += 1) {
|
|
42
|
+
const index = y * width + x;
|
|
43
|
+
if (kind[index] !== LAND)
|
|
44
|
+
continue;
|
|
45
|
+
let value = 0;
|
|
46
|
+
let flags = 0;
|
|
47
|
+
const river = riverBitmap[index];
|
|
48
|
+
if (river <= 11) {
|
|
49
|
+
flags |= BARRIER_RIVER;
|
|
50
|
+
value = Math.max(value, river >= 3 ? 100 + 14 * (river - 3) : 100);
|
|
51
|
+
}
|
|
52
|
+
if (x > 0 && x < width - 1 && y > 0 && y < height - 1) {
|
|
53
|
+
const gx = (heights[index + 1] - heights[index - 1]) / 2;
|
|
54
|
+
const gy = (heights[index + width] - heights[index - width]) / 2;
|
|
55
|
+
const gradient = Math.sqrt(gx * gx + gy * gy);
|
|
56
|
+
if (gradient >= 2) {
|
|
57
|
+
flags |= BARRIER_STEEP;
|
|
58
|
+
value = Math.max(value, Math.min(200, 60 + 25 * (gradient - 2)));
|
|
59
|
+
}
|
|
60
|
+
if (kind[index - 1] === LAKE ||
|
|
61
|
+
kind[index + 1] === LAKE ||
|
|
62
|
+
kind[index - width] === LAKE ||
|
|
63
|
+
kind[index + width] === LAKE) {
|
|
64
|
+
flags |= BARRIER_LAKE_SHORE;
|
|
65
|
+
value = Math.max(value, 90);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const cell = my * macro.width + (x >> 1);
|
|
69
|
+
if (macro.crest[cell] === 1 && macro.footprint[cell] >= 0.4) {
|
|
70
|
+
flags |= BARRIER_CREST;
|
|
71
|
+
value = Math.max(value, 150);
|
|
72
|
+
}
|
|
73
|
+
const wall = macro.barrier[cell];
|
|
74
|
+
if (wall === 1) {
|
|
75
|
+
flags |= BARRIER_WALL;
|
|
76
|
+
value = 255;
|
|
77
|
+
}
|
|
78
|
+
else if (wall === 2) {
|
|
79
|
+
flags |= BARRIER_FORT_LINE;
|
|
80
|
+
value = Math.max(value, 180);
|
|
81
|
+
}
|
|
82
|
+
strength[index] = Math.round(value);
|
|
83
|
+
sources[index] = flags;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return { width, height, strength, sources };
|
|
87
|
+
}
|
|
88
|
+
/** Build relief, hydrology, normals and fields for a geography model on a map of the given size. */
|
|
89
|
+
export function synthesizeTerrain(input) {
|
|
90
|
+
const { geography, width, height, seed, kind } = input;
|
|
91
|
+
const relief = { ...DEFAULT_RELIEF, ...input.relief };
|
|
92
|
+
const total = width * height;
|
|
93
|
+
let landPixels = 0;
|
|
94
|
+
for (let index = 0; index < total; index += 1)
|
|
95
|
+
if (kind[index] === LAND)
|
|
96
|
+
landPixels += 1;
|
|
97
|
+
const notes = [];
|
|
98
|
+
const stage = input.onStage ?? (() => undefined);
|
|
99
|
+
stage('macro relief');
|
|
100
|
+
const macro = synthesizeMacroRelief({
|
|
101
|
+
geography,
|
|
102
|
+
width,
|
|
103
|
+
height,
|
|
104
|
+
seed,
|
|
105
|
+
kind,
|
|
106
|
+
relief,
|
|
107
|
+
signal: input.signal,
|
|
108
|
+
});
|
|
109
|
+
const adjust = input.adjustHeight;
|
|
110
|
+
if (adjust !== undefined)
|
|
111
|
+
for (let y = 0; y < macro.height; y += 1)
|
|
112
|
+
for (let x = 0; x < macro.width; x += 1) {
|
|
113
|
+
const cell = y * macro.width + x;
|
|
114
|
+
if (macro.kind[cell] === LAND)
|
|
115
|
+
macro.heights[cell] = adjust(2 * x + 1, 2 * y + 1, macro.heights[cell]);
|
|
116
|
+
}
|
|
117
|
+
stage('hydrology');
|
|
118
|
+
const hydrology = synthesizeHydrology({
|
|
119
|
+
geography,
|
|
120
|
+
macro,
|
|
121
|
+
seed,
|
|
122
|
+
density: input.rivers?.density,
|
|
123
|
+
count: input.rivers?.count,
|
|
124
|
+
rainfall: input.rainfall,
|
|
125
|
+
landPixels,
|
|
126
|
+
signal: input.signal,
|
|
127
|
+
});
|
|
128
|
+
for (const note of hydrology.notes)
|
|
129
|
+
notes.push(note);
|
|
130
|
+
stage('full-resolution heights');
|
|
131
|
+
const floats = buildFullResolutionHeights({
|
|
132
|
+
macro: { ...macro, heights: hydrology.heights },
|
|
133
|
+
kind,
|
|
134
|
+
width,
|
|
135
|
+
height,
|
|
136
|
+
seed,
|
|
137
|
+
detail: relief.detail,
|
|
138
|
+
signal: input.signal,
|
|
139
|
+
});
|
|
140
|
+
stage('river pixels');
|
|
141
|
+
const raster = rasterizeRivers({
|
|
142
|
+
rivers: hydrology.rivers,
|
|
143
|
+
macroWidth: macro.width,
|
|
144
|
+
macroHeight: macro.height,
|
|
145
|
+
kind,
|
|
146
|
+
width,
|
|
147
|
+
height,
|
|
148
|
+
heights: floats,
|
|
149
|
+
seed,
|
|
150
|
+
pixelCap: Math.min(RIVER_PIXEL_CAP, Math.floor(0.025 * landPixels)),
|
|
151
|
+
signal: input.signal,
|
|
152
|
+
});
|
|
153
|
+
for (const note of raster.notes)
|
|
154
|
+
notes.push(note);
|
|
155
|
+
stage('quantise');
|
|
156
|
+
// Quantise once, at the end, monotonically: land from the floor, water by the bank rule.
|
|
157
|
+
const landDistance = landDistanceField(kind, width, height);
|
|
158
|
+
const heights = new Uint8Array(total);
|
|
159
|
+
const elevation = new Float32Array(total);
|
|
160
|
+
const snow = new Uint8Array(total);
|
|
161
|
+
for (let y = 0; y < height; y += 1) {
|
|
162
|
+
if (y % 64 === 0)
|
|
163
|
+
input.signal?.throwIfAborted();
|
|
164
|
+
for (let x = 0; x < width; x += 1) {
|
|
165
|
+
const index = y * width + x;
|
|
166
|
+
if (kind[index] === LAND) {
|
|
167
|
+
let value = floats[index];
|
|
168
|
+
value = value < LAND_FLOOR ? LAND_FLOOR : value > HEIGHT_CEILING ? HEIGHT_CEILING : value;
|
|
169
|
+
floats[index] = value;
|
|
170
|
+
const quantised = Math.round(value);
|
|
171
|
+
heights[index] = quantised;
|
|
172
|
+
elevation[index] = Math.min(1, Math.max(0, (quantised - LAND_FLOOR) / 110));
|
|
173
|
+
const line = macro.snowLine[(y >> 1) * macro.width + (x >> 1)];
|
|
174
|
+
if (line < 255 && quantised >= line)
|
|
175
|
+
snow[index] = 1;
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
const water = lakeHeight(landDistance[index]);
|
|
179
|
+
floats[index] = water;
|
|
180
|
+
heights[index] = water;
|
|
181
|
+
elevation[index] = (water - 94) / 30;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
stage('normals and fields');
|
|
186
|
+
const normals = normalMap(floats, width, height, input.signal);
|
|
187
|
+
const barrier = barrierField(width, height, kind, heights, raster.bitmap, macro, input.signal);
|
|
188
|
+
const valley = {
|
|
189
|
+
width: macro.width,
|
|
190
|
+
height: macro.height,
|
|
191
|
+
scale: 2,
|
|
192
|
+
data: hydrology.valley,
|
|
193
|
+
};
|
|
194
|
+
const crestDistance = chamferDistance(macro.crest, macro.width, macro.height, 255);
|
|
195
|
+
const crestData = new Uint8Array(crestDistance.length);
|
|
196
|
+
for (let index = 0; index < crestData.length; index += 1)
|
|
197
|
+
crestData[index] = Math.min(255, Math.round(crestDistance[index] * 2));
|
|
198
|
+
const mountainData = new Uint8Array(macro.footprint.length);
|
|
199
|
+
for (let index = 0; index < mountainData.length; index += 1)
|
|
200
|
+
mountainData[index] =
|
|
201
|
+
macro.kind[index] === LAND ? Math.round(macro.footprint[index] * 255) : 0;
|
|
202
|
+
const fields = {
|
|
203
|
+
barrier,
|
|
204
|
+
valley,
|
|
205
|
+
crestDistance: { width: macro.width, height: macro.height, scale: 2, data: crestData },
|
|
206
|
+
mountain: { width: macro.width, height: macro.height, scale: 2, data: mountainData },
|
|
207
|
+
};
|
|
208
|
+
stage('checks');
|
|
209
|
+
const heightRaster = { width, height, kind, heights };
|
|
210
|
+
const statistics = reliefStatistics(heightRaster);
|
|
211
|
+
for (const note of checkRangesAndFeatures(geography, macro.ranges, heightRaster, (peak) => peakHeight(peak, macro.scale), (peak) => peakHeight(peak, macro.scale)))
|
|
212
|
+
notes.push(note);
|
|
213
|
+
if (geography.ranges.length > 0 && statistics.landPixels > 0 && statistics.p99 < 150)
|
|
214
|
+
notes.push({
|
|
215
|
+
code: 'MAP_RELIEF_FLAT',
|
|
216
|
+
severity: 'warning',
|
|
217
|
+
message: `The 99th percentile of land height is ${statistics.p99} although the model has ${geography.ranges.length} range(s); raise their peaks, or relief.scale`,
|
|
218
|
+
details: { p99: statistics.p99, ranges: geography.ranges.length },
|
|
219
|
+
});
|
|
220
|
+
// Rivers: the report, the straight-run metric and the density.
|
|
221
|
+
const entries = [];
|
|
222
|
+
const paths = [];
|
|
223
|
+
let riverCount = 0;
|
|
224
|
+
let named = 0;
|
|
225
|
+
let distributaries = 0;
|
|
226
|
+
hydrology.rivers.forEach((river, index) => {
|
|
227
|
+
const pixelRiver = raster.rivers[index];
|
|
228
|
+
const path = pixelRiver.pixels;
|
|
229
|
+
if (path.length === 0)
|
|
230
|
+
return;
|
|
231
|
+
riverCount += 1;
|
|
232
|
+
paths.push({ id: river.id, pixels: path });
|
|
233
|
+
if (river.junction === 'distributary')
|
|
234
|
+
distributaries += 1;
|
|
235
|
+
else if (river.named)
|
|
236
|
+
named += 1;
|
|
237
|
+
const first = path[0];
|
|
238
|
+
const last = path[path.length - 1];
|
|
239
|
+
const chordX = (first % width) - (last % width);
|
|
240
|
+
const chordY = Math.floor(first / width) - Math.floor(last / width);
|
|
241
|
+
const chord = Math.sqrt(chordX * chordX + chordY * chordY);
|
|
242
|
+
let widest = 3;
|
|
243
|
+
for (const value of river.width)
|
|
244
|
+
if (value > widest)
|
|
245
|
+
widest = value;
|
|
246
|
+
const parentId = river.parent >= 0 ? hydrology.rivers[river.parent].id : undefined;
|
|
247
|
+
entries.push({
|
|
248
|
+
id: river.id,
|
|
249
|
+
...(river.name === undefined ? {} : { name: river.name }),
|
|
250
|
+
named: river.named,
|
|
251
|
+
pixels: path.length,
|
|
252
|
+
sinuosity: path.length <= 1 ? 1 : Math.round((chord / (path.length - 1)) * 1000) / 1000,
|
|
253
|
+
mouth: river.endsIn,
|
|
254
|
+
...(parentId === undefined ? {} : { parent: parentId }),
|
|
255
|
+
junction: river.junction,
|
|
256
|
+
width: widest,
|
|
257
|
+
order: river.order,
|
|
258
|
+
complete: pixelRiver.complete,
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
const straight = straightRiverShare(raster.bitmap, width, height);
|
|
262
|
+
const straightShare = straight.river === 0 ? 0 : straight.straight / straight.river;
|
|
263
|
+
if (straight.river > 400 && straightShare > 0.03)
|
|
264
|
+
notes.push({
|
|
265
|
+
code: 'MAP_RIVER_STRAIGHT_RUN',
|
|
266
|
+
severity: 'warning',
|
|
267
|
+
message: `${(straightShare * 100).toFixed(1)}% of river pixels lie on straight stretches of 41 pixels or more (the base game: 1.1%); the ground there is flat, so the relief gives the rivers nothing to bend around`,
|
|
268
|
+
details: { riverPixels: straight.river, straightPixels: straight.straight },
|
|
269
|
+
});
|
|
270
|
+
const share = landPixels === 0 ? 0 : raster.pixels / landPixels;
|
|
271
|
+
if (landPixels > 20_000 &&
|
|
272
|
+
(share > 0.03 || (input.rivers?.density === undefined && share < 0.01)))
|
|
273
|
+
notes.push({
|
|
274
|
+
code: 'MAP_RIVER_DENSITY',
|
|
275
|
+
severity: 'warning',
|
|
276
|
+
message: `Rivers cover ${(share * 100).toFixed(2)}% of the land (${raster.pixels} pixels); the base game has 2.3% and long networks slow the game`,
|
|
277
|
+
details: { pixels: raster.pixels, landShare: Math.round(share * 10_000) / 10_000 },
|
|
278
|
+
});
|
|
279
|
+
const riverStatistics = {
|
|
280
|
+
pixels: raster.pixels,
|
|
281
|
+
landShare: share,
|
|
282
|
+
count: riverCount,
|
|
283
|
+
named,
|
|
284
|
+
distributaries,
|
|
285
|
+
streams: riverCount - named - distributaries,
|
|
286
|
+
straightShare: Math.round(straightShare * 10_000) / 10_000,
|
|
287
|
+
};
|
|
288
|
+
return {
|
|
289
|
+
width,
|
|
290
|
+
height,
|
|
291
|
+
heights,
|
|
292
|
+
normals,
|
|
293
|
+
riverBitmap: raster.bitmap,
|
|
294
|
+
riverOrder: raster.order,
|
|
295
|
+
elevation,
|
|
296
|
+
snow,
|
|
297
|
+
fields,
|
|
298
|
+
rivers: entries,
|
|
299
|
+
riverPaths: paths,
|
|
300
|
+
relief: statistics,
|
|
301
|
+
riverStatistics,
|
|
302
|
+
notes,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
@@ -2,6 +2,8 @@ import sharp from 'sharp';
|
|
|
2
2
|
import { compareCodeUnits, canonicalJson, sha256Bytes } from '../core/canonical.js';
|
|
3
3
|
import { assertRenderDimensions, RenderBudget, RENDER_MAX_PIXELS } from '../core/render-budget.js';
|
|
4
4
|
import { ServiceError } from '../core/result.js';
|
|
5
|
+
import { borderQualityInputFromIndex } from './border-quality-input.js';
|
|
6
|
+
import { borderEdgePixels, forEachBorderEdge, measureBorderNaturalness } from './border-quality.js';
|
|
5
7
|
import { buildMapCatalog } from './catalog.js';
|
|
6
8
|
import { validateMapAsync } from './validation.js';
|
|
7
9
|
function stableColor(key) {
|
|
@@ -155,10 +157,44 @@ function mapCoordinateToPixel(index, x, z) {
|
|
|
155
157
|
? undefined
|
|
156
158
|
: pixel;
|
|
157
159
|
}
|
|
160
|
+
const borderRunColors = {
|
|
161
|
+
province: { r: 255, g: 255, b: 255 },
|
|
162
|
+
state: { r: 255, g: 64, b: 64 },
|
|
163
|
+
region: { r: 255, g: 224, b: 64 },
|
|
164
|
+
country: { r: 255, g: 64, b: 255 },
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
167
|
+
* Border naturalness: the state borders (the province borders when there are no states) in green
|
|
168
|
+
* where they follow a river, slope, lake shore or terrain change and in orange where they do not,
|
|
169
|
+
* and the worst artificial run of every layer drawn two pixels wide in the layer's colour.
|
|
170
|
+
*/
|
|
171
|
+
async function paintBorderQuality(index, bytes, signal) {
|
|
172
|
+
const raster = index.raster;
|
|
173
|
+
const { input } = borderQualityInputFromIndex(index);
|
|
174
|
+
const measured = await measureBorderNaturalness(input, {
|
|
175
|
+
collectPaint: true,
|
|
176
|
+
...(signal === undefined ? {} : { signal }),
|
|
177
|
+
});
|
|
178
|
+
const surface = input.layers.find(({ name }) => name === 'state') ?? input.layers[0];
|
|
179
|
+
if (surface !== undefined)
|
|
180
|
+
forEachBorderEdge(measured.table, surface.labels, (key, natural) => {
|
|
181
|
+
const [pixel] = borderEdgePixels(raster.width, key);
|
|
182
|
+
setPixel(bytes, raster.width, raster.height, pixel % raster.width, Math.floor(pixel / raster.width), natural ? { r: 86, g: 214, b: 126 } : { r: 226, g: 128, b: 48 });
|
|
183
|
+
});
|
|
184
|
+
for (const layer of input.layers) {
|
|
185
|
+
const color = borderRunColors[layer.name] ?? { r: 255, g: 255, b: 255 };
|
|
186
|
+
for (const keys of measured.paint?.get(layer.name) ?? [])
|
|
187
|
+
for (const key of keys)
|
|
188
|
+
for (const pixel of borderEdgePixels(raster.width, key))
|
|
189
|
+
setPixel(bytes, raster.width, raster.height, pixel % raster.width, Math.floor(pixel / raster.width), color);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
158
192
|
async function applyOverlays(index, bytes, overlays, signal) {
|
|
159
193
|
const raster = index.raster;
|
|
160
194
|
if (raster === undefined)
|
|
161
195
|
return;
|
|
196
|
+
if (overlays.has('border-quality'))
|
|
197
|
+
await paintBorderQuality(index, bytes, signal);
|
|
162
198
|
if (overlays.has('coastlines')) {
|
|
163
199
|
for (let y = 0; y < raster.height; y += 1) {
|
|
164
200
|
if (y % 32 === 0)
|