@adiholick/nvstudio-mcp 2.3.0 → 2.4.1

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.
@@ -19,6 +19,7 @@ const child_process_1 = require("child_process");
19
19
  const task_queue_1 = require("./task-queue");
20
20
  const ws_gateway_1 = require("./ws-gateway");
21
21
  const roblox_docs_1 = require("./roblox-docs");
22
+ const vfx_database_1 = require("./vfx-database");
22
23
  const { version: SERVER_VERSION } = require('../package.json');
23
24
  exports.isBridgeHosting = false;
24
25
  let resolveBridgeReady;
@@ -502,6 +503,113 @@ function startBridgeServer(port, isDaemon = false) {
502
503
  res.status(500).json({ error: err.message });
503
504
  }
504
505
  });
506
+ // ── /api/vfx Endpoints (NV-VFX Database Management) ───────────────────────
507
+ app.get('/api/vfx', (req, res) => {
508
+ try {
509
+ const db = (0, vfx_database_1.getVfxDatabase)();
510
+ const category = req.query.category ? String(req.query.category) : undefined;
511
+ const query = req.query.query ? String(req.query.query) : undefined;
512
+ const items = db.listVfx({ category, query });
513
+ res.json({ success: true, count: items.length, items });
514
+ }
515
+ catch (err) {
516
+ res.status(500).json({ error: err.message });
517
+ }
518
+ });
519
+ app.get('/api/vfx/categories', (req, res) => {
520
+ try {
521
+ const db = (0, vfx_database_1.getVfxDatabase)();
522
+ res.json({ success: true, categories: db.getCategories() });
523
+ }
524
+ catch (err) {
525
+ res.status(500).json({ error: err.message });
526
+ }
527
+ });
528
+ app.get('/api/vfx/:id', (req, res) => {
529
+ try {
530
+ const db = (0, vfx_database_1.getVfxDatabase)();
531
+ const item = db.getVfx(req.params.id);
532
+ if (!item)
533
+ return res.status(404).json({ error: 'VFX tidak ditemukan' });
534
+ res.json({ success: true, item });
535
+ }
536
+ catch (err) {
537
+ res.status(500).json({ error: err.message });
538
+ }
539
+ });
540
+ app.post('/api/vfx', (req, res) => {
541
+ try {
542
+ const db = (0, vfx_database_1.getVfxDatabase)();
543
+ const saved = db.saveVfx(req.body);
544
+ io.emit('vfx_updated', { action: 'saved', item: saved });
545
+ res.json({ success: true, item: saved });
546
+ }
547
+ catch (err) {
548
+ res.status(500).json({ error: err.message });
549
+ }
550
+ });
551
+ app.delete('/api/vfx/:id', (req, res) => {
552
+ try {
553
+ const db = (0, vfx_database_1.getVfxDatabase)();
554
+ const ok = db.deleteVfx(req.params.id);
555
+ if (ok) {
556
+ io.emit('vfx_updated', { action: 'deleted', id: req.params.id });
557
+ res.json({ success: true });
558
+ }
559
+ else {
560
+ res.status(404).json({ error: 'VFX tidak ditemukan' });
561
+ }
562
+ }
563
+ catch (err) {
564
+ res.status(500).json({ error: err.message });
565
+ }
566
+ });
567
+ app.post('/api/vfx/apply', async (req, res) => {
568
+ try {
569
+ const { id, target, scale, enabled, placeId, studioId } = req.body;
570
+ const db = (0, vfx_database_1.getVfxDatabase)();
571
+ const item = db.getVfx(id);
572
+ if (!item)
573
+ return res.status(404).json({ error: 'VFX tidak ditemukan di database' });
574
+ const targetPath = target || 'Workspace';
575
+ const payload = JSON.stringify({ data: item.data, scale: scale || 1, enabled });
576
+ const result = await (0, task_queue_1.addTaskToQueue)('apply_vfx', targetPath, payload, { placeId, studioId });
577
+ res.json({ success: true, result });
578
+ }
579
+ catch (err) {
580
+ res.status(500).json({ error: err.message });
581
+ }
582
+ });
583
+ app.post('/api/vfx/scan', async (req, res) => {
584
+ try {
585
+ const { target, name, category, tags, placeId, studioId } = req.body;
586
+ const targetPath = target || 'Workspace';
587
+ const scanPayload = JSON.stringify({ name, category, tags });
588
+ const result = await (0, task_queue_1.addTaskToQueue)('scan_vfx', targetPath, scanPayload, { placeId, studioId });
589
+ if (result && (result.status === 'success' || !result.error)) {
590
+ const scanned = result.result || result;
591
+ const db = (0, vfx_database_1.getVfxDatabase)();
592
+ const saved = db.saveVfx({
593
+ name: scanned.name || name || 'Scanned_VFX',
594
+ category: scanned.category || category || 'Ambient',
595
+ tags: scanned.tags || tags || [],
596
+ description: scanned.description,
597
+ sourcePlaceId: placeId,
598
+ stats: scanned.stats,
599
+ nodeCount: scanned.nodeCount,
600
+ data: scanned.data,
601
+ });
602
+ io.emit('vfx_updated', { action: 'saved', item: saved });
603
+ res.json({ success: true, item: saved, rawResult: result });
604
+ }
605
+ else {
606
+ res.status(400).json({ error: result?.error || 'Gagal memindai VFX dari Studio' });
607
+ }
608
+ }
609
+ catch (err) {
610
+ res.status(500).json({ error: err.message });
611
+ }
612
+ });
505
613
  // ── /api/stats ─────────────────────────────────────────────────────────────
