roblox-opencode 1.0.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 (248) hide show
  1. package/README.md +122 -0
  2. package/commands/setup-game.md +108 -0
  3. package/commands/sync-check.md +53 -0
  4. package/core/roblox-core.md +93 -0
  5. package/dist/server.js +167 -0
  6. package/package.json +35 -0
  7. package/skills/roblox-analytics/SKILL.md +277 -0
  8. package/skills/roblox-analytics/references/event-batcher.luau +75 -0
  9. package/skills/roblox-animation-vfx/SKILL.md +1325 -0
  10. package/skills/roblox-architecture/SKILL.md +863 -0
  11. package/skills/roblox-architecture/references/combat-systems.md +1381 -0
  12. package/skills/roblox-code-review/SKILL.md +687 -0
  13. package/skills/roblox-data/SKILL.md +889 -0
  14. package/skills/roblox-data/references/inventory-systems.md +1729 -0
  15. package/skills/roblox-debug/SKILL.md +99 -0
  16. package/skills/roblox-gui/SKILL.md +1103 -0
  17. package/skills/roblox-gui-fusion/SKILL.md +150 -0
  18. package/skills/roblox-gui-fusion/references/inventory.luau +427 -0
  19. package/skills/roblox-gui-fusion/references/settings-menu.luau +579 -0
  20. package/skills/roblox-gui-fusion/references/shop.luau +411 -0
  21. package/skills/roblox-luau-mastery/SKILL.md +1519 -0
  22. package/skills/roblox-monetization/SKILL.md +1084 -0
  23. package/skills/roblox-monetization/references/process-receipt.luau +131 -0
  24. package/skills/roblox-networking/SKILL.md +669 -0
  25. package/skills/roblox-networking/references/remote-validator.luau +193 -0
  26. package/skills/roblox-publish-checklist/SKILL.md +128 -0
  27. package/skills/roblox-runtime/SKILL.md +753 -0
  28. package/skills/roblox-sharp-edges/SKILL.md +295 -0
  29. package/skills/roblox-sync/SKILL.md +126 -0
  30. package/skills/roblox-testing/SKILL.md +943 -0
  31. package/skills/roblox-tooling/SKILL.md +150 -0
  32. package/vendor/LICENSES/ProfileStore-LICENSE +201 -0
  33. package/vendor/LICENSES/RbxUtil-LICENSE +7 -0
  34. package/vendor/LICENSES/promise-LICENSE +21 -0
  35. package/vendor/LICENSES/t-LICENSE +21 -0
  36. package/vendor/LICENSES/testez-LICENSE +201 -0
  37. package/vendor/README.md +84 -0
  38. package/vendor/fusion/Animation/ExternalTime.luau +84 -0
  39. package/vendor/fusion/Animation/Spring.luau +322 -0
  40. package/vendor/fusion/Animation/Stopwatch.luau +128 -0
  41. package/vendor/fusion/Animation/Tween.luau +187 -0
  42. package/vendor/fusion/Animation/getTweenDuration.luau +27 -0
  43. package/vendor/fusion/Animation/getTweenRatio.luau +47 -0
  44. package/vendor/fusion/Animation/lerpType.luau +164 -0
  45. package/vendor/fusion/Animation/packType.luau +100 -0
  46. package/vendor/fusion/Animation/springCoefficients.luau +80 -0
  47. package/vendor/fusion/Animation/unpackType.luau +103 -0
  48. package/vendor/fusion/Colour/Oklab.luau +70 -0
  49. package/vendor/fusion/Colour/sRGB.luau +55 -0
  50. package/vendor/fusion/External.luau +168 -0
  51. package/vendor/fusion/ExternalDebug.luau +70 -0
  52. package/vendor/fusion/Graph/Observer.luau +114 -0
  53. package/vendor/fusion/Graph/castToGraph.luau +29 -0
  54. package/vendor/fusion/Graph/change.luau +81 -0
  55. package/vendor/fusion/Graph/depend.luau +33 -0
  56. package/vendor/fusion/Graph/evaluate.luau +56 -0
  57. package/vendor/fusion/Instances/Attribute.luau +58 -0
  58. package/vendor/fusion/Instances/AttributeChange.luau +47 -0
  59. package/vendor/fusion/Instances/AttributeOut.luau +63 -0
  60. package/vendor/fusion/Instances/Child.luau +21 -0
  61. package/vendor/fusion/Instances/Children.luau +148 -0
  62. package/vendor/fusion/Instances/Hydrate.luau +33 -0
  63. package/vendor/fusion/Instances/New.luau +53 -0
  64. package/vendor/fusion/Instances/OnChange.luau +50 -0
  65. package/vendor/fusion/Instances/OnEvent.luau +54 -0
  66. package/vendor/fusion/Instances/Out.luau +69 -0
  67. package/vendor/fusion/Instances/applyInstanceProps.luau +149 -0
  68. package/vendor/fusion/Instances/defaultProps.luau +194 -0
  69. package/vendor/fusion/LICENSE +21 -0
  70. package/vendor/fusion/Logging/formatError.luau +49 -0
  71. package/vendor/fusion/Logging/messages.luau +52 -0
  72. package/vendor/fusion/Logging/parseError.luau +25 -0
  73. package/vendor/fusion/Memory/checkLifetime.luau +134 -0
  74. package/vendor/fusion/Memory/deriveScope.luau +24 -0
  75. package/vendor/fusion/Memory/deriveScopeImpl.luau +45 -0
  76. package/vendor/fusion/Memory/doCleanup.luau +79 -0
  77. package/vendor/fusion/Memory/innerScope.luau +34 -0
  78. package/vendor/fusion/Memory/legacyCleanup.luau +18 -0
  79. package/vendor/fusion/Memory/needsDestruction.luau +17 -0
  80. package/vendor/fusion/Memory/poisonScope.luau +34 -0
  81. package/vendor/fusion/Memory/scopePool.luau +55 -0
  82. package/vendor/fusion/Memory/scoped.luau +27 -0
  83. package/vendor/fusion/Memory/whichLivesLonger.luau +75 -0
  84. package/vendor/fusion/RobloxExternal.luau +98 -0
  85. package/vendor/fusion/State/Computed.luau +139 -0
  86. package/vendor/fusion/State/For/Disassembly.luau +211 -0
  87. package/vendor/fusion/State/For/ForTypes.luau +30 -0
  88. package/vendor/fusion/State/For/init.luau +110 -0
  89. package/vendor/fusion/State/ForKeys.luau +94 -0
  90. package/vendor/fusion/State/ForPairs.luau +97 -0
  91. package/vendor/fusion/State/ForValues.luau +94 -0
  92. package/vendor/fusion/State/Value.luau +88 -0
  93. package/vendor/fusion/State/castToState.luau +26 -0
  94. package/vendor/fusion/State/peek.luau +31 -0
  95. package/vendor/fusion/State/updateAll.luau +1 -0
  96. package/vendor/fusion/Types.luau +314 -0
  97. package/vendor/fusion/Utility/Contextual.luau +91 -0
  98. package/vendor/fusion/Utility/Safe.luau +23 -0
  99. package/vendor/fusion/Utility/isSimilar.luau +29 -0
  100. package/vendor/fusion/Utility/merge.luau +35 -0
  101. package/vendor/fusion/Utility/nameOf.luau +35 -0
  102. package/vendor/fusion/Utility/never.luau +14 -0
  103. package/vendor/fusion/Utility/nicknames.luau +11 -0
  104. package/vendor/fusion/Utility/xtypeof.luau +27 -0
  105. package/vendor/fusion/init.luau +82 -0
  106. package/vendor/profilestore/init.luau +2243 -0
  107. package/vendor/promise/init.luau +1982 -0
  108. package/vendor/rbxutil/buffer-util/Buffer.test.luau +25 -0
  109. package/vendor/rbxutil/buffer-util/BufferReader.luau +228 -0
  110. package/vendor/rbxutil/buffer-util/BufferWriter.luau +269 -0
  111. package/vendor/rbxutil/buffer-util/DataTypeBuffer.luau +223 -0
  112. package/vendor/rbxutil/buffer-util/Types.luau +60 -0
  113. package/vendor/rbxutil/buffer-util/index.d.ts +153 -0
  114. package/vendor/rbxutil/buffer-util/init.luau +41 -0
  115. package/vendor/rbxutil/buffer-util/package.json +16 -0
  116. package/vendor/rbxutil/buffer-util/wally.toml +9 -0
  117. package/vendor/rbxutil/comm/Client/ClientComm.luau +232 -0
  118. package/vendor/rbxutil/comm/Client/ClientRemoteProperty.luau +156 -0
  119. package/vendor/rbxutil/comm/Client/ClientRemoteSignal.luau +109 -0
  120. package/vendor/rbxutil/comm/Client/init.luau +135 -0
  121. package/vendor/rbxutil/comm/Server/RemoteProperty.luau +295 -0
  122. package/vendor/rbxutil/comm/Server/RemoteSignal.luau +211 -0
  123. package/vendor/rbxutil/comm/Server/ServerComm.luau +211 -0
  124. package/vendor/rbxutil/comm/Server/init.luau +140 -0
  125. package/vendor/rbxutil/comm/Types.luau +18 -0
  126. package/vendor/rbxutil/comm/Util.luau +27 -0
  127. package/vendor/rbxutil/comm/init.luau +35 -0
  128. package/vendor/rbxutil/comm/wally.toml +13 -0
  129. package/vendor/rbxutil/component/init.luau +759 -0
  130. package/vendor/rbxutil/component/init.test.luau +311 -0
  131. package/vendor/rbxutil/component/wally.toml +14 -0
  132. package/vendor/rbxutil/concur/init.luau +542 -0
  133. package/vendor/rbxutil/concur/init.test.luau +364 -0
  134. package/vendor/rbxutil/concur/wally.toml +8 -0
  135. package/vendor/rbxutil/enum-list/init.luau +101 -0
  136. package/vendor/rbxutil/enum-list/init.test.luau +91 -0
  137. package/vendor/rbxutil/enum-list/wally.toml +8 -0
  138. package/vendor/rbxutil/find/index.d.ts +20 -0
  139. package/vendor/rbxutil/find/init.luau +44 -0
  140. package/vendor/rbxutil/find/package.json +17 -0
  141. package/vendor/rbxutil/find/wally.toml +8 -0
  142. package/vendor/rbxutil/input/Gamepad.luau +559 -0
  143. package/vendor/rbxutil/input/Keyboard.luau +124 -0
  144. package/vendor/rbxutil/input/Mouse.luau +278 -0
  145. package/vendor/rbxutil/input/PreferredInput.luau +91 -0
  146. package/vendor/rbxutil/input/Touch.luau +120 -0
  147. package/vendor/rbxutil/input/init.luau +33 -0
  148. package/vendor/rbxutil/input/wally.toml +12 -0
  149. package/vendor/rbxutil/loader/index.d.ts +15 -0
  150. package/vendor/rbxutil/loader/init.luau +137 -0
  151. package/vendor/rbxutil/loader/wally.toml +8 -0
  152. package/vendor/rbxutil/log/index.d.ts +38 -0
  153. package/vendor/rbxutil/log/init.luau +746 -0
  154. package/vendor/rbxutil/log/wally.toml +8 -0
  155. package/vendor/rbxutil/net/init.luau +190 -0
  156. package/vendor/rbxutil/net/wally.toml +8 -0
  157. package/vendor/rbxutil/option/index.d.ts +44 -0
  158. package/vendor/rbxutil/option/init.luau +489 -0
  159. package/vendor/rbxutil/option/init.test.luau +342 -0
  160. package/vendor/rbxutil/option/wally.toml +8 -0
  161. package/vendor/rbxutil/pid/index.d.ts +53 -0
  162. package/vendor/rbxutil/pid/init.luau +195 -0
  163. package/vendor/rbxutil/pid/package.json +16 -0
  164. package/vendor/rbxutil/pid/wally.toml +9 -0
  165. package/vendor/rbxutil/quaternion/index.d.ts +117 -0
  166. package/vendor/rbxutil/quaternion/init.luau +570 -0
  167. package/vendor/rbxutil/quaternion/package.json +16 -0
  168. package/vendor/rbxutil/quaternion/wally.toml +9 -0
  169. package/vendor/rbxutil/query/index.d.ts +43 -0
  170. package/vendor/rbxutil/query/init.luau +117 -0
  171. package/vendor/rbxutil/query/package.json +18 -0
  172. package/vendor/rbxutil/query/wally.toml +9 -0
  173. package/vendor/rbxutil/sequent/index.d.ts +28 -0
  174. package/vendor/rbxutil/sequent/init.luau +340 -0
  175. package/vendor/rbxutil/sequent/package.json +16 -0
  176. package/vendor/rbxutil/sequent/wally.toml +9 -0
  177. package/vendor/rbxutil/ser/init.luau +175 -0
  178. package/vendor/rbxutil/ser/init.test.luau +50 -0
  179. package/vendor/rbxutil/ser/wally.toml +11 -0
  180. package/vendor/rbxutil/shake/index.d.ts +36 -0
  181. package/vendor/rbxutil/shake/init.luau +532 -0
  182. package/vendor/rbxutil/shake/init.test.luau +267 -0
  183. package/vendor/rbxutil/shake/package.json +16 -0
  184. package/vendor/rbxutil/shake/wally.toml +9 -0
  185. package/vendor/rbxutil/signal/index.d.ts +100 -0
  186. package/vendor/rbxutil/signal/init.luau +432 -0
  187. package/vendor/rbxutil/signal/init.test.luau +190 -0
  188. package/vendor/rbxutil/signal/package.json +17 -0
  189. package/vendor/rbxutil/signal/wally.toml +9 -0
  190. package/vendor/rbxutil/silo/TableWatcher.luau +65 -0
  191. package/vendor/rbxutil/silo/Util.luau +55 -0
  192. package/vendor/rbxutil/silo/init.luau +338 -0
  193. package/vendor/rbxutil/silo/init.test.luau +215 -0
  194. package/vendor/rbxutil/silo/wally.toml +8 -0
  195. package/vendor/rbxutil/spring/index.d.ts +40 -0
  196. package/vendor/rbxutil/spring/init.luau +97 -0
  197. package/vendor/rbxutil/spring/package.json +17 -0
  198. package/vendor/rbxutil/spring/wally.toml +8 -0
  199. package/vendor/rbxutil/stream/index.d.ts +88 -0
  200. package/vendor/rbxutil/stream/init.luau +597 -0
  201. package/vendor/rbxutil/stream/package.json +18 -0
  202. package/vendor/rbxutil/stream/wally.toml +9 -0
  203. package/vendor/rbxutil/streamable/Streamable.luau +202 -0
  204. package/vendor/rbxutil/streamable/StreamableUtil.luau +80 -0
  205. package/vendor/rbxutil/streamable/init.luau +8 -0
  206. package/vendor/rbxutil/streamable/wally.toml +12 -0
  207. package/vendor/rbxutil/symbol/init.luau +56 -0
  208. package/vendor/rbxutil/symbol/init.test.luau +37 -0
  209. package/vendor/rbxutil/symbol/wally.toml +8 -0
  210. package/vendor/rbxutil/table-util/init.luau +938 -0
  211. package/vendor/rbxutil/table-util/init.test.luau +439 -0
  212. package/vendor/rbxutil/table-util/wally.toml +8 -0
  213. package/vendor/rbxutil/task-queue/index.d.ts +27 -0
  214. package/vendor/rbxutil/task-queue/init.luau +97 -0
  215. package/vendor/rbxutil/task-queue/wally.toml +8 -0
  216. package/vendor/rbxutil/timer/index.d.ts +81 -0
  217. package/vendor/rbxutil/timer/init.luau +249 -0
  218. package/vendor/rbxutil/timer/init.test.luau +73 -0
  219. package/vendor/rbxutil/timer/wally.toml +11 -0
  220. package/vendor/rbxutil/tree/index.d.ts +15 -0
  221. package/vendor/rbxutil/tree/init.luau +137 -0
  222. package/vendor/rbxutil/tree/wally.toml +8 -0
  223. package/vendor/rbxutil/trove/index.d.ts +46 -0
  224. package/vendor/rbxutil/trove/init.luau +787 -0
  225. package/vendor/rbxutil/trove/init.test.luau +203 -0
  226. package/vendor/rbxutil/trove/wally.toml +8 -0
  227. package/vendor/rbxutil/typed-remote/init.luau +196 -0
  228. package/vendor/rbxutil/typed-remote/wally.toml +8 -0
  229. package/vendor/rbxutil/wait-for/index.d.ts +17 -0
  230. package/vendor/rbxutil/wait-for/init.luau +257 -0
  231. package/vendor/rbxutil/wait-for/init.test.luau +182 -0
  232. package/vendor/rbxutil/wait-for/wally.toml +11 -0
  233. package/vendor/t/t.lua +1350 -0
  234. package/vendor/testez/Context.lua +26 -0
  235. package/vendor/testez/Expectation.lua +311 -0
  236. package/vendor/testez/ExpectationContext.lua +38 -0
  237. package/vendor/testez/LifecycleHooks.lua +89 -0
  238. package/vendor/testez/Reporters/TeamCityReporter.lua +102 -0
  239. package/vendor/testez/Reporters/TextReporter.lua +106 -0
  240. package/vendor/testez/Reporters/TextReporterQuiet.lua +97 -0
  241. package/vendor/testez/TestBootstrap.lua +147 -0
  242. package/vendor/testez/TestEnum.lua +28 -0
  243. package/vendor/testez/TestPlan.lua +304 -0
  244. package/vendor/testez/TestPlanner.lua +40 -0
  245. package/vendor/testez/TestResults.lua +112 -0
  246. package/vendor/testez/TestRunner.lua +188 -0
  247. package/vendor/testez/TestSession.lua +243 -0
  248. package/vendor/testez/init.lua +40 -0
