# Forge The plan for Forge, Singe's authoring tool. Forge is distributed on its own, beside the engine and never inside it, and is planned on its own for the same reason: `PLAN.md` is the engine's. Its documentation is `docs/Forge.adoc`; its code is `forge/`; its tests are `testScripts/scene52.singe` onward. Section 1 is the first design and what was built to it. Section 2 re-reads that against the whole engine and lays out every kind of game. Section 3 records the owner's review of section 2 and what changed: nothing here owes anything to what was built before, and the Sierra and LucasArts adventure is a first-class target in 2D and in 3D. ## 1. Design: authoring tools (written 2026-09-13) **The first slice is built (2026-09-13); see "What exists" at the end of this section.** The rest of what follows is the design it was built to. The owner's ask: "Singe-based game authoring tools to allow users with very little programming skill the ability to create games", and, when the first sketch came back too laserdisc-shaped: "laserdisc games are the 'old' style game. We also need to think of the future" -- QTE games, hybrids like MACH3, 2D and 3D platformers, shooters, adventure games, "and more". ### What the library says is actually hard Counted rather than guessed, the way section 51 counted: * `CrimePatrol/Script/hitbox-stripbar.singe` is **12,241 lines** of `civillian[2741] = {295, 111, 307, 135}`. The largest artifact in a laserdisc game is not code. It is data entry against a video. * `ActionMax/38AmbushAlley.singe` is **14 lines** -- a gameID, five lengths, a background frame, two thresholds, four sensor coordinates, and `dofile(MYDIR .. "/Emulator.singe")`. A complete, shipping game. Five ActionMax titles share the one engine. So the repository already holds both the disease and the cure, and the cure is the pattern section 51 started: the game kit lifted the *utility* boilerplate out of 149 copies. This lifts the *structural* boilerplate, and what is left is data. ### Do not model genre. Model layers. The manual says it three times over: "the disc, the overlay, the GUIs, the 3D scene and the particles". That is a composite of independent layers, and a genre is only which of them a game uses: ``` light gun disc + overlay QTE disc + GUI, rules on time windows MACH3 disc + overlay + particles 2D platformer scene with physicsSet2D, or overlay 3D shooter scene + GUI + particles adventure disc or scene, + GUI ``` The document model therefore mirrors the engine's layer model and the tool never learns what a genre is. MACH3 stops being a hybrid special case and becomes an ordinary two-layer document. This is the decision the whole design rests on: a per-genre tool is N tools. ### Three nouns cover all of it * **Entities** -- a thing on a layer with a position, a look (sprite, model, text, video) and state. * **Behaviours** -- reusable bundles attached to an entity, and where the engine gets handed to non-programmers for nothing. `playerNew`, `playerMove`, `playerJump`, `playerIsOnGround`, `playerSetStep`, `playerSetSlope`, `playerSetSwim` already exist and are tested, so a "Platformer" behaviour is a checkbox over a character controller that works. So is "Follow Path" over `navAgentMoveTo`, and "Physics Body" over `bodyNew`. * **Rules** -- conditions to actions. The event sheet. ### The vocabulary is registered, not compiled in Conditions, actions and behaviours are declared in a manifest -- name, parameters, the Lua that implements them -- which the editor reads. Adding QTE support is registering "time is within window" and "button pressed" plus a couple of actions. **A genre is a pack, not an editor release**, and third parties can write one. The same goes for layer types, asset types and views. The test to apply to every later decision: *when this changes, is it data or is it a release?* ### Blockly: considered, not first Two problems. Blockly is browser technology and Singe has no web view and should not grow one. A native block canvas is a great deal of UI work -- layout, snapping, hit-testing, scrolling -- and RmlUi is a document engine, excellent at lists (the menu scrolls one) and unproven as a canvas. The event sheet gets most of the benefit for a fraction of the cost because it is a table. The move that keeps the door open: **rules are stored as structure, never as UI layout**, so a block canvas can be added later as a second view of the same data. Borrow the two things blocks are genuinely better at -- nested condition groups with AND/OR, and a small safe expression syntax with a picker (`score + 100`, `player.x < 50`). Rule lists that allow only constants are what send people back to code. ### Compile to Lua; do not interpret Walk the rules at build time and emit readable Lua, keeping the source data beside it in the `.game` for round-tripping. Three reasons: no per-frame interpreter on a Pi or a handheld; the output is an ordinary Singe game, debuggable through the ZeroBrane integration in `zbstudio/`; and it keeps the ladder continuous -- rules and hand-written Lua are the same material. ### The pressure valve, from the first release **A "Lua action" and a "Lua behaviour".** One rule needs something the vocabulary cannot say? Drop to Lua for that rule and keep the project. Without it every gap is a feature request and the tool grows without bound. ### The editor is a Singe game * WYSIWYG is literal: same renderer, same video decoder, same fonts, same overlay coordinates, same light-gun path. Not a preview -- the runtime. * No second toolchain to build, sign, ship or port. It runs wherever Singe does, including on the cabinet. * Play with no export step. * Precedent: `Tools.singe` is 40 KB of in-engine UI and `MenuDocument.singe` a full RmlUi document app. ### What the spike settled (2026-09-13) Chrome in RmlUi, canvas in the overlay. **Not RmlUi as the canvas.** Proven live, one frame showing all of it: a click on a chrome row consumed by RmlUi with its Lua handler firing; a click on the canvas falling through to `onInputPressed`, hit-testing the right rectangle and grabbing it; a click on empty canvas falling through with nothing under it; motion arriving throughout. Guaranteed by the dispatch rather than merely permitted: * `singe.c:15758` calls `_guiMouseMove()` then `_fireMouseMoved()` **unconditionally**. Motion is never consumed, so the canvas always knows the pointer even while it is over the GUI. * `singe.c:15767` offers buttons to the GUI first; they reach the script only if nothing used them. * RmlUi has `pointer-events: none | auto`, inherited, if a document ever must not block the canvas. **Re-run under a real window manager (2026-09-13), and the earlier reading was wrong.** `metacity` is installed, so the spike was repeated under it on an Xvfb display sized to the overlay, giving an exact 1:1 mapping -- confirmed by the engine reporting a press at `290,250` for an injected `290,250`. Pointer motion then arrived properly (17 events) and canvas picking worked perfectly. But **nothing reached the RmlUi document at all** -- not `dragstart`, and not `mouseover`, `mousedown` or a row click either, all of which were added to bisect it. So "RmlUi drag does not fire" was never the finding; in that configuration the document was not receiving pointer input in the first place, and the one earlier run where a row click *did* land had a mistaken coordinate scale. `guiMouseButton` and `guiMouseMove` feed RmlUi correctly (`gui.cpp:790`, `818`); the routing above them, `_guiPointer` (`singe.c:3358`), decides which GUI is under the pointer from the rectangles drawn last frame, and that is where the trail stops. **Why a full-screen document drawn with `guiDraw` and `guiSetInput(true)` gets no pointer under `-w` remains open.** Chased further on 2026-09-13 against the *bundled menu*, on the theory that if it were real then the menu's documented "click a row to select that game" would be broken in shipped behaviour. That is **not settled**: no run yet has both delivered pointer motion and clicked after `menuActive()` became true. One run delivered motion (`pointer 100,120`) but clicked during the intro, when `menuRowClick` correctly ignores it; every later run, with and without the pointer grab released, reported `pointer 0,0` throughout even though ManyMouse reported `Mice Found: 1` through XInput2. So the difficulty is in delivering synthetic pointer input to this engine under Xvfb, and **no claim should be made either way about the menu's mouse** until someone clicks a row on real hardware. That is a ten-second check for a person and has defeated a long series of attempts here. Because of it the event sheet was built **keyboard-first**, which is what the menu does and what a cabinet needs, and so does not depend on the answer. **What that changed in the editor.** The panel's drag tab was first an element in the document with a `mousedown` handler. On this evidence that is a path the editor cannot rely on, so the tab is drawn on the *canvas* just outside the panel and picked with `collidePointRect`, and `scene54` presses it through `authorEditPress` -- the same door a real pointer goes through. Reaching for the GUI would have looked fine in a test that called the handler directly. **Three harness facts, each of which cost a run:** a spike must quit on `singeGetTicks`, not a frame count, because with no vsync under Xvfb the frame rate is unbounded and it finishes before the driver's first move; the window is not the overlay (`-C 720x480` gave a 960x640 window, and `shoot.sh` forces `-w`, which changes input behaviour, so the binary had to be run directly); and `controls.cfg:56` maps the left mouse button to `INPUT_ACTION_3`, not `ACTION_1`. ### First slice: two unrelated genres, and not a light gun game Build the vertical slice with **a 2D platformer and a QTE video scene**. One genre teaches nothing about generality -- its assumptions end up in the core and are only discovered on the second. Two unrelated ones force the layer model, the behaviours and the rule vocabulary to be real. A light gun game afterwards is nearly free, and the whole thing does not end up shaped by the past. The keyframe point, worth more than most of the rest: nobody types 2,750 rectangles. Draw a box on frame 100, another on frame 130, interpolate. `tweenValue` already exists. ### What exists (2026-09-13) The first two slices: **the format, the vocabulary, the compiler, the runtime, and a first editor.** *(Paths below are as first built; everything now lives in `forge/` and the editor is `Forge.singe`. See the 2026-09-13 evening note at the end.)* * **`assets/Author.singe`** -- the runtime a compiled game calls, and the manifest itself. Layers `world2d`, `overlay`, `disc`; looks `box`, `sprite`, `text`; behaviours `solid`, `platformer`, `drift`; nine conditions and nine actions, each declaring its parameters and the Lua it emits. * **`assets/AuthorCompile.singe`** -- `authorCompile(game)` returns source, `authorBuild(description, output)` writes it. Written in Lua rather than as a `util/` script because the editor will be a Singe game and has to compile what it is editing without leaving the engine. * **`testScripts/author/platformer.forge`** and **`qte.forge`** -- the two unrelated genres, sharing nothing but the three nouns. * **`testScripts/scene52.singe`** and **`scene53.singe`** -- in the regression set. 52 compiles both descriptions, checks both parse, and plays the platformer; 53 plays the QTE over the disc. Both verified in both directions: the platformer reaches the prize (`score=100`) and the QTE scores 250 when answered and 0 with a `MISS` when it is not. * **`assets/AuthorEdit.singe`** and **`AuthorEdit.rml`** -- the editor, built to the spike: entity list and details in RmlUi, canvas in the overlay, picked with `collidePointRect`. `authorEditSave()` writes the description back, `authorEditBuild()` compiles what is on screen. * **`authorLoad`/`authorSave`** in the compiler, and the invariant the editor rests on: **a description survives the round trip** -- load, save, load, and it compiles to the same game byte for byte. Getting there found the bug worth knowing about the format: a condition is a *mixed* table (`{ "keyHeld", key = "LEFT" }`, an array part plus named keys) and a serialiser that writes only the array part silently drops every parameter. * **The event sheet**, in the same file: `Tab` swaps the panel between the entities and the rules, the selected rule opens in place with its conditions and actions under it, and `authorEditRuleNew`, `authorEditRuleAdd`, `authorEditPartSet` and `authorEditPartDelete` edit them. Adding a part selects it, and a new part gets a starting value per parameter type, so a rule compiles the moment it is made. The vocabulary comes from `AUTHOR`, so the rule editor needs no change when a condition or action is added. **Driven by keys** (`authorEditKey`) rather than the pointer -- see the window-manager finding above. * **`testScripts/scene54.singe`** -- opens the editor on a copy of a real description, picks an entity, drags it, saves, reloads from disc, and checks the move reached the compiled game. * **`testScripts/scene55.singe`** -- builds a rule through the editor (`keyHeld UP` and `onGround hero`, then `jump hero`), checks the compiled game carries all three, deletes a condition and checks it is gone again. * All are embedded and extracted like the other support files, and the manual has a chapter, "Describing a Game Instead of Writing One". **What running it in three configurations taught, which is worth keeping.** `scene52` first drove its keys by frame count: 100 under `--deterministic`, 0 under the sanitizer build. Driving on `authorTime()` instead was also wrong, and failed again as soon as the sanitizer build had a disc to decode -- `physicsUpdate` accumulates wall clock but takes at most `MAX_STEPS_PER_FRAME` (4, so 66 ms) per frame, so on a machine that cannot keep up the simulation deliberately falls behind the clock and a clock-driven script outruns it. **A test of an authored game has to be driven by what the game is doing**: hold right, jump when `playerIsOnGround` and past a mark, stop when the prize is taken. That version gives `score=100 after=2.5s` identically under `--deterministic`, free-running with a disc, and the sanitizer build with a disc. Any test of an authored game has to be written on the clock. **Known limits**, so they are not discovered as surprises: a rule is a flat AND of its conditions, with "either" expressed as two rules; there are no expressions beyond what the `lua` action carries; `once` is keyed by a tag the author picks and never resets; the 2D looks draw with `overlayLine` per row, which is fine for placeholders and is not a sprite pipeline; the editor had no undo and no way to add or delete an entity (both added that evening, see below); a rule's conditions are a flat AND, so "either" is still two rules; and parameter values are set from script rather than typed into the panel, which is the next thing the rule editor needs. The panel being under part of the world was the earlier limit and is fixed: it slides, by a tab drawn on the canvas, so nothing is permanently out of reach. The event sheet is the next slice and the one that matters most, because rules are where a non-programmer actually spends their time. ### Forge as a game (2026-09-13, evening) Until this pass `Forge.singe` was a library only: it defined the editor and nothing ran it, so the menu entry opened on a blank screen. Now: * **A chooser** when nothing is open: the descriptions in Forge's data directory, any in `Forge/` itself (opened as a copy -- packed, they are read only), and *New game*, which writes `forgeTemplate()` and opens it. * **Engine callbacks** -- keys to `forgeKey`, the left mouse button and motion to `forgePress`/`forgeDrag`/`forgeRelease`, the pad on the chooser. * **Play** (`P`): save, build beside a copy of the runtime, `scriptPush`. The engine reruns Forge from the top when the game ends, so the open file is noted in `last.txt` and reopened on the way back. * **Entities**: add, duplicate, delete, and every field typed through the same ENTER form a rule's parameters use -- `forgeFields()` says what the fields of the selection are and how to reach them, and the entity's come from the manifest (the look's parameters, then each behaviour's as `type.param`). Renaming an entity renames it in every rule. Sprite looks draw their image. * **Pickers** for conditions and actions (`C`, `T`), listing the manifest with its help line; rules reorder with `[` and `]`; the rule's note is editable. * **Undo** (`U`): a copy of the description before each change, forty deep. * `scene57` runs Forge as itself -- no `FORGE_LIBRARY` -- and drives the real callbacks through the chooser, the entity form, the pickers, undo, close and reopen, and build. * Fixed on the way: the compiler loaded `Singe/Author.singe`, which never shipped (it is `Forge/Author.singe`, and the compiler now loads `AUTHOR_RUNTIME`); `keyHeld` compared a key name against the scancode the engine delivers and never fired from a real keyboard; `authorTouching` and the `solid`/`platformer` behaviours read `look.w` on sprite and text looks that have none (`authorSize` now answers for every look); an unknown look type ended the game rather than drawing a box. Still open, so it is not rediscovered: no expressions beyond the `lua` action; a rule is a flat AND; `once` never resets; no light gun layer or keyframed hitboxes yet; the pointer path through the RmlUi document remains unproven on a real desktop (the canvas path is what the editor relies on); and the chooser is drawn with `fontPrint`, not the document, because the GUI is only created once a description is open. ### Out of scope, stated so it is not assumed A web view. A node-graph editor as the first UI. A separate desktop application. Importing existing hand-written Singe games. Any tool that cannot be dropped out of into Lua. And an editor grows without limit unless this list does too: add to it before adding features. ## 2. Design: Forge for every kind of game (written 2026-09-13) Section 1 was written for the first slice and built to it: a 2D platformer and a QTE over video, and then an editor that could move a box and type a rule. It was right about the shape of the thing and silent about most of the engine. The owner's ask now is broader and specific: "we especially want to be able to support 3D gun games like House of the Dead. Plan for every game type you think you can create." This section re-reads 58 against the engine as it actually is, says what is wrong or missing, extends the model until the whole catalogue below fits it, and orders the work. ### What section 1 got right, kept * **Layers, not genres.** Still the decision everything rests on. The catalogue below has thirty-odd types of game and every one of them is a choice of layers plus vocabulary; none needs a mode switch in the core. * **Three nouns** -- entities, behaviours, rules -- and a manifest the compiler and the editor read rather than know. * **Compile to Lua.** A rail shooter on a Pi cannot afford an interpreter in its frame either. * **The editor is a Singe game**, and the `lua` action is the way out. * **Keyboard first**, since the document pointer path is still unproven. ### What section 1 got wrong or left out, counted The engine registers **502** Lua calls. The manifest wraps **31** of them: three layers, three looks, three behaviours, nine conditions, nine actions, all of them 2D. By subsystem, what has no place in a description today: | Subsystem | Calls | In the manifest | |---|---|---| | Nodes, meshes, materials, lights, models, animation, camera, views | 31+9+20+6+4+10+3+3 | none | | Physics beyond a static box and the character controller: bodies, joints, vehicles, ragdolls, soft bodies, raycast, triggers | 17+5+18+9+8+5 | none | | Navigation (mesh, agents, crowd, raycast) | 18 | none | | Particles (2D and 3D emitters, trails, bursts) | 31 | none | | Sound, music, MIDI | 17+8+17 | none | | GUI documents, subtitles | 16+5 | none | | Video players beyond the disc, video on a material | 22 | none | | Saves, timers, tweens, the score bezel, the master service (boards, scores) | 7+4+3+7+8 | none | | Disc | 27 | play, seek, frame window | | Mice, guns, controllers, MIDI in | 7+8+6 | held keys and switches | And the structural gaps, which matter more than the counts: 1. **Positions are `x, y`.** There is no `z`, so there is no 3D at all. 2. **Every entity is a singleton declared up front.** A shoot-em-up has two hundred bullets alive; House of the Dead spawns zombies by the dozen and throws them away. There is no notion of a *type* with *instances*, and no `spawn`. 3. **Rules only poll.** The engine delivers events -- `onCollision`, `onTrigger`, `onNavArrived`, `onSoundCompleted`, `onInputPressed`, `onMouseMoved` -- and the manifest cannot express any of them, so `touching` is a rectangle test every frame and a key press has no edge. 4. **No state.** An entity has `drive`, `text`, and `visible`. No health, no ammo, no per-instance variables, and no state machine -- and a state machine (idle, walk, attack, hit, dead) *is* an enemy. 5. **No time structure.** No timeline, no waves, no camera path, no timers (`timerAfter` exists and is unused), no levels, no game over. 6. **No expressions and a flat AND.** `once` is keyed by a tag the author picks and never resets, which breaks the moment something respawns. 7. **No sound, no music, no HUD** beyond a text look, no lives or credits, no high scores -- the kit and the service (sections 51, 54, 55) are invisible. 8. **The keyframed hitbox** -- the thing section 1 called "worth more than most of the rest", the answer to the 12,241-line file -- was never built. 9. **Input is two conditions.** No pointer position, no "pressed this frame", no second gun. A gun game is nothing without the pointer. 10. **The editor** has a 2D canvas, no timeline, no asset list, no 3D view. None of it argues against the design. All of it says the vocabulary and the format have to grow along five axes -- depth, instances, events, state, time -- and the editor along two -- a timeline and a 3D viewport. ### The model, extended Every extension below is backward compatible: a section 1 description is a valid section 2 one. Nothing is added that a 2D platformer has to write. **Positions.** `x, y, z`. Absent `z` is zero; 2D layers ignore it. Rotation `rx, ry, rz` and `scale` likewise, all optional. **Layers.** Keep `world2d`, `overlay`, `disc`. Add: * `scene3d` -- `sceneEnable`, gravity, sky, ambient, fog, exposure, shadows. Parameters are the `sceneSet*` calls, nothing more. * `hud` -- an RmlUi document (`guiNew`/`guiLoad`) bound to variables: every `{{score}}`, `{{lives}}`, `{{self.health}}` in the document is refreshed when the value changes. The score bezel is the same layer with `bezel = true`. * `music` -- a track and a volume; `musicPlay` on begin. **Types and instances.** `types = { zombie = { look, behaviours, vars } }`. An entity in `entities` may name `kind = "zombie"` and override fields; a `spawn` action or a `spawner` behaviour makes more at run time. The runtime keeps instances in `AUTHOR_ORDER` as now, with an id per instance (`zombie#7`) and `authorEach("zombie")` to walk them. A rule may be scoped: `each = "zombie"` runs its conditions and actions once per live instance with `self` bound, which is how "every zombie that reaches the camera bites" is one rule and not one per zombie. **Per-instance state.** `vars = { health = 3, ammo = 6 }` on a type or an entity; `self.health` in expressions; `setVar`/`addVar` actions. `state` is a reserved var: `setState` changes it, `animator` and `sound` behaviours listen to it, `inState` tests it. **Looks.** Keep `box`, `sprite`, `text`. Add `sprite` frames and speed; `model` (`modelLoad`/`modelInstance`, file, scale, starting clip); `billboard` (a sprite in the scene, `nodeSetSprite` + `nodeSetBillboard`); `mesh` (box/sphere/plane/cylinder with a material: colour, texture, metallic, roughness, unlit); `light` (point/spot/sun with colour, range, cone, shadow); `particles` (an emitter preset); `text3d` (`nodeSetText`); `video` (a mesh with `materialSetVideo` -- the disc or a file on a surface); `gui` (a document on a material, `materialSetGui`). A look declares its own `size(entity)` so `touching` and the bodies stop assuming `w, h` (the `authorSize` fix of 2026-09-13 is the first step). **Behaviours.** The bundles, each a page of Lua over calls that exist: | Behaviour | Over | For | |---|---|---| | `solid` | `bodyNew` static | walls, floors (exists) | | `platformer` | player controller, 2D | (exists) | | `character` | player controller, 3D: move relative to the camera, jump, swim, step, slope | third-person and first-person games | | `body` | `bodyNew` dynamic: mass, bounce, friction, buoyancy; `bodyApplyImpulse` on demand | crates, balls, debris, projectiles | | `trigger` | `bodySetTrigger` | checkpoints, doors, kill volumes, weak spots | | `drift` | (exists) | clouds, platforms | | `path` | waypoints, speed, loop or once, `tweenValue`; raises `arrived` at each point | moving platforms, patrols, the camera rail | | `agent` | `navAgentNew`/`navAgentMoveTo`, target entity or point, speed | enemies that walk to the player, crowds | | `follow` | `nodeLookAt`/lerp toward a target at a distance | companions, homing shots | | `seek` | straight-line movement toward a target (no nav mesh) | zombies closing on the camera, missiles | | `vehicle` | `vehicleNew` + wheels, engine, gears, steering | cars, bikes, tanks, boats | | `thrust` | `bodyApplyForce` along facing, turn rate | ships, planes, hovercraft | | `health` | `vars.health`, `damage` action, `death` event, optional `ragdoll` on death, `invulnerable` seconds after a hit | anything that can die | | `shooter` | spawns a `projectile` type from a muzzle offset at a rate, on a switch or a rule | players and turrets | | `projectile` | speed, life, damage on hit, destroyed on contact | bullets, arrows, fireballs | | `gun` | the pointer: `mouseGetPosition` (and device index for two guns), 2D hit = `collidePointRect` against types, 3D hit = `sceneUnproject` twice + `physicsRaycast`; raises `hit`/`miss` events with the target; ammo and reload (a button, or a shot off-screen) | light gun games in 2D, over video and in 3D | | `hitboxes` | keyframed rectangles by disc frame or by time, interpolated | guns over video -- the 12,241-line file becomes a track in the editor | | `animator` | state to clip: `{ idle = "Idle", walk = "Walk", attack = "Bite", dead = "Die" }`, fades, `animationIsPlaying` polled for `animationDone` | every model with clips | | `ragdoll` | `ragdollNew`, activated on death, sleeps, and is removed after a time | shooter deaths | | `spawner` | type, where (a point, along a path, in a trigger, at a waypoint), every N seconds or on demand, max alive, waves | enemies, pickups, traffic | | `timer` | `timerAfter`/`timerEvery`, raises `timer` events by name | anything timed | | `sound` | clips by event: `{ hit = "grunt.wav", dead = "fall.wav", spawn = "moan.wav" }` | everything | | `emit` | an emitter parented to the entity, bursts by event | blood, muzzle flash, sparks, exhaust | | `camera` | `fixed`, `follow` (offset, lag), `orbit` (mouse look), `first` (on a character's eyes), `rail` (a `path` with look-at targets and stops) | every 3D game | | `collectible` | destroyed on touch by a named type, adds score or a var | coins, ammo, health packs | | `lives` | player deaths, continues, credits from the score bezel, game over | arcade games | | `keys` | maps keys/switches/pad axes to vars (`self.drive`, `self.aim`) so rules read intent rather than hardware | everything with a player | **Events.** A rule may say `on = "..."` instead of, or as well as, being polled every frame: `frame` (the default), `pressed`/`released` (key or switch, an edge), `collision` (two types, from `onCollision`), `enter`/`leave` (a trigger, from `onTrigger`), `hit`/`miss` (from `gun`), `death`, `spawn`, `arrived` (a path point or `onNavArrived`), `animationDone`, `soundDone`, `timer` (by name), `waveCleared`, `frameReached` (a disc frame), `levelStart`, `levelEnd`. The compiler emits one dispatcher per engine callback that routes to the rules by type and id; a description with no event rules compiles to what it does today. **Conditions.** Keep the nine. Add `pressed` (edge), `pointerIn(entity)`, `pointerOffscreen`, `inState`, `var` compare, `distance` less or more, `inView`, `count(type)` compare, `random(chance)`, `every(seconds)`, `animationDone`, `atWaypoint`, `waveCleared`, `onScreen`, `lineOfSight` (`navRaycast`/`physicsRaycast`). `once` gains `per = "instance"` and resets when the instance is destroyed or the level restarts. **Actions.** Keep the nine. Add `spawn`, `destroy`, `setVar`, `addVar`, `setState`, `damage`, `heal`, `playAnimation`, `playSound`, `playMusic`, `emit` (burst), `shake` (camera), `flash` (overlay), `pathGo`/`pathStop`/ `pathNext`, `cameraCut`, `lookAt`, `push` (impulse), `say` (subtitle or HUD line for a time), `timerStart`, `gameOver`, `restart`, `nextLevel`, `loadLevel`, `submitScore` (master service, queued offline), `saveSet`, `discBranch` (the branching FMV table), `credit`. **Expressions.** A small, safe syntax where a number goes today: numbers, strings, `+ - * / %`, comparisons, `and or not`, `self.var`, `score`, `lives`, `time`, `frame`, `entity("hero").x`, `count("zombie")`, `random(a, b)`, `distance(a, b)`. Parsed by a hundred-line recursive descent in AuthorCompile.singe into plain Lua against a whitelist -- no other calls, no indexing of anything but the names above. The picker offers the names. The `lua` action stays the door for everything else. This is the item that decides whether the tool sends people back to code; it goes in the first milestone, not the last. **Groups.** `when = { { ... }, any = { { ... }, { ... } } }`: a rule's conditions are an AND, an `any` inside them is an OR, and they nest. The editor shows an `any` as an indented block. **Tracks.** One structure for everything keyed by time or by disc frame: ``` tracks = { { name = "camera", key = "time", points = { { at = 0, x, y, z, look = "door" }, { at = 8, ... , stop = "wave1" } } }, { name = "bandit1", key = "frame", boxes = { { at = 1200, x, y, w, h }, { at = 1230, x, y, w, h } } }, { name = "waves", key = "time", spawns = { { at = 3, kind = "zombie", count = 3, from = "left" } } }, { name = "lines", key = "frame", say = { { at = 900, text = "Get down!" } } } } ``` The camera rail, the light-gun hitboxes, a wave schedule, a cut-scene, and a subtitle sheet are all tracks; the runtime interpolates points and boxes with `tweenValue`, and the editor's timeline edits all of them the same way. A `stop` on a camera point holds the rail until the named wave is cleared (or a timeout), which is exactly what House of the Dead does at every doorway. **Levels.** A description may hold `levels = { { title, entities, tracks }, ... }` with `types` and `rules` shared; `nextLevel` tears the world down and builds the next. A single-level description is `levels` of one, implicitly. Between levels the `hud` layer shows a results card from a template. **Players.** `players = 2` on the description; `keys` and `gun` behaviours take a `player` index (mouse device index in `MOUSE_MANY`, controller slot), score and lives are per player, the bezel's twin score is switched on. ### The catalogue Every kind of game the engine can carry, with the layers it uses, what it needs of the vocabulary above, and what is still missing (**E** = engine work, **V** = vocabulary only, **ED** = editor). A reference title says what is meant. Where a type is a combination of others it says so; that is the point of layers. **A. Over video (the laserdisc lineage)** 1. **Branching FMV** (Dragon's Lair, Space Ace). `disc` + `overlay` or `hud`. A `frameReached`/`discBetween` window per decision, `pressed` for the move, `discBranch` to the success or death clip, lives, a move table. V: the branch table as a track. Missing nothing in the engine; section 47 gave it every disc control. 2. **Light gun over video** (Mad Dog McCree, Crime Patrol, the ActionMax titles). `disc` + `overlay`. `gun` + `hitboxes` tracks, `hit` events, `frameReached` for reload prompts, scoring, civilians as boxes with a penalty. V + ED: the timeline with frame scrubbing and box drawing is the whole job; `tweenValue` interpolates between keyframes. 3. **QTE / rhythm over video** (Road Blaster, Sega's Time Gal). Built. Adds `pressed` edges and a `say` track for prompts. 4. **Video plus sprites** (MACH3, Firefox, Cobra Command). `disc` + `overlay` + 2D emitters. Sprite types spawned from a track by frame, `shooter`, `projectile`, `collision`. V. 5. **Sensor games** (ActionMax). A special case of 2: four hitboxes and a light sensor; `gun` with `pointerIn` and a frame window. V. 6. **Interactive movie / adventure** (Night Trap, Plumbers Don't Wear Ties). `disc` + `hud`. Choices as GUI buttons bound to `discBranch`, inventory as vars, `saveSet` for progress. V (the `hud` layer's bindings). 7. **3D over video** -- House of the Dead's enemies over a filmed background. `disc` shown on a `video` look filling the view, or the disc under the scene with a transparent clear, plus everything in 20. E: confirm the scene composites over the disc with alpha (the overlay does; the scene's clear colour needs an alpha of zero). **B. 2D** 8. **Platformer** (built). Gains `health`, `collectible`, `spawner`, `animator` on sprite frames, levels. 9. **Shoot-em-up / twin-stick** (Xenon, Robotron). `world2d` without gravity or `overlay` only, `keys` giving `self.aim`, `shooter`/`projectile` types, `spawner` waves from a track, `collision` events, `health`, `emit`. V. This is the second genre of milestone 1 because it forces types, spawn and events. 10. **Run-and-gun** (Contra): 8 + 9. 11. **Arcade physics** (Breakout, Pong, Pinball). `world2d` with `body` (bounce 1.0), `solid`, `collision` events, `spawner` for bricks from a grid. V. 12. **Maze / dot-eater** (Pac-Man). `world2d` on a grid: `agent` over a nav mesh built from the maze (`navBuild` on a plane with holes, or the 2D grid in `bump.lua` which is already embedded), `collectible`, `inState` for frightened ghosts. V, plus a `grid` look that lays a tile map (E: small -- a tile look drawing a sprite sheet per cell). 13. **Puzzle** (Tetris, match-three, sliding). `overlay` + a grid of types, `keys`, `timer`, expressions over a 2D array var. V, but the array var and a `grid` helper are needed; the first puzzle will find the gaps. 14. **Point-and-click adventure** (Monkey Island). `overlay` sprites, a `hud` verb bar, `pointerIn`, `pressed`, `say`, inventory vars, `agent` walking on a 2D nav mesh, `saveSet`. V. 15. **Visual novel / interactive fiction**. `hud` only: a document with a text box and choices, vars for flags, `say`, `playMusic`, `saveSet`. V. 16. **Tower defence** (2D). `world2d`, `agent` creeps along a path (a `path` behaviour suffices), turrets as `shooter` types placed by `pressed` on a grid, `health`, waves from a track, a `hud`. V. 17. **Top-down driving** (Micro Machines). `world2d`, `body` + `thrust` + a turn rate, `trigger` checkpoints and a lap counter, an `agent` for AI cars. V. (Jolt vehicles are 3D only: "a wheeled type in a 2D world" raises an error.) 18. **Fighting / beat-em-up** (Streets of Rage). `world2d`, `animator` on sprite frames, `state` machines, and hit/hurt boxes keyed to *animation frames* -- the same `hitboxes` track keyed by frame of a clip rather than of the disc. V: the track's key becomes `frame`, `time`, or `clip`. 19. **Rhythm** (Guitar Hero, Rhythm Heaven). `hud` + `music` or MIDI (`midiPlay`, `onMidiMessage`), a track of notes by time, `pressed` within a window, `every(seconds)`. V; MIDI input as switches is E (small). **C. 3D** 20. **Rail shooter** (House of the Dead, Time Crisis, Virtua Cop). Worked through in the next subsection. The reference for milestone 2. 21. **First-person shooter / arena** (Doom-like). `scene3d`, `character` + `camera first` + mouse look (`keys` from `onMouseMoved` relative), `gun` with a raycast from the view centre, `agent` enemies over a nav mesh, `health`, `spawner`, `collectible`, `hud`. V. 22. **Third-person action / adventure** (Tomb Raider, Zelda). `character` + `camera follow`/`orbit`, `agent` enemies, `trigger` doors and switches, `collectible`, `say`, `saveSet`, levels. V. 23. **3D platformer** (Mario 64). 22 with jumping first; the second genre of milestone 2 because it shares nothing with a rail shooter but the scene. 24. **Racing / driving** (Outrun, Mario Kart). `vehicle` cars on a heightmap or model track, `trigger` checkpoints, lap and position vars, `agent` opponents (a `path` with a vehicle following it), `camera follow`, a `hud` speedometer from `vehicleGetSpeed`. V. 25. **Flying / space / boats** (Star Fox, After Burner, Wave Race). `body` + `thrust` with pitch and roll from `keys`, `camera follow`, `shooter`, `spawner`; boats over `bodySetWater`. V. 26. **Sports with a ball** (bowling, golf, billiards, pinball in 3D). `body` with bounce and friction, `push` from a charged `pressed`, `trigger` pockets and gutters, `collision` events, `timer` for turns. V. 27. **Physics puzzle / toy** (Angry Birds in 3D, bridge builders). `body`, `joint` (a `joint` behaviour: hinge, ball, slider between two entities), `soft` bodies (`softNew`), `push`, `collision` scoring. V: the joint behaviour is the only new piece. 28. **Tower defence / RTS-lite in 3D**. 16 over a nav mesh with `agent` crowds and `navRandomPoint`; selection by `sceneUnproject` + raycast. V. 29. **Walking sim / horror** (Amnesia). `character` + `camera first`, `trigger` scares, `sound` and `emit` on events, `say` notes, `light` looks that flicker (`addVar` on intensity), `saveSet`. V. 30. **Rhythm in 3D** (Beat Saber-like without VR): 25's types coming down a `path` on a note track, `gun` or `pointerIn` to hit them. V. **D. Not games, built the same way** 31. **Attract modes, menus, kiosks**: `hud` + `music` + tracks; the bundled menu is a hand-written one. 32. **Cut-scenes**: a `camera rail` track, `say` lines, `playAnimation` actions by time, between levels. Nothing in the catalogue needs the core to know its genre, which is the test section 1 set. What it needs is the five axes above, built once. ### The rail shooter, worked House of the Dead is: a camera on a rail that stops at encounters; enemies that spawn at each stop, approach, and attack on a timer unless killed; a gun that hits what the pointer is over; civilians who must not be hit; ammo and a reload gesture; lives, continues, credits; bosses with weak points; branch points; a results screen. In the model above: ``` layers = { { kind = "scene3d", sky = "dusk.hdr", fog = { 40, 90 } }, { kind = "hud", document = "hud.rml" } } players = 2 types = { zombie = { look = { kind = "model", file = "zombie.glb", scale = 1 }, vars = { health = 3, damage = 1 }, behaviours = { { kind = "health", ragdoll = true }, { kind = "seek", target = "camera", speed = 1.2, stopAt = 1.5 }, { kind = "animator", idle = "Idle", walk = "Walk", attack = "Bite", hit = "Flinch", dead = "Die" }, { kind = "timer", name = "bite", after = 1.4 }, { kind = "sound", spawn = "moan.wav", hit = "grunt.wav", dead = "fall.wav" } } }, civilian = { look = { kind = "model", file = "hostage.glb" }, vars = { health = 1 }, behaviours = { { kind = "health" }, { kind = "path", speed = 2 } } }, weakspot = { look = { kind = "box3d", w = 0.3, h = 0.3, d = 0.3, visible = false }, behaviours = { { kind = "trigger" } } } } entities = { { id = "camera", behaviours = { { kind = "camera", mode = "rail", track = "rail" } } }, { id = "gun1", behaviours = { { kind = "gun", player = 1, ammo = 6, reload = "offscreen" } } }, { id = "gun2", behaviours = { { kind = "gun", player = 2, ammo = 6, reload = "offscreen" } } } } tracks = { { name = "rail", key = "time", points = { { at = 0, x = 0, y = 1.6, z = 0, look = { 0, 1.5, -10 } }, { at = 6, x = 0, y = 1.6, z = -8, look = "door", stop = "hall" }, { at = 14, x = 4, y = 1.6, z = -20, look = { 4, 1.5, -30 }, stop = "yard" } } }, { name = "waves", key = "stop", spawns = { { at = "hall", kind = "zombie", count = 3, from = { "doorL", "doorR", "stairs" }, every = 1.5 }, { at = "hall", kind = "civilian", count = 1, from = "corridor", path = "flee" }, { at = "yard", kind = "zombie", count = 6, from = "gate", every = 1.0 } } } } rules = { { note = "A shot lands", on = "hit", each = "zombie", act = { { "damage", entity = "self", amount = 1 }, { "emit", entity = "self", what = "blood" }, { "addScore", amount = 100 } } }, { note = "Headshot", on = "hit", each = "zombie", when = { { "hitPart", part = "Head" } }, act = { { "damage", entity = "self", amount = 3 }, { "addScore", amount = 400 } } }, { note = "Reached the camera", on = "arrived", each = "zombie", act = { { "setState", entity = "self", state = "attack" }, { "timerStart", entity = "self", name = "bite" } } }, { note = "The bite lands", on = "timer", each = "zombie", when = { { "inState", entity = "self", state = "attack" } }, act = { { "damage", entity = "player", amount = "self.damage" }, { "flash", color = "red" }, { "shake", amount = 0.3 } } }, { note = "Hit a civilian", on = "hit", each = "civilian", act = { { "damage", entity = "player", amount = 1 }, { "say", text = "Don't shoot!" } } }, { note = "Out of shots", when = { { "var", entity = "gun1", name = "ammo", is = "== 0" } }, act = { { "say", text = "RELOAD" } } }, { note = "The hall is clear", when = { { "count", kind = "zombie", is = "== 0" }, { "waveDone", name = "hall" } }, act = { { "pathNext", entity = "camera" } } }, { note = "Dead", on = "death", each = "player", act = { { "lives", change = -1 } } }, { note = "Level over", on = "levelEnd", act = { { "submitScore", board = "main" }, { "nextLevel" } } } } ``` How the runtime does each piece, over calls that exist: * **The rail.** `camera rail` is a `path` on the view's node: position and look-at interpolated between points with `tweenValue`, `nodeLookAt` each frame, a `stop` holding until the named wave is cleared (`count == 0` and the spawner has finished) or a timeout. Pure Lua. * **The gun.** `mouseGetPosition(device)` in `MOUSE_MANY` (one device per player; `--manymouse` and the Sinden border already exist), then `sceneUnproject(sx, sy, 0)` and `sceneUnproject(sx, sy, 100)` give the ray, `physicsRaycast` gives the node hit. The runtime maps the node to the instance (`AUTHOR_WORLD` by node) and raises `hit` with `part` = the node's name, so a ragdoll's `Head` bone or a `weakspot` child is a part. A shot outside the overlay is `miss` with `offscreen = true`, which `reload = "offscreen"` consumes. **E, to verify:** that `physicsRaycast` on a model with a ragdoll reports the bone's node rather than the instance root; if not, the runtime attaches invisible trigger boxes to named bones instead. * **Enemies.** `seek` moves the node toward the camera each frame and raises `arrived` at `stopAt`; `animator` switches clips on `state` (`animationPlay` with a fade); `health` takes `damage`, sets `hit` then the previous state after `animationDone`, and on zero sets `dead`, activates the ragdoll (`ragdollNew`/`ragdollActivate`) and destroys the instance once `ragdollIsResting` or after a time. `timer` is `timerAfter` by name. * **Civilians** are the same type machinery with a `path` and a penalty rule. * **Bosses** are a type with more health, `weakspot` children, an `animator` with attack clips chosen by rules, and a `hud` bar bound to `self.health` drawn with `sceneProject` above the node. * **Blood, flashes, shakes.** `emit` is a 3D emitter parented to the instance with `emitterBurst`; `flash` is a full-overlay box faded by a tween; `shake` offsets the view node for a moment. * **Players, lives, credits.** `lives` wraps the score bezel (`scoreBezelLives`, `scoreBezelCredits`, `SWITCH_COIN`), `players = 2` gives two guns and the twin score. `submitScore` queues to the master service through `Master.singe` and the game id in games.dat. * **Over video** (7): the same description with a `disc` layer under the scene; the rail's stops become `frameReached` stops on the disc instead of time. One key on the track changes and nothing else does. The test: a scene with a rail of two stops, three zombies, and a civilian, pointer injected through `authorPointer(x, y)` the way `scene52` injects keys, asserting kills, the penalty, the rail moving on when the hall is clear, ammo reaching zero and a reload, and a level end -- identical under `--deterministic` and free-running, driven by what the game does rather than by time (the lesson section 1 recorded). ### The editor, for time and depth Two additions carry most of the catalogue; neither changes the split of chrome in RmlUi and canvas on the overlay. * **The timeline.** A panel along the bottom: a ruler in seconds or disc frames (the disc scrubs with `discSkipToFrame` as the cursor moves, so a hitbox is drawn on the frame it belongs to), one row per track, keys as marks, the selected key's fields typed the same way everything else is. Draw a box on the canvas at the cursor and it is a key on the selected hitbox track; step frames and draw again; the interpolation is shown between. This is the light gun editor section 1 promised and the wave editor and the cut-scene editor at no extra cost. * **The 3D viewport.** When a description has a `scene3d` layer the canvas is the scene, drawn by the engine's own renderer with an editor camera (orbit with the pointer, fly with keys), and the chrome sits over it as now. Picking is `sceneUnproject` + `physicsRaycast` against editor-only trigger boxes on every entity; dragging moves along the ground plane through the click; height and rotation are typed or nudged with keys. No gizmos in the first version: typed values are exact and the canvas is for seeing. Play-from-here starts the game with the camera where the editor's is. * **Types and assets.** A third panel mode (`Tab` cycles entities, types, rules): types edited like entities, and the look's `file` field offers the models, sprites, and sounds found under the game's directory with `lfs.dir`. * **The picker grows a search**, since the vocabulary will be ten times the size: type to filter. The document pointer question from section 1 is still open and still does not block any of this: every new control is reachable by keys and by the canvas. ### Engine work implied Small, and all of it useful outside Forge: * `physicsRaycast` reporting the bone or child node it hit on a ragdoll or a compound (verify; add if not). * A `sceneSetBackground` alpha of zero drawing the scene over the disc and the under-overlay video, for 7 (verify). * A tile look for grids (12, 13): a sprite sheet drawn per cell from a table, in the overlay -- or `bump.lua`'s grid, already embedded, with sprites. * MIDI input as switches for 19 (`onMidiMessage` exists; a mapping in `controls.cfg` would do). * Nothing for the rail shooter beyond the raycast check. ### Milestones Each adds to the manifest, ships a scene, and -- section 1's rule -- proves itself on two genres that share nothing, so the core stays genre-free. 1. **Types, spawn, events, vars, states, expressions, groups, timers, sound, HUD.** Genres: a 2D shoot-em-up (9) and a light gun over video (2) with the hitbox track and the timeline in the editor. This is the milestone that decides whether the tool is usable at all: expressions and events are where a non-programmer either stays or leaves. 2. **`scene3d`, `model`/`mesh`/`light`/`billboard` looks, `animator`, `character`, `camera` modes, `gun` in 3D, `seek`, `health`, `ragdoll`, the rail.** Genres: the rail shooter (20) and a 3D platformer (23). The editor gains the viewport. 3. **`agent`, `vehicle`, `thrust`, `trigger`, `joint`, `body`, `push`.** Genres: racing (24) and tower defence (28). Nav mesh baking from the level's solids (`navBuild`) as an editor action. 4. **`hud` bindings, `say`, inventory vars, `discBranch`, `saveSet`, levels, results, `lives`, `submitScore`.** Genres: branching FMV (1) and a point-and-click adventure (14). Game over, continues, and high scores arrive here for everything before it. 5. **Editor: types panel, asset browser, picker search, play-from-here, undo across the timeline.** No new genre; every scene from 1 to 4 is rebuilt through the editor rather than from a hand-written description. 6. **Sprite frames and `hitboxes` by clip** (18), `grid` (12, 13), MIDI (19), boats and water (25), soft bodies (27). The long tail, each a page of manifest and a scene. The first two milestones are the ones the owner asked for; the rail shooter is milestone 2 and everything in milestone 1 is on its path (a zombie is a type, its bite is a timer, its death is an event, its health is a var). ### Corrections to section 1 as written * `assets/Author.singe`, `AuthorEdit.singe` and `Singe/AuthorCompile.singe` are `forge/Author.singe`, `Forge.singe` and `Forge/AuthorCompile.singe`; nothing of Forge is in `Singe/`. * "The keyframe point, worth more than most of the rest" was not built. It is milestone 1 here. * "The 2D looks draw with `overlayLine` per row" stays true for `box`; the `sprite` look draws its image in the editor now and needs frames. * `touching` assumed `look.w`; `authorSize` answers for every look now, and the model above moves size into the look. * "A rule is a flat AND" is fixed by `any` groups above; "`once` never resets" by `per = "instance"`. * The documentation for all of this is `docs/Forge.adoc`, not the manual. ### Out of scope, restated A web view. A node-graph editor as the first UI. A separate desktop application. Importing hand-written games. A modeller, a sprite painter or a sound editor -- assets come from Blender, Aseprite, and Audacity as glTF, PNG, and WAV. Networked multiplayer. VR. A scripting language of its own beyond the expressions above: past them is Lua, on purpose. And the list grows before the features do. ## 3. Revisions after review (written 2026-09-13) The owner read section 2 and changed two of its premises: **nothing has to be backward compatible** -- Forge is new and unreleased, so the format and the vocabulary are designed once and designed right, not grown around the first slice -- and **the Sierra and LucasArts adventure has to be a first-class target in both 2D and 3D**. He also asked whether Forge should have its own planning document; it does now, and this is it. ### Compatibility dropped: what section 2 hedged and this decides Section 2 kept every section 1 construction valid and added around it. Freed of that, these are the decisions taken instead; each is one thing where there were two. * **Everything is a type.** `types` is the only place a look, behaviours, and vars are declared; an `entities` entry names its type and gives what differs (position, id, a var). There is no anonymous entity with its own look. One path through the compiler and the editor rather than two, and a thing placed once is a thing that can be spawned later without rewriting it. * **Positions are always three numbers**, and every look declares its own size. 2D layers ignore `z` and the 2D editor never shows it. The runtime's `authorSize` and the `w, h` scattered through section 1 go. * **Rooms, not levels.** A game is `rooms`; a platformer's levels are rooms visited once, an adventure's rooms are visited over and over and remember themselves. Every room keeps its entity state when it is left (positions, vars, what was taken) unless the room says `reset = true`. Vars declared on the game (`score`, `flags`, inventory) live across rooms. A `hud`, `music`, and camera can be set per room and default from the game. * **Every rule has a trigger.** `on = "frame"` is written, not implied; the editor's picker for a new rule opens on the event list. Polling every frame is one choice among twenty, not the default a non-programmer falls into. * **Every action declares whether it waits.** `say` waits until the line is done, `walkTo` until the character arrives, `wait` for its seconds, `playAnimation` optionally until the clip ends, `fade` until the screen is black. A rule's actions run as a coroutine the runtime resumes each frame, so a cut-scene, a dialogue, and a door opening are the same rule shape as `addScore`. This is the piece adventures cannot exist without and the piece section 2 lacked: it had events and actions, and no notion that an action takes time. Rules on the same trigger for the same entity are serialised; a new `on = "pressed"` while a sequence is running is dropped unless the rule says `interrupt = true`. * **`once` is per instance and per room** by default and resets with the room; `once = "game"` is the old behaviour, opted into. * **Tracks and dialogues are assets of the room**, edited in their own panels, and referenced by name from behaviours and actions, rather than living inside the rule that uses them. * **`hitboxes` is the one keyframe mechanism** for disc guns, animation hit/hurt boxes, and hotspots that move with a video: a track of shapes keyed by frame, time, or clip frame. A shape is a rectangle, a circle, or a polygon (`collidePointPolygon` exists), so a hotspot in an adventure is a hitbox track with one key. * **Two players is the general case:** `players = N`, and `keys`, `gun`, score, and lives are indexed by player from the start rather than added. * The section 1 samples (`platformer.forge`, `qte.forge`) and scenes 52 to 57 are rewritten to the new format when milestone 1 lands; nothing loads the old one. ### Adventure games, worked Two lineages, one model. **Sierra** (King's Quest, Space Quest, Police Quest): rooms are painted screens with a walk area and exits on the edges, the player walks with the keys or a click, a text parser (AGI, SCI0) or an icon bar (SCI1) says what to do, death is frequent and the score counts, and the sentence "You can't do that here" is a rule. **LucasArts** (Maniac Mansion, Monkey Island, Day of the Tentacle): a verb bar and a sentence line, a click picks a hotspot, a dialogue tree with a choice list, no deaths, inventory as objects with verbs, cut-scenes that take the controls away, and puzzles that are state (`flags`) plus rules over it. **3D** (Grim Fandango, Escape from Monkey Island, Gabriel Knight 3): 3D characters on a nav mesh, either over a painted backdrop from a fixed camera per room or in a modelled room the camera follows; otherwise the same nouns. What the model gives them, and what it adds for them: * **Rooms** (above): a painted `sprite` or `video` backdrop look drawn first, or a `scene3d` room with a `camera` per room -- `fixed` (position, look-at, fov, matched to a painting), `follow`, or `orbit`. `exits` are `trigger` types at the edges or hotspots with a `goTo` action naming the room and the arrival point; in 2D the runtime scrolls nothing (Sierra rooms are a screen) unless the room is wider than the overlay, in which case a `camera follow` in 2D pans it. * **Walk areas.** A room declares `walk = { polygon... }` (several, with holes); the runtime triangulates them (ear clipping, sixty lines of Lua), builds a flat mesh with `meshNew`, and bakes a nav mesh with `navAddNode` and `navBuild`, so a 2D room's walking is the same `agent` behaviour a 3D room uses over its floor. Click-to-walk is `navAgentMoveTo` at the click (2D: the overlay point; 3D: `sceneUnproject` + `physicsRaycast` onto the floor). `walkTo` in a rule is the same call with `waits`. `onNavArrived` is the `arrived` event both lineages script against. * **Depth in a painted room.** `scaleBy = { { y = 300, scale = 0.5 }, { y = 460, scale = 1.0 } }` on the room scales sprites with their `y` (`spriteScale` exists), and draw order is by `y` so a character walks behind what is nearer the bottom of the picture. `behind` looks -- a cut-out of the painting drawn after the characters -- give a pillar to walk behind. In 3D over a painting the same needs **occluder** meshes that write depth and no colour: **E**, a material flag (`materialSetOccluder`), small, and nothing else in the 3D lineage is missing from the engine. * **Hotspots.** A `hotspot` type: a `hitboxes` shape (rectangle, circle, polygon) with no picture, a `name` for the sentence line ("look at *window*"), a `walkTo` point, and vars. `pointerIn` and `pressed` find it; `on = "verb"` rules (below) act on it. A hotspot may move with a video (a keyed track) or with an entity (parented). * **Verbs and the sentence line.** The game declares `verbs = { "look", "take", "use", "talk", "open", ... }` and the `hud` document shows them as buttons (LucasArts) or an icon bar (SCI1); the runtime builds the sentence from verb, hotspot, and, for `use`, a second hotspot or an inventory item, and raises `on = "verb", verb = "use", item = "key", target = "door"`. A rule matches on any subset (`verb` alone is "use anything on anything"); the most specific rule wins and the room's default rule ("That doesn't work") catches the rest. Right-click cycles verbs, the way the later LucasArts games do, as a `hud` option. * **The parser.** A `parser` layer for the Sierra lineage: `words = { look = { "look", "examine", "l" }, door = { "door", "gate" } }` and `on = "said", verb = "look", noun = "door"` rules; unknown words answer from a table; `keyboardSetMode` text entry with a prompt line in the `hud`. Both input styles raise the same `verb` event, so a room's rules do not care which the game uses, and a game can offer both. * **Inventory.** An `inventory` var on the player (a list of item types), `give`/`take` actions, the `hud` bound to it as a scrolling list, items usable as the `item` of a `verb` event, and `has(item)` in expressions. * **Dialogue.** A `dialogues` asset per room or game: nodes of `say` lines (who, text, optional clip, and portrait) and `choices`, each with `when` conditions, `act` actions, `goto`, and `once`; a `talk` action starts one and waits until it ends; the `hud` shows the choices; lines are `say` actions with a duration from their length (or a voice file's) and a `srt`-style subtitle placement over the speaker via `sceneProject` in 3D. A dialogue is a tree in the editor's panel, edited as an outline. * **Flags and puzzles.** Vars at game scope; `setVar`/`addVar`; `when` on them; `once`. A puzzle is state and rules, which the model already is. * **Cut-scenes.** A rule with `waits` actions and `controls = false` for its duration: `walkTo`, `say`, `playAnimation`, `fade`, `cameraCut`, `wait`, `goTo`. Tracks for anything timed to the frame. * **Death and scoring** (Sierra): a `die` action with a message and restore/restart/quit choices from the `hud`; `addScore` with `max` shown as "12 of 300"; `saveGame`/`loadGame` actions capture the whole state -- room, positions, vars, inventory, dialogue `once` flags -- through `saveSetAll`, and a `hud` restore list. * **3D specifics.** Characters are `model` types with `animator` (idle, walk, talk, pick up) and `agent` on the room's nav mesh (baked from the room's floor solids with `navBuild`); `camera fixed` per room with the painting on a `video` or `sprite` backdrop look filling the view, or `camera follow` in a modelled room; hotspots are `trigger` boxes or polygons on the floor; `lookAt` turns the head or the body toward a hotspot before a `say`; tank controls are `keys` mapping forward/turn onto `character`; point-and-click is the same `navAgentMoveTo`. Occluders as above. What the editor adds for them: polygon drawing on the canvas (walk areas, hotspots, `behind` cut-outs), an outline panel for dialogues, the verb list and parser words as tables typed like everything else, per-room camera set from the viewport ("use this view"), and play-from-this-room with the inventory and flags set as the editor has them. The test, in the spirit of section 1's two-genres rule: one description, two rooms, one hotspot each, a door that needs a key taken in the other room, a dialogue with a choice that sets the flag the door checks, a cut-scene on opening it, and a score of two -- built once over a painted backdrop in 2D and once in a modelled 3D room with a fixed camera, the rules identical between the two. Driven by `authorPointer` and `authorSay("use key on door")` rather than by time. ### Where the adventure sits in the milestones Section 2's milestone 4 was "HUD bindings, `say`, inventory, `discBranch`, saves, levels, results, lives, high scores" proved on branching FMV and a point-and-click adventure. With the owner's emphasis the order changes: 1. Types, events, vars, states, expressions, groups, timers, sound, HUD -- and **sequences** (`waits`), since they are the substance of milestone 3 and cheap to do here. Genres: the shoot-em-up and the light gun over video, as before. 2. `scene3d`, models, `animator`, `character`, `camera` modes, the 3D gun, `seek`, `health`, `ragdoll`, the rail. Genres: the rail shooter and the 3D platformer. 3. **The adventure**: rooms, walk areas and 2D nav, hotspots, verbs and the sentence line, the parser, inventory, dialogues, cut-scenes, saves, scaling, occluders (the one engine change). Genres: the same adventure in 2D and in 3D, as above. 4. `agent` crowds, `vehicle`, `thrust`, `joint`, `body`, `push`: racing and tower defence. 5. Branching FMV, lives, continues, `submitScore`, results: the arcade plumbing every earlier genre then inherits. 6. Editor: types panel, asset browser, picker search, timeline polish, dialogue outline, polygon tools, play-from-here. 7. The long tail: sprite frames and clip hitboxes, grids, MIDI, boats and water, soft bodies. ### Out of scope, carried forward As section 2 states it, plus: a text parser that understands grammar beyond verb, noun, and preposition (Sierra's did not either); lip sync; a dialogue *writing* tool beyond the outline (a writer's tool is a different program); and localisation, which is a table of strings and a milestone of its own once there is something to translate. ## 4. Milestone 1, built (2026-09-14) Types, rooms, events, vars and states, expressions, groups, timers, sound, a HUD, sequences, the hitbox track and the timeline. Proved on the shoot-em-up (`testScripts/author/shmup.forge`, scene 58) and the light gun over video (`gunvideo.forge`, scene 59); the first slice's platformer and QTE were rewritten to the new format and still pass (scenes 52 and 53), and the editor scenes (54 to 57) were adapted, with scene 60 for the timeline. **What was built, against section 3's decisions:** * `Author.singe` rewritten: `authorBegin(description)` builds the layers, the vars, and the first room; `authorRules(list)` takes the compiled rules by event; `authorFrame` runs timers, behaviours, sequences, the frame rules, the type-pair collision test, the HUD bindings, and the draw. Instances are made from types (`authorSpawn`, `authorDestroy`), rooms keep their instances when left (`authorGoTo`), and every instance has `vars` with `state`. * Events: `frame`, `roomStart`, `roomEnd`, `pressed`, `released`, `collision` (type pairs, an edge), `hit`, `miss`, `death`, `spawn`, `timer`, `frameReached`, `gameOver`. A rule names one with `on` and filters with the event's own parameters; `each` scopes it to a type with `self` bound. * Sequences: an action with `waits` (`say`, `wait`) yields from the rule's coroutine through `authorWait`; a rule without one runs plainly. `controls = false` takes the keys away for its duration; the same rule on the same instance does not restart unless `interrupt`. * Expressions: `AuthorCompile.singe` carries a recursive descent parser (`authorExpression`) with a whitelist -- `self.var`, `other.var`, `event.field`, `name.field`, game vars by name, `count`, `random`, `distance`, `has`, `abs`, `min`, `max`, `floor`, `time`, `frame`. Every `number` or `expression` parameter accepts one. `any` groups nest. * The manifest's `emit` functions receive Lua fragments, not values: the compiler resolves each parameter by its declared type. Behaviour parameters, which travel as data, resolve key and switch names at run time. * `authorCheck(description)` names undeclared types, looks, behaviours, conditions, actions, and events, and expressions that do not parse. * The editor: four panels (`Tab` cycles entities, types, rules, tracks), rooms (`PgUp`/`PgDn`, `N` adds one), types typed field by field with looks and behaviours from the manifest and renames that follow through the rules, entities as type plus overrides, rules with `E` picking the event and its filters typed, `O` for an `any` condition, and the timeline: the disc layer's video scrubbed under the cursor, keys made at it (`K`), dragged on the canvas, typed, and deleted. * `docs/ForgeVocabulary.adoc` is generated from the manifest by `util/forgeVocabulary.lua` (in `build-docs.sh` and the CMake rule), so the manual's tables cannot drift. **Decisions taken while building, for the record:** * A behaviour's parameter cannot be called `type` -- the key names the behaviour -- so `shooter` and `spawner` say `spawns`. * A gun hits only instances with a `target` or `hitbox` behaviour. The first version hit the score readout. * File names in a description are relative to the game's directory, like every other Singe path. * A description's `size = { w, h }` sets the overlay; without it the engine's default stands, which over a disc is the disc's own. * The spawner's count is a var (`self.made`), since rules read vars and not runtime fields. **Not yet, from the milestone's list:** sprite frame animation as an `animator` (the `sprite` look takes `frames` and the var `frame`, but nothing steps it yet); the `spawns` track type (waves are a `spawner` behaviour for now); `chance`/`every` are in, `soundDone` is not. All are milestone 6 or arrive with the genres that need them. ## 5. Milestone 2, built (2026-09-14) The scene: `scene3d`, the `model`, `mesh`, `light`, `billboard`, and `text3d` looks, `character`, `camera` (fixed, follow, orbit, first, rail), `seek`, `animator`, `target` with zones, `health` with ragdolls, `body`, `trigger`, the gun's ray, and the rail. Proved on the rail shooter (`testScripts/author/railshooter.forge`, scene 61: two stops, seven zombies killed, seven headshots, the rail moving on when each stop is clear, the end reached) and the 3D platformer (`platformer3d.forge`, scene 62: the controller moving relative to a following camera, a jump onto a ledge, a prize that is a trigger). The editor shows a 3D room as the scene under an orbiting camera, picks and drags by projection, and puts rail points where the camera stands (scene 63). **How the pieces went, against section 2's design:** * Every node an instance owns -- its own, a model's whole subtree, the body box, the zone boxes -- is registered in `AUTHOR_BY_NODE` when it is made, so a ray or a contact maps to its instance with a table lookup. The first version walked `nodeGetParent`, and physics reported a node a rule had just deleted (a prize destroyed as it was entered); asking the engine about a dead node ends the game, so the walk went. * `target` in 3D is a kinematic trigger box on a child node raised by half the look's height, since a model stands on its origin. Zones are boxes parented to named bones (`b_Head_05` on the Fox). The ray meets the body box first, so zones are found by how close the ray passes to each zone's centre -- no second cast, and no engine change: the raycast-hits-bone question from section 2 is answered without it. * The rail is `authorRailStep`: points by time, position and look-at interpolated, a `stop` held until `pathNext`, `stopped` and `railEnd` events. `shake` offsets the rail camera for a moment. * `character` moves relative to the camera's ground axes (`authorCameraAxes`), which come from a `first`/`orbit` camera's yaw or a `follow` camera's bearing to its target. * Ragdolls: `health` with `ragdoll = true` makes the ragdoll at attach and activates it on death, keeps the instance for `linger` seconds through a named timer, and takes it off the targets. * The editor's preview reuses the runtime's look `load` functions on a fake instance, rebuilt when what the entities *are* changes (a stamp of types and looks) and moved every frame; the camera orbits the selection; drags cross the plane of the entity's own height. **Not yet:** `first` and `orbit` cameras are in the manifest and untested (the FPS is milestone 4's territory); `billboard` and `text3d` likewise; `animationDone` is polled from `animationIsPlaying`; the timeline's `spawns` track is still a `spawner` behaviour and a rule per stop. ## 6. Milestone 3, built (2026-09-14): the adventure Rooms that remember themselves, walk areas baked to a navigation mesh (2D polygons or 3D floors), the `walker`, hotspots with polygons, verbs and the sentence line with most-specific-wins, the parser and `said`, the inventory, dialogue trees compiled with their conditions and actions, cut-scenes across rooms, fades, depth sorting and scaling, feet anchors, `saveGame`/`loadGame`, and `die`. Proved on `adventure2d.forge` (scene 64) and `adventure3d.forge` (scene 65), which takes the 2D game's rules, dialogue, verbs, and parser unchanged and supplies only types and rooms -- the test section 3 asked for. The editor types a room's own fields and draws its walk areas. **How the pieces went:** * Walking in 2D: the polygons are ear-clipped in Lua into a flat mesh scaled by a hundredth (`NAV_SCALE`), baked with `navNew`/`navAddNode`/ `navBuild`, and an agent drives a separate navigation node whose x and z are copied back to the instance's overlay x and y every frame. In 3D the agent drives the instance's node. **Engine fix:** `navAgentNew` read the node's world position as of the last drawn frame, so an agent made in a game that never draws the scene sat at the origin, off the mesh; it refreshes the transforms first now, as `navAddNode` already did. * Exclusive events: rules are tiered by the parameters they name (two each) plus one for having conditions, and the highest tier whose conditions hold acts; a rule's compiled function answers whether its conditions held, and a sequence that yielded has passed them. This is what lets "I do not know the word" (guarded, no parameters) beat "You cannot do that here" and fall through to it when there is no unknown word. * Sequences survive a room change: the cut-scene that walks through the door is the one changing rooms and has a fade-in still to do. They are cleared only by a restart or a load. * `goTo` with `at` names an entity in the destination room to arrive at, which is how one rule serves both worlds. * The dialogue's `next` was first `goto`, which Lua reserves. * Physics contacts now reach `collision` rules, so the rules' `a` and `b` types are matched (the first version matched neither, and a platformer's hero destroyed its own ground). **Not yet:** occluders for a 3D character walking behind a painted pillar (the `materialSetOccluder` engine change section 3 named); the editor's polygon drawing and dialogue outline (milestone 6); a sprite `animator` for walk cycles (`state` and `facing` are set, nothing steps frames yet). ## 7. Milestone 4, built (2026-09-14): vehicles, crowds, turrets, joints `vehicle` (car, motorcycle, tank, boat over `vehicleNew`, wheels hung from the look's corners, driven by the `keys` vars, `self.speed`), `racer` (a vehicle steering itself round a track of points from its yaw), `walker` `follow` (repathing after a target), `patrol` with `patrolEnd`, `turret` (nearest target in range, a projectile aimed by velocity overrides at spawn), `spawnAtPointer`, `thrust`, and `joint` (hinge, ball, slider, to another body or the world, made on the first frame so the other body exists). Proved on `racing.forge` (scene 66: the car reaches 22 units a second, passes two checkpoint triggers for a lap, the rival drives its track, the hovercraft moves under thrust, the hinged bar hangs) and `towerdefence.forge` (scene 67: ten creeps in waves along a patrol, two turrets placed by clicks projected onto the floor, ten kills, gold and lives kept by rules). Both worked on the first run, which says the model from the earlier milestones held. **Decided on the way:** the vehicle's forward is -Z, as the engine's is, so the throttle is `-dy`; the racer's steering is the signed angle between the chassis' heading (from `nodeGetRotation`) and the next point, clamped; a turret without a projectile type does its damage directly. ## 8. Milestone 5, built (2026-09-14): the arcade plumbing The `branches` track and the `branching` behaviour (`branchOpen`, `branchTaken`, `branchMissed`; the disc sent to the success or the fail frame), the `bezel` layer mirroring `score`, `lives`, `credits` (and a second player's) to the arcade panel, `credit`, `gameOver` with the results card and the best score kept in a save, and `submitScore` loading `Singe/Net.singe` and `Singe/Master.singe` on first use and pumping them after. Proved on `fmv.forge` (scene 68): a window answered in time, three missed, the lives spent, the score sent once (the master client stubbed by the scene), a coin, and a continue that starts over with three lives. The point-and-click adventure this milestone was to share with had already landed in milestone 3. ## 9. Milestone 6, built (2026-09-14): the editor Typing narrows every picker; a field that names a file (by the manifest's type) opens a picker of the files under the game's directory, two levels deep, in place of the prompt; `V` draws a walk area or a hotspot's outline corner by corner on the 2D canvas; the dialogues are a fifth panel, an outline of dialogues, nodes, choices, and a choice's actions, with `A` adding at the level selected and `T` picking an action; and `P` plays from the room on screen by building a copy with that room first. The types panel, the asset list, and undo across the timeline had landed with the earlier milestones. Scene 69 drives all of it and checks what reached the description and the compiled game. The one thing the milestone planned and this does not do is rebuild every earlier scene through the editor rather than from a hand-written description; scenes 54, 55, 57, 60, 63, and 69 between them exercise every editing path those descriptions use, so the rebuild was not worth its cost in scenes. ## 10. Milestone 7, built (2026-09-14): the long tail The `frames` behaviour stepping a sprite sheet by state, the `grid` look drawing a tile map from a sheet with `spriteDrawGrid`, the `midi` event from `onMidiMessage` (its filter is `pitch`, because `note` is what a rule's comment is called -- the first version tried to compile every rule's comment as a MIDI note), the `water` behaviour over `bodySetWater`, `buoyancy` on `body`, boats with `thrust` through the `vehicle` behaviour, and `soft` over `softNew`. Proved on `tail.forge` (scene 70: five frames of a walk cycle seen, a 5 by 3 grid measuring 160 by 96, middle C scoring and a second note counted) and `pool.forge` (scene 71: a ball that floats at the surface, a boat that travels nineteen units under its propeller, a balloon that inflates). With this every kind of game in section 2's catalogue has its vocabulary in the manifest, and every milestone of section 3 is built. What remains is listed where it was found: occluders for 3D over a painting (section 6), first-person and orbit cameras untested (section 5), the editor rebuilding every scene (section 9), and the light gun cabinet checks that need a person at a real machine (section 1). *Closed 2026-09-14:* occluders came with section 12; first and orbit cameras are driven by injected mouse motion and asserted on in scenes 72 (fps.forge) and 75 (thirdperson.forge), so "untested" was stale; `billboard` and `text3d` looks now stand in those samples (puffs at the rail shooter's stops, a sign in the arena) and are checked; the `spawns` track is built (a waves track: `authorWavesAtStop`, `updateWaves`, `W` and `K` in the editor, scenes 94 and 95, sample waves.forge); and `soundDone` arrived (`authorPlaySound` remembers the channel's file and owner, the compiled game binds `onSoundCompleted` to `authorSoundDone`). Only the cabinet checks remain. ## 11. The rest of the catalogue (2026-09-14) With the milestones built, the remaining types of game in section 2's catalogue were written as descriptions to find what the vocabulary still lacked. Each is a sample in `testScripts/author/` with a scene: * **First-person shooter** (`fps.forge`, scene 72): `camera first` on the hero's eyes turned by the mouse, `keys` moving relative to it, a `gun` with `aim = "centre"`, zombies on `walker follow`. Found: the gun's ray met the hero's own body first; it now passes through anything that is not a target (a few casts on from each hit) before counting a miss. * **Breakout** (`breakout.forge`, scene 73): `solid` with `moving` (a kinematic paddle that follows the `mover`), a `body` ball with bounce 1, `push` on `roomStart`. Found: events raised while the first room is built -- `roomStart`, every placed entity's `spawn` -- came before the compiled game handed over its rules, and were lost; they are kept and replayed by `authorRules` now. * **Maze** (`maze.forge`, scene 74): a `grid` look for the walls, corridors as walk areas, a ghost on `walker follow`, dots eaten by collision. * **Third person** (`thirdperson.forge`, scene 75): `camera orbit` under the mouse, `keys` relative to it, a sword swing as `on = "pressed"` with `each = "enemy"` and a distance test. Found: `each` on an event with no instance of its own (a key, the disc, a room) now fans the rule out over every instance of the type, as a frame rule does; `distance` takes ids as well as instances and measures through the scene in 3D. * **Space shooter** (`space.forge`, scene 76): `thrust` with no gravity, a `shooter` firing 3D projectiles, rocks as slow projectiles with `health`. * **Rhythm** (`rhythm.forge`, scene 77): a `music` layer, notes as instances falling to a marker, a press scored by where the note is. * **Bowling** (`bowling.forge`, scene 78): a heavy `body` ball shoved by `push`, pins as bodies counted by how far they lean, a `trigger` at the end of the lane. ## 12. Occluders and gizmos (2026-09-14) **Occluders**, the engine change section 3 named: a `PIPELINE_OCCLUDER` variant in `scene.c` whose colour target has its write mask cleared, so the mesh goes into the depth buffer and nowhere else; `materialSetOccluder` sets it, an occluder casts no shadow (the painting already has one), and the GLES shim already honoured the mask. Forge's `mesh` look takes `occluder = true`; the editor's preview marks its instances so they stay visible for placing. `painted.forge` (scene 79) is a picture on a flat mesh, an occluder pillar, and the Fox crossing behind it -- and found that `materialSetTexture` takes a KTX2 file or a sprite handle, so the look loads any other picture as a sprite first. **Gizmos** in the 3D viewport: three arms from the selected entity along the world axes, projected with `sceneProject`, each with a handle; a press on a handle starts a drag along that axis, the pointer's travel along the arm as a fraction of the arm being the distance moved. Scene 80 raises a wall by an arm's length on Y, brings it half an arm back on X, and undoes both. **Rotation and facing** (the two "minor" items): entities carry `rx`, `ry`, `rz` (applied to the instance's node by `make`, shown in the 3D preview, typed in the entities panel), and the gizmo has a fourth arm, yellow, the way the entity faces, whose handle dragged round the entity turns it about Y (scene 80, a quarter turn; scene 79 checks the rotation reaches the node). Sprite facing was data at first -- a `frames` range named `walkLeft` used while `facing` is left -- and is now an engine flip as well: `spriteFlip` mirrors a sprite for every draw, so a `sprite` look mirrors itself while the instance faces away from its art (`faces`, right unless said), and a `Left` or `Right` range is only for sheets drawn both ways; scene 70 holds LEFT and sees only the drawn frames, scene 81 sees the mirror. A per-entity `scale` (typed; no handle) scales the node -- look, body, and counted size -- with bodies given the look's unscaled size, since Jolt takes the node's scale itself (scene 79). **Sprite rotation and the rest of the sweep** (2026-09-14): a `sprite` turns with the var `angle` (seeded from the entity's `rz` in a 2D room; a turning instance gets an image of its own so the shared one is not rebuilt for every angle in the room), a `spin` behaviour turns the angle or the node about Y, a `projectile` with `aim` points its sprite along its flight, a `particles` look is a started emitter, `stopMusic` stops the track, and `authorCheck` names any field a look, a behaviour, or an event does not take. Found on the way: a 2D emitter draws nothing until `emitterDraw` is called each frame -- the shoot-em-up's explosions had never appeared -- so the frame draws every live instance's emitter in a 2D room. Scene 70 covers the spinner, the fire, and three deliberate misspellings. **Mirrors, turned sheets, clip volumes, the 2D turn handle, and thumbnails** (2026-09-14): the engine gained `spriteFlip` (a mirror applied to every draw of a sprite, frames included), and a `sprite` look mirrors itself while the instance faces away from its art (`faces`); a sheet entity with an angle draws its frame turned through `spriteRotateFrame`; `soundPlay` takes a volume and `soundSetChannelVolume` changes a channel's own, under the master (which used to be applied through the mixer's tag gain, overwriting the distance fade of positioned channels -- it is per track now), so the `playSound` action and the `sound` behaviour take a `volume` of 0 to 100; the 2D canvas has the yellow turn handle the scene had, dragged round the entity to set `rz`, and a sheet entity on the canvas shows its first frame turned; and the types panel shows each type's picture -- the sprite through RmlUi's ``, which reads the vfs, or a swatch of the box's colour -- beside its name. Scenes 81 (runtime) and 82 (editor). Still not doable here: the hardware checks (RmlUi's document pointer path on a real desktop, light gun coordinates, off-screen reload, and two guns). ## 13. Tutorials (planned 2026-09-14) The reference chapters say what Forge is; nothing yet says what to *do* first. The tutorials go at the front of `docs/Forge.adoc`, as a part of their own (`== Tutorials`) between *About Forge* and *Describing a Game Instead of Writing One*, so the book opens with a game being made and only then explains the pieces. They stay out of the Singe manual, like all Forge documentation. ### 13.1 Shape Every tutorial is one sitting and ends with a game that plays. Each has the same skeleton, so the tenth reads like the first: 1. **What you will make** -- one sentence and one picture of the finished game. 2. **What you need** -- the tutorial before it (or nothing), and which files of the kit (13.3) it uses. 3. **Steps** -- numbered, each one key or one form: *press `A` -- a new type appears at the top of the list*, with a picture wherever the screen changes in a way words would fumble. Never two ideas in one step. 4. **What happened** -- the concept the steps just used (an event, a behaviour, a track), in a paragraph, with a link to the reference chapter that owns it. This is where the tutorial teaches; the steps only do. 5. **Try this** -- two or three variations that need no new steps (change the jump, add a second coin, move the door), so the reader leaves the rails on purpose. 6. **The description** -- the finished `.forge` text, in full for the short ones and as a diff against the previous tutorial for the long ones, so a reader who lost their way can compare, and a reader who prefers a text editor can skip the steps entirely. Pictures are block images (`image::tutorials/01-chooser.png[]`) under `docs/images/`, PNG, embedded in Forge.pdf by asciidoctor-pdf. A full 720 by 480 frame of the editor reads well at page width; cropping is a later nicety, not a requirement. ### 13.2 The tutorials are tests The pictures are not taken by hand. Each tutorial has a scene (`testScripts/scene83.singe` upward, one per tutorial) that drives the editor through exactly the keys the text names -- `forgeKey`, `forgePress`, `forgeDrag`, the typed forms -- shooting a frame at each pictured step, and at the end asserts two things: that the description on screen equals the tutorial's shipped `.forge` (the round trip scene 54 already relies on), and that the built game runs a few seconds without a `FAIL`. A change to the editor that breaks a tutorial breaks its scene; a change that moves a panel regenerates the pictures. The refs sweep runs them like every other scene. Two small engine and editor pieces make that clean: - `singeScreenshot(name)` -- an optional file name, so a scene writes `01-chooser.png` rather than `singe007.png` for a rename step to guess at. Manual entry and CHANGELOG line. - a harness step (not in the repo, like the rest of the harness) that copies a tutorial scene's shots into `docs/images/tutorials/`. The images *are* in the repo, since the docs build reads them; the screenshots folder stays scratch. ### 13.3 The kit A tutorial cannot say "find a picture of a hero": the pictures must be there. `forge/kit/` ships in Forge.game and the first run copies it out beside Forge.pdf, where *New game* already writes descriptions, so every tutorial's file names are the same on every machine (`kit/hero.png`). What is in it, all ASCII-named, all small, all ours or CC0: - `hero.png` -- a sprite sheet, 8 columns: idle, a 4-frame walk facing right, a jump, a fall, a hit. Drawn once, mirrored by `faces`. - `coin.png`, `crate.png`, `door.png`, `spike.png` -- stills. - `tiles.png` -- a 4-column tile sheet for `grid` looks. - `enemy.png` -- a 4-column sheet for the shoot-em-up and the gun game. - `room.png`, `room.jpg` -- a painted adventure room, with a floor to walk and a pillar to walk behind (the 3D adventure reuses it under occluders). - `fox.glb` -- the Fox model (already used by seven samples; its licence goes in `kit/LICENSE`), `crate.glb` a box with a texture. - `jump.wav`, `coin.wav`, `shot.wav`, `hit.wav`, `click.wav`, and a short `loop.ogg` for a music layer. - `clip.mkv` -- twenty seconds of video with three obvious moments in it (a door opens, a target rises, a hand reaches), for the FMV, QTE, and gun tutorials. `Singe/menuBackground.mkv` will do until it is made. Two consequences for the editor, both gaps today: - `forgeExport` copies the runtime and nothing else; it must copy every file the description names (looks, sounds, music, video, models, `hud`) into the export folder, rewriting the names to be relative to it, or a packed game refers to a kit that is not there. - the file picker lists the game's directory; it should list `kit/` under it too, which it will once the kit is extracted beside the descriptions. ### 13.4 The tutorials Ten, in an order where each needs only what came before. The sample each grows into already exists and is named; the tutorial's own shipped description is a smaller, cleaner cousin of it (`Forge/tutorials/NN-name.forge`, packed and listed by the chooser under a *Tutorials* heading). | # | Title | Makes | Teaches | Grows into | Scene | |---|-------|-------|---------|------------|-------| | 1 | **Ten minutes** | The *New game* starter, played and changed | the chooser, the four panels, `Enter` forms, `P`, `S`, `Esc`, where Forge.pdf and the kit are | -- | 83 | | 2 | **A platformer** | coins to collect, spikes that kill, a door to a second room | types and instances, the `collision` event, `each`, `addScore`, `destroy`, rooms and `nextRoom`, the readout | platformer.forge | 84 | | 3 | **Sprites and sound** | the same game with the kit's hero sheet, mirrored facing, a jump sound, music | `sprite` with `frames` and `faces`, the `frames` behaviour and states, `setState` rules, the `sound` behaviour with `volume`, the `music` layer, `rz` and the turn handle | platformer.forge | 85 | | 4 | **A shoot-em-up** | waves from a spawner, a ship that fires, explosions, lives, game over | `world2d` without gravity, `spawner`, `projectile` with `aim`, `hp`, `emit` and `particles`, `lives`, `gameOver`, the results screen | shmup.forge | 86 | | 5 | **A quick-time event** | a clip with three prompts and a penalty for missing one | the `fmv` layer, tracks, the timeline (`,` `.` `[` `]` `K`), time windows, `prompt`, `disc` conditions, branching to a second clip | qte.forge, fmv.forge | 87 | | 6 | **A light gun game** | targets that rise from the clip, ammo, reload off screen, a score | the `gun` behaviour, target boxes on the timeline, `hit` and `miss`, `pointerOffscreen`, two players, what a cabinet needs (the light gun section) | gunvideo.forge | 88 | | 7 | **A point-and-click adventure** | two painted rooms, a walker, a locked door and a key, a conversation | walk areas (`V`), `depthSort` and `scaleBy`, hotspots and verbs, inventory, `said`, dialogues (the fifth panel), `fade`, `save` | adventure2d.forge | 89 | | 8 | **Into 3D** | the platformer again, in a scene: a fox on boxes with a camera behind it | `scene3d`, `model` and `mesh` looks, `light`, the 3D viewport keys, gizmos, `character`, `camera` follow, `ry` | platformer3d.forge | 90 | | 9 | **A rail shooter** | the camera rides a rail through a painted room; targets rise at stops and fall when shot | rail tracks with stops, `spawnAt`, the gun's ray and body parts, `ragdoll`, `occluder` meshes over a painting, `navFrom` | railshooter.forge, painted.forge | 91 | | 10 | **Releasing your game** | the platformer as a `.game` in the menu, with a high score board | `forgeExport`, `--pack`, `games.dat`, the bezel, `submitScore`, what the machine at the arcade needs | -- | 92 | Not tutorials, but a closing **Recipes** section, one paragraph each with a sample to open: racing (`racing.forge`), tower defence, pool and water, bowling, the maze, third person, space, rhythm and MIDI, the 3D adventure. A recipe names the behaviours and the sample; it does not walk the keys. ### 13.5 Order of work - **A. Foundations** -- `singeScreenshot(name)`; the kit drawn and packed, extracted on first run, listed by the file picker; `forgeExport` copies named files; the chooser's *Tutorials* heading; `== Tutorials` in `Forge.adoc` with `:imagesdir:` and the first picture in the PDF to prove the build embeds it. - **B. Tutorials 1 to 3** and their scenes. These settle the voice and the skeleton; the rest copy them. - **C. Tutorials 4 to 7.** - **D. Tutorials 8 to 10** and the recipes. - **E. A read-through** of the reference chapters against the tutorials, so every concept a tutorial teaches links to the paragraph that owns it, and nothing is explained twice in two ways. Each stage ends with `./build-docs.sh`, a look at Forge.pdf's pages, and the refs sweep. The pictures are regenerated whenever the editor's chrome changes, by running the tutorial scenes; a stale picture is a scene that was not rerun, and the sweep catches a tutorial whose keys no longer do what its text says. ### 13.6 Built (2026-09-14) All ten tutorials and the recipes are in `docs/Forge.adoc`, with their scenes 83 to 92 and the shipped descriptions in `forge/tutorials/`; the kit is drawn and synthesised by `util/forgeKit.py` into `forge/kit/` (the clip is 720 by 480, the overlay's size; a smaller disc was thought to draw nothing, but that was the editor's opaque ground leaking into the game, fixed since -- a 480 by 320 disc under a 720 by 480 overlay draws and scales as it should, checked 2026-09-14). What the tutorials forced into being, because a reader could not otherwise do what the text said: - `singeScreenshot(name)` in the engine. - The kit stays inside Forge.game and is named `Forge/kit/...`; nothing is extracted. The file picker lists it beside the game's directory. - `forgeExport` (and `X`) copies every file a description names, at the same relative name; the compiled script sets `AUTHOR_DIR` and the runtime resolves names beside the game first (`authorFile`). The editor resolves beside the description (`forgeFile`). - The game's own form when nothing is selected: title, players, vars, verbs, layers, and each layer's parameters, with tables typed as `r=1, g=2` or `1,2; 3,4`. A scene3d layer typed in starts the viewport. - `1` to `5` for the panels; `Delete` empties a field; a passed-over field keeps its value (it used to be emptied); behaviours and layers typed away and back keep their values (a stash), since the setter runs per keystroke; new parts and behaviours fill in only what must be there (self, a key, a switch), leaving numbers to their runtime defaults; the file picker starts on the file already named or on `(none)`; exact picker matches sort first (`miss` before `branchMissed`); the panel hides while a polygon is drawn; in a 3D room `A` places on the floor under the viewport's middle. - The runtime: the platformer sets `facing` and `state`; a flat game with no disc paints its own dark ground; `scaleBy` scales only what stands on the floor (anchor feet); text looks sort in front under `depthSort`; the chooser lives in the library (`forgeChooser*`) and lists `Forge/tutorials/`. - The compiler emits dialogue nodes in a fixed order, so the same description compiles to the same text every time. The pictures go into the book through `util/forgeShots.py`, which scales them to 768 wide and reduces them to a palette (a full-window PNG per step made Forge.pdf 7.5 MB; this keeps it small). **Renamed** (2026-09-14, the owner's call): what an entity is made from is a *type*, not a kind -- `types = { hero = ... }`, `type = "hero"` on an entity and in an event's filter, the Types panel, `forgeType*` in the editor -- everywhere at once, with no compatibility kept. `kind` stays as the discriminator on a look, a behaviour, a layer, and a rule part (`{ kind = "sprite" }`), which is the one thing it no longer collides with. The manifest's parameter type is `type` too. **Lists of records** (2026-09-14): a table parameter typed as text now takes records (`name=head, bone=b_Head_05, w=0.5; name=body, ...`, one per semicolon), maps of lists (`look=look|examine`), and lists of words, so `target.zones`, `parser.words`, and `parser.ignore` are typed in the form like everything else, and read back the same way. **The sunk hero** (2026-09-14): two things. Every flat body was half the size its look was drawn at -- the 2D branches of `solid`, `body`, and `trigger` handed `bodyNew` halves, which takes full sizes -- so the ground's top was ten pixels under the drawn top; and a player stands on its node (the node is the feet, as `playerNew` says) while the look was drawn centred on it, so the box sat half its height into the floor. Bodies are full size now and a platformer draws with anchor feet; the starter's hero says so and starts on the ground. Scene 92 packs the release with `--pack` when the harness hands it the engine's path in `SINGE_BIN`, and says so when it cannot. Packing found that the export's `games.dat` named the script by its absolute path, which `--pack` refuses: names in it are now the folder's own name first (`CoinRun/CoinRun.singe`), the folder is named for the game, and the chooser tells a packed `.game` from a description by the database header. **The first desktop run** (2026-09-14, the owner's): entities select and the panel drags, so the RmlUi pointer path is proven -- the open hardware check from section 1 is closed. What it found: no key but the switch ones reached the editor -- SPACE opened a game in the chooser because controls.cfg maps it to a button, and ENTER and TAB did nothing -- because Forge's launcher never asked for `MODE_FULL`, in which every key reaches `onKeyPressed`; it does now (the runtime always did). Beside that, the engine now hands TAB to a GUI document only while a text field is being typed in, since RmlUi takes it for focus and keeps it; the drawn pointer went under the panel (it is drawn after the GUI now); and a readout's box was centred on its point while its text hangs from it (the editor's bounds for a text look are its top left now, as the game draws it). The owner runs Forge from a loose copy in the test library, which no build refreshes; it was a day stale. **The second desktop run** (2026-09-14): TAB worked; the mouse could no longer select, and the cross was still under the panel. The mouse: in `MODE_FULL` the engine handed a mouse or pad button to `onInputPressed` as a keysym of 0 rather than as its switch, which no script could use; a button cannot be typed, so it now arrives as its `SWITCH_*` value in either mode, as the manual always said. The cross: the engine draws every GUI over the overlay after the frame, so nothing drawn on the overlay can sit over the panel; the cross is an element of Forge's own document now, moved to the pointer each frame, and draws with the chrome and over it. **Overlay layers round guiDraw** (2026-09-14, the owner's call): the document element was the wrong answer -- anyone writing a mouse or gun game would have had to fight the same thing -- so the engine now layers the overlay round each `guiDraw`: what a script draws after the call goes on a layer composited over that document, up to sixteen a frame, uploaded only when drawn on. Forge's pointer is two lines drawn after `guiDraw` again, and the manual's `guiDraw` entry and scene 93 (a box under a document, a cross and a second document over it, a box over that) cover it. **Rows and fields take clicks** (2026-09-14, the owner asked why they did not): only the grip had a handler. Every row the panel lists is a div with an id now, wired after the list is set to one dispatcher, `forgeRowClick`: a row selects what it names, a picker's row takes it, and a field in the box below starts typing it (`forgeEditField`). The keyboard stays complete, so a cabinet loses nothing; scene 82 drives the dispatcher. **Clicking a field, then typing** (2026-09-14): the owner could click a field and nothing happened. The engine's `guiSetHandler` kept one listener per id and never moved it: once a list was rebuilt through `inner_rml`, the listener sat on an element that no longer existed, so every row of every rebuilt list was deaf -- the entity rows included, from the first refresh on. A handler follows its id now (the engine re-attaches when the element behind the id has changed), scene 82 clicks rows through RmlUi's own `DispatchEvent` rather than the dispatcher, and the "panel at" and "moving the panel" messages are gone. A wrong turn on the way: `focus: none` on the rows, meant to keep RmlUi from turning ENTER on a focused row into a click, stopped every click instead -- RmlUi delivers a click to the nearest focusable element under the pointer, so an unfocusable row cannot be clicked at all. Rows are focusable; ENTER only becomes a click on an element whose `tab-index` is auto, which no row's is. Proven with RmlUi's own mouse processing driven from a scene: a click on an entity row selects it, a click on a field starts typing it, and the typed text lands. Labels in the box carry a colon, `id` shows as `ID`, and editable rows light up under the pointer. **Typing in place, and SHIFT** (2026-09-14): a value is typed in its own row now, with a caret, rather than echoed on the message line; and SHIFT did nothing because a keysym is the unshifted key -- the engine gained `onTextInput` (SDL text input runs while a script has `MODE_FULL`), the launcher feeds it to `forgeText`, and while typed text arrives the keys' own characters are not typed as well (`FORGE.textInput`). The scenes still type through keysyms, which is why they need no shift. **Editing in place, and the way back** (2026-09-14): a value now starts selected whole when its field is entered, so typing replaces it, and the arrows, Home, and End put a caret into it to edit it (`forgeEditingOf`, `forgeEditInsert`, `forgeEditBackspace`, `forgeEditDelete`, `forgeEditCaret`); DELETE on the selected value empties it, which is what the tutorials' "empty the entity" means. And the game's own form was out of reach once anything was selected: the game's name at the top of the panel and a press on an empty spot of the canvas select nothing now. **Pickers for every fixed-set field** (2026-09-14): the owner: "Editing items that have a fixed list of options does not display a list of options -- you're expected to know what you're allowed to type. This is bad. Dropdowns exist for a reason." `forgeFieldKind` says what a field is from the manifest (its parameter type, or the words a `choices` table on the manifest entry lists -- `anchor`, `faces`, a camera's `mode`, a light's `type`, a body's `shape`, ...) and from the editor's own fields (a type's look, a rule's event, its flags, an entity's type, a track's key, a dialogue's nodes); `forgeFieldChoices` turns that into picker items and `forgeFieldList` opens the picker, on the value the field has. Free lists (type, room, entity, state, track, node) offer the typed text first, so a name made later can still be typed, and a value the list lacks is offered "as it is", so walking past the field with ENTER keeps it (the first sweep lost `goTo.room = "cave"` that way, the room not being made yet). ENTER on a row takes it and moves on to the next field, as ENTER on a typed value does, so the form is still one ENTER per field; a click takes it and stays. Choosing the value a field already has does nothing (choosing a type's look again used to empty its parameters, and an event again its filter). `tutorialDrive.form` reaches a field with a while rather than a repeat, and leaves a list and then the field with ESC. A long list windows `PICK_ROWS` rows around the chosen one. The owner then found the list taking the panel's top list over confusing ("Isn't there a regular drop down?"): a field's picker (`FORGE.picker.field`) now unfolds under its own row in the fields box as a `.drop` element, absolutely positioned with no top or left so it takes its static place and floats over the rows beneath (`dropdown()` and `pickerRows()` in Forge.singe, `DROP_ROWS` 6); only the manifest pickers (C, T, E) still take the list over. Then "The dropdown should open upward when there's no room below. And what about scrollbars?": the panel is a flex column whose list and fields box share the height and scroll (`overflow-y: auto`, styled `scrollbarvertical`), and `forgePlace` -- run at the start of every draw, on the first draw after the frame a refresh happened in, since a key or click is handled before that frame's draw and new elements measure nothing until the engine's guiUpdate has laid them out -- scrolls the chosen row and the edited field into view (`scrollTo`, nearest) and lifts a dropdown above its row with a negative top margin when its bottom would pass the panel's. The dropdown is hidden until placed, so it never flashes below first. The RmlUi trap under all of it: the document body had no height, so the panel's `height: 100%` was zero and everything in it was overflow -- which is why `overflow-y: auto` on the panel "made it vanish" and a percentage `max-height` collapsed; `body { width: 100%; height: 100%; }` fixed both. A scrolling box is an offset parent, so a row's `offset_top` is relative to its box (`offsetWithin` sums the chain). The vocabulary tables print the words instead of "string" for a parameter with choices. **The build owns Forge.game** (2026-09-14, the owner's rule): the release build's `forge` target repacks `.builddir/Forge.game` whenever anything under `forge` changes -- scripts, kit, tutorials -- and Forge.pdf is rendered again whenever the book, its vocabulary, a tutorial's picture, or a shipped description changes; both globs are re-run on every build, so a new file counts too. Nothing about Forge is copied anywhere by hand. Not done: cropping the pictures (full frames read fine at page width); the light gun checks stand as before. ## 14. Porting the library (planned 2026-09-14) The owner: "I think a really good test of this would be to port some existing games to Forge projects." Then: "Ideally, we would be able to port EVERY game to Forge and they'd play identically to the current implementations." That is the goal of this section: every game in ~/claude/singetest (48 directories, 58 launcher entries) as a Forge description that plays the same, proved rather than eyeballed. ### 14.1 What "identically" means, and how it is proved A port and its original are run headless, on the deterministic clock, with the same scripted inputs -- pointer positions, trigger pulls, moves, coins -- at the same disc frames, and a trace of what matters is compared: the disc frame path (every search), the state or room each frame lands in, and the score, lives, hits, and misses. The original is driven by calling its own callbacks (onMouseMoved, onInputPressed) from a wrapper script that dofiles it; the port by authorSetPointer and authorSwitchDown. A port passes when the traces agree. Pixel-exact drawing is not the bar: positions come out of the same numbers, but a text measured by a different font path may sit a pixel off, and that is fine. The comparison scenes live in testScripts/ports/, one pair per family, and need the library beside the repo; they are not part of the reference sweep. ### 14.2 The families The library is not 48 different programs. It is four families and a few one-offs, and a family is ported by a converter that reads the original's own data tables and writes descriptions, so the port is mechanical and the vocabulary Forge lacks shows up as a list, once per family. 1. **ActionMax** (5 entries, one Emulator.singe, 558 lines): a light gun over VHS video. A shot is judged by comparing the brightness under the gun with a sensor spot on the picture, sampled across two frames. Forge lacked the sensor; it gains a `lightSensor` behaviour, a `pointer` behaviour (a sprite that follows the player's pointer, hidden when the engine says a real gun needs no crosshair), fonts on the text look, anchors on it, sprites drawn in copies (a row of bullets), discPlay and discPause actions, fire and miss clips on the sound behaviour, and pointerX and pointerY in expressions. Converter: util/forgePortActionMax.py reads each game's parameter file and the sprites' sizes and writes the five descriptions beside the originals. First, because it is the smallest complete game and every gap it finds is general. 2. **KarisFramework** (17 entries under KarisFramework/ plus the older per-game copies of the same author's "LUA SINGE 1.1" in the Italian Singe 2 games -- DragonTrainer, Conan, Daitarn3, SamuraiJack, PussInBoots, DragonsLairTvShow, SuperDonQuixote -- 25 or so in all): QTE laserdisc games. Data: Level[] tables (name, frames, death and resume frames), Death[] clips, Cfg/s*.cfg move lines (one encoded line per scene: frame, moves, mash and skip flags), menus at video frames, difficulty penalties, tiers and random order, map mode, trophies, hints, high scores, saves. Forge has the branching track, discTo, lives, credits, the results board, and submitScore; it will need the move line's extras (several moves in one window, mash, skip), difficulty as a var the windows read, an order of levels (sequence, tiers, random) as a track of rooms, and menus driven by video frames. Converter: util/forgePortKaris.py, reading the game's settings file and Cfg/ and writing one description; the framework's menu and service screens are Forge's own. The largest family and the largest payoff: one converter, twenty-five games. 3. **American Laser Games** (Mad Dog McCree 1 and 2, Crime Patrol 1 and 2, Space Pirates, Who Shot Johnny Rock, The Last Bounty Hunter; each in a Singe 2 and an HD edition, 14 entries; Platoon by the same hand): light gun over video with hitbox tables per scene (Script/hitbox-*.singe: boxes by frame), a scene graph of success and failure clips, service menus, high scores. Forge has hitbox tracks over video and the gun; it will need the scene graph as rooms with rules on frameReached, and whatever the service and manage scripts do that a player can see. Converter: util/forgePortALG.py, reading the hitbox files and the scene tables. 4. **One-offs**: Rollercoaster (a text adventure over the disc: the parser layer, rooms, discTo), Hologram Time Traveler (gun and branching, 6k lines), LINEA (MazescaterFramework), BatMaker (a tool that writes launchers, not a game: not ported). Each by hand, after the families, using what the families built. ### 14.3 Order of work ActionMax first (this section's first entry below), then Karis, then ALG, then the one-offs. Each family: read the original; list the vocabulary missing; add it to the manifest with a scene of its own; write the converter; run the comparison; fix what differs; note what was learned here. A family's converter is kept in util/ and re-run whenever the manifest changes, so the ports never drift from the editor. **The extension** (2026-09-14, the owner: "You're making a lot of Forge-related files with the extension '.game' that are just ASCII text. We're already using '.game' for our packed releases."): a description is a `.forge` file from here on -- the tutorials, the samples, the ports, what Forge saves, and the copy a release carries -- and `.game` is the packed release alone, so the chooser no longer sniffs a database header to tell them apart. Earlier sections say `.game` where they meant a description; read `.forge`. ### 14.4 ActionMax, ported (2026-09-14) Done: util/forgePortActionMax.py writes the five descriptions beside the originals in the library (38AmbushAlley.forge and the rest), and testScripts/ports/actionMaxOriginal.singe and actionMaxPort.singe run the emulator and the port on the inputs in actionMaxInputs.singe -- a pull on the title, a pull on the menu's left half, forty shots at three spots the picture flickers on and a few elsewhere -- and print the same trace: every state change with its disc frame, and at every input the state, the disc frame, and the shots, good hits, and bad hits. .38 Ambush Alley's traces agree line for line (one good hit among the forty), and Blue Thunder's. What the port taught, all of it general: * **The overlay size is part of "identically".** The emulator lays itself out on overlayGetWidth(), which is the engine's default over a disc -- half the video, 360x240 -- since a launcher entry's RESOLUTION keys size the window, not the overlay. Every number in the game (the sensor spot, the corner shots are not judged in) assumes 360x240; a port declares `size = { w = 360, h = 240 }`, and the original's wrapper must not set the overlay to anything else. The first comparison, at 720x480, found a "good hit" that was the sensor reading the wrong place in both. * **An event belongs to the room it happened in.** A press on the title that goes to the menu was also a press in the menu, in the same dispatch, and started a game; matches() now compares the rule's room with the room the event was raised in, and a frame's rules run for the room the frame began in (frameReached too). * **A built game finds files beside its description.** AUTHOR_DIR is the built script's directory, which is the data directory for a description compiled where it lives; the compiler now also writes AUTHOR_SOURCE_DIR and authorFile looks there second. * **Vocabulary:** lightSensor (the two-frame judgment, raising hit, miss, and fire; moved into the games' own Vocabulary.singe on 2026-09-15, see 14.7), pointer (a crosshair that follows the pointer, hidden when singeWantsCrosshairs says a real gun needs none), the text look's font, size, and anchor (centre, feet, top), the sprite look's copies (the var) and step, discPlay and discPause, fire and miss clips on sound, and pointerX and pointerY in expressions. Scene 96 (overlayBits.forge) covers the general ones from the repo alone. * **The wrapper for an original** sets what its launcher entry would have (SINGE_LEGACY_SPRITE_ARGS) and answers singeGetScriptPath with the original's own path while it loads, so MYDIR is right. * The two-frame sampling judges few shots: in forty aimed at flickering spots, one. That is the emulator's own behaviour and the port's. * **The editor opens a port**, and scrubs a frame file's video: the disc layer of an ActionMax game names frame_.txt, which lists videos by first frame; `frameFileVideo` picks the one with the longest run (the game's) and `FORGE.videoStart` puts the cursor's disc frame on the right picture. Not done: the editor's canvas stays 720x480 whatever the game's `size`, so a 360x240 game is drawn at its own coordinates in the top left quarter -- right, but small. * **Esc on a passed-over field wrote nothing back after all** -- it used to "put back" an equal value, which for a type's vars turned nothing into an empty table, and the shipped tutorial descriptions carried those tables. The sprite look's new `step` parameter moved which field the tutorial's Esc landed on, tutorial 3 stopped matching, and both ends were fixed: `forgeEditCancel` sets only what the typing changed, and the compiler leaves a type's empty vars table out, so a description with one and one without compile the same. ### 14.5 KarisFramework, ported (2026-09-15) Done: all seventeen entries (Time Gal, Dragon's Lair Enhanced, Dragon's Lair II Enhanced, Space Ace Enhanced, Asterix, Cliff Hanger, The Elder Scrolls, Oeil pour Oeil, Altered Carbon, Tron, Friday the 13th, Mononoke, Ninja Hayate HD, Titan A.E., Sucker Punch, Fire and Ice v1 and v2, Chantze's Stone and Triad Stone) play as their originals do under the same driver: testScripts/ports/karisDrive.singe presses START on the attract loop, answers every move three frames into its window -- the right answer, or a wrong one for every fifth move -- takes the one continue, and traces every disc seek and every change of screen, step, level, scene, move, score, and lives. All seventeen traces agree line for line, including the life-bar game type (Tron), the in-game difficulty screen (Space Ace, Tron), the level maps with a cursor (Sucker Punch, Friday the 13th), the random level orders (tiers in Time Gal, Dragon's Lair's shuffled last levels), the mirrored scenes, and each game's own scoring add-ons. How: * **The converter runs the game's own script.** util/forgePortKaris.lua loads a game's settings script with the engine stubbed out, so every table it declares (Level, Death, Tiers, PlayOrder, the offsets, the scores, the dips from Cfg/game.cfg or default.cfg, the high score board) is read as data, and setupMoves is called for every level and scene at every difficulty for a snapshot the editor can show. The description (`.forge`, beside the game) carries the snapshot under `qte` and names the script and Script/addons.singe. * **The runtime is the framework's loop, with the framework's names.** The `qte` behaviour in Author.singe keeps its state in one table under the framework's own variable names (iScore, iLives, currentMove, move[], stage[], SCOREMOVE, offsetGetReady, dip_Difficulty...) and plays the framework's states with the framework's numbers (levelNormal = 103, branch02 = 11...). A shim environment whose globals are that table runs the game's script and add-ons verbatim, so Space Ace's setupMoves reading which levels are beaten, Dragon's Lair's startConf shuffling the last levels and its per-move specialScore, Sucker Punch's map, and Space Ace's random game-over and death clips all behave as written. The framework's own functions the add-ons call (timerON, setupClip, soundPlay, spriteDraw, joyDelayDue...) are shim helpers. * **The random stream is shared.** The loop calls math.random exactly where the framework does (the level order, each scene's mirror, a random death, a choice's shuffle on its first drawing) and seeds from KARIS_SEED when a comparison sets it, so both runs draw the same numbers. * **A game hears the switches.** An event about nothing in particular (a press, a release) now reaches behaviours that set instance.hearsAll, which the loop does to read the framework's p1 flags. * **A built script must not be the game's own script.** The loop loads the game's files from beside the description first: building Timegal.forge to Timegal.singe in the same place loaded the built game inside itself and overflowed the C stack. * **The framework never calls discPlay.** A seek resumes a paused disc, and a discPlay a frame before the seek moved two games' traces by one frame. * **The frame clock and the CPU clock.** The framework's timers are os.clock; the port's are the game clock. The traces compare seeks and states, not times, and the driver acts on disc frames, so the difference never shows -- except that a screen left "after 120 seconds" costs two real minutes in the original and two virtual ones in the port. Not done: the high score board's name entry (the port plays its clips and enters nothing), the service menu, saves and loads, two-player mode, and the drawing -- the port draws no arrows, LCD text, or figures yet; it exposes score, lives, credits, level, and prompt as vars. The Italian Singe 2 games (DragonTrainer, Conan, Daitarn3, SamuraiJack, PussInBoots, DragonsLairTvShow, SuperDonQuixote) are the same author's earlier framework with per-game copies and a different level structure (createLevelNN, segments); they are the next family. ### 14.6 What the rest of the library is, and a decision to make (2026-09-15) The two families ported so far were data-driven: a script full of tables, one shared loop. The rest is not: * **The Italian Singe 2 QTE games** (Dragon Trainer, Puss in Boots, Samurai Jack, Daitarn 3, Conan, Dragon's Lair TV Show, Super Don Quixote) each carry a *copy* of the older KarisFramework (LUA SINGE 1.1) beside their data, and the copies are edited per game: Daitarn's by 29 lines, Conan's by 560, the TV Show's by 1,787, Super Don Quixote's by 7,907 -- the framework is part of the game. The loop is a simpler ancestor of the one now in Author.singe (segments, per-level offsets, a 2,000-line setupDeathClip that maps each death to its offsets by hand). * **The American Laser Games titles** (Mad Dog McCree I and II, Crime Patrol I and II, Space Pirates, Who Shot Johnny Rock, The Last Bounty Hunter, each in two editions, and Platoon) share a skeleton (move tables with hitbox ranges, hitmap arrays by frame, bullets and reloads, an undertaker) but every scene is a hand-written function -- doLevelSaloon, doLevelBank, twenty to forty per game -- with its own branches, items, and clips. There is no table to convert; the scene logic is the game. * **The one-offs** (Rollercoaster, Hologram Time Traveler, LINEA) are bespoke scripts. Two ways to make these Forge games that play identically: 1. **Reimplement each loop and rewrite each scene** as Forge rules and tracks. Faithful only if every quirk of every hand-written scene is carried over by hand; for the ALG family that is some two hundred scene functions, each to be compared move for move. Months. 2. **Host the game's own Lua** inside a Forge behaviour, as the qte behaviour already hosts a Karis game's script and add-ons: the description carries what Forge understands (the disc layer, the gun, hitbox tracks converted from the hitmap arrays so the editor can show and edit them, the release path), and a `hosted` behaviour runs the game's scripts in a shim with the engine's callbacks routed through it. Identical by construction; the port's value is Forge's structure around the game, not a rewrite of it. Days. The owner should say which of these counts as a port. Until then the order stands as written: the Italian family next (its base loop is small enough to reimplement, and its per-game copies could be hosted for the differences), then ALG. ### 14.7 A game's own vocabulary (2026-09-15) The owner asked whether a game's author can extend the kinds, looks, and behaviours without putting one game's hand-written piece into every game's Forge; the light sensor was the case in point. Yes, now: * A description may say `vocabulary = "Vocabulary.singe"`, a file of Lua beside it that adds entries to `AUTHOR` exactly as Author.singe does (a `bob` behaviour is `AUTHOR.behaviours.bob = { params, attach, step }`; a condition or action gives `emit`). The file may define any globals its entries emit or call. * `authorVocabulary(name, folder)` in Author.singe loads it -- beside the description first, then beside the built script, through the shared `authorBeside(name)` that the qte loop's script loading now uses too -- and records what it added; `authorVocabularyForget()` takes that out again. `authorKeyValue` and `authorSwitchValue` are public now so a vocabulary's behaviour can resolve a key or switch name. * The compiler loads it before `authorCheck` (so the checker knows the words) and emits `authorVocabulary("...")` after `AUTHOR_SOURCE_DIR` in the built script, so the game loads it wherever it runs. * The editor loads it in `forgeBegin` (forgetting the last game's), forgets in `forgeClose`, has a `vocabulary` field (kind file, `.singe` listed) on the game's form that reloads as it is set, and `forgeFilesNamed` takes the file so a release carries it. * The light sensor and its pixel reader left Author.singe for `~/claude/singetest/ActionMax/Vocabulary.singe`; its trigger is a `pressed` event heard through `hearsAll` and read in `on`, so the switch handler in the core no longer knows about sensors. The converter emits the `vocabulary` line; all five ActionMax compares still agree. * Scene 97 (`testScripts/author/vocab.forge` + `vocab.singe`) covers it from the repo alone: the editor knows `bob`, `bobbing`, and `setBob` while the game is open and not after, the built script names the vocabulary, and the game plays by it. The book has "Your own vocabulary" under The Vocabulary. ### 14.8 The ports in a project folder of their own (2026-09-15) The owner asked for the ported games in their own folders under a project folder parallel to the library: `~/claude/singePorts/`. `util/forgePorts.py` assembles it from the library and builds it, and is the thing to rerun when a converter, a description, or Forge changes: * One folder per game, named as the library names it (the two-game folders ChantzesStone and FireAndIce keep both games); the five ActionMax games, which share one folder in the library, get one each with their own video, frame file, and cabinet art plus the family's sprites, sounds, fonts, and Vocabulary.singe. * A KarisFramework game's folder travels as it is, less the framework copy and the original games.dat; the game's own script comes because the qte behaviour plays it. The ActionMax emulator and original scripts do not. * Video is hard-linked, not copied (the library's videos are never edited and copying them is some forty gigabytes); everything else is a copy. * Each folder gets the compiled port as `.port.singe` (never the game's own name -- the built script would load the game inside itself) beside a copy of Author.singe, compiled by the engine running AuthorCompile from a temporary directory with Forge beside Singe, and a games.dat made from the original's entry: the port as SCRIPT, the folder as DATA, LEGACY_SPRITE_ARGS off (the port speaks the engine's own API through the shim). * The README in the folder says all this in the owner's terms. Every port launched from its games.dat headless for twenty-five seconds without an error (`testScripts/ports/launch.sh`: Xvfb at 1920x1200, since SDL's offscreen desktop is smaller than the Karis games ask for, and the deterministic clock). The originals in the library are untouched; the descriptions beside them are what the tool copies, so the converters still write there. ### 14.9 The Hypseus library (2026-09-15) The owner asked about the Hypseus games, with the end in view: port as many as possible so the originals can eventually go. `~/claude/singetest/hypseus/` holds 60 title folders (169 GB, the upstream zip-ROM layout: `/singe/<Title>/` for the scripts, a framefile and `Video/` beside). Fifty-six have scripts; four are video-only alternates (two Hayates, two Time Gals). They are seven families: 1. **Karis Framework 3.32b** (11): Arcade Xperience 1, 2, and 3, Astroboy, Danmachi, Freddy, Starship Troopers, Sugar Rush, Survival, Esh's Aurunmilla, Badlands Lite. The library's copy is 3.31c with the owner's patches; 3.32b adds a language dip (an audio suffix), a hold-to-loop dip the loop already knew, a fourth-button secret combination, and a free-play guard on the two-player start. **Ported today:** the converter finds the framework where a game keeps it (`KarisFramework/Script`, `Structure`, or `../Framework`) and writes its version into the description; the loop reads the version for the secret combination and maps the fourth button; `compare.sh hypseus <Title>` stages a run directory with the title's `singe/` beside it (the scripts hardcode `BASEDIR = "singe"`); `forgePorts.py` flattens a title into `singePorts/<Title>/` with its framefile and video and writes the games.dat it never had. The comparison found an engine bug rather than a port bug: Freddy, Starship Troopers, and Sugar Rush died, original and port alike, at their map screen's `musicStop(sndHandle)`, because Singe's `musicPlay` answered nothing where Hypseus's answers the handle; it does now, and `musicStop` takes a nil handle as none. The earlier Hypseus sweep ran forty seconds a game and never reached the map. With the fix, all eleven traces agree with their originals, and every one of the thirty-five ports in singePorts launches from its games.dat. 2. **RDG's LUA SINGE 1.0 and 1.1, the map-mode lineage** (4 here, 7 in the library): Freedom Fighter, Starblazers, Jeeg, HQ Time Gal here, and the Italian Singe 2 games there (Dragon Trainer, Puss in Boots, Samurai Jack, Daitarn 3, Conan, Dragon's Lair TV Show, Super Don Quixote). One ancestor loop (`main.singe` of some 4,000 to 6,500 lines with `map.singe`), copied per game and edited a little in most and a lot in some. Future Boy and Road Blaster carry later Karis versions (3.31c with a map, and 2.2) in `Script/`, closer to this lineage than to the 3.32b loop. 3. **Kimmy Script Engine 4.0** (7): Brain Dead 13, Future Boy Conan (Kimmy), Mazinga Z, Fuma Conspiracy, Sonic 1996, Sintel, Hayate 1080. Karis's 2024 successor to the framework: a 5,900-line `main.singe` and a 4,900-line `moves.singe` (combos, skins, an LED panel), three versions across the set. Data-driven like the framework it grew from. 4. **American Laser Games, two-player HD editions** (11): Crime Patrol, Drug Wars, Last Bounty Hunter, Mad Dog I and II, Space Pirates, Who Shot Johnny Rock (twice), Johnny Rock Noir (twice), Mad Dog II Typing Edition. The same skeleton as the library's ALG family, with the hitbox files reworked for two players. 5. **Singe 1 monoliths** (7): Time Gal, Ninja Hayate (twice), Esh's Aurunmilla (the SBC port), Scrat -- each a 12,000-to-16,000-line `main.singe` of its own, no two alike -- and Cracke and Triad, an Italian 476-line framework plus a game. 6. **Widge's and Luthergond's games** (11): Cops, Fast Draw Showdown, Gallagher's Gallery, Marbella Vice, Platoon, Tierras Salvajes (the "multiplayer editions": a 35-line launcher and a `.bin.luac`, which `singetest/tools/decompile` turned back into source), Captain Power (four sub-games), Minesweeper, Rick's Revenge, Space Rocks, and SEGA Video Driver (seven sub-games on one engine). Not laserdisc QTEs: overlay games that happen to use the disc, each its own program. 7. **Data only** (3): Esh's extras, Road Blaster's screens, the Video Driver batch files. What this means for the end goal. Family 1 is done by the existing loop. Families 2 and 3 are the framework kind: a converter reading each game's tables and one more loop per lineage in Author.singe, as the qte loop was built -- days each, and they cover eleven and seven games. Families 4, 5, and 6 are hand-written programs (twenty-nine here, and the library's ALG and one-offs make it forty or so): no table to convert, so the choice in 14.6 decides them. Hosting the game's own Lua inside a Forge behaviour still lets the original folder go -- the script travels with the port, as the Karis games' scripts already do -- and it is the only route that is not months. The owner's answer was the goal "complete the Hypseus ports, starting with the Kimmy Script ones", and 14.10 is how: on a closer look the Kimmy engine went the hosted way too, because its three versions across the seven games are twenty thousand lines each and differ from one another, and a transcription of one would play the other six wrong. ### 14.10 Hosting, built and withdrawn (2026-09-15 to 2026-09-16) A `hosted` behaviour was built that ran a game's own Lua whole, framework and all, inside a shim, and fifty Hypseus titles were "ported" with it and shown to play as their originals. The owner's verdict on seeing what it was: "a game that we can't really edit that carries the burden of another framework." Right: a wrapper, not a port -- nothing in the description to edit, the framework and scripts still in every folder. It was withdrawn the next day: the behaviour, its draw hook and fan-outs, the disc layer's `paused` flag, the converter, the drive, scene 98, the book section, and the fifty descriptions are gone, and those originals are back in singetest/hypseus. What stays from the episode: `authorFan` (one fan-out for the instance-less events), the Karis converter's framework search and version, `musicPlay` answering its handle, and the Singe framework lines added to the Captain Power and Video Driver scripts and Sintel's framework copy (library fixes, not port artefacts). The direction now, the owner's choice: drop hosting; port the two framework families properly (a converter reading each game's tables and a loop of Forge's own per lineage, as the Karis family was done); then rewrite the hand-written games too, one by one, in Forge's terms, each with a comparison harness of its own. ### 14.11 The Kimmy Script Engine, properly (2026-09-16) **What it is.** Karis's 2024 successor to the framework the qte loop plays: the same state machine (gameflow, lvlState with the same branch numbers, the same level screens, Level[] and Death[] tables, setupMoves per scene, Cfg/game.cfg dips, a Script/addons.singe), with a new move system in a file of its own, moves.singe. A move is judged by a seven-bit mask of what is held (acombo, from the input handlers) against the mask the move asks for (gcombo, from fillMove), which gives combos (two inputs at once), diagonals, and a second accepted answer; mash has up/down, left/right, and button-pair variants with counters scaled by difficulty and window length; hold wants the combo held for a computed length; multi and loop are sequences (loop draws a wheel sprite chosen by the sequence's shape); path branches by direction with jumps to a later move (iPathAend/iPathAjmp) or a death (iPath > 1000); yes/no and timed branch too; way, wayout, toscene, and tolevel are jumps without an input; anything and nothing are what they say. Around that: tilt (a count of presses outside a window that kills at a threshold, with a warning), a life bar for game type 1, rewind modes 0/2/3 on death, a "next" hint of the coming move, hints on death, 2P alternation, a level select screen (dip_PlayStyle 4), difficulty 0 simplifications and MashtoRun/HoldtoLoop rewrites at setup (setupFramesMoves), and mirroring (bFlip) of every move kind. Seven games; their frameworks are three versions (BD13's; FBC's; mazinga/Fuma/Sonic's) that differ by ten lines of main and eighty-four of service from one another, plus hayate_1080's Structure copy (a later 4.0 with an LED panel and a Menu folder, 358 lines of main and 577 of service apart), plus FBC's and Fuma's own edited copies of main/service/toolbox in their Script folders (6/49/1 and 39/20/0 lines). The differences are catalogued below as they are read. **The design.** A `kimmy` behaviour beside `qte` in Author.singe, sharing everything of the Karis loop that Kimmy left alone (the Q helpers: timers, sounds, the shim, the script and add-on loading, the scoring and beat-game tests, the mixes and orderings, the continue, game over, and difficulty screens, the high-score board) and carrying its own K functions for what Kimmy changed: K.doLevel, K.doMove and the checks, K.fillMove, K.setupFramesMoves and K.getShortcuts, K.resultMove and K.resetVar, K.startGame, K.doIntro, K.doFinish, K.levelReplay, K.nextLevel, the level select and save screens, the input handlers with the combo masks, and the drawing (in Forge's terms: text and boxes, as the qte loop draws, not the framework's sprites). The converter grows a Kimmy mode: it finds the framework at ../FrameworkKimmy or Structure, stubs the Hypseus calls the framework makes at load, snapshots the same tables plus the path/timed/multi ones, and writes `kimmy = { ... framework = "4.0" ... }`. Per-version and per-game differences that a player can see go under version flags in the loop where they are a line or two, and into the game's own vocabulary file where they are a game's own thing (FBC's and Fuma's edits, hayate_1080's LED panel). The comparison drive grows a Kimmy policy that answers every move kind: hold for the computed length, mash at the counter's rate, the sequence for multi and loop, a direction for path, the button for yes/no, the timed input in its window. **Order.** Brain Dead 13 first (the reference framework, the fullest game), then mazinga, Fuma, Sonic (the second version), FBC (the third, with its own edits), Sintel, then hayate_1080. **Built (2026-09-16).** The `kimmy` behaviour is in Author.singe after the qte loop: the KIMMY constants, the masks, the tests (K.checkBasic, checkCombo, checkHold, checkLet, checkMash and its up/down, left/right, and button-pair variants, checkMulti, checkAny, checkNo, checkSkip), K.doMove with the path, yes/no, timed, and skip branches, K.getShortcuts (the shorthand kinds and the loop, multi, path, and yes/no texts read into tables), K.setupFramesMoves (game type 5, difficulty 0, MashtoRun, HoldtoLoop, mirroring, the play frames), K.setupFrames and setupLevel, K.addPoints (nothing for die-and-retry), K.newScore (deaths against the difficulty's record), K.setupDeathClip (no life lost in die-and-retry, the 2P bookkeeping), K.nextLevel (MazeGame keeps the level), K.levelReplay and onToNextLevel (play style 4 goes to the level select), K.doLevel with K.failed, afterDeath, sceneDone, enterScene, and resumePlay, and the screens: K.startGame, doIntro (fifteen-second stills, the alternate rankings still, START to the new-game menu in free play), doNG and updateNG over the description's snapshot of the save slots (K.loadSave, startSave, startMap), doLvlSelect and moveFrameLevel, doClear, doFinish, doContinue, doGameOver, doHighScore, doDiffSelect, and doExit. What is not played: the movie player (game type 6), the two-player screens (START2), the service and save menus, drawing. Q.doChoose is shared (it takes the loop's own addPoints). The converter finds FrameworkKimmy (and a Fuma-style Script/globals.singe), marks the description's behaviour `kimmy`, and snapshots the die-and-retry records and the six save slots; the drive plans a window's presses by kind (a tap, a pair, a hold to the window's end, a hold let go for LETGO, taps every other frame for a mash, the sequence for multi and loop, a path's first direction, the button for yes/no, the timed input in its window) and answers the new-game and level-select menus; `compare.sh` puts a game's Cfg folder back after the runs, since the originals autosave into it. Brain Dead 13's new-game menu runs under a level number the framework never defined (`levelNG` is nil in Kimmy's globals), which the port reproduces in its trace. **Native mash and hold (2026-09-16).** The owner's aside: "People love Mash for some reason. Should be native to moves available in Forge." So they are: a branch on Forge's own `branches` track may say `mash = 6` (the move that many times before the window closes) or `hold = 20` (the move held that many frames of the window), and `branchOpen` carries `event.mash` and `event.hold`. Scene 98 (`testScripts/author/mash.forge`) covers both. The Karis and Kimmy loops keep their own mash arithmetic, since a port must match its original's; a game written fresh in Forge gets the native kinds. **The Hayate 1080 variant (2026-09-16).** Its Structure copy of the engine (an LED panel, an arcade mode, a menu folder) differs from Brain Dead 13's in things a player can see, gated in the loop by `q.hayate` (its settings declare ArcadeMode): START sets the start level and scene to one in arcade mode; a level's first scene forces rewind mode 0; the "nothing" move with a second answer scores nothing; the coin prompt sound is not played from the level (that copy plays a prompt sound from its drawing instead, under its show-action setting); a level's end always clears the scene's death count; a continue restarts the stages at scene 0, move 1; the extra life comes regardless of a second player; credits count to 99. **Results (2026-09-16).** Brain Dead 13 plays as its original for the full sixty thousand frames of the comparison (1,662 trace lines: the new-game menu, the intro clips, play across the maze's levels in die-and-retry mode). Mazinga Z and Sonic 1996 agree at six thousand frames; Future Boy Conan (FBC) and Ninja Hayate 1080 agree through a whole game -- lives spent, the continue taken, back to the attract loop -- which is the drive's own end. Fuma Conspiracy's first run sat on its attract loop: it is not free play, and the drive had only ever pressed START (the Karis 3.32b titles were all free play); the drive now inserts a coin first when the game wants one. Sintel's framefile is beside its script rather than at the top of its folder, which the harness now allows; and its script was the one Kimmy game that never loaded Singe/Framework.singe, so its original died on a Hypseus call -- it has the two lines the other games got from an earlier library fix. With those, Fuma Conspiracy and Sintel agree through a whole game too. Mazinga Z and Sonic 1996 then agreed through a whole game as well. All seven Kimmy titles play as their originals do; they moved to singetest/ported/hypseus, and singePorts carries them: 42 ports in 40 folders now (24 from the library, 11 Karis 3.32b, 7 Kimmy). The library Karis family and the 3.32b family were regressed over the shared changes (the choose screen and the drive) and still agree. ### 14.12 The RDG map-mode lineage (surveyed 2026-09-16) **What it is.** RDG's "LUA SINGE 1.0/1.1", the ancestor of the Karis framework: the same gameflow and lvlState machine (lvlSetup, lvlRunning, lvlEnd, lvlPlayDeath, lvlPlayRest, branch01 to branch10), the same screens (intro, continue, game over, high score, service, movie, finish, save), and a doLevel of some seven hundred lines. A level is a stage of segments: `createLevelNN` fills `stage[]` and `segment[]` (a segment's id, done flag, and title), `setupLevelNN(segment)` sets the segment's frames and its `move[]` table (a move is `{ start, end, kind, death, 0, 0 }`, the kinds the eight directions and buttons, MASH, the four ACT combos, and SKIP), `SetupFramesLevel` adds the level's disc offset and a difficulty shift, `NextLevel` walks a fixed order or goes to the map, and `map.singe` is the level select over a still with a cursor. A game is its own copy of all of it: the level functions live in main.singe, not in a game script. The copies drift: freedomfighter is starblazers with 149 lines changed (a mouse-driven variant); jeeg 1,106; hq-timegal is another structure again (hq-*.singe); in the library, Dragon Trainer, Puss in Boots, and Samurai Jack share one copy exactly, Daitarn 3 is 28 lines off it, Conan 315, the TV Show 881, Super Don Quixote 4,425. Starblazers adds a mouse-picked level jump (choiceMode). **The design.** The same shape as the Karis and Kimmy ports, with one more thing run in the shim. Where a Karis game's script declares tables and a setupMoves, a map-mode game's main.singe declares its data as functions: createLevelNN (the segments), setupLevelNN(segment) (the moves, some chosen by a random draw), SetupFramesLevel (the level's disc offset and the difficulty shift), getIntroClip (the intro clip per level), setupDeathClip (the death number to its clip, two thousand lines of it in the Italian copies), NextLevel (the fixed order and a game's own jumps), initStages, and the map's setCursor and moveCursor (the layout). Those run in the shim as they are, over the loop's state, as the Karis games' setupMoves and add-ons do -- the loop calls them where the framework calls them -- and the loop itself (doLevel, the judging, the screens, the input) is Forge's `rdg` behaviour, a transcription of the ancestor's, with the copies' differences gated by what the game's script declares. The converter loads the game's files with the engine stubbed, records the settings and the dips, and names the script and its files; the moves are not snapshotted, since a random draw picks some of them at play time and the game's own functions must make that draw. The random stream is the game's: the scripts seed it from os.clock at load, so both drivers pin os.clock to the frame count (the hosted drive did), and the loop makes the same draws in the same order. The eleven games go in order of drift: starblazers, freedomfighter (a light-gun variant with checkZone and getPixelState), jeeg, hq-timegal (a different structure again, to be read); then the library's Dragon Trainer, Puss in Boots, and Samurai Jack (one copy), Daitarn 3, Conan, Dragon's Lair TV Show; Super Don Quixote is a hybrid of this lineage and Karis 3.x (checkHold, checkLet, doClear, doDiffSelect, getRes) and comes last, gated or with a loop of its own. **Built (2026-09-16).** The `rdg` behaviour is in Author.singe after the kimmy loop: R.doLevel (the ancestor's fourteen states), R.doIntro, startGame, doContinue, doGameOver, doFinish, doHighScore, startMovie, checkMash, checkSkip, scanInput (which lets every input go, as the ancestor's does), and the pair test for the button-with-direction and two-direction kinds. The shim gained two switches the behaviour sets: `loadsFiles` makes its dofile load the game's own files beside the description (globals, main, map, hscore, service, toolbox), and `namedSounds` makes its soundLoad answer the file's name, so the game's `sndright = soundLoad(MYDIR .. "right.wav")` becomes a handle the loop plays by name. The converter's rdg mode (a globals.singe beside the script and no framework folder) reads the dips and the board from the game's game.cfg and snapshots no levels, since the game's own functions make them at play. Library fixes on the way: the three Hypseus titles play a prompt sound they never load, which Hypseus let pass and Singe reports, so their scripts let a nil handle pass; the flat titles reach their scripts through a symlink the harness now follows. starblazers, freedomfighter, and jeeg have descriptions; the first comparison is running. On the way: a map-mode script sets `MYDIR = "singe/starblazers/"` and joins file names straight onto it, and the shim had been answering MYDIR with Forge's own directory without a slash; it keeps the game's value apart now and answers with the slash the game's ended in. hq-timegal is a variant of its own (a chooseLevel with tiers, a time-stop, setupLevel by difficulty arrays, no segments or map), to be read once the three others agree. starblazers' port then played as its original for six thousand frames on the first run that reached play (after the move table's absence at attach and the unnumbered finish screen, which the lineage never defined either, were seen to), and then for the full run. jeeg followed once the converter read the right file: a title's dips and board live in the file its service.singe opens (game.cfg, or the game's own name, jeeg.cfg), which the converter now takes from that line; and a title without a rewind dip (jeeg has none) rewinds as the ancestor's zero. (The Italian scripts carry Latin-1 bytes, which ugrep takes for binary and skips silently: grep them with -a.) freedomfighter is the light-gun variant: its original drew the crosshair at coordinates the mouse had never set, which Hypseus let pass and Singe reports, so the library copy starts them at zero; a down action whose row carries a zone (columns 7 to 10) is a shot, right when the button is down with the pointer inside the zone, silent, and the disc skips to the window's end; the loop hears the pointer as the ancestor's onMouseMoved did (Author fans "pointer" to the loops that hear everything, and the rdg loop keeps mouseX and mouseY by the game's ratios). What a title does differently without declaring it -- freedomfighter's right move sounds sndshot and its command screen holds fifteen seconds -- the converter names in a per-title `tweaks` table the description carries and the loop reads; the harness views hand the drive the zone and the ratios, and it puts the pointer in the zone (or well outside it for a wrong one) the frame before the button. All three then agree for six thousand frames and for the full runs, and moved to ported/hypseus (forgePorts assembles a flat title from its own folder, less what Hypseus put there). **The later copies (2026-09-16).** Dragon Trainer's generation (Puss in Boots and Samurai Jack share its main.singe exactly, Daitarn 3 is 29 lines off, Conan 560, the TV Show 1,787) declares a level order (LvlOrder), and the loop takes that as the mark of the generation (`q.later`): a game type dip orders the levels in sequence, at random, or by tiers (the game's own doMixSEQ, doMixRND, doMixTIE, run in the shim), from the map, or from the dips' level and segment; clip ends are tested "at or past"; the states after a win, a death, and the finish are numbered differently (the extended play waits in branch07, an extra death clip ends in branch08, a "get up" clip in branch09, the finish counts from branch01); a rewind resumes inline; the intro holds its command screen twelve seconds and its rankings eight, shows a filler after them, has no quit combination, and ends its secret in RIGHT; a CHOOSE move (only the TV Show has any) is a choice among three, R.doChoose; the offsets are copied under short names every title-clip frame (saveOffset); a difficulty penalty (iPenal) shifts the windows; and the finish screen is the game's own 900, which the loop uses when the game names one. The shim answers MYDIR with the game's own value when the game took it from the script's path (the files sit in Script/ under the game's folder), and the converter finds the dips in Cfg/ beside Script/, through the cfgReadPath its service.singe uses; forgePorts takes such a game's whole folder, and compare.sh puts its Cfg/ back from there. Dragon Trainer then agreed for six thousand frames and the full run, Puss in Boots, Conan, and the TV Show for six thousand; on the way, the port makes the game's save and config functions do nothing (setupLevel autosaves, through lfs and io the shim now stubs), and gives the map screen (the game's own doLevelSelect, which Samurai Jack and Daitarn 3 start from by their game type) the sprite table its drawing indexes. **The drive's coverage (2026-09-16, the owner asked).** The drive had answered every move right but every fifth, which it answered wrong (a stray input; a wrong pair; a shot outside the zone), so the wrong-move death, the life lost, the rewind or the segment restart, the continue, the game over, and the board were walked, but not a missed window, a wrong choice, or a broken mash. Now every seventh move (not a fifth) is left unanswered, a choice or time-stop on a fifth move takes a wrong option, a mash on a fifth gets a stray direction, and a move met again after a death is answered right (the later copies rewind to it, and a retry answered wrong again had been ending games at five deaths). Every port gets a sweep with the wider drive: testScripts/ports/sweep.sh runs every pair, two at a time. The first sweep (2026-09-16 evening, six thousand frames): 51 of 52 pairs agree -- the ActionMax five, the nineteen KarisFramework descriptions, the six map-mode games, and the twenty-two Hypseus titles -- and the one that did not (Daitarn 3) produced no port trace at all: compare.sh now refreshes Forge into its run directory at every start, and two pairs sharing one directory had one pull Forge from under the other. sweep.sh gives each pair a directory of its own; Daitarn 3, rerun, agrees, as does hq-timegal's full-length run (a game over ends it after 6,336 frames). singePorts is reassembled with the seven new titles (52 ports), launched, and the release repacked. **hq-timegal (2026-09-16).** The `timegal` behaviour follows the rdg one in Author.singe: T.doLevel (checkpoints, the tally, the resurrect and death clips by frame number, as the game hardcodes them), T.doTimeStop (three options, shuffled on their first drawing as drawOptions does), T.doIntro (the two intro clips by dip, the filler, the credits clip, the rankings still, the half-minute "press start" on a coin), T.startGame (the three level orders through the game's own doMixLevels functions), T.addPoints (the 1-up quota), the continue, the game over, and the board with the name unentered; T.setupClip is the game's toolbox one (no skip when the disc is a frame short of the clip). chooseLevel, setupLevel (with its mirror draw), fetchIntroVideo, doFillerFrame, and the board functions are the game's own. The converter finds hq-globals.singe before the Singe 1 globals.singe the title keeps beside it and reads the dips and board from hq-timegal.cfg through hq-service.singe; the views read Time Gal's row (clip start, window, clip end, death clip, kind) and its options at branch03. Its first comparison is running. It agreed for six thousand frames on the first run that reached play (levels, deaths, a time-stop, the second level) and moved to ported/hypseus with the six Italian titles (Samurai Jack and Daitarn 3 agreed once the map screen had its sprite table); their launchers went with them. **Super Don Quixote, read (2026-09-16).** The one hybrid: map-mode data (createLevelNN, setupLevelNN, SetupFramesLevel, getIntroClip, setupDeathClip, NextLevel, doLevelSelect, stage and scene tables, iCurPos and iSegPointer, a level order by game type) under a Karis 3.31c level loop (branch11 the right state, branch02 a wrong one with the hints dip's frame in branch03, HOLD through checkHold with a length by difficulty, LETGO, the three mashes with unMash and a counter by difficulty, DOUBLE, the timer-tested pairs, PATH and YESNO with the path table's jumps (iPath, iPathAend, iPathAjmp) and deaths over 1000, SKIP, a rewind that skips a LETGO), with its own additions: a difficulty select screen (doDiffSelect), a clear screen (doClear), trophies (trophy.singe, doTrophy), a display dip (dip_Res, drawing only), a practice game type (4: a wrong move costs nothing), a "get ready" clip in branch08 and an extra death clip in branch07, thirty-one levels, and addons.singe. Its doLevel is some 680 normalised lines off every Karis copy and the map-mode ones alike, so it is a loop of its own: the plan is the qte loop's judging (Q's 3.31c paths) over the rdg loop's data loading and screens, in a `sdq`-gated section, after the Hypseus goal is closed (release, docs, memory) -- it is a library title, not a Hypseus one. **Super Don Quixote, built (2026-09-16).** Its 311 move rows use only the five plain kinds, so the exotic judging is transcribed but unreached. The `sdq` behaviour follows the timegal one in Author.singe: S.doLevel (the game's states, including branch11 the right move, branch02 the wrong one, the hints frame, the practice game type, the two "get ready" clips, the rewind that skips a LETGO, the path jumps), S.doChoose, S.startGame (with the difficulty select), S.doIntro (eight stops), S.doDiffSelect and S.moveFrameDiff, S.doFinish then S.doClear every frame of the level's end as the ancestor runs them over one state, S.doContinue, and the judging helpers under the game's names; its attach is the rdg one plus the game's own state, the extra clips by the Extravid dip's table of eight, hints a boolean, the action and movie dips numbers (the ancestor compares the movie dip with booleans, so neither start holds). The converter marks the loop `sdq` when a map-mode globals names the Karis kinds (HOLDUP), reads the two next-level tables out of doLevel's text as data (`sdq.beaten`, `sdq.unfinished`), and carries the per-level percent records after the board (`percents`), reading the config the service opens through cfgReadPath before any MYDIR fallback; R.doHighScore ends without the two-second hold for it. The port agrees with the original on its first comparison (82 trace lines: play, wrong moves, deaths, rewinds). The last library title; moved to ported/ with its launcher, singePorts reassembled (53 ports). **hq-timegal, read (2026-09-16).** RDG2010's "Time Gal (Singe Edition)", the HD remake of the Singe 1 Time Gal: its own compact loop, not the map-mode one. A move is `{ inputStart, inputEnd, a second pair, deathStart, deathEnd, kind }` with the kinds UP, DOWN, LEFT, RIGHT, ACTION, and TIMESTOP; a level's moves come from arrayEasy, arrayNormal, or arrayHard by difficulty, mirrored by a random draw (an offset added to every frame, LEFT and RIGHT swapped); play has checkpoints (sections reached, resumed from after a death), a time-stop choice of three (opt[] with a right one and a death clip per wrong one, shuffled), a tally with a bonus by deaths, a 1-up quota, and a level order by tiers (doMixLevels), at random, or sequential, with chooseLevel picking the next unbeaten; the intro alternates two clips and a still per filler. About seventeen hundred lines with the drawing. A fourth behaviour, `timegal`, after the three map-mode titles agree; the Singe 1 `timegal` monolith in the library is its ancestor and may share it. ### 14.13 The American Laser Games family (surveyed 2026-09-16) **What it is.** Eleven Hypseus titles (Mad Dog McCree, Mad Dog II, Crime Patrol, Drug Wars, Last Bounty Hunter, Space Pirates, Who Shot Johnny Rock twice, Johnny Rock Noir twice, Mad Dog II Typing) and twelve of the library's (six originals, six HD), each a hand-written program: 15 to 27 `doLevelXxx` functions of a few hundred lines each, 36,000 to 82,000 lines a title with the hitbox files. Nothing is shared but the shape: the "skeleton" (doIntro, doContinue, doGameOver, the input handlers) differs completely between Mad Dog and Crime Patrol, and the library's HD Mad Dog is 5,000 normalised lines off the Hypseus one. A level's data is mechanical: `move` rows `{ shootStart, shootEnd, clipStart, clipEnd, hitmapStart, endFrmRandom }`, a `hitmap` per frame `{ frame, hitboxIndex, count, bonusIndex, civilianIndex, civilianCount }` over `hitbox`, `civilian`, and `powerup` rectangle arrays (`GetBankArray` and its kin, 12,000-line files), scaled by `ResHMx`/`ResHMy` to the overlay. A level's flow is code: states (lvlSetup, lvlRunning, lvlPlayRest, lvlPauseAction, lvl2ndChance, lvlPlayDeath, lvlUndertaker, lvlPlayAdvice, ...) with hardcoded frame triggers (`currentFrame == offsetlvlBank + 530`), random approaches, "first time" resumes, bullseye windows, a second-chance delay, and the undertaker's death clips by gender and lives. Two players share the loop: `p1BUTTON3`/`p2BUTTON3` the triggers, two pointers, two ammo counts (six, a reload by pointing offscreen or at the borders by dip, or immediate), two scores, one lives count; a town select by pointer quadrant; continues; a name entry. **The design.** The owner's choice (14.10) is a rewrite per game in Forge's terms, and Forge's rail-shooter model (section 2, "the keyframed hitbox") was drawn for exactly this: a `hitboxes` track keyed by disc frame is the hitmap and its rectangles; a `gun` per player with ammo and a reload gesture; `hit` events; a branching track of windows. So: 1. A converter, `util/forgePortAlg.py`, lifts what is mechanical: the move rows and the hitbox files into `hitboxes` tracks (bad guys, civilians, bonuses, each a rectangle keyed by the frame it belongs to), per level, with the level's disc offset applied. 2. An `alg` behaviour for what every title shares in kind if not in code: two guns, ammo and reload by dip, scores, lives, credits, continues, the death clip chosen by what was shot and how many lives remain, the level select, the intro, the name entry -- written once, with the per-title numbers (frames, sounds, clip lists) in the description. 3. Each level's flow rewritten as data: a `windows` list per level (`shoot` windows naming their hitmap range and what a hit skips to; `bonus` windows; `resume` points for a second visit; `trigger` frames that jump the move; `random` approaches; an `advice` clip), read by hand from its doLevel and written into the description. This is the per-game work, a level at a time, 15 to 27 per title. 4. A drive that shoots: the harness views hand the drive the current window's first hitbox (the pointer to its centre, the trigger on the window's third frame), a miss well outside it on every fifth, a civilian on every seventh where the frame has one, no shot on some, and the reload gesture when ammo is out; compare.sh gets an `alg` family for the Hypseus layout and the library one. The level vocabulary, read from Mad Dog's nineteen: a level is a run of `move` rows played in order; each row a `shoot` window (frameShootStart to frameShootEnd) over a hitmap range, a hit scoring SCORE_BADGUY and skipping to the window's end, the window's end going to a second chance (a pause of iDelay seconds still taking a shot) and then the row's death clip (frameDeathStart to frameDeathEnd), a life gone and the undertaker's clip by what happened (NORMAL, SHOTGUY, SHOTWOMAN, NOGENDER, GOODGUY, or by lives left), or to a rest (played to endFrmRandom, then a three-second pause before the next row's window); a civilian hit anywhere is a life and the undertaker; bullseye windows by frame with a bonus rectangle (ammo to twelve); play-clip states with hardcoded frames and skips; a random draw for an approach or a mirror; first-visit resumes; advice and hint stills; and the level's end deciding the map, the lives-left still, the maze, or the showdown. Mad Dog II and the others add their own (the typing edition types). One thing the survey settled against the converter lifting the data: a level's rows are shuffled at setup by a random draw (setupLevelBank's fRndPopOrder), so the port must draw at play as the map-mode ports do -- the game's own setupLevel and hitbox files run through the shim, from a vocabulary behaviour beside the game (as the ActionMax light sensor is), which judges shots against the live hitmap and answers the description's rules with conditions and actions of its own (shotHit, shotCivilian, bonusHit, undertaker); the level flows stay rules in the description, editable, over Forge's frameReached, timer, discTo, and the rest. Author gained what a two-gun game needs: a press carries its device, so the pressed event names the player and only that player's gun fires, and a hitbox track with step = true keeps a box only on the frames it keys. The pilot is Mad Dog McCree (Hypseus, 19 levels, the smallest of the HD set): the converter and the behaviour built against it, its levels rewritten one by one against the comparison, then the other ten and the library's twelve, each its own descriptions over the shared behaviour. **The pilot, begun (2026-09-16, evening).** The converter's `alg` mode runs the title's script and its files under the stubs (its hitbox tables, its board, its service) and reads the dips from the file its service opens through pcall; the description names `Vocabulary.singe` beside it. The vocabulary (`~/claude/singetest/hypseus/maddog-hd/singe/maddog-hd/`) is Mad Dog's loop over the game's own files loaded through the shim (authorShim and authorRunScript are the public doors): the start, the player select, the attract loop, the town select, the tutorial and the first level, the continue, the lives-left still, the game over, and the board so far, with the two players' twin state reached by number so each piece is written once. The harness: algDrive.singe (one gun joins by START, a coin and START begin the game, shots at the first hitbox of the window's frame three frames in, a miss on every fifth move, none on every seventh, a reload by pointing offscreen, the first unbeaten quadrant at the town select), algOriginal.singe and algPort.singe (the pointer put where the game's onMouseMoved would compute it, presses with their device, both drivers pinning os.time for the game's seed), and compare.sh's `alg` family. Two engine changes on the way: a mouse or gun switch now reaches onInputPressed with its device index, as Hypseus passes it (the original needs it to tell its players apart), and a sprite scaled to nothing draws as a pixel instead of ending the engine (the original's bullet shrinks to zero after a hit, which Hypseus let pass). The port agreed with the original through the player select, the attract loop, the tutorial, and the first level, and then level by level as each was transcribed: the Corral, the showdown, the Saloon, the Sheriff, the Bank (271 trace lines over twenty thousand frames agree), then the signpost, the fuse, the mine, the bottles, the maze and its signs, the pond, the plateau, the cliff, the canyon, the hideout, the house, and Mad Dog himself, with the tie's countdown; the vocabulary is some three thousand lines against the original's forty-four thousand, the two players' twin state reached by number and the shared shapes (a shot judged against the hitmap, the bonus, and the civilians; the death, undertaker, and end states; the wilderness levels' one shape) written once. The drive shoots the signpost's sign, the mine's lantern and item, and the maze's signs from targets the views hand it. What is not yet transcribed: the two-player tie shootout and the second player's board (a one-gun run never reaches them), the service menu, and the drawing (the port draws nothing yet). A forty-thousand-frame comparison with the first eleven levels in agreed (325 trace lines), and one with every level in agreed the same (the drive's deliberate misses keep a run in the four town levels, the showdown, the continue, the board, and the game over); the drive gained PORT_PERFECT=1, no wrong or missed shot, and a sixty-thousand-frame perfect run agreed too (567 trace lines): the town levels, the signpost, the fuse, the mine, the bottles, the maze and its signs, a showdown, the continue, the board, and the game over; the hideout, the house, and Mad Dog himself lie past where the drive's lives ran out. One trap on the way, worth knowing for the whole family: the original writes its board into the .cfg beside its script at every board entry, so harness runs had changed the shipped file and a reconversion had carried a run's scores into the description; the file is restored, compare.sh's alg family keeps and puts back the .cfg files beside the script, and compare.sh's per-side timeout is an hour. Mad Dog moved to ported/hypseus, singePorts reassembled (54 ports), the release repacked. The tie shootout and the second player's board are transcribed too, untried by a one-gun run. The library's single-player Mad Dog (8,600 lines) is the same disc with the same offsets but an older, simpler flow per level -- half to all of each level's lines differ even with the second player's stripped -- so it is a transcription of its own over the same shapes, as each of the other twenty-one titles will be. **Mad Dog II: The Lost Gold (2026-09-17, small hours).** The second title, the same way: the converter's `alg` mode read its script (9,200 lines and its globals, hitbox, and setup files) into a description with its dips and board, and a vocabulary beside it (`~/claude/singetest/hypseus/maddog2-hd/singe/maddog2-hd/Vocabulary.singe`, 2,700 lines) transcribes its loop over the game's own files: the start, the player select, the attract loop, the guide menu (three panels, the first unfinished guide when the twenty seconds run out), the first level with its bonus bottles and the monk's turn, the three guides' levels and finales (Beaver's four sections and random item, Bonnie's woman who must not be shot and her gun emptied at the second section, the Professor's two levels with his own second hit test), the two item levels with the guide's clip, the prospector's two (his skull from four at random), the two town levels, Mad Dog himself with the ending earned by game type and continues, the showdown, the continue, the lives-left still, the game over, both boards, and the tie-break. Its shapes differ from Mad Dog's enough to be its own transcription (a `finishLevel` end that goes back to the level, or the menu in game type 1; a draw lost that still runs the hit test for the record; three kinds of undertaker clip by whom the civilian was) but the vocabulary's helpers are the same in kind, and three of the game's habits are kept as they are because the trace must agree: it indexes its move rows by `hitmapStart`, a name it never defines (its hit tests take the range from the row themselves), it spares the attract loop's ammo by comparing with `lvlIntro`, also never defined, and its second item test names `thisItem`, likewise, so any of the three items ends the level. The harness views learned its tables (the hitmap range is `hitboxStart`, the move row has no shoot window so the move's own frames stand in, its levels are 1 to 15 and 104) and the drive shoots its guide panels. The first comparison (the drive's misses, fifteen thousand frames) agreed to the line: the player select, the attract loop, the first level, the menu, Beaver's level, the continue, the board, and the game over. With every level in, the views handing the drive the item and the skull to shoot and the drive putting a coin in before it takes the continue, a sixty-thousand-frame perfect run agreed (541 trace lines: the first level, the menu, Beaver's level and finale, a showdown, both item levels, the prospector's two, both town levels -- where the drive, shooting each move's first box, shoots the women and runs out of lives -- the continue, the board, and the game over) and a forty-thousand-frame run with the drive's misses agreed too (475 lines). Mad Dog himself lies past where the drive's lives run out; the tie-break and the second player's board are transcribed but untried by a one-gun run. **Crime Patrol and Drug Wars (2026-09-17, small hours).** The third and fourth titles are a template of their own (the same authors' port, the one program twice with different frames): a start and player select like Mad Dog II's, an attract loop, four mission menus of three or four panels each (shot by the game's own `panelHit` over line arrays, the first open panel when the clip runs out, a closing clip when a mission is done), twenty levels (Crime Patrol) or seventeen and a practice range (Drug Wars), a lives-left still, a continue, both boards, and a tie-break. A level here holds the disc at a move's last frame for the difficulty's delay instead of pausing at it, a death is a mission's "nag" clip chosen by the game's own function (which takes the life), a good guy passes by himself and shooting him is the friendly-killed nag or, at a few moves, a silent loss of the life, and most levels are the one shape with a table of what differs (an opening the trigger skips, a stretch it skips to a frame, moves whose ends play out as clips, the civilian rule, the bystander rule, a section to resume at, a closing clip); the ones that are not (Crime Patrol's practice range with its five-target still, its strip club, its hangar and nuclear plant runs; Drug Wars's boat, night vision, town, and final level with its last stand) are written out. Crime Patrol's vocabulary is 2,600 lines against the original's 8,700 and its hitbox files, Drug Wars's 1,900 against 8,700. Kept as the games have them: `lvlIntro` and, in Drug Wars's South America menu, `MENU_DELTA` and the hangar levels, names from Crime Patrol it never defines; the lives-left still's four resumable levels that neither game has. The shim gained stubs for sprite frame loads and colour keys (both games reset them at load). The harness views learned the menus' panels, Crime Patrol's practice still (in the game's own pointer coordinates) and cutouts, and Drug Wars's last stand. Crime Patrol agreed for fifteen thousand frames with the drive's misses (365 trace lines), sixty thousand of perfect play (545), and forty thousand with misses (499): the practice range, the store, the gang, and the warehouse, the continue, the board, and the game over. Drug Wars agreed for fifteen thousand (333), sixty thousand perfect (458), and forty thousand with misses (380): the Sierra intro and menu, the practice range, the bar, the continue, the board, and the game over. Neither run got past its first mission: the drive shot each move's first box, and in these games a move can be a good guy's, whose shooting is a death, so the views now hand the drive no window on a good guy's move. Two of the game's habits found on the way: Drug Wars names no `levelIntro` at all (its attract loop runs with the level nil, and the port dispatches on that), and its practice range counts as done however it ended. Both originals then moved into `ported/hypseus/`, `forgePorts.py` built fifty-seven ports, and the launch check ran them all headless (one run, 38AmbushAlley's, lost its X server mid-way and was rerun clean). **The Last Bounty Hunter (2026-09-17).** The fifth title is a program of its own again: the army base first, then a signpost still (the practice range, the wanted-poster menu, or a bonus to shoot), a menu of four outlaws whose panels read at large, captured, or dead, twenty-three outlaw levels in four chains (Cactus Kid's six, Dan's three, Harry's five, Loco's four with a coin-toss alternate), each outlaw's own move where a hit kills him and a civilian hit captures him alive, an advice clip once a game after a kill, a bonus level once a game by the game's own clock, a quick-draw showdown drawn at random after a menu choice (the first always), two final levels where the outlaws taken alive are met again (Harry's bribe, the Cactus Kid's showdown with the gun holstered), a shotgun of eighteen shells from bonus hits, a kill streak with its own bonus, and both boards with a girl's clip when every outlaw was taken alive. The vocabulary (`~/claude/singetest/hypseus/lbh-hd/singe/lbh-hd/Vocabulary.singe`, 2,700 lines against the original's 9,700) is the common shape once more with the hooks the levels need (the civilian rule while the disc is held, what a shot between the moves does, the outlaw's move, the level's own draw, the clip after a clip, the closing clip, the next level or a replay), and the practice range, the signs, the menu, the bonus level, the showdown, and the second final level written out; the game's own reload, scoring, streak, and bonus functions are reached by player. Its first comparison found the original itself dying at the army base's board: the game selects a font it never loaded (`fontscore1` for `fontScore1`), which Hypseus lets pass and Singe ended the script on; fontSelect now keeps the current font and warns. The rerun then showed the port a thousand points short after a miss: the game pays its kill streak from the pointer's splat animation in the frame loop (a hit starts the animation, and each of its frames that is not a hit's calls the miss handler), which the port had left with the drawing; the vocabulary now carries that bookkeeping (`blink` on a shot, `splat` for each gun after the level). One more: the Cactus Kid's first level names its intro clip's state differently from Harry's and Loco's (`lvlPlayClip1`, not `lvlPlayClip3`), which the traces compare, so the level table now names the state. The Last Bounty Hunter then agreed for fifteen thousand frames with the drive's misses (407 trace lines: the army base, the signs, the menu, Cactus Kid's and Loco's levels, Harry's first, the continue). **Space Pirates (2026-09-17).** The sixth title: an asteroid range, three doors (the second with a band of colour to shoot and the moves swapped when the other band is shot first, the third with the commander saved by a shot at Ursula's colour, then the crystal order and the cannon's whereabouts), a planet menu of four quadrants, four planets in eleven levels (Fellina's skull and arrow puzzles, the Mountain, the Reaper's counted laughs, the Scrapyard's move not to shoot first) with a crystal shown after each planet's first win and the cannon practice on whichever planet holds it, a crystal build against a ten-second clock, a ship battle of cannon shots judged on the next frame, Tallon, and the continue, the lives-left still, both boards, and the tie-break. The vocabulary (`~/claude/singetest/hypseus/spacepirates-hd/singe/spacepirates-hd/Vocabulary.singe`, 2,700 lines against the original's 11,500 and its hitbox files) is the common shape with the hooks these levels need (an intro clip or a frame to wait for, the level's own clip advance, a hit's own skip, the civilian rule, the closing, the level's own death clip, states of its own), a second shape for the object ranges (Fellina's, the Mountain's, and the Reaper's second halves), and the doors, Fellina's puzzles, the Reaper's laughs, the Scrapyard, the cannon practice, the ship battle, Tallon, and the crystal build written out; the crystal after a win is a state of the level (the game's `doShowCrystal`, which also runs the cannon practice from inside it). The game's own setups, hit tests (`shooterHit`, `civilianHit`, `bandHit`, `ursulaHit`, `fellinaHit`, `socketHit`), death clips by kind, laugh counts, crystal order, and menu clips run through the shim. Two habits kept: a press on an empty gun is left standing on the asteroid and object ranges (the flag clears only with ammo), and the Reaper's first level does nothing at all when its second section is reached (the original stays in its setup). The harness views learned the planet menu's quadrants (pointer coordinates scaled by the game's `UnX`/`UnY`), the band within its frames, Ursula within her colour's frames, the skull and the arrow, and the sockets in the order shown. Space Pirates agreed for fifteen thousand frames with the drive's misses (397 trace lines: the range, the first two doors with deaths, the continue, the board, the game over). Its perfect run then died at the second door's fourth move every time: the drive aims at the first hitbox of the frame it sees, but the press it makes is judged on the next frame, and this target moves enough that the middle of one frame's box lies outside every box of the next. The views now check that, and aim at the next frame's first box when the current one's middle would miss it (the slow targets of the earlier titles are aimed at as before). The same run found a hole in The Last Bounty Hunter's port (the advice clip's end hook called without the game's state, an error the misses run never reached) and a port view that indexed the game's band and line tables by unqualified names, so the port never shot the band the original hit. The rerun then showed the original itself stuck at the second door's colour hint: the game changes state on the clip's last frame and expects to read that same frame once more in the new state, which a game paced for an overlay faster than its disc can, and which the harness's deterministic clock never allowed, since `--deterministic` stepped the disc one video frame per engine frame whatever its step. The engine's deterministic mode now steps the disc by the virtual clock like everything else, and a second fault came out with it: the frame loop's overlay throttle compared the clock strictly, and on a virtual clock that moves exactly one step an iteration that ran the script every other iteration, a disc frame apart. With both put right each disc frame stays on for the two or three engine frames its time is worth at the default step, which is what real play does (a probe of the disc's frame against the clock's ticks confirmed it); the manual and the changelog say so, and every comparison below this point ran on that timing. The same perfect run reached Harry's last level in The Last Bounty Hunter and found its finished step missing from the port's table (the original goes back to the menu, the port replayed the level). On the corrected timing every port was run again with the drive's misses for fifteen thousand engine frames, and all agreed: Mad Dog McCree (174 trace lines), Mad Dog II (114), Crime Patrol (268), Drug Wars (198), The Last Bounty Hunter (183), Space Pirates (241), and both Johnny Rocks (106 and 111); The Last Bounty Hunter's perfect run agreed for a hundred and thirty thousand (483). Space Pirates's perfect run then walked the whole game and agreed for a hundred and thirty thousand (736 trace lines: the range, the three doors, Fellina's two levels with the puzzles, the Reaper's three with the laughs, the Scrapyard's first, the cannon practice, the Mountain's two, the crystal build, the ship battle, Tallon, the board, the game over). **Who Shot Johnny Rock? (2026-09-17).** The seventh title, the noir multiplayer edition, is the detective game: money in place of lives (the doctor's bill at every death, the undertaker when it runs out, and more bought with a credit or at the store), Johnny's office first, then a city map with a hint before it (the game's own rules for which), four places to clear for their clues (the casino, the garage, the pool hall, the warehouse's three parts), a store, random encounters on the way to a place (one of fourteen scenes), bonus puzzles opened by hints and expiring after a visit (roulette, cards, the pool balls still and rolling, the license plates, the elevator panel, the cans, each paying on the lucky number's target), the mansion in four parts (the moves, the four clue items in the order found, the bomb, the safe's dial by the combination), the killer's reveal, and a finale of four random scenes and the killer's own, then the boards and the tie-break. The guns fire from the frame loop against a real-time gun delay (`os.clock` and ninety milliseconds), the trigger flag standing until the release. The vocabulary (`~/claude/singetest/hypseus/wsjr-hd/singe/wsjr-hd/Vocabulary.singe`, 2,500 lines against the original's 13,300 and its hitbox files) is one common shape with two variants (a hit moves on at once, or its clip plays out with the civilian test live under it) and hooks for the intro, the ending clip's skip, the clue, the closing, and the level's own hit and running rules; the office, the map with its hint, clue, and kill-clip sub-machines, the store, the mansion's items and safe, the random scenes (shared by the encounters and the finale's four), the killer's scene, the seven puzzles (two shapes: moves, and panels on a still), the continue, the boards, and the tie-break are written out. The game's own setups (fourteen random scenes included), hit tests, nags, undertakers, hints' clips, safe digits, and boards run through the shim. Its first comparison diverged at the casino's move order: every level's setup spins the shot mark's sprite by a random angle, a draw the picture made that the generator counts, and the port now draws it too. The harness views learned the city map (the killer's place once revealed, the mansion once the clues are had, else the first place open), the clue items in the game's second scale, and the safe's dial when it shows the next digit; money stands in for lives in the trace. The second run agreed apart from the hints' timing, and that was the description's doing: the game's config reader keeps the showdown dip a number, and the game tests it against `false`, which a 0 never is, so its hints play with the dip off; the port had made it a boolean. Who Shot Johnny Rock then agreed for fifteen thousand frames with the drive's misses (203 trace lines: the office, the map with its hints, the casino three times over, the pool hall, the warehouse). **Who Shot Johnny Rock? (Singe edition, 2026-09-17).** The eighth title is the older one-player cut of the seventh, and its vocabulary (`~/claude/singetest/hypseus/johnnyrocknoir/singe/johnnyrocknoir/Vocabulary.singe`, 2,100 lines against the original's 9,900) is the multiplayer one with the twin state gone and every difference the diff of the two scripts showed: no player select (the disc's wake goes straight to the attract loop, which the drive now takes as joined), a move's end passed rather than reached (the disc put back on its last frame for the draw), the casino's, the mansion's, and the encounters' draws without a civilian test, the office's civilian at the draw passing the move by, the move orders drawn from fixed tables rather than shuffled, the one encounter with two bad guys at once (the second reached through the civilian test, a moment's pause for both, a death by whoever is left), other clip frames throughout, a single board that waits in a state the game names but never defines (and matches on all the same), and a gun of its own: a held trigger rolls for a jam once a frame (one chance in three hundred and ninety-one, a draw the generator counts) and otherwise arms a delay of its own clock, the puzzles' shots counting only between shots. The dips are typed as its config reader types them (the undertaker and the showdown booleans here). It agreed at its first comparison, fifteen thousand frames with the drive's misses (244 trace lines: the office, the map, the casino, two random encounters, the pool hall). **Mad Dog II, the typing edition (2026-09-17).** The ninth title is Mad Dog II played on a keyboard: every move a word (a letter on the train, a sum in the second item level, a phrase in town, six or eighteen letters sprinkled over the picture at the showdown and at Mad Dog) typed as it runs, the disc held at the move's end for the difficulty's seconds, the word untyped there a death, the trails ending in a thirty-second rush at three times the disc's speed with a tally after, and a title screen, a guide menu, and a board driven by the return key. The vocabulary (`~/claude/singetest/hypseus/typing-md2/singe/typing-md2/Vocabulary.singe`, 2,200 lines against the original's 19,000, most of them its word lists) is one typed shape with hooks (the word's field, a phrase or a moving letter, good guys whose words cost or kill, the clip's end, the draw missed, the level's own clips), a second for the three rushes, a third for the two sprinkled levels, and the office (its own three clips and sections) written out; the game's own setups with their word draws, its word and phrase lists, the train's hit tables, the undertaker's clips by the lives left, and the board run through the shim. The keys reach the port as the runtime hands them over, the keysym as a switch (the return and backspace keys, which the runtime keeps apart, named back from their scancodes), and the game reads one at a time from `iKey` as its own script does. The Johnny Rocks' perfect runs (the casino, the pool hall, the warehouse, the garage, the mansion's four parts, the finale) turned up two more faults: a level's unfinished hook called without the game's state in four of the vocabularies (an error the misses runs never reached), and a drift in the originals' own config files, which the games write at every board and which a comparison killed part-way never put back, so the multiplayer edition's safe combination came from a later entry of the pool than the description's; the files were restored from the descriptions, and compare.sh now puts them back however it ends. With both mended the two Johnny Rocks agreed for a hundred and thirty thousand frames of perfect play (848 and 715 trace lines: the office, the map, the casino, the pool hall, the warehouse's three parts, random encounters, the garage, the mansion's four parts, and the finale's random scenes, where the drive's luck runs out). The harness learned to type: the views name the key the game waits for (the next letter of the word, a sprinkled letter not yet typed, the magic word's, the return key at the title, the menu, and the board), the drive releases it (a wrong letter first on every fifth move and nothing at all on every seventh with misses on), and the converter recognises the edition by its globals file and reads its dips from the config its service file names. Its first comparisons found the views calling Mad Dog II's hitbox scaler at the guide menu (a game with no hitboxes has none), the converter keeping a frame rate the game never names (a stub function, written into the description), the drive waiting for a key slot the game starts at zero rather than at none, and, once the office was typed, the port's first word running longer than the original's: the picture flickers each letter already typed in a colour drawn three times a frame, draws the generator counts and the words are drawn from the same generator, so the port draws them too. With those mended the typing edition agreed for fifteen thousand frames with the drive's misses (263 trace lines: the title, the office, the continue), and its perfect run typed its way through the office, the guide menu, Beaver's trail, and the rush before the views' item branch (Mad Dog II's, another scaler call) ended both runs alike. With that branch kept to the games that scale, the perfect run agreed for a hundred and thirty thousand frames (the office, the menu, Beaver's trail and rush, both item levels, the prospector's two, the town, the continue, and the board, where the drive's return key had seeked the name's clip back every frame until the run ended -- pressed once now). All five then closed together: their originals into `ported/hypseus/`, `forgePorts.py` built sixty-two ports, and the launch check ran them all headless. The typing edition alone failed it: its script names its files by their place in the Hypseus tree (`singe/typing-md2/...`), which the shim's file resolution only stripped when the name began with the port's own directory; it now strips the tree's prefix too, and the ports were rebuilt and launched again. **The two Johnny Rock copies left (2026-09-17).** The library's `johnnyrocknoir__hypseus_singe_johnnyrocknoir_RGB` is the one-player edition's script to the line (only the launcher's header differs), a video-only alternate that stays in `hypseus/` unported. `WSJR_HD` is the same one-player edition in the HD layout (its files under Script/, its config under Cfg/, its hitbox scale 360 wide rather than 320), and every difference from `johnnyrocknoir` is drawing or paths, so it is ported with the one-player vocabulary copied beside it unchanged; the converter learned to load a title's files from a Script/ folder and to find its service file there. It agreed at once, both with the drive's misses for fifteen thousand frames (111 trace lines) and for a hundred and thirty thousand of perfect play (758: to the finale, as its twin), and closed with the others -- once `forgePorts.py` learned to keep a hand-written title's Fonts folder, which it leaves out of every framework title's port (those draw their own text) and which this one loads its board's face from. With that, sixty-three ports are built and the launch check runs every one of them clean. That is every hand-written American Laser Games title of the Hypseus library ported; the library's own twelve remain. **The library's own editions (2026-09-21).** The fourteen left are the older one-player Singe editions the library keeps at its root (six titles and the HD copy of each: Who Shot Johnny Rock?, Space Pirates, Mad Dog McCree, Mad Dog II, The Last Bounty Hunter, Crime Patrol, Drug Wars), laid out like the map-mode copies (the script and its files in `Script/`, the config in `Cfg/`, the video and its framefile in `Video/`), and each is a transcription of its own again: the HD Mad Dog is not the Hypseus one, and the one-player cut of every game is an older, plainer loop than its two-player edition (no player select, the pointer raw, a single trigger flag the levels spend, `iBullets > 0` tests, a level's rest and pause states with their own frame rules). The two Johnny Rocks went first, with the vocabulary the Hypseus one-player edition already had, and agreed at once (110 trace lines with the drive's misses, 757 of perfect play). The other ten each got a `Script/Vocabulary.singe` written from the game's own loop (the HD copy the same file, since its differences are scale and paths: Space Pirates's common level and object range shapes with a `clipDone` step and the game's own `doShowCrystal`, its START mirroring the seed the game draws from the clock; Mad Dog's wilderness shape with the per-level reset on a hit, its showdown by flag, its mute delay, and a board that waits in the game's own state; Mad Dog II's finale shape for Beaver and Bonnie, the menu that leaves the trigger standing, the board that waits in a state the game names as a frame number, and game type 2 keeping the panels; The Last Bounty Hunter's `bonusTry` over boxes and ranges and a level table of some twenty hooks, its showdown by flag; Crime Patrol's practice range of six cutouts, its menus, and the common shape with the run-up the trigger skips; Drug Wars's the same over its own levels, the final level without the last stand the two-player edition added). Crime Patrol's and Drug Wars's HD copies are not the same program as their originals, so their vocabularies tell the editions apart by the resolution the HD globals take from the disc: the HD copies take the bullet on the press (a free one first where a level grants it) and reload by the second button or, on the trigger's release, by the dip with the borders measured from the resolution, start at the part their dip names, run a different attract loop (frames of their own on timers), floor the score at the board, and skip to a move's clip only when the disc is not there already; the library editions take the bullet on the release, reload by the second button alone, and Crime Patrol's alternates two cuts of its story by a draw at start. What the family needed on the way: the engine hooks a bare `os.time()` under `--deterministic` (Space Pirates seeds its generator from it when a game starts, so the two sides drew different sequences); the shim's `io` reads a real file (Mad Dog and Mad Dog II read their config at load, and a stub that answered nothing ended the load at `setShootDelay`); the converter stubs `require("lfs")`, looks for a config under `Cfg/` beside `Script/`, and sets `MYDIR` to the title's folder; the assembler writes a `games.dat` entry from `Video/*.mp4` when no launcher names a title (the plain Space Pirates has none); the harness views scale the hitmap's coordinates only where the game does (the one-player editions aim in the hitmap's own), aim at Ursula from the game's own numbers before its hit test builds her table, and report two things the drive needed: the press of these editions takes the bullet before the level judges the shot, so the last one is never judged and the views name it a `spare` the drive keeps back (a rule the one-player Crime Patrol and Drug Wars turn off, since theirs judge first and take the bullet on the release), and `reloadSwitch` names what reloads (the second button for those two). Two faults of the ports' own: The Last Bounty Hunter's shared clip state ran before a level's own states, and Dan's third level and the first final keep a clip state past the last move (the level's own states now run first); and the scaling helper's first draft called itself where the game scales, which spun both HD Crime Patrols for an hour before the hook the view now offers (`PORT_HOOK=1` names the line every ten million instructions) found it. One false alarm: Mad Dog's first perfect run diverged at the Corral on an engine built before the clock hook, and agreed to the line once rebuilt. Results, each with the drive's misses for fifteen thousand frames and perfect play for a hundred and thirty thousand: Space Pirates 235 and 763 (the whole game: the range, the doors, every planet, the crystal build, the ship battle, Tallon, the board -- once the views aimed at Ursula, whose table the one-player edition builds only inside its hit test; the first run had ended at the third door for want of that shot); Mad Dog McCree 162 and the whole game (the town levels, the signpost, the fuse, the mine, the bottles, the maze, the hideout, the house, Mad Dog himself, and the board); Mad Dog II 122 and 498 (every guide, both item levels, the prospector, the town, Mad Dog, the board); The Last Bounty Hunter 198 and 734 (all four outlaws' chains, both finals, the board); Crime Patrol 302 and 1,208 (all nineteen levels to the board); Drug Wars 288 and 1,080 (every mission to the base, then night vision, where the drive's lives run out); and the two HD copies that are programs of their own, Crime Patrol HD 249 and 1,031 (the whole game) and Drug Wars HD 254 and 929 (the same path as its original). The other HD copies, with the same vocabularies as their originals, agreed the same way: Space Pirates HD 235 and 763, Mad Dog HD 162 and the whole game, Mad Dog II HD 123 and 498, The Last Bounty Hunter HD 189 with the drive's misses (its scale changes which shots land, so the count differs from its original's) and 753 of perfect play (every chain, both finals, the board). All twelve then closed: their folders and launchers into `ported/`, `forgePorts.py` built seventy-seven ports (a title with no launcher of its own, the plain Space Pirates, gets a `games.dat` entry from its video), the launch check ran every one clean one at a time (the owner asked for a single engine from here on), and the release was repacked. With that every American Laser Games title in the library is ported. ### 14.14 The one-offs (2026-09-21) Four bespoke scripts remain in the library's root after the families: Adventures in Videoland: Rollercoaster, Hologram Time Traveler, LINEA (the MazescaterFramework's one game), and Platoon (an American Laser Games shape by the same hand as the Singe 2 editions), each ported by hand, one at a time; BatMaker writes launchers and is not a game (14.2), and the menu is the engine's own. **Rollercoaster (2026-09-21).** David Lubar's 1982 text adventure over the laserdisc, in Scott Duensing's Singe port of Scott Lawrence's web one: eighteen rooms with exits, a name and description, and a Pioneer command each (a frame to park on as a still shipped as a picture, or a clip to play to its end), twelve objects and three pieces of furniture by location, a two-word parser of some thirty rules tried in order (several may apply to one line, and `GO` and `READ TICKET` re-enter it), five rooms with a scene of their own (the restaurant's meal, the dancer's tent and the bear, the top of the coaster, the guard and the uniform, the dark path), a prize to name, a question of where to put a thing, a bomb on a hundred and fifty turns, a play-again, and the win. The port keeps the split the plan drew for the family: the converter's `videoland` mode (told by the script's own state names) runs the script under the stubs and writes the rooms, objects, furniture, directions, and the turn count into the description as data, and `Vocabulary.singe` beside it carries what the description cannot say -- a `console` look (the Apple II screen of lines the game has printed, word-wrapped as the original wraps them by the font's own metrics, the prompt with its blinking cursor while the keyboard is on, and the still the disc is parked on) and the `videoland` behaviour, the game's states transcribed over the description's tables. The parser layer (14.7) was not used: the game reads the whole keyboard itself (a letter or a space into a line of forty, return to take it, backspace, and any key at all where it waits for one), drops what is typed while a clip plays, and splits a line at its first space with the noun echoed in its answers, so the behaviour hears the keys as the runtime hands them over (a keysym on the switch channel, the return and backspace keys by their scancodes) and keeps the game's own rules. The harness gained a `videoland` family (compare.sh, videolandOriginal/Port/Drive.singe): the drive types a walkthrough a character a frame whenever the game waits for a line (a solution of forty-five lines; with the drive's misses on, a prologue of unknown words, inventory play, the restaurant's death, and the coaster's car, each answered with a yes to play again), presses a key where the game waits for one, and traces the state, the room, the turns, the keyboard, the delays, the still, the clip's end, the count of lines printed, and the newest line of the screen. Two of the original's habits shaped the drive: the keyboard is still on for the frame a wait for any key begins in, so a space pressed there goes into the next line (the drive presses a full stop), and it stays on through the bomb's delay, where a line typed is taken before the play-again prompt and leaves the game in no state at all (the drive waits for the delay). Both runs agreed to the line, each to the win: 218 trace lines of the solution, 341 with the detours. The assembler keeps a hand-written library title's fonts, as it does a Hypseus one's, and Rollercoaster moved into `ported/` with its launcher. **Hologram Time Traveler (2026-09-21).** RDG2010's Singe edition of Sega's 1991 hologram game, converted for Singe 2 by POIU2020: a tutorial and eight time periods, each a stage of segments shuffled at the game's start (a random order and a random count, or every one by dip), each segment a run of move rows (an input window, the move wanted -- a direction, the action button, or a turn and then a shot -- a rest to the row's end, a death clip, an alternate death after a turn, and the wizard's advice by kind), a death's clip chosen by the level, the segment, the move, and what was pressed (a table of special cases in code), the wizard after a death, reversal cubes that rewind the disc frame by frame to the move's start, a map of the periods with a cursor on a fifth-second delay and six seconds to choose, a trader who sells cubes for coins, a hellgate whose lever rolls for lives, a continue with a limit by dip, a game over, an LCD of scrolling lines on the attract loop, and a board typed by the stick over the game's letters. The port is the family's shape: the converter reads the title by its own banner and runs its script under the stubs (its dips from the config its service file names under `Cfg/`, its board from the same file), and `Vocabulary.singe` beside the script transcribes the loop (the start, the attract loop, a game's start, the level with its windows, turns and shots, rests, deaths, cubes, and wizard, the map and its cursor, the trader, the hellgate, the continue, the game over, and the board) over the game's own files loaded through the shim: its globals, its segment shuffles, its level setups and move rows, its death-clip table, its intro and filler and advice clips, its clip and timer helpers, its letters and board. The port draws nothing yet, as the American Laser Games ports do not, and one habit is kept: the hellgate names a roll the game never defines. The harness gained a `timetraveler` family (compare.sh, timetravelerOriginal/Port/Drive): the drive puts a coin in and presses START, answers each window three frames in with the row's move (a turn, then the shot two frames on), a wrong move on every fifth and none on every seventh with misses on, skips the tutorial from the sign post with the right in perfect play and plays it otherwise, spends a cube against the first death, visits the trader once and buys cubes with coins, pulls the hellgate's lever (with misses on: a roll can take every life), takes the continue, and types a letter at the board; the trace carries the flow, the screen, the state, the period, the segment, the move, the score, the lives, the cubes, and the credits, but not the LCD's own state, which the original's drawing steps while the attract loop shows. Both runs agreed to the line: 488 trace lines with misses over forty thousand frames (the tutorial, the first two periods, the trader, a death rewound by a cube, the continue) and 1,494 of perfect play over a hundred and thirty thousand: the tutorial and all eight periods with the map between them, beaten without a death, the extended play the game grants for that through all eight again, the board, and the game over. (The first perfect run had pulled the hellgate's lever and lost every life to the roll at the second period, so the perfect drive now lets that clip pass.) Hologram Time Traveler moved into `ported/` with its launcher, its port assembled (seventy-nine ports) and launched clean. **LINEA (2026-09-21).** BLADESCATER's 2023 fan game over a video, the one title on the MazescaterFramework: Karis 3.31c (fifteen thousand lines, 2,701 of them changed) with compound move kinds numbered 149 to 239 -- thirty-six sequences of two or three inputs in turn (UL, DLB, DLU, and their kin), eight circles from any of four starts either way (LOOPLEFTU and the rest), mashes of one direction or button (MASHLEFT, MASHB2, MASHB3, each with a MIN and a MAX), MASH3 (BUTTON1 and BUTTON3 in turn), and a direction with BUTTON2 or BUTTON3 or two directions with a button (ACT2UP, ACT3DOWNLEFT, ACTUPLEFT) -- each judged by a check function of its own through the framework's step counter, its mash flags, or its pressed flags; besides those, a longer look at the hints (four seconds for two), MASH2 at six presses a second of the window for nine, every direction and the third button counted for either side of a mash, and DOUBLE numbered after MULTI (34 for 26). The kinds are data with three shapes, so the qte loop gained them as tables rather than branches: their numbers in QTE, MAZE_SEQ (each sequence's inputs in turn; a circle is five, back to its start) judged by one Q.checkSequence, MAZE_MASH (the input or pair a mash takes, its count per second, its MIN and MAX) by one Q.checkMashOf, and ACT_WANTS (the flags a combination wants together, the Karis pairs included) by the branch that judged the pairs; the four other differences are gated on the description's framework mark ("Mazescater 1.00", which the converter writes when the framework folder beside the game is the MazescaterFramework), and the assembler leaves that folder behind as it does the Karis copies. The Karis drive had answered single inputs only (a compound move timed out, which the earlier titles' runs allowed for); it now plans the compound kinds of both frameworks -- a hold pressed through the window, a let-go held from before it, a mash tapped every other frame (a run, MASH2, and MASH3 alternating their two), a sequence or circle a step every other frame, a multi as many times as its row asks, a combination together -- and takes PORT_WRONG_EVERY and PORT_MISS_EVERY for a run that goes deeper into a game with few lives. Both runs agreed to the line: 166 trace lines under the drive's usual policy (LINEA's five lives and one continue end at the thirtieth move) and 870 over sixty thousand frames with a wrong answer every fiftieth move and a miss every seventieth, through the fifth level's fifty-ninth move, every one of the fifty-six kinds the game uses met and passed. LINEA moved into `ported/` with its launcher, its port assembled (eighty ports) and launched clean, and the release repacked. **Platoon (2026-09-22).** POIU's 2020 prototype in the American Laser Games shape, from the same hand as the library's Singe 2 editions: six patrols (Charlie, Delta, Tango, Lima, Zulu, Bravo) of eight to twenty moves each, every move a window to hit one man in, judged by the editions' hit test against a hitmap of the frame whose boxes are, as often as not, thin lines given back to front; a first press of the trigger while a patrol's opening plays jumps past it, whatever the state; a miss holds the disc at the window's end for the draw the difficulty allows, then the move's death clip and the sergeant's after it; the continue, the lives-left still, the board with an opening clip of its own when Tango was beaten, and an attract loop the game files under a level it never defined (its levelIntro is nil, and nil equals nil). The port is the family's shape: the converter's alg mode reads the dips and the board out of Script/platoon.cfg, and `Script/Vocabulary.singe` (575 lines) carries the loop -- the six patrols as one function over a table of what differs (the length, the opening's skip, what follows), the intro, the continue, the lives left, the board, and the gun's input, with the game's own level setups, hitbox tables, hit test, and board functions run through the shim as they are; the nil level is kept as the original has it, since the trace must show it. The harness learnt three things from the title: the alg family looks for the framefile under Script/Video too; the drivers give the drive a target while the opening waits for its skip and name the reload button (the second, as the Crime Patrol family) with no spare bullet, since the release takes the bullet after the shot is judged; and the drive's aim skips a box given back to front (the game's own test can never hit one) and answers a move again in the game the continue starts over. Both runs agreed to the line: 305 trace lines with misses over sixty thousand frames (Charlie lost thrice and beaten after the continue, Delta, Tango, the board, the game over) and 336 of perfect play over ninety thousand (all six patrols without a death, the board with its Tango clip, the game over, the attract loop). Platoon moved into `ported/` with its launcher, its port assembled (eighty-one ports) and launched clean, and the release repacked. With it the one-offs are done, and every game of the library the plan set out to port has its port. ### 14.15 The HUDs and the service menus (2026-09-22) The owner's verdict on 14.14's close: "Not porting the service menus or HUDs means the ports are NOT COMPLETE. Finish your work!" Right: a port that plays the loop but shows no score, no lives, no credits, no prompt for the move, no board, and has no service screen to set its dips on is a port of the loop, not of the game. What every family draws and configures, and how each port gets it: * **The KarisFramework** (twenty-eight ports and LINEA): the score in the framework's number sprites (or rendered), the lives (sprites or a count), the credits or INSERT COIN or FREE PLAY blinking, the level title or scene number, the disk-slot and autosave marks, the get-ready and skip prompts, the LCD ticker of the attract loop (typed out a character at a time), the pause, the move prompt (`drawAction`, 534 lines: arrows that slide in from the edges, the buttons, the hold and mash gauges, the loops, the combinations, the timed and multi kinds; LINEA's framework adds its compound kinds and their hint texts), the choice menu of a CHOOSE move, the board (the name entered on a grid of forty-four cells, the rankings of the three modes, the percents, the trophies), and five service screens (the options, the graphics, the performance settings, the load/save slots, the exit) of 4,700 lines, with the config file they write. The framework's copies do not travel with the ports (14.8), and its loop was rewritten into Author.singe as the qte behaviour rather than run, so its drawing and its screens are rewritten the same way: a `qteHud` look in Author.singe that draws from the state the behaviour keeps, and the screens as states of the loop (`Q.doServiceMenu` and its kin), both as data where the framework repeats itself (the option tables: a label, the values it cycles, what gates it). The art travels: the assembler ships a title's Overlay/ (with Lores/) and Fonts/, and the framework's default font as `Fonts/default.ttf`, since a look needs them and they are art, not program. * **The Kimmy Script Engine** (seven ports): its own HUD over a skin (`FrameworkKimmy/Skin/DEFAULT`), the combo prompt (`drawAction`, 1,671 lines), the next-move preview, the life bar, the tilt, the warning, its board, and its five screens (the new-game menu is already a state of the kimmy loop). The same way as Karis: a `kimmyHud` look, screens as loop states, the skin shipped with the port. * **The map-mode lineage** (starblazers, freedomfighter, jeeg, hq-timegal, Super Don Quixote): their `main.singe` already runs through the shim for their levels, and holds their drawing and their one service screen; an `rdgHud` look calls those functions as they are, under the shim's sprite, font, and colour calls made real. * **The American Laser Games titles** (thirty-two ports, seventeen vocabularies): bullets, lives (hats, money), the score, the credits, the crosshair and its recoil, the reload animation, the board's letters and table, and the one service screen with the mouse as its cursor -- all in the games' own files, which the vocabularies already run through the shim. Each vocabulary gains an `algHud` look that calls the game's draw functions in the game's order (its `onOverlayUpdate` tail), and a `levelService` state that runs the game's `doServiceMenu`; the SERVICE switch, let go until now, opens it. * **Hologram Time Traveler**: its LCD, action, lives, cubes, score, and credits, and its service screen, from its own script, the same way as the ALG titles. **Rollercoaster** draws its console already and has no service screen. **ActionMax** shows its LED readouts through text and sprite looks the converter writes, and the console had no service screen. Three things the runtime gains for all of them: * **The shim's drawing calls are real.** `spriteLoad`, `spriteLoadFrames`, `fontToSprite`, `spriteGetWidth`, `spriteGetHeight`, `spriteUnload`, `spriteDrawFrame`, `colorForeground`, `fontSelect`, `fontQuality`, and `getMiddle` do what the engine does, on files found where the port carries them (`Q.gameFile`); a file that is not there gives -1, as a stub did, so a game's load never ends on missing art. `overlayClear` stays a no-op (Forge clears), and a look that drew with the game's font puts Forge's back. * **Dips persist.** A service screen's changes are written through the engine's save store (`saveSet("forge.cfg", ...)`) and read at attach over the description's dips, which remain the shipped defaults (what "Default" restores). The Karis save slots go the same way. The harness runs in a fresh data directory, so a comparison starts from the description as before. * **Two checks.** The drives gain a service walk (`PORT_SERVICE=1`: the SERVICE switch at the attract loop, every option stepped, the exit), so the screens' logic is traced against the original like the loop; and `testScripts/ports/shots.sh` runs an original and its port to the same moments (the attract loop, a prompt, the board, each service screen) and screenshots both into `screenshots/ports/`, since what a HUD shows is a picture, and a trace cannot say it is right. The order: the runtime's three gains and the ALG family first (the least new code: the drawing exists, the shim runs it), then Karis (the most, and the pattern Kimmy follows), then Kimmy, the map-mode titles, and Time Traveler; each family closed with its traces still agreeing and its screenshots beside the original's. **Parked (2026-09-22).** The first step of that order was taken and then stopped: the shim's drawing calls are real (queued through the behaviour's step and run by a look, with one sprite handle per file), dips and boards persist through the save store, the twenty-six ALG and Time Traveler vocabularies gained a `hud` look that calls the game's own draw functions and a service state that runs the game's own screen, the drives gained a service walk and screenshot moments, and shots.sh gathers the pictures. None of it was verified past a syntax check, and the owner's look at the sources ended it: "Most seem like a thin layer of Forge bolted on to a whole hell of a lot of existing code. That's not really meeting the stated goal of Forge." He is right, and Rollercoaster is the sharpest case: its description has an empty `rules` table and the whole game is 985 lines of Lua in its vocabulary. The ALG family is the same at scale, 54,627 lines of vocabulary Lua across twenty-six files, steering a shim that runs the games' own setups, hit tests, boards, and (as of this step) drawing and screens. By the measure that matters -- how much of the game lives in the description and how much in Lua -- only the ActionMax five and the framework loops meet the goal, and even Karis and Kimmy run the game's script at attach. The rule that should have governed section 14, for whenever this is taken up again: a port's vocabulary may add only general-purpose kinds; a game's logic and data live in its description; a family with mechanics Forge lacks gets them as runtime behaviours and looks driven by data, never as the game's code and never as the game's code rewritten by hand. The redo, in order: Rollercoaster (the smallest, and the only one that exercises the authoring side -- rooms, the parser layer, rules, sequences, vars, a console look -- so its gaps say what Forge is missing for adventures), then the ALG family (one generic behaviour and look, the converter extracting hitboxes, frames, boards, dips, and service options, the vocabularies gone), then Time Traveler, the map-mode five, and the Karis and Kimmy attach. The work is filed here; the owner's word was "get back to actually working on Singe." ### 14.16 The pixel reading, into the framework (2026-09-23) The owner asked whether the pixel reading could be added to Forge itself rather than sitting beside five games, and it could: it was general all along. `AUTHOR.behaviours.lightSensor` is in the manifest now, in the gun cluster after `gun`, `pointer`, and `target`, and the read behind it is one runtime function, `authorPictureState(x, y, high, low)`, in the section about judging shots over the picture. Nothing about it was ActionMax's: the spot, the corner it may not be shot in, the two thresholds, the trigger, and the player are parameters, and the numbers live in each description as they always did. `AUTHOR.conditions.pictureAt` came with it, so a rule can ask what the picture is at a point without a sensor at all -- a flash hidden in a game's own video, a colour cue, a lamp in a scene. The two thresholds are `PICTURE_BRIGHT` and `PICTURE_DARK` beside the other tuning constants, named once and read by both. A game with no disc reads dark everywhere, since `vldpGetPixel` answers zeroes when no video is playing. `util/forgePortActionMax.py` no longer writes a `vocabulary` line, the five descriptions are regenerated without one, and the file they used is kept aside as `Vocabulary.singe.old`. All five comparisons still agree with their originals. So the zero-vocabulary count is 54 of the 81 descriptions, and the ActionMax five are now ports in the full sense: a description, the framework, and nothing of their own. `docs/ForgeVocabulary.adoc` is regenerated from the manifest, and the book's "Your Own Vocabulary" now uses the test scene's `bob` as its example, with the rule that a word two games would want belongs in the framework. Scene 100 (`testScripts/author/picture.forge`) covers both words from the repository alone, since the ActionMax five are the only other thing that exercises them and they live outside it: the description compiles, the built game calls `authorPictureIs`, the framework has the behaviour, and the three answers add up to the frames drawn. Run plain, every frame must read dark, because no video is playing; run with `-v Singe/menuBackground.mkv`, the read must see something other than those zeroes. Which of bright and neither it sees is the video's business: the menu's backdrop is mid-tone, so it reads neither on 119 frames of 120 and never bright. Both runs are clean. ### 14.17 How much of a QTE port is really in its description (measured 2026-09-23) The owner put the obvious question to 14.5: the ports are called ports, yet the loop calls the game's own hooks. So: how much of each port would still play with the game's Lua taken away? It is a one-line experiment, since the `qte` behaviour only loads what the description names -- delete its `script` and `addons` lines and the loop has nothing but the snapshot the converter wrote. Five titles, each compared against its original as 14.1 compares them: | Title | Hooks it defines | Without its Lua | |---|---|---| | Time Gal | setupMoves, swapScene, doLevelSelect | plays as the original, 186 trace lines | | Asterix | setupMoves, swapScene, doLevelSelect | plays as the original, 218 trace lines | | Tron | setupMoves, swapScene | diverges at level 1 scene 5 | | Dragon's Lair Enhanced | setupMoves, startConf, swapLevel, specialScore | errors in `Q.doMixTIE` | | Space Ace Enhanced | setupMoves, startConf, swapLevel, swapDeath | diverges | So the hosting is not what makes a port play: for two of the five the description already holds everything, and `Q.loadMoves` reading the snapshot when no `setupMoves` exists is not a fallback nobody uses -- it is the whole game. Where it is load-bearing, what is missing is nameable. Dragon's Lair Enhanced fails on `q.Tiers[0]`: the tier tables that decide which levels are mixed into a run are declared by the game's script and the converter does not snapshot them. Tron's divergence is its `swapScene`, a per-difficulty choice of scene that the snapshot does not carry either. Neither is a mechanic Forge lacks; both are data the converter has not been taught to lift. The next step for 14.5, then, is not a redesign: teach `forgePortKaris.lua` to write `Tiers`, `PlayOrder`, and the swap decisions into the description, give the loop a `mix` of its own to read them, and re-run the comparison with the hooks gone. Every title that then plays without its Lua is a title whose port is a description, which is the measure 14.15 set. Thirty-eight of the seventy-five descriptions define `setupMoves`, so that is the size of the prize. ### 14.18 What "complete" means for a port, and where each one stands (2026-09-24) The owner asked for a port to be called complete only against something checkable, after this session called ports "playing as the original does" on the strength of a trace comparison that never looks at the screen. That phrase is the harness's and it means one row of the table below. So: a port is complete when every row holds for the same inputs, each row has a check of its own, and a claim names the row and the check rather than the word. | Row | What it means | The check | Where the framework ports stand | |---|---|---|---| | State | The same moves judged at the same frames, and the same score, lives, and level | `testScripts/ports/compare.sh` compares traces | 19 titles agree with none of the game's Lua; see the matrix | | Picture | The same screen: arrows, life bar, score, prompts | Screenshots diffed at fixed frames. Not built | Nothing of the game is drawn at all | | Screens | Service menu, save menu, name entry, two-player, movie player, each reachable and behaving | A drive into each. Not built | Not played: the loops send those states back to the attract loop | | Sound | The same clips at the same moments | Not built | The framework's eight sounds play, and the game's own play through the shim; unverified | | Persistence | Dips, saves, and boards survive a restart | Not built | Unknown | | Controls | Every switch the original honours, the service key included | Not built | Partial | | Independence | The port runs with none of the game's own files | `util/forgeStandalone.py` | 19 of 36 titles | Three rules go with it, and they bind the writing as much as the work: * **Name the check, not the conclusion.** "The traces agree" is a fact. "Plays as the original" is a claim about rows nobody tested. * **A row with no check is unknown, not done.** Sound, persistence, and controls above are unknown, and saying nothing about them is not the same as covering them. * **Complete means every row.** Anything less is named by its rows. These ports are state-complete and, for 19 titles, independent. Nothing more. Why the drawing is missing rather than broken: the shim collects every drawing call a game makes into a queue (`Q.queueDraw`), and a look is supposed to flush it. `authorFlushDraws` is there and nothing calls it, because the look that would have belonged to the drawing work parked in 14.15. A description's game entity therefore takes the `none` look, and the queue is dropped every frame. **The matrix.** "Without its Lua" is the standalone sweep of 2026-09-24, run after the converter was fixed to snapshot the tier tables and the scene swaps (14.16 is the pixel reading; the swaps are in this session's converter work). "As it ships" is the same comparison with the game's own files where they are, which is only interesting for a title that needs them. | Title | Family | Without its Lua | As it ships | |---|---|---|---| | AlteredCarbon | karis | agrees | agrees (ships without it) | | Arcade_Xperience_Vol1 | hypseus | agrees | agrees (ships without it) | | Arcade_Xperience_Vol2 | hypseus | differs | DIFFERS | | Arcade_Xperience_Vol3 | hypseus | differs | DIFFERS | | Asterix | karis | agrees | agrees (ships without it) | | Astroboy | hypseus | differs | DIFFERS | | badlands_lite | hypseus | agrees | agrees (ships without it) | | BD13 | hypseus | differs | agrees | | ChantzesStone | karis | agrees | agrees (ships without it) | | CliffHanger | karis | agrees | agrees (ships without it) | | Danmachi | hypseus | differs | DIFFERS | | DragonsLair2Enhanced | karis | agrees | agrees (ships without it) | | DragonsLairEnhanced | karis | differs | not checked | | Esh_Aurunmilla | hypseus | differs | agrees | | FBC | hypseus | differs | DIFFERS | | FireAndIce | karis | agrees | agrees (ships without it) | | Freddy | hypseus | differs | DIFFERS | | FridayThe13th | karis | differs | DIFFERS | | FumaConspiracy | hypseus | differs | DIFFERS | | hayate_1080 | hypseus | differs | agrees | | Linea | karis | agrees | agrees (ships without it) | | mazinga | hypseus | agrees | agrees (ships without it) | | Mononoke | karis | differs | DIFFERS | | NinjaHayateHD | karis | agrees | agrees (ships without it) | | OeilPourOeil | karis | agrees | agrees (ships without it) | | sintel | hypseus | differs | agrees | | Sonic_the_Hedgehog_1996 | hypseus | agrees | agrees (ships without it) | | SpaceAceEnhanced | karis | differs | not checked | | StarshipTroopers | hypseus | differs | agrees | | SuckerPunch | karis | agrees | agrees (ships without it) | | Sugar_Rush | hypseus | differs | DIFFERS | | Survival | hypseus | agrees | agrees (ships without it) | | TheElderScrolls | karis | agrees | agrees (ships without it) | | TimeGal | karis | agrees | agrees (ships without it) | | TitanAE | karis | agrees | agrees (ships without it) | | Tron | karis | agrees | agrees (ships without it) | Ten ports differ as they ship, which is the finding this table exists for: Arcade Xperience Volumes 2 and 3, Astroboy, Danmachi, Future Boy Conan, Freddy, Friday the 13th, The Fuma Conspiracy, Mononoke, and Sugar Rush. Every one of them was bisected and every one predates this session: each was run with its own pre-session description and the pre-session runtime (721a0d120, the 13,628-line Author.singe, before the scene swaps) and differed there too. So nothing in 14.16 or 14.17 caused them, and the cause of each is still unknown -- what is known is that a port that differs as it ships is not state-complete, and none of the ten should be offered as playable until it is. A trap for whoever bisects next: do not take HEAD for the pre-session runtime. The owner commits while work is in progress, so a session's own changes can already be in HEAD -- 25aa909c3 carries the scene swaps -- and the first bisection here compared the session's runtime against itself and proved nothing. Name the commit. Seven ports keep their Lua and agree: Dragon's Lair Enhanced, Space Ace Enhanced, Sintel, Esh's Aurunmilla, Starship Troopers, Brain Dead 13, and Ninja Hayate 1080. Those are state-complete but not independent. With the nineteen that stand alone, that is twenty-six of thirty-six titles state-complete and ten not. ### 14.19 The ten that differed, diagnosed (2026-09-26) All ten ports that differed as they shipped (14.18) now agree: the same traces as their originals under `testScripts/ports/compare.sh`. That is the State row and nothing more; the other rows of 14.18 are as they were. Three causes, all in the runtime rather than in any description, and none of them new: each predates 721a0d120, as 14.18's bisection already said. **How it was run.** A binary of its own, so another session could keep working on the one in `.builddir`: the inner project configured directly (zig, Release, the default preset's installed libraries, `SINGE_SUPERBUILD` off) into `~/claude/portwork/build`, built once from the working tree on 2026-09-26 and left frozen for every comparison below. Configured that way nothing copies the binary into `.builddir`. The diagnosis used copies of the drivers outside the repository with two additions: a per-frame line of the game's flow, screen, step, and disc frame after each update, and a line for every `disc*` call with the frame it was made on. Diffing those two runs frame by frame finds the first frame the port parts from the original, which the traces cannot: a trace records disc frames, so a port that is one overlay frame behind the original agrees on every line but the last. **1. The countdowns' arithmetic** (Astroboy, Friday the 13th, Mononoke, Sugar Rush). Astroboy's traces differed only in their last line, 30400 frames against 30401. The per-frame diff put the parting at frame 2783: the difficulty screen, whose `timerON(30)` ends it after thirty seconds, let go a frame later in the port and the port stayed a frame behind from there to the end. Thirty seconds on the deterministic clock's 15 ms step is exactly 2000 frames, so the frame the timer is due is decided by rounding. The framework sums `os.clock()` deltas, `iSecs = iSecs + thisSeconds - lastSeconds`, which Lua evaluates as `(iSecs + thisSeconds) - lastSeconds`, and compares the sum; the port compared `authorTime() - from` against the limit. Both are the same ticks, but the two sums round differently, and on the frame that matters one reaches 30 and the other falls a hair short. Every copy of `timerDue` in the library (148) and of `joyDelayDue` (95) does the framework's arithmetic, so the port's timer and joystick delay now share one countdown that does exactly that, with `os.clock()` as the clock, and holds while the game is paused as the framework's does. **2. The language** (FBC, Fuma Conspiracy, Danmachi's start). FBC's traces differed on one line: the first screen of a game was traced at disc 328 in the original and 329 in the port, and everything after agreed. The per-frame diff showed the same state on every frame, so the difference was between frames: the call log showed the original calling `discAudioSuffix` on the frame the game starts and the port calling nothing. That call is the framework's `setLang`, run by `readConfig` from `initJob`, and it holds the disc for the frame. `readConfig` only calls it when the saved config has a `dip_Lang` line; Astroboy's and Arcade Xperience's configs have none, which is why they agreed. More than a frame: Fuma Conspiracy's config says `dip_Lang = 1`, and `Video/fuma-it.ogg` is there, so the original plays in Italian and the port played the default track. With the fix, both runs call `discAudioSuffix("-it")` on frame 677 (checked from the call log; nobody has listened to it). The converter records a dip only when a config file names it, so the snapshot having `dip_Lang` is exactly the condition, and the KarisFramework and Kimmy loops now call the framework's `setLang` on it at init. **3. The game over after a continue** (Arcade Xperience 2 and 3, Danmachi, Freddy). At game over the original seeks to the game-over clip and the port did not. The port's `doGameOver` came from 3.31c, which plays on from the continue clip when the disc is at its end; 3.32b always seeks. All the test library's own ports are 3.31c; the Hypseus library's KarisFramework titles are 3.32b. The shortcut now applies to 3.31c only. | Title | Family | Now | Cause | |---|---|---|---| | Arcade_Xperience_Vol2 | hypseus | agrees, 188 lines | 3 | | Arcade_Xperience_Vol3 | hypseus | agrees, 224 lines | 3 | | Astroboy | hypseus | agrees, 226 lines | 1 | | Danmachi | hypseus | agrees, 83 lines | 2, 3 | | FBC | hypseus | agrees, 97 lines | 2 | | Freddy | hypseus | agrees, 165 lines | 3 | | FridayThe13th | karis | agrees, 344 lines | 1 | | FumaConspiracy | hypseus | agrees, 94 lines | 2 | | Mononoke | karis | agrees, 369 lines | 1 | | Sugar_Rush | hypseus | agrees, 289 lines | 1 | **Then shown.** Causes 1 and 2 change what every KarisFramework and Kimmy port does, including the twenty-six that already agreed, so they were not "fixed" until the whole sweep agreed again on the same binary; it did (the sweep, below). Also noted and not changed: the Kimmy loop's end of the attract clip leaves `bPause` and `lvlState` alone where the framework resets both, which shows in the per-frame state for one frame and in no trace. **The sweep, on the same binary (2026-09-26).** Every one of the 36 titles of 14.18's matrix agrees on State with causes 1 and 2 in place: the 26 that agreed before still do, and the ten above. So do the map-mode titles outside the matrix (Conan, Daitarn 3, the TV Show, Dragon Trainer, Puss in Boots, Samurai Jack, Super Don Quixote, Freedom Fighter, Time Gal HD, Star Blazers) and the rest of the Hypseus titles (Mazinga, Sintel, Sonic, Starship Troopers, Sugar Rush, Survival). That is State only, as before. **What the sweep itself got wrong.** `sweep.sh` ran every description under a library `Script/` folder with the KarisFramework driver, whatever its `kind`, so the gun games, the time-travel game, and the text adventure each produced a trace of one line and "differed" for that alone. It now picks the driver from the description's `kind` (`familyOf`), as its Hypseus loop already did. **Three defects the gun games had, found on the way.** The shim's `Q.loadSprite` called `spriteLoadFrames(path, frames)`, the wrong way round for the engine's `spriteLoadFrames(count, filename)`, and a Hypseus gun game stopped at its first sheet. A game that asks for the old sprite argument order and loads the engine's `Framework.singe` through the shim got its `spriteDraw` wrapped twice, once by the framework and once by the shim, so a y became a sprite handle; the shim now lets the framework wrap and then takes back the two calls it converts itself. And one game's vocabulary reimplemented `initJob` without the score digits' positions and sprites, which only showed once an uncapped run reached the score. The Reference's `spriteLoadFrames` example had the arguments reversed too, and is corrected. **Outside the matrix, with the right drivers (2026-09-26).** Every other family now agrees on State too -- the gun games in both libraries (Crime Patrol HD in both, Drug Wars and its HD, Last Bounty Hunter and its HD, Mad Dog McCree II and its HD, Space Pirates and its HD, Who Shot Johnny Rock and its HD, Johnny Rock Noir, and the typing edition), Platoon, Jeeg, the time-travel game, and the text adventure -- but for the exceptions below. Getting there took, as well as the three defects above: the vocabularies' own `initJob` put back what the original's draws on (score digits, lives stars, the LCD's words, the arrows), because an uncapped run reaches the drawing and the drawing stopped the run; the time-travel game's picture drawn only while the game runs, as the original's is; Last Bounty Hunter HD's pointer setting the cursor and the shot's mark as the original's `onMouseMoved` does; the shim's `spriteDrawFrame` taking every old form (with a scale, or two); the drivers' pair kinds skipping a constant the game does not define (Jeeg has no diagonals); and `compare.sh` finding a gun game's video when it keeps no framefile, and saying so when a title has no video at all. The exceptions, open: **the library's Crime Patrol** (not the HD) left no result in the batch; run alone, its two traces are the same 321 lines, but neither run ends with the drive's end line, so both were stopped by `compare.sh`'s hour rather than finishing. It agrees as far as both got, and why its drive never reaches an end is not known. **Mad Dog McCree HD** misses one seek, entering the town select after a continue (the original seeks back to the menu frame, the port is five frames further on), and agrees before and after it; finding why wants the per-frame instrumentation of 14.19 on the gun-game drivers. **The Hypseus `WSJR_HD`** is not compared: its description sits at the title's top level rather than in `Script/` beside its script, where 14.13 says it lived when it agreed, so no driver finds it; where it belongs is the owner's to say. All of this is the State row; the drawing that now runs is not checked. **Where the fixes live, and why that is temporary.** All three went into the framework-emulating loops in Author.singe, beside the code they correct: `Q.setLang`, a `Q.karis332` check, and the framework's countdown. That is framework knowledge in Forge's runtime under framework names, which section 15 sets out to end. They move with the rest of the loops when it is done. ## 15. Design: Forge as its own thing (written 2026-09-26) The owner, after 14.19 put three more framework fixes into Author.singe: "I want Forge to be as generic and capable as possible. If cloning the Karis framework is what provides Forge with QTE capabilities, that's fine. But we can't call it 'Karis' or refer to any other third party framework name. Ideally, Forge is its own thing that is flexible enough to handle everything these other frameworks provide." And: cover everything Forge can do, not just QTE, and where an existing game does something Forge cannot, find a generic way to support it. This section is the plan for that. Nothing in it is built. It was written from four surveys made the same day -- Forge's whole vocabulary and editor, the engine API against what Forge can reach, every family in the library against what its port needs, and the five game loops in Author.singe side by side -- and every defect it reports was checked against the code before it was written down (15.7). ### 15.1 The rules 1. **No third-party names in Forge.** Not in a behaviour, layer, look, event, condition, action, parameter, or `kind`; not in help text, the generated vocabulary, the editor, the book, or the runtime that ships inside every release. A framework, engine, company, or game named as the *source* of a behaviour is the thing this rules out. Library and format names a user needs to know (glTF, RmlUi, Jolt, WAV) are not sources of behaviour and stay. 2. **What a framework does becomes a Forge capability.** Cloning a framework's behaviour is fine; shipping it as that framework is not. The capability gets a Forge name, a Forge data model, and is open to every game, not only the ports it came from. 3. **Differences are options, not versions.** Where two frameworks (or two versions of one) disagree, the difference is a named option with a plain meaning, set in the description. Nothing at run time asks which framework or version a game came from. 4. **A game's logic is data, or its own vocabulary.** What a description can say, it says. What only one game does goes in that game's `Vocabulary.singe` (14.7), under names that are the game's own business. Running a game's original Lua through a shim is a porting stage, not a destination. 5. **Only the converter knows where a game came from.** It has to read those games, so it may name them; everything it writes uses Forge's vocabulary. It is renamed from `util/forgePortKaris.lua` to a generic name when the rest moves. ### 15.2 What Forge can do today For the record, so the gaps below are measured against something. The vocabulary is `docs/ForgeVocabulary.adoc`, generated from Author.singe. * **Layers:** bezel, disc, hud (an RmlUi document with ids bound to vars), music, overlay, parser, scene3d, world2d. * **Looks:** billboard, box, grid, light, mesh, model, none, particles, sprite, text, text3d. * **Behaviours:** animator, body, branching, camera, character, drift, frames, gun, health, hitbox, hotspot, joint, keys, lightSensor, mover, patrol, platformer, pointer, projectile, racer, seek, shooter, soft, solid, sound, spawner, spin, target, thrust, timer, trigger, turret, vehicle, walker, water; and the five game loops (15.3). * **Events:** animationDone, arrived, branchOpen, branchTaken, branchMissed, collision, death, enter, leave, frame, frameReached, gameOver, hit, miss, midi, patrolEnd, pressed, released, railEnd, roomStart, roomEnd, said, soundDone, spawn, stopped, timer, verb. * **Conditions:** below, chance, discBetween, every, has, hitPart, hover, inState, keyHeld, keyPressed, onGround, onScreen, once, pictureAt, pointerIn, pointerOffscreen, switchHeld, test, timeBetween, touching, waiting. * **Actions:** addScore, addVar, setVar, credit, damage, destroy, spawn, spawnAtPointer, show, moveTo, setState, setText, die, gameOver, restart, discPause, discPlay, discTo, emit, fade, flash, shake, face, lookAt, walkTo, walkToHotspot, walkToPointer, goTo, jump, run, saveGame, loadGame, lua, nextVerb, setVerb, useItem, give, take, pathNext, cameraCut, playAnimation, push, reload, playMusic, stopMusic, playSound, say, talk, submitScore, timerStart, wait. * **Structure:** types and instances with vars and states; rooms that keep their state; rules with triggers, `when`, `do`, `each`, `interrupt`, and cut-scene `controls = false`; expressions; tracks (hitbox boxes keyed by frame or time, branches, rail points with stops, racer lines); waves; sequences as coroutines; dialogue trees; walk polygons baked to a navmesh, hotspots, verbs, and a verb/noun parser; whole-game save slots and a best score; a per-game vocabulary file. * **Editor:** a chooser, five panels (entities, types, rules, tracks, dialogues), manifest-driven fields with pickers, rename that follows every rule, polygon drawing, a timeline strip over the disc, a 3D viewport with gizmos, undo and redo, save, build with diagnostics, play from the room, export, and publish to the master service. Of the engine's 510 registered functions, Forge's vocabulary reaches 169; 24 more are reached only by the game loops; the other 317 are reachable only through the `lua` action or a game's vocabulary file. 15.5 takes the ones worth a Forge feature. ### 15.3 Third-party names to retire | Where | Now | Becomes | |---|---|---| | behaviour | `qte` | stays `qte`: the one Forge QTE behaviour (15.4), which the other four fold into | | behaviour | `kimmy` | folded into `qte`; its differences are options | | behaviour | `rdg` | folded into `qte` (segmented levels, level map) | | behaviour | `timegal` | folded into `qte` (checkpoints, a timed choice, quota lives) | | behaviour | `sdq` | folded into `qte` (trophies, percent finish, order after a beaten or unfinished level) | | game vocabularies | `kind = "alg"` (25 titles) | a Forge `gunGame` capability built from 15.5's gun model; until then, each game's own vocabulary under a neutral name | | game vocabulary | `kind = "timetraveler"` | the game's own vocabulary under a neutral name, then Forge features (15.6) | | game vocabulary | `kind = "videoland"`, look `console` | a Forge `console` look and a Forge text-adventure capability (15.5, text input) | | game vocabularies | look `hud` (ALG, the time-travel game) | renamed: it collides with the `hud` layer, and becomes 15.5's HUD widgets | | help text | `branching`: "Dragon's Lair:" | a plain description of branch windows (help text is Forge itself: the editor and the generated vocabulary show it) | | help text | `parser`: "the Sierra way"; `die`: "The Sierra death" | plain descriptions, for the same reason | | help text | `qte`, `kimmy`, `rdg`, `timegal`, `sdq`: KarisFramework, MazescaterFramework, Kimmy Script Engine, Karis's 2024 framework, RDG's lineage, RDG2010, Time Gal, Super Don Quixote | the folded `qte`'s own help | | runtime | section headers naming KarisFramework, the Kimmy Script Engine, RDG's map mode, Time Gal, Super Don Quixote; comments naming Hypseus, American Laser Games, Mad Dog, ZeroBrane | Forge's own section names; comments say what, not whose | | runtime | `KIMMY`, `KIMMY_MASK`, `KIMMY_BIT`, `Q.mazescater`, `Q.karis332`, `q.hayate`, `q.kimmy`, `q.rdg`, `q.timegal`, `q.sdq`, `qte.framework` | gone: options (15.4) and one constants table | | data | `qte.framework = "3.31c"` and kin in every ported description | gone; the converter writes the options instead | | book | the ports paragraph (KarisFramework, Hypseus, LINEA, Mazescater, Kimmy, RDG, Time Gal, Super Don Quixote, American Laser Games, Mad Dog McCree, Platoon) | Forge's QTE and gun-game chapters, in Forge's words | **Describing a style of game in the manual is fine** (the owner, 2026-09-26): the book may say an adventure plays the way Sierra's and LucasArts' did, that branch windows are the Dragon's Lair kind of game, that a tutorial builds a gun game in the manner of Mad Dog McCree, or that the light sensor serves ActionMax tapes. Those names describe what the reader already knows. What they may not do is name anything in Forge itself -- a word of the vocabulary, a help string, the editor, the runtime -- or present a Forge capability as a framework's. The book's paragraph about the ports still goes, because it names frameworks as the source of Forge's behaviour. ### 15.4 Forge's QTE: one behaviour, its own model Everything the five loops do, as one behaviour whose model is Forge's. The loops already share one skeleton; what differs is a list of choices. **The game's shape.** A title clip, then the attract loop, then play. The attract loop is a list of steps, each a clip or a still with a hold time, any of which a button skips; credits (coins or free play, a cap), a start input, an optional secret input sequence that starts a hidden level. Play is levels of scenes of moves, with these screens around it, each optional and each a clip or a still with its own inputs: difficulty select, level select or map, new-game menu (new, continue an autosave), level intro and get-ready, hints on a death, continue, game over (normal and alternate), level clear with a bonus roll, finish (points, percent, or a tally), high score, trophies. **A move.** A window (`from`, `to` on the disc), a judge, and what follows: where play continues on a right answer, and what a wrong answer or no answer does -- a death clip (named, from a list, or drawn at random), harmless, or counted as right. A path move names where each answer leads (another move, a later scene, a death, the end of the scene). Frames may be written relative to the level's intro. Scenes may be mirrored (a random draw per scene, an offset for the scene and for its deaths, left and right swapped). **Judges** -- the kinds, named for what the player does: press (one input, optionally a second accepted one); chord (inputs together, nothing else); hold (for a length the difficulty sets); letGo (held at the start, released by the end); mash (a rate of presses of one input or alternating inputs, decaying, scaled by window length and difficulty); count (an input pressed n times); sequence (inputs in order); circle (the stick round, quarter, half, or whole, then an input); choose (n options shown shuffled, a timeout default); path; yesNo; timed (an input within a sub-window); skip (any input skips ahead); jump (to a scene or level, no input). Each judge's arithmetic -- how a mash decays, how long a hold must last at each difficulty -- is a parameter with the frameworks' values as presets, not a second implementation. Tilt (inputs outside a window count against the player, a warning at half the limit) is an option on the behaviour. **The rest of the model.** * *Lives:* a count per credit, a life bar (size, cost of a miss, right moves to regain one), one life, or none (practice); extra lives every n points or at a quota and every m after. * *Retry after a death:* restart the scene, replay the move from a lead-in, restart the level, carry on from the next move, or resume from the last checkpoint; per level, what a failed level does (replay, move on, one retry, come back later in the order). * *Order:* sequence, tiers, random with a fixed last level, map, or level select; optionally skipping beaten levels, and a next level chosen by a rule (15.5, progression). * *Scoring:* per move (plus per difficulty), per scene, a death penalty, per level, a no-death bonus, finishing the game, the secret level; or moves counted, or deaths counted, or a percent; a cap. * *Difficulty:* 0-3, each with a window delay, the judges' presets, and which hard judges fold into simple ones at the easiest setting. * *Extras:* per-difficulty scene swaps; clips a setting turns on (get-ready, after-death, level clear); a preview of the next move; a prompt sound; the disc's audio language (15.5). **Options.** The switches the five loops use today, each as what it means: | Today | Option | |---|---| | 3.32b's always-seek game over after a continue (`Q.karis332`) | `gameOverAfterContinue = "seek"` or `"playOn"` | | 3.32b's secret input, and the later map-mode copies' | `secret = { inputs = {...} }` | | Mazescater's compound kinds, 4 s hints, mash rate 6, any switch counts for a mash | the sequence and circle judges; `hints = 4`; the mash judge's `rate` and `anySwitch` | | Kimmy's held-input mask | the chord judge (it is exactly "these inputs, nothing else") | | Kimmy's text arguments for loops, paths, and multis | the circle, path, and count judges' own fields | | Kimmy's new-game menu, level select, exit menu, tilt, next-move preview, percent game | screens and options above | | `q.later` (map-mode copies) | `clipEnd = "exact"` or `"atOrPast"`; attract holds as data; a quit input | | `q.hayate` (a later Kimmy copy) | `extraLifeInTwoPlayer`, `retryAtLevelStart`, `creditCap`, `continueRestartsLevel`, `promptSound` | | `q.MazeGame` | `order.advance = false` | | `q.tweaks.*` (per-title, map-mode) | the option each tweak stands for | | `dip_GameType` (five different meanings across the loops) | split into `lives`, `scoring`, `simplify`, and `order` | | `dip_PlayStyle`, `dip_Rewind` (numbered differently in two lineages), `dip_Extravid`, `dip_Hints`, `dip_Display`, `dip_ShowAction`, `dip_MashtoRun`, `dip_HoldtoLoop`, `dip_MashRes`, `dip_Tilt`, `dip_Kidmode`, `dip_MustBeatLevel`, `dip_SortStyle` | named settings (15.5, settings), each with the values above | | the snapshot's `dips` present for `dip_Lang` (14.19) | `languages` and the chosen one | **The data.** The description's `qte` table today is a dump of the original script's globals under the frameworks' names (`dip_*`, `offsetGameOver`, `LangOpt`, positional move rows, a `settings` table of every global) with a separate snapshot of levels. It becomes Forge's model: `clips` (title, attract steps, get-ready, continue, game over, alternate, high score, rankings, clear, finish, hints, secret), `deaths` (a list of clips), `levels` (name, intro, mirror, what a failure does, scenes of named move rows, per-difficulty variants), `scoring`, `lives`, `order`, `secret`, `board`, `saves`, `languages`, and the options. One source for each: today level metadata lives twice (`levels` and `settings.Level`), the constants are copied from `settings` over Author's own table, and a dip can be in both `settings` and `dips`. **Hooks.** What data cannot say stays in the game's own vocabulary as named hooks the behaviour calls (a move's score, the next level, a death clip), and 15.6 turns the common ones into data. **Drawing.** The loops compute what a player should see -- `bShow*` flags, the prompt, score, lives, the bar -- and draw none of it. With the model in Forge's hands, that becomes 15.5's HUD widgets bound to the behaviour's vars, which is the Picture row of 14.18. ### 15.5 What Forge needs, for every kind of game Each item is a capability some game in the library has, or the engine offers and Forge does not reach, phrased as a Forge feature. "Needed by" names who is waiting on it; "engine" means the engine already has it and Forge only has to reach it. **Game structure** * **Random choice among authored alternatives.** A `choose` field wherever a value is written: one of these clips, orders, scenes, move rows, or deaths, drawn once per game, level, scene, or use, from the game's one seeded stream. Needed by: the seven hosted titles (15.6), the map-mode titles, the gun games, the time-travel game. * **Progression as data.** A level's successor as a rule: sequence, tiers, random, or "the first of these whose condition holds" over what is beaten, collected, or set; skip beaten; a bonus or secret level on a condition. Needed by: Space Ace Enhanced, Brain Dead 13, Starship Troopers, the gun games with outlaw chains, every QTE family. * **Per-level and per-move overrides.** Any constant (a move's score, a clip, an extra-life step) overridable on a level or a move. Needed by: Dragon's Lair Enhanced, Aurunmilla, Ninja Hayate 1080. * **Counters, quotas, and tallies.** Named counters with a deadline (a turn limit), countdowns, streaks, and a tally screen with a formula. Needed by: the text adventure's bomb, the gun games' streaks and crystal clock, the finish tallies. * **Settings.** A declared list of the game's options (label, values, default, what each gates), persisted, with a generic options screen and restore-defaults; any field of a description can be gated on one. This is what the frameworks' dips and service menus are. Needed by: every family. * **A service mode.** Entered by the service input, drawn by the options screen, with the pointer as a cursor. Needed by: every family (14.15). * **Score boards with name entry.** Several boards per game (by difficulty or mode), extra columns, entry by letter grid, stick, keyboard, or on-screen keys; shown locally and, with the master service, online (engine: `scoreBoard`, `scorePlayerName`, `scoreBegin`). Needed by: every family. * **Save slots with the game's own vars.** Progress plus any flags or collectibles, a load/save screen, erase slot, reset best (engine: `saveDelete`, `saveClear`). Needed by: the QTE families, Brain Dead 13. * **Two players.** Alternating (one set of controls, turns, per-player vars) and simultaneous (per-player vars, device routing, twin boards, a tie-break). Needed by: the QTE families, the gun games. * **Attract mode and movie mode.** An attract cycle of clips, stills, and fillers with a timed "press start"; playing a game straight through as a film. Needed by: every QTE family; kiosks. * **Languages.** A list of languages with an audio suffix or track each (engine: `discAudioSuffix`, `discSetAudioTrack`) and per-language text; chosen in settings, applied at start. Needed by: the Hypseus QTE titles, the Italian titles, the gun games with Spanish audio. **Input and judging** * **A judge vocabulary for any game**, not only the QTE behaviour: 15.4's judges as conditions a rule can use over any window, so an action game can ask for a chord or a mash as easily as a QTE can. * **Choices and menus over video.** N options on a still or a clip, as panels, quadrants, or cursor positions, shuffled or fixed, with a timeout default and skip-if-done. Needed by: Time Gal's time-stop, the TV Show, the gun games' menus, every map. * **A map or level select.** Positions, cursor movement rules, completion state, and a still per level. Needed by: Sucker Punch, Friday the 13th, Starship Troopers, the time-travel game, Triad. * **Text input.** A line with editing, any-key waits, typed words judged in a window, and a `console` look (wrap by font, cursor, fixed width). Needed by: the text adventure, the typing edition. * **Gamepads and rumble.** Analog sticks into dx and dy with a dead zone, a rumble action (engine: `controller*`, `controllerDoRumble`). Needed by: the Captain Power and Video Driver titles, any action game. * **An interactive GUI.** Click and change events from the hud document, show and hide actions, two-way binding (engine: `guiSetHandler`, `guiGetValue`, `guiShow`, `guiHide`). This is also how the options screen, name entry, and menus above get drawn. * **The gun model, completed.** Per-gun ammo, reload by offscreen, border, or button, bullet on press or release, fire delay, random jam, pickups, second-chance windows, hit classes (enemy, civilian, bonus) with rules, shots judged on the next frame. Needed by: the 25 gun-game titles. **Picture and sound** * **HUD widgets bound to vars.** Digits from a sprite font, icons for lives, a bar, an ammo row, blinking text, a scrolling ticker, gauges, and an animated move prompt that shows the judge. Needed by: every ported family (the Picture row); every original game. * **Disc control.** Speed changes, holding the disc for a delay, several videos in one game, audio tracks, subtitles, and the picture effects (engine: `discChangeSpeed`, `discSetAudioTrack`, `srt*`, `vldp*`). Needed by: the typing edition, Cops, the language titles. * **Colour reads from the picture.** `pictureAt` reads brightness; a colour or YUV read and a focus area (engine: `vldpGetPixel`, `vldpFocusArea`). Needed by: Captain Power, Video Driver. * **Video outside the disc.** A video look for the overlay or a 3D surface, and a play-video action that waits (engine: `video*`, `materialSetVideo`). * **Sound control and positional audio.** Stop, volume, pause, fade; a positional sound on an entity (engine: `soundStop`, `soundSetVolume`, `soundSetNode`, `soundSetRange`, `soundSetListener`, `musicPause`). A clip cannot be stopped today. * **Music playlists.** Several tracks switched by state, volume, a user-editable list. Needed by: Starship Troopers, Gallagher's Gallery, Space Rocks. * **Vector drawing.** Line, circle, ellipse, and polygon looks for overlay-only games (engine: `overlayCircle`, `overlayEllipse`, `overlayPlot`). Needed by: Space Rocks, Minesweeper. * **Tweens.** A tween action over any field or var with an easing curve and a duration, that can wait (engine: `tweenValue`, `EASE_*`). * **Particles with an appearance.** Texture, frames, blend, trail, collision, spin, direction, and drag, plus start and stop (engine: `emitterSet*`). Today they are coloured dots. **3D and physics** (engine features Forge does not reach; no ported game needs them, every original 3D game would): materials (normal, emissive, tiling, blend, double-sided) and a set-material action; labels over 3D entities (`sceneProject`); bloom, tonemap, environment, antialias, and shadow quality on the scene layer; several views (split screen, mirrors); animation layers with masks and weights, pause, and morphs; terrain from a heightmap; wander and line-of-sight for AI (`navRandomPoint`, `navRaycast`); velocity, enable, conveyors, hinge limits, pinned cloth, swim and slope on the character, gears and brakes on the vehicle. **MIDI.** A MIDI layer that opens the input port (the `midi` event needs Lua to open it today) and a note action for output. **The editor.** A canvas at the game's own size (ActionMax's is 360x240); play from here with the editor camera; orbiting by pointer; navmesh baking as an editor action; the chooser drawn by the RmlUi document. All were planned (sections 1-3) and not built. ### 15.6 The titles that still need their own Lua, and what absorbs it * **Dragon's Lair Enhanced:** per-level constants (overrides), one of three fixed orders drawn at random (random choice, progression), a bonus for 24 named moves (per-move overrides). * **Space Ace Enhanced:** a random game-over clip and extra death clip (random choice), the next level by what is beaten, the difficulty, and a coin flip (progression), move rows gated on difficulty and beaten levels (settings gating, progression conditions). * **Aurunmilla:** one draw per game picking some moves' death clips (random choice, drawn once per game), per-level move score (overrides). * **Starship Troopers:** a real map with skip-beaten and a bonus stage after four (map, progression), map music and two tracks (playlists). * **Brain Dead 13:** seven collectible flags set by moves and saved per slot, which pick an alternate final level (vars on moves, save slots with the game's vars, progression); random death clips by range and an extra death by level (random choice, overrides); a random alternate scene. * **Ninja Hayate 1080:** a random finishing bonus by difficulty, settings that force or gate other settings, extra clips gated on a setting (random choice, settings gating). * **Sintel:** no logic of its own (empty add-ons, 83 plain move rows). Why it needs its Lua is undiagnosed; the likely cause is a value the converter does not lift, not a mechanic. First to look at. * **The map-mode titles** run their `main.singe` data functions every play because their moves are drawn at random (random choice over move rows), and their per-death clip mappings are 2,000 lines of code (deaths as data). * **The gun games** (26 vocabularies, 54,627 lines): the gun model, hit classes, menus over video, counters, money instead of lives, puzzles as choices, two players. The puzzles (roulette, cards, a safe dial) are the last mile, and each is a game's own vocabulary under 15.1's rule 4. * **The text adventure:** text input and the console look; its parser's ordered rules that may re-enter are the one thing Forge's parser layer lacks (14.14). * **The time-travel game:** shuffled periods (random choice), deaths by cause (deaths as data), reversal to a mark (disc control), a map, a trader and a slot machine (menus, counters). The titles not ported at all (Future Boy, Road Blaster, Cracke, Triad, the Singe 1 monoliths, and the overlay programs such as Cops, Fast Draw, Gallagher's Gallery, Captain Power, Video Driver, Minesweeper, and Space Rocks) need the same list plus gamepads, rumble, colour reads, vector drawing, and speed changes. ### 15.7 Found while surveying, checked * **A game's colour calls do nothing.** The shim's helper table defines `colorForeground` twice (Author.singe 7562 and 7656); in a table constructor the later wins, so the no-op replaces the real one. A defect. * **Publish cannot be reached from the keyboard.** Forge's `onKeyPressed` sends P to `forgePlay` whenever no field or picker is open (Forge.singe 5096), so `forgeKey`'s P for `forgePublish` (4165) never runs. The book says both. A defect. * **The loops never clear what a game draws.** A game's own Lua draws through `Q.queueDraw`; only the game vocabularies' looks call `authorFlushDraws` or `authorDropDraws`, which Author.singe defines and never calls, so under the five loops the queue grows by every draw call a game's Lua makes, every frame. A defect, and the reason nothing a game draws is seen. * **The book is stale about the light sensor:** it says the sensor lives in the ActionMax games' own vocabulary, not Forge's; it is core now. * **Quirks copied faithfully, not defects:** the Kimmy loop's hold decay can never run (its test is always true), the map-mode Super Don Quixote loop plays a one-frame get-ready clip on one path, and a choice menu handles only two to four options. Each was checked against the original framework, which does the same; a port must, to agree. In Forge's own model they become the judges' presets and are free to be right. * **Checked and not true:** that Kimmy's scorer takes its arguments in a different order from the KarisFramework loop's. ### 15.8 Order of work, and how each step is proved The measure for every step that touches the loops is the port sweep: every title that agrees before the step agrees after it, on the same binary. The baseline is the sweep run on 2026-09-26 after 14.19 (recorded there when it finishes). A step that changes nothing a player sees must change no trace. 1. **The three defects in 15.7.** Independent of the rest. 2. **Names only.** The retirements of 15.3 inside the runtime, the help, the book, and the vocabulary files; `kind` values in descriptions regenerated by the converter. No behaviour changes; the sweep must not move. 3. **Versions to options.** Every check of a framework, version, or title becomes an option the converter writes (15.4's table). The sweep must not move. 4. **One loop.** Fold the loops into the one `qte` behaviour a piece at a time, starting with the screens that are near-copies in all five (continue, game over, high score, difficulty select, level setup, scene done, death clip setup), then the judges, then the level loop. The sweep after each piece. 5. **Forge's data model.** The converter writes 15.4's model instead of the globals dump; the loop reads it. The sweep. 6. **Generic features**, in the order that unblocks the most: languages, random choice and progression and overrides (these retire most of the hosted Lua), settings and the service mode, HUD widgets (the Picture row), boards with name entry, save slots, the judge vocabulary for any game, menus and maps, then the rest of 15.5. Each with a test scene and a Forge tutorial or recipe, since a feature only ports can reach is not generic. 7. **Retire the hosted Lua**, title by title (15.6), each proved by the sweep and by `util/forgeStandalone.py`. 8. **The converter renamed and its output checked** for any third-party name, which becomes a check in the sweep. ### Out of scope, stated so it is not assumed Binary compatibility with the frameworks' own save and config files. Keeping `kind = "kimmy"` and the other retired names working in old descriptions (the converter regenerates them, and section 3's rule stands: nothing has to be backward compatible). The completeness rows of 14.18 other than State, until the features in 15.5 that they need exist. ### 15.9 Steps 1 to 3, done (2026-09-27) **Step 1, the defects** (15.7): a game's colour calls, Publish (now Shift+P; P stays Play), and the draw queue the five loops never emptied. Done the day before, with scene 56 covering the export. **Step 2, names.** The four framework-named behaviours are `qteHeld` (was `kimmy`), `qteSegments` (`rdg`), `qteCheckpoints` (`timegal`), and `qteTrophies` (`sdq`) until step 4 folds them into `qte`; the constants table `KIMMY` and its kin are `HELD`, `HELD_MASK`, `HELD_BIT`, `HELD_WORD`, and `HELD_SIDE`; the trophy loop's helpers are `trophyWrong`, `trophyPath`, `trophyAfterDeath`, and `trophyStartScene`; `RDG_PAIRS` is `PAIR_SWITCHES`; the runtime's seed global is `AUTHOR_SEED` (the test driver sets it). Every help string, section header, and comment in Author.singe now says what a thing does rather than whose it was, the Forge book's paragraph on the ports does the same (and no longer says the light sensor is not Forge's), and the converter's header lines name no framework. The 18 descriptions that used the old kinds were changed in place, `kind` strings only. What is left in the descriptions is credits (an original author named as one) and players' initials on high-score boards, which are data. **Step 3, versions to options.** `Q.karis332` and `Q.mazescater` are gone, and nothing reads `qte.framework`. In their place `Q.option(q, name, default)` reads the description's `qte.options`: `gameOverAfterContinue` ("playOn" or "seek"), `secretInputs` (the switches held for the hidden level), `hintSeconds`, `alternateMashRate`, `mashAnySwitch`, and `doubleKind`. The defaults are the behaviour a description without the option always had. The converter writes the options from what it detects and no longer writes the version; the existing descriptions were migrated (11 got the seek-and-secret pair, one the compound-move four, the rest lost a line nothing read), and a converter run on three of them reproduces them. Proved by comparison on the frozen binary: one title of each behaviour and a gun game, capped at 15,000 frames, after each step, and Linea (the compound options) and Arcade Xperience 2 (the seeking game over) uncapped after step 3; all agree. The behaviour flags `q.kimmy`, `q.rdg`, `q.timegal`, and `q.sdq` remain: they say which loop is running, and step 4 retires them with the loops. **Found on the way: test runs had written to five titles' saved boards.** Aurunmilla, badlands, Survival, and Mazinga had a player called MOR in their `hscore.cfg` that the originals' zips do not have (Sugar Rush's zip does, so there it is real), and the descriptions of those four had been generated from the written boards. The boards are back to the zips' (the written ones are in `~/claude/portwork/polluted-boards`), the four descriptions were regenerated from them, and all four agree uncapped. Aurunmilla's converter output also changes from run to run, because its script draws `math.random(2)` as it loads; its description is whatever the last run drew, and it agrees. ### 15.10 Step 4, one loop: the styles and the shared screens (2026-09-27) **4a, one behaviour.** The four named behaviours are gone: `qte` is the only one, and `qte.options.style` picks how it plays: `windows` (the default, moves answered in a window of frames), `held` (moves judged by what is held), `segments`, `checkpoints`, or `trophies`. `QTE_STYLES` holds the five, each with its help and its attach; an unknown style is an error that names the ones there are. `q.qteStyle` replaced the four behaviour flags. **4b, the shared screens.** The held style kept its own copy of most of the windows style's screens. Each pair is now one function, and every real difference between them became an option in `qte.options` with the style's own default, so a description can ask for either behaviour whatever its style: | Option | What it does | Held default | |---|---|---| | `gameOverAfterContinue` | `"playOn"` from the continue clip when it adjoins, or `"seek"` | `"seek"` | | `rankingsAfterBoard` | the same choice for the rankings after the high score board | `"seek"` | | `levelSelect` | game types 2 and 4, and play style 4, choose their level | on | | `deathCountGameType` | the game type that counts deaths: no life lost, no points, fewest ranked, the total kept across levels | 4 | | `noGameOverGameType` | the game type that goes from the board back to the attract | 2 | | `saveIcon` | a disk shows where the game would save | on | | `managedHud` | the loop sets the score, lives, level, and record by the display setting through play, the get-ready, and a death, with a pause on the hints frame after a death on display 2 | on | | `barBonusRestarts` | the count to the life bar's bonus starts over at the bonus even with the bar full | on | | `hintedDeathCounts` | a death after the hints counts as the wrong move it was (the scene bonus reads the count) | on | | `pendingIsUnanswered` | a move still pending as its window closes counts as unanswered | on | | `wayOutPlaysTarget` | a way out plays the scene it names, done or not; off, it goes there if still to do and past it if done | on | | `extraDeathClipSkips` | BUTTON1 cuts the extra clip after a death short | on | | `waitForWindowStart` | a window that ends before it starts waits for its start, instead of closing at its end | on | | `outEndsSceneNow` | the out jump ends the scene in the same step, instead of the next | on | `Q.option` reads the description's options, then the style's defaults (`q.optionDefaults`), then the caller's. Where a shared screen must call the style's own start or frame setup, it goes through `q.qteFns`: `Q` for the windows style, and for the held style a table that gives its own function where it has one and the shared one otherwise. **4c, the level loop.** The two level loops are one, `Q.doLevel`. What is each style's own comes through `q.qteFns`: the judge (`Q.judge`, or the held style's move loop, now `K.judge`), the verdict on a wrong move (`Q.failed` or `K.failed`), and the resets of the judging state at a scene's start and after a right move. Tilt (`Q.checkTilt`), the next-move marker, two players' turns, and the scene and level jumps need no option: they act only on dips, settings, and move kinds the held descriptions have, and read through `q`, where the windows style has none of them. A move whose death is -3 passes as -1 does in both: no windows description has one. Two more differences surfaced only when the traces were compared, and are options: a window that ends before it starts (a windows title has one) closes at its end in the windows style and waits for its start in the held one (`waitForWindowStart`), and the out jump ends the scene in the same step in the held style and the next in the windows one (`outEndsSceneNow`). A scan of the descriptions had said no held title used the out jump; the held titles' paths are built by their hosted Lua, where a scan of the data cannot see, so the traces are the check, not the scan. Merged in 4b: enter scene, scene done, continue, game over, difficulty select, death clip setup, after death (with the resume and the scene-end test it used), the skip and let-go judges, level setup, next level, on to the next level, level replay, the high score board, the scorer, the board test, the start of a game, and the record shown (`Q.topFor`). The held style's local copies of the lives setup and the level-select jump are shared as `Q.livesForType` and `Q.toLevelSelect`. About 550 lines are gone. Two held names were a game's name and are not any more: `q.hayate` is `q.arcadeEdition` (the later engine's arcade mode, from its settings), and the comment that named the title says what the edition is. One latent fault went with the merge: the held attach put its scorer on `q`, where a game's own Lua calling `addPoints(points, value)` would have reached it without `q`; no description did, and the shim's helper now answers it for every style. Where the two copies differed in a way no option should keep, the shared one takes the more capable form, each checked against the library first: the game's `swapDeath` hook now works in every style (the windows copy ignored what it chose; no description defines it); `specialScore` sets the points in every style (the held copy wiped them; none defines it); the end-of-clip tests are `>=` everywhere (the frame is read after the seek); the right and wrong move counts start over with every new game (the windows copy skipped game type 3, where nothing reads them); a level replay setting below -1 draws the order again as the held copy did (no description has one). What is left of the held style is its own: the judges that test the mask of what is held, its move loop, level loop, attract, saves, new-game menu, level select, clear, finish (the percent made, where the windows finish counts a bonus down), and quit menu. The judges and the level loop are the next pieces. Proved by comparison on the frozen binary, to 20,000 frames: - 4a and 4b: the full port sweep (81 pairs) after the screen merges; 74 agree and none differ. The other seven were the segmented titles in the library's own layout, which `sweep.sh` had sent to the rdg family (it knows only the Hypseus layout, so the runs found no video); `sweep.sh` now sends them to the karis driver, and they are rerun below. One held default was missing from the swept code (`barBonusRestarts`); no held title plays game type 1, the only one it touches, so no trace could show it, and it is in. - 4c: run from a separate tree with its own copy of the library's small files, while the sweep held `forge/`: all seven held titles, and windows titles with way-outs, out jumps, death -2 moves, an inverted window, game types 1 and 2, and the sought game over (20 pairs); all agree. The first pass had two differ, which found `waitForWindowStart` and `outEndsSceneNow`. - The seven segmented titles, rerun under the karis driver on the code with 4c in: all agree. - The full sweep on the code with 4c in (81 pairs): 80 agree and none differ. The 81st, the library's plain Crime Patrol, agrees for the 321 trace lines both runs reach, and then neither goes on: the original sits there until the hour's limit stops it, and the port spun at full CPU, did not quit on the SIGTERM the limit sends, and ran 16 hours before it was killed. It was the open item of 14.19, and it is closed: the original loops there too. The game's own script picks the rookie's next nag (getRookieNags, with the Undertaker dip on) by drawing 1 to 5 until it finds one not yet used, and starts over only when 1 to 6 are all used; nothing sets the sixth, so the sixth time through it draws forever. The original's performance line shows a 275-second frame. The port runs the same code and hangs at the same place, which is what "plays as the original" asks. Four of the library's five copies of the game have it (the first-generation one draws differently). The engine's new script watchdog stops both runs cleanly on a SIGTERM and logs where they were; the old binary had ended the original only because timeout's two SIGTERMs arrived as two. `compare.sh` now kills a run 30 seconds after the SIGTERM (`timeout -k`), so one hung title cannot stall a sweep; the engine only sets a flag on SIGTERM, which a script that never returns never lets it read. **4d, the segmented lineage's screens.** The segments style (and the trophies style, which builds on it) had its own game over and continue. Both fold into the shared ones. The lineage gets its own `q.qteFns` (`SEG_FNS`: its start, the board test through its game's own Lua, and a step back over segments rather than scenes), and its defaults: | Option | What it does | Default for the lineage | |---|---|---| | `clipEndExact` | a clip ends exactly at its last frame, not at or past it (`Q.clipEnded`, which replaces the lineage's own test) | on for the first copies and trophies, off for the later copies | | `diffChoiceAfterGameOver` | the next game offers the difficulty again after a game over, even when a continue turned it off | off | | `continueDeclines` | BUTTON2 turns the continue down | off | `gameOverAfterContinue` is "seek" for the lineage, as the held style has it. Checked on the frozen binary to 20,000 frames after each of the two merges: the six segments titles in the library's own layout, the trophies title, the three segments titles and the checkpoints one from the Hypseus library, and TimeGal, FBC, and Astroboy of the others; eleven of the fourteen reach a game over three times and twelve a continue, so the screens that changed were played. After the game over: all fourteen agree. After the continue: all fourteen agree. **4e, the judges.** The trophies style's let-go and mash judges were the shared ones reading its constants from `q` (the same numbers); they are merged, with `mashClearsButton` (on; off for trophies: the press that ends a mash is not used up). The rest are not copies: the windows judge answers presses in a window of frames, the held judge tests a mask of what is held, and the segmented lineage numbers its inputs its own way (its skip accepts only the play switches, and its first copies a third button of their own), so each style keeps its own judge, reached through `q.qteFns`. The three segmented level loops (segments, checkpoints, trophies) are a different machine from the shared one (a fifth to a quarter alike), not copies, and fold only when step 5 gives every style one data model to play. Checked: the trophies title and TimeGal, Tron, and Danmachi of the windows style agree. **Names out of Forge's comments.** A scan of Author.singe against every title in the library found game names in nine comments (and the old behaviour names in two); all are reworded to say what the thing does. The trophies style's level order was kept under a data key named for a game; it is `trophyOrder` in the runtime, the converter, and the one description that has it, which agrees on a rerun. ### 15.11 Step 5, Forge's data model: one source for each (2026-09-28) The first slice of step 5 is the one 15.4 names last: a fact written in one place. Measured first: across the 49 QTE descriptions, 467 dips were in both `settings` and `dips` (the held titles disagreed on up to 15 of them; the runtime lays `dips` over `settings`, so the other copy was dead data that looked meaningful), and 38 descriptions carried their levels twice, as `settings.Level` rows and as `levels`. - **Reproducible conversion first.** The converter now seeds Lua's random generator (`CONVERT_SEED`), so a game that draws as it loads (a scene swap, a level's variant) is converted the same every time. Before the change, every description was regenerated and compared with the library's as data: 46 of 49 identical; BD13 and Esh Aurunmilla differed by their random draws, and hayate 1080 by a high-score board the description had taken from a test run's polluted board (restored from the zip since, but the description had kept the old score). Those three were replaced by the seeded converter's output, and all three agree. - **Levels in one place.** `levels` is the only home of a level's fields, every row the game declares included (a row past the last stage, a secret row a game cannot reach), and the eighth column the held style reads (the level select's still) is `selectStill`. The runtime builds the loops' positional rows from `levels` (`Q.applySnapshot`, which both attaches now share, with `QTE.SELECTSTILL` named); `settings.Level` is gone. A check over all 49 found the rows built from the new `levels` equal to the old `settings.Level` row for row. - **Dips in one place.** A setting `dips` also has is left out of `settings`. Every removal was checked to be one `dips` carries. Found on the way: the engine wrote a log line's text and its newline in two calls, so a line from another thread (the video thread's) could land inside it, which made one comparison's trace look different; a line is now written whole. Checked: Astroboy and Arcade Xperience 2 (added rows), FBC (the level select's still), the trophies title, Jeeg, and TimeGal agree. The full sweep (81 pairs, to 20,000 frames, on the frozen binary): all 81 agree, Crime Patrol for the 321 trace lines both halves reach before the game's own nag loop (15.10); its port half was ended at 33 minutes, the result being known. **Forge's names for the data (prepared, to go in after the sweep above).** Three more slices, each proved before any run by the runtime's own view: `QTE_CLIPS`, `QTE_FIELDS`, the constant tables, and `Q.applySnapshot` taken from Author.singe, each old and new description laid down on an empty state as its style's attach lays it, and the two states compared field by field. - **Clips and stills.** The game's `offsetX`/`offsetXend` globals are `clips` under Forge's names (`title`, `gameIntro`, `getReady`, `resurrect`, `extraDeath`, `continue`, `gameOver`, `gameOverAlt`, `clear`, `newHighScore`, `enterName`, `rankings`, `quit`, and `attract`, a list of the attract steps); its `frameX` globals are `stills` (`hints`, `rankings`, the difficulty stills, and the rest, each named by what follows "frame"). The names are written once, in Author.singe's `QTE_CLIPS`; the converter reads that table from there. - **Named fields.** `scoring` (move, perDifficulty, scene, deathPenalty, level, noDeathBonus, game, secret, extraLifeEvery), `lifeBar` (size, missCost, regainAfter), `windowDelay` (normal, hard, extreme), `order` (play, levels, tiers, mapStart), `deaths` (clips, and randomCount: how many of them a random death draws from, which in 16 descriptions is not all of them), `secret` (allowed), `video` (fps, relativeFrames); Author.singe's `QTE_FIELDS` maps each to the global the loops read. - **No copies of constants.** A setting equal to the constant the runtime lays down first (the QTE table, and HELD over it for a held game) is left out; and the segmented lineage's descriptions carry no settings at all, since its runtime reads none (its game's own Lua declares them). The 38 windows and held descriptions give the runtime the same state after each slice; the 11 segmented ones differ only by the settings their runtime never reads. The settings across the 49 went from 26,289 keys to 9,806. ## 16. TimeGal, complete (started 2026-09-28) The owner, on a week of step 4 and 5 work: "It just seems like we're spending a lot of time and not really getting much closer to actual, complete, ports of anything." Fair: every sweep re-proved the State row of 14.18 and nothing moved the other six. So one title goes through every row, each with a check, before anything general: TimeGal (the windows style, plays with none of its game's Lua, State agreeing). ### 16.1 Picture **The checks.** `karisDrive.singe` gains `PORT_SHOTS=N` (a numbered screenshot at each change the trace prints and one a frame into each move's window; the traces agree, so a number is the same moment in both) and `PORT_HUD=1` (the HUD's flags as letters on each trace line: S score, L lives, V level, T record, C credits, A action, K skip, G get-ready, H choices, D LCD, P pause, 1-4 disks). `testScripts/ports/shotDiff.py` pairs the screenshots by number and reports how much of each differs, with a side by side and the differing pixels marked for a person to look at; `compare.sh` now gathers screenshots from wherever the engine saves them. **What the checks found.** The port drew nothing of the game: no score, no lives, no record, and no time-stop menu, so a player could not see what the choice was between (the trace agreed because the drive does not look). And the port's HUD flags differed from the original's almost everywhere: the windows loop set them by a simpler rule than every framework copy does. **What changed.** - The HUD is a look, `qteHud` (14.15's design), drawing from the loop's state what the flags say, in the games' order, from the game's own pictures (`Overlay/`, `Overlay/Lores/` at the lower resolution) and font, laid out as the games lay it out: the score in the digit pictures, the record, the lives or the life bar, the credits (FREE PLAY, INSERT COIN, or the count, blinking), the level name, get-ready, skip, the choice menu, pause, and the move prompt (a direction's arrow sliding out from the middle, a button's picture, or the hint pictures by `dip_ShowAction`). It draws nothing until the game runs, as the games draw nothing during the title clip. It changes no state and draws no random number: where the games' drawing had a side effect (the level name clearing once play passes the first move), the loop does it, at the end of its step. The converter gives a qte game this look. - The windows style's `managedHud` default is on: all 21 framework copies that could be found set the HUD by the display setting. - The windows attach turns `ShowTop`, `ShowLCD`, and `ShowLevel` from 1 and 0 into true and false, as the games' config reader does (a 0 is true in Lua, so a switched-off record would have shown), and the start sets the record's flag as the games' `readConfig` does. - Forge's own font file is named once (`AUTHOR_FONT_FILE`). **Where it stands** (the frozen binary, the scripted run to 20,000 frames: the attract loop, play, the prompts, the time-stop menu, get-ready, deaths, five continues, three game overs): the trace with the HUD flags agrees on all 226 lines; 247 of 250 screenshots are identical to the original's. The other 3 are the FREE PLAY blink: the games blink by CPU time, the port by the engine's clock, from when a picture appears, so after minutes of play the phases part. Not yet reached, so not yet checked: the high score entry, the level clear and finish screens, pause, and the service screens (16.2). ### 16.2 Sound `PORT_SOUND=1` puts each sound played or stopped (by its file's name) and each change to the disc's audio on the trace line, the engine's calls wrapped before the game loads so the original's and the port's are caught the same way. The run of 16.1 plays 31 sounds (one coin, twenty right, ten wrong), the same files at the same frames in both; the perfect run below plays every sound it reaches the same. Not reached, so not checked: the victory and clear sounds of a run that finishes badly. ### 16.3 Screens and persistence **The drives.** `PORT_WRONG_EVERY` and `PORT_MISS_EVERY` set high make a perfect run: every level, the level clears, the finish, the high score screen, the game over. `PORT_ATTRACT=N` lets the attract loop run N frames before the coin and START, so its rankings and best-percent screens play. `PORT_SERVICE=1` walks the three service screens from the attract loop: the options' first row on to the graphics screen, every row changed once and "Default", on to the performance screen, the same, on to the options, the same, and EXIT; the trace carries each screen's row and every setting. `compare.sh` gains `PORT_TWICE=1`, each side run twice in the same data directory (the original's own files put back only after both), so the second run shows what the first saved. **What was missing, and is now there.** - *Pause:* TimeGal pauses only through its title clip, where no HUD draws; nothing to build. - *The autosave disk* after a level clear, two seconds, as the games time it (on the shared timer and altState, from the frame after it is set); `saveIcon` is on for the windows style too. - *The boards.* The description carries all three (`boards.normal`, `lifeBar`, `survivor`) and the best percent of each level (`boards.percent`), from the game's hscore.cfg; the record shown and the board a result goes on are the game type's; a result goes on above the first entry it equals or beats, and a name left empty is drawn from the games' ten names at random (the same draw, so what follows stays in step). The boards are kept through the engine's save store and read at attach. - *Name entry:* the 44-cell grid, CLR and END, the joystick's quarter second, the sounds, drawn with the cursor blinking; `nameEntry` is on for the windows style and off for the held one until its framework is read. - *The board screens:* the game type's board after a result, the three boards side by side on the attract loop's rankings, and the best percent. - *The service screens:* the options (game type, difficulty, play style, start level and scene, rewind, coins, lives, continues, prompts, hints, mash), the graphics screen (extra clips, titles, LCD, level, record, where the score, lives, and coins go, the font, its size, the two colours), and the performance screen (the machine model and what it sets, resolution, display, scanlines, score and lives drawn or rendered, font quality), one loop and one printer driven by a table per screen, the values changed by the games' own rules (the ties between them, and the asymmetries: hints the kid's type keeps going on, a colour stepping past the other to 13, the third model's resolution set differently each way). "Default" restores the game's `default.cfg`, which the description now carries as `defaults`; leaving writes the settings (`Q.configSave`). - *The start* (`Q.initJob`): what the games' `initJob` does again after the service screens, which the port had done only once: the attract loop from its first state, the counters cleared, the HUD back to the credits, the extra clips, language, and record read from the settings, and the overlay laid out again (`Q.applyResolution`, which also says whether the resolution has scanlines, sticky once found, as the games' flag is). - The windows switch handler opens and closes the service screens on SERVICE and takes no coins while they show. **Where it stands** (the frozen binary): - The perfect run (60,000 frames): 813 trace lines agree (state, HUD flags, sounds); 905 of 909 screenshots identical, the other four a blink phase (FREE PLAY three times, the name cursor once). - The attract loop run twice through (20,000 frames): 41 lines agree; 21 of 25 screenshots identical, the other four FREE PLAY. - The service walk and the play after it (9,000 frames): 259 lines agree; 185 of 187 screenshots identical, the other two FREE PLAY. - Persistence, the board: each side run twice (80,000 frames each, the attract loop, then a perfect run): 1,679 lines agree, and the second run's rankings show the first run's score in both; every screenshot pair that differs is a blink. **Not yet:** the save-slot screen, the exit menu, SERVICE from play (the games autosave and come back to the level; the port starts a new game), the in-game difficulty screen, the LCD ticker of the attract loop (off for TimeGal), and a check that the settings survive a restart (running). The "Show Moves" setting has a value the games print as "Cliff" (a prompt in that game's style), which names a game in Forge's code; the owner decides. ## 17. Forge can reproduce every game (proposed 2026-09-28) **The goal changes.** Sections 14 and 16 set out to make each port play exactly as its original, checked picture by picture. That meant writing each framework's behaviour into Forge again, one version at a time, and every check found another corner (16.1 to 16.3). It does not converge, and it is the opposite of Forge as its own thing (15.1). The goal is instead: **anything a game in the library does, a developer can do in Forge**, so a developer on another framework has no reason not to use Forge. A converted title is the evidence that Forge can reproduce it, not a replica to maintain. **What "can reproduce" means.** For each thing a game in the library does (a judge, a screen, a HUD element, a setting, a kind of persistence, a control), Forge has a capability that does it, described in the vocabulary and the manual, with its behaviour set by data and options rather than by which framework it came from. The look is the developer's: pictures, positions, fonts, and colours are data on HUD widgets and screens (15.5), so a developer can make it look like their game did, but Forge ships no copy of anyone's layout code. **The ledger.** One table, generated from the library and not written by hand: every capability the library's games use, which titles use it, and its state in Forge (built, partial, missing). The converter produces it: as it maps a title onto Forge's model (15.4) it names each thing it maps and each thing it cannot, and the ledger is the union. Today's move kinds alone: press, chord, diagonal, action-plus-direction, mash (one input, two, run), hold (four directions and a button), letGo, double, loop, multi/count, choose, path, yesNo, timed, way, wayOut, skip, and the compound kinds of 3.31c (Linea); most titles use a dozen or more of them. **The checks, per capability, not per title.** 1. *It exists:* a Forge test scene exercises it (as scenes 52 to 57 do now), and the vocabulary and manual describe it. 2. *It is enough:* at least one converted title that uses it plays the same state trace as its original (the State row of 14.18, which stays; it checks the data and the judging, not the picture). 3. *It is reachable:* a developer can author it from the editor or a description without Lua. A title is **reproducible** when the converter maps everything it uses, nothing unmapped, and its state trace agrees. Picture, Screens, and Sound (14.18) become "every element the title shows or plays is a capability in the ledger", not pixel equality. **What happens to the current code.** The five loops' styles (windows, held, segments, checkpoints, trophies) become option sets over one model, as 15.4 already planned; what was written to match one framework's pixels (the service screen's exact rows, blink phases, the copied default font, per-framework layout constants) is retired or turned into widget and screen data. The switches, saves, pause, and prompt work of 16.3 stays as capabilities (save slots, quick save, exit menu, pause during the title). **Order of work.** 1. The converter reports what it maps and what it cannot, per title; the ledger is generated from that over the whole library. 2. The ledger is sorted by how many titles each missing or partial capability blocks; work goes down that list, each capability through the three checks above. 3. Each style's framework-specific code is replaced by the generic capability as its turn comes, and removed when no title needs it. 4. Progress is reported as the ledger: capabilities built, titles reproducible, and what blocks the rest. ### 17.1 Step 1, the ledger (2026-09-28) `util/forgePortKaris.lua` with `FORGE_LEDGER=<file>` writes, instead of the description, one line for each thing it maps: **forge** (a field of Forge's model), **framework** (played under the framework's names or numbers, or a style), or **lua** (the game's own Lua still does it). `util/forgeLedger.py` runs it over every description the converter made and writes `testScripts/ports/ledger.md`. It takes about a second and plays nothing. Hooks in the add-ons file most titles ship unchanged (18 copies) count as framework; `swapScene` counts as forge, since the converter runs it into `scene.swaps`. **What it shows (76 titles):** * *In Forge's model already*, for the 38 windows and held titles: the screens and stills, scoring, the life bar, window delays, level order, deaths, boards, saves, and scene swaps. The other 38 have almost nothing in the model: their mechanics are Lua. * *Lua:* 25 gun games, the time-travel game, and the text adventure play through a hand-written `Vocabulary.singe`; 11 segmented titles run their own level functions; 12 titles have add-on hooks of their own (`startConf` and `swapLevel` 7, `doLevelSelect` and `moveCursor` 6, `swapDeath` 5, `specialScore` 4, `loadSavePlus` and `writeSavePlus` 3, and six of Chantze's Stone's own). * *Framework:* 21 judges, every move row still in the framework's numbering (Linea and Arcade Xperience 2 use compound kinds Forge has no name for, named in their frameworks like sequences: UL, DRU, LDB); 69 settings under `dip_` names; 455 globals nothing names, most of them the framework's own state flags and picture numbers rather than game data. **What it does not cover yet:** the five ActionMax titles (their converter is `util/forgePortActionMax.py`); the gun games' and segmented titles' mechanics are one "vocabulary" or "levelFunctions" line each, not itemized; Picture and Sound capabilities (prompts, gauges, HUD elements) are not in the ledger; "runs without its Lua" is `standalone.txt` as proved on 2026-09-24, before today's regeneration. **Order of work, by titles blocked:** 1. The gun model (15.5): 25 titles. First itemize what the vocabularies do, so the ledger lists the gun capabilities rather than one line. 2. The segmented lineage's level functions as data: 11 titles. 3. Move rows under Forge's judge names instead of framework numbers: 38 titles, mostly converter work; the compound kinds get a sequence judge. 4. Settings (15.5): the 69 `dip_` names as declared, named settings: 75 titles. 5. The globals: drop what is the framework's own state (checked by the State trace), name what is data: 38 titles. 6. The remaining hooks, most-used first. **The text adventure (Rollercoaster).** Not a priority of its own (owner, 2026-09-28), but it fits the Sierra model of section 6: its rooms, exits, disc stills and clips, two-word parser, objects, furniture, room scenes, and the bomb's turn count are rooms, `said` rules, the inventory, sequences, a counter, and `die`. Two capabilities are missing, and both belong to the Sierra lineage generally: a console look (a scrolling text window with wrapping and a cursor, 15.5) and a question a typed answer satisfies (the name, the prize, "put it where?"). With those, it becomes rooms and rules instead of a vocabulary; until then it stays as it is. ### 17.2 What each type of game needs (surveyed 2026-09-28) Four read-only surveys: the 2D gun games (the 18 distinct vocabularies), the QTE lineages still in Lua with ActionMax and the time-travel game, 2D and 3D platformers, and 3D light-gun games. The last two have no titles in the library, so they are measured against the genres. Each item was checked against `forge/Author.singe` and `docs/ForgeVocabulary.adoc`; "unchecked" marks what no scene proves. **Wrong today (checked in the code):** * A 3D shot passes through anything that is not a target, up to four bodies (`fire`, Author.singe ~4524): walls and cover never stop a shot. Meant for the player's own body and glass; should skip only those, and triggers, and a `seeThrough` flag. * A 3D target's zones: the last zone within reach wins, not the nearest. * The QTE loop never calls `writeSavePlus`, so collectibles saved per slot (DL2e, BD13) are not kept (reported, unchecked at play). * ActionMax's sensor is in every room, so the gunshot sounds on the title, menu, and game-over screens; the original sounds it in play only (reported, unchecked at play). A converter fix. * The QTE's held, segments, checkpoints, and trophies titles draw no HUD or prompts at all (`look = "none"`); `vars.prompt` is a framework kind number, not a judge name, so a custom HUD cannot bind to it. * The ledger counted empty add-on hooks as Lua (Sintel, BD13); it now reads each hook's body. **Shared by several types (the most leverage):** | Capability | Needed by | Forge today | |---|---|---| | Random choice among authored alternatives (clips, rows, orders, segments, deaths), drawn per game, level, or use, from the seeded stream | QTE (SAe, DLe, hayate, BD13, starblazers, SDQ, hq-timegal), all 24 gun games, the time-travel game | `chance`, `spawner pick=random` only | | Progression as data: next level by rule over beaten, collected, or set; unlocks; "any of these counts"; a finale on a condition | QTE maps (4), QTE hooks (DL2e, BD13, SAe), all gun games, the time-travel game, metroidvania | rules and `goTo` only | | A map screen: nodes, a cursor graph, stills by beaten set, completion markers, reveal clips, timeout | 4 QTE titles, 2 segmented, gun-game menus (panels by shot), the time-travel game, platformer world maps | missing (the QTE runs the game's `doLevelSelect`) | | Deaths as data: the clip by cause, by level, from a range, with an aftermath clip (nag, undertaker, second clip) and conditions | QTE (SAe, BD13, hayate), all gun games, the time-travel game | a flat random pick | | Per-level, per-move, and per-difficulty overrides of any constant, and settings that force others | QTE hooks (6 titles), gun games' difficulty tables, the segmented lineage's hardcoded scoring | missing | | Counters, streaks, quotas, tallies, and money (spend, buy, cap) | gun games (streaks, money as life, store), the time-travel game (items, trader), 3D gun accuracy and combos, collect-a-thons | vars and `addVar` by hand | | Checkpoints and respawn separate from `die` | gun games (sections reached, 22), platformers, 3D gun areas | QTE checkpoints style only | | Settings and a service screen for any game | every family | QTE loop only (15.5) | | Attract mode, credits and continue, boards with name entry (letter grid by shot, stick, keyboard), for any game | every family; gun games shoot the letters | QTE loop only; `credit`, best score, `submitScore` | | Two players: per-player vars, device assignment, join mid-game, twin HUD and boards, tie-break | 7 gun games, 3D gun games, QTE two-player modes | `gun.player`, pointer per mouse | | HUD widgets: digits, icon rows, bars, gauges (mash, hold, boss), blinking, ticker, prompts per judge, labels over 3D entities (`sceneProject`), binding to instance vars | every family | hud layer with game-var binding; the QTE prompt for presses only | | Tweens and eased motion (`tweenValue`) | rail speed, pop-outs, platformer feel, UI | engine only | | Rumble (`controllerDoRumble`), positional sound, sound stop, mute, channels | gun games, 3D gun, platformers | engine only | **By type, beyond the shared list:** *QTE (windows, held, segments, checkpoints, trophies).* The segmented lineage's level functions are data except their random draws: seven titles are pure tables (Conan, Daitarn3, DragonTrainer, PussInBoots, SamuraiJack, jeeg, freedomfighter) and can be converted now; the rest need random choice. Move rows under judge names; a sequence judge for the compound kinds; prompts and gauges for every judge; the HUD for all styles; a random hidden prompt; choice-menu text as data; collectibles set on a named move and saved with the slot. *2D gun games (25).* The converter must lift the data out of the original scripts first: the hitboxes live in `hitbox-*.singe` and the moves are built by the game's `setupLevel`, not in the descriptions. Then: several boxes per target per frame with a head zone in 2D; hit classes (enemy, civilian, bonus, capture) with a judging order; a shooting window with a second-chance hold; reload by border, bottom edge, button, on empty, or immediate, and reload disabled per window; ammo overfill; full-auto with a fire delay and a random jam; delayed judging (a shot that lands later); a quick draw with a holster rule; lives or money as life with a store; shooting menus with done panels refused and a timeout; crosshair choice, recoil sprite, reload animation; hints and clues without repeats. Title-specific, as hooks or data: the puzzles (roulette, cans, crystal sockets, the signpost maze), the case layer of one title, outlaw capture. The typing edition needs a typed-word judge (per letter, in a window, with its own score formula), which belongs with text input. *ActionMax (5).* Covered by Forge's core words; fixes: the sensor per room and the `every` phase (ticks from its own start, not wall-clock seconds). *The time-travel game.* Deaths by cause, hint clips by move with conditions, a rewind item spent during a death, a trader screen, a weighted slot machine, a bonus shrinking per death, a tutorial level, a perfect-run ending, and per-language frames. One title; after the shared list, little is left that is its own. *3D light-gun games (none in the library).* Shots stopped by solid bodies (above); rail speed, easing, a stop timeout, a cover offset (duck) that the rail does not overwrite, and a `setTrack` for forks; the finished gun model (auto-fire, damage, rate, spread, weapon pickups); hull or mesh targets; labels and bars over 3D entities; slow motion (a global time scale; missing in the engine); recoil output (missing in the engine); gun calibration in Forge. *2D platformers (none in the library).* **A 2D world cannot scroll**: looks draw at node coordinates in overlay space, with no camera or view offset (checked). Needed: a 2D camera (follow, dead zone, look-ahead, bounds, room lock, shake) with editor pan and zoom; tile collision from the `grid` look, tile properties, a tile painter, autotiling, parallax; controller options (acceleration and air control, coyote time, jump buffer, variable jump, air jumps, dash, wall slide and jump, ladders, swimming, slopes, knockback) on `playerSetVelocity` and `playerSetGravityScale`, which Forge does not reach; an edge-turning ground enemy; invincibility frames; sprite animation with one-shots and per-state rates. One-way platforms need an engine feature (collision filters). *3D platformers (none in the library).* The same controller options; momentum and turn rate; `solid` with a mesh or hull shape; heightmap terrain; camera collision, dead zone, auto-recentre, and stick orbit; a blob shadow; animation layers and speed blends (engine only); root motion, decals, and level streaming are missing in the engine too. The editor needs snapping, drop-to-surface, and multi-select. **Proposed order** (shared first, then by titles blocked, then new genres): 1. The wrong-today list above. 2. Random choice, deaths as data, and overrides: they unblock most QTE hooks, most of the segmented lineage, and the gun games' draws. 3. Settings, attract, credits, boards and name entry, and checkpoints for any game (lift them out of the QTE loop). 4. The gun-game converter (hitboxes and moves out of the scripts) and the 2D gun model; then the 3D gun items on the same model. 5. Progression as data and the map screen. 6. HUD widgets, prompts and gauges per judge, and 3D labels. 7. The 2D camera and scrolling world, tiles, and the controller options; then the 3D platformer items. 8. Engine work: collision filters (one-way), a time scale, recoil output, root motion, decals, streaming. ## 18. The plan: everything section 17 found (written 2026-09-28) To be implemented later, in the order given. Each package names what it adds to a description (in the syntax of `testScripts/author/*.forge`), where it lands in Author.singe, what the converters do with it, and how it is proved. Scene numbers from 101 are free. **Rules for every package.** * Generic first: a capability is designed for any game, then the QTE, gun, and other loops use it. No framework's name, numbering, or layout in Forge; a converted title's look is data in its own description. * One source for each: a value lives in one place in a description; a capability lives in one place in Author.singe, and the QTE loop's own copy (its attract, boards, service, deaths) is replaced, not kept beside. * Each capability is proved three ways (17): a scene that exercises it (with a headless screenshot check where it draws), a converted title that uses it keeping its State trace, and authoring it with no Lua. The vocabulary and the Forge book document it in the same change. * The ledger is regenerated after each package; its counts are the progress report. ### 18.0 Before everything: open the loops, and the birds test The owner's worry (2026-09-28): that Forge will do only what it is programmed to do. "What if a user wants a QTE game where they collect birds by visiting certain scenes? Will we need a birds property?" The answer must be no, and today it is only half no: birds can be counted with a var and a `frame` rule on `discBetween` frames, shown by `setText` or a sprite's `copies`, but the QTE loop is a sealed box. It raises no events of its own, publishes five vars (score, lives, credits, level, prompt), and takes no orders from rules. The same is true of the other loops. And the packages below, as first written, add a named field for each thing the library happened to do, which is the worry exactly. **Forge's generality is its rule system** (vars, events, conditions, actions, expressions, looks bound to vars). So every loop, and the shell of 18.5, takes full part in it: 1. **Events at every moment a game could care about:** `gameStart`, `levelStart`, `sceneStart`, `moveOpen`, `moveRight`, `moveWrong`, `moveMissed`, `death`, `continued`, `levelDone`, `gameOver`, and the loop's own screens (`screenShown` with its name). Each carries level, scene, move, judge, and input, and a rule may name any of them (`on = "sceneStart", level = 3, scene = 2`). 2. **Its whole state readable** as vars in expressions: level, scene, move, judge, input, lives, the life bar, deaths, difficulty, what is beaten. 3. **Actions that steer it:** `goLevel`, `goScene`, `skipMove`, `giveLife`, `takeLife`, `killPlayer` (with a cause), `setNextLevel`, `endLevel`, `showScreen`. 4. **Looks bound to any expression** (18.7's `bind`), so a count shows as digits or as a row of icons with no copying rule. 5. **No predefined vars.** Score, lives, credits, level, and the rest are the game's own vars, declared like any other; a loop keeps no private copy. Today the QTE loops hold `q.iScore`, `q.iLives`, and `q.iCredits` and copy them over the game vars every frame (`Q.publishVars` and three copies of it), so a rule that adds to `score` is undone a frame later. Instead a loop reads and writes the vars its description names (`qte = { vars = { score = "points", lives = "hearts" } }`, defaulting to the plain names). What the outside world needs to know is a mapping, not a name built in: the boards and `submitScore` rank by a named var (`by = "birds"`, deaths, time), the best score kept at `gameOver` is per board, the bezel layer maps its digits to vars (`show = { score = "p1.points", lives = "hearts", credits = "coins" }`), and the coin switch adds to the var the shell names. `addScore` and `credit` stay as shorthand for `addVar`, and `score` is no longer created when the game does not declare it. **Special fields are shorthand, not features.** Where 18.2 to 18.9 add a table for something rules could say (collectibles set on a move, deaths by cause, progression, per-level overrides), the table compiles to rules and does nothing rules cannot; a game that outgrows the table writes the rules. The converters may write either. A capability that rules cannot express is a missing event, condition, action, or var, and is fixed there, once. **The birds test.** Every package is also proved on a made-up game that no library title is: QTE birds collected by visiting scenes and shown as icons, lost on a death, a secret level at ten; a gun game where civilians pay you; a platformer whose double jump is bought with birds. Written as scenes without Lua; if one needs a new named property, the package is not done. This package comes first: the events, vars, and actions for the QTE loop (the loop already knows each moment; it has only to say so), then the same for each loop as it is touched. Proof: scene 113, the birds game above, headless, its bird count and secret level traced. ### 18.1 Package 0: wrong today 0.1 **3D shots stop at walls.** `fire` skips a body only when it is the firing player's own, a trigger, or its type says `seeThrough = true` (glass, foliage). Anything else ends the cast as a miss. Zones: the zone whose centre the ray passes closest to, relative to its reach, wins, not the last listed. Scene 101: a target behind a wall, one behind glass, a zombie with overlapping head and body zones. 0.2 **Saves carry game vars.** A slot saves the declared vars the game marks `saved = true` (18.4 counters) with the progress; the QTE's `loadSavePlus`/`writeSavePlus` hooks go. Until 18.4, the loop calls `writeSavePlus` where it writes a slot. Proved on DL2e and BD13 with PORT_TWICE (the second run loads what the first collected). 0.3 **ActionMax's sensor in play only.** `util/forgePortActionMax.py` puts the sensor entity in the play rooms, not the title, menu, and game-over rooms. Proved by the ActionMax sound trace. 0.4 **`every` on wall-clock phase.** `every` gains `from = "room"`: ticks at whole multiples of its period since the room began, not since its last tick. ActionMax's heartbeat uses it. 0.5 **The prompt var names the judge.** `vars.prompt` becomes the judge's name (`"press"`, `"mash"`, ...) with `vars.promptInput` (`"up"`, `"button1"`) and `vars.promptFill` (0 to 1, for gauges); the framework kind number leaves the vars. ### 18.2 Package 1: random choice A value form accepted by every field that takes a value, in a description or in a rule's action: { choose = { a, b, c }, weights = { 2, 1, 1 }, per = "game", key = "ending", noRepeat = true } * `per`: `"use"` (default, a fresh draw each read), `"room"`, `"level"`, `"scene"`, `"game"`, or `"load"`. A draw is kept until its scope ends. * `key`: draws with one key share one draw, which is how one draw fixes several fields (a level's rows and its intro). * `noRepeat`: a `"use"` draw does not repeat until every choice has come up. * `weights`: a weighted outcome (a slot machine's results). * Every draw comes from the game's one seeded stream, so `--deterministic` replays it, and a save keeps the draws in force. Lands in `authorValue(value, context)` (one resolver every reader calls), the draws in `AUTHOR_DRAWS`, saved with the game. The `chance` condition and `spawner pick = "random"` become users of the same stream. Converters: the QTE converter writes `choose` where a hook drew (SAe's game-over clip, DLe's three level orders, hayate's finish bonus, BD13's scene 4 or 5) by running the hook over a seeded range, as `swaps()` does, and recording every outcome it produces; a hook whose outcomes do not fit a choice stays a hook. The segmented converter writes a level's variants and its drawn rows (starblazers, SDQ's random correct input, hq-timegal's shuffled blocks); the gun converter writes random clip rows and the three-in-ten encounters. Proof: scene 102 (each `per`, `key`, `noRepeat`, and `weights`, printed over a thousand draws and replayed under `--deterministic`); SAe, DLe, and starblazers keep their traces. ### 18.3 Package 2: deaths as data, and named clips for any game A description's top-level `clips` names clips and stills for any game, not only the QTE: clips = { deathShot = { from = 1200, to = 1260 }, undertaker = { choose = { { from = 5000, to = 5100 }, { from = 5200, to = 5300 } } } } with a `playClip` action (`clip = "deathShot"`, `then = "hold"` or `"resume"`), a `clipDone` event, and `stills`. Deaths are rules over them: deaths = { { when = { { "test", expr = "event.cause == 'civilian'" } }, clip = "friendlyNag" }, { when = { { "test", expr = "level == 3" } }, clip = { choose = { "d31", "d32" } } }, { clip = { choose = { "d1", "d2", "d3" } }, after = { clip = "undertaker", when = { { "chance", percent = 50 } } } } } The first entry whose conditions hold is the death; `after` is its aftermath clip, which the trigger may skip. `event.cause` is `"wrong"`, `"timeout"`, `"civilian"`, a move's name, or what a rule passes to `die`. The QTE loop's `Q.setupDeathClip`, `swapDeath`, and extra-death clip become this; a range (`from`/`to` clip numbers) is written as a `choose` by the converter. Proof: scene 103; SAe, BD13, and hayate_1080 keep their traces with their `swapDeath` hooks gone. ### 18.4 Package 3: settings, overrides, and counters **Settings.** A description declares its options once: settings = { { name = "difficulty", label = "Difficulty", values = { "Easy", "Normal", "Hard", "Extreme" }, default = 2, group = "Game" }, { name = "reload", label = "Reload", values = { "Offscreen", "Border", "Button" }, default = 1, group = "Gun" }, forces = { { when = { { "test", expr = "settings.difficulty == 4" } }, set = { prompts = 1 } } } } Kept in the engine's save store per game; read in expressions as `settings.difficulty`; any field may be gated on one (`when` on a type, a rule, a room entity). A generic **service screen**, drawn by a Forge-generated RmlUi document, lists the groups and rows, restores the defaults, and opens on the service switch for any game (the QTE's `Q.doServiceScreen` becomes this, its framework's rows written by the converter as data). The QTE converter maps the 69 `dip_` settings to named settings once, in one table (15.4's), not per title. **Overrides.** A number field may be a table resolved in context: score = { base = 100, byLevel = { [3] = 250 }, byDifficulty = { 80, 100, 150, 200 }, bySetting = { reload = { 100, 90, 80 } } } `authorValue` resolves it with the level, difficulty, and settings in force. A move row may carry its own `score`, `sets = { gem3 = true }` (collectibles set on a named move), and `clip` overrides. The converter turns `startConf`, `swapLevel`'s constants, and `specialScore` into these by running them over each level and difficulty, as for random choice. **Counters.** A var may be declared with limits and behaviour: vars = { money = { value = 300, min = 0, max = 9999, saved = true }, streak = { value = 0, resetOn = "miss" }, gems = { value = 0, max = 11, saved = true } } with a `spend` action (`name`, `amount`, `else` actions when short), a `varReached` event, and `saved = true` (0.2). A **tally** screen action shows lines of label, expression, and running total, then adds the total to a var (finish bonuses, accuracy, a bonus shrinking per death). Proof: scenes 104 (settings, forces, and the service screen, driven headless) and 105 (overrides and counters); DL2e, DLe, hayate_1080, and Esh_Aurunmilla keep their traces with `startConf`, `swapLevel`, and `specialScore` gone; TimeGal keeps its service-walk trace (16.3). ### 18.5 Package 4: the game shell for any game What every arcade-style game has around its play, lifted out of the QTE loop into one `shell` table any description may use: shell = { attract = { steps = { { clip = "title" }, { still = "rankings", seconds = 5, show = "boards" }, { room = "demo", seconds = 20 } }, skip = "any" }, credits = { coinsPerCredit = "settings.coins", freePlay = "settings.freePlay", cap = 9 }, lives = { perCredit = 3, max = 9, extraEvery = 50000 }, continue = { clip = "continue", seconds = 10, cost = 1 }, boards = { { name = "normal", by = "score", size = 10, seed = { { "AAA", 10000 } } } }, nameEntry = { style = "grid", letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ.-", still = "enterName", select = "shoot" }, pause = { switch = "pause" } } * The shell runs attract, coin, start, play, continue, game over, name entry, and rankings, and hands play to the rooms and rules (or a loop behaviour). Events `gameStart`, `lifeLost`, `continued`, `gameOver`; actions `loseLife`, `endGame`. * **Name entry** styles: `grid` (a cursor over letters, stick or pointer), `stick` (up and down through letters), `keyboard`, and `select = "shoot"` (letters shot on a still or clip, the gun games' way, with the letters' boxes as a track). * **Checkpoints:** a `checkpoint` action records the place (room, rail point, disc frame, entity positions the type marks) and `die` with `respawn = true` returns there with a life lost; the QTE checkpoints style and the gun games' sections become this. * **Two players:** `players = 2` gives each player a var table (`p1.score`, `p2.ammo`; `player.score` in a rule with `event.player`), a device assignment screen (press to claim a mouse or gun), joining mid-game (`join` rule, a credit), twin boards, and a `tie` event the game answers (the gun games' draw-to-break). * The QTE loop's `Q.doIntro`, `Q.doHighScore`, `Q.doContinue`, `Q.doGameOver`, boards, and name entry become the shell, and the gun games' vocabularies stop carrying their own. Proof: scene 106 (a small non-QTE game through the whole shell, headless, coins, a continue, name entry by each style, the boards persisting across two runs); TimeGal's standard, attract, and persistence traces (16) keep agreeing; one gun game's attract and board. ### 18.6 Package 5: progression and the map **Progression.** A description's `levels` (any game's, not only the QTE's) with an `order`: order = { start = { choose = { { 1, 2, 3 }, { 2, 1, 3 }, { 3, 2, 1 } }, per = "game" }, next = { { when = { { "test", expr = "beatenAll{1,2,3,4} and gems == 11" } }, level = "secretFinal" }, { when = { { "test", expr = "beatenCount{1,2,3,4} >= 3" } }, level = "quartz" }, "sequence" }, skipBeaten = true, counts = { { 6, 7 } }, unlocks = { tier2 = "beatenCount{1,2,3,4} >= 3", finale = "beaten(6) or beaten(7)" } } Expression functions `beaten(n)`, `beatenAll{...}`, `beatenCount{...}`, `unlocked("tier2")`, and `level`; `counts` makes alternatives count as one (beating 6 or 7 clears the node); `unlocks` are derived flags, recomputed after a load (Chantze's `TestState`). A `nextLevel` action and a `levelDone` event serve non-QTE games. **The map screen**, for any game: map = { nodes = { { level = 1, x = 120, y = 90, up = 2, right = 3, still = { base = 4100, beatenOffset = 1 } }, ... }, stills = { byBeaten = true }, select = "stick", timeout = { seconds = 20, pick = "firstOpen" }, refuse = "beaten", markers = { look = { kind = "sprite", file = "Overlay/done.png" } }, reveals = { { unlock = "tier2", clip = "revealTier2" } }, music = "map" } `select` is `"stick"` (a cursor graph), `"pointer"`, or `"shoot"` (panels shot on a still, the gun games' menus, with `refuse` and `timeout`). Events `mapChosen`; the QTE's `levelMap` screen and `doLevelSelect`, `moveCursor`, `LvlMapComplete`, `AllowLvlEnd`, `AllowLvlQuartz`, `ShowComplete`, `mapConf`, `TestDips`, and `TestState` hooks become this, written by the converter where they are tables in the hook (the stills by beaten set and the neighbour lists are), and left as hooks where they are not. Proof: scene 107 (a map by stick, by pointer, and by shot, reveals and markers, a load re-deriving unlocks); SuckerPunch, f13, Chantze, Triad, Daitarn3, and SamuraiJack keep their traces with their map hooks gone; one gun game's menu. ### 18.7 Package 6: HUD widgets Looks for the `hud` and overlay layers, each bound to any expression (`bind = "gun1.ammo"`, `"p2.score"`, `"self.health / self.max"`), so a game var copy is never needed: * `digits`: a number drawn from a sprite font or a TTF, with padding. * `icons`: a row or column of a picture, one per unit, with `max`. * `bar`: a filled bar, horizontal or vertical, pictures or colours. * `gauge`: a picture sequence chosen by a fraction (mash and hold gauges, the gun games' power meters). * `prompt`: the current move's prompt, one picture or animation per judge and input (`pictures = { ["press.up"] = "arrowUp.png", mash = { "mash.png", "action.png" } }`), slides and blinks as options, and a hidden or "?" variant. * `ticker`: scrolling text. * `label3d`: any look pinned over a 3D entity (`sceneProject`), for warning rings and boss bars. * Every look gains `blink = { period, from = "appear" }` and `showWhen`. The QTE's `qteHud` look becomes a widget set: the converter writes each title's widgets, positions, pictures, and fonts into its description (the title's own layout as data), so all five QTE styles draw a HUD, and `Q.hudDraw` and its framework constants leave Author.singe. The gun games' ammo rows, badges, crosshair choice, recoil sprite, and reload animation are widgets and a `pointer` option (18.9). Proof: scene 108 (every widget, screenshots headless); TimeGal's HUD trace keeps agreeing with the widget set; a held and a segmented title draw their prompts (screenshots checked by a person once, then kept as references). ### 18.8 Package 7: the QTE model completed * **Move rows by judge name:** `{ from = 215, to = 235, judge = "press", input = "right", death = 17 }`, with `judge` from 15.4's list and a `sequence` judge (`inputs = { "up", "left" }`) for the compound kinds. The converter writes names; the loop's `QTE`/`HELD` kind numbers leave the runtime. * **One loop, options not styles:** windows, held, segments, checkpoints, and trophies become option sets (15.4); `Q.karis332`-style switches and the framework version go (15.3). * **The segmented lineage as data:** the converter runs each `setupLevelNN(seg)` over every segment under stubs and writes the rows; seven titles are pure tables and convert first, the rest after 18.2. The loop stops loading the game's `setupLevel`, `setupDeathClip`, `NextLevel`, and `doLevelSelect`. The segmented lineage's scoring constants become data (18.4). * **The time-travel game** on the same model plus: hints by move type with conditions (a `hint` field on a row, a rule), a rewind item (an action that reverses the disc to the move's start during a death, spending a counter), the trader and the slot machine (a map node and a weighted choice), and its tutorial level (a level with its own scoring override). * The retired globals (17.1's 455): the converter drops what is the framework's own state; each drop is checked by the title's trace. Proof: every QTE title's trace, the whole library once at the end of the package (the sweep, not per change); the ledger shows no `framework` judges and no `lua` level functions. ### 18.9 Package 8: the gun game model (2D and 3D) **Converter first.** `util/forgePortKaris.lua`'s gun-game branch lifts the data out of the original scripts: `hitbox-*.singe` into box tracks keyed by frame; the moves from the game's `setupLevel` under stubs into windows; menus into maps (18.6); deaths into 18.3; settings into 18.4. The ledger then itemizes each title. **The gun** (behaviour fields): { kind = "gun", player = 1, ammo = 6, max = 6, overfill = true, reload = "offscreen", margin = 10, auto = { rate = 0.12 }, jam = { chance = 0.0025, clear = "reload" }, damage = 1, spread = 0, holster = "bottom" } `reload` is `"offscreen"`, `"border"` (within `margin`), `"bottom"`, `"button"`, `"empty"`, or `"auto"`; the `canReload` var turns it off per window; `auto` fires while held; `jam` needs a reload to clear; `holster` and the `holstered` condition make the quick draw a rule. Weapons are types carrying these fields, and an `equip` action swaps them. **The target and the shot:** * `class = "enemy"` (or `"civilian"`, `"bonus"`, `"capture"`, any name), judged in the order `judge = { "enemy", "bonus", "civilian" }` on the game; `hit` carries `class`. * 2D boxes per target per frame with parts (`{ x, y, w, h, part = "head" }` in a box track), so a head zone works over video as in 3D. * A `window` on a box track: frames in which a shot counts, and `hold = { byDifficulty = { 1.0, 0.7, 0.5, 0.3 } }` seconds of second chance on the last frame; events `windowMissed`. * A delayed shot: `judge = "later"` with a clip, the hit judged when the clip ends (a cannon). * The typing edition's judge: a `typing` behaviour (words from a `choose`, per-letter matching in a window, wrong keys counted, a score expression, words that must not be typed). **3D on the same model:** 0.1's walls; `target shape = "hull"` or `"mesh"`; the rail gains `speed` (a var), `ease`, a stop `timeout`, a `cover = { switch = "pedal", offset = { 0, -0.8, 0 } }` added after the rail position (reload in cover by rule), and a `setTrack` action for forks; `label3d` warnings and boss bars (18.7); weapon pickups by `equip`; a `rumble` action (`controllerDoRumble`); positional sound on an entity (`soundSetNode`); slow motion and recoil wait for 18.12. Money as life, the store, streaks, and hints are 18.4's counters and a map or menu; the puzzles (roulette, cans, crystal sockets, the signpost maze) and one title's case layer stay hooks until a second game needs them. Proof: scenes 109 (2D gun: classes, parts, windows with holds, every reload mode, auto and jam, a quick draw, two players with a tie) and 110 (3D gun: cover, forks, easing, hull targets, pickups, a boss with a bar); every converted gun game's trace against its original (the ALG family's compare driver); the typing edition's trace. ### 18.10 Package 9: 2D platformers * **The 2D camera:** a `view` on the `world2d` or overlay layer: `{ follow = "hero", deadZone = { w = 80, h = 60 }, lookAhead = 40, bounds = { 0, 0, 4096, 480 }, lock = "room", shake = true }`. Every look in the layer draws at its position less the view's; a look with `parallax = 0.5` scrolls at half speed; the HUD layer does not scroll. `shake` works in 2D. * **Tiles:** a `tiles` look (a sheet, a tile size, layers of rows) with a `tileTypes` table (`solid`, `oneWay`, `hazard`, `ladder`, `water`, any tag the rules read as `tileAt(x, y)`); solid tiles merged into as few static boxes as fit; autotile rules (a tile chosen by its neighbours). * **The controller** (`platformer` fields, most on `playerSetVelocity` and `playerSetGravityScale`): `accel`, `decel`, `airControl`, `coyote`, `buffer`, `jumpCut`, `fallGravity`, `airJumps`, `dash = { speed, time, cooldown }`, `wallSlide`, `wallJump`, `climb` (in a ladder tile or area), `swim`, `slope`, `step`; a `launch` action (bounce, knockback, springs); abilities as vars (`canDash`) the fields read. * **Enemies and hazards:** a `groundWalker` that turns at edges and walls; `health` gains `iframes`; stomp as a rule on `collision` with `event.fromAbove` and `launch`. * **Sprites:** `frames` per state gains `fps` and `loop = false`, and raises `animationDone`. * Checkpoints are 18.5's. * **Editor:** pan and zoom on the 2D canvas, level and camera bounds, a tile painter (palette from the sheet, brush, fill, erase, layers, tile types, autotile preview). Proof: scene 111 (a scrolling level with every controller option, tiles, parallax, and a ladder; inputs driven headless, positions traced) and a small complete platformer sample in `testScripts/author/`, built without Lua. ### 18.11 Package 10: 3D platformers * The controller options of 18.10 on `character`, plus momentum and a turn rate (no snap to facing), long jump, ground pound, and wall kick as options. * `solid shape = "mesh"` or `"hull"` from the look's model; a `terrain` look from a heightmap (`meshHeightmap`, `terrainGetHeight`). * The camera: collision (a ray from target to camera), dead zone, auto-recentre, stick orbit with zoom steps, camera zones by trigger. * A blob shadow (a disc placed under the entity by a downward ray) and the shadow quality settings (`sceneSetShadow*`). * Animation layers and speed blends (`animationPlayLayer`, `animationSetLayerMask`, `animationSetLayerWeight`). * `wander` and `canSee` for enemies (`navRandomPoint`, `navRaycast`). * **Editor:** snapping, drop to surface, multi-select, duplicate, rotate and scale handles. Proof: scene 112 and a sample, as 18.10. ### 18.12 Package 11: engine work Things neither Forge nor the engine can do yet: * Collision filters or layers, and one-way bodies (one-way platforms, shots that pass teammates). * A global time scale for physics, animation, and timers (slow motion), with the disc and sound left at speed. * Gun recoil output (a solenoid or the gun's own recoil command: which guns and protocols is to be surveyed first). * Root motion from an animation clip. * Decals (blob shadows, bullet holes). * Loading a room or model without a stop (streaming). * A gun calibration screen in the menu. Each lands as an engine feature with its Reference entry and CHANGELOG line first, then its Forge word. ### 18.13 Also * **The ledger grows** as the packages land: Picture and Sound capabilities (widgets, prompts, sounds per event), the gun games itemized (18.9), ActionMax reported by its converter, and "runs without its Lua" regenerated by `util/forgeStandalone.py` rather than read from an old list. * **The text adventure** waits for a console look and a typed answer to a question (17.1); both can ride on 18.5's name entry by keyboard. * **Retired as packages land:** the QTE loop's copies of the shell, the deaths, the service screen, and the HUD; `qteHud`; the framework kind numbers; the style tables; every hook the converter absorbs; the gun vocabularies, title by title, as each plays from its description. * **Order and dependencies:** 18.0 first, then 0. 1 (random) before 2 (deaths) and before the converter work in 3, 7, and 8. 3 (settings and counters) before 4 (the shell reads settings and lives). 4 before 8 (the gun games use the shell). 5 after 1 and 3. 6 can go beside 3 to 5. 7 after 1 to 6. 8 after 4 to 6. 9 and 10 need only 3 to 6 and can go in parallel with 7 and 8. 11 whenever a package reaches it.