hoi4-agent-tools 3.5.1 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +43 -116
  3. package/dist/hoi4_agent_tools/gui/layout.js +237 -11
  4. package/dist/hoi4_agent_tools/gui/renderer.js +36 -3
  5. package/dist/hoi4_agent_tools/gui/scenario.js +18 -0
  6. package/dist/hoi4_agent_tools/gui/studio.js +73 -21
  7. package/dist/hoi4_agent_tools/mcp/server/instructions.js +1 -1
  8. package/dist/hoi4_agent_tools/version.js +1 -1
  9. package/docs/README.md +33 -18
  10. package/docs/examples.md +85 -0
  11. package/docs/gui.md +37 -0
  12. package/docs/setup.md +50 -26
  13. package/docs/tools.md +64 -0
  14. package/package.json +4 -2
  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/decision-inspect-input.schema.json +1 -1
  19. package/schemas/event-feature-manifest.schema.json +1 -1
  20. package/schemas/focus-plan.schema.json +1 -1
  21. package/schemas/focus-planning-sidecar.schema.json +1 -1
  22. package/schemas/gui-animation-source.schema.json +1 -1
  23. package/schemas/gui-helper.schema.json +1 -1
  24. package/schemas/gui-scenario.schema.json +139 -89
  25. package/schemas/helper-expansion-request.schema.json +1 -1
  26. package/schemas/helper-expansion-summary.schema.json +1 -1
  27. package/schemas/impact-inspect-input.schema.json +1 -1
  28. package/schemas/map-operation.schema.json +1 -1
  29. package/schemas/mechanic-test-input.schema.json +1 -1
  30. package/schemas/operation-result.schema.json +1 -1
  31. package/schemas/package-check-input.schema.json +1 -1
  32. package/schemas/package-manifest.schema.json +1 -1
  33. package/schemas/probability-analysis-result.schema.json +1 -1
  34. package/schemas/probability-compare-input.schema.json +1 -1
  35. package/schemas/probability-evaluate-input.schema.json +1 -1
  36. package/schemas/probability-inspect-input.schema.json +1 -1
  37. package/schemas/probability-render-input.schema.json +1 -1
  38. package/schemas/probability-scenario-set.schema.json +1 -1
  39. package/schemas/probability-sequence-input.schema.json +1 -1
  40. package/schemas/probability-simulate-input.schema.json +1 -1
  41. package/schemas/probability-sweep-input.schema.json +1 -1
  42. package/schemas/reference-context-input.schema.json +1 -1
  43. package/schemas/reference-read-input.schema.json +1 -1
  44. package/schemas/reference-search-input.schema.json +1 -1
  45. package/schemas/scenario-suite.schema.json +1 -1
  46. package/schemas/scenario-test-input.schema.json +1 -1
  47. package/schemas/shared-scenario.schema.json +1 -1
  48. package/schemas/source-lookup-input.schema.json +1 -1
  49. package/server.json +3 -3
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 3.6.0 - 2026-09-27
6
+
7
+ - Preview immediate tooltip text from source localisation with declared position, width and padding, using the source tooltip sprite and font.
8
+ - Keep unknown GUI visibility and enablement separate from confirmed states, branch coverage and click regions.
9
+ - Identify inferred scrollbar gutter geometry as approximate.
10
+ - Provide a shorter setup guide, practical examples with real renders, and a separate tool reference; remove redundant planning and release-history documents.
11
+
5
12
  ## 3.5.1 - 2026-09-27
6
13
 
7
14
  - Preserve installed-game and offline-wiki citations in compact reference context bundles, select question-relevant sections within required sources, and report sources omitted by the result limit separately from missing files.
package/README.md CHANGED
@@ -1,137 +1,64 @@
1
1
  # HOI4 Agent Tools
2
2
 
3
- HOI4 Agent Tools is an MCP server for coding agents to understand Hearts of Iron IV event chains, decisions, cross-system dependencies, technology systems, AI weights, and MTTH timing and to inspect, create, or clean up focus trees, scripted GUIs, and maps. It produces source-linked structural and visual evidence that agents can use inside a larger modding workflow.
3
+ Give your coding agent tools for Hearts of Iron IV modding.
4
+ Read mod files, find the right documentation, preview trees and interfaces, and make focused changes to focus trees, GUIs and maps.
4
5
 
