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.
- package/.claude/agents/langium-language-designer.md +38 -38
- package/.claude/agents/typescript-vscode-expert.md +29 -29
- package/.claude/agents/ui-ux-designer.md +36 -36
- package/.claude/settings.local.json +33 -33
- package/.idea/inspectionProfiles/Project_Default.xml +6 -6
- package/.idea/isdl.iml +13 -13
- package/.idea/modules.xml +8 -8
- package/.idea/vcs.xml +6 -6
- package/.idea/watcherTasks.xml +3 -3
- package/.vscodeignore +18 -18
- package/LICENSE +673 -673
- package/README.md +86 -86
- package/bin/cli.js +4 -4
- package/bin/lsp.js +8 -8
- package/out/_backgrounds.scss +91 -91
- package/out/_handlebars.scss +497 -497
- package/out/_isdlStyles.scss +1444 -1381
- package/out/_vuetifyOverrides.scss +425 -425
- package/out/_vuetifyStyles.scss +31957 -31957
- package/out/cli/components/_backgrounds.scss +91 -91
- package/out/cli/components/_handlebars.scss +497 -497
- package/out/cli/components/_isdlStyles.scss +1444 -1381
- package/out/cli/components/_vuetifyOverrides.scss +425 -425
- package/out/cli/components/_vuetifyStyles.scss +31957 -31957
- package/out/cli/components/active-effect-sheet-generator.js +453 -453
- package/out/cli/components/chat-card-generator.js +654 -651
- package/out/cli/components/chat-card-generator.js.map +1 -1
- package/out/cli/components/css-generator.js +4 -4
- package/out/cli/components/damage-roll-generator.js +160 -160
- package/out/cli/components/datamodel-generator.js +264 -257
- package/out/cli/components/datamodel-generator.js.map +1 -1
- package/out/cli/components/derived-data-generator.js +923 -923
- package/out/cli/components/hotbar-drop-hook-generator.js +82 -82
- package/out/cli/components/init-hook-generator.js +495 -495
- package/out/cli/components/language-generator.js +1 -1
- package/out/cli/components/language-generator.js.map +1 -1
- package/out/cli/components/measured-template-preview.js +221 -221
- package/out/cli/components/method-generator.js +979 -887
- package/out/cli/components/method-generator.js.map +1 -1
- package/out/cli/components/ready-hook-generator.js +404 -404
- package/out/cli/components/token-generator.js +116 -116
- package/out/cli/components/vue/base-components/vue-attribute.js +138 -138
- package/out/cli/components/vue/base-components/vue-boolean.js +64 -64
- package/out/cli/components/vue/base-components/vue-calculator.js +93 -93
- package/out/cli/components/vue/base-components/vue-damage-application.js +356 -356
- package/out/cli/components/vue/base-components/vue-damage-bonuses.js +165 -165
- package/out/cli/components/vue/base-components/vue-damage-resistances.js +196 -196
- package/out/cli/components/vue/base-components/vue-damage-track.js +121 -121
- package/out/cli/components/vue/base-components/vue-date-time.js +42 -42
- package/out/cli/components/vue/base-components/vue-dice.js +98 -98
- package/out/cli/components/vue/base-components/vue-die.js +73 -73
- package/out/cli/components/vue/base-components/vue-document-choice.js +149 -149
- package/out/cli/components/vue/base-components/vue-document-choices.js +179 -179
- package/out/cli/components/vue/base-components/vue-document-link.js +60 -60
- package/out/cli/components/vue/base-components/vue-extended-choice.js +88 -88
- package/out/cli/components/vue/base-components/vue-inventory.js +519 -519
- package/out/cli/components/vue/base-components/vue-macro-choice.js +138 -138
- package/out/cli/components/vue/base-components/vue-measured-template.js +530 -530
- package/out/cli/components/vue/base-components/vue-money.js +483 -483
- package/out/cli/components/vue/base-components/vue-number.js +174 -174
- package/out/cli/components/vue/base-components/vue-paperdoll.js +43 -43
- package/out/cli/components/vue/base-components/vue-parent-property-reference.js +76 -76
- package/out/cli/components/vue/base-components/vue-prosemirror.js +18 -18
- package/out/cli/components/vue/base-components/vue-resource.js +136 -136
- package/out/cli/components/vue/base-components/vue-roll-visualizer.js +286 -109
- package/out/cli/components/vue/base-components/vue-roll-visualizer.js.map +1 -1
- package/out/cli/components/vue/base-components/vue-self-property-reference.js +62 -62
- package/out/cli/components/vue/base-components/vue-string-choice.js +98 -98
- package/out/cli/components/vue/base-components/vue-string-choices.js +203 -203
- package/out/cli/components/vue/base-components/vue-string.js +60 -60
- package/out/cli/components/vue/base-components/vue-text-field.js +53 -53
- package/out/cli/components/vue/base-components/vue-tracker.js +431 -431
- package/out/cli/components/vue/vue-action-component-generator.js +64 -64
- package/out/cli/components/vue/vue-active-effect-sheet-generator.js +856 -856
- package/out/cli/components/vue/vue-datatable-sheet-class-generator.js +292 -292
- package/out/cli/components/vue/vue-datatable2-component-generator.js +824 -824
- package/out/cli/components/vue/vue-document-creation-app.js +121 -121
- package/out/cli/components/vue/vue-document-creation-sheet.js +94 -94
- package/out/cli/components/vue/vue-generator.js +40 -40
- package/out/cli/components/vue/vue-mixin.js +296 -296
- package/out/cli/components/vue/vue-pinned-datatable-component-generator.js +260 -260
- package/out/cli/components/vue/vue-prompt-generator.js +91 -76
- package/out/cli/components/vue/vue-prompt-generator.js.map +1 -1
- package/out/cli/components/vue/vue-prompt-sheet-class-generator.js +317 -317
- package/out/cli/components/vue/vue-sheet-application-generator.js +1177 -1167
- package/out/cli/components/vue/vue-sheet-application-generator.js.map +1 -1
- package/out/cli/components/vue/vue-sheet-class-generator.js +510 -510
- package/out/cli/generator.js +438 -433
- package/out/cli/generator.js.map +1 -1
- package/out/extension/github/githubAuthProvider.js +71 -29
- package/out/extension/github/githubAuthProvider.js.map +1 -1
- package/out/extension/github/githubGistManager.js +4 -3
- package/out/extension/github/githubGistManager.js.map +1 -1
- package/out/extension/github/githubManager.js +40 -38
- package/out/extension/github/githubManager.js.map +1 -1
- package/out/extension/github/githubQuickActions.js +120 -120
- package/out/extension/github/system-workflow.yml +47 -47
- package/out/extension/main.cjs +909 -532
- package/out/extension/main.cjs.map +3 -3
- package/out/extension/package.json +419 -419
- package/out/language/generated/ast.js +51 -2
- package/out/language/generated/ast.js.map +1 -1
- package/out/language/generated/grammar.js +14240 -13991
- package/out/language/generated/grammar.js.map +1 -1
- package/out/language/intelligent-system-design-language-validator.js +32 -2
- package/out/language/intelligent-system-design-language-validator.js.map +1 -1
- package/out/language/isdl-scope-provider.js +14 -1
- package/out/language/isdl-scope-provider.js.map +1 -1
- package/out/language/main.cjs +913 -569
- package/out/language/main.cjs.map +3 -3
- package/out/package.json +419 -419
- package/out/progressbar.min.js +6 -6
- package/out/styles.scss +762 -747
- package/out/test/validating/diagnostics.test.js +40 -0
- package/out/test/validating/diagnostics.test.js.map +1 -1
- package/package.json +419 -419
|
@@ -7,525 +7,525 @@ export default function generateInventoryComponent(destination, entry) {
|
|
|
7
7
|
if (!fs.existsSync(generatedFileDir)) {
|
|
8
8
|
fs.mkdirSync(generatedFileDir, { recursive: true });
|
|
9
9
|
}
|
|
10
|
-
const fileNode = expandToNode `
|
|
11
|
-
<script setup>
|
|
12
|
-
import { ref, computed, inject, onMounted, onUnmounted } from "vue";
|
|
13
|
-
|
|
14
|
-
const props = defineProps({
|
|
15
|
-
label: String,
|
|
16
|
-
systemPath: String,
|
|
17
|
-
context: Object,
|
|
18
|
-
editMode: Boolean,
|
|
19
|
-
icon: String,
|
|
20
|
-
color: String,
|
|
21
|
-
disabled: Boolean,
|
|
22
|
-
maxSlots: {
|
|
23
|
-
type: Number,
|
|
24
|
-
default: 20
|
|
25
|
-
},
|
|
26
|
-
columns: {
|
|
27
|
-
type: Number,
|
|
28
|
-
default: 5
|
|
29
|
-
},
|
|
30
|
-
rows: {
|
|
31
|
-
type: Number,
|
|
32
|
-
default: 3
|
|
33
|
-
},
|
|
34
|
-
slotSize: {
|
|
35
|
-
type: Number,
|
|
36
|
-
default: 60
|
|
37
|
-
},
|
|
38
|
-
documentType: String,
|
|
39
|
-
whereExpression: String,
|
|
40
|
-
globalAllowed: Boolean,
|
|
41
|
-
quantityField: String,
|
|
42
|
-
moneyField: String,
|
|
43
|
-
moneyFieldLabel: String,
|
|
44
|
-
moneyFieldIcon: String,
|
|
45
|
-
sumProperties: Array,
|
|
46
|
-
sumMax: Array,
|
|
47
|
-
sortProperty: String,
|
|
48
|
-
sortOrder: {
|
|
49
|
-
type: String,
|
|
50
|
-
default: 'asc'
|
|
51
|
-
},
|
|
52
|
-
emptySlots: {
|
|
53
|
-
type: String,
|
|
54
|
-
default: 'show'
|
|
55
|
-
},
|
|
56
|
-
summary: {
|
|
57
|
-
type: String,
|
|
58
|
-
default: 'full'
|
|
59
|
-
},
|
|
60
|
-
primaryColor: String,
|
|
61
|
-
secondaryColor: String,
|
|
62
|
-
tertiaryColor: String
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
const document = inject('rawDocument');
|
|
66
|
-
|
|
67
|
-
// Calculate grid configuration
|
|
68
|
-
const gridConfig = computed(() => {
|
|
69
|
-
const columns = props.columns || 5;
|
|
70
|
-
const rows = props.rows || 3;
|
|
71
|
-
|
|
72
|
-
// Calculate total slots from columns × rows
|
|
73
|
-
let totalSlots = columns * rows;
|
|
74
|
-
|
|
75
|
-
// Cap at maxSlots if specified
|
|
76
|
-
if (props.maxSlots) {
|
|
77
|
-
totalSlots = Math.min(totalSlots, props.maxSlots);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return { totalSlots, columns };
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
// Force update trigger for reactive updates
|
|
84
|
-
const updateKey = ref(0);
|
|
85
|
-
const forceUpdate = () => {
|
|
86
|
-
updateKey.value++;
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
// Get filtered items
|
|
90
|
-
const filteredItems = computed(() => {
|
|
91
|
-
// Depend on updateKey to trigger re-evaluation
|
|
92
|
-
const _ = updateKey.value;
|
|
93
|
-
let items = Array.from(document.items || []);
|
|
94
|
-
|
|
95
|
-
// Filter by document type
|
|
96
|
-
items = items.filter(item => item.type === props.documentType);
|
|
97
|
-
|
|
98
|
-
// Apply where expression if provided
|
|
99
|
-
if (props.whereExpression) {
|
|
100
|
-
try {
|
|
101
|
-
const filterFunc = new Function('item', 'system', \`return \${props.whereExpression}\`);
|
|
102
|
-
items = items.filter(item => filterFunc(item, props.context.system));
|
|
103
|
-
} catch (e) {
|
|
104
|
-
console.error('Error filtering inventory items:', e);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Add global items if allowed
|
|
109
|
-
if (props.globalAllowed) {
|
|
110
|
-
const gameItems = game.items.filter(item => item.type === props.documentType);
|
|
111
|
-
items = items.concat(gameItems);
|
|
112
|
-
|
|
113
|
-
const itemPacks = game.packs.filter(pack => pack.documentName === 'Item');
|
|
114
|
-
for (let pack of itemPacks) {
|
|
115
|
-
const packItems = pack.index.contents.filter(item => item.type === props.documentType);
|
|
116
|
-
packItems.forEach(item => {
|
|
117
|
-
item.compendium = pack;
|
|
118
|
-
});
|
|
119
|
-
items = items.concat(packItems);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Apply sorting if specified
|
|
124
|
-
if (props.sortProperty) {
|
|
125
|
-
items.sort((a, b) => {
|
|
126
|
-
const aProp = foundry.utils.getProperty(a, \`system.\${props.sortProperty.toLowerCase()}\`);
|
|
127
|
-
const bProp = foundry.utils.getProperty(b, \`system.\${props.sortProperty.toLowerCase()}\`);
|
|
128
|
-
|
|
129
|
-
let aVal = aProp;
|
|
130
|
-
let bVal = bProp;
|
|
131
|
-
|
|
132
|
-
// Handle nested values (like resource.value)
|
|
133
|
-
if (typeof aProp === 'object' && aProp?.value !== undefined) aVal = aProp.value;
|
|
134
|
-
if (typeof bProp === 'object' && bProp?.value !== undefined) bVal = bProp.value;
|
|
135
|
-
|
|
136
|
-
if (aVal < bVal) return props.sortOrder === 'asc' ? -1 : 1;
|
|
137
|
-
if (aVal > bVal) return props.sortOrder === 'asc' ? 1 : -1;
|
|
138
|
-
return 0;
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return items;
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
// Generate slots for display
|
|
146
|
-
const inventorySlots = computed(() => {
|
|
147
|
-
const slots = [];
|
|
148
|
-
const config = gridConfig.value;
|
|
149
|
-
let firstEmptyFound = false;
|
|
150
|
-
|
|
151
|
-
for (let i = 0; i < config.totalSlots; i++) {
|
|
152
|
-
if (i < filteredItems.value.length) {
|
|
153
|
-
const item = filteredItems.value[i];
|
|
154
|
-
let quantity = null;
|
|
155
|
-
|
|
156
|
-
if (props.quantityField) {
|
|
157
|
-
const quantityValue = foundry.utils.getProperty(item, \`system.\${props.quantityField.toLowerCase()}\`);
|
|
158
|
-
quantity = typeof quantityValue === 'object' ? quantityValue.value : quantityValue;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
slots.push({
|
|
162
|
-
item: item,
|
|
163
|
-
quantity: quantity,
|
|
164
|
-
empty: false,
|
|
165
|
-
isCreateSlot: false
|
|
166
|
-
});
|
|
167
|
-
} else if (props.emptySlots === 'show') {
|
|
168
|
-
const isCreateSlot = !firstEmptyFound;
|
|
169
|
-
if (isCreateSlot) firstEmptyFound = true;
|
|
170
|
-
|
|
171
|
-
slots.push({
|
|
172
|
-
item: null,
|
|
173
|
-
quantity: null,
|
|
174
|
-
empty: true,
|
|
175
|
-
isCreateSlot: isCreateSlot
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return slots;
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
// Calculate aggregations
|
|
184
|
-
const aggregations = computed(() => {
|
|
185
|
-
if (!props.sumProperties || props.sumProperties.length === 0) {
|
|
186
|
-
return [];
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
return props.sumProperties.map((propName, index) => {
|
|
190
|
-
let total = 0;
|
|
191
|
-
|
|
192
|
-
filteredItems.value.forEach(item => {
|
|
193
|
-
const propValue = foundry.utils.getProperty(item, \`system.\${propName.toLowerCase()}\`);
|
|
194
|
-
|
|
195
|
-
if (typeof propValue === 'number') {
|
|
196
|
-
total += propValue;
|
|
197
|
-
} else if (propValue?.value !== undefined) {
|
|
198
|
-
total += propValue.value;
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
const max = props.sumMax && props.sumMax[index] ? props.sumMax[index] : null;
|
|
203
|
-
|
|
204
|
-
return {
|
|
205
|
-
name: propName,
|
|
206
|
-
label: game.i18n.localize(propName),
|
|
207
|
-
value: total,
|
|
208
|
-
formatted: total.toLocaleString(),
|
|
209
|
-
max: max,
|
|
210
|
-
percentage: max ? Math.min(100, (total / max) * 100) : null
|
|
211
|
-
};
|
|
212
|
-
});
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
// Get money value
|
|
216
|
-
const moneyValue = computed(() => {
|
|
217
|
-
// Depend on updateKey to trigger re-evaluation
|
|
218
|
-
const _ = updateKey.value;
|
|
219
|
-
if (!props.moneyField) return null;
|
|
220
|
-
return foundry.utils.getProperty(document, \`system.\${props.moneyField.toLowerCase()}\`);
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
// Format money display
|
|
224
|
-
const formattedMoney = computed(() => {
|
|
225
|
-
if (!moneyValue.value) return '0';
|
|
226
|
-
|
|
227
|
-
if (typeof moneyValue.value === 'number') {
|
|
228
|
-
return moneyValue.value.toLocaleString();
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Multi-denomination money
|
|
232
|
-
if (typeof moneyValue.value === 'object') {
|
|
233
|
-
const parts = [];
|
|
234
|
-
for (const [denom, amount] of Object.entries(moneyValue.value)) {
|
|
235
|
-
if (amount > 0) {
|
|
236
|
-
parts.push(\`\${amount}\${denom.charAt(0)}\`);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
return parts.length > 0 ? parts.join(' ') : '0';
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
return '0';
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
// Get capacity color for a percentage
|
|
246
|
-
const getCapacityColor = (percentage) => {
|
|
247
|
-
if (!percentage) return 'success';
|
|
248
|
-
if (percentage > 100) return 'error';
|
|
249
|
-
if (percentage >= 90) return 'error';
|
|
250
|
-
if (percentage >= 70) return 'warning';
|
|
251
|
-
return 'success';
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
// Handle item click
|
|
255
|
-
const onItemClick = (slot) => {
|
|
256
|
-
if (slot.empty) {
|
|
257
|
-
if (slot.isCreateSlot) {
|
|
258
|
-
onCreateItem();
|
|
259
|
-
}
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
if (slot.item) {
|
|
263
|
-
slot.item.sheet.render(true);
|
|
264
|
-
}
|
|
265
|
-
};
|
|
266
|
-
|
|
267
|
-
// Handle create new item
|
|
268
|
-
const onCreateItem = async () => {
|
|
269
|
-
if (!props.documentType) return;
|
|
270
|
-
|
|
271
|
-
const itemData = {
|
|
272
|
-
name: game.i18n.localize(\`New \${props.documentType}\`),
|
|
273
|
-
type: props.documentType
|
|
274
|
-
};
|
|
275
|
-
|
|
276
|
-
const created = await document.createEmbeddedDocuments('Item', [itemData]);
|
|
277
|
-
if (created && created[0]) {
|
|
278
|
-
created[0].sheet.render(true);
|
|
279
|
-
}
|
|
280
|
-
};
|
|
281
|
-
|
|
282
|
-
// Handle delete item
|
|
283
|
-
const onDeleteItem = async (event, slot) => {
|
|
284
|
-
// Prevent the click from opening the item sheet
|
|
285
|
-
event.stopPropagation();
|
|
286
|
-
|
|
287
|
-
if (!slot.item) return;
|
|
288
|
-
|
|
289
|
-
const confirmed = await Dialog.confirm({
|
|
290
|
-
title: game.i18n.localize('Delete Item'),
|
|
291
|
-
content: \`<p>\${game.i18n.format('Are you sure you want to delete {name}?', { name: slot.item.name })}</p>\`,
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
if (confirmed) {
|
|
295
|
-
await slot.item.delete();
|
|
296
|
-
}
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
// Handle drag start
|
|
300
|
-
const onDragStart = (event, slot) => {
|
|
301
|
-
if (!slot.item) return;
|
|
302
|
-
|
|
303
|
-
const dragData = {
|
|
304
|
-
type: 'Item',
|
|
305
|
-
uuid: slot.item.uuid
|
|
306
|
-
};
|
|
307
|
-
|
|
308
|
-
event.dataTransfer.setData('text/plain', JSON.stringify(dragData));
|
|
309
|
-
|
|
310
|
-
// Add dragging class for visual feedback
|
|
311
|
-
event.target.classList.add('dragging');
|
|
312
|
-
};
|
|
313
|
-
|
|
314
|
-
// Handle drag end
|
|
315
|
-
const onDragEnd = (event) => {
|
|
316
|
-
// Remove dragging class
|
|
317
|
-
event.target.classList.remove('dragging');
|
|
318
|
-
};
|
|
319
|
-
|
|
320
|
-
// Get item name for tooltip
|
|
321
|
-
const getItemName = (slot) => {
|
|
322
|
-
if (slot.empty || !slot.item) return '';
|
|
323
|
-
return slot.item.name;
|
|
324
|
-
};
|
|
325
|
-
|
|
326
|
-
// Get item description for tooltip
|
|
327
|
-
const getItemDescription = (slot) => {
|
|
328
|
-
if (slot.empty || !slot.item) return '';
|
|
329
|
-
|
|
330
|
-
// Get description - handle both value and direct description
|
|
331
|
-
const description = slot.item.system?.description?.value || slot.item.system?.description;
|
|
332
|
-
if (!description) return '';
|
|
333
|
-
|
|
334
|
-
// Truncate if too long
|
|
335
|
-
if (description.length > 200) {
|
|
336
|
-
return description.substring(0, 200) + '...';
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
return description;
|
|
340
|
-
};
|
|
341
|
-
|
|
342
|
-
// Get summed property values for tooltip
|
|
343
|
-
const getItemSumProperties = (slot) => {
|
|
344
|
-
if (slot.empty || !slot.item || !props.sumProperties || props.sumProperties.length === 0) {
|
|
345
|
-
return [];
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
return props.sumProperties.map(propName => {
|
|
349
|
-
const propValue = foundry.utils.getProperty(slot.item, \`system.\${propName.toLowerCase()}\`);
|
|
350
|
-
|
|
351
|
-
let value = 0;
|
|
352
|
-
if (typeof propValue === 'number') {
|
|
353
|
-
value = propValue;
|
|
354
|
-
} else if (propValue?.value !== undefined) {
|
|
355
|
-
value = propValue.value;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
return {
|
|
359
|
-
name: propName,
|
|
360
|
-
label: game.i18n.localize(propName),
|
|
361
|
-
value: value,
|
|
362
|
-
formatted: value.toLocaleString()
|
|
363
|
-
};
|
|
364
|
-
}).filter(prop => prop.value > 0); // Only show properties with values
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
// Count items
|
|
368
|
-
const itemCount = computed(() => {
|
|
369
|
-
return filteredItems.value.length;
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
// Show summary based on mode
|
|
373
|
-
const showCount = computed(() => props.summary !== 'minimal');
|
|
374
|
-
const showAggregations = computed(() => props.summary === 'full');
|
|
375
|
-
const showMoney = computed(() => props.summary === 'full' && props.moneyField);
|
|
376
|
-
|
|
377
|
-
// Subscribe to item changes
|
|
378
|
-
const onItemChange = (item, options, userId) => {
|
|
379
|
-
// Check if this item belongs to our document
|
|
380
|
-
if (item.parent?.uuid === document.uuid) {
|
|
381
|
-
forceUpdate();
|
|
382
|
-
}
|
|
383
|
-
};
|
|
384
|
-
|
|
385
|
-
onMounted(() => {
|
|
386
|
-
Hooks.on('createItem', onItemChange);
|
|
387
|
-
Hooks.on('updateItem', onItemChange);
|
|
388
|
-
Hooks.on('deleteItem', onItemChange);
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
onUnmounted(() => {
|
|
392
|
-
Hooks.off('createItem', onItemChange);
|
|
393
|
-
Hooks.off('updateItem', onItemChange);
|
|
394
|
-
Hooks.off('deleteItem', onItemChange);
|
|
395
|
-
});
|
|
396
|
-
</script>
|
|
397
|
-
|
|
398
|
-
<template>
|
|
399
|
-
<div class="isdl-inventory" :key="updateKey">
|
|
400
|
-
<!-- Header -->
|
|
401
|
-
<div class="inventory-header" v-if="showCount">
|
|
402
|
-
<div class="header-content">
|
|
403
|
-
<v-icon v-if="icon" :icon="icon" size="small"></v-icon>
|
|
404
|
-
<span class="inventory-label">{{ game.i18n.localize(label) }}</span>
|
|
405
|
-
</div>
|
|
406
|
-
<span class="inventory-count">
|
|
407
|
-
{{ itemCount }}/{{ gridConfig.totalSlots }}
|
|
408
|
-
</span>
|
|
409
|
-
</div>
|
|
410
|
-
|
|
411
|
-
<!-- Grid Container -->
|
|
412
|
-
<div class="inventory-grid-container">
|
|
413
|
-
<div
|
|
414
|
-
class="inventory-grid"
|
|
415
|
-
:style="{
|
|
416
|
-
gridTemplateColumns: \`repeat(\${gridConfig.columns}, \${slotSize}px)\`,
|
|
417
|
-
}"
|
|
418
|
-
>
|
|
419
|
-
<v-tooltip
|
|
420
|
-
v-for="(slot, index) in inventorySlots"
|
|
421
|
-
:key="index"
|
|
422
|
-
:disabled="slot.empty && !slot.isCreateSlot"
|
|
423
|
-
location="top"
|
|
424
|
-
max-width="400"
|
|
425
|
-
>
|
|
426
|
-
<template v-slot:activator="{ props: tooltipProps }">
|
|
427
|
-
<div
|
|
428
|
-
v-bind="tooltipProps"
|
|
429
|
-
class="inventory-slot"
|
|
430
|
-
:class="{
|
|
431
|
-
'empty': slot.empty,
|
|
432
|
-
'filled': !slot.empty,
|
|
433
|
-
'create-slot': slot.isCreateSlot
|
|
434
|
-
}"
|
|
435
|
-
:style="{
|
|
436
|
-
width: \`\${slotSize}px\`,
|
|
437
|
-
height: \`\${slotSize}px\`
|
|
438
|
-
}"
|
|
439
|
-
:draggable="!slot.empty && !!slot.item"
|
|
440
|
-
@click="onItemClick(slot)"
|
|
441
|
-
@dragstart="onDragStart($event, slot)"
|
|
442
|
-
@dragend="onDragEnd($event)"
|
|
443
|
-
>
|
|
444
|
-
<img
|
|
445
|
-
v-if="!slot.empty && slot.item"
|
|
446
|
-
:src="slot.item.img"
|
|
447
|
-
:alt="slot.item.name"
|
|
448
|
-
class="slot-image"
|
|
449
|
-
/>
|
|
450
|
-
<v-icon
|
|
451
|
-
v-if="slot.isCreateSlot"
|
|
452
|
-
icon="fa-solid fa-plus"
|
|
453
|
-
size="large"
|
|
454
|
-
class="create-icon"
|
|
455
|
-
></v-icon>
|
|
456
|
-
<div
|
|
457
|
-
v-if="!slot.empty && slot.item"
|
|
458
|
-
class="delete-button"
|
|
459
|
-
@click="onDeleteItem($event, slot)"
|
|
460
|
-
:data-tooltip="game.i18n.localize('Delete')"
|
|
461
|
-
>
|
|
462
|
-
<v-icon icon="fa-solid fa-times" size="x-small"></v-icon>
|
|
463
|
-
</div>
|
|
464
|
-
<div
|
|
465
|
-
v-if="!slot.empty && slot.quantity && slot.quantity > 1"
|
|
466
|
-
class="quantity-badge"
|
|
467
|
-
>
|
|
468
|
-
{{ slot.quantity }}
|
|
469
|
-
</div>
|
|
470
|
-
</div>
|
|
471
|
-
</template>
|
|
472
|
-
<template v-slot:default>
|
|
473
|
-
<div v-if="slot.isCreateSlot" class="inventory-tooltip">
|
|
474
|
-
<div class="tooltip-title">{{ game.i18n.localize('Create New Item') }}</div>
|
|
475
|
-
</div>
|
|
476
|
-
<div v-else class="inventory-tooltip">
|
|
477
|
-
<div class="tooltip-title">{{ getItemName(slot) }}</div>
|
|
478
|
-
<div v-if="getItemDescription(slot)" class="tooltip-description" v-html="getItemDescription(slot)"></div>
|
|
479
|
-
<div v-if="getItemSumProperties(slot).length > 0" class="tooltip-properties">
|
|
480
|
-
<div v-for="prop in getItemSumProperties(slot)" :key="prop.name" class="tooltip-property">
|
|
481
|
-
<span class="property-label">{{ prop.label }}:</span>
|
|
482
|
-
<span class="property-value">{{ prop.formatted }}</span>
|
|
483
|
-
</div>
|
|
484
|
-
</div>
|
|
485
|
-
</div>
|
|
486
|
-
</template>
|
|
487
|
-
</v-tooltip>
|
|
488
|
-
</div>
|
|
489
|
-
</div>
|
|
490
|
-
|
|
491
|
-
<!-- Footer -->
|
|
492
|
-
<div class="inventory-footer" v-if="showAggregations || showMoney">
|
|
493
|
-
<v-divider class="footer-divider"></v-divider>
|
|
494
|
-
|
|
495
|
-
<!-- Money Display -->
|
|
496
|
-
<div v-if="showMoney" class="inventory-stat money-display">
|
|
497
|
-
<div class="stat-header">
|
|
498
|
-
<v-icon v-if="moneyFieldIcon" :icon="moneyFieldIcon" size="small"></v-icon>
|
|
499
|
-
<span class="stat-label">{{ game.i18n.localize(moneyFieldLabel || 'Currency') }}</span>
|
|
500
|
-
</div>
|
|
501
|
-
<span class="stat-value">{{ formattedMoney }}</span>
|
|
502
|
-
</div>
|
|
503
|
-
|
|
504
|
-
<!-- Aggregations -->
|
|
505
|
-
<div v-if="showAggregations" v-for="agg in aggregations" :key="agg.name" class="inventory-stat aggregation-display">
|
|
506
|
-
<div class="stat-header">
|
|
507
|
-
<v-icon icon="fa-solid fa-calculator" size="small"></v-icon>
|
|
508
|
-
<span class="stat-label">{{ agg.label }}</span>
|
|
509
|
-
</div>
|
|
510
|
-
<div class="stat-value-container">
|
|
511
|
-
<span class="stat-value">
|
|
512
|
-
{{ agg.formatted }}
|
|
513
|
-
<span v-if="agg.max" class="stat-max"> / {{ agg.max }}</span>
|
|
514
|
-
</span>
|
|
515
|
-
<!-- Progress bar for aggregations with max -->
|
|
516
|
-
<v-progress-linear
|
|
517
|
-
v-if="agg.max"
|
|
518
|
-
:model-value="agg.percentage"
|
|
519
|
-
:color="getCapacityColor(agg.percentage)"
|
|
520
|
-
height="6"
|
|
521
|
-
rounded
|
|
522
|
-
class="capacity-progress"
|
|
523
|
-
></v-progress-linear>
|
|
524
|
-
</div>
|
|
525
|
-
</div>
|
|
526
|
-
</div>
|
|
527
|
-
</div>
|
|
528
|
-
</template>
|
|
10
|
+
const fileNode = expandToNode `
|
|
11
|
+
<script setup>
|
|
12
|
+
import { ref, computed, inject, onMounted, onUnmounted } from "vue";
|
|
13
|
+
|
|
14
|
+
const props = defineProps({
|
|
15
|
+
label: String,
|
|
16
|
+
systemPath: String,
|
|
17
|
+
context: Object,
|
|
18
|
+
editMode: Boolean,
|
|
19
|
+
icon: String,
|
|
20
|
+
color: String,
|
|
21
|
+
disabled: Boolean,
|
|
22
|
+
maxSlots: {
|
|
23
|
+
type: Number,
|
|
24
|
+
default: 20
|
|
25
|
+
},
|
|
26
|
+
columns: {
|
|
27
|
+
type: Number,
|
|
28
|
+
default: 5
|
|
29
|
+
},
|
|
30
|
+
rows: {
|
|
31
|
+
type: Number,
|
|
32
|
+
default: 3
|
|
33
|
+
},
|
|
34
|
+
slotSize: {
|
|
35
|
+
type: Number,
|
|
36
|
+
default: 60
|
|
37
|
+
},
|
|
38
|
+
documentType: String,
|
|
39
|
+
whereExpression: String,
|
|
40
|
+
globalAllowed: Boolean,
|
|
41
|
+
quantityField: String,
|
|
42
|
+
moneyField: String,
|
|
43
|
+
moneyFieldLabel: String,
|
|
44
|
+
moneyFieldIcon: String,
|
|
45
|
+
sumProperties: Array,
|
|
46
|
+
sumMax: Array,
|
|
47
|
+
sortProperty: String,
|
|
48
|
+
sortOrder: {
|
|
49
|
+
type: String,
|
|
50
|
+
default: 'asc'
|
|
51
|
+
},
|
|
52
|
+
emptySlots: {
|
|
53
|
+
type: String,
|
|
54
|
+
default: 'show'
|
|
55
|
+
},
|
|
56
|
+
summary: {
|
|
57
|
+
type: String,
|
|
58
|
+
default: 'full'
|
|
59
|
+
},
|
|
60
|
+
primaryColor: String,
|
|
61
|
+
secondaryColor: String,
|
|
62
|
+
tertiaryColor: String
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const document = inject('rawDocument');
|
|
66
|
+
|
|
67
|
+
// Calculate grid configuration
|
|
68
|
+
const gridConfig = computed(() => {
|
|
69
|
+
const columns = props.columns || 5;
|
|
70
|
+
const rows = props.rows || 3;
|
|
71
|
+
|
|
72
|
+
// Calculate total slots from columns × rows
|
|
73
|
+
let totalSlots = columns * rows;
|
|
74
|
+
|
|
75
|
+
// Cap at maxSlots if specified
|
|
76
|
+
if (props.maxSlots) {
|
|
77
|
+
totalSlots = Math.min(totalSlots, props.maxSlots);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { totalSlots, columns };
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Force update trigger for reactive updates
|
|
84
|
+
const updateKey = ref(0);
|
|
85
|
+
const forceUpdate = () => {
|
|
86
|
+
updateKey.value++;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Get filtered items
|
|
90
|
+
const filteredItems = computed(() => {
|
|
91
|
+
// Depend on updateKey to trigger re-evaluation
|
|
92
|
+
const _ = updateKey.value;
|
|
93
|
+
let items = Array.from(document.items || []);
|
|
94
|
+
|
|
95
|
+
// Filter by document type
|
|
96
|
+
items = items.filter(item => item.type === props.documentType);
|
|
97
|
+
|
|
98
|
+
// Apply where expression if provided
|
|
99
|
+
if (props.whereExpression) {
|
|
100
|
+
try {
|
|
101
|
+
const filterFunc = new Function('item', 'system', \`return \${props.whereExpression}\`);
|
|
102
|
+
items = items.filter(item => filterFunc(item, props.context.system));
|
|
103
|
+
} catch (e) {
|
|
104
|
+
console.error('Error filtering inventory items:', e);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Add global items if allowed
|
|
109
|
+
if (props.globalAllowed) {
|
|
110
|
+
const gameItems = game.items.filter(item => item.type === props.documentType);
|
|
111
|
+
items = items.concat(gameItems);
|
|
112
|
+
|
|
113
|
+
const itemPacks = game.packs.filter(pack => pack.documentName === 'Item');
|
|
114
|
+
for (let pack of itemPacks) {
|
|
115
|
+
const packItems = pack.index.contents.filter(item => item.type === props.documentType);
|
|
116
|
+
packItems.forEach(item => {
|
|
117
|
+
item.compendium = pack;
|
|
118
|
+
});
|
|
119
|
+
items = items.concat(packItems);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Apply sorting if specified
|
|
124
|
+
if (props.sortProperty) {
|
|
125
|
+
items.sort((a, b) => {
|
|
126
|
+
const aProp = foundry.utils.getProperty(a, \`system.\${props.sortProperty.toLowerCase()}\`);
|
|
127
|
+
const bProp = foundry.utils.getProperty(b, \`system.\${props.sortProperty.toLowerCase()}\`);
|
|
128
|
+
|
|
129
|
+
let aVal = aProp;
|
|
130
|
+
let bVal = bProp;
|
|
131
|
+
|
|
132
|
+
// Handle nested values (like resource.value)
|
|
133
|
+
if (typeof aProp === 'object' && aProp?.value !== undefined) aVal = aProp.value;
|
|
134
|
+
if (typeof bProp === 'object' && bProp?.value !== undefined) bVal = bProp.value;
|
|
135
|
+
|
|
136
|
+
if (aVal < bVal) return props.sortOrder === 'asc' ? -1 : 1;
|
|
137
|
+
if (aVal > bVal) return props.sortOrder === 'asc' ? 1 : -1;
|
|
138
|
+
return 0;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return items;
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Generate slots for display
|
|
146
|
+
const inventorySlots = computed(() => {
|
|
147
|
+
const slots = [];
|
|
148
|
+
const config = gridConfig.value;
|
|
149
|
+
let firstEmptyFound = false;
|
|
150
|
+
|
|
151
|
+
for (let i = 0; i < config.totalSlots; i++) {
|
|
152
|
+
if (i < filteredItems.value.length) {
|
|
153
|
+
const item = filteredItems.value[i];
|
|
154
|
+
let quantity = null;
|
|
155
|
+
|
|
156
|
+
if (props.quantityField) {
|
|
157
|
+
const quantityValue = foundry.utils.getProperty(item, \`system.\${props.quantityField.toLowerCase()}\`);
|
|
158
|
+
quantity = typeof quantityValue === 'object' ? quantityValue.value : quantityValue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
slots.push({
|
|
162
|
+
item: item,
|
|
163
|
+
quantity: quantity,
|
|
164
|
+
empty: false,
|
|
165
|
+
isCreateSlot: false
|
|
166
|
+
});
|
|
167
|
+
} else if (props.emptySlots === 'show') {
|
|
168
|
+
const isCreateSlot = !firstEmptyFound;
|
|
169
|
+
if (isCreateSlot) firstEmptyFound = true;
|
|
170
|
+
|
|
171
|
+
slots.push({
|
|
172
|
+
item: null,
|
|
173
|
+
quantity: null,
|
|
174
|
+
empty: true,
|
|
175
|
+
isCreateSlot: isCreateSlot
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return slots;
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Calculate aggregations
|
|
184
|
+
const aggregations = computed(() => {
|
|
185
|
+
if (!props.sumProperties || props.sumProperties.length === 0) {
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return props.sumProperties.map((propName, index) => {
|
|
190
|
+
let total = 0;
|
|
191
|
+
|
|
192
|
+
filteredItems.value.forEach(item => {
|
|
193
|
+
const propValue = foundry.utils.getProperty(item, \`system.\${propName.toLowerCase()}\`);
|
|
194
|
+
|
|
195
|
+
if (typeof propValue === 'number') {
|
|
196
|
+
total += propValue;
|
|
197
|
+
} else if (propValue?.value !== undefined) {
|
|
198
|
+
total += propValue.value;
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const max = props.sumMax && props.sumMax[index] ? props.sumMax[index] : null;
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
name: propName,
|
|
206
|
+
label: game.i18n.localize(propName),
|
|
207
|
+
value: total,
|
|
208
|
+
formatted: total.toLocaleString(),
|
|
209
|
+
max: max,
|
|
210
|
+
percentage: max ? Math.min(100, (total / max) * 100) : null
|
|
211
|
+
};
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Get money value
|
|
216
|
+
const moneyValue = computed(() => {
|
|
217
|
+
// Depend on updateKey to trigger re-evaluation
|
|
218
|
+
const _ = updateKey.value;
|
|
219
|
+
if (!props.moneyField) return null;
|
|
220
|
+
return foundry.utils.getProperty(document, \`system.\${props.moneyField.toLowerCase()}\`);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Format money display
|
|
224
|
+
const formattedMoney = computed(() => {
|
|
225
|
+
if (!moneyValue.value) return '0';
|
|
226
|
+
|
|
227
|
+
if (typeof moneyValue.value === 'number') {
|
|
228
|
+
return moneyValue.value.toLocaleString();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Multi-denomination money
|
|
232
|
+
if (typeof moneyValue.value === 'object') {
|
|
233
|
+
const parts = [];
|
|
234
|
+
for (const [denom, amount] of Object.entries(moneyValue.value)) {
|
|
235
|
+
if (amount > 0) {
|
|
236
|
+
parts.push(\`\${amount}\${denom.charAt(0)}\`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return parts.length > 0 ? parts.join(' ') : '0';
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return '0';
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Get capacity color for a percentage
|
|
246
|
+
const getCapacityColor = (percentage) => {
|
|
247
|
+
if (!percentage) return 'success';
|
|
248
|
+
if (percentage > 100) return 'error';
|
|
249
|
+
if (percentage >= 90) return 'error';
|
|
250
|
+
if (percentage >= 70) return 'warning';
|
|
251
|
+
return 'success';
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// Handle item click
|
|
255
|
+
const onItemClick = (slot) => {
|
|
256
|
+
if (slot.empty) {
|
|
257
|
+
if (slot.isCreateSlot) {
|
|
258
|
+
onCreateItem();
|
|
259
|
+
}
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (slot.item) {
|
|
263
|
+
slot.item.sheet.render(true);
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// Handle create new item
|
|
268
|
+
const onCreateItem = async () => {
|
|
269
|
+
if (!props.documentType) return;
|
|
270
|
+
|
|
271
|
+
const itemData = {
|
|
272
|
+
name: game.i18n.localize(\`New \${props.documentType}\`),
|
|
273
|
+
type: props.documentType
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const created = await document.createEmbeddedDocuments('Item', [itemData]);
|
|
277
|
+
if (created && created[0]) {
|
|
278
|
+
created[0].sheet.render(true);
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
// Handle delete item
|
|
283
|
+
const onDeleteItem = async (event, slot) => {
|
|
284
|
+
// Prevent the click from opening the item sheet
|
|
285
|
+
event.stopPropagation();
|
|
286
|
+
|
|
287
|
+
if (!slot.item) return;
|
|
288
|
+
|
|
289
|
+
const confirmed = await Dialog.confirm({
|
|
290
|
+
title: game.i18n.localize('Delete Item'),
|
|
291
|
+
content: \`<p>\${game.i18n.format('Are you sure you want to delete {name}?', { name: slot.item.name })}</p>\`,
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
if (confirmed) {
|
|
295
|
+
await slot.item.delete();
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// Handle drag start
|
|
300
|
+
const onDragStart = (event, slot) => {
|
|
301
|
+
if (!slot.item) return;
|
|
302
|
+
|
|
303
|
+
const dragData = {
|
|
304
|
+
type: 'Item',
|
|
305
|
+
uuid: slot.item.uuid
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
event.dataTransfer.setData('text/plain', JSON.stringify(dragData));
|
|
309
|
+
|
|
310
|
+
// Add dragging class for visual feedback
|
|
311
|
+
event.target.classList.add('dragging');
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
// Handle drag end
|
|
315
|
+
const onDragEnd = (event) => {
|
|
316
|
+
// Remove dragging class
|
|
317
|
+
event.target.classList.remove('dragging');
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
// Get item name for tooltip
|
|
321
|
+
const getItemName = (slot) => {
|
|
322
|
+
if (slot.empty || !slot.item) return '';
|
|
323
|
+
return slot.item.name;
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
// Get item description for tooltip
|
|
327
|
+
const getItemDescription = (slot) => {
|
|
328
|
+
if (slot.empty || !slot.item) return '';
|
|
329
|
+
|
|
330
|
+
// Get description - handle both value and direct description
|
|
331
|
+
const description = slot.item.system?.description?.value || slot.item.system?.description;
|
|
332
|
+
if (!description) return '';
|
|
333
|
+
|
|
334
|
+
// Truncate if too long
|
|
335
|
+
if (description.length > 200) {
|
|
336
|
+
return description.substring(0, 200) + '...';
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return description;
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
// Get summed property values for tooltip
|
|
343
|
+
const getItemSumProperties = (slot) => {
|
|
344
|
+
if (slot.empty || !slot.item || !props.sumProperties || props.sumProperties.length === 0) {
|
|
345
|
+
return [];
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return props.sumProperties.map(propName => {
|
|
349
|
+
const propValue = foundry.utils.getProperty(slot.item, \`system.\${propName.toLowerCase()}\`);
|
|
350
|
+
|
|
351
|
+
let value = 0;
|
|
352
|
+
if (typeof propValue === 'number') {
|
|
353
|
+
value = propValue;
|
|
354
|
+
} else if (propValue?.value !== undefined) {
|
|
355
|
+
value = propValue.value;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
name: propName,
|
|
360
|
+
label: game.i18n.localize(propName),
|
|
361
|
+
value: value,
|
|
362
|
+
formatted: value.toLocaleString()
|
|
363
|
+
};
|
|
364
|
+
}).filter(prop => prop.value > 0); // Only show properties with values
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
// Count items
|
|
368
|
+
const itemCount = computed(() => {
|
|
369
|
+
return filteredItems.value.length;
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// Show summary based on mode
|
|
373
|
+
const showCount = computed(() => props.summary !== 'minimal');
|
|
374
|
+
const showAggregations = computed(() => props.summary === 'full');
|
|
375
|
+
const showMoney = computed(() => props.summary === 'full' && props.moneyField);
|
|
376
|
+
|
|
377
|
+
// Subscribe to item changes
|
|
378
|
+
const onItemChange = (item, options, userId) => {
|
|
379
|
+
// Check if this item belongs to our document
|
|
380
|
+
if (item.parent?.uuid === document.uuid) {
|
|
381
|
+
forceUpdate();
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
onMounted(() => {
|
|
386
|
+
Hooks.on('createItem', onItemChange);
|
|
387
|
+
Hooks.on('updateItem', onItemChange);
|
|
388
|
+
Hooks.on('deleteItem', onItemChange);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
onUnmounted(() => {
|
|
392
|
+
Hooks.off('createItem', onItemChange);
|
|
393
|
+
Hooks.off('updateItem', onItemChange);
|
|
394
|
+
Hooks.off('deleteItem', onItemChange);
|
|
395
|
+
});
|
|
396
|
+
</script>
|
|
397
|
+
|
|
398
|
+
<template>
|
|
399
|
+
<div class="isdl-inventory" :key="updateKey">
|
|
400
|
+
<!-- Header -->
|
|
401
|
+
<div class="inventory-header" v-if="showCount">
|
|
402
|
+
<div class="header-content">
|
|
403
|
+
<v-icon v-if="icon" :icon="icon" size="small"></v-icon>
|
|
404
|
+
<span class="inventory-label">{{ game.i18n.localize(label) }}</span>
|
|
405
|
+
</div>
|
|
406
|
+
<span class="inventory-count">
|
|
407
|
+
{{ itemCount }}/{{ gridConfig.totalSlots }}
|
|
408
|
+
</span>
|
|
409
|
+
</div>
|
|
410
|
+
|
|
411
|
+
<!-- Grid Container -->
|
|
412
|
+
<div class="inventory-grid-container">
|
|
413
|
+
<div
|
|
414
|
+
class="inventory-grid"
|
|
415
|
+
:style="{
|
|
416
|
+
gridTemplateColumns: \`repeat(\${gridConfig.columns}, \${slotSize}px)\`,
|
|
417
|
+
}"
|
|
418
|
+
>
|
|
419
|
+
<v-tooltip
|
|
420
|
+
v-for="(slot, index) in inventorySlots"
|
|
421
|
+
:key="index"
|
|
422
|
+
:disabled="slot.empty && !slot.isCreateSlot"
|
|
423
|
+
location="top"
|
|
424
|
+
max-width="400"
|
|
425
|
+
>
|
|
426
|
+
<template v-slot:activator="{ props: tooltipProps }">
|
|
427
|
+
<div
|
|
428
|
+
v-bind="tooltipProps"
|
|
429
|
+
class="inventory-slot"
|
|
430
|
+
:class="{
|
|
431
|
+
'empty': slot.empty,
|
|
432
|
+
'filled': !slot.empty,
|
|
433
|
+
'create-slot': slot.isCreateSlot
|
|
434
|
+
}"
|
|
435
|
+
:style="{
|
|
436
|
+
width: \`\${slotSize}px\`,
|
|
437
|
+
height: \`\${slotSize}px\`
|
|
438
|
+
}"
|
|
439
|
+
:draggable="!slot.empty && !!slot.item"
|
|
440
|
+
@click="onItemClick(slot)"
|
|
441
|
+
@dragstart="onDragStart($event, slot)"
|
|
442
|
+
@dragend="onDragEnd($event)"
|
|
443
|
+
>
|
|
444
|
+
<img
|
|
445
|
+
v-if="!slot.empty && slot.item"
|
|
446
|
+
:src="slot.item.img"
|
|
447
|
+
:alt="slot.item.name"
|
|
448
|
+
class="slot-image"
|
|
449
|
+
/>
|
|
450
|
+
<v-icon
|
|
451
|
+
v-if="slot.isCreateSlot"
|
|
452
|
+
icon="fa-solid fa-plus"
|
|
453
|
+
size="large"
|
|
454
|
+
class="create-icon"
|
|
455
|
+
></v-icon>
|
|
456
|
+
<div
|
|
457
|
+
v-if="!slot.empty && slot.item"
|
|
458
|
+
class="delete-button"
|
|
459
|
+
@click="onDeleteItem($event, slot)"
|
|
460
|
+
:data-tooltip="game.i18n.localize('Delete')"
|
|
461
|
+
>
|
|
462
|
+
<v-icon icon="fa-solid fa-times" size="x-small"></v-icon>
|
|
463
|
+
</div>
|
|
464
|
+
<div
|
|
465
|
+
v-if="!slot.empty && slot.quantity && slot.quantity > 1"
|
|
466
|
+
class="quantity-badge"
|
|
467
|
+
>
|
|
468
|
+
{{ slot.quantity }}
|
|
469
|
+
</div>
|
|
470
|
+
</div>
|
|
471
|
+
</template>
|
|
472
|
+
<template v-slot:default>
|
|
473
|
+
<div v-if="slot.isCreateSlot" class="inventory-tooltip">
|
|
474
|
+
<div class="tooltip-title">{{ game.i18n.localize('Create New Item') }}</div>
|
|
475
|
+
</div>
|
|
476
|
+
<div v-else class="inventory-tooltip">
|
|
477
|
+
<div class="tooltip-title">{{ getItemName(slot) }}</div>
|
|
478
|
+
<div v-if="getItemDescription(slot)" class="tooltip-description" v-html="getItemDescription(slot)"></div>
|
|
479
|
+
<div v-if="getItemSumProperties(slot).length > 0" class="tooltip-properties">
|
|
480
|
+
<div v-for="prop in getItemSumProperties(slot)" :key="prop.name" class="tooltip-property">
|
|
481
|
+
<span class="property-label">{{ prop.label }}:</span>
|
|
482
|
+
<span class="property-value">{{ prop.formatted }}</span>
|
|
483
|
+
</div>
|
|
484
|
+
</div>
|
|
485
|
+
</div>
|
|
486
|
+
</template>
|
|
487
|
+
</v-tooltip>
|
|
488
|
+
</div>
|
|
489
|
+
</div>
|
|
490
|
+
|
|
491
|
+
<!-- Footer -->
|
|
492
|
+
<div class="inventory-footer" v-if="showAggregations || showMoney">
|
|
493
|
+
<v-divider class="footer-divider"></v-divider>
|
|
494
|
+
|
|
495
|
+
<!-- Money Display -->
|
|
496
|
+
<div v-if="showMoney" class="inventory-stat money-display">
|
|
497
|
+
<div class="stat-header">
|
|
498
|
+
<v-icon v-if="moneyFieldIcon" :icon="moneyFieldIcon" size="small"></v-icon>
|
|
499
|
+
<span class="stat-label">{{ game.i18n.localize(moneyFieldLabel || 'Currency') }}</span>
|
|
500
|
+
</div>
|
|
501
|
+
<span class="stat-value">{{ formattedMoney }}</span>
|
|
502
|
+
</div>
|
|
503
|
+
|
|
504
|
+
<!-- Aggregations -->
|
|
505
|
+
<div v-if="showAggregations" v-for="agg in aggregations" :key="agg.name" class="inventory-stat aggregation-display">
|
|
506
|
+
<div class="stat-header">
|
|
507
|
+
<v-icon icon="fa-solid fa-calculator" size="small"></v-icon>
|
|
508
|
+
<span class="stat-label">{{ agg.label }}</span>
|
|
509
|
+
</div>
|
|
510
|
+
<div class="stat-value-container">
|
|
511
|
+
<span class="stat-value">
|
|
512
|
+
{{ agg.formatted }}
|
|
513
|
+
<span v-if="agg.max" class="stat-max"> / {{ agg.max }}</span>
|
|
514
|
+
</span>
|
|
515
|
+
<!-- Progress bar for aggregations with max -->
|
|
516
|
+
<v-progress-linear
|
|
517
|
+
v-if="agg.max"
|
|
518
|
+
:model-value="agg.percentage"
|
|
519
|
+
:color="getCapacityColor(agg.percentage)"
|
|
520
|
+
height="6"
|
|
521
|
+
rounded
|
|
522
|
+
class="capacity-progress"
|
|
523
|
+
></v-progress-linear>
|
|
524
|
+
</div>
|
|
525
|
+
</div>
|
|
526
|
+
</div>
|
|
527
|
+
</div>
|
|
528
|
+
</template>
|
|
529
529
|
`.appendNewLine();
|
|
530
530
|
fs.writeFileSync(generatedFilePath, toString(fileNode));
|
|
531
531
|
}
|