roblox-opencode 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/README.md +112 -122
  2. package/commands/setup-game.md +108 -108
  3. package/commands/sync-check.md +53 -53
  4. package/core/roblox-core.md +93 -93
  5. package/dist/server.js +189 -167
  6. package/package.json +35 -35
  7. package/skills/roblox-analytics/SKILL.md +277 -277
  8. package/skills/roblox-analytics/references/event-batcher.luau +75 -75
  9. package/skills/roblox-animation-vfx/SKILL.md +1325 -1325
  10. package/skills/roblox-architecture/SKILL.md +877 -863
  11. package/skills/roblox-architecture/references/combat-systems.md +1381 -1381
  12. package/skills/roblox-code-review/SKILL.md +686 -686
  13. package/skills/roblox-data/SKILL.md +889 -889
  14. package/skills/roblox-data/references/inventory-systems.md +1729 -1729
  15. package/skills/roblox-debug/SKILL.md +98 -98
  16. package/skills/roblox-gui/SKILL.md +1103 -1103
  17. package/skills/roblox-gui-fusion/SKILL.md +150 -150
  18. package/skills/roblox-gui-fusion/references/inventory.luau +427 -427
  19. package/skills/roblox-gui-fusion/references/settings-menu.luau +579 -579
  20. package/skills/roblox-gui-fusion/references/shop.luau +411 -411
  21. package/skills/roblox-luau-mastery/SKILL.md +1618 -1519
  22. package/skills/roblox-monetization/SKILL.md +1084 -1084
  23. package/skills/roblox-monetization/references/process-receipt.luau +131 -131
  24. package/skills/roblox-networking/SKILL.md +669 -669
  25. package/skills/roblox-networking/references/remote-validator.luau +193 -193
  26. package/skills/roblox-publish-checklist/SKILL.md +127 -127
  27. package/skills/roblox-runtime/SKILL.md +753 -753
  28. package/skills/roblox-sharp-edges/SKILL.md +294 -294
  29. package/skills/roblox-sync/SKILL.md +126 -126
  30. package/skills/roblox-testing/SKILL.md +943 -943
  31. package/skills/roblox-tooling/SKILL.md +149 -149
  32. package/vendor/LICENSES/ProfileStore-LICENSE +201 -201
  33. package/vendor/LICENSES/RbxUtil-LICENSE +7 -7
  34. package/vendor/LICENSES/promise-LICENSE +20 -20
  35. package/vendor/LICENSES/t-LICENSE +21 -21
  36. package/vendor/LICENSES/testez-LICENSE +200 -200
  37. package/vendor/README.md +83 -83
  38. package/vendor/fusion/Animation/ExternalTime.luau +83 -83
  39. package/vendor/fusion/Animation/Spring.luau +321 -321
  40. package/vendor/fusion/Animation/Stopwatch.luau +127 -127
  41. package/vendor/fusion/Animation/Tween.luau +187 -187
  42. package/vendor/fusion/Animation/getTweenDuration.luau +27 -27
  43. package/vendor/fusion/Animation/getTweenRatio.luau +47 -47
  44. package/vendor/fusion/Animation/lerpType.luau +163 -163
  45. package/vendor/fusion/Animation/packType.luau +99 -99
  46. package/vendor/fusion/Animation/springCoefficients.luau +80 -80
  47. package/vendor/fusion/Animation/unpackType.luau +102 -102
  48. package/vendor/fusion/Colour/Oklab.luau +70 -70
  49. package/vendor/fusion/Colour/sRGB.luau +54 -54
  50. package/vendor/fusion/External.luau +167 -167
  51. package/vendor/fusion/ExternalDebug.luau +69 -69
  52. package/vendor/fusion/Graph/Observer.luau +113 -113
  53. package/vendor/fusion/Graph/castToGraph.luau +28 -28
  54. package/vendor/fusion/Graph/change.luau +80 -80
  55. package/vendor/fusion/Graph/depend.luau +32 -32
  56. package/vendor/fusion/Graph/evaluate.luau +55 -55
  57. package/vendor/fusion/Instances/Attribute.luau +57 -57
  58. package/vendor/fusion/Instances/AttributeChange.luau +46 -46
  59. package/vendor/fusion/Instances/AttributeOut.luau +63 -63
  60. package/vendor/fusion/Instances/Child.luau +21 -21
  61. package/vendor/fusion/Instances/Children.luau +147 -147
  62. package/vendor/fusion/Instances/Hydrate.luau +32 -32
  63. package/vendor/fusion/Instances/New.luau +52 -52
  64. package/vendor/fusion/Instances/OnChange.luau +49 -49
  65. package/vendor/fusion/Instances/OnEvent.luau +53 -53
  66. package/vendor/fusion/Instances/Out.luau +69 -69
  67. package/vendor/fusion/Instances/applyInstanceProps.luau +148 -148
  68. package/vendor/fusion/Instances/defaultProps.luau +194 -194
  69. package/vendor/fusion/LICENSE +21 -21
  70. package/vendor/fusion/Logging/formatError.luau +48 -48
  71. package/vendor/fusion/Logging/messages.luau +51 -51
  72. package/vendor/fusion/Logging/parseError.luau +24 -24
  73. package/vendor/fusion/Memory/checkLifetime.luau +133 -133
  74. package/vendor/fusion/Memory/deriveScope.luau +23 -23
  75. package/vendor/fusion/Memory/deriveScopeImpl.luau +44 -44
  76. package/vendor/fusion/Memory/doCleanup.luau +78 -78
  77. package/vendor/fusion/Memory/innerScope.luau +33 -33
  78. package/vendor/fusion/Memory/legacyCleanup.luau +17 -17
  79. package/vendor/fusion/Memory/needsDestruction.luau +16 -16
  80. package/vendor/fusion/Memory/poisonScope.luau +33 -33
  81. package/vendor/fusion/Memory/scopePool.luau +54 -54
  82. package/vendor/fusion/Memory/scoped.luau +26 -26
  83. package/vendor/fusion/Memory/whichLivesLonger.luau +74 -74
  84. package/vendor/fusion/RobloxExternal.luau +97 -97
  85. package/vendor/fusion/State/Computed.luau +138 -138
  86. package/vendor/fusion/State/For/Disassembly.luau +210 -210
  87. package/vendor/fusion/State/For/ForTypes.luau +30 -30
  88. package/vendor/fusion/State/For/init.luau +109 -109
  89. package/vendor/fusion/State/ForKeys.luau +93 -93
  90. package/vendor/fusion/State/ForPairs.luau +96 -96
  91. package/vendor/fusion/State/ForValues.luau +93 -93
  92. package/vendor/fusion/State/Value.luau +87 -87
  93. package/vendor/fusion/State/castToState.luau +25 -25
  94. package/vendor/fusion/State/peek.luau +30 -30
  95. package/vendor/fusion/Types.luau +314 -314
  96. package/vendor/fusion/Utility/Contextual.luau +90 -90
  97. package/vendor/fusion/Utility/Safe.luau +22 -22
  98. package/vendor/fusion/Utility/isSimilar.luau +29 -29
  99. package/vendor/fusion/Utility/merge.luau +35 -35
  100. package/vendor/fusion/Utility/nameOf.luau +34 -34
  101. package/vendor/fusion/Utility/never.luau +13 -13
  102. package/vendor/fusion/Utility/nicknames.luau +10 -10
  103. package/vendor/fusion/Utility/xtypeof.luau +26 -26
  104. package/vendor/fusion/init.luau +82 -82
  105. package/vendor/profilestore/init.luau +2242 -2242
  106. package/vendor/promise/init.luau +1982 -1982
  107. package/vendor/rbxutil/buffer-util/Buffer.test.luau +25 -25
  108. package/vendor/rbxutil/buffer-util/BufferReader.luau +228 -228
  109. package/vendor/rbxutil/buffer-util/BufferWriter.luau +269 -269
  110. package/vendor/rbxutil/buffer-util/DataTypeBuffer.luau +223 -223
  111. package/vendor/rbxutil/buffer-util/Types.luau +60 -60
  112. package/vendor/rbxutil/buffer-util/index.d.ts +153 -153
  113. package/vendor/rbxutil/buffer-util/init.luau +41 -41
  114. package/vendor/rbxutil/buffer-util/package.json +16 -16
  115. package/vendor/rbxutil/buffer-util/wally.toml +9 -9
  116. package/vendor/rbxutil/comm/Client/ClientComm.luau +232 -232
  117. package/vendor/rbxutil/comm/Client/ClientRemoteProperty.luau +156 -156
  118. package/vendor/rbxutil/comm/Client/ClientRemoteSignal.luau +109 -109
  119. package/vendor/rbxutil/comm/Client/init.luau +135 -135
  120. package/vendor/rbxutil/comm/Server/RemoteProperty.luau +295 -295
  121. package/vendor/rbxutil/comm/Server/RemoteSignal.luau +211 -211
  122. package/vendor/rbxutil/comm/Server/ServerComm.luau +211 -211
  123. package/vendor/rbxutil/comm/Server/init.luau +140 -140
  124. package/vendor/rbxutil/comm/Types.luau +18 -18
  125. package/vendor/rbxutil/comm/Util.luau +27 -27
  126. package/vendor/rbxutil/comm/init.luau +35 -35
  127. package/vendor/rbxutil/comm/wally.toml +13 -13
  128. package/vendor/rbxutil/component/init.luau +759 -759
  129. package/vendor/rbxutil/component/init.test.luau +311 -311
  130. package/vendor/rbxutil/component/wally.toml +14 -14
  131. package/vendor/rbxutil/concur/init.luau +542 -542
  132. package/vendor/rbxutil/concur/init.test.luau +364 -364
  133. package/vendor/rbxutil/concur/wally.toml +8 -8
  134. package/vendor/rbxutil/enum-list/init.luau +101 -101
  135. package/vendor/rbxutil/enum-list/init.test.luau +91 -91
  136. package/vendor/rbxutil/enum-list/wally.toml +8 -8
  137. package/vendor/rbxutil/find/index.d.ts +20 -20
  138. package/vendor/rbxutil/find/init.luau +44 -44
  139. package/vendor/rbxutil/find/package.json +17 -17
  140. package/vendor/rbxutil/find/wally.toml +8 -8
  141. package/vendor/rbxutil/input/Gamepad.luau +559 -559
  142. package/vendor/rbxutil/input/Keyboard.luau +124 -124
  143. package/vendor/rbxutil/input/Mouse.luau +278 -278
  144. package/vendor/rbxutil/input/PreferredInput.luau +91 -91
  145. package/vendor/rbxutil/input/Touch.luau +120 -120
  146. package/vendor/rbxutil/input/init.luau +33 -33
  147. package/vendor/rbxutil/input/wally.toml +12 -12
  148. package/vendor/rbxutil/loader/index.d.ts +15 -15
  149. package/vendor/rbxutil/loader/init.luau +137 -137
  150. package/vendor/rbxutil/loader/wally.toml +8 -8
  151. package/vendor/rbxutil/log/index.d.ts +38 -38
  152. package/vendor/rbxutil/log/init.luau +746 -746
  153. package/vendor/rbxutil/log/wally.toml +8 -8
  154. package/vendor/rbxutil/net/init.luau +190 -190
  155. package/vendor/rbxutil/net/wally.toml +8 -8
  156. package/vendor/rbxutil/option/index.d.ts +44 -44
  157. package/vendor/rbxutil/option/init.luau +489 -489
  158. package/vendor/rbxutil/option/init.test.luau +342 -342
  159. package/vendor/rbxutil/option/wally.toml +8 -8
  160. package/vendor/rbxutil/pid/index.d.ts +53 -53
  161. package/vendor/rbxutil/pid/init.luau +195 -195
  162. package/vendor/rbxutil/pid/package.json +16 -16
  163. package/vendor/rbxutil/pid/wally.toml +9 -9
  164. package/vendor/rbxutil/quaternion/index.d.ts +117 -117
  165. package/vendor/rbxutil/quaternion/init.luau +570 -570
  166. package/vendor/rbxutil/quaternion/package.json +16 -16
  167. package/vendor/rbxutil/quaternion/wally.toml +9 -9
  168. package/vendor/rbxutil/query/index.d.ts +43 -43
  169. package/vendor/rbxutil/query/init.luau +117 -117
  170. package/vendor/rbxutil/query/package.json +18 -18
  171. package/vendor/rbxutil/query/wally.toml +9 -9
  172. package/vendor/rbxutil/sequent/index.d.ts +28 -28
  173. package/vendor/rbxutil/sequent/init.luau +340 -340
  174. package/vendor/rbxutil/sequent/package.json +16 -16
  175. package/vendor/rbxutil/sequent/wally.toml +9 -9
  176. package/vendor/rbxutil/ser/init.luau +175 -175
  177. package/vendor/rbxutil/ser/init.test.luau +50 -50
  178. package/vendor/rbxutil/ser/wally.toml +11 -11
  179. package/vendor/rbxutil/shake/index.d.ts +36 -36
  180. package/vendor/rbxutil/shake/init.luau +532 -532
  181. package/vendor/rbxutil/shake/init.test.luau +267 -267
  182. package/vendor/rbxutil/shake/package.json +16 -16
  183. package/vendor/rbxutil/shake/wally.toml +9 -9
  184. package/vendor/rbxutil/signal/index.d.ts +100 -100
  185. package/vendor/rbxutil/signal/init.luau +432 -432
  186. package/vendor/rbxutil/signal/init.test.luau +190 -190
  187. package/vendor/rbxutil/signal/package.json +17 -17
  188. package/vendor/rbxutil/signal/wally.toml +9 -9
  189. package/vendor/rbxutil/silo/TableWatcher.luau +65 -65
  190. package/vendor/rbxutil/silo/Util.luau +55 -55
  191. package/vendor/rbxutil/silo/init.luau +338 -338
  192. package/vendor/rbxutil/silo/init.test.luau +215 -215
  193. package/vendor/rbxutil/silo/wally.toml +8 -8
  194. package/vendor/rbxutil/spring/index.d.ts +40 -40
  195. package/vendor/rbxutil/spring/init.luau +97 -97
  196. package/vendor/rbxutil/spring/package.json +17 -17
  197. package/vendor/rbxutil/spring/wally.toml +8 -8
  198. package/vendor/rbxutil/stream/index.d.ts +88 -88
  199. package/vendor/rbxutil/stream/init.luau +597 -597
  200. package/vendor/rbxutil/stream/package.json +18 -18
  201. package/vendor/rbxutil/stream/wally.toml +9 -9
  202. package/vendor/rbxutil/streamable/Streamable.luau +202 -202
  203. package/vendor/rbxutil/streamable/StreamableUtil.luau +80 -80
  204. package/vendor/rbxutil/streamable/init.luau +8 -8
  205. package/vendor/rbxutil/streamable/wally.toml +12 -12
  206. package/vendor/rbxutil/symbol/init.luau +56 -56
  207. package/vendor/rbxutil/symbol/init.test.luau +37 -37
  208. package/vendor/rbxutil/symbol/wally.toml +8 -8
  209. package/vendor/rbxutil/table-util/init.luau +938 -938
  210. package/vendor/rbxutil/table-util/init.test.luau +439 -439
  211. package/vendor/rbxutil/task-queue/index.d.ts +27 -27
  212. package/vendor/rbxutil/task-queue/init.luau +97 -97
  213. package/vendor/rbxutil/task-queue/wally.toml +8 -8
  214. package/vendor/rbxutil/timer/index.d.ts +81 -81
  215. package/vendor/rbxutil/timer/init.luau +249 -249
  216. package/vendor/rbxutil/timer/init.test.luau +73 -73
  217. package/vendor/rbxutil/timer/wally.toml +11 -11
  218. package/vendor/rbxutil/tree/index.d.ts +15 -15
  219. package/vendor/rbxutil/tree/init.luau +137 -137
  220. package/vendor/rbxutil/tree/wally.toml +8 -8
  221. package/vendor/rbxutil/trove/index.d.ts +46 -46
  222. package/vendor/rbxutil/trove/init.luau +787 -787
  223. package/vendor/rbxutil/trove/init.test.luau +203 -203
  224. package/vendor/rbxutil/trove/wally.toml +8 -8
  225. package/vendor/rbxutil/typed-remote/init.luau +196 -196
  226. package/vendor/rbxutil/typed-remote/wally.toml +8 -8
  227. package/vendor/rbxutil/wait-for/index.d.ts +17 -17
  228. package/vendor/rbxutil/wait-for/init.luau +257 -257
  229. package/vendor/rbxutil/wait-for/init.test.luau +182 -182
  230. package/vendor/rbxutil/wait-for/wally.toml +11 -11
  231. package/vendor/t/t.lua +1350 -1350
  232. package/vendor/testez/Context.lua +26 -26
  233. package/vendor/testez/Expectation.lua +311 -311
  234. package/vendor/testez/ExpectationContext.lua +38 -38
  235. package/vendor/testez/LifecycleHooks.lua +89 -89
  236. package/vendor/testez/Reporters/TeamCityReporter.lua +101 -101
  237. package/vendor/testez/Reporters/TextReporter.lua +105 -105
  238. package/vendor/testez/Reporters/TextReporterQuiet.lua +96 -96
  239. package/vendor/testez/TestBootstrap.lua +146 -146
  240. package/vendor/testez/TestEnum.lua +27 -27
  241. package/vendor/testez/TestPlan.lua +304 -304
  242. package/vendor/testez/TestPlanner.lua +39 -39
  243. package/vendor/testez/TestResults.lua +111 -111
  244. package/vendor/testez/TestRunner.lua +188 -188
  245. package/vendor/testez/TestSession.lua +243 -243
  246. package/vendor/testez/init.lua +39 -39
