5403 lines
330 KiB
Markdown
5403 lines
330 KiB
Markdown
# 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 `<img>`, 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_<game>.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
|
|
(`<Game>.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 `<Stem>.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:
|
|
`<Title>/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.
|