@@ -0,0 +1,1325 @@
1
+ ---
2
+ name: roblox-animation-vfx
3
+ description: >
4
+ Animations, particles, tweens, ContentProvider, visual effects.
5
+ last_reviewed: 2026-05-25
6
+ ---
7
+
8
+ <!-- Source: brockmartin/roblox-game-skill (MIT) -->
9
+
10
+ # Animation & VFX Reference
11
+
12
+ ## Overview
13
+
14
+ Load this reference when working on:
15
+
16
+ - Character or NPC animations (idle, walk, attack, emotes)
17
+ - Particle effects (fire, smoke, sparkles, magic, weather)
18
+ - Beams and trails (lasers, sword swings, magic projectiles)
19
+ - TweenService-driven visual feedback (hit flashes, pulses, transitions)
20
+ - Lighting and post-processing (mood, atmosphere, glow)
21
+ - Sound design and positional audio
22
+ - Camera effects (shake, zoom, cutscenes)
23
+ - General visual polish and juice
24
+
25
+ ---
26
+
27
+ ## Quick Reference
28
+
29
+ **Load Full Reference below only when you need specific property values, recipes, or implementation details.**
30
+
31
+ Key rules:
32
+ - Animations need uploaded AnimationIds (rbxassetid://). Never invent IDs.
33
+ - Priority order: Core < Idle < Movement < Action. Higher priority overrides lower on same track.
34
+ - Always use `Animator` (on Humanoid/AnimationController), not deprecated `Humanoid:LoadAnimation()`
35
+ - MarkerReachedSignal for syncing sounds/VFX to animation frames
36
+ - ParticleEmitter: Rate=0 + Emit(count) for burst effects. Enabled=false to stop new particles.
37
+ - Beams need Attachment0 + Attachment1. Trails need one Attachment.
38
+ - Highlight: parent to target or set Adornee. Max 255 per client. AlwaysOnTop to see through geometry.
39
+ - TweenService: create TweenInfo once, reuse. Chain with Completed event, don't nest.
40
+ - Post-processing: keep subtle. Bloom + ColorCorrection + DepthOfField cover most moods.
41
+ - Clean up: Destroy() particles/beams when done. Use Trove for lifecycle.
42
+
43
+ ---
44
+
45
+ ## Full Reference
46
+
47
+ ## Character Animation
48
+
49
+ ### Animator Service on Humanoid
50
+
51
+ Every `Humanoid` has (or should have) an `Animator` child. The `Animator` is the engine that plays, blends, and prioritizes animation tracks on a character rig.
52
+
53
+ ```luau
54
+ local character = player.Character or player.CharacterAdded:Wait()
55
+ local humanoid = character:WaitForChild("Humanoid")
56
+ local animator = humanoid:FindFirstChildOfClass("Animator")
57
+ or humanoid:WaitForChild("Animator")
58
+ ```
59
+
60
+ ### Loading and Playing Animations
61
+
62
+ ```luau
63
+ -- 1. Create an Animation instance with the asset ID
64
+ local slashAnim = Instance.new("Animation")
65
+ slashAnim.AnimationId = "rbxassetid://123456789"
66
+
67
+ -- 2. Load it through the Animator (returns an AnimationTrack)
68
+ local slashTrack = animator:LoadAnimation(slashAnim)
69
+
70
+ -- 3. Play / Stop
71
+ slashTrack:Play()
72
+ -- Optional fade time and weight
73
+ slashTrack:Play(0.2) -- 0.2s fade-in
74
+ slashTrack:Stop(0.3) -- 0.3s fade-out
75
+
76
+ -- 4. Adjust speed at runtime
77
+ slashTrack:AdjustSpeed(1.5) -- 1.5x playback
78
+ slashTrack:AdjustWeight(0.8) -- 80% blend weight
79
+ ```
80
+
81
+ ### Animation Priorities
82
+
83
+ Priorities determine which animation wins when multiple tracks affect the same joints. Higher priority overrides lower.
84
+
85
+ | Priority | Use Case |
86
+ | ------------------------------------- | ---------------------------------- |
87
+ | `Enum.AnimationPriority.Idle` | Breathing, idle sway |
88
+ | `Enum.AnimationPriority.Movement` | Walk, run, jump, fall |
89
+ | `Enum.AnimationPriority.Action` | Attack, interact, emote |
90
+ | `Enum.AnimationPriority.Action2` | Higher-priority actions |
91
+ | `Enum.AnimationPriority.Action3` | Even higher-priority actions |
92
+ | `Enum.AnimationPriority.Action4` | Highest action tier |
93
+ | `Enum.AnimationPriority.Core` | Internal Roblox (avoid overriding) |
94
+
95
+ ```luau
96
+ slashTrack.Priority = Enum.AnimationPriority.Action
97
+ ```
98
+
99
+ ### MarkerReachedSignal
100
+
101
+ Animation events let you fire logic at exact frames inside an animation (set markers in the Animation Editor).
102
+
103
+ ```luau
104
+ slashTrack:GetMarkerReachedSignal("HitFrame"):Connect(function(paramValue: string)
105
+ -- Spawn hitbox, play sound, emit particles, etc.
106
+ print("Hit frame reached!", paramValue)
107
+ end)
108
+ ```
109
+
110
+ ### Blending Between Animations
111
+
112
+ Roblox automatically blends overlapping tracks based on priority and weight. To cross-fade manually:
113
+
114
+ ```luau
115
+ local walkTrack = animator:LoadAnimation(walkAnim)
116
+ local runTrack = animator:LoadAnimation(runAnim)
117
+
118
+ walkTrack:Play(0.2)
119
+
120
+ -- Later, cross-fade to run
121
+ walkTrack:Stop(0.3)
122
+ runTrack:Play(0.3)
123
+ ```
124
+
125
+ For partial-body layering (e.g., upper body attack while legs run), set different priorities and ensure the lower-priority animation only drives lower body joints.
126
+
127
+ ---
128
+
129
+ ## AnimationController
130
+
131
+ Use `AnimationController` for anything that is NOT a `Humanoid` -- props, doors, creatures with custom rigs, cutscene actors, etc.
132
+
133
+ ```luau
134
+ local model = workspace.DragonNPC
135
+ local animController = Instance.new("AnimationController")
136
+ animController.Parent = model
137
+
138
+ local flyAnim = Instance.new("Animation")
139
+ flyAnim.AnimationId = "rbxassetid://987654321"
140
+
141
+ local flyTrack = animController:LoadAnimation(flyAnim)
142
+ flyTrack.Looped = true
143
+ flyTrack:Play()
144
+ ```
145
+
146
+ ### Custom Rigs
147
+
148
+ - The model needs `Motor6D` joints connecting its parts, just like a character rig.
149
+ - Root part should be the `PrimaryPart` of the model.
150
+ - Animations are authored in the Animation Editor against this rig, then exported.
151
+
152
+ ### Attaching to Models
153
+
154
+ ```luau
155
+ -- Typical setup for an animated NPC without Humanoid
156
+ local npc = Instance.new("Model")
157
+ npc.Name = "CrystalGolem"
158
+
159
+ local rootPart = Instance.new("Part")
160
+ rootPart.Name = "HumanoidRootPart" -- convention for animation rigs
161
+ rootPart.Anchored = false
162
+ rootPart.Parent = npc
163
+ npc.PrimaryPart = rootPart
164
+
165
+ local animController = Instance.new("AnimationController")
166
+ animController.Parent = npc
167
+
168
+ -- Add Motor6D joints, mesh parts, then load animations
169
+ ```
170
+
171
+ ---
172
+
173
+ ## Particle Effects
174
+
175
+ ### ParticleEmitter Core Properties
176
+
177
+ | Property | Type | Description |
178
+ | --------------- | ----------------- | -------------------------------------------------- |
179
+ | `Rate` | `number` | Particles emitted per second (0 = manual Emit()) |
180
+ | `Lifetime` | `NumberRange` | How long each particle lives (seconds) |
181
+ | `Speed` | `NumberRange` | Initial velocity (studs/second) |
182
+ | `SpreadAngle` | `Vector2` | Cone spread in X and Y (degrees) |
183
+ | `Size` | `NumberSequence` | Size over particle lifetime |
184
+ | `Color` | `ColorSequence` | Color over particle lifetime |
185
+ | `Transparency` | `NumberSequence` | Transparency over particle lifetime |
186
+ | `Texture` | `string` | Decal/image asset ID for particle appearance |
187
+ | `RotSpeed` | `NumberRange` | Rotation speed (degrees/second) |
188
+ | `Acceleration` | `Vector3` | Constant force (gravity = `Vector3.new(0,-10,0)`) |
189
+ | `Drag` | `number` | Air resistance (0 = none, higher = more drag) |
190
+ | `LightEmission` | `number` | 0-1, additive blending (1 = fully additive/glowy) |
191
+ | `LightInfluence`| `number` | 0-1, how much scene lighting affects particles |
192
+ | `ZOffset` | `number` | Render order offset toward/away from camera |
193
+ | `Orientation` | `Enum.ParticleOrientation` | FacingCamera, VelocityParallel, etc. |
194
+
195
+ ### NumberSequence and ColorSequence
196
+
197
+ ```luau
198
+ -- Size: start at 1, peak at 2 at midlife, shrink to 0
199
+ local sizeSeq = NumberSequence.new({
200
+ NumberSequenceKeypoint.new(0, 1),
201
+ NumberSequenceKeypoint.new(0.5, 2),
202
+ NumberSequenceKeypoint.new(1, 0),
203
+ })
204
+
205
+ -- Color: orange to red
206
+ local colorSeq = ColorSequence.new({
207
+ ColorSequenceKeypoint.new(0, Color3.fromRGB(255, 170, 0)),
208
+ ColorSequenceKeypoint.new(1, Color3.fromRGB(200, 30, 0)),
209
+ })
210
+
211
+ -- Transparency: fade in then fade out
212
+ local transSeq = NumberSequence.new({
213
+ NumberSequenceKeypoint.new(0, 1),
214
+ NumberSequenceKeypoint.new(0.1, 0),
215
+ NumberSequenceKeypoint.new(0.8, 0),
216
+ NumberSequenceKeypoint.new(1, 1),
217
+ })
218
+ ```
219
+
220
+ ### Common Effect Recipes
221
+
222
+ #### Fire
223
+
224
+ ```luau
225
+ local fire = Instance.new("ParticleEmitter")
226
+ fire.Rate = 80
227
+ fire.Lifetime = NumberRange.new(0.4, 0.8)
228
+ fire.Speed = NumberRange.new(3, 6)
229
+ fire.SpreadAngle = Vector2.new(15, 15)
230
+ fire.Size = NumberSequence.new({
231
+ NumberSequenceKeypoint.new(0, 0.5),
232
+ NumberSequenceKeypoint.new(0.3, 1.5),
233
+ NumberSequenceKeypoint.new(1, 0),
234
+ })
235
+ fire.Color = ColorSequence.new({
236
+ ColorSequenceKeypoint.new(0, Color3.fromRGB(255, 220, 50)),
237
+ ColorSequenceKeypoint.new(0.4, Color3.fromRGB(255, 100, 0)),
238
+ ColorSequenceKeypoint.new(1, Color3.fromRGB(100, 20, 0)),
239
+ })
240
+ fire.Transparency = NumberSequence.new({
241
+ NumberSequenceKeypoint.new(0, 0.3),
242
+ NumberSequenceKeypoint.new(1, 1),
243
+ })
244
+ fire.LightEmission = 1
245
+ fire.Acceleration = Vector3.new(0, 4, 0)
246
+ fire.Texture = "rbxasset://textures/particles/fire_main.dds"
247
+ fire.Parent = somePart
248
+ ```
249
+
250
+ #### Smoke
251
+
252
+ ```luau
253
+ local smoke = Instance.new("ParticleEmitter")
254
+ smoke.Rate = 30
255
+ smoke.Lifetime = NumberRange.new(2, 4)
256
+ smoke.Speed = NumberRange.new(1, 3)
257
+ smoke.SpreadAngle = Vector2.new(30, 30)
258
+ smoke.Size = NumberSequence.new({
259
+ NumberSequenceKeypoint.new(0, 1),
260
+ NumberSequenceKeypoint.new(1, 5),
261
+ })
262
+ smoke.Color = ColorSequence.new(Color3.fromRGB(120, 120, 120))
263
+ smoke.Transparency = NumberSequence.new({
264
+ NumberSequenceKeypoint.new(0, 0.5),
265
+ NumberSequenceKeypoint.new(1, 1),
266
+ })
267
+ smoke.RotSpeed = NumberRange.new(-30, 30)
268
+ smoke.Acceleration = Vector3.new(0, 2, 0)
269
+ smoke.LightInfluence = 1
270
+ smoke.Parent = somePart
271
+ ```
272
+
273
+ #### Sparkles / Magic Particles
274
+
275
+ ```luau
276
+ local sparkle = Instance.new("ParticleEmitter")
277
+ sparkle.Rate = 40
278
+ sparkle.Lifetime = NumberRange.new(0.5, 1.2)
279
+ sparkle.Speed = NumberRange.new(2, 5)
280
+ sparkle.SpreadAngle = Vector2.new(180, 180)
281
+ sparkle.Size = NumberSequence.new({
282
+ NumberSequenceKeypoint.new(0, 0.3),
283
+ NumberSequenceKeypoint.new(0.5, 0.6),
284
+ NumberSequenceKeypoint.new(1, 0),
285
+ })
286
+ sparkle.Color = ColorSequence.new({
287
+ ColorSequenceKeypoint.new(0, Color3.fromRGB(200, 200, 255)),
288
+ ColorSequenceKeypoint.new(1, Color3.fromRGB(100, 100, 255)),
289
+ })
290
+ sparkle.LightEmission = 1
291
+ sparkle.Texture = "rbxasset://textures/particles/sparkles_main.dds"
292
+ sparkle.Parent = somePart
293
+ ```
294
+
295
+ #### Rain
296
+
297
+ ```luau
298
+ local rain = Instance.new("ParticleEmitter")
299
+ rain.Rate = 300
300
+ rain.Lifetime = NumberRange.new(0.8, 1.2)
301
+ rain.Speed = NumberRange.new(40, 60)
302
+ rain.SpreadAngle = Vector2.new(5, 5)
303
+ rain.Size = NumberSequence.new(0.05)
304
+ rain.Color = ColorSequence.new(Color3.fromRGB(180, 200, 220))
305
+ rain.Transparency = NumberSequence.new(0.4)
306
+ rain.Acceleration = Vector3.new(0, -80, 0)
307
+ rain.Drag = 0
308
+ rain.Orientation = Enum.ParticleOrientation.VelocityParallel
309
+ rain.Parent = largeCoverPart -- position above the play area
310
+ ```
311
+
312
+ #### Snow
313
+
314
+ ```luau
315
+ local snow = Instance.new("ParticleEmitter")
316
+ snow.Rate = 100
317
+ snow.Lifetime = NumberRange.new(4, 7)
318
+ snow.Speed = NumberRange.new(1, 3)
319
+ snow.SpreadAngle = Vector2.new(60, 60)
320
+ snow.Size = NumberSequence.new({
321
+ NumberSequenceKeypoint.new(0, 0.1),
322
+ NumberSequenceKeypoint.new(1, 0.15),
323
+ })
324
+ snow.Color = ColorSequence.new(Color3.new(1, 1, 1))
325
+ snow.Transparency = NumberSequence.new({
326
+ NumberSequenceKeypoint.new(0, 0),
327
+ NumberSequenceKeypoint.new(0.8, 0),
328
+ NumberSequenceKeypoint.new(1, 1),
329
+ })
330
+ snow.Acceleration = Vector3.new(0, -2, 0)
331
+ snow.RotSpeed = NumberRange.new(-60, 60)
332
+ snow.Drag = 3
333
+ snow.Parent = largeCoverPart
334
+ ```
335
+
336
+ #### Magic Aura (orbiting particles)
337
+
338
+ ```luau
339
+ local aura = Instance.new("ParticleEmitter")
340
+ aura.Rate = 25
341
+ aura.Lifetime = NumberRange.new(1, 2)
342
+ aura.Speed = NumberRange.new(0.5, 1.5)
343
+ aura.SpreadAngle = Vector2.new(180, 180)
344
+ aura.Size = NumberSequence.new({
345
+ NumberSequenceKeypoint.new(0, 0),
346
+ NumberSequenceKeypoint.new(0.3, 0.8),
347
+ NumberSequenceKeypoint.new(1, 0),
348
+ })
349
+ aura.Color = ColorSequence.new({
350
+ ColorSequenceKeypoint.new(0, Color3.fromRGB(100, 0, 255)),
351
+ ColorSequenceKeypoint.new(1, Color3.fromRGB(200, 50, 255)),
352
+ })
353
+ aura.Transparency = NumberSequence.new({
354
+ NumberSequenceKeypoint.new(0, 1),
355
+ NumberSequenceKeypoint.new(0.2, 0.2),
356
+ NumberSequenceKeypoint.new(1, 1),
357
+ })
358
+ aura.LightEmission = 1
359
+ aura.RotSpeed = NumberRange.new(-90, 90)
360
+ aura.Drag = 5
361
+ aura.Parent = character.HumanoidRootPart
362
+ ```
363
+
364
+ ---
365
+
366
+ ## Beam and Trail
367
+
368
+ ### Beam
369
+
370
+ A `Beam` renders a textured ribbon between two `Attachment` instances. Perfect for lasers, lightning, tethers, and energy connections.
371
+
372
+ ```luau
373
+ -- Setup: two parts with attachments
374
+ local att0 = Instance.new("Attachment")
375
+ att0.Parent = partA
376
+
377
+ local att1 = Instance.new("Attachment")
378
+ att1.Parent = partB
379
+
380
+ local beam = Instance.new("Beam")
381
+ beam.Attachment0 = att0
382
+ beam.Attachment1 = att1
383
+ beam.Width0 = 0.5
384
+ beam.Width1 = 0.5
385
+ beam.Color = ColorSequence.new(Color3.fromRGB(0, 150, 255))
386
+ beam.Transparency = NumberSequence.new({
387
+ NumberSequenceKeypoint.new(0, 0),
388
+ NumberSequenceKeypoint.new(1, 0.5),
389
+ })
390
+ beam.LightEmission = 1
391
+ beam.FaceCamera = true
392
+ beam.Segments = 20 -- more segments = smoother curves
393
+ beam.CurveSize0 = 2 -- bend near Attachment0
394
+ beam.CurveSize1 = -2 -- bend near Attachment1
395
+ beam.TextureLength = 1
396
+ beam.TextureSpeed = 1 -- scrolling texture
397
+ beam.Texture = "rbxassetid://123456789"
398
+ beam.Parent = partA
399
+ ```
400
+
401
+ ### Key Beam Properties
402
+
403
+ | Property | Description |
404
+ | ---------------- | -------------------------------------------------------- |
405
+ | `Attachment0/1` | Start and end points |
406
+ | `Width0/1` | Width at each attachment (studs) |
407
+ | `Color` | `ColorSequence` along the beam length |
408
+ | `Transparency` | `NumberSequence` along the beam length |
409
+ | `CurveSize0/1` | Bezier curve magnitude at each end |
410
+ | `Segments` | Number of straight segments (more = smoother curves) |
411
+ | `FaceCamera` | Always faces the camera for billboard effect |
412
+ | `TextureSpeed` | Scrolls the texture along the beam |
413
+ | `LightEmission` | Additive blending for glow |
414
+
415
+ ### Trail
416
+
417
+ A `Trail` renders a ribbon behind a moving part. Requires two `Attachment` instances on the same part (defining the trail's width axis).
418
+
419
+ ```luau
420
+ local part = workspace.Sword.Blade
421
+
422
+ local att0 = Instance.new("Attachment")
423
+ att0.Position = Vector3.new(0, 0, -2) -- base of blade
424
+ att0.Parent = part
425
+
426
+ local att1 = Instance.new("Attachment")
427
+ att1.Position = Vector3.new(0, 0, 2) -- tip of blade
428
+ att1.Parent = part
429
+
430
+ local trail = Instance.new("Trail")
431
+ trail.Attachment0 = att0
432
+ trail.Attachment1 = att1
433
+ trail.Lifetime = 0.3 -- how long segments persist
434
+ trail.MinLength = 0.05 -- minimum distance before new segment
435
+ trail.FaceCamera = true
436
+ trail.Color = ColorSequence.new({
437
+ ColorSequenceKeypoint.new(0, Color3.fromRGB(255, 255, 255)),
438
+ ColorSequenceKeypoint.new(1, Color3.fromRGB(100, 100, 255)),
439
+ })
440
+ trail.Transparency = NumberSequence.new({
441
+ NumberSequenceKeypoint.new(0, 0),
442
+ NumberSequenceKeypoint.new(1, 1),
443
+ })
444
+ trail.LightEmission = 0.8
445
+ trail.WidthScale = NumberSequence.new(1)
446
+ trail.Parent = part
447
+ ```
448
+
449
+ ### Common Uses
450
+
451
+ - **Laser beams**: `Beam` between gun barrel attachment and hit-point attachment.
452
+ - **Sword trails**: `Trail` on blade with short `Lifetime` (0.2-0.4s).
453
+ - **Magic effects**: `Beam` with high `CurveSize` values and scrolling texture for arcane tethers.
454
+ - **Lightning**: `Beam` with many `Segments`, rapidly randomizing `CurveSize0/1` each frame.
455
+
456
+ ---
457
+
458
+ ### Parent Destruction Behavior
459
+
460
+ When a `Part` or `Model` containing effects is destroyed (`:Destroy()`, player leave, workspace clear), all child `ParticleEmitter`, `Trail`, `Beam`, and `Attachment` are destroyed instantly. Active particles vanish mid-flight, trails cut off, beams disappear.
461
+
462
+ **Solution - Debris + temporary holder:** If you need an effect to finish gracefully after its parent is gone, reparent it to a temporary part and let `Debris` clean up.
463
+
464
+ ```luau
465
+ local Debris = game:GetService("Debris")
466
+
467
+ local function destroyWithGrace(effect: Instance, parent: Instance, gracePeriod: number)
468
+ -- Create a temporary invisible holder
469
+ local holder = Instance.new("Part")
470
+ holder.Anchored = true
471
+ holder.CanCollide = false
472
+ holder.Transparency = 1
473
+ holder.Size = Vector3.one
474
+ holder.Parent = workspace
475
+
476
+ -- Reparent the effect so it survives the original parent
477
+ effect.Parent = holder
478
+
479
+ -- Destroy the original parent (effect is now safe)
480
+ parent:Destroy()
481
+
482
+ -- Debris cleans up the holder + effect after the grace period
483
+ Debris:AddItem(holder, gracePeriod)
484
+ end
485
+
486
+ -- Example: Trail with 1s lifetime - give it 1.1s to fade out cleanly
487
+ local trail = -- ... setup trail on a sword part ...
488
+ destroyWithGrace(trail, swordPart, 1.1)
489
+ ```
490
+
491
+ For `Trail` specifically, set `Debris:AddItem(holder, trail.Lifetime + 0.1)` so the trail's existing segments finish rendering before cleanup.
492
+
493
+ ---
494
+
495
+ ## Highlight
496
+
497
+ A `Highlight` instance draws a colored outline around a `BasePart` or `Model` to call attention to it. Every highlight has two layers: an **outline** (silhouette edge) and an **interior** (overlay fill), each independently customizable.
498
+
499
+ ### Basic Usage
500
+
501
+ ```luau
502
+ local highlight = Instance.new("Highlight")
503
+ highlight.Adornee = targetPart
504
+ highlight.FillColor = Color3.fromRGB(255, 50, 50)
505
+ highlight.FillTransparency = 0.3
506
+ highlight.OutlineColor = Color3.new(1, 1, 1)
507
+ highlight.Parent = targetPart
508
+ ```
509
+
510
+ ### Properties
511
+
512
+ | Property | Type | Default | Description |
513
+ |----------|------|---------|-------------|
514
+ | `Adornee` | Instance | - | The `BasePart` or `Model` to highlight |
515
+ | `DepthMode` | Enum.HighlightDepthMode | `AlwaysOnTop` | `AlwaysOnTop` = visible through objects, `Occluded` = hidden by obstructions |
516
+ | `Enabled` | boolean | `true` | Toggle visibility |
517
+ | `FillColor` | Color3 | `[255, 200, 50]` | Interior overlay color |
518
+ | `FillTransparency` | number | `0.5` | 0 = opaque, 1 = invisible |
519
+ | `OutlineColor` | Color3 | `[255, 255, 255]` | Edge outline color |
520
+ | `OutlineTransparency` | number | `0` | 0 = opaque, 1 = invisible |
521
+
522
+ ### Common Pattern - Team Highlight
523
+
524
+ ```luau
525
+ local function addTeamHighlight(character: Model, teamColor: Color3)
526
+ local hl = Instance.new("Highlight")
527
+ hl.Adornee = character
528
+ hl.FillColor = teamColor
529
+ hl.FillTransparency = 0.6
530
+ hl.OutlineColor = teamColor
531
+ hl.DepthMode = Enum.HighlightDepthMode.AlwaysOnTop
532
+ hl.Parent = character
533
+ end
534
+ ```
535
+
536
+ ### Limitations
537
+
538
+ - **Max 255 simultaneous** Highlight instances per client. Excess instances are silently ignored.
539
+ - Disabled highlights still count toward the 255 limit - `:Destroy()` instead of `Enabled = false` if permanently unused.
540
+ - The `Highlight` itself is **not** destroyed when its `Adornee` is destroyed. Clean up manually.
541
+
542
+ ### Cleanup on Adornee Destroyed
543
+
544
+ ```luau
545
+ local function attachHighlight(adornee: Instance): Highlight
546
+ local hl = Instance.new("Highlight")
547
+ hl.Adornee = adornee
548
+ hl.Parent = adornee
549
+
550
+ adornee.AncestryChanged:Connect(function()
551
+ if not adornee:IsDescendantOf(game) then
552
+ hl:Destroy()
553
+ end
554
+ end)
555
+ return hl
556
+ end
557
+ ```
558
+
559
+ ---
560
+
561
+ ## TweenService for VFX
562
+
563
+ `TweenService` interpolates any numeric or color property over time. It is the backbone of procedural visual feedback.
564
+
565
+ ### TweenInfo
566
+
567
+ ```luau
568
+ local TweenService = game:GetService("TweenService")
569
+
570
+ local info = TweenInfo.new(
571
+ 0.5, -- Duration (seconds)
572
+ Enum.EasingStyle.Quad, -- Easing style
573
+ Enum.EasingDirection.Out, -- Easing direction
574
+ 0, -- RepeatCount (0 = no repeat, -1 = infinite)
575
+ false, -- Reverses (plays backward after forward)
576
+ 0 -- DelayTime (seconds before starting)
577
+ )
578
+ ```
579
+
580
+ ### Common Easing Styles
581
+
582
+ | Style | Feel |
583
+ | ----------- | --------------------------------------- |
584
+ | `Linear` | Constant speed, mechanical |
585
+ | `Quad` | Gentle acceleration/deceleration |
586
+ | `Cubic` | Stronger ease |
587
+ | `Quart` | Even stronger |
588
+ | `Sine` | Smooth, organic |
589
+ | `Back` | Overshoots then settles |
590
+ | `Bounce` | Bounces at the end |
591
+ | `Elastic` | Springy overshoot |
592
+ | `Exponential` | Very sharp acceleration |
593
+
594
+ ### Tweening Part Properties
595
+
596
+ ```luau
597
+ -- Flash on hit: turn white then revert
598
+ local function flashPart(part: BasePart, originalColor: Color3)
599
+ part.Color = Color3.new(1, 1, 1) -- instant white
600
+ local tweenBack = TweenService:Create(part, TweenInfo.new(0.3, Enum.EasingStyle.Quad), {
601
+ Color = originalColor,
602
+ })
603
+ tweenBack:Play()
604
+ end
605
+ ```
606
+
607
+ ```luau
608
+ -- Pulse effect: scale up then back
609
+ local function pulse(part: BasePart)
610
+ local originalSize = part.Size
611
+ local tweenGrow = TweenService:Create(part, TweenInfo.new(0.15, Enum.EasingStyle.Back, Enum.EasingDirection.Out), {
612
+ Size = originalSize * 1.3,
613
+ })
614
+ local tweenShrink = TweenService:Create(part, TweenInfo.new(0.2, Enum.EasingStyle.Quad, Enum.EasingDirection.In), {
615
+ Size = originalSize,
616
+ })
617
+ tweenGrow:Play()
618
+ tweenGrow.Completed:Connect(function()
619
+ tweenShrink:Play()
620
+ end)
621
+ end
622
+ ```
623
+
624
+ ```luau
625
+ -- Grow and fade out (explosion ring)
626
+ local function expandAndFade(part: BasePart)
627
+ local tween = TweenService:Create(part, TweenInfo.new(0.6, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), {
628
+ Size = part.Size * 5,
629
+ Transparency = 1,
630
+ })
631
+ tween:Play()
632
+ tween.Completed:Connect(function()
633
+ part:Destroy()
634
+ end)
635
+ end
636
+ ```
637
+
638
+ ```luau
639
+ -- Color transition (damage indicator)
640
+ local function colorTransition(part: BasePart, targetColor: Color3, duration: number)
641
+ local tween = TweenService:Create(part, TweenInfo.new(duration, Enum.EasingStyle.Sine), {
642
+ Color = targetColor,
643
+ })
644
+ tween:Play()
645
+ end
646
+ ```
647
+
648
+ ### Chaining Tweens
649
+
650
+ Use the `Completed` event to sequence tweens without coroutines:
651
+
652
+ ```luau
653
+ local function chainedEffect(part: BasePart)
654
+ local step1 = TweenService:Create(part, TweenInfo.new(0.2), { Transparency = 0.5 })
655
+ local step2 = TweenService:Create(part, TweenInfo.new(0.3), { Size = part.Size * 2 })
656
+ local step3 = TweenService:Create(part, TweenInfo.new(0.2), { Transparency = 1 })
657
+
658
+ step1:Play()
659
+ step1.Completed:Connect(function()
660
+ step2:Play()
661
+ end)
662
+ step2.Completed:Connect(function()
663
+ step3:Play()
664
+ end)
665
+ step3.Completed:Connect(function()
666
+ part:Destroy()
667
+ end)
668
+ end
669
+ ```
670
+
671
+ ---
672
+
673
+ ## Lighting Effects
674
+
675
+ ### Dynamic Lights
676
+
677
+ ```luau
678
+ -- PointLight: omnidirectional, good for torches, explosions
679
+ local pointLight = Instance.new("PointLight")
680
+ pointLight.Brightness = 2
681
+ pointLight.Color = Color3.fromRGB(255, 180, 50)
682
+ pointLight.Range = 20
683
+ pointLight.Shadows = true
684
+ pointLight.Parent = torchPart
685
+
686
+ -- SpotLight: directional cone, good for flashlights, spotlights
687
+ local spotLight = Instance.new("SpotLight")
688
+ spotLight.Brightness = 3
689
+ spotLight.Color = Color3.new(1, 1, 1)
690
+ spotLight.Range = 40
691
+ spotLight.Angle = 30 -- cone half-angle in degrees
692
+ spotLight.Face = Enum.NormalId.Front
693
+ spotLight.Parent = flashlightPart
694
+
695
+ -- SurfaceLight: emits from a surface, good for screens, signs
696
+ local surfLight = Instance.new("SurfaceLight")
697
+ surfLight.Brightness = 1
698
+ surfLight.Color = Color3.fromRGB(0, 200, 255)
699
+ surfLight.Range = 10
700
+ surfLight.Face = Enum.NormalId.Front
701
+ surfLight.Parent = screenPart
702
+ ```
703
+
704
+ ### Post-Processing Effects
705
+
706
+ All post-processing objects go in `Lighting` or `Camera`.
707
+
708
+ #### Atmosphere
709
+
710
+ ```luau
711
+ local atmo = Instance.new("Atmosphere")
712
+ atmo.Density = 0.3 -- 0-1, how thick the atmosphere is
713
+ atmo.Offset = 0.25 -- shifts haze up/down
714
+ atmo.Color = Color3.fromRGB(200, 210, 230) -- scatter color
715
+ atmo.Decay = Color3.fromRGB(120, 140, 170) -- far-away color
716
+ atmo.Glare = 0.2 -- sun glare intensity
717
+ atmo.Haze = 1.5 -- haze amount
718
+ atmo.Parent = game.Lighting
719
+ ```
720
+
721
+ #### ColorCorrection
722
+
723
+ ```luau
724
+ local cc = Instance.new("ColorCorrectionEffect")
725
+ cc.Brightness = 0.05 -- -1 to 1
726
+ cc.Contrast = 0.1 -- -1 to 1
727
+ cc.Saturation = 0.15 -- -1 to 1
728
+ cc.TintColor = Color3.new(1, 0.95, 0.9) -- warm tint
729
+ cc.Parent = game.Lighting
730
+ ```
731
+
732
+ #### Bloom
733
+
734
+ ```luau
735
+ local bloom = Instance.new("BloomEffect")
736
+ bloom.Intensity = 0.8 -- glow strength
737
+ bloom.Size = 24 -- glow spread (pixels)
738
+ bloom.Threshold = 1.2 -- brightness threshold to bloom
739
+ bloom.Parent = game.Lighting
740
+ ```
741
+
742
+ #### DepthOfField
743
+
744
+ ```luau
745
+ local dof = Instance.new("DepthOfFieldEffect")
746
+ dof.FarIntensity = 0.3 -- blur intensity far from focus
747
+ dof.FocusDistance = 30 -- distance in studs to focus point
748
+ dof.InFocusRadius = 20 -- radius of sharp area
749
+ dof.NearIntensity = 0.2 -- blur intensity near camera
750
+ dof.Parent = game.Lighting
751
+ ```
752
+
753
+ #### SunRays
754
+
755
+ ```luau
756
+ local sunRays = Instance.new("SunRaysEffect")
757
+ sunRays.Intensity = 0.15 -- ray visibility
758
+ sunRays.Spread = 0.8 -- how far rays extend
759
+ sunRays.Parent = game.Lighting
760
+ ```
761
+
762
+ ### Setting Mood with Global Lighting
763
+
764
+ ```luau
765
+ local lighting = game.Lighting
766
+
767
+ -- Daytime bright
768
+ lighting.ClockTime = 14
769
+ lighting.Brightness = 2
770
+ lighting.Ambient = Color3.fromRGB(140, 140, 140)
771
+ lighting.OutdoorAmbient = Color3.fromRGB(130, 130, 130)
772
+
773
+ -- Nighttime spooky
774
+ lighting.ClockTime = 0
775
+ lighting.Brightness = 0.5
776
+ lighting.Ambient = Color3.fromRGB(20, 20, 40)
777
+ lighting.OutdoorAmbient = Color3.fromRGB(10, 10, 30)
778
+ lighting.FogEnd = 200
779
+ lighting.FogColor = Color3.fromRGB(15, 15, 30)
780
+ ```
781
+
782
+ ---
783
+
784
+ ## Sound + Animation Sync
785
+
786
+ For general sound (SoundService, positional audio, SoundGroups), use mcp-roblox-docs. This section covers only the animation-specific pattern.
787
+
788
+ ### Triggering Sounds with Animation Events
789
+
790
+
791
+ ```luau
792
+ -- Sync a footstep sound to walk animation markers
793
+ walkTrack:GetMarkerReachedSignal("Footstep"):Connect(function()
794
+ local footstep = Instance.new("Sound")
795
+ footstep.SoundId = "rbxassetid://112233445"
796
+ footstep.Volume = 0.5
797
+ footstep.PlaybackSpeed = 0.9 + math.random() * 0.2 -- slight variation
798
+ footstep.Parent = character.HumanoidRootPart
799
+ footstep:Play()
800
+ footstep.Ended:Connect(function()
801
+ footstep:Destroy()
802
+ end)
803
+ end)
804
+ ```
805
+
806
+ ---
807
+
808
+ ## Camera Effects
809
+
810
+ ### Camera Shake
811
+
812
+ ```luau
813
+ local camera = workspace.CurrentCamera
814
+ local RunService = game:GetService("RunService")
815
+
816
+ local function shakeCamera(intensity: number, duration: number)
817
+ local elapsed = 0
818
+ local originalCFrame = camera.CFrame
819
+ local connection: RBXScriptConnection
820
+
821
+ connection = RunService.RenderStepped:Connect(function(dt: number)
822
+ elapsed += dt
823
+ if elapsed >= duration then
824
+ connection:Disconnect()
825
+ -- Camera returns to normal since CameraType is usually
826
+ -- "Custom" (player-controlled)
827
+ return
828
+ end
829
+
830
+ local progress = 1 - (elapsed / duration) -- decay over time
831
+ local shakeX = (math.random() - 0.5) * 2 * intensity * progress
832
+ local shakeY = (math.random() - 0.5) * 2 * intensity * progress
833
+ local shakeZ = (math.random() - 0.5) * 2 * intensity * progress
834
+
835
+ camera.CFrame = camera.CFrame * CFrame.new(shakeX, shakeY, shakeZ)
836
+ end)
837
+ end
838
+
839
+ -- Usage: moderate shake for 0.3 seconds
840
+ shakeCamera(0.5, 0.3)
841
+ ```
842
+
843
+ ### Zoom Effect
844
+
845
+ ```luau
846
+ local TweenService = game:GetService("TweenService")
847
+ local camera = workspace.CurrentCamera
848
+
849
+ local function zoomCamera(targetFOV: number, duration: number)
850
+ local tween = TweenService:Create(camera, TweenInfo.new(duration, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), {
851
+ FieldOfView = targetFOV,
852
+ })
853
+ tween:Play()
854
+ return tween
855
+ end
856
+
857
+ -- Zoom in for dramatic moment
858
+ local zoomIn = zoomCamera(40, 0.5)
859
+ zoomIn.Completed:Connect(function()
860
+ task.wait(1)
861
+ zoomCamera(70, 0.8) -- zoom back to normal
862
+ end)
863
+ ```
864
+
865
+ ### Focus / CFrame Lerp
866
+
867
+ ```luau
868
+ local function focusOnPoint(targetCFrame: CFrame, duration: number)
869
+ camera.CameraType = Enum.CameraType.Scriptable
870
+
871
+ local startCFrame = camera.CFrame
872
+ local elapsed = 0
873
+ local connection: RBXScriptConnection
874
+
875
+ connection = RunService.RenderStepped:Connect(function(dt: number)
876
+ elapsed += dt
877
+ local alpha = math.clamp(elapsed / duration, 0, 1)
878
+ -- Smooth step for natural feel
879
+ local smoothAlpha = alpha * alpha * (3 - 2 * alpha)
880
+ camera.CFrame = startCFrame:Lerp(targetCFrame, smoothAlpha)
881
+
882
+ if alpha >= 1 then
883
+ connection:Disconnect()
884
+ end
885
+ end)
886
+ end
887
+ ```
888
+
889
+ ### Cutscene Waypoint System
890
+
891
+ ```luau
892
+ type CutsceneWaypoint = {
893
+ cframe: CFrame,
894
+ duration: number,
895
+ easingStyle: Enum.EasingStyle?,
896
+ holdTime: number?, -- pause at this point before moving on
897
+ }
898
+
899
+ local function playCutscene(waypoints: { CutsceneWaypoint })
900
+ camera.CameraType = Enum.CameraType.Scriptable
901
+
902
+ for i, waypoint in waypoints do
903
+ local style = waypoint.easingStyle or Enum.EasingStyle.Quad
904
+ local info = TweenInfo.new(waypoint.duration, style, Enum.EasingDirection.InOut)
905
+
906
+ local tween = TweenService:Create(camera, info, {
907
+ CFrame = waypoint.cframe,
908
+ })
909
+ tween:Play()
910
+ tween.Completed:Wait()
911
+
912
+ if waypoint.holdTime and waypoint.holdTime > 0 then
913
+ task.wait(waypoint.holdTime)
914
+ end
915
+ end
916
+
917
+ -- Return camera control to player
918
+ camera.CameraType = Enum.CameraType.Custom
919
+ end
920
+
921
+ -- Usage
922
+ playCutscene({
923
+ { cframe = CFrame.new(0, 50, 0) * CFrame.Angles(math.rad(-90), 0, 0), duration = 2, holdTime = 1 },
924
+ { cframe = CFrame.new(20, 10, 20) * CFrame.lookAt(Vector3.new(20, 10, 20), Vector3.zero), duration = 3 },
925
+ { cframe = camera.CFrame, duration = 1.5, easingStyle = Enum.EasingStyle.Sine },
926
+ })
927
+ ```
928
+
929
+ ---
930
+
931
+ ## Best Practices
932
+
933
+ ### Performance Budgets
934
+
935
+ - **Max ~200 particles per emitter** -- more than this and you risk frame drops, especially on mobile.
936
+ - **Limit total active emitters** -- aim for fewer than 20-30 active emitters visible at once in any scene.
937
+ - **Particle texture size** -- keep textures small (64x64 or 128x128 PNG). Avoid large or high-res particle textures.
938
+ - **Beams** -- keep `Segments` reasonable (10-30). Very high segment counts cost draw calls.
939
+ - **Tweens** -- hundreds of simultaneous tweens are fine; thousands may cause issues. Cancel/destroy tweens when no longer needed.
940
+ - **Sounds** -- limit simultaneous playing sounds to ~20-30. Destroy one-shot sounds after `Ended`.
941
+
942
+ ### Disable Effects on Low-End Devices
943
+
944
+ ```luau
945
+ local function getQualityLevel(): string
946
+ local quality = UserSettings().GameSettings.SavedQualityLevel
947
+ -- quality is Enum.SavedQualitySetting or an int 1-10
948
+ if quality == Enum.SavedQualitySetting.Automatic then
949
+ -- Use a heuristic: check current graphics quality
950
+ local level = settings().Rendering.QualityLevel
951
+ if level <= 3 then return "low"
952
+ elseif level <= 6 then return "medium"
953
+ else return "high" end
954
+ end
955
+ local numQuality = quality.Value
956
+ if numQuality <= 3 then return "low"
957
+ elseif numQuality <= 6 then return "medium"
958
+ else return "high" end
959
+ end
960
+
961
+ local function applyQualitySettings()
962
+ local level = getQualityLevel()
963
+ if level == "low" then
964
+ -- Disable post-processing
965
+ for _, effect in game.Lighting:GetChildren() do
966
+ if effect:IsA("PostEffect") then
967
+ effect.Enabled = false
968
+ end
969
+ end
970
+ -- Reduce particle rates
971
+ -- Disable shadows on dynamic lights
972
+ end
973
+ end
974
+ ```
975
+
976
+ ### Pool VFX Objects
977
+
978
+ Avoid creating and destroying particles every frame. Pre-create a pool and enable/disable or reposition them.
979
+
980
+ ```luau
981
+ local VFXPool = {}
982
+ VFXPool.__index = VFXPool
983
+
984
+ function VFXPool.new(template: Instance, poolSize: number): typeof(setmetatable({}, VFXPool))
985
+ local self = setmetatable({}, VFXPool)
986
+ self._pool = table.create(poolSize)
987
+ self._available = table.create(poolSize)
988
+
989
+ for i = 1, poolSize do
990
+ local clone = template:Clone()
991
+ clone.Parent = workspace.VFXFolder
992
+ -- Disable all emitters
993
+ for _, emitter in clone:GetDescendants() do
994
+ if emitter:IsA("ParticleEmitter") then
995
+ emitter.Enabled = false
996
+ end
997
+ end
998
+ self._pool[i] = clone
999
+ self._available[i] = clone
1000
+ end
1001
+
1002
+ return self
1003
+ end
1004
+
1005
+ function VFXPool:get(): Instance?
1006
+ local obj = table.remove(self._available)
1007
+ return obj
1008
+ end
1009
+
1010
+ function VFXPool:release(obj: Instance)
1011
+ -- Reset position, disable emitters
1012
+ for _, emitter in obj:GetDescendants() do
1013
+ if emitter:IsA("ParticleEmitter") then
1014
+ emitter.Enabled = false
1015
+ end
1016
+ end
1017
+ table.insert(self._available, obj)
1018
+ end
1019
+ ```
1020
+
1021
+ ### Sync Sound with Visuals
1022
+
1023
+ - Use `MarkerReachedSignal` to trigger sounds at exact animation frames.
1024
+ - Play impact sounds at the moment of collision, not when the swing starts.
1025
+ - Match `PlaybackSpeed` to animation speed adjustments.
1026
+ - Use `task.delay` or tween `Completed` events for sequenced audio.
1027
+
1028
+ ---
1029
+
1030
+ ## Anti-Patterns
1031
+
1032
+ ### Unlimited Particles
1033
+
1034
+ ```luau
1035
+ -- BAD: unbounded particle creation with no cleanup
1036
+ RunService.Heartbeat:Connect(function()
1037
+ local emitter = Instance.new("ParticleEmitter")
1038
+ emitter.Rate = 500 -- extremely high rate
1039
+ emitter.Parent = somePart
1040
+ -- never destroyed, accumulates forever
1041
+ end)
1042
+
1043
+ -- GOOD: reuse a single emitter, burst when needed
1044
+ local emitter = Instance.new("ParticleEmitter")
1045
+ emitter.Rate = 0 -- manual emission only
1046
+ emitter.Parent = somePart
1047
+
1048
+ local function burstParticles(count: number)
1049
+ emitter:Emit(count)
1050
+ end
1051
+ ```
1052
+
1053
+ ### Unoptimized Particle Textures
1054
+
1055
+ ```luau
1056
+ -- BAD: 1024x1024 high-res texture for tiny particles
1057
+ emitter.Texture = "rbxassetid://huge_4k_texture"
1058
+
1059
+ -- GOOD: 64x64 or 128x128 simple shape on transparent background
1060
+ emitter.Texture = "rbxassetid://small_optimized_circle"
1061
+ -- Use LightEmission = 1 with simple shapes for clean glow effects
1062
+ ```
1063
+
1064
+ ### Synchronous Animation Loading Blocking Gameplay
1065
+
1066
+ ```luau
1067
+ -- BAD: loading animations in a hot path synchronously
1068
+ local function onAttack()
1069
+ local anim = Instance.new("Animation")
1070
+ anim.AnimationId = "rbxassetid://123456789"
1071
+ local track = animator:LoadAnimation(anim) -- may yield on first load
1072
+ track:Play()
1073
+ end
1074
+
1075
+ -- GOOD: preload animations at character spawn
1076
+ local attackAnim = Instance.new("Animation")
1077
+ attackAnim.AnimationId = "rbxassetid://123456789"
1078
+ local attackTrack: AnimationTrack -- forward declare
1079
+
1080
+ local function onCharacterAdded(character: Model)
1081
+ local animator = character:WaitForChild("Humanoid"):WaitForChild("Animator")
1082
+ attackTrack = animator:LoadAnimation(attackAnim)
1083
+ attackTrack.Priority = Enum.AnimationPriority.Action
1084
+ end
1085
+
1086
+ local function onAttack()
1087
+ if attackTrack then
1088
+ attackTrack:Play()
1089
+ end
1090
+ end
1091
+ ```
1092
+
1093
+ ### Other Anti-Patterns to Avoid
1094
+
1095
+ - **Tweening properties every frame via RenderStepped instead of using TweenService** -- TweenService is optimized internally and handles cleanup.
1096
+ - **Not disconnecting camera shake connections** -- leads to permanent jitter.
1097
+ - **Setting `Camera.CameraType` to `Scriptable` and forgetting to restore it** -- player loses control.
1098
+ - **Not cleaning up Highlight after its Adornee is destroyed** -- orphaned Highlights waste one of the 255 slots. Use `AncestryChanged` to auto-destroy.
1099
+ - **Destroying a part while effects are active** -- particle bursts, trails, and beams vanish instantly. Reparent to a temporary holder + Debris if you need graceful cleanup.
1100
+ - **Playing sounds without ever destroying them** -- memory leak. Always clean up one-shot sounds via `Ended`.
1101
+ - **Creating hundreds of PointLights with shadows enabled** -- massive performance hit. Use `Shadows = false` for most dynamic lights.
1102
+
1103
+ ---
1104
+
1105
+ ## Complete Hit Effect System
1106
+
1107
+ A production-ready system combining white flash, particle burst, sound stinger, and camera shake. Designed for client-side use (LocalScript or module required from a LocalScript).
1108
+
1109
+ ```luau
1110
+ --[[
1111
+ HitEffectSystem
1112
+ Combines visual and audio feedback for combat hit registration.
1113
+ Run on the CLIENT only (camera shake and local VFX).
1114
+ ]]
1115
+
1116
+ local TweenService = game:GetService("TweenService")
1117
+ local RunService = game:GetService("RunService")
1118
+
1119
+ local HitEffectSystem = {}
1120
+
1121
+ -- Configuration
1122
+ local DEFAULT_CONFIG = {
1123
+ -- Flash
1124
+ flashColor = Color3.new(1, 1, 1),
1125
+ flashDuration = 0.15,
1126
+ flashRevertDuration = 0.25,
1127
+
1128
+ -- Particles
1129
+ particleBurstCount = 20,
1130
+ particleLifetime = NumberRange.new(0.2, 0.5),
1131
+ particleSpeed = NumberRange.new(8, 15),
1132
+ particleSize = NumberSequence.new({
1133
+ NumberSequenceKeypoint.new(0, 0.5),
1134
+ NumberSequenceKeypoint.new(1, 0),
1135
+ }),
1136
+ particleColor = ColorSequence.new({
1137
+ ColorSequenceKeypoint.new(0, Color3.new(1, 1, 1)),
1138
+ ColorSequenceKeypoint.new(1, Color3.fromRGB(255, 200, 50)),
1139
+ }),
1140
+ particleTransparency = NumberSequence.new({
1141
+ NumberSequenceKeypoint.new(0, 0),
1142
+ NumberSequenceKeypoint.new(0.7, 0.3),
1143
+ NumberSequenceKeypoint.new(1, 1),
1144
+ }),
1145
+
1146
+ -- Sound
1147
+ hitSoundId = "rbxassetid://123456789", -- replace with your asset
1148
+ hitSoundVolume = 0.8,
1149
+ hitSoundPitchVariation = 0.15, -- random pitch +/- this amount
1150
+
1151
+ -- Camera shake
1152
+ shakeIntensity = 0.4,
1153
+ shakeDuration = 0.2,
1154
+ }
1155
+
1156
+ -- Pre-create a reusable particle emitter template
1157
+ local function createHitEmitterTemplate(config: typeof(DEFAULT_CONFIG)): ParticleEmitter
1158
+ local emitter = Instance.new("ParticleEmitter")
1159
+ emitter.Name = "HitBurst"
1160
+ emitter.Rate = 0 -- manual emission only
1161
+ emitter.Lifetime = config.particleLifetime
1162
+ emitter.Speed = config.particleSpeed
1163
+ emitter.SpreadAngle = Vector2.new(180, 180) -- omnidirectional burst
1164
+ emitter.Size = config.particleSize
1165
+ emitter.Color = config.particleColor
1166
+ emitter.Transparency = config.particleTransparency
1167
+ emitter.LightEmission = 1
1168
+ emitter.Drag = 5
1169
+ emitter.RotSpeed = NumberRange.new(-180, 180)
1170
+ return emitter
1171
+ end
1172
+
1173
+ -- Emitter cache: one emitter per part (lazily created)
1174
+ local emitterCache: { [BasePart]: ParticleEmitter } = {}
1175
+ local emitterTemplate: ParticleEmitter? = nil
1176
+
1177
+ local function getOrCreateEmitter(part: BasePart, config: typeof(DEFAULT_CONFIG)): ParticleEmitter
1178
+ if emitterCache[part] then
1179
+ return emitterCache[part]
1180
+ end
1181
+
1182
+ if not emitterTemplate then
1183
+ emitterTemplate = createHitEmitterTemplate(config)
1184
+ end
1185
+
1186
+ local emitter = emitterTemplate:Clone()
1187
+ emitter.Parent = part
1188
+ emitterCache[part] = emitter
1189
+
1190
+ -- Clean up if part is destroyed
1191
+ part.Destroying:Connect(function()
1192
+ emitterCache[part] = nil
1193
+ end)
1194
+
1195
+ return emitter
1196
+ end
1197
+
1198
+ --[[
1199
+ Flash the target part white and tween back to original color.
1200
+ ]]
1201
+ local function flashPart(part: BasePart, config: typeof(DEFAULT_CONFIG))
1202
+ local originalColor = part.Color
1203
+ part.Color = config.flashColor
1204
+
1205
+ local tweenBack = TweenService:Create(
1206
+ part,
1207
+ TweenInfo.new(config.flashRevertDuration, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),
1208
+ { Color = originalColor }
1209
+ )
1210
+ tweenBack:Play()
1211
+ end
1212
+
1213
+ --[[
1214
+ Emit a burst of particles from the hit location.
1215
+ ]]
1216
+ local function emitParticleBurst(part: BasePart, config: typeof(DEFAULT_CONFIG))
1217
+ local emitter = getOrCreateEmitter(part, config)
1218
+ emitter:Emit(config.particleBurstCount)
1219
+ end
1220
+
1221
+ --[[
1222
+ Play a one-shot hit sound with slight pitch variation.
1223
+ ]]
1224
+ local function playHitSound(part: BasePart, config: typeof(DEFAULT_CONFIG))
1225
+ local sound = Instance.new("Sound")
1226
+ sound.SoundId = config.hitSoundId
1227
+ sound.Volume = config.hitSoundVolume
1228
+ sound.PlaybackSpeed = 1 + (math.random() - 0.5) * 2 * config.hitSoundPitchVariation
1229
+ sound.RollOffMaxDistance = 80
1230
+ sound.RollOffMinDistance = 5
1231
+ sound.Parent = part
1232
+ sound:Play()
1233
+
1234
+ sound.Ended:Connect(function()
1235
+ sound:Destroy()
1236
+ end)
1237
+ end
1238
+
1239
+ --[[
1240
+ Apply a short screen shake that decays over time.
1241
+ ]]
1242
+ local function shakeCamera(config: typeof(DEFAULT_CONFIG))
1243
+ local camera = workspace.CurrentCamera
1244
+ if not camera then return end
1245
+
1246
+ local elapsed = 0
1247
+ local intensity = config.shakeIntensity
1248
+ local duration = config.shakeDuration
1249
+ local connection: RBXScriptConnection
1250
+
1251
+ connection = RunService.RenderStepped:Connect(function(dt: number)
1252
+ elapsed += dt
1253
+ if elapsed >= duration then
1254
+ connection:Disconnect()
1255
+ return
1256
+ end
1257
+
1258
+ local decay = 1 - (elapsed / duration)
1259
+ local offsetX = (math.random() - 0.5) * 2 * intensity * decay
1260
+ local offsetY = (math.random() - 0.5) * 2 * intensity * decay
1261
+
1262
+ camera.CFrame = camera.CFrame * CFrame.new(offsetX, offsetY, 0)
1263
+ end)
1264
+ end
1265
+
1266
+ --[[
1267
+ Main entry point: trigger the full hit effect on a target part.
1268
+
1269
+ @param targetPart The BasePart that was hit (e.g., a character limb or NPC body).
1270
+ @param overrides Optional table to override any DEFAULT_CONFIG values.
1271
+ ]]
1272
+ function HitEffectSystem.play(targetPart: BasePart, overrides: { [string]: any }?)
1273
+ -- Merge config with overrides
1274
+ local config = table.clone(DEFAULT_CONFIG)
1275
+ if overrides then
1276
+ for key, value in overrides do
1277
+ (config :: any)[key] = value
1278
+ end
1279
+ end
1280
+
1281
+ -- Fire all effects simultaneously
1282
+ flashPart(targetPart, config)
1283
+ emitParticleBurst(targetPart, config)
1284
+ playHitSound(targetPart, config)
1285
+ shakeCamera(config)
1286
+ end
1287
+
1288
+ --[[
1289
+ Clean up cached emitters (call when resetting scene or on player leave).
1290
+ ]]
1291
+ function HitEffectSystem.cleanup()
1292
+ for part, emitter in emitterCache do
1293
+ emitter:Destroy()
1294
+ end
1295
+ table.clear(emitterCache)
1296
+ end
1297
+
1298
+ return HitEffectSystem
1299
+
1300
+ --[[
1301
+ USAGE EXAMPLE (in a LocalScript):
1302
+
1303
+ local HitEffectSystem = require(script.Parent.HitEffectSystem)
1304
+
1305
+ -- When a hit is confirmed (e.g., via RemoteEvent from server):
1306
+ hitRemote.OnClientEvent:Connect(function(targetPart: BasePart)
1307
+ HitEffectSystem.play(targetPart)
1308
+ end)
1309
+
1310
+ -- Custom overrides for a critical hit:
1311
+ hitRemote.OnClientEvent:Connect(function(targetPart: BasePart, isCritical: boolean)
1312
+ if isCritical then
1313
+ HitEffectSystem.play(targetPart, {
1314
+ flashColor = Color3.fromRGB(255, 50, 50),
1315
+ particleBurstCount = 40,
1316
+ shakeIntensity = 0.8,
1317
+ shakeDuration = 0.4,
1318
+ hitSoundId = "rbxassetid://critical_hit_sound_id",
1319
+ })
1320
+ else
1321
+ HitEffectSystem.play(targetPart)
1322
+ end
1323
+ end)
1324
+ ]]
1325
+ ```