hoi4-agent-tools 2.6.0 → 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 (30) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/hoi4_agent_tools/gui/assets.js +15 -3
  3. package/dist/hoi4_agent_tools/gui/layout.js +270 -75
  4. package/dist/hoi4_agent_tools/gui/renderer.js +185 -22
  5. package/dist/hoi4_agent_tools/gui/source-graph.js +40 -11
  6. package/dist/hoi4_agent_tools/gui/studio.js +35 -7
  7. package/dist/hoi4_agent_tools/version.js +1 -1
  8. package/docs/gui.md +5 -1
  9. package/package.json +1 -1
  10. package/schemas/configuration.schema.json +1 -1
  11. package/schemas/continuous-focus-palette.schema.json +1 -1
  12. package/schemas/custom-weighted-pool.schema.json +1 -1
  13. package/schemas/event-feature-manifest.schema.json +1 -1
  14. package/schemas/focus-plan.schema.json +1 -1
  15. package/schemas/focus-planning-sidecar.schema.json +1 -1
  16. package/schemas/gui-animation-source.schema.json +1 -1
  17. package/schemas/gui-helper.schema.json +1 -1
  18. package/schemas/gui-scenario.schema.json +1 -1
  19. package/schemas/map-operation.schema.json +1 -1
  20. package/schemas/operation-result.schema.json +1 -1
  21. package/schemas/probability-analysis-result.schema.json +1 -1
  22. package/schemas/probability-compare-input.schema.json +1 -1
  23. package/schemas/probability-evaluate-input.schema.json +1 -1
  24. package/schemas/probability-inspect-input.schema.json +1 -1
  25. package/schemas/probability-render-input.schema.json +1 -1
  26. package/schemas/probability-scenario-set.schema.json +1 -1
  27. package/schemas/probability-sequence-input.schema.json +1 -1
  28. package/schemas/probability-simulate-input.schema.json +1 -1
  29. package/schemas/probability-sweep-input.schema.json +1 -1
  30. package/server.json +2 -2
@@ -86,10 +86,23 @@ function renderText(element, toolText) {
86
86
  (glyphLine === undefined || glyphLine.source === 'deterministic-fallback'
87
87
  ? text.lineHeight * 0.8
88
88
  : glyphLine.baseline);
89
+ const horizontalScale = glyphLine !== undefined && glyphLine.width > 0 && width > 0 ? width / glyphLine.width : 1;
90
+ const inlineIconMarkup = (text.inlineIcons ?? [])
91
+ .filter((icon) => icon.lineIndex === index)
92
+ .map((icon) => {
93
+ const iconRect = {
94
+ x: originX + icon.offsetX * horizontalScale,
95
+ y: lineTop + (text.lineHeight - icon.height) / 2,
96
+ width: icon.width * horizontalScale,
97
+ height: icon.height,
98
+ };
99
+ if (icon.sprite?.supported === true && icon.sprite.dataUri !== undefined)
100
+ return `<image data-inline-icon="${escapeXml(icon.token)}" ${rectAttributes(iconRect)} href="${icon.sprite.dataUri}" preserveAspectRatio="xMidYMid meet"/>`;
101
+ return `<g data-inline-icon-missing="${escapeXml(icon.token)}"><rect ${rectAttributes(iconRect)} fill="#331f3f" stroke="#ff42d0" stroke-width="1"/><path d="M ${finite(iconRect.x)} ${finite(iconRect.y)} L ${finite(iconRect.x + iconRect.width)} ${finite(iconRect.y + iconRect.height)} M ${finite(iconRect.x + iconRect.width)} ${finite(iconRect.y)} L ${finite(iconRect.x)} ${finite(iconRect.y + iconRect.height)}" stroke="#ff42d0"/></g>`;
102
+ })
103
+ .join('');
89
104
  const colourRuns = text.colourRuns?.[index] ?? [];
90
105
  if (colourRuns.length > 0) {
91
- const runWidth = colourRuns.reduce((maximum, run) => Math.max(maximum, run.offsetX + run.width), 0);
92
- const horizontalScale = runWidth > 0 && width > 0 ? width / runWidth : 1;
93
106
  return `<g data-hoi4-colour-runs="true">${colourRuns
94
107
  .map((run) => toolText.render(run.text, {
95
108
  x: originX + run.offsetX * horizontalScale,
@@ -100,23 +113,22 @@ function renderText(element, toolText) {
100
113
  strokeWidth: 0.6,
101
114
  ...(run.width > 0 ? { targetWidth: run.width * horizontalScale } : {}),
102
115
  }))
103
- .join('')}</g>`;
116
+ .join('')}${inlineIconMarkup}</g>`;
104
117
  }
105
118
  if (glyphLine?.source === 'fontkit-path') {
106
- const horizontalScale = glyphLine.width > 0 && width > 0 ? width / glyphLine.width : 1;
107
119
  return `<g data-font-sha256="${glyphLine.sourceHash}">${glyphLine.glyphs
108
120
  .filter((glyph) => glyph.kind === 'outline')
109
121
  .map((glyph) => `<use href="#${outlineDefinitionId(glyph.key)}" transform="translate(${finite(originX + glyph.x * horizontalScale)} ${finite(baseline + glyph.y)}) scale(${finite(glyph.scale * horizontalScale)} ${finite(-glyph.scale)})"/>`)
110
- .join('')}</g>`;
122
+ .join('')}${inlineIconMarkup}</g>`;
111
123
  }