5
- ## What it does
6
+ ## Get started
6
7
 
7
- - Focus trees: inspect structure and references, render layouts, create trees, and reorganize existing branches.
8
- - Scripted GUIs: trace GUI, GFX, scripted-GUI, and localisation links; render value-driven variants, dynamic country flags, inline text icons, states, resolutions, hierarchy, and click regions; diagnose alignment, visibility, clipping, panel containment, and button-label centering; create or repair interface source.
9
- - Maps: navigate the complete rendered map by ID or localised name; inspect provinces, states, regions, adjacency, supply, railways, and positions; create states and provinces; change IDs; and repair connected map data.
10
- - Event chains: scan definitions and call sites, trace routes and state flow, lint references, render graphs, and compare revisions without editing event source.
11
- - Cross-system impact and decisions: trace definitions and consumers across source systems, evaluate declared actor and target scenarios, inspect costs and mission paths, and compare in-memory proposals.
12
- - Technology trees: reconstruct technology and doctrine paths, folder layouts, unlocks, bonuses, grants, metadata, assets, and structural changes.
13
- - AI and MTTH: evaluate weighted choices and timing across explicit scenarios, bind special scope chains, enumerate dynamic target pools, sweep uncertain inputs, simulate distributions, compare patches, and analyze declared stateful pools.
14
- - Local references: search and read cited offline wiki pages, installed game documentation, optional script documentation, and exact source symbols without loading whole manuals into the conversation.
8
+ Install [Node.js](https://nodejs.org/) 22.19+ in the Node 22 line, or Node.js 24, then run:
15
9
 
16
- ## Real results
10
+ ```bash
11
+ npm install --global hoi4-agent-tools
12
+ hoi4-agent-tools-setup --print-client-config
13
+ ```
17
14
 
18
- The examples use real installed vanilla and Chaos Redux source, supplied game captures, and explicit reproducible display scenarios. They demonstrate source-backed review, not a claim of 99.9% whole-image accuracy. Images, source revisions, hashes, and known differences are in [Visual comparisons](docs/visual-comparisons.md) and [Interface examples](docs/gui-comparisons.md).
15
+ Add the printed configuration to your MCP client and open your mod as its workspace.
16
+ Keep an installed copy of HOI4 available for vanilla examples, artwork and fonts.
17
+ The [setup guide](docs/setup.md) covers connecting your agent and choosing game or mod paths.
19
18
 
20
- | Options Video in game | Options Video from the MCP renderer |
21
- | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------- |
22
- | ![Options Video in game](docs/images/comparisons/gui/options-video-ingame.png) | ![Options Video rendered from installed source](docs/images/comparisons/gui/options-video-mcp.png) |
19
+ Then try:
23
20
 
24
- | Škoda Priority in game | Škoda Priority from the MCP renderer |
25
- | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
26
- | ![Škoda Priority in game](docs/images/comparisons/gui/skoda-priority-ingame.png) | ![Škoda Priority rendered from installed source](docs/images/comparisons/gui/skoda-priority-mcp.png) |
21
+ > Render this mod's focus trees and show which prerequisites are alternatives.
27
22
 
28
- The interface gallery also includes Chaos Redux Settings, Event Log, and Chaos Meter, plus populated vanilla decision and occupation templates. Their manifests separate source-defined layout from declared native runtime fields.
23
+ > Find the installed documentation for `save_event_target_as`, read the relevant section, and show a vanilla example.
29
24
 
30
- The other workbenches return complete source-linked layouts and catalogs rather than prose-only summaries.
25
+ > Preview this scripted GUI at 100% and 125% UI scale and check for clipped text or overlapping controls.
31
26
 
32
- For the supplied focus and technology game captures beside current-source MCP renders, see [Visual examples and comparison limits](https://github.com/klimPaskov/hoi4-agent-tools/blob/main/docs/visual-comparisons.md). These comparisons do not establish 99.9% whole-image accuracy.
27
+ ## What you can do
33
28
 
34
- | Fury focus tree from Chaos Redux | Infantry technology folder from Chaos Redux and vanilla |
35
- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
36
- | ![Fury focus tree rendered from Chaos Redux source](docs/images/comparisons/fury-mcp.png) | ![Infantry technology folder rendered from installed source](docs/images/comparisons/infantry-mcp.png) |
29
+ | Task | Guide |
30
+ | -------------------------------------------------------------------- | ------------------------------------------------------------------------ |
31
+ | Find the right wiki section, game documentation or source definition | [Local references](docs/reference.md) |
32
+ | Inspect, preview, create or reorganize a focus tree | [Focus trees](docs/focus.md) |
33
+ | Trace event chains, decisions, costs and dependencies | [Events](docs/events.md) · [Decisions](docs/analysis.md) |
34
+ | Preview technology and doctrine trees | [Technology trees](docs/technology.md) |
35
+ | Inspect and repair scripted interfaces | [Scripted GUIs](docs/gui.md) |
36
+ | Find states and provinces, preview maps and edit connected map data | [Maps](docs/map.md) |
37
+ | Compare AI weights and test declared mechanic scenarios | [AI analysis](docs/probability.md) · [Mechanic tests](docs/mechanics.md) |
37
38
 
38
- | Map | Event options |
39
- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
40
- | ![Installed map rendered by the MCP](docs/images/comparisons/source/map-mcp.png) | ![Chaos Redux event chain rendered by the MCP](docs/images/comparisons/source/event-chain-mcp.png) |
39
+ ## Real examples
41
40
 
42
- These two views retain native output resolution. Their [source manifest](docs/images/comparisons/source/manifest.json) records the map revision and the exact Fury event used for the option view. The event renderer's omitted-node count includes the rest of the workspace graph outside that selection.
41
+ These previews were rendered from Chaos Redux and installed HOI4 files.
42
+ Click an image to open it at full size.
43
43
 
44
- ## Use from a coding agent
44
+ | Fury focus tree | Vanilla Options interface |
45
+ | ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
46
+ | [![Fury focus tree](docs/images/comparisons/fury-mcp.png)](docs/images/comparisons/fury-mcp.png) | [![Options interface](docs/images/comparisons/gui/options-video-mcp.png)](docs/images/comparisons/gui/options-video-mcp.png) |
45
47
 
46
- Requires Node.js 22.19 or later in the Node 22 line, or Node.js 24.
48
+ Follow the [examples guide](docs/examples.md) for useful prompts and expected results.
49
+ See [focus and technology comparisons](docs/visual-comparisons.md) and [interface comparisons](docs/gui-comparisons.md) for supplied in-game captures beside the renders.
47
50
 
48
- ```bash
49
- npm install --global hoi4-agent-tools@latest
50
- ```
51
+ ## How it fits your workflow
51
52
 
52
- ```toml
53
- [mcp_servers.hoi4_agent_tools]
54
- command = "hoi4-agent-tools.cmd"
55
- startup_timeout_sec = 120
56
- tool_timeout_sec = 600
57
- ```
53
+ The server works through your coding agent and follows the mod workspace you provide.
54
+ It keeps large renders and reports in linked files, and retrieves relevant documentation sections instead of whole manuals.
55
+ Your repository instructions and review process still apply.
58
56
 
59
- On non-Windows systems, use `hoi4-agent-tools` as the command. Agentic HOI4 repositories can include these steps in their agent template so the coding agent installs and registers the server itself; manual installation is not required in that workflow. Once connected, the server follows the active mod supplied by the client and the agent can call its HOI4 tools directly.
60
-
61
- ## Tools
62
-
63
- | Tool | Purpose |
64
- | --------------------------- | ----------------------------------------------------------------------------------------- |
65
- | `hoi4.focus_inspect` | Read focus trees, continuous-focus placement, and structural or reference problems. |
66
- | `hoi4.focus_render` | Produce fast HTML, SVG, JSON, and source-linked layout artifacts. |
67
- | `hoi4.focus_raster` | Produce a high-fidelity PNG review with decoded source icons. |
68
- | `hoi4.focus_rewrite` | Create or update a focus tree. |
69
- | `hoi4.gui_inspect` | Read a scripted GUI and its linked assets and logic. |
70
- | `hoi4.gui_render` | Render generated and explicit GUI scenarios, states, resolutions, and layout diagnostics. |
71
- | `hoi4.gui_rewrite` | Create or update a GUI source package. |
72
- | `hoi4.map_inspect` | Search, click, navigate, and inspect the complete rendered map and its linked data. |
73
- | `hoi4.map_render` | Render full-map layers, overlays, names, IDs, coordinates, and source-linked catalogs. |
74
- | `hoi4.map_rewrite` | Create or update states, provinces, IDs, networks, positions, and connected map data. |
75
- | `hoi4.reference_context` | Get compact, cited wiki and installed documentation pointers for a modding surface. |
76
- | `hoi4.reference_search` | Search bounded local documentation sections. |
77
- | `hoi4.reference_read` | Read one revision-bound section, with line continuation. |
78
- | `hoi4.source_lookup` | Find exact definitions, overrides, usages, and narrow source blocks. |
79
- | `hoi4.event_inspect` | Scan, trace, explain, lint, or assess event chains and their state flow. |
80
- | `hoi4.event_render` | Render source-linked event routes, options, timing, state, scope, and unresolved edges. |
81
- | `hoi4.event_compare` | Compare event-chain topology and diagnostics between revisions. |
82
- | `hoi4.impact_inspect` | Trace symbol and changed-file consumers across source systems and compare proposals. |
83
- | `hoi4.decision_inspect` | Inventory and evaluate decisions or missions under declared scenarios and source changes. |
84
- | `hoi4.mechanic_test` | Execute bounded source effects on a copied declared scenario and check assertions. |
85
- | `hoi4.package_check` | Check declarative package definitions, calls, registrations, assets, and case links. |
86
- | `hoi4.scenario_test` | Run named source and domain cases in resumable, revision-bound batches. |
87
- | `hoi4.job_inspect` | Inspect durable background work or retrieve its completed tool result. |
88
- | `hoi4.job_cancel` | Durably request cancellation of authorized background work. |
89
- | `hoi4.tech_inspect` | Scan, trace, explain, lint, and assess technology and doctrine systems. |
90
- | `hoi4.tech_render` | Render source layouts with real item sizes and year guides, plus dependencies and assets. |
91
- | `hoi4.tech_compare` | Compare technology graphs, placements, references, diagnostics, and source overlays. |
92
- | `hoi4.probability_inspect` | Locate weighted logic and discover compatible adapters, candidates, and required inputs. |
93
- | `hoi4.probability_evaluate` | Evaluate exact values, probabilities, timing, bounds, and unresolved inputs. |
94
- | `hoi4.probability_sweep` | Find sensitivity, breakpoints, cliffs, and rank reversals across declared ranges. |
95
- | `hoi4.probability_simulate` | Run deterministic sampled analysis with confidence and convergence data. |
96
- | `hoi4.probability_sequence` | Analyze declared recovery, caps, cooldowns, resets, timers, and terminal states. |
97
- | `hoi4.probability_compare` | Attribute AI-weight and MTTH changes between real or proposed source. |
98
- | `hoi4.probability_render` | Render cached rankings, matrices, timing, sensitivity, sequence, and comparisons. |
99
-
100
- Set `HOI4_AGENT_TOOLS_CHAOSX=1` on the server process to expose the optional `chaosx.focus_country_assets` and `chaosx.visual_revision` tools used by ChaosX workflows. They remain absent from the default public tool list.
101
-
102
- Large outputs are linked `hoi4-agent://` resources. For resources over 1 MiB, follow the `continuationUri` returned in `_meta` until it is `null`; clients may also request byte ranges with `?offset=<bytes>&length=<bytes>`.
103
-
104
- Long-running domain calls advertise optional MCP task support. A task survives client disconnects and can be retrieved after reconnect; clients without task support keep the ordinary synchronous result contract. Native background rewrites require a stable caller-supplied `requestKey` so retries resolve to one durable mutation receipt. See [Persistent jobs and MCP tasks](docs/jobs.md).
105
-
106
- ## Coexistence with agent workflows
107
-
108
- HOI4 Agent Tools provides HOI4 domain operations without replacing repository instructions such as `AGENTS.md`, skills, plans, or subagents. Its optional weighted-logic prompt helps an agent scope one probability analysis and then returns control to the normal workflow.
109
-
110
- Connecting and listing tools does not scan mod source. Compact tool schemas and linked resources keep large diagnostics, renders, and diffs out of the agent's working context until needed. Event, impact, decision, technology, and probability tools analyze source without editing it; only `hoi4.*_rewrite` calls edit mod source.
111
-
112
- ## Create or clean content
113
-
114
- Ask your agent in normal task language. A typical workflow is inspect, render, rewrite, then inspect the result. The agent can call a raster tool when a pixel review is useful without paying that cost during every structural operation.
115
-
116
- - Focus trees: "Create a complete national focus tree for this route specification," or "Compact this existing tree into a balanced, readable layout." Existing trees can use a plan-free compact reflow; new trees use a complete plan. See [Focus trees](docs/focus.md).
117
- - Scripted GUIs: "Create a scripted GUI for this mechanic," or "Render every value-driven version of this window and fix hidden controls, off-center button text, background alignment, clipping, and click-region conflicts." See [Scripted GUIs](docs/gui.md).
118
- - Maps: "Render the whole map and find this state by name," "Create a state from these provinces," "Create a province inside this exact rectangle," or "Swap these state IDs and update connected references." See [Maps](docs/map.md).
119
- - Event chains: "Trace every route from this event and explain where its flags and variables change," or "Compare the workspace event graph with its previous revision and render the affected routes." See [Event chains](docs/events.md).
120
- - Cross-system impact and decisions: "Find every direct and transitive consumer of this idea," or "Compare who can take this decision, what they pay, and its mission end paths for these actor and target scenarios." See [Cross-system impact and decisions](docs/analysis.md).
121
- - Mechanics and packages: "Test this transfer's conservation and payment," or "Check every declared package link and resume its named scenario suite." See [Mechanic tests and scenario suites](docs/mechanics.md).
122
- - Technology trees: "Explain everything this technology requires and unlocks," or "Compare this technology patch and render every affected folder and doctrine branch." See [Technology trees](docs/technology.md).
123
- - AI and MTTH: "Compare these focus weights across peace, defensive-war, and low-stability scenarios," or "Show when this MTTH event becomes likely and which unknown inputs control the result." See [AI and MTTH analysis](docs/probability.md).
124
- - References: "Find the installed effect signature and an exact vanilla example," or "Show the pertinent wiki and installed documentation for this GUI task." See [Local references](docs/reference.md).
125
-
126
- ## HTTP
127
-
128
- Use stdio for local MCP clients. For shared or remote deployments, see [HTTP](docs/http.md).
129
-
130
- ## Development
57
+ Inspection and rendering do not edit the mod.
58
+ The focus, GUI and map rewrite tools can change source files after checking the proposed edit.
59
+ The server does not launch or control the game; previews and source tests cannot prove live clicks, AI choices or engine timing.
131
60
 
132
- ```bash
133
- npm ci
134
- npm run check
135
- ```
61
+ [Documentation](docs/README.md) · [Tool reference](docs/tools.md) · [Contributing](CONTRIBUTING.md) · [Security](SECURITY.md)
136
62
 
137
- See [Development](docs/development.md). Apache-2.0 licensed. Hearts of Iron IV and Paradox Interactive are trademarks of their respective owners; this project is unaffiliated.
63
+ Apache-2.0 licensed.
64
+ Hearts of Iron IV artwork belongs to its respective owners; this project is unaffiliated with Paradox Interactive.
@@ -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.clickable = reason === undefined;
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, 'modelled', '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);
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 clickable = interactive && disabledReason === undefined;
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 (scenario.values[key] === undefined)
2040
- resolvedScriptedProperties[key] = resolved;
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
- addFidelity(fidelity, 'unresolved', 'scripted_gui_trigger', `${trigger.name} requires more scenario facts.`);
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 ? [] : [element.rect];
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),