hoi4-agent-tools 3.5.1 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +137 -116
- package/dist/hoi4_agent_tools/core/engine.js +7 -7
- package/dist/hoi4_agent_tools/core/file-glob.js +164 -0
- package/dist/hoi4_agent_tools/core/index-segments.js +4 -0
- package/dist/hoi4_agent_tools/core/job-operation-registration.js +31 -0
- package/dist/hoi4_agent_tools/core/job-worker-host.js +27 -11
- package/dist/hoi4_agent_tools/core/job-worker.js +8 -16
- package/dist/hoi4_agent_tools/core/persistent-analysis-cache.js +37 -6
- package/dist/hoi4_agent_tools/core/scanner.js +133 -47
- package/dist/hoi4_agent_tools/core/source/cache.js +4 -0
- package/dist/hoi4_agent_tools/gui/layout.js +237 -11
- package/dist/hoi4_agent_tools/gui/renderer.js +36 -3
- package/dist/hoi4_agent_tools/gui/scenario.js +18 -0
- package/dist/hoi4_agent_tools/gui/studio.js +73 -21
- package/dist/hoi4_agent_tools/map/service.js +2 -2
- package/dist/hoi4_agent_tools/mcp/server/instructions.js +1 -1
- package/dist/hoi4_agent_tools/mcp/tools/reference.js +10 -1
- package/dist/hoi4_agent_tools/reference/lines.js +12 -0
- package/dist/hoi4_agent_tools/reference/operations.js +45 -2
- package/dist/hoi4_agent_tools/reference/script-validation.js +277 -0
- package/dist/hoi4_agent_tools/reference/service.js +139 -67
- package/dist/hoi4_agent_tools/reference/source-lookup.js +91 -15
- package/dist/hoi4_agent_tools/reference/source-navigation.js +130 -0
- package/dist/hoi4_agent_tools/schemas/reference.js +55 -0
- package/dist/hoi4_agent_tools/schemas/script-validation.js +69 -0
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/README.md +34 -18
- package/docs/examples.md +75 -0
- package/docs/gui.md +23 -0
- package/docs/reference.md +43 -2
- package/docs/research/reference-benchmark.md +41 -0
- package/docs/setup.md +50 -26
- package/docs/tools.md +65 -0
- package/npm-shrinkwrap.json +7088 -0
- package/package.json +10 -7
- 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/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 +139 -89
- 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/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 +9 -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 +92 -0
- package/schemas/shared-scenario.schema.json +1 -1
- package/schemas/source-lookup-input.schema.json +68 -2
- package/server.json +3 -3
|
@@ -5,7 +5,7 @@ import { ServiceError } from '../core/result.js';
|
|
|
5
5
|
import { GuiAssetCatalog } from './assets.js';
|
|
6
6
|
import { GUI_GRAPH_MAX_EDGES, GUI_GRAPH_MAX_ELEMENTS, GUI_GRAPH_MAX_NODES, GUI_SCENE_MAX_DEPTH, GUI_SCENE_MAX_ELEMENTS, GUI_SCENE_MAX_TEXT_CHARACTERS, GUI_SCENE_MAX_TEXT_LAYOUT_OPERATIONS, GUI_SCENE_MAX_WORK, GUI_TEXT_MAX_CHARACTERS, GUI_TEXT_MAX_MISSING_GLYPH_SAMPLES, } from './limits.js';
|
|
7
7
|
import { guiElementAttributeFidelity } from './source-graph.js';
|
|
8
|
-
import { emptyFidelityReport } from './types.js';
|
|
8
|
+
import { emptyFidelityReport, fidelityCategories } from './types.js';
|
|
9
9
|
import { ClausewitzEvaluationDefinitions } from '../core/clausewitz-evaluation.js';
|
|
10
10
|
import { evaluateGuiCondition, guiConditionScenario } from './scenario-conditions.js';
|
|
11
11
|
import { parseClausewitz } from '../core/source/index.js';
|
|
@@ -636,13 +636,29 @@ function refreshSceneClips(context) {
|
|
|
636
636
|
: geometry.appearanceVisible
|
|
637
637
|
? 'outside_clip'
|
|
638
638
|
: (element.visibilityReason ?? 'visibility_false');
|
|
639
|
+
element.visibilityStatus =
|
|
640
|
+
intersection === undefined
|
|
641
|
+
? 'hidden'
|
|
642
|
+
: element.visibilityStatusReason !== undefined
|
|
643
|
+
? 'unresolved'
|
|
644
|
+
: element.visible
|
|
645
|
+
? 'shown'
|
|
646
|
+
: 'hidden';
|
|
639
647
|
if (clickableTypes.test(element.elementType)) {
|
|
640
648
|
const reason = element.visible ? geometry.interactionBlock : 'hidden';
|
|
641
649
|
if (reason === undefined)
|
|
642
650
|
delete element.disabledReason;
|
|
643
651
|
else
|
|
644
652
|
element.disabledReason = reason;
|
|
645
|
-
element.
|
|
653
|
+
element.enablement =
|
|
654
|
+
geometry.interactionBlock !== undefined
|
|
655
|
+
? 'disabled'
|
|
656
|
+
: element.visibilityStatus === 'unresolved' || element.enablementReason !== undefined
|
|
657
|
+
? 'unresolved'
|
|
658
|
+
: reason === undefined
|
|
659
|
+
? 'enabled'
|
|
660
|
+
: 'disabled';
|
|
661
|
+
element.clickable = element.enablement === 'enabled';
|
|
646
662
|
}
|
|
647
663
|
}
|
|
648
664
|
}
|
|
@@ -1142,7 +1158,7 @@ async function layoutAttachedScrollbars(definition, scrollbars, owner, childOutp
|
|
|
1142
1158
|
vertical.rect.y = Math.max(verticalRect.y, horizontalRect.y + horizontalRect.height);
|
|
1143
1159
|
vertical.rect.height = Math.max(0, verticalRect.y + verticalRect.height - vertical.rect.y);
|
|
1144
1160
|
}
|
|
1145
|
-
addFidelity(context.fidelity, '
|
|
1161
|
+
addFidelity(context.fidelity, 'approximated', 'orthogonal_scrollbar_gutters', 'Visible horizontal and vertical scrollbars reserve their anchored cross-axis gutters instead of drawing their end buttons over the same corner.', definition);
|
|
1146
1162
|
}
|
|
1147
1163
|
for (const bar of barLayouts) {
|
|
1148
1164
|
await layoutElement({
|
|
@@ -1232,6 +1248,11 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
1232
1248
|
if (height < 0)
|
|
1233
1249
|
height = Math.max(0, parentRect.height - localY + height);
|
|
1234
1250
|
const explicitlyVisible = explicitElementVisibility(definition, scenario, rowValues);
|
|
1251
|
+
const unresolvedVisibilityReason = context.unresolvedElementVisibility.get(definition.name);
|
|
1252
|
+
const parentSceneElement = parentInstanceId === undefined ? undefined : context.instancesById.get(parentInstanceId);
|
|
1253
|
+
const parentUnresolvedVisibility = parentSceneElement?.visibilityStatus === 'unresolved'
|
|
1254
|
+
? parentSceneElement.visibilityStatusReason
|
|
1255
|
+
: undefined;
|
|
1235
1256
|
const declaredHidden = scalarBoolean(property(definition.attributes, 'hide', 'hidden')) ?? false;
|
|
1236
1257
|
const sourceVisible = scalarBoolean(property(definition.attributes, 'visible')) ?? !declaredHidden;
|
|
1237
1258
|
const resolveVisibleAppearance = inheritedVisible && (explicitlyVisible ?? sourceVisible);
|
|
@@ -1240,6 +1261,11 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
1240
1261
|
scenario.scriptedGui[`${definition.name}.enabled`] ??
|
|
1241
1262
|
context.constantElementEnabled[definition.name] ??
|
|
1242
1263
|
scalarBoolean(property(definition.attributes, 'enabled'));
|
|
1264
|
+
const unresolvedEnabledReason = context.unresolvedElementEnabled.get(definition.name);
|
|
1265
|
+
const explicitEnabled = rowValues?.[`${definition.name}.enabled`] ??
|
|
1266
|
+
scenario.values[`${definition.name}.enabled`] ??
|
|
1267
|
+
scenario.scriptedGui[`${definition.name}.enabled`];
|
|
1268
|
+
const enablementUnresolved = unresolvedEnabledReason !== undefined && typeof explicitEnabled !== 'boolean';
|
|
1243
1269
|
const requestedState = scenario.elementStates[definition.name] ?? scenario.state;
|
|
1244
1270
|
const state = scriptedEnabled === false ? 'disabled' : requestedState;
|
|
1245
1271
|
const backgroundValue = property(definition.attributes, 'background');
|
|
@@ -1595,6 +1621,12 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
1595
1621
|
const clipped = clipRect !== undefined &&
|
|
1596
1622
|
(availableClip === undefined || !equalRect(unclippedRect, availableClip));
|
|
1597
1623
|
const visible = resolveVisibleAppearance && (availableClip !== undefined || inheritedClip === undefined);
|
|
1624
|
+
const visibilityStatusReason = explicitlyVisible === false
|
|
1625
|
+
? undefined
|
|
1626
|
+
: (parentUnresolvedVisibility ??
|
|
1627
|
+
(inheritedVisible && explicitlyVisible === undefined
|
|
1628
|
+
? unresolvedVisibilityReason
|
|
1629
|
+
: undefined));
|
|
1598
1630
|
const clickThrough = scalarBoolean(property(definition.attributes, 'clickThrough', 'alwaystransparent', 'allwaystransparent')) ?? false;
|
|
1599
1631
|
const interactive = clickableTypes.test(definition.elementType);
|
|
1600
1632
|
const interactionBlock = !interactive
|
|
@@ -1607,7 +1639,21 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
1607
1639
|
? 'scenario_state'
|
|
1608
1640
|
: undefined;
|
|
1609
1641
|
const disabledReason = interactive && !visible ? 'hidden' : interactionBlock;
|
|
1610
|
-
const
|
|
1642
|
+
const visibilityStatus = availableClip === undefined && inheritedClip !== undefined
|
|
1643
|
+
? 'hidden'
|
|
1644
|
+
: visibilityStatusReason !== undefined
|
|
1645
|
+
? 'unresolved'
|
|
1646
|
+
: visible
|
|
1647
|
+
? 'shown'
|
|
1648
|
+
: 'hidden';
|
|
1649
|
+
const enablement = interactionBlock !== undefined
|
|
1650
|
+
? 'disabled'
|
|
1651
|
+
: visibilityStatus === 'unresolved' || enablementUnresolved
|
|
1652
|
+
? 'unresolved'
|
|
1653
|
+
: disabledReason === undefined
|
|
1654
|
+
? 'enabled'
|
|
1655
|
+
: 'disabled';
|
|
1656
|
+
const clickable = interactive && enablement === 'enabled';
|
|
1611
1657
|
const visibilityReason = visible
|
|
1612
1658
|
? 'visible'
|
|
1613
1659
|
: !inheritedVisible
|
|
@@ -1661,7 +1707,11 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
1661
1707
|
zIndex: context.output.length,
|
|
1662
1708
|
visible,
|
|
1663
1709
|
visibilityReason,
|
|
1710
|
+
visibilityStatus,
|
|
1711
|
+
...(visibilityStatusReason === undefined ? {} : { visibilityStatusReason }),
|
|
1664
1712
|
clickable,
|
|
1713
|
+
...(interactive ? { enablement } : {}),
|
|
1714
|
+
...(interactive && enablementUnresolved ? { enablementReason: unresolvedEnabledReason } : {}),
|
|
1665
1715
|
...(disabledReason === undefined ? {} : { disabledReason }),
|
|
1666
1716
|
clickThrough,
|
|
1667
1717
|
rect: availableClip ?? { x: unclippedRect.x, y: unclippedRect.y, width: 0, height: 0 },
|
|
@@ -1977,6 +2027,8 @@ export async function buildGuiScene(graph, scannedFiles, windowName, scenario, c
|
|
|
1977
2027
|
const scriptedWindowVisibility = {};
|
|
1978
2028
|
const constantElementVisibility = {};
|
|
1979
2029
|
const constantElementEnabled = {};
|
|
2030
|
+
const unresolvedElementEnabled = new Map();
|
|
2031
|
+
const unresolvedElementVisibility = new Map();
|
|
1980
2032
|
const resolvedScriptedProperties = {};
|
|
1981
2033
|
const dynamicListsByName = new Map();
|
|
1982
2034
|
const countryFlagProperties = new Map();
|
|
@@ -2033,11 +2085,25 @@ export async function buildGuiScene(graph, scannedFiles, windowName, scenario, c
|
|
|
2033
2085
|
...scenario.variables,
|
|
2034
2086
|
...scenario.values,
|
|
2035
2087
|
});
|
|
2036
|
-
if (resolved === undefined)
|
|
2037
|
-
continue;
|
|
2038
2088
|
const key = `${propertyDefinition.elementName}.${suffix}`;
|
|
2039
|
-
if (
|
|
2040
|
-
|
|
2089
|
+
if (suffix === 'visible' || suffix === 'enabled') {
|
|
2090
|
+
const booleanValue = scalarBoolean(resolved ?? expression);
|
|
2091
|
+
if (booleanValue === undefined) {
|
|
2092
|
+
const reason = `${scripted.name}.${key}: scenario has no boolean value for ${typeof expression === 'string' ? expression : JSON.stringify(expression)}.`;
|
|
2093
|
+
(suffix === 'visible' ? unresolvedElementVisibility : unresolvedElementEnabled).set(propertyDefinition.elementName, reason);
|
|
2094
|
+
addFidelity(fidelity, 'unresolved', 'scripted_gui_scenario_property', reason);
|
|
2095
|
+
continue;
|
|
2096
|
+
}
|
|
2097
|
+
(suffix === 'visible' ? unresolvedElementVisibility : unresolvedElementEnabled).delete(propertyDefinition.elementName);
|
|
2098
|
+
if (scenario.values[key] === undefined)
|
|
2099
|
+
resolvedScriptedProperties[key] = booleanValue;
|
|
2100
|
+
}
|
|
2101
|
+
else {
|
|
2102
|
+
if (resolved === undefined)
|
|
2103
|
+
continue;
|
|
2104
|
+
if (scenario.values[key] === undefined)
|
|
2105
|
+
resolvedScriptedProperties[key] = resolved;
|
|
2106
|
+
}
|
|
2041
2107
|
addFidelity(fidelity, 'modelled', 'scripted_gui_scenario_property', `${key} resolved from a scenario runtime value.`);
|
|
2042
2108
|
}
|
|
2043
2109
|
}
|
|
@@ -2051,13 +2117,23 @@ export async function buildGuiScene(graph, scannedFiles, windowName, scenario, c
|
|
|
2051
2117
|
? undefined
|
|
2052
2118
|
: evaluated.state === 'true';
|
|
2053
2119
|
if (result === undefined) {
|
|
2054
|
-
|
|
2120
|
+
if (trigger.name.endsWith('_visible'))
|
|
2121
|
+
unresolvedElementVisibility.set(trigger.elementName, `${scripted.name}.${trigger.name}: ${evaluated?.unresolved.map(({ message }) => message).join('; ') ?? 'condition requires more scenario facts'}`);
|
|
2122
|
+
if (trigger.name.endsWith('_click_enabled'))
|
|
2123
|
+
unresolvedElementEnabled.set(trigger.elementName, `${scripted.name}.${trigger.name}: ${evaluated?.unresolved.map(({ message }) => message).join('; ') ?? 'condition requires more scenario facts'}`);
|
|
2124
|
+
addFidelity(fidelity, 'unresolved', 'scripted_gui_trigger', unresolvedElementVisibility.get(trigger.elementName) ??
|
|
2125
|
+
unresolvedElementEnabled.get(trigger.elementName) ??
|
|
2126
|
+
`${scripted.name}.${trigger.name} requires more scenario facts.`);
|
|
2055
2127
|
continue;
|
|
2056
2128
|
}
|
|
2057
2129
|
if (trigger.name.endsWith('_visible'))
|
|
2058
2130
|
constantElementVisibility[trigger.elementName] = result;
|
|
2131
|
+
if (trigger.name.endsWith('_visible'))
|
|
2132
|
+
unresolvedElementVisibility.delete(trigger.elementName);
|
|
2059
2133
|
if (trigger.name.endsWith('_click_enabled'))
|
|
2060
2134
|
constantElementEnabled[trigger.elementName] = result;
|
|
2135
|
+
if (trigger.name.endsWith('_click_enabled'))
|
|
2136
|
+
unresolvedElementEnabled.delete(trigger.elementName);
|
|
2061
2137
|
addFidelity(fidelity, 'modelled', trigger.constantResult === undefined
|
|
2062
2138
|
? 'scripted_gui_trigger'
|
|
2063
2139
|
: 'scripted_gui_constant_trigger', `${trigger.name} resolves to ${result ? 'yes' : 'no'}.`);
|
|
@@ -2117,6 +2193,8 @@ export async function buildGuiScene(graph, scannedFiles, windowName, scenario, c
|
|
|
2117
2193
|
dynamicListsByName,
|
|
2118
2194
|
countryFlagProperties,
|
|
2119
2195
|
constantElementEnabled,
|
|
2196
|
+
unresolvedElementEnabled,
|
|
2197
|
+
unresolvedElementVisibility,
|
|
2120
2198
|
spritesByName: new Map(graph.sprites.map((sprite) => [sprite.name.toLowerCase(), sprite])),
|
|
2121
2199
|
localisation,
|
|
2122
2200
|
output,
|
|
@@ -2171,11 +2249,158 @@ export async function buildGuiScene(graph, scannedFiles, windowName, scenario, c
|
|
|
2171
2249
|
});
|
|
2172
2250
|
}
|
|
2173
2251
|
refreshSceneClips(context);
|
|
2252
|
+
let tooltipPreview;
|
|
2253
|
+
if (scenario.tooltip !== undefined) {
|
|
2254
|
+
const { target: targetToken, x, y, width, padding } = scenario.tooltip;
|
|
2255
|
+
const textWidth = width - 2 * padding;
|
|
2256
|
+
const skipTooltip = (code, message) => {
|
|
2257
|
+
diagnostics.push({ code, severity: 'warning', category: 'rendering', message });
|
|
2258
|
+
addFidelity(fidelity, 'unresolved', 'immediate_tooltip_preview', message);
|
|
2259
|
+
};
|
|
2260
|
+
const matches = output.filter(({ id, name }) => id === targetToken || name === targetToken);
|
|
2261
|
+
const target = matches[0];
|
|
2262
|
+
if (matches.length === 0)
|
|
2263
|
+
skipTooltip('GUI_TOOLTIP_TARGET_MISSING', `Tooltip target ${targetToken} is absent from the scene.`);
|
|
2264
|
+
else if (matches.length > 1)
|
|
2265
|
+
skipTooltip('GUI_TOOLTIP_TARGET_AMBIGUOUS', `Tooltip target ${targetToken} matches ${matches.length} scene elements; select an instance id.`);
|
|
2266
|
+
else if (target?.visibilityStatus === 'unresolved')
|
|
2267
|
+
skipTooltip('GUI_TOOLTIP_TARGET_UNRESOLVED', `Tooltip target ${targetToken} has unresolved visibility: ${target.visibilityStatusReason ?? 'more scenario facts are required'}.`);
|
|
2268
|
+
else if (target?.visible !== true)
|
|
2269
|
+
skipTooltip('GUI_TOOLTIP_TARGET_HIDDEN', `Tooltip target ${targetToken} is hidden in this preview.`);
|
|
2270
|
+
else {
|
|
2271
|
+
const source = elementsById.get(target.sourceId);
|
|
2272
|
+
const localisationKey = source === undefined ? undefined : scalarString(property(source.attributes, 'pdx_tooltip'));
|
|
2273
|
+
const localised = localisationKey === undefined
|
|
2274
|
+
? undefined
|
|
2275
|
+
: (scenario.localisation[localisationKey] ?? localisation.get(localisationKey));
|
|
2276
|
+
const tooltipSprite = graph.sprites.find(({ name }) => name.toLowerCase() === 'tooltip');
|
|
2277
|
+
const tooltipFont = catalog.fontDefinition('cg_16b');
|
|
2278
|
+
if (localisationKey === undefined)
|
|
2279
|
+
skipTooltip('GUI_TOOLTIP_KEY_MISSING', `Tooltip target ${targetToken} has no pdx_tooltip localisation key.`);
|
|
2280
|
+
else if (localised === undefined)
|
|
2281
|
+
skipTooltip('GUI_TOOLTIP_LOCALISATION_MISSING', `Tooltip localisation ${localisationKey} is unavailable in ${scenario.language}.`);
|
|
2282
|
+
else if (tooltipSprite === undefined)
|
|
2283
|
+
skipTooltip('GUI_TOOLTIP_SPRITE_MISSING', 'The declared ToolTip text sprite is unavailable.');
|
|
2284
|
+
else if (tooltipFont === undefined ||
|
|
2285
|
+
catalog.resolvedFontMetrics('cg_16b').source === 'approximation')
|
|
2286
|
+
skipTooltip('GUI_TOOLTIP_FONT_MISSING', 'The declared cg_16b tooltip font or its metrics are unavailable.');
|
|
2287
|
+
else if (x + width > scenario.resolution.width || y >= scenario.resolution.height)
|
|
2288
|
+
skipTooltip('GUI_TOOLTIP_BOUNDS', 'Caller-declared tooltip position and width exceed the viewport.');
|
|
2289
|
+
else {
|
|
2290
|
+
const spriteFrame = await catalog.loadSpriteFrame(tooltipSprite, 0);
|
|
2291
|
+
if (spriteFrame?.supported !== true || spriteFrame.dataUri === undefined)
|
|
2292
|
+
skipTooltip('GUI_TOOLTIP_SPRITE_MISSING', spriteFrame?.reason ?? 'The ToolTip texture is unavailable.');
|
|
2293
|
+
else {
|
|
2294
|
+
const missingEngineText = [];
|
|
2295
|
+
const suppliedText = replaceTextBounded(localised, /\[![^\]]+\]/gu, (match) => {
|
|
2296
|
+
const token = match[0];
|
|
2297
|
+
const supplied = scenario.values[token] ?? scenario.scriptedGui[token];
|
|
2298
|
+
if (typeof supplied === 'string' && supplied.length > 0)
|
|
2299
|
+
return supplied;
|
|
2300
|
+
missingEngineText.push(token);
|
|
2301
|
+
return token;
|
|
2302
|
+
}, 'GUI tooltip supplied effect text');
|
|
2303
|
+
const resolved = resolveTokenText(suppliedText, scenario, localisation);
|
|
2304
|
+
const unresolved = [
|
|
2305
|
+
...new Set([
|
|
2306
|
+
...missingEngineText,
|
|
2307
|
+
...resolved.unresolved,
|
|
2308
|
+
...[...resolved.text.matchAll(/\[![^\]]+\]/gu)].map(([token]) => token),
|
|
2309
|
+
]),
|
|
2310
|
+
];
|
|
2311
|
+
if (unresolved.length > 0)
|
|
2312
|
+
skipTooltip('GUI_TOOLTIP_TEXT_UNRESOLVED', `Tooltip ${localisationKey} requires supplied values for ${unresolved.join(', ')}.`);
|
|
2313
|
+
else {
|
|
2314
|
+
const tooltipTextDefinition = {
|
|
2315
|
+
id: `tooltip-preview:${target.id}:text`,
|
|
2316
|
+
name: `${target.name}_immediate_tooltip_text`,
|
|
2317
|
+
elementType: 'instantTextBoxType',
|
|
2318
|
+
sourcePath: source?.sourcePath ?? target.sourcePath,
|
|
2319
|
+
childIds: [],
|
|
2320
|
+
attributes: {
|
|
2321
|
+
position: { x: (x + padding) / baseScale, y: (y + padding) / baseScale },
|
|
2322
|
+
size: { width: textWidth / baseScale, height: 0 },
|
|
2323
|
+
text: suppliedText,
|
|
2324
|
+
font: 'cg_16b',
|
|
2325
|
+
multiline: true,
|
|
2326
|
+
fixedsize: true,
|
|
2327
|
+
},
|
|
2328
|
+
unsupportedAttributes: [],
|
|
2329
|
+
rawSource: '',
|
|
2330
|
+
definitionOrder: source?.definitionOrder ?? 0,
|
|
2331
|
+
};
|
|
2332
|
+
const tooltipContext = {
|
|
2333
|
+
...context,
|
|
2334
|
+
output: [],
|
|
2335
|
+
instancesById: new Map(),
|
|
2336
|
+
geometry: new Map(),
|
|
2337
|
+
fidelity: emptyFidelityReport(),
|
|
2338
|
+
diagnostics: [],
|
|
2339
|
+
work: new GuiSceneWorkBudget(),
|
|
2340
|
+
};
|
|
2341
|
+
await layoutElement(tooltipTextDefinition, viewport, viewport, baseScale, 0, tooltipContext);
|
|
2342
|
+
refreshSceneClips(tooltipContext);
|
|
2343
|
+
const textElement = tooltipContext.output[0];
|
|
2344
|
+
const popupText = textElement?.text;
|
|
2345
|
+
if (textElement === undefined ||
|
|
2346
|
+
popupText === undefined ||
|
|
2347
|
+
popupText.unresolvedTokens.length > 0 ||
|
|
2348
|
+
popupText.metricSource === 'approximation' ||
|
|
2349
|
+
popupText.glyphLines.some(({ source: glyphSource, missingGlyphs }) => glyphSource === 'deterministic-fallback' || missingGlyphs.length > 0) ||
|
|
2350
|
+
popupText.inlineIcons?.some(({ sprite }) => sprite?.supported !== true || sprite.dataUri === undefined))
|
|
2351
|
+
skipTooltip('GUI_TOOLTIP_ASSET_OR_TEXT_UNRESOLVED', `Tooltip ${localisationKey} has unresolved text, glyph, icon, or texture data.`);
|
|
2352
|
+
else if (popupText.overflowX ||
|
|
2353
|
+
popupText.overflowY ||
|
|
2354
|
+
y + popupText.measuredHeight + 2 * padding > scenario.resolution.height)
|
|
2355
|
+
skipTooltip('GUI_TOOLTIP_BOUNDS', 'Tooltip text does not fit the caller-declared inset or the padded layout exceeds the viewport.');
|
|
2356
|
+
else {
|
|
2357
|
+
const backgroundDefinition = {
|
|
2358
|
+
...tooltipTextDefinition,
|
|
2359
|
+
id: `tooltip-preview:${target.id}`,
|
|
2360
|
+
name: `${target.name}_immediate_tooltip`,
|
|
2361
|
+
elementType: 'iconType',
|
|
2362
|
+
attributes: {
|
|
2363
|
+
position: { x: x / baseScale, y: y / baseScale },
|
|
2364
|
+
size: {
|
|
2365
|
+
width: width / baseScale,
|
|
2366
|
+
height: (popupText.measuredHeight + 2 * padding) / baseScale,
|
|
2367
|
+
},
|
|
2368
|
+
spriteType: tooltipSprite.name,
|
|
2369
|
+
},
|
|
2370
|
+
};
|
|
2371
|
+
await layoutElement(backgroundDefinition, viewport, viewport, baseScale, 0, tooltipContext);
|
|
2372
|
+
const backgroundElement = tooltipContext.instancesById.get(backgroundDefinition.id);
|
|
2373
|
+
backgroundElement.zIndex = 0;
|
|
2374
|
+
textElement.zIndex = 1;
|
|
2375
|
+
textElement.depth = 1;
|
|
2376
|
+
textElement.parentId = backgroundElement.id;
|
|
2377
|
+
tooltipPreview = {
|
|
2378
|
+
targetId: target.id,
|
|
2379
|
+
localisationKey,
|
|
2380
|
+
placement: 'caller-declared',
|
|
2381
|
+
element: backgroundElement,
|
|
2382
|
+
textElement,
|
|
2383
|
+
};
|
|
2384
|
+
for (const category of fidelityCategories)
|
|
2385
|
+
fidelity[category].push(...tooltipContext.fidelity[category]);
|
|
2386
|
+
fidelity.ignored = fidelity.ignored.filter(({ field, elementId }) => field !== 'pdx_tooltip' || elementId !== target.sourceId);
|
|
2387
|
+
addFidelity(fidelity, 'approximated', 'immediate_tooltip_preview', `Immediate tooltip ${localisationKey} uses caller-declared viewport position, width, and ${padding}px padding with ToolTip and cg_16b; native placement, padding, and timing remain unverified.`);
|
|
2388
|
+
addFidelity(fidelity, 'approximated', 'tooltip_text_sprite_composition', 'ToolTip textSpriteType texture is stretched to the measured text height plus caller-declared padding; native frame composition is unverified.');
|
|
2389
|
+
addFidelity(fidelity, 'unsupported', 'pdx_tooltip_delayed', 'Delayed tooltip timing is not previewed.');
|
|
2390
|
+
addFidelity(fidelity, 'unsupported', 'click_to_front', 'Tooltip preview does not model click_to_front behavior.');
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2174
2397
|
output.sort((left, right) => left.zIndex - right.zIndex || compareCodeUnits(left.id, right.id));
|
|
2175
|
-
const bounds = unionRects(output.flatMap((element) => {
|
|
2398
|
+
const bounds = unionRects([...output, ...(tooltipPreview === undefined ? [] : [tooltipPreview.element])].flatMap((element) => {
|
|
2176
2399
|
if (!element.visible)
|
|
2177
2400
|
return [];
|
|
2178
|
-
const painted = element.sprite === undefined && element.progressColours === undefined
|
|
2401
|
+
const painted = element.sprite === undefined && element.progressColours === undefined
|
|
2402
|
+
? []
|
|
2403
|
+
: [element.rect];
|
|
2179
2404
|
const text = element.text;
|
|
2180
2405
|
if (text !== undefined && text.text.length > 0) {
|
|
2181
2406
|
const rect = element.unclippedRect;
|
|
@@ -2209,6 +2434,7 @@ export async function buildGuiScene(graph, scannedFiles, windowName, scenario, c
|
|
|
2209
2434
|
scenario,
|
|
2210
2435
|
resolution: scenario.resolution,
|
|
2211
2436
|
elements: output,
|
|
2437
|
+
...(tooltipPreview === undefined ? {} : { tooltipPreview }),
|
|
2212
2438
|
bounds,
|
|
2213
2439
|
fidelity,
|
|
2214
2440
|
diagnostics: sortDiagnostics(diagnostics),
|
|
@@ -39,7 +39,10 @@ function bitmapDefinitionId(key, layer = 'face') {
|
|
|
39
39
|
function sceneGlyphDefinitions(scene) {
|
|
40
40
|
const outlines = new Map();
|
|
41
41
|
const bitmaps = new Map();
|
|
42
|
-
for (const element of
|
|
42
|
+
for (const element of [
|
|
43
|
+
...scene.elements,
|
|
44
|
+
...(scene.tooltipPreview === undefined ? [] : [scene.tooltipPreview.textElement]),
|
|
45
|
+
]) {
|
|
43
46
|
for (const line of element.text?.glyphLines ?? []) {
|
|
44
47
|
for (const glyph of line.glyphs) {
|
|
45
48
|
if (glyph.kind === 'outline')
|
|
@@ -400,6 +403,8 @@ function renderOverlay(element, variant, toolText) {
|
|
|
400
403
|
if (rect.width <= 0 || rect.height <= 0)
|
|
401
404
|
return '';
|
|
402
405
|
if (variant === 'click-regions') {
|
|
406
|
+
if (element.enablement === 'unresolved')
|
|
407
|
+
return `<g><rect ${rectAttributes(rect)} fill="#ffb84d" fill-opacity="0.12" stroke="#ffb84d" stroke-width="2" stroke-dasharray="5 3"/><title>${escapeXml(`Potential click region: ${element.enablementReason ?? element.visibilityStatusReason ?? element.name}`)}</title>${toolText.render(`${element.name} (?)`, { x: rect.x + 3, y: rect.y + 13, fontSize: 11, fill: '#ffb84d' })}</g>`;
|
|
403
408
|
if (!element.clickable)
|
|
404
409
|
return '';
|
|
405
410
|
return `<g><rect ${rectAttributes(rect)} fill="#00d4ff" fill-opacity="0.2" stroke="#00d4ff" stroke-width="2"/>${toolText.render(element.name, { x: rect.x + 3, y: rect.y + 13, fontSize: 11, fill: '#00d4ff' })}</g>`;
|
|
@@ -418,6 +423,13 @@ function renderOverlay(element, variant, toolText) {
|
|
|
418
423
|
}
|
|
419
424
|
return '';
|
|
420
425
|
}
|
|
426
|
+
function renderTooltipPreview(scene, toolText) {
|
|
427
|
+
const tooltip = scene.tooltipPreview;
|
|
428
|
+
if (tooltip === undefined)
|
|
429
|
+
return '';
|
|
430
|
+
return (renderBaseElement(tooltip.element, undefined, toolText) +
|
|
431
|
+
renderBaseElement(tooltip.textElement, undefined, toolText));
|
|
432
|
+
}
|
|
421
433
|
function viewFor(scene, variant) {
|
|
422
434
|
if (variant !== 'cropped' || scene.bounds.width <= 0 || scene.bounds.height <= 0) {
|
|
423
435
|
return {
|
|
@@ -453,7 +465,7 @@ export function sceneToSvg(scene, variant) {
|
|
|
453
465
|
: [`<clipPath id="clip-${index}"><rect ${rectAttributes(element.clipRect)}/></clipPath>`]);
|
|
454
466
|
const body = scene.elements
|
|
455
467
|
.map((element, index) => renderBaseElement(element, element.clipRect === undefined ? undefined : `clip-${index}`, toolText))
|
|
456
|
-
.join('');
|
|
468
|
+
.join('') + renderTooltipPreview(scene, toolText);
|
|
457
469
|
const overlays = scene.elements
|
|
458
470
|
.map((element) => renderOverlay(element, variant, toolText))
|
|
459
471
|
.join('');
|
|
@@ -479,7 +491,7 @@ async function sceneToSvgCooperative(scene, variant, signal) {
|
|
|
479
491
|
const clipDefinitions = await cooperativeParts(scene.elements, (element, index) => element.clipRect === undefined
|
|
480
492
|
? ''
|
|
481
493
|
: `<clipPath id="clip-${index}"><rect ${rectAttributes(element.clipRect)}/></clipPath>`, signal);
|
|
482
|
-
const body = await cooperativeParts(scene.elements, (element, index) => renderBaseElement(element, element.clipRect === undefined ? undefined : `clip-${index}`, toolText), signal);
|
|
494
|
+
const body = (await cooperativeParts(scene.elements, (element, index) => renderBaseElement(element, element.clipRect === undefined ? undefined : `clip-${index}`, toolText), signal)) + renderTooltipPreview(scene, toolText);
|
|
483
495
|
const overlays = await cooperativeParts(scene.elements, (element) => renderOverlay(element, variant, toolText), signal);
|
|
484
496
|
const fidelity = escapeXml(canonicalJson(scene.fidelity));
|
|
485
497
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${view.width}" height="${view.height}" viewBox="${finite(view.viewBox.x)} ${finite(view.viewBox.y)} ${finite(view.viewBox.width)} ${finite(view.viewBox.height)}"><metadata data-renderer="hoi4-agent-tools" data-mode="offline">${fidelity}</metadata><defs>${clipDefinitions}${sceneGlyphDefinitions(scene)}${toolText.definitions()}</defs><rect ${rectAttributes(view.viewBox)} fill="#17202a"/>${body}${overlays}</svg>`;
|
|
@@ -526,6 +538,27 @@ export function hierarchyToSvg(scene) {
|
|
|
526
538
|
function sceneLayoutEvidence(scene) {
|
|
527
539
|
return {
|
|
528
540
|
...scene,
|
|
541
|
+
...(scene.tooltipPreview === undefined
|
|
542
|
+
? {}
|
|
543
|
+
: {
|
|
544
|
+
tooltipPreview: {
|
|
545
|
+
targetId: scene.tooltipPreview.targetId,
|
|
546
|
+
localisationKey: scene.tooltipPreview.localisationKey,
|
|
547
|
+
placement: scene.tooltipPreview.placement,
|
|
548
|
+
element: {
|
|
549
|
+
id: scene.tooltipPreview.element.id,
|
|
550
|
+
rect: scene.tooltipPreview.element.rect,
|
|
551
|
+
sprite: scene.tooltipPreview.element.sprite?.spriteName,
|
|
552
|
+
},
|
|
553
|
+
textElement: {
|
|
554
|
+
id: scene.tooltipPreview.textElement.id,
|
|
555
|
+
rect: scene.tooltipPreview.textElement.rect,
|
|
556
|
+
text: scene.tooltipPreview.textElement.text?.text,
|
|
557
|
+
lines: scene.tooltipPreview.textElement.text?.lines,
|
|
558
|
+
fontName: scene.tooltipPreview.textElement.text?.fontName,
|
|
559
|
+
},
|
|
560
|
+
},
|
|
561
|
+
}),
|
|
529
562
|
elements: scene.elements.map((element) => ({
|
|
530
563
|
...element,
|
|
531
564
|
...(element.sprite === undefined
|
|
@@ -103,6 +103,24 @@ const GuiPreviewScenarioBodySchema = z
|
|
|
103
103
|
.min(1)
|
|
104
104
|
.max(128)
|
|
105
105
|
.regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u),
|
|
106
|
+
tooltip: z
|
|
107
|
+
.object({
|
|
108
|
+
target: elementNameSchema,
|
|
109
|
+
x: z.number().nonnegative(),
|
|
110
|
+
y: z.number().nonnegative(),
|
|
111
|
+
width: z.number().positive().max(RENDER_MAX_DIMENSION),
|
|
112
|
+
padding: z
|
|
113
|
+
.number()
|
|
114
|
+
.nonnegative()
|
|
115
|
+
.max(RENDER_MAX_DIMENSION / 2)
|
|
116
|
+
.describe('Caller-declared inset on each side, in viewport pixels; no native padding is inferred.'),
|
|
117
|
+
})
|
|
118
|
+
.strict()
|
|
119
|
+
.refine(({ padding, width }) => padding * 2 < width, {
|
|
120
|
+
path: ['padding'],
|
|
121
|
+
message: 'Tooltip padding must leave a positive text width.',
|
|
122
|
+
})
|
|
123
|
+
.optional(),
|
|
106
124
|
description: z.string().max(500).optional(),
|
|
107
125
|
date: GuiScenarioDateSchema.optional(),
|
|
108
126
|
controls: ConditionControlMapSchema.optional(),
|