@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,392 @@
1
+ --!strict
2
+ -- ══════════════════════════════════════════════════════════════════════
3
+ -- NVStudio Subsystem: nv_auto_rigger
4
+ -- Deskripsi: Mesin Auto-Rigging Deterministik & Heuristik Geometris
5
+ -- Membangun struktur rig lengkap untuk Avatar R6, R15, Senjata/Prop,
6
+ -- dan Assembly Model secara instan dengan zero-dislocation math.
7
+ -- ══════════════════════════════════════════════════════════════════════
8
+
9
+ local jointMathMod = script.Parent:WaitForChild("nv_joint_math")
10
+ local JointMath = require(jointMathMod)
11
+
12
+ local AutoRigger = {}
13
+
14
+ -- ── Helper: Dapatkan atau buat Motor6D ─────────────────────────────────────────
15
+ local function createOrUpdateMotor(name: string, part0: BasePart, part1: BasePart, parent: Instance?): Motor6D
16
+ part1.Anchored = false
17
+ local existing = part0:FindFirstChild(name) or part1:FindFirstChild(name)
18
+ local motor: Motor6D
19
+ if existing and existing:IsA("Motor6D") then
20
+ motor = existing
21
+ else
22
+ motor = Instance.new("Motor6D")
23
+ motor.Name = name
24
+ end
25
+
26
+ motor.Part0 = part0
27
+ motor.Part1 = part1
28
+ local c0, c1 = JointMath.computeInitialOffsets(part0, part1)
29
+ motor.C0 = c0
30
+ motor.C1 = c1
31
+ motor.Parent = parent or part0
32
+ return motor
33
+ end
34
+
35
+ -- ── 1. Auto-Rig R6 Avatar ─────────────────────────────────────────────────────
36
+ function AutoRigger.rigR6(model: Model): { [string]: any }
37
+ assert(model and model:IsA("Model"), "Target harus berupa Model")
38
+
39
+ local parts: { [string]: BasePart } = {}
40
+ for _, desc in ipairs(model:GetDescendants()) do
41
+ if desc:IsA("BasePart") then
42
+ parts[desc.Name] = desc
43
+ end
44
+ end
45
+
46
+ local torso = parts["Torso"] or parts["UpperTorso"]
47
+ if not torso then
48
+ return { status = "error", error = "Model tidak memiliki part 'Torso' untuk R6 rig." }
49
+ end
50
+
51
+ local rootPart = parts["HumanoidRootPart"]
52
+ if not rootPart then
53
+ -- Buat HumanoidRootPart transparan jika belum ada
54
+ rootPart = Instance.new("Part")
55
+ rootPart.Name = "HumanoidRootPart"
56
+ rootPart.Size = Vector3.new(2, 2, 1)
57
+ rootPart.Transparency = 1
58
+ rootPart.CanCollide = false
59
+ rootPart.CFrame = torso.CFrame
60
+ rootPart.Parent = model
61
+ parts["HumanoidRootPart"] = rootPart
62
+ model.PrimaryPart = rootPart
63
+ end
64
+
65
+ local created = {}
66
+
67
+ -- 1. RootJoint: HumanoidRootPart -> Torso
68
+ local rootJoint = createOrUpdateMotor("RootJoint", rootPart, torso, rootPart)
69
+ table.insert(created, "RootJoint")
70
+
71
+ -- 2. Neck: Torso -> Head
72
+ if parts["Head"] then
73
+ createOrUpdateMotor("Neck", torso, parts["Head"], torso)
74
+ table.insert(created, "Neck")
75
+ end
76
+
77
+ -- 3. Shoulders: Torso -> Arms
78
+ local lArm = parts["Left Arm"] or parts["LeftArm"]
79
+ if lArm then
80
+ createOrUpdateMotor("Left Shoulder", torso, lArm, torso)
81
+ table.insert(created, "Left Shoulder")
82
+ end
83
+
84
+ local rArm = parts["Right Arm"] or parts["RightArm"]
85
+ if rArm then
86
+ createOrUpdateMotor("Right Shoulder", torso, rArm, torso)
87
+ table.insert(created, "Right Shoulder")
88
+ end
89
+
90
+ -- 4. Hips: Torso -> Legs
91
+ local lLeg = parts["Left Leg"] or parts["LeftLeg"]
92
+ if lLeg then
93
+ createOrUpdateMotor("Left Hip", torso, lLeg, torso)
94
+ table.insert(created, "Left Hip")
95
+ end
96
+
97
+ local rLeg = parts["Right Leg"] or parts["RightLeg"]
98
+ if rLeg then
99
+ createOrUpdateMotor("Right Hip", torso, rLeg, torso)
100
+ table.insert(created, "Right Hip")
101
+ end
102
+
103
+ -- Pastikan Humanoid bertipe R6
104
+ local humanoid = model:FindFirstChildOfClass("Humanoid")
105
+ if humanoid then
106
+ humanoid.RigType = Enum.HumanoidRigType.R6
107
+ end
108
+
109
+ return {
110
+ status = "success",
111
+ rigType = "R6",
112
+ jointsCreated = #created,
113
+ jointNames = created,
114
+ rootPart = rootPart.Name
115
+ }
116
+ end
117
+
118
+ -- ── 2. Auto-Rig R15 Avatar ────────────────────────────────────────────────────
119
+ function AutoRigger.rigR15(model: Model): { [string]: any }
120
+ assert(model and model:IsA("Model"), "Target harus berupa Model")
121
+
122
+ local parts: { [string]: BasePart } = {}
123
+ for _, desc in ipairs(model:GetDescendants()) do
124
+ if desc:IsA("BasePart") then
125
+ parts[desc.Name] = desc
126
+ end
127
+ end
128
+
129
+ local lowerTorso = parts["LowerTorso"]
130
+ local upperTorso = parts["UpperTorso"]
131
+ if not lowerTorso or not upperTorso then
132
+ return { status = "error", error = "Model tidak memiliki 'LowerTorso' dan 'UpperTorso' untuk R15 rig." }
133
+ end
134
+
135
+ local rootPart = parts["HumanoidRootPart"]
136
+ if not rootPart then
137
+ rootPart = Instance.new("Part")
138
+ rootPart.Name = "HumanoidRootPart"
139
+ rootPart.Size = Vector3.new(2, 2, 1)
140
+ rootPart.Transparency = 1
141
+ rootPart.CanCollide = false
142
+ rootPart.CFrame = lowerTorso.CFrame
143
+ rootPart.Parent = model
144
+ parts["HumanoidRootPart"] = rootPart
145
+ model.PrimaryPart = rootPart
146
+ end
147
+
148
+ local r15Hierarchy = {
149
+ { name = "Root", p0 = "HumanoidRootPart", p1 = "LowerTorso" },
150
+ { name = "Waist", p0 = "LowerTorso", p1 = "UpperTorso" },
151
+ { name = "Neck", p0 = "UpperTorso", p1 = "Head" },
152
+
153
+ -- Left Arm Chain
154
+ { name = "LeftShoulder", p0 = "UpperTorso", p1 = "LeftUpperArm" },
155
+ { name = "LeftElbow", p0 = "LeftUpperArm", p1 = "LeftLowerArm" },
156
+ { name = "LeftWrist", p0 = "LeftLowerArm", p1 = "LeftHand" },
157
+
158
+ -- Right Arm Chain
159
+ { name = "RightShoulder", p0 = "UpperTorso", p1 = "RightUpperArm" },
160
+ { name = "RightElbow", p0 = "RightUpperArm", p1 = "RightLowerArm" },
161
+ { name = "RightWrist", p0 = "RightLowerArm", p1 = "RightHand" },
162
+
163
+ -- Left Leg Chain
164
+ { name = "LeftHip", p0 = "LowerTorso", p1 = "LeftUpperLeg" },
165
+ { name = "LeftKnee", p0 = "LeftUpperLeg", p1 = "LeftLowerLeg" },
166
+ { name = "LeftAnkle", p0 = "LeftLowerLeg", p1 = "LeftFoot" },
167
+
168
+ -- Right Leg Chain
169
+ { name = "RightHip", p0 = "LowerTorso", p1 = "RightUpperLeg" },
170
+ { name = "RightKnee", p0 = "RightUpperLeg", p1 = "RightLowerLeg" },
171
+ { name = "RightAnkle", p0 = "RightLowerLeg", p1 = "RightFoot" }
172
+ }
173
+
174
+ local created = {}
175
+ for _, item in ipairs(r15Hierarchy) do
176
+ local p0 = parts[item.p0]
177
+ local p1 = parts[item.p1]
178
+ if p0 and p1 then
179
+ createOrUpdateMotor(item.name, p0, p1, p0)
180
+ table.insert(created, item.name)
181
+ end
182
+ end
183
+
184
+ local humanoid = model:FindFirstChildOfClass("Humanoid")
185
+ if humanoid then
186
+ humanoid.RigType = Enum.HumanoidRigType.R15
187
+ end
188
+
189
+ return {
190
+ status = "success",
191
+ rigType = "R15",
192
+ jointsCreated = #created,
193
+ jointNames = created,
194
+ rootPart = rootPart.Name
195
+ }
196
+ end
197
+
198
+ -- ── 3. Rig Weapon / Tool ke Karakter ──────────────────────────────────────────
199
+ function AutoRigger.rigWeapon(characterModel: Model, weaponInstance: Instance, targetLimbName: string?): { [string]: any }
200
+ assert(characterModel and characterModel:IsA("Model"), "Character target harus berupa Model")
201
+ assert(weaponInstance and weaponInstance:IsA("Instance"), "Weapon target tidak valid")
202
+
203
+ local parts: { [string]: BasePart } = {}
204
+ for _, desc in ipairs(characterModel:GetDescendants()) do
205
+ if desc:IsA("BasePart") then
206
+ parts[desc.Name] = desc
207
+ end
208
+ end
209
+
210
+ local limb = targetLimbName and parts[targetLimbName]
211
+ if not limb then
212
+ limb = parts["RightHand"] or parts["Right Arm"] or parts["RightArm"] or parts["HumanoidRootPart"]
213
+ end
214
+ if not limb then
215
+ return { status = "error", error = "Tidak dapat menemukan anggota tubuh target (RightHand / Right Arm) pada karakter." }
216
+ end
217
+
218
+ -- Cari handle pada senjata
219
+ local weaponHandle: BasePart? = nil
220
+ if weaponInstance:IsA("BasePart") then
221
+ weaponHandle = weaponInstance
222
+ elseif weaponInstance:IsA("Model") or weaponInstance:IsA("Tool") then
223
+ weaponHandle = (weaponInstance :: any).PrimaryPart
224
+ or weaponInstance:FindFirstChild("Handle") :: BasePart?
225
+ or weaponInstance:FindFirstChildWhichIsA("BasePart")
226
+ end
227
+
228
+ if not weaponHandle then
229
+ return { status = "error", error = "Senjata tidak memiliki BasePart atau Handle." }
230
+ end
231
+
232
+ -- 1. Buat Motor6D penaut karakter ke Handle
233
+ local weaponMotor = createOrUpdateMotor("WeaponGrip", limb, weaponHandle, limb)
234
+
235
+ -- 2. Las seluruh bagian senjata lain ke Handle
236
+ local weldedCount = 0
237
+ if weaponInstance ~= weaponHandle then
238
+ for _, desc in ipairs(weaponInstance:GetDescendants()) do
239
+ if desc:IsA("BasePart") and desc ~= weaponHandle then
240
+ desc.Anchored = false
241
+ local weld = desc:FindFirstChild("WeldToHandle") or Instance.new("Weld")
242
+ weld.Name = "WeldToHandle"
243
+ weld.Part0 = weaponHandle
244
+ weld.Part1 = desc
245
+ weld.C0 = weaponHandle.CFrame:ToObjectSpace(desc.CFrame)
246
+ weld.C1 = CFrame.identity
247
+ weld.Parent = weaponHandle
248
+ weldedCount += 1
249
+ end
250
+ end
251
+ end
252
+
253
+ return {
254
+ status = "success",
255
+ attachedTo = limb.Name,
256
+ handleName = weaponHandle.Name,
257
+ weldedComponents = weldedCount,
258
+ message = string.format("Senjata '%s' berhasil dirig ke '%s' (%d part dilas)", weaponInstance.Name, limb.Name, weldedCount)
259
+ }
260
+ end
261
+
262
+ -- ── 4. Weld Assembly (Props, Armor, Kendaraan) ────────────────────────────────
263
+ function AutoRigger.weldAssembly(container: Instance, primaryPart: BasePart?): { [string]: any }
264
+ assert(container and container:IsA("Instance"), "Container tidak valid")
265
+
266
+ local rootPart: BasePart? = primaryPart
267
+ if not rootPart and container:IsA("Model") then
268
+ rootPart = container.PrimaryPart
269
+ end
270
+
271
+ local allParts: { BasePart } = {}
272
+ for _, desc in ipairs(container:GetDescendants()) do
273
+ if desc:IsA("BasePart") then
274
+ table.insert(allParts, desc)
275
+ end
276
+ end
277
+ if container:IsA("BasePart") then
278
+ table.insert(allParts, container)
279
+ end
280
+
281
+ if #allParts == 0 then
282
+ return { status = "error", error = "Tidak ada BasePart di dalam kontainer untuk dilas." }
283
+ end
284
+
285
+ -- Jika belum ada rootPart, tentukan berdasarkan volume terbesar
286
+ if not rootPart then
287
+ local maxVol = -1
288
+ for _, p in ipairs(allParts) do
289
+ local vol = p.Size.X * p.Size.Y * p.Size.Z
290
+ if vol > maxVol then
291
+ maxVol = vol
292
+ rootPart = p
293
+ end
294
+ end
295
+ end
296
+
297
+ if not rootPart then
298
+ return { status = "error", error = "Gagal menentukan Root Part untuk pengelasan." }
299
+ end
300
+
301
+ local weldsCreated = 0
302
+ for _, p in ipairs(allParts) do
303
+ if p ~= rootPart then
304
+ p.Anchored = false
305
+ local w = p:FindFirstChild("AssemblyWeld") or Instance.new("Weld")
306
+ w.Name = "AssemblyWeld"
307
+ w.Part0 = rootPart
308
+ w.Part1 = p
309
+ w.C0 = rootPart.CFrame:ToObjectSpace(p.CFrame)
310
+ w.C1 = CFrame.identity
311
+ w.Parent = rootPart
312
+ weldsCreated += 1
313
+ end
314
+ end
315
+
316
+ return {
317
+ status = "success",
318
+ rootPart = rootPart.Name,
319
+ weldsCreated = weldsCreated,
320
+ message = string.format("Berhasil mengelas %d part ke Root '%s'", weldsCreated, rootPart.Name)
321
+ }
322
+ end
323
+
324
+ -- ── 5. Spatial Geometric Limb Analyzer (Heuristik Bounding Box) ───────────────
325
+ -- Menganalisis model dengan nama part acak/impor dan mengklasifikasikan anggota tubuh
326
+ function AutoRigger.discoverLimbsBySpatialGeometry(model: Model): { [string]: BasePart }
327
+ local parts: { BasePart } = {}
328
+ for _, desc in ipairs(model:GetDescendants()) do
329
+ if desc:IsA("BasePart") then
330
+ table.insert(parts, desc)
331
+ end
332
+ end
333
+
334
+ if #parts == 0 then return {} end
335
+
336
+ -- 1. Hitung bounding box pusat model
337
+ local cframe, size = model:GetBoundingBox()
338
+ local center = cframe.Position
339
+
340
+ -- 2. Temukan torso (part dengan volume besar di sekitar pusat vertikal)
341
+ local bestTorso: BasePart = parts[1]
342
+ local bestScore = -1e9
343
+
344
+ for _, p in ipairs(parts) do
345
+ local vol = p.Size.X * p.Size.Y * p.Size.Z
346
+ local distToCenter = (p.Position - center).Magnitude
347
+ local score = vol / (distToCenter + 1)
348
+ if score > bestScore then
349
+ bestScore = score
350
+ bestTorso = p
351
+ end
352
+ end
353
+
354
+ local identified: { [string]: BasePart } = {
355
+ Torso = bestTorso
356
+ }
357
+
358
+ local torsoPos = bestTorso.Position
359
+
360
+ -- 3. Klasifikasi part lainnya relatif terhadap Torso
361
+ for _, p in ipairs(parts) do
362
+ if p ~= bestTorso then
363
+ local relPos = bestTorso.CFrame:PointToObjectSpace(p.Position)
364
+
365
+ if relPos.Y > bestTorso.Size.Y * 0.5 then
366
+ -- Bagian atas -> Head
367
+ if not identified["Head"] or p.Position.Y > identified["Head"].Position.Y then
368
+ identified["Head"] = p
369
+ end
370
+ elseif relPos.Y < -bestTorso.Size.Y * 0.3 then
371
+ -- Bagian bawah -> Legs
372
+ if relPos.X < 0 then
373
+ identified["Left Leg"] = p
374
+ else
375
+ identified["Right Leg"] = p
376
+ end
377
+ else
378
+ -- Bagian samping -> Arms
379
+ if relPos.X < 0 then
380
+ identified["Left Arm"] = p
381
+ else
382
+ identified["Right Arm"] = p
383
+ end
384
+ end
385
+ end
386
+ end
387
+
388
+ return identified
389
+ end
390
+
391
+ return AutoRigger
392
+
@@ -0,0 +1,260 @@
1
+ --!strict
2
+ --!native
3
+ -- ══════════════════════════════════════════════════════════════════════
4
+ -- NVStudio Subsystem: nv_curves
5
+ -- Deskripsi: Mesin Kurva Cubic Bezier, Easing Functions & Quaternion SLERP
6
+ -- Menghitung interpolasi waktu dan rotasi sendi secara ultra-halus
7
+ -- layaknya kurva Graph Editor animator profesional.
8
+ -- ══════════════════════════════════════════════════════════════════════
9
+
10
+ local Curves = {}
11
+ local PI = math.pi
12
+ local HALF_PI = math.pi * 0.5
13
+ local TAU = math.pi * 2
14
+ local DEFAULT_OVERSHOOT = 1.70158
15
+
16
+ -- ── 1. Cubic Bezier Solver ───────────────────────────────────────────────────
17
+ function Curves.bezierParameter(alpha: number, x1: number, x2: number): number
18
+ if alpha <= 0 then return 0 end
19
+ if alpha >= 1 then return 1 end
20
+
21
+ x1 = math.clamp(x1, 0, 1)
22
+ x2 = math.clamp(x2, 0, 1)
23
+
24
+ local lo, hi, u = 0, 1, alpha
25
+ for _ = 1, 32 do
26
+ local v = 1 - u
27
+ local x = 3 * v * v * u * x1 + 3 * v * u * u * x2 + u * u * u
28
+ local delta = x - alpha
29
+ if math.abs(delta) <= 1e-7 then
30
+ break
31
+ end
32
+
33
+ if delta < 0 then
34
+ lo = u
35
+ else
36
+ hi = u
37
+ end
38
+
39
+ local deriv = 3 * v * v * x1 + 6 * v * u * (x2 - x1) + 3 * u * u * (1 - x2)
40
+ local cand = deriv > 1e-7 and (u - delta / deriv) or -1
41
+ u = (cand > lo and cand < hi) and cand or (lo + hi) * 0.5
42
+ end
43
+
44
+ return u
45
+ end
46
+
47
+ function Curves.cubic(a: number, b: number, c: number, d: number, u: number): number
48
+ local v = 1 - u
49
+ return a * (v * v * v) + b * (3 * v * v * u) + c * (3 * v * u * u) + d * (u * u * u)
50
+ end
51
+
52
+ function Curves.bezierAlpha(t: number, x1: number?, y1: number?, x2: number?, y2: number?): number
53
+ if t <= 0 then return 0 end
54
+ if t >= 1 then return 1 end
55
+
56
+ local px1 = x1 or 0.42
57
+ local py1 = y1 or 0.0
58
+ local px2 = x2 or 0.58
59
+ local py2 = y2 or 1.0
60
+
61
+ local u = Curves.bezierParameter(t, px1, px2)
62
+ return Curves.cubic(0, py1, py2, 1, u)
63
+ end
64
+
65
+ -- ── 2. Standard Easing Functions ─────────────────────────────────────────────
66
+ local EasingTable: { [string]: (t: number) -> number } = {
67
+ Linear = function(t) return t end,
68
+
69
+ SineIn = function(t) return 1 - math.cos(t * HALF_PI) end,
70
+ SineOut = function(t) return math.sin(t * HALF_PI) end,
71
+ SineInOut = function(t) return -(math.cos(PI * t) - 1) * 0.5 end,
72
+
73
+ QuadIn = function(t) return t * t end,
74
+ QuadOut = function(t) return t * (2 - t) end,
75
+ QuadInOut = function(t) return t < 0.5 and (2 * t * t) or (-1 + (4 - 2 * t) * t) end,
76
+
77
+ CubicIn = function(t) return t * t * t end,
78
+ CubicOut = function(t) local f = t - 1 return f * f * f + 1 end,
79
+ CubicInOut = function(t) return t < 0.5 and (4 * t * t * t) or ((t - 1) * (2 * t - 2) * (2 * t - 2) + 1) end,
80
+
81
+ QuartIn = function(t) return t * t * t * t end,
82
+ QuartOut = function(t) local f = t - 1 return 1 - f * f * f * f end,
83
+ QuartInOut = function(t) local f = t - 1 return t < 0.5 and (8 * t * t * t * t) or (1 - 8 * f * f * f * f) end,
84
+
85
+ ExpoIn = function(t) return t == 0 and 0 or math.pow(2, 10 * (t - 1)) end,
86
+ ExpoOut = function(t) return t == 1 and 1 or (1 - math.pow(2, -10 * t)) end,
87
+ ExpoInOut = function(t)
88
+ if t == 0 then return 0 end
89
+ if t == 1 then return 1 end
90
+ if t < 0.5 then return math.pow(2, 20 * t - 10) * 0.5 end
91
+ return (2 - math.pow(2, -20 * t + 10)) * 0.5
92
+ end,
93
+
94
+ BackIn = function(t) local s = DEFAULT_OVERSHOOT return t * t * ((s + 1) * t - s) end,
95
+ BackOut = function(t) local s = DEFAULT_OVERSHOOT local f = t - 1 return f * f * ((s + 1) * f + s) + 1 end,
96
+ BackInOut = function(t)
97
+ local s = DEFAULT_OVERSHOOT * 1.525
98
+ local f = t * 2
99
+ if f < 1 then return 0.5 * (f * f * ((s + 1) * f - s)) end
100
+ f = f - 2
101
+ return 0.5 * (f * f * ((s + 1) * f + s) + 2)
102
+ end,
103
+
104
+ BounceOut = function(t)
105
+ if t < (1 / 2.75) then
106
+ return 7.5625 * t * t
107
+ elseif t < (2 / 2.75) then
108
+ local f = t - (1.5 / 2.75)
109
+ return 7.5625 * f * f + 0.75
110
+ elseif t < (2.5 / 2.75) then
111
+ local f = t - (2.25 / 2.75)
112
+ return 7.5625 * f * f + 0.9375
113
+ else
114
+ local f = t - (2.625 / 2.75)
115
+ return 7.5625 * f * f + 0.984375
116
+ end
117
+ end,
118
+ BounceIn = function(t)
119
+ local fn = nil
120
+ -- forward declaration
121
+ return 1 - (function(x)
122
+ if x < (1 / 2.75) then return 7.5625 * x * x
123
+ elseif x < (2 / 2.75) then local f = x - (1.5 / 2.75) return 7.5625 * f * f + 0.75
124
+ elseif x < (2.5 / 2.75) then local f = x - (2.25 / 2.75) return 7.5625 * f * f + 0.9375
125
+ else local f = x - (2.625 / 2.75) return 7.5625 * f * f + 0.984375 end
126
+ end)(1 - t)
127
+ end,
128
+ BounceInOut = function(t)
129
+ local bounceOut = function(x: number): number
130
+ if x < (1 / 2.75) then return 7.5625 * x * x
131
+ elseif x < (2 / 2.75) then local f = x - (1.5 / 2.75) return 7.5625 * f * f + 0.75
132
+ elseif x < (2.5 / 2.75) then local f = x - (2.25 / 2.75) return 7.5625 * f * f + 0.9375
133
+ else local f = x - (2.625 / 2.75) return 7.5625 * f * f + 0.984375 end
134
+ end
135
+ if t < 0.5 then
136
+ return (1 - bounceOut(1 - t * 2)) * 0.5
137
+ end
138
+ return bounceOut(t * 2 - 1) * 0.5 + 0.5
139
+ end,
140
+
141
+ ElasticOut = function(t)
142
+ if t == 0 then return 0 end
143
+ if t == 1 then return 1 end
144
+ local p = 0.3
145
+ local s = p / 4
146
+ return math.pow(2, -10 * t) * math.sin((t - s) * TAU / p) + 1
147
+ end,
148
+ ElasticIn = function(t)
149
+ if t == 0 then return 0 end
150
+ if t == 1 then return 1 end
151
+ local p = 0.3
152
+ local s = p / 4
153
+ local f = t - 1
154
+ return -(math.pow(2, 10 * f) * math.sin((f - s) * TAU / p))
155
+ end,
156
+ ElasticInOut = function(t)
157
+ if t == 0 then return 0 end
158
+ if t == 1 then return 1 end
159
+ local p = 0.3 * 1.5
160
+ local s = p / 4
161
+ local f = t * 2 - 1
162
+ if f < 0 then
163
+ return -0.5 * (math.pow(2, 10 * f) * math.sin((f - s) * TAU / p))
164
+ end
165
+ return math.pow(2, -10 * f) * math.sin((f - s) * TAU / p) * 0.5 + 1
166
+ end
167
+ }
168
+
169
+ function Curves.evaluate(easingName: string?, t: number): number
170
+ t = math.clamp(t, 0, 1)
171
+ local name = easingName or "QuadInOut"
172
+ local fn = EasingTable[name]
173
+ if fn then
174
+ return fn(t)
175
+ end
176
+ return t
177
+ end
178
+
179
+ -- ── 3. Quaternion SLERP (Spherical Linear Interpolation) ──────────────────────
180
+ -- Representasi Quaternion: { x, y, z, w }
181
+ type Quat = { x: number, y: number, z: number, w: number }
182
+
183
+ local function cframeToQuat(cf: CFrame): (Vector3, Quat)
184
+ local _, _, _, m00, m01, m02, m10, m11, m12, m20, m21, m22 = cf:GetComponents()
185
+ local trace = m00 + m11 + m22
186
+ local q: Quat
187
+ if trace > 0 then
188
+ local s = math.sqrt(trace + 1) * 2
189
+ q = { x = (m21 - m12) / s, y = (m02 - m20) / s, z = (m10 - m01) / s, w = 0.25 * s }
190
+ elseif (m00 > m11) and (m00 > m22) then
191
+ local s = math.sqrt(1 + m00 - m11 - m22) * 2
192
+ q = { x = 0.25 * s, y = (m01 + m10) / s, z = (m02 + m20) / s, w = (m21 - m12) / s }
193
+ elseif m11 > m22 then
194
+ local s = math.sqrt(1 + m11 - m00 - m22) * 2
195
+ q = { x = (m01 + m10) / s, y = 0.25 * s, z = (m12 + m21) / s, w = (m02 - m20) / s }
196
+ else
197
+ local s = math.sqrt(1 + m22 - m00 - m11) * 2
198
+ q = { x = (m02 + m20) / s, y = (m12 + m21) / s, z = 0.25 * s, w = (m10 - m01) / s }
199
+ end
200
+ return cf.Position, q
201
+ end
202
+
203
+ local function quatToCFrame(pos: Vector3, q: Quat): CFrame
204
+ local x, y, z, w = q.x, q.y, q.z, q.w
205
+ local xx, yy, zz = x * x, y * y, z * z
206
+ local xy, xz, yz = x * y, x * z, y * z
207
+ local wx, wy, wz = w * x, w * y, w * z
208
+
209
+ return CFrame.new(
210
+ pos.X, pos.Y, pos.Z,
211
+ 1 - 2 * (yy + zz), 2 * (xy - wz), 2 * (xz + wy),
212
+ 2 * (xy + wz), 1 - 2 * (xx + zz), 2 * (yz - wx),
213
+ 2 * (xz - wy), 2 * (yz + wx), 1 - 2 * (xx + yy)
214
+ )
215
+ end
216
+
217
+ function Curves.slerp(cfA: CFrame, cfB: CFrame, alpha: number): CFrame
218
+ if alpha <= 0 then return cfA end
219
+ if alpha >= 1 then return cfB end
220
+
221
+ local posA, qA = cframeToQuat(cfA)
222
+ local posB, qB = cframeToQuat(cfB)
223
+
224
+ local cosHalfTheta = qA.x * qB.x + qA.y * qB.y + qA.z * qB.z + qA.w * qB.w
225
+ if cosHalfTheta < 0 then
226
+ qB = { x = -qB.x, y = -qB.y, z = -qB.z, w = -qB.w }
227
+ cosHalfTheta = -cosHalfTheta
228
+ end
229
+
230
+ local qResult: Quat
231
+ if math.abs(cosHalfTheta) >= 1 then
232
+ qResult = qA
233
+ else
234
+ local halfTheta = math.acos(math.clamp(cosHalfTheta, -1, 1))
235
+ local sinHalfTheta = math.sqrt(1 - cosHalfTheta * cosHalfTheta)
236
+ if math.abs(sinHalfTheta) < 1e-4 then
237
+ qResult = {
238
+ x = qA.x * (1 - alpha) + qB.x * alpha,
239
+ y = qA.y * (1 - alpha) + qB.y * alpha,
240
+ z = qA.z * (1 - alpha) + qB.z * alpha,
241
+ w = qA.w * (1 - alpha) + qB.w * alpha,
242
+ }
243
+ else
244
+ local ratioA = math.sin((1 - alpha) * halfTheta) / sinHalfTheta
245
+ local ratioB = math.sin(alpha * halfTheta) / sinHalfTheta
246
+ qResult = {
247
+ x = qA.x * ratioA + qB.x * ratioB,
248
+ y = qA.y * ratioA + qB.y * ratioB,
249
+ z = qA.z * ratioA + qB.z * ratioB,
250
+ w = qA.w * ratioA + qB.w * ratioB,
251
+ }
252
+ end
253
+ end
254
+
255
+ local interpPos = posA:Lerp(posB, alpha)
256
+ return quatToCFrame(interpPos, qResult)
257
+ end
258
+
259
+ return Curves
260
+