112
124
  if (glyphLine?.source === 'bmfont-atlas') {
113
- const horizontalScale = glyphLine.width > 0 && width > 0 ? width / glyphLine.width : 1;
114
125
  return `<g data-font-sha256="${glyphLine.sourceHash}">${glyphLine.glyphs
115
126
  .filter((glyph) => glyph.kind === 'bitmap')
116
127
  .map((glyph) => `<use href="#${bitmapDefinitionId(glyph.key)}" transform="translate(${finite(originX + glyph.x * horizontalScale)} ${finite(baseline + glyph.y - glyphLine.baseline)}) scale(${finite(horizontalScale)} 1)"/>`)
117
- .join('')}</g>`;
128
+ .join('')}${inlineIconMarkup}</g>`;
118
129
  }
119
- return toolText.render(line, {
130
+ const visibleLine = line.replace(/[\uE000-\uF8FF]/gu, ' ');
131
+ return `<g>${toolText.render(visibleLine, {
120
132
  x: originX,
121
133
  y: baseline,
122
134
  fontSize: text.fontSize,
@@ -124,9 +136,153 @@ function renderText(element, toolText) {
124
136
  stroke: '#12151a',
125
137
  strokeWidth: 0.6,
126
138
  ...(width > 0 ? { targetWidth: width } : {}),
127
- });
139
+ })}${inlineIconMarkup}</g>`;
128
140
  });
129
- return `<g data-source-id="${escapeXml(element.sourceId)}" fill="#f5f2e8" stroke="#12151a" stroke-width="0.6" paint-order="stroke">${lines.join('')}</g>`;
141
+ const textClipId = `gui-text-clip-${sha256Bytes(element.id).slice(0, 20)}`;
142
+ const clipDefinition = text.fixedSize
143
+ ? `<defs><clipPath id="${textClipId}"><rect ${rectAttributes(rect)}/></clipPath></defs>`
144
+ : '';
145
+ const clip = text.fixedSize ? ` clip-path="url(#${textClipId})"` : '';
146
+ return `${clipDefinition}<g data-source-id="${escapeXml(element.sourceId)}" fill="#f5f2e8" stroke="#12151a" stroke-width="0.6" paint-order="stroke"${clip}>${lines.join('')}</g>`;
147
+ }
148
+ function renderSpriteImage(sprite, rect) {
149
+ if (!sprite.supported || sprite.dataUri === undefined)
150
+ return '';
151
+ return `<image ${rectAttributes(rect)} href="${sprite.dataUri}" preserveAspectRatio="none"/>`;
152
+ }
153
+ function renderSpriteSlice(sprite, source, destination) {
154
+ if (!sprite.supported ||
155
+ sprite.dataUri === undefined ||
156
+ source.width <= 0 ||
157
+ source.height <= 0 ||
158
+ destination.width <= 0 ||
159
+ destination.height <= 0)
160
+ return '';
161
+ return `<svg ${rectAttributes(destination)} viewBox="${finite(source.x)} ${finite(source.y)} ${finite(source.width)} ${finite(source.height)}" preserveAspectRatio="none" overflow="hidden"><image x="0" y="0" width="${finite(sprite.width)}" height="${finite(sprite.height)}" href="${sprite.dataUri}" preserveAspectRatio="none"/></svg>`;
162
+ }
163
+ function renderTiledSpriteSlice(element, sprite, source, destination) {
164
+ if (!sprite.supported ||
165
+ sprite.dataUri === undefined ||
166
+ source.width <= 0 ||
167
+ source.height <= 0 ||
168
+ destination.width <= 0 ||
169
+ destination.height <= 0)
170
+ return '';
171
+ const tileWidth = Math.max(1, source.width * element.scale);
172
+ const tileHeight = Math.max(1, source.height * element.scale);
173
+ const id = `gui-tile-${sha256Bytes(`${element.id}:${source.x}:${source.y}:${source.width}:${source.height}`).slice(0, 20)}`;
174
+ return `<defs><pattern id="${id}" patternUnits="userSpaceOnUse" x="${finite(destination.x)}" y="${finite(destination.y)}" width="${finite(tileWidth)}" height="${finite(tileHeight)}"><svg width="${finite(tileWidth)}" height="${finite(tileHeight)}" viewBox="${finite(source.x)} ${finite(source.y)} ${finite(source.width)} ${finite(source.height)}" preserveAspectRatio="none" overflow="hidden"><image x="0" y="0" width="${finite(sprite.width)}" height="${finite(sprite.height)}" href="${sprite.dataUri}" preserveAspectRatio="none"/></svg></pattern></defs><rect ${rectAttributes(destination)} fill="url(#${id})"/>`;
175
+ }
176
+ function renderCorneredTile(element) {
177
+ const sprite = element.sprite;
178
+ const rect = element.unclippedRect;
179
+ const border = element.spriteBorderSize;
180
+ if (sprite?.supported !== true ||
181
+ sprite.dataUri === undefined ||
182
+ border === undefined ||
183
+ border.width <= 0 ||
184
+ border.height <= 0)
185
+ return sprite === undefined ? '' : renderSpriteImage(sprite, rect);
186
+ const sourceBorderX = Math.min(border.width, sprite.width / 2);
187
+ const sourceBorderY = Math.min(border.height, sprite.height / 2);
188
+ const destinationBorderX = Math.min(sourceBorderX * element.scale, rect.width / 2);
189
+ const destinationBorderY = Math.min(sourceBorderY * element.scale, rect.height / 2);
190
+ const sourceWidths = [sourceBorderX, sprite.width - sourceBorderX * 2, sourceBorderX];
191
+ const sourceHeights = [sourceBorderY, sprite.height - sourceBorderY * 2, sourceBorderY];
192
+ const destinationWidths = [
193
+ destinationBorderX,
194
+ rect.width - destinationBorderX * 2,
195
+ destinationBorderX,
196
+ ];
197
+ const destinationHeights = [
198
+ destinationBorderY,
199
+ rect.height - destinationBorderY * 2,
200
+ destinationBorderY,
201
+ ];
202
+ const sourceXs = [0, sourceBorderX, sprite.width - sourceBorderX];
203
+ const sourceYs = [0, sourceBorderY, sprite.height - sourceBorderY];
204
+ const destinationXs = [
205
+ rect.x,
206
+ rect.x + destinationBorderX,
207
+ rect.x + rect.width - destinationBorderX,
208
+ ];
209
+ const destinationYs = [
210
+ rect.y,
211
+ rect.y + destinationBorderY,
212
+ rect.y + rect.height - destinationBorderY,
213
+ ];
214
+ const parts = [];
215
+ for (let row = 0; row < 3; row += 1) {
216
+ for (let column = 0; column < 3; column += 1) {
217
+ const source = {
218
+ x: sourceXs[column] ?? 0,
219
+ y: sourceYs[row] ?? 0,
220
+ width: sourceWidths[column] ?? 0,
221
+ height: sourceHeights[row] ?? 0,
222
+ };
223
+ const destination = {
224
+ x: destinationXs[column] ?? rect.x,
225
+ y: destinationYs[row] ?? rect.y,
226
+ width: destinationWidths[column] ?? 0,
227
+ height: destinationHeights[row] ?? 0,
228
+ };
229
+ parts.push(row === 1 && column === 1 && element.spriteTilingCenter === true
230
+ ? renderTiledSpriteSlice(element, sprite, source, destination)
231
+ : renderSpriteSlice(sprite, source, destination));
232
+ }
233
+ }
234
+ return parts.join('');
235
+ }
236
+ function renderProgressbar(element) {
237
+ const rect = element.unclippedRect;
238
+ const sprite = element.sprite;
239
+ const secondary = element.secondarySprite;
240
+ const ratio = Math.max(0, Math.min(1, element.progressRatio ?? 1));
241
+ const background = secondary === undefined ? '' : renderSpriteImage(secondary, rect);
242
+ if (sprite?.supported !== true || sprite.dataUri === undefined)
243
+ return background;
244
+ if (ratio <= 0)
245
+ return background;
246
+ const horizontal = element.progressHorizontal !== false;
247
+ const filled = horizontal
248
+ ? { x: rect.x, y: rect.y, width: rect.width * ratio, height: rect.height }
249
+ : {
250
+ x: rect.x,
251
+ y: rect.y + rect.height * (1 - ratio),
252
+ width: rect.width,
253
+ height: rect.height * ratio,
254
+ };
255
+ const source = horizontal
256
+ ? { x: 0, y: 0, width: sprite.width * ratio, height: sprite.height }
257
+ : {
258
+ x: 0,
259
+ y: sprite.height * (1 - ratio),
260
+ width: sprite.width,
261
+ height: sprite.height * ratio,
262
+ };
263
+ return `${background}${renderSpriteSlice(sprite, source, filled)}`;
264
+ }
265
+ function renderMaskedShield(element) {
266
+ const rect = element.unclippedRect;
267
+ const sprite = element.sprite;
268
+ const mask = element.secondarySprite;
269
+ if (sprite?.supported !== true || sprite.dataUri === undefined)
270
+ return '';
271
+ if (mask?.supported !== true || mask.dataUri === undefined)
272
+ return renderSpriteImage(sprite, rect);
273
+ const id = `gui-mask-${sha256Bytes(element.id).slice(0, 20)}`;
274
+ return `<defs><mask id="${id}" maskUnits="userSpaceOnUse" ${rectAttributes(rect)}><image ${rectAttributes(rect)} href="${mask.dataUri}" preserveAspectRatio="none"/></mask></defs><image ${rectAttributes(rect)} href="${sprite.dataUri}" preserveAspectRatio="none" mask="url(#${id})"/>`;
275
+ }
276
+ function renderElementSprite(element) {
277
+ if (element.sprite?.supported !== true || element.sprite.dataUri === undefined)
278
+ return '';
279
+ if (element.spriteRenderMode === 'cornered-tile')
280
+ return renderCorneredTile(element);
281
+ if (element.spriteRenderMode === 'progressbar')
282
+ return renderProgressbar(element);
283
+ if (element.spriteRenderMode === 'masked-shield')
284
+ return renderMaskedShield(element);
285
+ return renderSpriteImage(element.sprite, element.unclippedRect);
130
286
  }
131
287
  function renderBaseElement(element, clipId, toolText) {
132
288
  if (!element.visible)
@@ -135,25 +291,15 @@ function renderBaseElement(element, clipId, toolText) {
135
291
  const clip = clipId === undefined ? '' : ` clip-path="url(#${clipId})"`;
136
292
  const content = [];
137
293
  if (element.sprite?.supported === true && element.sprite.dataUri !== undefined) {
138
- content.push(`<image ${rectAttributes(rect)} href="${element.sprite.dataUri}" preserveAspectRatio="none"/>`);
294
+ content.push(renderElementSprite(element));
139
295
  }
140
296
  else if (element.sprite !== undefined) {
141
297
  content.push(`<rect ${rectAttributes(rect)} fill="#331f3f" stroke="#ff42d0" stroke-width="1"/><path d="M ${finite(rect.x)} ${finite(rect.y)} L ${finite(rect.x + rect.width)} ${finite(rect.y + rect.height)} M ${finite(rect.x + rect.width)} ${finite(rect.y)} L ${finite(rect.x)} ${finite(rect.y + rect.height)}" stroke="#ff42d0"/>`);
142
298
  }
143
- if (element.progressRatio !== undefined) {
299
+ if (element.progressRatio !== undefined && element.spriteRenderMode !== 'progressbar') {
144
300
  content.push(`<rect x="${finite(rect.x)}" y="${finite(rect.y)}" width="${finite(rect.width * element.progressRatio)}" height="${finite(rect.height)}" fill="#5ecf8d" fill-opacity="0.65"/>`);
145
301
  }
146
302
  content.push(renderText(element, toolText));
147
- if (element.state === 'hover')
148
- content.push(`<rect ${rectAttributes(rect)} fill="#fff" opacity="0.08"/>`);
149
- if (element.state === 'selected' || element.state === 'active')
150
- content.push(`<rect ${rectAttributes(rect)} fill="none" stroke="#ffd166" stroke-width="2"/>`);
151
- if (element.state === 'locked' || element.state === 'disabled')
152
- content.push(`<rect ${rectAttributes(rect)} fill="#111820" opacity="0.5"/>`);
153
- if (element.state === 'warning')
154
- content.push(`<rect ${rectAttributes(rect)} fill="none" stroke="#ff5d5d" stroke-width="3"/>`);
155
- if (element.state === 'completed')
156
- content.push(`<rect ${rectAttributes(rect)} fill="none" stroke="#5ecf8d" stroke-width="2"/>`);
157
303
  return `<g id="${escapeXml(element.id)}" data-source="${escapeXml(element.sourcePath)}" data-source-id="${escapeXml(element.sourceId)}"${clip}>${content.join('')}</g>`;
158
304
  }
159
305
  function renderOverlay(element, variant, toolText) {
@@ -299,6 +445,23 @@ function sceneLayoutEvidence(scene) {
299
445
  ...(element.sprite.reason === undefined ? {} : { reason: element.sprite.reason }),
300
446
  },
301
447
  }),
448
+ ...(element.secondarySprite === undefined
449
+ ? {}
450
+ : {
451
+ secondarySprite: {
452
+ spriteName: element.secondarySprite.spriteName,
453
+ texturePath: element.secondarySprite.texturePath,
454
+ frame: element.secondarySprite.frame,
455
+ frameCount: element.secondarySprite.frameCount,
456
+ width: element.secondarySprite.width,
457
+ height: element.secondarySprite.height,
458
+ format: element.secondarySprite.format,
459
+ supported: element.secondarySprite.supported,
460
+ ...(element.secondarySprite.reason === undefined
461
+ ? {}
462
+ : { reason: element.secondarySprite.reason }),
463
+ },
464
+ }),
302
465
  ...(element.text === undefined
303
466
  ? {}
304
467
  : {
@@ -54,6 +54,8 @@ const modelledElementAttributes = new Set([
54
54
  'font',
55
55
  'fontSize',
56
56
  'maxWidth',
57
+ 'maxHeight',
58
+ 'fixedsize',
57
59
  'format',
58
60
  'buttonText',
59
61
  'buttonFont',
@@ -65,11 +67,10 @@ const modelledElementAttributes = new Set([
65
67
  'minValue',
66
68
  'maxValue',
67
69
  'startValue',
70
+ 'centerposition',
68
71
  ]);
69
72
  const ignoredElementAttributes = new Set([
70
73
  'margin',
71
- 'maxHeight',
72
- 'fixedsize',
73
74
  'pdx_tooltip',
74
75
  'pdx_tooltip_delayed',
75
76
  'hint_tag',
@@ -84,7 +85,6 @@ const ignoredElementAttributes = new Set([
84
85
  'smooth_scrolling',
85
86
  'borderSize',
86
87
  'stepSize',
87
- 'centerposition',
88
88
  'fullScreen',
89
89
  'moveable',
90
90
  'show_position',
@@ -194,6 +194,14 @@ function sizeFrom(block) {
194
194
  const height = numberScalar(size, 'height', 'y');
195
195
  return width === undefined || height === undefined ? undefined : { width, height };
196
196
  }
197
+ function nestedSizeFrom(block, key) {
198
+ const nested = assignments(block).find((assignment) => assignment.key.value.toLowerCase() === key.toLowerCase() && assignment.value.type === 'block')?.value;
199
+ if (nested?.type !== 'block')
200
+ return undefined;
201
+ const width = numberScalar(nested, 'width', 'x');
202
+ const height = numberScalar(nested, 'height', 'y');
203
+ return width === undefined || height === undefined ? undefined : { width, height };
204
+ }
197
205
  function raw(document, node) {
198
206
  return document.text.slice(node.start, node.end);
199
207
  }
@@ -335,7 +343,7 @@ function indexSpritesAndFonts(document, file, fileNodeId, nodes, edges, sprites,
335
343
  const name = firstScalarInsensitive(child, 'name');
336
344
  if (name !== undefined) {
337
345
  const id = deterministicId('gui_sprite', { path: file.displayPath, name });
338
- const texturePath = firstScalarInsensitive(child, 'texturefile', 'textureFile');
346
+ const texturePath = firstScalarInsensitive(child, 'texturefile', 'textureFile', 'textureFile1');
339
347
  const texturePath2 = firstScalarInsensitive(child, 'textureFile2');
340
348
  const staticFallback = firstScalarInsensitive(child, 'static_fallback', 'staticFallback');
341
349
  const animationRateFps = numberScalar(child, 'animation_rate_fps');
@@ -344,6 +352,10 @@ function indexSpritesAndFonts(document, file, fileNodeId, nodes, edges, sprites,
344
352
  const pauseOnLoop = numberScalar(child, 'pause_on_loop');
345
353
  const effectFile = firstScalarInsensitive(child, 'effectFile');
346
354
  const declaredSize = sizeFrom(child);
355
+ const borderSize = nestedSizeFrom(child, 'borderSize');
356
+ const tilingCenter = boolScalar(child, 'tilingCenter');
357
+ const horizontal = boolScalar(child, 'horizontal');
358
+ const steps = numberScalar(child, 'steps');
347
359
  const definition = {
348
360
  id,
349
361
  name,
@@ -361,6 +373,10 @@ function indexSpritesAndFonts(document, file, fileNodeId, nodes, edges, sprites,
361
373
  ...(effectFile === undefined ? {} : { effectFile }),
362
374
  ...(staticFallback === undefined ? {} : { staticFallback }),
363
375
  ...(declaredSize === undefined ? {} : { declaredSize }),
376
+ ...(borderSize === undefined ? {} : { borderSize }),
377
+ ...(tilingCenter === undefined ? {} : { tilingCenter }),
378
+ ...(horizontal === undefined ? {} : { horizontal }),
379
+ ...(steps === undefined ? {} : { steps: Math.trunc(steps) }),
364
380
  rawSource: raw(document, assignment),
365
381
  };
366
382
  addDomainEntry(sprites, definition, 'sprite');
@@ -374,6 +390,19 @@ function indexSpritesAndFonts(document, file, fileNodeId, nodes, edges, sprites,
374
390
  spriteType: definition.spriteType,
375
391
  frameCount: definition.frameCount,
376
392
  frameAnimated: definition.frameAnimated,
393
+ ...(definition.borderSize === undefined
394
+ ? {}
395
+ : {
396
+ borderSize: {
397
+ width: definition.borderSize.width,
398
+ height: definition.borderSize.height,
399
+ },
400
+ }),
401
+ ...(definition.tilingCenter === undefined
402
+ ? {}
403
+ : { tilingCenter: definition.tilingCenter }),
404
+ ...(definition.horizontal === undefined ? {} : { horizontal: definition.horizontal }),
405
+ ...(definition.steps === undefined ? {} : { steps: definition.steps }),
377
406
  ...(definition.animationRateFps === undefined
378
407
  ? {}
379
408
  : { animationRateFps: definition.animationRateFps }),
@@ -607,9 +636,9 @@ function indexAnimationSourceManifest(file, fileNodeId, nodes, edges, manifests,
607
636
  addEdge(edges, 'contains', fileNodeId, id, true, {}, location);
608
637
  }
609
638
  function linkAnimationSources(nodes, edges, manifests, sprites, textureNodes, frameNodes) {
610
- const spritesByName = new Map(sprites.map((sprite) => [sprite.name, sprite]));
639
+ const spritesByName = new Map(sprites.map((sprite) => [sprite.name.toLocaleLowerCase('en-US'), sprite]));
611
640
  for (const manifest of manifests) {
612
- const sprite = spritesByName.get(manifest.sprite);
641
+ const sprite = spritesByName.get(manifest.sprite.toLocaleLowerCase('en-US'));
613
642
  addEdge(edges, 'animation_provenance', sprite?.id ?? `sprite:${manifest.sprite}`, manifest.id, sprite !== undefined, { sprite: manifest.sprite }, manifest.location);
614
643
  const sheet = textureNodes.get(normalizeAssetPath(manifest.sheet.path));
615
644
  addEdge(edges, 'animation_sheet', manifest.id, sheet ?? `texture:${normalizeAssetPath(manifest.sheet.path)}`, sheet !== undefined, { texturePath: manifest.sheet.path }, manifest.location);
@@ -688,8 +717,8 @@ function indexDecisionEntries(document, file, nodes, pendingDecisionEdges) {
688
717
  walk(document.root);
689
718
  }
690
719
  function linkGraph(nodes, edges, elements, sprites, fonts, scriptedGuis, scriptedLocalisation, localisation, textureNodes, fontAssetNodes, pendingDecisionEdges) {
691
- const spriteByName = new Map(sprites.map((sprite) => [sprite.name, sprite]));
692
- const fontByName = new Map(fonts.map((font) => [font.name, font]));
720
+ const spriteByName = new Map(sprites.map((sprite) => [sprite.name.toLocaleLowerCase('en-US'), sprite]));
721
+ const fontByName = new Map(fonts.map((font) => [font.name.toLocaleLowerCase('en-US'), font]));
693
722
  const elementByName = new Map();
694
723
  for (const element of [...elements].sort((a, b) => a.definitionOrder - b.definitionOrder)) {
695
724
  if (!elementByName.has(element.name))
@@ -719,7 +748,7 @@ function linkGraph(nodes, edges, elements, sprites, fonts, scriptedGuis, scripte
719
748
  `${sprite.name}_fallback`,
720
749
  ].filter((value) => value !== undefined);
721
750
  const fallback = candidates
722
- .map((name) => spriteByName.get(name))
751
+ .map((name) => spriteByName.get(name.toLocaleLowerCase('en-US')))
723
752
  .find((value) => value !== undefined);
724
753
  if (fallback !== undefined)
725
754
  addEdge(edges, 'static_fallback', sprite.id, fallback.id, true, {}, sprite.location);
@@ -739,12 +768,12 @@ function linkGraph(nodes, edges, elements, sprites, fonts, scriptedGuis, scripte
739
768
  for (const element of elements) {
740
769
  const spriteName = [element.attributes.spriteType, element.attributes.quadTextureSprite].find((value) => typeof value === 'string' && value.trim().length > 0);
741
770
  if (spriteName !== undefined) {
742
- const sprite = spriteByName.get(spriteName);
771
+ const sprite = spriteByName.get(spriteName.toLocaleLowerCase('en-US'));
743
772
  addEdge(edges, 'uses_sprite', element.id, sprite?.id ?? `sprite:${spriteName}`, sprite !== undefined, { spriteName }, element.location);
744
773
  }
745
774
  const fontName = [element.attributes.font, element.attributes.buttonFont].find((value) => typeof value === 'string' && value.trim().length > 0);
746
775
  if (fontName !== undefined) {
747
- const font = fontByName.get(fontName);
776
+ const font = fontByName.get(fontName.toLocaleLowerCase('en-US'));
748
777
  addEdge(edges, 'uses_font', element.id, font?.id ?? `font:${fontName}`, font !== undefined, { fontName }, element.location);
749
778
  }
750
779
  for (const key of localisationAttributeNames) {
@@ -195,7 +195,10 @@ function guiLocalisationPatterns(workspace, languages = ['l_english']) {
195
195
  ].sort((left, right) => compareCodeUnits(left, right));
196
196
  }
197
197
  function normalizedAssetReference(value) {
198
- const normalized = value.replaceAll('\\', '/').replace(/^\/+|^\.\//u, '');
198
+ const normalized = value
199
+ .replaceAll('\\', '/')
200
+ .replace(/^\/+|^\.\//u, '')
201
+ .replace(/\/+/gu, '/');
199
202
  return isSafeAnimationSourcePath(normalized) ? normalized : undefined;
200
203
  }
201
204
  function referenceVariants(value) {
@@ -264,29 +267,54 @@ function collectNamedAttributes(value, keys, output) {
264
267
  collectNamedAttributes(entry, keys, output);
265
268
  }
266
269
  }
270
+ function localisationIconTokens(value) {
271
+ return [...value.matchAll(/£([A-Za-z0-9_.:+-]+)/gu)]
272
+ .map((match) => match[1])
273
+ .filter((token) => token !== undefined && token.length > 0);
274
+ }
275
+ function localisationIconSpriteCandidates(token) {
276
+ const normalized = token.startsWith('GFX_') ? token : `GFX_${token}`;
277
+ return [...new Set([normalized, `${normalized}_texticon`, `${normalized}_text_icon`])];
278
+ }
267
279
  function referencedAssetPatternsForWindow(graph, windowName) {
268
280
  const spriteNames = new Set();
269
281
  const fontNames = new Set();
282
+ const textValues = new Set();
270
283
  for (const element of selectedElements(graph, windowName)) {
271
284
  collectNamedAttributes(element.attributes, new Set(['spriteType', 'quadTextureSprite']), spriteNames);
272
285
  collectNamedAttributes(element.attributes, new Set(['font', 'buttonFont']), fontNames);
286
+ collectNamedAttributes(element.attributes, new Set(['text', 'buttonText']), textValues);
287
+ }
288
+ for (const localisation of graph.localisation) {
289
+ if (textValues.has(localisation.key))
290
+ textValues.add(localisation.value);
273
291
  }
274
- const spritesByName = new Map(graph.sprites.map((sprite) => [sprite.name, sprite]));
292
+ for (const value of textValues) {
293
+ for (const token of localisationIconTokens(value)) {
294
+ for (const candidate of localisationIconSpriteCandidates(token))
295
+ spriteNames.add(candidate);
296
+ }
297
+ }
298
+ const spritesByName = new Map(graph.sprites.map((sprite) => [sprite.name.toLocaleLowerCase('en-US'), sprite]));
275
299
  const selectedSprites = new Map();
276
300
  const pendingSprites = [...spriteNames];
277
301
  while (pendingSprites.length > 0) {
278
302
  const name = pendingSprites.pop();
279
- if (name === undefined || selectedSprites.has(name))
303
+ if (name === undefined)
304
+ continue;
305
+ const key = name.toLocaleLowerCase('en-US');
306
+ if (selectedSprites.has(key))
280
307
  continue;
281
- const sprite = spritesByName.get(name);
308
+ const sprite = spritesByName.get(key);
282
309
  if (sprite === undefined)
283
310
  continue;
284
- selectedSprites.set(name, sprite);
311
+ selectedSprites.set(key, sprite);
285
312
  if (sprite.staticFallback !== undefined)
286
313
  pendingSprites.push(sprite.staticFallback);
287
314
  }
288
- const selectedManifests = graph.animationSources.filter(({ sprite }) => selectedSprites.has(sprite));
289
- const selectedFonts = graph.fonts.filter(({ name }) => fontNames.has(name));
315
+ const selectedManifests = graph.animationSources.filter(({ sprite }) => selectedSprites.has(sprite.toLocaleLowerCase('en-US')));
316
+ const selectedFontNames = new Set([...fontNames].map((name) => name.toLocaleLowerCase('en-US')));
317
+ const selectedFonts = graph.fonts.filter(({ name }) => selectedFontNames.has(name.toLocaleLowerCase('en-US')));
290
318
  const references = [
291
319
  ...[...selectedSprites.values()].flatMap(({ texturePath, texturePath2 }) => [texturePath, texturePath2].filter((value) => value !== undefined)),
292
320
  ...selectedFonts.flatMap(({ assetPaths }) => assetPaths),
@@ -1,6 +1,6 @@
1
1
  import { LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/sdk/types.js';
2
2
  export const PACKAGE_NAME = 'hoi4-agent-tools';
3
- export const PACKAGE_VERSION = '2.6.0';
3
+ export const PACKAGE_VERSION = '2.7.0';
4
4
  export const MCP_PROTOCOL_VERSION = LATEST_PROTOCOL_VERSION;
5
5
  export const CONFIG_VERSION = 1;
6
6
  export const TRANSACTION_VERSION = 1;
package/docs/gui.md CHANGED
@@ -31,6 +31,10 @@ Rewrite once, then inspect and render the result.
31
31
 
32
32
  Inspection checks missing assets and localisation, invalid sizes, overlap, clipping, overflow, button-label centering, content crossing a background boundary, conflicting click regions, invisible blockers, broken parents or contexts, list-row cuts, state conflicts, trigger/effect gaps, and resolution drift. `hoi4.gui_render` returns a scripted-scenario gallery and JSON delta report alongside the generic state and resolution galleries.
33
33
 
34
+ The renderer resolves sprites and fonts through the complete mod, dependency, and installed-game load order. Mod windows therefore render ordinary vanilla sprites, tiled vanilla panels, progress bars, masked shields, button frames, and localisation icons such as `£command_power` without copying game assets into the mod. Case differences in GFX and font identifiers do not prevent a valid asset from resolving.
35
+
36
+ Layout follows Clausewitz orientation and element-origin anchors, including `CENTER_LEFT`, `CENTER_RIGHT`, `CENTER_UP`, `CENTER_DOWN`, `origo`, `centerposition`, inherited container coordinate origins, local scale, and both `%` and `%%` relative dimensions. Text boxes use native font metrics together with `maxWidth`, `maxHeight`, alignment, wrapping, and `fixedsize` clipping. Cornered tile sprites use fixed nine-slice borders and optional center tiling; progress bars composite their filled and background textures; masked shields composite their background and mask textures.
37
+
34
38
  Preview scenarios can include exact layout expectations:
35
39
 
36
40
  ```json
