hoi4-agent-tools 2.6.0 → 2.8.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 (44) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +1 -1
  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/mcp/tools/probability.js +9 -0
  9. package/dist/hoi4_agent_tools/probability/adapters.js +5 -0
  10. package/dist/hoi4_agent_tools/probability/evaluation.js +6 -1
  11. package/dist/hoi4_agent_tools/probability/scenario-state.js +165 -0
  12. package/dist/hoi4_agent_tools/probability/scope-pools.js +199 -0
  13. package/dist/hoi4_agent_tools/probability/simulation.js +4 -3
  14. package/dist/hoi4_agent_tools/probability/sweep.js +6 -2
  15. package/dist/hoi4_agent_tools/probability/trigger-evaluator.js +35 -28
  16. package/dist/hoi4_agent_tools/probability/value-evaluator.js +4 -0
  17. package/dist/hoi4_agent_tools/schemas/probability.js +90 -0
  18. package/dist/hoi4_agent_tools/version.js +1 -1
  19. package/docs/gui.md +5 -1
  20. package/docs/probability.md +46 -0
  21. package/docs/research/probability-adapter-evidence.md +2 -1
  22. package/examples/probability/scoped-dynamic-pools-evaluate-input.json +50 -0
  23. package/package.json +1 -1
  24. package/schemas/configuration.schema.json +1 -1
  25. package/schemas/continuous-focus-palette.schema.json +1 -1
  26. package/schemas/custom-weighted-pool.schema.json +1 -1
  27. package/schemas/event-feature-manifest.schema.json +1 -1
  28. package/schemas/focus-plan.schema.json +1 -1
  29. package/schemas/focus-planning-sidecar.schema.json +1 -1
  30. package/schemas/gui-animation-source.schema.json +1 -1
  31. package/schemas/gui-helper.schema.json +1 -1
  32. package/schemas/gui-scenario.schema.json +1 -1
  33. package/schemas/map-operation.schema.json +1 -1
  34. package/schemas/operation-result.schema.json +1 -1
  35. package/schemas/probability-analysis-result.schema.json +175 -66
  36. package/schemas/probability-compare-input.schema.json +251 -66
  37. package/schemas/probability-evaluate-input.schema.json +252 -67
  38. package/schemas/probability-inspect-input.schema.json +1 -1
  39. package/schemas/probability-render-input.schema.json +1 -1
  40. package/schemas/probability-scenario-set.schema.json +231 -46
  41. package/schemas/probability-sequence-input.schema.json +273 -88
  42. package/schemas/probability-simulate-input.schema.json +252 -67
  43. package/schemas/probability-sweep-input.schema.json +254 -69
  44. 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),
@@ -120,6 +120,8 @@ const analysisDataSchema = z
120
120
  cacheKey: sha256Schema,
121
121
  scenarios: nonNegativeIntegerSchema,
122
122
  candidates: nonNegativeIntegerSchema,
123
+ scopePools: nonNegativeIntegerSchema.optional(),
124
+ scopePoolCandidates: nonNegativeIntegerSchema.optional(),
123
125
  unresolved: nonNegativeIntegerSchema,
124
126
  diagnostics: nonNegativeIntegerSchema,
125
127
  sweepPoints: nonNegativeIntegerSchema.optional(),
@@ -194,6 +196,13 @@ function analysisData(result) {
194
196
  cacheKey: result.metadata.cacheKey,
195
197
  scenarios: result.scenarios.length,
196
198
  candidates: result.scenarios.reduce((sum, scenario) => sum + scenario.candidates.length, 0),
199
+ ...(result.scenarios.some(({ scopePools }) => (scopePools?.length ?? 0) > 0)
200
+ ? {
201
+ scopePools: result.scenarios.reduce((sum, scenario) => sum + (scenario.scopePools?.length ?? 0), 0),
202
+ scopePoolCandidates: result.scenarios.reduce((sum, scenario) => sum +
203
+ (scenario.scopePools?.reduce((poolSum, pool) => poolSum + pool.candidates.length, 0) ?? 0), 0),
204
+ }
205
+ : {}),
197
206
  unresolved: result.unresolved.length,
198
207
  diagnostics: result.diagnostics.length,
199
208
  ...(result.sweep === undefined ? {} : { sweepPoints: result.sweep.points.length }),
@@ -326,6 +326,11 @@ const descriptors = Object.fromEntries(Object.entries(sources).map(([id, source]
326
326
  'Apply declared external factors only after source-local operations.',
327
327
  ],
328
328
  poolNormalizationRules: normalizationRule(source.selectionRule),
329
+ supportedExpressions: [
330
+ ...source.supportedExpressions,
331
+ 'scenario-bound ROOT/THIS/PREV/FROM scope chains, saved scopes, and event targets',
332
+ 'declared dynamic scope-pool filters with uniform or proportional selection',
333
+ ],
329
334
  timingConversion: source.selectionRule === 'median_daily_hazard'
330
335
  ? [
331
336
  'Treat the effective duration as the median of verified daily checks while active.',
@@ -1,6 +1,8 @@
1
1
  import { compareCodeUnits } from '../core/canonical.js';
2
2
  import { assignments } from '../core/source/index.js';
3
3
  import { Rational, sumRationals, uniformRaceProbabilities } from './rational.js';
4
+ import { withScenarioPathValue } from './scenario-state.js';
5
+ import { evaluateScopePools } from './scope-pools.js';
4
6
  import { evaluateTriggerBlock } from './trigger-evaluator.js';
5
7
  import { evaluateCandidateValue } from './value-evaluator.js';
6
8
  const BRANCH_LIMIT = 4_096;
@@ -305,7 +307,7 @@ function scenarioBranches(surface, scenario) {
305
307
  });
306
308
  break;
307
309
  }
308
- branches = branches.flatMap((branch) => values.map((value) => ({ ...branch, state: { ...branch.state, [input.path]: value } })));
310
+ branches = branches.flatMap((branch) => values.map((value) => withScenarioPathValue(branch, input.path, value)));
309
311
  if (values.length > 1)
310
312
  bounded = true;
311
313
  }
@@ -599,10 +601,12 @@ export function evaluateSurfaceScenarios(surface, scenarios, definitions, horizo
599
601
  return result;
600
602
  });
601
603
  rankCandidates(candidates);
604
+ const scopePools = evaluateScopePools(scenario, definitions);
602
605
  const allUnresolved = [
603
606
  ...surface.unsupported,
604
607
  ...expanded.unresolved,
605
608
  ...candidates.flatMap(({ unresolved }) => unresolved),
609
+ ...scopePools.flatMap(({ unresolved }) => unresolved),
606
610
  ];
607
611
  const timing = horizonDays === undefined
608
612
  ? { unresolved: [] }
@@ -632,6 +636,7 @@ export function evaluateSurfaceScenarios(surface, scenarios, definitions, horizo
632
636
  ? 'score_only'
633
637
  : 'exact',
634
638
  candidates,
639
+ ...(scopePools.length === 0 ? {} : { scopePools }),
635
640
  ...(exactValues.length === candidates.length &&
636
641
  surface.adapter.selectionRule === 'proportional_categorical'
637
642
  ? { poolTotal: sumRationals(exactValues).toJSON() }