hoi4-agent-tools 2.5.2 → 2.6.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +5 -5
  3. package/dist/hoi4_agent_tools/core/index.js +22 -6
  4. package/dist/hoi4_agent_tools/focus/compact.js +103 -12
  5. package/dist/hoi4_agent_tools/focus/continuous-render.js +4 -10
  6. package/dist/hoi4_agent_tools/focus/layout.js +15 -7
  7. package/dist/hoi4_agent_tools/focus/render.js +2 -8
  8. package/dist/hoi4_agent_tools/gui/dds.js +5 -2
  9. package/dist/hoi4_agent_tools/gui/layout.js +1 -1
  10. package/dist/hoi4_agent_tools/gui/renderer.js +4 -16
  11. package/dist/hoi4_agent_tools/gui/source-graph.js +9 -8
  12. package/dist/hoi4_agent_tools/gui/studio.js +58 -4
  13. package/dist/hoi4_agent_tools/map/catalog.js +398 -0
  14. package/dist/hoi4_agent_tools/map/index.js +1 -0
  15. package/dist/hoi4_agent_tools/map/operations.js +337 -30
  16. package/dist/hoi4_agent_tools/map/render.js +40 -147
  17. package/dist/hoi4_agent_tools/map/service.js +1 -0
  18. package/dist/hoi4_agent_tools/mcp/tools/map.js +85 -9
  19. package/dist/hoi4_agent_tools/mcp/transports/http.js +4 -5
  20. package/dist/hoi4_agent_tools/mcp/transports/protocol-gate.js +7 -7
  21. package/dist/hoi4_agent_tools/schemas/map.js +69 -2
  22. package/dist/hoi4_agent_tools/technology/render.js +65 -19
  23. package/dist/hoi4_agent_tools/technology/service.js +67 -3
  24. package/dist/hoi4_agent_tools/version.js +1 -1
  25. package/docs/focus.md +1 -1
  26. package/docs/gui.md +2 -0
  27. package/docs/map.md +60 -23
  28. package/docs/technology.md +1 -1
  29. package/package.json +3 -2
  30. package/schemas/configuration.schema.json +1 -1
  31. package/schemas/continuous-focus-palette.schema.json +1 -1
  32. package/schemas/custom-weighted-pool.schema.json +1 -1
  33. package/schemas/event-feature-manifest.schema.json +1 -1
  34. package/schemas/focus-plan.schema.json +1 -1
  35. package/schemas/focus-planning-sidecar.schema.json +1 -1
  36. package/schemas/gui-animation-source.schema.json +1 -1
  37. package/schemas/gui-helper.schema.json +1 -1
  38. package/schemas/gui-scenario.schema.json +1 -1
  39. package/schemas/map-operation.schema.json +228 -74
  40. package/schemas/operation-result.schema.json +1 -1
  41. package/schemas/probability-analysis-result.schema.json +1 -1
  42. package/schemas/probability-compare-input.schema.json +1 -1
  43. package/schemas/probability-evaluate-input.schema.json +1 -1
  44. package/schemas/probability-inspect-input.schema.json +1 -1
  45. package/schemas/probability-render-input.schema.json +1 -1
  46. package/schemas/probability-scenario-set.schema.json +1 -1
  47. package/schemas/probability-sequence-input.schema.json +1 -1
  48. package/schemas/probability-simulate-input.schema.json +1 -1
  49. package/schemas/probability-sweep-input.schema.json +1 -1
  50. package/server.json +2 -2
@@ -11,6 +11,7 @@ import { ServiceError } from '../core/result.js';
11
11
  import { WorkspaceScanner } from '../core/scanner.js';
12
12
  import { applyReplacements, decodeSource, encodeSource, parseClausewitz, parseLocalisation, SOURCE_MAX_BYTES, sourcePartialLimitDiagnostics, } from '../core/source/index.js';
13
13
  import { readDependenciesFromScannedFiles, } from '../core/transactions.js';
