= Forge Scott Duensing :revnumber: 3.00 :revdate: 2026 :doctype: book :toc: left :toclevels: 3 :sectnums: :sectnumlevels: 3 :source-highlighter: rouge :icons: font :experimental: [preface] == About Forge Forge is Singe's authoring tool: a way to make a game by describing it -- placing things, attaching behaviours, writing rules -- and having ordinary Singe Lua written for you. It is itself a Singe game and is distributed on its own, beside the engine rather than inside it. This document covers the description format, the vocabulary, the compiler, and the editor. The engine calls the generated code makes (`playerNew`, `collidePointRect`, `onKeyPressed`, `scriptPush`, and the rest) are documented in the Singe Manual. == Describing a Game Instead of Writing One A game can be written as a *description* -- a table of kinds, rooms, and rules -- and compiled into an ordinary Singe game. `Forge/AuthorCompile.singe` does the compiling and `Author.singe` is the runtime the result calls. **No part of Forge ships with Singe** -- not the editor, not the compiler, not the runtime -- so a game built with it carries its own copy of that runtime and stands entirely on its own. Nothing is interpreted at run time: the rules become real Lua functions, so a description costs nothing per frame on a Raspberry Pi, and the game it produces can be opened, read, and edited by hand like any other. There is no notion of genre anywhere in it. A game declares which of the engine's own layers it uses, and that is the only difference between a light gun game, a platformer, and a quick-time event over video. === The nouns *Layers* are what the game draws through: `world2d` (physics in the XY plane, drawn into the overlay), `scene3d` (the 3D scene, with a sun, a sky, and fog; positions are world units), `overlay` (flat drawing), `disc` (the video the game is played over), `hud` (an RmlUi document bound to the game's vars), and `music`. A game lists the ones it wants. *Kinds* are what things are: a `look` (`box`, `sprite`, `text`, or `none` in 2D; `model`, `mesh`, `light`, `billboard`, or `text3d` in the scene), any number of `behaviours`, and `vars` -- the state every instance starts with. Everything placed or spawned is an instance of a kind. *Rooms* are where things are. A room lists its `entities` -- a kind, an id, a position, and any vars that differ -- and its `tracks`. A room keeps its state when it is left unless it says `reset = true`; vars declared on the game live across rooms. *Rules* are a trigger, conditions, and actions. `on` names the event -- every frame, a collision between two kinds, a shot landing, a death, a timer, a disc frame passing -- and `each` scopes the rule to every instance of a kind, with `self` bound. All of a rule's conditions must hold for its actions to run; an `any` group inside them is an OR. .A description, in full [source,lua] ---- return { title = "One rule", layers = { { kind = "world2d", gravity = 1500 } }, vars = { score = 0 }, kinds = { ground = { look = { kind = "box", w = 720, h = 40, r = 60, g = 70, b = 90 }, behaviours = { { kind = "solid" } } }, hero = { look = { kind = "box", w = 24, h = 44, r = 230, g = 90, b = 170 }, behaviours = { { kind = "platformer", speed = 210, jump = 620 } } } }, rooms = { { name = "start", entities = { { kind = "ground", id = "ground", x = 360, y = 440 }, { kind = "hero", id = "hero", x = 120, y = 380 } } } }, rules = { { note = "Run right", on = "frame", when = { { "keyHeld", key = "RIGHT" } }, act = { { "run", entity = "hero", direction = 1 } } } } } ---- Compile it and run what comes out: [source,lua] ---- dofile("Forge/AuthorCompile.singe") dofile(authorBuild("mygame.game", singeGetDataPath() .. "mygame.singe")) ---- `testScripts/author/` holds the worked examples -- a platformer, a QTE over video, a shoot-em-up, a light gun game over video, a rail shooter, a 3D platformer, an adventure in 2D and in 3D, a racing game, a tower defence, branching video, a first-person shooter, breakout, a maze, third-person action, a space shooter, a rhythm game, and bowling, with two more for the sprite, tile, MIDI, water, and soft-body pieces -- and `testScripts/scene52.singe` onward compile and play them. === Instances, vars, and states A kind's `vars` are what each instance starts with: `vars = { health = 3, ammo = 6 }`. An entity in a room may override them (`vars = { health = 5 }`), and rules read and change them: `self.health` in an expression, `setVar` and `addVar` as actions. `state` is a var every instance has, `"idle"` to begin with; `setState` changes it and `inState` tests it. Vars declared on the game -- `vars = { score = 0, lives = 3 }` -- belong to no instance and are read by name: `score`, `lives`. `setVar` and `addVar` with no entity change them, and `addScore` is `addVar` on `score`. Instances are made when a room is entered, by a `spawner` or `shooter` behaviour, or by the `spawn` action, and go with `destroy`, with a `health` that reaches zero, or with a `projectile` that leaves the picture. An instance made at run time has an id of its own (`raider#7`); rules reach it as `self` or `other`, or through `each`. === Events A rule's `on` says what triggers it: [cols="1,3"] |=== | `frame` | every frame, the default | `pressed`, `released` | a key or a switch, once per press; `key` or `switch` narrows it | `collision` | an instance of kind `a` began touching one of kind `b`; `self` is `a`, `other` is `b` | `hit`, `miss` | a gun's shot landed on `self` (`other` is the gun), or landed on nothing; `event.offscreen` says whether a miss left the picture | `death`, `spawn` | `self`'s health reached zero; `self` was just made | `timer` | a timer on `self` went off; `event.name` says which | `frameReached` | the disc passed `frame` | `enter`, `leave` | an instance of kind `b` entered or left a trigger of kind `a` | `arrived` | a seeking instance reached its target | `stopped`, `railEnd` | a rail camera reached a stop (`event.name`), or the end of its track | `animationDone` | a clip that does not loop ended on `self` | `roomStart`, `roomEnd`, `gameOver` | the game's own moments |=== The event's parameters are written on the rule itself (`on = "collision", a = "shot", b = "raider"`). `each = "raider"` runs a frame rule once per live raider, and narrows an event rule to that kind; `room = "hall"` keeps a rule to one room. === Expressions Anywhere a number goes, an expression may go instead, as a string: numbers, strings, `+ - * / % ^ ..`, comparisons, `and`, `or`, `not`, and the names below. Nothing else is allowed -- an expression is checked when the game is built -- and the `lua` action is the way past it. [cols="1,2"] |=== | `self.health`, `other.x`, `event.frame` | the instance the rule is about, the other one, the event's own fields | `hero.x` | a placed entity's field or var, by id | `score`, `lives` | the game's vars | `time`, `frame` | seconds since the game began; the disc frame | `count("raider")` | live instances of a kind | `random(a, b)`, `distance(a, b)`, `has("key")` | a number in a range; between two instances; whether the inventory holds an item | `abs`, `min`, `max`, `floor` | arithmetic |=== A `test` condition takes one: `{ "test", expr = "count('raider') == 0" }`. === Sequences Some actions take time. `say` shows a line and waits for it; `wait` waits. A rule with one of them runs as a coroutine, its actions in order, each waiting its turn -- a cut-scene is the same shape as `addScore`. While it runs the same rule does not start again for the same instance unless the rule says `interrupt = true`, and `controls = false` on the rule takes the keys away for its duration. === Tracks A track is keys by disc frame (`key = "frame"`) or by time (`key = "time"`), and a room may hold any number. A track of `boxes` is a hitbox that moves with the picture: [source,lua] ---- tracks = { { name = "bandit", key = "frame", boxes = { { at = 120, x = 100, y = 200, w = 60, h = 100 }, { at = 220, x = 400, y = 200, w = 60, h = 100 } } } } ---- Between keys the box is interpolated; outside them there is none. A kind with `{ kind = "hitbox", track = "bandit" }` is hit where the track says, which is how a light gun game over video is 2,750 rectangles no longer. A track of `points` is a rail for a camera: positions by time, each looking at a point (`look = { x, y, z }`) or at an entity by id, and any point may be a `stop` that holds the camera there until a `pathNext` action sends it on -- which is what House of the Dead does at every doorway. [source,lua] ---- { name = "rail", key = "time", points = { { at = 0, x = 0, y = 1.6, z = 0, look = { 0, 1.2, -10 } }, { at = 4, x = 0, y = 1.6, z = -6, look = { 0, 1.2, -16 }, stop = "hall" } } } ---- === Games in the scene A game with a `scene3d` layer places its kinds in world units and draws them through the engine's renderer. A `model` look is a glTF file with a scale and a starting clip; `mesh` is a box, sphere, cylinder, or plane with a colour; `light` and `billboard` are what they say. The 2D looks still draw on the overlay over the scene, which is how a score readout works in both. The behaviours that live there: `character` is the engine's controller moving by the `keys` vars relative to the camera; `camera` draws the scene from the instance -- `fixed`, `follow` (an offset from a target), `orbit` and `first` (the mouse looks), or `rail`; `seek` walks toward a target and raises `arrived`; `animator` plays a clip per state and raises `animationDone`; `target` gives a `gun` something to hit, with `zones` on named bones so a shot says which part it landed on (`hitPart`); `health` with `ragdoll` lets a model fall limp on death; `solid`, `body`, and `trigger` are the physics. A gun in the scene casts a ray through the pointer, so the same `hit` and `miss` rules serve a light gun over video and a rail shooter. `testScripts/author/railshooter.game` is House of the Dead in a table: a rail with two stops, zombies that spawn there, walk to the camera, and bite on a timer unless they are killed, headshots through a zone on the head bone, and the rail moving on when the stop is clear. === Adventures: rooms, walking, hotspots, verbs, and talk A point-and-click adventure -- Sierra's or LucasArts' -- is rooms that remember themselves, a character that walks where the player clicks, hotspots a verb is used on, an inventory, dialogue, and cut-scenes. All of it is the same nouns. *Walking.* A 2D room declares `walk = { { x, y, x, y, ... }, ... }`, its walkable floor as polygons in overlay coordinates; a 3D room declares `navFrom = { "floor", "ledge" }`, the entities whose meshes are its floor. Either is baked into a navigation mesh when the room is entered, and a kind with a `walker` behaviour walks it: `walkTo` sends it to a point (and waits), `walkToPointer` to where the player clicked, `walkToHotspot` to a hotspot's walk point; it raises `arrived`, and its state is `walk` or `idle` as it goes. A painted room may say `depthSort = true`, so what is lower on the picture is drawn over what is higher, and `scaleBy = { { y = 300, scale = 0.6 }, { y = 460, scale = 1 } }`, so a character further up the picture is drawn smaller; looks with `anchor = "feet"` stand on their position rather than being centred on it, which is what a walking character wants. *3D over a painting.* A room in the scene may be a picture on a flat `mesh` (its `texture`, `unlit`) with 3D characters in front of it. Whatever stands in the picture -- a pillar, a doorway, a table -- that a character has to walk behind is a `mesh` look with `occluder = true`: an invisible box that hides what is behind it while the picture shows through it. The editor's 3D viewport keeps occluders visible so they can be placed; the game hides them. `testScripts/author/painted.game` and scene 79 show a character crossing a painted room behind such a pillar. *Hotspots.* A kind with `{ kind = "hotspot", name = "door", walkX = 600, walkY = 330 }` is something a verb can be used on: its look's box, or a `polygon` of x,y pairs, in 2D; a box the size of its look, met by the ray through the pointer, in 3D. The name is what the sentence line shows. *Verbs.* A game with `verbs = { "walk", "look", "take", "use", "open", "talk" }` builds a sentence line from the current verb, the hotspot under the pointer, and the item in hand (`sentence`, `hover`, `verb`, and `item` are game vars, so a text look or a HUD element can show them). A click on a hotspot raises `verb` with `verb`, `target`, and `item`, and **the most specific rule wins**: `verb = "use", item = "key", target = "door"` beats `verb = "use", target = "door"` beats `verb = "use"` beats a rule that names nothing, which is the "That doesn't work" every adventure needs. A rule with conditions ranks above one without among the same parameters, so "open the door while the guard objects" and "open the door" can share their parameters and differ in a `test`. `setVerb`, `nextVerb`, and `useItem` choose; the first verb in the list is what a click on the floor means, and the `pressed` event carries the pointer's `x` and `y` for a `walkToPointer` rule. *The parser.* A `parser` layer takes typing at a prompt: `words = { look = { "look", "examine", "l" }, door = { "door", "gate" } }` says what the game knows, and a typed line raises `said` with its `verb`, `noun`, and `second` as the words table knows them -- or `event.unknown`, the first word it did not -- with the same most-specific-wins rule. A game may offer both a verb line and a parser; the rules do not care which was used. *Inventory.* `give` and `take` keep a list in the `inventory` var; `has("key")` tests it in an expression and `has` as a condition. *Dialogue.* The game's `dialogues` are trees: [source,lua] ---- dialogues = { guard = { start = "hello", nodes = { hello = { who = "Guard", text = "Nobody passes.", choices = { { text = "Why not?", next = "why" }, { text = "Fine." } } }, why = { who = "Guard", text = "The door is locked.", choices = { { text = "I have a key.", when = 'has("key")', act = { { "setVar", name = "allowed", value = true } }, next = "ok" }, { text = "I see." } } }, ok = { who = "Guard", text = "Then go ahead." } } } } ---- `talk` runs one and waits for it: each node's line is said, its choices (those whose `when` holds, and not those already chosen once with `once`) are offered and picked with the number keys or a click, the choice's `act` runs, and `next` names the node after. With a HUD, the choices go in the element called `choices`. *Cut-scenes and rooms.* A rule with `controls = false` takes the keys away while it runs; `fade` (out, then back with `out = false`) and `goTo` change the picture and the room, and `goTo` with `at = "arrival"` puts the carried entity where the entity of that id stands in the new room, so the same rule serves a painted room and a modelled one. Sequences survive a room change. *Saving.* `saveGame` keeps the whole game -- the room, every visited room's instances and their vars, the game's vars, the inventory -- in a numbered slot, and `loadGame` brings it back. A save taken while a sequence runs records the world as it stands; the sequence does not resume. `die` is the Sierra death: a line, a fade, and the game over from the start. `testScripts/author/adventure2d.game` is the two-room adventure in a painted room, and `adventure3d.game` is the same game in a modelled room from a fixed camera -- it takes the 2D description's rules, dialogue, verbs, and parser as they are and supplies only kinds and rooms. Scenes 64 and 65 play them. === Vehicles, crowds, turrets, and physics toys `vehicle` puts a kind on the engine's vehicle physics -- a car, a motorcycle, a tank, or a boat -- with wheels hung at the corners its look's size gives, driven by the `keys` vars (`dy` throttle, up is forward; `dx` steering), and `self.speed` says how fast. `racer` drives a vehicle round a track of `points` on its own, steering toward the next and easing off in bends, which is every opponent in a racing game. `camera` in `follow` mode with a `lag` is what sits behind the car. `walker` with `follow = "hero"` keeps an instance after an entity (or the camera) across the navigation mesh, asking again every so often -- a crowd of enemies in a shooter, a companion. `patrol` walks a list of points in turn, looping or stopping, and raises `patrolEnd` at the last: a creep's lane, a guard's round. `turret` fires at the nearest instance of a kind within range, no faster than its rate, spawning a projectile aimed at it (or doing the damage itself), and `spawnAtPointer` makes an instance where the player clicked -- on the floor the ray meets, in 3D -- which is how turrets are placed. `body` is a thing physics moves (mass, bounce, friction); `thrust` pushes a body along its nose by `dy` and turns it by `dx` (a hovercraft, a ship); `joint` hangs a body from another's, or from the world, by a hinge, a ball, or a slider through a point along an axis; and `push` shoves a body, or a ragdoll's bone. `testScripts/author/racing.game` (a car, a rival on a track, checkpoint triggers, a hinged bar, a hovercraft) and `towerdefence.game` (creeps in waves on a lane, turrets placed by clicks, gold and lives) are the worked examples; scenes 66 and 67 play them. === Arcade plumbing: branching video, lives, credits, the board A `branches` track is Dragon's Lair: each branch is a window of disc frames, the `move` (a key) or `switch` it wants, and the frames the disc goes to on `success` and on `fail`; a kind with `{ kind = "branching", track = "moves" }` watches it and raises `branchOpen` (with `event.move`), `branchTaken`, and `branchMissed`. Lives, credits, and continues are vars and rules -- `credit` on `SWITCH_COIN1`, a `restart` on `SWITCH_START1` when `credits > 0` -- and a `bezel` layer shows `score`, `lives`, and `credits` (and a second player's `score2` and `lives2` with `twin`) on the arcade panel around the picture. `gameOver` ends the rules, keeps the best score across runs, and shows the results card; `submitScore` sends the score to the master service's board for the game, queued until it can go. `testScripts/author/fmv.game` is the worked example and scene 68 plays it: one window answered, three missed, the lives spent, the score sent, a coin, and a continue. === The long tail: sprite sheets, tiles, MIDI, water, soft bodies `distance(a, b)` takes instances or ids (`distance(self, "hero")`) and measures through the scene in 3D. A rule with `each` on an event that has no instance of its own -- a key, a room, the disc -- runs once per instance of the kind, as a frame rule does, which is how "a swing of the sword hits every enemy in reach" is one rule. A `solid` with `moving = true` is a body that follows wherever the instance is moved: a paddle, a lift. A `sprite` look turns with the var `angle` (degrees, clockwise), which an entity's `rz` starts and which rules, a `spin` behaviour, or a `projectile` with `aim` change -- a turning image is a whole one, not a sheet. `spin` turns a 3D instance about Y the same way. A `particles` look is a steady stream from the instance -- a fire, smoke, a thruster, rain -- with a colour, a rate, a size, a speed, and a life, and the `emit` action is a burst from any instance. A `sprite` look with `frames` is a sheet of that many columns, and a `frames` behaviour steps it: `states = "idle=1-1, walk=2-5"` gives each state its run of frames, played at `fps`, so a walker's walk cycle follows the state the `walker` (or a rule) sets. Art need only face one way: the look's `faces` says which (`right` unless said), and while the var `facing` -- which the `walker` and the `mover` set as they go -- is the other way, the image is mirrored. A sheet drawn facing both ways names the other way's frames for the state with `Left` or `Right` on the end (`walkLeft=6-9`), and those are used instead of the mirror; `faces = "none"` never mirrors, for a top-down sprite that turns rather than flips. A sheet entity with an `rz` (or a var `angle`) draws its frame turned. A `grid` look draws a map of tiles from a sheet -- `columns` tiles wide, each `tile` pixels square -- from rows of tile numbers (`map = "1 2 1; 2 1 2"`, `0` for nothing), which is a maze or a level in a string. A game that opened a MIDI port hears `midi` events with `event.pitch` and `event.velocity` for every note struck. In the scene, a mesh look with `water` is filled with water: its top is the surface, and a `body` with `buoyancy` floats in it; a `vehicle` of type `boat` with `thrust` is pushed by its propeller while that sits under the surface; and `soft` makes a mesh look a cloth or an inflated body. `testScripts/author/tail.game` and `pool.game` are the worked examples; scenes 70 and 71 play them. === The way out The `lua` action takes a line of Lua and emits it as it stands. It is there on purpose: when a rule needs something the vocabulary cannot say, that rule drops to Lua and the rest of the game is unaffected. A description is a convenience, not a cage, and the compiled output is a normal game you can stop describing and start editing whenever it suits you. == The Vocabulary Conditions, actions, behaviours, looks, layers, and events are not built into the compiler. Each is an entry in the `AUTHOR` table in `Author.singe` declaring its parameters and the Lua it emits, so a new kind of game is a set of entries rather than a new release. The tables below are generated from that manifest. The parameter types: `number` (a number or an expression), `expression`, `string`, `boolean`, `entity` (`"self"`, `"other"`, or an entity's id), `kind`, `scancode` (a key name from `SCANCODE`), `switch` (a `SWITCH_*` name), `file`, `state`, `track`, `room`, and `lua`. A behaviour's parameters are plain values in the description; key and switch names among them are resolved when the game runs. include::ForgeVocabulary.adoc[] == The Editor `Forge/Forge.singe` edits a description, and it is itself a Singe game. It has its own directory beside the games, appears in the menu like one, and packs to `Forge.game`; the build does that itself (the `forge` target), with this manual inside. Nothing in it is a preview: the canvas is the same overlay at the same coordinates the game will be played in, so what is placed is what is seen. Started from the menu it opens on a chooser: the descriptions in its data directory, the samples inside `Forge.game` and any dropped into the `Forge` directory itself (those are opened as a copy, since inside a `.game` they are read only), and *New game*, which writes a starter -- ground, a hero that runs and jumps, a score readout -- and opens it. The first run copies this manual, `Forge.pdf`, out of `Forge.game` into that data directory, and the chooser says where it is. `Esc` in the editor closes the description (twice, when it has unsaved changes) and `Esc` on the chooser leaves Forge. `P` plays: the description is saved, compiled beside a copy of the runtime, and handed to the engine with `scriptPush`; when the game ends Forge comes back on the same file. A script can drive the editor instead, which is how the test scenes do it: [source,lua] ---- FORGE_LIBRARY = true dofile("Forge/Forge.singe") forgeBegin("mygame.game") function onOverlayUpdate() local x, y = mouseGetPosition(0) forgeDraw(x, y) return OVERLAY_UPDATED end ---- The panel is an RmlUi document; the canvas beside it is drawn into the overlay and picked with `collidePointRect`. The two compose because the engine offers a button to the GUI first and passes on what it did not use, while pointer motion is never consumed at all -- so point `forgePress`, `forgeDrag`, and `forgeRelease` at the mouse callbacks and clicks on the panel will not reach the canvas. The panel slides: drag the tab on its outer edge, or use the arrow keys, so an entity that lives underneath it is never permanently out of reach. === The four panels `Tab` cycles the panel through the room's *entities*, the *kinds*, the *rules*, the room's *tracks*, and the *dialogues*. Whatever is selected is edited the same way: `Enter` walks its fields and each is typed; `Enter` again moves to the next, `Esc` puts the value back. A typed number comes back a number, because `100` and `"100"` compile to different source. *Entities*: `A` adds one of the selected kind in the middle of the canvas, `D` duplicates the selected one, `Delete` removes it, and dragging moves it. Its fields are `id`, `kind`, `x`, `y`, `z`, `scale` -- and in a 3D room `rx`, `ry`, `rz`, its turn about each axis in degrees -- and each var its kind declares, overridable here. A scale scales the look, the body, and how big the instance counts as for touching. Renaming an entity renames it in every rule that talks about it. `PgUp` and `PgDn` move between rooms; `N` adds a room. With nothing selected, `Enter` edits the room itself: its `name`, `reset`, `depthSort`, `walk` areas (polygons of x,y pairs, separated by semicolons, drawn on the canvas), `navFrom`, and `scaleBy` (as `300:0.6, 460:1`). In every picker, typing narrows the list to the names that contain what was typed, and `Backspace` widens it again. A field that names a file -- a look's image or model, a sound, a HUD document -- opens a picker of the files under the game's directory instead of a prompt; `Esc` on it leaves the field to be typed. *Kinds*: `A` adds one, `D` duplicates, `Delete` removes one no room still places. Its fields are `name`, `look` and what that look takes (`look.w`, `look.file`, ...), `vars` typed as `health=3, ammo=6`, `behaviours` typed as a list of names (`keys, mover, shooter`), and each behaviour's own parameters as `mover.speed` and so on. The fields come from the manifest, so a new look or behaviour is editable the moment it is declared; renaming a kind renames it in every entity and rule. In a 2D room, `V` draws a polygon corner by corner on the canvas: with the selected entity's kind carrying a `hotspot` behaviour it becomes that hotspot's outline, otherwise it is added to the room's walk areas. `V` or `Enter` closes it, `Esc` drops it. *Rules*: the selected rule opens in place with its conditions and actions under it, because a rule only means anything whole. `N` adds a rule, `E` picks what it is on (the event's own parameters then appear as fields on the rule), `C` adds a condition, `O` a condition in the rule's `any` group, `T` an action -- each picked from the vocabulary with its help beside it -- and `[` and `]` move the rule up and down the sheet, since rules run in order. `Delete` removes the selected condition or action, or the rule itself. *Tracks*: the room's tracks and their keys. The canvas shows the frame of the disc layer's video under the cursor and every track's box there; `,` and `.` step the cursor a frame, `[` and `]` leap ten. `N` adds a track, `K` a key at the cursor -- taking the box interpolated there, or the nearest key's -- and a key is moved by dragging its box on the canvas or by typing. `Delete` removes the selected key, or the track. In a 3D room a new track is a rail of points, drawn through the scene with its stops marked, and `K` puts a point where the editor camera stands, looking where it looks: stand where the player should and press it. *Dialogues*: an outline of every dialogue, the selected one's nodes under it, the selected node's choices under that, and a choice's actions under that. `A` adds at the level selected -- a dialogue, a node in it, a choice in the node -- `T` adds an action to the selected choice from the vocabulary, and `Delete` removes what is selected. A dialogue's `name` and `start`, a node's `name`, `who`, `text`, `seconds`, and `next`, and a choice's `text`, `when`, `next`, and `once` are typed like everything else. `P` plays from the room on screen: the built game begins in it, with the other rooms following in order. On the 2D canvas the selected entity carries the yellow turn handle alone: an arm the way its `rz` points, past its box, dragged round the entity to turn it, and a sheet entity shows its first frame turned the same way. In the kinds panel each kind's picture stands beside its name -- the sprite itself (one frame of a sheet), `Aa` for a text, or a swatch of the box's colour -- so a kind can be told from its neighbours without opening it. `testScripts/scene82.singe` drives both. === The 3D viewport A room in a game with a `scene3d` layer is shown as the scene itself, built from the kinds' looks exactly as the runtime builds them, under an editor camera that orbits whatever is selected: `J` and `L` orbit, `Y` and `H` tilt, `I` and `K` close in and back off. Nothing steps -- no behaviours, no physics -- so it is the description that is on screen, not a running game. An entity is picked by where it projects and dragged across the floor it stands on, and the selected one carries a gizmo: three arms along the world axes, red for X, green for Y, blue for Z, each ending in a handle that drags the entity along that axis alone -- the pointer's travel along the arm, as a fraction of the arm, is the distance moved -- and a fourth, yellow, the way the entity faces, whose handle dragged round the entity turns it about Y. `testScripts/scene63.singe` and `scene80` drive it. === Keys [cols="1,4"] |=== | `Tab` | entities, kinds, rules, tracks | Up, Down | move through the list, and through the parts of the open rule or track | Left, Right | slide the panel | `PgUp`, `PgDn` | the previous or next room | `Enter` | type a value for whatever is selected; again for its next value | `Esc` | put the value back; otherwise close the description | `A`, `D`, `Delete` | add, duplicate, delete an entity or a kind | `N` | a new rule, a new track, or (in the entities) a new room | `E` | pick what the rule is on | `C`, `O`, `T` | add a condition, an any-of condition, an action | `[`, `]` | move the rule up or down; in the tracks, leap the cursor | `,`, `.` | step the cursor | `V` | draw a walk area, or the selected hotspot's outline, corner by corner | `K` | a key at the cursor; in a 3D room, a rail point where the camera stands | `J`, `L`, `Y`, `H`, `I`, `K` | in a 3D room: orbit, tilt, and zoom the editor camera | `U`, `R` | undo and redo, forty steps deep; a drag or a typed form is one step, and a change by hand ends the redo history | `S`, `B`, `P` | save, build, play |=== From a script the same work is `forgeEntityAdd`, `forgeEntityDuplicate`, `forgeEntityDelete`, `forgeEntitySet`, `forgeKindAdd`, `forgeKindSet`, `forgeRuleNew`, `forgeRuleOn`, `forgeRuleAdd("when"|"any"|"act", name)`, `forgePartSet`, `forgePartDelete`, `forgeRuleMove`, `forgeTrackNew`, `forgeKeyAdd`, `forgeCursorTo`, `forgeUndo`, and `forgeRedo`. A description survives the round trip: loading one, saving it, and loading it again compiles to the same game, byte for byte. The editor depends on that and `testScripts/scene54.singe` asserts it. === Releasing a game `forgeExport(folder, name)` writes everything a finished game needs into a directory of its own: the compiled script, a `games.dat` so the menu lists it, the description it was built from so it can be opened again, and **a copy of the runtime**, taken out of Forge. `--pack` turns that directory into a `.game` like any other, and it runs on a machine that has never had Forge on it. Every built game finds its own directory to load that runtime from, using `debug.getinfo` rather than `DIR`: `DIR` is the directory of the script the engine was *launched* with, so a game reached by `dofile` -- a test, a launcher, a preview -- would otherwise look beside the caller. `forgeBuild(path)` compiles what is on screen (and puts the runtime beside it, so the result plays); what `authorCheck` has to say about the description -- a kind that is not declared, an expression that does not parse, a field a look, a behaviour, or an event does not take (a misspelling, nine times in ten) -- goes to the console and the panel.