hoi4-agent-tools 2.5.3 → 2.7.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 (39) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +5 -5
  3. package/dist/hoi4_agent_tools/gui/assets.js +15 -3
  4. package/dist/hoi4_agent_tools/gui/layout.js +270 -75
  5. package/dist/hoi4_agent_tools/gui/renderer.js +185 -22
  6. package/dist/hoi4_agent_tools/gui/source-graph.js +40 -11
  7. package/dist/hoi4_agent_tools/gui/studio.js +35 -7
  8. package/dist/hoi4_agent_tools/map/catalog.js +398 -0
  9. package/dist/hoi4_agent_tools/map/index.js +1 -0
  10. package/dist/hoi4_agent_tools/map/operations.js +337 -30
  11. package/dist/hoi4_agent_tools/map/render.js +40 -147
  12. package/dist/hoi4_agent_tools/map/service.js +1 -0
  13. package/dist/hoi4_agent_tools/mcp/tools/map.js +85 -9
  14. package/dist/hoi4_agent_tools/schemas/map.js +69 -2
  15. package/dist/hoi4_agent_tools/version.js +1 -1
  16. package/docs/gui.md +5 -1
  17. package/docs/map.md +60 -23
  18. package/package.json +1 -1
  19. package/schemas/configuration.schema.json +1 -1
  20. package/schemas/continuous-focus-palette.schema.json +1 -1
  21. package/schemas/custom-weighted-pool.schema.json +1 -1
  22. package/schemas/event-feature-manifest.schema.json +1 -1
  23. package/schemas/focus-plan.schema.json +1 -1
  24. package/schemas/focus-planning-sidecar.schema.json +1 -1
  25. package/schemas/gui-animation-source.schema.json +1 -1
  26. package/schemas/gui-helper.schema.json +1 -1
  27. package/schemas/gui-scenario.schema.json +1 -1
  28. package/schemas/map-operation.schema.json +228 -74
  29. package/schemas/operation-result.schema.json +1 -1
  30. package/schemas/probability-analysis-result.schema.json +1 -1
  31. package/schemas/probability-compare-input.schema.json +1 -1
  32. package/schemas/probability-evaluate-input.schema.json +1 -1
  33. package/schemas/probability-inspect-input.schema.json +1 -1
  34. package/schemas/probability-render-input.schema.json +1 -1
  35. package/schemas/probability-scenario-set.schema.json +1 -1
  36. package/schemas/probability-sequence-input.schema.json +1 -1
  37. package/schemas/probability-simulate-input.schema.json +1 -1
  38. package/schemas/probability-sweep-input.schema.json +1 -1
  39. package/server.json +2 -2
@@ -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';