roblox-opencode 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/README.md +112 -122
  2. package/commands/setup-game.md +108 -108
  3. package/commands/sync-check.md +53 -53
  4. package/core/roblox-core.md +93 -93
  5. package/dist/server.js +189 -167
  6. package/package.json +35 -35
  7. package/skills/roblox-analytics/SKILL.md +277 -277
  8. package/skills/roblox-analytics/references/event-batcher.luau +75 -75
  9. package/skills/roblox-animation-vfx/SKILL.md +1325 -1325
  10. package/skills/roblox-architecture/SKILL.md +877 -863
  11. package/skills/roblox-architecture/references/combat-systems.md +1381 -1381
  12. package/skills/roblox-code-review/SKILL.md +686 -686
  13. package/skills/roblox-data/SKILL.md +889 -889
  14. package/skills/roblox-data/references/inventory-systems.md +1729 -1729
  15. package/skills/roblox-debug/SKILL.md +98 -98
  16. package/skills/roblox-gui/SKILL.md +1103 -1103
  17. package/skills/roblox-gui-fusion/SKILL.md +150 -150
  18. package/skills/roblox-gui-fusion/references/inventory.luau +427 -427
  19. package/skills/roblox-gui-fusion/references/settings-menu.luau +579 -579
  20. package/skills/roblox-gui-fusion/references/shop.luau +411 -411
  21. package/skills/roblox-luau-mastery/SKILL.md +1618 -1519
  22. package/skills/roblox-monetization/SKILL.md +1084 -1084
  23. package/skills/roblox-monetization/references/process-receipt.luau +131 -131
  24. package/skills/roblox-networking/SKILL.md +669 -669
  25. package/skills/roblox-networking/references/remote-validator.luau +193 -193
  26. package/skills/roblox-publish-checklist/SKILL.md +127 -127
  27. package/skills/roblox-runtime/SKILL.md +753 -753
  28. package/skills/roblox-sharp-edges/SKILL.md +294 -294
  29. package/skills/roblox-sync/SKILL.md +126 -126
  30. package/skills/roblox-testing/SKILL.md +943 -943
  31. package/skills/roblox-tooling/SKILL.md +149 -149
  32. package/vendor/LICENSES/ProfileStore-LICENSE +201 -201
  33. package/vendor/LICENSES/RbxUtil-LICENSE +7 -7
  34. package/vendor/LICENSES/promise-LICENSE +20 -20
  35. package/vendor/LICENSES/t-LICENSE +21 -21
  36. package/vendor/LICENSES/testez-LICENSE +200 -200
  37. package/vendor/README.md +83 -83
  38. package/vendor/fusion/Animation/ExternalTime.luau +83 -83
  39. package/vendor/fusion/Animation/Spring.luau +321 -321
  40. package/vendor/fusion/Animation/Stopwatch.luau +127 -127
  41. package/vendor/fusion/Animation/Tween.luau +187 -187
  42. package/vendor/fusion/Animation/getTweenDuration.luau +27 -27
  43. package/vendor/fusion/Animation/getTweenRatio.luau +47 -47
  44. package/vendor/fusion/Animation/lerpType.luau +163 -163
  45. package/vendor/fusion/Animation/packType.luau +99 -99
  46. package/vendor/fusion/Animation/springCoefficients.luau +80 -80
  47. package/vendor/fusion/Animation/unpackType.luau +102 -102
  48. package/vendor/fusion/Colour/Oklab.luau +70 -70
  49. package/vendor/fusion/Colour/sRGB.luau +54 -54
  50. package/vendor/fusion/External.luau +167 -167
  51. package/vendor/fusion/ExternalDebug.luau +69 -69
  52. package/vendor/fusion/Graph/Observer.luau +113 -113
  53. package/vendor/fusion/Graph/castToGraph.luau +28 -28
  54. package/vendor/fusion/Graph/change.luau +80 -80
  55. package/vendor/fusion/Graph/depend.luau +32 -32
  56. package/vendor/fusion/Graph/evaluate.luau +55 -55
  57. package/vendor/fusion/Instances/Attribute.luau +57 -57
  58. package/vendor/fusion/Instances/AttributeChange.luau +46 -46
  59. package/vendor/fusion/Instances/AttributeOut.luau +63 -63
  60. package/vendor/fusion/Instances/Child.luau +21 -21
  61. package/vendor/fusion/Instances/Children.luau +147 -147
  62. package/vendor/fusion/Instances/Hydrate.luau +32 -32
  63. package/vendor/fusion/Instances/New.luau +52 -52
  64. package/vendor/fusion/Instances/OnChange.luau +49 -49
  65. package/vendor/fusion/Instances/OnEvent.luau +53 -53
  66. package/vendor/fusion/Instances/Out.luau +69 -69
  67. package/vendor/fusion/Instances/applyInstanceProps.luau +148 -148
  68. package/vendor/fusion/Instances/defaultProps.luau +194 -194
  69. package/vendor/fusion/LICENSE +21 -21
  70. package/vendor/fusion/Logging/formatError.luau +48 -48
  71. package/vendor/fusion/Logging/messages.luau +51 -51
  72. package/vendor/fusion/Logging/parseError.luau +24 -24
  73. package/vendor/fusion/Memory/checkLifetime.luau +133 -133
  74. package/vendor/fusion/Memory/deriveScope.luau +23 -23
  75. package/vendor/fusion/Memory/deriveScopeImpl.luau +44 -44
  76. package/vendor/fusion/Memory/doCleanup.luau +78 -78
  77. package/vendor/fusion/Memory/innerScope.luau +33 -33
  78. package/vendor/fusion/Memory/legacyCleanup.luau +17 -17
  79. package/vendor/fusion/Memory/needsDestruction.luau +16 -16
  80. package/vendor/fusion/Memory/poisonScope.luau +33 -33
  81. package/vendor/fusion/Memory/scopePool.luau +54 -54
  82. package/vendor/fusion/Memory/scoped.luau +26 -26
  83. package/vendor/fusion/Memory/whichLivesLonger.luau +74 -74
  84. package/vendor/fusion/RobloxExternal.luau +97 -97
  85. package/vendor/fusion/State/Computed.luau +138 -138
  86. package/vendor/fusion/State/For/Disassembly.luau +210 -210
  87. package/vendor/fusion/State/For/ForTypes.luau +30 -30
  88. package/vendor/fusion/State/For/init.luau +109 -109
  89. package/vendor/fusion/State/ForKeys.luau +93 -93
  90. package/vendor/fusion/State/ForPairs.luau +96 -96
  91. package/vendor/fusion/State/ForValues.luau +93 -93
  92. package/vendor/fusion/State/Value.luau +87 -87
  93. package/vendor/fusion/State/castToState.luau +25 -25
  94. package/vendor/fusion/State/peek.luau +30 -30
  95. package/vendor/fusion/Types.luau +314 -314
  96. package/vendor/fusion/Utility/Contextual.luau +90 -90
  97. package/vendor/fusion/Utility/Safe.luau +22 -22
  98. package/vendor/fusion/Utility/isSimilar.luau +29 -29
  99. package/vendor/fusion/Utility/merge.luau +35 -35
  100. package/vendor/fusion/Utility/nameOf.luau +34 -34
  101. package/vendor/fusion/Utility/never.luau +13 -13
  102. package/vendor/fusion/Utility/nicknames.luau +10 -10
  103. package/vendor/fusion/Utility/xtypeof.luau +26 -26
  104. package/vendor/fusion/init.luau +82 -82
  105. package/vendor/profilestore/init.luau +2242 -2242
  106. package/vendor/promise/init.luau +1982 -1982
  107. package/vendor/rbxutil/buffer-util/Buffer.test.luau +25 -25
  108. package/vendor/rbxutil/buffer-util/BufferReader.luau +228 -228
  109. package/vendor/rbxutil/buffer-util/BufferWriter.luau +269 -269
  110. package/vendor/rbxutil/buffer-util/DataTypeBuffer.luau +223 -223
  111. package/vendor/rbxutil/buffer-util/Types.luau +60 -60
  112. package/vendor/rbxutil/buffer-util/index.d.ts +153 -153
  113. package/vendor/rbxutil/buffer-util/init.luau +41 -41
  114. package/vendor/rbxutil/buffer-util/package.json +16 -16
  115. package/vendor/rbxutil/buffer-util/wally.toml +9 -9
  116. package/vendor/rbxutil/comm/Client/ClientComm.luau +232 -232
  117. package/vendor/rbxutil/comm/Client/ClientRemoteProperty.luau +156 -156
  118. package/vendor/rbxutil/comm/Client/ClientRemoteSignal.luau +109 -109
  119. package/vendor/rbxutil/comm/Client/init.luau +135 -135
  120. package/vendor/rbxutil/comm/Server/RemoteProperty.luau +295 -295
  121. package/vendor/rbxutil/comm/Server/RemoteSignal.luau +211 -211
  122. package/vendor/rbxutil/comm/Server/ServerComm.luau +211 -211
  123. package/vendor/rbxutil/comm/Server/init.luau +140 -140
  124. package/vendor/rbxutil/comm/Types.luau +18 -18
  125. package/vendor/rbxutil/comm/Util.luau +27 -27
  126. package/vendor/rbxutil/comm/init.luau +35 -35
  127. package/vendor/rbxutil/comm/wally.toml +13 -13
  128. package/vendor/rbxutil/component/init.luau +759 -759
  129. package/vendor/rbxutil/component/init.test.luau +311 -311
  130. package/vendor/rbxutil/component/wally.toml +14 -14
  131. package/vendor/rbxutil/concur/init.luau +542 -542
  132. package/vendor/rbxutil/concur/init.test.luau +364 -364
  133. package/vendor/rbxutil/concur/wally.toml +8 -8
  134. package/vendor/rbxutil/enum-list/init.luau +101 -101
  135. package/vendor/rbxutil/enum-list/init.test.luau +91 -91
  136. package/vendor/rbxutil/enum-list/wally.toml +8 -8
  137. package/vendor/rbxutil/find/index.d.ts +20 -20
  138. package/vendor/rbxutil/find/init.luau +44 -44
  139. package/vendor/rbxutil/find/package.json +17 -17
  140. package/vendor/rbxutil/find/wally.toml +8 -8
  141. package/vendor/rbxutil/input/Gamepad.luau +559 -559
  142. package/vendor/rbxutil/input/Keyboard.luau +124 -124
  143. package/vendor/rbxutil/input/Mouse.luau +278 -278
  144. package/vendor/rbxutil/input/PreferredInput.luau +91 -91
  145. package/vendor/rbxutil/input/Touch.luau +120 -120
  146. package/vendor/rbxutil/input/init.luau +33 -33
  147. package/vendor/rbxutil/input/wally.toml +12 -12
  148. package/vendor/rbxutil/loader/index.d.ts +15 -15
  149. package/vendor/rbxutil/loader/init.luau +137 -137
  150. package/vendor/rbxutil/loader/wally.toml +8 -8
  151. package/vendor/rbxutil/log/index.d.ts +38 -38
  152. package/vendor/rbxutil/log/init.luau +746 -746
  153. package/vendor/rbxutil/log/wally.toml +8 -8
  154. package/vendor/rbxutil/net/init.luau +190 -190
  155. package/vendor/rbxutil/net/wally.toml +8 -8
  156. package/vendor/rbxutil/option/index.d.ts +44 -44
  157. package/vendor/rbxutil/option/init.luau +489 -489
  158. package/vendor/rbxutil/option/init.test.luau +342 -342
  159. package/vendor/rbxutil/option/wally.toml +8 -8
  160. package/vendor/rbxutil/pid/index.d.ts +53 -53
  161. package/vendor/rbxutil/pid/init.luau +195 -195
  162. package/vendor/rbxutil/pid/package.json +16 -16
  163. package/vendor/rbxutil/pid/wally.toml +9 -9
  164. package/vendor/rbxutil/quaternion/index.d.ts +117 -117
  165. package/vendor/rbxutil/quaternion/init.luau +570 -570
  166. package/vendor/rbxutil/quaternion/package.json +16 -16
  167. package/vendor/rbxutil/quaternion/wally.toml +9 -9
  168. package/vendor/rbxutil/query/index.d.ts +43 -43
  169. package/vendor/rbxutil/query/init.luau +117 -117
  170. package/vendor/rbxutil/query/package.json +18 -18
  171. package/vendor/rbxutil/query/wally.toml +9 -9
  172. package/vendor/rbxutil/sequent/index.d.ts +28 -28
  173. package/vendor/rbxutil/sequent/init.luau +340 -340
  174. package/vendor/rbxutil/sequent/package.json +16 -16
  175. package/vendor/rbxutil/sequent/wally.toml +9 -9
  176. package/vendor/rbxutil/ser/init.luau +175 -175
  177. package/vendor/rbxutil/ser/init.test.luau +50 -50
  178. package/vendor/rbxutil/ser/wally.toml +11 -11
  179. package/vendor/rbxutil/shake/index.d.ts +36 -36
  180. package/vendor/rbxutil/shake/init.luau +532 -532
  181. package/vendor/rbxutil/shake/init.test.luau +267 -267
  182. package/vendor/rbxutil/shake/package.json +16 -16
  183. package/vendor/rbxutil/shake/wally.toml +9 -9
  184. package/vendor/rbxutil/signal/index.d.ts +100 -100
  185. package/vendor/rbxutil/signal/init.luau +432 -432
  186. package/vendor/rbxutil/signal/init.test.luau +190 -190
  187. package/vendor/rbxutil/signal/package.json +17 -17
  188. package/vendor/rbxutil/signal/wally.toml +9 -9
  189. package/vendor/rbxutil/silo/TableWatcher.luau +65 -65
  190. package/vendor/rbxutil/silo/Util.luau +55 -55
  191. package/vendor/rbxutil/silo/init.luau +338 -338
  192. package/vendor/rbxutil/silo/init.test.luau +215 -215
  193. package/vendor/rbxutil/silo/wally.toml +8 -8
  194. package/vendor/rbxutil/spring/index.d.ts +40 -40
  195. package/vendor/rbxutil/spring/init.luau +97 -97
  196. package/vendor/rbxutil/spring/package.json +17 -17
  197. package/vendor/rbxutil/spring/wally.toml +8 -8
  198. package/vendor/rbxutil/stream/index.d.ts +88 -88
  199. package/vendor/rbxutil/stream/init.luau +597 -597
  200. package/vendor/rbxutil/stream/package.json +18 -18
  201. package/vendor/rbxutil/stream/wally.toml +9 -9
  202. package/vendor/rbxutil/streamable/Streamable.luau +202 -202
  203. package/vendor/rbxutil/streamable/StreamableUtil.luau +80 -80
  204. package/vendor/rbxutil/streamable/init.luau +8 -8
  205. package/vendor/rbxutil/streamable/wally.toml +12 -12
  206. package/vendor/rbxutil/symbol/init.luau +56 -56
  207. package/vendor/rbxutil/symbol/init.test.luau +37 -37
  208. package/vendor/rbxutil/symbol/wally.toml +8 -8
  209. package/vendor/rbxutil/table-util/init.luau +938 -938
  210. package/vendor/rbxutil/table-util/init.test.luau +439 -439
  211. package/vendor/rbxutil/task-queue/index.d.ts +27 -27
  212. package/vendor/rbxutil/task-queue/init.luau +97 -97
  213. package/vendor/rbxutil/task-queue/wally.toml +8 -8
  214. package/vendor/rbxutil/timer/index.d.ts +81 -81
  215. package/vendor/rbxutil/timer/init.luau +249 -249
  216. package/vendor/rbxutil/timer/init.test.luau +73 -73
  217. package/vendor/rbxutil/timer/wally.toml +11 -11
  218. package/vendor/rbxutil/tree/index.d.ts +15 -15
  219. package/vendor/rbxutil/tree/init.luau +137 -137
  220. package/vendor/rbxutil/tree/wally.toml +8 -8
  221. package/vendor/rbxutil/trove/index.d.ts +46 -46
  222. package/vendor/rbxutil/trove/init.luau +787 -787
  223. package/vendor/rbxutil/trove/init.test.luau +203 -203
  224. package/vendor/rbxutil/trove/wally.toml +8 -8
  225. package/vendor/rbxutil/typed-remote/init.luau +196 -196
  226. package/vendor/rbxutil/typed-remote/wally.toml +8 -8
  227. package/vendor/rbxutil/wait-for/index.d.ts +17 -17
  228. package/vendor/rbxutil/wait-for/init.luau +257 -257
  229. package/vendor/rbxutil/wait-for/init.test.luau +182 -182
  230. package/vendor/rbxutil/wait-for/wally.toml +11 -11
  231. package/vendor/t/t.lua +1350 -1350
  232. package/vendor/testez/Context.lua +26 -26
  233. package/vendor/testez/Expectation.lua +311 -311
  234. package/vendor/testez/ExpectationContext.lua +38 -38
  235. package/vendor/testez/LifecycleHooks.lua +89 -89
  236. package/vendor/testez/Reporters/TeamCityReporter.lua +101 -101
  237. package/vendor/testez/Reporters/TextReporter.lua +105 -105
  238. package/vendor/testez/Reporters/TextReporterQuiet.lua +96 -96
  239. package/vendor/testez/TestBootstrap.lua +146 -146
  240. package/vendor/testez/TestEnum.lua +27 -27
  241. package/vendor/testez/TestPlan.lua +304 -304
  242. package/vendor/testez/TestPlanner.lua +39 -39
  243. package/vendor/testez/TestResults.lua +111 -111
  244. package/vendor/testez/TestRunner.lua +188 -188
  245. package/vendor/testez/TestSession.lua +243 -243
  246. package/vendor/testez/init.lua +39 -39
