@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.
- package/LICENSE +82 -0
- package/README.md +39 -0
- package/out/effects/beam.luau +312 -0
- package/out/effects/bezier.luau +392 -0
- package/out/effects/camera_shake.luau +200 -0
- package/out/effects/lightning.luau +1183 -0
- package/out/effects/mesh.luau +466 -0
- package/out/effects/particle.luau +64 -0
- package/out/effects/randomizer.luau +110 -0
- package/out/effects/screen.luau +61 -0
- package/out/effects/shockwave_debris.luau +277 -0
- package/out/effects/shockwave_line.luau +356 -0
- package/out/effects/shockwave_ring.luau +252 -0
- package/out/effects/sound.luau +311 -0
- package/out/effects/spin.luau +88 -0
- package/out/effects/tweener.luau +122 -0
- package/out/emitters.luau +387 -0
- package/out/index.d.ts +341 -0
- package/out/init.luau +279 -0
- package/out/mod/attributes.luau +227 -0
- package/out/mod/color/Oklab.luau +93 -0
- package/out/mod/color/sRGB.luau +71 -0
- package/out/mod/common/bezier.luau +372 -0
- package/out/mod/common/flipbook.luau +102 -0
- package/out/mod/lerp.luau +210 -0
- package/out/mod/logger.luau +20 -0
- package/out/mod/shape.luau +207 -0
- package/out/mod/tween.luau +161 -0
- package/out/mod/utility.luau +707 -0
- package/out/obj/Bezier.luau +268 -0
- package/out/obj/ObjectCache.luau +289 -0
- package/out/services/caches.luau +62 -0
- package/out/services/effects.luau +234 -0
- package/out/services/enabled_effects.luau +120 -0
- package/out/services/texture_loader.luau +174 -0
- package/out/tsconfig.tsbuildinfo +1 -0
- package/out/types.luau +43 -0
- package/package.json +63 -0
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
--!native
|
|
2
|
+
--!nolint LocalShadow
|
|
3
|
+
local RunService = game:GetService("RunService")
|
|
4
|
+
local PhysicsService = game:GetService("PhysicsService")
|
|
5
|
+
|
|
6
|
+
local attr = require("@mod/attributes")
|
|
7
|
+
local logger = require("@mod/logger")
|
|
8
|
+
|
|
9
|
+
local Promise = require("@pkg/Promise")
|
|
10
|
+
|
|
11
|
+
local utility = {}
|
|
12
|
+
|
|
13
|
+
utility.PLUGIN_CONTEXT = script:FindFirstAncestorOfClass("Plugin")
|
|
14
|
+
or (RunService:IsStudio() and not RunService:IsRunning())
|
|
15
|
+
|
|
16
|
+
utility.SERVER_CONTEXT = RunService:IsServer() or utility.PLUGIN_CONTEXT
|
|
17
|
+
|
|
18
|
+
utility.DEG_TO_RAD = math.pi / 180
|
|
19
|
+
|
|
20
|
+
utility.BEZIER_TAG = "BezierParticle"
|
|
21
|
+
utility.LIGHTNING_TAG = "LightningBolt"
|
|
22
|
+
utility.SHOCKWAVE_TAG = "Shockwave"
|
|
23
|
+
utility.SCREENSHAKE_TAG = "CameraShake"
|
|
24
|
+
|
|
25
|
+
utility.PROPERTY_TWEENER_TAG = "PropertyTweener"
|
|
26
|
+
utility.PROPERTY_RANDOMIZER_TAG = "PropertyRandomizer"
|
|
27
|
+
|
|
28
|
+
utility.ATTRIBUTE_TWEENER_TAG = "AttributeTweener"
|
|
29
|
+
utility.ATTRIBUTE_RANDOMIZER_TAG = "AttributeRandomizer"
|
|
30
|
+
|
|
31
|
+
utility.ENABLED_VFX_TAG = "ConstantVFX"
|
|
32
|
+
utility.TEXTURE_LOAD_TAG = "LoadVFXTextures"
|
|
33
|
+
|
|
34
|
+
utility.CLEANUP_TAG = "__forge__cleanupOnExit"
|
|
35
|
+
utility.EMIT_EXCLUDE_TAG = "__forge_excludeFromEmit"
|
|
36
|
+
|
|
37
|
+
utility.RENDER_PRIORITY = Enum.RenderPriority.Camera.Value + 1
|
|
38
|
+
|
|
39
|
+
utility.COLLISION_GROUPS = {
|
|
40
|
+
StudioSelectable = {},
|
|
41
|
+
|
|
42
|
+
ForgeDebris = {
|
|
43
|
+
ForgeDebris = false,
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
ForgeMouseIgnore = {
|
|
47
|
+
StudioSelectable = false,
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
utility.COPY_SPECIALMESH_PROPERTIES = {
|
|
52
|
+
"MeshId",
|
|
53
|
+
"MeshType",
|
|
54
|
+
"Offset",
|
|
55
|
+
"Scale",
|
|
56
|
+
"TextureId",
|
|
57
|
+
"VertexColor",
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
utility.COPY_PART_PROPERTIES = {
|
|
61
|
+
"CastShadow",
|
|
62
|
+
|
|
63
|
+
"Color",
|
|
64
|
+
|
|
65
|
+
"Material",
|
|
66
|
+
"MaterialVariant",
|
|
67
|
+
"Reflectance",
|
|
68
|
+
|
|
69
|
+
"Shape",
|
|
70
|
+
|
|
71
|
+
"FrontSurface",
|
|
72
|
+
"BackSurface",
|
|
73
|
+
"LeftSurface",
|
|
74
|
+
"RightSurface",
|
|
75
|
+
"TopSurface",
|
|
76
|
+
"BottomSurface",
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
utility.COPY_EXTENDED_PART_PROPERTIES = {
|
|
80
|
+
"Size",
|
|
81
|
+
"Transparency",
|
|
82
|
+
|
|
83
|
+
"CustomPhysicalProperties",
|
|
84
|
+
|
|
85
|
+
"CanCollide",
|
|
86
|
+
"CanQuery",
|
|
87
|
+
"CanTouch",
|
|
88
|
+
|
|
89
|
+
"CollisionGroup",
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
local LOCKS = setmetatable({} :: { [Instance]: boolean }, { __mode = "k" })
|
|
93
|
+
|
|
94
|
+
function utility.lock(ref: Instance)
|
|
95
|
+
if LOCKS[ref] then
|
|
96
|
+
return true
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
LOCKS[ref] = coroutine.running()
|
|
100
|
+
|
|
101
|
+
return false
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
function utility.unlock(ref: Instance, key: thread?)
|
|
105
|
+
local thread = LOCKS[ref]
|
|
106
|
+
|
|
107
|
+
if coroutine.running() ~= thread and key ~= thread then
|
|
108
|
+
logger.error("attempt to unlock an instance owned by a different thread")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
LOCKS[ref] = nil
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
function utility.setCollisionGroups(groups)
|
|
115
|
+
local rules = {}
|
|
116
|
+
|
|
117
|
+
for name, list in groups do
|
|
118
|
+
PhysicsService:RegisterCollisionGroup(name)
|
|
119
|
+
|
|
120
|
+
if not name:match("Studio") or RunService:IsStudio() then
|
|
121
|
+
rules[name] = list
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
for name, list in rules do
|
|
126
|
+
for group, collidable in list do
|
|
127
|
+
if group:match("Studio") and not RunService:IsStudio() then
|
|
128
|
+
continue
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
PhysicsService:CollisionGroupSetCollidable(name, group, collidable)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
local last_id = 0
|
|
137
|
+
|
|
138
|
+
function utility.getRandomId()
|
|
139
|
+
last_id += 1
|
|
140
|
+
return tostring(last_id)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
function utility.copyProperties(from: Instance, to: Instance, list: { string })
|
|
144
|
+
for _, k in list do
|
|
145
|
+
to[k] = from[k]
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
function utility.lerp(a: number, b: number, t: number)
|
|
150
|
+
return a + (b - a) * t
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
function utility.try<R...>(msg: string, func: (...any) -> R..., ...: any): (boolean, R...)
|
|
154
|
+
local res = { xpcall(func, function(err)
|
|
155
|
+
logger.warn(string.format(msg, err))
|
|
156
|
+
end, ...) }
|
|
157
|
+
|
|
158
|
+
return res[1], table.unpack(res, 2)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
function utility.reboundfn<A>(sec: number, func: (...A) -> never): (...A) -> never
|
|
162
|
+
local lastRunThread
|
|
163
|
+
|
|
164
|
+
return function(...)
|
|
165
|
+
if lastRunThread then
|
|
166
|
+
task.cancel(lastRunThread)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
local args = { ... }
|
|
170
|
+
|
|
171
|
+
lastRunThread = task.delay(sec, function()
|
|
172
|
+
lastRunThread = nil
|
|
173
|
+
func(table.unpack(args))
|
|
174
|
+
end)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
function utility.randomUnitVector(min: Vector3, max: Vector3, rng: Random?)
|
|
179
|
+
local rng = rng or Random.new()
|
|
180
|
+
return Vector3.new(rng:NextNumber(min.X, max.X), rng:NextNumber(min.Y, max.Y), rng:NextNumber(min.Z, max.Z))
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
function utility.getImpulseForce(start: Vector3, goal: Vector3, duration: number)
|
|
184
|
+
return (goal - start) / duration + Vector3.new(0, workspace.Gravity * duration * 0.5, 0)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
function utility.isMeshVFX(obj: Instance)
|
|
188
|
+
return obj
|
|
189
|
+
and obj:IsA("Model")
|
|
190
|
+
and obj:FindFirstChild("Start")
|
|
191
|
+
and (obj :: any).Start:IsA("BasePart")
|
|
192
|
+
and obj:FindFirstChild("End")
|
|
193
|
+
and (obj :: any).End:IsA("BasePart")
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
function utility.isSpinModelStatic(model: Model)
|
|
197
|
+
return attr.get(model, "SpinRotation", vector.zero, true) == vector.zero
|
|
198
|
+
and attr.get(model, "Scale_Start", 1, true) == 1
|
|
199
|
+
and attr.get(model, "Scale_End", 1, true) == 1
|
|
200
|
+
and (attr.get(model, "SyncPosition", false, true) == false)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
function utility.shouldSkipNested(obj: Instance)
|
|
204
|
+
return obj:IsA("Beam")
|
|
205
|
+
or obj:HasTag(utility.BEZIER_TAG)
|
|
206
|
+
or obj:HasTag(utility.LIGHTNING_TAG)
|
|
207
|
+
or utility.isMeshVFX(obj)
|
|
208
|
+
or (obj:IsA("BasePart") and utility.findFirstClassWithTag(obj, "Attachment", utility.SHOCKWAVE_TAG) ~= nil)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
function utility.getTarget(obj: Instance): Instance?
|
|
212
|
+
local objectValue = obj:FindFirstChildOfClass("ObjectValue")
|
|
213
|
+
|
|
214
|
+
if objectValue and objectValue.Value then
|
|
215
|
+
return objectValue.Value
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
local parent = obj.Parent
|
|
219
|
+
|
|
220
|
+
if not parent then
|
|
221
|
+
return nil
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
while parent:IsA("Folder") do
|
|
225
|
+
parent = parent.Parent
|
|
226
|
+
|
|
227
|
+
if not parent then
|
|
228
|
+
return nil
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
return parent
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
function utility.createEmitPromise(effects: any, instance: Instance, effectDepth: number, emitFunc: any)
|
|
236
|
+
return Promise.new(function(resolve)
|
|
237
|
+
local scope = {}
|
|
238
|
+
scope.depth = effectDepth
|
|
239
|
+
scope.effects = effects
|
|
240
|
+
|
|
241
|
+
emitFunc(scope)
|
|
242
|
+
|
|
243
|
+
utility.cleanupScope(scope)
|
|
244
|
+
|
|
245
|
+
resolve()
|
|
246
|
+
end)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
function utility.cleanupScope(scope: { unknown })
|
|
250
|
+
for k, v in scope do
|
|
251
|
+
-- Skip scope metadata fields
|
|
252
|
+
if k == "depth" or k == "effects" then
|
|
253
|
+
continue
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
local t = typeof(v)
|
|
257
|
+
|
|
258
|
+
if t == "Instance" then
|
|
259
|
+
v:Destroy()
|
|
260
|
+
elseif t == "RBXScriptConnection" then
|
|
261
|
+
v:Disconnect()
|
|
262
|
+
elseif t == "thread" then
|
|
263
|
+
if coroutine.status(v) ~= "dead" then
|
|
264
|
+
task.cancel(v)
|
|
265
|
+
end
|
|
266
|
+
elseif t == "function" then
|
|
267
|
+
task.spawn(v)
|
|
268
|
+
elseif t == "table" then
|
|
269
|
+
utility.cleanupScope(v)
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
table.clear(scope)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
function utility.protectParent(scope: { unknown }, parent: Instance)
|
|
277
|
+
table.insert(
|
|
278
|
+
scope,
|
|
279
|
+
parent.AncestryChanged:Connect(function(_, new)
|
|
280
|
+
if parent.Parent == workspace.Terrain then
|
|
281
|
+
return
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
parent.Parent = workspace.Terrain
|
|
285
|
+
end)
|
|
286
|
+
)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
function utility.findFirstClassWithTag(obj: Instance?, class: string, tag: string)
|
|
290
|
+
if not obj or obj.Parent == game then
|
|
291
|
+
return
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
if obj.ClassName == class and obj:HasTag(tag) then
|
|
295
|
+
return obj
|
|
296
|
+
else
|
|
297
|
+
return utility.findFirstClassWithTag(obj.Parent, class, tag)
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
function utility.cloneParticleAncestry(
|
|
302
|
+
particle: ParticleEmitter,
|
|
303
|
+
map: { [Instance?]: Instance }
|
|
304
|
+
): (Instance?, Instance?)
|
|
305
|
+
if not particle:FindFirstAncestorWhichIsA("BasePart") and not particle:FindFirstAncestorOfClass("Attachment") then
|
|
306
|
+
return
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
local function findAncestor(obj: Instance?)
|
|
310
|
+
if not obj then
|
|
311
|
+
return
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
local ancestor = obj
|
|
315
|
+
|
|
316
|
+
if obj.Parent and (obj.Parent:IsA("BasePart") or obj.Parent:IsA("Attachment")) then
|
|
317
|
+
ancestor = findAncestor(obj.Parent)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
return ancestor
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
local function recurse(obj: Instance?)
|
|
324
|
+
if not obj then
|
|
325
|
+
return
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
local created = map[obj]
|
|
329
|
+
|
|
330
|
+
if created then
|
|
331
|
+
return created, if obj == particle then map[obj.Parent] else map[findAncestor(obj.Parent)] or map[obj]
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
local clone = Instance.fromExisting(obj)
|
|
335
|
+
clone.Archivable = false
|
|
336
|
+
|
|
337
|
+
if clone:IsA("BasePart") then
|
|
338
|
+
clone.Locked = true
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
local ancestor = clone
|
|
342
|
+
|
|
343
|
+
if map then
|
|
344
|
+
map[obj] = clone
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
if obj.Parent and (obj.Parent:IsA("BasePart") or obj.Parent:IsA("Attachment")) then
|
|
348
|
+
local parent, pa = recurse(obj.Parent)
|
|
349
|
+
|
|
350
|
+
if pa then
|
|
351
|
+
ancestor = pa
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
if parent then
|
|
355
|
+
clone.Parent = parent
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
return clone, ancestor
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
return recurse(particle.Parent)
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
function utility.getTransformedOriginExtents(parent: Instance)
|
|
366
|
+
local originCFrame, originSize = CFrame.identity, vector.zero
|
|
367
|
+
|
|
368
|
+
if parent:IsA("BasePart") then
|
|
369
|
+
originSize = parent.Size
|
|
370
|
+
originCFrame = parent.CFrame
|
|
371
|
+
elseif parent:IsA("Attachment") then
|
|
372
|
+
originCFrame = parent.WorldCFrame
|
|
373
|
+
|
|
374
|
+
local newParent = parent.Parent
|
|
375
|
+
|
|
376
|
+
-- step 1: find the full origin cframe by walking up the tree, and processing the upmost attachment
|
|
377
|
+
local positionScale = attr.get(parent, "PositionScale", vector.zero, true)
|
|
378
|
+
|
|
379
|
+
if newParent then
|
|
380
|
+
if newParent:IsA("BasePart") and positionScale ~= vector.zero then
|
|
381
|
+
local offset = (newParent.Size / 2) * positionScale
|
|
382
|
+
originCFrame = originCFrame + offset
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
-- step 2: apply rotation on final origin cframe
|
|
387
|
+
local overrideWorldRotation = attr.get(parent, "OverrideWorldRotation", false, true)
|
|
388
|
+
local worldRotation = attr.get(parent, "WorldRotation", vector.zero, true)
|
|
389
|
+
|
|
390
|
+
if overrideWorldRotation then
|
|
391
|
+
local rotRad = worldRotation * utility.DEG_TO_RAD
|
|
392
|
+
local position = originCFrame.Position
|
|
393
|
+
|
|
394
|
+
originCFrame = CFrame.new(position) * CFrame.fromOrientation(rotRad.X, rotRad.Y, rotRad.Z)
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
return originCFrame, originSize
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
function utility.getMeshDecals(ref: Model, start: BasePart)
|
|
402
|
+
local legacy_isFlipbook = attr.get(ref, "Flipbook", false, true)
|
|
403
|
+
|
|
404
|
+
local decals = {}
|
|
405
|
+
local flipbooks = {}
|
|
406
|
+
local fromToMap = {}
|
|
407
|
+
|
|
408
|
+
local function filter(list: { Instance }): { Decal }
|
|
409
|
+
local filtered = {}
|
|
410
|
+
|
|
411
|
+
for _, v in list do
|
|
412
|
+
if v:IsA("Decal") then
|
|
413
|
+
table.insert(filtered, v)
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
return filtered
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
if legacy_isFlipbook then
|
|
421
|
+
decals = filter(start:GetChildren())
|
|
422
|
+
else
|
|
423
|
+
local originalEnd = ref:FindFirstChild("End")
|
|
424
|
+
local originalStart = ref:FindFirstChild("Start")
|
|
425
|
+
|
|
426
|
+
if originalEnd then
|
|
427
|
+
for _, v in start:GetChildren() do
|
|
428
|
+
if not v:IsA("Decal") then
|
|
429
|
+
continue
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
local from = v
|
|
433
|
+
|
|
434
|
+
local to = originalEnd:FindFirstChild(v.Name)
|
|
435
|
+
|
|
436
|
+
if not to or not to:IsA("Decal") then
|
|
437
|
+
continue
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
table.insert(decals, v)
|
|
441
|
+
|
|
442
|
+
fromToMap[from] = to
|
|
443
|
+
|
|
444
|
+
if not v:GetAttribute("FlipbookEnabled") then
|
|
445
|
+
table.insert(decals, v)
|
|
446
|
+
else
|
|
447
|
+
flipbooks[v] = utility.deserializeFlipbook(v:GetAttribute("FlipbookTextures"))
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
for _, v in originalStart:GetChildren() do
|
|
452
|
+
if not v:IsA("Decal") then
|
|
453
|
+
continue
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
-- convert children decals to flipbooks
|
|
457
|
+
local children = v:GetChildren()
|
|
458
|
+
|
|
459
|
+
do
|
|
460
|
+
local offset = 0
|
|
461
|
+
|
|
462
|
+
for i = 1, #children do
|
|
463
|
+
i -= offset
|
|
464
|
+
|
|
465
|
+
local obj = children[i]
|
|
466
|
+
|
|
467
|
+
if not obj:IsA("Decal") then
|
|
468
|
+
table.remove(children, i)
|
|
469
|
+
offset += 1
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
if #children ~= 0 then
|
|
475
|
+
local function idx(str: string)
|
|
476
|
+
return tonumber(str:match("%d+")) or 0
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
table.sort(children, function(a, b)
|
|
480
|
+
return idx(a.Name) < idx(b.Name)
|
|
481
|
+
end)
|
|
482
|
+
|
|
483
|
+
local ids = {}
|
|
484
|
+
|
|
485
|
+
for _, obj in children do
|
|
486
|
+
table.insert(ids, idx(obj.Texture))
|
|
487
|
+
obj:Destroy()
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
local buf = utility.serializeFlipbook(ids)
|
|
491
|
+
|
|
492
|
+
v:SetAttribute("FlipbookEnabled", true)
|
|
493
|
+
v:SetAttribute("FlipbookTextures", buffer.tostring(buf))
|
|
494
|
+
end
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
return decals, flipbooks, fromToMap
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
function utility.assembleMeshVFX(start: BasePart, scope: { any }, shared_part_cache: any)
|
|
503
|
+
local id = utility.getRandomId()
|
|
504
|
+
|
|
505
|
+
if start:IsA("Part") then
|
|
506
|
+
local objAbstr = shared_part_cache:get(id)
|
|
507
|
+
objAbstr.CFrame = start.CFrame
|
|
508
|
+
|
|
509
|
+
local obj = objAbstr._getReal()
|
|
510
|
+
|
|
511
|
+
utility.copyProperties(start, obj, utility.COPY_PART_PROPERTIES)
|
|
512
|
+
utility.copyProperties(start, obj, utility.COPY_EXTENDED_PART_PROPERTIES)
|
|
513
|
+
|
|
514
|
+
local clone = start:Clone()
|
|
515
|
+
|
|
516
|
+
for _, child in clone:GetChildren() do
|
|
517
|
+
child.Parent = obj
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
clone:Destroy()
|
|
521
|
+
|
|
522
|
+
table.insert(scope, function()
|
|
523
|
+
shared_part_cache:free(id)
|
|
524
|
+
end)
|
|
525
|
+
|
|
526
|
+
return objAbstr
|
|
527
|
+
else
|
|
528
|
+
local clone = start:Clone()
|
|
529
|
+
clone.Archivable = false
|
|
530
|
+
clone.Locked = true
|
|
531
|
+
clone.Parent = workspace.Terrain
|
|
532
|
+
|
|
533
|
+
clone:AddTag(utility.CLEANUP_TAG)
|
|
534
|
+
|
|
535
|
+
table.insert(scope, clone)
|
|
536
|
+
|
|
537
|
+
return clone
|
|
538
|
+
end
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
function utility.getBezierPoints(root: Attachment, metadata: boolean?)
|
|
542
|
+
local objs = root:GetChildren()
|
|
543
|
+
|
|
544
|
+
table.sort(objs, function(a, b)
|
|
545
|
+
return tonumber(a.Name) < tonumber(b.Name)
|
|
546
|
+
end)
|
|
547
|
+
|
|
548
|
+
local points: { vector } = {}
|
|
549
|
+
local attachments: { Attachment } = {}
|
|
550
|
+
local attachmentToPointMap: { [Attachment]: vector } = {}
|
|
551
|
+
|
|
552
|
+
local function vec(p: Attachment)
|
|
553
|
+
local w = root.WorldCFrame:PointToObjectSpace(p.WorldPosition)
|
|
554
|
+
local pos = vector.create(w.X, w.Y, w.Z)
|
|
555
|
+
|
|
556
|
+
table.insert(points, pos)
|
|
557
|
+
|
|
558
|
+
if metadata then
|
|
559
|
+
attachmentToPointMap[p] = pos
|
|
560
|
+
table.insert(attachments, p)
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
return pos
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
for i, p in objs do
|
|
567
|
+
local t0 = p:FindFirstChild("T0")
|
|
568
|
+
local t1 = p:FindFirstChild("T1")
|
|
569
|
+
|
|
570
|
+
if i == 1 then
|
|
571
|
+
vec(p)
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
-- left
|
|
575
|
+
if i ~= 1 then
|
|
576
|
+
if t1 then
|
|
577
|
+
vec(t1)
|
|
578
|
+
else
|
|
579
|
+
vec(p)
|
|
580
|
+
end
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
if i ~= 1 and i ~= #objs then
|
|
584
|
+
vec(p)
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
-- right
|
|
588
|
+
if i ~= #objs then
|
|
589
|
+
if t0 then
|
|
590
|
+
vec(t0)
|
|
591
|
+
else
|
|
592
|
+
vec(p)
|
|
593
|
+
end
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
if i == #objs then
|
|
597
|
+
vec(p)
|
|
598
|
+
end
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
return points, attachments, attachmentToPointMap
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
function utility.scaleNumberSequence(
|
|
605
|
+
seq: NumberSequence,
|
|
606
|
+
scale: number | (value: number, envelope: number) -> (number, number)
|
|
607
|
+
)
|
|
608
|
+
if scale == 1 then
|
|
609
|
+
return seq
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
local scaled = {}
|
|
613
|
+
|
|
614
|
+
for _, keypoint in seq.Keypoints do
|
|
615
|
+
local value, envelope
|
|
616
|
+
|
|
617
|
+
if typeof(scale) == "function" then
|
|
618
|
+
value, envelope = scale(keypoint.Value, keypoint.Envelope)
|
|
619
|
+
else
|
|
620
|
+
value, envelope = keypoint.Value * scale, keypoint.Envelope * scale
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
table.insert(scaled, NumberSequenceKeypoint.new(keypoint.Time, value, envelope))
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
return NumberSequence.new(scaled)
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
function utility.serializePath(points: { Path2DControlPoint })
|
|
630
|
+
local buf = buffer.create(#points * 4 * 6)
|
|
631
|
+
|
|
632
|
+
local offset = 0
|
|
633
|
+
|
|
634
|
+
for i, p in points do
|
|
635
|
+
local px = p.Position.X.Scale
|
|
636
|
+
local py = p.Position.Y.Scale
|
|
637
|
+
|
|
638
|
+
if i ~= 1 then
|
|
639
|
+
buffer.writef32(buf, offset, px + p.LeftTangent.X.Scale)
|
|
640
|
+
buffer.writef32(buf, offset + 4, py + p.LeftTangent.Y.Scale)
|
|
641
|
+
|
|
642
|
+
offset += 8
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
buffer.writef32(buf, offset, px)
|
|
646
|
+
buffer.writef32(buf, offset + 4, py)
|
|
647
|
+
|
|
648
|
+
offset += 8
|
|
649
|
+
|
|
650
|
+
if i ~= #points then
|
|
651
|
+
buffer.writef32(buf, offset, px + p.RightTangent.X.Scale)
|
|
652
|
+
buffer.writef32(buf, offset + 4, py + p.RightTangent.Y.Scale)
|
|
653
|
+
|
|
654
|
+
offset += 8
|
|
655
|
+
end
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
return buf
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
function utility.deserializePath(data: string | buffer)
|
|
662
|
+
local buf = typeof(data) == "string" and buffer.fromstring(data) or data
|
|
663
|
+
local count = buffer.len(buf) / 24
|
|
664
|
+
|
|
665
|
+
local points = {}
|
|
666
|
+
|
|
667
|
+
for i = 0, count - 1 do
|
|
668
|
+
local offset = i * 4 * 6
|
|
669
|
+
|
|
670
|
+
local px = buffer.readf32(buf, offset)
|
|
671
|
+
local py = buffer.readf32(buf, offset + 4)
|
|
672
|
+
|
|
673
|
+
table.insert(points, vector.create(px, py))
|
|
674
|
+
|
|
675
|
+
if i ~= count - 1 then
|
|
676
|
+
local t1x = buffer.readf32(buf, offset + 8)
|
|
677
|
+
local t1y = buffer.readf32(buf, offset + 12)
|
|
678
|
+
|
|
679
|
+
local t2x = buffer.readf32(buf, offset + 16)
|
|
680
|
+
local t2y = buffer.readf32(buf, offset + 20)
|
|
681
|
+
|
|
682
|
+
table.insert(points, vector.create(t1x, t1y))
|
|
683
|
+
table.insert(points, vector.create(t2x, t2y))
|
|
684
|
+
end
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
return points
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
local flipbook_common = require("@mod/common/flipbook")
|
|
691
|
+
|
|
692
|
+
utility.serializeFlipbook = flipbook_common.serialize
|
|
693
|
+
utility.deserializeFlipbook = flipbook_common.deserialize
|
|
694
|
+
|
|
695
|
+
-- out cubic
|
|
696
|
+
utility.default_bezier = buffer.tostring(utility.serializePath({
|
|
697
|
+
Path2DControlPoint.new(UDim2.fromScale(0, 1), UDim2.new(), UDim2.fromScale(0.215, -0.61)),
|
|
698
|
+
Path2DControlPoint.new(UDim2.fromScale(1, 0), UDim2.fromScale(-0.645, 0), UDim2.new()),
|
|
699
|
+
}))
|
|
700
|
+
|
|
701
|
+
-- linear
|
|
702
|
+
utility.linear_bezier = buffer.tostring(utility.serializePath({
|
|
703
|
+
Path2DControlPoint.new(UDim2.fromScale(0, 1)),
|
|
704
|
+
Path2DControlPoint.new(UDim2.fromScale(1, 0)),
|
|
705
|
+
}))
|
|
706
|
+
|
|
707
|
+
return utility
|