roblox-opencode 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +112 -122
- package/commands/setup-game.md +108 -108
- package/commands/sync-check.md +53 -53
- package/core/roblox-core.md +93 -93
- package/dist/server.js +189 -167
- package/package.json +35 -35
- package/skills/roblox-analytics/SKILL.md +277 -277
- package/skills/roblox-analytics/references/event-batcher.luau +75 -75
- package/skills/roblox-animation-vfx/SKILL.md +1325 -1325
- package/skills/roblox-architecture/SKILL.md +863 -863
- package/skills/roblox-architecture/references/combat-systems.md +1381 -1381
- package/skills/roblox-code-review/SKILL.md +686 -686
- package/skills/roblox-data/SKILL.md +889 -889
- package/skills/roblox-data/references/inventory-systems.md +1729 -1729
- package/skills/roblox-debug/SKILL.md +98 -98
- package/skills/roblox-gui/SKILL.md +1103 -1103
- package/skills/roblox-gui-fusion/SKILL.md +150 -150
- package/skills/roblox-gui-fusion/references/inventory.luau +427 -427
- package/skills/roblox-gui-fusion/references/settings-menu.luau +579 -579
- package/skills/roblox-gui-fusion/references/shop.luau +411 -411
- package/skills/roblox-luau-mastery/SKILL.md +1519 -1519
- package/skills/roblox-monetization/SKILL.md +1084 -1084
- package/skills/roblox-monetization/references/process-receipt.luau +131 -131
- package/skills/roblox-networking/SKILL.md +669 -669
- package/skills/roblox-networking/references/remote-validator.luau +193 -193
- package/skills/roblox-publish-checklist/SKILL.md +127 -127
- package/skills/roblox-runtime/SKILL.md +753 -753
- package/skills/roblox-sharp-edges/SKILL.md +294 -294
- package/skills/roblox-sync/SKILL.md +126 -126
- package/skills/roblox-testing/SKILL.md +943 -943
- package/skills/roblox-tooling/SKILL.md +149 -149
- package/vendor/LICENSES/ProfileStore-LICENSE +201 -201
- package/vendor/LICENSES/RbxUtil-LICENSE +7 -7
- package/vendor/LICENSES/promise-LICENSE +20 -20
- package/vendor/LICENSES/t-LICENSE +21 -21
- package/vendor/LICENSES/testez-LICENSE +200 -200
- package/vendor/README.md +83 -83
- package/vendor/fusion/Animation/ExternalTime.luau +83 -83
- package/vendor/fusion/Animation/Spring.luau +321 -321
- package/vendor/fusion/Animation/Stopwatch.luau +127 -127
- package/vendor/fusion/Animation/Tween.luau +187 -187
- package/vendor/fusion/Animation/getTweenDuration.luau +27 -27
- package/vendor/fusion/Animation/getTweenRatio.luau +47 -47
- package/vendor/fusion/Animation/lerpType.luau +163 -163
- package/vendor/fusion/Animation/packType.luau +99 -99
- package/vendor/fusion/Animation/springCoefficients.luau +80 -80
- package/vendor/fusion/Animation/unpackType.luau +102 -102
- package/vendor/fusion/Colour/Oklab.luau +70 -70
- package/vendor/fusion/Colour/sRGB.luau +54 -54
- package/vendor/fusion/External.luau +167 -167
- package/vendor/fusion/ExternalDebug.luau +69 -69
- package/vendor/fusion/Graph/Observer.luau +113 -113
- package/vendor/fusion/Graph/castToGraph.luau +28 -28
- package/vendor/fusion/Graph/change.luau +80 -80
- package/vendor/fusion/Graph/depend.luau +32 -32
- package/vendor/fusion/Graph/evaluate.luau +55 -55
- package/vendor/fusion/Instances/Attribute.luau +57 -57
- package/vendor/fusion/Instances/AttributeChange.luau +46 -46
- package/vendor/fusion/Instances/AttributeOut.luau +63 -63
- package/vendor/fusion/Instances/Child.luau +21 -21
- package/vendor/fusion/Instances/Children.luau +147 -147
- package/vendor/fusion/Instances/Hydrate.luau +32 -32
- package/vendor/fusion/Instances/New.luau +52 -52
- package/vendor/fusion/Instances/OnChange.luau +49 -49
- package/vendor/fusion/Instances/OnEvent.luau +53 -53
- package/vendor/fusion/Instances/Out.luau +69 -69
- package/vendor/fusion/Instances/applyInstanceProps.luau +148 -148
- package/vendor/fusion/Instances/defaultProps.luau +194 -194
- package/vendor/fusion/LICENSE +21 -21
- package/vendor/fusion/Logging/formatError.luau +48 -48
- package/vendor/fusion/Logging/messages.luau +51 -51
- package/vendor/fusion/Logging/parseError.luau +24 -24
- package/vendor/fusion/Memory/checkLifetime.luau +133 -133
- package/vendor/fusion/Memory/deriveScope.luau +23 -23
- package/vendor/fusion/Memory/deriveScopeImpl.luau +44 -44
- package/vendor/fusion/Memory/doCleanup.luau +78 -78
- package/vendor/fusion/Memory/innerScope.luau +33 -33
- package/vendor/fusion/Memory/legacyCleanup.luau +17 -17
- package/vendor/fusion/Memory/needsDestruction.luau +16 -16
- package/vendor/fusion/Memory/poisonScope.luau +33 -33
- package/vendor/fusion/Memory/scopePool.luau +54 -54
- package/vendor/fusion/Memory/scoped.luau +26 -26
- package/vendor/fusion/Memory/whichLivesLonger.luau +74 -74
- package/vendor/fusion/RobloxExternal.luau +97 -97
- package/vendor/fusion/State/Computed.luau +138 -138
- package/vendor/fusion/State/For/Disassembly.luau +210 -210
- package/vendor/fusion/State/For/ForTypes.luau +30 -30
- package/vendor/fusion/State/For/init.luau +109 -109
- package/vendor/fusion/State/ForKeys.luau +93 -93
- package/vendor/fusion/State/ForPairs.luau +96 -96
- package/vendor/fusion/State/ForValues.luau +93 -93
- package/vendor/fusion/State/Value.luau +87 -87
- package/vendor/fusion/State/castToState.luau +25 -25
- package/vendor/fusion/State/peek.luau +30 -30
- package/vendor/fusion/Types.luau +314 -314
- package/vendor/fusion/Utility/Contextual.luau +90 -90
- package/vendor/fusion/Utility/Safe.luau +22 -22
- package/vendor/fusion/Utility/isSimilar.luau +29 -29
- package/vendor/fusion/Utility/merge.luau +35 -35
- package/vendor/fusion/Utility/nameOf.luau +34 -34
- package/vendor/fusion/Utility/never.luau +13 -13
- package/vendor/fusion/Utility/nicknames.luau +10 -10
- package/vendor/fusion/Utility/xtypeof.luau +26 -26
- package/vendor/fusion/init.luau +82 -82
- package/vendor/profilestore/init.luau +2242 -2242
- package/vendor/promise/init.luau +1982 -1982
- package/vendor/rbxutil/buffer-util/Buffer.test.luau +25 -25
- package/vendor/rbxutil/buffer-util/BufferReader.luau +228 -228
- package/vendor/rbxutil/buffer-util/BufferWriter.luau +269 -269
- package/vendor/rbxutil/buffer-util/DataTypeBuffer.luau +223 -223
- package/vendor/rbxutil/buffer-util/Types.luau +60 -60
- package/vendor/rbxutil/buffer-util/index.d.ts +153 -153
- package/vendor/rbxutil/buffer-util/init.luau +41 -41
- package/vendor/rbxutil/buffer-util/package.json +16 -16
- package/vendor/rbxutil/buffer-util/wally.toml +9 -9
- package/vendor/rbxutil/comm/Client/ClientComm.luau +232 -232
- package/vendor/rbxutil/comm/Client/ClientRemoteProperty.luau +156 -156
- package/vendor/rbxutil/comm/Client/ClientRemoteSignal.luau +109 -109
- package/vendor/rbxutil/comm/Client/init.luau +135 -135
- package/vendor/rbxutil/comm/Server/RemoteProperty.luau +295 -295
- package/vendor/rbxutil/comm/Server/RemoteSignal.luau +211 -211
- package/vendor/rbxutil/comm/Server/ServerComm.luau +211 -211
- package/vendor/rbxutil/comm/Server/init.luau +140 -140
- package/vendor/rbxutil/comm/Types.luau +18 -18
- package/vendor/rbxutil/comm/Util.luau +27 -27
- package/vendor/rbxutil/comm/init.luau +35 -35
- package/vendor/rbxutil/comm/wally.toml +13 -13
- package/vendor/rbxutil/component/init.luau +759 -759
- package/vendor/rbxutil/component/init.test.luau +311 -311
- package/vendor/rbxutil/component/wally.toml +14 -14
- package/vendor/rbxutil/concur/init.luau +542 -542
- package/vendor/rbxutil/concur/init.test.luau +364 -364
- package/vendor/rbxutil/concur/wally.toml +8 -8
- package/vendor/rbxutil/enum-list/init.luau +101 -101
- package/vendor/rbxutil/enum-list/init.test.luau +91 -91
- package/vendor/rbxutil/enum-list/wally.toml +8 -8
- package/vendor/rbxutil/find/index.d.ts +20 -20
- package/vendor/rbxutil/find/init.luau +44 -44
- package/vendor/rbxutil/find/package.json +17 -17
- package/vendor/rbxutil/find/wally.toml +8 -8
- package/vendor/rbxutil/input/Gamepad.luau +559 -559
- package/vendor/rbxutil/input/Keyboard.luau +124 -124
- package/vendor/rbxutil/input/Mouse.luau +278 -278
- package/vendor/rbxutil/input/PreferredInput.luau +91 -91
- package/vendor/rbxutil/input/Touch.luau +120 -120
- package/vendor/rbxutil/input/init.luau +33 -33
- package/vendor/rbxutil/input/wally.toml +12 -12
- package/vendor/rbxutil/loader/index.d.ts +15 -15
- package/vendor/rbxutil/loader/init.luau +137 -137
- package/vendor/rbxutil/loader/wally.toml +8 -8
- package/vendor/rbxutil/log/index.d.ts +38 -38
- package/vendor/rbxutil/log/init.luau +746 -746
- package/vendor/rbxutil/log/wally.toml +8 -8
- package/vendor/rbxutil/net/init.luau +190 -190
- package/vendor/rbxutil/net/wally.toml +8 -8
- package/vendor/rbxutil/option/index.d.ts +44 -44
- package/vendor/rbxutil/option/init.luau +489 -489
- package/vendor/rbxutil/option/init.test.luau +342 -342
- package/vendor/rbxutil/option/wally.toml +8 -8
- package/vendor/rbxutil/pid/index.d.ts +53 -53
- package/vendor/rbxutil/pid/init.luau +195 -195
- package/vendor/rbxutil/pid/package.json +16 -16
- package/vendor/rbxutil/pid/wally.toml +9 -9
- package/vendor/rbxutil/quaternion/index.d.ts +117 -117
- package/vendor/rbxutil/quaternion/init.luau +570 -570
- package/vendor/rbxutil/quaternion/package.json +16 -16
- package/vendor/rbxutil/quaternion/wally.toml +9 -9
- package/vendor/rbxutil/query/index.d.ts +43 -43
- package/vendor/rbxutil/query/init.luau +117 -117
- package/vendor/rbxutil/query/package.json +18 -18
- package/vendor/rbxutil/query/wally.toml +9 -9
- package/vendor/rbxutil/sequent/index.d.ts +28 -28
- package/vendor/rbxutil/sequent/init.luau +340 -340
- package/vendor/rbxutil/sequent/package.json +16 -16
- package/vendor/rbxutil/sequent/wally.toml +9 -9
- package/vendor/rbxutil/ser/init.luau +175 -175
- package/vendor/rbxutil/ser/init.test.luau +50 -50
- package/vendor/rbxutil/ser/wally.toml +11 -11
- package/vendor/rbxutil/shake/index.d.ts +36 -36
- package/vendor/rbxutil/shake/init.luau +532 -532
- package/vendor/rbxutil/shake/init.test.luau +267 -267
- package/vendor/rbxutil/shake/package.json +16 -16
- package/vendor/rbxutil/shake/wally.toml +9 -9
- package/vendor/rbxutil/signal/index.d.ts +100 -100
- package/vendor/rbxutil/signal/init.luau +432 -432
- package/vendor/rbxutil/signal/init.test.luau +190 -190
- package/vendor/rbxutil/signal/package.json +17 -17
- package/vendor/rbxutil/signal/wally.toml +9 -9
- package/vendor/rbxutil/silo/TableWatcher.luau +65 -65
- package/vendor/rbxutil/silo/Util.luau +55 -55
- package/vendor/rbxutil/silo/init.luau +338 -338
- package/vendor/rbxutil/silo/init.test.luau +215 -215
- package/vendor/rbxutil/silo/wally.toml +8 -8
- package/vendor/rbxutil/spring/index.d.ts +40 -40
- package/vendor/rbxutil/spring/init.luau +97 -97
- package/vendor/rbxutil/spring/package.json +17 -17
- package/vendor/rbxutil/spring/wally.toml +8 -8
- package/vendor/rbxutil/stream/index.d.ts +88 -88
- package/vendor/rbxutil/stream/init.luau +597 -597
- package/vendor/rbxutil/stream/package.json +18 -18
- package/vendor/rbxutil/stream/wally.toml +9 -9
- package/vendor/rbxutil/streamable/Streamable.luau +202 -202
- package/vendor/rbxutil/streamable/StreamableUtil.luau +80 -80
- package/vendor/rbxutil/streamable/init.luau +8 -8
- package/vendor/rbxutil/streamable/wally.toml +12 -12
- package/vendor/rbxutil/symbol/init.luau +56 -56
- package/vendor/rbxutil/symbol/init.test.luau +37 -37
- package/vendor/rbxutil/symbol/wally.toml +8 -8
- package/vendor/rbxutil/table-util/init.luau +938 -938
- package/vendor/rbxutil/table-util/init.test.luau +439 -439
- package/vendor/rbxutil/task-queue/index.d.ts +27 -27
- package/vendor/rbxutil/task-queue/init.luau +97 -97
- package/vendor/rbxutil/task-queue/wally.toml +8 -8
- package/vendor/rbxutil/timer/index.d.ts +81 -81
- package/vendor/rbxutil/timer/init.luau +249 -249
- package/vendor/rbxutil/timer/init.test.luau +73 -73
- package/vendor/rbxutil/timer/wally.toml +11 -11
- package/vendor/rbxutil/tree/index.d.ts +15 -15
- package/vendor/rbxutil/tree/init.luau +137 -137
- package/vendor/rbxutil/tree/wally.toml +8 -8
- package/vendor/rbxutil/trove/index.d.ts +46 -46
- package/vendor/rbxutil/trove/init.luau +787 -787
- package/vendor/rbxutil/trove/init.test.luau +203 -203
- package/vendor/rbxutil/trove/wally.toml +8 -8
- package/vendor/rbxutil/typed-remote/init.luau +196 -196
- package/vendor/rbxutil/typed-remote/wally.toml +8 -8
- package/vendor/rbxutil/wait-for/index.d.ts +17 -17
- package/vendor/rbxutil/wait-for/init.luau +257 -257
- package/vendor/rbxutil/wait-for/init.test.luau +182 -182
- package/vendor/rbxutil/wait-for/wally.toml +11 -11
- package/vendor/t/t.lua +1350 -1350
- package/vendor/testez/Context.lua +26 -26
- package/vendor/testez/Expectation.lua +311 -311
- package/vendor/testez/ExpectationContext.lua +38 -38
- package/vendor/testez/LifecycleHooks.lua +89 -89
- package/vendor/testez/Reporters/TeamCityReporter.lua +101 -101
- package/vendor/testez/Reporters/TextReporter.lua +105 -105
- package/vendor/testez/Reporters/TextReporterQuiet.lua +96 -96
- package/vendor/testez/TestBootstrap.lua +146 -146
- package/vendor/testez/TestEnum.lua +27 -27
- package/vendor/testez/TestPlan.lua +304 -304
- package/vendor/testez/TestPlanner.lua +39 -39
- package/vendor/testez/TestResults.lua +111 -111
- package/vendor/testez/TestRunner.lua +188 -188
- package/vendor/testez/TestSession.lua +243 -243
- package/vendor/testez/init.lua +39 -39
|
@@ -1,1519 +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
|
-
|
|
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
|
+
|