7.7 KiB
Code style
Code should have clear indentation and generous spacing between logical steps, so a reader can follow it without unpacking a dense block.
Tabs and layout
- Use real tabs for Luau/Lua indentation, displayed at four columns.
- Use spaces around operators and after commas. Let StyLua handle continuation indentation; do not align columns by hand.
- Keep lines within 120 columns where practical. Wrap long signatures and calls with one argument per line.
- Prefer double-quoted strings, LF line endings, a final newline, and no trailing whitespace.
- Keep one statement per line. Do not compress guards, loops, or functions onto a single line.
stylua.toml defines formatter settings. .editorconfig gives editors the same tab and newline conventions.
Vertical spacing
Use one blank line to separate meaningful groups:
- Module comments, services, dependencies, types/constants, and the module implementation.
- Local declarations from the work that uses them.
- Guard clauses from the main path.
- Loops and conditionals from the next independent step.
- A side effect or calculation from the final return.
- Function definitions, including methods inside a returned API table.
Keep related assignments together, such as updating a position's X/Y values or configuring a UI object. Do not put blank lines between every table field or every assignment. Avoid several consecutive blank lines, decorative dividers, and empty lines immediately inside a block.
local function join(player: Player, token: number): ()
local entry = players[player]
if not entry or entry.token ~= token then
return
end
local room = rooms[entry.roomId]
if not room then
publish(player, "This lobby is no longer available.")
return
end
room.guest = player
entry.ready = true
startMatch(room)
end
StyLua preserves intentional blank lines; grouping statements remains the author's responsibility.
Block endings
After the final end in a guard, loop, conditional branch, callback, or function, leave one blank line before the next independent statement. This makes the end of one piece of logic visible before the next piece begins. Keep else, elseif, until, closing parentheses, commas, and table braces adjacent to the end they continue.
if not player then
return
end
local profile = profiles[player]
for _, item in ipairs(items) do
process(item)
end
publish(profile)
Do not add blank lines between an end and the end of its containing block. Do not use several blank lines or put an empty line immediately after an opening block. One blank line is the standard separator.
Local declarations
Keep consecutive local declarations together. After the final local variable declaration, leave one blank line before the statement that uses those values. This separates setup from work while keeping related variables easy to scan.
local bounceProgress = 1 - bounceTime / BOUNCE_DURATION
local compression = math.exp(-progress * 4) * math.cos(progress * 8)
local normal = 1 - compression * 0.42 * bounceStrength
local tangent = 1 + compression * 0.5 * bounceStrength
setBallSize(
BALL_WIDTH * (if bounceHorizontal then normal else tangent),
BALL_HEIGHT * (if bounceHorizontal then tangent else normal)
)
Do not put blank lines between variables that belong to the same setup group. Keep multiline declarations together until their closing expression.
Assignment groups
Treat assignments that update state the same way as local declarations. Consecutive assignments to related variables may stay together, but separate that group from calls, conditionals, loops, and other work with one blank line on each side. This keeps a state change visually distinct from the operation that follows it.
setBallSize(width, height)
bounceTime = math.max(0, bounceTime - dt)
setBallSize(BALL_WIDTH, BALL_HEIGHT)
Keep assignments that belong to one transition together:
bounceTime = BOUNCE_DURATION
bounceHorizontal = false
bounceStrength = 1
touchingWall = atWall
String composition and property edits
When a string is built across several lines with .., keep the complete assignment together, then leave one blank line before the next condition, call, or assignment. Treat property edits the same way: an assignment such as label.TextColor3 = color gets its own space before the next operation. A related run of property assignments may stay together when they form one transition.
rewards.Text = "<font color=\""
.. color
.. "\">"
.. xp
.. " XP</font>"
if result.xpRequired then
xpLabel.TextColor3 = gold
animateXP(result)
end
Modules and names
Start runtime Luau modules with --!strict. Keep services and shared dependencies near the top, followed by types, constants, module state, helpers, and public functions. End with the module return.
Use PascalCase for module names and exported types, camelCase for functions, variables, and record fields, and UPPER_SNAKE_CASE for local constants. Configuration files keep their established PascalCase keys. Prefer descriptive names; short coordinate names such as x, y, and dt are appropriate in simulation/rendering code.
Use one name for one concept throughout a module. Choose names that describe the value's role (player, profile, room, entry, settings) and keep related names parallel (host/guest, up/down, left/right). Avoid switching between abbreviations and full names for the same value. Use value only for a genuinely generic value and prefer a domain name when its meaning is known.
Keep each module responsible for one concern. Managers own state and rules; handlers connect those rules to Roblox services or presentation; libraries contain reusable behavior. Put first-party libraries in Modules/Libraries/Internal and third-party libraries in Modules/Libraries/External, under ReplicatedStorage for shared code or ServerStorage for private code. Preserve third-party licenses and formatting; .styluaignore excludes external libraries. Use shared types from src/ReplicatedStorage/Modules/Types instead of copying contracts.
Use consistent public APIs for related behavior, with explicit parameters and shared implementations.
Functions, types, and tables
Prefer guard clauses over deeply nested branches. Keep simple value choices as short if expressions; use ordinary if/elseif/else statements when a choice becomes hard to read.
Annotate public function parameters and return values where useful, and use shared domain types. Infer obvious local types. Avoid any; validate unknown remote or saved data at its boundary.
Small records may stay inline if they are easy to scan. Larger records and nested configuration should use one field per line with trailing commas. Group related fields with a blank line only when it clarifies the structure.
Use multiple assignment for values that belong to the same operation, such as coordinates or a coordinated state transition. Declare unrelated values separately.
Comments and rendering
Comments explain intent, constraints, or surprising behavior. Avoid narrating obvious assignments or repeating a function's name.
Keep work in frame callbacks bounded and avoid unnecessary allocations. Disconnect event connections and release owned instances when their owner is destroyed. Verify rendering and input in Studio. Formatting changes must not alter runtime behavior.
Before finishing a change
Format first-party source and checks:
nix-shell --run 'stylua src tests tools'
For behavior changes, run the existing checks:
nix-shell --run 'bash tools/check.sh'
Use focused regression tests for bugs and real Studio checks for rendering changes. Do not reformat vendored packages or backup archives. Keep formatting-only changes separate from behavior changes when reviewing the diff.