14
+ import { TRANSACTION_MAX_DIAGNOSTICS } from '../core/transaction-limits.js';
14
15
  import { PACKAGE_VERSION } from '../version.js';
15
16
  import { isSafeAnimationSourcePath } from './animation-manifest.js';
16
17
  import { GuiAssetCatalog, parseBmFont } from './assets.js';
@@ -30,6 +31,43 @@ const staticGuiDefinitionPatterns = [
30
31
  ];
31
32
  export const GUI_TEXT_PACKAGE_MAX_FILES = 32;
32
33
  export const GUI_TEXT_PACKAGE_MAX_BYTES = 16_777_216;
34
+ const GUI_TRANSACTION_PHASE_DIAGNOSTIC_BUDGET = Math.floor(TRANSACTION_MAX_DIAGNOSTICS / 3);
35
+ function hardDiagnostic(diagnostic) {
36
+ return diagnostic.severity === 'error' || diagnostic.severity === 'blocker';
37
+ }
38
+ export function boundedGuiTransactionDiagnostics(diagnostics, maximumDiagnostics, evidenceArtifactName) {
39
+ if (diagnostics.length <= maximumDiagnostics)
40
+ return [...diagnostics];
41
+ if (maximumDiagnostics <= 0)
42
+ return [];
43
+ const retainedLimit = maximumDiagnostics - 1;
44
+ const hard = diagnostics.filter(hardDiagnostic);
45
+ const retainedHard = hard.slice(0, retainedLimit);
46
+ const retainedHardSet = new Set(retainedHard);
47
+ const retainedSoft = diagnostics
48
+ .filter((diagnostic) => !hardDiagnostic(diagnostic) && !retainedHardSet.has(diagnostic))
49
+ .slice(0, retainedLimit - retainedHard.length);
50
+ const omittedHard = hard.length - retainedHard.length;
51
+ return [
52
+ ...retainedHard,
53
+ ...retainedSoft,
54
+ {
55
+ code: 'GUI_REWRITE_DIAGNOSTICS_TRUNCATED',
56
+ severity: omittedHard > 0 ? 'blocker' : 'warning',
57
+ category: 'validation',
58
+ message: evidenceArtifactName === undefined
59
+ ? `GUI rewrite validation produced ${diagnostics.length} diagnostics; transaction evidence retains the highest-priority ${maximumDiagnostics}`
60
+ : `GUI rewrite validation produced ${diagnostics.length} diagnostics; complete evidence is stored in ${evidenceArtifactName}`,
61
+ details: {
62
+ total: diagnostics.length,
63
+ returned: maximumDiagnostics,
64
+ omitted: diagnostics.length - retainedLimit,
65
+ omittedHard,
66
+ ...(evidenceArtifactName === undefined ? {} : { evidenceArtifactName }),
67
+ },
68
+ },
69
+ ];
70
+ }
33
71
  function normalizeConfiguredRoot(value) {
34
72
  const normalized = value.replaceAll('\\', '/').replace(/^\.\//u, '').replace(/\/$/u, '');
35
73
  if (!isSafeAnimationSourcePath(normalized)) {
@@ -439,7 +477,7 @@ export function guiArtifactProvenance(graph, kind, primaryScene, scenarioScenes
439
477
  })),
440
478
  },
441
479
  metadata: {
442
- rendererLabel: 'OFFLINE APPROXIMATION · NOT HOI4',
480
+ rendererLabel: 'HOI4 Agent Tools deterministic GUI render',
443
481
  ...metadata,
444
482
  sourceHashInventory: sourceEvidence.inventory,
445
483
  },
