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
|
@@ -55,6 +55,50 @@ const mapSemanticDiffSchema = z
|
|
|
55
55
|
normalAdjacenciesChanged: z.boolean(),
|
|
56
56
|
})
|
|
57
57
|
.strict();
|
|
58
|
+
const borderRunSchema = z
|
|
59
|
+
.object({
|
|
60
|
+
sides: z.tuple([z.string().max(64), z.string().max(64)]),
|
|
61
|
+
from: z.object({ x: nonNegativeIntegerSchema, y: nonNegativeIntegerSchema }).strict(),
|
|
62
|
+
to: z.object({ x: nonNegativeIntegerSchema, y: nonNegativeIntegerSchema }).strict(),
|
|
63
|
+
edges: nonNegativeIntegerSchema,
|
|
64
|
+
chord: z.number().nonnegative(),
|
|
65
|
+
})
|
|
66
|
+
.strict();
|
|
67
|
+
const borderLayerSummarySchema = z
|
|
68
|
+
.object({
|
|
69
|
+
layer: z.string().max(32),
|
|
70
|
+
status: z.enum(['ok', 'not-meaningful', 'too-short', 'no-borders']),
|
|
71
|
+
groups: nonNegativeIntegerSchema,
|
|
72
|
+
borderEdges: nonNegativeIntegerSchema,
|
|
73
|
+
coastEdges: nonNegativeIntegerSchema,
|
|
74
|
+
alignment: z.number().nullable(),
|
|
75
|
+
baseRate: z.number(),
|
|
76
|
+
enrichment: z.number().nullable(),
|
|
77
|
+
naturalShare: z.number().nullable(),
|
|
78
|
+
naturalShareWithCoast: z.number().nullable(),
|
|
79
|
+
straightFraction: z.number().nullable(),
|
|
80
|
+
longRunShare: z.number().nullable(),
|
|
81
|
+
artificialRuns: nonNegativeIntegerSchema,
|
|
82
|
+
longestArtificialRunEdges: nonNegativeIntegerSchema,
|
|
83
|
+
worstRuns: z.array(borderRunSchema).max(3),
|
|
84
|
+
})
|
|
85
|
+
.strict();
|
|
86
|
+
const borderQualitySummarySchema = z
|
|
87
|
+
.object({
|
|
88
|
+
inputs: z
|
|
89
|
+
.object({
|
|
90
|
+
heights: z.boolean(),
|
|
91
|
+
rivers: z.boolean(),
|
|
92
|
+
terrain: z.boolean(),
|
|
93
|
+
extra: z.boolean(),
|
|
94
|
+
})
|
|
95
|
+
.strict(),
|
|
96
|
+
skippedInputs: z
|
|
97
|
+
.array(z.object({ input: z.string().max(32), reason: z.string().max(256) }).strict())
|
|
98
|
+
.max(8),
|
|
99
|
+
layers: z.array(borderLayerSummarySchema).max(8),
|
|
100
|
+
})
|
|
101
|
+
.strict();
|
|
58
102
|
const mapWorkspaceInspectDataSchema = z
|
|
59
103
|
.object({
|
|
60
104
|
revision: sha256Schema,
|
|
@@ -82,6 +126,7 @@ const mapWorkspaceInspectDataSchema = z
|
|
|
82
126
|
.object({ references: nonNegativeIntegerSchema, missing: nonNegativeIntegerSchema })
|
|
83
127
|
.strict()
|
|
84
128
|
.optional(),
|
|
129
|
+
quality: borderQualitySummarySchema.optional(),
|
|
85
130
|
})
|
|
86
131
|
.strict();
|
|
87
132
|
const mapRenderDataSchema = z
|
|
@@ -116,6 +161,29 @@ const mapPlanOutputSchema = strictOperationResultSchema(z
|
|
|
116
161
|
semanticDiffTruncated: z.boolean(),
|
|
117
162
|
fileCount: nonNegativeIntegerSchema,
|
|
118
163
|
artifactCount: nonNegativeIntegerSchema,
|
|
164
|
+
geography: z
|
|
165
|
+
.array(z
|
|
166
|
+
.object({
|
|
167
|
+
operationId: z.string().min(1).max(256),
|
|
168
|
+
modelHash: sha256Schema,
|
|
169
|
+
valid: z.boolean(),
|
|
170
|
+
source: z.enum(['inline', 'file']),
|
|
171
|
+
sourceSha256: sha256Schema,
|
|
172
|
+
width: nonNegativeIntegerSchema,
|
|
173
|
+
height: nonNegativeIntegerSchema,
|
|
174
|
+
featureCount: nonNegativeIntegerSchema,
|
|
175
|
+
pointCount: nonNegativeIntegerSchema,
|
|
176
|
+
counts: z.record(z.string().max(32), nonNegativeIntegerSchema),
|
|
177
|
+
errorCount: nonNegativeIntegerSchema,
|
|
178
|
+
warningCount: nonNegativeIntegerSchema,
|
|
179
|
+
synthesis: z
|
|
180
|
+
.enum(['available', 'unavailable'])
|
|
181
|
+
.describe('available: a request without preview builds relief, rivers and lakes from the model; unavailable: the model declares parts the synthesis cannot use yet (see unsupported) and such a request is blocked'),
|
|
182
|
+
unsupported: z.array(z.string().max(128)).max(16),
|
|
183
|
+
})
|
|
184
|
+
.strict())
|
|
185
|
+
.max(16)
|
|
186
|
+
.optional(),
|
|
119
187
|
preview: z
|
|
120
188
|
.object({
|
|
121
189
|
offlineRepresentation: z.literal(true),
|
|
@@ -149,7 +217,7 @@ export const mapTaskTools = [
|
|
|
149
217
|
{
|
|
150
218
|
name: 'hoi4.map_rewrite',
|
|
151
219
|
title: 'Build a world or edit the map',
|
|
152
|
-
description: 'Build a world from a
|
|
220
|
+
description: 'Build a world from a reference image or a geography model (geography, geographyPath), or edit provinces, states, regions and supply. Example: {"preview":true,"operations":[{"kind":"create_world","reference":{"imagePath":"references/world.png"}}]}. Preview first to check coastlines, terrain and countries, or a model overlay and report; omit preview to write the generated map and starting scenario (models with biomes or climate are blocked until that stage exists). Returns counts, diagnostics and linked PNGs.',
|
|
153
221
|
inputSchema: mapRewriteRequestSchema,
|
|
154
222
|
outputSchema: mapPlanOutputSchema,
|
|
155
223
|
annotations: {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { hashCanonical } from '../core/canonical.js';
|
|
2
2
|
import { parseClausewitz } from '../core/source/index.js';
|
|
3
|
-
import { requireOperationScope,
|
|
3
|
+
import { requireOperationScope, resolveOperationWorkspaceForSource, } from '../core/operation-context.js';
|
|
4
4
|
import { emptyServiceResult } from '../core/result.js';
|
|
5
5
|
import { toolResult } from '../core/operation-result.js';
|
|
6
6
|
import { referenceContextRequestSchema, referenceReadRequestSchema, referenceSearchRequestSchema, sourceLookupRequestSchema, } from '../schemas/reference.js';
|
|
@@ -66,7 +66,8 @@ export class ReferenceToolService {
|
|
|
66
66
|
async call(name, input, context, signal) {
|
|
67
67
|
requireOperationScope(context, 'hoi4:read');
|
|
68
68
|
const parsed = schemas[name].parse(input);
|
|
69
|
-
|
|
69
|
+
// A validated file names its mod when the client's roots and working directory do not.
|
|
70
|
+
const workspaceId = await resolveOperationWorkspaceForSource(this.engine, context, parsed.workspaceId, name === 'hoi4.script_validate' ? scriptValidateRequestSchema.parse(parsed).path : undefined, signal);
|
|
70
71
|
const workspace = this.engine.resolver.get(workspaceId, context.principal);
|
|
71
72
|
const data = name === 'hoi4.reference_search'
|
|
72
73
|
? await this.references.search(workspace, referenceSearchRequestSchema.parse(parsed), signal)
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
import { workspaceRelativePathSchema } from './common.js';
|
|
3
|
+
/**
|
|
4
|
+
* Geography model, version 1: a typed feature list that describes a world before any pixel exists.
|
|
5
|
+
* Coordinates are normalised to the whole world frame, origin top-left, x east, y south, exactly
|
|
6
|
+
* like `reference.labels`. Lengths (radii, widths) are fractions of the map width, so a circle stays
|
|
7
|
+
* a circle at every aspect ratio. Heights are 0 to 1 of the relief scale. Every array is optional.
|
|
8
|
+
*/
|
|
9
|
+
export const GEOGRAPHY_VERSION = 1;
|
|
10
|
+
/** A model file or inline model larger than this is refused before parsing. */
|
|
11
|
+
export const GEOGRAPHY_SOURCE_MAX_BYTES = 4_194_304;
|
|
12
|
+
export const GEOGRAPHY_MAX_FEATURES = 4_000;
|
|
13
|
+
export const GEOGRAPHY_MAX_POINTS = 200_000;
|
|
14
|
+
export const GEOGRAPHY_MAX_RING_VERTICES = 2_000;
|
|
15
|
+
export const GEOGRAPHY_MAX_PATH_VERTICES = 2_000;
|
|
16
|
+
// eslint-disable-next-line no-control-regex -- portable JSON Schema pattern rejects grammar-breaking controls.
|
|
17
|
+
const namePattern = /^[^\u0000-\u001f\u007f]+$/u;
|
|
18
|
+
const nameSchema = z
|
|
19
|
+
.string()
|
|
20
|
+
.min(1)
|
|
21
|
+
.max(64)
|
|
22
|
+
.regex(namePattern, 'Names must not contain control characters');
|
|
23
|
+
const tagSchema = z.string().min(1).max(48);
|
|
24
|
+
const unit = z.number().min(0).max(1);
|
|
25
|
+
const positiveFraction = (max) => z.number().gt(0).max(max);
|
|
26
|
+
/** Feature identifiers are unique across the whole model, so a reference needs no kind prefix. */
|
|
27
|
+
export const geographyFeatureIdSchema = z
|
|
28
|
+
.string()
|
|
29
|
+
.regex(/^[A-Za-z][A-Za-z0-9_.-]{0,63}$/u, 'Feature ids start with a letter and use letters, digits, underscore, dot or hyphen (at most 64 characters)');
|
|
30
|
+
const pointObject = z.object({ x: unit, y: unit }).strict();
|
|
31
|
+
/** A point is `{x, y}` or the compact pair `[x, y]`. */
|
|
32
|
+
export const geographyPointSchema = z.union([pointObject, z.tuple([unit, unit])]);
|
|
33
|
+
const pathPointObject = z
|
|
34
|
+
.object({
|
|
35
|
+
x: unit,
|
|
36
|
+
y: unit,
|
|
37
|
+
halfWidth: positiveFraction(0.25).optional(),
|
|
38
|
+
peak: unit.optional(),
|
|
39
|
+
})
|
|
40
|
+
.strict();
|
|
41
|
+
/** A path vertex may override the range half-width and crest height at that vertex. */
|
|
42
|
+
export const geographyPathPointSchema = z.union([pathPointObject, z.tuple([unit, unit])]);
|
|
43
|
+
export const geographyRingSchema = z
|
|
44
|
+
.array(geographyPointSchema)
|
|
45
|
+
.min(3)
|
|
46
|
+
.max(GEOGRAPHY_MAX_RING_VERTICES);
|
|
47
|
+
/** Outer rings (`polygons`) and cut-outs (`holes`); a hole lies inside exactly one polygon. */
|
|
48
|
+
const areaFields = {
|
|
49
|
+
polygons: z.array(geographyRingSchema).min(1).max(64),
|
|
50
|
+
holes: z.array(geographyRingSchema).max(64).optional(),
|
|
51
|
+
};
|
|
52
|
+
const featureBase = {
|
|
53
|
+
id: geographyFeatureIdSchema,
|
|
54
|
+
name: nameSchema.optional(),
|
|
55
|
+
/** Free labels (for example `established` or `proposed`); sorted and de-duplicated. */
|
|
56
|
+
tags: z.array(tagSchema).max(16).optional(),
|
|
57
|
+
note: z.string().max(2_000).optional(),
|
|
58
|
+
};
|
|
59
|
+
export const geographyDepthClassSchema = z.enum(['shelf', 'open', 'abyss']);
|
|
60
|
+
export const geographyBiomeKindSchema = z.enum([
|
|
61
|
+
'forest',
|
|
62
|
+
'desert',
|
|
63
|
+
'marsh',
|
|
64
|
+
'ice',
|
|
65
|
+
'plains',
|
|
66
|
+
'farmland',
|
|
67
|
+
'jungle',
|
|
68
|
+
'steppe',
|
|
69
|
+
'tundra',
|
|
70
|
+
'hills',
|
|
71
|
+
]);
|
|
72
|
+
export const geographySiteKindSchema = z.enum([
|
|
73
|
+
'capital',
|
|
74
|
+
'city',
|
|
75
|
+
'fortress',
|
|
76
|
+
'port',
|
|
77
|
+
'ruin',
|
|
78
|
+
'temple',
|
|
79
|
+
'mine',
|
|
80
|
+
'landmark',
|
|
81
|
+
]);
|
|
82
|
+
export const geographyPlaceKindSchema = z.enum([
|
|
83
|
+
'cape',
|
|
84
|
+
'bay',
|
|
85
|
+
'gulf',
|
|
86
|
+
'peninsula',
|
|
87
|
+
'reef',
|
|
88
|
+
'landmark',
|
|
89
|
+
'other',
|
|
90
|
+
]);
|
|
91
|
+
const seaSchema = z
|
|
92
|
+
.object({
|
|
93
|
+
...featureBase,
|
|
94
|
+
...areaFields,
|
|
95
|
+
depthClass: geographyDepthClassSchema.optional(),
|
|
96
|
+
ice: unit.optional(),
|
|
97
|
+
kind: z.enum(['ocean', 'sea', 'gulf', 'fjord']).optional(),
|
|
98
|
+
})
|
|
99
|
+
.strict();
|
|
100
|
+
const landSchema = z
|
|
101
|
+
.object({
|
|
102
|
+
...featureBase,
|
|
103
|
+
...areaFields,
|
|
104
|
+
kind: z.enum(['continent', 'island', 'archipelago', 'peninsula']).optional(),
|
|
105
|
+
region: geographyFeatureIdSchema.optional(),
|
|
106
|
+
biome: geographyBiomeKindSchema.optional(),
|
|
107
|
+
fertility: unit.optional(),
|
|
108
|
+
elevationBase: unit.optional(),
|
|
109
|
+
coastDetail: unit.optional(),
|
|
110
|
+
})
|
|
111
|
+
.strict();
|
|
112
|
+
/** Target share of each terrain inside a region, 0 to 1 (they need not sum to exactly 1). */
|
|
113
|
+
const terrainSharesSchema = z
|
|
114
|
+
.object({
|
|
115
|
+
plains: unit.optional(),
|
|
116
|
+
forest: unit.optional(),
|
|
117
|
+
hills: unit.optional(),
|
|
118
|
+
mountain: unit.optional(),
|
|
119
|
+
desert: unit.optional(),
|
|
120
|
+
marsh: unit.optional(),
|
|
121
|
+
jungle: unit.optional(),
|
|
122
|
+
urban: unit.optional(),
|
|
123
|
+
snow: unit.optional(),
|
|
124
|
+
})
|
|
125
|
+
.strict();
|
|
126
|
+
const provincesPerStateBound = z.number().int().min(1).max(64);
|
|
127
|
+
const regionSchema = z
|
|
128
|
+
.object({
|
|
129
|
+
...featureBase,
|
|
130
|
+
polygons: areaFields.polygons.optional(),
|
|
131
|
+
holes: areaFields.holes,
|
|
132
|
+
terrain: terrainSharesSchema.optional(),
|
|
133
|
+
/** Typical states (`[smallest, usual, largest]` provinces) and province size in pixels. */
|
|
134
|
+
density: z
|
|
135
|
+
.object({
|
|
136
|
+
provincesPerState: z
|
|
137
|
+
.tuple([provincesPerStateBound, provincesPerStateBound, provincesPerStateBound])
|
|
138
|
+
.optional(),
|
|
139
|
+
provinceAreaPx: z.number().int().min(20).max(50_000).optional(),
|
|
140
|
+
})
|
|
141
|
+
.strict()
|
|
142
|
+
.optional(),
|
|
143
|
+
})
|
|
144
|
+
.strict();
|
|
145
|
+
const rangeSchema = z
|
|
146
|
+
.object({
|
|
147
|
+
...featureBase,
|
|
148
|
+
path: z.array(geographyPathPointSchema).min(2).max(GEOGRAPHY_MAX_PATH_VERTICES),
|
|
149
|
+
closed: z.boolean().optional(),
|
|
150
|
+
halfWidth: positiveFraction(0.25).optional(),
|
|
151
|
+
peak: unit.optional(),
|
|
152
|
+
profile: z.enum(['ridge', 'massif', 'escarpment', 'wall']).optional(),
|
|
153
|
+
asymmetry: z.number().min(-1).max(1).optional(),
|
|
154
|
+
snowline: unit.optional(),
|
|
155
|
+
spurs: unit.optional(),
|
|
156
|
+
barrier: z.enum(['none', 'impassable', 'fort-line']).optional(),
|
|
157
|
+
})
|
|
158
|
+
.strict();
|
|
159
|
+
/**
|
|
160
|
+
* A gap in a range. Give `range` and `at` (0 to 1 along the range by arc length), or give a
|
|
161
|
+
* `position` point and let the tool find the range (the named `range`, else the nearest one) and
|
|
162
|
+
* the position along it.
|
|
163
|
+
*/
|
|
164
|
+
const passSchema = z
|
|
165
|
+
.object({
|
|
166
|
+
...featureBase,
|
|
167
|
+
range: geographyFeatureIdSchema.optional(),
|
|
168
|
+
at: unit.optional(),
|
|
169
|
+
position: geographyPointSchema.optional(),
|
|
170
|
+
width: positiveFraction(0.1).optional(),
|
|
171
|
+
depth: unit.optional(),
|
|
172
|
+
fort: z.number().int().min(0).max(5).optional(),
|
|
173
|
+
})
|
|
174
|
+
.strict();
|
|
175
|
+
const volcanoSchema = z
|
|
176
|
+
.object({
|
|
177
|
+
...featureBase,
|
|
178
|
+
at: geographyPointSchema,
|
|
179
|
+
radius: positiveFraction(0.25).optional(),
|
|
180
|
+
peak: unit.optional(),
|
|
181
|
+
crater: positiveFraction(0.25).optional(),
|
|
182
|
+
active: z.boolean().optional(),
|
|
183
|
+
})
|
|
184
|
+
.strict();
|
|
185
|
+
const plateauSchema = z
|
|
186
|
+
.object({
|
|
187
|
+
...featureBase,
|
|
188
|
+
...areaFields,
|
|
189
|
+
/** Absolute relief inside the plateau; a value below the surroundings makes a basin or chasm. */
|
|
190
|
+
height: unit,
|
|
191
|
+
edge: z.enum(['cliff', 'ramp']).optional(),
|
|
192
|
+
})
|
|
193
|
+
.strict();
|
|
194
|
+
/** A river end is a point, a lake (`{lake}`), a sea (`{sea}`, mouths only) or a river (`{river}`, mouths only). */
|
|
195
|
+
const riverSourceSchema = z.union([
|
|
196
|
+
geographyPointSchema,
|
|
197
|
+
z.object({ lake: geographyFeatureIdSchema }).strict(),
|
|
198
|
+
]);
|
|
199
|
+
const riverMouthSchema = z.union([
|
|
200
|
+
geographyPointSchema,
|
|
201
|
+
z.object({ sea: geographyFeatureIdSchema }).strict(),
|
|
202
|
+
z.object({ lake: geographyFeatureIdSchema }).strict(),
|
|
203
|
+
z.object({ river: geographyFeatureIdSchema }).strict(),
|
|
204
|
+
]);
|
|
205
|
+
const riverSchema = z
|
|
206
|
+
.object({
|
|
207
|
+
...featureBase,
|
|
208
|
+
source: riverSourceSchema,
|
|
209
|
+
mouth: riverMouthSchema,
|
|
210
|
+
via: z.array(geographyPointSchema).max(512).optional(),
|
|
211
|
+
size: z.number().int().min(1).max(5).optional(),
|
|
212
|
+
delta: z.boolean().optional(),
|
|
213
|
+
})
|
|
214
|
+
.strict();
|
|
215
|
+
const lakeSchema = z
|
|
216
|
+
.object({
|
|
217
|
+
...featureBase,
|
|
218
|
+
polygons: areaFields.polygons.optional(),
|
|
219
|
+
holes: areaFields.holes,
|
|
220
|
+
center: geographyPointSchema.optional(),
|
|
221
|
+
radius: positiveFraction(0.25).optional(),
|
|
222
|
+
depthClass: geographyDepthClassSchema.optional(),
|
|
223
|
+
outflow: geographyFeatureIdSchema.optional(),
|
|
224
|
+
gameplay: z.enum(['lake', 'inland-sea']).optional(),
|
|
225
|
+
})
|
|
226
|
+
.strict();
|
|
227
|
+
const biomeSchema = z
|
|
228
|
+
.object({
|
|
229
|
+
...featureBase,
|
|
230
|
+
...areaFields,
|
|
231
|
+
kind: geographyBiomeKindSchema,
|
|
232
|
+
density: unit.optional(),
|
|
233
|
+
fertility: unit.optional(),
|
|
234
|
+
/** Overlap priority: higher wins, and two different kinds may only overlap at different values. */
|
|
235
|
+
z: z.number().int().min(-1_000).max(1_000).optional(),
|
|
236
|
+
})
|
|
237
|
+
.strict();
|
|
238
|
+
const strait = z.union([
|
|
239
|
+
geographyPointSchema,
|
|
240
|
+
z.object({ land: geographyFeatureIdSchema }).strict(),
|
|
241
|
+
]);
|
|
242
|
+
/**
|
|
243
|
+
* A channel between two lands. Give `between` (two points on land, or land ids), a `path` through
|
|
244
|
+
* the water, or both; with only a path the lands nearest its two ends are used.
|
|
245
|
+
*/
|
|
246
|
+
const straitSchema = z
|
|
247
|
+
.object({
|
|
248
|
+
...featureBase,
|
|
249
|
+
between: z.tuple([strait, strait]).optional(),
|
|
250
|
+
path: z.array(geographyPointSchema).min(2).max(64).optional(),
|
|
251
|
+
rule: z.enum(['open', 'army-closed', 'navy-closed']).optional(),
|
|
252
|
+
width: positiveFraction(0.1).optional(),
|
|
253
|
+
})
|
|
254
|
+
.strict();
|
|
255
|
+
const siteSchema = z
|
|
256
|
+
.object({
|
|
257
|
+
...featureBase,
|
|
258
|
+
kind: geographySiteKindSchema,
|
|
259
|
+
at: geographyPointSchema,
|
|
260
|
+
importance: z.number().int().min(1).max(5).optional(),
|
|
261
|
+
country: z
|
|
262
|
+
.string()
|
|
263
|
+
.regex(/^[A-Z][A-Z0-9]{2}$/u)
|
|
264
|
+
.optional(),
|
|
265
|
+
/** The site stands in water (a landing, a shrine on a reef) and need not be on land. */
|
|
266
|
+
onWater: z.boolean().optional(),
|
|
267
|
+
/** A lake the site belongs to, for a port on a lake shore. */
|
|
268
|
+
lake: geographyFeatureIdSchema.optional(),
|
|
269
|
+
})
|
|
270
|
+
.strict();
|
|
271
|
+
const roadSchema = z
|
|
272
|
+
.object({
|
|
273
|
+
...featureBase,
|
|
274
|
+
path: z.array(geographyPointSchema).min(2).max(GEOGRAPHY_MAX_PATH_VERTICES),
|
|
275
|
+
importance: z.number().int().min(1).max(3).optional(),
|
|
276
|
+
})
|
|
277
|
+
.strict();
|
|
278
|
+
/** A named point of any surface (cape, bay, gulf, reef, landmark) that gives later stages a name to use. */
|
|
279
|
+
const placeSchema = z
|
|
280
|
+
.object({
|
|
281
|
+
...featureBase,
|
|
282
|
+
kind: geographyPlaceKindSchema,
|
|
283
|
+
at: geographyPointSchema,
|
|
284
|
+
})
|
|
285
|
+
.strict();
|
|
286
|
+
const climateSchema = z
|
|
287
|
+
.object({
|
|
288
|
+
/** Compass bearing the prevailing wind blows from: 0 north, 90 east, 180 south, 270 west. */
|
|
289
|
+
windFrom: z.number().min(0).lt(360).optional(),
|
|
290
|
+
/** Mean annual temperature in degrees Celsius at the warmest and coldest latitude bands. */
|
|
291
|
+
temperature: z
|
|
292
|
+
.object({ warm: z.number().min(-60).max(60), cold: z.number().min(-90).max(40) })
|
|
293
|
+
.strict()
|
|
294
|
+
.optional(),
|
|
295
|
+
/** y of the warmest latitude band, 0 north to 1 south. */
|
|
296
|
+
warmBandY: unit.optional(),
|
|
297
|
+
rainScale: z.number().min(0.1).max(4).optional(),
|
|
298
|
+
seasonality: unit.optional(),
|
|
299
|
+
})
|
|
300
|
+
.strict();
|
|
301
|
+
export const geographyModelSchema = z
|
|
302
|
+
.object({
|
|
303
|
+
version: z.literal(GEOGRAPHY_VERSION),
|
|
304
|
+
name: nameSchema.optional(),
|
|
305
|
+
description: z.string().max(2_000).optional(),
|
|
306
|
+
seas: z.array(seaSchema).max(GEOGRAPHY_MAX_FEATURES).optional(),
|
|
307
|
+
lands: z.array(landSchema).max(GEOGRAPHY_MAX_FEATURES).optional(),
|
|
308
|
+
regions: z.array(regionSchema).max(GEOGRAPHY_MAX_FEATURES).optional(),
|
|
309
|
+
ranges: z.array(rangeSchema).max(GEOGRAPHY_MAX_FEATURES).optional(),
|
|
310
|
+
passes: z.array(passSchema).max(GEOGRAPHY_MAX_FEATURES).optional(),
|
|
311
|
+
volcanoes: z.array(volcanoSchema).max(GEOGRAPHY_MAX_FEATURES).optional(),
|
|
312
|
+
plateaus: z.array(plateauSchema).max(GEOGRAPHY_MAX_FEATURES).optional(),
|
|
313
|
+
rivers: z.array(riverSchema).max(GEOGRAPHY_MAX_FEATURES).optional(),
|
|
314
|
+
lakes: z.array(lakeSchema).max(GEOGRAPHY_MAX_FEATURES).optional(),
|
|
315
|
+
biomes: z.array(biomeSchema).max(GEOGRAPHY_MAX_FEATURES).optional(),
|
|
316
|
+
straits: z.array(straitSchema).max(GEOGRAPHY_MAX_FEATURES).optional(),
|
|
317
|
+
sites: z.array(siteSchema).max(GEOGRAPHY_MAX_FEATURES).optional(),
|
|
318
|
+
roads: z.array(roadSchema).max(GEOGRAPHY_MAX_FEATURES).optional(),
|
|
319
|
+
places: z.array(placeSchema).max(GEOGRAPHY_MAX_FEATURES).optional(),
|
|
320
|
+
climate: climateSchema.optional(),
|
|
321
|
+
})
|
|
322
|
+
.strict()
|
|
323
|
+
.describe('Geography model version 1. Normalized 0..1 coordinates over the whole world frame, origin top-left; lengths are fractions of the map width.');
|
|
324
|
+
/** A workspace-relative `.json` file holding a geography model. */
|
|
325
|
+
export const geographyPathSchema = workspaceRelativePathSchema.refine((value) => !value.includes(':') && /\.json$/iu.test(value), 'Geography must be a workspace-relative .json file');
|
|
@@ -31,6 +31,7 @@ export const mapOverlaySchema = z.enum([
|
|
|
31
31
|
'building-positions',
|
|
32
32
|
'unit-positions',
|
|
33
33
|
'weather-positions',
|
|
34
|
+
'border-quality',
|
|
34
35
|
]);
|
|
35
36
|
const mapTileSchema = z
|
|
36
37
|
.object({
|
|
@@ -97,10 +98,18 @@ export const mapInspectRequestSchema = z
|
|
|
97
98
|
.boolean()
|
|
98
99
|
.default(false)
|
|
99
100
|
.describe('Also check events, focuses, decisions, history and AI areas for states, provinces and regions the map lacks, and state tags without a country.'),
|
|
101
|
+
quality: z
|
|
102
|
+
.boolean()
|
|
103
|
+
.default(false)
|
|
104
|
+
.describe('Also measure how well province, state, region and country borders follow rivers, slopes, lake shores and terrain changes: enrichment over chance, the natural share of border length, straightness, and the worst artificial runs. Reads the height, river and terrain bitmaps; full inspection only.'),
|
|
100
105
|
layer: mapLayerSchema
|
|
101
106
|
.optional()
|
|
102
107
|
.describe('Render this base layer; combine with overlays, tile or area.'),
|
|
103
|
-
overlays: z
|
|
108
|
+
overlays: z
|
|
109
|
+
.array(mapOverlaySchema)
|
|
110
|
+
.max(12)
|
|
111
|
+
.optional()
|
|
112
|
+
.describe('Render these overlays on the base layer. border-quality colours state borders by whether they follow a natural feature and highlights the worst artificial runs.'),
|
|
104
113
|
scale: z.number().int().min(1).max(16).optional(),
|
|
105
114
|
tile: mapTileSchema.optional().describe('Render one top-left bitmap area.'),
|
|
106
115
|
area: mapAreaSchema
|
|
@@ -115,7 +124,16 @@ export const mapInspectRequestSchema = z
|
|
|
115
124
|
path: ['area'],
|
|
116
125
|
message: 'Choose tile or area, not both',
|
|
117
126
|
});
|
|
118
|
-
|
|
127
|
+
const viewRequested = MAP_VIEW_FIELDS.some((field) => value[field] !== undefined);
|
|
128
|
+
if (value.quality && (viewRequested || value.lookupOnly))
|
|
129
|
+
context.addIssue({
|
|
130
|
+
code: 'custom',
|
|
131
|
+
path: ['quality'],
|
|
132
|
+
message: viewRequested
|
|
133
|
+
? 'quality belongs to full inspection; request the border-quality overlay for a rendered view'
|
|
134
|
+
: 'quality needs full inspection; remove lookupOnly',
|
|
135
|
+
});
|
|
136
|
+
if (!viewRequested)
|
|
119
137
|
return;
|
|
120
138
|
for (const field of MAP_LOOKUP_FIELDS) {
|
|
121
139
|
const fieldValue = value[field];
|