roblox-opencode 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/README.md +122 -0
  2. package/commands/setup-game.md +108 -0
  3. package/commands/sync-check.md +53 -0
  4. package/core/roblox-core.md +93 -0
  5. package/dist/server.js +167 -0
  6. package/package.json +35 -0
  7. package/skills/roblox-analytics/SKILL.md +277 -0
  8. package/skills/roblox-analytics/references/event-batcher.luau +75 -0
  9. package/skills/roblox-animation-vfx/SKILL.md +1325 -0
  10. package/skills/roblox-architecture/SKILL.md +863 -0
  11. package/skills/roblox-architecture/references/combat-systems.md +1381 -0
  12. package/skills/roblox-code-review/SKILL.md +687 -0
  13. package/skills/roblox-data/SKILL.md +889 -0
  14. package/skills/roblox-data/references/inventory-systems.md +1729 -0
  15. package/skills/roblox-debug/SKILL.md +99 -0
  16. package/skills/roblox-gui/SKILL.md +1103 -0
  17. package/skills/roblox-gui-fusion/SKILL.md +150 -0
  18. package/skills/roblox-gui-fusion/references/inventory.luau +427 -0
  19. package/skills/roblox-gui-fusion/references/settings-menu.luau +579 -0
  20. package/skills/roblox-gui-fusion/references/shop.luau +411 -0
  21. package/skills/roblox-luau-mastery/SKILL.md +1519 -0
  22. package/skills/roblox-monetization/SKILL.md +1084 -0
  23. package/skills/roblox-monetization/references/process-receipt.luau +131 -0
  24. package/skills/roblox-networking/SKILL.md +669 -0
  25. package/skills/roblox-networking/references/remote-validator.luau +193 -0
  26. package/skills/roblox-publish-checklist/SKILL.md +128 -0
  27. package/skills/roblox-runtime/SKILL.md +753 -0
  28. package/skills/roblox-sharp-edges/SKILL.md +295 -0
  29. package/skills/roblox-sync/SKILL.md +126 -0
  30. package/skills/roblox-testing/SKILL.md +943 -0
  31. package/skills/roblox-tooling/SKILL.md +150 -0
  32. package/vendor/LICENSES/ProfileStore-LICENSE +201 -0
  33. package/vendor/LICENSES/RbxUtil-LICENSE +7 -0
  34. package/vendor/LICENSES/promise-LICENSE +21 -0
  35. package/vendor/LICENSES/t-LICENSE +21 -0
  36. package/vendor/LICENSES/testez-LICENSE +201 -0
  37. package/vendor/README.md +84 -0
  38. package/vendor/fusion/Animation/ExternalTime.luau +84 -0
  39. package/vendor/fusion/Animation/Spring.luau +322 -0
  40. package/vendor/fusion/Animation/Stopwatch.luau +128 -0
  41. package/vendor/fusion/Animation/Tween.luau +187 -0
  42. package/vendor/fusion/Animation/getTweenDuration.luau +27 -0
  43. package/vendor/fusion/Animation/getTweenRatio.luau +47 -0
  44. package/vendor/fusion/Animation/lerpType.luau +164 -0
  45. package/vendor/fusion/Animation/packType.luau +100 -0
  46. package/vendor/fusion/Animation/springCoefficients.luau +80 -0
  47. package/vendor/fusion/Animation/unpackType.luau +103 -0
  48. package/vendor/fusion/Colour/Oklab.luau +70 -0
  49. package/vendor/fusion/Colour/sRGB.luau +55 -0
  50. package/vendor/fusion/External.luau +168 -0
  51. package/vendor/fusion/ExternalDebug.luau +70 -0
  52. package/vendor/fusion/Graph/Observer.luau +114 -0
  53. package/vendor/fusion/Graph/castToGraph.luau +29 -0
  54. package/vendor/fusion/Graph/change.luau +81 -0
  55. package/vendor/fusion/Graph/depend.luau +33 -0
  56. package/vendor/fusion/Graph/evaluate.luau +56 -0
  57. package/vendor/fusion/Instances/Attribute.luau +58 -0
  58. package/vendor/fusion/Instances/AttributeChange.luau +47 -0
  59. package/vendor/fusion/Instances/AttributeOut.luau +63 -0
  60. package/vendor/fusion/Instances/Child.luau +21 -0
  61. package/vendor/fusion/Instances/Children.luau +148 -0
  62. package/vendor/fusion/Instances/Hydrate.luau +33 -0
  63. package/vendor/fusion/Instances/New.luau +53 -0
  64. package/vendor/fusion/Instances/OnChange.luau +50 -0
  65. package/vendor/fusion/Instances/OnEvent.luau +54 -0
  66. package/vendor/fusion/Instances/Out.luau +69 -0
  67. package/vendor/fusion/Instances/applyInstanceProps.luau +149 -0
  68. package/vendor/fusion/Instances/defaultProps.luau +194 -0
  69. package/vendor/fusion/LICENSE +21 -0
  70. package/vendor/fusion/Logging/formatError.luau +49 -0
  71. package/vendor/fusion/Logging/messages.luau +52 -0
  72. package/vendor/fusion/Logging/parseError.luau +25 -0
  73. package/vendor/fusion/Memory/checkLifetime.luau +134 -0
  74. package/vendor/fusion/Memory/deriveScope.luau +24 -0
  75. package/vendor/fusion/Memory/deriveScopeImpl.luau +45 -0
  76. package/vendor/fusion/Memory/doCleanup.luau +79 -0
  77. package/vendor/fusion/Memory/innerScope.luau +34 -0
  78. package/vendor/fusion/Memory/legacyCleanup.luau +18 -0
  79. package/vendor/fusion/Memory/needsDestruction.luau +17 -0
  80. package/vendor/fusion/Memory/poisonScope.luau +34 -0
  81. package/vendor/fusion/Memory/scopePool.luau +55 -0
  82. package/vendor/fusion/Memory/scoped.luau +27 -0
  83. package/vendor/fusion/Memory/whichLivesLonger.luau +75 -0
  84. package/vendor/fusion/RobloxExternal.luau +98 -0
  85. package/vendor/fusion/State/Computed.luau +139 -0
  86. package/vendor/fusion/State/For/Disassembly.luau +211 -0
  87. package/vendor/fusion/State/For/ForTypes.luau +30 -0
  88. package/vendor/fusion/State/For/init.luau +110 -0
  89. package/vendor/fusion/State/ForKeys.luau +94 -0
  90. package/vendor/fusion/State/ForPairs.luau +97 -0
  91. package/vendor/fusion/State/ForValues.luau +94 -0
  92. package/vendor/fusion/State/Value.luau +88 -0
  93. package/vendor/fusion/State/castToState.luau +26 -0
  94. package/vendor/fusion/State/peek.luau +31 -0
  95. package/vendor/fusion/State/updateAll.luau +1 -0
  96. package/vendor/fusion/Types.luau +314 -0
  97. package/vendor/fusion/Utility/Contextual.luau +91 -0
  98. package/vendor/fusion/Utility/Safe.luau +23 -0
  99. package/vendor/fusion/Utility/isSimilar.luau +29 -0
  100. package/vendor/fusion/Utility/merge.luau +35 -0
  101. package/vendor/fusion/Utility/nameOf.luau +35 -0
  102. package/vendor/fusion/Utility/never.luau +14 -0
  103. package/vendor/fusion/Utility/nicknames.luau +11 -0
  104. package/vendor/fusion/Utility/xtypeof.luau +27 -0
  105. package/vendor/fusion/init.luau +82 -0
  106. package/vendor/profilestore/init.luau +2243 -0
  107. package/vendor/promise/init.luau +1982 -0
  108. package/vendor/rbxutil/buffer-util/Buffer.test.luau +25 -0
  109. package/vendor/rbxutil/buffer-util/BufferReader.luau +228 -0
  110. package/vendor/rbxutil/buffer-util/BufferWriter.luau +269 -0
  111. package/vendor/rbxutil/buffer-util/DataTypeBuffer.luau +223 -0
  112. package/vendor/rbxutil/buffer-util/Types.luau +60 -0
  113. package/vendor/rbxutil/buffer-util/index.d.ts +153 -0
  114. package/vendor/rbxutil/buffer-util/init.luau +41 -0
  115. package/vendor/rbxutil/buffer-util/package.json +16 -0
  116. package/vendor/rbxutil/buffer-util/wally.toml +9 -0
  117. package/vendor/rbxutil/comm/Client/ClientComm.luau +232 -0
  118. package/vendor/rbxutil/comm/Client/ClientRemoteProperty.luau +156 -0
  119. package/vendor/rbxutil/comm/Client/ClientRemoteSignal.luau +109 -0
  120. package/vendor/rbxutil/comm/Client/init.luau +135 -0
  121. package/vendor/rbxutil/comm/Server/RemoteProperty.luau +295 -0
  122. package/vendor/rbxutil/comm/Server/RemoteSignal.luau +211 -0
  123. package/vendor/rbxutil/comm/Server/ServerComm.luau +211 -0
  124. package/vendor/rbxutil/comm/Server/init.luau +140 -0
  125. package/vendor/rbxutil/comm/Types.luau +18 -0
  126. package/vendor/rbxutil/comm/Util.luau +27 -0
  127. package/vendor/rbxutil/comm/init.luau +35 -0
  128. package/vendor/rbxutil/comm/wally.toml +13 -0
  129. package/vendor/rbxutil/component/init.luau +759 -0
  130. package/vendor/rbxutil/component/init.test.luau +311 -0
  131. package/vendor/rbxutil/component/wally.toml +14 -0
  132. package/vendor/rbxutil/concur/init.luau +542 -0
  133. package/vendor/rbxutil/concur/init.test.luau +364 -0
  134. package/vendor/rbxutil/concur/wally.toml +8 -0
  135. package/vendor/rbxutil/enum-list/init.luau +101 -0
  136. package/vendor/rbxutil/enum-list/init.test.luau +91 -0
  137. package/vendor/rbxutil/enum-list/wally.toml +8 -0
  138. package/vendor/rbxutil/find/index.d.ts +20 -0
  139. package/vendor/rbxutil/find/init.luau +44 -0
  140. package/vendor/rbxutil/find/package.json +17 -0
  141. package/vendor/rbxutil/find/wally.toml +8 -0
  142. package/vendor/rbxutil/input/Gamepad.luau +559 -0
  143. package/vendor/rbxutil/input/Keyboard.luau +124 -0
  144. package/vendor/rbxutil/input/Mouse.luau +278 -0
  145. package/vendor/rbxutil/input/PreferredInput.luau +91 -0
  146. package/vendor/rbxutil/input/Touch.luau +120 -0
  147. package/vendor/rbxutil/input/init.luau +33 -0
  148. package/vendor/rbxutil/input/wally.toml +12 -0
  149. package/vendor/rbxutil/loader/index.d.ts +15 -0
  150. package/vendor/rbxutil/loader/init.luau +137 -0
  151. package/vendor/rbxutil/loader/wally.toml +8 -0
  152. package/vendor/rbxutil/log/index.d.ts +38 -0
  153. package/vendor/rbxutil/log/init.luau +746 -0
  154. package/vendor/rbxutil/log/wally.toml +8 -0
  155. package/vendor/rbxutil/net/init.luau +190 -0
  156. package/vendor/rbxutil/net/wally.toml +8 -0
  157. package/vendor/rbxutil/option/index.d.ts +44 -0
  158. package/vendor/rbxutil/option/init.luau +489 -0
  159. package/vendor/rbxutil/option/init.test.luau +342 -0
  160. package/vendor/rbxutil/option/wally.toml +8 -0
  161. package/vendor/rbxutil/pid/index.d.ts +53 -0
  162. package/vendor/rbxutil/pid/init.luau +195 -0
  163. package/vendor/rbxutil/pid/package.json +16 -0
  164. package/vendor/rbxutil/pid/wally.toml +9 -0
  165. package/vendor/rbxutil/quaternion/index.d.ts +117 -0
  166. package/vendor/rbxutil/quaternion/init.luau +570 -0
  167. package/vendor/rbxutil/quaternion/package.json +16 -0
  168. package/vendor/rbxutil/quaternion/wally.toml +9 -0
  169. package/vendor/rbxutil/query/index.d.ts +43 -0
  170. package/vendor/rbxutil/query/init.luau +117 -0
  171. package/vendor/rbxutil/query/package.json +18 -0
  172. package/vendor/rbxutil/query/wally.toml +9 -0
  173. package/vendor/rbxutil/sequent/index.d.ts +28 -0
  174. package/vendor/rbxutil/sequent/init.luau +340 -0
  175. package/vendor/rbxutil/sequent/package.json +16 -0
  176. package/vendor/rbxutil/sequent/wally.toml +9 -0
  177. package/vendor/rbxutil/ser/init.luau +175 -0
  178. package/vendor/rbxutil/ser/init.test.luau +50 -0
  179. package/vendor/rbxutil/ser/wally.toml +11 -0
  180. package/vendor/rbxutil/shake/index.d.ts +36 -0
  181. package/vendor/rbxutil/shake/init.luau +532 -0
  182. package/vendor/rbxutil/shake/init.test.luau +267 -0
  183. package/vendor/rbxutil/shake/package.json +16 -0
  184. package/vendor/rbxutil/shake/wally.toml +9 -0
  185. package/vendor/rbxutil/signal/index.d.ts +100 -0
  186. package/vendor/rbxutil/signal/init.luau +432 -0
  187. package/vendor/rbxutil/signal/init.test.luau +190 -0
  188. package/vendor/rbxutil/signal/package.json +17 -0
  189. package/vendor/rbxutil/signal/wally.toml +9 -0
  190. package/vendor/rbxutil/silo/TableWatcher.luau +65 -0
  191. package/vendor/rbxutil/silo/Util.luau +55 -0
  192. package/vendor/rbxutil/silo/init.luau +338 -0
  193. package/vendor/rbxutil/silo/init.test.luau +215 -0
  194. package/vendor/rbxutil/silo/wally.toml +8 -0
  195. package/vendor/rbxutil/spring/index.d.ts +40 -0
  196. package/vendor/rbxutil/spring/init.luau +97 -0
  197. package/vendor/rbxutil/spring/package.json +17 -0
  198. package/vendor/rbxutil/spring/wally.toml +8 -0
  199. package/vendor/rbxutil/stream/index.d.ts +88 -0
  200. package/vendor/rbxutil/stream/init.luau +597 -0
  201. package/vendor/rbxutil/stream/package.json +18 -0
  202. package/vendor/rbxutil/stream/wally.toml +9 -0
  203. package/vendor/rbxutil/streamable/Streamable.luau +202 -0
  204. package/vendor/rbxutil/streamable/StreamableUtil.luau +80 -0
  205. package/vendor/rbxutil/streamable/init.luau +8 -0
  206. package/vendor/rbxutil/streamable/wally.toml +12 -0
  207. package/vendor/rbxutil/symbol/init.luau +56 -0
  208. package/vendor/rbxutil/symbol/init.test.luau +37 -0
  209. package/vendor/rbxutil/symbol/wally.toml +8 -0
  210. package/vendor/rbxutil/table-util/init.luau +938 -0
  211. package/vendor/rbxutil/table-util/init.test.luau +439 -0
  212. package/vendor/rbxutil/table-util/wally.toml +8 -0
  213. package/vendor/rbxutil/task-queue/index.d.ts +27 -0
  214. package/vendor/rbxutil/task-queue/init.luau +97 -0
  215. package/vendor/rbxutil/task-queue/wally.toml +8 -0
  216. package/vendor/rbxutil/timer/index.d.ts +81 -0
  217. package/vendor/rbxutil/timer/init.luau +249 -0
  218. package/vendor/rbxutil/timer/init.test.luau +73 -0
  219. package/vendor/rbxutil/timer/wally.toml +11 -0
  220. package/vendor/rbxutil/tree/index.d.ts +15 -0
  221. package/vendor/rbxutil/tree/init.luau +137 -0
  222. package/vendor/rbxutil/tree/wally.toml +8 -0
  223. package/vendor/rbxutil/trove/index.d.ts +46 -0
  224. package/vendor/rbxutil/trove/init.luau +787 -0
  225. package/vendor/rbxutil/trove/init.test.luau +203 -0
  226. package/vendor/rbxutil/trove/wally.toml +8 -0
  227. package/vendor/rbxutil/typed-remote/init.luau +196 -0
  228. package/vendor/rbxutil/typed-remote/wally.toml +8 -0
  229. package/vendor/rbxutil/wait-for/index.d.ts +17 -0
  230. package/vendor/rbxutil/wait-for/init.luau +257 -0
  231. package/vendor/rbxutil/wait-for/init.test.luau +182 -0
  232. package/vendor/rbxutil/wait-for/wally.toml +11 -0
  233. package/vendor/t/t.lua +1350 -0
  234. package/vendor/testez/Context.lua +26 -0
  235. package/vendor/testez/Expectation.lua +311 -0
  236. package/vendor/testez/ExpectationContext.lua +38 -0
  237. package/vendor/testez/LifecycleHooks.lua +89 -0
  238. package/vendor/testez/Reporters/TeamCityReporter.lua +102 -0
  239. package/vendor/testez/Reporters/TextReporter.lua +106 -0
  240. package/vendor/testez/Reporters/TextReporterQuiet.lua +97 -0
  241. package/vendor/testez/TestBootstrap.lua +147 -0
  242. package/vendor/testez/TestEnum.lua +28 -0
  243. package/vendor/testez/TestPlan.lua +304 -0
  244. package/vendor/testez/TestPlanner.lua +40 -0
  245. package/vendor/testez/TestResults.lua +112 -0
  246. package/vendor/testez/TestRunner.lua +188 -0
  247. package/vendor/testez/TestSession.lua +243 -0
  248. package/vendor/testez/init.lua +40 -0
@@ -0,0 +1,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-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
+