@kunosyn/shatterbox 0.0.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.
- package/LICENSE +21 -0
- package/README.md +35 -0
- package/package.json +36 -0
- package/src/Effects.d.ts +134 -0
- package/src/Effects.luau +114 -0
- package/src/Settings.luau +75 -0
- package/src/index.d.ts +617 -0
- package/src/init.luau +2580 -0
- package/src/lib/Client.luau +4020 -0
- package/src/lib/InitializeShatterboxClients.client.luau +10 -0
- package/src/lib/ObjectCache.luau +198 -0
- package/src/lib/PartOperations.luau +673 -0
- package/src/lib/Server.luau +4425 -0
- package/src/lib/VertexMath.luau +403 -0
- package/src/types.ts +504 -0
- package/src/util/HitboxVisualizer.luau +101 -0
- package/src/util/TaggedArray.luau +66 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
/// <reference types="@rbxts/types" />
|
|
2
|
+
|
|
3
|
+
/** Represents a 3D hitbox for destruction operations */
|
|
4
|
+
export interface Hitbox {
|
|
5
|
+
CFrame: CFrame;
|
|
6
|
+
Size: Vector3;
|
|
7
|
+
Shape: Enum.PartType;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** World information used for area-based operations */
|
|
11
|
+
export interface WorldInfo {
|
|
12
|
+
CFrame: CFrame;
|
|
13
|
+
Size: Vector3;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Information about a destroyed voxel passed to callbacks */
|
|
17
|
+
export interface DestroyedVoxelInfo {
|
|
18
|
+
DirtyGroupID?: string;
|
|
19
|
+
CuttingPart?: Hitbox;
|
|
20
|
+
IsEdge?: boolean;
|
|
21
|
+
IsAlreadyDebris?: boolean;
|
|
22
|
+
UserData?: Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type OnVoxelDestruct = (voxel: Part, info: DestroyedVoxelInfo) => void;
|
|
26
|
+
|
|
27
|
+
export type OnDestructCompleted = (
|
|
28
|
+
destroyedVoxelCount: number,
|
|
29
|
+
affectedDirtyGroups: Map<string, boolean>
|
|
30
|
+
) => void;
|
|
31
|
+
|
|
32
|
+
/** An imaginary voxel that hasn't been instantiated yet */
|
|
33
|
+
export interface ImaginaryVoxel {
|
|
34
|
+
CFrame: CFrame;
|
|
35
|
+
Size: Vector3;
|
|
36
|
+
DirtyGroupID: string;
|
|
37
|
+
DestructionID: string;
|
|
38
|
+
GridSize: number;
|
|
39
|
+
isEdge?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Parameters for destruction operations */
|
|
43
|
+
export interface DestructionParams {
|
|
44
|
+
/** The cutting part or hitbox performing the destruction */
|
|
45
|
+
CuttingPart?: Part | Model | Hitbox;
|
|
46
|
+
/** Alternative syntax: provide CFrame, Size, and Shape directly */
|
|
47
|
+
CFrame?: CFrame;
|
|
48
|
+
Size?: Vector3;
|
|
49
|
+
Shape?: Enum.PartType;
|
|
50
|
+
/** Filter destruction to only parts with these tags */
|
|
51
|
+
FilterTagged?: string | Array<string>;
|
|
52
|
+
/** Delay before cleaning up destroyed parts (smooth cleanup) */
|
|
53
|
+
CleanupDelay?: number;
|
|
54
|
+
/** Name of the registered OnVoxelDestruct callback */
|
|
55
|
+
OnVoxelDestruct?: string;
|
|
56
|
+
/** Grid size for voxelization */
|
|
57
|
+
GridSize?: number;
|
|
58
|
+
/** Skip creating voxels that are fully encapsulated */
|
|
59
|
+
SkipEncapsulatedVoxels?: boolean;
|
|
60
|
+
/** Callback invoked when destruction completes */
|
|
61
|
+
OnDestructCompleted?: OnDestructCompleted;
|
|
62
|
+
/** Custom user data passed to callbacks */
|
|
63
|
+
UserData?: Record<string, unknown>;
|
|
64
|
+
/** Players to exclude from replication (server-side only) */
|
|
65
|
+
ExcludePlayersReplication?: Player | Array<Player>;
|
|
66
|
+
/** Skip destroying floor surfaces */
|
|
67
|
+
SkipFloors?: boolean;
|
|
68
|
+
/** Skip destroying wall surfaces */
|
|
69
|
+
SkipWalls?: boolean;
|
|
70
|
+
/** Unique identifier for this destruction operation */
|
|
71
|
+
ID?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Parameters specific to imaginary voxel operations */
|
|
75
|
+
export interface ImaginaryDestructionParams extends Omit<DestructionParams, "OnVoxelDestruct" | "OnDestructCompleted"> {}
|
|
76
|
+
|
|
77
|
+
/** Settings configuration object */
|
|
78
|
+
export interface Settings {
|
|
79
|
+
/** Whether to use client-server replication */
|
|
80
|
+
USE_CLIENT_SERVER?: boolean;
|
|
81
|
+
/** Default grid size for voxelization */
|
|
82
|
+
DefaultGridSize?: number;
|
|
83
|
+
/** Default smooth cleanup delay in seconds */
|
|
84
|
+
DefaultSmoothCleanupDelay?: number;
|
|
85
|
+
/** Whether to use smooth cleanup */
|
|
86
|
+
UseSmoothCleanup?: boolean;
|
|
87
|
+
/** Whether to use greedy meshing optimization */
|
|
88
|
+
UseGreedyMeshing?: boolean;
|
|
89
|
+
/** Number of concurrent greedy meshing workers */
|
|
90
|
+
GMWorkerCount?: number;
|
|
91
|
+
/** Traversals per frame for greedy meshing */
|
|
92
|
+
GMTraversalsPerFrame?: number;
|
|
93
|
+
/** Part creations per frame for greedy meshing */
|
|
94
|
+
GMPartCreationsPerFrame?: number;
|
|
95
|
+
/** Maximum divisions per frame */
|
|
96
|
+
MaxDivisionsPerFrame?: number;
|
|
97
|
+
/** Maximum operations per frame */
|
|
98
|
+
MaxOpsPerFrame?: number;
|
|
99
|
+
/** Whether to use priority queue for operations */
|
|
100
|
+
UsePriorityQueue?: boolean;
|
|
101
|
+
/** Number of recent operations to prioritize */
|
|
102
|
+
PrioritizeRecentN?: number;
|
|
103
|
+
/** Maximum puppet count for replication */
|
|
104
|
+
PuppetMaxCount?: number;
|
|
105
|
+
/** Puppet replication frequency in Hz */
|
|
106
|
+
PuppetReplicationFrequency?: number;
|
|
107
|
+
/** Velocity threshold for puppet sleep */
|
|
108
|
+
PuppetSleepVelocity?: number;
|
|
109
|
+
/** Timeout before anchoring sleeping puppets */
|
|
110
|
+
PuppetAnchorTimeout?: number;
|
|
111
|
+
/** Whether to tween puppets on client */
|
|
112
|
+
ClientTweenPuppets?: boolean;
|
|
113
|
+
/** Distance limit for client-side tweening */
|
|
114
|
+
ClientTweenDistanceLimit?: number;
|
|
115
|
+
/** Behavior for non-divisible parts */
|
|
116
|
+
NonDivisibleInteraction?: "NONE" | "FALL" | "REMOVE";
|
|
117
|
+
/** Custom instance skip check function */
|
|
118
|
+
SkipInstanceCheck?: (instance: Instance) => boolean;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** A hitbox object with additional functionality */
|
|
122
|
+
export interface HitboxObject {
|
|
123
|
+
CFrame: CFrame;
|
|
124
|
+
Size: Vector3;
|
|
125
|
+
Shape: Enum.PartType;
|
|
126
|
+
DestructDelay?: number;
|
|
127
|
+
FilterTagged?: string | Array<string>;
|
|
128
|
+
OnVoxelDestruct?: string;
|
|
129
|
+
UserData: Record<string, unknown>;
|
|
130
|
+
ExcludePlayersReplication: Array<Player>;
|
|
131
|
+
OnDestructCompleted?: OnDestructCompleted;
|
|
132
|
+
CleanupDelay?: number;
|
|
133
|
+
GridSize?: number;
|
|
134
|
+
SkipEncapsulatedVoxels?: boolean;
|
|
135
|
+
SkipWalls?: boolean;
|
|
136
|
+
SkipFloors?: boolean;
|
|
137
|
+
DestructionType: "DEFAULT" | "IMAGINARY";
|
|
138
|
+
ImaginaryCallback?: (imaginaryVoxels: Array<ImaginaryVoxel>, existingDebris: Array<BasePart>) => void;
|
|
139
|
+
StartConnectionEvent: RBXScriptSignal;
|
|
140
|
+
WeldConnectionEvent: RBXScriptSignal;
|
|
141
|
+
VelocityPrediction: boolean;
|
|
142
|
+
VelocityBias?: number;
|
|
143
|
+
|
|
144
|
+
/** Performs voxel destruction around the hitbox */
|
|
145
|
+
Destroy(): void;
|
|
146
|
+
/** Returns imaginary voxels bounded by the hitbox */
|
|
147
|
+
ImaginaryVoxels(): LuaTuple<[Array<ImaginaryVoxel>, Array<BasePart>]>;
|
|
148
|
+
/** Welds the hitbox to follow a part */
|
|
149
|
+
WeldTo(part: BasePart): void;
|
|
150
|
+
/** Unwelds the hitbox */
|
|
151
|
+
Unweld(): void;
|
|
152
|
+
/** Starts continuous destruction */
|
|
153
|
+
Start(): void;
|
|
154
|
+
/** Stops continuous destruction */
|
|
155
|
+
Stop(): void;
|
|
156
|
+
/** Destroys the hitbox and disconnects all connections */
|
|
157
|
+
DestroyHitbox(): void;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export type EffectHook = (voxel: BasePart, info: DestroyedVoxelInfo) => void;
|
|
161
|
+
|
|
162
|
+
export type ClientEffectName<T extends string> = 'Default' | 'Rough' | T
|
|
163
|
+
export type ServerEffectName<T extends string> = 'Default' | 'MappingVoxelSpace' | 'BumpyFloorBreakWalls' | T
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
/// <reference types="@rbxts/types" />
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Function type for testing if a point is contained within a part shape
|
|
170
|
+
*/
|
|
171
|
+
type PartTypeContainsPoint = (cframe: CFrame, size: Vector3, p: Vector3) => boolean;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Function type for getting vertices of a part shape
|
|
175
|
+
*/
|
|
176
|
+
type GetVertsFunction = (cframe: CFrame, size: Vector3) => Array<Vector3>;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Function type for getting normals of a part shape
|
|
180
|
+
*/
|
|
181
|
+
type GetNormalsFunction = (cframe: CFrame, size?: Vector3) => Array<Vector3>;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Collection of vertex calculation functions for different part shapes
|
|
185
|
+
*/
|
|
186
|
+
interface GetVertsFunctions {
|
|
187
|
+
/**
|
|
188
|
+
* Gets the 8 corner vertices of a block part
|
|
189
|
+
*/
|
|
190
|
+
Block: GetVertsFunction;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Gets the 6 vertices of a wedge part
|
|
194
|
+
*/
|
|
195
|
+
Wedge: GetVertsFunction;
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Gets the 5 vertices of a corner wedge part
|
|
199
|
+
*/
|
|
200
|
+
CornerWedge: GetVertsFunction;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Collection of normal calculation functions for different part shapes
|
|
205
|
+
*/
|
|
206
|
+
interface GetNormalsFunctions {
|
|
207
|
+
/**
|
|
208
|
+
* Gets the 6 face normals of a block part
|
|
209
|
+
* @param cframe The CFrame of the block
|
|
210
|
+
*/
|
|
211
|
+
Block: (cframe: CFrame) => Array<Vector3>;
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Gets the face normals of a wedge part
|
|
215
|
+
* @param cframe The CFrame of the wedge
|
|
216
|
+
* @param size The size of the wedge
|
|
217
|
+
*/
|
|
218
|
+
Wedge: (cframe: CFrame, size: Vector3) => Array<Vector3>;
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Gets the face normals of a corner wedge part
|
|
222
|
+
* @param cframe The CFrame of the corner wedge
|
|
223
|
+
* @param size The size of the corner wedge
|
|
224
|
+
*/
|
|
225
|
+
CornerWedge: (cframe: CFrame, size: Vector3) => Array<Vector3>;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* VertexMath - Geometric intersection and containment testing utilities
|
|
230
|
+
*
|
|
231
|
+
* Provides efficient algorithms for:
|
|
232
|
+
* - Vertex and normal calculations for different part shapes
|
|
233
|
+
* - Separating Axis Theorem (SAT) intersection tests
|
|
234
|
+
* - Point-in-shape containment tests
|
|
235
|
+
* - Shape-shape intersection tests
|
|
236
|
+
*
|
|
237
|
+
* @example
|
|
238
|
+
* ```ts
|
|
239
|
+
* import VertexMath from "./VertexMath";
|
|
240
|
+
*
|
|
241
|
+
* // Get vertices of a block
|
|
242
|
+
* const blockVerts = VertexMath.GetVerts.Block(
|
|
243
|
+
* new CFrame(0, 10, 0),
|
|
244
|
+
* new Vector3(10, 5, 8)
|
|
245
|
+
* );
|
|
246
|
+
*
|
|
247
|
+
* // Test if a part contains all vertices
|
|
248
|
+
* const part = game.Workspace.WaitForChild("TestPart") as Part;
|
|
249
|
+
* const contained = VertexMath.PartContainsAllVerts(part, blockVerts);
|
|
250
|
+
* ```
|
|
251
|
+
*/
|
|
252
|
+
export interface VertexMath {
|
|
253
|
+
/**
|
|
254
|
+
* Collection of functions to get vertices for different part shapes.
|
|
255
|
+
*
|
|
256
|
+
* Each function returns an array of Vector3 positions representing
|
|
257
|
+
* the corners of the shape in world space.
|
|
258
|
+
*
|
|
259
|
+
* @example
|
|
260
|
+
* ```ts
|
|
261
|
+
* // Get block vertices
|
|
262
|
+
* const blockVerts = VertexMath.GetVerts.Block(
|
|
263
|
+
* part.CFrame,
|
|
264
|
+
* part.Size
|
|
265
|
+
* );
|
|
266
|
+
*
|
|
267
|
+
* // Get wedge vertices
|
|
268
|
+
* const wedgeVerts = VertexMath.GetVerts.Wedge(
|
|
269
|
+
* wedgePart.CFrame,
|
|
270
|
+
* wedgePart.Size
|
|
271
|
+
* );
|
|
272
|
+
* ```
|
|
273
|
+
*/
|
|
274
|
+
GetVerts: GetVertsFunctions;
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Collection of functions to get normal vectors for different part shapes.
|
|
278
|
+
*
|
|
279
|
+
* Each function returns an array of Vector3 unit vectors representing
|
|
280
|
+
* the outward-facing normals of the shape's faces.
|
|
281
|
+
*
|
|
282
|
+
* @example
|
|
283
|
+
* ```ts
|
|
284
|
+
* // Get block normals (6 face normals)
|
|
285
|
+
* const blockNormals = VertexMath.GetNormals.Block(part.CFrame);
|
|
286
|
+
*
|
|
287
|
+
* // Get wedge normals (requires size for slope calculation)
|
|
288
|
+
* const wedgeNormals = VertexMath.GetNormals.Wedge(
|
|
289
|
+
* wedgePart.CFrame,
|
|
290
|
+
* wedgePart.Size
|
|
291
|
+
* );
|
|
292
|
+
* ```
|
|
293
|
+
*/
|
|
294
|
+
GetNormals: GetNormalsFunctions;
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Separating Axis Theorem (SAT) intersection test.
|
|
298
|
+
*
|
|
299
|
+
* Tests whether two convex shapes intersect by checking if there exists
|
|
300
|
+
* a separating axis between them. This is the fundamental algorithm used
|
|
301
|
+
* for collision detection in Shatterbox.
|
|
302
|
+
*
|
|
303
|
+
* The algorithm tests separation on all candidate axes:
|
|
304
|
+
* - Face normals of shape A
|
|
305
|
+
* - Face normals of shape B
|
|
306
|
+
* - Cross products of edge directions from both shapes
|
|
307
|
+
*
|
|
308
|
+
* @param VA Vertices of shape A
|
|
309
|
+
* @param NA Normal vectors of shape A
|
|
310
|
+
* @param VB Vertices of shape B
|
|
311
|
+
* @param NB Normal vectors of shape B
|
|
312
|
+
* @returns True if shapes intersect, false if separated
|
|
313
|
+
*
|
|
314
|
+
* @example
|
|
315
|
+
* ```ts
|
|
316
|
+
* const partA = game.Workspace.PartA as Part;
|
|
317
|
+
* const partB = game.Workspace.PartB as Part;
|
|
318
|
+
*
|
|
319
|
+
* const vertsA = VertexMath.GetVerts.Block(partA.CFrame, partA.Size);
|
|
320
|
+
* const normalsA = VertexMath.GetNormals.Block(partA.CFrame);
|
|
321
|
+
* const vertsB = VertexMath.GetVerts.Block(partB.CFrame, partB.Size);
|
|
322
|
+
* const normalsB = VertexMath.GetNormals.Block(partB.CFrame);
|
|
323
|
+
*
|
|
324
|
+
* if (VertexMath.SAT(vertsA, normalsA, vertsB, normalsB)) {
|
|
325
|
+
* print("Parts are intersecting!");
|
|
326
|
+
* }
|
|
327
|
+
* ```
|
|
328
|
+
*/
|
|
329
|
+
SAT(VA: Array<Vector3>, NA: Array<Vector3>, VB: Array<Vector3>, NB: Array<Vector3>): boolean;
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Tests if a part fully encapsulates a block part.
|
|
333
|
+
*
|
|
334
|
+
* This is an optimized check that tests if all 8 corners of a block
|
|
335
|
+
* are contained within another part of any shape.
|
|
336
|
+
*
|
|
337
|
+
* @param part The container part (can be Block, Ball, Cylinder, Wedge, or CornerWedge)
|
|
338
|
+
* @param blockCFrame CFrame of the block to test
|
|
339
|
+
* @param blockSize Size of the block to test
|
|
340
|
+
* @returns True if the block is fully inside the part
|
|
341
|
+
*
|
|
342
|
+
* @example
|
|
343
|
+
* ```ts
|
|
344
|
+
* const container = game.Workspace.Container as Part;
|
|
345
|
+
* const blockCF = new CFrame(0, 10, 0);
|
|
346
|
+
* const blockSize = new Vector3(5, 5, 5);
|
|
347
|
+
*
|
|
348
|
+
* if (VertexMath.PartEncapsulatesBlockPart(container, blockCF, blockSize)) {
|
|
349
|
+
* print("Block is fully inside container");
|
|
350
|
+
* }
|
|
351
|
+
* ```
|
|
352
|
+
*/
|
|
353
|
+
PartEncapsulatesBlockPart(part: Part, blockCFrame: CFrame, blockSize: Vector3): boolean;
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Tests if a part contains all given vertices.
|
|
357
|
+
*
|
|
358
|
+
* Works with any part shape (Block, Ball, Cylinder, Wedge, CornerWedge).
|
|
359
|
+
* Uses shape-specific containment algorithms for efficiency.
|
|
360
|
+
*
|
|
361
|
+
* @param part The part to test against
|
|
362
|
+
* @param verts Array of vertex positions in world space
|
|
363
|
+
* @returns True if all vertices are inside the part
|
|
364
|
+
*
|
|
365
|
+
* @example
|
|
366
|
+
* ```ts
|
|
367
|
+
* const part = game.Workspace.TestPart as Part;
|
|
368
|
+
* const testVerts = [
|
|
369
|
+
* new Vector3(0, 10, 0),
|
|
370
|
+
* new Vector3(1, 10, 1),
|
|
371
|
+
* new Vector3(-1, 10, -1)
|
|
372
|
+
* ];
|
|
373
|
+
*
|
|
374
|
+
* if (VertexMath.PartContainsAllVerts(part, testVerts)) {
|
|
375
|
+
* print("All vertices are inside the part");
|
|
376
|
+
* }
|
|
377
|
+
* ```
|
|
378
|
+
*/
|
|
379
|
+
PartContainsAllVerts(part: Part, verts: Array<Vector3>): boolean;
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Tests if a part contains at least one vertex.
|
|
383
|
+
*
|
|
384
|
+
* Returns the index (1-based) of the first contained vertex,
|
|
385
|
+
* or false if no vertices are contained.
|
|
386
|
+
*
|
|
387
|
+
* @param part The part to test against
|
|
388
|
+
* @param verts Array of vertex positions in world space
|
|
389
|
+
* @returns Index of first contained vertex (1-based), or false if none
|
|
390
|
+
*
|
|
391
|
+
* @example
|
|
392
|
+
* ```ts
|
|
393
|
+
* const part = game.Workspace.TestPart as Part;
|
|
394
|
+
* const verts = VertexMath.GetVerts.Block(
|
|
395
|
+
* new CFrame(0, 10, 0),
|
|
396
|
+
* new Vector3(10, 10, 10)
|
|
397
|
+
* );
|
|
398
|
+
*
|
|
399
|
+
* const result = VertexMath.PartContainsAVert(part, verts);
|
|
400
|
+
* if (typeIs(result, "number")) {
|
|
401
|
+
* print(`Vertex ${result} is inside the part`);
|
|
402
|
+
* verts.remove(result - 1); // Convert to 0-based for array access
|
|
403
|
+
* }
|
|
404
|
+
* ```
|
|
405
|
+
*/
|
|
406
|
+
PartContainsAVert(part: Part, verts: Array<Vector3>): number | false;
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Tests if a ball (sphere) intersects with a block.
|
|
410
|
+
*
|
|
411
|
+
* Uses an optimized algorithm that finds the closest point on the block
|
|
412
|
+
* to the sphere center, then checks if that point is within the sphere radius.
|
|
413
|
+
*
|
|
414
|
+
* @param sphereCFrame CFrame of the sphere center
|
|
415
|
+
* @param sphereSize Size of the sphere (uses minimum dimension as diameter)
|
|
416
|
+
* @param boxCFrame CFrame of the box
|
|
417
|
+
* @param boxSize Size of the box
|
|
418
|
+
* @returns True if the sphere and box intersect
|
|
419
|
+
*
|
|
420
|
+
* @example
|
|
421
|
+
* ```ts
|
|
422
|
+
* const explosionPos = new CFrame(0, 10, 0);
|
|
423
|
+
* const explosionSize = new Vector3(8, 8, 8); // 4-stud radius
|
|
424
|
+
*
|
|
425
|
+
* const wall = game.Workspace.Wall as Part;
|
|
426
|
+
*
|
|
427
|
+
* if (VertexMath.BallIntersectsBlock(
|
|
428
|
+
* explosionPos,
|
|
429
|
+
* explosionSize,
|
|
430
|
+
* wall.CFrame,
|
|
431
|
+
* wall.Size
|
|
432
|
+
* )) {
|
|
433
|
+
* print("Explosion hits wall!");
|
|
434
|
+
* }
|
|
435
|
+
* ```
|
|
436
|
+
*/
|
|
437
|
+
BallIntersectsBlock(
|
|
438
|
+
sphereCFrame: CFrame,
|
|
439
|
+
sphereSize: Vector3,
|
|
440
|
+
boxCFrame: CFrame,
|
|
441
|
+
boxSize: Vector3
|
|
442
|
+
): boolean;
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Tests if a cylinder intersects with a block.
|
|
446
|
+
*
|
|
447
|
+
* This is a complex intersection test that combines multiple algorithms:
|
|
448
|
+
* - Line segment vs cylinder tests for all block edges
|
|
449
|
+
* - SAT test using cylinder bounding box
|
|
450
|
+
* - Plane vs cylinder tests for all block faces
|
|
451
|
+
*
|
|
452
|
+
* The cylinder is oriented along its X-axis (right vector).
|
|
453
|
+
*
|
|
454
|
+
* @param cylinderCFrame CFrame of the cylinder
|
|
455
|
+
* @param cylinderSize Size of the cylinder (X = length, Y/Z = diameter)
|
|
456
|
+
* @param cylinderBoxVerts Pre-calculated bounding box vertices of cylinder
|
|
457
|
+
* @param cylinderBoxNormals Pre-calculated bounding box normals of cylinder
|
|
458
|
+
* @param boxCFrame CFrame of the box
|
|
459
|
+
* @param boxSize Size of the box
|
|
460
|
+
* @param boxVerts Vertices of the box
|
|
461
|
+
* @param boxNormals Normals of the box
|
|
462
|
+
* @returns True if the cylinder and box intersect
|
|
463
|
+
*
|
|
464
|
+
* @example
|
|
465
|
+
* ```ts
|
|
466
|
+
* const cylinder = game.Workspace.Cylinder as Part;
|
|
467
|
+
* const block = game.Workspace.Block as Part;
|
|
468
|
+
*
|
|
469
|
+
* // Pre-calculate cylinder bounding box
|
|
470
|
+
* const cylinderVerts = VertexMath.GetVerts.Block(
|
|
471
|
+
* cylinder.CFrame,
|
|
472
|
+
* cylinder.Size
|
|
473
|
+
* );
|
|
474
|
+
* const cylinderNormals = VertexMath.GetNormals.Block(cylinder.CFrame);
|
|
475
|
+
*
|
|
476
|
+
* // Get block geometry
|
|
477
|
+
* const blockVerts = VertexMath.GetVerts.Block(block.CFrame, block.Size);
|
|
478
|
+
* const blockNormals = VertexMath.GetNormals.Block(block.CFrame);
|
|
479
|
+
*
|
|
480
|
+
* if (VertexMath.CylinderIntersectsBlock(
|
|
481
|
+
* cylinder.CFrame,
|
|
482
|
+
* cylinder.Size,
|
|
483
|
+
* cylinderVerts,
|
|
484
|
+
* cylinderNormals,
|
|
485
|
+
* block.CFrame,
|
|
486
|
+
* block.Size,
|
|
487
|
+
* blockVerts,
|
|
488
|
+
* blockNormals
|
|
489
|
+
* )) {
|
|
490
|
+
* print("Cylinder intersects block!");
|
|
491
|
+
* }
|
|
492
|
+
* ```
|
|
493
|
+
*/
|
|
494
|
+
CylinderIntersectsBlock(
|
|
495
|
+
cylinderCFrame: CFrame,
|
|
496
|
+
cylinderSize: Vector3,
|
|
497
|
+
cylinderBoxVerts: Array<Vector3>,
|
|
498
|
+
cylinderBoxNormals: Array<Vector3>,
|
|
499
|
+
boxCFrame: CFrame,
|
|
500
|
+
boxSize: Vector3,
|
|
501
|
+
boxVerts: Array<Vector3>,
|
|
502
|
+
boxNormals: Array<Vector3>
|
|
503
|
+
): boolean;
|
|
504
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--!native
|
|
3
|
+
--!optimize 2
|
|
4
|
+
--#selene: allow(multiple_statements)
|
|
5
|
+
|
|
6
|
+
--[[ USAGE
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
local visualizer = HitboxVisualizer.Create(hitbox)
|
|
10
|
+
|
|
11
|
+
-- set properties (visualizer.property = someValue)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
-- shows the hitbox once
|
|
15
|
+
visualizer:ShowOnce(dur, useFade)
|
|
16
|
+
|
|
17
|
+
-- shows the hitbox constantly
|
|
18
|
+
visualizer:StartShowing()
|
|
19
|
+
|
|
20
|
+
-- stops showing the hitbox constantly
|
|
21
|
+
visualizer:StopShowing()
|
|
22
|
+
]]
|
|
23
|
+
|
|
24
|
+
local RunService = game:GetService("RunService")
|
|
25
|
+
local TweenService = game:GetService("TweenService")
|
|
26
|
+
local FadeParams = { Transparency = 1 }
|
|
27
|
+
|
|
28
|
+
local HitboxVisualizer = {}
|
|
29
|
+
|
|
30
|
+
type HitboxType = { CFrame : CFrame, Size : Vector3, Shape : Enum.PartType }
|
|
31
|
+
|
|
32
|
+
function HitboxVisualizer.Create(hitbox : HitboxType)
|
|
33
|
+
|
|
34
|
+
local visualizer = {}
|
|
35
|
+
visualizer.Transparency = 0.5 :: number
|
|
36
|
+
visualizer.Color = Color3.new(1) :: Color3
|
|
37
|
+
|
|
38
|
+
visualizer.DefualtOnceDuration = 2
|
|
39
|
+
visualizer.DefaultFade = true
|
|
40
|
+
|
|
41
|
+
local vPart = Instance.new("Part")
|
|
42
|
+
vPart.Material = Enum.Material.SmoothPlastic
|
|
43
|
+
vPart.Anchored = true
|
|
44
|
+
vPart.CanCollide = false
|
|
45
|
+
vPart.CanTouch = false
|
|
46
|
+
vPart.CanQuery = false
|
|
47
|
+
vPart.Name = "VisualizedHitbox"
|
|
48
|
+
|
|
49
|
+
function visualizer:ShowOnce(duration : number?, useFade : boolean?)
|
|
50
|
+
if not duration then duration = visualizer.DefualtOnceDuration end
|
|
51
|
+
if useFade == nil then useFade = visualizer.DefaultFade end
|
|
52
|
+
|
|
53
|
+
local v = vPart:Clone()
|
|
54
|
+
v.Color = visualizer.Color
|
|
55
|
+
v.Transparency = visualizer.Transparency
|
|
56
|
+
v.Shape = hitbox.Shape
|
|
57
|
+
v.Size = hitbox.Size
|
|
58
|
+
v.CFrame = hitbox.CFrame
|
|
59
|
+
v.Parent = workspace
|
|
60
|
+
|
|
61
|
+
if not useFade then return end
|
|
62
|
+
|
|
63
|
+
local tween = TweenService:Create(v, TweenInfo.new(duration, Enum.EasingStyle.Linear), FadeParams)
|
|
64
|
+
|
|
65
|
+
tween.Completed:Connect(function() v:Destroy(); end)
|
|
66
|
+
|
|
67
|
+
tween:Play()
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
local ccon : RBXScriptConnection
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
function visualizer:StartShowing()
|
|
75
|
+
if ccon then return end
|
|
76
|
+
vPart.Parent = workspace
|
|
77
|
+
ccon = RunService.PreSimulation:Connect(function()
|
|
78
|
+
vPart.Color = visualizer.Color
|
|
79
|
+
vPart.Transparency = visualizer.Transparency
|
|
80
|
+
vPart.Shape = hitbox.Shape
|
|
81
|
+
vPart.Size = hitbox.Size
|
|
82
|
+
vPart.CFrame = hitbox.CFrame
|
|
83
|
+
end)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
function visualizer:StopShowing()
|
|
88
|
+
if not ccon then return end
|
|
89
|
+
ccon:Disconnect()
|
|
90
|
+
ccon = nil
|
|
91
|
+
vPart.Parent = nil
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
return visualizer
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
return HitboxVisualizer
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--!native
|
|
3
|
+
--!optimize 2
|
|
4
|
+
|
|
5
|
+
--#selene: allow(shadowing)
|
|
6
|
+
|
|
7
|
+
--[[ USAGE:
|
|
8
|
+
|
|
9
|
+
Call the module with the tag you want. Example: TaggedArray("Destructibles") returns a continuously updating array of destructibles.
|
|
10
|
+
|
|
11
|
+
Call TaggedArray.Destroy(tag) to stop the continous updates associated with the array.
|
|
12
|
+
|
|
13
|
+
Caching is used, so separate calls requesting the same tagged array will recieve the same tagged array (instead of new arrays and connections each time)
|
|
14
|
+
]]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
local CollectionService = game:GetService("CollectionService")
|
|
18
|
+
|
|
19
|
+
type TaggedArrayInternal = {
|
|
20
|
+
Array : {Instance},
|
|
21
|
+
Connections : {
|
|
22
|
+
Added : RBXScriptConnection,
|
|
23
|
+
Removed : RBXScriptConnection
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
local TaggedArrays = {} :: { [string] : TaggedArrayInternal }
|
|
28
|
+
|
|
29
|
+
local function GetTaggedArray(_, tag : string) : {Instance}
|
|
30
|
+
local found = TaggedArrays[tag]
|
|
31
|
+
if found then return found.Array end
|
|
32
|
+
|
|
33
|
+
local taggedArray = CollectionService:GetTagged(tag)
|
|
34
|
+
|
|
35
|
+
TaggedArrays[tag] = {
|
|
36
|
+
Array = taggedArray,
|
|
37
|
+
|
|
38
|
+
Connections = {
|
|
39
|
+
Added =
|
|
40
|
+
CollectionService:GetInstanceAddedSignal(tag):Connect(function(i : Instance)
|
|
41
|
+
table.insert(taggedArray, i)
|
|
42
|
+
end),
|
|
43
|
+
Removed =
|
|
44
|
+
CollectionService:GetInstanceRemovedSignal(tag):Connect(function(i : Instance)
|
|
45
|
+
local found = table.find(taggedArray, i)
|
|
46
|
+
if not found then return end
|
|
47
|
+
table.remove(taggedArray, found)
|
|
48
|
+
end)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return taggedArray
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
local TaggedArray = setmetatable({}, { __call = GetTaggedArray })
|
|
56
|
+
|
|
57
|
+
function TaggedArray.Destroy(tag : string)
|
|
58
|
+
local found = TaggedArrays[tag]
|
|
59
|
+
if not found then return end
|
|
60
|
+
local connections = found.Connections
|
|
61
|
+
connections.Added:Disconnect()
|
|
62
|
+
connections.Removed:Disconnect()
|
|
63
|
+
TaggedArrays[tag] = nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
return TaggedArray
|