hoi4-agent-tools 4.0.3 → 4.0.5
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.
- package/CHANGELOG.md +18 -0
- package/dist/hoi4_agent_tools/core/clausewitz-evaluation.js +5 -3
- package/dist/hoi4_agent_tools/core/condition-evaluator.js +5 -1
- package/dist/hoi4_agent_tools/core/index.js +11 -0
- package/dist/hoi4_agent_tools/core/job-executor.js +3 -1
- package/dist/hoi4_agent_tools/core/job-worker-host.js +1 -1
- package/dist/hoi4_agent_tools/core/render-progress.js +7 -0
- package/dist/hoi4_agent_tools/gui/layout.js +76 -33
- package/dist/hoi4_agent_tools/gui/native-state.js +40 -1
- package/dist/hoi4_agent_tools/gui/scenario-generator.js +3 -3
- package/dist/hoi4_agent_tools/gui/source-graph.js +67 -32
- package/dist/hoi4_agent_tools/gui/studio.js +101 -32
- package/dist/hoi4_agent_tools/gui/tool-operations.js +8 -2
- package/dist/hoi4_agent_tools/mcp/server/create-modern-operations.js +2 -1
- package/dist/hoi4_agent_tools/mcp/server/job-task-store.js +3 -1
- package/dist/hoi4_agent_tools/mcp/server/modern-tasks.js +2 -0
- package/dist/hoi4_agent_tools/mcp/server/progress.js +12 -0
- package/dist/hoi4_agent_tools/mcp/server/request-lifecycle.js +2 -2
- package/dist/hoi4_agent_tools/mcp/server/task-request-context.js +5 -2
- package/dist/hoi4_agent_tools/technology/job-operations.js +1 -0
- package/dist/hoi4_agent_tools/technology/service.js +18 -37
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/gui.md +14 -2
- package/docs/technology.md +2 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/schemas/configuration.schema.json +1 -1
- package/schemas/continuous-focus-palette.schema.json +1 -1
- package/schemas/custom-weighted-pool.schema.json +1 -1
- package/schemas/decision-inspect-input.schema.json +1 -1
- package/schemas/error-log-input.schema.json +1 -1
- package/schemas/event-feature-manifest.schema.json +1 -1
- package/schemas/focus-plan.schema.json +1 -1
- package/schemas/focus-planning-sidecar.schema.json +1 -1
- package/schemas/gui-animation-source.schema.json +1 -1
- package/schemas/gui-helper.schema.json +1 -1
- package/schemas/gui-scenario.schema.json +1 -1
- package/schemas/helper-expansion-request.schema.json +1 -1
- package/schemas/helper-expansion-summary.schema.json +1 -1
- package/schemas/impact-inspect-input.schema.json +1 -1
- package/schemas/map-operation.schema.json +1 -1
- package/schemas/mechanic-test-input.schema.json +1 -1
- package/schemas/mod-index-input.schema.json +1 -1
- package/schemas/operation-result.schema.json +1 -1
- package/schemas/package-check-input.schema.json +1 -1
- package/schemas/package-manifest.schema.json +1 -1
- package/schemas/probability-analysis-result.schema.json +1 -1
- package/schemas/probability-compare-input.schema.json +1 -1
- package/schemas/probability-evaluate-input.schema.json +1 -1
- package/schemas/probability-inspect-input.schema.json +1 -1
- package/schemas/probability-render-input.schema.json +1 -1
- package/schemas/probability-scenario-set.schema.json +1 -1
- package/schemas/probability-sequence-input.schema.json +1 -1
- package/schemas/probability-simulate-input.schema.json +1 -1
- package/schemas/probability-sweep-input.schema.json +1 -1
- package/schemas/reference-context-input.schema.json +1 -1
- package/schemas/reference-read-input.schema.json +1 -1
- package/schemas/reference-search-input.schema.json +1 -1
- package/schemas/scenario-suite.schema.json +1 -1
- package/schemas/scenario-test-input.schema.json +1 -1
- package/schemas/script-validate-input.schema.json +1 -1
- package/schemas/shared-scenario.schema.json +1 -1
- package/schemas/source-lookup-input.schema.json +1 -1
- package/server.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 4.0.5 - 2026-10-14
|
|
6
|
+
|
|
7
|
+
- Keep child anchors independent of parent `origo`, so centered native windows place their controls inside the frame.
|
|
8
|
+
- Preview division and HQ branches separately and close native selector overlays by default. Existing scenario values and visibility bindings select panels explicitly.
|
|
9
|
+
- Preserve dropdown button and expanded-panel boundaries, including closed visibility, settled positions, backgrounds, clipping and attached scrollbars.
|
|
10
|
+
- Render background-free scroll containers using their declared viewport bounds; retain findings for explicitly disabled clipping and invalid boundaries or references.
|
|
11
|
+
- Resolve public scrollbar templates despite unrelated private namesakes. Public tool schemas and dependency versions are unchanged.
|
|
12
|
+
|
|
13
|
+
## 4.0.4 - 2026-10-14
|
|
14
|
+
|
|
15
|
+
- Preview individual GUI windows without redundant full symbol indexes, workspace-wide asset fallback or unbound country-flag inventories; reuse condition definitions across render variants.
|
|
16
|
+
- Index technology sources once across layers and scope folder rendering to definitions, layouts and unlock targets. Full inspection retains inbound grants and references.
|
|
17
|
+
- Report render phases and include the last phase in worker deadline errors. Existing parsing, raster-memory, canonical-path and stale-source limits remain enforced.
|
|
18
|
+
- Resolve transitive localisation `$KEY$` references across files and bounded scenario substitutions; retain diagnostics for cycles and missing runtime values.
|
|
19
|
+
- Honour numeric flag presence in scalar visibility checks, matching valued-flag super-event localisation conditions.
|
|
20
|
+
- Preserve supplied GUI scenarios by default; exploratory `generatedScenarios` is now opt-in. Explicit state/resolution matrices and public schema shapes are unchanged.
|
|
21
|
+
- Retain regression coverage for hidden sub-windows, timed animated-sprite frames, 16-bit DDS portraits, declared font colours, worker recovery and render-memory ceilings.
|
|
22
|
+
|
|
5
23
|
## 4.0.3 - 2026-10-12
|
|
6
24
|
|
|
7
25
|
- Publish the README with the supplied HOI4 MCP Server thumbnail and direct links to the demo video on YouTube.
|
|
@@ -31,11 +31,13 @@ export class ClausewitzEvaluationDefinitions {
|
|
|
31
31
|
scriptedTriggers = new Map();
|
|
32
32
|
localConstants = new Map();
|
|
33
33
|
static build(snapshot) {
|
|
34
|
-
const cached = definitionCache.get(snapshot);
|
|
34
|
+
const cached = definitionCache.get(snapshot.files);
|
|
35
35
|
if (cached !== undefined)
|
|
36
36
|
return cached;
|
|
37
37
|
const result = new ClausewitzEvaluationDefinitions();
|
|
38
|
-
for (const file of snapshot.files.filter(({ shadowedBy, relativePath }) => shadowedBy === undefined &&
|
|
38
|
+
for (const file of snapshot.files.filter(({ shadowedBy, relativePath, bytes }) => shadowedBy === undefined &&
|
|
39
|
+
relativePath.toLowerCase().endsWith('.txt') &&
|
|
40
|
+
bytes.includes(0x40))) {
|
|
39
41
|
const document = parseClausewitz(file.bytes, file.displayPath);
|
|
40
42
|
const locals = new Map();
|
|
41
43
|
for (const assignment of scalarAssignments(document.root)) {
|
|
@@ -84,7 +86,7 @@ export class ClausewitzEvaluationDefinitions {
|
|
|
84
86
|
});
|
|
85
87
|
}
|
|
86
88
|
}
|
|
87
|
-
definitionCache.set(snapshot, result);
|
|
89
|
+
definitionCache.set(snapshot.files, result);
|
|
88
90
|
return result;
|
|
89
91
|
}
|
|
90
92
|
}
|
|
@@ -286,7 +286,11 @@ function evaluateAssignmentCore(assignment, scenario, candidate, definitions, he
|
|
|
286
286
|
: { state: value ? 'true' : 'false', unresolved: [] };
|
|
287
287
|
}
|
|
288
288
|
if (key === 'has_country_flag' || key === 'has_global_flag' || key === 'has_state_flag') {
|
|
289
|
-
const
|
|
289
|
+
const stored = key === 'has_global_flag'
|
|
290
|
+
? (scopeStateValue(scenario, `global_flag.${right}`, scopeContext) ??
|
|
291
|
+
scopeStateValue(scenario, `flag.${right}`, scopeContext))
|
|
292
|
+
: scopeStateValue(scenario, `flag.${right}`, scopeContext);
|
|
293
|
+
const explicit = typeof stored === 'number' && Number.isFinite(stored) ? true : bool(stored);
|
|
290
294
|
if (explicit !== undefined)
|
|
291
295
|
return { state: explicit ? 'true' : 'false', unresolved: [] };
|
|
292
296
|
const flags = key === 'has_global_flag' && scenario.globalFlags !== undefined
|
|
@@ -383,6 +383,17 @@ export class SymbolIndex {
|
|
|
383
383
|
},
|
|
384
384
|
});
|
|
385
385
|
}
|
|
386
|
+
/** Record parser coverage without allocating symbols for a domain that builds its own graph. */
|
|
387
|
+
recordSourceCoverage(file, diagnostics) {
|
|
388
|
+
const limits = sourcePartialLimitDiagnostics(diagnostics);
|
|
389
|
+
if (limits.length > 0) {
|
|
390
|
+
this.recordSkippedSource(file, limits);
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
if (isWorkspaceOwnedSource(file) && file.shadowedBy === undefined)
|
|
394
|
+
this.addDiagnostics(diagnostics);
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
386
397
|
addSymbol(record) {
|
|
387
398
|
if (this.symbols.length >= INDEX_RECORD_LIMIT) {
|
|
388
399
|
this.#complete = false;
|
|
@@ -106,10 +106,11 @@ export class JobExecutor {
|
|
|
106
106
|
const controller = new AbortController();
|
|
107
107
|
const watchStop = new AbortController();
|
|
108
108
|
let watchFailure;
|
|
109
|
+
let lastProgressMessage = claimed.record.progress?.message;
|
|
109
110
|
const deadlineTimer = options.deadlineMs === undefined || operation.mutation
|
|
110
111
|
? undefined
|
|
111
112
|
: setTimeout(() => {
|
|
112
|
-
watchFailure = new ServiceError('JOB_DEADLINE_EXCEEDED',
|
|
113
|
+
watchFailure = new ServiceError('JOB_DEADLINE_EXCEEDED', `The job ran past its deadline and was stopped${lastProgressMessage === undefined ? '' : ` during: ${lastProgressMessage.slice(0, 160)}`}; narrow its selector or limits, or raise jobDeadlineSeconds`);
|
|
113
114
|
controller.abort(watchFailure);
|
|
114
115
|
}, Math.max(0, options.deadlineMs - Date.now()));
|
|
115
116
|
const watch = (async () => {
|
|
@@ -136,6 +137,7 @@ export class JobExecutor {
|
|
|
136
137
|
signal: controller.signal,
|
|
137
138
|
checkpoint: claimed.record.checkpoint,
|
|
138
139
|
progress: async (progress) => {
|
|
140
|
+
lastProgressMessage = progress.message;
|
|
139
141
|
await this.jobs.store.updateOwned(scope, id, owner.token, { progress }, controller.signal);
|
|
140
142
|
},
|
|
141
143
|
saveCheckpoint: async (checkpoint) => {
|
|
@@ -309,7 +309,7 @@ export class JobWorkerHost {
|
|
|
309
309
|
await stopChild(child, this.stoppedByHost);
|
|
310
310
|
await this.jobs.failInterrupted(workspaceId, id, {
|
|
311
311
|
code: 'JOB_DEADLINE_EXCEEDED',
|
|
312
|
-
message: `The job ran longer than ${jobDeadlineSeconds} seconds and was stopped; narrow its selector or limits, or raise jobDeadlineSeconds`,
|
|
312
|
+
message: `The job ran longer than ${jobDeadlineSeconds} seconds and was stopped${record.progress === undefined ? '' : ` during: ${record.progress.message.slice(0, 160)}`}; narrow its selector or limits, or raise jobDeadlineSeconds`,
|
|
313
313
|
}, principal);
|
|
314
314
|
return;
|
|
315
315
|
}
|
|
@@ -219,15 +219,20 @@ function resolveTokenText(value, scenario, localisation, rowValues) {
|
|
|
219
219
|
/^[A-Za-z0-9_.-]+$/u.test(value) &&
|
|
220
220
|
(value.includes('_') || value === value.toUpperCase()))
|
|
221
221
|
missingLocalisation = true;
|
|
222
|
-
|
|
222
|
+
const expandTokens = (input, stack, depth) => replaceTextBounded(input, /\$([A-Za-z0-9_.-]+)\$/gu, (match) => {
|
|
223
223
|
const key = match[1] ?? '';
|
|
224
|
-
const replacement = scenario.localisation[key] ??
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
224
|
+
const replacement = scenario.localisation[key] ??
|
|
225
|
+
localisation.get(key) ??
|
|
226
|
+
rowValues?.[key] ??
|
|
227
|
+
scenario.values[key] ??
|
|
228
|
+
scenario.variables[key];
|
|
229
|
+
if (replacement === undefined || stack.has(key) || depth >= 16) {
|
|
230
|
+
unresolved.push(match[0]);
|
|
231
|
+
return match[0];
|
|
228
232
|
}
|
|
229
|
-
return replacement;
|
|
233
|
+
return expandTokens(String(replacement), new Set([...stack, key]), depth + 1);
|
|
230
234
|
}, 'GUI localisation substitution');
|
|
235
|
+
text = expandTokens(text, new Set([value]), 0);
|
|
231
236
|
text = replaceTextBounded(text, /\[\?([^\]|]+)(?:\|[^\]]+)?\]/gu, (match) => {
|
|
232
237
|
const key = match[1] ?? '';
|
|
233
238
|
const shortKey = key.slice(key.lastIndexOf('.') + 1);
|
|
@@ -624,11 +629,12 @@ function explicitElementVisibility(definition, scenario, rowValues) {
|
|
|
624
629
|
visibilityValue(scripted));
|
|
625
630
|
}
|
|
626
631
|
/** Source visibility: `visible` and `show` mean shown when true, `hide` and `hidden` when false. */
|
|
627
|
-
function declaredVisibility(attributes) {
|
|
632
|
+
function declaredVisibility(attributes, fallback = true) {
|
|
628
633
|
const visible = scalarBoolean(property(attributes, 'visible', 'show'));
|
|
629
634
|
if (visible !== undefined)
|
|
630
635
|
return visible;
|
|
631
|
-
|
|
636
|
+
const hidden = scalarBoolean(property(attributes, 'hide', 'hidden'));
|
|
637
|
+
return hidden === undefined ? fallback : !hidden;
|
|
632
638
|
}
|
|
633
639
|
function clippedByChain(chain) {
|
|
634
640
|
let result;
|
|
@@ -689,7 +695,7 @@ function refreshSceneClips(context) {
|
|
|
689
695
|
}
|
|
690
696
|
}
|
|
691
697
|
function attachedScrollbars(definition, context) {
|
|
692
|
-
if (!/^(?:containerwindowtype|windowtype)$/iu.test(definition.elementType))
|
|
698
|
+
if (!/^(?:containerwindowtype|windowtype|expandedwindow)$/iu.test(definition.elementType))
|
|
693
699
|
return [];
|
|
694
700
|
const result = [];
|
|
695
701
|
const add = (candidate, horizontal, reference) => {
|
|
@@ -713,8 +719,10 @@ function attachedScrollbars(definition, context) {
|
|
|
713
719
|
for (const horizontal of [false, true]) {
|
|
714
720
|
const name = scalarString(property(definition.attributes, horizontal ? 'horizontalScrollbar' : 'verticalScrollbar'));
|
|
715
721
|
if (name !== undefined) {
|
|
716
|
-
const candidate = context.
|
|
717
|
-
|
|
722
|
+
const candidate = context.elementsByNameCandidates
|
|
723
|
+
.get(name)
|
|
724
|
+
?.find((element) => element.parentId === undefined);
|
|
725
|
+
add(candidate, horizontal, name);
|
|
718
726
|
}
|
|
719
727
|
}
|
|
720
728
|
for (const id of definition.childIds) {
|
|
@@ -1213,10 +1221,13 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
1213
1221
|
const clipChain = inheritedClipChain ?? (inheritedClip === undefined ? [] : [inheritedClip]);
|
|
1214
1222
|
const localScale = scalarNumber(property(definition.attributes, 'scale'), 1) ?? 1;
|
|
1215
1223
|
const scale = parentScale * localScale;
|
|
1216
|
-
const
|
|
1224
|
+
const expandedWindow = definition.elementType.toLowerCase() === 'expandedwindow';
|
|
1225
|
+
const shownPosition = depth === 0 || expandedWindow
|
|
1226
|
+
? objectProperty(property(definition.attributes, 'show_position'))
|
|
1227
|
+
: undefined;
|
|
1217
1228
|
const position = shownPosition ?? objectProperty(property(definition.attributes, 'position'));
|
|
1218
1229
|
if (shownPosition !== undefined) {
|
|
1219
|
-
addFidelity(fidelity, 'modelled', 'window_show_position', 'The requested
|
|
1230
|
+
addFidelity(fidelity, 'modelled', 'window_show_position', 'The requested window uses its declared settled show position.', definition);
|
|
1220
1231
|
}
|
|
1221
1232
|
const size = objectProperty(property(definition.attributes, 'size'));
|
|
1222
1233
|
const scriptedX = rowValues?.[`${definition.name}.x`] ??
|
|
@@ -1272,13 +1283,19 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
1272
1283
|
width = Math.max(0, parentRect.width - localX + width);
|
|
1273
1284
|
if (height < 0)
|
|
1274
1285
|
height = Math.max(0, parentRect.height - localY + height);
|
|
1275
|
-
const explicitlyVisible = explicitElementVisibility(definition, scenario, rowValues)
|
|
1286
|
+
const explicitlyVisible = explicitElementVisibility(definition, scenario, rowValues) ??
|
|
1287
|
+
context.nativeVisibility.get(definition.id);
|
|
1276
1288
|
const unresolvedVisibilityReason = context.unresolvedElementVisibility.get(definition.name);
|
|
1277
1289
|
const parentSceneElement = parentInstanceId === undefined ? undefined : context.instancesById.get(parentInstanceId);
|
|
1278
1290
|
const parentUnresolvedVisibility = parentSceneElement?.visibilityStatus === 'unresolved'
|
|
1279
1291
|
? parentSceneElement.visibilityStatusReason
|
|
1280
1292
|
: undefined;
|
|
1281
|
-
const
|
|
1293
|
+
const dropdown = definition.parentId === undefined ? undefined : context.elementsById.get(definition.parentId);
|
|
1294
|
+
const sourceVisible = declaredVisibility(definition.attributes, !expandedWindow ||
|
|
1295
|
+
depth === 0 ||
|
|
1296
|
+
visibilityValue(rowValues?.[`${dropdown?.name}.expanded`] ?? scenario.values[`${dropdown?.name}.expanded`]) === true);
|
|
1297
|
+
if (expandedWindow)
|
|
1298
|
+
addFidelity(fidelity, 'modelled', 'native_dropdown', 'Dropdown expansion is closed by default; bind <dropdown>.expanded or explicit panel visibility to show its settled position.', definition);
|
|
1282
1299
|
const resolveVisibleAppearance = inheritedVisible && (explicitlyVisible ?? sourceVisible);
|
|
1283
1300
|
const scriptedEnabled = rowValues?.[`${definition.name}.enabled`] ??
|
|
1284
1301
|
scenario.values[`${definition.name}.enabled`] ??
|
|
@@ -1614,12 +1631,13 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
1614
1631
|
else if (origoAxes.vertical === 'bottom')
|
|
1615
1632
|
y -= height;
|
|
1616
1633
|
const unclippedRect = { x, y, width: Math.max(0, width), height: Math.max(0, height) };
|
|
1617
|
-
const ownClipping = scalarBoolean(property(definition.attributes, 'clipping')) ??
|
|
1618
|
-
(backgrounds.length > 0 && /(?:containerwindow|windowtype)/iu.test(definition.elementType));
|
|
1619
1634
|
let scrollbars = attachedScrollbars(definition, context);
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1635
|
+
const ownClipping = scalarBoolean(property(definition.attributes, 'clipping')) ??
|
|
1636
|
+
((backgrounds.length > 0 || scrollbars.length > 0) &&
|
|
1637
|
+
/(?:containerwindow|windowtype|expandedwindow)/iu.test(definition.elementType));
|
|
1638
|
+
if (scrollbars.length > 0 && (!ownClipping || width <= 0 || height <= 0)) {
|
|
1639
|
+
diagnostics.push(diagnostic('GUI_SCROLLBAR_CONTAINER_BOUNDARY_MISSING', 'warning', `Container ${definition.name} needs a positive-sized clipping boundary for attached scrolling.`, definition));
|
|
1640
|
+
addFidelity(fidelity, 'unsupported', 'attached_scrollbar', 'The container has no active clipping boundary.', definition);
|
|
1623
1641
|
scrollbars = [];
|
|
1624
1642
|
}
|
|
1625
1643
|
const margin = scrollbars.length === 0 ? undefined : objectProperty(property(definition.attributes, 'margin'));
|
|
@@ -1830,10 +1848,12 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
1830
1848
|
const childDefinitions = await scrollbarChildren(definition, definition.childIds
|
|
1831
1849
|
.map((id) => context.elementsById.get(id))
|
|
1832
1850
|
.filter((element) => element !== undefined)
|
|
1833
|
-
.filter((element) => !/^(?:containerwindowtype|windowtype)$/iu.test(definition.elementType) ||
|
|
1851
|
+
.filter((element) => !/^(?:containerwindowtype|windowtype|expandedwindow)$/iu.test(definition.elementType) ||
|
|
1834
1852
|
element.elementType.toLowerCase() !== 'extendedscrollbartype')
|
|
1835
|
-
.sort((left, right) => (
|
|
1836
|
-
(
|
|
1853
|
+
.sort((left, right) => Number(context.nativeForeground.has(left.id)) -
|
|
1854
|
+
Number(context.nativeForeground.has(right.id)) ||
|
|
1855
|
+
(scalarNumber(property(left.attributes, 'priority'), 0) ?? 0) -
|
|
1856
|
+
(scalarNumber(property(right.attributes, 'priority'), 0) ?? 0) ||
|
|
1837
1857
|
left.definitionOrder - right.definitionOrder), unclippedRect, scale, context, rowValues);
|
|
1838
1858
|
const rowList = rowValues?.[`${definition.name}.list`];
|
|
1839
1859
|
let rows = (typeof rowList === 'string' ? scenario.lists[rowList] : undefined) ??
|
|
@@ -1903,7 +1923,7 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
1903
1923
|
...(slotWidth === undefined ? {} : { width: slotWidth }),
|
|
1904
1924
|
...(slotHeight === undefined ? {} : { height: slotHeight }),
|
|
1905
1925
|
};
|
|
1906
|
-
await layoutElement(child, shiftedParent, childClip, scale, depth + 1, context, rowSuffix(index), index, row, instanceId,
|
|
1926
|
+
await layoutElement(child, shiftedParent, childClip, scale, depth + 1, context, rowSuffix(index), index, row, instanceId, 'upper_left', resolveVisibleAppearance, childClipChain);
|
|
1907
1927
|
const rendered = context.instancesById.get(`${child.id}${rowSuffix(index)}`);
|
|
1908
1928
|
rowHeight = Math.max(rowHeight, rendered?.unclippedRect.height ?? 0);
|
|
1909
1929
|
}
|
|
@@ -1955,7 +1975,7 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
1955
1975
|
...(slotWidth === undefined ? {} : { width: slotWidth }),
|
|
1956
1976
|
...(slotHeight === undefined ? {} : { height: slotHeight }),
|
|
1957
1977
|
};
|
|
1958
|
-
await layoutElement(template, shiftedParent, childClip, scale, depth + 1, context, rowSuffix(index), index, row, instanceId,
|
|
1978
|
+
await layoutElement(template, shiftedParent, childClip, scale, depth + 1, context, rowSuffix(index), index, row, instanceId, 'upper_left', resolveVisibleAppearance, childClipChain);
|
|
1959
1979
|
const rendered = context.instancesById.get(`${template.id}${rowSuffix(index)}`);
|
|
1960
1980
|
rowY += Math.max(slotHeight ?? 0, rendered?.unclippedRect.height ?? 0) + spacingY;
|
|
1961
1981
|
renderedRows += 1;
|
|
@@ -1965,7 +1985,7 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
1965
1985
|
else {
|
|
1966
1986
|
for (const child of childDefinitions) {
|
|
1967
1987
|
context.work.spend('nested child expansion');
|
|
1968
|
-
await layoutElement(child, childParentRect, childClip, scale, depth + 1, context, instanceSuffix, rowIndex, rowValues, instanceId,
|
|
1988
|
+
await layoutElement(child, childParentRect, childClip, scale, depth + 1, context, instanceSuffix, rowIndex, rowValues, instanceId, 'upper_left', resolveVisibleAppearance, childClipChain);
|
|
1969
1989
|
}
|
|
1970
1990
|
}
|
|
1971
1991
|
const childOutput = context.output.slice(firstChildOutput);
|
|
@@ -2026,7 +2046,9 @@ export async function buildGuiScene(graph, scannedFiles, windowName, scenario, c
|
|
|
2026
2046
|
const localisation = new Map(graph.localisation
|
|
2027
2047
|
.filter((entry) => entry.language.toLowerCase() === scenario.language.toLowerCase() ||
|
|
2028
2048
|
entry.language.toLowerCase() === `l_${scenario.language.toLowerCase()}`)
|
|
2029
|
-
|
|
2049
|
+
// Graph localisation is ordered with the active (highest-priority) definition first.
|
|
2050
|
+
.map((entry) => [entry.key, entry.value])
|
|
2051
|
+
.reverse());
|
|
2030
2052
|
if (localisation.size === 0) {
|
|
2031
2053
|
for (const entry of graph.localisation)
|
|
2032
2054
|
if (!localisation.has(entry.key))
|
|
@@ -2058,7 +2080,7 @@ export async function buildGuiScene(graph, scannedFiles, windowName, scenario, c
|
|
|
2058
2080
|
const countryFlagProperties = new Map();
|
|
2059
2081
|
const conditionDefinitions = relevantScriptedGuiNames.size === 0
|
|
2060
2082
|
? undefined
|
|
2061
|
-
: ClausewitzEvaluationDefinitions.build({ files:
|
|
2083
|
+
: ClausewitzEvaluationDefinitions.build({ files: scannedFiles });
|
|
2062
2084
|
const conditionScenario = guiConditionScenario(scenario);
|
|
2063
2085
|
for (const scripted of graph.scriptedGuis.filter(({ name }) => relevantScriptedGuiNames.has(name))) {
|
|
2064
2086
|
const source = scannedFiles.find(({ displayPath }) => displayPath === scripted.sourcePath);
|
|
@@ -2188,10 +2210,14 @@ export async function buildGuiScene(graph, scannedFiles, windowName, scenario, c
|
|
|
2188
2210
|
if (native !== undefined) {
|
|
2189
2211
|
for (const note of native.notes)
|
|
2190
2212
|
addFidelity(fidelity, 'modelled', 'native_window_state', note);
|
|
2191
|
-
const
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2213
|
+
for (const note of native.approximations ?? [])
|
|
2214
|
+
addFidelity(fidelity, 'approximated', 'native_window_state', note);
|
|
2215
|
+
const sources = native.state?.sources;
|
|
2216
|
+
if (native.state !== undefined &&
|
|
2217
|
+
sources !== undefined &&
|
|
2218
|
+
(native.state.tag === undefined ||
|
|
2219
|
+
sources.faction === 'default' ||
|
|
2220
|
+
sources.balanceOfPower === 'default'))
|
|
2195
2221
|
addFidelity(fidelity, 'approximated', 'native_window_state', native.state.tag === undefined
|
|
2196
2222
|
? 'The scenario names no country, so the window shows the state of a country in no faction and without a balance of power; set country.tag or the country faction, balanceOfPower and ideology facts.'
|
|
2197
2223
|
: `No history for ${native.state.tag} was available, so its faction and balance of power default to none; set country.faction, country.balanceOfPower and country.ideology to override.`);
|
|
@@ -2204,7 +2230,6 @@ export async function buildGuiScene(graph, scannedFiles, windowName, scenario, c
|
|
|
2204
2230
|
...scenario.values,
|
|
2205
2231
|
},
|
|
2206
2232
|
visibility: {
|
|
2207
|
-
...(native?.visibility ?? {}),
|
|
2208
2233
|
...scriptedWindowVisibility,
|
|
2209
2234
|
...constantElementVisibility,
|
|
2210
2235
|
...scenario.visibility,
|
|
@@ -2222,6 +2247,24 @@ export async function buildGuiScene(graph, scannedFiles, windowName, scenario, c
|
|
|
2222
2247
|
elementsByNameCandidates.set(element.name, candidates);
|
|
2223
2248
|
}
|
|
2224
2249
|
const context = {
|
|
2250
|
+
nativeForeground: new Set(graph.elements
|
|
2251
|
+
.filter((element) => element.parentId === root.id && native?.foreground?.includes(element.name))
|
|
2252
|
+
.map(({ id }) => id)),
|
|
2253
|
+
nativeVisibility: new Map(graph.elements.flatMap((element) => {
|
|
2254
|
+
const visible = native?.visibility[element.name];
|
|
2255
|
+
if (visible === undefined)
|
|
2256
|
+
return [];
|
|
2257
|
+
// The designer reuses names (including division_title) inside selector panels.
|
|
2258
|
+
// Its branch decisions apply to root controls, not identically named descendants.
|
|
2259
|
+
if (windowName === 'countrydivisiondesignerview' && element.parentId !== root.id) {
|
|
2260
|
+
const parent = element.parentId === undefined ? undefined : elementsById.get(element.parentId);
|
|
2261
|
+
if (element.name !== 'hq_badge' ||
|
|
2262
|
+
parent?.name !== 'template_emblem_slot' ||
|
|
2263
|
+
parent.parentId !== root.id)
|
|
2264
|
+
return [];
|
|
2265
|
+
}
|
|
2266
|
+
return [[element.id, visible]];
|
|
2267
|
+
})),
|
|
2225
2268
|
graph,
|
|
2226
2269
|
scenario: layoutScenario,
|
|
2227
2270
|
catalog,
|
|
@@ -205,13 +205,52 @@ export function resolveNativeCountryState(scenario, files) {
|
|
|
205
205
|
}
|
|
206
206
|
/** Whether any native decision can apply to this window, so the country state is worth resolving. */
|
|
207
207
|
export function windowHasNativeState(windowName, elementNames) {
|
|
208
|
-
return (
|
|
208
|
+
return (windowName === 'countrydivisiondesignerview' ||
|
|
209
|
+
NATIVE_BRANCHES[windowName] !== undefined ||
|
|
209
210
|
Object.keys(IDEOLOGY_FRAME_ICONS).some((name) => elementNames.has(name)));
|
|
210
211
|
}
|
|
211
212
|
/** The engine-selected branches and frames for a scene, below any explicit scenario input. */
|
|
212
213
|
export function nativeWindowState(windowName, elementNames, scenario, files) {
|
|
213
214
|
if (!windowHasNativeState(windowName, elementNames))
|
|
214
215
|
return undefined;
|
|
216
|
+
if (windowName === 'countrydivisiondesignerview') {
|
|
217
|
+
const hq = scenario.values['countrydivisiondesignerview.hq'] === true;
|
|
218
|
+
const panel = scenario.values['countrydivisiondesignerview.panel'];
|
|
219
|
+
const visibility = {};
|
|
220
|
+
for (const name of ['hq_overlay_images', 'subunits_hq', 'hq_view', 'hq_title', 'hq_badge'])
|
|
221
|
+
if (elementNames.has(name))
|
|
222
|
+
visibility[name] = hq;
|
|
223
|
+
for (const name of [
|
|
224
|
+
'subunits',
|
|
225
|
+
'non_hq_view',
|
|
226
|
+
'division_title',
|
|
227
|
+
'division_composition_label',
|
|
228
|
+
'combat_width_view',
|
|
229
|
+
])
|
|
230
|
+
if (elementNames.has(name))
|
|
231
|
+
visibility[name] = !hq;
|
|
232
|
+
const selectors = [
|
|
233
|
+
'designerregimentswindow',
|
|
234
|
+
'division_equipment_view',
|
|
235
|
+
'div_templ_custom_icon_view',
|
|
236
|
+
'div_template_select_model',
|
|
237
|
+
'niche_icon_selector',
|
|
238
|
+
];
|
|
239
|
+
for (const name of selectors)
|
|
240
|
+
if (elementNames.has(name))
|
|
241
|
+
visibility[name] = panel === name;
|
|
242
|
+
return {
|
|
243
|
+
visibility,
|
|
244
|
+
values: {},
|
|
245
|
+
foreground: selectors,
|
|
246
|
+
notes: [
|
|
247
|
+
`Division designer uses the ${hq ? 'HQ' : 'division'} branch and only the explicitly selected native selector panel.`,
|
|
248
|
+
],
|
|
249
|
+
approximations: [
|
|
250
|
+
'Native division state is a preview convention: defaults to a division with selectors closed. Set values countrydivisiondesignerview.hq and countrydivisiondesignerview.panel, or explicit element visibility. Engine-populated rows and statistics require scenario bindings.',
|
|
251
|
+
],
|
|
252
|
+
};
|
|
253
|
+
}
|
|
215
254
|
const state = resolveNativeCountryState(scenario, files);
|
|
216
255
|
const visibility = {};
|
|
217
256
|
const values = {};
|
|
@@ -3,7 +3,7 @@ import { ClausewitzEvaluationDefinitions } from '../core/clausewitz-evaluation.j
|
|
|
3
3
|
import { evaluateGuiCondition, guiConditionScenario, guiConditionVariables, guiExplicitConditionValues, } from './scenario-conditions.js';
|
|
4
4
|
import { parsePreviewScenario } from './scenario.js';
|
|
5
5
|
const countryNames = ['Germany', 'France', 'Poland', 'Italy', 'Brazil', 'India', 'Canada'];
|
|
6
|
-
const
|
|
6
|
+
export const generatedCountryTags = ['GER', 'FRA', 'POL', 'ITA', 'BRA', 'RAJ', 'CAN'];
|
|
7
7
|
const countryIdeologies = [
|
|
8
8
|
'fascism',
|
|
9
9
|
'democratic',
|
|
@@ -435,7 +435,7 @@ function generatedRows(count, random, options) {
|
|
|
435
435
|
return Array.from({ length: count }, (_unused, index) => {
|
|
436
436
|
const countryIndex = (countryOffset + index) % countryNames.length;
|
|
437
437
|
const name = countryNames[countryIndex];
|
|
438
|
-
const tag =
|
|
438
|
+
const tag = generatedCountryTags[countryIndex];
|
|
439
439
|
const ideology = countryIdeologies[countryIndex];
|
|
440
440
|
const value = generatedNumber(options, random);
|
|
441
441
|
return {
|
|
@@ -587,7 +587,7 @@ export function generateGuiPreviewScenarios(graph, windowName, base, options, de
|
|
|
587
587
|
const elementStates = { ...base.elementStates };
|
|
588
588
|
const scopedCountries = new Map();
|
|
589
589
|
const generatedCountryIndex = random.integer(0, countryNames.length - 1);
|
|
590
|
-
const generatedCountryTag =
|
|
590
|
+
const generatedCountryTag = generatedCountryTags[generatedCountryIndex];
|
|
591
591
|
for (const key of tokens.numeric)
|
|
592
592
|
setIfMissing(values, key, generatedNumberForKey(key, options, random, numericConstraints.ranges.get(key)));
|
|
593
593
|
for (const [elementName, { minimum, maximum }] of progressRanges) {
|