@rbxts/vfx-forge 2.2.2

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 (38) hide show
  1. package/LICENSE +82 -0
  2. package/README.md +39 -0
  3. package/out/effects/beam.luau +312 -0
  4. package/out/effects/bezier.luau +392 -0
  5. package/out/effects/camera_shake.luau +200 -0
  6. package/out/effects/lightning.luau +1183 -0
  7. package/out/effects/mesh.luau +466 -0
  8. package/out/effects/particle.luau +64 -0
  9. package/out/effects/randomizer.luau +110 -0
  10. package/out/effects/screen.luau +61 -0
  11. package/out/effects/shockwave_debris.luau +277 -0
  12. package/out/effects/shockwave_line.luau +356 -0
  13. package/out/effects/shockwave_ring.luau +252 -0
  14. package/out/effects/sound.luau +311 -0
  15. package/out/effects/spin.luau +88 -0
  16. package/out/effects/tweener.luau +122 -0
  17. package/out/emitters.luau +387 -0
  18. package/out/index.d.ts +341 -0
  19. package/out/init.luau +279 -0
  20. package/out/mod/attributes.luau +227 -0
  21. package/out/mod/color/Oklab.luau +93 -0
  22. package/out/mod/color/sRGB.luau +71 -0
  23. package/out/mod/common/bezier.luau +372 -0
  24. package/out/mod/common/flipbook.luau +102 -0
  25. package/out/mod/lerp.luau +210 -0
  26. package/out/mod/logger.luau +20 -0
  27. package/out/mod/shape.luau +207 -0
  28. package/out/mod/tween.luau +161 -0
  29. package/out/mod/utility.luau +707 -0
  30. package/out/obj/Bezier.luau +268 -0
  31. package/out/obj/ObjectCache.luau +289 -0
  32. package/out/services/caches.luau +62 -0
  33. package/out/services/effects.luau +234 -0
  34. package/out/services/enabled_effects.luau +120 -0
  35. package/out/services/texture_loader.luau +174 -0
  36. package/out/tsconfig.tsbuildinfo +1 -0
  37. package/out/types.luau +43 -0
  38. package/package.json +63 -0
