@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.
- package/README.md +57 -47
- package/agent-plugin/mcp_config.json +8 -0
- package/agent-plugin/plugin.json +6 -0
- package/agent-plugin/rules/AGENTS.md +118 -0
- package/agent-plugin/skills/nvstudio-animation-vfx/SKILL.md +120 -0
- package/agent-plugin/skills/nvstudio-assets-terrain-guide/SKILL.md +27 -0
- package/agent-plugin/skills/nvstudio-gui-standards/SKILL.md +419 -0
- package/agent-plugin/skills/nvstudio-mcp-guide/SKILL.md +83 -0
- package/agent-plugin/skills/nvstudio-rig-engine/SKILL.md +170 -0
- package/agent-plugin/skills/nvstudio-roblox-sync-guide/SKILL.md +16 -0
- package/dist/build-plugin.js +55 -0
- package/dist/index.js +841 -3
- package/dist/luau-validator.js +99 -34
- package/dist/roblox-bridge.js +152 -4
- package/dist/roblox-docs.js +264 -0
- package/dist/vfx-database.js +507 -0
- package/package.json +6 -4
- package/public/index.html +1590 -180
- 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/batch_create_instances.lua +128 -0
- package/studio-plugin/batch_set_properties.lua +69 -0
- package/studio-plugin/edit_rig.lua +35 -0
- package/studio-plugin/insert_asset.lua +85 -12
- 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 +3824 -60
- package/studio-plugin/preview_asset.lua +99 -0
- package/studio-plugin/scan_security.lua +81 -0
- package/studio-plugin/scan_vfx.lua +85 -0
- package/studio-plugin/scrub_timeline.lua +122 -0
- package/studio-plugin/security_scanner.lua +346 -0
- package/studio-plugin/set_properties.lua +83 -0
- package/studio-plugin/solve_limb_ik.lua +63 -0
- 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
|
+
|