Appearance
Project layout
kiln.config.ts fixedHz, seed, mode ('3d' | '2d'), hotReload, vsync, maxFps
src/main.ts default-exports () => createApp(config)...: the whole schedule
src/data.ts traits, resources and events
src/prefabs.ts spawn recipes (and spawn helpers)
src/systems.ts systems, grouped by phase
src/plugins/<name>/ vendored community plugins (yours to read and edit)
scenarios/*.test.ts headless tests
recordings/ play sessions saved with F8 in the dev server
public/ assets, loaded by path ('models/ship.glb' → public/models/ship.glb)
AGENTS.md the Kiln guide plus a generated section for this gameStart with these few files and split one only when it gets long (src/systems/combat.ts, src/systems/ui.ts, ...). Import everything from 'kiln'; physics comes from 'kiln/physics' and test helpers from 'kiln/test'.
The entry module
src/main.ts default-exports a function that builds the app and returns it. It is the only place the schedule is written down, so reading it top to bottom tells you everything that runs:
ts
export default () =>
createApp(config)
.use(CorePlugins, CameraPlugin)
.init(Score, Screen, Hit)
.add(Startup, SpawnLevel)
.add(Fixed, runIf(inState(Screen, 'playing'), chain(PlayerMove, Movement, CollectCoins)))
.add(Update, UpdateHud).use(...)adds plugins..init(...)registers resources and events. Resources also create themselves on first use, but listing them here makes them show inkiln describeand snapshots from the start..add(Phase, ...)schedules systems in a phase.
The function may be async. The browser runtime, the CLI and the scenario runner all call it, so it must not call .start() or touch the DOM.
A different entry file can be set with entry in kiln.config.ts.
Names come from exports
export const Movement = system(...) is named Movement. The same goes for traits, relations, resources, events, prefabs, plugins and states. Those names appear in errors, in kiln describe, in snapshots, in recordings and in AGENTS.md, and kiln run --snapshot Health finds a trait by its name.
A small runtime transform does the naming: it rewrites const X = system(...) into const X = __kiln_named("X", system(...)). The kiln() Vite plugin applies it in the browser and kiln/preload applies it under Bun (the template's bunfig.toml preloads it for bun test). Types are untouched. Two consequences:
- Moving code between files changes nothing. Kiln never names things after their file.
- Define Kiln objects with
constand a direct factory call.let X = system(...), or a system returned from a helper function, isn't named; call.named('X')on it (or pass thenameoption tosystem).
JSDoc is the documentation
Give every export a one-line JSDoc comment:
ts
/** Hit points; the entity despawns at 0. */
export const Health = trait({ hp: 3 })kiln docs copies these lines into the generated part of AGENTS.md, next to each system's place in the schedule, and lists exports that have none. See Working with agents.
Files and hot reload
Keep systems in files that define only systems (plain constants and helper functions are fine), and export each one. Editing a systems-only file swaps those systems in place and keeps the world. A file that also defines a trait, resource, event, prefab or plugin reloads the app instead. See Hot reload.
Configuration
kiln.config.ts default-exports a config object:
ts
import { defineConfig } from 'kiln'
export default defineConfig({
fixedHz: 60,
seed: 1,
mode: '3d',
hotReload: 'keep',
})Every field is optional. The full list is in Configuration.