Project structure
KILLSCRIPT ultimately loads one client script and, for Reflex, one server script. Your source does not need to imitate that limitation. Static imports are bundled, so files can follow feature boundaries instead of generated-file boundaries.
Recommended starting layout
Section titled “Recommended starting layout”src/├── client/│ ├── main.ts│ ├── controls.ts│ ├── network.ts│ └── features/│ ├── module-toggle.ts│ └── status-hud.ts├── server/ # Reflex only│ ├── main.ts│ ├── network.ts│ └── features/│ └── state-control.ts└── shared/ ├── messages.ts └── settings.tspublic/├── images/├── sounds/└── ui/killscript.config.jsonThis is a direction, not a required empty-folder template. Create a directory when the module actually has code that belongs there.
Entry files compose features
Section titled “Entry files compose features”Keep main.ts intentionally boring:
import "./features/module-toggle";import "./features/status-hud";An imported feature registers its module-lifetime handlers at the top level:
import { controls } from "../controls";
let enabled = false;
controls.toggle.onPressed(() => { enabled = !enabled; NotificationController.ShowHint( enabled ? "Module enabled" : "Module disabled", 1.5, );});Static modules execute once when the bundle starts. You do not need an
initSomething() function whose only job is to register top-level callbacks.
Export functions when another feature genuinely needs to call a behavior, not
as ceremony around every file.
Put code where its owner can use it
Section titled “Put code where its owner can use it”client/may use cameras, controls, UI, audio and other client globals;server/may use Reflex globals and make authoritative decisions;shared/should contain plain types, constants and data transformations that do not assume either runtime API;public/contains files copied into the module package;- settings may be shared when both contexts need the same identical definition.
The compiler rejects a client implementation import from server/ and the
reverse. This catches an architecture mistake before generated Lua reaches the
game.
Registrations may be colocated
Section titled “Registrations may be colocated”Settings, controls and network declarations may appear in any reachable file,
but must initialize a top-level const:
// Valid: the setting belongs to this feature.const settings = defineSettings("statusHud", { visible: true,});// Rejected: generated JSON would depend on runtime control flow.function registerLater(): void { const settings = defineSettings("statusHud", { visible: true });}Use a shared declaration file when several features import the same handle; otherwise colocating a declaration with its only consumer is clearer.
What the compiler emits
Section titled “What the compiler emits”Only files reachable from an entry are bundled:
src/client/main.ts ──▶ scripts/main.luasrc/server/main.ts ──▶ scripts/server.luapublic/** ──▶ copied module assetsRelative imports omit the extension. Do not call require() manually; the
generated bundle contains its own static module loader and does not depend on
the game’s nested path resolution.
Continue with Patterns for toggleable features, resource ownership and shared state.