hoi4-agent-tools 2.8.5 → 2.8.7

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 (35) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/hoi4_agent_tools/core/artifacts.js +113 -44
  3. package/dist/hoi4_agent_tools/gui/assets.js +0 -5
  4. package/dist/hoi4_agent_tools/gui/layout.js +4 -1
  5. package/dist/hoi4_agent_tools/gui/renderer.js +12 -1
  6. package/dist/hoi4_agent_tools/gui/scenario-generator.js +402 -69
  7. package/dist/hoi4_agent_tools/gui/scenario.js +1 -1
  8. package/dist/hoi4_agent_tools/gui/source-graph.js +61 -9
  9. package/dist/hoi4_agent_tools/gui/studio.js +45 -4
  10. package/dist/hoi4_agent_tools/gui/validators.js +6 -1
  11. package/dist/hoi4_agent_tools/probability/service.js +3 -2
  12. package/dist/hoi4_agent_tools/version.js +1 -1
  13. package/docs/gui.md +4 -4
  14. package/package.json +1 -1
  15. package/schemas/configuration.schema.json +1 -1
  16. package/schemas/continuous-focus-palette.schema.json +1 -1
  17. package/schemas/custom-weighted-pool.schema.json +1 -1
  18. package/schemas/event-feature-manifest.schema.json +1 -1
  19. package/schemas/focus-plan.schema.json +1 -1
  20. package/schemas/focus-planning-sidecar.schema.json +1 -1
  21. package/schemas/gui-animation-source.schema.json +1 -1
  22. package/schemas/gui-helper.schema.json +1 -1
  23. package/schemas/gui-scenario.schema.json +1 -1
  24. package/schemas/map-operation.schema.json +1 -1
  25. package/schemas/operation-result.schema.json +1 -1
  26. package/schemas/probability-analysis-result.schema.json +1 -1
  27. package/schemas/probability-compare-input.schema.json +1 -1
  28. package/schemas/probability-evaluate-input.schema.json +1 -1
  29. package/schemas/probability-inspect-input.schema.json +1 -1
  30. package/schemas/probability-render-input.schema.json +1 -1
  31. package/schemas/probability-scenario-set.schema.json +1 -1
  32. package/schemas/probability-sequence-input.schema.json +1 -1
  33. package/schemas/probability-simulate-input.schema.json +1 -1
  34. package/schemas/probability-sweep-input.schema.json +1 -1
  35. package/server.json +2 -2
@@ -11,6 +11,20 @@ const countryAdjectives = [
11
11
  'Canadian',
12
12
  ];
13
13
  const leaderNames = ['Anna Keller', 'Michael Byrne', 'Elena Rossi', 'Jan Kowalski', 'Rafael Silva'];