@@ -859,6 +897,7 @@ export class ScriptedGuiStudio {
859
897
  const preflightDiagnostics = [];
860
898
  const preflightChecks = [];
861
899
  const preflightArtifacts = [];
900
+ let rewriteEvidenceArtifactName;
862
901
  let dependencyFiles = baseline.files;
863
902
  if (input.windowName !== undefined && input.scenario !== undefined) {
864
903
  let proposedFiles = overlayGuiTextPackageFiles(baseline.files, prepared);
@@ -942,6 +981,7 @@ export class ScriptedGuiStudio {
942
981
  }
943
982
  const comparison = await compareGuiImages(baselinePng, proposedImage.png, renderBudget, input.signal);
944
983
  const slug = safeSlug(input.windowName);
984
+ rewriteEvidenceArtifactName = `${slug}-rewrite-validation.json`;
945
985
  const metadata = {
946
986
  relativePath: input.relativePath,
947
987
  packageFiles: prepared.map(({ relativePath }) => relativePath),
@@ -982,10 +1022,24 @@ export class ScriptedGuiStudio {
982
1022
  })}\n`,
983
1023
  provenance: guiArtifactProvenance(proposedGraph, 'gui-proposed-fidelity', proposedScene, [proposedScene], metadata),
984
1024
  },
1025
+ {
1026
+ name: rewriteEvidenceArtifactName,
1027
+ mimeType: 'application/json',
1028
+ content: `${canonicalJson({
1029
+ sourceDiagnostics: proposedSourceDiagnostics,
1030
+ graphDiagnostics: proposedGraph.diagnostics,
1031
+ sceneDiagnostics: proposedScene.diagnostics,
1032
+ validation: proposedValidation,
1033
+ transactionDiagnostics: preflightDiagnostics,
1034
+ transactionChecks: preflightChecks,
1035
+ })}\n`,
1036
+ provenance: guiArtifactProvenance(proposedGraph, 'gui-rewrite-validation', proposedScene, [proposedScene], metadata),
1037
+ },
985
1038
  ];
986
1039
  const stored = await this.artifacts.withAtomicWrites(workspace, writes, (artifacts) => Promise.resolve([...artifacts]), input.signal);
987
1040
  preflightArtifacts.push(...stored.map(publicArtifactLink));
988
1041
  }
1042
+ const transactionPreflightDiagnostics = boundedGuiTransactionDiagnostics(preflightDiagnostics, GUI_TRANSACTION_PHASE_DIAGNOSTIC_BUDGET, rewriteEvidenceArtifactName);
989
1043
  const operationId = deterministicId('gui_source_change', {
990
1044
  relativePath: input.relativePath,
991
1045
  files: prepared.map(({ relativePath, bytes }) => ({
@@ -1020,7 +1074,7 @@ export class ScriptedGuiStudio {
1020
1074
  })),
1021
1075
  readDependencies: readDependenciesFromScannedFiles(dependencyFiles),
1022
1076
  artifacts: preflightArtifacts,
1023
- diagnostics: preflightDiagnostics,
1077
+ diagnostics: transactionPreflightDiagnostics,
1024
1078
  validate: (proposed, signal) => {
1025
1079
  const diagnostics = [];
1026
1080
  let present = true;
@@ -1049,7 +1103,7 @@ export class ScriptedGuiStudio {
1049
1103
  diagnostics.push(...blocking.map((diagnostic) => ({ ...diagnostic, operationId })));
1050
1104
  }
1051
1105
  return Promise.resolve({
1052
- diagnostics,
1106
+ diagnostics: boundedGuiTransactionDiagnostics(diagnostics, GUI_TRANSACTION_PHASE_DIAGNOSTIC_BUDGET, rewriteEvidenceArtifactName),
1053
1107
  checks: [
1054
1108
  ...preflightChecks,
1055
1109
  {
@@ -1077,7 +1131,7 @@ export class ScriptedGuiStudio {
1077
1131
  const scanned = await this.scan(workspaceId, principal, validationSignal);
1078
1132
  const passed = !scanned.graph.diagnostics.some(({ severity }) => severity === 'error' || severity === 'blocker');
1079
1133
  return {
1080
- diagnostics: scanned.graph.diagnostics,
1134
+ diagnostics: boundedGuiTransactionDiagnostics(scanned.graph.diagnostics, Math.max(0, TRANSACTION_MAX_DIAGNOSTICS - _manifest.diagnostics.length), _manifest.artifacts.find(({ name }) => name.endsWith('-rewrite-validation.json'))?.name),
1081
1135
  checks: [
1082
1136
  {
1083
1137
  id: 'post-write-gui-graph',
@@ -0,0 +1,398 @@
1
+ import { compareCodeUnits } from '../core/canonical.js';
2
+ import { nodeLocation } from '../core/source/parser.js';
3
+ function localisedValues(entries) {
4
+ return entries
5
+ .map(({ file, entry }) => ({
6
+ language: entry.language,
7
+ value: entry.value,
8
+ sourcePath: file.displayPath,
9
+ line: entry.line,
10
+ loadOrder: file.loadOrder,
11
+ }))
12
+ .sort((left, right) => compareCodeUnits(left.language, right.language) ||
13
+ left.loadOrder - right.loadOrder ||
14
+ compareCodeUnits(left.sourcePath, right.sourcePath) ||
15
+ left.line - right.line)
16
+ .map(({ loadOrder: _loadOrder, ...value }) => value);
17
+ }
18
+ function resolvedName(entriesByKey, keys, fallback) {
19
+ for (const key of keys) {
20
+ const values = localisedValues(entriesByKey.get(key) ?? []);
21
+ if (values.length === 0)
22
+ continue;
23
+ const english = values.filter(({ language }) => language === 'l_english').at(-1);
24
+ return { key, displayName: english?.value ?? values.at(-1)?.value ?? fallback, values };
25
+ }
26
+ const key = keys[0] ?? fallback;
27
+ return { key, displayName: fallback, values: [] };
28
+ }
29
+ function aggregateGeometry(index, provinceIds) {
30
+ const geometries = provinceIds.flatMap((id) => {
31
+ const geometry = index.raster?.geometry.get(id);
32
+ return geometry === undefined ? [] : [geometry];
33
+ });
34
+ if (geometries.length === 0 || index.raster === undefined)
35
+ return null;
36
+ const pixelCount = geometries.reduce((total, { pixelCount: count }) => total + count, 0);
37
+ if (pixelCount <= 0)
38
+ return null;
39
+ const centerX = geometries.reduce((total, geometry) => total + geometry.centerX * geometry.pixelCount, 0) /
40
+ pixelCount;
41
+ const centerY = geometries.reduce((total, geometry) => total + geometry.centerY * geometry.pixelCount, 0) /
42
+ pixelCount;
43
+ return {
44
+ pixelCount,
45
+ minX: geometries.reduce((value, geometry) => Math.min(value, geometry.minX), Infinity),
46
+ minY: geometries.reduce((value, geometry) => Math.min(value, geometry.minY), Infinity),
47
+ maxX: geometries.reduce((value, geometry) => Math.max(value, geometry.maxX), -Infinity),
48
+ maxY: geometries.reduce((value, geometry) => Math.max(value, geometry.maxY), -Infinity),
49
+ centerX,
50
+ centerY,
51
+ mapX: centerX,
52
+ mapZ: index.raster.height - 1 - centerY,
53
+ };
54
+ }
55
+ function sortedObject(values) {
56
+ return Object.fromEntries([...values].sort(([left], [right]) => compareCodeUnits(left, right)));
57
+ }
58
+ export function buildMapCatalog(index) {
59
+ const raster = index.raster;
60
+ const width = raster?.width ?? 0;
61
+ const height = raster?.height ?? 0;
62
+ const localisationEntriesByKey = new Map();
63
+ for (const entry of index.localisationEntries) {
64
+ const values = localisationEntriesByKey.get(entry.entry.key) ?? [];
65
+ values.push(entry);
66
+ localisationEntriesByKey.set(entry.entry.key, values);
67
+ }
68
+ const portsByProvince = new Map(index.ports.map((port) => [port.provinceId, port]));
69
+ const supplyNodeLevelsByProvince = new Map();
70
+ for (const node of index.supplyNodes) {
71
+ const levels = supplyNodeLevelsByProvince.get(node.provinceId) ?? [];
72
+ levels.push(node.level);
73
+ supplyNodeLevelsByProvince.set(node.provinceId, levels);
74
+ }
75
+ const buildingPositionsByProvince = new Map();
76
+ for (const [ordinal, position] of index.buildingPositions.entries()) {
77
+ const provinceId = index.provinceAtMapCoordinate(position.x, position.z);
78
+ if (provinceId === undefined)
79
+ continue;
80
+ const ordinals = buildingPositionsByProvince.get(provinceId) ?? [];
81
+ ordinals.push(ordinal);
82
+ buildingPositionsByProvince.set(provinceId, ordinals);
83
+ }
84
+ const unitPositionsByProvince = new Map();
85
+ for (const [ordinal, position] of index.unitPositions.entries()) {
86
+ const ordinals = unitPositionsByProvince.get(position.provinceId) ?? [];
87
+ ordinals.push(ordinal);
88
+ unitPositionsByProvince.set(position.provinceId, ordinals);
89
+ }
90
+ const railwayOrdinalsByProvince = new Map();
91
+ for (const [ordinal, railway] of index.railways.entries()) {
92
+ for (const provinceId of new Set(railway.provinces)) {
93
+ const ordinals = railwayOrdinalsByProvince.get(provinceId) ?? [];
94
+ ordinals.push(ordinal);
95
+ railwayOrdinalsByProvince.set(provinceId, ordinals);
96
+ }
97
+ }
98
+ const specialAdjacencyOrdinalsByProvince = new Map();
99
+ for (const [ordinal, adjacency] of index.adjacencies.entries()) {
100
+ for (const provinceId of new Set([adjacency.from, adjacency.to, adjacency.through])) {
101
+ if (provinceId < 0)
102
+ continue;
103
+ const ordinals = specialAdjacencyOrdinalsByProvince.get(provinceId) ?? [];
104
+ ordinals.push(ordinal);
105
+ specialAdjacencyOrdinalsByProvince.set(provinceId, ordinals);
106
+ }
107
+ }
108
+ const provinces = [...index.definitions]
109
+ .sort((left, right) => left.id - right.id)
110
+ .map((definition) => {
111
+ const id = definition.id;
112
+ const port = portsByProvince.get(id);
113
+ return {
114
+ kind: 'province',
115
+ id,
116
+ name: resolvedName(localisationEntriesByKey, [`PROV${id}`, `VICTORY_POINTS_${id}`], `Province ${id}`),
117
+ definition: {
118
+ color: definition.color,
119
+ type: definition.type,
120
+ coastal: definition.coastal,
121
+ terrain: definition.terrain,
122
+ continent: definition.continent,
123
+ sourcePath: definition.document.file.displayPath,
124
+ line: definition.line,
125
+ },
126
+ geometry: aggregateGeometry(index, [id]),
127
+ stateIds: index.stateForProvince(id).map(({ id: stateId }) => stateId),
128
+ regionIds: index.regionForProvince(id).map(({ id: regionId }) => regionId),
129
+ neighborProvinceIds: [...(raster?.adjacency.get(id) ?? [])].sort((a, b) => a - b),
130
+ victoryPoints: index.victoryPointsByProvince
131
+ .get(id)
132
+ ?.map(({ stateId, value }) => ({ stateId, value })) ?? [],
133
+ provinceBuildings: index.provinceBuildingsByProvince.get(id)?.map(({ stateId, buildings }) => ({
134
+ stateId,
135
+ buildings: sortedObject(buildings),
136
+ })) ?? [],
137
+ port: port === undefined
138
+ ? null
139
+ : {
140
+ stateId: port.stateId,
141
+ level: port.level,
142
+ coastal: port.coastal,
143
+ adjacentSeaProvinceIds: [...port.adjacentSeaProvinceIds],
144
+ },
145
+ supplyNodeLevels: supplyNodeLevelsByProvince.get(id) ?? [],
146
+ railwayOrdinals: railwayOrdinalsByProvince.get(id) ?? [],
147
+ specialAdjacencyOrdinals: specialAdjacencyOrdinalsByProvince.get(id) ?? [],
148
+ buildingPositionOrdinals: buildingPositionsByProvince.get(id) ?? [],
149
+ unitPositionOrdinals: unitPositionsByProvince.get(id) ?? [],
150
+ };
151
+ });
152
+ const states = [...index.states]
153
+ .sort((left, right) => left.id - right.id)
154
+ .map((state) => ({
155
+ kind: 'state',
156
+ id: state.id,
157
+ name: resolvedName(localisationEntriesByKey, [state.name], state.name),
158
+ geometry: aggregateGeometry(index, state.provinces),
159
+ capital: state.capital ?? null,
160
+ manpower: state.manpower,
161
+ category: state.category,
162
+ provinceIds: [...state.provinces].sort((a, b) => a - b),
163
+ regionIds: [
164
+ ...new Set(state.provinces.flatMap((provinceId) => index.regionForProvince(provinceId).map(({ id }) => id))),
165
+ ].sort((a, b) => a - b),
166
+ resources: sortedObject(state.resources),
167
+ owner: state.owner ?? null,
168
+ controller: state.controller ?? null,
169
+ cores: [...state.cores].sort(compareCodeUnits),
170
+ claims: [...state.claims].sort(compareCodeUnits),
171
+ victoryPoints: state.victoryPoints
172
+ .map(({ provinceId, value }) => ({ provinceId, value }))
173
+ .sort((left, right) => left.provinceId - right.provinceId || left.value - right.value),
174
+ stateBuildings: sortedObject(state.stateBuildings),
175
+ provinceBuildings: Object.fromEntries([...state.provinceBuildings]
176
+ .sort(([left], [right]) => left - right)
177
+ .map(([provinceId, buildings]) => [String(provinceId), sortedObject(buildings)])),
178
+ sourcePath: state.file.displayPath,
179
+ sourceLine: nodeLocation(state.document, state.assignment).start.line,
180
+ }));
181
+ const strategicRegions = [...index.regions]
182
+ .sort((left, right) => left.id - right.id)
183
+ .map((region) => ({
184
+ kind: 'strategic-region',
185
+ id: region.id,
186
+ name: resolvedName(localisationEntriesByKey, [region.name], region.name),
187
+ geometry: aggregateGeometry(index, region.provinces),
188
+ provinceIds: [...region.provinces].sort((a, b) => a - b),
189
+ stateIds: [
190
+ ...new Set(region.provinces.flatMap((provinceId) => index.stateForProvince(provinceId).map(({ id }) => id))),
191
+ ].sort((a, b) => a - b),
192
+ navalTerrain: region.navalTerrain ?? null,
193
+ sourcePath: region.file.displayPath,
194
+ sourceLine: nodeLocation(region.document, region.assignment).start.line,
195
+ }));
196
+ const normalAdjacencies = [...(raster?.adjacency ?? [])]
197
+ .flatMap(([from, neighbors]) => [...neighbors].flatMap((to) => (from < to ? [{ from, to }] : [])))
198
+ .sort((left, right) => left.from - right.from || left.to - right.to);
199
+ return {
200
+ schemaVersion: 2,
201
+ dimensions: { width, height },
202
+ coordinateSystems: {
203
+ pixel: { origin: 'top-left', xDirection: 'right', yDirection: 'down' },
204
+ map: { origin: 'bottom-left', xDirection: 'right', zDirection: 'up' },
205
+ },
206
+ counts: {
207
+ provinces: provinces.length,
208
+ states: states.length,
209
+ strategicRegions: strategicRegions.length,
210
+ normalAdjacencies: normalAdjacencies.length,
211
+ specialAdjacencies: index.adjacencies.length,
212
+ supplyNodes: index.supplyNodes.length,
213
+ railways: index.railways.length,
214
+ ports: index.ports.length,
215
+ victoryPoints: [...index.victoryPointsByProvince.values()].reduce((count, entries) => count + entries.length, 0),
216
+ buildingPositions: index.buildingPositions.length,
217
+ unitPositions: index.unitPositions.length,
218
+ weatherPositions: index.weatherPositions.length,
219
+ entityLocators: index.entityLocators.length,
220
+ },
221
+ provinces,
222
+ states,
223
+ strategicRegions,
224
+ normalAdjacencies,
225
+ specialAdjacencies: index.adjacencies.map((adjacency, ordinal) => ({
226
+ ordinal,
227
+ from: adjacency.from,
228
+ to: adjacency.to,
229
+ type: adjacency.type,
230
+ through: adjacency.through,
231
+ startX: adjacency.startX,
232
+ startY: adjacency.startY,
233
+ stopX: adjacency.stopX,
234
+ stopY: adjacency.stopY,
235
+ rule: adjacency.rule,
236
+ comment: adjacency.comment,
237
+ sourcePath: adjacency.document.file.displayPath,
238
+ line: adjacency.line,
239
+ })),
240
+ supplyNodes: index.supplyNodes.map((node, ordinal) => ({
241
+ ordinal,
242
+ level: node.level,
243
+ provinceId: node.provinceId,
244
+ sourcePath: node.document.file.displayPath,
245
+ line: node.line,
246
+ })),
247
+ railways: index.railways.map((railway, ordinal) => ({
248
+ ordinal,
249
+ level: railway.level,
250
+ declaredCount: railway.declaredCount,
251
+ provinceIds: [...railway.provinces],
252
+ sourcePath: railway.document.file.displayPath,
253
+ line: railway.line,
254
+ })),
255
+ ports: index.ports.map(({ stateId, provinceId, level, coastal, adjacentSeaProvinceIds }) => ({
256
+ stateId,
257
+ provinceId,
258
+ level,
259
+ coastal,
260
+ adjacentSeaProvinceIds: [...adjacentSeaProvinceIds].sort((left, right) => left - right),
261
+ })),
262
+ victoryPoints: [...index.victoryPointsByProvince.values()]
263
+ .flatMap((entries) => entries.map(({ stateId, provinceId, value }) => ({ stateId, provinceId, value })))
264
+ .sort((left, right) => left.stateId - right.stateId ||
265
+ left.provinceId - right.provinceId ||
266
+ left.value - right.value),
267
+ buildingPositions: index.buildingPositions.map((position, ordinal) => ({
268
+ ordinal,
269
+ stateId: position.stateId,
270
+ building: position.building,
271
+ x: position.x,
272
+ y: position.y,
273
+ z: position.z,
274
+ rotation: position.rotation,
275
+ adjacentSeaProvince: position.adjacentSeaProvince,
276
+ sourcePath: position.document.file.displayPath,
277
+ line: position.line,
278
+ })),
279
+ unitPositions: index.unitPositions.map((position, ordinal) => ({
280
+ ordinal,
281
+ provinceId: position.provinceId,
282
+ type: position.type,
283
+ x: position.x,
284
+ y: position.y,
285
+ z: position.z,
286
+ rotation: position.rotation,
287
+ offset: position.offset,
288
+ sourcePath: position.document.file.displayPath,
289
+ line: position.line,
290
+ })),
291
+ weatherPositions: index.weatherPositions.map((position, ordinal) => ({
292
+ ordinal,
293
+ strategicRegionId: position.strategicRegionId,
294
+ x: position.x,
295
+ y: position.y,
296
+ z: position.z,
297
+ size: position.size,
298
+ sourcePath: position.document.file.displayPath,
299
+ line: position.line,
300
+ })),
301
+ entityLocators: [...index.entityLocators]
302
+ .sort((left, right) => compareCodeUnits(left.entity, right.entity) || compareCodeUnits(left.name, right.name))
303
+ .map(({ entity, name, position, file }) => ({
304
+ entity,
305
+ name,
306
+ position: [...position],
307
+ sourcePath: file.displayPath,
308
+ })),
309
+ };
310
+ }
311
+ function searchScore(query, id, key, displayName, alternatives) {
312
+ const idText = String(id);
313
+ const normalizedKey = key.toLocaleLowerCase('en-US');
314
+ const normalizedName = displayName.toLocaleLowerCase('en-US');
315
+ const normalizedAlternatives = alternatives.map((value) => value.toLocaleLowerCase('en-US'));
316
+ if (query === idText ||
317
+ query === normalizedKey ||
318
+ query === normalizedName ||
319
+ normalizedAlternatives.includes(query))
320
+ return 0;
321
+ if (idText.startsWith(query) ||
322
+ normalizedKey.startsWith(query) ||
323
+ normalizedName.startsWith(query) ||
324
+ normalizedAlternatives.some((value) => value.startsWith(query)))
325
+ return 1;
326
+ if (normalizedKey.includes(query) ||
327
+ normalizedName.includes(query) ||
328
+ normalizedAlternatives.some((value) => value.includes(query)))
329
+ return 2;
330
+ return undefined;
331
+ }
332
+ export function searchMapCatalog(catalog, query, limit = 100) {
333
+ const normalized = query.trim().toLocaleLowerCase('en-US');
334
+ if (normalized === '')
335
+ return [];
336
+ const entries = [
337
+ ...catalog.provinces.map(({ id, name, geometry }) => ({
338
+ kind: 'province',
339
+ id,
340
+ key: name.key,
341
+ displayName: name.displayName,
342
+ alternatives: name.values.map(({ value }) => value),
343
+ geometry,
344
+ })),
345
+ ...catalog.states.map(({ id, name, geometry }) => ({
346
+ kind: 'state',
347
+ id,
348
+ key: name.key,
349
+ displayName: name.displayName,
350
+ alternatives: name.values.map(({ value }) => value),
351
+ geometry,
352
+ })),
353
+ ...catalog.strategicRegions.map(({ id, name, geometry }) => ({
354
+ kind: 'strategic-region',
355
+ id,
356
+ key: name.key,
357
+ displayName: name.displayName,
358
+ alternatives: name.values.map(({ value }) => value),
359
+ geometry,
360
+ })),
361
+ ];
362
+ return entries
363
+ .flatMap((entry) => {
364
+ const score = searchScore(normalized, entry.id, entry.key, entry.displayName, entry.alternatives);
365
+ if (score === undefined)
366
+ return [];
367
+ const { alternatives: _alternatives, ...result } = entry;
368
+ return [{ ...result, score }];
369
+ })
370
+ .sort((left, right) => left.score - right.score || compareCodeUnits(left.kind, right.kind) || left.id - right.id)
371
+ .slice(0, Math.max(0, limit));
372
+ }
373
+ export function lookupMapCoordinate(index, coordinate) {
374
+ const raster = index.raster;
375
+ const pixel = coordinate.kind === 'pixel'
376
+ ? { x: Math.floor(coordinate.x), y: Math.floor(coordinate.y) }
377
+ : raster === undefined
378
+ ? null
379
+ : { x: Math.floor(coordinate.x), y: raster.height - 1 - Math.floor(coordinate.z) };
380
+ if (raster === undefined ||
381
+ pixel === null ||
382
+ pixel.x < 0 ||
383
+ pixel.y < 0 ||
384
+ pixel.x >= raster.width ||
385
+ pixel.y >= raster.height) {
386
+ return { coordinate, pixel: null, provinceId: null, stateIds: [], regionIds: [] };
387
+ }
388
+ const provinceId = raster.provinceIds[pixel.y * raster.width + pixel.x] ?? -1;
389
+ if (provinceId < 0)
390
+ return { coordinate, pixel, provinceId: null, stateIds: [], regionIds: [] };
391
+ return {
392
+ coordinate,
393
+ pixel,
394
+ provinceId,
395
+ stateIds: index.stateForProvince(provinceId).map(({ id }) => id),
396
+ regionIds: index.regionForProvince(provinceId).map(({ id }) => id),
397
+ };
398
+ }
@@ -1,4 +1,5 @@
1
1
  export * from './bmp.js';
2
+ export * from './catalog.js';
2
3
  export * from './limits.js';
3
4
  export * from './model.js';
4
5
  export * from './operations.js';