@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.
@@ -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
+
@@ -33,6 +33,79 @@ return function(targetInstance, data, ctx, targetPath)
33
33
  targetInstance[prop] = BrickColor.new(val)
34
34
  elseif typeof(currentVal) == "CFrame" and type(val) == "table" then
35
35
  targetInstance[prop] = CFrame.new(unpack(val))
36
+ elseif typeof(currentVal) == "ColorSequence" or (type(val) == "table" and val.t == "ColorSequence") then
37
+ local kps = {}
38
+ local rawList = (type(val) == "table" and val.t == "ColorSequence") and val.v or val
39
+ if type(rawList) == "table" then
40
+ for _, kp in ipairs(rawList) do
41
+ local t = kp[1] or 0
42
+ local col = kp[2]
43
+ local c3
44
+ if typeof(col) == "Color3" then
45
+ c3 = col
46
+ elseif type(col) == "table" and col.t == "Color3" then
47
+ c3 = Color3.new(col.v[1], col.v[2], col.v[3])
48
+ elseif type(col) == "table" and #col >= 3 then
49
+ c3 = (col[1] > 1 or col[2] > 1 or col[3] > 1) and Color3.fromRGB(col[1], col[2], col[3]) or Color3.new(col[1], col[2], col[3])
50
+ else
51
+ c3 = Color3.new(1, 1, 1)
52
+ end
53
+ table.insert(kps, ColorSequenceKeypoint.new(math.clamp(t, 0, 1), c3))
54
+ end
55
+ end
56
+ if #kps >= 2 then
57
+ table.sort(kps, function(a, b) return a.Time < b.Time end)
58
+ -- Pastikan keypoint pertama t=0 dan terakhir t=1
59
+ if kps[1].Time ~= 0 then
60
+ kps[1] = ColorSequenceKeypoint.new(0, kps[1].Value)
61
+ end
62
+ if kps[#kps].Time ~= 1 then
63
+ kps[#kps] = ColorSequenceKeypoint.new(1, kps[#kps].Value)
64
+ end
65
+ -- Pastikan waktu meningkat secara monoton murni
66
+ for i = 2, #kps do
67
+ if kps[i].Time <= kps[i - 1].Time then
68
+ local newT = math.min(1.0, kps[i - 1].Time + 0.001)
69
+ kps[i] = ColorSequenceKeypoint.new(newT, kps[i].Value)
70
+ end
71
+ end
72
+ targetInstance[prop] = ColorSequence.new(kps)
73
+ elseif #kps == 1 then
74
+ targetInstance[prop] = ColorSequence.new(kps[1].Value)
75
+ end
76
+ elseif typeof(currentVal) == "NumberSequence" or (type(val) == "table" and val.t == "NumberSequence") then
77
+ local kps = {}
78
+ local rawList = (type(val) == "table" and val.t == "NumberSequence") and val.v or val
79
+ if type(rawList) == "table" then
80
+ for _, kp in ipairs(rawList) do
81
+ if type(kp) == "table" then
82
+ local t = kp[1] or 0
83
+ local v = kp[2] or 0
84
+ local env = kp[3] or 0
85
+ table.insert(kps, NumberSequenceKeypoint.new(math.clamp(t, 0, 1), v, env))
86
+ end
87
+ end
88
+ end
89
+ if #kps >= 2 then
90
+ table.sort(kps, function(a, b) return a.Time < b.Time end)
91
+ -- Pastikan keypoint pertama t=0 dan terakhir t=1
92
+ if kps[1].Time ~= 0 then
93
+ kps[1] = NumberSequenceKeypoint.new(0, kps[1].Value, kps[1].Envelope)
94
+ end
95
+ if kps[#kps].Time ~= 1 then
96
+ kps[#kps] = NumberSequenceKeypoint.new(1, kps[#kps].Value, kps[#kps].Envelope)
97
+ end
98
+ -- Pastikan waktu meningkat secara monoton murni
99
+ for i = 2, #kps do
100
+ if kps[i].Time <= kps[i - 1].Time then
101
+ local newT = math.min(1.0, kps[i - 1].Time + 0.001)
102
+ kps[i] = NumberSequenceKeypoint.new(newT, kps[i].Value, kps[i].Envelope)
103
+ end
104
+ end
105
+ targetInstance[prop] = NumberSequence.new(kps)
106
+ elseif #kps == 1 then
107
+ targetInstance[prop] = NumberSequence.new(kps[1].Value)
108
+ end
36
109
  elseif typeof(currentVal) == "EnumItem" and type(val) == "string" then
37
110
  local enumType = tostring(currentVal.EnumType)
38
111
  local successEnum, enumValue = pcall(function() return Enum[enumType][val] end)
@@ -0,0 +1,63 @@
1
+ --!strict
2
+ -- ══════════════════════════════════════════════════════════════════════
3
+ -- Skill Module: solve_limb_ik
4
+ -- Deskripsi: Semantic Tool untuk AI Agent menghitung orientasi sendi
5
+ -- lengan / kaki menuju koordinat sasaran 3D secara analitis O(1).
6
+ -- ══════════════════════════════════════════════════════════════════════
7
+
8
+ local HttpService = game:GetService("HttpService")
9
+ local kinematicsMod = script.Parent:WaitForChild("nv_kinematics")
10
+ local Kinematics = require(kinematicsMod)
11
+
12
+ return function(targetInstance: Instance?, data: any, ctx: any, targetPath: string?)
13
+ if not targetInstance then
14
+ return { status = "error", error = "Target karakter rig untuk solve_limb_ik tidak ditemukan: " .. tostring(targetPath) }
15
+ end
16
+
17
+ local rigInfo = Kinematics.inspectRig(targetInstance:IsA("Model") and targetInstance or targetInstance:FindFirstAncestorOfClass("Model") :: any)
18
+ if not rigInfo then
19
+ return { status = "error", error = "Target bukan model karakter rig (R6/R15) yang valid: " .. targetInstance:GetFullName() }
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 limb = tostring(payload.limb or "RightArm")
29
+ local goalArr = payload.goal
30
+ if not goalArr or type(goalArr) ~= "table" or #goalArr < 3 then
31
+ return { status = "error", error = "Parameter 'goal' [X, Y, Z] wajib diisi." }
32
+ end
33
+
34
+ local goalPos = Vector3.new(goalArr[1], goalArr[2], goalArr[3])
35
+ local polePos: Vector3? = nil
36
+ if payload.pole and type(payload.pole) == "table" and #payload.pole >= 3 then
37
+ polePos = Vector3.new(payload.pole[1], payload.pole[2], payload.pole[3])
38
+ end
39
+
40
+ local apply = payload.apply_to_rig ~= false
41
+ local solved = Kinematics.solveLimb(rigInfo, limb, goalPos, polePos, apply)
42
+
43
+ if not solved then
44
+ return { status = "error", error = "Gagal menyelesaikan IK untuk anggota tubuh: " .. limb }
45
+ end
46
+
47
+ local resultC0: { [string]: any } = {}
48
+ for mName, cf in pairs(solved) do
49
+ resultC0[mName] = { t = "CFrame", v = { cf:GetComponents() } }
50
+ end
51
+
52
+ return {
53
+ status = "success",
54
+ result = {
55
+ limb = limb,
56
+ rigType = rigInfo.isR15 and "R15" or "R6",
57
+ applied = apply,
58
+ targetGoal = { goalPos.X, goalPos.Y, goalPos.Z },
59
+ solvedMotors = resultC0
60
+ }
61
+ }
62
+ end
63
+
@@ -95,7 +95,7 @@ function UIManager.init(pluginInstance, studioId)
95
95
 
96
96
  -- Version
97
97
  local versionLabel = Instance.new("TextLabel")
98
- versionLabel.Text = "v2.3.0"
98
+ versionLabel.Text = "v2.4.1"
99
99
  versionLabel.Font = Enum.Font.Gotham
100
100
  versionLabel.TextSize = 11
101
101
  versionLabel.TextColor3 = Color3.fromRGB(120, 120, 130)