@adiholick/nvstudio-mcp 2.2.3 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +57 -47
  2. package/agent-plugin/mcp_config.json +8 -0
  3. package/agent-plugin/plugin.json +6 -0
  4. package/agent-plugin/rules/AGENTS.md +118 -0
  5. package/agent-plugin/skills/nvstudio-animation-vfx/SKILL.md +120 -0
  6. package/agent-plugin/skills/nvstudio-assets-terrain-guide/SKILL.md +27 -0
  7. package/agent-plugin/skills/nvstudio-gui-standards/SKILL.md +419 -0
  8. package/agent-plugin/skills/nvstudio-mcp-guide/SKILL.md +83 -0
  9. package/agent-plugin/skills/nvstudio-rig-engine/SKILL.md +170 -0
  10. package/agent-plugin/skills/nvstudio-roblox-sync-guide/SKILL.md +16 -0
  11. package/dist/build-plugin.js +55 -0
  12. package/dist/index.js +841 -3
  13. package/dist/luau-validator.js +99 -34
  14. package/dist/roblox-bridge.js +152 -4
  15. package/dist/roblox-docs.js +264 -0
  16. package/dist/vfx-database.js +507 -0
  17. package/package.json +6 -4
  18. package/public/index.html +1590 -180
  19. package/studio-plugin/Main.server.lua +1 -1
  20. package/studio-plugin/animate_character.lua +36 -0
  21. package/studio-plugin/apply_vfx.lua +59 -0
  22. package/studio-plugin/batch_create_instances.lua +128 -0
  23. package/studio-plugin/batch_set_properties.lua +69 -0
  24. package/studio-plugin/edit_rig.lua +35 -0
  25. package/studio-plugin/insert_asset.lua +85 -12
  26. package/studio-plugin/nv_anim_dispatcher.lua +239 -0
  27. package/studio-plugin/nv_auto_rigger.lua +392 -0
  28. package/studio-plugin/nv_curves.lua +260 -0
  29. package/studio-plugin/nv_joint_math.lua +138 -0
  30. package/studio-plugin/nv_kinematics.lua +248 -0
  31. package/studio-plugin/nv_motion_generator.lua +279 -0
  32. package/studio-plugin/nv_rig_dispatcher.lua +319 -0
  33. package/studio-plugin/nv_vfx_serializer.lua +363 -0
  34. package/studio-plugin/nvstudio_mcp.rbxmx +3824 -60
  35. package/studio-plugin/preview_asset.lua +99 -0
  36. package/studio-plugin/scan_security.lua +81 -0
  37. package/studio-plugin/scan_vfx.lua +85 -0
  38. package/studio-plugin/scrub_timeline.lua +122 -0
  39. package/studio-plugin/security_scanner.lua +346 -0
  40. package/studio-plugin/set_properties.lua +83 -0
  41. package/studio-plugin/solve_limb_ik.lua +63 -0
  42. package/studio-plugin/ui_manager.lua +1 -1