506
614
  app.get('/api/stats', (req, res) => {
507
615
  const activeStudios = getConnectedStudioCount();
@@ -0,0 +1,507 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.VfxDatabase = void 0;
7
+ exports.normalizeVfxCategory = normalizeVfxCategory;
8
+ exports.getVfxDatabase = getVfxDatabase;
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const uuid_1 = require("uuid");
12
+ const DATA_DIR = path_1.default.join(process.cwd(), 'data');
13
+ const DB_FILE = path_1.default.join(DATA_DIR, 'vfx-database.json');
14
+ // ── Default Seed Presets ───────────────────────────────────────────────────────
15
+ const DEFAULT_PRESETS = [
16
+ {
17
+ id: 'preset-cyan-slash',
18
+ name: 'Cyan Horizon Slash',
19
+ category: 'Slash',
20
+ tags: ['sword', 'cyan', 'combat', 'slash', 'beam'],
21
+ description: 'High-speed sharp cybernetic slash effect with dual-layer brightness beams.',
22
+ author: 'NVStudio',
23
+ createdAt: new Date().toISOString(),
24
+ nodeCount: 5,
25
+ stats: { emitters: 0, beams: 2, trails: 0, lights: 0 },
26
+ data: {
27
+ version: 1,
28
+ rootName: 'CyanHorizonSlash',
29
+ rootClass: 'Folder',
30
+ nodeCount: 5,
31
+ stats: { emitters: 0, beams: 2, trails: 0, lights: 0 },
32
+ nodes: [
33
+ { class: 'Folder', name: 'CyanHorizonSlash', properties: { Name: 'CyanHorizonSlash' } },
34
+ {
35
+ parent: 1,
36
+ class: 'Attachment',
37
+ name: 'Att0',
38
+ properties: {
39
+ Name: 'Att0',
40
+ Visible: false,
41
+ CFrame: { t: 'CFrame', v: [2, 0.5, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1] },
42
+ },
43
+ },
44
+ {
45
+ parent: 1,
46
+ class: 'Attachment',
47
+ name: 'Att1',
48
+ properties: {
49
+ Name: 'Att1',
50
+ Visible: false,
51
+ CFrame: { t: 'CFrame', v: [-2, -0.5, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1] },
52
+ },
53
+ },
54
+ {
55
+ parent: 1,
56
+ class: 'Beam',
57
+ name: 'SlashCore',
58
+ properties: {
59
+ Name: 'SlashCore',
60
+ Enabled: true,
61
+ Attachment0: { t: 'Ref', v: 2 },
62
+ Attachment1: { t: 'Ref', v: 3 },
63
+ Brightness: 3000,
64
+ LightEmission: 1,
65
+ LightInfluence: 0,
66
+ Width0: 1.2,
67
+ Width1: 0.1,
68
+ Texture: 'rbxassetid://85772363699785',
69
+ TextureSpeed: 5,
70
+ TextureLength: 1,
71
+ Color: {
72
+ t: 'ColorSequence',
73
+ v: [
74
+ [0, { t: 'Color3', v: [0, 0.8, 1] }],
75
+ [1, { t: 'Color3', v: [0, 1, 0.8] }],
76
+ ],
77
+ },
78
+ Transparency: {
79
+ t: 'NumberSequence',
80
+ v: [
81
+ [0, 0, 0],
82
+ [1, 1, 0],
83
+ ],
84
+ },
85
+ },
86
+ },
87
+ {
88
+ parent: 1,
89
+ class: 'Beam',
90
+ name: 'SlashGlow',
91
+ properties: {
92
+ Name: 'SlashGlow',
93
+ Enabled: true,
94
+ Attachment0: { t: 'Ref', v: 2 },
95
+ Attachment1: { t: 'Ref', v: 3 },
96
+ Brightness: 1500,
97
+ LightEmission: 0.8,
98
+ Width0: 2.5,
99
+ Width1: 0.5,
100
+ TextureSpeed: 3,
101
+ Color: {
102
+ t: 'ColorSequence',
103
+ v: [
104
+ [0, { t: 'Color3', v: [0.1, 0.5, 1] }],
105
+ [1, { t: 'Color3', v: [0.2, 0.8, 1] }],
106
+ ],
107
+ },
108
+ Transparency: {
109
+ t: 'NumberSequence',
110
+ v: [
111
+ [0, 0.2, 0],
112
+ [1, 1, 0],
113
+ ],
114
+ },
115
+ },
116
+ },
117
+ ],
118
+ },
119
+ },
120
+ {
121
+ id: 'preset-solar-burst',
122
+ name: 'Solar Flare Burst',
123
+ category: 'Explosion',
124
+ tags: ['fire', 'orange', 'explosion', 'particles', 'burst'],
125
+ description: 'Radial fiery shockwave burst with expanding sparks and heat glow.',
126
+ author: 'NVStudio',
127
+ createdAt: new Date().toISOString(),
128
+ nodeCount: 3,
129
+ stats: { emitters: 2, beams: 0, trails: 0, lights: 1 },
130
+ data: {
131
+ version: 1,
132
+ rootName: 'SolarFlareBurst',
133
+ rootClass: 'Folder',
134
+ nodeCount: 3,
135
+ stats: { emitters: 2, beams: 0, trails: 0, lights: 1 },
136
+ nodes: [
137
+ { class: 'Folder', name: 'SolarFlareBurst', properties: { Name: 'SolarFlareBurst' } },
138
+ {
139
+ parent: 1,
140
+ class: 'ParticleEmitter',
141
+ name: 'Sparks',
142
+ properties: {
143
+ Name: 'Sparks',
144
+ Enabled: true,
145
+ Rate: 200,
146
+ Speed: { t: 'NumberRange', v: [25, 45] },
147
+ SpreadAngle: { t: 'Vector2', v: [180, 180] },
148
+ Lifetime: { t: 'NumberRange', v: [0.4, 0.8] },
149
+ LightEmission: 1,
150
+ LightInfluence: 0,
151
+ Brightness: 10,
152
+ Size: {
153
+ t: 'NumberSequence',
154
+ v: [
155
+ [0, 1.2, 0],
156
+ [0.7, 0.4, 0],
157
+ [1, 0, 0],
158
+ ],
159
+ },
160
+ Color: {
161
+ t: 'ColorSequence',
162
+ v: [
163
+ [0, { t: 'Color3', v: [1, 0.9, 0.5] }],
164
+ [0.5, { t: 'Color3', v: [1, 0.4, 0] }],
165
+ [1, { t: 'Color3', v: [0.8, 0.1, 0] }],
166
+ ],
167
+ },
168
+ Transparency: {
169
+ t: 'NumberSequence',
170
+ v: [
171
+ [0, 0, 0],
172
+ [1, 1, 0],
173
+ ],
174
+ },
175
+ Texture: 'rbxassetid://244221448',
176
+ },
177
+ },
178
+ {
179
+ parent: 1,
180
+ class: 'PointLight',
181
+ name: 'ExplosionLight',
182
+ properties: {
183
+ Name: 'ExplosionLight',
184
+ Enabled: true,
185
+ Brightness: 8,
186
+ Range: 24,
187
+ Color: { t: 'Color3', v: [1, 0.5, 0.1] },
188
+ Shadows: true,
189
+ },
190
+ },
191
+ ],
192
+ },
193
+ },
194
+ {
195
+ id: 'preset-dragon-aura',
196
+ name: 'Azure Dragon Aura',
197
+ category: 'Aura',
198
+ tags: ['aura', 'magic', 'energy', 'cyan', 'blue'],
199
+ description: 'Swirling ethereal flame aura for boss phases, charging states, and powerups.',
200
+ author: 'NVStudio',
201
+ createdAt: new Date().toISOString(),
202
+ nodeCount: 2,
203
+ stats: { emitters: 1, beams: 0, trails: 0, lights: 1 },
204
+ data: {
205
+ version: 1,
206
+ rootName: 'AzureDragonAura',
207
+ rootClass: 'Folder',
208
+ nodeCount: 2,
209
+ stats: { emitters: 1, beams: 0, trails: 0, lights: 1 },
210
+ nodes: [
211
+ { class: 'Folder', name: 'AzureDragonAura', properties: { Name: 'AzureDragonAura' } },
212
+ {
213
+ parent: 1,
214
+ class: 'ParticleEmitter',
215
+ name: 'FlameAura',
216
+ properties: {
217
+ Name: 'FlameAura',
218
+ Enabled: true,
219
+ Rate: 35,
220
+ Speed: { t: 'NumberRange', v: [2, 5] },
221
+ EmissionDirection: { t: 'EnumItem', e: 'NormalId', v: 'Top' },
222
+ Lifetime: { t: 'NumberRange', v: [0.8, 1.4] },
223
+ LightEmission: 0.9,
224
+ LightInfluence: 0,
225
+ Brightness: 5,
226
+ Drag: 2,
227
+ Acceleration: { t: 'Vector3', v: [0, 8, 0] },
228
+ Size: {
229
+ t: 'NumberSequence',
230
+ v: [
231
+ [0, 0.5, 0],
232
+ [0.5, 2.5, 0],
233
+ [1, 0.2, 0],
234
+ ],
235
+ },
236
+ Color: {
237
+ t: 'ColorSequence',
238
+ v: [
239
+ [0, { t: 'Color3', v: [0, 0.7, 1] }],
240
+ [0.6, { t: 'Color3', v: [0.2, 0.2, 0.9] }],
241
+ [1, { t: 'Color3', v: [0.05, 0, 0.4] }],
242
+ ],
243
+ },
244
+ Transparency: {
245
+ t: 'NumberSequence',
246
+ v: [
247
+ [0, 0.8, 0],
248
+ [0.3, 0.2, 0],
249
+ [1, 1, 0],
250
+ ],
251
+ },
252
+ Texture: 'rbxassetid://258128463',
253
+ },
254
+ },
255
+ ],
256
+ },
257
+ },
258
+ {
259
+ id: 'preset-arcane-rune',
260
+ name: 'Arcane Summoning Circle',
261
+ category: 'Magic',
262
+ tags: ['magic', 'spell', 'circle', 'rune', 'purple'],
263
+ description: 'Luminous spinning arcane circle with floating magical motes.',
264
+ author: 'NVStudio',
265
+ createdAt: new Date().toISOString(),
266
+ nodeCount: 3,
267
+ stats: { emitters: 1, beams: 0, trails: 0, lights: 1 },
268
+ data: {
269
+ version: 1,
270
+ rootName: 'ArcaneSummoningCircle',
271
+ rootClass: 'Folder',
272
+ nodeCount: 3,
273
+ stats: { emitters: 1, beams: 0, trails: 0, lights: 1 },
274
+ nodes: [
275
+ { class: 'Folder', name: 'ArcaneSummoningCircle', properties: { Name: 'ArcaneSummoningCircle' } },
276
+ {
277
+ parent: 1,
278
+ class: 'ParticleEmitter',
279
+ name: 'MagicSparks',
280
+ properties: {
281
+ Name: 'MagicSparks',
282
+ Enabled: true,
283
+ Rate: 40,
284
+ Speed: { t: 'NumberRange', v: [1, 4] },
285
+ Lifetime: { t: 'NumberRange', v: [1, 2] },
286
+ LightEmission: 1,
287
+ Size: {
288
+ t: 'NumberSequence',
289
+ v: [
290
+ [0, 0.2, 0],
291
+ [0.5, 0.6, 0],
292
+ [1, 0, 0],
293
+ ],
294
+ },
295
+ Color: {
296
+ t: 'ColorSequence',
297
+ v: [
298
+ [0, { t: 'Color3', v: [0.8, 0.2, 1] }],
299
+ [1, { t: 'Color3', v: [0.4, 0.8, 1] }],
300
+ ],
301
+ },
302
+ Transparency: {
303
+ t: 'NumberSequence',
304
+ v: [
305
+ [0, 0, 0],
306
+ [1, 1, 0],
307
+ ],
308
+ },
309
+ Texture: 'rbxassetid://244221448',
310
+ },
311
+ },
312
+ {
313
+ parent: 1,
314
+ class: 'PointLight',
315
+ name: 'RuneGlow',
316
+ properties: {
317
+ Name: 'RuneGlow',
318
+ Enabled: true,
319
+ Brightness: 4,
320
+ Range: 16,
321
+ Color: { t: 'Color3', v: [0.7, 0.2, 1] },
322
+ },
323
+ },
324
+ ],
325
+ },
326
+ },
327
+ {
328
+ id: 'preset-emerald-beam',
329
+ name: 'Emerald Laser Beam',
330
+ category: 'Beam',
331
+ tags: ['beam', 'laser', 'emerald', 'green', 'energy'],
332
+ description: 'High-density focused energy beam with glowing outer edge.',
333
+ author: 'NVStudio',
334
+ createdAt: new Date().toISOString(),
335
+ nodeCount: 4,
336
+ stats: { emitters: 0, beams: 1, trails: 0, lights: 1 },
337
+ data: {
338
+ version: 1,
339
+ rootName: 'EmeraldLaserBeam',
340
+ rootClass: 'Folder',
341
+ nodeCount: 4,
342
+ stats: { emitters: 0, beams: 1, trails: 0, lights: 1 },
343
+ nodes: [
344
+ { class: 'Folder', name: 'EmeraldLaserBeam', properties: { Name: 'EmeraldLaserBeam' } },
345
+ {
346
+ parent: 1,
347
+ class: 'Attachment',
348
+ name: 'Origin',
349
+ properties: { Name: 'Origin', CFrame: { t: 'CFrame', v: [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1] } },
350
+ },
351
+ {
352
+ parent: 1,
353
+ class: 'Attachment',
354
+ name: 'Target',
355
+ properties: { Name: 'Target', CFrame: { t: 'CFrame', v: [0, 0, -30, 1, 0, 0, 0, 1, 0, 0, 0, 1] } },
356
+ },
357
+ {
358
+ parent: 1,
359
+ class: 'Beam',
360
+ name: 'LaserCore',
361
+ properties: {
362
+ Name: 'LaserCore',
363
+ Enabled: true,
364
+ Attachment0: { t: 'Ref', v: 2 },
365
+ Attachment1: { t: 'Ref', v: 3 },
366
+ Brightness: 2500,
367
+ LightEmission: 1,
368
+ Width0: 1.5,
369
+ Width1: 1.5,
370
+ TextureSpeed: 8,
371
+ Color: {
372
+ t: 'ColorSequence',
373
+ v: [
374
+ [0, { t: 'Color3', v: [0.2, 1, 0.4] }],
375
+ [1, { t: 'Color3', v: [0, 0.8, 0.2] }],
376
+ ],
377
+ },
378
+ Transparency: {
379
+ t: 'NumberSequence',
380
+ v: [
381
+ [0, 0.1, 0],
382
+ [1, 0.1, 0],
383
+ ],
384
+ },
385
+ },
386
+ },
387
+ ],
388
+ },
389
+ },
390
+ ];
391
+ function normalizeVfxCategory(rawCategory) {
392
+ if (!rawCategory || typeof rawCategory !== 'string')
393
+ return 'Ambient';
394
+ const trimmed = rawCategory.trim();
395
+ if (!trimmed)
396
+ return 'Ambient';
397
+ return trimmed.charAt(0).toUpperCase() + trimmed.slice(1).toLowerCase();
398
+ }
399
+ class VfxDatabase {
400
+ items = new Map();
401
+ constructor() {
402
+ this.ensureDirectory();
403
+ this.loadDatabase();
404
+ }
405
+ ensureDirectory() {
406
+ if (!fs_1.default.existsSync(DATA_DIR)) {
407
+ fs_1.default.mkdirSync(DATA_DIR, { recursive: true });
408
+ }
409
+ }
410
+ loadDatabase() {
411
+ if (fs_1.default.existsSync(DB_FILE)) {
412
+ try {
413
+ const raw = fs_1.default.readFileSync(DB_FILE, 'utf-8');
414
+ const list = JSON.parse(raw);
415
+ for (const item of list) {
416
+ item.category = normalizeVfxCategory(item.category);
417
+ this.items.set(item.id, item);
418
+ }
419
+ }
420
+ catch (err) {
421
+ console.error('[VfxDatabase] Gagal membaca vfx-database.json, menggunakan preset default:', err);
422
+ this.seedPresets();
423
+ }
424
+ }
425
+ else {
426
+ this.seedPresets();
427
+ }
428
+ }
429
+ seedPresets() {
430
+ for (const preset of DEFAULT_PRESETS) {
431
+ this.items.set(preset.id, preset);
432
+ }
433
+ this.saveToFile();
434
+ }
435
+ saveToFile() {
436
+ try {
437
+ this.ensureDirectory();
438
+ const list = Array.from(this.items.values());
439
+ fs_1.default.writeFileSync(DB_FILE, JSON.stringify(list, null, 2), 'utf-8');
440
+ }
441
+ catch (err) {
442
+ console.error('[VfxDatabase] Gagal menyimpan ke vfx-database.json:', err);
443
+ }
444
+ }
445
+ saveVfx(item) {
446
+ const id = item.id || `vfx-${(0, uuid_1.v4)().slice(0, 8)}`;
447
+ const createdAt = item.createdAt || new Date().toISOString();
448
+ const category = normalizeVfxCategory(item.category);
449
+ const vfxItem = {
450
+ ...item,
451
+ id,
452
+ category,
453
+ createdAt,
454
+ tags: Array.isArray(item.tags) ? item.tags : [category.toLowerCase()],
455
+ nodeCount: item.nodeCount || (item.data?.nodes ? item.data.nodes.length : 1),
456
+ stats: item.stats || { emitters: 0, beams: 0, trails: 0, lights: 0 },
457
+ };
458
+ this.items.set(id, vfxItem);
459
+ this.saveToFile();
460
+ return vfxItem;
461
+ }
462
+ getVfx(id) {
463
+ return this.items.get(id);
464
+ }
465
+ listVfx(filter) {
466
+ let result = Array.from(this.items.values());
467
+ if (filter?.category && filter.category !== 'All' && filter.category !== 'Semua') {
468
+ const catLower = filter.category.toLowerCase();
469
+ result = result.filter(item => item.category.toLowerCase() === catLower);
470
+ }
471
+ if (filter?.query && filter.query.trim()) {
472
+ const q = filter.query.toLowerCase().trim();
473
+ result = result.filter(item => item.name.toLowerCase().includes(q) ||
474
+ item.category.toLowerCase().includes(q) ||
475
+ (item.description && item.description.toLowerCase().includes(q)) ||
476
+ (item.tags && item.tags.some(t => t.toLowerCase().includes(q))));
477
+ }
478
+ return result.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
479
+ }
480
+ deleteVfx(id) {
481
+ const deleted = this.items.delete(id);
482
+ if (deleted) {
483
+ this.saveToFile();
484
+ }
485
+ return deleted;
486
+ }
487
+ getCategories() {
488
+ const counts = new Map();
489
+ for (const item of this.items.values()) {
490
+ const cat = normalizeVfxCategory(item.category);
491
+ counts.set(cat, (counts.get(cat) || 0) + 1);
492
+ }
493
+ const categories = [];
494
+ for (const [name, count] of counts.entries()) {
495
+ categories.push({ name, count });
496
+ }
497
+ return categories.sort((a, b) => b.count - a.count);
498
+ }
499
+ }
500
+ exports.VfxDatabase = VfxDatabase;
501
+ let instance = null;
502
+ function getVfxDatabase() {
503
+ if (!instance) {
504
+ instance = new VfxDatabase();
505
+ }
506
+ return instance;
507
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@adiholick/nvstudio-mcp",
3
- "version": "2.3.0",
4
- "description": "Local Bridge Server MCP for Roblox Studio",
3
+ "version": "2.4.1",
4
+ "description": "Local Bridge Server MCP for Roblox Studio with Animation, VFX, and Rigging Engines",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
7
  "nvstudio-mcp": "dist/index.js"