@@ -60,4 +64,4 @@ The renderer does not run the game engine. Each render includes a fidelity repor
60
64
 
61
65
  Unresolved numeric runtime values such as `[?variable|format]` render as `[X]`. Unresolved text-returning scripted or scoped localisation such as `[GetStatusText]`, `[FROM.GetName]`, and `[?leader_scope.GetName]` renders as `[dynamic_loc]`. Provide scenario values when the exact value matters. Supported HOI4 `§` localisation colour controls are applied to preview text and `§!` restores the default colour.
62
66
 
63
- A rewrite stops if malformed or unsupported GUI script makes the requested change ambiguous. Runtime animation, masking, tiling, hardcoded controls, and dynamic values may require exact game precedents even when a useful offline preview is available.
67
+ A rewrite stops if malformed or unsupported GUI script makes the requested change ambiguous. Frame-sheet animation, nine-slice tiling, progress composition, masking, and the primary and secondary textures referenced by vanilla GFX definitions render offline. Engine shader programs, hardcoded controls, and dynamic values that are not supplied by a scenario remain fidelity-report entries; an unexecuted shader never hides its resolved base texture.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hoi4-agent-tools",
3
3
  "mcpName": "io.github.klimPaskov/hoi4-agent-tools",
4
- "version": "2.6.0",
4
+ "version": "2.7.0",
5
5
  "description": "Analyze HOI4 events, tech trees, AI weights, and MTTH; build focus trees, GUIs, and maps.",