@@ -1,863 +1,877 @@
1
- ---
2
- name: roblox-architecture
3
- description: Service hierarchy, 7 foundational patterns, cross-platform input. Client-server architecture, module patterns, framework options.
4
- last_reviewed: 2026-05-21
5
- ---
6
-
7
- <!-- Source: brockmartin/roblox-game-skill (MIT) -->
8
-
9
- # Roblox Game Architecture Reference
10
-
11
- ---
12
-
13
- ## 1. Overview
14
-
15
- **Load this reference when:**
16
-
17
- - Starting a new Roblox game from scratch and need to decide where code and assets go
18
- - Organizing or refactoring an existing codebase that has grown unwieldy
19
- - Answering architecture questions: "Where should this script go?", "How should client talk to server?", "How do I structure modules?"
20
- - Onboarding onto a Roblox project and need to understand the standard conventions
21
- - Choosing between a flat script layout and a modular loader pattern
22
-
23
- This document covers the Roblox data model, service hierarchy, script types, client-server communication, module patterns, framework options, folder organization, best practices, and anti-patterns.
24
-
25
- ---
26
-
27
- ## Quick Reference
28
-
29
- **Load Full Reference below only when you need specific folder layouts or framework comparisons.**
30
-
31
- Key rules:
32
- - ServerScriptService: server logic (never visible to client)
33
- - ServerStorage: server-only assets/data
34
- - ReplicatedStorage: shared modules, RemoteEvents, assets both sides need
35
- - StarterPlayerScripts: client controllers (run once per player)
36
- - StarterGui: ScreenGuis (cloned to PlayerGui on spawn)
37
- - Script types: Script (server), LocalScript (client), ModuleScript (shared, returns one table)
38
- - Communication: RemoteEvent (fire-and-forget), RemoteFunction (request-response, avoid for client→server)
39
- - Module pattern: return a table of functions. One module = one responsibility.
40
- - Avoid circular requires. Use events/signals for cross-module communication.
41
- - Single entry point per side: one server Script requires service modules, one LocalScript requires controllers.
42
-
43
- ---
44
-
45
- ## Full Reference
46
-
47
- ## 2. Core Concepts
48
-
49
- ### The Data Model
50
-
51
- Roblox games are built on a **tree of Instances**. Every object (parts, scripts, UI elements, sounds) is an Instance that lives somewhere in a hierarchy rooted at `game` (class `DataModel`). The hierarchy determines behavior: a `Script` placed in `ServerScriptService` runs on the server, but the same `Script` placed in `StarterPlayerScripts` will not run at all (only `LocalScript` and client-side `Script` run there).
52
-
53
- ### Client vs. Server Execution
54
-
55
- | Aspect | Server | Client |
56
- |---|---|---|
57
- | **Runs on** | Roblox datacenter (one instance per game server) | Each player's device |
58
- | **Script types** | `Script` (Luau), `ModuleScript` (when required by a server script) | `LocalScript`, `ModuleScript` (when required by a local script) |
59
- | **Trust level** | Authoritative -- owns game state, data stores, physics arbitration | Untrusted -- can be exploited; never trust client input blindly |
60
- | **Access** | Can see everything in the data model | Cannot see `ServerScriptService` or `ServerStorage` |
61
-
62
- ### Replication Model
63
-
64
- Roblox automatically replicates (syncs) parts of the data model from server to clients:
65
-
66
- - **Server to all clients:** Anything in `Workspace`, `ReplicatedStorage`, `ReplicatedFirst`, `Lighting`, `SoundService`, `Chat`, and `Teams` is visible to every connected client.
67
- - **Server-only (hidden from clients):** `ServerScriptService` and `ServerStorage` are never sent to clients. This is the correct place for server logic and secret assets.
68
- - **Per-player:** Each player gets their own `PlayerGui` (cloned from `StarterGui`), `PlayerScripts` (cloned from `StarterPlayerScripts`), `StarterGear` (cloned from `StarterPack`), and `Backpack`.
69
- - **Client to server:** Clients can modify their own character and certain local objects, but those changes do **not** replicate to the server unless the server has granted network ownership of that instance.
70
-
71
- **Key rule:** If the server creates or changes an instance in a replicated container, all clients see it. If a client creates something locally, only that client sees it (unless the server replicates it).
72
-
73
- ---
74
-
75
- ## 3. Service Hierarchy
76
-
77
- ### ServerScriptService
78
-
79
- **Purpose:** Contains `Script` instances that run exclusively on the server. Clients cannot see or access anything inside.
80
-
81
- **Use for:**
82
- - Core game logic (round systems, match management, scoring)
83
- - Data persistence (DataStoreService calls)
84
- - Server-side validation of player actions
85
- - Anti-cheat enforcement
86
- - NPC/AI controllers
87
- - Server-side modules required only by server scripts
88
-
89
- ```
90
- ServerScriptService/
91
- GameManager.server.lua -- Script: round system
92
- DataManager.server.lua -- Script: save/load player data
93
- Modules/
94
- CombatService.lua -- ModuleScript: damage calculations
95
- ShopService.lua -- ModuleScript: purchase validation
96
- ```
97
-
98
- ### ServerStorage
99
-
100
- **Purpose:** A server-only container for assets and modules that clients should never see or download.
101
-
102
- **Use for:**
103
- - Map models that get cloned into Workspace on demand
104
- - Enemy/NPC models before spawning
105
- - Server-only ModuleScripts (utility libraries, data schemas)
106
- - Templates that should not exist on the client until needed
107
-
108
- ```
109
- ServerStorage/
110
- Maps/
111
- DesertArena.rbxm
112
- ForestArena.rbxm
113
- Templates/
114
- Loot/
115
- CommonChest.rbxm
116
- Modules/
117
- DataSchema.lua
118
- ```
119
-
120
- ### ReplicatedStorage
121
-
122
- **Purpose:** Shared container visible to both server and client. Content is replicated to every client on join.
123
-
124
- **Use for:**
125
- - `RemoteEvent` and `RemoteFunction` instances (the communication bridge)
126
- - Shared `ModuleScript` modules (utilities, constants, types, shared classes)
127
- - Assets both sides reference (item models, particle effects, shared animations)
128
- - Configuration values / game settings that both sides need
129
-
130
- ```
131
- ReplicatedStorage/
132
- Remotes/
133
- DamageEvent.RemoteEvent
134
- ShopPurchase.RemoteFunction
135
- Modules/
136
- ItemData.lua -- shared item definitions
137
- MathUtils.lua -- shared utility functions
138
- Types.lua -- shared type definitions
139
- Assets/
140
- Effects/
141
- HitEffect.rbxm
142
- ```
143
-
144
- ### ReplicatedFirst
145
-
146
- **Purpose:** Scripts here run on the client **before** anything else loads. Content replicates to clients first.
147
-
148
- **Use for:**
149
- - Loading screens (show UI while the game streams in)
150
- - Early client initialization
151
- - Keeping it minimal -- only what is needed before the game fully loads
152
-
153
- ```
154
- ReplicatedFirst/
155
- LoadingScreen.client.lua -- LocalScript: shows loading UI
156
- ```
157
-
158
- ### StarterGui
159
-
160
- **Purpose:** UI template container. On each player spawn (or respawn, depending on `ResetOnSpawn`), the contents are **cloned** into that player's `PlayerGui`.
161
-
162
- **Use for:**
163
- - HUD elements (health bars, score displays, minimaps)
164
- - Menu screens (settings, inventory, shop)
165
- - UI-controlling LocalScripts that live inside ScreenGui
166
-
167
- ```
168
- StarterGui/
169
- HUD.ScreenGui
170
- HealthBar.Frame
171
- ScoreLabel.TextLabel
172
- HUDController.client.lua -- LocalScript managing HUD updates
173
- ShopMenu.ScreenGui
174
- ShopController.client.lua
175
- ```
176
-
177
- > **Note:** Set `ScreenGui.ResetOnSpawn = false` for persistent UI that should not re-clone on character respawn.
178
-
179
- ### StarterPlayer / StarterPlayerScripts
180
-
181
- **Purpose:** `LocalScript` instances here are cloned into each player's `PlayerScripts` folder once on join. They persist across respawns.
182
-
183
- **Use for:**
184
- - Camera controllers
185
- - Input handling systems
186
- - Client-side game managers
187
- - Music/ambient sound controllers
188
-
189
- ```
190
- StarterPlayer/
191
- StarterPlayerScripts/
192
- CameraController.client.lua
193
- InputManager.client.lua
194
- ClientBootstrap.client.lua
195
- ```
196
-
197
- ### StarterPlayer / StarterCharacterScripts
198
-
199
- **Purpose:** Scripts here are cloned into the player's `Character` model each time the character spawns. They are destroyed when the character dies.
200
-
201
- **Use for:**
202
- - Per-character behaviors (footstep sounds, animation controllers)
203
- - Character-specific effects (trails, auras)
204
- - Anything that should reset on death
205
-
206
- ```
207
- StarterPlayer/
208
- StarterCharacterScripts/
209
- FootstepSounds.client.lua
210
- AnimationController.client.lua
211
- ```
212
-
213
- ### StarterPack
214
-
215
- **Purpose:** `Tool` instances here are cloned into each player's `Backpack` on spawn.
216
-
217
- **Use for:**
218
- - Default weapons or items every player starts with
219
- - Tools with embedded LocalScripts and Scripts
220
-
221
- ```
222
- StarterPack/
223
- Sword.Tool
224
- SwordClient.client.lua
225
- SwordServer.server.lua
226
- Handle.Part
227
- ```
228
-
229
- ### Workspace
230
-
231
- **Purpose:** The 3D world. Everything visible in the game exists here at runtime: parts, models, terrain, cameras.
232
-
233
- **Use for:**
234
- - The physical game world (terrain, buildings, decorations)
235
- - Spawned entities at runtime (cloned from ServerStorage)
236
- - The Camera (each client has a local `Workspace.CurrentCamera`)
237
-
238
- **Do NOT place Scripts directly in Workspace.** Use `ServerScriptService` instead. Workspace scripts are accessible to exploiters and create organizational chaos.
239
-
240
- ---
241
-
242
- ## 4. Script Types
243
-
244
- ### Script (Server Script)
245
-
246
- - **Runs on:** Server (default RunContext)
247
- - **Valid containers:** `ServerScriptService`, `ServerStorage` (when parented under certain conditions), `Workspace` (discouraged)
248
- - **File convention:** `*.server.lua` in Rojo projects
249
- - **Access:** Full access to server APIs (`DataStoreService`, `MessagingService`, `HttpService`, etc.)
250
-
251
- ```lua
252
- -- ServerScriptService/GameManager.server.lua
253
- local Players = game:GetService("Players")
254
-
255
- Players.PlayerAdded:Connect(function(player)
256
- print(`{player.Name} joined the server`)
257
- end)
258
- ```
259
-
260
- ### RunContext (Script Behavior Override)
261
-
262
- By default, `Script` runs on the server. Setting `Script.RunContext` changes this:
263
-
264
- - **`Enum.RunContext.Server`** (default) - runs on the server, only in server-valid containers
265
- - **`Enum.RunContext.Client`** - runs on the client, can be placed ANYWHERE (Workspace, ReplicatedStorage, etc.)
266
-
267
- `RunContext = Client` is powerful for workspace-local effects, proximity scripts, or anything the client sees but doesn't belong in StarterPlayerScripts:
268
-
269
- ```lua
270
- -- A Script in Workspace with RunContext = Client
271
- -- Runs on every client, perfect for local visual effects
272
- local part = script.Parent
273
- local RunService = game:GetService("RunService")
274
-
275
- RunService.Heartbeat:Connect(function(dt)
276
- part.CFrame *= CFrame.Angles(0, math.rad(30 * dt), 0)
277
- end)
278
- ```
279
-
280
- > **Note:** `LocalScript` does NOT have RunContext. It always runs on the client in client-valid containers only. Use `Script` + `RunContext = Client` when you need client scripts outside the usual client containers.
281
-
282
- ### LocalScript (Client Script)
283
-
284
- - **Runs on:** Client (the specific player's device)
285
- - **Valid containers:** `StarterPlayerScripts`, `StarterCharacterScripts`, `StarterGui`, `StarterPack`, a player's `Backpack`, `PlayerGui`, `PlayerScripts`, or `Character`
286
- - **File convention:** `*.client.lua` in Rojo projects
287
- - **Access:** Client APIs (`UserInputService`, `ContextActionService`, `Camera`, local player's GUI)
288
-
289
- ```lua
290
- -- StarterPlayerScripts/InputManager.client.lua
291
- local UserInputService = game:GetService("UserInputService")
292
-
293
- UserInputService.InputBegan:Connect(function(input, gameProcessed)
294
- if gameProcessed then return end
295
- if input.KeyCode == Enum.KeyCode.E then
296
- print("Player pressed E")
297
- end
298
- end)
299
- ```
300
-
301
- ### ModuleScript
302
-
303
- - **Runs on:** Whichever side `require()`s it (server if required by a Script, client if required by a LocalScript)
304
- - **Valid containers:** Anywhere, but location determines who can access it
305
- - `ServerScriptService` or `ServerStorage` -- server-only modules
306
- - `ReplicatedStorage` -- shared modules (accessible by both sides)
307
- - `StarterPlayerScripts` -- client-only modules
308
- - **File convention:** `*.lua` (no `.server` or `.client` suffix) in Rojo projects
309
- - **Returns:** Exactly one value (typically a table/dictionary acting as a module)
310
-
311
- ```lua
312
- -- ReplicatedStorage/Modules/MathUtils.lua
313
- local MathUtils = {}
314
-
315
- function MathUtils.lerp(a: number, b: number, t: number): number
316
- return a + (b - a) * t
317
- end
318
-
319
- function MathUtils.clamp(value: number, min: number, max: number): number
320
- return math.max(min, math.min(max, value))
321
- end
322
-
323
- return MathUtils
324
- ```
325
-
326
- **Requiring:**
327
- ```lua
328
- local ReplicatedStorage = game:GetService("ReplicatedStorage")
329
- local MathUtils = require(ReplicatedStorage.Modules.MathUtils)
330
-
331
- local result = MathUtils.lerp(0, 100, 0.5) -- 50
332
- ```
333
-
334
- ---
335
-
336
- ## 5. Client-Server Communication
337
-
338
- For implementation details (RemoteEvent, RemoteFunction, UnreliableRemoteEvent, BindableEvent, validation patterns), see **roblox-networking** → Client-Server Communication.
339
-
340
- **Conceptual overview:**
341
-
342
- | Type | Direction | Blocking? | Use Case |
343
- |---|---|---|---|
344
- | `RemoteEvent` | Client ↔ Server | No | Actions, notifications, state changes |
345
- | `RemoteFunction` | Client → Server only (safe) | Yes (yields) | Data requests, queries |
346
- | `BindableEvent` | Same side | N/A (local) | Decoupled same-side messaging |
347
- | `UnreliableRemoteEvent` | Client ↔ Server | No | High-frequency cosmetic data |
348
-
349
- **Core rules:**
350
- - Server is authoritative. Never trust client input.
351
- - Use `RemoteEvent` for fire-and-forget. Use `RemoteFunction` only when the caller needs a return value.
352
- - Never use `RemoteFunction:InvokeClient()` - if the client errors or disconnects, the server thread hangs forever.
353
- - `UnreliableRemoteEvent` for cosmetic data only (cursor position, facing direction). Never for damage, purchases, or state.
354
-
355
- ---
356
-
357
- ## 6. Module Script Architecture
358
-
359
- ### Basic Module Pattern
360
-
361
- Every ModuleScript returns a single table. Functions and data are fields on that table.
362
-
363
- ```lua
364
- -- ReplicatedStorage/Modules/InventoryModule.lua
365
- local InventoryModule = {}
366
-
367
- local playerInventories: { [Player]: { [string]: number } } = {}
368
-
369
- function InventoryModule.init()
370
- -- Called once during startup to wire up connections
371
- game:GetService("Players").PlayerRemoving:Connect(function(player)
372
- playerInventories[player] = nil
373
- end)
374
- end
375
-
376
- function InventoryModule.addItem(player: Player, itemId: string, quantity: number)
377
- local inv = playerInventories[player]
378
- if not inv then
379
- inv = {}
380
- playerInventories[player] = inv
381
- end
382
- inv[itemId] = (inv[itemId] or 0) + quantity
383
- end
384
-
385
- function InventoryModule.getItem(player: Player, itemId: string): number
386
- local inv = playerInventories[player]
387
- if not inv then return 0 end
388
- return inv[itemId] or 0
389
- end
390
-
391
- function InventoryModule.getAll(player: Player): { [string]: number }
392
- return playerInventories[player] or {}
393
- end
394
-
395
- return InventoryModule
396
- ```
397
-
398
- ### The `require()` Pattern
399
-
400
- ```lua
401
- -- ServerScriptService/Main.server.lua
402
- local ReplicatedStorage = game:GetService("ReplicatedStorage")
403
- local ServerStorage = game:GetService("ServerStorage")
404
-
405
- -- Shared modules (both sides can require these)
406
- local ItemData = require(ReplicatedStorage.Modules.ItemData)
407
-
408
- -- Server-only modules
409
- local InventoryModule = require(ServerStorage.Modules.InventoryModule)
410
-
411
- -- Initialize modules that need setup
412
- InventoryModule.init()
413
- ```
414
-
415
- **Key facts about `require()`:**
416
- - The module runs **once**. Subsequent `require()` calls return the cached result.
417
- - The module runs in the **context** of the first requirer (server or client).
418
- - If a server Script requires a module in `ReplicatedStorage`, it runs on the server. If a LocalScript requires the same module, a separate client-side copy runs.
419
-
420
- ### Avoiding Circular Dependencies
421
-
422
- Circular dependencies occur when Module A requires Module B, and Module B requires Module A. This causes an infinite loop or returns `nil`.
423
-
424
- **Problem:**
425
- ```lua
426
- -- ModuleA.lua
427
- local ModuleB = require(script.Parent.ModuleB) -- ModuleB hasn't finished loading
428
- -- ...
429
- return ModuleA
430
-
431
- -- ModuleB.lua
432
- local ModuleA = require(script.Parent.ModuleA) -- ModuleA hasn't finished loading
433
- ```
434
-
435
- **Solution 1: Deferred initialization with `init()` pattern**
436
- ```lua
437
- -- ModuleA.lua
438
- local ModuleA = {}
439
- local ModuleB -- forward declaration
440
-
441
- function ModuleA.init(modules)
442
- ModuleB = modules.ModuleB
443
- end
444
-
445
- function ModuleA.doSomething()
446
- ModuleB.helper()
447
- end
448
-
449
- return ModuleA
450
- ```
451
-
452
- ```lua
453
- -- Main.server.lua (the bootstrapper)
454
- local ModuleA = require(path.to.ModuleA)
455
- local ModuleB = require(path.to.ModuleB)
456
-
457
- -- Wire up cross-references after all modules are loaded
458
- local modules = { ModuleA = ModuleA, ModuleB = ModuleB }
459
- ModuleA.init(modules)
460
- ModuleB.init(modules)
461
-
462
- -- Now start the game
463
- ModuleA.start()
464
- ```
465
-
466
- **Solution 2: Dependency inversion -- extract shared logic into a third module**
467
-
468
- Instead of A and B depending on each other, extract the shared concern into module C that both depend on. Dependencies flow in one direction.
469
-
470
- **Solution 3: Event-driven decoupling with BindableEvents**
471
-
472
- Instead of calling directly into another module, fire a BindableEvent that the other module listens to. Neither module requires the other.
473
-
474
- ### OOP Module Pattern (Class-based)
475
-
476
- For metatable-based classes, type annotations, inheritance, and the `.` vs `:` conventions, see **roblox-luau-mastery** → OOP Patterns.
477
-
478
- The architecture-specific pattern: modules that return a class table. The class is defined in a ModuleScript, required by whoever needs it, and instances are created via `ClassName.new()`.
479
-
480
- ```lua
481
- -- ReplicatedStorage/Modules/Weapon.lua
482
- local Weapon = {}
483
- Weapon.__index = Weapon
484
-
485
- export type Weapon = typeof(setmetatable({} :: {
486
- name: string,
487
- damage: number,
488
- cooldown: number,
489
- _lastFired: number,
490
- }, Weapon))
491
-
492
- function Weapon.new(name: string, damage: number, cooldown: number): Weapon
493
- local self = setmetatable({}, Weapon)
494
- self.name = name
495
- self.damage = damage
496
- self.cooldown = cooldown
497
- self._lastFired = 0
498
- return self
499
- end
500
-
501
- function Weapon:canFire(): boolean
502
- return (os.clock() - self._lastFired) >= self.cooldown
503
- end
504
-
505
- function Weapon:fire(): number?
506
- if not self:canFire() then return nil end
507
- self._lastFired = os.clock()
508
- return self.damage
509
- end
510
-
511
- return Weapon
512
- ```
513
-
514
- ```lua
515
- -- Usage
516
- local Weapon = require(ReplicatedStorage.Modules.Weapon)
517
- local sword = Weapon.new("Iron Sword", 25, 0.8)
518
-
519
- if sword:canFire() then
520
- local dmg = sword:fire()
521
- end
522
- ```
523
-
524
- ---
525
-
526
- ## 7. Framework Patterns
527
-
528
- ### Single Script Architecture (SSA)
529
-
530
- One Script on the server, one LocalScript on the client. Each requires a "loader" module that initializes all other modules in order.
531
-
532
- ```lua
533
- -- ServerScriptService/Main.server.lua
534
- require(game:GetService("ServerStorage").Loader)
535
- ```
536
-
537
- ```lua
538
- -- ServerStorage/Loader.lua
539
- local Loader = {}
540
-
541
- local modules = {
542
- require(script.Parent.Modules.DataService),
543
- require(script.Parent.Modules.CombatService),
544
- require(script.Parent.Modules.ShopService),
545
- }
546
-
547
- -- Init phase (no cross-dependencies yet)
548
- for _, mod in modules do
549
- if mod.init then mod:init() end
550
- end
551
-
552
- -- Start phase (all modules available)
553
- for _, mod in modules do
554
- if mod.start then mod:start() end
555
- end
556
-
557
- return Loader
558
- ```
559
-
560
- ### Comparison
561
-
562
- | Aspect | No Framework (Manual) | Single Script Architecture |
563
- |---|---|---|
564
- | **Complexity** | Low | Medium |
565
- | **Boilerplate** | Minimal | Low |
566
- | **Networking** | Manual RemoteEvent setup | Manual |
567
- | **Learning curve** | Just Roblox APIs | Small pattern to learn |
568
- | **Scalability** | Degrades without discipline | Good with init/start pattern |
569
- | **Dependency management** | Manual require chains | Centralized loader |
570
- | **Best for** | Jams, small projects, learning | Medium projects, teams wanting control |
571
-
572
- ---
573
-
574
- ## 8. Folder Organization
575
-
576
- ### Small Game (Simple / Flat)
577
-
578
- For game jams, prototypes, or games with under ~10 scripts.
579
-
580
- ```
581
- game
582
- +-- ServerScriptService
583
- || +-- GameManager.server.lua
584
- || +-- DataHandler.server.lua
585
- +-- ServerStorage
586
- || +-- Maps/
587
- +-- ReplicatedStorage
588
- || +-- Remotes/
589
- || | +-- DamageEvent (RemoteEvent)
590
- || | +-- ShopPurchase (RemoteFunction)
591
- || +-- SharedConfig.lua (ModuleScript)
592
- +-- ReplicatedFirst
593
- || +-- LoadingScreen.client.lua
594
- +-- StarterGui
595
- || +-- HUD (ScreenGui)
596
- +-- StarterPlayer
597
- || +-- StarterPlayerScripts
598
- || +-- CameraController.client.lua
599
- +-- Workspace
600
- +-- Map/
601
- +-- SpawnLocations/
602
- ```
603
-
604
- ### Medium Game (Modular)
605
-
606
- Organized into feature folders. Each feature has its own server module, client module, and shared definitions.
607
-
608
- ```
609
- game
610
- +-- ServerScriptService
611
- || +-- Main.server.lua -- Bootstrapper: requires and inits all modules
612
- || +-- Services/
613
- || | +-- CombatService.lua
614
- || | +-- DataService.lua
615
- || | +-- ShopService.lua
616
- || | +-- RoundService.lua
617
- || +-- Components/
618
- || +-- LootDrop.lua
619
- || +-- DoorSystem.lua
620
- +-- ServerStorage
621
- || +-- Assets/
622
- || | +-- Maps/
623
- || | +-- NPCs/
624
- || +-- Modules/
625
- || +-- DataSchema.lua
626
- +-- ReplicatedStorage
627
- || +-- Remotes/ -- All RemoteEvents/Functions here
628
- || +-- Modules/
629
- || | +-- ItemData.lua
630
- || | +-- Constants.lua
631
- || | +-- Types.lua
632
- || | +-- MathUtils.lua
633
- || +-- Assets/
634
- || +-- Effects/
635
- || +-- Animations/
636
- +-- ReplicatedFirst
637
- || +-- LoadingScreen.client.lua
638
- +-- StarterGui
639
- || +-- HUD (ScreenGui)
640
- || +-- ShopMenu (ScreenGui)
641
- || +-- SettingsMenu (ScreenGui)
642
- +-- StarterPlayer
643
- || +-- StarterPlayerScripts
644
- || +-- ClientMain.client.lua -- Client bootstrapper
645
- || +-- Controllers/
646
- || +-- CameraController.lua
647
- || +-- InputController.lua
648
- || +-- UIController.lua
649
- +-- Workspace
650
- +-- World/
651
- +-- Lighting/
652
- ```
653
-
654
- ### Large Game (Framework-Based with Rojo)
655
-
656
- Uses a loader pattern (SSA), Rojo for file sync, and a strict folder convention. File system mirrors the Roblox hierarchy.
657
-
658
- ```
659
- src/
660
- +-- server/ --> syncs to ServerScriptService
661
- || +-- Main.server.lua -- Bootstrapper
662
- || +-- services/
663
- || | +-- PlayerDataService.lua
664
- || | +-- CombatService.lua
665
- || | +-- EconomyService.lua
666
- || | +-- MatchService.lua
667
- || | +-- LeaderboardService.lua
668
- || +-- components/
669
- || +-- Destructible.lua
670
- || +-- Interactable.lua
671
- +-- client/ --> syncs to StarterPlayerScripts
672
- || +-- ClientMain.client.lua -- Client bootstrapper
673
- || +-- controllers/
674
- || | +-- InputController.lua
675
- || | +-- CameraController.lua
676
- || | +-- UIController.lua
677
- || | +-- SoundController.lua
678
- || +-- ui/
679
- || +-- components/
680
- || | +-- Button.lua
681
- || | +-- HealthBar.lua
682
- || +-- screens/
683
- || +-- ShopScreen.lua
684
- || +-- InventoryScreen.lua
685
- +-- shared/ --> syncs to ReplicatedStorage
686
- || +-- modules/
687
- || | +-- ItemData.lua
688
- || | +-- Constants.lua
689
- || | +-- Types.lua
690
- || | +-- Util.lua
691
- || +-- network/
692
- || | +-- Remotes.lua -- Central remote definitions
693
- || +-- assets/
694
- +-- serverStorage/ --> syncs to ServerStorage
695
- || +-- assets/
696
- || | +-- maps/
697
- || | +-- npcs/
698
- || +-- modules/
699
- || +-- DataSchema.lua
700
- +-- starterGui/ --> syncs to StarterGui
701
- || +-- HUD.gui.lua
702
- +-- first/ --> syncs to ReplicatedFirst
703
- +-- Loading.client.lua
704
- ```
705
-
706
- ---
707
-
708
- ## 9. Best Practices
709
-
710
- ### Separation of Concerns
711
-
712
- Each script or module should have a single, clear responsibility. A `CombatService` handles damage and health. A `DataService` handles saving and loading. They communicate via well-defined interfaces, not by reaching into each other's internals.
713
-
714
- ### Single Responsibility
715
-
716
- One module = one job. If a module handles inventory AND crafting AND trading, split it into `InventoryModule`, `CraftingModule`, and `TradingModule`.
717
-
718
- ### Minimal Coupling
719
-
720
- Modules should depend on abstractions (function calls, events), not on internal state of other modules. If module A needs data from module B, A calls `B.getData()` rather than reading `B._internalTable` directly.
721
-
722
- ### Server Owns State
723
-
724
- The server is the **single source of truth** for all game state. Clients render and predict, but the server validates and authorizes.
725
-
726
- ### Validate All Client Input
727
-
728
- For implementation details (type checking, range checking, ownership, rate limiting), see **roblox-networking** → Client Validation.
729
-
730
- **Core rules:**
731
- - Every `OnServerEvent` handler must validate types, ranges, and ownership before processing.
732
- - Never let the client set currency, health, or any authoritative value directly.
733
- - Client sends intent ("I attacked target X"), server calculates outcome.
734
-
735
- ### Use ModuleScripts Everywhere
736
-
737
- Avoid putting game logic directly in Scripts or LocalScripts. Instead, keep Scripts/LocalScripts as thin bootstrappers that require and initialize ModuleScripts. This makes code reusable, testable, and organized.
738
-
739
- ### Centralize Remote Definitions
740
-
741
- Create all RemoteEvents and RemoteFunctions in one place rather than scattering `Instance.new("RemoteEvent")` across multiple scripts.
742
-
743
- ```lua
744
- -- ServerScriptService/CreateRemotes.server.lua (runs first)
745
- local remoteFolder = Instance.new("Folder")
746
- remoteFolder.Name = "Remotes"
747
- remoteFolder.Parent = game:GetService("ReplicatedStorage")
748
-
749
- local remotes = {
750
- "DamageEvent",
751
- "ShopPurchase",
752
- "ChatMessage",
753
- "PlayerReady",
754
- }
755
-
756
- for _, name in remotes do
757
- local remote = Instance.new("RemoteEvent")
758
- remote.Name = name
759
- remote.Parent = remoteFolder
760
- end
761
- ```
762
-
763
- ### Use Types and Constants
764
-
765
- Define shared constants and Luau types in `ReplicatedStorage` so both sides use the same definitions.
766
-
767
- ```lua
768
- -- ReplicatedStorage/Modules/Constants.lua
769
- local Constants = {
770
- MAX_HEALTH = 100,
771
- WALK_SPEED = 16,
772
- SPRINT_SPEED = 24,
773
- MAX_INVENTORY_SLOTS = 20,
774
- INTERACTION_RANGE = 10,
775
-
776
- ItemRarity = {
777
- Common = 1,
778
- Uncommon = 2,
779
- Rare = 3,
780
- Epic = 4,
781
- Legendary = 5,
782
- },
783
- }
784
-
785
- return Constants
786
- ```
787
-
788
- ---
789
-
790
- ## 10. Anti-Patterns
791
-
792
- ### God Scripts
793
-
794
- **Problem:** One massive script (500+ lines) that handles spawning, combat, data, UI, and everything else. Impossible to debug, modify, or collaborate on.
795
-
796
- **Fix:** Break into ModuleScripts with clear responsibilities. The main script becomes a thin bootstrapper.
797
-
798
- ### Circular Requires
799
-
800
- **Problem:** Module A requires Module B, which requires Module A. Causes one of them to receive an incomplete (empty table) reference.
801
-
802
- **Fix:** Use the `init()` pattern (Section 6), dependency inversion, or event-driven decoupling.
803
-
804
- ### Server Logic in ReplicatedStorage
805
-
806
- **Problem:** Placing server-only game logic modules in `ReplicatedStorage`. Exploiters can read the source code, understand validation logic, and craft exploits.
807
-
808
- **Fix:** Keep server logic in `ServerScriptService` or `ServerStorage`. Only put genuinely shared code (types, constants, utilities) in `ReplicatedStorage`.
809
-
810
- ### Scripts in Workspace
811
-
812
- **Problem:** Placing Scripts directly as children of Workspace or models in Workspace. These are visible to clients (exploiters can read them), are hard to find during development, and create organizational debt.
813
-
814
- **Fix:** Put all server scripts in `ServerScriptService`. If you need a script to reference a specific model, have the script in `ServerScriptService` and use a path reference or CollectionService tag to find the model.
815
-
816
- ### Not Using ModuleScripts
817
-
818
- **Problem:** Duplicating logic across multiple Scripts/LocalScripts instead of extracting shared code into ModuleScripts.
819
-
820
- **Fix:** If two scripts share logic, extract it into a ModuleScript in the appropriate location (`ReplicatedStorage` for shared, `ServerStorage` for server-only).
821
-
822
- ### Trusting Client Data
823
-
824
- **Problem:** Server blindly applies whatever the client sends (damage values, item quantities, positions).
825
-
826
- **Fix:** The server must independently validate and calculate. The client sends **intent** ("I want to attack this target"), not **outcome** ("I dealt 500 damage").
827
-
828
- ### Polling Instead of Events
829
-
830
- For polling vs event-driven patterns, see **roblox-luau-mastery** → Anti-Patterns.
831
-
832
- **Core rule:** Use events (`.Changed`, `GetPropertyChangedSignal()`, `Died`, etc.) instead of `while true do task.wait()` loops.
833
-
834
- ### Overusing RemoteFunctions
835
-
836
- **Problem:** Using `RemoteFunction` for everything, including fire-and-forget actions that do not need a return value. Each `InvokeServer` call yields the client thread until the server responds.
837
-
838
- **Fix:** Use `RemoteEvent` for actions that do not need a response. Reserve `RemoteFunction` for when the client genuinely needs data back from the server.
839
-
840
- ### Ignoring `task` Library
841
-
842
- For deprecated `wait()`/`spawn()`/`delay()` vs the `task` library, see **roblox-luau-mastery** → Task Library.
843
-
844
- ### Instance.new with Parent Argument
845
-
846
- **Problem:** `Instance.new("Part", workspace)` passes the parent as the second argument. This causes the instance to be parented immediately during construction, which yields internally and can create race conditions - the instance replicates to clients before you've finished setting its properties.
847
-
848
- ```luau
849
- -- BAD: parent during construction, yields, race condition
850
- local part = Instance.new("Part", workspace)
851
- part.Size = Vector3.new(4, 1, 4) -- may already be visible to clients
852
- part.Anchored = true
853
- part.BrickColor = BrickColor.new("Bright red")
854
-
855
- -- GOOD: create, configure, then parent
856
- local part = Instance.new("Part")
857
- part.Size = Vector3.new(4, 1, 4)
858
- part.Anchored = true
859
- part.BrickColor = BrickColor.new("Bright red")
860
- part.Parent = workspace -- parent last
861
- ```
862
-
863
- **Rule:** Always set `Parent` last. Create the instance, configure all properties, then parent it.
1
+ ---
2
+ name: roblox-architecture
3
+ description: Service hierarchy, 7 foundational patterns, cross-platform input. Client-server architecture, module patterns, framework options.
4
+ last_reviewed: 2026-05-26
5
+ ---
6
+
7
+ <!-- Source: brockmartin/roblox-game-skill (MIT) -->
8
+
9
+ # Roblox Game Architecture Reference
10
+
11
+ ---
12
+
13
+ ## 1. Overview
14
+
15
+ **Load this reference when:**
16
+
17
+ - Starting a new Roblox game from scratch and need to decide where code and assets go
18
+ - Organizing or refactoring an existing codebase that has grown unwieldy
19
+ - Answering architecture questions: "Where should this script go?", "How should client talk to server?", "How do I structure modules?"
20
+ - Onboarding onto a Roblox project and need to understand the standard conventions
21
+ - Choosing between a flat script layout and a modular loader pattern
22
+
23
+ This document covers the Roblox data model, service hierarchy, script types, client-server communication, module patterns, framework options, folder organization, best practices, and anti-patterns.
24
+
25
+ ---
26
+
27
+ ## Quick Reference
28
+
29
+ **Load Full Reference below only when you need specific folder layouts or framework comparisons.**
30
+
31
+ Key rules:
32
+ - ServerScriptService: server logic (never visible to client)
33
+ - ServerStorage: server-only assets/data
34
+ - ReplicatedStorage: shared modules, RemoteEvents, assets both sides need
35
+ - StarterPlayerScripts: client controllers (run once per player)
36
+ - StarterGui: ScreenGuis (cloned to PlayerGui on spawn)
37
+ - Script types: Script (server), LocalScript (client), ModuleScript (shared, returns one table)
38
+ - Communication: RemoteEvent (fire-and-forget), RemoteFunction (request-response, avoid for client→server)
39
+ - Module pattern: return a table of functions. One module = one responsibility.
40
+ - Avoid circular requires. Use events/signals for cross-module communication.
41
+ - Single entry point per side: one server Script requires service modules, one LocalScript requires controllers.
42
+
43
+ ---
44
+
45
+ ## Full Reference
46
+
47
+ ## 2. Core Concepts
48
+
49
+ ### The Data Model
50
+
51
+ Roblox games are built on a **tree of Instances**. Every object (parts, scripts, UI elements, sounds) is an Instance that lives somewhere in a hierarchy rooted at `game` (class `DataModel`). The hierarchy determines behavior: a `Script` placed in `ServerScriptService` runs on the server, but the same `Script` placed in `StarterPlayerScripts` will not run at all (only `LocalScript` and client-side `Script` run there).
52
+
53
+ ### Client vs. Server Execution
54
+
55
+ | Aspect | Server | Client |
56
+ |---|---|---|
57
+ | **Runs on** | Roblox datacenter (one instance per game server) | Each player's device |
58
+ | **Script types** | `Script` (Luau), `ModuleScript` (when required by a server script) | `LocalScript`, `ModuleScript` (when required by a local script) |
59
+ | **Trust level** | Authoritative -- owns game state, data stores, physics arbitration | Untrusted -- can be exploited; never trust client input blindly |
60
+ | **Access** | Can see everything in the data model | Cannot see `ServerScriptService` or `ServerStorage` |
61
+
62
+ ### Replication Model
63
+
64
+ Roblox automatically replicates (syncs) parts of the data model from server to clients:
65
+
66
+ - **Server to all clients:** Anything in `Workspace`, `ReplicatedStorage`, `ReplicatedFirst`, `Lighting`, `SoundService`, `Chat`, and `Teams` is visible to every connected client.
67
+ - **Server-only (hidden from clients):** `ServerScriptService` and `ServerStorage` are never sent to clients. This is the correct place for server logic and secret assets.
68
+ - **Per-player:** Each player gets their own `PlayerGui` (cloned from `StarterGui`), `PlayerScripts` (cloned from `StarterPlayerScripts`), `StarterGear` (cloned from `StarterPack`), and `Backpack`.
69
+ - **Client to server:** Clients can modify their own character and certain local objects, but those changes do **not** replicate to the server unless the server has granted network ownership of that instance.
70
+
71
+ **Key rule:** If the server creates or changes an instance in a replicated container, all clients see it. If a client creates something locally, only that client sees it (unless the server replicates it).
72
+
73
+ ---
74
+
75
+ ## 3. Service Hierarchy
76
+
77
+ ### ServerScriptService
78
+
79
+ **Purpose:** Contains `Script` instances that run exclusively on the server. Clients cannot see or access anything inside.
80
+
81
+ **Use for:**
82
+ - Core game logic (round systems, match management, scoring)
83
+ - Data persistence (DataStoreService calls)
84
+ - Server-side validation of player actions
85
+ - Anti-cheat enforcement
86
+ - NPC/AI controllers
87
+ - Server-side modules required only by server scripts
88
+
89
+ ```
90
+ ServerScriptService/
91
+ GameManager.server.lua -- Script: round system
92
+ DataManager.server.lua -- Script: save/load player data
93
+ Modules/
94
+ CombatService.lua -- ModuleScript: damage calculations
95
+ ShopService.lua -- ModuleScript: purchase validation
96
+ ```
97
+
98
+ ### ServerStorage
99
+
100
+ **Purpose:** A server-only container for assets and modules that clients should never see or download.
101
+
102
+ **Use for:**
103
+ - Map models that get cloned into Workspace on demand
104
+ - Enemy/NPC models before spawning
105
+ - Server-only ModuleScripts (utility libraries, data schemas)
106
+ - Templates that should not exist on the client until needed
107
+
108
+ ```
109
+ ServerStorage/
110
+ Maps/
111
+ DesertArena.rbxm
112
+ ForestArena.rbxm
113
+ Templates/
114
+ Loot/
115
+ CommonChest.rbxm
116
+ Modules/
117
+ DataSchema.lua
118
+ ```
119
+
120
+ ### ReplicatedStorage
121
+
122
+ **Purpose:** Shared container visible to both server and client. Content is replicated to every client on join.
123
+
124
+ **Use for:**
125
+ - `RemoteEvent` and `RemoteFunction` instances (the communication bridge)
126
+ - Shared `ModuleScript` modules (utilities, constants, types, shared classes)
127
+ - Assets both sides reference (item models, particle effects, shared animations)
128
+ - Configuration values / game settings that both sides need
129
+
130
+ ```
131
+ ReplicatedStorage/
132
+ Remotes/
133
+ DamageEvent.RemoteEvent
134
+ ShopPurchase.RemoteFunction
135
+ Modules/
136
+ ItemData.lua -- shared item definitions
137
+ MathUtils.lua -- shared utility functions
138
+ Types.lua -- shared type definitions
139
+ Assets/
140
+ Effects/
141
+ HitEffect.rbxm
142
+ ```
143
+
144
+ ### ReplicatedFirst
145
+
146
+ **Purpose:** Scripts here run on the client **before** anything else loads. Content replicates to clients first.
147
+
148
+ **Use for:**
149
+ - Loading screens (show UI while the game streams in)
150
+ - Early client initialization
151
+ - Keeping it minimal -- only what is needed before the game fully loads
152
+
153
+ ```
154
+ ReplicatedFirst/
155
+ LoadingScreen.client.lua -- LocalScript: shows loading UI
156
+ ```
157
+
158
+ ### StarterGui
159
+
160
+ **Purpose:** UI template container. On each player spawn (or respawn, depending on `ResetOnSpawn`), the contents are **cloned** into that player's `PlayerGui`.
161
+
162
+ **Use for:**
163
+ - HUD elements (health bars, score displays, minimaps)
164
+ - Menu screens (settings, inventory, shop)
165
+ - UI-controlling LocalScripts that live inside ScreenGui
166
+
167
+ ```
168
+ StarterGui/
169
+ HUD.ScreenGui
170
+ HealthBar.Frame
171
+ ScoreLabel.TextLabel
172
+ HUDController.client.lua -- LocalScript managing HUD updates
173
+ ShopMenu.ScreenGui
174
+ ShopController.client.lua
175
+ ```
176
+
177
+ > **Note:** Set `ScreenGui.ResetOnSpawn = false` for persistent UI that should not re-clone on character respawn.
178
+
179
+ ### StarterPlayer / StarterPlayerScripts
180
+
181
+ **Purpose:** `LocalScript` instances here are cloned into each player's `PlayerScripts` folder once on join. They persist across respawns.
182
+
183
+ **Use for:**
184
+ - Camera controllers
185
+ - Input handling systems
186
+ - Client-side game managers
187
+ - Music/ambient sound controllers
188
+
189
+ ```
190
+ StarterPlayer/
191
+ StarterPlayerScripts/
192
+ CameraController.client.lua
193
+ InputManager.client.lua
194
+ ClientBootstrap.client.lua
195
+ ```
196
+
197
+ ### StarterPlayer / StarterCharacterScripts
198
+
199
+ **Purpose:** Scripts here are cloned into the player's `Character` model each time the character spawns. They are destroyed when the character dies.
200
+
201
+ **Use for:**
202
+ - Per-character behaviors (footstep sounds, animation controllers)
203
+ - Character-specific effects (trails, auras)
204
+ - Anything that should reset on death
205
+
206
+ ```
207
+ StarterPlayer/
208
+ StarterCharacterScripts/
209
+ FootstepSounds.client.lua
210
+ AnimationController.client.lua
211
+ ```
212
+
213
+ ### StarterPack
214
+
215
+ **Purpose:** `Tool` instances here are cloned into each player's `Backpack` on spawn.
216
+
217
+ **Use for:**
218
+ - Default weapons or items every player starts with
219
+ - Tools with embedded LocalScripts and Scripts
220
+
221
+ ```
222
+ StarterPack/
223
+ Sword.Tool
224
+ SwordClient.client.lua
225
+ SwordServer.server.lua
226
+ Handle.Part
227
+ ```
228
+
229
+ ### Workspace
230
+
231
+ **Purpose:** The 3D world. Everything visible in the game exists here at runtime: parts, models, terrain, cameras.
232
+
233
+ **Use for:**
234
+ - The physical game world (terrain, buildings, decorations)
235
+ - Spawned entities at runtime (cloned from ServerStorage)
236
+ - The Camera (each client has a local `Workspace.CurrentCamera`)
237
+
238
+ **Do NOT place Scripts directly in Workspace.** Use `ServerScriptService` instead. Workspace scripts are accessible to exploiters and create organizational chaos.
239
+
240
+ ---
241
+
242
+ ## 4. Script Types
243
+
244
+ ### Script (Server Script)
245
+
246
+ - **Runs on:** Server (default RunContext)
247
+ - **Valid containers:** `ServerScriptService`, `ServerStorage` (when parented under certain conditions), `Workspace` (discouraged)
248
+ - **File convention:** `*.server.lua` in Rojo projects
249
+ - **Access:** Full access to server APIs (`DataStoreService`, `MessagingService`, `HttpService`, etc.)
250
+
251
+ ```lua
252
+ -- ServerScriptService/GameManager.server.lua
253
+ local Players = game:GetService("Players")
254
+
255
+ Players.PlayerAdded:Connect(function(player)
256
+ print(`{player.Name} joined the server`)
257
+ end)
258
+ ```
259
+
260
+ ### RunContext (Script Behavior Override)
261
+
262
+ `Script.RunContext` overrides where a script runs, breaking the traditional container-based restrictions:
263
+
264
+ | Value | Behavior | Notes |
265
+ |-------|----------|-------|
266
+ | `Enum.RunContext.Legacy` | Script runs only in traditional server containers (ServerScriptService, ServerStorage) | **Default** when not set |
267
+ | `Enum.RunContext.Server` | Script runs anywhere in the place, on the server | Useful for scripts in ReplicatedStorage or Workspace |
268
+ | `Enum.RunContext.Client` | Script runs anywhere in the place, on the client | Place in Workspace for local effects, or ReplicatedStorage for shared UI logic |
269
+
270
+ `Legacy` replicates the old behavior - `Script` must be in server-valid containers, `LocalScript` in client-valid containers. Setting `Server` or `Client` frees the script from those container restrictions.
271
+
272
+ `RunContext = Client` is powerful for workspace-local effects or anything the client sees but doesn't belong in StarterPlayerScripts:
273
+
274
+ ```lua
275
+ -- A Script in Workspace with RunContext = Client
276
+ -- Runs on every client, perfect for local visual effects
277
+ local part = script.Parent
278
+ local RunService = game:GetService("RunService")
279
+
280
+ RunService.Heartbeat:Connect(function(dt)
281
+ part.CFrame *= CFrame.Angles(0, math.rad(30 * dt), 0)
282
+ end)
283
+ ```
284
+
285
+ `RunContext = Server` lets you place server scripts outside the usual server containers:
286
+
287
+ ```lua
288
+ -- A Script in ReplicatedStorage with RunContext = Server
289
+ -- Server logic lives alongside shared modules
290
+ local SharedConfig = require(script.Parent.SharedConfig)
291
+ print(`Server running with: {SharedConfig.setting}`)
292
+ ```
293
+
294
+ > **Note:** `LocalScript` does NOT have RunContext. It always runs on the client in client-valid containers only. Use `Script` + `RunContext = Client` when you need client scripts outside the usual client containers.
295
+
296
+ ### LocalScript (Client Script)
297
+
298
+ - **Runs on:** Client (the specific player's device)
299
+ - **Valid containers:** `StarterPlayerScripts`, `StarterCharacterScripts`, `StarterGui`, `StarterPack`, a player's `Backpack`, `PlayerGui`, `PlayerScripts`, or `Character`
300
+ - **File convention:** `*.client.lua` in Rojo projects
301
+ - **Access:** Client APIs (`UserInputService`, `ContextActionService`, `Camera`, local player's GUI)
302
+
303
+ ```lua
304
+ -- StarterPlayerScripts/InputManager.client.lua
305
+ local UserInputService = game:GetService("UserInputService")
306
+
307
+ UserInputService.InputBegan:Connect(function(input, gameProcessed)
308
+ if gameProcessed then return end
309
+ if input.KeyCode == Enum.KeyCode.E then
310
+ print("Player pressed E")
311
+ end
312
+ end)
313
+ ```
314
+
315
+ ### ModuleScript
316
+
317
+ - **Runs on:** Whichever side `require()`s it (server if required by a Script, client if required by a LocalScript)
318
+ - **Valid containers:** Anywhere, but location determines who can access it
319
+ - `ServerScriptService` or `ServerStorage` -- server-only modules
320
+ - `ReplicatedStorage` -- shared modules (accessible by both sides)
321
+ - `StarterPlayerScripts` -- client-only modules
322
+ - **File convention:** `*.lua` (no `.server` or `.client` suffix) in Rojo projects
323
+ - **Returns:** Exactly one value (typically a table/dictionary acting as a module)
324
+
325
+ ```lua
326
+ -- ReplicatedStorage/Modules/MathUtils.lua
327
+ local MathUtils = {}
328
+
329
+ function MathUtils.lerp(a: number, b: number, t: number): number
330
+ return a + (b - a) * t
331
+ end
332
+
333
+ function MathUtils.clamp(value: number, min: number, max: number): number
334
+ return math.max(min, math.min(max, value))
335
+ end
336
+
337
+ return MathUtils
338
+ ```
339
+
340
+ **Requiring:**
341
+ ```lua
342
+ local ReplicatedStorage = game:GetService("ReplicatedStorage")
343
+ local MathUtils = require(ReplicatedStorage.Modules.MathUtils)
344
+
345
+ local result = MathUtils.lerp(0, 100, 0.5) -- 50
346
+ ```
347
+
348
+ ---
349
+
350
+ ## 5. Client-Server Communication
351
+
352
+ For implementation details (RemoteEvent, RemoteFunction, UnreliableRemoteEvent, BindableEvent, validation patterns), see **roblox-networking** Client-Server Communication.
353
+
354
+ **Conceptual overview:**
355
+
356
+ | Type | Direction | Blocking? | Use Case |
357
+ |---|---|---|---|
358
+ | `RemoteEvent` | Client ↔ Server | No | Actions, notifications, state changes |
359
+ | `RemoteFunction` | Client → Server only (safe) | Yes (yields) | Data requests, queries |
360
+ | `BindableEvent` | Same side | N/A (local) | Decoupled same-side messaging |
361
+ | `UnreliableRemoteEvent` | Client Server | No | High-frequency cosmetic data |
362
+
363
+ **Core rules:**
364
+ - Server is authoritative. Never trust client input.
365
+ - Use `RemoteEvent` for fire-and-forget. Use `RemoteFunction` only when the caller needs a return value.
366
+ - Never use `RemoteFunction:InvokeClient()` - if the client errors or disconnects, the server thread hangs forever.
367
+ - `UnreliableRemoteEvent` for cosmetic data only (cursor position, facing direction). Never for damage, purchases, or state.
368
+
369
+ ---
370
+
371
+ ## 6. Module Script Architecture
372
+
373
+ ### Basic Module Pattern
374
+
375
+ Every ModuleScript returns a single table. Functions and data are fields on that table.
376
+
377
+ ```lua
378
+ -- ReplicatedStorage/Modules/InventoryModule.lua
379
+ local InventoryModule = {}
380
+
381
+ local playerInventories: { [Player]: { [string]: number } } = {}
382
+
383
+ function InventoryModule.init()
384
+ -- Called once during startup to wire up connections
385
+ game:GetService("Players").PlayerRemoving:Connect(function(player)
386
+ playerInventories[player] = nil
387
+ end)
388
+ end
389
+
390
+ function InventoryModule.addItem(player: Player, itemId: string, quantity: number)
391
+ local inv = playerInventories[player]
392
+ if not inv then
393
+ inv = {}
394
+ playerInventories[player] = inv
395
+ end
396
+ inv[itemId] = (inv[itemId] or 0) + quantity
397
+ end
398
+
399
+ function InventoryModule.getItem(player: Player, itemId: string): number
400
+ local inv = playerInventories[player]
401
+ if not inv then return 0 end
402
+ return inv[itemId] or 0
403
+ end
404
+
405
+ function InventoryModule.getAll(player: Player): { [string]: number }
406
+ return playerInventories[player] or {}
407
+ end
408
+
409
+ return InventoryModule
410
+ ```
411
+
412
+ ### The `require()` Pattern
413
+
414
+ ```lua
415
+ -- ServerScriptService/Main.server.lua
416
+ local ReplicatedStorage = game:GetService("ReplicatedStorage")
417
+ local ServerStorage = game:GetService("ServerStorage")
418
+
419
+ -- Shared modules (both sides can require these)
420
+ local ItemData = require(ReplicatedStorage.Modules.ItemData)
421
+
422
+ -- Server-only modules
423
+ local InventoryModule = require(ServerStorage.Modules.InventoryModule)
424
+
425
+ -- Initialize modules that need setup
426
+ InventoryModule.init()
427
+ ```
428
+
429
+ **Key facts about `require()`:**
430
+ - The module runs **once**. Subsequent `require()` calls return the cached result.
431
+ - The module runs in the **context** of the first requirer (server or client).
432
+ - If a server Script requires a module in `ReplicatedStorage`, it runs on the server. If a LocalScript requires the same module, a separate client-side copy runs.
433
+
434
+ ### Avoiding Circular Dependencies
435
+
436
+ Circular dependencies occur when Module A requires Module B, and Module B requires Module A. This causes an infinite loop or returns `nil`.
437
+
438
+ **Problem:**
439
+ ```lua
440
+ -- ModuleA.lua
441
+ local ModuleB = require(script.Parent.ModuleB) -- ModuleB hasn't finished loading
442
+ -- ...
443
+ return ModuleA
444
+
445
+ -- ModuleB.lua
446
+ local ModuleA = require(script.Parent.ModuleA) -- ModuleA hasn't finished loading
447
+ ```
448
+
449
+ **Solution 1: Deferred initialization with `init()` pattern**
450
+ ```lua
451
+ -- ModuleA.lua
452
+ local ModuleA = {}
453
+ local ModuleB -- forward declaration
454
+
455
+ function ModuleA.init(modules)
456
+ ModuleB = modules.ModuleB
457
+ end
458
+
459
+ function ModuleA.doSomething()
460
+ ModuleB.helper()
461
+ end
462
+
463
+ return ModuleA
464
+ ```
465
+
466
+ ```lua
467
+ -- Main.server.lua (the bootstrapper)
468
+ local ModuleA = require(path.to.ModuleA)
469
+ local ModuleB = require(path.to.ModuleB)
470
+
471
+ -- Wire up cross-references after all modules are loaded
472
+ local modules = { ModuleA = ModuleA, ModuleB = ModuleB }
473
+ ModuleA.init(modules)
474
+ ModuleB.init(modules)
475
+
476
+ -- Now start the game
477
+ ModuleA.start()
478
+ ```
479
+
480
+ **Solution 2: Dependency inversion -- extract shared logic into a third module**
481
+
482
+ Instead of A and B depending on each other, extract the shared concern into module C that both depend on. Dependencies flow in one direction.
483
+
484
+ **Solution 3: Event-driven decoupling with BindableEvents**
485
+
486
+ Instead of calling directly into another module, fire a BindableEvent that the other module listens to. Neither module requires the other.
487
+
488
+ ### OOP Module Pattern (Class-based)
489
+
490
+ For metatable-based classes, type annotations, inheritance, and the `.` vs `:` conventions, see **roblox-luau-mastery** → OOP Patterns.
491
+
492
+ The architecture-specific pattern: modules that return a class table. The class is defined in a ModuleScript, required by whoever needs it, and instances are created via `ClassName.new()`.
493
+
494
+ ```lua
495
+ -- ReplicatedStorage/Modules/Weapon.lua
496
+ local Weapon = {}
497
+ Weapon.__index = Weapon
498
+
499
+ export type Weapon = typeof(setmetatable({} :: {
500
+ name: string,
501
+ damage: number,
502
+ cooldown: number,
503
+ _lastFired: number,
504
+ }, Weapon))
505
+
506
+ function Weapon.new(name: string, damage: number, cooldown: number): Weapon
507
+ local self = setmetatable({}, Weapon)
508
+ self.name = name
509
+ self.damage = damage
510
+ self.cooldown = cooldown
511
+ self._lastFired = 0
512
+ return self
513
+ end
514
+
515
+ function Weapon:canFire(): boolean
516
+ return (os.clock() - self._lastFired) >= self.cooldown
517
+ end
518
+
519
+ function Weapon:fire(): number?
520
+ if not self:canFire() then return nil end
521
+ self._lastFired = os.clock()
522
+ return self.damage
523
+ end
524
+
525
+ return Weapon
526
+ ```
527
+
528
+ ```lua
529
+ -- Usage
530
+ local Weapon = require(ReplicatedStorage.Modules.Weapon)
531
+ local sword = Weapon.new("Iron Sword", 25, 0.8)
532
+
533
+ if sword:canFire() then
534
+ local dmg = sword:fire()
535
+ end
536
+ ```
537
+
538
+ ---
539
+
540
+ ## 7. Framework Patterns
541
+
542
+ ### Single Script Architecture (SSA)
543
+
544
+ One Script on the server, one LocalScript on the client. Each requires a "loader" module that initializes all other modules in order.
545
+
546
+ ```lua
547
+ -- ServerScriptService/Main.server.lua
548
+ require(game:GetService("ServerStorage").Loader)
549
+ ```
550
+
551
+ ```lua
552
+ -- ServerStorage/Loader.lua
553
+ local Loader = {}
554
+
555
+ local modules = {
556
+ require(script.Parent.Modules.DataService),
557
+ require(script.Parent.Modules.CombatService),
558
+ require(script.Parent.Modules.ShopService),
559
+ }
560
+
561
+ -- Init phase (no cross-dependencies yet)
562
+ for _, mod in modules do
563
+ if mod.init then mod:init() end
564
+ end
565
+
566
+ -- Start phase (all modules available)
567
+ for _, mod in modules do
568
+ if mod.start then mod:start() end
569
+ end
570
+
571
+ return Loader
572
+ ```
573
+
574
+ ### Comparison
575
+
576
+ | Aspect | No Framework (Manual) | Single Script Architecture |
577
+ |---|---|---|
578
+ | **Complexity** | Low | Medium |
579
+ | **Boilerplate** | Minimal | Low |
580
+ | **Networking** | Manual RemoteEvent setup | Manual |
581
+ | **Learning curve** | Just Roblox APIs | Small pattern to learn |
582
+ | **Scalability** | Degrades without discipline | Good with init/start pattern |
583
+ | **Dependency management** | Manual require chains | Centralized loader |
584
+ | **Best for** | Jams, small projects, learning | Medium projects, teams wanting control |
585
+
586
+ ---
587
+
588
+ ## 8. Folder Organization
589
+
590
+ ### Small Game (Simple / Flat)
591
+
592
+ For game jams, prototypes, or games with under ~10 scripts.
593
+
594
+ ```
595
+ game
596
+ +-- ServerScriptService
597
+ || +-- GameManager.server.lua
598
+ || +-- DataHandler.server.lua
599
+ +-- ServerStorage
600
+ || +-- Maps/
601
+ +-- ReplicatedStorage
602
+ || +-- Remotes/
603
+ || | +-- DamageEvent (RemoteEvent)
604
+ || | +-- ShopPurchase (RemoteFunction)
605
+ || +-- SharedConfig.lua (ModuleScript)
606
+ +-- ReplicatedFirst
607
+ || +-- LoadingScreen.client.lua
608
+ +-- StarterGui
609
+ || +-- HUD (ScreenGui)
610
+ +-- StarterPlayer
611
+ || +-- StarterPlayerScripts
612
+ || +-- CameraController.client.lua
613
+ +-- Workspace
614
+ +-- Map/
615
+ +-- SpawnLocations/
616
+ ```
617
+
618
+ ### Medium Game (Modular)
619
+
620
+ Organized into feature folders. Each feature has its own server module, client module, and shared definitions.
621
+
622
+ ```
623
+ game
624
+ +-- ServerScriptService
625
+ || +-- Main.server.lua -- Bootstrapper: requires and inits all modules
626
+ || +-- Services/
627
+ || | +-- CombatService.lua
628
+ || | +-- DataService.lua
629
+ || | +-- ShopService.lua
630
+ || | +-- RoundService.lua
631
+ || +-- Components/
632
+ || +-- LootDrop.lua
633
+ || +-- DoorSystem.lua
634
+ +-- ServerStorage
635
+ || +-- Assets/
636
+ || | +-- Maps/
637
+ || | +-- NPCs/
638
+ || +-- Modules/
639
+ || +-- DataSchema.lua
640
+ +-- ReplicatedStorage
641
+ || +-- Remotes/ -- All RemoteEvents/Functions here
642
+ || +-- Modules/
643
+ || | +-- ItemData.lua
644
+ || | +-- Constants.lua
645
+ || | +-- Types.lua
646
+ || | +-- MathUtils.lua
647
+ || +-- Assets/
648
+ || +-- Effects/
649
+ || +-- Animations/
650
+ +-- ReplicatedFirst
651
+ || +-- LoadingScreen.client.lua
652
+ +-- StarterGui
653
+ || +-- HUD (ScreenGui)
654
+ || +-- ShopMenu (ScreenGui)
655
+ || +-- SettingsMenu (ScreenGui)
656
+ +-- StarterPlayer
657
+ || +-- StarterPlayerScripts
658
+ || +-- ClientMain.client.lua -- Client bootstrapper
659
+ || +-- Controllers/
660
+ || +-- CameraController.lua
661
+ || +-- InputController.lua
662
+ || +-- UIController.lua
663
+ +-- Workspace
664
+ +-- World/
665
+ +-- Lighting/
666
+ ```
667
+
668
+ ### Large Game (Framework-Based with Rojo)
669
+
670
+ Uses a loader pattern (SSA), Rojo for file sync, and a strict folder convention. File system mirrors the Roblox hierarchy.
671
+
672
+ ```
673
+ src/
674
+ +-- server/ --> syncs to ServerScriptService
675
+ || +-- Main.server.lua -- Bootstrapper
676
+ || +-- services/
677
+ || | +-- PlayerDataService.lua
678
+ || | +-- CombatService.lua
679
+ || | +-- EconomyService.lua
680
+ || | +-- MatchService.lua
681
+ || | +-- LeaderboardService.lua
682
+ || +-- components/
683
+ || +-- Destructible.lua
684
+ || +-- Interactable.lua
685
+ +-- client/ --> syncs to StarterPlayerScripts
686
+ || +-- ClientMain.client.lua -- Client bootstrapper
687
+ || +-- controllers/
688
+ || | +-- InputController.lua
689
+ || | +-- CameraController.lua
690
+ || | +-- UIController.lua
691
+ || | +-- SoundController.lua
692
+ || +-- ui/
693
+ || +-- components/
694
+ || | +-- Button.lua
695
+ || | +-- HealthBar.lua
696
+ || +-- screens/
697
+ || +-- ShopScreen.lua
698
+ || +-- InventoryScreen.lua
699
+ +-- shared/ --> syncs to ReplicatedStorage
700
+ || +-- modules/
701
+ || | +-- ItemData.lua
702
+ || | +-- Constants.lua
703
+ || | +-- Types.lua
704
+ || | +-- Util.lua
705
+ || +-- network/
706
+ || | +-- Remotes.lua -- Central remote definitions
707
+ || +-- assets/
708
+ +-- serverStorage/ --> syncs to ServerStorage
709
+ || +-- assets/
710
+ || | +-- maps/
711
+ || | +-- npcs/
712
+ || +-- modules/
713
+ || +-- DataSchema.lua
714
+ +-- starterGui/ --> syncs to StarterGui
715
+ || +-- HUD.gui.lua
716
+ +-- first/ --> syncs to ReplicatedFirst
717
+ +-- Loading.client.lua
718
+ ```
719
+
720
+ ---
721
+
722
+ ## 9. Best Practices
723
+
724
+ ### Separation of Concerns
725
+
726
+ Each script or module should have a single, clear responsibility. A `CombatService` handles damage and health. A `DataService` handles saving and loading. They communicate via well-defined interfaces, not by reaching into each other's internals.
727
+
728
+ ### Single Responsibility
729
+
730
+ One module = one job. If a module handles inventory AND crafting AND trading, split it into `InventoryModule`, `CraftingModule`, and `TradingModule`.
731
+
732
+ ### Minimal Coupling
733
+
734
+ Modules should depend on abstractions (function calls, events), not on internal state of other modules. If module A needs data from module B, A calls `B.getData()` rather than reading `B._internalTable` directly.
735
+
736
+ ### Server Owns State
737
+
738
+ The server is the **single source of truth** for all game state. Clients render and predict, but the server validates and authorizes.
739
+
740
+ ### Validate All Client Input
741
+
742
+ For implementation details (type checking, range checking, ownership, rate limiting), see **roblox-networking** → Client Validation.
743
+
744
+ **Core rules:**
745
+ - Every `OnServerEvent` handler must validate types, ranges, and ownership before processing.
746
+ - Never let the client set currency, health, or any authoritative value directly.
747
+ - Client sends intent ("I attacked target X"), server calculates outcome.
748
+
749
+ ### Use ModuleScripts Everywhere
750
+
751
+ Avoid putting game logic directly in Scripts or LocalScripts. Instead, keep Scripts/LocalScripts as thin bootstrappers that require and initialize ModuleScripts. This makes code reusable, testable, and organized.
752
+
753
+ ### Centralize Remote Definitions
754
+
755
+ Create all RemoteEvents and RemoteFunctions in one place rather than scattering `Instance.new("RemoteEvent")` across multiple scripts.
756
+
757
+ ```lua
758
+ -- ServerScriptService/CreateRemotes.server.lua (runs first)
759
+ local remoteFolder = Instance.new("Folder")
760
+ remoteFolder.Name = "Remotes"
761
+ remoteFolder.Parent = game:GetService("ReplicatedStorage")
762
+
763
+ local remotes = {
764
+ "DamageEvent",
765
+ "ShopPurchase",
766
+ "ChatMessage",
767
+ "PlayerReady",
768
+ }
769
+
770
+ for _, name in remotes do
771
+ local remote = Instance.new("RemoteEvent")
772
+ remote.Name = name
773
+ remote.Parent = remoteFolder
774
+ end
775
+ ```
776
+
777
+ ### Use Types and Constants
778
+
779
+ Define shared constants and Luau types in `ReplicatedStorage` so both sides use the same definitions.
780
+
781
+ ```lua
782
+ -- ReplicatedStorage/Modules/Constants.lua
783
+ local Constants = {
784
+ MAX_HEALTH = 100,
785
+ WALK_SPEED = 16,
786
+ SPRINT_SPEED = 24,
787
+ MAX_INVENTORY_SLOTS = 20,
788
+ INTERACTION_RANGE = 10,
789
+
790
+ ItemRarity = {
791
+ Common = 1,
792
+ Uncommon = 2,
793
+ Rare = 3,
794
+ Epic = 4,
795
+ Legendary = 5,
796
+ },
797
+ }
798
+
799
+ return Constants
800
+ ```
801
+
802
+ ---
803
+
804
+ ## 10. Anti-Patterns
805
+
806
+ ### God Scripts
807
+
808
+ **Problem:** One massive script (500+ lines) that handles spawning, combat, data, UI, and everything else. Impossible to debug, modify, or collaborate on.
809
+
810
+ **Fix:** Break into ModuleScripts with clear responsibilities. The main script becomes a thin bootstrapper.
811
+
812
+ ### Circular Requires
813
+
814
+ **Problem:** Module A requires Module B, which requires Module A. Causes one of them to receive an incomplete (empty table) reference.
815
+
816
+ **Fix:** Use the `init()` pattern (Section 6), dependency inversion, or event-driven decoupling.
817
+
818
+ ### Server Logic in ReplicatedStorage
819
+
820
+ **Problem:** Placing server-only game logic modules in `ReplicatedStorage`. Exploiters can read the source code, understand validation logic, and craft exploits.
821
+
822
+ **Fix:** Keep server logic in `ServerScriptService` or `ServerStorage`. Only put genuinely shared code (types, constants, utilities) in `ReplicatedStorage`.
823
+
824
+ ### Scripts in Workspace
825
+
826
+ **Problem:** Placing Scripts directly as children of Workspace or models in Workspace. These are visible to clients (exploiters can read them), are hard to find during development, and create organizational debt.
827
+
828
+ **Fix:** Put all server scripts in `ServerScriptService`. If you need a script to reference a specific model, have the script in `ServerScriptService` and use a path reference or CollectionService tag to find the model.
829
+
830
+ ### Not Using ModuleScripts
831
+
832
+ **Problem:** Duplicating logic across multiple Scripts/LocalScripts instead of extracting shared code into ModuleScripts.
833
+
834
+ **Fix:** If two scripts share logic, extract it into a ModuleScript in the appropriate location (`ReplicatedStorage` for shared, `ServerStorage` for server-only).
835
+
836
+ ### Trusting Client Data
837
+
838
+ **Problem:** Server blindly applies whatever the client sends (damage values, item quantities, positions).
839
+
840
+ **Fix:** The server must independently validate and calculate. The client sends **intent** ("I want to attack this target"), not **outcome** ("I dealt 500 damage").
841
+
842
+ ### Polling Instead of Events
843
+
844
+ For polling vs event-driven patterns, see **roblox-luau-mastery** → Anti-Patterns.
845
+
846
+ **Core rule:** Use events (`.Changed`, `GetPropertyChangedSignal()`, `Died`, etc.) instead of `while true do task.wait()` loops.
847
+
848
+ ### Overusing RemoteFunctions
849
+
850
+ **Problem:** Using `RemoteFunction` for everything, including fire-and-forget actions that do not need a return value. Each `InvokeServer` call yields the client thread until the server responds.
851
+
852
+ **Fix:** Use `RemoteEvent` for actions that do not need a response. Reserve `RemoteFunction` for when the client genuinely needs data back from the server.
853
+
854
+ ### Ignoring `task` Library
855
+
856
+ For deprecated `wait()`/`spawn()`/`delay()` vs the `task` library, see **roblox-luau-mastery** → Task Library.
857
+
858
+ ### Instance.new with Parent Argument
859
+
860
+ **Problem:** `Instance.new("Part", workspace)` passes the parent as the second argument. This causes the instance to be parented immediately during construction, which yields internally and can create race conditions - the instance replicates to clients before you've finished setting its properties.
861
+
862
+ ```luau
863
+ -- BAD: parent during construction, yields, race condition
864
+ local part = Instance.new("Part", workspace)
865
+ part.Size = Vector3.new(4, 1, 4) -- may already be visible to clients
866
+ part.Anchored = true
867
+ part.BrickColor = BrickColor.new("Bright red")
868
+
869
+ -- GOOD: create, configure, then parent
870
+ local part = Instance.new("Part")
871
+ part.Size = Vector3.new(4, 1, 4)
872
+ part.Anchored = true
873
+ part.BrickColor = BrickColor.new("Bright red")
874
+ part.Parent = workspace -- parent last
875
+ ```
876
+
877
+ **Rule:** Always set `Parent` last. Create the instance, configure all properties, then parent it.