hoi4-agent-tools 4.2.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 +30 -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/domain-validation.js +6 -1
- package/dist/hoi4_agent_tools/core/engine.js +3 -0
- package/dist/hoi4_agent_tools/core/game-documentation.js +24 -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/decision/service.js +1 -22
- package/dist/hoi4_agent_tools/event/graph.js +8 -1
- package/dist/hoi4_agent_tools/event/service.js +8 -0
- package/dist/hoi4_agent_tools/event/source-analysis.js +10 -6
- package/dist/hoi4_agent_tools/focus/importer.js +8 -6
- package/dist/hoi4_agent_tools/focus/native-effects.js +3 -1
- package/dist/hoi4_agent_tools/focus/tool-support.js +10 -1
- 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/probability/service.js +18 -18
- 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/probability.js +7 -1
- package/dist/hoi4_agent_tools/schemas/reference-sheets.js +92 -0
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/analysis.md +1 -1
- package/docs/configuration.md +17 -14
- package/docs/map.md +258 -15
- package/docs/probability.md +1 -1
- 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 +31 -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
|
@@ -9,9 +9,19 @@ import { ServiceError } from '../core/result.js';
|
|
|
9
9
|
import { createBmp } from './bmp.js';
|
|
10
10
|
import { generateWorldDrainage } from './world-drainage.js';
|
|
11
11
|
import { generateWorldProvinces } from './world-provinces.js';
|
|
12
|
+
import { clusterRegions, clusterStates, partitionWorldFromRequest, } from './partition.js';
|
|
13
|
+
import { repairSeamCorners, smoothDiagonalContacts } from './partition-fields.js';
|
|
14
|
+
import { PARTITION_PROVINCE_ERROR, PARTITION_PROVINCE_WARNING } from './partition-tuning.js';
|
|
15
|
+
import { geographyPartitionFields, provinceCountNote, surfacePartitionFields, } from './world-partition.js';
|
|
16
|
+
import { fractal, lattice } from './world-noise.js';
|
|
17
|
+
import { calibratedColourMap, COLOUR_CLASSES, } from './colormap.js';
|
|
18
|
+
import { ambientObjectText, applyEdgePolicy, assertFeaturesOutsideBand, edgeWaterDarkness, RIM_COLOUR_FACTOR, rimAdjacencyRows, rimHeight, rimStrength, } from './world-edge.js';
|
|
19
|
+
import { landDistanceField, lakeHeight, normalizeLakes, waterColourLevel } from './water.js';
|
|
20
|
+
import { overlayGeographyLakes, rasterizeGeographyKind } from './relief/land.js';
|
|
21
|
+
import { synthesizeTerrain } from './relief/terrain-synthesis.js';
|
|
12
22
|
// ---------------------------------------------------------------------------------------------
|
|
13
23
|
// Deterministic randomness and noise
|
|
14
|
-
class Random {
|
|
24
|
+
export class Random {
|
|
15
25
|
#state;
|
|
16
26
|
constructor(seed) {
|
|
17
27
|
this.#state = (seed ^ 0x9e3779b9) >>> 0;
|
|
@@ -27,38 +37,6 @@ class Random {
|
|
|
27
37
|
return Math.floor(this.next() * limit);
|
|
28
38
|
}
|
|
29
39
|
}
|
|
30
|
-
function lattice(seed, x, y) {
|
|
31
|
-
let hash = Math.imul(x, 374761393) + Math.imul(y, 668265263) + Math.imul(seed, 982451653);
|
|
32
|
-
hash = Math.imul(hash ^ (hash >>> 13), 1274126177);
|
|
33
|
-
return ((hash ^ (hash >>> 16)) >>> 0) / 4294967296;
|
|
34
|
-
}
|
|
35
|
-
function smooth(t) {
|
|
36
|
-
return t * t * (3 - 2 * t);
|
|
37
|
-
}
|
|
38
|
-
function valueNoise(seed, x, y) {
|
|
39
|
-
const x0 = Math.floor(x);
|
|
40
|
-
const y0 = Math.floor(y);
|
|
41
|
-
const tx = smooth(x - x0);
|
|
42
|
-
const ty = smooth(y - y0);
|
|
43
|
-
const a = lattice(seed, x0, y0);
|
|
44
|
-
const b = lattice(seed, x0 + 1, y0);
|
|
45
|
-
const c = lattice(seed, x0, y0 + 1);
|
|
46
|
-
const d = lattice(seed, x0 + 1, y0 + 1);
|
|
47
|
-
return a + (b - a) * tx + (c - a) * ty + (a - b - c + d) * tx * ty;
|
|
48
|
-
}
|
|
49
|
-
function fractal(seed, x, y, scale, octaves = 5) {
|
|
50
|
-
let total = 0;
|
|
51
|
-
let amplitude = 1;
|
|
52
|
-
let weight = 0;
|
|
53
|
-
let frequency = 1 / scale;
|
|
54
|
-
for (let octave = 0; octave < octaves; octave += 1) {
|
|
55
|
-
total += valueNoise(seed + octave * 101, x * frequency, y * frequency) * amplitude;
|
|
56
|
-
weight += amplitude;
|
|
57
|
-
amplitude *= 0.5;
|
|
58
|
-
frequency *= 2;
|
|
59
|
-
}
|
|
60
|
-
return total / weight;
|
|
61
|
-
}
|
|
62
40
|
// ---------------------------------------------------------------------------------------------
|
|
63
41
|
// Raster helpers
|
|
64
42
|
const LAND = 1;
|
|
@@ -204,10 +182,99 @@ function generateSurface(spec, random) {
|
|
|
204
182
|
}
|
|
205
183
|
return { kind, elevation, moisture };
|
|
206
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Surface of a world described by a geography model: land and lakes from the model (or lakes laid
|
|
187
|
+
* over a reference picture's coast); elevation is filled in by the terrain synthesis later.
|
|
188
|
+
*/
|
|
189
|
+
/** Province count the vanilla map holds; defaulted sizes grow towards it. */
|
|
190
|
+
const PROVINCE_COUNT_GOAL = 13_000;
|
|
191
|
+
/**
|
|
192
|
+
* Check the province count the sizes give before partitioning. Defaulted sizes above the warning
|
|
193
|
+
* grow until the world holds about the base game's count; sizes the caller chose that give more
|
|
194
|
+
* than the text tables allow are refused before any work.
|
|
195
|
+
*/
|
|
196
|
+
function provinceBudget(spec, kind) {
|
|
197
|
+
let land = 0;
|
|
198
|
+
let water = 0;
|
|
199
|
+
for (const value of kind)
|
|
200
|
+
if (value === LAND)
|
|
201
|
+
land += 1;
|
|
202
|
+
else
|
|
203
|
+
water += 1;
|
|
204
|
+
const expected = land / spec.landProvincePixels + water / spec.seaProvincePixels;
|
|
205
|
+
const sizes = {
|
|
206
|
+
landProvincePixels: spec.landProvincePixels,
|
|
207
|
+
seaProvincePixels: spec.seaProvincePixels,
|
|
208
|
+
};
|
|
209
|
+
if (expected <= PARTITION_PROVINCE_WARNING)
|
|
210
|
+
return sizes;
|
|
211
|
+
if (spec.provinceSizesDefaulted === true) {
|
|
212
|
+
const factor = expected / PROVINCE_COUNT_GOAL;
|
|
213
|
+
const landProvincePixels = Math.ceil(spec.landProvincePixels * factor);
|
|
214
|
+
const seaProvincePixels = Math.ceil(spec.seaProvincePixels * factor);
|
|
215
|
+
return {
|
|
216
|
+
landProvincePixels,
|
|
217
|
+
seaProvincePixels,
|
|
218
|
+
note: {
|
|
219
|
+
code: 'MAP_WORLD_PROVINCE_SIZE_RAISED',
|
|
220
|
+
severity: 'info',
|
|
221
|
+
message: `The default province sizes would give about ${Math.round(expected)} provinces on this much land; they were raised to ${landProvincePixels} land and ${seaProvincePixels} sea pixels for about ${PROVINCE_COUNT_GOAL}. Set landProvincePixels and seaProvincePixels to choose them`,
|
|
222
|
+
details: { expectedProvinces: Math.round(expected), landProvincePixels, seaProvincePixels },
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
if (expected > PARTITION_PROVINCE_ERROR)
|
|
227
|
+
throw new ServiceError('MAP_PARTITION_PROVINCE_COUNT_HIGH', `landProvincePixels ${spec.landProvincePixels} and seaProvincePixels ${spec.seaProvincePixels} give about ${Math.round(expected)} provinces on this map, above the ${PARTITION_PROVINCE_ERROR} the text tables allow; raise them`, { expectedProvinces: Math.round(expected), maximum: PARTITION_PROVINCE_ERROR });
|
|
228
|
+
return sizes;
|
|
229
|
+
}
|
|
230
|
+
function geographySurface(spec, geography, reference) {
|
|
231
|
+
const { width, height } = spec;
|
|
232
|
+
const total = width * height;
|
|
233
|
+
const options = { width, height, seed: spec.seed };
|
|
234
|
+
let kind;
|
|
235
|
+
let moisture;
|
|
236
|
+
if (reference === undefined) {
|
|
237
|
+
kind = rasterizeGeographyKind(geography, options);
|
|
238
|
+
moisture = new Float32Array(total);
|
|
239
|
+
const scale = Math.max(width, height) / 6;
|
|
240
|
+
for (let y = 0; y < height; y += 1)
|
|
241
|
+
for (let x = 0; x < width; x += 1)
|
|
242
|
+
moisture[y * width + x] = fractal(spec.seed + 7919, x, y, scale * 0.8, 4);
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
kind = new Uint8Array(reference.kind);
|
|
246
|
+
overlayGeographyLakes(kind, geography, options);
|
|
247
|
+
moisture = new Float32Array(reference.moisture);
|
|
248
|
+
}
|
|
249
|
+
return { kind, elevation: new Float32Array(total), moisture };
|
|
250
|
+
}
|
|
251
|
+
function terrainSummaryNote(terrain, geography) {
|
|
252
|
+
const { relief, riverStatistics } = terrain;
|
|
253
|
+
return {
|
|
254
|
+
code: 'MAP_RELIEF_SYNTHESIZED',
|
|
255
|
+
severity: 'info',
|
|
256
|
+
message: `Synthesised relief and rivers from the geography model: land height median ${relief.median}, 99th percentile ${relief.p99}, maximum ${relief.maximum}, mean step ${relief.meanStep.toFixed(2)} per half-resolution pixel; ${riverStatistics.count} rivers (${riverStatistics.named} named) on ${(riverStatistics.landShare * 100).toFixed(2)}% of the land`,
|
|
257
|
+
details: {
|
|
258
|
+
modelHash: geography.hash,
|
|
259
|
+
relief,
|
|
260
|
+
rivers: riverStatistics,
|
|
261
|
+
// The first of the named rivers only: the response budget is shared with everything else the
|
|
262
|
+
// world reports, and a river that did not complete is always listed.
|
|
263
|
+
namedRivers: terrain.rivers
|
|
264
|
+
.filter((river) => river.named && river.junction !== 'distributary')
|
|
265
|
+
.slice(0, 24)
|
|
266
|
+
.map(({ id, pixels, mouth, complete }) => ({ id, pixels, mouth, complete })),
|
|
267
|
+
incompleteRivers: terrain.rivers
|
|
268
|
+
.filter((river) => river.named && !river.complete)
|
|
269
|
+
.slice(0, 24)
|
|
270
|
+
.map(({ id }) => id),
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
}
|
|
207
274
|
// ---------------------------------------------------------------------------------------------
|
|
208
275
|
// Grouping provinces into regions and states
|
|
209
276
|
/** Grow clusters of about `target` members over a graph from spread-out seeds. */
|
|
210
|
-
function growClusters(members, neighbors, target, random, weight = () => 1) {
|
|
277
|
+
export function growClusters(members, neighbors, target, random, weight = () => 1) {
|
|
211
278
|
const cluster = new Map();
|
|
212
279
|
const memberSet = new Set(members);
|
|
213
280
|
const pending = new Set(members);
|
|
@@ -324,10 +391,23 @@ const DEFAULT_TERRAIN_INDEX = new Map([
|
|
|
324
391
|
['mountain', 6],
|
|
325
392
|
['marsh', 9],
|
|
326
393
|
['urban', 13],
|
|
327
|
-
['lakes',
|
|
394
|
+
['lakes', 15],
|
|
328
395
|
['ocean', 15],
|
|
329
396
|
['jungle', 21],
|
|
330
397
|
]);
|
|
398
|
+
/**
|
|
399
|
+
* A graphical terrain index of the base game's numbering (farmland 5, sparse forest 4, mountain
|
|
400
|
+
* with grass 20), used only when the active terrain files confirm that index still has that
|
|
401
|
+
* category and draws a tile. Without any terrain files the base game's numbering is assumed.
|
|
402
|
+
*/
|
|
403
|
+
function terrainVariant(context, index, type) {
|
|
404
|
+
const entries = context.terrainEntries;
|
|
405
|
+
if (entries === undefined)
|
|
406
|
+
return context.terrainIndex === undefined ? index : undefined;
|
|
407
|
+
return entries.some((entry) => entry.index === index && entry.type === type && entry.texture !== 255 && !entry.permSnow)
|
|
408
|
+
? index
|
|
409
|
+
: undefined;
|
|
410
|
+
}
|
|
331
411
|
/** Approximate seasonal weather: signed latitude is positive north of the equator. */
|
|
332
412
|
function weatherBlock(signedLatitude, desert) {
|
|
333
413
|
const latitude = Math.abs(signedLatitude);
|
|
@@ -358,7 +438,7 @@ function weatherBlock(signedLatitude, desert) {
|
|
|
358
438
|
}
|
|
359
439
|
return `\tweather={\n${periods.join('\n')}\n\t}`;
|
|
360
440
|
}
|
|
361
|
-
export function generateWorld(spec, context) {
|
|
441
|
+
export function generateWorld(spec, context, options = {}) {
|
|
362
442
|
const { width, height } = spec;
|
|
363
443
|
if (width % 256 !== 0 || height % 256 !== 0)
|
|
364
444
|
throw new ServiceError('MAP_WORLD_DIMENSIONS_INVALID', 'Both map dimensions must be multiples of 256', { width, height });
|
|
@@ -382,34 +462,171 @@ export function generateWorld(spec, context) {
|
|
|
382
462
|
const reference = spec.referenceSurface;
|
|
383
463
|
if (reference !== undefined && (reference.width !== width || reference.height !== height))
|
|
384
464
|
throw new ServiceError('MAP_WORLD_REFERENCE_DIMENSIONS', 'Reference raster dimensions do not match the world');
|
|
385
|
-
const
|
|
386
|
-
|
|
465
|
+
const geography = spec.geography;
|
|
466
|
+
if (geography !== undefined &&
|
|
467
|
+
(geography.frame.width !== width || geography.frame.height !== height))
|
|
468
|
+
throw new ServiceError('MAP_GEOGRAPHY_FRAME_MISMATCH', 'The geography model was resolved for a different map size than the world being created', {
|
|
469
|
+
model: { width: geography.frame.width, height: geography.frame.height },
|
|
470
|
+
world: { width, height },
|
|
471
|
+
});
|
|
472
|
+
const surface = geography !== undefined
|
|
473
|
+
? geographySurface(spec, geography, reference)
|
|
474
|
+
: reference === undefined
|
|
475
|
+
? generateSurface(spec, random)
|
|
476
|
+
: {
|
|
477
|
+
kind: new Uint8Array(reference.kind),
|
|
478
|
+
elevation: new Float32Array(reference.elevation),
|
|
479
|
+
moisture: new Float32Array(reference.moisture),
|
|
480
|
+
};
|
|
481
|
+
const notes = [];
|
|
482
|
+
const edge = spec.edge ?? { mode: 'none', bandPx: 0, polarPx: 0, rimPx: 0 };
|
|
483
|
+
if (edge.mode !== 'none') {
|
|
484
|
+
assertFeaturesOutsideBand(edge, width, height, (spec.referenceCities ?? []).map(({ name, x, y }) => ({ name, x, y, kind: 'city' })));
|
|
485
|
+
const applied = applyEdgePolicy(surface.kind, surface.elevation, width, height, edge, spec.seed, Math.max(40, Math.round(spec.landProvincePixels / 4)));
|
|
486
|
+
notes.push(...applied.notes.map((note) => note));
|
|
487
|
+
}
|
|
488
|
+
// Lakes need a minimum size and may not touch the open sea (ADR 0053).
|
|
489
|
+
for (const note of normalizeLakes(surface.kind, surface.elevation, width, height))
|
|
490
|
+
notes.push({
|
|
491
|
+
code: note.code,
|
|
492
|
+
severity: 'warning',
|
|
493
|
+
message: `${note.message} near (${note.x}, ${note.y})`,
|
|
494
|
+
details: { pixels: note.pixels, x: note.x, y: note.y },
|
|
495
|
+
});
|
|
496
|
+
// A coast that touches itself only at corners cannot be cut into valid provinces, so the
|
|
497
|
+
// feature-aware partition smooths it when it builds its own fields. A geography world smooths
|
|
498
|
+
// before the synthesis, so its heights, rivers and fields follow the smoothed mask; any other
|
|
499
|
+
// world smooths after its drainage and patches those rasters, as it always has.
|
|
500
|
+
const smoothMask = spec.partition?.mode === 'feature-aware' && spec.partition.fields === undefined;
|
|
501
|
+
let smoothed = smoothMask && geography !== undefined
|
|
502
|
+
? smoothDiagonalContacts(surface.kind, width, height)
|
|
503
|
+
: [];
|
|
504
|
+
// A coast crossing the east-west seam is repaired in every world, before the synthesis of a
|
|
505
|
+
// geography world and after the drainage of any other, like the smoothing.
|
|
506
|
+
let seamRepaired = geography !== undefined ? repairSeamCorners(surface.kind, width, height) : [];
|
|
507
|
+
// A geography model gets its relief and rivers from the synthesis of ADR 0056; every other world
|
|
508
|
+
// keeps the original elevation classes and drainage, byte for byte.
|
|
509
|
+
const synthesized = geography === undefined
|
|
510
|
+
? undefined
|
|
511
|
+
: synthesizeTerrain({
|
|
512
|
+
geography,
|
|
513
|
+
width,
|
|
514
|
+
height,
|
|
515
|
+
seed: spec.seed,
|
|
516
|
+
kind: surface.kind,
|
|
517
|
+
relief: spec.relief,
|
|
518
|
+
rivers: { density: spec.riverDensity, count: spec.riverCount },
|
|
519
|
+
adjustHeight: edge.mode === 'rim'
|
|
520
|
+
? (x, _y, value) => rimHeight(value, rimStrength(edge, width, x))
|
|
521
|
+
: undefined,
|
|
522
|
+
signal: options.signal,
|
|
523
|
+
});
|
|
524
|
+
if (synthesized !== undefined) {
|
|
525
|
+
for (const note of synthesized.notes) {
|
|
526
|
+
if (note.severity === 'error')
|
|
527
|
+
throw new ServiceError(note.code, note.message, note.details);
|
|
528
|
+
notes.push({
|
|
529
|
+
code: note.code,
|
|
530
|
+
severity: note.severity,
|
|
531
|
+
message: note.message,
|
|
532
|
+
...(note.details === undefined ? {} : { details: note.details }),
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
notes.push(terrainSummaryNote(synthesized, geography));
|
|
536
|
+
}
|
|
537
|
+
const drainage = synthesized === undefined
|
|
538
|
+
? generateWorldDrainage({
|
|
539
|
+
width,
|
|
540
|
+
height,
|
|
541
|
+
kind: surface.kind,
|
|
542
|
+
elevation: surface.elevation,
|
|
543
|
+
seed: spec.seed,
|
|
544
|
+
count: spec.rivers,
|
|
545
|
+
...(reference === undefined ? {} : { referenceRivers: reference.rivers }),
|
|
546
|
+
})
|
|
387
547
|
: {
|
|
388
|
-
|
|
389
|
-
elevation:
|
|
390
|
-
|
|
548
|
+
riverPixels: synthesized.riverBitmap,
|
|
549
|
+
elevation: synthesized.elevation,
|
|
550
|
+
count: synthesized.riverStatistics.count,
|
|
551
|
+
warnings: [],
|
|
391
552
|
};
|
|
392
|
-
const drainage = generateWorldDrainage({
|
|
393
|
-
width,
|
|
394
|
-
height,
|
|
395
|
-
kind: surface.kind,
|
|
396
|
-
elevation: surface.elevation,
|
|
397
|
-
seed: spec.seed,
|
|
398
|
-
count: spec.rivers,
|
|
399
|
-
...(reference === undefined ? {} : { referenceRivers: reference.rivers }),
|
|
400
|
-
});
|
|
401
553
|
surface.elevation = drainage.elevation;
|
|
402
|
-
|
|
403
|
-
width,
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
554
|
+
if (smoothMask && geography === undefined) {
|
|
555
|
+
smoothed = smoothDiagonalContacts(surface.kind, width, height);
|
|
556
|
+
for (const pixel of smoothed) {
|
|
557
|
+
const isLand = surface.kind[pixel] === LAND;
|
|
558
|
+
surface.elevation[pixel] = isLand ? 0.02 : -0.02;
|
|
559
|
+
drainage.riverPixels[pixel] = isLand ? 255 : 254;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
if (geography === undefined) {
|
|
563
|
+
seamRepaired = repairSeamCorners(surface.kind, width, height);
|
|
564
|
+
for (const pixel of seamRepaired) {
|
|
565
|
+
const isLand = surface.kind[pixel] === LAND;
|
|
566
|
+
surface.elevation[pixel] = isLand ? 0.02 : -0.02;
|
|
567
|
+
drainage.riverPixels[pixel] = isLand ? 255 : 254;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if (seamRepaired.length > 0)
|
|
571
|
+
notes.push({
|
|
572
|
+
code: 'MAP_WORLD_SEAM_CORNER_REPAIRED',
|
|
573
|
+
severity: 'info',
|
|
574
|
+
message: `Changed ${seamRepaired.length} pixel(s) at the east-west seam where a coast crosses it, because province parts cannot join across the seam`,
|
|
575
|
+
details: { pixels: seamRepaired.length },
|
|
576
|
+
});
|
|
577
|
+
if (smoothed.length > 0)
|
|
578
|
+
notes.push({
|
|
579
|
+
code: 'MAP_PARTITION_MASK_SMOOTHED',
|
|
580
|
+
severity: 'info',
|
|
581
|
+
message: `Changed ${smoothed.length} coast pixel(s) that touched their own kind only at a corner, which no province partition can represent`,
|
|
582
|
+
details: { pixels: smoothed.length },
|
|
583
|
+
});
|
|
584
|
+
const sizes = provinceBudget(spec, surface.kind);
|
|
585
|
+
if (sizes.note !== undefined)
|
|
586
|
+
notes.push(sizes.note);
|
|
587
|
+
let featurePartition;
|
|
588
|
+
let provinces;
|
|
589
|
+
if (spec.partition?.mode === 'feature-aware') {
|
|
590
|
+
const surfaceInput = {
|
|
591
|
+
width,
|
|
592
|
+
height,
|
|
593
|
+
kind: surface.kind,
|
|
594
|
+
elevation: surface.elevation,
|
|
595
|
+
moisture: surface.moisture,
|
|
596
|
+
riverPixels: drainage.riverPixels,
|
|
597
|
+
...(reference === undefined ? {} : { referenceTerrain: reference.terrain }),
|
|
598
|
+
};
|
|
599
|
+
// A geography world hands the partition its regions, densities and walls, the synthesised
|
|
600
|
+
// heights and the model-drawn lines of the synthesis (ADR 0058).
|
|
601
|
+
featurePartition = partitionWorldFromRequest(spec.partition.fields ??
|
|
602
|
+
(synthesized === undefined || geography === undefined
|
|
603
|
+
? surfacePartitionFields(surfaceInput)
|
|
604
|
+
: geographyPartitionFields({ ...surfaceInput, heights: synthesized.heights }, geography, synthesized.fields)), spec.seed, spec.partition.tuning, {
|
|
605
|
+
landProvincePixels: sizes.landProvincePixels,
|
|
606
|
+
seaProvincePixels: sizes.seaProvincePixels,
|
|
607
|
+
provincesPerState: spec.provincesPerState,
|
|
608
|
+
landProvincesPerRegion: spec.landProvincesPerRegion,
|
|
609
|
+
seaProvincesPerRegion: spec.seaProvincesPerRegion,
|
|
610
|
+
}, options.signal);
|
|
611
|
+
provinces = featurePartition.provinces;
|
|
612
|
+
for (const note of featurePartition.notes)
|
|
613
|
+
notes.push(note);
|
|
614
|
+
}
|
|
615
|
+
else
|
|
616
|
+
provinces = generateWorldProvinces({
|
|
617
|
+
width,
|
|
618
|
+
height,
|
|
619
|
+
seed: spec.seed,
|
|
620
|
+
kind: surface.kind,
|
|
621
|
+
elevation: surface.elevation,
|
|
622
|
+
moisture: surface.moisture,
|
|
623
|
+
landProvincePixels: sizes.landProvincePixels,
|
|
624
|
+
seaProvincePixels: sizes.seaProvincePixels,
|
|
625
|
+
});
|
|
412
626
|
const count = provinces.kind.length;
|
|
627
|
+
const countNote = provinceCountNote(count, featurePartition === undefined ? 'legacy' : 'feature-aware');
|
|
628
|
+
if (countNote !== undefined)
|
|
629
|
+
notes.push(countNote);
|
|
413
630
|
const center = (id) => ({
|
|
414
631
|
x: provinces.sumX[id] / provinces.size[id],
|
|
415
632
|
y: provinces.sumY[id] / provinces.size[id],
|
|
@@ -704,8 +921,12 @@ export function generateWorld(spec, context) {
|
|
|
704
921
|
}
|
|
705
922
|
// Strategic regions: land regions over the land graph, sea regions over the sea graph.
|
|
706
923
|
const graph = (id) => provinces.neighbors[id];
|
|
707
|
-
const landRegion =
|
|
708
|
-
|
|
924
|
+
const landRegion = featurePartition === undefined
|
|
925
|
+
? growClusters(landIds, graph, spec.landProvincesPerRegion, random)
|
|
926
|
+
: clusterRegions(featurePartition, landIds, 'land');
|
|
927
|
+
const seaRegion = featurePartition === undefined
|
|
928
|
+
? growClusters(seaIds, graph, spec.seaProvincesPerRegion, random)
|
|
929
|
+
: clusterRegions(featurePartition, seaIds, 'sea');
|
|
709
930
|
const regionOf = new Map();
|
|
710
931
|
const landRegionCount = Math.max(-1, ...landRegion.values()) + 1;
|
|
711
932
|
for (const [id, region] of landRegion)
|
|
@@ -734,7 +955,9 @@ export function generateWorld(spec, context) {
|
|
|
734
955
|
const stateOf = new Map();
|
|
735
956
|
for (const key of [...groups.keys()].sort()) {
|
|
736
957
|
const members = groups.get(key);
|
|
737
|
-
const clusters =
|
|
958
|
+
const clusters = featurePartition === undefined
|
|
959
|
+
? growClusters(members, graph, spec.provincesPerState, random)
|
|
960
|
+
: clusterStates(featurePartition, members);
|
|
738
961
|
const byCluster = new Map();
|
|
739
962
|
for (const [id, cluster] of clusters) {
|
|
740
963
|
const list = byCluster.get(cluster) ?? [];
|
|
@@ -951,10 +1174,27 @@ export function generateWorld(spec, context) {
|
|
|
951
1174
|
'',
|
|
952
1175
|
].join('\n')));
|
|
953
1176
|
add(`${map}/continent.txt`, text(`continents = {\n${[...new Set([...continentNames.map((_, index) => `world_continent_${index + 1}`), ...(context.continentKeys ?? [])])].map((key) => `\t${key}`).join('\n')}\n}\n`));
|
|
954
|
-
|
|
1177
|
+
const rimRows = edge.mode === 'rim'
|
|
1178
|
+
? rimAdjacencyRows({
|
|
1179
|
+
provincePixels: provinces.pixel,
|
|
1180
|
+
provinceKinds: provinces.kind,
|
|
1181
|
+
width,
|
|
1182
|
+
height,
|
|
1183
|
+
})
|
|
1184
|
+
: [];
|
|
1185
|
+
if (rimRows.length > 0)
|
|
1186
|
+
notes.push({
|
|
1187
|
+
code: 'MAP_EDGE_RIM_ADJACENCIES',
|
|
1188
|
+
severity: 'info',
|
|
1189
|
+
message: `The rim edge policy wrote ${rimRows.length} impassable adjacency row(s) for provinces that meet across the seam`,
|
|
1190
|
+
details: { rows: rimRows.length },
|
|
1191
|
+
});
|
|
1192
|
+
add(`${map}/adjacencies.csv`, text(`From;To;Type;Through;start_x;start_y;stop_x;stop_y;adjacency_rule_name;Comment\r\n${rimRows.map((row) => `${row}\r\n`).join('')}-1;-1;;-1;-1;-1;-1;-1;-1\r\n`));
|
|
955
1193
|
add(`${map}/adjacency_rules.txt`, text('# No straits or canals in this world yet.\n'));
|
|
956
1194
|
add(`${map}/positions.txt`, text(''));
|
|
957
|
-
add(`${map}/ambient_object.txt`, text(
|
|
1195
|
+
add(`${map}/ambient_object.txt`, text(spec.ambientFrame === true
|
|
1196
|
+
? ambientObjectText(width, height)
|
|
1197
|
+
: '# No ambient objects: the base-game map frame does not fit this map.\n'));
|
|
958
1198
|
// City groups use palette 15 only for urban footprints. Water and undeveloped land stay 0.
|
|
959
1199
|
const cityPixels = new Uint8Array(width * height);
|
|
960
1200
|
const nearestCenter = new Int32Array(count).fill(-1);
|
|
@@ -996,33 +1236,90 @@ export function generateWorld(spec, context) {
|
|
|
996
1236
|
const type = terrain.get(provinces.pixel[pixel]) ?? 'plains';
|
|
997
1237
|
return type === 'urban' ? 'plains' : type;
|
|
998
1238
|
};
|
|
999
|
-
// heightmap.bmp: sea below 95, land from 96 up.
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1239
|
+
// heightmap.bmp: sea below 95, land from 96 up. Lakes sit at the sea's interior height with a
|
|
1240
|
+
// one to two pixel translucent bank, so the water layer draws them opaque (ADR 0053).
|
|
1241
|
+
const landDistance = landDistanceField(surface.kind, width, height);
|
|
1242
|
+
const heights = synthesized?.heights ?? new Uint8Array(width * height);
|
|
1243
|
+
if (synthesized === undefined)
|
|
1244
|
+
for (let pixel = 0; pixel < width * height; pixel += 1) {
|
|
1245
|
+
const value = surface.elevation[pixel];
|
|
1246
|
+
heights[pixel] =
|
|
1247
|
+
surface.kind[pixel] === LAND
|
|
1248
|
+
? rimHeight(Math.min(255, Math.round(96 + value * 110)), rimStrength(edge, width, pixel % width))
|
|
1249
|
+
: surface.kind[pixel] === LAKE
|
|
1250
|
+
? lakeHeight(landDistance[pixel])
|
|
1251
|
+
: Math.max(60, Math.round(94 + value * 30));
|
|
1252
|
+
}
|
|
1010
1253
|
const grey = Array.from({ length: 256 }, (_, index) => ({ r: index, g: index, b: index }));
|
|
1011
1254
|
add(`${map}/heightmap.bmp`, createBmp({ width, height, bitsPerPixel: 8, palette: grey, indexedPixels: heights }), 'image/bmp');
|
|
1012
1255
|
// terrain.bmp from province terrain, with snow on high-latitude mountains.
|
|
1013
1256
|
const terrainIndex = context.terrainIndex ?? DEFAULT_TERRAIN_INDEX;
|
|
1257
|
+
// Lake pixels use the ocean's graphical terrain: the lake entry names no atlas tile.
|
|
1258
|
+
const lakeIndex = terrainIndex.get('lakes') ?? terrainIndex.get('ocean') ?? 15;
|
|
1259
|
+
const calibrated = spec.colormapProfile === 'calibrated';
|
|
1260
|
+
const farmlandIndex = calibrated ? terrainVariant(context, 5, 'plains') : undefined;
|
|
1261
|
+
const sparseForestIndex = calibrated ? terrainVariant(context, 4, 'forest') : undefined;
|
|
1262
|
+
const mountainGrassIndex = calibrated ? terrainVariant(context, 20, 'mountain') : undefined;
|
|
1014
1263
|
const terrainPixels = new Uint8Array(width * height);
|
|
1264
|
+
const colourClasses = calibrated ? new Uint8Array(width * height) : undefined;
|
|
1265
|
+
const classCode = (name) => COLOUR_CLASSES.indexOf(name);
|
|
1266
|
+
const flatSlope = (x, y) => Math.abs(heights[y * width + Math.min(width - 1, x + 1)] - heights[y * width + Math.max(0, x - 1)]) +
|
|
1267
|
+
Math.abs(heights[Math.min(height - 1, y + 1) * width + x] -
|
|
1268
|
+
heights[Math.max(0, y - 1) * width + x]) <=
|
|
1269
|
+
2;
|
|
1015
1270
|
for (let pixel = 0; pixel < width * height; pixel += 1) {
|
|
1016
1271
|
const id = provinces.pixel[pixel];
|
|
1017
1272
|
const type = visualTerrain(pixel);
|
|
1018
|
-
const snowy =
|
|
1019
|
-
?
|
|
1020
|
-
|
|
1021
|
-
|
|
1273
|
+
const snowy = synthesized !== undefined
|
|
1274
|
+
? (synthesized.snow[pixel] === 1 || reference?.terrain[pixel] === 4) &&
|
|
1275
|
+
surface.kind[pixel] === LAND
|
|
1276
|
+
: reference === undefined
|
|
1277
|
+
? type === 'mountain' && latitude(id) > 0.7
|
|
1278
|
+
: reference.terrain[pixel] === 4 && surface.kind[pixel] === LAND;
|
|
1279
|
+
const x = pixel % width;
|
|
1280
|
+
const rimmed = rimStrength(edge, width, x) > 0.35 && surface.kind[pixel] === LAND;
|
|
1281
|
+
let index = snowy
|
|
1022
1282
|
? reference === undefined
|
|
1023
1283
|
? 16
|
|
1024
1284
|
: 19
|
|
1025
|
-
:
|
|
1285
|
+
: rimmed
|
|
1286
|
+
? 16
|
|
1287
|
+
: type === 'lakes'
|
|
1288
|
+
? lakeIndex
|
|
1289
|
+
: (terrainIndex.get(type) ?? DEFAULT_TERRAIN_INDEX.get(type) ?? 0);
|
|
1290
|
+
let colour = type === 'lakes' || type === 'ocean' || type === 'urban'
|
|
1291
|
+
? type === 'urban'
|
|
1292
|
+
? 'urban'
|
|
1293
|
+
: type
|
|
1294
|
+
: snowy || rimmed
|
|
1295
|
+
? 'snow'
|
|
1296
|
+
: type;
|
|
1297
|
+
if (calibrated && !snowy && !rimmed && surface.kind[pixel] === LAND) {
|
|
1298
|
+
// Variant graphical terrains by rule, so one class is not one flat texture.
|
|
1299
|
+
const y = (pixel - x) / width;
|
|
1300
|
+
if (type === 'plains' &&
|
|
1301
|
+
farmlandIndex !== undefined &&
|
|
1302
|
+
surface.moisture[pixel] >= 0.6 &&
|
|
1303
|
+
flatSlope(x, y)) {
|
|
1304
|
+
index = farmlandIndex;
|
|
1305
|
+
colour = 'farmland';
|
|
1306
|
+
}
|
|
1307
|
+
else if (type === 'forest' &&
|
|
1308
|
+
sparseForestIndex !== undefined &&
|
|
1309
|
+
fractal(spec.seed + 7001, x, y, Math.max(16, width / 30), 3) < 0.42)
|
|
1310
|
+
index = sparseForestIndex;
|
|
1311
|
+
else if (type === 'mountain' &&
|
|
1312
|
+
mountainGrassIndex !== undefined &&
|
|
1313
|
+
surface.moisture[pixel] > 0.45 &&
|
|
1314
|
+
fractal(spec.seed + 7919, x, y, Math.max(16, width / 20), 3) > 0.5)
|
|
1315
|
+
index = mountainGrassIndex;
|
|
1316
|
+
}
|
|
1317
|
+
terrainPixels[pixel] = index;
|
|
1318
|
+
if (colourClasses !== undefined) {
|
|
1319
|
+
if (!COLOUR_CLASSES.includes(colour))
|
|
1320
|
+
colour = 'plains';
|
|
1321
|
+
colourClasses[pixel] = classCode(colour);
|
|
1322
|
+
}
|
|
1026
1323
|
}
|
|
1027
1324
|
add(`${map}/terrain.bmp`, createBmp({
|
|
1028
1325
|
width,
|
|
@@ -1098,23 +1395,33 @@ export function generateWorld(spec, context) {
|
|
|
1098
1395
|
palette: bmpPalette(context.palettes.cities, (index) => ({ r: index, g: index, b: index })),
|
|
1099
1396
|
indexedPixels: cityPixels,
|
|
1100
1397
|
}), 'image/bmp');
|
|
1101
|
-
// world_normal.bmp at half size from the height
|
|
1398
|
+
// world_normal.bmp at half size. Geography worlds shade from the float height field at the
|
|
1399
|
+
// base game's response; every other world from the quantised heights.
|
|
1102
1400
|
const halfWidth = width / 2;
|
|
1103
1401
|
const halfHeight = height / 2;
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1402
|
+
if (synthesized !== undefined)
|
|
1403
|
+
add(`${map}/world_normal.bmp`, createBmp({
|
|
1404
|
+
width: halfWidth,
|
|
1405
|
+
height: halfHeight,
|
|
1406
|
+
bitsPerPixel: 24,
|
|
1407
|
+
rgbBytes: synthesized.normals,
|
|
1408
|
+
}), 'image/bmp');
|
|
1409
|
+
else {
|
|
1410
|
+
const normals = new Array(halfWidth * halfHeight);
|
|
1411
|
+
const heightAt = (x, y) => heights[Math.min(height - 1, Math.max(0, y)) * width + Math.min(width - 1, Math.max(0, x))];
|
|
1412
|
+
for (let y = 0; y < halfHeight; y += 1)
|
|
1413
|
+
for (let x = 0; x < halfWidth; x += 1) {
|
|
1414
|
+
const dx = (heightAt(x * 2 + 2, y * 2) - heightAt(x * 2 - 2, y * 2)) / 4;
|
|
1415
|
+
const dy = (heightAt(x * 2, y * 2 - 2) - heightAt(x * 2, y * 2 + 2)) / 4;
|
|
1416
|
+
const length = Math.hypot(dx, dy, 8);
|
|
1417
|
+
normals[y * halfWidth + x] = {
|
|
1418
|
+
r: Math.round(128 - (dx / length) * 127),
|
|
1419
|
+
g: Math.round(128 + (dy / length) * 127),
|
|
1420
|
+
b: Math.round(128 + (8 / length) * 127),
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
add(`${map}/world_normal.bmp`, createBmp({ width: halfWidth, height: halfHeight, bitsPerPixel: 24, rgbPixels: normals }), 'image/bmp');
|
|
1424
|
+
}
|
|
1118
1425
|
// Colour maps: land tint and city lights at half size; water at half, quarter and eighth.
|
|
1119
1426
|
const tint = {
|
|
1120
1427
|
plains: { r: 121, g: 133, b: 76 },
|
|
@@ -1128,36 +1435,65 @@ export function generateWorld(spec, context) {
|
|
|
1128
1435
|
lakes: { r: 70, g: 96, b: 110 },
|
|
1129
1436
|
ocean: { r: 50, g: 70, b: 92 },
|
|
1130
1437
|
};
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1438
|
+
let colormap;
|
|
1439
|
+
if (colourClasses !== undefined) {
|
|
1440
|
+
const lights = new Uint8Array(width * height);
|
|
1441
|
+
for (let pixel = 0; pixel < lights.length; pixel += 1)
|
|
1442
|
+
if (cityPixels[pixel] === 15)
|
|
1443
|
+
lights[pixel] = 200;
|
|
1444
|
+
colormap = calibratedColourMap({
|
|
1445
|
+
width,
|
|
1446
|
+
height,
|
|
1447
|
+
seed: spec.seed,
|
|
1448
|
+
classes: colourClasses,
|
|
1449
|
+
moisture: surface.moisture,
|
|
1450
|
+
heights,
|
|
1451
|
+
lights,
|
|
1452
|
+
look: spec.look ?? 'vanilla-temperate',
|
|
1453
|
+
...(spec.colormapPalette === undefined ? {} : { overrides: spec.colormapPalette }),
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
else {
|
|
1457
|
+
colormap = new Uint8Array(halfWidth * halfHeight * 4);
|
|
1458
|
+
for (let y = 0; y < halfHeight; y += 1)
|
|
1459
|
+
for (let x = 0; x < halfWidth; x += 1) {
|
|
1460
|
+
// DDS rows run top to bottom, like the bitmaps' top-left coordinates used here.
|
|
1461
|
+
const source = y * 2 * width + x * 2;
|
|
1462
|
+
const color = tint[visualTerrain(source)];
|
|
1463
|
+
const shade = 0.85 + surface.moisture[source] * 0.3;
|
|
1464
|
+
const offset = (y * halfWidth + x) * 4;
|
|
1465
|
+
colormap[offset] = Math.min(255, Math.round(color.b * shade));
|
|
1466
|
+
colormap[offset + 1] = Math.min(255, Math.round(color.g * shade));
|
|
1467
|
+
colormap[offset + 2] = Math.min(255, Math.round(color.r * shade));
|
|
1468
|
+
colormap[offset + 3] = cityPixels[source] === 15 ? 200 : 0;
|
|
1158
1469
|
}
|
|
1159
|
-
add(`${map}/terrain/colormap_water_${level}.dds`, encodeDds(levelWidth, levelHeight, water), 'image/vnd-ms.dds');
|
|
1160
1470
|
}
|
|
1471
|
+
if (edge.mode === 'rim')
|
|
1472
|
+
for (let y = 0; y < halfHeight; y += 1)
|
|
1473
|
+
for (let x = 0; x < halfWidth; x += 1) {
|
|
1474
|
+
const strength = rimStrength(edge, width, x * 2);
|
|
1475
|
+
if (strength === 0 || surface.kind[y * 2 * width + x * 2] !== LAND)
|
|
1476
|
+
continue;
|
|
1477
|
+
const factor = 1 - (1 - RIM_COLOUR_FACTOR) * strength;
|
|
1478
|
+
const offset = (y * halfWidth + x) * 4;
|
|
1479
|
+
colormap[offset] = Math.round(colormap[offset] * factor);
|
|
1480
|
+
colormap[offset + 1] = Math.round(colormap[offset + 1] * factor);
|
|
1481
|
+
colormap[offset + 2] = Math.round(colormap[offset + 2] * factor);
|
|
1482
|
+
}
|
|
1483
|
+
add(`${map}/terrain/colormap_rgb_cityemissivemask_a.dds`, encodeDds(halfWidth, halfHeight, colormap), 'image/vnd-ms.dds');
|
|
1484
|
+
// Water colour maps: sea tint by distance from land, lake tint with a shore gradient, and the
|
|
1485
|
+
// darkest tint at the world edge (ADR 0053, 0061).
|
|
1486
|
+
for (const level of [0, 1, 2])
|
|
1487
|
+
add(`${map}/terrain/colormap_water_${level}.dds`, encodeDds(width / 2 ** (level + 1), height / 2 ** (level + 1), waterColourLevel({
|
|
1488
|
+
width,
|
|
1489
|
+
height,
|
|
1490
|
+
kind: surface.kind,
|
|
1491
|
+
landDistance,
|
|
1492
|
+
seed: spec.seed,
|
|
1493
|
+
...(edge.mode === 'none'
|
|
1494
|
+
? {}
|
|
1495
|
+
: { darken: (x, y) => edgeWaterDarkness(edge, width, height, x, y) }),
|
|
1496
|
+
}, level)), 'image/vnd-ms.dds');
|
|
1161
1497
|
// Strategic regions with weather by latitude.
|
|
1162
1498
|
const regionMembers = new Map();
|
|
1163
1499
|
for (const [id, region] of regionOf) {
|
|
@@ -1385,6 +1721,8 @@ export function generateWorld(spec, context) {
|
|
|
1385
1721
|
return {
|
|
1386
1722
|
files,
|
|
1387
1723
|
warnings: [...interpretationWarnings, ...drainage.warnings],
|
|
1724
|
+
notes,
|
|
1725
|
+
...(synthesized === undefined ? {} : { terrain: synthesized }),
|
|
1388
1726
|
summary: {
|
|
1389
1727
|
width,
|
|
1390
1728
|
height,
|