intelligent-system-design-language 0.3.22 → 0.3.23

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 (91) hide show
  1. package/.claude/agents/langium-language-designer.md +38 -38
  2. package/.claude/agents/typescript-vscode-expert.md +29 -29
  3. package/.claude/agents/ui-ux-designer.md +36 -36
  4. package/.claude/settings.local.json +33 -33
  5. package/.idea/inspectionProfiles/Project_Default.xml +6 -6
  6. package/.idea/isdl.iml +13 -13
  7. package/.idea/modules.xml +8 -8
  8. package/.idea/vcs.xml +6 -6
  9. package/.idea/watcherTasks.xml +3 -3
  10. package/.vscodeignore +18 -18
  11. package/LICENSE +673 -673
  12. package/README.md +86 -86
  13. package/bin/cli.js +4 -4
  14. package/bin/lsp.js +8 -8
  15. package/out/_backgrounds.scss +91 -91
  16. package/out/_handlebars.scss +497 -497
  17. package/out/_isdlStyles.scss +1444 -1444
  18. package/out/_vuetifyOverrides.scss +425 -425
  19. package/out/_vuetifyStyles.scss +31957 -31957
  20. package/out/cli/components/_backgrounds.scss +91 -91
  21. package/out/cli/components/_handlebars.scss +497 -497
  22. package/out/cli/components/_isdlStyles.scss +1444 -1444
  23. package/out/cli/components/_vuetifyOverrides.scss +425 -425
  24. package/out/cli/components/_vuetifyStyles.scss +31957 -31957
  25. package/out/cli/components/active-effect-sheet-generator.js +453 -453
  26. package/out/cli/components/chat-card-generator.js +654 -654
  27. package/out/cli/components/css-generator.js +4 -4
  28. package/out/cli/components/damage-roll-generator.js +160 -160
  29. package/out/cli/components/datamodel-generator.js +257 -257
  30. package/out/cli/components/derived-data-generator.js +923 -923
  31. package/out/cli/components/hotbar-drop-hook-generator.js +82 -82
  32. package/out/cli/components/init-hook-generator.js +495 -495
  33. package/out/cli/components/measured-template-preview.js +221 -221
  34. package/out/cli/components/method-generator.js +548 -548
  35. package/out/cli/components/ready-hook-generator.js +404 -404
  36. package/out/cli/components/token-generator.js +116 -116
  37. package/out/cli/components/vue/base-components/vue-attribute.js +138 -138
  38. package/out/cli/components/vue/base-components/vue-boolean.js +64 -64
  39. package/out/cli/components/vue/base-components/vue-calculator.js +93 -93
  40. package/out/cli/components/vue/base-components/vue-damage-application.js +356 -356
  41. package/out/cli/components/vue/base-components/vue-damage-bonuses.js +165 -165
  42. package/out/cli/components/vue/base-components/vue-damage-resistances.js +196 -196
  43. package/out/cli/components/vue/base-components/vue-damage-track.js +121 -121
  44. package/out/cli/components/vue/base-components/vue-date-time.js +42 -42
  45. package/out/cli/components/vue/base-components/vue-dice.js +98 -98
  46. package/out/cli/components/vue/base-components/vue-die.js +73 -73
  47. package/out/cli/components/vue/base-components/vue-document-choice.js +149 -149
  48. package/out/cli/components/vue/base-components/vue-document-choices.js +179 -179
  49. package/out/cli/components/vue/base-components/vue-document-link.js +60 -60
  50. package/out/cli/components/vue/base-components/vue-extended-choice.js +88 -88
  51. package/out/cli/components/vue/base-components/vue-inventory.js +519 -519
  52. package/out/cli/components/vue/base-components/vue-macro-choice.js +138 -138
  53. package/out/cli/components/vue/base-components/vue-measured-template.js +530 -530
  54. package/out/cli/components/vue/base-components/vue-money.js +483 -483
  55. package/out/cli/components/vue/base-components/vue-number.js +174 -174
  56. package/out/cli/components/vue/base-components/vue-paperdoll.js +43 -43
  57. package/out/cli/components/vue/base-components/vue-parent-property-reference.js +76 -76
  58. package/out/cli/components/vue/base-components/vue-prosemirror.js +18 -18
  59. package/out/cli/components/vue/base-components/vue-resource.js +136 -136
  60. package/out/cli/components/vue/base-components/vue-roll-visualizer.js +285 -285
  61. package/out/cli/components/vue/base-components/vue-self-property-reference.js +62 -62
  62. package/out/cli/components/vue/base-components/vue-string-choice.js +98 -98
  63. package/out/cli/components/vue/base-components/vue-string-choices.js +203 -203
  64. package/out/cli/components/vue/base-components/vue-string.js +60 -60
  65. package/out/cli/components/vue/base-components/vue-text-field.js +53 -53
  66. package/out/cli/components/vue/base-components/vue-tracker.js +431 -431
  67. package/out/cli/components/vue/vue-action-component-generator.js +64 -64
  68. package/out/cli/components/vue/vue-active-effect-sheet-generator.js +856 -856
  69. package/out/cli/components/vue/vue-datatable-sheet-class-generator.js +292 -292
  70. package/out/cli/components/vue/vue-datatable2-component-generator.js +824 -824
  71. package/out/cli/components/vue/vue-document-creation-app.js +121 -121
  72. package/out/cli/components/vue/vue-document-creation-sheet.js +94 -94
  73. package/out/cli/components/vue/vue-generator.js +40 -40
  74. package/out/cli/components/vue/vue-mixin.js +296 -296
  75. package/out/cli/components/vue/vue-pinned-datatable-component-generator.js +260 -260
  76. package/out/cli/components/vue/vue-prompt-generator.js +80 -80
  77. package/out/cli/components/vue/vue-prompt-sheet-class-generator.js +317 -317
  78. package/out/cli/components/vue/vue-sheet-application-generator.js +1171 -1171
  79. package/out/cli/components/vue/vue-sheet-class-generator.js +510 -510
  80. package/out/cli/generator.js +438 -438
  81. package/out/extension/github/githubManager.js +35 -35
  82. package/out/extension/github/githubQuickActions.js +120 -120
  83. package/out/extension/github/system-workflow.yml +47 -47
  84. package/out/extension/main.cjs.map +1 -1
  85. package/out/extension/package.json +419 -419
  86. package/out/language/generated/grammar.js +14240 -14240
  87. package/out/language/main.cjs.map +1 -1
  88. package/out/package.json +419 -419
  89. package/out/progressbar.min.js +6 -6
  90. package/out/styles.scss +762 -762
  91. package/package.json +419 -419
@@ -7,291 +7,291 @@ export default function generateRollVisualizerComponent(destination) {
7
7
  if (!fs.existsSync(generatedFileDir)) {
8
8
  fs.mkdirSync(generatedFileDir, { recursive: true });
9
9
  }
10
- const fileNode = expandToNode `
11
- <script>
12
- // Module-scoped cache (shared across all roll-visualizer instances for the page
13
- // session, and surviving sheet re-renders/re-opens). Keyed by the RESOLVED formula
14
- // (after @refs are substituted), so the same dice math -- especially the expensive
15
- // simulation path -- is only computed once. Cleared on page reload.
16
- const __rollVisualizerCache = new Map();
17
- </script>
18
- <script setup>
19
- import { ref, watch, computed, inject } from "vue";
20
-
21
- const props = defineProps({
22
- label: String,
23
- icon: String,
24
- color: String,
25
- systemPath: String,
26
- context: Object,
27
- // The Foundry roll formula (may contain @refs), compiled from the field's value: expression.
28
- formula: { type: String, default: "" },
29
- // Data object resolving the @refs, bound from the live (reactive) document/prompt data.
30
- rollData: { type: Object, default: () => ({}) }
31
- });
32
-
33
- // ----- Distribution engine ---------------------------------------------------
34
- // For purely additive dice formulas (dice + constants, +/- only, no dice
35
- // modifiers) we compute the EXACT probability distribution by convolution.
36
- // Anything more exotic (keep-highest/lowest, exploding, rerolls, multiplied
37
- // dice, fate dice, pools, functions) falls back to Monte Carlo simulation.
38
-
39
- // A distribution is a Map<outcomeValue, probability>.
40
- const dieDist = (faces) => {
41
- const m = new Map();
42
- for (let f = 1; f <= faces; f++) m.set(f, 1 / faces);
43
- return m;
44
- };
45
- const convolve = (a, b) => {
46
- const out = new Map();
47
- for (const [va, pa] of a) {
48
- for (const [vb, pb] of b) {
49
- const v = va + vb;
50
- out.set(v, (out.get(v) || 0) + pa * pb);
51
- }
52
- }
53
- return out;
54
- };
55
- const negate = (a) => {
56
- const m = new Map();
57
- for (const [v, p] of a) m.set(-v, p);
58
- return m;
59
- };
60
- const shift = (a, k) => {
61
- const m = new Map();
62
- for (const [v, p] of a) m.set(v + k, p);
63
- return m;
64
- };
65
-
66
- // Classify a Foundry roll term. Foundry does not minify these class names in
67
- // either v12 or v13, so constructor.name is a reliable signal.
68
- const classify = (term) => {
69
- const n = term?.constructor?.name;
70
- if (n === "Die") return "die";
71
- if (n === "NumericTerm") return "num";
72
- if (n === "OperatorTerm") return "op";
73
- return "other";
74
- };
75
-
76
- // Decide whether a parsed roll can be solved exactly. Guard the convolution
77
- // against pathological sizes (huge dice pools) by falling back to simulation.
78
- const MAX_BUCKETS = 5000;
79
- const canConvolve = (terms) => {
80
- let minTotal = 0, maxTotal = 0;
81
- for (const term of terms) {
82
- const kind = classify(term);
83
- if (kind === "op") {
84
- if (term.operator !== "+" && term.operator !== "-") return false;
85
- continue;
86
- }
87
- if (kind === "die") {
88
- if (Array.isArray(term.modifiers) && term.modifiers.length > 0) return false;
89
- const number = Number(term.number);
90
- const faces = Number(term.faces);
91
- if (!Number.isInteger(number) || !Number.isInteger(faces) || faces < 1 || number < 0) return false;
92
- minTotal += number;
93
- maxTotal += number * faces;
94
- continue;
95
- }
96
- if (kind === "num") {
97
- if (!Number.isFinite(Number(term.number))) return false;
98
- continue;
99
- }
100
- return false; // unknown term type
101
- }
102
- return (maxTotal - minTotal) <= MAX_BUCKETS;
103
- };
104
-
105
- // Build the exact PMF for an additive term list.
106
- const convolveTerms = (terms) => {
107
- let acc = new Map([[0, 1]]);
108
- let sign = 1;
109
- for (const term of terms) {
110
- const kind = classify(term);
111
- if (kind === "op") {
112
- sign = term.operator === "-" ? -1 : 1;
113
- continue;
114
- }
115
- if (kind === "die") {
116
- let dist = new Map([[0, 1]]);
117
- for (let i = 0; i < Number(term.number); i++) dist = convolve(dist, dieDist(Number(term.faces)));
118
- if (sign < 0) dist = negate(dist);
119
- acc = convolve(acc, dist);
120
- }
121
- else if (kind === "num") {
122
- acc = shift(acc, sign * Number(term.number));
123
- }
124
- sign = 1;
125
- }
126
- return acc;
127
- };
128
-
129
- // Derive the display payload (average, min, max, chart series) from a PMF.
130
- const summarize = (dist, approximate) => {
131
- const entries = [...dist.entries()].sort((a, b) => a[0] - b[0]);
132
- let average = 0, min = Infinity, max = -Infinity;
133
- for (const [v, p] of entries) {
134
- average += v * p;
135
- if (v < min) min = v;
136
- if (v > max) max = v;
137
- }
138
- const outcomeValues = entries.map(([v]) => v);
139
- const values = entries.map(([, p]) => +(p * 100).toFixed(2));
140
- // Thin the x-axis labels so they don't cluster/overlap: pick a "nice" step
141
- // (1, 2, 5, 10, ...) targeting ~8 labels, and only label outcomes that land on
142
- // a multiple of it -- so the axis reads in round numbers (5, 10, 15, 20 ...).
143
- const lo = outcomeValues.length ? outcomeValues[0] : 0;
144
- const hi = outcomeValues.length ? outcomeValues[outcomeValues.length - 1] : 0;
145
- const span = Math.max(1, hi - lo);
146
- const rawStep = span / 8;
147
- const pow = Math.pow(10, Math.floor(Math.log10(rawStep)));
148
- const norm = rawStep / pow;
149
- const niceStep = (norm <= 1 ? 1 : norm <= 2 ? 2 : norm <= 5 ? 5 : 10) * pow;
150
- const step = Math.max(1, Math.round(niceStep));
151
- return {
152
- average: +average.toFixed(2),
153
- min: entries.length ? min : 0,
154
- max: entries.length ? max : 0,
155
- labels: outcomeValues,
156
- values,
157
- step,
158
- approximate,
159
- hasData: entries.length > 0
160
- };
161
- };
162
-
163
- // ----- Reactive computation --------------------------------------------------
164
-
165
- const result = ref({ average: 0, min: 0, max: 0, labels: [], values: [], step: 1, approximate: false, hasData: false });
166
- const iterations = ref(0);
167
- let runToken = 0;
168
-
169
- // Monte Carlo fallback. Roll.simulate builds a full Roll per sample (~0.2ms each),
170
- // so a large batch blocks the main thread for seconds and freezes the sheet on
171
- // render. Instead, sample a modest target in small chunks and yield to the event
172
- // loop between each, refining the curve in place. The sheet stays responsive and
173
- // the chart fills in progressively.
174
- const SIM_TARGET = 2000;
175
- const SIM_CHUNK = 200;
176
- const runSimulation = async (formula, token, cacheKey) => {
177
- const counts = new Map();
178
- let done = 0;
179
- while (done < SIM_TARGET) {
180
- if (token !== runToken) return; // a newer run superseded us
181
- const n = Math.min(SIM_CHUNK, SIM_TARGET - done);
182
- const batch = await Roll.simulate(formula, n);
183
- if (token !== runToken) return;
184
- for (const v of batch) counts.set(v, (counts.get(v) || 0) + 1);
185
- done += n;
186
- const dist = new Map();
187
- for (const [v, c] of counts) dist.set(v, c / done);
188
- result.value = summarize(dist, true);
189
- iterations.value = done;
190
- // Yield so the browser can paint and handle input between chunks.
191
- await new Promise(r => setTimeout(r, 0));
192
- }
193
- // Cache the completed estimate so this formula isn't re-simulated later.
194
- __rollVisualizerCache.set(cacheKey, result.value);
195
- };
196
-
197
- const recompute = () => {
198
- const token = ++runToken;
199
- const raw = (props.formula || "").trim();
200
- if (!raw) {
201
- result.value = { average: 0, min: 0, max: 0, labels: [], values: [], step: 1, approximate: false, hasData: false };
202
- return;
203
- }
204
- let roll;
205
- try {
206
- roll = new Roll(raw, props.rollData || {});
207
- }
208
- catch (e) {
209
- result.value = { average: 0, min: 0, max: 0, labels: [], values: [], step: 1, approximate: false, hasData: false };
210
- return;
211
- }
212
- // Cache hit on the resolved formula -> reuse the result, no recompute.
213
- const cacheKey = roll.formula;
214
- const cached = __rollVisualizerCache.get(cacheKey);
215
- if (cached) {
216
- result.value = cached;
217
- iterations.value = cached.approximate ? SIM_TARGET : 0;
218
- return;
219
- }
220
- // After construction Foundry has substituted @refs into the terms.
221
- const terms = roll.terms || [];
222
- if (canConvolve(terms)) {
223
- iterations.value = 0;
224
- const summary = summarize(convolveTerms(terms), false);
225
- __rollVisualizerCache.set(cacheKey, summary);
226
- result.value = summary;
227
- }
228
- else {
229
- // Simulate against the resolved formula (no @refs remain in roll.formula).
230
- runSimulation(roll.formula, token, cacheKey);
231
- }
232
- };
233
-
234
- // Debounce so rapid field edits don't kick off redundant simulations.
235
- let debounceHandle = null;
236
- watch(
237
- () => [props.formula, JSON.stringify(props.rollData || {})],
238
- () => {
239
- if (debounceHandle) clearTimeout(debounceHandle);
240
- debounceHandle = setTimeout(recompute, 200);
241
- },
242
- { immediate: true }
243
- );
244
-
245
- const getLabel = computed(() => {
246
- const localized = game.i18n.localize(props.label);
247
- if (props.icon) return \`<i class="fa-solid \${props.icon}"></i> \${localized}\`;
248
- return localized;
249
- });
250
- const accentColor = computed(() => props.color || "#92aed9");
251
- const averageText = computed(() => (result.value.approximate ? "≈ " : "") + result.value.average);
252
- </script>
253
-
254
- <template>
255
- <v-card class="isdl-roll-visualizer" :name="systemPath" variant="tonal" density="compact">
256
- <div class="isdl-roll-visualizer__header">
257
- <span class="isdl-roll-visualizer__label" v-html="getLabel"></span>
258
- <span class="isdl-roll-visualizer__avg" :style="{ color: accentColor }">{{ averageText }}</span>
259
- </div>
260
- <v-sparkline
261
- v-if="result.hasData"
262
- :labels="result.labels"
263
- :model-value="result.values"
264
- :color="accentColor"
265
- line-width="2"
266
- padding="8"
267
- smooth="6"
268
- :label-size="10"
269
- auto-draw
270
- preserveAspectRatio="none"
271
- >
272
- <!-- Render a label only on "nice" round outcomes; blank elsewhere so the
273
- axis doesn't cluster and never falls back to showing the raw value. -->
274
- <template #label="item">
275
- {{ Number(item.value) % result.step === 0 ? item.value : "" }}
276
- </template>
277
- </v-sparkline>
278
- <div v-else class="isdl-roll-visualizer__empty text-caption">
279
- {{ game.i18n.localize("ROLLVISUALIZER.NoFormula") }}
280
- </div>
281
- <div class="isdl-roll-visualizer__footer text-caption">
282
- <span v-if="result.hasData">
283
- {{ game.i18n.localize("ROLLVISUALIZER.Min") }}: {{ result.min }}
284
- &middot;
285
- {{ game.i18n.localize("ROLLVISUALIZER.Max") }}: {{ result.max }}
286
- &middot;
287
- {{ game.i18n.localize("ROLLVISUALIZER.Average") }}: {{ averageText }}
288
- </span>
289
- <span v-if="result.approximate && iterations > 0" class="isdl-roll-visualizer__approx">
290
- ({{ iterations }} {{ game.i18n.localize("ROLLVISUALIZER.Simulations") }})
291
- </span>
292
- </div>
293
- </v-card>
294
- </template>
10
+ const fileNode = expandToNode `
11
+ <script>
12
+ // Module-scoped cache (shared across all roll-visualizer instances for the page
13
+ // session, and surviving sheet re-renders/re-opens). Keyed by the RESOLVED formula
14
+ // (after @refs are substituted), so the same dice math -- especially the expensive
15
+ // simulation path -- is only computed once. Cleared on page reload.
16
+ const __rollVisualizerCache = new Map();
17
+ </script>
18
+ <script setup>
19
+ import { ref, watch, computed, inject } from "vue";
20
+
21
+ const props = defineProps({
22
+ label: String,
23
+ icon: String,
24
+ color: String,
25
+ systemPath: String,
26
+ context: Object,
27
+ // The Foundry roll formula (may contain @refs), compiled from the field's value: expression.
28
+ formula: { type: String, default: "" },
29
+ // Data object resolving the @refs, bound from the live (reactive) document/prompt data.
30
+ rollData: { type: Object, default: () => ({}) }
31
+ });
32
+
33
+ // ----- Distribution engine ---------------------------------------------------
34
+ // For purely additive dice formulas (dice + constants, +/- only, no dice
35
+ // modifiers) we compute the EXACT probability distribution by convolution.
36
+ // Anything more exotic (keep-highest/lowest, exploding, rerolls, multiplied
37
+ // dice, fate dice, pools, functions) falls back to Monte Carlo simulation.
38
+
39
+ // A distribution is a Map<outcomeValue, probability>.
40
+ const dieDist = (faces) => {
41
+ const m = new Map();
42
+ for (let f = 1; f <= faces; f++) m.set(f, 1 / faces);
43
+ return m;
44
+ };
45
+ const convolve = (a, b) => {
46
+ const out = new Map();
47
+ for (const [va, pa] of a) {
48
+ for (const [vb, pb] of b) {
49
+ const v = va + vb;
50
+ out.set(v, (out.get(v) || 0) + pa * pb);
51
+ }
52
+ }
53
+ return out;
54
+ };
55
+ const negate = (a) => {
56
+ const m = new Map();
57
+ for (const [v, p] of a) m.set(-v, p);
58
+ return m;
59
+ };
60
+ const shift = (a, k) => {
61
+ const m = new Map();
62
+ for (const [v, p] of a) m.set(v + k, p);
63
+ return m;
64
+ };
65
+
66
+ // Classify a Foundry roll term. Foundry does not minify these class names in
67
+ // either v12 or v13, so constructor.name is a reliable signal.
68
+ const classify = (term) => {
69
+ const n = term?.constructor?.name;
70
+ if (n === "Die") return "die";
71
+ if (n === "NumericTerm") return "num";
72
+ if (n === "OperatorTerm") return "op";
73
+ return "other";
74
+ };
75
+
76
+ // Decide whether a parsed roll can be solved exactly. Guard the convolution
77
+ // against pathological sizes (huge dice pools) by falling back to simulation.
78
+ const MAX_BUCKETS = 5000;
79
+ const canConvolve = (terms) => {
80
+ let minTotal = 0, maxTotal = 0;
81
+ for (const term of terms) {
82
+ const kind = classify(term);
83
+ if (kind === "op") {
84
+ if (term.operator !== "+" && term.operator !== "-") return false;
85
+ continue;
86
+ }
87
+ if (kind === "die") {
88
+ if (Array.isArray(term.modifiers) && term.modifiers.length > 0) return false;
89
+ const number = Number(term.number);
90
+ const faces = Number(term.faces);
91
+ if (!Number.isInteger(number) || !Number.isInteger(faces) || faces < 1 || number < 0) return false;
92
+ minTotal += number;
93
+ maxTotal += number * faces;
94
+ continue;
95
+ }
96
+ if (kind === "num") {
97
+ if (!Number.isFinite(Number(term.number))) return false;
98
+ continue;
99
+ }
100
+ return false; // unknown term type
101
+ }
102
+ return (maxTotal - minTotal) <= MAX_BUCKETS;
103
+ };
104
+
105
+ // Build the exact PMF for an additive term list.
106
+ const convolveTerms = (terms) => {
107
+ let acc = new Map([[0, 1]]);
108
+ let sign = 1;
109
+ for (const term of terms) {
110
+ const kind = classify(term);
111
+ if (kind === "op") {
112
+ sign = term.operator === "-" ? -1 : 1;
113
+ continue;
114
+ }
115
+ if (kind === "die") {
116
+ let dist = new Map([[0, 1]]);
117
+ for (let i = 0; i < Number(term.number); i++) dist = convolve(dist, dieDist(Number(term.faces)));
118
+ if (sign < 0) dist = negate(dist);
119
+ acc = convolve(acc, dist);
120
+ }
121
+ else if (kind === "num") {
122
+ acc = shift(acc, sign * Number(term.number));
123
+ }
124
+ sign = 1;
125
+ }
126
+ return acc;
127
+ };
128
+
129
+ // Derive the display payload (average, min, max, chart series) from a PMF.
130
+ const summarize = (dist, approximate) => {
131
+ const entries = [...dist.entries()].sort((a, b) => a[0] - b[0]);
132
+ let average = 0, min = Infinity, max = -Infinity;
133
+ for (const [v, p] of entries) {
134
+ average += v * p;
135
+ if (v < min) min = v;
136
+ if (v > max) max = v;
137
+ }
138
+ const outcomeValues = entries.map(([v]) => v);
139
+ const values = entries.map(([, p]) => +(p * 100).toFixed(2));
140
+ // Thin the x-axis labels so they don't cluster/overlap: pick a "nice" step
141
+ // (1, 2, 5, 10, ...) targeting ~8 labels, and only label outcomes that land on
142
+ // a multiple of it -- so the axis reads in round numbers (5, 10, 15, 20 ...).
143
+ const lo = outcomeValues.length ? outcomeValues[0] : 0;
144
+ const hi = outcomeValues.length ? outcomeValues[outcomeValues.length - 1] : 0;
145
+ const span = Math.max(1, hi - lo);
146
+ const rawStep = span / 8;
147
+ const pow = Math.pow(10, Math.floor(Math.log10(rawStep)));
148
+ const norm = rawStep / pow;
149
+ const niceStep = (norm <= 1 ? 1 : norm <= 2 ? 2 : norm <= 5 ? 5 : 10) * pow;
150
+ const step = Math.max(1, Math.round(niceStep));
151
+ return {
152
+ average: +average.toFixed(2),
153
+ min: entries.length ? min : 0,
154
+ max: entries.length ? max : 0,
155
+ labels: outcomeValues,
156
+ values,
157
+ step,
158
+ approximate,
159
+ hasData: entries.length > 0
160
+ };
161
+ };
162
+
163
+ // ----- Reactive computation --------------------------------------------------
164
+
165
+ const result = ref({ average: 0, min: 0, max: 0, labels: [], values: [], step: 1, approximate: false, hasData: false });
166
+ const iterations = ref(0);
167
+ let runToken = 0;
168
+
169
+ // Monte Carlo fallback. Roll.simulate builds a full Roll per sample (~0.2ms each),
170
+ // so a large batch blocks the main thread for seconds and freezes the sheet on
171
+ // render. Instead, sample a modest target in small chunks and yield to the event
172
+ // loop between each, refining the curve in place. The sheet stays responsive and
173
+ // the chart fills in progressively.
174
+ const SIM_TARGET = 2000;
175
+ const SIM_CHUNK = 200;
176
+ const runSimulation = async (formula, token, cacheKey) => {
177
+ const counts = new Map();
178
+ let done = 0;
179
+ while (done < SIM_TARGET) {
180
+ if (token !== runToken) return; // a newer run superseded us
181
+ const n = Math.min(SIM_CHUNK, SIM_TARGET - done);
182
+ const batch = await Roll.simulate(formula, n);
183
+ if (token !== runToken) return;
184
+ for (const v of batch) counts.set(v, (counts.get(v) || 0) + 1);
185
+ done += n;
186
+ const dist = new Map();
187
+ for (const [v, c] of counts) dist.set(v, c / done);
188
+ result.value = summarize(dist, true);
189
+ iterations.value = done;
190
+ // Yield so the browser can paint and handle input between chunks.
191
+ await new Promise(r => setTimeout(r, 0));
192
+ }
193
+ // Cache the completed estimate so this formula isn't re-simulated later.
194
+ __rollVisualizerCache.set(cacheKey, result.value);
195
+ };
196
+
197
+ const recompute = () => {
198
+ const token = ++runToken;
199
+ const raw = (props.formula || "").trim();
200
+ if (!raw) {
201
+ result.value = { average: 0, min: 0, max: 0, labels: [], values: [], step: 1, approximate: false, hasData: false };
202
+ return;
203
+ }
204
+ let roll;
205
+ try {
206
+ roll = new Roll(raw, props.rollData || {});
207
+ }
208
+ catch (e) {
209
+ result.value = { average: 0, min: 0, max: 0, labels: [], values: [], step: 1, approximate: false, hasData: false };
210
+ return;
211
+ }
212
+ // Cache hit on the resolved formula -> reuse the result, no recompute.
213
+ const cacheKey = roll.formula;
214
+ const cached = __rollVisualizerCache.get(cacheKey);
215
+ if (cached) {
216
+ result.value = cached;
217
+ iterations.value = cached.approximate ? SIM_TARGET : 0;
218
+ return;
219
+ }
220
+ // After construction Foundry has substituted @refs into the terms.
221
+ const terms = roll.terms || [];
222
+ if (canConvolve(terms)) {
223
+ iterations.value = 0;
224
+ const summary = summarize(convolveTerms(terms), false);
225
+ __rollVisualizerCache.set(cacheKey, summary);
226
+ result.value = summary;
227
+ }
228
+ else {
229
+ // Simulate against the resolved formula (no @refs remain in roll.formula).
230
+ runSimulation(roll.formula, token, cacheKey);
231
+ }
232
+ };
233
+
234
+ // Debounce so rapid field edits don't kick off redundant simulations.
235
+ let debounceHandle = null;
236
+ watch(
237
+ () => [props.formula, JSON.stringify(props.rollData || {})],
238
+ () => {
239
+ if (debounceHandle) clearTimeout(debounceHandle);
240
+ debounceHandle = setTimeout(recompute, 200);
241
+ },
242
+ { immediate: true }
243
+ );
244
+
245
+ const getLabel = computed(() => {
246
+ const localized = game.i18n.localize(props.label);
247
+ if (props.icon) return \`<i class="fa-solid \${props.icon}"></i> \${localized}\`;
248
+ return localized;
249
+ });
250
+ const accentColor = computed(() => props.color || "#92aed9");
251
+ const averageText = computed(() => (result.value.approximate ? "≈ " : "") + result.value.average);
252
+ </script>
253
+
254
+ <template>
255
+ <v-card class="isdl-roll-visualizer" :name="systemPath" variant="tonal" density="compact">
256
+ <div class="isdl-roll-visualizer__header">
257
+ <span class="isdl-roll-visualizer__label" v-html="getLabel"></span>
258
+ <span class="isdl-roll-visualizer__avg" :style="{ color: accentColor }">{{ averageText }}</span>
259
+ </div>
260
+ <v-sparkline
261
+ v-if="result.hasData"
262
+ :labels="result.labels"
263
+ :model-value="result.values"
264
+ :color="accentColor"
265
+ line-width="2"
266
+ padding="8"
267
+ smooth="6"
268
+ :label-size="10"
269
+ auto-draw
270
+ preserveAspectRatio="none"
271
+ >
272
+ <!-- Render a label only on "nice" round outcomes; blank elsewhere so the
273
+ axis doesn't cluster and never falls back to showing the raw value. -->
274
+ <template #label="item">
275
+ {{ Number(item.value) % result.step === 0 ? item.value : "" }}
276
+ </template>
277
+ </v-sparkline>
278
+ <div v-else class="isdl-roll-visualizer__empty text-caption">
279
+ {{ game.i18n.localize("ROLLVISUALIZER.NoFormula") }}
280
+ </div>
281
+ <div class="isdl-roll-visualizer__footer text-caption">
282
+ <span v-if="result.hasData">
283
+ {{ game.i18n.localize("ROLLVISUALIZER.Min") }}: {{ result.min }}
284
+ &middot;
285
+ {{ game.i18n.localize("ROLLVISUALIZER.Max") }}: {{ result.max }}
286
+ &middot;
287
+ {{ game.i18n.localize("ROLLVISUALIZER.Average") }}: {{ averageText }}
288
+ </span>
289
+ <span v-if="result.approximate && iterations > 0" class="isdl-roll-visualizer__approx">
290
+ ({{ iterations }} {{ game.i18n.localize("ROLLVISUALIZER.Simulations") }})
291
+ </span>
292
+ </div>
293
+ </v-card>
294
+ </template>
295
295
  `.appendNewLine();
296
296
  fs.writeFileSync(generatedFilePath, toString(fileNode));
297
297
  }