singe/docs/Forge.adoc
2026-09-14 14:20:17 -05:00

635 lines
32 KiB
Text

= Forge
Scott Duensing <scott@kangaroopunch.com>
: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.