Scripts are Luau files attached to Blueprints, Areas and the Module in the editor. The engine calls your handlers (OnUsed, OnOpened, OnEnter, OnModuleStart…) when things happen in the world. Each event runs in a fresh, bounded VM; a script that goes over a limit stops with an error instead of stalling the server.
Light-effect scripts may make 2048 native calls.
Edit with VS Code
- Install Visual Studio Code and its Luau Language Server extension (
JohnnyMorganz.luau-lsp). We test and document Script editing with VS Code. Scripts are plain.luaufiles, so other editors with a Luau language server can work too, but we do not test them or help set them up. - Create or open your project in Ironspawn Editor, including Crypt or First Steps. The editor creates/refreshes
.vscode/settings.json,.vscode/extensions.jsonand.luaurc, and copies matching types to.ironspawn/ironspawn.d.luauin that project's root. Open that folder in VS Code and accept its extension recommendation. No hand-written configuration or downloaded type file is needed. The installed editor supplies the matchingEditorResources/Scripting/ironspawn.d.luau; the dev editor copies the repository'sdocs/scripting/ironspawn.d.luau. The copy is written only when its bytes differ. - Create/register the Script in Ironspawn Editor's Content Browser. Open its Script editor, apply source drafts, then choose Save and open externally. This saves committed Document values before opening the
.luaufile in VS Code. A loose, unregistered.luaufile is not a Script asset. A Script's.rpgscriptdescriptor beside it holds its stable identity; keep that descriptor. - Save in VS Code, return to Ironspawn Editor and use File > Reload from disk after the changed-file notification. Resolve any dirty-work choice explicitly, then Build & Play. External saving does not automatically replace the Document.
The generated settings select the standard platform, disable Rojo sourcemaps, map @ironspawn to .ironspawn/ironspawn.d.luau and disable absent standard-library globals from the SA1 registry. Other workspace settings, definition packages and extension recommendations survive refresh. Commit .vscode, .luaurc and .ironspawn with your project to share setup through git. The definition path is relative to the workspace folder, so moving/cloning the project needs no path edit. Open the project root as the first VS Code workspace folder. These are editor tooling files, outside source inventory, cook input and exact source conflicts; they never enter a published module. This repository git-ignores all three under modules/*/source.
Invalid JSON (including JSONC comments/trailing commas) is left alone, with a Script tooling notice in the editor and script-tooling-warning in its log. Correct the indicated file and reopen the project to refresh it. Non-object roots and incompatible owned containers are also reported rather than replaced.
.luaurc uses nonstrict: ordinary unannotated event handlers and literal metadata remain convenient while unknown globals and known API argument types are checked. UnknownGlobal, UninitializedLocal, DuplicateLocal, ImplicitReturn and UnreachableCode are enabled. LocalUnused and FunctionUnused are disabled because the host consumes metadata locals and exported entry functions outside normal Luau call flow. Other lint choices survive refresh. Luau documents its lint behavior.
Runtime rules beyond the language server
- Use numeric
for(for i = 1, #objects do). Generalized iteration andfor ... inare rejected, even if the language server accepts them. - Use ordinary quoted strings; backtick string interpolation is rejected.
- There is no standard library: no
print,assert,pairs,ipairs,require,math,stringortable, including string-library method calls. Operators, numeric loops and#arraywork without libraries. - Declare capabilities literally. A light Script declares
light_effecttags (literal tags or$exported_property) andself = trueto access its own Lights. Dialogue entries declare their parameters, reads and proposed effects throughexported_functions. Definitions cannot prove these per-entry permissions, quotas, lifetimes or transaction rules. Cook/runtime admission remains authoritative.
self:GetProperty returns boolean | number | string; assert the known exported type, as below. Dialogue.Param is any in tooling because its type comes from the entry's literal parameter declaration; runtime still validates those arguments.
Worked example: lever and torches
Create a stationary mesh-backed lever Placeable with a Script component. Copy the complete lever Script into its Script asset:
local exported_properties = {
target_tag = { type = "string", default = "crypt_torch" },
}
local light_effect = { tags = { "$target_tag" }, self = true }
function OnUsed(user)
local objects = Area.FindByTag(self:GetProperty("target_tag") :: string)
for i = 1, #objects do
if objects[i] ~= self then
local lights = objects[i]:GetLights()
for j = 1, #lights do
lights[j].Enabled = not lights[j].Enabled
end
end
end
local lamps = self:GetLights()
for i = 1, #lamps do
lamps[i].Enabled = not lamps[i].Enabled
end
endTag each torch placement crypt_torch and give it a Light component. The exported target_tag can be changed in the Inspector; $target_tag grants the corresponding query capability. The first numeric loop visits matching objects in this Area, the second toggles their Lights, and the final loop toggles the lever's optional lamp. Skipping self in the query avoids double toggles if the lever has the tag. Apply, Save, reload after external edits, then Build & Play and Use the lever. Watch ground illumination/shadows: this changes Lights, not flame meshes.
Other editors that run luau-lsp can load the same definition file and disabled-globals list from .vscode/settings.json; their users configure luau-lsp themselves. This is unsupported.
Lifecycle events
Named lifecycle handlers use one Script slot per owner. The Inspector lists Handles and warns when a function cannot fire on that owner. Door Open/Close and Container per-viewer Open/Close use OnOpened(user) / OnClosed(user); explicit Use calls only OnUsed. Page changes do not close and reopen a Container.
Choose a Script in the Module Script card or the selected Area Script card in Module editor. Area OnEnter(character) / OnExit(character) track membership. Module OnPlayerJoin(character) precedes Area entry; Area exit precedes OnPlayerLeave(character). OnModuleStart() runs after restore and recovery departures, before new joins; IsFirstStart() is true for the first admitted start only, including a faulting handler. Lifecycle effects and completion are saved together. They may run while paused; there is no persistent VM or runtime Connect.
Examples: Door light toggle, Area arrival lights, and Module start counter. Character arguments expose a captured Name; Area/Module self supports GetProperty and equality. Module Scripts are counter-only; Area light Scripts query declared tags and have no self lights. Lifecycle handlers return nothing and may make no writes.