@@ -0,0 +1,99 @@
1
+ --!strict
2
+ -- ============================================================================
3
+ -- Skill Module: preview_asset
4
+ -- Deskripsi: Pratinjau & Pemindaian Keamanan Aset Creator Store Secara Terisolasi
5
+ -- ============================================================================
6
+
7
+ local HttpService = game:GetService("HttpService")
8
+ local InsertService = game:GetService("InsertService")
9
+
10
+ local SecurityScanner = require(script.Parent:WaitForChild("security_scanner"))
11
+
12
+ return function(targetInstance: Instance?, data: any, ctx: any, targetPath: string?)
13
+ local assetIdStr = ""
14
+ local deepScan = true
15
+
16
+ if typeof(data) == "string" then
17
+ local success, parsed = pcall(function()
18
+ return HttpService:JSONDecode(data)
19
+ end)
20
+ if success and typeof(parsed) == "table" then
21
+ assetIdStr = tostring(parsed.asset_id or parsed.assetId or "")
22
+ if parsed.deep_scan ~= nil then deepScan = parsed.deep_scan end
23
+ else
24
+ assetIdStr = data
25
+ end
26
+ elseif typeof(data) == "table" then
27
+ assetIdStr = tostring(data.asset_id or data.assetId or "")
28
+ if data.deep_scan ~= nil then deepScan = data.deep_scan end
29
+ end
30
+
31
+ local cleanIdStr = string.gsub(assetIdStr, "rbxassetid://", "")
32
+ cleanIdStr = string.gsub(cleanIdStr, "%s+", "")
33
+ local assetId = tonumber(cleanIdStr)
34
+
35
+ if not assetId or assetId <= 0 then
36
+ return { status = "error", error = "Asset ID tidak valid. Berikan Asset ID angka (contoh: 123456789)." }
37
+ end
38
+
39
+ -- Muat Aset secara terisolasi (unparented / memory only)
40
+ local ok, model = pcall(function()
41
+ return InsertService:LoadAsset(assetId)
42
+ end)
43
+
44
+ if not ok or not model then
45
+ return {
46
+ status = "error",
47
+ error = string.format("Gagal memuat aset ID %d. Pastikan Asset ID publik dan terdaftar di Creator Store.", assetId)
48
+ }
49
+ end
50
+
51
+ local scanResult
52
+ local scanOk, err = pcall(function()
53
+ scanResult = SecurityScanner.scanInstance(model, deepScan)
54
+ end)
55
+
56
+ local rootName = model.Name
57
+ local childrenOverview = {}
58
+ for _, child in ipairs(model:GetChildren()) do
59
+ table.insert(childrenOverview, {
60
+ name = child.Name,
61
+ className = child.ClassName,
62
+ })
63
+ end
64
+
65
+ -- Hancurkan model sementara dari memori (Cleanup Mutlak)
66
+ pcall(function()
67
+ model:Destroy()
68
+ end)
69
+
70
+ if not scanOk or not scanResult then
71
+ return {
72
+ status = "error",
73
+ error = "Gagal memindai aset: " .. tostring(err)
74
+ }
75
+ end
76
+
77
+ local recommendation = "Aman untuk dimasukkan langsung menggunakan insert_asset."
78
+ if scanResult.riskLevel == "DANGEROUS" then
79
+ recommendation = "PERINGATAN: Aset ini memiliki ancaman BERBAHAYA! Wajib gunakan 'insert_asset' dengan strip_scripts = true, atau hindari aset ini."
80
+ elseif scanResult.riskLevel == "WARNING" then
81
+ recommendation = "Perhatian: Aset mengandung skrip atau peringatan moderat. Periksa kembali atau gunakan strip_scripts = true jika skrip tidak diperlukan."
82
+ end
83
+
84
+ return {
85
+ status = "success",
86
+ result = {
87
+ assetId = assetId,
88
+ assetName = rootName,
89
+ riskLevel = scanResult.riskLevel,
90
+ recommendation = recommendation,
91
+ stats = scanResult.stats,
92
+ highThreatsCount = scanResult.highThreats,
93
+ mediumThreatsCount = scanResult.mediumThreats,
94
+ threats = scanResult.threats,
95
+ topLevelChildren = childrenOverview,
96
+ }
97
+ }
98
+ end
99
+
@@ -0,0 +1,81 @@
1
+ --!strict
2
+ -- ============================================================================
3
+ -- Skill Module: scan_security
4
+ -- Deskripsi: Audit Keamanan Instance/Model yang Sudah Ada di Tempat Kerja Studio
5
+ -- ============================================================================
6
+
7
+ local HttpService = game:GetService("HttpService")
8
+ local SecurityScanner = require(script.Parent:WaitForChild("security_scanner"))
9
+
10
+ return function(targetInstance: Instance?, data: any, ctx: any, targetPath: string?)
11
+ local target = targetInstance or game.Workspace
12
+ local deepScan = true
13
+ local cleanThreats = false
14
+
15
+ if typeof(data) == "string" and #data > 0 then
16
+ local success, parsed = pcall(function()
17
+ return HttpService:JSONDecode(data)
18
+ end)
19
+ if success and typeof(parsed) == "table" then
20
+ if parsed.deep_scan ~= nil then deepScan = parsed.deep_scan end
21
+ if parsed.clean_threats ~= nil then cleanThreats = parsed.clean_threats end
22
+ end
23
+ elseif typeof(data) == "table" then
24
+ if data.deep_scan ~= nil then deepScan = data.deep_scan end
25
+ if data.clean_threats ~= nil then cleanThreats = data.clean_threats end
26
+ end
27
+
28
+ local scanResult
29
+ local ok, err = pcall(function()
30
+ scanResult = SecurityScanner.scanInstance(target, deepScan)
31
+ end)
32
+
33
+ if not ok or not scanResult then
34
+ return {
35
+ status = "error",
36
+ error = "Gagal memindai target: " .. tostring(err)
37
+ }
38
+ end
39
+
40
+ local cleanedScripts = {}
41
+ if cleanThreats and #scanResult.threats > 0 then
42
+ -- Hapus skrip yang terdeteksi bahaya (HIGH severity)
43
+ local scriptsToDestroy = {}
44
+ for _, threat in ipairs(scanResult.threats) do
45
+ if threat.severity == "HIGH" and threat.scriptPath then
46
+ scriptsToDestroy[threat.scriptPath] = true
47
+ end
48
+ end
49
+
50
+ local function purge(instance: Instance)
51
+ for _, desc in ipairs(instance:GetDescendants()) do
52
+ if desc:IsA("LuaSourceContainer") and scriptsToDestroy[desc:GetFullName()] then
53
+ table.insert(cleanedScripts, {
54
+ name = desc.Name,
55
+ className = desc.ClassName,
56
+ path = desc:GetFullName(),
57
+ })
58
+ pcall(function() desc:Destroy() end)
59
+ end
60
+ end
61
+ end
62
+
63
+ purge(target)
64
+ end
65
+
66
+ return {
67
+ status = "success",
68
+ result = {
69
+ target = target:GetFullName(),
70
+ riskLevel = scanResult.riskLevel,
71
+ stats = scanResult.stats,
72
+ highThreatsCount = scanResult.highThreats,
73
+ mediumThreatsCount = scanResult.mediumThreats,
74
+ threats = scanResult.threats,
75
+ cleanedThreatsCount = #cleanedScripts,
76
+ cleanedScripts = cleanedScripts,
77
+ isCleaned = cleanThreats,
78
+ }
79
+ }
80
+ end
81
+
@@ -0,0 +1,85 @@
1
+ --!strict
2
+ -- ══════════════════════════════════════════════════════════════════════
3
+ -- Skill Module: scan_vfx
4
+ -- Deskripsi: Memindai efek visual (ParticleEmitter, Beam, Trail, Light)
5
+ -- pada objek target di Roblox Studio dan mengubahnya menjadi tabel murni
6
+ -- ══════════════════════════════════════════════════════════════════════
7
+
8
+ local HttpService = game:GetService("HttpService")
9
+ local serializerMod = script.Parent:WaitForChild("nv_vfx_serializer")
10
+ local Serializer = require(serializerMod)
11
+
12
+ local function detectCategory(name: string, data: any): string
13
+ local lower = string.lower(name)
14
+ if lower:find("slash") or lower:find("blade") or lower:find("sword") or lower:find("cut") then
15
+ return "Slash"
16
+ elseif lower:find("explosion") or lower:find("burst") or lower:find("blast") or lower:find("boom") or lower:find("fire") then
17
+ return "Explosion"
18
+ elseif lower:find("aura") or lower:find("glow") or lower:find("shield") or lower:find("barrier") then
19
+ return "Aura"
20
+ elseif lower:find("laser") or lower:find("beam") or lower:find("ray") then
21
+ return "Beam"
22
+ elseif lower:find("magic") or lower:find("spell") or lower:find("rune") or lower:find("circle") or lower:find("portal") then
23
+ return "Magic"
24
+ elseif lower:find("hit") or lower:find("impact") or lower:find("spark") or lower:find("clash") then
25
+ return "Impact"
26
+ elseif lower:find("projectile") or lower:find("bullet") or lower:find("arrow") or lower:find("missile") then
27
+ return "Projectile"
28
+ end
29
+ return "Ambient"
30
+ end
31
+
32
+ return function(targetInstance: Instance?, data: any, ctx: any, targetPath: string?)
33
+ if not targetInstance then
34
+ return { status = "error", error = "Target instance untuk scan_vfx tidak ditemukan: " .. tostring(targetPath) }
35
+ end
36
+
37
+ local parsedData = {}
38
+ if data and data ~= "" then
39
+ pcall(function()
40
+ if type(data) == "table" then
41
+ parsedData = data
42
+ else
43
+ parsedData = HttpService:JSONDecode(tostring(data))
44
+ end
45
+ end)
46
+ end
47
+
48
+ local vfxName = parsedData.name or targetInstance.Name or "Unnamed_VFX"
49
+ local rawCat = parsedData.category or detectCategory(vfxName, parsedData)
50
+ local category = "Ambient"
51
+ if type(rawCat) == "string" and #rawCat > 0 then
52
+ category = string.upper(string.sub(rawCat, 1, 1)) .. string.lower(string.sub(rawCat, 2))
53
+ end
54
+ local tags = parsedData.tags or { string.lower(category) }
55
+
56
+ local ok, serialized = pcall(function()
57
+ return Serializer.serialize(targetInstance)
58
+ end)
59
+
60
+ if not ok or not serialized then
61
+ return { status = "error", error = "Gagal memindai VFX: " .. tostring(serialized) }
62
+ end
63
+
64
+ if serialized.nodeCount == 0 or (serialized.stats.emitters == 0 and serialized.stats.beams == 0 and serialized.stats.trails == 0 and serialized.stats.lights == 0) then
65
+ return {
66
+ status = "error",
67
+ error = "Objek '" .. targetInstance.Name .. "' tidak memiliki komponen VFX (ParticleEmitter, Beam, Trail, atau Light)."
68
+ }
69
+ end
70
+
71
+ return {
72
+ status = "success",
73
+ result = {
74
+ name = vfxName,
75
+ category = category,
76
+ tags = tags,
77
+ description = parsedData.description or ("Scanned VFX from " .. targetInstance:GetFullName()),
78
+ stats = serialized.stats,
79
+ nodeCount = serialized.nodeCount,
80
+ rootClass = serialized.rootClass,
81
+ data = serialized
82
+ }
83
+ }
84
+ end
85
+
@@ -0,0 +1,122 @@
1
+ --!strict
2
+ -- ══════════════════════════════════════════════════════════════════════
3
+ -- Skill Module: scrub_timeline
4
+ -- Deskripsi: Headless Timeline Scrubbing & VFX Playhead Controller
5
+ -- Mengevaluasi pose rig dan memicu burst/scrub VFX pada timestamp spesifik (t)
6
+ -- secara deterministik langsung di Viewport Roblox Studio.
7
+ -- ══════════════════════════════════════════════════════════════════════
8
+
9
+ local HttpService = game:GetService("HttpService")
10
+ local kinematicsMod = script.Parent:WaitForChild("nv_kinematics")
11
+ local Kinematics = require(kinematicsMod)
12
+ local curvesMod = script.Parent:WaitForChild("nv_curves")
13
+ local Curves = require(curvesMod)
14
+ local motionMod = script.Parent:WaitForChild("nv_motion_generator")
15
+ local Motion = require(motionMod)
16
+
17
+ return function(targetInstance: Instance?, data: any, ctx: any, targetPath: string?)
18
+ if not targetInstance then
19
+ return { status = "error", error = "Target instance untuk scrub_timeline tidak ditemukan: " .. tostring(targetPath) }
20
+ end
21
+
22
+ local payload: { [string]: any } = {}
23
+ pcall(function()
24
+ if type(data) == "table" then payload = data
25
+ else payload = HttpService:JSONDecode(tostring(data)) end
26
+ end)
27
+
28
+ local timePos = tonumber(payload.time) or 0.0
29
+ local action = payload.action or "melee_slash"
30
+ local emitVfx = payload.emit_vfx == true or payload.emit == true
31
+ local emitCount = tonumber(payload.emit_count) or 20
32
+
33
+ local rigInfo = Kinematics.inspectRig(targetInstance:IsA("Model") and targetInstance or targetInstance:FindFirstAncestorOfClass("Model") :: any)
34
+ local appliedMotors: { string } = {}
35
+
36
+ -- 1. Scrub Rig Joint Poses jika target adalah Rig
37
+ if rigInfo then
38
+ local keyframes: { Motion.KeyframeData }
39
+ if action == "walk_cycle" then
40
+ keyframes = Motion.generateWalkCycle(rigInfo, payload.parameters)
41
+ elseif action == "punch" then
42
+ keyframes = Motion.generatePunch(rigInfo, payload.parameters)
43
+ elseif action == "idle" or action == "idle_breath" then
44
+ keyframes = Motion.generateIdle(rigInfo, payload.parameters)
45
+ else
46
+ keyframes = Motion.generateMeleeSlash(rigInfo, payload.parameters)
47
+ end
48
+
49
+ if #keyframes > 0 then
50
+ table.sort(keyframes, function(a, b) return a.time < b.time end)
51
+ local totalDur = keyframes[#keyframes].time
52
+ local clampedTime = math.clamp(timePos, 0, totalDur)
53
+
54
+ -- Cari segmen [kA, kB]
55
+ local kA = keyframes[1]
56
+ local kB = keyframes[#keyframes]
57
+ for i = 1, #keyframes - 1 do
58
+ if clampedTime >= keyframes[i].time and clampedTime <= keyframes[i + 1].time then
59
+ kA = keyframes[i]
60
+ kB = keyframes[i + 1]
61
+ break
62
+ end
63
+ end
64
+
65
+ local segDur = kB.time - kA.time
66
+ local alpha = segDur > 1e-5 and ((clampedTime - kA.time) / segDur) or 0
67
+ local curvedAlpha = Curves.evaluate(payload.easing or "QuadInOut", alpha)
68
+
69
+ for motorName, cfA in pairs(kA.poses) do
70
+ local cfB = kB.poses[motorName] or cfA
71
+ local interpCF = Curves.slerp(cfA, cfB, curvedAlpha)
72
+ local m = rigInfo.motors[motorName]
73
+ if m then
74
+ pcall(function()
75
+ m.C0 = interpCF
76
+ table.insert(appliedMotors, motorName)
77
+ end)
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ -- 2. Scrub & Trigger VFX Emitters
84
+ local emittersTriggered = 0
85
+ local beamsToggled = 0
86
+
87
+ local function checkVfx(inst: Instance)
88
+ if inst:IsA("ParticleEmitter") then
89
+ if emitVfx then
90
+ pcall(function()
91
+ inst:Emit(emitCount)
92
+ emittersTriggered += 1
93
+ end)
94
+ end
95
+ elseif inst:IsA("Beam") or inst:IsA("Trail") then
96
+ if payload.enabled ~= nil then
97
+ pcall(function()
98
+ inst.Enabled = payload.enabled
99
+ beamsToggled += 1
100
+ end)
101
+ end
102
+ end
103
+ end
104
+
105
+ checkVfx(targetInstance)
106
+ for _, desc in ipairs(targetInstance:GetDescendants()) do
107
+ checkVfx(desc)
108
+ end
109
+
110
+ return {
111
+ status = "success",
112
+ result = {
113
+ time = timePos,
114
+ action = action,
115
+ motorsPosed = #appliedMotors,
116
+ emittersTriggered = emittersTriggered,
117
+ beamsToggled = beamsToggled,
118
+ message = string.format("Timeline di-scrub ke t=%.2fs (Pose: %d joint, VFX: %d emitter burst)", timePos, #appliedMotors, emittersTriggered)
119
+ }
120
+ }
121
+ end
122
+