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
|
@@ -87,7 +87,11 @@ export function selectActiveFiles(files) {
|
|
|
87
87
|
}
|
|
88
88
|
export function parseTextDocument(file) {
|
|
89
89
|
if (file.bytes.length > MAP_TEXT_MAX_BYTES) {
|
|
90
|
-
throw new ServiceError('MAP_TEXT_FILE_LIMIT',
|
|
90
|
+
throw new ServiceError('MAP_TEXT_FILE_LIMIT', `Map text table ${file.relativePath} exceeds the supported 16 MiB parsing limit (${file.bytes.length} bytes)`, {
|
|
91
|
+
relativePath: file.relativePath,
|
|
92
|
+
bytes: file.bytes.length,
|
|
93
|
+
maximumBytes: MAP_TEXT_MAX_BYTES,
|
|
94
|
+
});
|
|
91
95
|
}
|
|
92
96
|
const decoded = decodeSource(file.bytes);
|
|
93
97
|
const lines = [];
|
|
@@ -101,7 +105,11 @@ export function parseTextDocument(file) {
|
|
|
101
105
|
const text = full.replace(/(?:\r\n|\n|\r)$/u, '');
|
|
102
106
|
const start = match.index;
|
|
103
107
|
if (index >= MAP_TEXT_MAX_RECORDS) {
|
|
104
|
-
throw new ServiceError('MAP_TEXT_RECORD_LIMIT',
|
|
108
|
+
throw new ServiceError('MAP_TEXT_RECORD_LIMIT', `Map text table ${file.relativePath} exceeds the supported record limit`, {
|
|
109
|
+
relativePath: file.relativePath,
|
|
110
|
+
records: index + 1,
|
|
111
|
+
maximumRecords: MAP_TEXT_MAX_RECORDS,
|
|
112
|
+
});
|
|
105
113
|
}
|
|
106
114
|
lines.push({ index, start, end: start + text.length, fullEnd: start + full.length, text });
|
|
107
115
|
index += 1;
|
|
@@ -1114,6 +1122,9 @@ export class MapWorkspaceIndex {
|
|
|
1114
1122
|
railwayFile;
|
|
1115
1123
|
/** The active heightmap, when the scan included it; decoded on first use. */
|
|
1116
1124
|
heightmapFile;
|
|
1125
|
+
/** The active river and terrain bitmaps, when the scan included them (border quality). */
|
|
1126
|
+
riversFile;
|
|
1127
|
+
terrainBitmapFile;
|
|
1117
1128
|
#heightmap;
|
|
1118
1129
|
/**
|
|
1119
1130
|
* Map height (the Y coordinate) at a bitmap pixel: the heightmap value divided by ten, as the
|
|
@@ -1297,6 +1308,8 @@ export class MapWorkspaceIndex {
|
|
|
1297
1308
|
? undefined
|
|
1298
1309
|
: parseTerrainCategories(terrainFiles, this.diagnostics);
|
|
1299
1310
|
this.heightmapFile = mapFile(this.activeFiles, defaultMapSelectorValue(this.defaultMapFile, 'heightmap', 'heightmap.bmp'), this.sourceRoots.map);
|
|
1311
|
+
this.riversFile = mapFile(this.activeFiles, defaultMapSelectorValue(this.defaultMapFile, 'rivers', 'rivers.bmp'), this.sourceRoots.map);
|
|
1312
|
+
this.terrainBitmapFile = mapFile(this.activeFiles, defaultMapSelectorValue(this.defaultMapFile, 'terrain', 'terrain.bmp'), this.sourceRoots.map);
|
|
1300
1313
|
const buildingFiles = this.activeFiles.all.filter((file) => /^common\/buildings\/[^/]+\.txt$/iu.test(normalizedPath(file.relativePath)));
|
|
1301
1314
|
const buildings = parseBuildingDefinitions(buildingFiles, []);
|
|
1302
1315
|
this.buildingDefinitions = buildings.definitions;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { setImmediate as yieldToEventLoop } from 'node:timers/promises';
|
|
3
|
-
import { compareCodeUnits, deterministicId, sha256Bytes } from '../core/canonical.js';
|
|
3
|
+
import { canonicalJson, compareCodeUnits, deterministicId, sha256Bytes, } from '../core/canonical.js';
|
|
4
4
|
import { ServiceError } from '../core/result.js';
|
|
5
5
|
import { applyReplacements, assignments, encodeSource, parseLocalisation, parseClausewitz, } from '../core/source/index.js';
|
|
6
6
|
import { rgbKey } from './bmp.js';
|
|
@@ -8,9 +8,12 @@ import { MAP_MASK_CELL_LIMIT, MAP_POLYGON_WORK_LIMIT, MAP_SELECTED_OFFSET_BYTES,
|
|
|
8
8
|
import { derivedStateCapital, encodeTextDocument, LEVEL_STATE_BUILDINGS, MapWorkspaceIndex, parseMapDate, parseTextDocument, } from './model.js';
|
|
9
9
|
import { planReferenceRemap, ReferenceRemapError } from './references.js';
|
|
10
10
|
import { generateWorld } from './world.js';
|
|
11
|
+
import { resolveEdge } from './world-edge.js';
|
|
11
12
|
import { planMapPositions, planSupplyRebuild } from './derived.js';
|
|
12
13
|
import { buildWorldContext } from './world-context.js';
|
|
13
14
|
import { reconcileWorldOutputs } from './world-outputs.js';
|
|
15
|
+
import { resolvePartitionMode } from './partition.js';
|
|
16
|
+
import { unsupportedGeographyParts } from './relief/index.js';
|
|
14
17
|
function* cancellationCheckpoint(signal, iteration = 0, stride = 1) {
|
|
15
18
|
if (iteration % stride !== 0)
|
|
16
19
|
return;
|
|
@@ -3272,23 +3275,60 @@ function descriptorWithReplacePaths(index, roots) {
|
|
|
3272
3275
|
const body = textValue.endsWith('\n') ? textValue : `${textValue}${newline}`;
|
|
3273
3276
|
return Buffer.from(`${body}${missing.map((root) => `replace_path="${root}"`).join(newline)}${missing.length > 0 ? newline : ''}`, 'utf8');
|
|
3274
3277
|
}
|
|
3275
|
-
function
|
|
3278
|
+
function colormapPaletteOverrides(palette) {
|
|
3279
|
+
return Object.fromEntries(Object.entries(palette).flatMap(([name, color]) => color === undefined ? [] : [[name, [color.r, color.g, color.b]]]));
|
|
3280
|
+
}
|
|
3281
|
+
function applyCreateWorld(base, changes, operation, context, signal) {
|
|
3282
|
+
const geography = context.geographyModels?.get(operation.id);
|
|
3283
|
+
const hasGeography = operation.geography !== undefined || operation.geographyPath !== undefined;
|
|
3284
|
+
if (hasGeography) {
|
|
3285
|
+
if (geography === undefined)
|
|
3286
|
+
throw new ServiceError('MAP_GEOGRAPHY_NOT_PREPARED', 'Geography models must be prepared by the authorized workspace service');
|
|
3287
|
+
if (!geography.valid)
|
|
3288
|
+
throw new ServiceError('MAP_GEOGRAPHY_INVALID', 'The geography model has errors; fix them (see the MAP_GEOGRAPHY_* diagnostics and map-geography-report.json) before generating a world', { modelHash: geography.geography.hash });
|
|
3289
|
+
const pending = unsupportedGeographyParts(geography.geography);
|
|
3290
|
+
if (pending.length > 0)
|
|
3291
|
+
throw new ServiceError('MAP_GEOGRAPHY_SYNTHESIS_UNAVAILABLE', `The geography model is valid and its relief, rivers and lakes can be synthesized, but this version cannot yet use ${pending.join(', ')} (the climate and biome stage); remove them from the model to generate relief and rivers only, or use preview:true to review the model overlay and report`, {
|
|
3292
|
+
modelHash: geography.geography.hash,
|
|
3293
|
+
available: ['relief', 'rivers', 'lakes'],
|
|
3294
|
+
unavailable: pending,
|
|
3295
|
+
});
|
|
3296
|
+
}
|
|
3297
|
+
else {
|
|
3298
|
+
if (operation.relief !== undefined)
|
|
3299
|
+
throw new ServiceError('MAP_RELIEF_REQUIRES_GEOGRAPHY', 'relief shapes the terrain synthesised from a geography model; give geography or geographyPath, or remove relief');
|
|
3300
|
+
if (typeof operation.rivers === 'object')
|
|
3301
|
+
throw new ServiceError('MAP_RIVER_DENSITY_REQUIRES_GEOGRAPHY', 'rivers { density } sets the river network of a geography world; give geography or geographyPath, or use an integer count');
|
|
3302
|
+
}
|
|
3276
3303
|
const reference = context.referenceWorlds?.get(operation.id);
|
|
3277
3304
|
if (operation.reference !== undefined && reference === undefined)
|
|
3278
3305
|
throw new ServiceError('MAP_REFERENCE_NOT_PREPARED', 'Reference worlds must be prepared by the authorized workspace service');
|
|
3279
3306
|
const mapRoot = base.sourceRoots.map[0] ?? 'map';
|
|
3280
3307
|
const startingScenario = operation.startingScenario ?? reference !== undefined;
|
|
3308
|
+
const worldWidth = operation.width ?? reference?.surface.width ?? 1_024;
|
|
3309
|
+
const worldHeight = operation.height ?? reference?.surface.height ?? 512;
|
|
3310
|
+
const edge = resolveEdge(operation.edge, worldWidth, worldHeight);
|
|
3311
|
+
const look = operation.look;
|
|
3312
|
+
const colormapProfile = operation.colormap?.profile ?? (look === undefined ? 'legacy' : 'calibrated');
|
|
3313
|
+
if (look === 'custom' && operation.colormap?.palette === undefined)
|
|
3314
|
+
throw new ServiceError('MAP_LOOK_CUSTOM_PALETTE_REQUIRED', 'look "custom" needs colormap.palette with at least one class colour');
|
|
3315
|
+
if (colormapProfile === 'legacy' && look !== undefined)
|
|
3316
|
+
throw new ServiceError('MAP_LOOK_REQUIRES_CALIBRATED_COLORMAP', 'look selects a calibrated palette; remove colormap.profile "legacy" or look');
|
|
3317
|
+
const partitionMode = resolvePartitionMode(operation.partition, hasGeography);
|
|
3318
|
+
if (partitionMode === 'legacy' && operation.partitionTuning !== undefined)
|
|
3319
|
+
throw new ServiceError('MAP_PARTITION_TUNING_REQUIRES_FEATURE_AWARE', 'partitionTuning applies to the feature-aware partition; set partition to "feature-aware" or remove it');
|
|
3281
3320
|
const world = generateWorld({
|
|
3282
3321
|
startingScenario,
|
|
3283
3322
|
...(operation.worldName === undefined ? {} : { worldName: operation.worldName }),
|
|
3284
|
-
width:
|
|
3285
|
-
height:
|
|
3323
|
+
width: worldWidth,
|
|
3324
|
+
height: worldHeight,
|
|
3286
3325
|
seed: operation.seed ?? 1,
|
|
3287
3326
|
landCoverage: operation.landCoverage ?? 0.35,
|
|
3288
3327
|
continents: operation.continents ?? 3,
|
|
3289
3328
|
...(operation.landPolygons === undefined ? {} : { landPolygons: operation.landPolygons }),
|
|
3290
3329
|
landProvincePixels: operation.landProvincePixels ?? 400,
|
|
3291
3330
|
seaProvincePixels: operation.seaProvincePixels ?? 2_500,
|
|
3331
|
+
provinceSizesDefaulted: operation.landProvincePixels === undefined && operation.seaProvincePixels === undefined,
|
|
3292
3332
|
provincesPerState: operation.provincesPerState ?? (reference === undefined ? 5 : 12),
|
|
3293
3333
|
landProvincesPerRegion: operation.landProvincesPerRegion ?? (reference === undefined ? 25 : 60),
|
|
3294
3334
|
seaProvincesPerRegion: operation.seaProvincesPerRegion ?? (reference === undefined ? 10 : 30),
|
|
@@ -3310,8 +3350,47 @@ function applyCreateWorld(base, changes, operation, context) {
|
|
|
3310
3350
|
? {}
|
|
3311
3351
|
: { continentNames: operation.continentNames }),
|
|
3312
3352
|
startDate: operation.startDate ?? '1936.1.1',
|
|
3313
|
-
rivers: operation.rivers
|
|
3314
|
-
|
|
3353
|
+
rivers: typeof operation.rivers === 'number' ? operation.rivers : reference === undefined ? 8 : 80,
|
|
3354
|
+
...(geography === undefined
|
|
3355
|
+
? {}
|
|
3356
|
+
: {
|
|
3357
|
+
geography: geography.geography,
|
|
3358
|
+
...(operation.relief === undefined
|
|
3359
|
+
? {}
|
|
3360
|
+
: {
|
|
3361
|
+
relief: {
|
|
3362
|
+
...(operation.relief.scale === undefined
|
|
3363
|
+
? {}
|
|
3364
|
+
: { scale: operation.relief.scale }),
|
|
3365
|
+
...(operation.relief.detail === undefined
|
|
3366
|
+
? {}
|
|
3367
|
+
: { detail: operation.relief.detail }),
|
|
3368
|
+
},
|
|
3369
|
+
}),
|
|
3370
|
+
...(typeof operation.rivers === 'object'
|
|
3371
|
+
? { riverDensity: operation.rivers.density }
|
|
3372
|
+
: typeof operation.rivers === 'number'
|
|
3373
|
+
? { riverCount: operation.rivers }
|
|
3374
|
+
: {}),
|
|
3375
|
+
}),
|
|
3376
|
+
...(edge.mode === 'none' ? {} : { edge }),
|
|
3377
|
+
...(operation.ambientFrame === true ? { ambientFrame: true } : {}),
|
|
3378
|
+
colormapProfile,
|
|
3379
|
+
...(partitionMode === 'legacy'
|
|
3380
|
+
? {}
|
|
3381
|
+
: {
|
|
3382
|
+
partition: {
|
|
3383
|
+
mode: partitionMode,
|
|
3384
|
+
...(operation.partitionTuning === undefined
|
|
3385
|
+
? {}
|
|
3386
|
+
: { tuning: operation.partitionTuning }),
|
|
3387
|
+
},
|
|
3388
|
+
}),
|
|
3389
|
+
...(look === undefined ? {} : { look }),
|
|
3390
|
+
...(operation.colormap?.palette === undefined
|
|
3391
|
+
? {}
|
|
3392
|
+
: { colormapPalette: colormapPaletteOverrides(operation.colormap.palette) }),
|
|
3393
|
+
}, buildWorldContext(base), { signal });
|
|
3315
3394
|
const roots = operation.replaceBaseGame === false
|
|
3316
3395
|
? []
|
|
3317
3396
|
: [...WORLD_REPLACED_ROOTS, ...(startingScenario ? STARTING_SCENARIO_REPLACED_ROOTS : [])];
|
|
@@ -3349,6 +3428,14 @@ function applyCreateWorld(base, changes, operation, context) {
|
|
|
3349
3428
|
operationId: operation.id,
|
|
3350
3429
|
message,
|
|
3351
3430
|
})),
|
|
3431
|
+
...world.notes.map((note) => ({
|
|
3432
|
+
code: note.code,
|
|
3433
|
+
severity: note.severity,
|
|
3434
|
+
category: 'map',
|
|
3435
|
+
operationId: operation.id,
|
|
3436
|
+
message: note.message,
|
|
3437
|
+
...(note.details === undefined ? {} : { details: note.details }),
|
|
3438
|
+
})),
|
|
3352
3439
|
...(world.warnings.length === 0
|
|
3353
3440
|
? []
|
|
3354
3441
|
: [
|
|
@@ -3456,6 +3543,22 @@ function applyEntityLocator(base, changes, operation) {
|
|
|
3456
3543
|
]);
|
|
3457
3544
|
addChange(changes, locator.file.relativePath, output, operation.id, 'text/plain');
|
|
3458
3545
|
}
|
|
3546
|
+
/**
|
|
3547
|
+
* The operation as the transaction manifest records it. An inline geography model can hold
|
|
3548
|
+
* megabytes; the manifest keeps its SHA-256 and size instead, which still bind the plan hash to
|
|
3549
|
+
* the exact model, and a model given by path is bound by the file's read dependency.
|
|
3550
|
+
*/
|
|
3551
|
+
function manifestOperationData(operation) {
|
|
3552
|
+
const data = JSON.parse(JSON.stringify(operation));
|
|
3553
|
+
if (operation.kind === 'create_world' && operation.geography !== undefined) {
|
|
3554
|
+
const canonical = canonicalJson(operation.geography);
|
|
3555
|
+
data.geography = {
|
|
3556
|
+
sha256: sha256Bytes(canonical),
|
|
3557
|
+
bytes: Buffer.byteLength(canonical, 'utf8'),
|
|
3558
|
+
};
|
|
3559
|
+
}
|
|
3560
|
+
return data;
|
|
3561
|
+
}
|
|
3459
3562
|
function* planMapOperationSteps(base, operations, signal, context = {}) {
|
|
3460
3563
|
yield* cancellationCheckpoint(signal);
|
|
3461
3564
|
const changes = new Map();
|
|
@@ -3480,7 +3583,7 @@ function* planMapOperationSteps(base, operations, signal, context = {}) {
|
|
|
3480
3583
|
id: operation.id,
|
|
3481
3584
|
kind: operation.kind,
|
|
3482
3585
|
summary: operation.summary ?? operation.kind.replaceAll('_', ' '),
|
|
3483
|
-
data:
|
|
3586
|
+
data: manifestOperationData(operation),
|
|
3484
3587
|
});
|
|
3485
3588
|
const changesCheckpoint = new Map([...changes].map(([key, change]) => [
|
|
3486
3589
|
key,
|
|
@@ -3494,7 +3597,7 @@ function* planMapOperationSteps(base, operations, signal, context = {}) {
|
|
|
3494
3597
|
const boundsCheckpoint = expectedChangedBounds;
|
|
3495
3598
|
try {
|
|
3496
3599
|
if (operation.kind === 'create_world')
|
|
3497
|
-
diagnostics.push(...applyCreateWorld(base, changes, operation, context));
|
|
3600
|
+
diagnostics.push(...applyCreateWorld(base, changes, operation, context, signal));
|
|
3498
3601
|
else if (operation.kind === 'move_state_provinces')
|
|
3499
3602
|
applyMoveStateProvinces(base, changes, operation);
|
|
3500
3603
|
else if (operation.kind === 'update_state')
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import { ServiceError } from '../core/result.js';
|
|
2
|
+
/**
|
|
3
|
+
* Inputs of the feature-aware partition (ADR 0058): one byte (or two) per pixel describing the
|
|
4
|
+
* natural lines and the character of the land, in one typed contract. The contract exists so the
|
|
5
|
+
* partition does not care where the numbers come from. `partitionFieldsFromBitmaps` computes a
|
|
6
|
+
* basic version from the height, terrain and river bitmaps a map already has; the generator fills
|
|
7
|
+
* them from its own rasters and, for a geography world, from the synthesised heights, the model
|
|
8
|
+
* (`partitionFieldsFromGeography` adds regions and wall ranges) and the lines of the synthesis
|
|
9
|
+
* (`partitionFieldsWithTerrainLines`). Only `+ - * /` and `sqrt` are used, so every field is
|
|
10
|
+
* identical on every platform.
|
|
11
|
+
*/
|
|
12
|
+
/** Terrain classes the partition distinguishes; `water` marks pixels that are not land. */
|
|
13
|
+
export const PARTITION_TERRAIN = {
|
|
14
|
+
plains: 0,
|
|
15
|
+
farmland: 1,
|
|
16
|
+
forest: 2,
|
|
17
|
+
hills: 3,
|
|
18
|
+
mountain: 4,
|
|
19
|
+
desert: 5,
|
|
20
|
+
marsh: 6,
|
|
21
|
+
jungle: 7,
|
|
22
|
+
snow: 8,
|
|
23
|
+
urban: 9,
|
|
24
|
+
water: 255,
|
|
25
|
+
};
|
|
26
|
+
/** Pixel kinds, as the province generator already uses them. */
|
|
27
|
+
export const PARTITION_KIND = { sea: 0, land: 1, lake: 2 };
|
|
28
|
+
const MAP_PIXEL_LIMIT = 13_238_272;
|
|
29
|
+
function invalid(message, details) {
|
|
30
|
+
throw new ServiceError('MAP_PARTITION_FIELDS_INVALID', message, details);
|
|
31
|
+
}
|
|
32
|
+
/** Check the sizes, kinds and region table of a set of fields; throws `MAP_PARTITION_FIELDS_INVALID`. */
|
|
33
|
+
export function assertPartitionFields(fields) {
|
|
34
|
+
const { width, height } = fields;
|
|
35
|
+
if (!Number.isInteger(width) || !Number.isInteger(height) || width < 16 || height < 16)
|
|
36
|
+
invalid('Partition dimensions must be integers of at least 16 pixels', { width, height });
|
|
37
|
+
const total = width * height;
|
|
38
|
+
if (!Number.isSafeInteger(total) || total > MAP_PIXEL_LIMIT)
|
|
39
|
+
invalid('Partition dimensions exceed the 13,238,272-pixel map limit', { width, height });
|
|
40
|
+
const planes = [
|
|
41
|
+
['kind', fields.kind, Uint8Array],
|
|
42
|
+
['riverDistance', fields.riverDistance, Uint8Array],
|
|
43
|
+
['ridge', fields.ridge, Uint8Array],
|
|
44
|
+
['slope', fields.slope, Uint8Array],
|
|
45
|
+
['terrainClass', fields.terrainClass, Uint8Array],
|
|
46
|
+
['regionId', fields.regionId, Uint16Array],
|
|
47
|
+
['fertility', fields.fertility, Uint8Array],
|
|
48
|
+
['barrier', fields.barrier, Uint8Array],
|
|
49
|
+
];
|
|
50
|
+
for (const [name, plane, type] of planes) {
|
|
51
|
+
if (!(plane instanceof type))
|
|
52
|
+
invalid(`Partition field ${name} must be a ${type.name}`, {
|
|
53
|
+
field: name,
|
|
54
|
+
});
|
|
55
|
+
if (plane.length !== total)
|
|
56
|
+
invalid(`Partition field ${name} does not match the map dimensions`, {
|
|
57
|
+
field: name,
|
|
58
|
+
length: plane.length,
|
|
59
|
+
expected: total,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
for (let p = 0; p < total; p++)
|
|
63
|
+
if (fields.kind[p] > 2)
|
|
64
|
+
invalid('Partition kind contains a value other than sea (0), land (1) or lake (2)', {
|
|
65
|
+
pixel: p,
|
|
66
|
+
value: fields.kind[p],
|
|
67
|
+
});
|
|
68
|
+
const ids = new Set();
|
|
69
|
+
for (const region of fields.regions ?? []) {
|
|
70
|
+
if (!Number.isInteger(region.id) || region.id < 1 || region.id > 65_535 || ids.has(region.id))
|
|
71
|
+
invalid('Partition region ids must be unique integers from 1 to 65,535', { id: region.id });
|
|
72
|
+
ids.add(region.id);
|
|
73
|
+
if (region.provinceAreaPx !== undefined &&
|
|
74
|
+
(!Number.isFinite(region.provinceAreaPx) || region.provinceAreaPx < 20))
|
|
75
|
+
invalid('A region province area must be at least 20 pixels', { id: region.id });
|
|
76
|
+
const per = region.provincesPerState;
|
|
77
|
+
if (per !== undefined &&
|
|
78
|
+
(per.some((value) => !Number.isInteger(value) || value < 1) ||
|
|
79
|
+
per[0] > per[1] ||
|
|
80
|
+
per[1] > per[2]))
|
|
81
|
+
invalid('A region provincesPerState must be three increasing positive integers', {
|
|
82
|
+
id: region.id,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
if (ids.size > 0)
|
|
86
|
+
for (let p = 0; p < total; p++) {
|
|
87
|
+
const id = fields.regionId[p];
|
|
88
|
+
if (id !== 0 && !ids.has(id))
|
|
89
|
+
invalid('A pixel names a region that the region table does not list', { pixel: p, id });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// ---------------------------------------------------------------------------------------------
|
|
93
|
+
// Diagonal contacts
|
|
94
|
+
/**
|
|
95
|
+
* Remove the one mask pattern no partition can represent: a 2 x 2 block whose diagonals are two
|
|
96
|
+
* kinds (land and sea, say) so that each kind touches itself only at a corner. Four provinces
|
|
97
|
+
* would meet there ("X crossing"), and neither pair can be merged because the pieces are not
|
|
98
|
+
* four-connected. One pixel of the non-land kind becomes the other kind (land beats sea and lake, sea
|
|
99
|
+
* beats lake: the pixel of the weaker kind on the north-east or north-west corner changes),
|
|
100
|
+
* repeating until no block is left. Returns the changed pixel indices; `kind` is modified in
|
|
101
|
+
* place. The east-west seam wraps.
|
|
102
|
+
*/
|
|
103
|
+
export function smoothDiagonalContacts(kind, width, height, maxPasses = 8) {
|
|
104
|
+
const changed = [];
|
|
105
|
+
for (let pass = 0; pass < maxPasses; pass++) {
|
|
106
|
+
let found = false;
|
|
107
|
+
for (let y = 0; y + 1 < height; y++) {
|
|
108
|
+
const row = y * width;
|
|
109
|
+
for (let x = 0; x < width; x++) {
|
|
110
|
+
const next = x + 1 < width ? x + 1 : 0;
|
|
111
|
+
const nw = row + x;
|
|
112
|
+
const ne = row + next;
|
|
113
|
+
const sw = nw + width;
|
|
114
|
+
const se = ne + width;
|
|
115
|
+
const a = kind[nw];
|
|
116
|
+
const b = kind[ne];
|
|
117
|
+
if (a === b || kind[se] !== a || kind[sw] !== b)
|
|
118
|
+
continue;
|
|
119
|
+
// The kind that survives: land over lake over sea; the other diagonal gives way.
|
|
120
|
+
const rank = (value) => value === PARTITION_KIND.land ? 2 : value === PARTITION_KIND.lake ? 0 : 1;
|
|
121
|
+
const keep = rank(a) >= rank(b) ? a : b;
|
|
122
|
+
const target = keep === a ? ne : nw;
|
|
123
|
+
kind[target] = keep;
|
|
124
|
+
changed.push(target);
|
|
125
|
+
found = true;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (!found)
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
return changed;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Province parts never join across the east-west seam, so a 2x2 window that straddles it holds
|
|
135
|
+
* four provinces whenever the kind changes vertically on both sides (a coast that crosses the
|
|
136
|
+
* seam). Make the east column (x = 0) agree with its upper pixel there, one pixel per crossing,
|
|
137
|
+
* and return the changed pixels.
|
|
138
|
+
*/
|
|
139
|
+
export function repairSeamCorners(kind, width, height, maxPasses = 8) {
|
|
140
|
+
const changed = [];
|
|
141
|
+
if (width < 2)
|
|
142
|
+
return changed;
|
|
143
|
+
for (let pass = 0; pass < maxPasses; pass++) {
|
|
144
|
+
let found = false;
|
|
145
|
+
for (let y = 0; y + 1 < height; y++) {
|
|
146
|
+
const west = y * width + width - 1;
|
|
147
|
+
const east = y * width;
|
|
148
|
+
if (kind[west] === kind[west + width] || kind[east] === kind[east + width])
|
|
149
|
+
continue;
|
|
150
|
+
kind[east + width] = kind[east];
|
|
151
|
+
changed.push(east + width);
|
|
152
|
+
found = true;
|
|
153
|
+
}
|
|
154
|
+
if (!found)
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
return changed;
|
|
158
|
+
}
|
|
159
|
+
// ---------------------------------------------------------------------------------------------
|
|
160
|
+
// Adapter from bitmaps
|
|
161
|
+
/**
|
|
162
|
+
* Terrain class of each graphical terrain index of the base game's `terrain.bmp`: 0 plains,
|
|
163
|
+
* 1 forest, 2 and 17 hills, 3 desert, 4 sparse forest, 5 farmland, 6, 16 and 20 mountain, 9 marsh,
|
|
164
|
+
* 13 urban, 15 ocean, 19 permanent snow, 21 and 22 jungle. Others are plains. A mod with other
|
|
165
|
+
* indices passes its own table (the border metric derives one from `common/terrain`).
|
|
166
|
+
*/
|
|
167
|
+
export function defaultTerrainClassTable() {
|
|
168
|
+
const table = new Uint8Array(256).fill(PARTITION_TERRAIN.plains);
|
|
169
|
+
const set = (name, ...indices) => {
|
|
170
|
+
for (const index of indices)
|
|
171
|
+
table[index] = PARTITION_TERRAIN[name];
|
|
172
|
+
};
|
|
173
|
+
set('forest', 1, 4);
|
|
174
|
+
set('hills', 2, 17);
|
|
175
|
+
set('desert', 3);
|
|
176
|
+
set('farmland', 5);
|
|
177
|
+
set('mountain', 6, 16, 20);
|
|
178
|
+
set('marsh', 9);
|
|
179
|
+
set('urban', 13);
|
|
180
|
+
set('water', 15, 14);
|
|
181
|
+
set('snow', 19);
|
|
182
|
+
set('jungle', 21, 22);
|
|
183
|
+
return table;
|
|
184
|
+
}
|
|
185
|
+
/** Three-four chamfer distance in pixels (capped at 255) to the nearest pixel of `source`. */
|
|
186
|
+
export function chamferDistance(source, width, height) {
|
|
187
|
+
const cap = 255 * 3;
|
|
188
|
+
const distance = new Uint16Array(width * height);
|
|
189
|
+
let any = false;
|
|
190
|
+
for (let p = 0; p < distance.length; p++) {
|
|
191
|
+
if (source(p)) {
|
|
192
|
+
distance[p] = 0;
|
|
193
|
+
any = true;
|
|
194
|
+
}
|
|
195
|
+
else
|
|
196
|
+
distance[p] = cap;
|
|
197
|
+
}
|
|
198
|
+
const output = new Uint8Array(width * height);
|
|
199
|
+
if (!any) {
|
|
200
|
+
output.fill(255);
|
|
201
|
+
return output;
|
|
202
|
+
}
|
|
203
|
+
// The east-west seam wraps, so two sweeps of both directions reach every pixel.
|
|
204
|
+
for (let sweep = 0; sweep < 2; sweep++) {
|
|
205
|
+
for (let y = 0; y < height; y++) {
|
|
206
|
+
const row = y * width;
|
|
207
|
+
for (let x = 0; x < width; x++) {
|
|
208
|
+
const left = x === 0 ? width - 1 : x - 1;
|
|
209
|
+
const right = x === width - 1 ? 0 : x + 1;
|
|
210
|
+
let best = distance[row + x];
|
|
211
|
+
const west = distance[row + left] + 3;
|
|
212
|
+
if (west < best)
|
|
213
|
+
best = west;
|
|
214
|
+
if (y > 0) {
|
|
215
|
+
const above = row - width;
|
|
216
|
+
const n = distance[above + x] + 3;
|
|
217
|
+
const nw = distance[above + left] + 4;
|
|
218
|
+
const ne = distance[above + right] + 4;
|
|
219
|
+
if (n < best)
|
|
220
|
+
best = n;
|
|
221
|
+
if (nw < best)
|
|
222
|
+
best = nw;
|
|
223
|
+
if (ne < best)
|
|
224
|
+
best = ne;
|
|
225
|
+
}
|
|
226
|
+
distance[row + x] = best > cap ? cap : best;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
for (let y = height - 1; y >= 0; y--) {
|
|
230
|
+
const row = y * width;
|
|
231
|
+
for (let x = width - 1; x >= 0; x--) {
|
|
232
|
+
const left = x === 0 ? width - 1 : x - 1;
|
|
233
|
+
const right = x === width - 1 ? 0 : x + 1;
|
|
234
|
+
let best = distance[row + x];
|
|
235
|
+
const east = distance[row + right] + 3;
|
|
236
|
+
if (east < best)
|
|
237
|
+
best = east;
|
|
238
|
+
if (y < height - 1) {
|
|
239
|
+
const below = row + width;
|
|
240
|
+
const s = distance[below + x] + 3;
|
|
241
|
+
const sw = distance[below + left] + 4;
|
|
242
|
+
const se = distance[below + right] + 4;
|
|
243
|
+
if (s < best)
|
|
244
|
+
best = s;
|
|
245
|
+
if (sw < best)
|
|
246
|
+
best = sw;
|
|
247
|
+
if (se < best)
|
|
248
|
+
best = se;
|
|
249
|
+
}
|
|
250
|
+
distance[row + x] = best > cap ? cap : best;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
for (let p = 0; p < output.length; p++) {
|
|
255
|
+
const value = distance[p];
|
|
256
|
+
output[p] = value >= cap ? 255 : Math.min(254, Math.floor((value + 1) / 3));
|
|
257
|
+
}
|
|
258
|
+
return output;
|
|
259
|
+
}
|
|
260
|
+
/** Central-difference gradient magnitude times 16, clamped to 255; zero off land. */
|
|
261
|
+
export function slopeField(heights, kind, width, height) {
|
|
262
|
+
const output = new Uint8Array(width * height);
|
|
263
|
+
for (let y = 0; y < height; y++) {
|
|
264
|
+
const row = y * width;
|
|
265
|
+
const above = (y > 0 ? y - 1 : y) * width;
|
|
266
|
+
const below = (y < height - 1 ? y + 1 : y) * width;
|
|
267
|
+
for (let x = 0; x < width; x++) {
|
|
268
|
+
if (kind[row + x] !== PARTITION_KIND.land)
|
|
269
|
+
continue;
|
|
270
|
+
const left = x === 0 ? width - 1 : x - 1;
|
|
271
|
+
const right = x === width - 1 ? 0 : x + 1;
|
|
272
|
+
// The step down to the water is not a slope: a neighbour that is not land counts as level.
|
|
273
|
+
const here = heights[row + x];
|
|
274
|
+
const hl = kind[row + left] === PARTITION_KIND.land ? heights[row + left] : here;
|
|
275
|
+
const hr = kind[row + right] === PARTITION_KIND.land ? heights[row + right] : here;
|
|
276
|
+
const ha = kind[above + x] === PARTITION_KIND.land ? heights[above + x] : here;
|
|
277
|
+
const hb = kind[below + x] === PARTITION_KIND.land ? heights[below + x] : here;
|
|
278
|
+
const dx = hr - hl;
|
|
279
|
+
const dy = hb - ha;
|
|
280
|
+
// |gradient| = sqrt(dx^2 + dy^2) / 2 per pixel; times 16.
|
|
281
|
+
const value = Math.sqrt(dx * dx + dy * dy) * 8;
|
|
282
|
+
output[row + x] = value > 255 ? 255 : Math.round(value);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return output;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Ridge strength: how far a land pixel stands above the mean of its two neighbours three pixels
|
|
289
|
+
* away along the best of four axes, times 10. A crest line is a band a few pixels wide.
|
|
290
|
+
*/
|
|
291
|
+
export function ridgeField(heights, kind, width, height, reach = 3) {
|
|
292
|
+
const output = new Uint8Array(width * height);
|
|
293
|
+
const axes = [
|
|
294
|
+
[1, 0],
|
|
295
|
+
[0, 1],
|
|
296
|
+
[1, 1],
|
|
297
|
+
[1, -1],
|
|
298
|
+
];
|
|
299
|
+
for (let y = 0; y < height; y++) {
|
|
300
|
+
const row = y * width;
|
|
301
|
+
for (let x = 0; x < width; x++) {
|
|
302
|
+
const p = row + x;
|
|
303
|
+
if (kind[p] !== PARTITION_KIND.land)
|
|
304
|
+
continue;
|
|
305
|
+
const here = heights[p];
|
|
306
|
+
let best = 0;
|
|
307
|
+
for (const [ax, ay] of axes) {
|
|
308
|
+
const y0 = y - ay * reach;
|
|
309
|
+
const y1 = y + ay * reach;
|
|
310
|
+
if (y0 < 0 || y1 >= height || y0 >= height || y1 < 0)
|
|
311
|
+
continue;
|
|
312
|
+
const x0 = (((x - ax * reach) % width) + width) % width;
|
|
313
|
+
const x1 = (x + ax * reach) % width;
|
|
314
|
+
const p0 = y0 * width + x0;
|
|
315
|
+
const p1 = y1 * width + x1;
|
|
316
|
+
// Water at either end is no valley: it counts as level ground.
|
|
317
|
+
const h0 = kind[p0] === PARTITION_KIND.land ? heights[p0] : here;
|
|
318
|
+
const h1 = kind[p1] === PARTITION_KIND.land ? heights[p1] : here;
|
|
319
|
+
const lift = here - (h0 + h1) / 2;
|
|
320
|
+
if (lift > best)
|
|
321
|
+
best = lift;
|
|
322
|
+
}
|
|
323
|
+
const value = best * 10;
|
|
324
|
+
output[p] = value > 255 ? 255 : Math.round(value);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return output;
|
|
328
|
+
}
|
|
329
|
+
const FERTILITY_BASE = {
|
|
330
|
+
[PARTITION_TERRAIN.plains]: 150,
|
|
331
|
+
[PARTITION_TERRAIN.farmland]: 225,
|
|
332
|
+
[PARTITION_TERRAIN.forest]: 110,
|
|
333
|
+
[PARTITION_TERRAIN.hills]: 85,
|
|
334
|
+
[PARTITION_TERRAIN.mountain]: 20,
|
|
335
|
+
[PARTITION_TERRAIN.desert]: 25,
|
|
336
|
+
[PARTITION_TERRAIN.marsh]: 70,
|
|
337
|
+
[PARTITION_TERRAIN.jungle]: 95,
|
|
338
|
+
[PARTITION_TERRAIN.snow]: 5,
|
|
339
|
+
[PARTITION_TERRAIN.urban]: 140,
|
|
340
|
+
};
|
|
341
|
+
/**
|
|
342
|
+
* Fertility from terrain class, river proximity (valleys are rich), slope and optional moisture.
|
|
343
|
+
* The relief and climate stages replace it with their own field; the partition only reads bytes.
|
|
344
|
+
*/
|
|
345
|
+
export function fertilityField(kind, terrainClass, riverDistance, slope, moisture) {
|
|
346
|
+
const output = new Uint8Array(kind.length);
|
|
347
|
+
for (let p = 0; p < kind.length; p++) {
|
|
348
|
+
if (kind[p] !== PARTITION_KIND.land)
|
|
349
|
+
continue;
|
|
350
|
+
let value = FERTILITY_BASE[terrainClass[p]] ?? 100;
|
|
351
|
+
const river = riverDistance[p];
|
|
352
|
+
if (river <= 14)
|
|
353
|
+
value += (14 - river) * 4;
|
|
354
|
+
value -= slope[p] * 0.5;
|
|
355
|
+
if (moisture !== undefined)
|
|
356
|
+
value += (moisture[p] - 128) * 0.25;
|
|
357
|
+
output[p] = value < 0 ? 0 : value > 255 ? 255 : Math.round(value);
|
|
358
|
+
}
|
|
359
|
+
return output;
|
|
360
|
+
}
|
|
361
|
+
/** Compute the partition fields from the bitmaps a map already has. */
|
|
362
|
+
export function partitionFieldsFromBitmaps(input) {
|
|
363
|
+
const { width, height, kind } = input;
|
|
364
|
+
const total = width * height;
|
|
365
|
+
const riverMax = input.riverMaxIndex ?? 11;
|
|
366
|
+
const rivers = input.rivers;
|
|
367
|
+
const riverDistance = rivers === undefined
|
|
368
|
+
? new Uint8Array(total).fill(255)
|
|
369
|
+
: chamferDistance((p) => rivers[p] <= riverMax && kind[p] === PARTITION_KIND.land, width, height);
|
|
370
|
+
const slope = input.heights === undefined
|
|
371
|
+
? new Uint8Array(total)
|
|
372
|
+
: slopeField(input.heights, kind, width, height);
|
|
373
|
+
const ridge = input.heights === undefined
|
|
374
|
+
? new Uint8Array(total)
|
|
375
|
+
: ridgeField(input.heights, kind, width, height);
|
|
376
|
+
const terrainClass = new Uint8Array(total).fill(PARTITION_TERRAIN.water);
|
|
377
|
+
const table = input.terrainClasses ?? defaultTerrainClassTable();
|
|
378
|
+
for (let p = 0; p < total; p++)
|
|
379
|
+
if (kind[p] === PARTITION_KIND.land)
|
|
380
|
+
terrainClass[p] =
|
|
381
|
+
input.terrain === undefined ? PARTITION_TERRAIN.plains : table[input.terrain[p]];
|
|
382
|
+
for (let p = 0; p < total; p++)
|
|
383
|
+
if (kind[p] === PARTITION_KIND.land && terrainClass[p] === PARTITION_TERRAIN.water)
|
|
384
|
+
terrainClass[p] = PARTITION_TERRAIN.plains;
|
|
385
|
+
return {
|
|
386
|
+
width,
|
|
387
|
+
height,
|
|
388
|
+
kind,
|
|
389
|
+
riverDistance,
|
|
390
|
+
ridge,
|
|
391
|
+
slope,
|
|
392
|
+
terrainClass,
|
|
393
|
+
regionId: input.regionId ?? new Uint16Array(total),
|
|
394
|
+
fertility: fertilityField(kind, terrainClass, riverDistance, slope, input.moisture),
|
|
395
|
+
barrier: input.barrier ?? new Uint8Array(total),
|
|
396
|
+
...(input.regions === undefined ? {} : { regions: input.regions }),
|
|
397
|
+
};
|
|
398
|
+
}
|