@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.
package/public/index.html CHANGED
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>nvstudio-mcp v2.3 | Bridge Dashboard</title>
6
+ <title>nvstudio-mcp v2.4 | Bridge Dashboard</title>
7
7
  <link rel="preconnect" href="https://fonts.googleapis.com">
8
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
9
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
@@ -684,6 +684,410 @@
684
684
  font-size: 12px;
685
685
  }
686
686
 
687
+ /* ── VFX Database & Library Styles ── */
688
+ .vfx-sidebar-badge {
689
+ font-size: 10px;
690
+ font-weight: 700;
691
+ background: var(--accent);
692
+ color: #fff;
693
+ padding: 1px 7px;
694
+ border-radius: 10px;
695
+ margin-left: auto;
696
+ letter-spacing: 0.02em;
697
+ }
698
+
699
+ .vfx-container {
700
+ display: flex;
701
+ flex-direction: column;
702
+ gap: 20px;
703
+ padding: 24px 32px 60px 32px;
704
+ overflow-y: auto;
705
+ height: 100%;
706
+ }
707
+
708
+ .vfx-header {
709
+ display: flex;
710
+ justify-content: space-between;
711
+ align-items: flex-start;
712
+ gap: 16px;
713
+ flex-wrap: wrap;
714
+ }
715
+ .vfx-header-info h2 {
716
+ font-size: 22px;
717
+ font-weight: 700;
718
+ color: var(--text);
719
+ display: flex;
720
+ align-items: center;
721
+ gap: 10px;
722
+ }
723
+ .vfx-header-info p {
724
+ font-size: 13px;
725
+ color: var(--text-muted);
726
+ margin-top: 4px;
727
+ }
728
+
729
+ .vfx-actions-bar {
730
+ display: flex;
731
+ gap: 10px;
732
+ align-items: center;
733
+ }
734
+
735
+ .vfx-btn-primary {
736
+ background: linear-gradient(135deg, #0088cc 0%, #00aaff 100%);
737
+ color: #fff;
738
+ font-weight: 600;
739
+ font-size: 13px;
740
+ padding: 8px 16px;
741
+ border-radius: 8px;
742
+ border: none;
743
+ cursor: pointer;
744
+ display: inline-flex;
745
+ align-items: center;
746
+ gap: 8px;
747
+ box-shadow: 0 4px 12px var(--accent-glow);
748
+ transition: all 0.2s;
749
+ }
750
+ .vfx-btn-primary:hover {
751
+ transform: translateY(-1px);
752
+ box-shadow: 0 6px 16px var(--accent-glow);
753
+ filter: brightness(1.1);
754
+ }
755
+
756
+ .vfx-btn-secondary {
757
+ background: rgba(255, 255, 255, 0.05);
758
+ color: var(--text);
759
+ border: 1px solid var(--border);
760
+ font-weight: 600;
761
+ font-size: 13px;
762
+ padding: 8px 14px;
763
+ border-radius: 8px;
764
+ cursor: pointer;
765
+ display: inline-flex;
766
+ align-items: center;
767
+ gap: 6px;
768
+ transition: all 0.2s;
769
+ }
770
+ .vfx-btn-secondary:hover {
771
+ background: rgba(255, 255, 255, 0.1);
772
+ border-color: var(--border-highlight);
773
+ }
774
+
775
+ .vfx-search-box {
776
+ position: relative;
777
+ flex: 1;
778
+ min-width: 260px;
779
+ max-width: 480px;
780
+ }
781
+ .vfx-search-box input {
782
+ width: 100%;
783
+ background: #0d0d0d;
784
+ border: 1px solid var(--border);
785
+ border-radius: 8px;
786
+ padding: 9px 14px 9px 36px;
787
+ color: var(--text);
788
+ font-size: 13px;
789
+ outline: none;
790
+ transition: all 0.2s;
791
+ }
792
+ .vfx-search-box input:focus {
793
+ border-color: var(--accent);
794
+ box-shadow: 0 0 10px var(--accent-glow);
795
+ }
796
+ .vfx-search-box svg {
797
+ position: absolute;
798
+ left: 12px;
799
+ top: 50%;
800
+ transform: translateY(-50%);
801
+ width: 15px;
802
+ height: 15px;
803
+ color: var(--text-dim);
804
+ }
805
+
806
+ /* Category Filter Pills */
807
+ .vfx-category-tabs {
808
+ display: flex;
809
+ align-items: center;
810
+ gap: 8px;
811
+ overflow-x: auto;
812
+ padding-bottom: 6px;
813
+ }
814
+ .vfx-cat-pill {
815
+ background: #18181d;
816
+ border: 1px solid var(--border);
817
+ color: var(--text-muted);
818
+ font-size: 12px;
819
+ font-weight: 600;
820
+ padding: 6px 14px;
821
+ border-radius: 20px;
822
+ cursor: pointer;
823
+ white-space: nowrap;
824
+ display: inline-flex;
825
+ align-items: center;
826
+ gap: 6px;
827
+ transition: all 0.2s;
828
+ }
829
+ .vfx-cat-pill:hover {
830
+ color: var(--text);
831
+ border-color: var(--border-highlight);
832
+ background: #222228;
833
+ }
834
+ .vfx-cat-pill.active {
835
+ background: var(--accent);
836
+ color: #fff;
837
+ border-color: var(--accent);
838
+ box-shadow: 0 2px 10px var(--accent-glow);
839
+ }
840
+ .vfx-cat-count {
841
+ font-size: 10px;
842
+ background: rgba(0, 0, 0, 0.35);
843
+ padding: 1px 6px;
844
+ border-radius: 10px;
845
+ opacity: 0.9;
846
+ }
847
+
848
+ /* Summary Stats Cards */
849
+ .vfx-stats-row {
850
+ display: grid;
851
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
852
+ gap: 14px;
853
+ }
854
+ .vfx-stat-box {
855
+ background: var(--surface-card);
856
+ border: 1px solid var(--border);
857
+ border-radius: 10px;
858
+ padding: 16px 20px;
859
+ display: flex;
860
+ flex-direction: column;
861
+ gap: 4px;
862
+ }
863
+ .vfx-stat-label { font-size: 11px; text-transform: uppercase; color: var(--text-dim); font-weight: 600; letter-spacing: 0.05em; }
864
+ .vfx-stat-val { font-size: 24px; font-weight: 700; color: var(--text); }
865
+ .vfx-stat-sub { font-size: 12px; color: var(--text-muted); }
866
+
867
+ /* VFX Grid & Cards */
868
+ .vfx-grid {
869
+ display: grid;
870
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
871
+ gap: 16px;
872
+ }
873
+ .vfx-card {
874
+ background: #18181d;
875
+ border: 1px solid var(--border);
876
+ border-radius: 10px;
877
+ padding: 20px;
878
+ display: flex;
879
+ flex-direction: column;
880
+ gap: 14px;
881
+ transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1);
882
+ position: relative;
883
+ }
884
+ .vfx-card:hover {
885
+ border-color: rgba(0, 170, 255, 0.4);
886
+ transform: translateY(-2px);
887
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
888
+ }
889
+
890
+ .vfx-card-top {
891
+ display: flex;
892
+ justify-content: space-between;
893
+ align-items: flex-start;
894
+ gap: 10px;
895
+ }
896
+ .vfx-card-title-group h4 {
897
+ font-size: 15px;
898
+ font-weight: 600;
899
+ color: var(--text);
900
+ letter-spacing: -0.01em;
901
+ }
902
+ .vfx-card-date {
903
+ font-size: 11px;
904
+ color: var(--text-dim);
905
+ margin-top: 2px;
906
+ }
907
+
908
+ .vfx-badge-cat {
909
+ font-size: 10px;
910
+ font-weight: 700;
911
+ text-transform: uppercase;
912
+ padding: 3px 8px;
913
+ border-radius: 6px;
914
+ letter-spacing: 0.04em;
915
+ }
916
+ .vfx-badge-Slash { background: rgba(0, 170, 255, 0.15); color: #00aaff; border: 1px solid rgba(0, 170, 255, 0.3); }
917
+ .vfx-badge-Explosion { background: rgba(245, 158, 11, 0.15); color: #f59e0b; border: 1px solid rgba(245, 158, 11, 0.3); }
918
+ .vfx-badge-Aura { background: rgba(16, 185, 129, 0.15); color: #10b981; border: 1px solid rgba(16, 185, 129, 0.3); }
919
+ .vfx-badge-Magic { background: rgba(168, 85, 247, 0.15); color: #c084fc; border: 1px solid rgba(168, 85, 247, 0.3); }
920
+ .vfx-badge-Impact { background: rgba(239, 68, 68, 0.15); color: #f87171; border: 1px solid rgba(239, 68, 68, 0.3); }
921
+ .vfx-badge-Beam { background: rgba(6, 182, 212, 0.15); color: #22d3ee; border: 1px solid rgba(6, 182, 212, 0.3); }
922
+ .vfx-badge-Projectile { background: rgba(236, 72, 153, 0.15); color: #f472b6; border: 1px solid rgba(236, 72, 153, 0.3); }
923
+ .vfx-badge-Ambient { background: rgba(161, 161, 170, 0.15); color: #a1a1aa; border: 1px solid rgba(161, 161, 170, 0.3); }
924
+
925
+ .vfx-card-desc {
926
+ font-size: 13px;
927
+ color: var(--text-muted);
928
+ line-height: 1.5;
929
+ min-height: 38px;
930
+ }
931
+
932
+ .vfx-composition-badges {
933
+ display: flex;
934
+ flex-wrap: wrap;
935
+ gap: 6px;
936
+ }
937
+ .vfx-comp-pill {
938
+ font-size: 11px;
939
+ background: rgba(255, 255, 255, 0.05);
940
+ border: 1px solid rgba(255, 255, 255, 0.08);
941
+ color: var(--text-muted);
942
+ padding: 2px 7px;
943
+ border-radius: 4px;
944
+ font-family: 'JetBrains Mono', monospace;
945
+ }
946
+
947
+ .vfx-card-tags {
948
+ display: flex;
949
+ flex-wrap: wrap;
950
+ gap: 4px;
951
+ }
952
+ .vfx-tag-chip {
953
+ font-size: 11px;
954
+ color: var(--text-dim);
955
+ background: #0d0d0d;
956
+ padding: 2px 6px;
957
+ border-radius: 4px;
958
+ }
959
+
960
+ .vfx-card-footer {
961
+ margin-top: auto;
962
+ display: flex;
963
+ gap: 8px;
964
+ padding-top: 12px;
965
+ border-top: 1px solid rgba(255, 255, 255, 0.06);
966
+ }
967
+ .vfx-btn-apply {
968
+ flex: 1;
969
+ background: rgba(0, 170, 255, 0.12);
970
+ color: var(--accent);
971
+ border: 1px solid rgba(0, 170, 255, 0.3);
972
+ font-size: 12px;
973
+ font-weight: 600;
974
+ padding: 7px 12px;
975
+ border-radius: 6px;
976
+ cursor: pointer;
977
+ display: inline-flex;
978
+ align-items: center;
979
+ justify-content: center;
980
+ gap: 6px;
981
+ transition: all 0.2s;
982
+ }
983
+ .vfx-btn-apply:hover {
984
+ background: var(--accent);
985
+ color: #fff;
986
+ box-shadow: 0 0 12px var(--accent-glow);
987
+ }
988
+
989
+ .vfx-btn-json {
990
+ background: rgba(255, 255, 255, 0.04);
991
+ color: var(--text-muted);
992
+ border: 1px solid var(--border);
993
+ font-size: 12px;
994
+ padding: 7px 10px;
995
+ border-radius: 6px;
996
+ cursor: pointer;
997
+ transition: all 0.2s;
998
+ }
999
+ .vfx-btn-json:hover {
1000
+ background: rgba(255, 255, 255, 0.09);
1001
+ color: var(--text);
1002
+ }
1003
+
1004
+ .vfx-btn-del {
1005
+ background: rgba(239, 68, 68, 0.08);
1006
+ color: #f87171;
1007
+ border: 1px solid rgba(239, 68, 68, 0.2);
1008
+ font-size: 12px;
1009
+ padding: 7px 10px;
1010
+ border-radius: 6px;
1011
+ cursor: pointer;
1012
+ transition: all 0.2s;
1013
+ }
1014
+ .vfx-btn-del:hover {
1015
+ background: #ef4444;
1016
+ color: #fff;
1017
+ }
1018
+
1019
+ /* Modals */
1020
+ .vfx-modal-backdrop {
1021
+ display: none;
1022
+ position: fixed;
1023
+ top: 0; left: 0; right: 0; bottom: 0;
1024
+ background: rgba(0, 0, 0, 0.7);
1025
+ backdrop-filter: blur(8px);
1026
+ z-index: 1000;
1027
+ align-items: center;
1028
+ justify-content: center;
1029
+ padding: 20px;
1030
+ }
1031
+ .vfx-modal-backdrop.open { display: flex; }
1032
+ .vfx-modal-card {
1033
+ background: #18181d;
1034
+ border: 1px solid var(--border-highlight);
1035
+ border-radius: 12px;
1036
+ max-width: 600px;
1037
+ width: 100%;
1038
+ max-height: 85vh;
1039
+ display: flex;
1040
+ flex-direction: column;
1041
+ box-shadow: 0 16px 40px rgba(0, 0, 0, 0.6);
1042
+ overflow: hidden;
1043
+ }
1044
+ .vfx-modal-header {
1045
+ padding: 18px 24px;
1046
+ border-bottom: 1px solid var(--border);
1047
+ display: flex;
1048
+ align-items: center;
1049
+ justify-content: space-between;
1050
+ }
1051
+ .vfx-modal-header h3 { font-size: 16px; font-weight: 600; color: #fff; }
1052
+ .vfx-modal-close {
1053
+ background: none; border: none; color: var(--text-dim); cursor: pointer; font-size: 20px; line-height: 1;
1054
+ }
1055
+ .vfx-modal-close:hover { color: #fff; }
1056
+ .vfx-modal-body {
1057
+ padding: 20px 24px;
1058
+ overflow-y: auto;
1059
+ display: flex;
1060
+ flex-direction: column;
1061
+ gap: 16px;
1062
+ }
1063
+ .vfx-modal-footer {
1064
+ padding: 16px 24px;
1065
+ border-top: 1px solid var(--border);
1066
+ display: flex;
1067
+ justify-content: flex-end;
1068
+ gap: 10px;
1069
+ background: rgba(0, 0, 0, 0.2);
1070
+ }
1071
+ .vfx-form-group {
1072
+ display: flex;
1073
+ flex-direction: column;
1074
+ gap: 6px;
1075
+ }
1076
+ .vfx-form-label { font-size: 12px; font-weight: 600; color: var(--text-muted); }
1077
+ .vfx-form-input, .vfx-form-select {
1078
+ background: #0d0d0d;
1079
+ border: 1px solid var(--border);
1080
+ border-radius: 6px;
1081
+ padding: 9px 12px;
1082
+ color: var(--text);
1083
+ font-size: 13px;
1084
+ outline: none;
1085
+ }
1086
+ .vfx-form-input:focus, .vfx-form-select:focus {
1087
+ border-color: var(--accent);
1088
+ box-shadow: 0 0 8px var(--accent-glow);
1089
+ }
1090
+
687
1091
  /* Scrollbar */
688
1092
  ::-webkit-scrollbar { width: 6px; height: 6px; }
689
1093
  ::-webkit-scrollbar-track { background: transparent; }
@@ -734,6 +1138,11 @@
734
1138
  </div>
735
1139
 
736
1140
  <div class="nav-group-label" style="margin-top:20px;">Engine Tools</div>
1141
+ <div class="nav-item" data-page="vfx-database">
1142
+ <svg viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
1143
+ VFX Database
1144
+ <span class="vfx-sidebar-badge" id="vfx-sidebar-count">0</span>
1145
+ </div>
737
1146
  <div class="nav-item" data-page="docs">
738
1147
  <svg viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
739
1148
  Docs Explorer
@@ -742,12 +1151,12 @@
742
1151
  <div class="nav-group-label" style="margin-top:20px;">Info</div>
743
1152
  <div class="nav-item" data-page="about">
744
1153
  <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
745
- About v2.3
1154
+ About v2.4
746
1155
  </div>
747
1156
  </nav>
748
1157
 
749
1158
  <div class="sidebar-footer">
750
- <span class="badge-v2" id="pkg-version">v2.3.0</span>
1159
+ <span class="badge-v2" id="pkg-version">v2.4.1</span>
751
1160
  <span style="font-size:10px; opacity:0.6;">Model Context Protocol</span>
752
1161
  </div>
753
1162
  </aside>
@@ -917,11 +1326,165 @@
917
1326
  </div>
918
1327
  </div>
919
1328
 
1329
+ <!-- VFX Database & Library Page -->
1330
+ <div class="page" id="page-vfx-database">
1331
+ <div class="vfx-container">
1332
+ <div class="vfx-header">
1333
+ <div class="vfx-header-info">
1334
+ <h2>✨ NV-VFX Library & Database <span class="v2-chip" id="vfx-hero-badge">Portable</span></h2>
1335
+ <p>Katalog efek visual portabel lintas-proyek Roblox (ParticleEmitter, Beam, Trail, Light). Pindai dari satu place dan pasang ke place lain secara instan.</p>
1336
+ </div>
1337
+ <div class="vfx-actions-bar">
1338
+ <div class="vfx-search-box">
1339
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
1340
+ <input type="text" id="vfx-search-input" placeholder="Cari VFX berdasarkan nama, tag, atau kategori...">
1341
+ </div>
1342
+ <button class="vfx-btn-primary" id="btn-open-scan-modal">
1343
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
1344
+ Scan dari Studio
1345
+ </button>
1346
+ <button class="vfx-btn-secondary" id="btn-refresh-vfx" title="Muat ulang data">
1347
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M23 4v6h-6"/><path d="M1 20v-6h6"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>
1348
+ </button>
1349
+ </div>
1350
+ </div>
1351
+
1352
+ <!-- Category Pills Segment Bar -->
1353
+ <div class="vfx-category-tabs" id="vfx-category-tabs">
1354
+ <button class="vfx-cat-pill active" data-cat="All">Semua <span class="vfx-cat-count" id="vfx-count-all">0</span></button>
1355
+ </div>
1356
+
1357
+ <!-- Stats Overview Row -->
1358
+ <div class="vfx-stats-row">
1359
+ <div class="vfx-stat-box">
1360
+ <div class="vfx-stat-label">Total Efek Visual</div>
1361
+ <div class="vfx-stat-val" id="vfx-stat-total">0</div>
1362
+ <div class="vfx-stat-sub">Tersimpan di database MCP</div>
1363
+ </div>
1364
+ <div class="vfx-stat-box">
1365
+ <div class="vfx-stat-label">Kategori Aktif</div>
1366
+ <div class="vfx-stat-val" id="vfx-stat-categories" style="color:#00aaff;">0</div>
1367
+ <div class="vfx-stat-sub">Slash, Aura, Magic, Explosion dll</div>
1368
+ </div>
1369
+ <div class="vfx-stat-box">
1370
+ <div class="vfx-stat-label">Total Emitters</div>
1371
+ <div class="vfx-stat-val" id="vfx-stat-emitters" style="color:#f59e0b;">0</div>
1372
+ <div class="vfx-stat-sub">ParticleEmitter aktif</div>
1373
+ </div>
1374
+ <div class="vfx-stat-box">
1375
+ <div class="vfx-stat-label">Beams & Trails</div>
1376
+ <div class="vfx-stat-val" id="vfx-stat-beams" style="color:#10b981;">0</div>
1377
+ <div class="vfx-stat-sub">Elemen trajektori & sinar</div>
1378
+ </div>
1379
+ </div>
1380
+
1381
+ <!-- Cards Grid -->
1382
+ <div class="vfx-grid" id="vfx-cards-grid">
1383
+ <!-- Kartu VFX di-render secara dinamis oleh JavaScript -->
1384
+ </div>
1385
+ </div>
1386
+ </div>
1387
+
1388
+ <!-- Modal: Scan VFX dari Studio -->
1389
+ <div class="vfx-modal-backdrop" id="modal-scan-vfx">
1390
+ <div class="vfx-modal-card">
1391
+ <div class="vfx-modal-header">
1392
+ <h3>📥 Pindai VFX dari Studio Aktif</h3>
1393
+ <button class="vfx-modal-close" onclick="closeVfxModals()">&times;</button>
1394
+ </div>
1395
+ <div class="vfx-modal-body">
1396
+ <div class="vfx-form-group">
1397
+ <label class="vfx-form-label">Path Objek di Studio (Target)</label>
1398
+ <input type="text" class="vfx-form-input" id="scan-input-target" placeholder="contoh: Workspace.FlameBurst atau Selection" value="Workspace.">
1399
+ </div>
1400
+ <div class="vfx-form-group">
1401
+ <label class="vfx-form-label">Nama VFX</label>
1402
+ <input type="text" class="vfx-form-input" id="scan-input-name" placeholder="contoh: Crimson_Flame_Slash">
1403
+ </div>
1404
+ <div class="vfx-form-group">
1405
+ <label class="vfx-form-label">Kategori</label>
1406
+ <select class="vfx-form-select" id="scan-select-category">
1407
+ <option value="Slash">Slash (Tebasan & Bilah)</option>
1408
+ <option value="Explosion">Explosion (Ledakan & Api)</option>
1409
+ <option value="Aura">Aura (Glow & Perisai)</option>
1410
+ <option value="Magic">Magic (Sihir & Lingkaran Rune)</option>
1411
+ <option value="Beam">Beam (Laser & Sinar Energi)</option>
1412
+ <option value="Impact">Impact (Benturan & Hantaman)</option>
1413
+ <option value="Projectile">Projectile (Proyektil & Peluru)</option>
1414
+ <option value="Ambient">Ambient (Lingkungan & Partikel Halus)</option>
1415
+ </select>
1416
+ </div>
1417
+ <div class="vfx-form-group">
1418
+ <label class="vfx-form-label">Tags (Pisahkan dengan koma)</label>
1419
+ <input type="text" class="vfx-form-input" id="scan-input-tags" placeholder="contoh: anime, sword, fire, fast">
1420
+ </div>
1421
+ </div>
1422
+ <div class="vfx-modal-footer">
1423
+ <button class="vfx-btn-secondary" onclick="closeVfxModals()">Batal</button>
1424
+ <button class="vfx-btn-primary" id="btn-submit-scan">Mulai Pindai</button>
1425
+ </div>
1426
+ </div>
1427
+ </div>
1428
+
1429
+ <!-- Modal: Apply VFX ke Studio -->
1430
+ <div class="vfx-modal-backdrop" id="modal-apply-vfx">
1431
+ <div class="vfx-modal-card">
1432
+ <div class="vfx-modal-header">
1433
+ <h3>⚡ Terapkan VFX ke Studio</h3>
1434
+ <button class="vfx-modal-close" onclick="closeVfxModals()">&times;</button>
1435
+ </div>
1436
+ <div class="vfx-modal-body">
1437
+ <input type="hidden" id="apply-vfx-id">
1438
+ <div class="vfx-form-group">
1439
+ <label class="vfx-form-label">VFX Terpilih</label>
1440
+ <div id="apply-vfx-title" style="font-weight:600; color:var(--accent);"></div>
1441
+ </div>
1442
+ <div class="vfx-form-group">
1443
+ <label class="vfx-form-label">Target Instance di Studio</label>
1444
+ <input type="text" class="vfx-form-input" id="apply-input-target" placeholder="contoh: Workspace.Dummy.RightHand atau Workspace" value="Workspace">
1445
+ </div>
1446
+ <div class="vfx-form-group">
1447
+ <label class="vfx-form-label">Skala Ukuran (Scale Factor)</label>
1448
+ <input type="number" class="vfx-form-input" id="apply-input-scale" value="1.0" step="0.1" min="0.1" max="10">
1449
+ </div>
1450
+ <div class="vfx-form-group">
1451
+ <label class="vfx-form-label">Status Partikel (Enabled)</label>
1452
+ <select class="vfx-form-select" id="apply-select-enabled">
1453
+ <option value="default">Sesuai Preset / Default</option>
1454
+ <option value="true">Paksa Aktif (Enabled = true)</option>
1455
+ <option value="false">Paksa Nonaktif (Enabled = false)</option>
1456
+ </select>
1457
+ </div>
1458
+ </div>
1459
+ <div class="vfx-modal-footer">
1460
+ <button class="vfx-btn-secondary" onclick="closeVfxModals()">Batal</button>
1461
+ <button class="vfx-btn-primary" id="btn-submit-apply">Terapkan Sekarang</button>
1462
+ </div>
1463
+ </div>
1464
+ </div>
1465
+
1466
+ <!-- Modal: View JSON / Inspect Data -->
1467
+ <div class="vfx-modal-backdrop" id="modal-json-vfx">
1468
+ <div class="vfx-modal-card" style="max-width:700px;">
1469
+ <div class="vfx-modal-header">
1470
+ <h3 id="json-modal-title">Inspeksi Data Murni VFX</h3>
1471
+ <button class="vfx-modal-close" onclick="closeVfxModals()">&times;</button>
1472
+ </div>
1473
+ <div class="vfx-modal-body" style="padding:0;">
1474
+ <pre id="json-modal-content" style="background:#0d0d0d; padding:16px; margin:0; font-family:'JetBrains Mono', monospace; font-size:12px; color:#e0e0e0; overflow:auto; max-height:55vh;"></pre>
1475
+ </div>
1476
+ <div class="vfx-modal-footer">
1477
+ <button class="vfx-btn-secondary" id="btn-copy-vfx-json">Salin JSON</button>
1478
+ <button class="vfx-btn-primary" onclick="closeVfxModals()">Tutup</button>
1479
+ </div>
1480
+ </div>
1481
+ </div>
1482
+
920
1483
  <!-- About Page -->
921
1484
  <div class="page" id="page-about">
922
1485
  <div class="about-section">
923
1486
  <div class="about-hero">
924
- <h2>nvstudio-mcp <span class="v2-chip" id="about-version-chip">v2.3.0</span></h2>
1487
+ <h2>nvstudio-mcp <span class="v2-chip" id="about-version-chip">v2.4.1</span></h2>
925
1488
  <p>Jembatan pintar berbasis <strong>Model Context Protocol (MCP)</strong> yang menghubungkan AI Agent (Antigravity IDE, Cursor, Claude Desktop, Windsurf) secara langsung ke dalam <strong>Roblox Studio</strong>.</p>
926
1489
  <div class="about-links">
927
1490
  <a class="about-link" href="https://github.com/Adiholick/nvstudio-mcp" target="_blank">
@@ -936,23 +1499,25 @@
936
1499
  </div>
937
1500
 
938
1501
  <div class="about-card">
939
- <h3>🚀 Fitur Baru v2.3.0</h3>
1502
+ <h3>🚀 Fitur Baru v2.4.1</h3>
940
1503
  <ul>
941
- <li><strong>⚡ Batch Instance Builder:</strong> Membangun pohon hierarki GUI puluhan tingkat dalam 1 kali roundtrip via <code>batch_create_instances</code> dan update massal via <code>batch_set_properties</code>.</li>
942
- <li><strong>🛡️ Asset Security Scanner:</strong> Memindai dan memusnahkan 10 jenis ancaman backdoor/malware pada aset sebelum dimasukkan ke game via <code>preview_asset</code> dan <code>scan_security</code>.</li>
943
- <li><strong>📚 Live Creator Docs Lookup:</strong> Mengambil dokumentasi engine resmi dari Roblox Creator Docs secara offline/online dengan disk cache berkecepatan ~2 ms via <code>get_roblox_docs</code>.</li>
944
- <li><strong>🎨 NV-Series GUI Standard Taste:</strong> Standardisasi tata letak Frame, LayoutOrder teratur, Spacer margin ScrollingFrame, dan deteksi perangkat responsif pintar (PC, Phone, Tablet, Console).</li>
1504
+ <li><strong>🦴 NV-Rig Engine:</strong> Rigging headless presisi $O(1)$ untuk R6, R15, dan Custom Rig. Menyediakan inspeksi sendi (<code>inspect_rig</code>), pemindahan poros C0/C1 simultan anti-dislokasi (<code>edit_rig_joint</code>), auto-rigging instan (<code>auto_rig</code>), dan assembly welding senjata/prop (<code>weld_assembly</code>).</li>
1505
+ <li><strong>🎬 NV-Animation Engine:</strong> Pembuatan animasi karakter halus alami (Walk, Slash, Punch, Idle, IK) langsung menjadi <code>KeyframeSequence</code> resmi dengan interpolasi Bezier (Cubic/Quad) dan penegakan batas anatomis aman tanpa glitch.</li>
1506
+ <li><strong>✨ NV-VFX Subsystem & Database:</strong> Serialisasi efek visual (ParticleEmitter, Beam, Trail, PointLight) lintas-proyek dengan tool <code>scan_vfx</code> dan rekonstruksi instan via <code>apply_vfx</code>, dilengkapi web category browser interaktif.</li>
1507
+ <li><strong>⚡ Batch Instance Builder & Security:</strong> Membangun hierarki massal, sanitasi keamanan backdoor aset, dan live Creator Docs lookup.</li>
945
1508
  </ul>
946
1509
  </div>
947
1510
 
948
1511
  <div class="about-card">
949
1512
  <h3>🛠️ Daftar Semantic MCP Tools Lengkap</h3>
950
1513
  <ul>
951
- <li><code>batch_create_instances</code> — Pembuatan objek/hierarki massal pohon bersarang.</li>
952
- <li><code>batch_set_properties</code> — Pembaruan properti multi-instance secara atomik.</li>
1514
+ <li><code>inspect_rig</code> / <code>edit_rig_joint</code> / <code>auto_rig</code> / <code>weld_assembly</code> — NV-Rig Engine headless.</li>
1515
+ <li><code>animate_character</code> / <code>solve_limb_ik</code> / <code>scrub_timeline</code> — NV-Animation Engine.</li>
1516
+ <li><code>scan_vfx</code> / <code>apply_vfx</code> / <code>list_vfx_database</code> / <code>delete_vfx</code> — NV-VFX Subsystem & Database.</li>
1517
+ <li><code>batch_create_instances</code> / <code>batch_set_properties</code> — Pembuatan objek/hierarki massal pohon bersarang.</li>
953
1518
  <li><code>get_roblox_docs</code> — Lookup dokumentasi resmi engine Roblox (Class, Datatype, Service, Enum).</li>
954
1519
  <li><code>preview_asset</code> / <code>scan_security</code> — Audit keamanan backdoor & sanitasi script model.</li>
955
- <li><code>patch_script_source</code> — Mengedit bagian baris kode tertentu secara parsial.</li>
1520
+ <li><code>patch_script_source</code> / <code>update_script_source</code> — Mengedit script Roblox secara terisolasi.</li>
956
1521
  <li><code>execute_luau</code> — Eksekusi sembarang kode Luau dinamis di Studio.</li>
957
1522
  <li><code>get_full_tree</code> / <code>get_children</code> — Eksplorasi hierarki DataModel.</li>
958
1523
  <li><code>insert_asset</code> / <code>generate_terrain</code> — Memasukkan model atau membuat terrain voxel.</li>
@@ -990,8 +1555,9 @@
990
1555
  const pageTitles = {
991
1556
  activity: 'Activity Log',
992
1557
  stats: 'Session Diagnostics & Studios',
1558
+ 'vfx-database': 'NV-VFX Database & Library',
993
1559
  docs: 'Roblox Docs Explorer',
994
- about: 'About v2.3'
1560
+ about: 'About v2.4'
995
1561
  };
996
1562
 
997
1563
  navItems.forEach(item => {
@@ -1083,7 +1649,13 @@
1083
1649
  }
1084
1650
 
1085
1651
  function escapeHtml(str) {
1086
- return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
1652
+ if (str === null || str === undefined) return '';
1653
+ return String(str)
1654
+ .replace(/&/g, '&amp;')
1655
+ .replace(/</g, '&lt;')
1656
+ .replace(/>/g, '&gt;')
1657
+ .replace(/"/g, '&quot;')
1658
+ .replace(/'/g, '&#39;');
1087
1659
  }
1088
1660
 
1089
1661
  function shouldShowLog(log) {
@@ -1364,6 +1936,329 @@
1364
1936
  }
1365
1937
  });
1366
1938
 
1939
+ /* ── VFX Database & Library Controller ── */
1940
+ let currentVfxItems = [];
1941
+ let currentVfxCategories = [];
1942
+ let activeVfxCategory = 'All';
1943
+ let vfxSearchTimeout = null;
1944
+
1945
+ const vfxSidebarCount = document.getElementById('vfx-sidebar-count');
1946
+ const vfxCategoryTabs = document.getElementById('vfx-category-tabs');
1947
+ const vfxCardsGrid = document.getElementById('vfx-cards-grid');
1948
+ const vfxSearchInput = document.getElementById('vfx-search-input');
1949
+ const vfxStatTotal = document.getElementById('vfx-stat-total');
1950
+ const vfxStatCategories = document.getElementById('vfx-stat-categories');
1951
+ const vfxStatEmitters = document.getElementById('vfx-stat-emitters');
1952
+ const vfxStatBeams = document.getElementById('vfx-stat-beams');
1953
+
1954
+ const modalScanVfx = document.getElementById('modal-scan-vfx');
1955
+ const modalApplyVfx = document.getElementById('modal-apply-vfx');
1956
+ const modalJsonVfx = document.getElementById('modal-json-vfx');
1957
+
1958
+ window.closeVfxModals = function() {
1959
+ modalScanVfx.classList.remove('open');
1960
+ modalApplyVfx.classList.remove('open');
1961
+ modalJsonVfx.classList.remove('open');
1962
+ };
1963
+
1964
+ window.openApplyModal = function(id) {
1965
+ const decodedId = decodeURIComponent(id);
1966
+ const item = currentVfxItems.find(i => i.id === decodedId);
1967
+ if (!item) return;
1968
+ document.getElementById('apply-vfx-id').value = item.id;
1969
+ document.getElementById('apply-vfx-title').textContent = item.name;
1970
+ modalApplyVfx.classList.add('open');
1971
+ };
1972
+
1973
+ window.openJsonModal = function(id) {
1974
+ const decodedId = decodeURIComponent(id);
1975
+ const item = currentVfxItems.find(i => i.id === decodedId);
1976
+ if (!item) return;
1977
+ document.getElementById('json-modal-title').textContent = 'Pure Data: ' + item.name;
1978
+ document.getElementById('json-modal-content').textContent = JSON.stringify(item.data, null, 2);
1979
+ modalJsonVfx.classList.add('open');
1980
+ };
1981
+
1982
+ window.deleteVfxItem = async function(id) {
1983
+ const decodedId = decodeURIComponent(id);
1984
+ const item = currentVfxItems.find(i => i.id === decodedId);
1985
+ const name = item ? item.name : decodedId;
1986
+ if (!confirm(`Apakah Anda yakin ingin menghapus VFX "${name}" dari database?`)) return;
1987
+ try {
1988
+ const res = await fetch('/api/vfx/' + encodeURIComponent(decodedId), { method: 'DELETE' });
1989
+ if (res.ok) {
1990
+ loadVfxDatabase();
1991
+ } else {
1992
+ alert('Gagal menghapus VFX');
1993
+ }
1994
+ } catch (err) {
1995
+ alert('Error: ' + err.message);
1996
+ }
1997
+ };
1998
+
1999
+ async function loadVfxDatabase() {
2000
+ try {
2001
+ const [vfxRes, catRes] = await Promise.all([
2002
+ fetch('/api/vfx' + (activeVfxCategory !== 'All' ? `?category=${encodeURIComponent(activeVfxCategory)}` : '')),
2003
+ fetch('/api/vfx/categories')
2004
+ ]);
2005
+ if (!vfxRes.ok || !catRes.ok) return;
2006
+
2007
+ const vfxData = await vfxRes.json();
2008
+ const catData = await catRes.json();
2009
+
2010
+ currentVfxItems = vfxData.items || [];
2011
+ currentVfxCategories = catData.categories || [];
2012
+
2013
+ renderVfxCategories();
2014
+ renderVfxStats();
2015
+ renderVfxCards();
2016
+
2017
+ // Hitung total di sidebar
2018
+ let totalCount = 0;
2019
+ currentVfxCategories.forEach(c => totalCount += c.count);
2020
+ if (vfxSidebarCount) vfxSidebarCount.textContent = totalCount;
2021
+ } catch (err) {
2022
+ console.error('Gagal memuat VFX Database:', err);
2023
+ }
2024
+ }
2025
+
2026
+ function renderVfxCategories() {
2027
+ if (!vfxCategoryTabs) return;
2028
+ let totalCount = 0;
2029
+ currentVfxCategories.forEach(c => totalCount += c.count);
2030
+
2031
+ let html = `
2032
+ <button class="vfx-cat-pill ${activeVfxCategory === 'All' ? 'active' : ''}" data-cat="All">
2033
+ Semua <span class="vfx-cat-count">${totalCount}</span>
2034
+ </button>
2035
+ `;
2036
+
2037
+ currentVfxCategories.forEach(cat => {
2038
+ html += `
2039
+ <button class="vfx-cat-pill ${activeVfxCategory === cat.name ? 'active' : ''}" data-cat="${escapeHtml(cat.name)}">
2040
+ ${escapeHtml(cat.name)} <span class="vfx-cat-count">${cat.count}</span>
2041
+ </button>
2042
+ `;
2043
+ });
2044
+
2045
+ vfxCategoryTabs.innerHTML = html;
2046
+
2047
+ vfxCategoryTabs.querySelectorAll('.vfx-cat-pill').forEach(btn => {
2048
+ btn.addEventListener('click', () => {
2049
+ activeVfxCategory = btn.dataset.cat;
2050
+ vfxCategoryTabs.querySelectorAll('.vfx-cat-pill').forEach(b => b.classList.remove('active'));
2051
+ btn.classList.add('active');
2052
+ loadVfxDatabase();
2053
+ });
2054
+ });
2055
+ }
2056
+
2057
+ function renderVfxStats() {
2058
+ let totalCount = 0;
2059
+ let totalEmitters = 0;
2060
+ let totalBeams = 0;
2061
+
2062
+ currentVfxItems.forEach(item => {
2063
+ totalCount++;
2064
+ if (item.stats) {
2065
+ totalEmitters += (item.stats.emitters || 0);
2066
+ totalBeams += ((item.stats.beams || 0) + (item.stats.trails || 0));
2067
+ }
2068
+ });
2069
+
2070
+ if (vfxStatTotal) vfxStatTotal.textContent = totalCount;
2071
+ if (vfxStatCategories) vfxStatCategories.textContent = currentVfxCategories.length;
2072
+ if (vfxStatEmitters) vfxStatEmitters.textContent = totalEmitters;
2073
+ if (vfxStatBeams) vfxStatBeams.textContent = totalBeams;
2074
+ }
2075
+
2076
+ function renderVfxCards() {
2077
+ if (!vfxCardsGrid) return;
2078
+ if (currentVfxItems.length === 0) {
2079
+ vfxCardsGrid.innerHTML = `
2080
+ <div style="grid-column: 1 / -1; padding: 48px; text-align: center; color: var(--text-dim); background: var(--surface-card); border: 1px dashed var(--border); border-radius: 12px;">
2081
+ <div style="font-size: 32px; margin-bottom: 8px;">✨</div>
2082
+ <div style="font-size: 15px; font-weight: 600; color: var(--text);">Belum ada efek visual di kategori ini</div>
2083
+ <div style="font-size: 13px; margin-top: 4px;">Klik tombol "Scan dari Studio" di atas untuk memindai partikel dari proyek aktif Anda.</div>
2084
+ </div>
2085
+ `;
2086
+ return;
2087
+ }
2088
+
2089
+ const filterQuery = (vfxSearchInput ? vfxSearchInput.value.toLowerCase().trim() : '');
2090
+ const items = filterQuery
2091
+ ? currentVfxItems.filter(i =>
2092
+ i.name.toLowerCase().includes(filterQuery) ||
2093
+ i.category.toLowerCase().includes(filterQuery) ||
2094
+ (i.description && i.description.toLowerCase().includes(filterQuery)) ||
2095
+ (i.tags && i.tags.some(t => t.toLowerCase().includes(filterQuery))))
2096
+ : currentVfxItems;
2097
+
2098
+ vfxCardsGrid.innerHTML = items.map(item => {
2099
+ const dateStr = item.createdAt ? new Date(item.createdAt).toLocaleDateString('id-ID', { day:'numeric', month:'short', year:'numeric' }) : 'NVStudio';
2100
+ const stats = item.stats || { emitters: 0, beams: 0, trails: 0, lights: 0 };
2101
+ const compPills = [];
2102
+ if (stats.emitters > 0) compPills.push(`${stats.emitters} Emitters`);
2103
+ if (stats.beams > 0) compPills.push(`${stats.beams} Beams`);
2104
+ if (stats.trails > 0) compPills.push(`${stats.trails} Trails`);
2105
+ if (stats.lights > 0) compPills.push(`${stats.lights} Lights`);
2106
+ if (compPills.length === 0) compPills.push(`${item.nodeCount || 1} Nodes`);
2107
+
2108
+ const tagChips = (item.tags || []).slice(0, 4).map(t => `<span class="vfx-tag-chip">#${escapeHtml(t)}</span>`).join('');
2109
+
2110
+ return `
2111
+ <div class="vfx-card">
2112
+ <div class="vfx-card-top">
2113
+ <div class="vfx-card-title-group">
2114
+ <h4>${escapeHtml(item.name)}</h4>
2115
+ <div class="vfx-card-date">${escapeHtml(dateStr)} • ${escapeHtml(item.author || 'NVStudio')}</div>
2116
+ </div>
2117
+ <span class="vfx-badge-cat vfx-badge-${escapeHtml(item.category || 'Ambient')}">${escapeHtml(item.category || 'Ambient')}</span>
2118
+ </div>
2119
+
2120
+ <div class="vfx-card-desc">${escapeHtml(item.description || 'Efek visual siap pakai untuk proyek Roblox.')}</div>
2121
+
2122
+ <div class="vfx-composition-badges">
2123
+ ${compPills.map(p => `<span class="vfx-comp-pill">${escapeHtml(p)}</span>`).join('')}
2124
+ </div>
2125
+
2126
+ ${tagChips ? `<div class="vfx-card-tags">${tagChips}</div>` : ''}
2127
+
2128
+ <div class="vfx-card-footer">
2129
+ <button class="vfx-btn-apply" onclick="openApplyModal('${encodeURIComponent(item.id)}')">
2130
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
2131
+ Apply ke Studio
2132
+ </button>
2133
+ <button class="vfx-btn-json" title="Inspeksi Data JSON" onclick="openJsonModal('${encodeURIComponent(item.id)}')">
2134
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
2135
+ </button>
2136
+ <button class="vfx-btn-del" title="Hapus dari Database" onclick="deleteVfxItem('${encodeURIComponent(item.id)}')">
2137
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
2138
+ </button>
2139
+ </div>
2140
+ </div>
2141
+ `;
2142
+ }).join('');
2143
+ }
2144
+
2145
+ // Search debounce
2146
+ if (vfxSearchInput) {
2147
+ vfxSearchInput.addEventListener('input', () => {
2148
+ clearTimeout(vfxSearchTimeout);
2149
+ vfxSearchTimeout = setTimeout(() => {
2150
+ renderVfxCards();
2151
+ }, 180);
2152
+ });
2153
+ }
2154
+
2155
+ // Modal triggers & handlers
2156
+ const btnOpenScanModal = document.getElementById('btn-open-scan-modal');
2157
+ if (btnOpenScanModal) {
2158
+ btnOpenScanModal.addEventListener('click', () => {
2159
+ modalScanVfx.classList.add('open');
2160
+ });
2161
+ }
2162
+
2163
+ const btnRefreshVfx = document.getElementById('btn-refresh-vfx');
2164
+ if (btnRefreshVfx) {
2165
+ btnRefreshVfx.addEventListener('click', () => {
2166
+ loadVfxDatabase();
2167
+ });
2168
+ }
2169
+
2170
+ // Submit Scan form
2171
+ const btnSubmitScan = document.getElementById('btn-submit-scan');
2172
+ if (btnSubmitScan) {
2173
+ btnSubmitScan.addEventListener('click', async () => {
2174
+ const target = document.getElementById('scan-input-target').value.trim();
2175
+ const name = document.getElementById('scan-input-name').value.trim();
2176
+ const category = document.getElementById('scan-select-category').value;
2177
+ const tags = document.getElementById('scan-input-tags').value.split(',').map(t => t.trim()).filter(Boolean);
2178
+
2179
+ if (!target) {
2180
+ alert('Harap isi path target di Studio');
2181
+ return;
2182
+ }
2183
+
2184
+ btnSubmitScan.disabled = true;
2185
+ btnSubmitScan.textContent = 'Memindai...';
2186
+ try {
2187
+ const res = await fetch('/api/vfx/scan', {
2188
+ method: 'POST',
2189
+ headers: { 'Content-Type': 'application/json' },
2190
+ body: JSON.stringify({ target, name, category, tags })
2191
+ });
2192
+ const data = await res.json();
2193
+ if (res.ok && data.success) {
2194
+ closeVfxModals();
2195
+ loadVfxDatabase();
2196
+ alert(`✅ Sukses memindai VFX "${data.item.name}" (${data.item.nodeCount} node)!`);
2197
+ } else {
2198
+ alert('Gagal scan: ' + (data.error || 'Unknown error'));
2199
+ }
2200
+ } catch (err) {
2201
+ alert('Error: ' + err.message);
2202
+ } finally {
2203
+ btnSubmitScan.disabled = false;
2204
+ btnSubmitScan.textContent = 'Mulai Pindai';
2205
+ }
2206
+ });
2207
+ }
2208
+
2209
+ // Submit Apply form
2210
+ const btnSubmitApply = document.getElementById('btn-submit-apply');
2211
+ if (btnSubmitApply) {
2212
+ btnSubmitApply.addEventListener('click', async () => {
2213
+ const id = document.getElementById('apply-vfx-id').value;
2214
+ const target = document.getElementById('apply-input-target').value.trim();
2215
+ const scale = parseFloat(document.getElementById('apply-input-scale').value) || 1.0;
2216
+ const enabledVal = document.getElementById('apply-select-enabled').value;
2217
+ const enabled = enabledVal === 'default' ? undefined : (enabledVal === 'true');
2218
+
2219
+ if (!target) {
2220
+ alert('Harap isi target di Studio');
2221
+ return;
2222
+ }
2223
+
2224
+ btnSubmitApply.disabled = true;
2225
+ btnSubmitApply.textContent = 'Menerapkan...';
2226
+ try {
2227
+ const res = await fetch('/api/vfx/apply', {
2228
+ method: 'POST',
2229
+ headers: { 'Content-Type': 'application/json' },
2230
+ body: JSON.stringify({ id, target, scale, enabled })
2231
+ });
2232
+ const data = await res.json();
2233
+ if (res.ok && data.success) {
2234
+ closeVfxModals();
2235
+ alert(`✅ VFX berhasil diterapkan ke "${target}" di Studio!`);
2236
+ } else {
2237
+ alert('Gagal apply: ' + (data.error || 'Unknown error'));
2238
+ }
2239
+ } catch (err) {
2240
+ alert('Error: ' + err.message);
2241
+ } finally {
2242
+ btnSubmitApply.disabled = false;
2243
+ btnSubmitApply.textContent = 'Terapkan Sekarang';
2244
+ }
2245
+ });
2246
+ }
2247
+
2248
+ // Copy JSON button
2249
+ const btnCopyVfxJson = document.getElementById('btn-copy-vfx-json');
2250
+ if (btnCopyVfxJson) {
2251
+ btnCopyVfxJson.addEventListener('click', () => {
2252
+ const content = document.getElementById('json-modal-content').textContent;
2253
+ navigator.clipboard.writeText(content);
2254
+ btnCopyVfxJson.textContent = 'Tersalin ✓';
2255
+ setTimeout(() => { btnCopyVfxJson.textContent = 'Salin JSON'; }, 1500);
2256
+ });
2257
+ }
2258
+
2259
+ // Muat VFX Database saat pertama kali dibuka
2260
+ loadVfxDatabase();
2261
+
1367
2262
  /* ── Socket.IO Event Listeners ── */
1368
2263
  socket.on('connect', () => {});
1369
2264
  socket.on('disconnect', () => setStudioStatus(false));
@@ -1385,6 +2280,10 @@
1385
2280
  updateStats(s);
1386
2281
  });
1387
2282
 
2283
+ socket.on('vfx_updated', () => {
2284
+ loadVfxDatabase();
2285
+ });
2286
+
1388
2287
  socket.on('studio-status', ({ connected, studioCount, connectedStudios, version }) => {
1389
2288
  setStudioStatus(connected, studioCount);
1390
2289
  if (connectedStudios) updateStudiosTable(connectedStudios);