@@ -1,1519 +1,1618 @@
1
- ---
2
- name: roblox-luau-mastery
3
- description: >
4
- Luau language fundamentals, type system, OOP, deprecation table, error patterns.
5
- last_reviewed: 2026-05-21
6
- ---
7
-
8
- <!-- Source: brockmartin/roblox-game-skill (MIT) -->
9
-
10
- # Luau Language Reference
11
-
12
- ## Overview
13
-
14
- Load this reference when the task involves:
15
-
16
- - General Luau syntax questions or code generation
17
- - Type system usage, annotations, or generics
18
- - Roblox-specific API patterns (services, events, instances)
19
- - OOP design with metatables and module-based classes
20
- - Async/concurrent programming (coroutines, Promises, task library)
21
- - Performance optimization or idiomatic Luau style
22
- - Debugging common pitfalls (1-based indexing, nil in tables, deprecated APIs)
23
-
24
- Luau is Roblox's fork of Lua 5.1 with gradual typing, performance improvements, and additional built-in functions. It is NOT standard Lua 5.1 - it has its own type system, generics, `continue` keyword, compound assignment operators (`+=`, `-=`, etc.), string interpolation, and other extensions.
25
-
26
- ### Helper Modules (vendored in this harness)
27
-
28
- The harness ships vendored copies of these libraries. Use them instead of raw Roblox equivalents:
29
-
30
- - **Promise** (evaera/roblox-lua-promise) - async control flow, retry, chaining. Use instead of raw coroutines for async work.
31
- - **Trove** (Sleitnick/RbxUtil) - cleanup/lifecycle management. Use instead of manually tracking connections and instances.
32
- - **Signal** (Sleitnick/RbxUtil) - typed custom signals. Use instead of BindableEvent for module-to-module communication.
33
- - **Comm** (Sleitnick/RbxUtil) - typed client-server remotes. Use instead of raw RemoteEvent/RemoteFunction.
34
- - **Component** (Sleitnick/RbxUtil) - CollectionService tag binding with lifecycle. Use instead of manual tag listeners.
35
- - **ProfileStore** (loleris/MadStudioRoblox) - session-locked DataStore with retry. Use instead of raw DataStoreService.
36
- - **t** (osyrisrblx/t) - runtime type checking for RemoteEvent validation, function arguments, DataStore schemas. Use instead of manual typeof() chains.
37
- - **TestEZ** (Roblox/testez) - BDD testing framework. Use to write .spec files for your modules.
38
-
39
- The agent will recommend these when relevant. You can veto by saying "use my own" or having an existing equivalent in your project.
40
-
41
- ---
42
-
43
- ## Quick Reference
44
-
45
- **Load Full Reference below only when you need specific syntax examples or implementation details.**
46
-
47
- Key rules:
48
- - Luau is NOT Lua 5.1. Has: generics, `continue`, `+=`, string interpolation (backticks), floor division `//`
49
- - Arrays are 1-based. `#tbl` for length. Generalized iteration: `for k, v in tbl do`
50
- - Always use `task.wait/spawn/delay` (never deprecated `wait/spawn/delay`)
51
- - Instance.new: configure properties THEN set Parent last (replication race)
52
- - Services: `game:GetService("Name")` at top of script, stored in locals
53
- - OOP: `.` for constructors, `:` for methods. `__index = self` pattern.
54
- - Type system: gradual typing, `typeof()` for narrowing, `::` for casting, `export type` for cross-module
55
- - Prefer backtick interpolation over `..` concatenation
56
- - Use vendored libs (Promise, Trove, Signal, Comm, Component, ProfileStore) over raw equivalents
57
-
58
- ---
59
-
60
- ## Full Reference
61
-
62
- ## Core Concepts
63
-
64
- ### Luau Extensions (not in Lua 5.1)
65
-
66
- ```luau
67
- -- Compound assignment operators
68
- score += 10
69
- score -= 5
70
- score *= 2
71
-
72
- -- continue keyword (skips to next iteration)
73
- for i = 1, 10 do
74
- if i % 2 == 0 then continue end
75
- print(i)
76
- end
77
-
78
- -- Generalized iteration (preferred over ipairs/pairs)
79
- for index, item in items do print(index, item) end
80
- for key, value in stats do print(key, value) end
81
- ```
82
-
83
- ### Tables
84
-
85
- Tables are the only compound data structure. They serve as arrays, dictionaries, objects, and namespaces.
86
-
87
- ```luau
88
- -- Dictionary (string keys)
89
- -- NOTE: name = "Alice" is shorthand for ["name"] = "Alice".
90
- -- Luau tables are NOT JSON objects. Keys are strings, not identifiers.
91
- local player = {
92
- name = "Alice",
93
- health = 100,
94
- inventory = {},
95
- }
96
- print(player.name) --> "Alice"
97
- print(player["health"]) --> 100
98
-
99
- -- Dynamic keys REQUIRE bracket notation
100
- local fieldName = "health"
101
- print(player[fieldName]) --> 100
102
-
103
- -- Arrays are 1-based, NOT 0-based
104
- local items = { "sword", "shield", "potion" }
105
- print(items[1]) --> "sword"
106
- print(#items) --> 3 (length operator)
107
- ```
108
-
109
-
110
-
111
- ### String Interpolation
112
-
113
- ```luau
114
- -- ALWAYS prefer backtick interpolation over .. concatenation
115
- local name = "Alice"
116
- local level = 42
117
- local message = `{name} reached level {level}!`
118
-
119
- -- Expressions in interpolation
120
- local price = 19.99
121
- local tax = 0.08
122
- print(`Total: ${price * (1 + tax)}`)
123
-
124
- -- string.split (Luau extension)
125
- local parts = string.split("a,b,c", ",")
126
- ```
127
-
128
- ### Luau-Specific Math Extensions
129
-
130
- ```luau
131
- local intDiv = 10 // 3 --> 3 (floor division, Luau extension)
132
- print(math.clamp(15, 0, 10)) --> 10 (Luau extension)
133
- print(math.sign(-7)) --> -1 (Luau extension)
134
- print(math.round(3.5)) --> 4 (Luau extension)
135
-
136
- -- For better randomness, use Random.new()
137
- local rng = Random.new()
138
- print(rng:NextNumber()) --> [0, 1) float
139
- print(rng:NextInteger(1, 100)) --> [1, 100] integer
140
- ```
141
-
142
- ---
143
-
144
- ## Type System
145
-
146
- Luau uses **gradual typing**: types are optional and can be added incrementally. The type checker runs at analysis time and does not affect runtime behavior.
147
-
148
- **2025-2026 Updates:**
149
- - **New Type Solver** (GA Nov 2025): faster, more accurate type checking. `--!nonstrict` is now the default for all scripts.
150
- - **Parallel Luau** (mature): Actor-based multithreading with `SharedTable` for cross-Actor data. Use `task.synchronize()` / `task.desynchronize()` to switch contexts.
151
-
152
- ### Basic Type Annotations
153
-
154
- ```luau
155
- -- Variable annotations
156
- local name: string = "Alice"
157
- local health: number = 100
158
- local isAlive: boolean = true
159
- local data: any = nil -- opt out of type checking
160
-
161
- -- Function parameter and return types
162
- local function add(a: number, b: number): number
163
- return a + b
164
- end
165
-
166
- -- Optional parameters
167
- local function greet(name: string, title: string?): string
168
- if title then
169
- return `{title} {name}`
170
- end
171
- return name
172
- end
173
- ```
174
-
175
- ### Table Types
176
-
177
- ```luau
178
- -- Array type
179
- local scores: { number } = { 100, 95, 87 }
180
-
181
- -- Dictionary type
182
- local config: { [string]: boolean } = {
183
- shadows = true,
184
- particles = false,
185
- }
186
-
187
- -- Typed table / record
188
- type PlayerData = {
189
- name: string,
190
- level: number,
191
- inventory: { string },
192
- stats: {
193
- health: number,
194
- mana: number,
195
- },
196
- }
197
-
198
- local player: PlayerData = {
199
- name = "Alice",
200
- level = 10,
201
- inventory = { "sword", "shield" },
202
- stats = {
203
- health = 100,
204
- mana = 50,
205
- },
206
- }
207
- ```
208
-
209
- ### Union and Intersection Types
210
-
211
- ```luau
212
- -- Union type: value can be one of several types
213
- local id: string | number = "abc123"
214
- id = 42 -- also valid
215
-
216
- -- Optional is shorthand for T | nil
217
- local nickname: string? = nil -- equivalent to string | nil
218
-
219
- -- Useful for function returns that may fail
220
- local function findPlayer(name: string): Player | nil
221
- -- ...
222
- return nil
223
- end
224
- ```
225
-
226
- ### Type Narrowing and Guards
227
-
228
- ```luau
229
- -- typeof narrows types (Roblox-aware, preferred over type())
230
- local function process(value: string | number)
231
- if typeof(value) == "string" then
232
- -- value is narrowed to string here
233
- print(string.upper(value))
234
- else
235
- -- value is narrowed to number here
236
- print(value * 2)
237
- end
238
- end
239
-
240
- -- Instance type checking with :IsA()
241
- local function handlePart(instance: Instance)
242
- if instance:IsA("BasePart") then
243
- -- instance is narrowed to BasePart
244
- instance.Anchored = true
245
- instance.BrickColor = BrickColor.new("Bright red")
246
- end
247
- end
248
-
249
- -- assert for non-nil narrowing
250
- local function getPlayerData(player: Player): PlayerData
251
- local leaderstats = player:FindFirstChild("leaderstats")
252
- assert(leaderstats, "Player missing leaderstats")
253
- -- leaderstats is now narrowed to non-nil
254
- return parseStats(leaderstats)
255
- end
256
- ```
257
-
258
- ### Generics
259
-
260
- ```luau
261
- -- Generic function
262
- local function first<T>(list: { T }): T?
263
- return list[1]
264
- end
265
-
266
- local name = first({ "Alice", "Bob" }) -- inferred as string?
267
- local num = first({ 1, 2, 3 }) -- inferred as number?
268
-
269
- -- Generic type alias
270
- type Result<T> = {
271
- success: boolean,
272
- value: T?,
273
- error: string?,
274
- }
275
-
276
- local function fetchData(): Result<PlayerData>
277
- return {
278
- success = true,
279
- value = { name = "Alice", level = 10, inventory = {}, stats = { health = 100, mana = 50 } },
280
- error = nil,
281
- }
282
- end
283
-
284
- -- Generic class-like pattern
285
- type Stack<T> = {
286
- items: { T },
287
- push: (self: Stack<T>, value: T) -> (),
288
- pop: (self: Stack<T>) -> T?,
289
- peek: (self: Stack<T>) -> T?,
290
- }
291
-
292
- -- NOTE: In type definitions, self is explicit (it's a function signature).
293
- -- In actual method definitions, use : to hide self (see OOP Patterns).
294
- ```
295
-
296
- ### Type Exports
297
-
298
- ```luau
299
- -- In a ModuleScript, export types for other modules to use
300
- -- File: ReplicatedStorage/Types.lua
301
-
302
- export type WeaponData = {
303
- name: string,
304
- damage: number,
305
- rarity: "Common" | "Rare" | "Epic" | "Legendary",
306
- durability: number,
307
- }
308
-
309
- export type InventorySlot = {
310
- item: WeaponData?,
311
- quantity: number,
312
- }
313
-
314
- -- Consumers import with require
315
- -- File: ServerScriptService/WeaponService.lua
316
- local Types = require(game.ReplicatedStorage.Types)
317
-
318
- local function createWeapon(name: string, damage: number): Types.WeaponData
319
- return {
320
- name = name,
321
- damage = damage,
322
- rarity = "Common",
323
- durability = 100,
324
- }
325
- end
326
- ```
327
-
328
- ### Common Roblox Types
329
-
330
- ```luau
331
- -- Instance hierarchy types
332
- local part: Part = Instance.new("Part")
333
- local model: Model = Instance.new("Model")
334
- local player: Player = game.Players.LocalPlayer
335
- local character: Model = player.Character or player.CharacterAdded:Wait()
336
- local humanoid: Humanoid = character:FindFirstChildWhichIsA("Humanoid") :: Humanoid
337
-
338
- -- Value types (these are NOT instances - they are value types / structs)
339
- local position: Vector3 = Vector3.new(10, 5, 0)
340
- local rotation: CFrame = CFrame.new(0, 10, 0) * CFrame.Angles(0, math.rad(90), 0)
341
- local color: Color3 = Color3.fromRGB(255, 0, 0)
342
- local size: Vector2 = Vector2.new(100, 50)
343
- local region: Region3 = Region3.new(Vector3.new(-10, 0, -10), Vector3.new(10, 20, 10))
344
- local ray: Ray = Ray.new(Vector3.new(0, 10, 0), Vector3.new(0, -1, 0))
345
- local udim2: UDim2 = UDim2.new(0.5, 0, 0.5, 0)
346
-
347
- -- Enum types
348
- local material: Enum.Material = Enum.Material.Grass
349
- local partType: Enum.PartType = Enum.PartType.Ball
350
- ```
351
-
352
- ---
353
-
354
- ## Roblox-Specific Patterns
355
-
356
- ### Instance Creation
357
-
358
- ```luau
359
- -- Create, configure, then ALWAYS set Parent last (avoids replication race)
360
- local part = Instance.new("Part")
361
- part.Name = "Floor"
362
- part.Size = Vector3.new(50, 1, 50)
363
- part.Anchored = true
364
- part.Parent = workspace -- Parent last!
365
- ```
366
-
367
- ### Service Access
368
-
369
- ```luau
370
- -- GetService is the canonical way to access Roblox services
371
- local Players = game:GetService("Players")
372
- local ReplicatedStorage = game:GetService("ReplicatedStorage")
373
- local ServerStorage = game:GetService("ServerStorage")
374
- local RunService = game:GetService("RunService")
375
- local UserInputService = game:GetService("UserInputService")
376
- local TweenService = game:GetService("TweenService")
377
- local HttpService = game:GetService("HttpService")
378
- local CollectionService = game:GetService("CollectionService")
379
- local PhysicsService = game:GetService("PhysicsService")
380
- local MarketplaceService = game:GetService("MarketplaceService")
381
- local DataStoreService = game:GetService("DataStoreService")
382
- local Debris = game:GetService("Debris")
383
-
384
- -- Services should be declared at the top of each script
385
- -- and stored in local variables for performance and clarity
386
- ```
387
-
388
- ### Event Connections
389
-
390
- ```luau
391
- -- Connecting to events returns an RBXScriptConnection
392
- local Players = game:GetService("Players")
393
-
394
- local connection: RBXScriptConnection
395
- connection = Players.PlayerAdded:Connect(function(player: Player)
396
- print(`{player.Name} joined the game`)
397
- end)
398
-
399
- -- Disconnecting when no longer needed (prevents memory leaks)
400
- connection:Disconnect()
401
-
402
- -- One-shot connection with :Once()
403
- Players.PlayerAdded:Once(function(player: Player)
404
- print(`First player to join: {player.Name}`)
405
- -- Automatically disconnects after firing once
406
- end)
407
-
408
- -- Waiting for an event to fire (yields the current thread)
409
- local player = Players.PlayerAdded:Wait()
410
- print(`{player.Name} joined`)
411
-
412
- -- Common event patterns
413
- local RunService = game:GetService("RunService")
414
-
415
- -- Heartbeat fires every frame after physics (use for most game logic)
416
- RunService.Heartbeat:Connect(function(deltaTime: number)
417
- -- deltaTime is seconds since last frame
418
- end)
419
-
420
- -- Stepped fires every frame before physics
421
- RunService.Stepped:Connect(function(elapsedTime: number, deltaTime: number)
422
- -- use for input processing or pre-physics logic
423
- end)
424
-
425
- -- Property change events
426
- local part = workspace:FindFirstChild("MyPart") :: Part
427
- part:GetPropertyChangedSignal("Position"):Connect(function()
428
- print(`Part moved to {part.Position}`)
429
- end)
430
-
431
- -- Child events
432
- workspace.ChildAdded:Connect(function(child: Instance)
433
- print(`New child: {child.Name}`)
434
- end)
435
- ```
436
-
437
- ### Task Library
438
-
439
- The `task` library is the modern replacement for deprecated globals `wait()`, `spawn()`, and `delay()`.
440
-
441
- ```luau
442
- -- task.wait: yields the current thread for a duration (returns actual elapsed time)
443
- local elapsed = task.wait(2) -- waits ~2 seconds
444
- print(`Actually waited {elapsed} seconds`)
445
-
446
- -- task.spawn: runs a function immediately in a new thread (resumes caller after)
447
- task.spawn(function()
448
- print("This runs immediately in a new coroutine")
449
- task.wait(5)
450
- print("This runs 5 seconds later")
451
- end)
452
- print("This also runs immediately, after the spawned function yields")
453
-
454
- -- task.delay: runs a function after a delay
455
- task.delay(3, function()
456
- print("This runs after 3 seconds")
457
- end)
458
-
459
- -- task.defer: runs a function at the end of the current resumption cycle
460
- -- Useful for deferring work without a delay
461
- task.defer(function()
462
- print("This runs after the current thread and any task.spawn calls finish")
463
- end)
464
-
465
- -- task.cancel: cancels a thread created by task.spawn or task.delay
466
- local thread = task.delay(10, function()
467
- print("This will never run")
468
- end)
469
- task.cancel(thread)
470
-
471
- -- task.synchronize / task.desynchronize: for Parallel Luau
472
- -- task.synchronize() -- switch to serial execution
473
- -- task.desynchronize() -- switch to parallel execution
474
- ```
475
-
476
- ### RemoteEvents and RemoteFunctions
477
-
478
- For server-client communication patterns (RemoteEvent, RemoteFunction, UnreliableRemoteEvent, BindableEvent), see **roblox-networking** → Client-Server Communication.
479
-
480
- ---
481
-
482
- ## OOP Patterns
483
-
484
- ### Metatable-Based Classes
485
-
486
- ```luau
487
- -- Standard OOP pattern using metatables
488
- local Weapon = {}
489
- Weapon.__index = Weapon
490
-
491
- export type Weapon = typeof(setmetatable(
492
- {} :: {
493
- name: string,
494
- damage: number,
495
- durability: number,
496
- maxDurability: number,
497
- },
498
- Weapon
499
- ))
500
-
501
- -- Constructor uses . (static - no instance yet)
502
- function Weapon.new(name: string, damage: number, durability: number): Weapon
503
- local self = setmetatable({}, Weapon)
504
- self.name = name
505
- self.damage = damage
506
- self.durability = durability
507
- self.maxDurability = durability
508
- return self
509
- end
510
-
511
- -- Methods use : (self is implicit, don't write it as a parameter)
512
- function Weapon:attack(target: Humanoid): boolean
513
- if self.durability <= 0 then
514
- warn(`{self.name} is broken!`)
515
- return false
516
- end
517
-
518
- target:TakeDamage(self.damage)
519
- self.durability -= 1
520
- return true
521
- end
522
-
523
- function Weapon:repair()
524
- self.durability = self.maxDurability
525
- end
526
-
527
- function Weapon:toString(): string
528
- return `{self.name} (DMG: {self.damage}, DUR: {self.durability}/{self.maxDurability})`
529
- end
530
-
531
- -- Usage: . for constructor, : for methods
532
- local sword = Weapon.new("Iron Sword", 25, 100)
533
- sword:attack(targetHumanoid)
534
- print(sword:toString())
535
- ```
536
-
537
- ### Inheritance via Metatable Chaining
538
-
539
- ```luau
540
- -- Base class
541
- local Entity = {}
542
- Entity.__index = Entity
543
-
544
- export type Entity = typeof(setmetatable(
545
- {} :: {
546
- name: string,
547
- health: number,
548
- maxHealth: number,
549
- position: Vector3,
550
- },
551
- Entity
552
- ))
553
-
554
- function Entity.new(name: string, health: number, position: Vector3): Entity
555
- local self = setmetatable({}, Entity)
556
- self.name = name
557
- self.health = health
558
- self.maxHealth = health
559
- self.position = position
560
- return self
561
- end
562
-
563
- function Entity:takeDamage(amount: number)
564
- self.health = math.max(0, self.health - amount)
565
- end
566
-
567
- function Entity:isAlive(): boolean
568
- return self.health > 0
569
- end
570
-
571
- -- Derived class
572
- local Enemy = {}
573
- Enemy.__index = Enemy
574
- setmetatable(Enemy, { __index = Entity }) -- inherit from Entity
575
-
576
- export type Enemy = typeof(setmetatable(
577
- {} :: {
578
- name: string,
579
- health: number,
580
- maxHealth: number,
581
- position: Vector3,
582
- -- Enemy-specific fields
583
- attackDamage: number,
584
- aggroRange: number,
585
- },
586
- Enemy
587
- ))
588
-
589
- function Enemy.new(name: string, health: number, position: Vector3, attackDamage: number): Enemy
590
- -- Call the parent constructor logic manually
591
- local self = setmetatable({}, Enemy) :: any
592
- self.name = name
593
- self.health = health
594
- self.maxHealth = health
595
- self.position = position
596
- self.attackDamage = attackDamage
597
- self.aggroRange = 50
598
- return self
599
- end
600
-
601
- function Enemy:attackTarget(target: Entity)
602
- local distance = (target.position - self.position).Magnitude
603
- if distance <= self.aggroRange then
604
- target:takeDamage(self.attackDamage)
605
- end
606
- end
607
-
608
- -- Usage: inherited methods also use :
609
- local goblin = Enemy.new("Goblin", 50, Vector3.new(0, 0, 0), 10)
610
- goblin:takeDamage(20) -- inherited from Entity
611
- goblin:attackTarget(player) -- defined on Enemy
612
- print(goblin:isAlive()) -- inherited from Entity
613
- ```
614
-
615
- ### Module-Based Service Pattern
616
-
617
- ```luau
618
- -- A common Roblox pattern: modules that act as singletons/services
619
- -- File: ServerScriptService/Services/CombatService.lua
620
-
621
- local ReplicatedStorage = game:GetService("ReplicatedStorage")
622
- local Players = game:GetService("Players")
623
-
624
- local CombatService = {}
625
-
626
- local activeBuffs: { [Player]: { string } } = {}
627
-
628
- function CombatService.init()
629
- Players.PlayerRemoving:Connect(function(player: Player)
630
- activeBuffs[player] = nil -- cleanup on leave
631
- end)
632
- end
633
-
634
- function CombatService.calculateDamage(attacker: Player, baseDamage: number): number
635
- local multiplier = 1.0
636
- local buffs = activeBuffs[attacker]
637
- if buffs then
638
- for _, buff in buffs do
639
- if buff == "strength" then
640
- multiplier += 0.5
641
- end
642
- end
643
- end
644
- return math.floor(baseDamage * multiplier)
645
- end
646
-
647
- function CombatService.addBuff(player: Player, buffName: string)
648
- if not activeBuffs[player] then
649
- activeBuffs[player] = {}
650
- end
651
- table.insert(activeBuffs[player], buffName)
652
- end
653
-
654
- function CombatService.removeBuff(player: Player, buffName: string)
655
- local buffs = activeBuffs[player]
656
- if not buffs then
657
- return
658
- end
659
- local index = table.find(buffs, buffName)
660
- if index then
661
- table.remove(buffs, index)
662
- end
663
- end
664
-
665
- return CombatService
666
- ```
667
-
668
- ---
669
-
670
- ## Async Patterns
671
-
672
- ### pcall and xpcall for Error Handling
673
-
674
- ```luau
675
- -- pcall wraps a function call and catches errors
676
- local success, result = pcall(function()
677
- return game:GetService("DataStoreService"):GetDataStore("PlayerData")
678
- end)
679
-
680
- if success then
681
- print("Got data store:", result)
682
- else
683
- warn("Failed to get data store:", result)
684
- end
685
-
686
- -- pcall with arguments (passed after the function)
687
- local success, data = pcall(dataStore.GetAsync, dataStore, "player_123")
688
-
689
- -- xpcall provides a custom error handler with stack trace
690
- local success, result = xpcall(function()
691
- error("Something went wrong")
692
- end, function(err)
693
- -- err is the error message
694
- warn("Error:", err)
695
- warn("Stack:", debug.traceback())
696
- return err -- returned as 'result' if success is false
697
- end)
698
-
699
- -- Pattern: retry with pcall
700
- local function retryAsync<T>(maxAttempts: number, delayBetween: number, fn: () -> T): T?
701
- for attempt = 1, maxAttempts do
702
- local success, result = pcall(fn)
703
- if success then
704
- return result
705
- end
706
- if attempt < maxAttempts then
707
- warn(`Attempt {attempt} failed: {result}. Retrying in {delayBetween}s...`)
708
- task.wait(delayBetween)
709
- else
710
- warn(`All {maxAttempts} attempts failed. Last error: {result}`)
711
- end
712
- end
713
- return nil
714
- end
715
-
716
- -- Usage: retry DataStore calls
717
- local data = retryAsync(3, 1, function()
718
- return dataStore:GetAsync("player_123")
719
- end)
720
- ```
721
-
722
- ### Coroutines
723
-
724
- ```luau
725
- -- Coroutines allow cooperative multitasking
726
- local function producer(): ()
727
- for i = 1, 5 do
728
- coroutine.yield(i)
729
- end
730
- end
731
-
732
- local co = coroutine.create(producer)
733
- for i = 1, 5 do
734
- local success, value = coroutine.resume(co)
735
- print(value) --> 1, 2, 3, 4, 5
736
- end
737
-
738
- -- coroutine.wrap creates a function that resumes automatically
739
- local nextValue = coroutine.wrap(producer)
740
- print(nextValue()) --> 1
741
- print(nextValue()) --> 2
742
-
743
- -- Practical example: staggered initialization
744
- local function initSystems(systems: { { name: string, init: () -> () } })
745
- for _, system in systems do
746
- task.spawn(function()
747
- local success, err = pcall(system.init)
748
- if not success then
749
- warn(`Failed to initialize {system.name}: {err}`)
750
- else
751
- print(`{system.name} initialized`)
752
- end
753
- end)
754
- end
755
- end
756
- ```
757
-
758
- ### Promise Pattern (roblox-lua-promise)
759
-
760
- The `Promise` library is the community-standard for async control flow in Roblox. It must be installed as a module (e.g., via Wally or manually).
761
-
762
- ```luau
763
- local Promise = require(ReplicatedStorage.Packages.Promise)
764
-
765
- -- Creating a Promise
766
- local function loadPlayerData(player: Player)
767
- return Promise.new(function(resolve, reject, onCancel)
768
- local key = `player_{player.UserId}`
769
-
770
- -- Support cancellation
771
- local cancelled = false
772
- onCancel(function()
773
- cancelled = true
774
- end)
775
-
776
- local success, data = pcall(dataStore.GetAsync, dataStore, key)
777
- if cancelled then
778
- return
779
- end
780
-
781
- if success then
782
- resolve(data or {})
783
- else
784
- reject(`Failed to load data: {data}`)
785
- end
786
- end)
787
- end
788
-
789
- -- Chaining promises
790
- loadPlayerData(player)
791
- :andThen(function(data)
792
- print("Data loaded:", data)
793
- return processData(data)
794
- end)
795
- :andThen(function(processed)
796
- applyData(player, processed)
797
- end)
798
- :catch(function(err)
799
- warn("Error:", err)
800
- end)
801
- :finally(function()
802
- print("Load attempt complete")
803
- end)
804
-
805
- -- Promise.all: wait for multiple promises
806
- Promise.all({
807
- loadPlayerData(player),
808
- loadInventory(player),
809
- loadSettings(player),
810
- }):andThen(function(results)
811
- local data, inventory, settings = results[1], results[2], results[3]
812
- -- All loaded successfully
813
- end):catch(function(err)
814
- warn("One or more loads failed:", err)
815
- end)
816
-
817
- -- Promise.race: first to resolve wins
818
- Promise.race({
819
- fetchFromPrimary(),
820
- Promise.delay(5):andThen(function()
821
- return fetchFromBackup()
822
- end),
823
- })
824
-
825
- -- Promise.retry
826
- Promise.retry(function()
827
- return loadPlayerData(player)
828
- end, 3):andThen(function(data)
829
- print("Loaded after retry")
830
- end)
831
-
832
- -- Wrapping yielding code in a Promise
833
- local function waitForCharacter(player: Player)
834
- return Promise.new(function(resolve)
835
- local character = player.Character or player.CharacterAdded:Wait()
836
- resolve(character)
837
- end)
838
- end
839
- ```
840
-
841
- ---
842
-
843
- ## Common Idioms
844
-
845
- ### Ternary with and/or
846
-
847
- Luau has no ternary operator. Use `and`/`or` chains for single-value conditions:
848
-
849
- ```luau
850
- -- Basic ternary: condition and truthy_value or falsy_value
851
- local status = (health > 0 and "alive" or "dead")
852
- local label = (isAdmin and "Admin" or "User")
853
- local color = (isActive and Color3.new(0, 1, 0) or Color3.new(1, 0, 0))
854
-
855
- -- With function calls
856
- local displayName = (player.DisplayName ~= "" and player.DisplayName or player.Name)
857
-
858
- -- Nested (use sparingly - readability drops fast)
859
- local tier = (score >= 90 and "S" or score >= 70 and "A" or score >= 50 and "B" or "C")
860
-
861
- -- CAVEAT: if the truthy value is nil or false, the expression breaks:
862
- -- (condition and nil or "fallback") returns "fallback" even when condition is true
863
- -- In that case, use a proper if/else block
864
- ```
865
-
866
- ### Table Operations
867
-
868
- ```luau
869
- -- table.insert: append to array
870
- local queue = {}
871
- table.insert(queue, "task1")
872
- table.insert(queue, "task2")
873
- -- queue = {"task1", "task2"}
874
-
875
- -- table.insert at index: insert at position (shifts others right)
876
- table.insert(queue, 1, "urgent")
877
- -- queue = {"urgent", "task1", "task2"}
878
-
879
- -- table.remove: remove by index (shifts others left), returns removed value
880
- local removed = table.remove(queue, 1) --> "urgent"
881
-
882
- -- table.remove without index removes last element
883
- local last = table.remove(queue) --> "task2"
884
-
885
- -- table.find: search for value in array (returns index or nil)
886
- local fruits = { "apple", "banana", "cherry" }
887
- local index = table.find(fruits, "banana") --> 2
888
- local missing = table.find(fruits, "grape") --> nil
889
-
890
- -- table.sort: in-place sort
891
- local numbers = { 5, 3, 8, 1, 9 }
892
- table.sort(numbers) -- ascending by default
893
- -- numbers = {1, 3, 5, 8, 9}
894
-
895
- -- Custom sort comparator
896
- local players = {
897
- { name = "Alice", score = 150 },
898
- { name = "Bob", score = 200 },
899
- { name = "Charlie", score = 100 },
900
- }
901
- table.sort(players, function(a, b)
902
- return a.score > b.score -- descending by score
903
- end)
904
-
905
- -- table.concat: join array elements into string
906
- local parts = { "Hello", "world", "!" }
907
- print(table.concat(parts, " ")) --> "Hello world !"
908
-
909
- -- table.freeze / table.isfrozen (Luau extension - immutable tables)
910
- local CONFIG = table.freeze({
911
- MAX_PLAYERS = 50,
912
- ROUND_TIME = 300,
913
- MAP_SIZE = 500,
914
- })
915
- -- CONFIG.MAX_PLAYERS = 100 --> ERROR: attempt to modify a frozen table
916
-
917
- -- table.clone (Luau extension - shallow copy)
918
- local original = { 1, 2, 3, sub = { 4, 5 } }
919
- local copy = table.clone(original)
920
- copy[1] = 99
921
- print(original[1]) --> 1 (not affected)
922
- -- NOTE: sub-tables are still shared references (shallow copy)
923
-
924
- -- table.move (copy elements between tables or within a table)
925
- local src = { 10, 20, 30, 40, 50 }
926
- local dst = {}
927
- table.move(src, 2, 4, 1, dst) -- copy src[2..4] into dst starting at dst[1]
928
- -- dst = {20, 30, 40}
929
-
930
- -- table.clear (Luau extension - remove all keys, keep table reference)
931
- local t = { 1, 2, 3 }
932
- table.clear(t) -- t is now empty but same reference
933
-
934
- -- Deep copy utility (not built-in - write your own)
935
- local function deepCopy<T>(original: T): T
936
- if typeof(original) ~= "table" then
937
- return original
938
- end
939
- local copy = table.clone(original :: any)
940
- for key, value in copy do
941
- if typeof(value) == "table" then
942
- copy[key] = deepCopy(value)
943
- end
944
- end
945
- return copy :: T
946
- end
947
- ```
948
-
949
- ### String Patterns
950
-
951
- Luau uses **Lua patterns**, which are NOT regular expressions. They are simpler and more limited.
952
-
953
- ```luau
954
- -- Character classes
955
- -- %a letters %A non-letters
956
- -- %d digits %D non-digits
957
- -- %l lowercase %L non-lowercase
958
- -- %u uppercase %U non-uppercase
959
- -- %w alphanumeric %W non-alphanumeric
960
- -- %s whitespace %S non-whitespace
961
- -- %p punctuation %P non-punctuation
962
- -- . any character
963
- -- %% literal %
964
-
965
- -- Quantifiers
966
- -- * 0 or more (greedy)
967
- -- + 1 or more (greedy)
968
- -- - 0 or more (lazy)
969
- -- ? 0 or 1
970
-
971
- -- string.match: extract matches
972
- local year, month, day = string.match("2026-03-04", "(%d+)-(%d+)-(%d+)")
973
- print(year, month, day) --> "2026" "03" "04"
974
-
975
- -- string.gmatch: iterate over all matches
976
- local text = "score=100, level=42, health=75"
977
- for key, value in string.gmatch(text, "(%w+)=(%d+)") do
978
- print(key, value)
979
- end
980
-
981
- -- string.gsub: replace matches
982
- local cleaned = string.gsub("Hello World", "%s+", " ")
983
- print(cleaned) --> "Hello World"
984
-
985
- -- Escaping pattern characters: use % before special chars
986
- -- Special chars: ( ) . % + - * ? [ ] ^ $
987
- local escaped = string.gsub("file.txt", "%.", "_")
988
- print(escaped) --> "file_txt"
989
-
990
- -- Anchors
991
- -- ^ matches start of string
992
- -- $ matches end of string
993
- local isEmail = string.match("user@example.com", "^%w+@%w+%.%w+$") ~= nil
994
- ```
995
-
996
- ### Instance Tree Traversal
997
-
998
- ```luau
999
- -- FindFirstChild: returns first direct child with name (or nil)
1000
- local head = character:FindFirstChild("Head")
1001
- if head then
1002
- print("Found head")
1003
- end
1004
-
1005
- -- FindFirstChild with recursive flag
1006
- local sword = workspace:FindFirstChild("Sword", true) -- searches entire subtree
1007
-
1008
- -- FindFirstChildOfClass: by ClassName
1009
- local humanoid = character:FindFirstChildOfClass("Humanoid")
1010
-
1011
- -- FindFirstChildWhichIsA: by class hierarchy (includes inherited classes)
1012
- local basePart = model:FindFirstChildWhichIsA("BasePart")
1013
-
1014
- -- WaitForChild: yields until child exists (with optional timeout)
1015
- local tool = player.Backpack:WaitForChild("Sword")
1016
- local toolOrNil = player.Backpack:WaitForChild("Sword", 5) -- 5 second timeout
1017
-
1018
- -- GetChildren: returns array of direct children
1019
- local children = workspace:GetChildren()
1020
- for _, child in children do
1021
- print(child.Name)
1022
- end
1023
-
1024
- -- GetDescendants: returns array of ALL descendants (recursive)
1025
- local allParts: { BasePart } = {}
1026
- for _, descendant in workspace:GetDescendants() do
1027
- if descendant:IsA("BasePart") then
1028
- table.insert(allParts, descendant)
1029
- end
1030
- end
1031
-
1032
- -- Filtering with CollectionService (tag-based)
1033
- local CollectionService = game:GetService("CollectionService")
1034
- local enemies = CollectionService:GetTagged("Enemy")
1035
- for _, enemy in enemies do
1036
- print(enemy.Name)
1037
- end
1038
-
1039
- -- Listen for tagged instances
1040
- CollectionService:GetInstanceAddedSignal("Enemy"):Connect(function(instance)
1041
- setupEnemy(instance)
1042
- end)
1043
-
1044
- CollectionService:GetInstanceRemovedSignal("Enemy"):Connect(function(instance)
1045
- cleanupEnemy(instance)
1046
- end)
1047
- ```
1048
-
1049
- ### Math Helpers
1050
-
1051
- ```luau
1052
- -- Clamping values
1053
- local health = math.clamp(currentHealth, 0, MAX_HEALTH)
1054
-
1055
- -- Linear interpolation
1056
- local function lerp(a: number, b: number, t: number): number
1057
- return a + (b - a) * t
1058
- end
1059
-
1060
- -- Mapping a value from one range to another
1061
- local function map(value: number, inMin: number, inMax: number, outMin: number, outMax: number): number
1062
- return outMin + (outMax - outMin) * ((value - inMin) / (inMax - inMin))
1063
- end
1064
-
1065
- -- Distance between two Vector3s
1066
- local distance = (posA - posB).Magnitude
1067
-
1068
- -- Normalized direction
1069
- local direction = (target - origin).Unit
1070
-
1071
- -- Rounding to decimal places
1072
- local function roundTo(value: number, places: number): number
1073
- local factor = 10 ^ places
1074
- return math.round(value * factor) / factor
1075
- end
1076
- print(roundTo(3.14159, 2)) --> 3.14
1077
- ```
1078
-
1079
- ---
1080
-
1081
- ## Best Practices
1082
-
1083
- ### Naming Conventions
1084
-
1085
- ```luau
1086
- -- PascalCase: classes, modules, services, types, enums
1087
- local CombatService = {}
1088
- local WeaponManager = require(script.WeaponManager)
1089
- type PlayerData = { name: string, level: number }
1090
-
1091
- -- camelCase: variables, function names, method names, parameters
1092
- local playerHealth = 100
1093
- local function calculateDamage(baseDamage: number): number end
1094
- function Weapon:getDurability(): number end
1095
-
1096
- -- UPPER_CASE: constants
1097
- local MAX_HEALTH = 100
1098
- local RESPAWN_DELAY = 5
1099
- local DEFAULT_SPEED = 16
1100
-
1101
- -- Prefix private methods with underscore (convention, not enforced)
1102
- function MyClass:_internalMethod() end
1103
- local _cachedValue = nil
1104
- ```
1105
-
1106
- ### Module Structure
1107
-
1108
- ```luau
1109
- -- Standard module template
1110
- -- File: ReplicatedStorage/Modules/InventoryManager.lua
1111
-
1112
- -- Services at the top
1113
- local ReplicatedStorage = game:GetService("ReplicatedStorage")
1114
- local Players = game:GetService("Players")
1115
-
1116
- -- Dependencies
1117
- local Types = require(ReplicatedStorage.Shared.Types)
1118
- local Signal = require(ReplicatedStorage.Packages.Signal)
1119
-
1120
- -- Constants
1121
- local MAX_SLOTS = 20
1122
- local STACK_LIMIT = 99
1123
-
1124
- -- Module table
1125
- local InventoryManager = {}
1126
-
1127
- -- Private state
1128
- local inventories: { [Player]: Types.Inventory } = {}
1129
-
1130
- -- Public API with type annotations
1131
- function InventoryManager.getInventory(player: Player): Types.Inventory?
1132
- return inventories[player]
1133
- end
1134
-
1135
- function InventoryManager.addItem(player: Player, itemId: string, quantity: number): boolean
1136
- local inventory = inventories[player]
1137
- if not inventory then
1138
- return false
1139
- end
1140
- -- ... implementation
1141
- return true
1142
- end
1143
-
1144
- -- Initialization
1145
- function InventoryManager.init()
1146
- Players.PlayerAdded:Connect(function(player: Player)
1147
- inventories[player] = { slots = {}, gold = 0 }
1148
- end)
1149
-
1150
- Players.PlayerRemoving:Connect(function(player: Player)
1151
- inventories[player] = nil
1152
- end)
1153
- end
1154
-
1155
- return InventoryManager
1156
- ```
1157
-
1158
- ### General Guidelines
1159
-
1160
- - Use `local` for every variable and function declaration.
1161
- - Add type annotations on all public module function signatures.
1162
- - Use `task.wait()` / `task.spawn()` / `task.delay()` / `task.defer()` instead of deprecated globals.
1163
- - Use `typeof()` instead of `type()` for Roblox-aware type checking.
1164
- - Set `Instance.Parent` last after configuring all properties (avoids unnecessary replication and change events).
1165
- - Clean up event connections and instances when no longer needed to avoid memory leaks.
1166
- - Validate all data received from clients on the server. Never trust the client.
1167
- - Use `pcall` / `xpcall` around any call that can fail (DataStores, HTTP, etc.).
1168
- - Use backtick interpolation (`{expr}`) for all string building. Never use `..` concatenation.
1169
- - Use `table.freeze()` for configuration tables that should not be modified.
1170
-
1171
- ---
1172
-
1173
- ## Anti-Patterns
1174
-
1175
- ### Deprecated Global Functions
1176
-
1177
- ```luau
1178
- -- BAD: deprecated, unpredictable resume timing, no cancellation
1179
- wait(2)
1180
- spawn(function() end)
1181
- delay(2, function() end)
1182
-
1183
- -- GOOD: modern task library equivalents
1184
- task.wait(2)
1185
- task.spawn(function() end)
1186
- task.delay(2, function() end)
1187
- ```
1188
-
1189
- ### Polling Instead of Events
1190
-
1191
- ```luau
1192
- -- BAD: polling wastes CPU cycles
1193
- while true do
1194
- local target = findNearestEnemy()
1195
- if target then
1196
- attack(target)
1197
- end
1198
- task.wait(0.1)
1199
- end
1200
-
1201
- -- GOOD: use events or Heartbeat with state checks
1202
- local RunService = game:GetService("RunService")
1203
- RunService.Heartbeat:Connect(function(dt: number)
1204
- local target = findNearestEnemy()
1205
- if target then
1206
- attack(target)
1207
- end
1208
- end)
1209
-
1210
- -- GOOD: use events when possible
1211
- CollectionService:GetInstanceAddedSignal("Enemy"):Connect(function(enemy)
1212
- onEnemySpawned(enemy)
1213
- end)
1214
- ```
1215
-
1216
- ### String Concatenation
1217
-
1218
- ```luau
1219
- -- BAD: .. concatenation is verbose and error-prone in hot paths
1220
- local greeting = "Hello, " .. name .. "!"
1221
-
1222
- -- BAD: creates a new string every iteration (O(n^2) memory)
1223
- local result = ""
1224
- for i = 1, 1000 do
1225
- result = result .. tostring(i) .. ","
1226
- end
1227
-
1228
- -- GOOD: use backtick interpolation for all string building
1229
- local greeting = `Hello, {name}!`
1230
-
1231
- -- GOOD: collect into table, join once for loops (O(n))
1232
- local parts = {}
1233
- for i = 1, 1000 do
1234
- table.insert(parts, tostring(i))
1235
- end
1236
- local result = table.concat(parts, ",")
1237
- ```
1238
-
1239
- ### Global Variables
1240
-
1241
- ```luau
1242
- -- BAD: pollutes shared environment, hard to track, no type checking
1243
- score = 0
1244
- function updateScore(amount)
1245
- score += amount
1246
- end
1247
-
1248
- -- GOOD: local variables, module scope
1249
- local score = 0
1250
- local function updateScore(amount: number)
1251
- score += amount
1252
- end
1253
- ```
1254
-
1255
- ### Missing pcall on Fallible Calls
1256
-
1257
- ```luau
1258
- -- BAD: crashes the script if the call fails
1259
- local data = dataStore:GetAsync("key")
1260
- local response = HttpService:RequestAsync({ Url = "https://api.example.com" })
1261
-
1262
- -- GOOD: wrap in pcall
1263
- local success, data = pcall(dataStore.GetAsync, dataStore, "key")
1264
- if not success then
1265
- warn("DataStore read failed:", data)
1266
- data = {} -- fallback
1267
- end
1268
-
1269
- local success, response = pcall(HttpService.RequestAsync, HttpService, {
1270
- Url = "https://api.example.com",
1271
- })
1272
- if not success then
1273
- warn("HTTP request failed:", response)
1274
- end
1275
- ```
1276
-
1277
- ### Trusting Client Input
1278
-
1279
- For server-authoritative validation patterns (type checking, range checking, ownership, rate limiting), see **roblox-networking** → Client Validation.
1280
-
1281
- **Core rule:** Never trust client input. Every `OnServerEvent` handler must validate types, ranges, and ownership before processing.
1282
-
1283
- ---
1284
-
1285
- ## Sharp Edges
1286
-
1287
- ### 1-Based Indexing
1288
-
1289
- Luau arrays are 1-indexed. The first element is `array[1]`, not `array[0]`.
1290
-
1291
- ```luau
1292
- local items = { "first", "second", "third" }
1293
- print(items[1]) --> "first"
1294
- print(items[0]) --> nil (NOT an error, just nil)
1295
-
1296
- -- Off-by-one errors are common when porting from other languages
1297
- for i = 1, #items do -- correct: 1 to length
1298
- print(items[i])
1299
- end
1300
- ```
1301
-
1302
- ### The `#` Operator and Nil Gaps
1303
-
1304
- The `#` (length) operator is only reliable for **contiguous arrays** with no nil gaps.
1305
-
1306
- ```luau
1307
- -- Reliable: contiguous array
1308
- local a = { 1, 2, 3, 4, 5 }
1309
- print(#a) --> 5 (correct)
1310
-
1311
- -- UNRELIABLE: array with nil gap
1312
- local b = { 1, 2, nil, 4, 5 }
1313
- print(#b) --> could be 2 or 5 (undefined behavior!)
1314
-
1315
- -- The length operator finds ANY valid boundary where t[n] ~= nil and t[n+1] == nil
1316
- -- With gaps, multiple boundaries exist, and the result is unpredictable
1317
-
1318
- -- SAFE: if you need to handle sparse data, use a dictionary with explicit count
1319
- local sparse: { [number]: string } = {}
1320
- local count = 0
1321
- sparse[1] = "a"
1322
- count += 1
1323
- sparse[5] = "e"
1324
- count += 1
1325
- -- Use count, not #sparse
1326
- ```
1327
-
1328
- ### Nil in Tables
1329
-
1330
- ```luau
1331
- -- Setting a table value to nil REMOVES the key
1332
- local t = { a = 1, b = 2, c = 3 }
1333
- t.b = nil
1334
- -- t is now { a = 1, c = 3 } - "b" key no longer exists
1335
-
1336
- -- This means you cannot store nil as a meaningful value in a table
1337
- -- Use a sentinel value instead if you need to distinguish "absent" from "nil"
1338
- local NONE = newproxy(false) -- unique sentinel
1339
- local cache = {}
1340
- cache["key"] = NONE -- means "we checked, value is absent"
1341
- -- cache["other"] is nil, meaning "we haven't checked yet"
1342
-
1343
- -- nil in arrays causes gaps (see # operator issue above)
1344
- local list = { 1, 2, 3 }
1345
- list[2] = nil -- creates a gap - DO NOT DO THIS
1346
- -- Use table.remove(list, 2) instead to shift elements down
1347
- ```
1348
-
1349
- ### Metatables: Powerful but Error-Prone
1350
-
1351
- ```luau
1352
- -- Common mistake: forgetting __index
1353
- local MyClass = {}
1354
- -- Missing: MyClass.__index = MyClass
1355
-
1356
- function MyClass.new()
1357
- return setmetatable({}, MyClass)
1358
- end
1359
-
1360
- function MyClass:doSomething()
1361
- print("doing something")
1362
- end
1363
-
1364
- local obj = MyClass.new()
1365
- obj:doSomething() --> ERROR: attempt to call a nil value
1366
- -- Because __index is not set, method lookup fails
1367
-
1368
- -- Common mistake: using . instead of : for method definitions
1369
- function MyClass.method(self: any) end -- explicit self with . (verbose, avoid)
1370
- function MyClass:method() end -- implicit self with : (idiomatic, use this)
1371
- -- Use : for all instance methods. Use . only for static constructors (new).
1372
-
1373
- -- Common mistake: modifying the metatable instead of the instance
1374
- function MyClass:setName(name: string)
1375
- -- BAD: this sets it on the class table, shared by all instances!
1376
- MyClass.name = name
1377
-
1378
- -- GOOD: set on the instance
1379
- self.name = name
1380
- end
1381
- ```
1382
-
1383
- ### Equality and Type Coercion
1384
-
1385
- ```luau
1386
- -- Luau does NOT coerce types in comparisons (unlike JavaScript)
1387
- print(0 == "0") --> false
1388
- print(1 == true) --> false
1389
- print("" == false) --> false
1390
-
1391
- -- Only nil and false are falsy
1392
- -- 0, "", and empty tables are TRUTHY
1393
- if 0 then print("0 is truthy") end --> prints
1394
- if "" then print("empty string is truthy") end --> prints
1395
- if {} then print("empty table is truthy") end --> prints
1396
-
1397
- -- This means you cannot use `if value then` to check for empty strings or zero
1398
- -- Be explicit:
1399
- if value ~= nil and value ~= "" then end
1400
- if value ~= nil and value ~= 0 then end
1401
- ```
1402
-
1403
- ### Table Reference Semantics
1404
-
1405
- ```luau
1406
- -- Tables are passed and assigned by REFERENCE, not by value
1407
- local original = { 1, 2, 3 }
1408
- local alias = original
1409
- alias[1] = 99
1410
- print(original[1]) --> 99 (both point to the same table)
1411
-
1412
- -- To get an independent copy, use table.clone (shallow) or a deep copy function
1413
- local copy = table.clone(original)
1414
- copy[1] = 0
1415
- print(original[1]) --> 99 (unaffected)
1416
-
1417
- -- But nested tables are still shared in a shallow clone
1418
- local nested = { data = { 1, 2, 3 } }
1419
- local shallowCopy = table.clone(nested)
1420
- shallowCopy.data[1] = 99
1421
- print(nested.data[1]) --> 99 (shared reference!)
1422
- -- Use a deep copy for nested structures
1423
- ```
1424
-
1425
- ### Scope and Closures
1426
-
1427
- ```luau
1428
- -- Common loop closure bug
1429
- local functions = {}
1430
- for i = 1, 5 do
1431
- functions[i] = function()
1432
- return i
1433
- end
1434
- end
1435
- -- In Luau, each loop iteration creates a new 'i' variable,
1436
- -- so this actually works correctly (unlike some other languages)
1437
- print(functions[1]()) --> 1
1438
- print(functions[5]()) --> 5
1439
-
1440
- -- But watch out with while loops - the variable is shared
1441
- local fns = {}
1442
- local i = 1
1443
- while i <= 5 do
1444
- fns[i] = function()
1445
- return i
1446
- end
1447
- i += 1
1448
- end
1449
- print(fns[1]()) --> 6 (all functions share the same 'i' which is now 6)
1450
-
1451
- -- Fix: capture the value in a local
1452
- local fns2 = {}
1453
- local j = 1
1454
- while j <= 5 do
1455
- local captured = j
1456
- fns2[j] = function()
1457
- return captured
1458
- end
1459
- j += 1
1460
- end
1461
- print(fns2[1]()) --> 1 (correct)
1462
- ```
1463
-
1464
- ---
1465
-
1466
- ## JS → Luau Translation Table
1467
-
1468
- AI models trained on JavaScript commonly generate patterns that don't exist in Luau. This table covers the most frequent mistakes.
1469
-
1470
- | JavaScript | Luau | Notes |
1471
- |------------|------|-------|
1472
- | `arr.map(fn)` | `table.create(#arr)` + for loop, or use a utility | No built-in map/filter/reduce on tables |
1473
- | `arr.filter(fn)` | Loop with `table.insert` into new table | No built-in filter |
1474
- | `arr.find(fn)` | Loop with early return | No built-in find |
1475
- | `arr.includes(x)` | `table.find(arr, x) ~= nil` | Returns index or nil |
1476
- | `arr.push(x)` | `table.insert(arr, x)` | |
1477
- | `arr.pop()` | `table.remove(arr)` | Removes and returns last element |
1478
- | `arr.splice(i, n)` | `table.remove(arr, i)` in a loop | No splice equivalent |
1479
- | `arr.length` or `arr.length` | `#arr` | `#` operator, not a property |
1480
- | `obj.keys(x)` | No direct equivalent - use `for k in x do` | |
1481
- | `obj.values(x)` | `for _, v in x do` | |
1482
- | `Object.assign(a, b)` | `for k, v in b do a[k] = v end` | No spread operator |
1483
- | `const x = ...` | `local x = ...` | No const/let/var |
1484
- | `let x = ...` | `local x = ...` | |
1485
- | `function(x) { return x }` | `function(x) return x end` | No arrow functions |
1486
- | `(x) => x * 2` | `function(x) return x * 2 end` | No arrow functions |
1487
- | `x === y` | `x == y` | No `===` in Luau, `==` is strict |
1488
- | `x !== y` | `x ~= y` | Not `!=` |
1489
- | `null` | `nil` | No null/undefined distinction |
1490
- | `typeof x` | `typeof(x)` for Roblox types, `type(x)` for Luau types | Parentheses required |
1491
- | `console.log(x)` | `print(x)` | |
1492
- | `x ?? y` | `x or y` | Luau `or` returns the value, not a boolean |
1493
- | `x?.y` | `x and x.y` | No optional chaining |
1494
- | `{...obj}` | Manual table copy with loop | No spread operator |
1495
- | `[...arr]` | Manual copy with loop or `table.move` | No spread operator |
1496
- | `new Map()` | Regular table `{}` | Luau tables are dictionaries by default |
1497
- | `new Set()` | `{[value] = true}` pattern | Use table as set |
1498
- | `Promise.all(arr)` | `Promise.all(arr)` | Same if using evaera/Promise |
1499
- | `async/await` | `coroutine` or Promise chains | No async/await syntax |
1500
- | `try/catch` | `pcall(fn)` or `xpcall(fn, handler)` | No try/catch |
1501
- | `throw error` | `error("message")` | |
1502
- | `class Foo { }` | `local Foo = {} Foo.__index = Foo` | Prototype-based OOP |
1503
- | `new Foo()` | `setmetatable({}, Foo)` | |
1504
- | `import x from "y"` | `local x = require(y)` | No ES modules |
1505
- | `export default` | `return module` | Module returns its public API |
1506
- | `str1 + str2` | `` `{str1}{str2}` `` | Use backtick interpolation, NOT `..` |
1507
- | `"hello " + name` | `` `hello {name}` `` | Backticks are the Luau way |
1508
-
1509
- ### Type-Specific Confusion
1510
-
1511
- | JavaScript | Luau | Why AI Gets It Wrong |
1512
- |------------|------|---------------------|
1513
- | `0 == ""` → `true` | `0 == ""` → `false` | Luau has no type coercion in `==` |
1514
- | `"" == false` → `true` | `"" == false` → `false` | Only `nil` and `false` are falsy |
1515
- | `if (0)` → falsy | `if 0 then` → truthy | `0`, `""`, `{}` are all truthy in Luau |
1516
- | `x = null` typeof `object` | `x = nil` → type `nil` | No null/undefined split |
1517
- | `Array.isArray(x)` | `type(x) == "table"` | No Array type distinction |
1518
- | `x.push()` on string | N/A - strings are not indexable | No string methods, use `string.*` library |
1519
-
1
+ ---
2
+ name: roblox-luau-mastery
3
+ description: >
4
+ Luau language fundamentals, type system, OOP, deprecation table, error patterns.
5
+ last_reviewed: 2026-05-26
6
+ ---
7
+
8
+ <!-- Source: brockmartin/roblox-game-skill (MIT) -->
9
+
10
+ # Luau Language Reference
11
+
12
+ ## Overview
13
+
14
+ Load this reference when the task involves:
15
+
16
+ - General Luau syntax questions or code generation
17
+ - Type system usage, annotations, or generics
18
+ - Roblox-specific API patterns (services, events, instances)
19
+ - OOP design with metatables and module-based classes
20
+ - Async/concurrent programming (coroutines, Promises, task library)
21
+ - Performance optimization or idiomatic Luau style
22
+ - Debugging common pitfalls (1-based indexing, nil in tables, deprecated APIs)
23
+
24
+ Luau is Roblox's fork of Lua 5.1 with gradual typing, performance improvements, and additional built-in functions. It is NOT standard Lua 5.1 - it has its own type system, generics, `continue` keyword, compound assignment operators (`+=`, `-=`, etc.), string interpolation, and other extensions.
25
+
26
+ ### Helper Modules (vendored in this harness)
27
+
28
+ The harness ships vendored copies of these libraries. Use them instead of raw Roblox equivalents:
29
+
30
+ - **Promise** (evaera/roblox-lua-promise) - async control flow, retry, chaining. Use instead of raw coroutines for async work.
31
+ - **Trove** (Sleitnick/RbxUtil) - cleanup/lifecycle management. Use instead of manually tracking connections and instances.
32
+ - **Signal** (Sleitnick/RbxUtil) - typed custom signals. Use instead of BindableEvent for module-to-module communication.
33
+ - **Comm** (Sleitnick/RbxUtil) - typed client-server remotes. Use instead of raw RemoteEvent/RemoteFunction.
34
+ - **Component** (Sleitnick/RbxUtil) - CollectionService tag binding with lifecycle. Use instead of manual tag listeners.
35
+ - **ProfileStore** (loleris/MadStudioRoblox) - session-locked DataStore with retry. Use instead of raw DataStoreService.
36
+ - **t** (osyrisrblx/t) - runtime type checking for RemoteEvent validation, function arguments, DataStore schemas. Use instead of manual typeof() chains.
37
+ - **TestEZ** (Roblox/testez) - BDD testing framework. Use to write .spec files for your modules.
38
+
39
+ The agent will recommend these when relevant. You can veto by saying "use my own" or having an existing equivalent in your project.
40
+
41
+ ---
42
+
43
+ ## Quick Reference
44
+
45
+ **Load Full Reference below only when you need specific syntax examples or implementation details.**
46
+
47
+ Key rules:
48
+ - Luau is NOT Lua 5.1. Has: generics, `continue`, `+=`, string interpolation (backticks), floor division `//`
49
+ - Arrays are 1-based. `#tbl` for length. Generalized iteration: `for k, v in tbl do`
50
+ - Always use `task.wait/spawn/delay` (never deprecated `wait/spawn/delay`)
51
+ - Instance.new: configure properties THEN set Parent last (replication race)
52
+ - Services: `game:GetService("Name")` at top of script, stored in locals
53
+ - Methods: use `:` (implicit self) for instance methods, `.` (explicit self) for constructors/static methods. Prefix private methods with `_`.
54
+ - Type system: gradual typing, `typeof()` for narrowing, `::` for casting, `export type` for cross-module
55
+ - Prefer backtick interpolation over `..` concatenation
56
+ - Use vendored libs (Promise, Trove, Signal, Comm, Component, ProfileStore) over raw equivalents
57
+ - Local function order: callees above callers (no hoisting). Forward-declare for mutual recursion.
58
+
59
+ ---
60
+
61
+ ## Full Reference
62
+
63
+ ## Core Concepts
64
+
65
+ ### Luau Extensions (not in Lua 5.1)
66
+
67
+ ```luau
68
+ -- Compound assignment operators
69
+ score += 10
70
+ score -= 5
71
+ score *= 2
72
+
73
+ -- continue keyword (skips to next iteration)
74
+ for i = 1, 10 do
75
+ if i % 2 == 0 then continue end
76
+ print(i)
77
+ end
78
+
79
+ -- Generalized iteration (preferred over ipairs/pairs)
80
+ for index, item in items do print(index, item) end
81
+ for key, value in stats do print(key, value) end
82
+ ```
83
+
84
+ ### Tables
85
+
86
+ Tables are the only compound data structure. They serve as arrays, dictionaries, objects, and namespaces.
87
+
88
+ ```luau
89
+ -- Dictionary (string keys)
90
+ -- NOTE: name = "Alice" is shorthand for ["name"] = "Alice".
91
+ -- Luau tables are NOT JSON objects. Keys are strings, not identifiers.
92
+ local player = {
93
+ name = "Alice",
94
+ health = 100,
95
+ inventory = {},
96
+ }
97
+ print(player.name) --> "Alice"
98
+ print(player["health"]) --> 100
99
+
100
+ -- Dynamic keys REQUIRE bracket notation
101
+ local fieldName = "health"
102
+ print(player[fieldName]) --> 100
103
+
104
+ -- Arrays are 1-based, NOT 0-based
105
+ local items = { "sword", "shield", "potion" }
106
+ print(items[1]) --> "sword"
107
+ print(#items) --> 3 (length operator)
108
+ ```
109
+
110
+
111
+
112
+ ### String Interpolation
113
+
114
+ ```luau
115
+ -- ALWAYS prefer backtick interpolation over .. concatenation
116
+ local name = "Alice"
117
+ local level = 42
118
+ local message = `{name} reached level {level}!`
119
+
120
+ -- Expressions in interpolation
121
+ local price = 19.99
122
+ local tax = 0.08
123
+ print(`Total: ${price * (1 + tax)}`)
124
+
125
+ -- string.split (Luau extension)
126
+ local parts = string.split("a,b,c", ",")
127
+ ```
128
+
129
+ ### Luau-Specific Math Extensions
130
+
131
+ ```luau
132
+ local intDiv = 10 // 3 --> 3 (floor division, Luau extension)
133
+ print(math.clamp(15, 0, 10)) --> 10 (Luau extension)
134
+ print(math.sign(-7)) --> -1 (Luau extension)
135
+ print(math.round(3.5)) --> 4 (Luau extension)
136
+
137
+ -- For better randomness, use Random.new()
138
+ local rng = Random.new()
139
+ print(rng:NextNumber()) --> [0, 1) float
140
+ print(rng:NextInteger(1, 100)) --> [1, 100] integer
141
+ ```
142
+
143
+ ---
144
+
145
+ ## Type System
146
+
147
+ Luau uses **gradual typing**: types are optional and can be added incrementally. The type checker runs at analysis time and does not affect runtime behavior.
148
+
149
+ **2025-2026 Updates:**
150
+ - **New Type Solver** (GA Nov 2025): faster, more accurate type checking. `--!nonstrict` is now the default for all scripts.
151
+ - **Parallel Luau** (mature): Actor-based multithreading with `SharedTable` for cross-Actor data. Use `task.synchronize()` / `task.desynchronize()` to switch contexts.
152
+
153
+ ### Basic Type Annotations
154
+
155
+ ```luau
156
+ -- Variable annotations
157
+ local name: string = "Alice"
158
+ local health: number = 100
159
+ local isAlive: boolean = true
160
+ local data: any = nil -- opt out of type checking
161
+
162
+ -- Function parameter and return types
163
+ local function add(a: number, b: number): number
164
+ return a + b
165
+ end
166
+
167
+ -- Optional parameters
168
+ local function greet(name: string, title: string?): string
169
+ if title then
170
+ return `{title} {name}`
171
+ end
172
+ return name
173
+ end
174
+ ```
175
+
176
+ ### Table Types
177
+
178
+ ```luau
179
+ -- Array type
180
+ local scores: { number } = { 100, 95, 87 }
181
+
182
+ -- Dictionary type
183
+ local config: { [string]: boolean } = {
184
+ shadows = true,
185
+ particles = false,
186
+ }
187
+
188
+ -- Typed table / record
189
+ type PlayerData = {
190
+ name: string,
191
+ level: number,
192
+ inventory: { string },
193
+ stats: {
194
+ health: number,
195
+ mana: number,
196
+ },
197
+ }
198
+
199
+ local player: PlayerData = {
200
+ name = "Alice",
201
+ level = 10,
202
+ inventory = { "sword", "shield" },
203
+ stats = {
204
+ health = 100,
205
+ mana = 50,
206
+ },
207
+ }
208
+ ```
209
+
210
+ ### Union and Intersection Types
211
+
212
+ ```luau
213
+ -- Union type: value can be one of several types
214
+ local id: string | number = "abc123"
215
+ id = 42 -- also valid
216
+
217
+ -- Optional is shorthand for T | nil
218
+ local nickname: string? = nil -- equivalent to string | nil
219
+
220
+ -- Useful for function returns that may fail
221
+ local function findPlayer(name: string): Player | nil
222
+ -- ...
223
+ return nil
224
+ end
225
+ ```
226
+
227
+ ### Type Narrowing and Guards
228
+
229
+ ```luau
230
+ -- typeof narrows types (Roblox-aware, preferred over type())
231
+ local function process(value: string | number)
232
+ if typeof(value) == "string" then
233
+ -- value is narrowed to string here
234
+ print(string.upper(value))
235
+ else
236
+ -- value is narrowed to number here
237
+ print(value * 2)
238
+ end
239
+ end
240
+
241
+ -- Instance type checking with :IsA()
242
+ local function handlePart(instance: Instance)
243
+ if instance:IsA("BasePart") then
244
+ -- instance is narrowed to BasePart
245
+ instance.Anchored = true
246
+ instance.BrickColor = BrickColor.new("Bright red")
247
+ end
248
+ end
249
+
250
+ -- assert for non-nil narrowing
251
+ local function getPlayerData(player: Player): PlayerData
252
+ local leaderstats = player:FindFirstChild("leaderstats")
253
+ assert(leaderstats, "Player missing leaderstats")
254
+ -- leaderstats is now narrowed to non-nil
255
+ return parseStats(leaderstats)
256
+ end
257
+ ```
258
+
259
+ ### Generics
260
+
261
+ ```luau
262
+ -- Generic function
263
+ local function first<T>(list: { T }): T?
264
+ return list[1]
265
+ end
266
+
267
+ local name = first({ "Alice", "Bob" }) -- inferred as string?
268
+ local num = first({ 1, 2, 3 }) -- inferred as number?
269
+
270
+ -- Generic type alias
271
+ type Result<T> = {
272
+ success: boolean,
273
+ value: T?,
274
+ error: string?,
275
+ }
276
+
277
+ local function fetchData(): Result<PlayerData>
278
+ return {
279
+ success = true,
280
+ value = { name = "Alice", level = 10, inventory = {}, stats = { health = 100, mana = 50 } },
281
+ error = nil,
282
+ }
283
+ end
284
+
285
+ -- Generic class-like pattern
286
+ type Stack<T> = {
287
+ items: { T },
288
+ push: (self: Stack<T>, value: T) -> (),
289
+ pop: (self: Stack<T>) -> T?,
290
+ peek: (self: Stack<T>) -> T?,
291
+ }
292
+
293
+ -- NOTE: In type definitions, self is explicit (it's a function signature).
294
+ -- In actual method definitions, use : to hide self (see OOP Patterns).
295
+ ```
296
+
297
+ ### Type Exports
298
+
299
+ ```luau
300
+ -- In a ModuleScript, export types for other modules to use
301
+ -- File: ReplicatedStorage/Types.lua
302
+
303
+ export type WeaponData = {
304
+ name: string,
305
+ damage: number,
306
+ rarity: "Common" | "Rare" | "Epic" | "Legendary",
307
+ durability: number,
308
+ }
309
+
310
+ export type InventorySlot = {
311
+ item: WeaponData?,
312
+ quantity: number,
313
+ }
314
+
315
+ -- Consumers import with require
316
+ -- File: ServerScriptService/WeaponService.lua
317
+ local Types = require(game.ReplicatedStorage.Types)
318
+
319
+ local function createWeapon(name: string, damage: number): Types.WeaponData
320
+ return {
321
+ name = name,
322
+ damage = damage,
323
+ rarity = "Common",
324
+ durability = 100,
325
+ }
326
+ end
327
+ ```
328
+
329
+ ### Common Roblox Types
330
+
331
+ ```luau
332
+ -- Instance hierarchy types
333
+ local part: Part = Instance.new("Part")
334
+ local model: Model = Instance.new("Model")
335
+ local player: Player = game.Players.LocalPlayer
336
+ local character: Model = player.Character or player.CharacterAdded:Wait()
337
+ local humanoid: Humanoid = character:FindFirstChildWhichIsA("Humanoid") :: Humanoid
338
+
339
+ -- Value types (these are NOT instances - they are value types / structs)
340
+ local position: Vector3 = Vector3.new(10, 5, 0)
341
+ local rotation: CFrame = CFrame.new(0, 10, 0) * CFrame.Angles(0, math.rad(90), 0)
342
+ local color: Color3 = Color3.fromRGB(255, 0, 0)
343
+ local size: Vector2 = Vector2.new(100, 50)
344
+ local region: Region3 = Region3.new(Vector3.new(-10, 0, -10), Vector3.new(10, 20, 10))
345
+ local ray: Ray = Ray.new(Vector3.new(0, 10, 0), Vector3.new(0, -1, 0))
346
+ local udim2: UDim2 = UDim2.new(0.5, 0, 0.5, 0)
347
+
348
+ -- Enum types
349
+ local material: Enum.Material = Enum.Material.Grass
350
+ local partType: Enum.PartType = Enum.PartType.Ball
351
+ ```
352
+
353
+ ---
354
+
355
+ ## Roblox-Specific Patterns
356
+
357
+ ### Instance Creation
358
+
359
+ ```luau
360
+ -- Create, configure, then ALWAYS set Parent last (avoids replication race)
361
+ local part = Instance.new("Part")
362
+ part.Name = "Floor"
363
+ part.Size = Vector3.new(50, 1, 50)
364
+ part.Anchored = true
365
+ part.Parent = workspace -- Parent last!
366
+ ```
367
+
368
+ ### Service Access
369
+
370
+ ```luau
371
+ -- GetService is the canonical way to access Roblox services
372
+ local Players = game:GetService("Players")
373
+ local ReplicatedStorage = game:GetService("ReplicatedStorage")
374
+ local ServerStorage = game:GetService("ServerStorage")
375
+ local RunService = game:GetService("RunService")
376
+ local UserInputService = game:GetService("UserInputService")
377
+ local TweenService = game:GetService("TweenService")
378
+ local HttpService = game:GetService("HttpService")
379
+ local CollectionService = game:GetService("CollectionService")
380
+ local PhysicsService = game:GetService("PhysicsService")
381
+ local MarketplaceService = game:GetService("MarketplaceService")
382
+ local DataStoreService = game:GetService("DataStoreService")
383
+ local Debris = game:GetService("Debris")
384
+
385
+ -- Services should be declared at the top of each script
386
+ -- and stored in local variables for performance and clarity
387
+ ```
388
+
389
+ ### Event Connections
390
+
391
+ ```luau
392
+ -- Connecting to events returns an RBXScriptConnection
393
+ local Players = game:GetService("Players")
394
+
395
+ local connection: RBXScriptConnection
396
+ connection = Players.PlayerAdded:Connect(function(player: Player)
397
+ print(`{player.Name} joined the game`)
398
+ end)
399
+
400
+ -- Disconnecting when no longer needed (prevents memory leaks)
401
+ connection:Disconnect()
402
+
403
+ -- One-shot connection with :Once()
404
+ Players.PlayerAdded:Once(function(player: Player)
405
+ print(`First player to join: {player.Name}`)
406
+ -- Automatically disconnects after firing once
407
+ end)
408
+
409
+ -- Waiting for an event to fire (yields the current thread)
410
+ local player = Players.PlayerAdded:Wait()
411
+ print(`{player.Name} joined`)
412
+
413
+ -- Common event patterns
414
+ local RunService = game:GetService("RunService")
415
+
416
+ -- Heartbeat fires every frame after physics (use for most game logic)
417
+ RunService.Heartbeat:Connect(function(deltaTime: number)
418
+ -- deltaTime is seconds since last frame
419
+ end)
420
+
421
+ -- Stepped fires every frame before physics
422
+ RunService.Stepped:Connect(function(elapsedTime: number, deltaTime: number)
423
+ -- use for input processing or pre-physics logic
424
+ end)
425
+
426
+ -- Property change events
427
+ local part = workspace:FindFirstChild("MyPart") :: Part
428
+ part:GetPropertyChangedSignal("Position"):Connect(function()
429
+ print(`Part moved to {part.Position}`)
430
+ end)
431
+
432
+ -- Child events
433
+ workspace.ChildAdded:Connect(function(child: Instance)
434
+ print(`New child: {child.Name}`)
435
+ end)
436
+ ```
437
+
438
+ ### Task Library
439
+
440
+ The `task` library is the modern replacement for deprecated globals `wait()`, `spawn()`, and `delay()`.
441
+
442
+ ```luau
443
+ -- task.wait: yields the current thread for a duration (returns actual elapsed time)
444
+ local elapsed = task.wait(2) -- waits ~2 seconds
445
+ print(`Actually waited {elapsed} seconds`)
446
+
447
+ -- task.spawn: runs a function immediately in a new thread (resumes caller after)
448
+ task.spawn(function()
449
+ print("This runs immediately in a new coroutine")
450
+ task.wait(5)
451
+ print("This runs 5 seconds later")
452
+ end)
453
+ print("This also runs immediately, after the spawned function yields")
454
+
455
+ -- task.delay: runs a function after a delay
456
+ task.delay(3, function()
457
+ print("This runs after 3 seconds")
458
+ end)
459
+
460
+ -- task.defer: runs a function at the end of the current resumption cycle
461
+ -- Useful for deferring work without a delay
462
+ task.defer(function()
463
+ print("This runs after the current thread and any task.spawn calls finish")
464
+ end)
465
+
466
+ -- task.cancel: cancels a thread created by task.spawn or task.delay
467
+ local thread = task.delay(10, function()
468
+ print("This will never run")
469
+ end)
470
+ task.cancel(thread)
471
+
472
+ -- task.synchronize / task.desynchronize: for Parallel Luau
473
+ -- task.synchronize() -- switch to serial execution
474
+ -- task.desynchronize() -- switch to parallel execution
475
+ ```
476
+
477
+ ### RemoteEvents and RemoteFunctions
478
+
479
+ For server-client communication patterns (RemoteEvent, RemoteFunction, UnreliableRemoteEvent, BindableEvent), see **roblox-networking** → Client-Server Communication.
480
+
481
+ ---
482
+
483
+ ## OOP Patterns
484
+
485
+ ### Metatable-Based Classes
486
+
487
+ ```luau
488
+ -- Standard OOP pattern using metatables
489
+ local Weapon = {}
490
+ Weapon.__index = Weapon
491
+
492
+ export type Weapon = typeof(setmetatable(
493
+ {} :: {
494
+ name: string,
495
+ damage: number,
496
+ durability: number,
497
+ maxDurability: number,
498
+ },
499
+ Weapon
500
+ ))
501
+
502
+ -- Constructor uses . (static - no instance yet)
503
+ function Weapon.new(name: string, damage: number, durability: number): Weapon
504
+ local self = setmetatable({}, Weapon)
505
+ self.name = name
506
+ self.damage = damage
507
+ self.durability = durability
508
+ self.maxDurability = durability
509
+ return self
510
+ end
511
+
512
+ -- Methods use : (self is implicit, don't write it as a parameter)
513
+ function Weapon:attack(target: Humanoid): boolean
514
+ if self.durability <= 0 then
515
+ warn(`{self.name} is broken!`)
516
+ return false
517
+ end
518
+
519
+ target:TakeDamage(self.damage)
520
+ self.durability -= 1
521
+ return true
522
+ end
523
+
524
+ function Weapon:repair()
525
+ self.durability = self.maxDurability
526
+ end
527
+
528
+ function Weapon:toString(): string
529
+ return `{self.name} (DMG: {self.damage}, DUR: {self.durability}/{self.maxDurability})`
530
+ end
531
+
532
+ -- Usage: . for constructor, : for methods
533
+ local sword = Weapon.new("Iron Sword", 25, 100)
534
+ sword:attack(targetHumanoid)
535
+ print(sword:toString())
536
+ ```
537
+
538
+ ### Inheritance via Metatable Chaining
539
+
540
+ ```luau
541
+ -- Base class
542
+ local Entity = {}
543
+ Entity.__index = Entity
544
+
545
+ export type Entity = typeof(setmetatable(
546
+ {} :: {
547
+ name: string,
548
+ health: number,
549
+ maxHealth: number,
550
+ position: Vector3,
551
+ },
552
+ Entity
553
+ ))
554
+
555
+ function Entity.new(name: string, health: number, position: Vector3): Entity
556
+ local self = setmetatable({}, Entity)
557
+ self.name = name
558
+ self.health = health
559
+ self.maxHealth = health
560
+ self.position = position
561
+ return self
562
+ end
563
+
564
+ function Entity:takeDamage(amount: number)
565
+ self.health = math.max(0, self.health - amount)
566
+ end
567
+
568
+ function Entity:isAlive(): boolean
569
+ return self.health > 0
570
+ end
571
+
572
+ -- Derived class
573
+ local Enemy = {}
574
+ Enemy.__index = Enemy
575
+ setmetatable(Enemy, { __index = Entity }) -- inherit from Entity
576
+
577
+ export type Enemy = typeof(setmetatable(
578
+ {} :: {
579
+ name: string,
580
+ health: number,
581
+ maxHealth: number,
582
+ position: Vector3,
583
+ -- Enemy-specific fields
584
+ attackDamage: number,
585
+ aggroRange: number,
586
+ },
587
+ Enemy
588
+ ))
589
+
590
+ function Enemy.new(name: string, health: number, position: Vector3, attackDamage: number): Enemy
591
+ -- Call the parent constructor logic manually
592
+ local self = setmetatable({}, Enemy) :: any
593
+ self.name = name
594
+ self.health = health
595
+ self.maxHealth = health
596
+ self.position = position
597
+ self.attackDamage = attackDamage
598
+ self.aggroRange = 50
599
+ return self
600
+ end
601
+
602
+ function Enemy:attackTarget(target: Entity)
603
+ local distance = (target.position - self.position).Magnitude
604
+ if distance <= self.aggroRange then
605
+ target:takeDamage(self.attackDamage)
606
+ end
607
+ end
608
+
609
+ -- Usage: inherited methods also use :
610
+ local goblin = Enemy.new("Goblin", 50, Vector3.new(0, 0, 0), 10)
611
+ goblin:takeDamage(20) -- inherited from Entity
612
+ goblin:attackTarget(player) -- defined on Enemy
613
+ print(goblin:isAlive()) -- inherited from Entity
614
+ ```
615
+
616
+ ### Module-Based Service Pattern
617
+
618
+ ```luau
619
+ -- A common Roblox pattern: modules that act as singletons/services
620
+ -- File: ServerScriptService/Services/CombatService.lua
621
+
622
+ local ReplicatedStorage = game:GetService("ReplicatedStorage")
623
+ local Players = game:GetService("Players")
624
+
625
+ local CombatService = {}
626
+
627
+ local activeBuffs: { [Player]: { string } } = {}
628
+
629
+ function CombatService.init()
630
+ Players.PlayerRemoving:Connect(function(player: Player)
631
+ activeBuffs[player] = nil -- cleanup on leave
632
+ end)
633
+ end
634
+
635
+ function CombatService.calculateDamage(attacker: Player, baseDamage: number): number
636
+ local multiplier = 1.0
637
+ local buffs = activeBuffs[attacker]
638
+ if buffs then
639
+ for _, buff in buffs do
640
+ if buff == "strength" then
641
+ multiplier += 0.5
642
+ end
643
+ end
644
+ end
645
+ return math.floor(baseDamage * multiplier)
646
+ end
647
+
648
+ function CombatService.addBuff(player: Player, buffName: string)
649
+ if not activeBuffs[player] then
650
+ activeBuffs[player] = {}
651
+ end
652
+ table.insert(activeBuffs[player], buffName)
653
+ end
654
+
655
+ function CombatService.removeBuff(player: Player, buffName: string)
656
+ local buffs = activeBuffs[player]
657
+ if not buffs then
658
+ return
659
+ end
660
+ local index = table.find(buffs, buffName)
661
+ if index then
662
+ table.remove(buffs, index)
663
+ end
664
+ end
665
+
666
+ return CombatService
667
+ ```
668
+
669
+ ---
670
+
671
+ ## Async Patterns
672
+
673
+ ### pcall and xpcall for Error Handling
674
+
675
+ ```luau
676
+ -- pcall wraps a function call and catches errors
677
+ local success, result = pcall(function()
678
+ return game:GetService("DataStoreService"):GetDataStore("PlayerData")
679
+ end)
680
+
681
+ if success then
682
+ print("Got data store:", result)
683
+ else
684
+ warn("Failed to get data store:", result)
685
+ end
686
+
687
+ -- pcall with arguments (passed after the function)
688
+ local success, data = pcall(dataStore.GetAsync, dataStore, "player_123")
689
+
690
+ -- xpcall provides a custom error handler with stack trace
691
+ local success, result = xpcall(function()
692
+ error("Something went wrong")
693
+ end, function(err)
694
+ -- err is the error message
695
+ warn("Error:", err)
696
+ warn("Stack:", debug.traceback())
697
+ return err -- returned as 'result' if success is false
698
+ end)
699
+
700
+ -- Pattern: retry with pcall
701
+ local function retryAsync<T>(maxAttempts: number, delayBetween: number, fn: () -> T): T?
702
+ for attempt = 1, maxAttempts do
703
+ local success, result = pcall(fn)
704
+ if success then
705
+ return result
706
+ end
707
+ if attempt < maxAttempts then
708
+ warn(`Attempt {attempt} failed: {result}. Retrying in {delayBetween}s...`)
709
+ task.wait(delayBetween)
710
+ else
711
+ warn(`All {maxAttempts} attempts failed. Last error: {result}`)
712
+ end
713
+ end
714
+ return nil
715
+ end
716
+
717
+ -- Usage: retry DataStore calls
718
+ local data = retryAsync(3, 1, function()
719
+ return dataStore:GetAsync("player_123")
720
+ end)
721
+ ```
722
+
723
+ ### Coroutines
724
+
725
+ ```luau
726
+ -- Coroutines allow cooperative multitasking
727
+ local function producer(): ()
728
+ for i = 1, 5 do
729
+ coroutine.yield(i)
730
+ end
731
+ end
732
+
733
+ local co = coroutine.create(producer)
734
+ for i = 1, 5 do
735
+ local success, value = coroutine.resume(co)
736
+ print(value) --> 1, 2, 3, 4, 5
737
+ end
738
+
739
+ -- coroutine.wrap creates a function that resumes automatically
740
+ local nextValue = coroutine.wrap(producer)
741
+ print(nextValue()) --> 1
742
+ print(nextValue()) --> 2
743
+
744
+ -- Practical example: staggered initialization
745
+ local function initSystems(systems: { { name: string, init: () -> () } })
746
+ for _, system in systems do
747
+ task.spawn(function()
748
+ local success, err = pcall(system.init)
749
+ if not success then
750
+ warn(`Failed to initialize {system.name}: {err}`)
751
+ else
752
+ print(`{system.name} initialized`)
753
+ end
754
+ end)
755
+ end
756
+ end
757
+ ```
758
+
759
+ ### Promise Pattern (roblox-lua-promise)
760
+
761
+ The `Promise` library is the community-standard for async control flow in Roblox. It must be installed as a module (e.g., via Wally or manually).
762
+
763
+ ```luau
764
+ local Promise = require(ReplicatedStorage.Packages.Promise)
765
+
766
+ -- Creating a Promise
767
+ local function loadPlayerData(player: Player)
768
+ return Promise.new(function(resolve, reject, onCancel)
769
+ local key = `player_{player.UserId}`
770
+
771
+ -- Support cancellation
772
+ local cancelled = false
773
+ onCancel(function()
774
+ cancelled = true
775
+ end)
776
+
777
+ local success, data = pcall(dataStore.GetAsync, dataStore, key)
778
+ if cancelled then
779
+ return
780
+ end
781
+
782
+ if success then
783
+ resolve(data or {})
784
+ else
785
+ reject(`Failed to load data: {data}`)
786
+ end
787
+ end)
788
+ end
789
+
790
+ -- Chaining promises
791
+ loadPlayerData(player)
792
+ :andThen(function(data)
793
+ print("Data loaded:", data)
794
+ return processData(data)
795
+ end)
796
+ :andThen(function(processed)
797
+ applyData(player, processed)
798
+ end)
799
+ :catch(function(err)
800
+ warn("Error:", err)
801
+ end)
802
+ :finally(function()
803
+ print("Load attempt complete")
804
+ end)
805
+
806
+ -- Promise.all: wait for multiple promises
807
+ Promise.all({
808
+ loadPlayerData(player),
809
+ loadInventory(player),
810
+ loadSettings(player),
811
+ }):andThen(function(results)
812
+ local data, inventory, settings = results[1], results[2], results[3]
813
+ -- All loaded successfully
814
+ end):catch(function(err)
815
+ warn("One or more loads failed:", err)
816
+ end)
817
+
818
+ -- Promise.race: first to resolve wins
819
+ Promise.race({
820
+ fetchFromPrimary(),
821
+ Promise.delay(5):andThen(function()
822
+ return fetchFromBackup()
823
+ end),
824
+ })
825
+
826
+ -- Promise.retry
827
+ Promise.retry(function()
828
+ return loadPlayerData(player)
829
+ end, 3):andThen(function(data)
830
+ print("Loaded after retry")
831
+ end)
832
+
833
+ -- Wrapping yielding code in a Promise
834
+ local function waitForCharacter(player: Player)
835
+ return Promise.new(function(resolve)
836
+ local character = player.Character or player.CharacterAdded:Wait()
837
+ resolve(character)
838
+ end)
839
+ end
840
+ ```
841
+
842
+ ---
843
+
844
+ ## Common Idioms
845
+
846
+ ### Ternary with and/or
847
+
848
+ Luau has no ternary operator. Use `and`/`or` chains for single-value conditions:
849
+
850
+ ```luau
851
+ -- Basic ternary: condition and truthy_value or falsy_value
852
+ local status = (health > 0 and "alive" or "dead")
853
+ local label = (isAdmin and "Admin" or "User")
854
+ local color = (isActive and Color3.new(0, 1, 0) or Color3.new(1, 0, 0))
855
+
856
+ -- With function calls
857
+ local displayName = (player.DisplayName ~= "" and player.DisplayName or player.Name)
858
+
859
+ -- Nested (use sparingly - readability drops fast)
860
+ local tier = (score >= 90 and "S" or score >= 70 and "A" or score >= 50 and "B" or "C")
861
+
862
+ -- CAVEAT: if the truthy value is nil or false, the expression breaks:
863
+ -- (condition and nil or "fallback") returns "fallback" even when condition is true
864
+ -- In that case, use a proper if/else block
865
+ ```
866
+
867
+ ### Table Operations
868
+
869
+ ```luau
870
+ -- table.insert: append to array
871
+ local queue = {}
872
+ table.insert(queue, "task1")
873
+ table.insert(queue, "task2")
874
+ -- queue = {"task1", "task2"}
875
+
876
+ -- table.insert at index: insert at position (shifts others right)
877
+ table.insert(queue, 1, "urgent")
878
+ -- queue = {"urgent", "task1", "task2"}
879
+
880
+ -- table.remove: remove by index (shifts others left), returns removed value
881
+ local removed = table.remove(queue, 1) --> "urgent"
882
+
883
+ -- table.remove without index removes last element
884
+ local last = table.remove(queue) --> "task2"
885
+
886
+ -- table.find: search for value in array (returns index or nil)
887
+ local fruits = { "apple", "banana", "cherry" }
888
+ local index = table.find(fruits, "banana") --> 2
889
+ local missing = table.find(fruits, "grape") --> nil
890
+
891
+ -- table.sort: in-place sort
892
+ local numbers = { 5, 3, 8, 1, 9 }
893
+ table.sort(numbers) -- ascending by default
894
+ -- numbers = {1, 3, 5, 8, 9}
895
+
896
+ -- Custom sort comparator
897
+ local players = {
898
+ { name = "Alice", score = 150 },
899
+ { name = "Bob", score = 200 },
900
+ { name = "Charlie", score = 100 },
901
+ }
902
+ table.sort(players, function(a, b)
903
+ return a.score > b.score -- descending by score
904
+ end)
905
+
906
+ -- table.concat: join array elements into string
907
+ local parts = { "Hello", "world", "!" }
908
+ print(table.concat(parts, " ")) --> "Hello world !"
909
+
910
+ -- table.freeze / table.isfrozen (Luau extension - immutable tables)
911
+ local CONFIG = table.freeze({
912
+ MAX_PLAYERS = 50,
913
+ ROUND_TIME = 300,
914
+ MAP_SIZE = 500,
915
+ })
916
+ -- CONFIG.MAX_PLAYERS = 100 --> ERROR: attempt to modify a frozen table
917
+
918
+ -- table.clone (Luau extension - shallow copy)
919
+ local original = { 1, 2, 3, sub = { 4, 5 } }
920
+ local copy = table.clone(original)
921
+ copy[1] = 99
922
+ print(original[1]) --> 1 (not affected)
923
+ -- NOTE: sub-tables are still shared references (shallow copy)
924
+
925
+ -- table.move (copy elements between tables or within a table)
926
+ local src = { 10, 20, 30, 40, 50 }
927
+ local dst = {}
928
+ table.move(src, 2, 4, 1, dst) -- copy src[2..4] into dst starting at dst[1]
929
+ -- dst = {20, 30, 40}
930
+
931
+ -- table.clear (Luau extension - remove all keys, keep table reference)
932
+ local t = { 1, 2, 3 }
933
+ table.clear(t) -- t is now empty but same reference
934
+
935
+ -- Deep copy utility (not built-in - write your own)
936
+ local function deepCopy<T>(original: T): T
937
+ if typeof(original) ~= "table" then
938
+ return original
939
+ end
940
+ local copy = table.clone(original :: any)
941
+ for key, value in copy do
942
+ if typeof(value) == "table" then
943
+ copy[key] = deepCopy(value)
944
+ end
945
+ end
946
+ return copy :: T
947
+ end
948
+ ```
949
+
950
+ ### String Patterns
951
+
952
+ Luau uses **Lua patterns**, which are NOT regular expressions. They are simpler and more limited.
953
+
954
+ ```luau
955
+ -- Character classes
956
+ -- %a letters %A non-letters
957
+ -- %d digits %D non-digits
958
+ -- %l lowercase %L non-lowercase
959
+ -- %u uppercase %U non-uppercase
960
+ -- %w alphanumeric %W non-alphanumeric
961
+ -- %s whitespace %S non-whitespace
962
+ -- %p punctuation %P non-punctuation
963
+ -- . any character
964
+ -- %% literal %
965
+
966
+ -- Quantifiers
967
+ -- * 0 or more (greedy)
968
+ -- + 1 or more (greedy)
969
+ -- - 0 or more (lazy)
970
+ -- ? 0 or 1
971
+
972
+ -- string.match: extract matches
973
+ local year, month, day = string.match("2026-03-04", "(%d+)-(%d+)-(%d+)")
974
+ print(year, month, day) --> "2026" "03" "04"
975
+
976
+ -- string.gmatch: iterate over all matches
977
+ local text = "score=100, level=42, health=75"
978
+ for key, value in string.gmatch(text, "(%w+)=(%d+)") do
979
+ print(key, value)
980
+ end
981
+
982
+ -- string.gsub: replace matches
983
+ local cleaned = string.gsub("Hello World", "%s+", " ")
984
+ print(cleaned) --> "Hello World"
985
+
986
+ -- Escaping pattern characters: use % before special chars
987
+ -- Special chars: ( ) . % + - * ? [ ] ^ $
988
+ local escaped = string.gsub("file.txt", "%.", "_")
989
+ print(escaped) --> "file_txt"
990
+
991
+ -- Anchors
992
+ -- ^ matches start of string
993
+ -- $ matches end of string
994
+ local isEmail = string.match("user@example.com", "^%w+@%w+%.%w+$") ~= nil
995
+ ```
996
+
997
+ ### Instance Tree Traversal
998
+
999
+ ```luau
1000
+ -- FindFirstChild: returns first direct child with name (or nil)
1001
+ local head = character:FindFirstChild("Head")
1002
+ if head then
1003
+ print("Found head")
1004
+ end
1005
+
1006
+ -- FindFirstChild with recursive flag
1007
+ local sword = workspace:FindFirstChild("Sword", true) -- searches entire subtree
1008
+
1009
+ -- FindFirstChildOfClass: by ClassName
1010
+ local humanoid = character:FindFirstChildOfClass("Humanoid")
1011
+
1012
+ -- FindFirstChildWhichIsA: by class hierarchy (includes inherited classes)
1013
+ local basePart = model:FindFirstChildWhichIsA("BasePart")
1014
+
1015
+ -- WaitForChild: yields until child exists (with optional timeout)
1016
+ local tool = player.Backpack:WaitForChild("Sword")
1017
+ local toolOrNil = player.Backpack:WaitForChild("Sword", 5) -- 5 second timeout
1018
+
1019
+ -- GetChildren: returns array of direct children
1020
+ local children = workspace:GetChildren()
1021
+ for _, child in children do
1022
+ print(child.Name)
1023
+ end
1024
+
1025
+ -- GetDescendants: returns array of ALL descendants (recursive)
1026
+ local allParts: { BasePart } = {}
1027
+ for _, descendant in workspace:GetDescendants() do
1028
+ if descendant:IsA("BasePart") then
1029
+ table.insert(allParts, descendant)
1030
+ end
1031
+ end
1032
+
1033
+ -- Filtering with CollectionService (tag-based)
1034
+ local CollectionService = game:GetService("CollectionService")
1035
+ local enemies = CollectionService:GetTagged("Enemy")
1036
+ for _, enemy in enemies do
1037
+ print(enemy.Name)
1038
+ end
1039
+
1040
+ -- Listen for tagged instances
1041
+ CollectionService:GetInstanceAddedSignal("Enemy"):Connect(function(instance)
1042
+ setupEnemy(instance)
1043
+ end)
1044
+
1045
+ CollectionService:GetInstanceRemovedSignal("Enemy"):Connect(function(instance)
1046
+ cleanupEnemy(instance)
1047
+ end)
1048
+ ```
1049
+
1050
+ ### Math Helpers
1051
+
1052
+ ```luau
1053
+ -- Clamping values
1054
+ local health = math.clamp(currentHealth, 0, MAX_HEALTH)
1055
+
1056
+ -- Linear interpolation
1057
+ local function lerp(a: number, b: number, t: number): number
1058
+ return a + (b - a) * t
1059
+ end
1060
+
1061
+ -- Mapping a value from one range to another
1062
+ local function map(value: number, inMin: number, inMax: number, outMin: number, outMax: number): number
1063
+ return outMin + (outMax - outMin) * ((value - inMin) / (inMax - inMin))
1064
+ end
1065
+
1066
+ -- Distance between two Vector3s
1067
+ local distance = (posA - posB).Magnitude
1068
+
1069
+ -- Normalized direction
1070
+ local direction = (target - origin).Unit
1071
+
1072
+ -- Rounding to decimal places
1073
+ local function roundTo(value: number, places: number): number
1074
+ local factor = 10 ^ places
1075
+ return math.round(value * factor) / factor
1076
+ end
1077
+ print(roundTo(3.14159, 2)) --> 3.14
1078
+ ```
1079
+
1080
+ ---
1081
+
1082
+ ## Best Practices
1083
+
1084
+ ### Naming Conventions
1085
+
1086
+ ```luau
1087
+ -- PascalCase: classes, modules, services, types, enums
1088
+ local CombatService = {}
1089
+ local WeaponManager = require(script.WeaponManager)
1090
+ type PlayerData = { name: string, level: number }
1091
+
1092
+ -- camelCase: variables, function names, method names, parameters
1093
+ local playerHealth = 100
1094
+ local function calculateDamage(baseDamage: number): number end
1095
+ function Weapon:getDurability(): number end
1096
+
1097
+ -- UPPER_CASE: constants
1098
+ local MAX_HEALTH = 100
1099
+ local RESPAWN_DELAY = 5
1100
+ local DEFAULT_SPEED = 16
1101
+
1102
+ -- Prefix private methods with underscore (convention, not enforced)
1103
+ function MyClass:_internalMethod() end
1104
+ local _cachedValue = nil
1105
+ ```
1106
+
1107
+ ### Module Structure
1108
+
1109
+ ```luau
1110
+ -- Standard module template
1111
+ -- File: ReplicatedStorage/Modules/InventoryManager.lua
1112
+
1113
+ -- Services at the top
1114
+ local ReplicatedStorage = game:GetService("ReplicatedStorage")
1115
+ local Players = game:GetService("Players")
1116
+
1117
+ -- Dependencies
1118
+ local Types = require(ReplicatedStorage.Shared.Types)
1119
+ local Signal = require(ReplicatedStorage.Packages.Signal)
1120
+
1121
+ -- Constants
1122
+ local MAX_SLOTS = 20
1123
+ local STACK_LIMIT = 99
1124
+
1125
+ -- Module table
1126
+ local InventoryManager = {}
1127
+
1128
+ -- Private state
1129
+ local inventories: { [Player]: Types.Inventory } = {}
1130
+
1131
+ -- Public API with type annotations
1132
+ function InventoryManager.getInventory(player: Player): Types.Inventory?
1133
+ return inventories[player]
1134
+ end
1135
+
1136
+ function InventoryManager.addItem(player: Player, itemId: string, quantity: number): boolean
1137
+ local inventory = inventories[player]
1138
+ if not inventory then
1139
+ return false
1140
+ end
1141
+ -- ... implementation
1142
+ return true
1143
+ end
1144
+
1145
+ -- Initialization
1146
+ function InventoryManager.init()
1147
+ Players.PlayerAdded:Connect(function(player: Player)
1148
+ inventories[player] = { slots = {}, gold = 0 }
1149
+ end)
1150
+
1151
+ Players.PlayerRemoving:Connect(function(player: Player)
1152
+ inventories[player] = nil
1153
+ end)
1154
+ end
1155
+
1156
+ return InventoryManager
1157
+ ```
1158
+
1159
+ ### Method Definitions
1160
+
1161
+ - Use `:` (colon) for instance methods - self is implicit
1162
+ - Use `.` (dot) for constructors and static methods - self must be explicit
1163
+
1164
+ ```luau
1165
+ -- : for instance methods (self is implicit)
1166
+ function MyClass:methodName()
1167
+ -- self refers to the instance
1168
+ end
1169
+
1170
+ -- . for constructors and static methods (self must be explicit)
1171
+ function MyClass.new()
1172
+ local self = setmetatable({}, MyClass)
1173
+ return self
1174
+ end
1175
+
1176
+ -- Calling conventions match definition
1177
+ obj:methodName() -- colon: self passed implicitly
1178
+ MyClass.new() -- dot: no self
1179
+ ```
1180
+
1181
+ **Key rule:** `:` is syntactic sugar for `.` with automatic `self` injection. `obj:method(a)` is equivalent to `obj.method(obj, a)`.
1182
+
1183
+ ### General Guidelines
1184
+
1185
+ - Use `local` for every variable and function declaration.
1186
+ - Add type annotations on all public module function signatures.
1187
+ - Use `task.wait()` / `task.spawn()` / `task.delay()` / `task.defer()` instead of deprecated globals.
1188
+ - Use `typeof()` instead of `type()` for Roblox-aware type checking.
1189
+ - Set `Instance.Parent` last after configuring all properties (avoids unnecessary replication and change events).
1190
+ - Clean up event connections and instances when no longer needed to avoid memory leaks.
1191
+ - Validate all data received from clients on the server. Never trust the client.
1192
+ - Use `pcall` / `xpcall` around any call that can fail (DataStores, HTTP, etc.).
1193
+ - Use backtick interpolation (`{expr}`) for all string building. Never use `..` concatenation.
1194
+ - Use `table.freeze()` for configuration tables that should not be modified.
1195
+ - Never use Luau reserved keywords (`return`, `continue`, `local`, `end`, `function`, etc.) as identifiers - parameter names, local variables, function names, or module methods like `module:return()` all cause parse-time syntax errors.
1196
+ - Declare local functions before they are called - Luau has no hoisting. Callees above callers. Use forward declaration (`local fnName`) for mutual recursion.
1197
+
1198
+ ---
1199
+
1200
+ ## Anti-Patterns
1201
+
1202
+ ### Deprecated Global Functions
1203
+
1204
+ ```luau
1205
+ -- BAD: deprecated, unpredictable resume timing, no cancellation
1206
+ wait(2)
1207
+ spawn(function() end)
1208
+ delay(2, function() end)
1209
+
1210
+ -- GOOD: modern task library equivalents
1211
+ task.wait(2)
1212
+ task.spawn(function() end)
1213
+ task.delay(2, function() end)
1214
+ ```
1215
+
1216
+ ### Polling Instead of Events
1217
+
1218
+ ```luau
1219
+ -- BAD: polling wastes CPU cycles
1220
+ while true do
1221
+ local target = findNearestEnemy()
1222
+ if target then
1223
+ attack(target)
1224
+ end
1225
+ task.wait(0.1)
1226
+ end
1227
+
1228
+ -- GOOD: use events or Heartbeat with state checks
1229
+ local RunService = game:GetService("RunService")
1230
+ RunService.Heartbeat:Connect(function(dt: number)
1231
+ local target = findNearestEnemy()
1232
+ if target then
1233
+ attack(target)
1234
+ end
1235
+ end)
1236
+
1237
+ -- GOOD: use events when possible
1238
+ CollectionService:GetInstanceAddedSignal("Enemy"):Connect(function(enemy)
1239
+ onEnemySpawned(enemy)
1240
+ end)
1241
+ ```
1242
+
1243
+ ### String Concatenation
1244
+
1245
+ ```luau
1246
+ -- BAD: .. concatenation is verbose and error-prone in hot paths
1247
+ local greeting = "Hello, " .. name .. "!"
1248
+
1249
+ -- BAD: creates a new string every iteration (O(n^2) memory)
1250
+ local result = ""
1251
+ for i = 1, 1000 do
1252
+ result = result .. tostring(i) .. ","
1253
+ end
1254
+
1255
+ -- GOOD: use backtick interpolation for all string building
1256
+ local greeting = `Hello, {name}!`
1257
+
1258
+ -- GOOD: collect into table, join once for loops (O(n))
1259
+ local parts = {}
1260
+ for i = 1, 1000 do
1261
+ table.insert(parts, tostring(i))
1262
+ end
1263
+ local result = table.concat(parts, ",")
1264
+ ```
1265
+
1266
+ ### Global Variables
1267
+
1268
+ ```luau
1269
+ -- BAD: pollutes shared environment, hard to track, no type checking
1270
+ score = 0
1271
+ function updateScore(amount)
1272
+ score += amount
1273
+ end
1274
+
1275
+ -- GOOD: local variables, module scope
1276
+ local score = 0
1277
+ local function updateScore(amount: number)
1278
+ score += amount
1279
+ end
1280
+ ```
1281
+
1282
+ ### Missing pcall on Fallible Calls
1283
+
1284
+ ```luau
1285
+ -- BAD: crashes the script if the call fails
1286
+ local data = dataStore:GetAsync("key")
1287
+ local response = HttpService:RequestAsync({ Url = "https://api.example.com" })
1288
+
1289
+ -- GOOD: wrap in pcall
1290
+ local success, data = pcall(dataStore.GetAsync, dataStore, "key")
1291
+ if not success then
1292
+ warn("DataStore read failed:", data)
1293
+ data = {} -- fallback
1294
+ end
1295
+
1296
+ local success, response = pcall(HttpService.RequestAsync, HttpService, {
1297
+ Url = "https://api.example.com",
1298
+ })
1299
+ if not success then
1300
+ warn("HTTP request failed:", response)
1301
+ end
1302
+ ```
1303
+
1304
+ ### Trusting Client Input
1305
+
1306
+ For server-authoritative validation patterns (type checking, range checking, ownership, rate limiting), see **roblox-networking** → Client Validation.
1307
+
1308
+ **Core rule:** Never trust client input. Every `OnServerEvent` handler must validate types, ranges, and ownership before processing.
1309
+
1310
+ ---
1311
+
1312
+ ## Sharp Edges
1313
+
1314
+ ### 1-Based Indexing
1315
+
1316
+ Luau arrays are 1-indexed. The first element is `array[1]`, not `array[0]`.
1317
+
1318
+ ```luau
1319
+ local items = { "first", "second", "third" }
1320
+ print(items[1]) --> "first"
1321
+ print(items[0]) --> nil (NOT an error, just nil)
1322
+
1323
+ -- Off-by-one errors are common when porting from other languages
1324
+ for i = 1, #items do -- correct: 1 to length
1325
+ print(items[i])
1326
+ end
1327
+ ```
1328
+
1329
+ ### The `#` Operator and Nil Gaps
1330
+
1331
+ The `#` (length) operator is only reliable for **contiguous arrays** with no nil gaps.
1332
+
1333
+ ```luau
1334
+ -- Reliable: contiguous array
1335
+ local a = { 1, 2, 3, 4, 5 }
1336
+ print(#a) --> 5 (correct)
1337
+
1338
+ -- UNRELIABLE: array with nil gap
1339
+ local b = { 1, 2, nil, 4, 5 }
1340
+ print(#b) --> could be 2 or 5 (undefined behavior!)
1341
+
1342
+ -- The length operator finds ANY valid boundary where t[n] ~= nil and t[n+1] == nil
1343
+ -- With gaps, multiple boundaries exist, and the result is unpredictable
1344
+
1345
+ -- SAFE: if you need to handle sparse data, use a dictionary with explicit count
1346
+ local sparse: { [number]: string } = {}
1347
+ local count = 0
1348
+ sparse[1] = "a"
1349
+ count += 1
1350
+ sparse[5] = "e"
1351
+ count += 1
1352
+ -- Use count, not #sparse
1353
+ ```
1354
+
1355
+ ### Nil in Tables
1356
+
1357
+ ```luau
1358
+ -- Setting a table value to nil REMOVES the key
1359
+ local t = { a = 1, b = 2, c = 3 }
1360
+ t.b = nil
1361
+ -- t is now { a = 1, c = 3 } - "b" key no longer exists
1362
+
1363
+ -- This means you cannot store nil as a meaningful value in a table
1364
+ -- Use a sentinel value instead if you need to distinguish "absent" from "nil"
1365
+ local NONE = newproxy(false) -- unique sentinel
1366
+ local cache = {}
1367
+ cache["key"] = NONE -- means "we checked, value is absent"
1368
+ -- cache["other"] is nil, meaning "we haven't checked yet"
1369
+
1370
+ -- nil in arrays causes gaps (see # operator issue above)
1371
+ local list = { 1, 2, 3 }
1372
+ list[2] = nil -- creates a gap - DO NOT DO THIS
1373
+ -- Use table.remove(list, 2) instead to shift elements down
1374
+ ```
1375
+
1376
+ ### Metatables: Powerful but Error-Prone
1377
+
1378
+ ```luau
1379
+ -- Common mistake: forgetting __index
1380
+ local MyClass = {}
1381
+ -- Missing: MyClass.__index = MyClass
1382
+
1383
+ function MyClass.new()
1384
+ return setmetatable({}, MyClass)
1385
+ end
1386
+
1387
+ function MyClass:doSomething()
1388
+ print("doing something")
1389
+ end
1390
+
1391
+ local obj = MyClass.new()
1392
+ obj:doSomething() --> ERROR: attempt to call a nil value
1393
+ -- Because __index is not set, method lookup fails
1394
+
1395
+ -- See ### Method Definitions in Best Practices for . vs : conventions
1396
+
1397
+ -- Common mistake: modifying the metatable instead of the instance
1398
+ function MyClass:setName(name: string)
1399
+ -- BAD: this sets it on the class table, shared by all instances!
1400
+ MyClass.name = name
1401
+
1402
+ -- GOOD: set on the instance
1403
+ self.name = name
1404
+ end
1405
+ ```
1406
+
1407
+ ### Reserved Keywords as Identifiers
1408
+
1409
+ Luau reserves certain words for the language syntax. These cannot be used as identifiers - variable names, function names, parameter names, or module method names:
1410
+
1411
+ ```
1412
+ and, break, do, else, elseif, end, false, for, function, if, in,
1413
+ local, nil, not, or, repeat, return, then, true, until, while,
1414
+ continue (Luau-specific)
1415
+ ```
1416
+
1417
+ ```luau
1418
+ -- BAD: keyword used as parameter name - syntax error
1419
+ local function onComplete(return: number) end -- ERROR
1420
+ local function process(continue: boolean) end -- ERROR
1421
+
1422
+ -- BAD: keyword used as module method - syntax error
1423
+ local module = {}
1424
+ function module:return() end -- ERROR
1425
+ function module:continue() end -- ERROR
1426
+ ```
1427
+
1428
+ Simply rename to avoid the keyword:
1429
+
1430
+ ```luau
1431
+ -- GOOD: renamed to avoid reserved keyword
1432
+ local function onComplete(result: number) end
1433
+ local function process(shouldContinue: boolean) end
1434
+
1435
+ local module = {}
1436
+ function module:onReturn() end
1437
+ function module:onResume() end
1438
+ ```
1439
+
1440
+ ### Local Function Declaration Order
1441
+
1442
+ Luau has no hoisting - a `local function` is invisible to code above its declaration. This is a common pitfall because many languages (JS, Lua, Python) do hoist function declarations.
1443
+
1444
+ ```luau
1445
+ -- BAD: helperB is nil when functionA runs
1446
+ local function functionA()
1447
+ helperB() -- ERROR: attempt to call a nil value
1448
+ end
1449
+
1450
+ local function helperB()
1451
+ print("helper")
1452
+ end
1453
+
1454
+ -- GOOD: callee declared before caller
1455
+ local function helperB()
1456
+ print("helper")
1457
+ end
1458
+
1459
+ local function functionA()
1460
+ helperB() -- works
1461
+ end
1462
+ ```
1463
+
1464
+ For mutual recursion (A calls B, B calls A), use a forward declaration:
1465
+
1466
+ ```luau
1467
+ local functionB -- forward declaration (declares variable, no assignment)
1468
+
1469
+ local function functionA(x: number)
1470
+ if x <= 0 then return end
1471
+ functionB(x - 1)
1472
+ end
1473
+
1474
+ function functionB(x: number) -- no 'local' here (already declared above)
1475
+ if x <= 0 then return end
1476
+ functionA(x - 1)
1477
+ end
1478
+ ```
1479
+
1480
+ **Rule:** Callees above callers, always. If a `local function` is called by code above its definition, that is a runtime nil-error bug.
1481
+
1482
+ ### Equality and Type Coercion
1483
+
1484
+ ```luau
1485
+ -- Luau does NOT coerce types in comparisons (unlike JavaScript)
1486
+ print(0 == "0") --> false
1487
+ print(1 == true) --> false
1488
+ print("" == false) --> false
1489
+
1490
+ -- Only nil and false are falsy
1491
+ -- 0, "", and empty tables are TRUTHY
1492
+ if 0 then print("0 is truthy") end --> prints
1493
+ if "" then print("empty string is truthy") end --> prints
1494
+ if {} then print("empty table is truthy") end --> prints
1495
+
1496
+ -- This means you cannot use `if value then` to check for empty strings or zero
1497
+ -- Be explicit:
1498
+ if value ~= nil and value ~= "" then end
1499
+ if value ~= nil and value ~= 0 then end
1500
+ ```
1501
+
1502
+ ### Table Reference Semantics
1503
+
1504
+ ```luau
1505
+ -- Tables are passed and assigned by REFERENCE, not by value
1506
+ local original = { 1, 2, 3 }
1507
+ local alias = original
1508
+ alias[1] = 99
1509
+ print(original[1]) --> 99 (both point to the same table)
1510
+
1511
+ -- To get an independent copy, use table.clone (shallow) or a deep copy function
1512
+ local copy = table.clone(original)
1513
+ copy[1] = 0
1514
+ print(original[1]) --> 99 (unaffected)
1515
+
1516
+ -- But nested tables are still shared in a shallow clone
1517
+ local nested = { data = { 1, 2, 3 } }
1518
+ local shallowCopy = table.clone(nested)
1519
+ shallowCopy.data[1] = 99
1520
+ print(nested.data[1]) --> 99 (shared reference!)
1521
+ -- Use a deep copy for nested structures
1522
+ ```
1523
+
1524
+ ### Scope and Closures
1525
+
1526
+ ```luau
1527
+ -- Common loop closure bug
1528
+ local functions = {}
1529
+ for i = 1, 5 do
1530
+ functions[i] = function()
1531
+ return i
1532
+ end
1533
+ end
1534
+ -- In Luau, each loop iteration creates a new 'i' variable,
1535
+ -- so this actually works correctly (unlike some other languages)
1536
+ print(functions[1]()) --> 1
1537
+ print(functions[5]()) --> 5
1538
+
1539
+ -- But watch out with while loops - the variable is shared
1540
+ local fns = {}
1541
+ local i = 1
1542
+ while i <= 5 do
1543
+ fns[i] = function()
1544
+ return i
1545
+ end
1546
+ i += 1
1547
+ end
1548
+ print(fns[1]()) --> 6 (all functions share the same 'i' which is now 6)
1549
+
1550
+ -- Fix: capture the value in a local
1551
+ local fns2 = {}
1552
+ local j = 1
1553
+ while j <= 5 do
1554
+ local captured = j
1555
+ fns2[j] = function()
1556
+ return captured
1557
+ end
1558
+ j += 1
1559
+ end
1560
+ print(fns2[1]()) --> 1 (correct)
1561
+ ```
1562
+
1563
+ ---
1564
+
1565
+ ## JS → Luau Translation Table
1566
+
1567
+ AI models trained on JavaScript commonly generate patterns that don't exist in Luau. This table covers the most frequent mistakes.
1568
+
1569
+ | JavaScript | Luau | Notes |
1570
+ |------------|------|-------|
1571
+ | `arr.map(fn)` | `table.create(#arr)` + for loop, or use a utility | No built-in map/filter/reduce on tables |
1572
+ | `arr.filter(fn)` | Loop with `table.insert` into new table | No built-in filter |
1573
+ | `arr.find(fn)` | Loop with early return | No built-in find |
1574
+ | `arr.includes(x)` | `table.find(arr, x) ~= nil` | Returns index or nil |
1575
+ | `arr.push(x)` | `table.insert(arr, x)` | |
1576
+ | `arr.pop()` | `table.remove(arr)` | Removes and returns last element |
1577
+ | `arr.splice(i, n)` | `table.remove(arr, i)` in a loop | No splice equivalent |
1578
+ | `arr.length` or `arr.length` | `#arr` | `#` operator, not a property |
1579
+ | `obj.keys(x)` | No direct equivalent - use `for k in x do` | |
1580
+ | `obj.values(x)` | `for _, v in x do` | |
1581
+ | `Object.assign(a, b)` | `for k, v in b do a[k] = v end` | No spread operator |
1582
+ | `const x = ...` | `local x = ...` | No const/let/var |
1583
+ | `let x = ...` | `local x = ...` | |
1584
+ | `function(x) { return x }` | `function(x) return x end` | No arrow functions |
1585
+ | `(x) => x * 2` | `function(x) return x * 2 end` | No arrow functions |
1586
+ | `x === y` | `x == y` | No `===` in Luau, `==` is strict |
1587
+ | `x !== y` | `x ~= y` | Not `!=` |
1588
+ | `null` | `nil` | No null/undefined distinction |
1589
+ | `typeof x` | `typeof(x)` for Roblox types, `type(x)` for Luau types | Parentheses required |
1590
+ | `console.log(x)` | `print(x)` | |
1591
+ | `x ?? y` | `x or y` | Luau `or` returns the value, not a boolean |
1592
+ | `x?.y` | `x and x.y` | No optional chaining |
1593
+ | `{...obj}` | Manual table copy with loop | No spread operator |
1594
+ | `[...arr]` | Manual copy with loop or `table.move` | No spread operator |
1595
+ | `new Map()` | Regular table `{}` | Luau tables are dictionaries by default |
1596
+ | `new Set()` | `{[value] = true}` pattern | Use table as set |
1597
+ | `Promise.all(arr)` | `Promise.all(arr)` | Same if using evaera/Promise |
1598
+ | `async/await` | `coroutine` or Promise chains | No async/await syntax |
1599
+ | `try/catch` | `pcall(fn)` or `xpcall(fn, handler)` | No try/catch |
1600
+ | `throw error` | `error("message")` | |
1601
+ | `class Foo { }` | `local Foo = {} Foo.__index = Foo` | Prototype-based OOP |
1602
+ | `new Foo()` | `setmetatable({}, Foo)` | |
1603
+ | `import x from "y"` | `local x = require(y)` | No ES modules |
1604
+ | `export default` | `return module` | Module returns its public API |
1605
+ | `str1 + str2` | `` `{str1}{str2}` `` | Use backtick interpolation, NOT `..` |
1606
+ | `"hello " + name` | `` `hello {name}` `` | Backticks are the Luau way |
1607
+
1608
+ ### Type-Specific Confusion
1609
+
1610
+ | JavaScript | Luau | Why AI Gets It Wrong |
1611
+ |------------|------|---------------------|
1612
+ | `0 == ""` → `true` | `0 == ""` → `false` | Luau has no type coercion in `==` |
1613
+ | `"" == false` → `true` | `"" == false` → `false` | Only `nil` and `false` are falsy |
1614
+ | `if (0)` → falsy | `if 0 then` → truthy | `0`, `""`, `{}` are all truthy in Luau |
1615
+ | `x = null` → typeof `object` | `x = nil` → type `nil` | No null/undefined split |
1616
+ | `Array.isArray(x)` | `type(x) == "table"` | No Array type distinction |
1617
+ | `x.push()` on string | N/A - strings are not indexable | No string methods, use `string.*` library |
1618
+