14
+ const monthNames = [
15
+ 'January',
16
+ 'February',
17
+ 'March',
18
+ 'April',
19
+ 'May',
20
+ 'June',
21
+ 'July',
22
+ 'August',
23
+ 'September',
24
+ 'October',
25
+ 'November',
26
+ 'December',
27
+ ];
14
28
  const variedStates = [
15
29
  'normal',
16
30
  'selected',
@@ -19,6 +33,7 @@ const variedStates = [
19
33
  'disabled',
20
34
  'completed',
21
35
  ];
36
+ const transientWindowPattern = /(?:confirm|details|dialog|dropdown|menu|modal|overlay|popup|tooltip)/iu;
22
37
  class ScenarioRandom {
23
38
  #state;
24
39
  constructor(seed) {
@@ -41,26 +56,50 @@ class ScenarioRandom {
41
56
  return values[Math.min(values.length - 1, Math.floor(this.next() * values.length))];
42
57
  }
43
58
  }
44
- function relevantElements(graph, windowName) {
45
- const root = graph.elements.find((element) => element.parentId === undefined && element.name === windowName);
46
- if (root === undefined)
47
- return graph.elements.filter(({ name }) => name === windowName);
59
+ function relatedScriptedGuis(graph, windowName) {
60
+ const names = new Set(graph.scriptedGuis
61
+ .filter((definition) => definition.windowName === windowName || definition.parentWindowName === windowName)
62
+ .map(({ name }) => name));
63
+ let added = true;
64
+ while (added) {
65
+ added = false;
66
+ for (const definition of graph.scriptedGuis) {
67
+ if (definition.parentScriptedGui === undefined ||
68
+ !names.has(definition.parentScriptedGui) ||
69
+ names.has(definition.name))
70
+ continue;
71
+ names.add(definition.name);
72
+ added = true;
73
+ }
74
+ }
75
+ return graph.scriptedGuis.filter(({ name }) => names.has(name));
76
+ }
77
+ function relevantElements(graph, windowName, scripted) {
78
+ const rootNames = new Set([windowName]);
79
+ for (const definition of scripted) {
80
+ if (definition.windowName !== undefined)
81
+ rootNames.add(definition.windowName);
82
+ for (const dynamicList of definition.dynamicListDefinitions) {
83
+ if (dynamicList.entryContainer !== undefined)
84
+ rootNames.add(dynamicList.entryContainer);
85
+ if (dynamicList.countryScopeEntryContainer !== undefined)
86
+ rootNames.add(dynamicList.countryScopeEntryContainer);
87
+ }
88
+ }
48
89
  const byId = new Map(graph.elements.map((element) => [element.id, element]));
49
- const pending = [root.id];
50
- const selected = [];
51
- const seen = new Set();
90
+ const pending = graph.elements.filter(({ name }) => rootNames.has(name)).map(({ id }) => id);
91
+ const selected = new Map();
52
92
  while (pending.length > 0) {
53
93
  const id = pending.pop();
54
- if (seen.has(id))
94
+ if (id === undefined || selected.has(id))
55
95
  continue;
56
- seen.add(id);
57
96
  const element = byId.get(id);
58
97
  if (element === undefined)
59
98
  continue;
60
- selected.push(element);
99
+ selected.set(id, element);
61
100
  pending.push(...element.childIds);
62
101
  }
63
- return selected;
102
+ return [...selected.values()];
64
103
  }
65
104
  function stringsIn(value) {
66
105
  if (typeof value === 'string')
@@ -70,18 +109,37 @@ function stringsIn(value) {
70
109
  return Object.values(value).flatMap(stringsIn);
71
110
  }
72
111
  function localisationText(graph, language, key) {
73
- return graph.localisation.find((entry) => entry.language === language && entry.key === key)
74
- ?.value;
112
+ const normalizedLanguage = language.toLocaleLowerCase('en-US');
113
+ return graph.localisation.find((entry) => entry.key === key && entry.language.toLocaleLowerCase('en-US') === normalizedLanguage)?.value;
75
114
  }
76
115
  function sourceTexts(graph, elements, language, scripted) {
77
116
  const values = elements.flatMap((element) => Object.values(element.attributes).flatMap(stringsIn));
78
- const propertyValues = scripted.flatMap((definition) => definition.propertyDefinitions.flatMap((property) => Object.values(property.attributes).flatMap(stringsIn)));
79
- return [...values, ...propertyValues].flatMap((value) => [
117
+ const resolvedValues = values.flatMap((value) => [
80
118
  value,
81
119
  localisationText(graph, language, value) ?? '',
82
120
  ]);
121
+ const relatedScriptedLocalisationKeys = new Set(graph.scriptedLocalisation
122
+ .filter((definition) => resolvedValues.some((value) => value.includes(`[${definition.name}]`)))
123
+ .flatMap(({ localisationKeys }) => localisationKeys));
124
+ const propertyValues = scripted.flatMap(({ propertyDefinitions }) => propertyDefinitions.flatMap(({ attributes }) => Object.values(attributes).flatMap(stringsIn)));
125
+ const normalizedLanguage = language.toLocaleLowerCase('en-US');
126
+ return [
127
+ ...resolvedValues,
128
+ ...propertyValues,
129
+ ...graph.localisation
130
+ .filter(({ language: entryLanguage, key }) => entryLanguage.toLocaleLowerCase('en-US') === normalizedLanguage &&
131
+ relatedScriptedLocalisationKeys.has(key))
132
+ .map(({ value }) => value),
133
+ ];
134
+ }
135
+ function expressionToken(value) {
136
+ if (typeof value !== 'string')
137
+ return undefined;
138
+ const variable = /^\[\?([^\]|]+)(?:\|[^\]]+)?\]$/u.exec(value);
139
+ const dynamic = /^\[([^\]]+)\]$/u.exec(value);
140
+ return variable?.[1] ?? dynamic?.[1];
83
141
  }
84
- function dynamicTokens(texts) {
142
+ function tokensIn(texts) {
85
143
  const numeric = new Set();
86
144
  const textual = new Set();
87
145
  for (const text of texts) {
@@ -98,33 +156,248 @@ function dynamicTokens(texts) {
98
156
  textual.add(key);
99
157
  }
100
158
  }
101
- return {
102
- numeric: [...numeric].sort(),
103
- textual: [...textual].sort(),
104
- };
159
+ return { numeric, textual };
105
160
  }
106
- function plausibleText(key, random, samples) {
161
+ function plausibleText(key, random, samples, scopedCountries) {
107
162
  const lower = key.toLocaleLowerCase('en-US');
163
+ if (lower.includes('date'))
164
+ return `${random.integer(1, 28)} ${random.pick(monthNames)} ${random.integer(1936, 1950)}`;
108
165
  if (lower.includes('adjective'))
109
166
  return random.pick(countryAdjectives);
110
167
  if (lower.includes('leader') || lower.includes('character'))
111
168
  return random.pick(leaderNames);
112
- if (lower.includes('country') || lower.endsWith('getname') || lower.includes('.getname'))
113
- return random.pick(countryNames);
169
+ if (lower.includes('country') || lower.endsWith('getname') || lower.includes('.getname')) {
170
+ const scope = key.includes('.') ? key.slice(0, key.indexOf('.')) : 'ROOT';
171
+ const existing = scopedCountries.get(scope);
172
+ if (existing !== undefined)
173
+ return existing;
174
+ const used = new Set(scopedCountries.values());
175
+ const choices = countryNames.filter((candidate) => !used.has(candidate));
176
+ const value = random.pick(choices.length === 0 ? countryNames : choices);
177
+ scopedCountries.set(scope, value);
178
+ return value;
179
+ }
114
180
  return random.pick(samples);
115
181
  }
116
- function generatedNumber(options, random) {
117
- const value = options.numericMinimum + random.next() * (options.numericMaximum - options.numericMinimum);
182
+ function generatedNumber(options, random, minimum = options.numericMinimum, maximum = options.numericMaximum) {
183
+ const boundedMinimum = Math.max(options.numericMinimum, minimum);
184
+ const boundedMaximum = Math.min(options.numericMaximum, maximum);
185
+ const low = Math.min(boundedMinimum, boundedMaximum);
186
+ const high = Math.max(boundedMinimum, boundedMaximum);
187
+ const value = low + random.next() * (high - low);
118
188
  return options.integerValues ? Math.round(value) : Math.round(value * 100) / 100;
119
189
  }
190
+ function generatedNumberForKey(key, options, random) {
191
+ const lower = key.toLocaleLowerCase('en-US');
192
+ if (/(?:ratio|fraction|progress)$/u.test(lower))
193
+ return generatedNumber(options, random, 0, 1);
194
+ if (/(?:percent|percentage|support|stability|war_support|legitimacy|threat|charge)/u.test(lower))
195
+ return generatedNumber(options, random, 0, 100);
196
+ return generatedNumber(options, random);
197
+ }
120
198
  function setIfMissing(target, key, value) {
121
199
  target[key] ??= value;
122
- const shortKey = key.slice(key.lastIndexOf('.') + 1);
123
- target[shortKey] ??= value;
200
+ }
201
+ function scriptedLocalisationValue(graph, token, language, random, kind, values) {
202
+ const shortToken = token.slice(token.lastIndexOf('.') + 1);
203
+ const definitions = graph.scriptedLocalisation.filter(({ name }) => name === token || name === shortToken);
204
+ if (definitions.length === 0)
205
+ return undefined;
206
+ const sprites = new Set(graph.sprites.map(({ name }) => name.toLocaleLowerCase('en-US')));
207
+ const choices = definitions.flatMap(({ choices }) => choices);
208
+ const eligible = choices.filter(({ localisationKey }) => kind === 'image'
209
+ ? sprites.has(localisationKey.toLocaleLowerCase('en-US'))
210
+ : localisationText(graph, language, localisationKey) !== undefined);
211
+ if (eligible.length === 0)
212
+ return undefined;
213
+ const evaluated = eligible.map((choice) => ({
214
+ choice,
215
+ result: evaluateSimpleTrigger(choice.triggerExpression, values),
216
+ }));
217
+ const matched = evaluated.find(({ result }) => result === true)?.choice;
218
+ const firstThreshold = evaluated
219
+ .map(({ choice }) => simpleThreshold(choice.triggerExpression))
220
+ .find((threshold) => threshold !== undefined && typeof values[threshold.variable] === 'number');
221
+ const unknownExists = evaluated.some(({ result }) => result === undefined);
222
+ const ranked = matched === undefined && unknownExists && firstThreshold !== undefined
223
+ ? eligible[Math.min(eligible.length - 1, Math.max(0, Math.round((1 - Math.max(0, Math.min(100, Number(values[firstThreshold.variable]))) / 100) *
224
+ (eligible.length - 1))))]
225
+ : undefined;
226
+ const fallback = evaluated.find(({ result }) => result === 'fallback')?.choice;
227
+ const selected = matched ?? ranked ?? fallback ?? random.pick(eligible);
228
+ if (kind === 'image')
229
+ return selected.localisationKey;
230
+ return localisationText(graph, language, selected.localisationKey);
231
+ }
232
+ function simpleThreshold(triggerExpression) {
233
+ if (triggerExpression === undefined)
234
+ return undefined;
235
+ const direct = /([A-Za-z_][A-Za-z0-9_.:^]*)\s*([<>])\s*(-?(?:\d+\.?\d*|\.\d+))/u.exec(triggerExpression);
236
+ if (direct !== null)
237
+ return {
238
+ variable: direct[1],
239
+ operator: direct[2],
240
+ threshold: Number(direct[3]),
241
+ };
242
+ const variable = /\bvar\s*=\s*([A-Za-z_][A-Za-z0-9_.:^]*)/u.exec(triggerExpression)?.[1];
243
+ const threshold = /\bvalue\s*=\s*(-?(?:\d+\.?\d*|\.\d+))/u.exec(triggerExpression)?.[1];
244
+ const compare = /\bcompare\s*=\s*(greater_than|less_than)/u.exec(triggerExpression)?.[1];
245
+ if (variable === undefined || threshold === undefined || compare === undefined)
246
+ return undefined;
247
+ return {
248
+ variable,
249
+ operator: compare === 'greater_than' ? '>' : '<',
250
+ threshold: Number(threshold),
251
+ };
252
+ }
253
+ function evaluateSimpleTrigger(triggerExpression, values) {
254
+ if (triggerExpression === undefined || /\balways\s*=\s*yes\b/u.test(triggerExpression))
255
+ return 'fallback';
256
+ if (/\balways\s*=\s*no\b/u.test(triggerExpression))
257
+ return false;
258
+ const threshold = simpleThreshold(triggerExpression);
259
+ if (threshold === undefined)
260
+ return undefined;
261
+ const value = values[threshold.variable];
262
+ if (typeof value !== 'number')
263
+ return undefined;
264
+ return threshold.operator === '>' ? value > threshold.threshold : value < threshold.threshold;
265
+ }
266
+ function materializeDynamicText(template, values) {
267
+ const valueFor = (key) => {
268
+ const value = values[key] ?? values[key.slice(key.lastIndexOf('.') + 1)];
269
+ return value === undefined ? undefined : String(value);
270
+ };
271
+ return template
272
+ .replace(/\[\?([^\]|]+)(?:\|[^\]]+)?\]/gu, (match, key) => valueFor(key) ?? match)
273
+ .replace(/\[([A-Za-z][A-Za-z0-9_.:-]+)\]/gu, (match, key) => {
274
+ if (key === 'X' || key === 'dynamic_loc')
275
+ return match;
276
+ return valueFor(key) ?? match;
277
+ });
278
+ }
279
+ function scalarNumber(value) {
280
+ if (typeof value === 'number' && Number.isFinite(value))
281
+ return value;
282
+ if (typeof value !== 'string' || !/^-?(?:\d+\.?\d*|\.\d+)$/u.test(value))
283
+ return undefined;
284
+ return Number(value);
285
+ }
286
+ function pointOrSize(element, attribute) {
287
+ const value = element.attributes[attribute];
288
+ if (typeof value !== 'object' || Array.isArray(value))
289
+ return undefined;
290
+ const x = scalarNumber(value.x ?? value.width);
291
+ const y = scalarNumber(value.y ?? value.height);
292
+ return x === undefined || y === undefined ? undefined : { x, y };
293
+ }
294
+ function rootRect(graph, windowName) {
295
+ if (windowName === undefined)
296
+ return undefined;
297
+ const root = graph.elements.find((element) => element.name === windowName && element.parentId === undefined);
298
+ if (root === undefined)
299
+ return undefined;
300
+ const position = pointOrSize(root, 'position') ?? { x: 0, y: 0 };
301
+ const size = pointOrSize(root, 'size');
302
+ if (size === undefined || size.x <= 0 || size.y <= 0)
303
+ return undefined;
304
+ return { x: position.x, y: position.y, width: size.x, height: size.y };
305
+ }
306
+ function substantiallyOverlaps(left, right) {
307
+ if (left === undefined || right === undefined)
308
+ return false;
309
+ const width = Math.max(0, Math.min(left.x + left.width, right.x + right.width) - Math.max(left.x, right.x));
310
+ const height = Math.max(0, Math.min(left.y + left.height, right.y + right.height) - Math.max(left.y, right.y));
311
+ const smallerArea = Math.min(left.width * left.height, right.width * right.height);
312
+ return smallerArea > 0 && (width * height) / smallerArea >= 0.5;
313
+ }
314
+ function generatedVisibility(graph, scripted, options, random, base) {
315
+ const visibility = { ...base };
316
+ const dynamic = scripted.filter(({ visibleExpression }) => visibleExpression !== undefined);
317
+ if (options.visibility === 'show-all') {
318
+ for (const definition of dynamic)
319
+ visibility[definition.name] ??= true;
320
+ return visibility;
321
+ }
322
+ for (const definition of dynamic) {
323
+ if (visibility[definition.name] !== undefined)
324
+ continue;
325
+ visibility[definition.name] =
326
+ definition.parentScriptedGui === undefined ||
327
+ !transientWindowPattern.test(`${definition.name} ${definition.windowName ?? ''}`);
328
+ }
329
+ const siblingGroups = new Map();
330
+ for (const definition of dynamic) {
331
+ if (definition.parentScriptedGui === undefined)
332
+ continue;
333
+ const siblings = siblingGroups.get(definition.parentScriptedGui) ?? [];
334
+ siblings.push(definition);
335
+ siblingGroups.set(definition.parentScriptedGui, siblings);
336
+ }
337
+ for (const siblings of siblingGroups.values()) {
338
+ const unvisited = new Set(siblings);
339
+ while (unvisited.size > 0) {
340
+ const first = unvisited.values().next().value;
341
+ if (first === undefined)
342
+ break;
343
+ unvisited.delete(first);
344
+ const component = [first];
345
+ for (const current of component) {
346
+ for (const candidate of [...unvisited]) {
347
+ if (substantiallyOverlaps(rootRect(graph, current.windowName), rootRect(graph, candidate.windowName))) {
348
+ component.push(candidate);
349
+ unvisited.delete(candidate);
350
+ }
351
+ }
352
+ }
353
+ if (component.length < 2)
354
+ continue;
355
+ const selectable = component.filter((definition) => !transientWindowPattern.test(`${definition.name} ${definition.windowName ?? ''}`));
356
+ const selected = random.pick(selectable.length === 0 ? component : selectable);
357
+ for (const definition of component) {
358
+ if (base[definition.name] === undefined)
359
+ visibility[definition.name] = definition === selected;
360
+ }
361
+ }
362
+ }
363
+ const visiblePageNames = dynamic
364
+ .filter(({ name }) => visibility[name] === true)
365
+ .flatMap(({ name, windowName: visibleWindowName }) => [name, visibleWindowName ?? ''])
366
+ .map((name) => name.toLocaleLowerCase('en-US'));
367
+ const triggerVisibilityTargets = [
368
+ ...new Set(scripted.flatMap(({ triggerDefinitions }) => triggerDefinitions
369
+ .filter(({ name }) => name.endsWith('_visible'))
370
+ .map(({ elementName }) => elementName))),
371
+ ];
372
+ const triggerGroups = new Map();
373
+ for (const target of triggerVisibilityTargets) {
374
+ const key = target.replace(/_(?:active|disabled|enabled|idle|off|on|selected|unselected)$/iu, '');
375
+ const group = triggerGroups.get(key) ?? [];
376
+ group.push(target);
377
+ triggerGroups.set(key, group);
378
+ }
379
+ for (const [baseName, targets] of triggerGroups) {
380
+ if (targets.length === 1) {
381
+ visibility[targets[0]] ??= !transientWindowPattern.test(targets[0]);
382
+ continue;
383
+ }
384
+ const topic = baseName.replace(/_(?:tab_)?button$/iu, '').toLocaleLowerCase('en-US');
385
+ const pageSelected = visiblePageNames.some((name) => name.includes(topic));
386
+ const preferredSuffix = pageSelected
387
+ ? /_(?:active|on|selected)$/iu
388
+ : /_(?:idle|off|unselected)$/iu;
389
+ const preferred = targets.find((target) => preferredSuffix.test(target)) ?? random.pick(targets);
390
+ for (const target of targets)
391
+ if (base[target] === undefined)
392
+ visibility[target] = target === preferred;
393
+ }
394
+ return visibility;
124
395
  }
125
396
  function generatedRows(count, random, options) {
397
+ const countryOffset = random.integer(0, countryNames.length - 1);
126
398
  return Array.from({ length: count }, (_unused, index) => {
127
- const name = plausibleText('GetName', random, options.textSamples);
399
+ const countryIndex = (countryOffset + index) % countryNames.length;
400
+ const name = countryNames[countryIndex];
128
401
  const value = generatedNumber(options, random);
129
402
  return {
130
403
  index: index + 1,
@@ -132,70 +405,131 @@ function generatedRows(count, random, options) {
132
405
  label: name,
133
406
  name,
134
407
  GetName: name,
408
+ GetAdjective: countryAdjectives[countryIndex],
135
409
  value,
136
410
  amount: value,
137
411
  percentage: Math.max(0, Math.min(100, value)),
138
412
  selected: index === 0,
139
- enabled: random.boolean(options.trueProbability),
413
+ enabled: true,
140
414
  };
141
415
  });
142
416
  }
143
417
  export function generateGuiPreviewScenarios(graph, windowName, base, options) {
144
418
  if (!options.enabled)
145
419
  return [];
146
- const elements = relevantElements(graph, windowName);
147
- const scripted = graph.scriptedGuis.filter((definition) => definition.windowName === windowName || definition.parentWindowName === windowName);
148
- const tokens = dynamicTokens(sourceTexts(graph, elements, base.language, scripted));
420
+ const scripted = relatedScriptedGuis(graph, windowName);
421
+ const elements = relevantElements(graph, windowName, scripted);
422
+ const tokens = tokensIn(sourceTexts(graph, elements, base.language, scripted));
423
+ const imageTokens = new Set();
424
+ const booleanTokens = new Set();
425
+ const frameTokens = new Set();
426
+ for (const definition of scripted) {
427
+ for (const property of definition.propertyDefinitions) {
428
+ for (const [attribute, expression] of Object.entries(property.attributes)) {
429
+ const token = expressionToken(expression);
430
+ if (token === undefined)
431
+ continue;
432
+ const normalized = attribute.toLocaleLowerCase('en-US');
433
+ if (normalized === 'image')
434
+ imageTokens.add(token);
435
+ else if (normalized === 'visible' || normalized === 'enabled')
436
+ booleanTokens.add(token);
437
+ else if (normalized === 'frame')
438
+ frameTokens.add(token);
439
+ else
440
+ tokensIn(stringsIn(expression)).numeric.forEach((value) => tokens.numeric.add(value));
441
+ }
442
+ }
443
+ }
444
+ for (const token of [...imageTokens, ...booleanTokens, ...frameTokens]) {
445
+ tokens.numeric.delete(token);
446
+ tokens.textual.delete(token);
447
+ }
448
+ const relevantDynamicTokens = new Set([...tokens.textual, ...imageTokens]);
449
+ for (const definition of graph.scriptedLocalisation) {
450
+ if (![...relevantDynamicTokens].some((token) => token === definition.name || token.endsWith(`.${definition.name}`)))
451
+ continue;
452
+ for (const choice of definition.choices) {
453
+ const threshold = simpleThreshold(choice.triggerExpression);
454
+ if (threshold !== undefined)
455
+ tokens.numeric.add(threshold.variable);
456
+ }
457
+ }
458
+ const progressRanges = new Map();
459
+ for (const element of elements) {
460
+ if (!/progressbar/iu.test(element.elementType))
461
+ continue;
462
+ const minimum = scalarNumber(element.attributes.minValue) ?? 0;
463
+ const maximum = scalarNumber(element.attributes.maxValue) ?? 100;
464
+ progressRanges.set(element.name, { minimum, maximum });
465
+ const startValue = element.attributes.startValue;
466
+ if (typeof startValue === 'string' && !/^-?(?:\d+\.?\d*|\.\d+)$/u.test(startValue))
467
+ tokens.numeric.add(startValue);
468
+ }
149
469
  const dynamicLists = [...new Set(scripted.flatMap(({ dynamicLists }) => dynamicLists))].sort();
150
- const visibilityTargets = new Set(scripted.flatMap((definition) => definition.propertyDefinitions
151
- .filter((property) => Object.keys(property.attributes).some((key) => key.toLocaleLowerCase('en-US') === 'visible'))
152
- .map(({ elementName }) => elementName)));
153
470
  const enabledTargets = new Set(scripted.flatMap((definition) => [
154
471
  ...definition.triggerDefinitions.map(({ elementName }) => elementName),
155
472
  ...definition.propertyDefinitions
156
473
  .filter((property) => Object.keys(property.attributes).some((key) => key.toLocaleLowerCase('en-US') === 'enabled'))
157
474
  .map(({ elementName }) => elementName),
158
475
  ]));
159
- const spritesByName = new Map(graph.sprites.map((sprite) => [sprite.name.toLocaleLowerCase('en-US'), sprite]));
160
476
  const sourceSeed = options.seed === 'auto' ? `${windowName}:${base.id}:${hashSourceGraph(graph)}` : options.seed;
161
477
  return Array.from({ length: options.count }, (_unused, scenarioIndex) => {
162
478
  const random = new ScenarioRandom(`${sourceSeed}:${scenarioIndex}`);
163
479
  const values = { ...base.values };
164
480
  const lists = Object.fromEntries(Object.entries(base.lists).map(([key, rows]) => [key, rows.map((row) => ({ ...row }))]));
165
- const visibility = { ...base.visibility };
481
+ const visibility = generatedVisibility(graph, scripted, options, random, base.visibility);
166
482
  const elementStates = { ...base.elementStates };
167
- const selectedFrames = { ...base.selectedFrames };
483
+ const scopedCountries = new Map();
168
484
  for (const key of tokens.numeric)
169
- setIfMissing(values, key, generatedNumber(options, random));
485
+ setIfMissing(values, key, generatedNumberForKey(key, options, random));
486
+ for (const [elementName, { minimum, maximum }] of progressRanges) {
487
+ const element = elements.find(({ name }) => name === elementName);
488
+ const startValueToken = typeof element?.attributes.startValue === 'string'
489
+ ? element.attributes.startValue
490
+ : undefined;
491
+ const existingElementValue = values[elementName];
492
+ const existingTokenValue = startValueToken === undefined ? undefined : values[startValueToken];
493
+ const value = typeof existingElementValue === 'number'
494
+ ? existingElementValue
495
+ : typeof existingTokenValue === 'number'
496
+ ? existingTokenValue
497
+ : generatedNumber(options, random, minimum, maximum);
498
+ setIfMissing(values, elementName, value);
499
+ if (startValueToken !== undefined)
500
+ setIfMissing(values, startValueToken, value);
501
+ }
502
+ for (const key of tokens.numeric) {
503
+ if (!key.endsWith('_bar') || base.values[key] !== undefined)
504
+ continue;
505
+ const sourceKey = key.slice(0, -'_bar'.length);
506
+ const sourceValue = values[sourceKey];
507
+ if (typeof sourceValue === 'number')
508
+ values[key] = sourceValue;
509
+ }
170
510
  for (const key of tokens.textual)
171
- setIfMissing(values, key, plausibleText(key, random, options.textSamples));
172
- for (const element of elements) {
173
- if (values[element.name] === undefined && /progressbar/iu.test(element.elementType))
174
- values[element.name] = generatedNumber(options, random);
175
- if (visibilityTargets.has(element.name) && visibility[element.name] === undefined)
176
- visibility[element.name] =
177
- options.visibility === 'show-all' || random.boolean(options.trueProbability);
178
- elementStates[element.name] ??= enabledTargets.has(element.name)
179
- ? random.boolean(options.trueProbability)
180
- ? options.elementStates === 'normal'
181
- ? 'normal'
182
- : random.pick(variedStates)
183
- : 'disabled'
184
- : options.elementStates === 'normal'
185
- ? 'normal'
186
- : random.pick(variedStates);
187
- const spriteName = [
188
- element.attributes.spriteType,
189
- element.attributes.quadTextureSprite,
190
- element.attributes.background,
191
- ].find((candidate) => typeof candidate === 'string');
192
- const frameCount = spriteName === undefined
193
- ? 1
194
- : (spritesByName.get(spriteName.toLocaleLowerCase('en-US'))?.frameCount ?? 1);
195
- if (Number.isFinite(frameCount) &&
196
- frameCount > 1 &&
197
- selectedFrames[element.name] === undefined)
198
- selectedFrames[element.name] = random.integer(0, Math.trunc(frameCount) - 1);
511
+ setIfMissing(values, key, plausibleText(key, random, options.textSamples, scopedCountries));
512
+ const scriptedTextTemplates = new Map();
513
+ for (const key of tokens.textual) {
514
+ const scriptedValue = scriptedLocalisationValue(graph, key, base.language, random, 'text', values);
515
+ if (scriptedValue !== undefined && base.values[key] === undefined)
516
+ scriptedTextTemplates.set(key, scriptedValue);
517
+ }
518
+ for (let pass = 0; pass < 3; pass += 1)
519
+ for (const [key, template] of scriptedTextTemplates)
520
+ values[key] = materializeDynamicText(template, values);
521
+ for (const key of imageTokens) {
522
+ const sprite = scriptedLocalisationValue(graph, key, base.language, random, 'image', values);
523
+ if (sprite !== undefined)
524
+ setIfMissing(values, key, sprite);
525
+ }
526
+ for (const key of booleanTokens)
527
+ setIfMissing(values, key, random.boolean(options.trueProbability));
528
+ for (const key of frameTokens)
529
+ setIfMissing(values, key, 1);
530
+ if (options.elementStates === 'varied') {
531
+ for (const elementName of enabledTargets)
532
+ elementStates[elementName] ??= random.pick(variedStates);
199
533
  }
200
534
  for (const listName of dynamicLists) {
201
535
  if (lists[listName] !== undefined)
@@ -206,12 +540,11 @@ export function generateGuiPreviewScenarios(graph, windowName, base, options) {
206
540
  return parsePreviewScenario({
207
541
  ...base,
208
542
  id: `${base.id}-${options.idPrefix}-${scenarioIndex + 1}`,
209
- description: `Generated plausible scenario ${scenarioIndex + 1} (seed ${sourceSeed})`,
543
+ description: `Generated source-aware scenario ${scenarioIndex + 1} (seed ${sourceSeed})`,
210
544
  values,
211
545
  lists,
212
546
  visibility,
213
547
  elementStates,
214
- selectedFrames,
215
548
  });
216
549
  });
217
550
  }
@@ -204,7 +204,7 @@ export const GuiGeneratedScenarioOptionsSchema = z
204
204
  .max(64)
205
205
  .regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u)
206
206
  .default('generated'),
207
- preservePlaceholder: z.boolean().default(true),
207
+ preservePlaceholder: z.boolean().default(false),
208
208
  numericMinimum: z.number().min(-1_000_000_000).max(1_000_000_000).default(0),
209
209
  numericMaximum: z.number().min(-1_000_000_000).max(1_000_000_000).default(100),
210
210
  integerValues: z.boolean().default(true),