@@ -0,0 +1,252 @@
1
+ local CollectionService = game:GetService("CollectionService")
2
+
3
+ local attr = require("@mod/attributes")
4
+ local tween = require("@mod/tween")
5
+ local types = require("@root/types")
6
+ local utility = require("@mod/utility")
7
+
8
+ local Promise = require("@pkg/Promise")
9
+ local ObjectCache = require("@obj/ObjectCache")
10
+
11
+ local rng = Random.new()
12
+
13
+ local ring = {}
14
+
15
+ local part_cache: ObjectCache.ObjectCache?
16
+
17
+ function ring.init(cache)
18
+ part_cache = cache
19
+ end
20
+
21
+ function ring.deinit()
22
+ part_cache = nil
23
+ end
24
+
25
+ function ring.emit(origin: Attachment, ref: Part, scope: types.scope)
26
+ if not part_cache then
27
+ return
28
+ end
29
+
30
+ local raydir = attr.get(origin, "RayDirection", vector.create(0, -50, 0))
31
+
32
+ local rayColGroup = attr.get(origin, "RayCollisionGroup", "Default")
33
+ local rayFilterTag = attr.get(origin, "FilterTag", "")
34
+ local rayFilterType = attr.get(origin, "FilterType", "Exclude")
35
+
36
+ local rayIgnoreWater = attr.get(origin, "IgnoreWater", true)
37
+ local rayIgnoreCanCollide = attr.get(origin, "IgnoreCanCollide", false)
38
+
39
+ local params = RaycastParams.new()
40
+ params.CollisionGroup = rayColGroup
41
+ params.IgnoreWater = rayIgnoreWater
42
+ params.RespectCanCollide = not rayIgnoreCanCollide
43
+ params.FilterType = Enum.RaycastFilterType[rayFilterType]
44
+ params.FilterDescendantsInstances = CollectionService:GetTagged(rayFilterTag)
45
+
46
+ if rayFilterType == "Exclude" then
47
+ params:AddToFilter({ workspace.Terrain })
48
+ end
49
+
50
+ local emitDelay = attr.get(ref, "EmitDelay", 0)
51
+
52
+ local radius = attr.get(ref, "Radius", 5)
53
+ local segments = attr.get(ref, "Segments", 7)
54
+
55
+ local lifetime = attr.getRange(ref, "Lifetime", NumberRange.new(2, 3), NumberRange.new(0, math.huge))
56
+
57
+ local old_partOffset = attr.get(ref, "PartOffset", vector.zero, true)
58
+
59
+ local baseOffset = attr.get(ref, "BaseOffset", old_partOffset)
60
+ local offsetStart = attr.get(ref, "Offset_Start", old_partOffset)
61
+ local offsetEnd = attr.get(ref, "Offset_End", old_partOffset)
62
+
63
+ local offsetStartDuration = attr.get(ref, "Offset_Start_Duration", 0.5)
64
+ local offsetEndDuration = attr.get(ref, "Offset_End_Duration", 0.5)
65
+
66
+ local sizeScaleStart = attr.get(ref, "SizeScaleStart", vector.zero)
67
+ local sizeScaleEnd = attr.get(ref, "SizeScaleEnd", vector.zero)
68
+
69
+ local minSize = attr.get(ref, "MinSize", vector.create(2, 1, 2))
70
+ local maxSize = attr.get(ref, "MaxSize", vector.create(3, 2, 3))
71
+
72
+ local old_sizeCurve = attr.get(ref, "Size_Curve", utility.default_bezier, true)
73
+ local old_sizeDuration = attr.get(ref, "Size_Duration", 0.5, true)
74
+
75
+ local sizeStartCurve = attr.get(ref, "Size_Start_Curve", old_sizeCurve)
76
+ local sizeEndCurve = attr.get(ref, "Size_End_Curve", old_sizeCurve)
77
+
78
+ local sizeStartDuration = attr.get(ref, "Size_Start_Duration", old_sizeDuration)
79
+ local sizeEndDuration = attr.get(ref, "Size_End_Duration", old_sizeDuration)
80
+
81
+ local tpDuration = attr.get(ref, "Transparency_Duration", 0.5)
82
+
83
+ local tpStart = attr.get(ref, "Transparency_Start", 0)
84
+ local tpEnd = attr.get(ref, "Transparency_End", 0)
85
+
86
+ task.wait(emitDelay)
87
+
88
+ local promises = {}
89
+ local originCFrame = utility.getTransformedOriginExtents(origin)
90
+
91
+ for i = 0, segments - 1 do
92
+ local angle = i / segments * math.pi * 2
93
+
94
+ local x = radius * math.cos(angle)
95
+ local y = radius * math.sin(angle)
96
+
97
+ local result =
98
+ workspace:Raycast((originCFrame * CFrame.new(x, 0, y)).Position, originCFrame:VectorToWorldSpace(raydir), params)
99
+
100
+ if not result then
101
+ continue
102
+ end
103
+
104
+ local width = rng:NextNumber(minSize.X, maxSize.X)
105
+
106
+ local height = rng:NextNumber(minSize.Y, maxSize.Y)
107
+ local length = rng:NextNumber(minSize.Z, maxSize.Z)
108
+
109
+ local id = utility.getRandomId()
110
+
111
+ local part = part_cache:get(id)
112
+
113
+ local realPart = part._getReal()
114
+
115
+ utility.copyProperties(ref, realPart, utility.COPY_PART_PROPERTIES)
116
+ utility.copyProperties(ref, realPart, utility.COPY_EXTENDED_PART_PROPERTIES)
117
+
118
+ if #ref:GetChildren() ~= 0 then
119
+ local clone = ref:Clone()
120
+
121
+ for _, child in clone:GetChildren() do
122
+ child.Parent = realPart
123
+ end
124
+
125
+ clone:Destroy()
126
+ end
127
+
128
+ local emitOnFinish = scope.effects.prepareEmitOnFinish(part, scope)
129
+
130
+ table.insert(scope, function()
131
+ if part_cache then
132
+ part_cache:free(id)
133
+ end
134
+ end)
135
+
136
+ part.Color = result.Instance.Color
137
+ part.Material = result.Material
138
+ part.Transparency = result.Instance.Transparency
139
+
140
+ if part.Transparency == 0 then
141
+ part.Transparency = tpStart
142
+ end
143
+
144
+ part.Size = Vector3.zero
145
+
146
+ local rx = -math.cos(angle)
147
+ local rz = -math.sin(angle)
148
+
149
+ local dir = originCFrame:VectorToWorldSpace(Vector3.new(rx, 0, rz))
150
+ local right = dir:Cross(result.Normal).Unit
151
+
152
+ local rotCF = CFrame.fromMatrix(result.Position, right, result.Normal)
153
+ * CFrame.fromOrientation(-math.atan(height / length), 0, 0)
154
+
155
+ part.CFrame = CFrame.new(offsetStart) * rotCF
156
+
157
+ local sizeTween
158
+ local offsetTween
159
+
160
+ local currentOffset = Vector3.zero
161
+
162
+ if offsetStart ~= baseOffset then
163
+ offsetTween = tween.fromParams(
164
+ attr.get(ref, "Offset_Start_Curve", utility.default_bezier),
165
+ offsetStartDuration,
166
+ function(alpha, deltaTime)
167
+ currentOffset = offsetStart:Lerp(baseOffset, alpha)
168
+ part.CFrame = CFrame.new(currentOffset) * rotCF
169
+ return deltaTime
170
+ end
171
+ )
172
+
173
+ table.insert(scope, sizeTween)
174
+ end
175
+
176
+ local size = Vector3.new(width, height, length)
177
+
178
+ if size * sizeScaleStart ~= size then
179
+ sizeTween = tween.fromParams(sizeStartCurve, sizeStartDuration, function(alpha, deltaTime)
180
+ part.Size = (size * sizeScaleStart):Lerp(size, alpha)
181
+ return deltaTime
182
+ end)
183
+
184
+ table.insert(scope, sizeTween)
185
+ else
186
+ part.Size = size * sizeScaleStart
187
+ end
188
+
189
+ local env = scope.effects.emitNested(part, scope.depth + 1)
190
+ table.insert(promises, env.Finished)
191
+
192
+ task.delay(rng:NextNumber(lifetime.Min, lifetime.Max), function()
193
+ if sizeTween then
194
+ sizeTween:Disconnect()
195
+ end
196
+
197
+ if offsetTween then
198
+ offsetTween:Disconnect()
199
+ end
200
+
201
+ local startSize = part.Size
202
+ local startTp = part.Transparency
203
+
204
+ if startSize ~= size * sizeScaleEnd then
205
+ table.insert(
206
+ scope,
207
+ tween.fromParams(sizeEndCurve, sizeEndDuration, function(alpha, deltaTime)
208
+ part.Size = startSize:Lerp(size * sizeScaleEnd, alpha)
209
+ return deltaTime
210
+ end)
211
+ )
212
+ end
213
+
214
+ if currentOffset ~= offsetEnd then
215
+ table.insert(
216
+ scope,
217
+ tween.fromParams(
218
+ attr.get(ref, "Offset_End_Curve", utility.default_bezier),
219
+ offsetEndDuration,
220
+ function(alpha, deltaTime)
221
+ part.CFrame = CFrame.new(currentOffset:Lerp(offsetEnd, alpha)) * rotCF
222
+ return deltaTime
223
+ end
224
+ )
225
+ )
226
+ end
227
+
228
+ if startTp ~= tpEnd then
229
+ table.insert(
230
+ scope,
231
+ tween.fromParams(
232
+ attr.get(ref, "Transparency_Curve", utility.default_bezier),
233
+ tpDuration,
234
+ function(alpha, deltaTime)
235
+ part.Transparency = utility.lerp(startTp, tpEnd, alpha)
236
+ return deltaTime
237
+ end
238
+ )
239
+ )
240
+ end
241
+
242
+ local env = scope.effects.emitOnFinish(emitOnFinish, part, scope.depth + 1)
243
+ table.insert(promises, env.Finished)
244
+ end)
245
+ end
246
+
247
+ task.wait(lifetime.Max + math.max(sizeEndDuration, offsetEndDuration))
248
+
249
+ Promise.all(promises):await()
250
+ end
251
+
252
+ return ring
@@ -0,0 +1,311 @@
1
+ local RunService = game:GetService("RunService")
2
+ local CollectionService = game:GetService("CollectionService")
3
+
4
+ local attr = require("@mod/attributes")
5
+
6
+ local tween = require("@mod/tween")
7
+ local types = require("@root/types")
8
+ local utility = require("@mod/utility")
9
+ local Promise = require("@pkg/Promise")
10
+
11
+ local rng = Random.new()
12
+
13
+ local sound = {}
14
+
15
+ local soundWidget: DockWidgetPluginGui?
16
+
17
+ local function selectRandomSound(ref: Sound): Sound?
18
+ local poolTag = attr.get(ref, "SoundPoolTag", "")
19
+
20
+ if poolTag == "" then
21
+ return ref
22
+ end
23
+
24
+ local sounds = CollectionService:GetTagged(poolTag)
25
+
26
+ local weights: { number } = {}
27
+ local validSounds: { Sound } = {}
28
+
29
+ local totalWeight = 0
30
+
31
+ for _, obj in sounds do
32
+ if not obj:IsA("Sound") then
33
+ continue
34
+ end
35
+
36
+ table.insert(validSounds, obj)
37
+
38
+ local weight = attr.get(obj, "SoundPoolWeight", 1)
39
+
40
+ table.insert(weights, weight)
41
+ totalWeight += weight
42
+ end
43
+
44
+ if #validSounds == 0 then
45
+ return ref
46
+ end
47
+
48
+ if totalWeight > 0 then
49
+ local roll = rng:NextNumber() * totalWeight
50
+ local accumulated = 0
51
+
52
+ for i, weight in weights do
53
+ accumulated += weight
54
+
55
+ if roll <= accumulated then
56
+ return validSounds[i]
57
+ end
58
+ end
59
+ end
60
+
61
+ return validSounds[rng:NextInteger(1, #validSounds)]
62
+ end
63
+
64
+ local function createSoundClone(ref: Sound, sourceSound: Sound, scope: types.scope): (Sound, Part?)
65
+ local clone = sourceSound:Clone()
66
+
67
+ for _, tag in clone:GetTags() do
68
+ clone:RemoveTag(tag)
69
+ end
70
+
71
+ local parent = ref:FindFirstAncestorWhichIsA("BasePart")
72
+
73
+ local partClone
74
+
75
+ if utility.PLUGIN_CONTEXT then
76
+ if parent then
77
+ partClone = Instance.new("Part")
78
+
79
+ partClone.Name = "SoundEmitter"
80
+ partClone.Anchored = true
81
+ partClone.CanCollide = false
82
+ partClone.CanQuery = false
83
+ partClone.CanTouch = false
84
+ partClone.Transparency = 1
85
+ partClone.Size = Vector3.one
86
+ partClone.CFrame = parent.CFrame
87
+ partClone.Parent = soundWidget
88
+
89
+ clone.Parent = partClone
90
+
91
+ table.insert(scope, partClone)
92
+ else
93
+ clone.Parent = soundWidget
94
+ table.insert(scope, clone)
95
+ end
96
+ else
97
+ if parent then
98
+ clone.Parent = parent
99
+ else
100
+ clone.Parent = workspace.Terrain
101
+ end
102
+
103
+ table.insert(scope, clone)
104
+ end
105
+
106
+ return clone, partClone
107
+ end
108
+
109
+ local function playSingleSound(
110
+ ref: Sound,
111
+ src: Sound,
112
+ clone: Sound,
113
+ partClone: Part?,
114
+ scope: types.scope,
115
+ emitDuration: number
116
+ ): RBXScriptConnection?
117
+ local currentSpeed = clone.PlaybackSpeed
118
+
119
+ local volumeEnd = attr.get(src, "Volume_End", clone.Volume)
120
+ local volumeStart = attr.get(src, "Volume_Start", clone.Volume)
121
+
122
+ clone.Volume = volumeStart
123
+
124
+ if volumeStart ~= volumeEnd then
125
+ local volumeDuration = attr.get(src, "Volume_Duration", clone.TimeLength)
126
+
127
+ table.insert(
128
+ scope,
129
+ tween.fromParams(attr.get(src, "Volume_Curve", utility.default_bezier), volumeDuration, function(alpha, deltaTime)
130
+ clone.Volume = utility.lerp(volumeStart, volumeEnd, alpha)
131
+ return deltaTime * currentSpeed
132
+ end)
133
+ )
134
+ else
135
+ clone.Volume = volumeStart
136
+ end
137
+
138
+ local speedEnd = attr.get(src, "Speed_End", clone.PlaybackSpeed)
139
+ local speedStart = attr.get(src, "Speed_Start", clone.PlaybackSpeed)
140
+
141
+ currentSpeed = speedStart
142
+
143
+ local speedTween: RBXScriptConnection? = nil
144
+
145
+ if speedStart ~= speedEnd then
146
+ local speedDuration = attr.get(src, "Speed_Duration", clone.TimeLength)
147
+
148
+ speedTween = tween.fromParams(
149
+ attr.get(src, "Speed_Curve", utility.default_bezier),
150
+ speedDuration,
151
+ function(alpha, deltaTime)
152
+ currentSpeed = utility.lerp(speedStart, speedEnd, alpha)
153
+ clone.PlaybackSpeed = currentSpeed
154
+ return deltaTime
155
+ end
156
+ )
157
+
158
+ table.insert(scope, speedTween)
159
+ else
160
+ clone.PlaybackSpeed = speedStart
161
+ end
162
+
163
+ local rolloffEnd = attr.get(src, "RollOff_End", clone.RollOffMinDistance)
164
+ local rolloffStart = attr.get(src, "RollOff_Start", clone.RollOffMinDistance)
165
+
166
+ if rolloffStart ~= rolloffEnd then
167
+ local rolloffDuration = attr.get(src, "RollOff_Duration", clone.TimeLength)
168
+
169
+ table.insert(
170
+ scope,
171
+ tween.fromParams(
172
+ attr.get(src, "RollOff_Curve", utility.default_bezier),
173
+ rolloffDuration,
174
+ function(alpha, deltaTime)
175
+ clone.RollOffMinDistance = utility.lerp(rolloffStart, rolloffEnd, alpha)
176
+ return deltaTime * currentSpeed
177
+ end,
178
+ speedTween
179
+ )
180
+ )
181
+ end
182
+
183
+ if clone.PlayOnRemove and not utility.PLUGIN_CONTEXT then
184
+ clone:Destroy()
185
+ else
186
+ clone:Play()
187
+ end
188
+
189
+ if partClone then
190
+ local parent = ref:FindFirstAncestorWhichIsA("BasePart")
191
+
192
+ if parent then
193
+ table.insert(
194
+ scope,
195
+ RunService.Heartbeat:Connect(function()
196
+ if partClone.Parent then
197
+ partClone.CFrame = parent.CFrame
198
+ end
199
+ end)
200
+ )
201
+ end
202
+ end
203
+
204
+ if emitDuration > 0 then
205
+ task.wait(emitDuration)
206
+ clone:Stop()
207
+ else
208
+ tween.timer(clone.TimeLength, function(deltaTime, elapsed)
209
+ return if clone.PlaybackSpeed > 0 or (elapsed > 0 and speedTween and speedTween.Connected)
210
+ then deltaTime * clone.PlaybackSpeed
211
+ else nil
212
+ end, speedTween, scope)
213
+ end
214
+
215
+ return speedTween
216
+ end
217
+
218
+ function sound.init()
219
+ if not utility.PLUGIN_CONTEXT then
220
+ return
221
+ end
222
+
223
+ local plugin = script:FindFirstAncestorOfClass("Plugin")
224
+
225
+ if not plugin then
226
+ return
227
+ end
228
+
229
+ soundWidget = plugin:CreateDockWidgetPluginGui(
230
+ "VFXForgeSoundPlayer",
231
+ DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Float, false, true, 200, 100, 100, 50)
232
+ )
233
+
234
+ if soundWidget then
235
+ soundWidget.Name = "VFXForgeSoundPlayer"
236
+ end
237
+ end
238
+
239
+ function sound.deinit()
240
+ if soundWidget then
241
+ soundWidget:Destroy()
242
+ soundWidget = nil
243
+ end
244
+ end
245
+
246
+ function sound.emit(ref: Sound, scope: types.scope)
247
+ if ref.Playing then
248
+ ref:Stop()
249
+ end
250
+
251
+ local isSource = attr.get(ref, "SoundPoolIsSource", false)
252
+ local inheritPoolAttributes = attr.get(ref, "SoundPoolInheritAttributes", false)
253
+
254
+ if inheritPoolAttributes then
255
+ ref = not isSource and selectRandomSound(ref) or ref
256
+ end
257
+
258
+ local emitDelay = attr.get(ref, "EmitDelay", 0)
259
+ local emitDuration = attr.get(ref, "EmitDuration", 0)
260
+
261
+ local emitCount = attr.get(ref, "EmitCount", 1)
262
+ local emitInterval = attr.getRange(ref, "EmitInterval", NumberRange.new(0, 0))
263
+
264
+ local repeatCount = attr.get(ref, "RepeatCount", 1)
265
+ local repeatInterval = attr.getRange(ref, "RepeatInterval", NumberRange.new(0, 0))
266
+
267
+ task.wait(emitDelay)
268
+
269
+ local promises = {}
270
+
271
+ for i = 1, emitCount do
272
+ local sourceSound = inheritPoolAttributes and ref or (not isSource and selectRandomSound(ref) or ref)
273
+
274
+ if not sourceSound then
275
+ continue
276
+ end
277
+
278
+ local src = if inheritPoolAttributes and sourceSound ~= ref then sourceSound else ref
279
+
280
+ for r = 1, repeatCount do
281
+ table.insert(
282
+ promises,
283
+ Promise.new(function(resolve)
284
+ local clone, partClone = createSoundClone(ref, sourceSound, scope)
285
+
286
+ playSingleSound(ref, src, clone, partClone, scope, emitDuration)
287
+
288
+ if partClone then
289
+ partClone:Destroy()
290
+ else
291
+ clone:Destroy()
292
+ end
293
+
294
+ resolve()
295
+ end)
296
+ )
297
+
298
+ if r < repeatCount and repeatInterval.Max > 0 then
299
+ task.wait(rng:NextNumber(repeatInterval.Min, repeatInterval.Max))
300
+ end
301
+ end
302
+
303
+ if i < emitCount and emitInterval.Max > 0 then
304
+ task.wait(rng:NextNumber(emitInterval.Min, emitInterval.Max))
305
+ end
306
+ end
307
+
308
+ Promise.all(promises):await()
309
+ end
310
+
311
+ return sound
@@ -0,0 +1,88 @@
1
+ local attr = require("@mod/attributes")
2
+ local tween = require("@mod/tween")
3
+ local types = require("@root/types")
4
+ local utility = require("@mod/utility")
5
+
6
+ local spin = {}
7
+
8
+ function spin.emit(ref: Model, scope: types.scope)
9
+ if utility.isSpinModelStatic(ref) then
10
+ return
11
+ end
12
+
13
+ local rotation = attr.get(ref, "SpinRotation", vector.zero) * utility.DEG_TO_RAD
14
+
15
+ local scaleStart = attr.get(ref, "Scale_Start", 1)
16
+ local scaleEnd = attr.get(ref, "Scale_End", 1)
17
+
18
+ local emitDelay = attr.get(ref, "EmitDelay", 0)
19
+ local resetDelay = attr.get(ref, "ResetDelay", 0)
20
+ local resetOnFinish = attr.get(ref, "ResetOnFinish", true)
21
+
22
+ local sync = attr.get(ref, "SyncPosition", false)
23
+ local duration = attr.get(ref, "SpinDuration", 0.5)
24
+
25
+ local speedDuration = attr.get(ref, "SpinSpeed_Duration", 0.1)
26
+
27
+ local speedStart = attr.get(ref, "SpinSpeed_Start", 0)
28
+ local speedEnd = attr.get(ref, "SpinSpeed_End", 1)
29
+
30
+ local currentSpeed = speedStart
31
+
32
+ task.wait(emitDelay)
33
+
34
+ local speedTween
35
+
36
+ if speedStart ~= speedEnd then
37
+ speedTween = tween.fromParams(
38
+ attr.get(ref, "SpinSpeed_Curve", utility.default_bezier),
39
+ speedDuration,
40
+ function(alpha, deltaTime)
41
+ currentSpeed = utility.lerp(speedStart, speedEnd, alpha)
42
+ return deltaTime
43
+ end
44
+ )
45
+
46
+ table.insert(scope, speedTween)
47
+ end
48
+
49
+ local originalPivot = ref:GetPivot()
50
+ local originalScale = ref:GetScale()
51
+
52
+ if scaleStart ~= scaleEnd then
53
+ table.insert(
54
+ scope,
55
+ tween.fromParams(attr.get(ref, "Scale_Curve", utility.default_bezier), duration, function(alpha, deltaTime)
56
+ ref:ScaleTo(utility.lerp(scaleStart, scaleEnd, alpha))
57
+ return deltaTime
58
+ end)
59
+ )
60
+ else
61
+ ref:ScaleTo(scaleStart)
62
+ end
63
+
64
+ local parent = ref:FindFirstAncestorOfClass("Attachment") or ref:FindFirstAncestorWhichIsA("BasePart")
65
+
66
+ if resetOnFinish then
67
+ table.insert(scope, function()
68
+ ref:PivotTo(parent and utility.getTransformedOriginExtents(parent) or originalPivot)
69
+ ref:ScaleTo(originalScale)
70
+ end)
71
+ end
72
+
73
+ rotation *= duration
74
+
75
+ tween.timer(duration + resetDelay, function(deltaTime, elapsed)
76
+ local alpha = math.clamp(elapsed / duration, 0, 1)
77
+
78
+ local r = rotation * alpha
79
+ local cf = (if parent and sync then utility.getTransformedOriginExtents(parent) else originalPivot)
80
+ * CFrame.fromOrientation(r.x, r.y, r.z)
81
+
82
+ ref:PivotTo(cf)
83
+
84
+ return if currentSpeed > 0 or (elapsed > 0 and speedTween.Connected) then deltaTime * currentSpeed else nil
85
+ end, speedTween, scope, utility.RENDER_PRIORITY + scope.depth)
86
+ end
87
+
88
+ return spin