@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/README.md +17 -3
- package/agent-plugin/rules/AGENTS.md +56 -0
- package/agent-plugin/skills/nvstudio-animation-vfx/SKILL.md +217 -0
- package/agent-plugin/skills/nvstudio-mcp-guide/SKILL.md +14 -0
- package/agent-plugin/skills/nvstudio-rig-engine/SKILL.md +170 -0
- package/dist/index.js +625 -0
- package/dist/roblox-bridge.js +108 -0
- package/dist/vfx-database.js +507 -0
- package/package.json +2 -2
- package/public/index.html +913 -14
- package/studio-plugin/Main.server.lua +1 -1
- package/studio-plugin/animate_character.lua +36 -0
- package/studio-plugin/apply_vfx.lua +59 -0
- package/studio-plugin/edit_rig.lua +35 -0
- package/studio-plugin/nv_anim_dispatcher.lua +239 -0
- package/studio-plugin/nv_auto_rigger.lua +392 -0
- package/studio-plugin/nv_curves.lua +260 -0
- package/studio-plugin/nv_joint_math.lua +138 -0
- package/studio-plugin/nv_kinematics.lua +248 -0
- package/studio-plugin/nv_motion_generator.lua +279 -0
- package/studio-plugin/nv_rig_dispatcher.lua +319 -0
- package/studio-plugin/nv_vfx_serializer.lua +363 -0
- package/studio-plugin/nvstudio_mcp.rbxmx +3008 -115
- package/studio-plugin/scan_vfx.lua +85 -0
- package/studio-plugin/scrub_timeline.lua +122 -0
- package/studio-plugin/set_properties.lua +73 -0
- package/studio-plugin/solve_limb_ik.lua +63 -0
- package/studio-plugin/ui_manager.lua +1 -1
package/dist/index.js
CHANGED
|
@@ -13,6 +13,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
13
13
|
const path_1 = __importDefault(require("path"));
|
|
14
14
|
const luau_validator_1 = require("./luau-validator");
|
|
15
15
|
const roblox_docs_1 = require("./roblox-docs");
|
|
16
|
+
const vfx_database_1 = require("./vfx-database");
|
|
16
17
|
const HISTORY_DIR = path_1.default.join(__dirname, '..', '.history');
|
|
17
18
|
if (!fs_1.default.existsSync(HISTORY_DIR)) {
|
|
18
19
|
fs_1.default.mkdirSync(HISTORY_DIR, { recursive: true });
|
|
@@ -641,6 +642,372 @@ const TOOLS = [
|
|
|
641
642
|
required: ["updates"],
|
|
642
643
|
},
|
|
643
644
|
},
|
|
645
|
+
{
|
|
646
|
+
name: "scan_vfx",
|
|
647
|
+
description: "Pindai efek visual (ParticleEmitter, Beam, Trail, Light) pada objek di Roblox Studio dan simpan ke database persisten MCP untuk digunakan lintas-proyek.",
|
|
648
|
+
inputSchema: {
|
|
649
|
+
type: "object",
|
|
650
|
+
properties: {
|
|
651
|
+
target: {
|
|
652
|
+
type: "string",
|
|
653
|
+
description: "Path objek target di Roblox Studio (contoh: 'Workspace.MagicSpell', 'ReplicatedStorage.SlashFX').",
|
|
654
|
+
},
|
|
655
|
+
name: {
|
|
656
|
+
type: "string",
|
|
657
|
+
description: "Nama untuk efek visual ini di database MCP (opsional, default: nama objek).",
|
|
658
|
+
},
|
|
659
|
+
category: {
|
|
660
|
+
type: "string",
|
|
661
|
+
description: "Kategori efek visual (contoh: 'Slash', 'Explosion', 'Aura', 'Projectile', 'Magic', 'Impact', 'Beam', 'Ambient').",
|
|
662
|
+
enum: ["Slash", "Explosion", "Aura", "Projectile", "Magic", "Impact", "Beam", "Ambient"],
|
|
663
|
+
},
|
|
664
|
+
tags: {
|
|
665
|
+
type: "array",
|
|
666
|
+
items: { type: "string" },
|
|
667
|
+
description: "Daftar kata kunci pencarian (contoh: ['sword', 'cyan', 'fast']).",
|
|
668
|
+
},
|
|
669
|
+
description: {
|
|
670
|
+
type: "string",
|
|
671
|
+
description: "Catatan penjelasan tentang efek visual ini.",
|
|
672
|
+
},
|
|
673
|
+
placeId: {
|
|
674
|
+
type: "number",
|
|
675
|
+
description: "Target Place ID untuk multi-Studio routing.",
|
|
676
|
+
},
|
|
677
|
+
studioId: {
|
|
678
|
+
type: "string",
|
|
679
|
+
description: "Target Studio session ID.",
|
|
680
|
+
},
|
|
681
|
+
},
|
|
682
|
+
required: ["target"],
|
|
683
|
+
},
|
|
684
|
+
},
|
|
685
|
+
{
|
|
686
|
+
name: "apply_vfx",
|
|
687
|
+
description: "Terapkan efek visual yang tersimpan di database persisten MCP ke objek target di Roblox Studio aktif dengan fidelitas 100%.",
|
|
688
|
+
inputSchema: {
|
|
689
|
+
type: "object",
|
|
690
|
+
properties: {
|
|
691
|
+
vfx_id: {
|
|
692
|
+
type: "string",
|
|
693
|
+
description: "ID efek visual dari database MCP (contoh: 'preset-cyan-slash', atau ID hasil scan).",
|
|
694
|
+
},
|
|
695
|
+
target: {
|
|
696
|
+
type: "string",
|
|
697
|
+
description: "Path objek tujuan di Studio tempat VFX akan dipasang (contoh: 'Workspace.Player.RightHand', 'Workspace.Map').",
|
|
698
|
+
},
|
|
699
|
+
scale: {
|
|
700
|
+
type: "number",
|
|
701
|
+
description: "Faktor skala ukuran efek visual (contoh: 1.0 = normal, 1.5 = 50% lebih besar, 0.5 = separuh). Default: 1.0.",
|
|
702
|
+
},
|
|
703
|
+
enabled: {
|
|
704
|
+
type: "boolean",
|
|
705
|
+
description: "Paksa status Enabled pada seluruh emitter/beam (opsional).",
|
|
706
|
+
},
|
|
707
|
+
placeId: {
|
|
708
|
+
type: "number",
|
|
709
|
+
description: "Target Place ID untuk multi-Studio routing.",
|
|
710
|
+
},
|
|
711
|
+
studioId: {
|
|
712
|
+
type: "string",
|
|
713
|
+
description: "Target Studio session ID.",
|
|
714
|
+
},
|
|
715
|
+
},
|
|
716
|
+
required: ["vfx_id", "target"],
|
|
717
|
+
},
|
|
718
|
+
},
|
|
719
|
+
{
|
|
720
|
+
name: "list_vfx_database",
|
|
721
|
+
description: "Lihat katalog efek visual yang tersimpan di database persisten MCP, lengkap dengan rincian node emitter/beam dan filter kategori.",
|
|
722
|
+
inputSchema: {
|
|
723
|
+
type: "object",
|
|
724
|
+
properties: {
|
|
725
|
+
category: {
|
|
726
|
+
type: "string",
|
|
727
|
+
description: "Filter berdasarkan kategori (contoh: 'Slash', 'Aura', 'Explosion', 'Magic', 'Beam', 'Impact').",
|
|
728
|
+
},
|
|
729
|
+
query: {
|
|
730
|
+
type: "string",
|
|
731
|
+
description: "Filter pencarian kata kunci berdasarkan nama, tag, atau deskripsi.",
|
|
732
|
+
},
|
|
733
|
+
},
|
|
734
|
+
},
|
|
735
|
+
},
|
|
736
|
+
{
|
|
737
|
+
name: "delete_vfx",
|
|
738
|
+
description: "Hapus efek visual dari database persisten MCP berdasarkan ID.",
|
|
739
|
+
inputSchema: {
|
|
740
|
+
type: "object",
|
|
741
|
+
properties: {
|
|
742
|
+
id: {
|
|
743
|
+
type: "string",
|
|
744
|
+
description: "ID efek visual yang akan dihapus dari database MCP.",
|
|
745
|
+
},
|
|
746
|
+
},
|
|
747
|
+
required: ["id"],
|
|
748
|
+
},
|
|
749
|
+
},
|
|
750
|
+
{
|
|
751
|
+
name: "animate_character",
|
|
752
|
+
description: "Buat animasi karakter yang halus (Walk, Slash, Punch, Idle, atau Custom) menggunakan NV-Animation Engine (Analytical LimbIK, Bezier Curves, Easing, dan Floor Pinning), lalu simpan sebagai KeyframeSequence resmi.",
|
|
753
|
+
inputSchema: {
|
|
754
|
+
type: "object",
|
|
755
|
+
properties: {
|
|
756
|
+
target: {
|
|
757
|
+
type: "string",
|
|
758
|
+
description: "Path model karakter rig R6 atau R15 di Studio (contoh: 'Workspace.Dummy', 'Workspace.Zombie').",
|
|
759
|
+
},
|
|
760
|
+
action: {
|
|
761
|
+
type: "string",
|
|
762
|
+
description: "Jenis animasi yang ingin dibuat.",
|
|
763
|
+
enum: ["walk_cycle", "melee_slash", "punch", "idle_breath", "custom"],
|
|
764
|
+
},
|
|
765
|
+
parameters: {
|
|
766
|
+
type: "object",
|
|
767
|
+
description: "Parameter kustomisasi animasi: { duration: number, intensity: number, easing: string, loop: boolean, clipName: string }.",
|
|
768
|
+
},
|
|
769
|
+
key_poses: {
|
|
770
|
+
type: "array",
|
|
771
|
+
description: "Array pose kunci jika menggunakan action 'custom': [{ time: 0, poses: { MotorName: { angles: [X,Y,Z] } } }].",
|
|
772
|
+
},
|
|
773
|
+
placeId: {
|
|
774
|
+
type: "number",
|
|
775
|
+
description: "Target Place ID untuk multi-Studio routing.",
|
|
776
|
+
},
|
|
777
|
+
studioId: {
|
|
778
|
+
type: "string",
|
|
779
|
+
description: "Target Studio session ID.",
|
|
780
|
+
},
|
|
781
|
+
},
|
|
782
|
+
required: ["target", "action"],
|
|
783
|
+
},
|
|
784
|
+
},
|
|
785
|
+
{
|
|
786
|
+
name: "solve_limb_ik",
|
|
787
|
+
description: "Hitung dan terapkan rotasi sendi lengan/kaki (analytical 2-link LimbIK O(1)) menuju koordinat sasaran 3D secara instan dan halus.",
|
|
788
|
+
inputSchema: {
|
|
789
|
+
type: "object",
|
|
790
|
+
properties: {
|
|
791
|
+
target: {
|
|
792
|
+
type: "string",
|
|
793
|
+
description: "Path model karakter rig R6 atau R15 di Studio (contoh: 'Workspace.Dummy').",
|
|
794
|
+
},
|
|
795
|
+
limb: {
|
|
796
|
+
type: "string",
|
|
797
|
+
description: "Anggota tubuh yang ingin digerakkan.",
|
|
798
|
+
enum: ["LeftArm", "RightArm", "LeftLeg", "RightLeg"],
|
|
799
|
+
},
|
|
800
|
+
goal: {
|
|
801
|
+
type: "array",
|
|
802
|
+
items: { type: "number" },
|
|
803
|
+
description: "Target posisi 3D di world space: [X, Y, Z].",
|
|
804
|
+
},
|
|
805
|
+
pole: {
|
|
806
|
+
type: "array",
|
|
807
|
+
items: { type: "number" },
|
|
808
|
+
description: "Arah tekukan siku/lutut (pole vector 3D): [X, Y, Z] (opsional).",
|
|
809
|
+
},
|
|
810
|
+
apply_to_rig: {
|
|
811
|
+
type: "boolean",
|
|
812
|
+
description: "Apakah orientasi sendi langsung diterapkan ke model karakter di Studio (default: true).",
|
|
813
|
+
},
|
|
814
|
+
placeId: {
|
|
815
|
+
type: "number",
|
|
816
|
+
description: "Target Place ID untuk multi-Studio routing.",
|
|
817
|
+
},
|
|
818
|
+
studioId: {
|
|
819
|
+
type: "string",
|
|
820
|
+
description: "Target Studio session ID.",
|
|
821
|
+
},
|
|
822
|
+
},
|
|
823
|
+
required: ["target", "limb", "goal"],
|
|
824
|
+
},
|
|
825
|
+
},
|
|
826
|
+
{
|
|
827
|
+
name: "scrub_timeline",
|
|
828
|
+
description: "Lakukan scrubbing playhead timeline animasi dan efek visual ke timestamp tertentu (t) secara langsung di Roblox Studio untuk melihat pose karakter dan memicu burst partikel VFX.",
|
|
829
|
+
inputSchema: {
|
|
830
|
+
type: "object",
|
|
831
|
+
properties: {
|
|
832
|
+
target: {
|
|
833
|
+
type: "string",
|
|
834
|
+
description: "Path objek target di Studio (Model karakter atau folder VFX).",
|
|
835
|
+
},
|
|
836
|
+
time: {
|
|
837
|
+
type: "number",
|
|
838
|
+
description: "Posisi waktu playhead timeline dalam detik (contoh: 0.35).",
|
|
839
|
+
},
|
|
840
|
+
action: {
|
|
841
|
+
type: "string",
|
|
842
|
+
description: "Aksi animasi yang ingin di-scrub (contoh: 'melee_slash', 'walk_cycle', 'punch', 'idle_breath').",
|
|
843
|
+
},
|
|
844
|
+
emit_vfx: {
|
|
845
|
+
type: "boolean",
|
|
846
|
+
description: "Apakah memicu semburan (burst) partikel pada frame waktu ini (default: true).",
|
|
847
|
+
},
|
|
848
|
+
emit_count: {
|
|
849
|
+
type: "number",
|
|
850
|
+
description: "Jumlah partikel yang disemburkan (default: 20).",
|
|
851
|
+
},
|
|
852
|
+
enabled: {
|
|
853
|
+
type: "boolean",
|
|
854
|
+
description: "Paksa status Enabled pada Beam/Trail pada playhead waktu ini.",
|
|
855
|
+
},
|
|
856
|
+
placeId: {
|
|
857
|
+
type: "number",
|
|
858
|
+
description: "Target Place ID untuk multi-Studio routing.",
|
|
859
|
+
},
|
|
860
|
+
studioId: {
|
|
861
|
+
type: "string",
|
|
862
|
+
description: "Target Studio session ID.",
|
|
863
|
+
},
|
|
864
|
+
},
|
|
865
|
+
required: ["target", "time"],
|
|
866
|
+
},
|
|
867
|
+
},
|
|
868
|
+
{
|
|
869
|
+
name: "inspect_rig",
|
|
870
|
+
description: "Inspeksi topologi dan hierarki rig karakter/objek di Studio secara headless. Menganalisis pohon sendi (Motor6D & Weld), mendeteksi circular dependency, memeriksa status anchor, dan melaporkan posisi poros di dunia 3D.",
|
|
871
|
+
inputSchema: {
|
|
872
|
+
type: "object",
|
|
873
|
+
properties: {
|
|
874
|
+
target: {
|
|
875
|
+
type: "string",
|
|
876
|
+
description: "Path objek Model karakter atau objek di Studio (contoh: 'Workspace.Dummy').",
|
|
877
|
+
},
|
|
878
|
+
placeId: { type: "number", description: "Target Place ID untuk multi-Studio routing." },
|
|
879
|
+
studioId: { type: "string", description: "Target Studio session ID." },
|
|
880
|
+
},
|
|
881
|
+
required: ["target"],
|
|
882
|
+
},
|
|
883
|
+
},
|
|
884
|
+
{
|
|
885
|
+
name: "edit_rig_joint",
|
|
886
|
+
description: "Operasi headless sendi NV-Rig Engine: buat Motor6D/Weld baru, geser titik tumpu (pivot) tanpa mendislokasi part anak, putar orientasi poros, setel ulang (reset), selaraskan (align), atau hapus sendi.",
|
|
887
|
+
inputSchema: {
|
|
888
|
+
type: "object",
|
|
889
|
+
properties: {
|
|
890
|
+
target: {
|
|
891
|
+
type: "string",
|
|
892
|
+
description: "Path Model karakter/objek di Studio (contoh: 'Workspace.Dummy').",
|
|
893
|
+
},
|
|
894
|
+
action: {
|
|
895
|
+
type: "string",
|
|
896
|
+
description: "Aksi manipulasi sendi.",
|
|
897
|
+
enum: ["create_joint", "create_weld", "edit_pivot", "rotate_pivot", "reset_pivot", "align_joint", "remove_joint"],
|
|
898
|
+
},
|
|
899
|
+
joint_name: {
|
|
900
|
+
type: "string",
|
|
901
|
+
description: "Nama sendi yang ingin diedit/dihapus (contoh: 'RightShoulder', 'Neck').",
|
|
902
|
+
},
|
|
903
|
+
part0: {
|
|
904
|
+
type: "string",
|
|
905
|
+
description: "Nama part induk (Part0) untuk create_joint/create_weld (contoh: 'Torso', 'UpperTorso').",
|
|
906
|
+
},
|
|
907
|
+
part1: {
|
|
908
|
+
type: "string",
|
|
909
|
+
description: "Nama part anak (Part1) untuk create_joint/create_weld (contoh: 'Right Arm', 'RightUpperArm').",
|
|
910
|
+
},
|
|
911
|
+
delta_world: {
|
|
912
|
+
type: "array",
|
|
913
|
+
items: { type: "number" },
|
|
914
|
+
description: "Pergeseran poros di koordinat dunia [X, Y, Z] untuk edit_pivot.",
|
|
915
|
+
},
|
|
916
|
+
target_world: {
|
|
917
|
+
type: "array",
|
|
918
|
+
items: { type: "number" },
|
|
919
|
+
description: "Posisi absolut poros target di dunia [X, Y, Z] untuk edit_pivot.",
|
|
920
|
+
},
|
|
921
|
+
rotate_euler_deg: {
|
|
922
|
+
type: "array",
|
|
923
|
+
items: { type: "number" },
|
|
924
|
+
description: "Rotasi poros [Pitch, Yaw, Roll] dalam derajat untuk rotate_pivot.",
|
|
925
|
+
},
|
|
926
|
+
align_to: {
|
|
927
|
+
type: "string",
|
|
928
|
+
description: "Penyelarasan orientasi poros ke 'Part0' atau 'Part1' untuk align_joint.",
|
|
929
|
+
enum: ["Part0", "Part1"],
|
|
930
|
+
},
|
|
931
|
+
reanchor: {
|
|
932
|
+
type: "boolean",
|
|
933
|
+
description: "Setel Part1 kembali ke 'Anchored = true' saat menghapus sendi (opsional).",
|
|
934
|
+
},
|
|
935
|
+
placeId: { type: "number", description: "Target Place ID untuk multi-Studio routing." },
|
|
936
|
+
studioId: { type: "string", description: "Target Studio session ID." },
|
|
937
|
+
},
|
|
938
|
+
required: ["target", "action"],
|
|
939
|
+
},
|
|
940
|
+
},
|
|
941
|
+
{
|
|
942
|
+
name: "auto_rig",
|
|
943
|
+
description: "Auto-rigging cerdas 1-klik untuk Avatar R6, R15, atau Penautan Senjata (Weapon/Tool) dengan matematika kompensasi spasial otomatis.",
|
|
944
|
+
inputSchema: {
|
|
945
|
+
type: "object",
|
|
946
|
+
properties: {
|
|
947
|
+
target: {
|
|
948
|
+
type: "string",
|
|
949
|
+
description: "Path Model karakter di Studio (contoh: 'Workspace.Dummy', 'Workspace.CustomAvatar').",
|
|
950
|
+
},
|
|
951
|
+
rig_type: {
|
|
952
|
+
type: "string",
|
|
953
|
+
description: "Tipe rig yang ingin dibangun secara otomatis.",
|
|
954
|
+
enum: ["R6", "R15", "Weapon"],
|
|
955
|
+
},
|
|
956
|
+
weapon_target: {
|
|
957
|
+
type: "string",
|
|
958
|
+
description: "Path objek senjata/prop di Studio jika rig_type='Weapon' (contoh: 'Workspace.Sword').",
|
|
959
|
+
},
|
|
960
|
+
target_limb: {
|
|
961
|
+
type: "string",
|
|
962
|
+
description: "Anggota tubuh tempat senjata dipasang (contoh: 'RightHand' atau 'Right Arm').",
|
|
963
|
+
},
|
|
964
|
+
placeId: { type: "number", description: "Target Place ID untuk multi-Studio routing." },
|
|
965
|
+
studioId: { type: "string", description: "Target Studio session ID." },
|
|
966
|
+
},
|
|
967
|
+
required: ["target", "rig_type"],
|
|
968
|
+
},
|
|
969
|
+
},
|
|
970
|
+
{
|
|
971
|
+
name: "weld_assembly",
|
|
972
|
+
description: "Las (Weld) seluruh komponen BasePart pada sebuah model (aksesori, armor, prop, kendaraan) ke PrimaryPart secara otomatis dengan unanchor aman.",
|
|
973
|
+
inputSchema: {
|
|
974
|
+
type: "object",
|
|
975
|
+
properties: {
|
|
976
|
+
target: {
|
|
977
|
+
type: "string",
|
|
978
|
+
description: "Path Model atau Folder di Studio yang ingin dilas (contoh: 'Workspace.ArmorSet').",
|
|
979
|
+
},
|
|
980
|
+
primary_part: {
|
|
981
|
+
type: "string",
|
|
982
|
+
description: "Nama part utama sebagai jangkar (opsional, jika kosong menggunakan PrimaryPart atau part terbesar).",
|
|
983
|
+
},
|
|
984
|
+
placeId: { type: "number", description: "Target Place ID untuk multi-Studio routing." },
|
|
985
|
+
studioId: { type: "string", description: "Target Studio session ID." },
|
|
986
|
+
},
|
|
987
|
+
required: ["target"],
|
|
988
|
+
},
|
|
989
|
+
},
|
|
990
|
+
{
|
|
991
|
+
name: "mirror_rig",
|
|
992
|
+
description: "Cerminkan sambungan sendi bilateral (Left <-> Right) sepanjang sumbu simetri X secara matematis, menghemat waktu konfigurasi karakter.",
|
|
993
|
+
inputSchema: {
|
|
994
|
+
type: "object",
|
|
995
|
+
properties: {
|
|
996
|
+
target: {
|
|
997
|
+
type: "string",
|
|
998
|
+
description: "Path Model karakter di Studio (contoh: 'Workspace.Dummy').",
|
|
999
|
+
},
|
|
1000
|
+
source_side: {
|
|
1001
|
+
type: "string",
|
|
1002
|
+
description: "Sisi sumber yang ingin dicerminkan.",
|
|
1003
|
+
enum: ["Left", "Right"],
|
|
1004
|
+
},
|
|
1005
|
+
placeId: { type: "number", description: "Target Place ID untuk multi-Studio routing." },
|
|
1006
|
+
studioId: { type: "string", description: "Target Studio session ID." },
|
|
1007
|
+
},
|
|
1008
|
+
required: ["target"],
|
|
1009
|
+
},
|
|
1010
|
+
},
|
|
644
1011
|
];
|
|
645
1012
|
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
|
|
646
1013
|
return { tools: TOOLS };
|
|
@@ -1003,6 +1370,236 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
1003
1370
|
});
|
|
1004
1371
|
break;
|
|
1005
1372
|
}
|
|
1373
|
+
case "scan_vfx": {
|
|
1374
|
+
const target = String(args.target ?? "");
|
|
1375
|
+
if (!target)
|
|
1376
|
+
throw new Error("Parameter 'target' wajib diisi.");
|
|
1377
|
+
const payload = JSON.stringify({
|
|
1378
|
+
name: args.name,
|
|
1379
|
+
category: args.category,
|
|
1380
|
+
tags: args.tags,
|
|
1381
|
+
description: args.description,
|
|
1382
|
+
});
|
|
1383
|
+
const scanRes = await dispatchTaskToStudio("scan_vfx", target, payload, {
|
|
1384
|
+
placeId: args.placeId,
|
|
1385
|
+
studioId: args.studioId,
|
|
1386
|
+
});
|
|
1387
|
+
if (scanRes && (scanRes.status === "success" || !scanRes.error)) {
|
|
1388
|
+
const scanned = scanRes.result || scanRes;
|
|
1389
|
+
const db = (0, vfx_database_1.getVfxDatabase)();
|
|
1390
|
+
const saved = db.saveVfx({
|
|
1391
|
+
name: scanned.name || args.name || "Scanned_VFX",
|
|
1392
|
+
category: scanned.category || args.category || "Ambient",
|
|
1393
|
+
tags: scanned.tags || args.tags || [],
|
|
1394
|
+
description: scanned.description || args.description,
|
|
1395
|
+
sourcePlaceId: args.placeId,
|
|
1396
|
+
stats: scanned.stats,
|
|
1397
|
+
nodeCount: scanned.nodeCount,
|
|
1398
|
+
data: scanned.data,
|
|
1399
|
+
});
|
|
1400
|
+
result = {
|
|
1401
|
+
status: "success",
|
|
1402
|
+
message: `VFX '${saved.name}' berhasil dipindai dan disimpan ke database persisten MCP dengan ID '${saved.id}'`,
|
|
1403
|
+
item: saved,
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
else {
|
|
1407
|
+
throw new Error(scanRes?.error || "Gagal memindai VFX dari Studio.");
|
|
1408
|
+
}
|
|
1409
|
+
break;
|
|
1410
|
+
}
|
|
1411
|
+
case "apply_vfx": {
|
|
1412
|
+
const vfxId = String(args.vfx_id ?? "");
|
|
1413
|
+
const target = String(args.target ?? "");
|
|
1414
|
+
if (!vfxId)
|
|
1415
|
+
throw new Error("Parameter 'vfx_id' wajib diisi.");
|
|
1416
|
+
if (!target)
|
|
1417
|
+
throw new Error("Parameter 'target' wajib diisi.");
|
|
1418
|
+
const db = (0, vfx_database_1.getVfxDatabase)();
|
|
1419
|
+
const item = db.getVfx(vfxId);
|
|
1420
|
+
if (!item)
|
|
1421
|
+
throw new Error(`VFX dengan ID '${vfxId}' tidak ditemukan di database MCP.`);
|
|
1422
|
+
const payload = JSON.stringify({
|
|
1423
|
+
data: item.data,
|
|
1424
|
+
scale: args.scale,
|
|
1425
|
+
enabled: args.enabled,
|
|
1426
|
+
});
|
|
1427
|
+
result = await dispatchTaskToStudio("apply_vfx", target, payload, {
|
|
1428
|
+
placeId: args.placeId,
|
|
1429
|
+
studioId: args.studioId,
|
|
1430
|
+
});
|
|
1431
|
+
break;
|
|
1432
|
+
}
|
|
1433
|
+
case "list_vfx_database": {
|
|
1434
|
+
const db = (0, vfx_database_1.getVfxDatabase)();
|
|
1435
|
+
const items = db.listVfx({ category: args.category, query: args.query });
|
|
1436
|
+
const categories = db.getCategories();
|
|
1437
|
+
result = {
|
|
1438
|
+
total: items.length,
|
|
1439
|
+
categories,
|
|
1440
|
+
items: items.map(i => ({
|
|
1441
|
+
id: i.id,
|
|
1442
|
+
name: i.name,
|
|
1443
|
+
category: i.category,
|
|
1444
|
+
tags: i.tags,
|
|
1445
|
+
description: i.description,
|
|
1446
|
+
nodeCount: i.nodeCount,
|
|
1447
|
+
stats: i.stats,
|
|
1448
|
+
createdAt: i.createdAt,
|
|
1449
|
+
})),
|
|
1450
|
+
};
|
|
1451
|
+
break;
|
|
1452
|
+
}
|
|
1453
|
+
case "delete_vfx": {
|
|
1454
|
+
const id = String(args.id ?? "");
|
|
1455
|
+
if (!id)
|
|
1456
|
+
throw new Error("Parameter 'id' wajib diisi.");
|
|
1457
|
+
const db = (0, vfx_database_1.getVfxDatabase)();
|
|
1458
|
+
const deleted = db.deleteVfx(id);
|
|
1459
|
+
if (!deleted)
|
|
1460
|
+
throw new Error(`VFX dengan ID '${id}' tidak ditemukan di database MCP.`);
|
|
1461
|
+
result = { status: "success", message: `VFX '${id}' berhasil dihapus dari database.` };
|
|
1462
|
+
break;
|
|
1463
|
+
}
|
|
1464
|
+
case "animate_character": {
|
|
1465
|
+
const target = String(args.target ?? "");
|
|
1466
|
+
const action = String(args.action ?? "");
|
|
1467
|
+
if (!target)
|
|
1468
|
+
throw new Error("Parameter 'target' wajib diisi.");
|
|
1469
|
+
if (!action)
|
|
1470
|
+
throw new Error("Parameter 'action' wajib diisi.");
|
|
1471
|
+
const payload = JSON.stringify({
|
|
1472
|
+
action,
|
|
1473
|
+
parameters: args.parameters || {},
|
|
1474
|
+
key_poses: args.key_poses,
|
|
1475
|
+
});
|
|
1476
|
+
result = await dispatchTaskToStudio("animate_character", target, payload, {
|
|
1477
|
+
placeId: args.placeId,
|
|
1478
|
+
studioId: args.studioId,
|
|
1479
|
+
});
|
|
1480
|
+
break;
|
|
1481
|
+
}
|
|
1482
|
+
case "solve_limb_ik": {
|
|
1483
|
+
const target = String(args.target ?? "");
|
|
1484
|
+
const limb = String(args.limb ?? "");
|
|
1485
|
+
if (!target)
|
|
1486
|
+
throw new Error("Parameter 'target' wajib diisi.");
|
|
1487
|
+
if (!limb)
|
|
1488
|
+
throw new Error("Parameter 'limb' wajib diisi.");
|
|
1489
|
+
if (!args.goal || !Array.isArray(args.goal) || args.goal.length < 3) {
|
|
1490
|
+
throw new Error("Parameter 'goal' [X, Y, Z] wajib diisi.");
|
|
1491
|
+
}
|
|
1492
|
+
const payload = JSON.stringify({
|
|
1493
|
+
limb,
|
|
1494
|
+
goal: args.goal,
|
|
1495
|
+
pole: args.pole,
|
|
1496
|
+
apply_to_rig: args.apply_to_rig !== false,
|
|
1497
|
+
});
|
|
1498
|
+
result = await dispatchTaskToStudio("solve_limb_ik", target, payload, {
|
|
1499
|
+
placeId: args.placeId,
|
|
1500
|
+
studioId: args.studioId,
|
|
1501
|
+
});
|
|
1502
|
+
break;
|
|
1503
|
+
}
|
|
1504
|
+
case "scrub_timeline": {
|
|
1505
|
+
const target = String(args.target ?? "");
|
|
1506
|
+
if (!target)
|
|
1507
|
+
throw new Error("Parameter 'target' wajib diisi.");
|
|
1508
|
+
const payload = JSON.stringify({
|
|
1509
|
+
time: args.time,
|
|
1510
|
+
action: args.action,
|
|
1511
|
+
emit_vfx: args.emit_vfx !== false,
|
|
1512
|
+
emit_count: args.emit_count,
|
|
1513
|
+
enabled: args.enabled,
|
|
1514
|
+
});
|
|
1515
|
+
result = await dispatchTaskToStudio("scrub_timeline", target, payload, {
|
|
1516
|
+
placeId: args.placeId,
|
|
1517
|
+
studioId: args.studioId,
|
|
1518
|
+
});
|
|
1519
|
+
break;
|
|
1520
|
+
}
|
|
1521
|
+
case "inspect_rig": {
|
|
1522
|
+
const target = String(args.target ?? "");
|
|
1523
|
+
if (!target)
|
|
1524
|
+
throw new Error("Parameter 'target' wajib diisi.");
|
|
1525
|
+
const payload = JSON.stringify({ action: "inspect" });
|
|
1526
|
+
result = await dispatchTaskToStudio("edit_rig", target, payload, {
|
|
1527
|
+
placeId: args.placeId,
|
|
1528
|
+
studioId: args.studioId,
|
|
1529
|
+
});
|
|
1530
|
+
break;
|
|
1531
|
+
}
|
|
1532
|
+
case "edit_rig_joint": {
|
|
1533
|
+
const target = String(args.target ?? "");
|
|
1534
|
+
const action = String(args.action ?? "edit_pivot");
|
|
1535
|
+
if (!target)
|
|
1536
|
+
throw new Error("Parameter 'target' wajib diisi.");
|
|
1537
|
+
const payload = JSON.stringify({
|
|
1538
|
+
action,
|
|
1539
|
+
joint_name: args.joint_name,
|
|
1540
|
+
part0: args.part0,
|
|
1541
|
+
part1: args.part1,
|
|
1542
|
+
delta_world: args.delta_world,
|
|
1543
|
+
target_world: args.target_world,
|
|
1544
|
+
rotate_euler_deg: args.rotate_euler_deg,
|
|
1545
|
+
align_to: args.align_to,
|
|
1546
|
+
reanchor: args.reanchor,
|
|
1547
|
+
});
|
|
1548
|
+
result = await dispatchTaskToStudio("edit_rig", target, payload, {
|
|
1549
|
+
placeId: args.placeId,
|
|
1550
|
+
studioId: args.studioId,
|
|
1551
|
+
});
|
|
1552
|
+
break;
|
|
1553
|
+
}
|
|
1554
|
+
case "auto_rig": {
|
|
1555
|
+
const target = String(args.target ?? "");
|
|
1556
|
+
const rigType = String(args.rig_type ?? "R15");
|
|
1557
|
+
if (!target)
|
|
1558
|
+
throw new Error("Parameter 'target' wajib diisi.");
|
|
1559
|
+
let action = "auto_rig_r15";
|
|
1560
|
+
if (rigType === "R6")
|
|
1561
|
+
action = "auto_rig_r6";
|
|
1562
|
+
else if (rigType === "Weapon")
|
|
1563
|
+
action = "rig_weapon";
|
|
1564
|
+
const payload = JSON.stringify({
|
|
1565
|
+
action,
|
|
1566
|
+
weapon_target: args.weapon_target,
|
|
1567
|
+
target_limb: args.target_limb,
|
|
1568
|
+
});
|
|
1569
|
+
result = await dispatchTaskToStudio("edit_rig", target, payload, {
|
|
1570
|
+
placeId: args.placeId,
|
|
1571
|
+
studioId: args.studioId,
|
|
1572
|
+
});
|
|
1573
|
+
break;
|
|
1574
|
+
}
|
|
1575
|
+
case "weld_assembly": {
|
|
1576
|
+
const target = String(args.target ?? "");
|
|
1577
|
+
if (!target)
|
|
1578
|
+
throw new Error("Parameter 'target' wajib diisi.");
|
|
1579
|
+
const payload = JSON.stringify({
|
|
1580
|
+
action: "weld_assembly",
|
|
1581
|
+
primary_part: args.primary_part,
|
|
1582
|
+
});
|
|
1583
|
+
result = await dispatchTaskToStudio("edit_rig", target, payload, {
|
|
1584
|
+
placeId: args.placeId,
|
|
1585
|
+
studioId: args.studioId,
|
|
1586
|
+
});
|
|
1587
|
+
break;
|
|
1588
|
+
}
|
|
1589
|
+
case "mirror_rig": {
|
|
1590
|
+
const target = String(args.target ?? "");
|
|
1591
|
+
if (!target)
|
|
1592
|
+
throw new Error("Parameter 'target' wajib diisi.");
|
|
1593
|
+
const payload = JSON.stringify({
|
|
1594
|
+
action: "mirror",
|
|
1595
|
+
source_side: args.source_side || "Left",
|
|
1596
|
+
});
|
|
1597
|
+
result = await dispatchTaskToStudio("edit_rig", target, payload, {
|
|
1598
|
+
placeId: args.placeId,
|
|
1599
|
+
studioId: args.studioId,
|
|
1600
|
+
});
|
|
1601
|
+
break;
|
|
1602
|
+
}
|
|
1006
1603
|
case "call_mcp_tool": {
|
|
1007
1604
|
const { command, target, data, placeId, studioId } = args;
|
|
1008
1605
|
if (!command)
|
|
@@ -1034,6 +1631,34 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
1034
1631
|
result = await handleSearchAsset(queryParams.keyword || "", queryParams.category);
|
|
1035
1632
|
break;
|
|
1036
1633
|
}
|
|
1634
|
+
// Intercept VFX database tools di Node.js
|
|
1635
|
+
if (command === "list_vfx_database") {
|
|
1636
|
+
const queryParams = typeof data === "string" ? JSON.parse(data || "{}") : (data || {});
|
|
1637
|
+
const db = (0, vfx_database_1.getVfxDatabase)();
|
|
1638
|
+
result = {
|
|
1639
|
+
total: db.listVfx(queryParams).length,
|
|
1640
|
+
categories: db.getCategories(),
|
|
1641
|
+
items: db.listVfx(queryParams),
|
|
1642
|
+
};
|
|
1643
|
+
break;
|
|
1644
|
+
}
|
|
1645
|
+
if (command === "delete_vfx") {
|
|
1646
|
+
const queryParams = typeof data === "string" ? JSON.parse(data || "{}") : (data || {});
|
|
1647
|
+
const db = (0, vfx_database_1.getVfxDatabase)();
|
|
1648
|
+
const ok = db.deleteVfx(queryParams.id || target || "");
|
|
1649
|
+
result = { success: ok };
|
|
1650
|
+
break;
|
|
1651
|
+
}
|
|
1652
|
+
if (command === "apply_vfx") {
|
|
1653
|
+
const queryParams = typeof data === "string" ? JSON.parse(data || "{}") : (data || {});
|
|
1654
|
+
const db = (0, vfx_database_1.getVfxDatabase)();
|
|
1655
|
+
const item = db.getVfx(queryParams.vfx_id || queryParams.id || "");
|
|
1656
|
+
if (!item)
|
|
1657
|
+
throw new Error("VFX tidak ditemukan di database MCP.");
|
|
1658
|
+
const payload = JSON.stringify({ data: item.data, scale: queryParams.scale, enabled: queryParams.enabled });
|
|
1659
|
+
result = await dispatchTaskToStudio("apply_vfx", target || queryParams.target || "Workspace", payload, { placeId, studioId });
|
|
1660
|
+
break;
|
|
1661
|
+
}
|
|
1037
1662
|
result = await dispatchTaskToStudio(command, target || "", data, { placeId, studioId });
|
|
1038
1663
|
break;
|
|
1039
1664
|
}
|