6
6
  "type": "module",
7
7
  "license": "Apache-2.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.6.0/schemas/configuration.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.7.0/schemas/configuration.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.6.0/schemas/continuous-focus-palette.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.7.0/schemas/continuous-focus-palette.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.6.0/schemas/custom-weighted-pool.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.7.0/schemas/custom-weighted-pool.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.6.0/schemas/event-feature-manifest.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.7.0/schemas/event-feature-manifest.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.6.0/schemas/focus-plan.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.7.0/schemas/focus-plan.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.6.0/schemas/focus-planning-sidecar.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.7.0/schemas/focus-planning-sidecar.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.6.0/schemas/gui-animation-source.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.7.0/schemas/gui-animation-source.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.6.0/schemas/gui-helper.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.7.0/schemas/gui-helper.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.6.0/schemas/gui-scenario.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.7.0/schemas/gui-scenario.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.6.0/schemas/map-operation.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.7.0/schemas/map-operation.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "oneOf": [
5
5
  {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.6.0/schemas/operation-result.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.7.0/schemas/operation-result.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.6.0/schemas/probability-analysis-result.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.7.0/schemas/probability-analysis-result.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.6.0/schemas/probability-compare-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.7.0/schemas/probability-compare-input.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.6.0/schemas/probability-evaluate-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.7.0/schemas/probability-evaluate-input.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {