intelligent-system-design-language 0.3.21 → 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 (116) 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 -1381
  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 -1381
  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 -651
  27. package/out/cli/components/chat-card-generator.js.map +1 -1
  28. package/out/cli/components/css-generator.js +4 -4
  29. package/out/cli/components/damage-roll-generator.js +160 -160
  30. package/out/cli/components/datamodel-generator.js +264 -257
  31. package/out/cli/components/datamodel-generator.js.map +1 -1
  32. package/out/cli/components/derived-data-generator.js +923 -923
  33. package/out/cli/components/hotbar-drop-hook-generator.js +82 -82
  34. package/out/cli/components/init-hook-generator.js +495 -495
  35. package/out/cli/components/language-generator.js +1 -1
  36. package/out/cli/components/language-generator.js.map +1 -1
  37. package/out/cli/components/measured-template-preview.js +221 -221
  38. package/out/cli/components/method-generator.js +979 -887
  39. package/out/cli/components/method-generator.js.map +1 -1
  40. package/out/cli/components/ready-hook-generator.js +404 -404
  41. package/out/cli/components/token-generator.js +116 -116
  42. package/out/cli/components/vue/base-components/vue-attribute.js +138 -138
  43. package/out/cli/components/vue/base-components/vue-boolean.js +64 -64
  44. package/out/cli/components/vue/base-components/vue-calculator.js +93 -93
  45. package/out/cli/components/vue/base-components/vue-damage-application.js +356 -356
  46. package/out/cli/components/vue/base-components/vue-damage-bonuses.js +165 -165
  47. package/out/cli/components/vue/base-components/vue-damage-resistances.js +196 -196
  48. package/out/cli/components/vue/base-components/vue-damage-track.js +121 -121
  49. package/out/cli/components/vue/base-components/vue-date-time.js +42 -42
  50. package/out/cli/components/vue/base-components/vue-dice.js +98 -98
  51. package/out/cli/components/vue/base-components/vue-die.js +73 -73
  52. package/out/cli/components/vue/base-components/vue-document-choice.js +149 -149
  53. package/out/cli/components/vue/base-components/vue-document-choices.js +179 -179
  54. package/out/cli/components/vue/base-components/vue-document-link.js +60 -60
  55. package/out/cli/components/vue/base-components/vue-extended-choice.js +88 -88
  56. package/out/cli/components/vue/base-components/vue-inventory.js +519 -519
  57. package/out/cli/components/vue/base-components/vue-macro-choice.js +138 -138
  58. package/out/cli/components/vue/base-components/vue-measured-template.js +530 -530
  59. package/out/cli/components/vue/base-components/vue-money.js +483 -483
  60. package/out/cli/components/vue/base-components/vue-number.js +174 -174
  61. package/out/cli/components/vue/base-components/vue-paperdoll.js +43 -43
  62. package/out/cli/components/vue/base-components/vue-parent-property-reference.js +76 -76
  63. package/out/cli/components/vue/base-components/vue-prosemirror.js +18 -18
  64. package/out/cli/components/vue/base-components/vue-resource.js +136 -136
  65. package/out/cli/components/vue/base-components/vue-roll-visualizer.js +286 -109
  66. package/out/cli/components/vue/base-components/vue-roll-visualizer.js.map +1 -1
  67. package/out/cli/components/vue/base-components/vue-self-property-reference.js +62 -62
  68. package/out/cli/components/vue/base-components/vue-string-choice.js +98 -98
  69. package/out/cli/components/vue/base-components/vue-string-choices.js +203 -203
  70. package/out/cli/components/vue/base-components/vue-string.js +60 -60
  71. package/out/cli/components/vue/base-components/vue-text-field.js +53 -53
  72. package/out/cli/components/vue/base-components/vue-tracker.js +431 -431
  73. package/out/cli/components/vue/vue-action-component-generator.js +64 -64
  74. package/out/cli/components/vue/vue-active-effect-sheet-generator.js +856 -856
  75. package/out/cli/components/vue/vue-datatable-sheet-class-generator.js +292 -292
  76. package/out/cli/components/vue/vue-datatable2-component-generator.js +824 -824
  77. package/out/cli/components/vue/vue-document-creation-app.js +121 -121
  78. package/out/cli/components/vue/vue-document-creation-sheet.js +94 -94
  79. package/out/cli/components/vue/vue-generator.js +40 -40
  80. package/out/cli/components/vue/vue-mixin.js +296 -296
  81. package/out/cli/components/vue/vue-pinned-datatable-component-generator.js +260 -260
  82. package/out/cli/components/vue/vue-prompt-generator.js +91 -76
  83. package/out/cli/components/vue/vue-prompt-generator.js.map +1 -1
  84. package/out/cli/components/vue/vue-prompt-sheet-class-generator.js +317 -317
  85. package/out/cli/components/vue/vue-sheet-application-generator.js +1177 -1167
  86. package/out/cli/components/vue/vue-sheet-application-generator.js.map +1 -1
  87. package/out/cli/components/vue/vue-sheet-class-generator.js +510 -510
  88. package/out/cli/generator.js +438 -433
  89. package/out/cli/generator.js.map +1 -1
  90. package/out/extension/github/githubAuthProvider.js +71 -29
  91. package/out/extension/github/githubAuthProvider.js.map +1 -1
  92. package/out/extension/github/githubGistManager.js +4 -3
  93. package/out/extension/github/githubGistManager.js.map +1 -1
  94. package/out/extension/github/githubManager.js +40 -38
  95. package/out/extension/github/githubManager.js.map +1 -1
  96. package/out/extension/github/githubQuickActions.js +120 -120
  97. package/out/extension/github/system-workflow.yml +47 -47
  98. package/out/extension/main.cjs +909 -532
  99. package/out/extension/main.cjs.map +3 -3
  100. package/out/extension/package.json +419 -419
  101. package/out/language/generated/ast.js +51 -2
  102. package/out/language/generated/ast.js.map +1 -1
  103. package/out/language/generated/grammar.js +14240 -13991
  104. package/out/language/generated/grammar.js.map +1 -1
  105. package/out/language/intelligent-system-design-language-validator.js +32 -2
  106. package/out/language/intelligent-system-design-language-validator.js.map +1 -1
  107. package/out/language/isdl-scope-provider.js +14 -1
  108. package/out/language/isdl-scope-provider.js.map +1 -1
  109. package/out/language/main.cjs +913 -569
  110. package/out/language/main.cjs.map +3 -3
  111. package/out/package.json +419 -419
  112. package/out/progressbar.min.js +6 -6
  113. package/out/styles.scss +762 -747
  114. package/out/test/validating/diagnostics.test.js +40 -0
  115. package/out/test/validating/diagnostics.test.js.map +1 -1
  116. package/package.json +419 -419
@@ -13,554 +13,557 @@ export function generateChatCardClass(entry, destination) {
13
13
  function generateHpElement(document) {
14
14
  const healthResource = getAllOfType(document.body, isResourceExp).find(x => x.tag == "health");
15
15
  if (!healthResource) {
16
- return expandToNode `
17
- case '${document.name.toLocaleLowerCase()}':
18
- // No health resource found.
19
- break;
16
+ return expandToNode `
17
+ case '${document.name.toLocaleLowerCase()}':
18
+ // No health resource found.
19
+ break;
20
20
  `;
21
21
  }
22
- return expandToNode `
23
- case '${document.name.toLocaleLowerCase()}':
24
-
25
- // If the type is temp, add to the temp health.
26
- if ( type === 'temp' ) {
27
- update['${getSystemPath(healthResource, ['temp'], undefined, false)}'] = target.actor.${getSystemPath(healthResource, ['temp'], undefined, false)} + (finalDamage !== undefined ? finalDamage : roll);
28
- break;
29
- }
30
-
31
- // If the type is damage and we have temp health, apply to temp health first.
32
- if ( type === 'damage' && target.actor.${getSystemPath(healthResource, ['temp'], undefined, false)} > 0 ) {
33
- const appliedDamage = finalDamage !== undefined ? finalDamage : roll;
34
- update['${getSystemPath(healthResource, ['temp'], undefined, false)}'] = target.actor.${getSystemPath(healthResource, ['temp'], undefined, false)} - appliedDamage;
35
-
36
- if ( update['${getSystemPath(healthResource, ['temp'], undefined, false)}'] < 0 ) {
37
- update['${getSystemPath(healthResource)}'] = target.actor.${getSystemPath(healthResource)} + update['${getSystemPath(healthResource, ['temp'], undefined, false)}'];
38
- update['${getSystemPath(healthResource, ['temp'], undefined, false)}'] = 0;
39
- }
40
- }
41
- else {
42
- // Otherwise, apply to the main health.
43
- update['${getSystemPath(healthResource)}'] = target.actor.${getSystemPath(healthResource)} - (finalDamage !== undefined ? finalDamage : roll);
44
- }
45
- break;
22
+ return expandToNode `
23
+ case '${document.name.toLocaleLowerCase()}':
24
+
25
+ // If the type is temp, add to the temp health.
26
+ if ( type === 'temp' ) {
27
+ update['${getSystemPath(healthResource, ['temp'], undefined, false)}'] = target.actor.${getSystemPath(healthResource, ['temp'], undefined, false)} + (finalDamage !== undefined ? finalDamage : roll);
28
+ break;
29
+ }
30
+
31
+ // If the type is damage and we have temp health, apply to temp health first.
32
+ if ( type === 'damage' && target.actor.${getSystemPath(healthResource, ['temp'], undefined, false)} > 0 ) {
33
+ const appliedDamage = finalDamage !== undefined ? finalDamage : roll;
34
+ update['${getSystemPath(healthResource, ['temp'], undefined, false)}'] = target.actor.${getSystemPath(healthResource, ['temp'], undefined, false)} - appliedDamage;
35
+
36
+ if ( update['${getSystemPath(healthResource, ['temp'], undefined, false)}'] < 0 ) {
37
+ update['${getSystemPath(healthResource)}'] = target.actor.${getSystemPath(healthResource)} + update['${getSystemPath(healthResource, ['temp'], undefined, false)}'];
38
+ update['${getSystemPath(healthResource, ['temp'], undefined, false)}'] = 0;
39
+ }
40
+ }
41
+ else {
42
+ // Otherwise, apply to the main health.
43
+ update['${getSystemPath(healthResource)}'] = target.actor.${getSystemPath(healthResource)} - (finalDamage !== undefined ? finalDamage : roll);
44
+ }
45
+ break;
46
46
  `;
47
47
  }
48
- const fileNode = expandToNode `
49
- import { ContextMenu2 } from '../contextMenu2.js';
50
-
51
- export default class ${entry.config.name}ChatCard {
52
-
53
- static activateListeners(html) {
54
- html.on("click", ".collapsible", ${entry.config.name}ChatCard._onChatCardToggleCollapsible.bind(this));
55
- html.on("click", ".action", ${entry.config.name}ChatCard._handleActionClick.bind(this));
56
- html.on("click", ".revert-target-damage", ${entry.config.name}ChatCard._onRevertTargetDamage.bind(this));
57
- html.on("click", ".dice-roll", event => {
58
- const rollElement = event.currentTarget;
59
- rollElement.classList.toggle("expanded");
60
- });
61
-
62
- // Customize the drag data of effects
63
- html.find(".effect").each((i, li) => {
64
- li.setAttribute("draggable", true);
65
- li.addEventListener("dragstart", async ev => {
66
- let dragData = {
67
- type: "ActiveEffect",
68
- uuid: li.dataset.uuid
69
- };
70
- ev.dataTransfer.setData("text/plain", JSON.stringify(dragData));
71
- }, false);
72
- });
73
-
74
- // If this is not the latest message, default to collapsed
75
- const thisMessageId = html.data("messageId");
76
- const messages = Array.from(game.messages);
77
- const latestMessageId = messages[game.messages.size - 1]._id;
78
- if (thisMessageId !== latestMessageId) {
79
- html.find(".collapsible").addClass("collapsed");
80
- }
81
-
82
- // Collapse the previous message automatically if it is not already collapsed
83
- const previousMessageId = messages[game.messages.size - 2]?._id;
84
- const previousMessage = window.document.querySelector(\`#chat .chat-message[data-message-id="\${previousMessageId}"]\`);
85
- if (previousMessage) {
86
- for (const collapsible of previousMessage.querySelectorAll(".collapsible") ?? []) {
87
- if (!collapsible.classList.contains("collapsed")) {
88
- collapsible.classList.add("collapsed");
89
- }
90
- }
91
- }
92
-
93
- function uuidv4() {
94
- return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
95
- var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
96
- return v.toString(16);
97
- });
98
- }
99
-
100
- function applyMenus(roll) {
101
- var uuid = uuidv4();
102
-
103
- // Add a way to uniquely identify this roll
104
- $(this)[0].dataset.uuid = uuid;
105
- $(this).off("contextmenu");
106
-
107
- // Determine if applying damage to targets is allowed.
108
- const allowTargeting = game.settings.get('${id}', 'allowTargetDamageApplication');
109
- let targetType = game.settings.get('${id}', 'userTargetDamageApplicationType');
110
- if (!allowTargeting && targetType !== 'selected') {
111
- game.settings.set('${id}', 'userTargetDamageApplicationType', 'selected');
112
- targetType = 'selected';
113
- }
114
-
115
- let menuItems = [];
116
-
117
- function getRollFromElement(rollElement) {
118
- const element = rollElement.hasClass('inline-roll')
119
- ? rollElement
120
- : rollElement.find('.result');
121
-
122
- if (element.length === 0) return null;
123
-
124
- // Check if this is a damage roll by looking for damage roll attributes
125
- const isDamageRoll = rollElement.hasClass('damage-roll') || rollElement.closest('.damage-roll').length > 0;
126
- const damageRollElement = isDamageRoll ? (rollElement.hasClass('damage-roll') ? rollElement : rollElement.closest('.damage-roll')) : null;
127
-
128
- const rollValue = getRollValue(element);
129
-
130
- // If this is a damage roll, extract metadata
131
- if (isDamageRoll && damageRollElement && damageRollElement.length > 0) {
132
- const damageType = damageRollElement.attr('data-damage-type') || null;
133
- const damageTypeSpan = damageRollElement.find('.damage-type');
134
- const damageColor = damageTypeSpan.length > 0 ? damageTypeSpan.css('color') : null;
135
- const damageIcon = damageRollElement.find('i').first().attr('class') || null;
136
-
137
- // Extract custom metadata from damage-metadata section if present
138
- const metadata = {};
139
- damageRollElement.find('.damage-metadata .damage-property').each(function() {
140
- const property = $(this).attr('data-property');
141
- const text = $(this).text();
142
- const colonIndex = text.indexOf(':');
143
- if (colonIndex !== -1 && property) {
144
- const value = text.substring(colonIndex + 1).trim();
145
- metadata[property] = value;
146
- }
147
- });
148
-
149
- return {
150
- value: rollValue,
151
- isDamageRoll: true,
152
- damageType: damageType,
153
- damageColor: damageColor,
154
- damageIcon: damageIcon,
155
- metadata: metadata
156
- };
157
- }
158
-
159
- return { value: rollValue, isDamageRoll: false };
160
- }
161
-
162
- function getRollValue(roll) {
163
- if (Number.isInteger(roll)) {
164
- return roll;
165
- }
166
- if (roll instanceof Roll) {
167
- return roll.total;
168
- }
169
- // Try the regex for expanded rolls.
170
- const REGEX_EXPANDED_INLINE_ROLL = /.*=\s(\d+)/gm;
171
- let match = REGEX_EXPANDED_INLINE_ROLL.exec(roll[0].innerText);
172
- if (match) return Number.parseInt(match[1]);
173
-
174
- // Regex failed to match, try grabbing the inner text.
175
- match = Number.parseInt(roll[0].innerText.trim());
176
- return match || 0; // Fallback if we failed to parse
177
- }
178
-
179
- function getTargets(targetType) {
180
- const targets = targetType === 'targeted'
181
- ? [...game.user.targets]
182
- : (canvas?.tokens?.controlled ?? []);
183
-
184
- if (!targets || targets?.length < 1) {
185
- ui.notifications.warn(game.i18n.localize(\`NOTIFICATIONS.\${targetType === 'targeted' ? 'NoTokenTargeted' : 'NoTokenSelected'}\`));
186
- return [];
187
- }
188
-
189
- return targets;
190
- }
191
-
192
- async function apply(element, event, type) {
193
- const menu = element.find('#context-menu2')?.[0];
194
- const applyTargetType = menu?.dataset?.target ?? 'selected';
195
- const applyMod = menu?.dataset?.mod ? Number(menu.dataset.mod) : 1;
196
- const bonusDamage = menu?.dataset?.bonus ? Number(menu.dataset.bonus) : 0;
197
-
198
- let rollData = getRollFromElement(element);
199
- if ( !rollData ) return;
200
-
201
- let baseRoll = rollData.value;
202
- if ( type === 'healing' ) {
203
- baseRoll = -baseRoll;
204
- }
205
-
206
- baseRoll *= applyMod;
207
-
208
- const targets = getTargets(applyTargetType);
209
- const applicationSummary = {
210
- type: type,
211
- originalDamage: rollData.value,
212
- bonusDamage: bonusDamage,
213
- multiplier: applyMod,
214
- totalBaseDamage: baseRoll,
215
- damageType: rollData.isDamageRoll ? rollData.damageType : null,
216
- damageColor: rollData.isDamageRoll ? rollData.damageColor : null,
217
- damageIcon: rollData.isDamageRoll ? rollData.damageIcon : null,
218
- targets: [],
219
- timestamp: Date.now(),
220
- userId: game.user.id
221
- };
222
-
223
- for ( const target of targets ) {
224
- console.log(type, baseRoll, target);
225
- const update = {};
226
-
227
- let roll = foundry.utils.duplicate(baseRoll);
228
-
229
- // Create enhanced context with damage type and metadata if available
230
- const context = {
231
- amount: roll,
232
- damageType: rollData.isDamageRoll ? rollData.damageType : null,
233
- damageMetadata: rollData.isDamageRoll ? rollData.metadata : {},
234
- color: rollData.isDamageRoll ? rollData.damageColor : null,
235
- icon: rollData.isDamageRoll ? rollData.damageIcon : null,
236
- isDamageRoll: rollData.isDamageRoll
237
- };
238
-
239
- await Hooks.callAllAsync('preApply' + type.titleCase(), target.actor, context);
240
- roll = context.amount;
241
-
242
- // Calculate resistance information for this target
243
- let flatResistance = 0;
244
- let percentResistance = 0;
245
- let finalDamage = roll;
246
-
247
- if (rollData.isDamageRoll && rollData.damageType && type === 'damage') {
248
- // Get resistance information from actor using datamodel field names
249
- const damageTypeKey = rollData.damageType.toLowerCase().replace(/\s+/g, '');
250
- const flatResistanceField = \`\${damageTypeKey}damageresistanceflat\`;
251
- const percentResistanceField = \`\${damageTypeKey}damageresistancepercent\`;
252
-
253
- flatResistance = target.actor.system[flatResistanceField] || 0;
254
- percentResistance = target.actor.system[percentResistanceField] || 0;
255
-
256
- if (flatResistance > 0 || percentResistance > 0) {
257
- // Apply resistance: (damage - flat) * (1 - percent/100)
258
- finalDamage = Math.max(0, (roll - flatResistance) * (1 - percentResistance / 100));
259
- finalDamage = Math.floor(finalDamage);
260
- }
261
- }
262
-
263
- // Store pre-application state for revert functionality
264
- const preState = {};
265
- const targetSummary = {
266
- uuid: target.document.uuid,
267
- name: target.actor.name,
268
- appliedAmount: roll,
269
- finalDamage: finalDamage,
270
- flatResistance: flatResistance,
271
- percentResistance: percentResistance,
272
- preState: {},
273
- postState: {}
274
- };
275
-
276
- switch ( target.actor.type ) {
277
- ${joinToNode(entry.documents, document => generateHpElement(document), { appendNewLineIfNotEmpty: true })}
278
- }
279
-
280
- // Store pre-state for revert
281
- for (const [key, value] of Object.entries(update)) {
282
- targetSummary.preState[key] = foundry.utils.getProperty(target.actor, key);
283
- targetSummary.postState[key] = value;
284
- }
285
-
286
- await target.actor.update(update);
287
- applicationSummary.targets.push(targetSummary);
288
-
289
- // Call the applied hook with enhanced context
290
- Hooks.callAll('applied' + type.titleCase(), target.actor, context);
291
- }
292
-
293
- // Create damage application summary chat card
294
- await createDamageApplicationChatCard(applicationSummary);
295
- }
296
-
297
- async function createDamageApplicationChatCard(summary) {
298
- const setting = game.settings.get('${id}', 'damageApplicationChatCard');
299
- if (setting === 'none') return;
300
-
301
- const whisper = setting === 'gm' ? ChatMessage.getWhisperRecipients('GM') : [];
302
- const typeLabel = summary.type === 'damage' ? 'Damage' : summary.type === 'healing' ? 'Healing' : 'Temp HP';
303
-
304
- // Create HTML content for the chat card
305
- const targetRows = summary.targets.map((target, index) => {
306
- let resistanceText = 'N/A';
307
-
308
- if (summary.type === 'damage' && summary.damageType) {
309
- const hasFlat = target.flatResistance > 0;
310
- const hasPercent = target.percentResistance > 0;
311
-
312
- if (hasFlat || hasPercent) {
313
- const parts = [];
314
- if (hasFlat) parts.push(\`\${target.flatResistance}\`);
315
- if (hasPercent) parts.push(\`\${target.percentResistance}%\`);
316
- resistanceText = parts.join(', ');
317
- } else {
318
- resistanceText = 'None';
319
- }
320
- }
321
-
322
- return \`
323
- <tr class="target-row" data-target-index="\${index}">
324
- <td><strong>\${target.name}</strong></td>
325
- <td>\${resistanceText}</td>
326
- <td><strong>\${summary.type === 'healing' ? '+' : ''}\${target.finalDamage}</strong></td>
327
- <td>
328
- <button type="button" class="revert-target-damage" data-target-index="\${index}" data-summary='\${JSON.stringify(summary)}' title="Revert this target">
329
- <i class="fas fa-undo"></i>
330
- </button>
331
- </td>
332
- </tr>
333
- \`;
334
- }).join('');
335
-
336
- const content = \`
337
- <div class="damage-application-summary">
338
- <div class="card-header">
339
- <h3><i class="fas fa-sword-cross"></i> \${typeLabel} Applied</h3>
340
- </div>
341
-
342
- <div class="damage-summary">
343
- <div class="base-damage">
344
- <strong>Base Damage:</strong> \${summary.originalDamage}
345
- \${summary.bonusDamage > 0 ? \`+ \${summary.bonusDamage} bonus\` : ''}
346
- \${summary.multiplier !== 1 ? \` × \${summary.multiplier}\` : ''}
347
- = <strong>\${summary.totalBaseDamage}</strong>
348
- \${summary.damageType ? \`<br><strong>Type:</strong> <span class="damage-type" style="color: \${summary.damageColor || '#666'}">\${summary.damageIcon ? \`<i class="\${summary.damageIcon}"></i> \` : ''}\${summary.damageType}</span>\` : ''}
349
- </div>
350
- </div>
351
-
352
- <div class="targets-summary">
353
- <h4>Applied to Targets:</h4>
354
- <table class="damage-targets">
355
- <thead>
356
- <tr>
357
- <th>Target</th>
358
- <th>Resistance</th>
359
- <th>Final Damage</th>
360
- <th>Revert</th>
361
- </tr>
362
- </thead>
363
- <tbody>
364
- \${targetRows}
365
- </tbody>
366
- </table>
367
- </div>
368
- </div>
369
- \`;
370
-
371
- await ChatMessage.create({
372
- user: game.user.id,
373
- content: content,
374
- whisper: whisper,
375
- flags: {
376
- [game.system.id]: {
377
- damageApplicationSummary: summary
378
- }
379
- }
380
- });
381
- }
382
-
383
- if ( allowTargeting ) {
384
- menuItems.push({
385
- name: \`
386
- <div class="damage-target flex flexrow">
387
- <button type="button" data-target="targeted"><i class="fa-solid fa-bullseye"></i> \${game.i18n.localize('Targeted')}</button>
388
- <button type="button" data-target="selected"><i class="fa-solid fa-expand"></i> \${game.i18n.localize('Selected')}</button>
389
- </div>\`,
390
- id: 'targets',
391
- icon: '',
392
- preventClose: true,
393
- callback: (inlineRoll, event) => {
394
- const button = event?.target ?? event?.currentTarget;
395
- if (button?.dataset?.target) {
396
- // Deactivate the other target type.
397
- const activeButtons = inlineRoll.find('button[data-target].active');
398
- activeButtons.removeClass('active');
399
- // Set the target type on the menu for later reference.
400
- const menu = inlineRoll.find('#context-menu2')[0];
401
- if (menu) {
402
- menu.dataset.target = button.dataset.target;
403
- }
404
- // Toggle the active button and update the user setting.
405
- button.classList.add('active');
406
- game.settings.set('${id}', 'userTargetDamageApplicationType', button.dataset.target);
407
- }
408
- }
409
- });
410
- }
411
-
412
- // Add damage multipliers.
413
- menuItems.push({
414
- name: \`
415
- <div class="damage-modifiers flex flexrow">
416
- <button type="button" data-mod="0.25">&frac14;x</button>
417
- <button type="button" data-mod="0.5">&frac12;x</button>
418
- <button type="button" data-mod="1" class="active">1x</button>
419
- <button type="button" data-mod="1.5">1.5x</button>
420
- <button type="button" data-mod="2">2x</button>
421
- <button type="button" data-mod="3">3x</button>
422
- <button type="button" data-mod="4">4x</button>
423
- </div>\`,
424
- id: 'modifiers',
425
- icon: '',
426
- preventClose: true,
427
- callback: (inlineRoll, event) => {
428
- const button = event?.target ?? event?.currentTarget;
429
- if (button?.dataset?.mod) {
430
- // Deactivate the other target type.
431
- const activeButtons = inlineRoll.find('button[data-mod].active');
432
- activeButtons.removeClass('active');
433
-
434
- // Set the target type on the menu for later reference.
435
- const menu = inlineRoll.find('#context-menu2')[0];
436
- if (menu) {
437
- menu.dataset.mod = button.dataset.mod;
438
- }
439
-
440
- // Toggle the active button and update the user setting.
441
- button.classList.add('active');
442
- }
443
- }
444
- });
445
-
446
- menuItems.push(
447
- {
448
- name: game.i18n.localize("CONTEXT.ApplyDamage"),
449
- id: 'damage',
450
- icon: '<i class="fas fa-tint"></i>',
451
- callback: (inlineRoll, event) => apply(inlineRoll, event, 'damage')
452
- },
453
- {
454
- name: game.i18n.localize("CONTEXT.ApplyHealing"),
455
- id: 'healing',
456
- icon: '<i class="fas fa-medkit"></i>',
457
- callback: (inlineRoll, event) => apply(inlineRoll, event, 'healing')
458
- },
459
- {
460
- name: game.i18n.localize("CONTEXT.ApplyTemp"),
461
- id: 'temp-healing',
462
- icon: '<i class="fas fa-heart"></i>',
463
- callback: (inlineRoll, event) => apply(inlineRoll, event, 'temp')
464
- }
465
- );
466
- new ContextMenu2($(this).parent(), \`[data-uuid=\${uuid}]\`, menuItems);
467
- }
468
- html.find('.inline-roll').each(applyMenus);
469
- html.find('.dice-total').each(applyMenus);
470
- }
471
-
472
- /* -------------------------------------------- */
473
-
474
- static _onChatCardToggleCollapsible(event) {
475
- const target = event.currentTarget;
476
-
477
- // If the target is a content-link, ignore the click event
478
- if (event.target.classList.contains("content-link")) return;
479
-
480
- event.preventDefault();
481
- target.classList.toggle("collapsed");
482
-
483
- // Clear the height from the chat popout container so that it appropriately resizes.
484
- const popout = target.closest(".chat-popout");
485
- if ( popout ) popout.style.height = "";
486
- }
487
-
488
- /* -------------------------------------------- */
489
-
490
- static _handleActionClick(event) {
491
- event.preventDefault();
492
- const action = event.currentTarget.dataset.action;
493
-
494
- switch (action) {
495
- case "place":
496
- const template = event.currentTarget.closest(".measured-template");
497
- if (!template) return;
498
-
499
- const context = {
500
- type: template.dataset.type,
501
- distance: template.dataset.distance,
502
- direction: template.dataset.direction,
503
- angle: template.dataset.angle,
504
- width: template.dataset.width
505
- };
506
-
507
- // Trigger the place action on the template
508
- game.system.measuredTemplatePreviewClass.place(context, game.user.character?.sheet);
509
- break;
510
- }
511
- }
512
-
513
- static async _onRevertTargetDamage(event) {
514
- const button = event.currentTarget;
515
- const summaryData = JSON.parse(button.dataset.summary);
516
- const targetIndex = parseInt(button.dataset.targetIndex);
517
- const targetData = summaryData.targets[targetIndex];
518
-
519
- if (!targetData) {
520
- ui.notifications.error("Target data not found");
521
- return;
522
- }
523
-
524
- // Confirm revert
525
- const confirm = await Dialog.confirm({
526
- title: "Revert Target Damage",
527
- content: \`<p>Are you sure you want to revert the \${summaryData.type} application to <strong>\${targetData.name}</strong>?</p><p>This will restore the actor to their previous state.</p>\`,
528
- yes: () => true,
529
- no: () => false
530
- });
531
-
532
- if (!confirm) return;
533
-
534
- const tokenDocument = await fromUuid(targetData.uuid);
535
- if (!tokenDocument || !tokenDocument.actor) {
536
- ui.notifications.warn(\`Cannot find target: \${targetData.name}\`);
537
- return;
538
- }
539
-
540
- try {
541
- await tokenDocument.actor.update(targetData.preState);
542
- console.log(\`Reverted \${targetData.name} to pre-application state\`);
543
-
544
- // Update the button to show it's been reverted
545
- button.disabled = true;
546
- button.innerHTML = '<i class="fas fa-check"></i>';
547
- button.classList.add('reverted');
548
- button.title = 'Reverted';
549
-
550
- // Mark the table row as reverted
551
- const targetRow = button.closest('.target-row');
552
- if (targetRow) {
553
- targetRow.classList.add('reverted');
554
- targetRow.style.opacity = '0.6';
555
- targetRow.style.textDecoration = 'line-through';
556
- }
557
-
558
- ui.notifications.info(\`Damage application reverted for \${targetData.name}\`);
559
- } catch (error) {
560
- ui.notifications.error(\`Failed to revert \${targetData.name}: \${error.message}\`);
561
- }
562
- }
563
- }
48
+ const fileNode = expandToNode `
49
+ import { ContextMenu2 } from '../contextMenu2.js';
50
+
51
+ export default class ${entry.config.name}ChatCard {
52
+
53
+ static activateListeners(html) {
54
+ html.on("click", ".collapsible", ${entry.config.name}ChatCard._onChatCardToggleCollapsible.bind(this));
55
+ html.on("click", ".action", ${entry.config.name}ChatCard._handleActionClick.bind(this));
56
+ html.on("click", ".revert-target-damage", ${entry.config.name}ChatCard._onRevertTargetDamage.bind(this));
57
+ html.on("click", ".dice-roll", event => {
58
+ const rollElement = event.currentTarget;
59
+ rollElement.classList.toggle("expanded");
60
+ });
61
+
62
+ // Customize the drag data of effects
63
+ html.find(".effect").each((i, li) => {
64
+ li.setAttribute("draggable", true);
65
+ li.addEventListener("dragstart", async ev => {
66
+ let dragData = {
67
+ type: "ActiveEffect",
68
+ uuid: li.dataset.uuid
69
+ };
70
+ ev.dataTransfer.setData("text/plain", JSON.stringify(dragData));
71
+ }, false);
72
+ });
73
+
74
+ // If this is not the latest message, default to collapsed
75
+ const thisMessageId = html.data("messageId");
76
+ const messages = Array.from(game.messages);
77
+ const latestMessageId = messages[game.messages.size - 1]._id;
78
+ if (thisMessageId !== latestMessageId) {
79
+ html.find(".collapsible").addClass("collapsed");
80
+ }
81
+
82
+ // Collapse the previous message automatically if it is not already collapsed
83
+ const previousMessageId = messages[game.messages.size - 2]?._id;
84
+ const previousMessage = window.document.querySelector(\`#chat .chat-message[data-message-id="\${previousMessageId}"]\`);
85
+ if (previousMessage) {
86
+ for (const collapsible of previousMessage.querySelectorAll(".collapsible") ?? []) {
87
+ if (!collapsible.classList.contains("collapsed")) {
88
+ collapsible.classList.add("collapsed");
89
+ }
90
+ }
91
+ }
92
+
93
+ function uuidv4() {
94
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
95
+ var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
96
+ return v.toString(16);
97
+ });
98
+ }
99
+
100
+ function applyMenus(roll) {
101
+ var uuid = uuidv4();
102
+
103
+ // Add a way to uniquely identify this roll
104
+ $(this)[0].dataset.uuid = uuid;
105
+ $(this).off("contextmenu");
106
+
107
+ // Determine if applying damage to targets is allowed.
108
+ const allowTargeting = game.settings.get('${id}', 'allowTargetDamageApplication');
109
+ let targetType = game.settings.get('${id}', 'userTargetDamageApplicationType');
110
+ if (!allowTargeting && targetType !== 'selected') {
111
+ game.settings.set('${id}', 'userTargetDamageApplicationType', 'selected');
112
+ targetType = 'selected';
113
+ }
114
+
115
+ let menuItems = [];
116
+
117
+ function getRollFromElement(rollElement) {
118
+ const element = rollElement.hasClass('inline-roll')
119
+ ? rollElement
120
+ : rollElement.find('.result');
121
+
122
+ if (element.length === 0) return null;
123
+
124
+ // Check if this is a damage roll by looking for damage roll attributes
125
+ const isDamageRoll = rollElement.hasClass('damage-roll') || rollElement.closest('.damage-roll').length > 0;
126
+ const damageRollElement = isDamageRoll ? (rollElement.hasClass('damage-roll') ? rollElement : rollElement.closest('.damage-roll')) : null;
127
+
128
+ const rollValue = getRollValue(element);
129
+
130
+ // If this is a damage roll, extract metadata
131
+ if (isDamageRoll && damageRollElement && damageRollElement.length > 0) {
132
+ const damageType = damageRollElement.attr('data-damage-type') || null;
133
+ const damageTypeSpan = damageRollElement.find('.damage-type');
134
+ const damageColor = damageTypeSpan.length > 0 ? damageTypeSpan.css('color') : null;
135
+ const damageIcon = damageRollElement.find('i').first().attr('class') || null;
136
+
137
+ // Extract custom metadata from damage-metadata section if present
138
+ const metadata = {};
139
+ damageRollElement.find('.damage-metadata .damage-property').each(function() {
140
+ const property = $(this).attr('data-property');
141
+ const text = $(this).text();
142
+ const colonIndex = text.indexOf(':');
143
+ if (colonIndex !== -1 && property) {
144
+ const value = text.substring(colonIndex + 1).trim();
145
+ metadata[property] = value;
146
+ }
147
+ });
148
+
149
+ return {
150
+ value: rollValue,
151
+ isDamageRoll: true,
152
+ damageType: damageType,
153
+ damageColor: damageColor,
154
+ damageIcon: damageIcon,
155
+ metadata: metadata
156
+ };
157
+ }
158
+
159
+ return { value: rollValue, isDamageRoll: false };
160
+ }
161
+
162
+ function getRollValue(roll) {
163
+ if (Number.isInteger(roll)) {
164
+ return roll;
165
+ }
166
+ if (roll instanceof Roll) {
167
+ return roll.total;
168
+ }
169
+ // Try the regex for expanded rolls.
170
+ const REGEX_EXPANDED_INLINE_ROLL = /.*=\s(\d+)/gm;
171
+ let match = REGEX_EXPANDED_INLINE_ROLL.exec(roll[0].innerText);
172
+ if (match) return Number.parseInt(match[1]);
173
+
174
+ // Regex failed to match, try grabbing the inner text.
175
+ match = Number.parseInt(roll[0].innerText.trim());
176
+ return match || 0; // Fallback if we failed to parse
177
+ }
178
+
179
+ function getTargets(targetType) {
180
+ const targets = targetType === 'targeted'
181
+ ? [...game.user.targets]
182
+ : (canvas?.tokens?.controlled ?? []);
183
+
184
+ if (!targets || targets?.length < 1) {
185
+ ui.notifications.warn(game.i18n.localize(\`NOTIFICATIONS.\${targetType === 'targeted' ? 'NoTokenTargeted' : 'NoTokenSelected'}\`));
186
+ return [];
187
+ }
188
+
189
+ return targets;
190
+ }
191
+
192
+ async function apply(element, event, type) {
193
+ // The menu is mounted on <body>, not inside the roll element, so look it up by id.
194
+ const menu = $('#context-menu2')?.[0];
195
+ const applyTargetType = menu?.dataset?.target ?? 'selected';
196
+ const applyMod = menu?.dataset?.mod ? Number(menu.dataset.mod) : 1;
197
+ const bonusDamage = menu?.dataset?.bonus ? Number(menu.dataset.bonus) : 0;
198
+
199
+ let rollData = getRollFromElement(element);
200
+ if ( !rollData ) return;
201
+
202
+ let baseRoll = rollData.value;
203
+ if ( type === 'healing' ) {
204
+ baseRoll = -baseRoll;
205
+ }
206
+
207
+ baseRoll *= applyMod;
208
+
209
+ const targets = getTargets(applyTargetType);
210
+ const applicationSummary = {
211
+ type: type,
212
+ originalDamage: rollData.value,
213
+ bonusDamage: bonusDamage,
214
+ multiplier: applyMod,
215
+ totalBaseDamage: baseRoll,
216
+ damageType: rollData.isDamageRoll ? rollData.damageType : null,
217
+ damageColor: rollData.isDamageRoll ? rollData.damageColor : null,
218
+ damageIcon: rollData.isDamageRoll ? rollData.damageIcon : null,
219
+ targets: [],
220
+ timestamp: Date.now(),
221
+ userId: game.user.id
222
+ };
223
+
224
+ for ( const target of targets ) {
225
+ console.log(type, baseRoll, target);
226
+ const update = {};
227
+
228
+ let roll = foundry.utils.duplicate(baseRoll);
229
+
230
+ // Create enhanced context with damage type and metadata if available
231
+ const context = {
232
+ amount: roll,
233
+ damageType: rollData.isDamageRoll ? rollData.damageType : null,
234
+ damageMetadata: rollData.isDamageRoll ? rollData.metadata : {},
235
+ color: rollData.isDamageRoll ? rollData.damageColor : null,
236
+ icon: rollData.isDamageRoll ? rollData.damageIcon : null,
237
+ isDamageRoll: rollData.isDamageRoll
238
+ };
239
+
240
+ await Hooks.callAllAsync('preApply' + type.titleCase(), target.actor, context);
241
+ roll = context.amount;
242
+
243
+ // Calculate resistance information for this target
244
+ let flatResistance = 0;
245
+ let percentResistance = 0;
246
+ let finalDamage = roll;
247
+
248
+ if (rollData.isDamageRoll && rollData.damageType && type === 'damage') {
249
+ // Get resistance information from actor using datamodel field names
250
+ const damageTypeKey = rollData.damageType.toLowerCase().replace(/\s+/g, '');
251
+ const flatResistanceField = \`\${damageTypeKey}damageresistanceflat\`;
252
+ const percentResistanceField = \`\${damageTypeKey}damageresistancepercent\`;
253
+
254
+ flatResistance = target.actor.system[flatResistanceField] || 0;
255
+ percentResistance = target.actor.system[percentResistanceField] || 0;
256
+
257
+ if (flatResistance > 0 || percentResistance > 0) {
258
+ // Apply resistance: (damage - flat) * (1 - percent/100)
259
+ finalDamage = Math.max(0, (roll - flatResistance) * (1 - percentResistance / 100));
260
+ finalDamage = Math.floor(finalDamage);
261
+ }
262
+ }
263
+
264
+ // Store pre-application state for revert functionality
265
+ const preState = {};
266
+ const targetSummary = {
267
+ uuid: target.document.uuid,
268
+ name: target.actor.name,
269
+ appliedAmount: roll,
270
+ finalDamage: finalDamage,
271
+ flatResistance: flatResistance,
272
+ percentResistance: percentResistance,
273
+ preState: {},
274
+ postState: {}
275
+ };
276
+
277
+ switch ( target.actor.type ) {
278
+ ${joinToNode(entry.documents, document => generateHpElement(document), { appendNewLineIfNotEmpty: true })}
279
+ }
280
+
281
+ // Store pre-state for revert
282
+ for (const [key, value] of Object.entries(update)) {
283
+ targetSummary.preState[key] = foundry.utils.getProperty(target.actor, key);
284
+ targetSummary.postState[key] = value;
285
+ }
286
+
287
+ await target.actor.update(update);
288
+ applicationSummary.targets.push(targetSummary);
289
+
290
+ // Call the applied hook with enhanced context
291
+ Hooks.callAll('applied' + type.titleCase(), target.actor, context);
292
+ }
293
+
294
+ // Create damage application summary chat card
295
+ await createDamageApplicationChatCard(applicationSummary);
296
+ }
297
+
298
+ async function createDamageApplicationChatCard(summary) {
299
+ const setting = game.settings.get('${id}', 'damageApplicationChatCard');
300
+ if (setting === 'none') return;
301
+
302
+ const whisper = setting === 'gm' ? ChatMessage.getWhisperRecipients('GM') : [];
303
+ const typeLabel = summary.type === 'damage' ? 'Damage' : summary.type === 'healing' ? 'Healing' : 'Temp HP';
304
+
305
+ // Create HTML content for the chat card
306
+ const targetRows = summary.targets.map((target, index) => {
307
+ let resistanceText = 'N/A';
308
+
309
+ if (summary.type === 'damage' && summary.damageType) {
310
+ const hasFlat = target.flatResistance > 0;
311
+ const hasPercent = target.percentResistance > 0;
312
+
313
+ if (hasFlat || hasPercent) {
314
+ const parts = [];
315
+ if (hasFlat) parts.push(\`\${target.flatResistance}\`);
316
+ if (hasPercent) parts.push(\`\${target.percentResistance}%\`);
317
+ resistanceText = parts.join(', ');
318
+ } else {
319
+ resistanceText = 'None';
320
+ }
321
+ }
322
+
323
+ return \`
324
+ <tr class="target-row" data-target-index="\${index}">
325
+ <td><strong>\${target.name}</strong></td>
326
+ <td>\${resistanceText}</td>
327
+ <td><strong>\${summary.type === 'healing' ? '+' : ''}\${target.finalDamage}</strong></td>
328
+ <td>
329
+ <button type="button" class="revert-target-damage" data-target-index="\${index}" data-summary='\${JSON.stringify(summary)}' title="Revert this target">
330
+ <i class="fas fa-undo"></i>
331
+ </button>
332
+ </td>
333
+ </tr>
334
+ \`;
335
+ }).join('');
336
+
337
+ const content = \`
338
+ <div class="damage-application-summary">
339
+ <div class="card-header">
340
+ <h3><i class="fas fa-sword-cross"></i> \${typeLabel} Applied</h3>
341
+ </div>
342
+
343
+ <div class="damage-summary">
344
+ <div class="base-damage">
345
+ <strong>Base Damage:</strong> \${summary.originalDamage}
346
+ \${summary.bonusDamage > 0 ? \`+ \${summary.bonusDamage} bonus\` : ''}
347
+ \${summary.multiplier !== 1 ? \` × \${summary.multiplier}\` : ''}
348
+ = <strong>\${summary.totalBaseDamage}</strong>
349
+ \${summary.damageType ? \`<br><strong>Type:</strong> <span class="damage-type" style="color: \${summary.damageColor || '#666'}">\${summary.damageIcon ? \`<i class="\${summary.damageIcon}"></i> \` : ''}\${summary.damageType}</span>\` : ''}
350
+ </div>
351
+ </div>
352
+
353
+ <div class="targets-summary">
354
+ <h4>Applied to Targets:</h4>
355
+ <table class="damage-targets">
356
+ <thead>
357
+ <tr>
358
+ <th>Target</th>
359
+ <th>Resistance</th>
360
+ <th>Final Damage</th>
361
+ <th>Revert</th>
362
+ </tr>
363
+ </thead>
364
+ <tbody>
365
+ \${targetRows}
366
+ </tbody>
367
+ </table>
368
+ </div>
369
+ </div>
370
+ \`;
371
+
372
+ await ChatMessage.create({
373
+ user: game.user.id,
374
+ content: content,
375
+ whisper: whisper,
376
+ flags: {
377
+ [game.system.id]: {
378
+ damageApplicationSummary: summary
379
+ }
380
+ }
381
+ });
382
+ }
383
+
384
+ if ( allowTargeting ) {
385
+ menuItems.push({
386
+ name: \`
387
+ <div class="damage-target flex flexrow">
388
+ <button type="button" data-target="targeted"><i class="fa-solid fa-bullseye"></i> \${game.i18n.localize('Targeted')}</button>
389
+ <button type="button" data-target="selected"><i class="fa-solid fa-expand"></i> \${game.i18n.localize('Selected')}</button>
390
+ </div>\`,
391
+ id: 'targets',
392
+ icon: '',
393
+ preventClose: true,
394
+ callback: (inlineRoll, event) => {
395
+ const button = event?.target ?? event?.currentTarget;
396
+ if (button?.dataset?.target) {
397
+ // Deactivate the other target type. The menu lives on <body>,
398
+ // so scope lookups to the menu element rather than the roll.
399
+ const menuEl = $('#context-menu2');
400
+ menuEl.find('button[data-target].active').removeClass('active');
401
+ // Set the target type on the menu for later reference.
402
+ const menu = menuEl[0];
403
+ if (menu) {
404
+ menu.dataset.target = button.dataset.target;
405
+ }
406
+ // Toggle the active button and update the user setting.
407
+ button.classList.add('active');
408
+ game.settings.set('${id}', 'userTargetDamageApplicationType', button.dataset.target);
409
+ }
410
+ }
411
+ });
412
+ }
413
+
414
+ // Add damage multipliers.
415
+ menuItems.push({
416
+ name: \`
417
+ <div class="damage-modifiers flex flexrow">
418
+ <button type="button" data-mod="0.25">&frac14;x</button>
419
+ <button type="button" data-mod="0.5">&frac12;x</button>
420
+ <button type="button" data-mod="1" class="active">1x</button>
421
+ <button type="button" data-mod="1.5">1.5x</button>
422
+ <button type="button" data-mod="2">2x</button>
423
+ <button type="button" data-mod="3">3x</button>
424
+ <button type="button" data-mod="4">4x</button>
425
+ </div>\`,
426
+ id: 'modifiers',
427
+ icon: '',
428
+ preventClose: true,
429
+ callback: (inlineRoll, event) => {
430
+ const button = event?.target ?? event?.currentTarget;
431
+ if (button?.dataset?.mod) {
432
+ // Deactivate the other multiplier. The menu lives on <body>, so
433
+ // scope lookups to the menu element rather than the roll.
434
+ const menuEl = $('#context-menu2');
435
+ menuEl.find('button[data-mod].active').removeClass('active');
436
+
437
+ // Set the multiplier on the menu for later reference.
438
+ const menu = menuEl[0];
439
+ if (menu) {
440
+ menu.dataset.mod = button.dataset.mod;
441
+ }
442
+
443
+ // Toggle the active button and update the user setting.
444
+ button.classList.add('active');
445
+ }
446
+ }
447
+ });
448
+
449
+ menuItems.push(
450
+ {
451
+ name: game.i18n.localize("CONTEXT.ApplyDamage"),
452
+ id: 'damage',
453
+ icon: '<i class="fas fa-tint"></i>',
454
+ callback: (inlineRoll, event) => apply(inlineRoll, event, 'damage')
455
+ },
456
+ {
457
+ name: game.i18n.localize("CONTEXT.ApplyHealing"),
458
+ id: 'healing',
459
+ icon: '<i class="fas fa-medkit"></i>',
460
+ callback: (inlineRoll, event) => apply(inlineRoll, event, 'healing')
461
+ },
462
+ {
463
+ name: game.i18n.localize("CONTEXT.ApplyTemp"),
464
+ id: 'temp-healing',
465
+ icon: '<i class="fas fa-heart"></i>',
466
+ callback: (inlineRoll, event) => apply(inlineRoll, event, 'temp')
467
+ }
468
+ );
469
+ new ContextMenu2($(this).parent(), \`[data-uuid=\${uuid}]\`, menuItems);
470
+ }
471
+ html.find('.inline-roll').each(applyMenus);
472
+ html.find('.dice-total').each(applyMenus);
473
+ }
474
+
475
+ /* -------------------------------------------- */
476
+
477
+ static _onChatCardToggleCollapsible(event) {
478
+ const target = event.currentTarget;
479
+
480
+ // If the target is a content-link, ignore the click event
481
+ if (event.target.classList.contains("content-link")) return;
482
+
483
+ event.preventDefault();
484
+ target.classList.toggle("collapsed");
485
+
486
+ // Clear the height from the chat popout container so that it appropriately resizes.
487
+ const popout = target.closest(".chat-popout");
488
+ if ( popout ) popout.style.height = "";
489
+ }
490
+
491
+ /* -------------------------------------------- */
492
+
493
+ static _handleActionClick(event) {
494
+ event.preventDefault();
495
+ const action = event.currentTarget.dataset.action;
496
+
497
+ switch (action) {
498
+ case "place":
499
+ const template = event.currentTarget.closest(".measured-template");
500
+ if (!template) return;
501
+
502
+ const context = {
503
+ type: template.dataset.type,
504
+ distance: template.dataset.distance,
505
+ direction: template.dataset.direction,
506
+ angle: template.dataset.angle,
507
+ width: template.dataset.width
508
+ };
509
+
510
+ // Trigger the place action on the template
511
+ game.system.measuredTemplatePreviewClass.place(context, game.user.character?.sheet);
512
+ break;
513
+ }
514
+ }
515
+
516
+ static async _onRevertTargetDamage(event) {
517
+ const button = event.currentTarget;
518
+ const summaryData = JSON.parse(button.dataset.summary);
519
+ const targetIndex = parseInt(button.dataset.targetIndex);
520
+ const targetData = summaryData.targets[targetIndex];
521
+
522
+ if (!targetData) {
523
+ ui.notifications.error("Target data not found");
524
+ return;
525
+ }
526
+
527
+ // Confirm revert
528
+ const confirm = await Dialog.confirm({
529
+ title: "Revert Target Damage",
530
+ content: \`<p>Are you sure you want to revert the \${summaryData.type} application to <strong>\${targetData.name}</strong>?</p><p>This will restore the actor to their previous state.</p>\`,
531
+ yes: () => true,
532
+ no: () => false
533
+ });
534
+
535
+ if (!confirm) return;
536
+
537
+ const tokenDocument = await fromUuid(targetData.uuid);
538
+ if (!tokenDocument || !tokenDocument.actor) {
539
+ ui.notifications.warn(\`Cannot find target: \${targetData.name}\`);
540
+ return;
541
+ }
542
+
543
+ try {
544
+ await tokenDocument.actor.update(targetData.preState);
545
+ console.log(\`Reverted \${targetData.name} to pre-application state\`);
546
+
547
+ // Update the button to show it's been reverted
548
+ button.disabled = true;
549
+ button.innerHTML = '<i class="fas fa-check"></i>';
550
+ button.classList.add('reverted');
551
+ button.title = 'Reverted';
552
+
553
+ // Mark the table row as reverted
554
+ const targetRow = button.closest('.target-row');
555
+ if (targetRow) {
556
+ targetRow.classList.add('reverted');
557
+ targetRow.style.opacity = '0.6';
558
+ targetRow.style.textDecoration = 'line-through';
559
+ }
560
+
561
+ ui.notifications.info(\`Damage application reverted for \${targetData.name}\`);
562
+ } catch (error) {
563
+ ui.notifications.error(\`Failed to revert \${targetData.name}: \${error.message}\`);
564
+ }
565
+ }
566
+ }
564
567
  `.appendNewLineIfNotEmpty();
565
568
  fs.writeFileSync(generatedFilePath, toString(fileNode));
566
569
  }
@@ -570,113 +573,113 @@ export function generateStandardChatCardTemplate(destination) {
570
573
  if (!fs.existsSync(generatedFileDir)) {
571
574
  fs.mkdirSync(generatedFileDir, { recursive: true });
572
575
  }
573
- const fileNode = expandToNode `
574
- <div class="{{cssClass}} standard-chat-card chat-card">
575
- {{#if hasDescription}}
576
- <div class="chat-header collapsible collapsed">
577
- <header class="flexrow">
578
- <img src="{{document.img}}" title="{{document.name}}" width="50" height="50">
579
- <div class="title">
580
- <div class="name">{{document.name}}</div>
581
- <div class="type">{{localize document.type}}</div>
582
- </div>
583
- <i class="collapse-icon fas fa-chevron-down fa-fw"></i>
584
- </header>
585
-
586
- <section class="description collapsible-content">
587
- {{{description}}}
588
- </section>
589
- </div>
590
- {{else}}
591
- <div class="chat-header">
592
- <header class="flexrow">
593
- <img src="{{document.img}}" title="{{document.name}}" width="50" height="50">
594
- <div class="title">
595
- <div class="name">{{document.name}}</div>
596
- <div class="type">{{localize document.type}}</div>
597
- </div>
598
- </header>
599
- </div>
600
- {{/if}}
601
- <div class="chat-info">
602
- <dl>
603
- {{#each parts}}
604
- {{#if this.isRoll}}
605
- {{#if this.isDamageRoll}}
606
- <div class="dice-roll damage-roll wide" data-damage-type="{{this.damageType}}">
607
- <div class="dice-result">
608
- <h4 class="dice-total">
609
- {{#if this.damageIcon}}<i class="{{this.damageIcon}}" style="color: {{this.damageColor}};"></i>{{else}}<i class="fa-solid fa-dice-d20"></i>{{/if}}
610
- <span class="dice-info" data-tooltip="{{this.value.cleanFormula}}">
611
- <span class="label">{{this.label}}:</span>
612
- <span class="formula">{{#if this.value._displayFormula}}{{this.value._displayFormula}}{{else}}{{this.value.cleanFormula}}{{/if}}</span>
613
- {{#if this.damageType}}<span class="damage-type" style="color: {{this.damageColor}};">[{{this.damageType}}]</span>{{/if}}
614
- </span>
615
- <span class="result">{{this.value._total}}</span>
616
- </h4>
617
- {{{this.tooltip}}}
618
- </div>
619
- </div>
620
- {{else}}
621
- <div class="dice-roll wide">
622
- <div class="dice-result">
623
- <h4 class="dice-total"><i class="fa-solid fa-dice-d20"></i> <span class="dice-info" data-tooltip="{{this.value.cleanFormula}}"><span class="label">{{this.label}}:</span> <span class="formula">{{#if this.value._displayFormula}}{{this.value._displayFormula}}{{else}}{{this.value.cleanFormula}}{{/if}}</span></span> <span class="result">{{this.value._total}}</span></h4>
624
- {{{this.tooltip}}}
625
- </div>
626
- </div>
627
- {{/if}}
628
- {{else if this.isMeasuredTemplate}}
629
- <div class="measured-template wide" data-type="{{this.object.type}}" data-distance="{{this.object.distance}}" data-direction="{{this.object.direction}}" data-angle="{{this.object.angle}}" data-width="{{this.object.width}}">
630
- <div class="measured-template-button">
631
- <h4 class="summary"><i class="fa-solid fa-ruler-combined"></i> <span class="info">{{this.value}}</span><span class="result action" data-action="place" data-tooltip="Place"><i class="fa-solid fa-border-outer"></i></span></h4>
632
- </div>
633
- </div>
634
- {{else if this.isParagraph}}
635
- <div class="wide">
636
- <dt>{{this.value}}</dt>
637
- <dd></dd>
638
- </div>
639
- {{else}}
640
- {{#if this.hasValue}}
641
- {{#if this.wide}}
642
- <div class="wide collapsible">
643
- <dt class="title">{{this.label}} <i class="collapse-icon fas fa-chevron-down fa-fw"></i></dt>
644
- <dd class="collapsible-content">{{{this.value}}}</dd>
645
- </div>
646
- {{else}}
647
- <dt>{{this.label}}</dt>
648
- <dd>{{{this.value}}}</dd>
649
- {{/if}}
650
- {{/if}}
651
- {{/if}}
652
- {{/each}}
653
- </dl>
654
-
655
- <div class="chat-info-tags">
656
- {{#each tags}}
657
- {{#if this.hasValue}}
658
- <div class="tag"><span class="label">{{this.label}}</span> {{this.value}}</div>
659
- {{/if}}
660
- {{/each}}
661
- </div>
662
- </div>
663
- {{#if hasEffects}}
664
- <div class="chat-effects collapsible">
665
- <h3 class="title">{{localize "EFFECTS.TabEffects"}} <i class="collapse-icon fas fa-chevron-down fa-fw"></i></h3>
666
- <div class="effects collapsible-content">
667
- {{#each document.effects}}
668
- <div class="effect" draggable="true" data-uuid="{{this.uuid}}">
669
- <header class="flexrow">
670
- <img src="{{this.img}}" title="{{this.name}}" width="30" height="30">
671
- <div class="name">{{this.name}}</div>
672
- </header>
673
- <div class="effect-content">{{{this.description}}}</div>
674
- </div>
675
- {{/each}}
676
- </div>
677
- </div>
678
- {{/if}}
679
- </div>
576
+ const fileNode = expandToNode `
577
+ <div class="{{cssClass}} standard-chat-card chat-card">
578
+ {{#if hasDescription}}
579
+ <div class="chat-header collapsible collapsed">
580
+ <header class="flexrow">
581
+ <img src="{{document.img}}" title="{{document.name}}" width="50" height="50">
582
+ <div class="title">
583
+ <div class="name">{{document.name}}</div>
584
+ <div class="type">{{localize document.type}}</div>
585
+ </div>
586
+ <i class="collapse-icon fas fa-chevron-down fa-fw"></i>
587
+ </header>
588
+
589
+ <section class="description collapsible-content">
590
+ {{{description}}}
591
+ </section>
592
+ </div>
593
+ {{else}}
594
+ <div class="chat-header">
595
+ <header class="flexrow">
596
+ <img src="{{document.img}}" title="{{document.name}}" width="50" height="50">
597
+ <div class="title">
598
+ <div class="name">{{document.name}}</div>
599
+ <div class="type">{{localize document.type}}</div>
600
+ </div>
601
+ </header>
602
+ </div>
603
+ {{/if}}
604
+ <div class="chat-info">
605
+ <dl>
606
+ {{#each parts}}
607
+ {{#if this.isRoll}}
608
+ {{#if this.isDamageRoll}}
609
+ <div class="dice-roll damage-roll wide" data-damage-type="{{this.damageType}}">
610
+ <div class="dice-result">
611
+ <h4 class="dice-total">
612
+ {{#if this.damageIcon}}<i class="{{this.damageIcon}}" style="color: {{this.damageColor}};"></i>{{else}}<i class="fa-solid fa-dice-d20"></i>{{/if}}
613
+ <span class="dice-info" data-tooltip="{{this.value.cleanFormula}}">
614
+ <span class="label">{{this.label}}:</span>
615
+ <span class="formula">{{#if this.value._displayFormula}}{{this.value._displayFormula}}{{else}}{{this.value.cleanFormula}}{{/if}}</span>
616
+ {{#if this.damageType}}<span class="damage-type" style="color: {{this.damageColor}};">[{{this.damageType}}]</span>{{/if}}
617
+ </span>
618
+ <span class="result">{{this.value._total}}</span>
619
+ </h4>
620
+ {{{this.tooltip}}}
621
+ </div>
622
+ </div>
623
+ {{else}}
624
+ <div class="dice-roll wide">
625
+ <div class="dice-result">
626
+ <h4 class="dice-total"><i class="fa-solid fa-dice-d20"></i> <span class="dice-info" data-tooltip="{{this.value.cleanFormula}}"><span class="label">{{this.label}}:</span> <span class="formula">{{#if this.value._displayFormula}}{{this.value._displayFormula}}{{else}}{{this.value.cleanFormula}}{{/if}}</span></span> <span class="result">{{this.value._total}}</span></h4>
627
+ {{{this.tooltip}}}
628
+ </div>
629
+ </div>
630
+ {{/if}}
631
+ {{else if this.isMeasuredTemplate}}
632
+ <div class="measured-template wide" data-type="{{this.object.type}}" data-distance="{{this.object.distance}}" data-direction="{{this.object.direction}}" data-angle="{{this.object.angle}}" data-width="{{this.object.width}}">
633
+ <div class="measured-template-button">
634
+ <h4 class="summary"><i class="fa-solid fa-ruler-combined"></i> <span class="info">{{this.value}}</span><span class="result action" data-action="place" data-tooltip="Place"><i class="fa-solid fa-border-outer"></i></span></h4>
635
+ </div>
636
+ </div>
637
+ {{else if this.isParagraph}}
638
+ <div class="wide">
639
+ <dt>{{this.value}}</dt>
640
+ <dd></dd>
641
+ </div>
642
+ {{else}}
643
+ {{#if this.hasValue}}
644
+ {{#if this.wide}}
645
+ <div class="wide collapsible">
646
+ <dt class="title">{{this.label}} <i class="collapse-icon fas fa-chevron-down fa-fw"></i></dt>
647
+ <dd class="collapsible-content">{{{this.value}}}</dd>
648
+ </div>
649
+ {{else}}
650
+ <dt>{{this.label}}</dt>
651
+ <dd>{{{this.value}}}</dd>
652
+ {{/if}}
653
+ {{/if}}
654
+ {{/if}}
655
+ {{/each}}
656
+ </dl>
657
+
658
+ <div class="chat-info-tags">
659
+ {{#each tags}}
660
+ {{#if this.hasValue}}
661
+ <div class="tag"><span class="label">{{this.label}}</span> {{this.value}}</div>
662
+ {{/if}}
663
+ {{/each}}
664
+ </div>
665
+ </div>
666
+ {{#if hasEffects}}
667
+ <div class="chat-effects collapsible">
668
+ <h3 class="title">{{localize "EFFECTS.TabEffects"}} <i class="collapse-icon fas fa-chevron-down fa-fw"></i></h3>
669
+ <div class="effects collapsible-content">
670
+ {{#each document.effects}}
671
+ <div class="effect" draggable="true" data-uuid="{{this.uuid}}">
672
+ <header class="flexrow">
673
+ <img src="{{this.img}}" title="{{this.name}}" width="30" height="30">
674
+ <div class="name">{{this.name}}</div>
675
+ </header>
676
+ <div class="effect-content">{{{this.description}}}</div>
677
+ </div>
678
+ {{/each}}
679
+ </div>
680
+ </div>
681
+ {{/if}}
682
+ </div>
680
683
  `.appendNewLineIfNotEmpty();
681
684
  fs.writeFileSync(generatedFilePath, toString(fileNode));
682
685
  }