SOURCE / PINNED RELEASE
Made of little things.
Supersonic RC Revive
- Release
- 1ba42f1ca1d6…
- Author-recorded commit
- baecad10b1cd…
- License
- LICENSE
- Author’s source reference
- nostr://npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr/wss%3A%2F%2Fgit.napplet.soy%2F/n-143146b0d6f
Archive hash verified: 90d22b206672eba4…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
# SuperSonic RC — Original Game Specification (port contract)
Source: decompiled Macromedia Director 8.5/MX (Shockwave 3D + Havok Xtra) movie.
Lingo sources: `ref-out/decompiled/game/casts/Internal/*.ls` (game), `ref-out/decompiled/GUILibrary/casts/External/*.ls` (Mach8 GUI library, author "Lucas Meijer - Mach8").
Assets: `ref-out/assets/manifest.json`, `ref-out/assets/{bitmaps,sounds,3d,glb}`. Score/labels decoded from `ref-out/chunks/game/chunks/VWSC-5212.bin` / `VWLB-5214.bin`; movie config from `DRCF-5207.json`.
Conventions used in this document:
- Director/Shockwave 3D is **Z-up, right-handed**, units are arbitrary "world units" (car is ~34 units long, the level spans roughly x∈[-5400,5400], y∈[-8600,8000]). Euler angles are **degrees**. `transform.rotation` is an XYZ Euler vector in degrees.
- Lingo integer arithmetic: `int / int` is integer division (truncating); `integer(x)` **rounds to nearest** (`integer(2.5)=3`). Lingo `VOID` in arithmetic behaves as `0`; `VOID` in a condition is false.
- `keyPressed(n)` with an integer is a Mac virtual key code; `keyPressed("c")` tests a character.
- "Per chunk" means once per `drivingManager.update()` invocation (see §2); "per frame" means once per Director frame (`stepFrame`).
- Script names cited as *(script: name)*.
---
## 0. Movie configuration
*(DRCF, VWLB, VWSC)*
- Stage: **640×480** (movieRect 50,50 → 690,530). Bit depth 32.
- Movie tempo: **40 fps** (`frameRate: 40`). Director therefore never ticks faster than 25 ms/frame. All real-time-dependent behaviour below should be evaluated assuming a 40 Hz frame clock (slower if the machine cannot keep up).
- Cast libraries: `Internal` (game scripts + 3D/sound/text members), `GUILibrary` (external `GUILibrary.cst`), `dynamic` (empty at start; runtime-created members go here; erased in `prepareMovie`), `art` (bitmaps, w3d sub-members).
### 0.1 Score labels (frame numbers)
| Label | Frame | Contents (decoded sprite channels) |
|---|---|---|
| `driving_layout` | 5 | ch1: member `3d` (Shockwave 3D), loc (320,240), 640×480, behavior `nameASprite` pName `"3d"` |
| `selectlevel_layout` | 15 | ch1 `Background` (640×480 @320,240) name `"bg"`; ch2–6 buttons `Supersonic`,`Scavenger`,`Stunt`,`Checkpoint`,`Free`, all at loc (320,240), names `"supersonic"`,`"scavenger"`,`"stunt"`,`"checkpoint"`,`"free"` |
| `title_layout` | 30 | ch1 `Title-v8` 640×480 @320,240 name `"bg"` |
| `help_layout` | 40 | ch1 `Help-v5` 640×480 @320,240 name `"bg"` |
| `loading_layout` | 50 | ch1 text member (art #9, likely the "Initializing" text, Arial/Verdana), loc (173,206), 318×44, ink 36 (background transparent), name `"text"` |
| `driving` | 70 | empty; frame script = `BehaviorScript 39` (`go(the frame)` loop) |
| `selectlevel` | 80 | empty; frame-loop script |
| `title` | 90 | empty; frame-loop script |
| `help` | 95 | empty; frame-loop script |
| `loading` | 100 | empty; frame-loop script |
The `*_layout` frames are templates only; the Mach8 sprite system reads sprite settings from `<label>_layout` and puppets pooled sprite channels 60–280 when the playhead is at `<label>` (see §1.6).
Note on bitmaps: the extracted `Title-v8.png` / `Help-v5.png` carry a bogus alpha channel — render their RGB only (ink 0 / copy). `Help-v5` RGB shows the controls screen ("SUPERSONIC RC CONTROLS", ↑ Accelerate, → Steer Right, ↓ Brake/Reverse, ← Steer Left, space Hard Brake, esc Quit to Menu, S Sound On/Off, arrow explanation, "Click to Start"). `Title-v8` RGB shows the LEGO Supersonic RC title art with "Click to Start".
---
## 1. Architecture
### 1.1 Global objects
| Global | Set by | Meaning |
|---|---|---|
| `gMach8GUIManager` | GUILibrary `prepareMovie` → `MACH8_GUI_PrepareMovie` | Mach8 sprite/GUI manager |
| `gMainManager` | `startMovie` *(MovieScript 1)* | root object |
| `gGameTypeManager` | `gameTypeManager.start` | the active game-mode instance |
| `gJo` | `lightMapManager.new` | debug handle, unused |
| `tDifL` | `havokManager.update` | last clamped dt, unused |
### 1.2 Startup sequence
```
prepareMovie (GUILibrary MovieScript 24):
actorList = []
gMach8GUIManager = Mach8GUIManager.new() // pools sprites 60..280, appends itself to actorList
erase every member of castLib "dynamic"
startMovie (MovieScript 1):
exitLock = 1
if runMode <> "Plugin": import 3d.w3d into member "3d", havok.hke into member "havok" // authoring only
actorList = [] // NOTE: removes Mach8GUIManager from actorList (its stepFrame/mouse manager never runs again)
gMainManager = mainManager.rawNew(); gMainManager.new()
stopMovie: gMainManager.destroy() (GUILibrary also defines stopMovie → gMach8GUIManager.destroy())
```
`mainManager.new` *(ParentScript 6)*:
```
pSelectLevelManager = selectLevelManager.new()
pDrivingManager = drivingManager.new()
pTitleManager = titleManager.new()
pHelpManager = helpManager.new()
sprite.new([#range: ["loading"], #name: "text"]) // the loading-screen text sprite
if debugModus() then return pSelectLevelManager.start() // debugModus() is hard-coded false: `return 0 and (runMode = "Author")`
pTitleManager.start()
```
Getters: `getSelectLevelManager`, `getDrivingManager`, `getHelpManager`. **There is no `getCar`** (see camera_fromadistance bug).
### 1.3 Object graph (who creates whom)
```
mainManager
├── selectLevelManager (bg sprite + 5 standardButtons → map button → game-mode script)
├── titleManager (standardButton "bg" full-screen)
├── helpManager (standardButton "bg" full-screen; holds the mode instance to start)
├── drivingManager (sprite object for the 3D sprite "3d" in range ["driving"])
│ ├── [start()] member "3d2" = duplicate of member "3d" (raycast world, see §3.9)
│ ├── lightMapManager
│ ├── havokManager
│ ├── car
│ │ ├── wheel ×4
│ │ ├── carsound
│ │ ├── shadow model "carshadow"
│ │ └── pickupManager (respawn / un-flip)
│ └── camera (or camera_fromadistance if key "1" held at start)
└── (game mode, created by helpManager click: script.new())
gameTypeManager ← ancestor of all modes
├── checkpoint_gametype (ancestor = gameTypeManager)
│ ├── free_driving (ancestor = checkpoint_gametype)
│ ├── checkpoint_chase (ancestor = checkpoint_gametype)
│ └── supersonic_challenge (ancestor = checkpoint_gametype)
├── stunt_rally (ancestor = gameTypeManager)
└── scavenger_hunt(ancestor = gameTypeManager)
each mode owns (created in gameTypeManager.realStart):
pTimer = chasetimer.new(me) (ancestor = timer)
pPointsCounter = getPointsCounterScript().new(me) → pointsgoalcounter (default) | pointscounter
pTextMessageManager = textmessagemanager.new()
pStuntDetector = stuntdetector.new(me)
checkpoint_gametype also owns pArrow = arrow.new() and the checkpoint star models.
UI helpers: fadein / fadeout (ancestor = GUILibrary "sprite"), timeout,
insceneoverlay, 3d_textsprite (ancestor = insceneoverlay)
```
Inheritance is Lingo `ancestor`: `x = script("child").rawNew()` / `new()` creates `ancestor = script("parent").rawNew()` and `callAncestor(#new, me, ...)`. Properties of ancestors are accessed as `me.pX`. Unhandled messages fall through to the ancestor.
### 1.4 Per-frame update order
Director frame order: `stepFrame` is sent to every object in `the actorList` (in list order) → `prepareFrame` → render → `enterFrame` → `exitFrame` (sprite behaviors / frame script `go(the frame)`).
actorList during driving (order of `append`, after `selectLevelManager.start` cleared it):
1. `drivingManager` (appended in `start`) → `stepFrame` → `update(tDif)` → physics + car + camera + havok subscribers (§2)
2. `car` → `stepFrame`: shadow placement ray
3. `pickupManager` → respawn / unflip
4. `fadeout` (transient, removed after the loading fade completes)
5. timer's label `3d_textsprite` ("Time remaining") → blend fade
6. `chasetimer` → time check, `outOfTime`, digit textures
7. points counter's label `3d_textsprite` ("Score / Goal" or "Points")
8. `textmessagemanager` → 5 s expiry
9. `arrow` (checkpoint_gametype modes only) → orient arrow
10. game-mode instance (appended in `realStart2`; has no `stepFrame` handler — no-op)
11. any later `3d_textsprite` message sprites, `timeout` objects, etc. (appended as created)
Havok "subscribers" (called from `drivingManager.update` per chunk via `call(#havokUpdate, list, tDif)`), in order:
1. game-mode instance (`registerForUpdate` in `gameTypeManager.realStart`)
2. `stuntdetector`
`car` and every `wheel` call `pHavok.registerStepCallback(#havokStep, me)` but **neither defines `havokStep`** → no-op. `car.stepFrame` calls `callAncestor(#stepFrame,[me])` but car has no ancestor → no-op.
### 1.5 Screen flow / state machine
```
title ──click bg──▶ fadeout(1s) ──▶ selectLevelManager.start
selectlevel ──click enabled level button──▶ fadeout ──▶ helpManager.start(levelScript)
help ──click bg──▶ sound(8).fadeOut(500); fadeout ──▶ (levelScript.new()).start() // .new() runs at click time
gameTypeManager.start:
pStopping = 0; fadein(callback realStart); mach8_go("loading"); gGameTypeManager = me
fadein completes (≈1 s) → realStart:
drivingManager.start(); registerForUpdate(me); pCar = car RB; pCarObject = car
fadeout(callback realStart2) // physics is ALREADY running behind the loading screen during this 1 s
pTimer = chasetimer.new(me); pPointsCounter = getPointsCounterScript().new(me)
pTextMessageManager = textmessagemanager.new(); pStuntDetector = stuntdetector.new(me)
(subclass realStart continues after callAncestor: setTime, setGoal, spawn checkpoints/prizes, arrow)
fadeout completes → realStart2:
mach8_go("driving"); drivingManager.firstFrame(); actorList.append(me)
(checkpoint_gametype.realStart2 then calls pArrow.spriteReady())
fadeout object destroyed → driving view appears abruptly (no fade-in into driving)
driving ──ESC (any time, incl. end screens)──▶ stopDriving(); destroy() ──▶ select level
driving ──win/lose──▶ timeout(6000 ms) ──▶ destroy() ──▶ select level
```
`gameTypeManager.destroy`: destroy timer (removes from actorList only), `pTextMessageManager.destroy()`, `drivingManager.stop()`, `selectLevelManager.start()` (which sets `the actorList = []`, killing every remaining actor: arrow, overlays' label sprites, timeouts, etc.). Points counter and stunt detector are never destroyed (garbage; their 3D models are wiped by the next `resetWorld`).
### 1.6 GUILibrary (Mach8) — what the game uses
- **`sprite`** *(ParentScript 1)*: a sprite *object* not bound to a fixed channel. `new([#range: [labels], #name: "x"])` reads all properties (blend, loc, ink, member, locZ, size, …) from the sprite carrying behavior `nameASprite` with `pName = "x"` on frame `<range[1]>_layout`. When the playhead is inside `range` (`#all` = every frame 1..1000), it borrows a free puppet channel from the pool (60–280) and applies its properties (`aboutToGo1`/`aboutToGo2` hooks run around every `mach8_go`). Setters (`setBlend`, `setLoc`, `setMember`, `SetWidth`, …) cache values and forward to the live channel. If `pNeedEvents`, attaches `eventCatcher` which forwards mouse events (`spriteMouseDown` etc.) — suppressed while `gMach8GUIManager.getGloballyDisabled(me)`.
- **`mach8_go(label)`** *(MovieScript 24)*: `aboutToGo1(label)` on all sprite objects; `updateLock=1`; `go(label)`; `aboutToGo2(label)`; `updateLock=0`.
- **`doCallback(cb, msg)`**: if `cb` is a `callback` instance → `cb.execute(msg)`; if a propList `[#handler,#object,#msg]` → `call(...)`; VOID/0 ignored.
- **`callback`** *(ParentScript 13)*: `new(handler, object, p1..p5)`; `execute(t1..t5)` calls `handler` on `object` with the stored args if `p1` non-void, else with the execute args.
- **`buttonBase`** *(ParentScript 6)*: state machine with members `#normal`, `#rollover`, `#pressed`, `#disabled`; `mouseEnter`→rollover (or pressed if held), `mouseLeave`→normal (if held: lock interface to this button), `mouseDown`→pressed, `mouseUp`→rollover. `Disable()`/`enable()`. Size re-fit to member on every state change (`correctRectToMember`). Safety: if pressed and the mouse is up for 5 consecutive `exitFrame`s → fake `mouseUpOutside`.
- **`spriteWithButtonMembers`** *(ParentScript 5)*: derives state members by name from the normal member: `<name>Over`, `<name>Down`, `<name>Gray`; falls back to the normal member if absent.
- **`standardButton`** *(ParentScript 7)*: `mouseDown` → **`sound(1).play(member "buttonclick")` (plays even when the button is disabled)** then base; `mouseUp` → if it was pressed: `call(#Action)` and `doCallback(pActionCallback)`.
- **`Mach8GUIManager`** *(ParentScript 23)*: sprite pool, `disableInterfaceExcept(list)` / `enableInterface()` (used by fadeout to block clicks during fades).
- Unused by the game: slider, scroller, spinner, togglebutton, multistatebutton, pulsingbutton, selectionGroup, valuedisplay, numberDisplay*, draggable/followMouseDragSprite, 3dpi_* buttons, 3DPI_Gfx_Generator, cachedScoreDataTools, Mach8MouseManager (registered methods never called).
---
## 2. Frame / timestep model
### 2.1 drivingManager *(ParentScript 2)*
```ts
stepFrame() {
if (!pRunning) return;
let tDif = now_ms - pLastFrameMS; pLastFrameMS = now_ms;
tDif = Math.min(tDif, 100);
update(tDif);
}
update(tDif) { // recursive 20 ms chunking; REMAINDER FIRST
if (tDif > 20) { update(tDif - 20); tDif = 20; }
if (!pRunning) return;
havokManager.update(tDif); // apply forces queued in the PREVIOUS chunk, step, clear queue
camera.update(tDif);
car.update(tDif); // carsound.update(); wheels[1..4].update() → queue new forces
for (s of pHavokSubscribers) s.havokUpdate(tDif); // gameMode, stuntdetector
}
```
Chunk sequence for a frame of `T` ms (T≤100): with `k = ceil(T/20) - 1`, first one chunk of `T - 20k` ms (in (0,20]), then `k` chunks of 20 ms. Examples: 25 ms → [5, 20]; 33 → [13, 20]; 45 → [5, 20, 20]; 100 → [20,20,20,20,20]; 16 → [16].
Consequences: forces computed in chunk *n* are applied in the Havok step of chunk *n+1*. Everything that is "per chunk" without dt (steering ramp, camera lerp, odometer, checkpoint star rotation) is frame-rate dependent.
### 2.2 havokManager *(ParentScript 3)*
```ts
new() {
pHavok = member("havok"); pHavok.Initialize(member("3d")); // from havok.hke
pHavok.dragParameters = [0, 0]; // no linear/angular drag
pHavok.gravity = pHavok.gravity * 0.2; // hke gravity found as (0,0,-9.81) → (0,0,-1.962) in Havok units (see Open Questions re world scale)
for each rigidBody: restitution = 0;
pForces = []; pForcesOnPoint = [];
// pNextHavokUpdate, pHavokStepSize = 1/60 are set but unused
}
update(tDifMs) {
let dt = tDifMs / 1000;
dt = clamp(dt, 0.003, 0.015);
for ([rb, f] of pForces) rb.applyForce(f);
for ([rb, a, b] of pForcesOnPoint) rb.applyForceAtPoint(a, b); // see §3.8 argument-order note
pHavok.step(dt * 2.5, 7); // timeIncrement = dt*2.5 seconds, 7 sub-steps
pForces = []; pForcesOnPoint = [];
}
applyForceAtPoint(rb, tPoint, tForce) { pForcesOnPoint.push([rb, tPoint, tForce]); }
applyForce(rb, tForce) { pForces.push([rb, tForce]); }
getSimTime() { return pHavok.simTime; }
destroy() { pHavok.shutDown(); } // only called from mainManager.destroy — NOT between races
```
Forces are one-shot: applied for exactly one `step()` call (all 7 sub-steps), then the queue is cleared.
### 2.3 Sim time vs real time
Per chunk of `c` ms, sim time advances `2.5 * clamp(c/1000, 0.003, 0.015)` s. Game clocks use `simTime / 3`.
| Real frame | Chunks | sim s per real s | game-clock s per real s |
|---|---|---|---|
| 25 ms (40 fps, nominal) | 5 + 20 | 2.000 | 0.667 |
| 33.3 ms (30 fps) | 13.3 + 20 | 2.10 | 0.70 |
| 16.7 ms (60 fps) | 16.7 | 2.25 | 0.75 |
| 50 ms (20 fps) | 10+20+20 | 2.00 | 0.667 |
| 100 ms (≤10 fps) | 5×20 | 1.875 | 0.625 |
So at the authored 40 fps the "2:30" free-driving clock lasts ≈225 real seconds. A faithful port should run a fixed 40 Hz frame clock feeding this exact chunking.
---
## 3. Car & wheel physics
### 3.1 car.new *(ParentScript 4)*
```ts
pW3D = member("3d"); pHavok = member("havok");
pmodel = pW3D.model("car"); // collision proxy mesh from 3d.w3d (authored at (155.6,-77.5,40.4)); later hidden
pmodel.addModifier(#meshDeform); updateStage();
pRB = pHavok.makeMovableRigidBody("car", 3, 1); // mass 3, isConvex = 1 (convex hull of model "car")
pRay3d = member("3d2");
const tLiftBody = 2;
pStopped = 0;
pRB.position = (2854.77, -5445.81, 345.6299); // overwritten immediately:
pRB.position = (3790.17, -5696.19, 338.6598); // START POSITION
// rotation = whatever the "car" model has in 3d.w3d (not set by code)
pRB.shiftCenterOfMass(vector(0, 0, -11.5 - tLiftBody)); // (0,0,-13.5)
pRB.friction = 10;
pRB.restitution = 0.01;
member("holypolycrap").preload();
pW3D.cloneModelFromCastmember("car2", "car", member("holypolycrap")); // visual car incl. 4 wheel children
car2.parent = pmodel;
pmodel.visibility = #none;
car2.transform = { scale: (0.17,0.17,0.17), rotation: (0,0,180), position: (0,0, 4 - tLiftBody) /* (0,0,2) */ };
pSkidding = 0;
pWheels = [
wheel.new(me, (-9.5, 17, -5 - tLiftBody), traction=1, index=1), // front-left (z = -7)
wheel.new(me, ( 9.5, 17, -7), traction=1, index=2), // front-right
wheel.new(me, (-9.5, -17, -7), traction=0, index=3), // rear-left
wheel.new(me, ( 9.5, -17, -7), traction=0, index=4), // rear-right
];
actorList.append(me);
pHavok.registerStepCallback(#havokStep, me); // no handler → no-op
pHavok.registerInterest("car", #all, 0, 0, #collision, me); // every car collision → me.collision(details)
pStep = 0;
pCarSound = carsound.new(me, pWheels);
pShadow = pW3D.newModel("carshadow", pW3D.modelResource("defaultModel"));
shadowshader = newShader("shadowshader", #standard): texture = newTexture("carshadowtex", #fromCastMember, member("carshadow","art")) renderFormat #rgba8888;
blend 50, emissive rgb(255,255,255), specular rgb(0,0,0), ambient rgb(0,0,0), diffuse rgb(0,0,0)
pShadow.visibility = #both;
pPickUpManager = pickupManager.new();
pCrashSoundsDown = [crash1_glass, crash2_glass, crash3_glass]; // used when NOT upright
pCrashSoundsUp = [crash1, crash2, …, crash8]; // used when upright
```
Front = +Y in car-local space. Front wheels are the steered/driven/braked ones ("traction"); rear wheels only provide springs and lateral grip (front-wheel drive).
### 3.2 car.stepFrame (per frame) — shadow
```ts
r = pRay3d.modelsUnderRay(pRB.position, (0,0,-1), 1, #detailed);
if (r.length) {
pShadow.transform.identity();
pShadow.transform.scale = (32, 78, 1);
pShadow.worldPosition = r[0].isectPosition + r[0].isectNormal * 2;
pShadow.pointAt(pShadow.worldPosition + r[0].isectNormal); // default pointAtOrientation of a model
pShadow.rotate(0, 0, -pmodel.transform.rotation[3]); // rotate about own z by -car yaw (Euler z)
}
```
The ray is cast in member `3d2` (static world), so it never hits the car itself.
### 3.3 car.update (per chunk)
```ts
pCarSound.update();
for (w of pWheels) w.update(tDif); // order 1,2,3,4
```
### 3.4 car.collision (Havok interest callback, every car contact)
```ts
collision(t1 /* details list */) {
const tMax = 4;
if (pRB.angularMomentum.length > tMax) pRB.angularMomentum = pRB.angularMomentum.normalized() * tMax;
if (pRB.angularVelocity.length > tMax) pRB.angularVelocity = pRB.angularVelocity.normalized() * tMax;
if (t1[5] > 1) { // 5th element of details (impact magnitude, presumably)
if (sound(3).member is not a #member) { // channel 3 idle
let m;
if (pmodel.transform.zAxis.z > 0.5) { m = pCrashSoundsUp.shift(); pCrashSoundsUp.push(m); } // round-robin
else { m = pCrashSoundsDown.shift(); pCrashSoundsDown.push(m); }
sound(3).play({member: m});
sound(3).volume = 125;
}
}
}
```
Note the angular clamp only happens on collision frames (so in the air spin is not limited).
### 3.5 car misc
- `stopDriving()`: `pCarSound.stopDriving()`; `gGameTypeManager.getTimer().stop()`; every wheel `stopDriving()` (pStopped=1); `pStopped = 1`.
- `prepareForRay()` removes car2 from world — **never called**. `getRayCaster()` returns `pRayCaster` which is **never assigned** (VOID).
- `destroy()`: pickupManager.destroy, remove from actorList, `call(#destroy, pWheels)` (wheel has no destroy → no-op), carsound.destroy.
### 3.6 wheel.new *(ParentScript 5)*
```ts
pCar = tCar; pStopped = 0; pTraction = tTraction; pIndex = tIndex;
p3d = member("3d"); pRay3d = member("3d2"); pHavok = member("havok");
pHavokManager = gMainManager.getDrivingManager().getHavokManager();
pRotation = 0;
pRayCaster = pCar.getRayCaster(); // VOID, unused
res = p3d.modelResource("wheel") ?? p3d.newModelResource("wheel", #box) {length 5, width 5, height 5};
pForceMultiplier = 0.0003; // unused
pLeft = tPosition.x < 0 ? 1 : 0;
pmodel = p3d.newModel("wheel" & me, res); // invisible "hub" node, child of car model
// --- visual wheel ---
pVisualModel = pCar.getModel().child[1].child[1]; // car → car2 → first remaining child of car2
tScale = pVisualModel.getWorldTransform().scale;
pVisualModel.removeFromWorld(); // detaches it: next wheel picks the next child of car2
pVisualModel.transform.scale = tScale;
pVisualGroup = p3d.newGroup("visualwheel" & me); // world-root group
pVisualGroup.transform = pVisualModel.transform; // copies (old local pos, rot, world scale)
pVisualModel.transform.identity();
tFlipGroup = p3d.newGroup("flipgroup" & me); tFlipGroup.parent = pVisualGroup;
if (pIndex == 1 || pIndex == 2) tFlipGroup.rotate(0, 180, 0); // front wheels flipped 180° about Y
pVisualModel.parent = tFlipGroup;
// --- hub ---
pmodel.parent = tCar.getModel();
pmodel.transform.position = tPosition; pPosition = tPosition;
pmodel.visibility = #none;
pHavok.registerStepCallback(#havokStep, me); // no handler
pSkidding = 0; pRadius = 25 /*unused*/; pRestLength = 30; pDistanceCovered = 0;
tRBModel = p3d.newModel("wheelrb" & me, res) + meshDeform, worldPosition (550,550,450)+tPosition, removeFromWorld(); // unused leftover
pCarRB = pCar.getRB(); pSpringLength = pRestLength; pGroundNormal = (0,0,1);
```
Visual wheel assignment = the first four children of the holypolycrap `Car` model **in W3D child order**. In `holypolycrap.glb` the child order is `Wheel2, Wheel1, Wheel3, Wheel4` (local translations Wheel2 (56.7, 89.0, -13.5), Wheel1 (-56.7, 89.0, -13.5), Wheel3 (-52.9, -80.6, -13.5), Wheel4 (52.9, -80.6, -13.5), in car2 space which is rotated 180° about Z and scaled 0.17 relative to the car). So wheel index 1 → mesh `Wheel2`, 2 → `Wheel1`, 3 → `Wheel3`, 4 → `Wheel4` if the glb order matches the W3D order (verify). Only the mesh/scale matter: position and rotation of `pVisualGroup` are overwritten every update.
### 3.7 wheel.update (per chunk) — full
```ts
update() {
let up, down, left, right;
if (pStopped) up = down = left = right = 0;
else { up = keyPressed(126); down = keyPressed(125); left = keyPressed(123); right = keyPressed(124); }
pVisualGroup.transform.rotation = pCar.getModel().transform.rotation; // world-root group takes car Euler rotation
findRayISect();
// point velocity of hub
pSpeed = pCarRB.linearVelocity
+ pCarRB.angularVelocity.cross(pmodel.worldPosition - (pCarRB.position + pCarRB.centerOfMass));
const tSpeed = {...pSpeed, z: 0};
if (pOnGround) pDistanceCovered += tSpeed.length * 0.1; // odometer, per chunk, used as roll angle in degrees
const tDown = -pmodel.getWorldTransform().zAxis.normalized();
pRealWorldPos = tDown * (pSpringLength - 25) + pmodel.worldPosition; // = ground contact point when on ground
actAsSpring();
let side = pGroundNormal.cross(pmodel.getWorldTransform().yAxis).normalized();
const fwd = (-pGroundNormal.cross(side)).normalized(); // computed BEFORE the pLeft flip → same for all wheels
if (pLeft) side = -side; // irrelevant: lateral force is sign-invariant in side
const vF = pSpeed.dot(fwd);
let tLong; // VOID (=0) when no key
if (up) tLong = 6300;
let tBraking = 0; // unused
pCar.pReversing = 0; // written by every wheel; wheel 4's value wins
if (down) {
if (vF < 0) { tLong = -4000; pCar.pReversing = 1; }
else { tLong = -10000; tBraking = 1; }
}
if ((keyPressed(" ") && Math.abs(vF) > 10) || pStopped) {
tLong = -10000 * sign(vF); // sign(0) = +1 → when stopped at exactly 0 speed pushes backward
tBraking = 1;
}
const tWheelPos = (pCar.getModel().transform.inverse() * pRealWorldPos); // CONTACT POINT IN CAR-LOCAL COORDS
let F = vector(0,0,0);
if (pTraction && pOnGround) {
if (pCar.pSkidding) tLong *= 0.5;
F += fwd * tLong;
}
if (pOnGround) {
let tDrag = vF * 6;
if (pCar.pSkidding) tDrag *= 0.5;
F -= fwd * tDrag; // rolling resistance, all wheels
}
// steering (front wheels only), integer degrees, ±1 per chunk
if (pTraction == 1) {
if (left) pRotation = Math.min(5, pRotation + 1);
else if (right) pRotation = Math.max(-5, pRotation - 1);
else if (Math.abs(pRotation) > 0) pRotation -= sign(pRotation);
pmodel.transform.rotation = (0, 0, pRotation); // hub yaw ±5° → steers force directions
pVisualModel.transform.rotation = (0, 0, -pRotation * 3); // visual ±15° (inside the flipped group)
} else {
pVisualModel.transform.rotation = (0, 0, 0);
}
pVisualModel.transform.preRotate(pDistanceCovered, 0, 0); // rolling: degrees about local X
if (pOnGround) {
const vS = pSpeed.dot(side);
let tSideDrag = vS * 150;
let tMaxDrag = 3.0 * pLoad + pTraction * 100; // pLoad from actAsSpring this chunk
if (pCar.pSkidding) tMaxDrag *= 0.75;
if (keyPressed(" ") && pTraction == 0) tMaxDrag *= 0.3; // handbrake: rear grip ×0.3 (keyPressed, NOT gated by pStopped)
if (Math.abs(tSideDrag) > tMaxDrag) { tSideDrag *= 0.5; pCar.pSkidding = 1; }
else pCar.pSkidding = 0; // shared flag, last on-ground wheel wins
F -= side * tSideDrag;
}
pHavokManager.applyForceAtPoint(pCarRB, F, tWheelPos); // queued → rb.applyForceAtPoint(F, tWheelPos)
}
```
Note that space-bar braking via `keyPressed(" ")` is not suppressed by `pStopped` in the handbrake-grip line (it is in the brake line only because `pStopped` forces the brake anyway).
### 3.8 wheel.actAsSpring
```ts
actAsSpring() {
pLoad = 0;
if (!pOnGround) return;
const tStrength = pCarRB.mass * pHavok.gravity.length / 4 * 10; // ((m*|g|)/4)*10, |g| AFTER the ×0.2 scaling
const dir = (pRayISect - pRayStart).normalized(); // ≈ world "down" of the hub
const tForceLength = tStrength * (pRestLength - pSpringLength); // rest 30, length clamped [20,30] → compression 0..10
let vDir = pSpeed.dot(dir);
vDir = clamp(vDir, -30, 30);
const tDamping = tStrength * 0.09 * vDir; // positive when moving toward the ground
pLoad = tForceLength + tDamping; // may go negative (rebound pulls down)
const tForce = pGroundNormal.normalized() * pLoad;
pHavokManager.applyForceAtPoint(pCarRB, tForce, pmodel.transform.position); // hub LOCAL position (constant = tPosition)
}
```
(Globals `tSpeed, tDamping, tForce, tForceLength, tSpeedInDirectionOfForce` are just debug globals.)
**Suspicious — force application point.** `havokManager.applyForceAtPoint(me, tRB, tPoint, tForce)` stores `[rb, tPoint, tForce]` and later calls `rb.applyForceAtPoint(tPoint, tForce)`. Callers pass `(rb, force, point)`, so the parameter names are swapped twice and the final Havok call is `applyForceAtPoint(force, point)` — the documented Director order. **However both callers pass points in car-local coordinates**: wheel forces use `inverse(carTransform) * contactWorld`, spring forces use `pmodel.transform.position` (hub local position, e.g. (-9.5,17,-7)). If the Havok Xtra interpreted the point as world space, the lever arm would be ~6800 units (car spawns at (3790,-5696)) and the car would be uncontrollable; since the game was tuned and shipped, the Xtra almost certainly treated the point as **body-relative**. Open question whether it is relative to the body origin (model position) or the shifted centre of mass, and whether it is rotated by the body orientation (the code supplies un-rotated *local* coordinates, so a world-oriented offset interpretation would be wrong when the car is tilted/yawed — the authors' tuning would then include that). Recommended port: treat point as car-local, transform by the car rotation to get the world offset from the body origin, and compare behaviour.
Other notes:
- `pCarRB.centerOfMass` is added to `pCarRB.position` un-rotated (suggesting it's a world-space offset or the authors' assumption).
- Spring force is along the ground normal, not along the suspension axis.
- No force cap besides the damping-velocity clamp; `pSpringLength ≥ 20` caps compression force at `10*tStrength`.
### 3.9 wheel.findRayISect & the "3d2" raycast world
```ts
findRayISect() {
const tDown = -pmodel.getWorldTransform().zAxis.normalized();
const tStartOffset = 25;
pRayStart = pmodel.worldPosition + pmodel.getWorldTransform().zAxis.normalized() * tStartOffset; // 25 above hub
const r = pRay3d.modelsUnderRay(pRayStart, tDown, 1, #detailed); // nearest hit only
if (r.length) {
pRayISect = r[0].isectPosition;
const d = pRayISect.distanceTo(pRayStart);
if (d <= pRestLength /*30*/) {
pSpringLength = Math.max(20, d);
pGroundNormal = r[0].isectNormal;
pVisualGroup.worldPosition = pRayStart + tDown * pSpringLength + vector(0, 0, 5.2); // contact + 5.2 WORLD up
pOnGround = 1;
return;
}
}
pOnGround = 0;
pSpringLength = pRestLength;
pVisualGroup.worldPosition = pRayStart + tDown * (pSpringLength - 5.2); // 24.8 along local down (inconsistent with the +world-Z case)
// pGroundNormal keeps its last value when airborne
}
```
So the suspension ray spans `[hub+25 up, hub+5 down]` (length 30); hub sits 7 below the body origin.
**Member `3d2`** is created in `drivingManager.start` *before* `p3d.resetWorld()`:
```
t = getDynamicSlot(); member("3d").duplicate(t); t.name = "3d2"; member("3d").resetWorld(); member("3d2").preload()
```
It is a frozen copy of the authored 3d.w3d scene and is used for **every** raycast: wheel suspension, car shadow, camera occlusion, pickupManager respawn. It contains all authored models — every level mesh (the same set as Havok bodies plus visual-only meshes such as `Box03`–`Box18` at z≈1400–2830 which have **no** Havok body), the hidden `Lighting` mesh (z≈2200), the scene `arrow` model at (357.7,269.4,259.7), and the **static `car` collision proxy at its authored position (155.6,-77.5,40.4)** — an invisible ray obstacle near the origin. It contains **none** of the runtime models (car2, wheels, shadow, checkpoint stars, prizes, overlays). On subsequent races a new duplicate is made each time but `member("3d2")` resolves to the first one created, so it stays the pristine copy.
### 3.10 pickupManager *(ParentScript 12)* — respawn and un-flip (per frame)
```ts
stepFrame() {
let upsideNow = 0;
if (pCarRB.position.z < -10) {
const p = {...pCarRB.position, z: 800};
const r = member("3d2").modelsUnderRay(p, (0,0,-1), 1, #detailed);
pCarRB.position = r.length ? r[0].isectPosition + (0,0,30) : {...p, z: 100}; // velocity NOT reset
}
if (pCarRB.linearVelocity.length < 10) {
if (pmodel.getWorldTransform().zAxis.angleBetween(pUp /* (0,0,-1) */) < 20) { // degrees; car is upside down
if (pUpSideDown == 0) pUpSideDownStartTime = pHavok.simTime;
else if (pHavok.simTime - pUpSideDownStartTime > 8) { // 8 SIM seconds (~4 real s @40fps)
pCarRB.position += (0,0,20);
pCarRB.rotation = [ (0,0,1), 0 ]; // axis-angle identity: heading lost
}
pUpSideDown = 1; upsideNow = 1;
}
}
if (!upsideNow) pUpSideDown = 0;
}
```
### 3.11 Unused: lingoRayCaster *(ParentScript 27)*
A pure-Lingo sector-grid triangle raycaster (50×50 sectors, Möller–Trumbore with ε=0.00001). Never instantiated. Ignore.
---
## 4. Cameras, fades, overlays
### 4.1 camera *(ParentScript 7)* — default chase camera
```ts
new() {
pcamera = member("3d").camera[1]; // first camera of 3d.w3d (hither/yon from file)
pCar = drivingManager.getCar();
pcamera.worldPosition = (-2000, -584, 250);
pcamera.fieldOfView = 70; // vertical FOV degrees
pOldBehindDir = (0,0,1); // unused
pWannaBe = pcamera.worldPosition;
pFolllowing = 1; // TYPO: sets a local; property pFollowing stays VOID (false)
// black 1×1 overlay covering the screen until firstFrame
tex = newTexture("black", #fromImageObject, 1×1 black image);
pcamera.addOverlay(tex, point(0,0), 0); overlay[1].scale = 640; overlay[1].loc = (0,0);
pcamera.colorBuffer.clearAtRender = 0; // main camera never clears color (world is enclosed)
}
fadeIn() { pcamera.removeOverlay(1); } // called from drivingManager.firstFrame (instant, no fade)
carStoppedDriving() { pOrbit = 1; }
update() { // per chunk
const m = pCar.getModel();
if (!pOrbit) {
let tDontFollow = 0;
if (pCar.getRB().linearVelocity.length < 10) tDontFollow = 1;
if (pFollowing && !tDontFollow) {
let b = -pCar.getRB().linearVelocity; b.z = 0; b.normalize(); // behind the velocity direction
pWannaBe = m.worldPosition + b * 50;
} else {
const b = -m.getWorldTransform().yAxis; // behind the car's nose (3D, not flattened)
pWannaBe = m.worldPosition + b * 50;
if (m.worldPosition.distanceTo(pcamera.worldPosition) > 300) pFollowing = 1; // latches forever
}
pWannaBe.z = m.worldPosition.z + 10;
} else {
const t = now_ms / 1000; // radians
pWannaBe = m.worldPosition + (Math.sin(t) * 150, Math.cos(t) * 150, 50); // orbit after stopDriving
}
pcamera.worldPosition += (pWannaBe - pcamera.worldPosition) * 0.1; // per chunk, no dt
const target = m.worldPosition + (0,0,10);
const r = member("3d2").modelsUnderRay(target, pcamera.worldPosition - target, 1, #detailed);
if (r.length && r[0].isectPosition.distanceTo(target) < pcamera.worldPosition.distanceTo(target))
pcamera.worldPosition = r[0].isectPosition; // snap to the occluder hit point (no offset)
pcamera.pointAt(target, (0, 0.01, 1));
}
```
Because the camera starts at (-2000,-584,250), >300 from the car, `pFollowing` becomes 1 on the first update. Effectively: speed ≥10 → trail behind the velocity heading; speed <10 → sit behind the nose. Distance 50, height +10 relative to the car, look at car+10z. Hazard: `normalize()` of a zero horizontal velocity when |v|≥10 is purely vertical.
### 4.2 camera_fromadistance *(ParentScript 19)* — debug camera (hold key "1" while the race starts)
```ts
pcamera = camera[1]; pCar = gMainManager.getCar(); // BUG: mainManager has no getCar → script error in the original
pcamera.worldPosition = (0,-584,250); fieldOfView = 45; pheight = 640; pRadius = 480; pSpeed = 10;
update: p = car.xy; my = cam.xy; d = p - my;
if (|d| > pRadius) my += d.normalized() * pSpeed;
my.z = max(pheight, car.z + 200); cam.pos = my; cam.pointAt(car.worldPosition, (0,0,1));
```
Note `drivingManager` never calls `fadeIn` differently, and camera_fromadistance has no `fadeIn`/`carStoppedDriving` handlers. Not needed for the port except as an optional debug view.
### 4.3 drivingManager.firstFrame (called from realStart2)
```
pLightMapManager.doApplyUVs()
pcamera.fadeIn() // remove black overlay
sprite3d.addCamera(member("3d").camera("overlays")); that camera's rect = rect(0,0,640,480)
```
Then (checkpoint modes) `arrow.spriteReady()` adds `arrowcam` after it. Render order: camera[1] (world) → `overlays` (HUD) → `arrowcam` (arrow on top of HUD). Neither extra camera clears the color buffer (`clearAtRender = 0`).
### 4.4 fadein / fadeout *(ParentScripts 17, 18)*
Both are full-screen sprites (`sprite` ancestor, `#range: #all`) with member `spritenameshape` (a shape; assume solid black), locZ 1000, 640×480.
```ts
fadein(cb): blend = 100; each stepFrame: blend = max(0, blend - dtMs/10); if (blend <= 0) { destroy(); } // destroy → doCallback(cb)
fadeout(cb): blend = 0; gMach8GUIManager.disableInterfaceExcept([]); // all clicks blocked
each stepFrame: blend += dtMs/10; if (blend >= 100) { doCallback(cb); destroy(); } // destroy → enableInterface()
```
Duration: 1000 ms each (blend units are percent; 0.1 %/ms). fadein's first frame uses dt since construction.
### 4.5 insceneoverlay *(ParentScript 15)* — HUD quads in 3D
All HUD elements (timer digits, score digits, labels, messages) are textured planes parented to a dedicated camera `overlays`:
```ts
new(scene) {
pmodel = scene.newModel("overlay" & random(1000000), scene.modelResource("defaultmodel")); // Director default model resource (a plane is assumed)
pmodel.visibility = #both; pmodel.removeFromWorld(); pOn = 0;
pShader = newShader(..., #standard): specular 0, emissive #FFFFFF, diffuse 0, ambient 0, textureRepeatList[1] = 0 (clamp)
pmodel.shaderList[1] = pmodel.shaderList[2] = pShader;
if (!scene.camera("overlays")) { c = newCamera("overlays"); c.rootNode = c; c.removeFromWorld(); c.colorBuffer.clearAtRender = 0; }
setDepth(500); setPosition(point(100, 50)); stageResize();
}
setDepth(d) { pDepth = scene.camera[1].hither + d; figureOutUnitsAtDepth(); if (pPos) positionModel(); }
figureOutUnitsAtDepth() {
pVerticalUnits = pDepth * tan(camera("overlays").fieldOfView * 0.5 * PI/180) * 2; // overlays cam keeps Director's default FOV
pHorizontalUnits = pVerticalUnits * 640 / 480;
}
positionModel() {
const tX = pPos.x / 640, tY = 1 - pPos.y / 480;
pmodel.transform.position = ((tX - 0.5) * pHorizontalUnits, (tY - 0.5) * pVerticalUnits, -pDepth);
pmodel.transform.axisAngle = rotate(0,180,0).axisAngle; // face the camera
const [w, h] = pTexture ? [pTexture.width, pTexture.height] : [1, 1];
pmodel.transform.scale.x = w / 640 * pHorizontalUnits;
pmodel.transform.scale.y = h / 480 * pVerticalUnits;
}
// ⇒ a quad of texture-pixel size w×h, CENTRED at screen pixel pPos (y down), on a 640×480 screen.
setBlend(b) → pShader.blend = b; setColor(c) → pShader.emissive = c; setTexture(t) → textureList[1] = t; positionModel()
turnOn() { pmodel.parent = camera("overlays"); pOn = 1; } turnOff() { pmodel.removeFromWorld(); }
destroy() { deleteShader; deleteModel; }
```
Port equivalent: orthographic 640×480 HUD; quad centred at (x,y) of exactly the texture's pixel size; colour = texture × emissive; opacity = blend/100. All overlays sit at the same depth (draw order unspecified in the original).
### 4.6 3d_textsprite *(ParentScript 25)*
```ts
new(tText?) {
gen = member("textgenerator");
if (tText !== undefined) { gen.text = tText; gen.fontSize = 24; gen.fontStyle = [#bold]; } // never used with an arg
h = gen.height; pow = first of [2,4,8,16,32,64,128,256,512] >= h;
image 512×pow, 32-bit; alpha = gen.image.extractAlpha(); copy gen.image (gen.rect) to (0,0); setAlpha
pTexture = newTexture("text" & random(42342342), #fromImageObject, image) renderFormat #rgba8888;
insceneoverlay.new(member("3d")); setTexture(pTexture); turnOn(); setPosition(point(330, 200)); setBlend(0);
actorList.append(me); pLastFrameMS = now; pBlendTarget = 100;
}
stepFrame() {
const tDif = Math.max(1, now - pLastFrameMS); pLastFrameMS = now;
const b = clamp(getBlend() + (pBlendTarget - getBlend()) * Math.pow(0.003, 15.0 / tDif), 0, 100);
setBlend(b);
if (pBlendTarget == 0 && b == 0) destroy();
}
fadeOutAndDestroy() { pBlendTarget = 0; } // returns VOID
destroy() { actorList.remove(me); insceneoverlay.destroy(); deleteTexture(pTexture.name); }
```
Fade formula verbatim: per frame `k = 0.003^(15/tDif)`; at 25 ms (40 fps) `k = 0.003^0.6 ≈ 0.0306`, i.e. ~3 % of the remaining distance per frame (≈75 frames ≈ 1.9 s to reach 90 %). The rate is inverted versus normal smoothing (longer frames → larger step). Fading to 0 is asymptotic: whether it ever hits exactly 0 (and is destroyed) depends on float/blend precision — in practice the object lingers invisibly until the actorList is cleared.
The texture is 512 px wide regardless of text width, so text alignment inside the generator member determines horizontal placement (the generator's alignment is not visible in Lingo; assume left-aligned within the member rect; the member is probably centered-aligned — verify against `textgenerator` member data).
---
## 5. Game modes
### 5.1 gameTypeManager *(ParentScript 21)* — common base
- `start()`, `realStart()`, `realStart2()` — see §1.5.
- `getPointsCounterScript()` → `script("pointsgoalcounter")` (overridden by free_driving and checkpoint_chase to `pointscounter`).
- `havokUpdate()` (per chunk):
```
if keyPressed(53) { me.stopDriving(); me.destroy(); } // ESC → back to level select
if keyPressed("s") { the soundEnabled = not the soundEnabled } // toggles EVERY chunk while held (flicker)
```
- `stuntEvent(tPoints)`: `pTextMessageManager.setText(getStuntText(tPoints) & RETURN & tPoints & " Points!")`; `pPointsCounter.addPoints(tPoints)`.
- `stopDriving()`: `pStopping = 1`; `pTimer.stop()`; `drivingManager.stopDriving()` → `camera.carStoppedDriving()` (orbit) + `car.stopDriving()`.
- `destroy()`: see §1.5.
Important: modes that end via `me.pCarObject.stopDriving()` (stunt_rally, scavenger_hunt, supersonic_challenge) **do not** set `pStopping` and **do not** switch the camera to orbit; only checkpoint_chase (and ESC) use `me.stopDriving()`. After such an ending, prize/checkpoint detection keeps running (points/stings still occur; text is frozen because the text manager was stopped).
### 5.2 checkpoint_gametype *(ParentScript 46)* — base for checkpoint modes
```ts
realStart() {
super.realStart();
member("star").preload();
const tStar = p3d.cloneModelFromCastmember("star", "star", member("star"));
tStar.removeFromWorld(); tStar.scale(3, 3, 3);
pCheckPoints = [];
for (let i = 1; ; i++) {
const g = p3d.group("Checkpoint_" + i); // w3d contains groups "checkpoint_1".."checkpoint_10" (Director names are case-insensitive)
if (!g) break;
const m = tStar.clone("checkpoint" + i);
m.worldPosition = g.worldPosition - (0, 0, 50);
m.addToWorld(); // ALL stars visible until the first one is passed
pCheckPoints.push(m);
}
pCheckPointIndex = 1;
pArrow = arrow.new(); pArrow.pointAt(pCheckPoints[1].worldPosition);
}
realStart2() { super.realStart2(); pArrow.spriteReady(); }
havokUpdate() { // per chunk
super.havokUpdate(); // ESC / sound toggle
if (me.pStopping || pHidden) return;
if (me.pCar.position.distanceTo(pCheckPoints[pCheckPointIndex].worldPosition) < 300) { // 3D distance, RB position
sound(4).play({member: "stingcheckpoint"});
me.passedCheckPoint(); // subclass
me.showThisCheckpoint(pCheckPointIndex); // only the (new) current star is in the world
}
if (pCheckPointIndex <= pCheckPoints.count) pCheckPoints[pCheckPointIndex].rotate(0, 0, 3); // 3°/chunk spin
}
hideCheckpoints() { all.visibility = #none; pHidden = 1; } // stops detection permanently
showThisCheckpoint(i) { all.removeFromWorld(); pCheckPoints[i].addToWorld(); }
destroy() { pArrow.destroy(); super.destroy(); actorList.remove(me); }
```
Checkpoints are strictly ordered 1..N (N = 10 in the shipped level); only the current one is detected. Detection radius 300 world units from the car RB position to the star (group position − 50 z).
### 5.3 free_driving *(ParentScript 9)* — "Free Driving"
- Counter: `pointscounter` (5-digit "Points"; stunts add points via base `stuntEvent`).
- `realStart`: `pTimer.setTime(150)` → "02:30".
- `passedCheckPoint`:
```
setText("Checkpoint!" + RET + "+2 Seconds"); pTimer.addTime(2); index++
if index > count: index = 1; pLaps++
if (!pReachedGoal && pLaps > 1): // 2 laps
sound(4).play("stingwin")
selectLevelManager.unlock("checkpoint_chase")
setText("You did it!" + RET + "Checkpoint Chase unlocked!" + RET + "Free Driving Mode - Press ESC to exit")
pReachedGoal = 1; pArrow.hide()
pArrow.pointAt(cp[index].worldPosition)
```
After the goal, driving continues indefinitely; checkpoints keep cycling with "+2 Seconds" messages; arrow stays hidden.
- `outOfTime`: if `pReachedGoal` return; else `setText("Out of Time!" + RET + "Finish 2 laps in under 2:30 to unlock Checkpoint Chase" + RET + "Free Driving Mode - Press ESC to exit")`, `hideCheckpoints()`, `pArrow.hide()`. Car is **not** stopped; the player keeps free-driving until ESC. (No sting; `pOutOfTime = 1` is a local typo.)
- `destroy`: base + `actorList.deleteOne(me)`.
### 5.4 checkpoint_chase *(ParentScript 10)* — "Checkpoint Chase"
- Counter: `pointscounter`.
- `realStart`: `pTimer.setTime(10)` → "00:10".
- `passedCheckPoint`:
```
setText("Checkpoint!" + RET + "+10 Seconds"); pTimer.addTime(10); index++
if index > count: index = 1; pLaps++
if pLaps > 1: // 2 laps
sound(4).play("stingwin"); unlock("stunt_rally")
setText("You did it!" + RET + "Stunt Rally unlocked!" + RET); pTextMessageManager.stop()
me.stopDriving() // gameType version: pStopping, orbit camera
timeout(6000 ms → me.destroy()); pArrow.hide()
pArrow.pointAt(cp[index].worldPosition)
```
Time accumulates (unused seconds carry over) despite the menu text "Make it to each checkpoint in under 10 seconds".
- `outOfTime`: `sound(4).play("stingloseloop")`; `setText("Out of Time!" + RET + "Hit all Checkpoints to unlock Stunt Rally")`; text manager `stop()`; `me.stopDriving()`; `timeout(6000 → destroy)`; `pArrow.hide()`.
### 5.5 stunt_rally *(ParentScript 11)* — "Stunt Rally"
- Counter: `pointsgoalcounter`. `realStart`: `setTime(120)` ("02:00"), `setGoal(2500)`.
- `stuntEvent(p)`: `setText(getStuntText(p) + RET + p + " Points!")`; `addPoints(p)`.
- `reachedGoal` (from points counter): if `pOutOfTime` return; `pReachedGoal = 1`; `sound(4).play("stingwin")`; `unlock("scavenger_hunt")`; `setText("You did it!" + RET + "Scavenger Hunt Unlocked!")`; text `stop()`; `timeout(6000 → destroy)`; `pCarObject.stopDriving()`.
- `outOfTime`: if `pReachedGoal` return; `pOutOfTime = 1`; `pCarObject.stopDriving()`; `sound(4).play("stingloseloop")`; `setText("Out of Time!" + RET + "Get 2500 points to unlock Scavenger Hunt")`; text `stop()`; `timeout(6000 → destroy)`.
- No checkpoints, no arrow.
### 5.6 scavenger_hunt *(ParentScript 47)* — "Scavenger Hunt"
- Counter: `pointsgoalcounter`. `realStart`:
```
member("prize").preload(); tPrize = cloneModelFromCastmember("prize", "prizesbox", member("prize")); removeFromWorld
for i = 1 .. 139: g = group("prizes_" + i); if (!g) continue;
m = tPrize.clone("prize" + i); m.worldPosition = g.worldPosition; addToWorld; pPrizesNotFound.push(m)
setGoal(3000); setTime(5 * 60) // "05:00"
```
Shipped level has **54** prize groups: 1,2,4,6,7,8,9,10,11,12,16,18,19,20,24,25,27,28,29,30,31,32,41,48,50,56,60,64,65,71,75,79,84,88,95,98,102,105,113,115,117,119,121,123,124,126,127,129,130,131,132,134,136,138 (max 5400 points).
- `stuntEvent()` overridden empty — **stunts give nothing** (no message, no points).
- `havokUpdate` (per chunk): base; if `pStopping` return; for each remaining prize (reverse order): if `car.position.distanceTo(prize.worldPosition) < 100` → remove, `sound(4).play("stinghiddentreasure")`, `addPoints(100)`, `setText("+100 Points")`. Prizes do not rotate.
- `reachedGoal`: if `pOutOfTime` return; `pReachedGoal=1`; `stingwin`; `unlock("supersonic_challenge")`; `setText("You did it!" + RET + "Supersonic Challenge Unlocked!")`; text `stop()`; `pCarObject.stopDriving()`; `timeout(6000 → destroy)`.
- `outOfTime`: if `pReachedGoal` return; `pOutOfTime=1`; `stingloseloop`; `setText("Out of Time!" + RET + "Get 3000 points to unlock Supersonic Challenge" + RET + "Press ESC to exit")`; text `stop()`; `pCarObject.stopDriving()`; `timeout(6000 → destroy)`.
### 5.7 supersonic_challenge *(ParentScript 40)* — "Supersonic Challenge"
- Ancestor `checkpoint_gametype` (checkpoints + arrow) **plus** prizes (same spawn loop as scavenger) **plus** stunts. Counter `pointsgoalcounter`. `realStart`: `setTime(5*60)`, `setGoal(15000)`.
- `havokUpdate`: base (checkpoint logic) then, if not `pStopping`, prize pickup exactly as scavenger (radius 100, +100, `stinghiddentreasure`, "+100 Points").
- `stuntEvent(p)`: `setText(getStuntText(p) + RET + "+" + p + " Points!")`; `addPoints(p)`.
- `passedCheckPoint`:
```
index++
if index > count:
index = 1; pLaps++; pArrow.hide(); hideCheckpoints() // after ONE lap checkpoints are gone for good
if (pLaps > 0 && pReachedGoal) { win(); return; }
pArrow.pointAt(cp[index].worldPosition)
pTimer.addTime(2); addPoints(500) // may trigger reachedGoal → win() (laps>0)
setText("Checkpoint!" + RET + "+2 Seconds! +500 Points!")
```
- `reachedGoal`: if `pOutOfTime` return; `pReachedGoal = 1`; if `pLaps > 0` → `win()`.
- Win condition: **≥15000 points AND 1 completed lap** (in either order).
- `win()`: `stingwin`; `setText(RET + "Congratulations! You've won the Supersonic Challenge!" + RET + "Press ESC to return to the main menu.")`; text `stop()`; `pCarObject.stopDriving()`; `timeout(6000 → destroy)`. (No unlock; there is no flag preventing a second `win()` other than the stopped text manager.)
- `outOfTime`: `if (pReachedGoal && pLaps > 1) return;` (can never be true — laps max 1) → `pOutOfTime=1`; `stingloseloop`; `setText("Out of Time!" + RET + "Get 15000 Points and 1 Lap to win the game." + RET + "Press ESC to exit")`; text `stop()`; `pCarObject.stopDriving()`; `timeout(6000 → destroy)`. (After a win the timer is frozen by `car.stopDriving`, so outOfTime can't fire later.)
- Max non-stunt points: 10 checkpoints × 500 + 54 prizes × 100 = 10400 → ≥4600 must come from stunts.
### 5.8 selectLevelManager / unlocking *(ParentScript 8)*
- Buttons (order = unlock chain): `free`→free_driving, `checkpoint`→checkpoint_chase, `stunt`→stunt_rally, `scavenger`→scavenger_hunt, `supersonic`→supersonic_challenge.
- At creation (startup) every button except `free` is `Disable()`d, unless `debugModus()` (false) **or Control is held during startup** → all enabled.
- `unlock(name)`: enables the button whose script is `script(name)`. Unlocks persist only for the session (no save).
- `start()`: `the actorList = []`; `fadein.new()`; `mach8_go("selectlevel")`; `sound(8).play({member: "SSRMusicSplashScreen", loopCount: 0})` (loop forever).
- `buttonClicked(btn)`: `fadeout.new(callback(#start, helpManager, levelScript))`.
Unlock rules shown on `Background.png` (menu text, verbatim):
1. Free Driving: "Race through all checkpoints. Complete two laps in under 2:30 to unlock Checkpoint Chase."
2. Checkpoint Chase: "Complete two laps. Make it to each checkpoint in under 10 seconds to unlock Stunt Rally."
3. Stunt Rally: "Pull off stunts to gain points. Reach 2500 points in 2:00 to unlock Scavenger Hunt."
4. Scavenger Hunt: "Find as many hidden prizes as you can to add points. Gain 3000 to unlock Supersonic Challenge."
5. Supersonic Challenge: "Race through checkpoints, pull stunts, and find prizes. Get 15,000 points to win the Challenge!"
---
## 6. Points, stunts, timers, messages, arrow
### 6.1 stuntdetector *(ParentScript 35)*
```ts
new(gameType) {
drivingManager.registerForUpdate(me);
pCar, pCarRB, pWheels, pHavok = havok member;
pHavok.registerInterest("car", #all, 1, 0.1, #chassisCollision, me); // frequency 1, threshold 0.1
pJumpInProgress = 0;
}
havokUpdate() { // per chunk, after the game mode
let air = 1;
for (w of pWheels) if (w.pOnGround) air = 0; // jump = all four wheels off ground (also true when upside down!)
if (pJumpInProgress) {
pAngularDistance += pCarRB.angularVelocity.length * pHavok.timeStep; // havok member's timeStep property, NOT dt
pJumpMaxHeight = Math.max(pCarRB.position.z - pJumpStartPos.z, pJumpMaxHeight); // logged only
}
if (!air && pJumpInProgress) { // landed
const t = {...pCarRB.position, z: 0}; pJumpStartPos.z = 0;
const tDistanceCovered = pJumpStartPos.distanceTo(t); // logged only
let p = 2;
const tDuration = integer(simTime - pJumpStartTime); // rounded sim seconds
p *= Math.max(1, tDuration / 20.0);
p *= Math.max(1, pAngularDistance);
p *= 10;
if (p > 20) gameType.stuntEvent(integer(p));
// `put` debug lines: "duration ", "height ", "distanceCovered :", "angularDistance :", "maxcrash : ", "--"
}
if (air && !pJumpInProgress) { pJumpStartTime = simTime; pJumpStartPos = pCarRB.position; pJumpMaxHeight = 0; pAngularDistance = 0; pMaxCrash = 0; }
pJumpInProgress = air;
}
chassisCollision(info) { pMaxCrash = Math.max(pMaxCrash, info[5]); } // logged only
destroy() { drivingManager.unregisterForUpdate(me) /* handler doesn't exist */; } // never called
```
Effective scoring: `points = integer(20 * max(1, airSimSeconds_rounded/20) * max(1, angularDistance))`, awarded only if `> 20` — i.e. requires `angularDistance > 1` (in Havok's angular-velocity units × timeStep; the air-time factor only matters after >20 sim s ≈ 10 real s airborne). Pure airtime without rotation scores nothing. Note `angularDistance` integrates per chunk using a constant `timeStep`, not the real chunk dt, so it is frame-rate dependent.
### 6.2 getStuntText *(MovieScript 1)*
`tPoints = tPoints / 10` (integer division), then first match:
| tPoints/10 | Text |
|---|---|
| > 80 | "Inconceivable!" |
| > 75 | "Phenomenal!" |
| > 70 | "Huge stunt!" |
| > 65 | "Fantastic stunt!" |
| > 60 | "Mind-blowing!" |
| > 55 | "Extraordinary!" |
| > 50 | "Astonishing!" |
| > 45 | "Unbelievable!" |
| > 40 | "Wicked stunt!" |
| > 35 | "Incredible!" |
| > 30 | "Awesome!" |
| > 25 | "Wow!" |
| > 20 | "Amazing!" |
| > 15 | "Great stunt!" |
| > 10 | "Nice one!" |
| > 5 | "Nice stunt!" |
| else | "Not bad!" |
(e.g. 21–59 pts → "Not bad!", 60–109 → "Nice stunt!", ≥810 → "Inconceivable!").
### 6.3 timer / chasetimer *(ParentScripts 14, 34)*
Digit textures (created once per race by `timer.new`):
```
gen = member("timerfontgenerator"); chars = "1234567890:/ "
for each char c: gen.text = c; img = 32×64 32-bit filled white; a = 32×64 8-bit grayscale filled white;
a.copyPixels(gen.image, gen.rect, gen.rect); img.setAlpha(a); useAlpha = 1
texture "char_" + c (#fromImageObject, rgba8888) // glyph shape is in ALPHA (black text → opaque?) — see note
```
Note: alpha = generator image copied into a white-filled grayscale image; with black text on white, alpha is 0 where ink is and 255 elsewhere *unless* the generator image is white-on-transparent. Verify visually; the intended look is white digits (RGB is pure white; colour comes from shader emissive).
HUD:
- 5 overlays at `point(16 + ii*18, 50)`, ii = 1..5 → x = 34, 52, 70, 88, 106; y = 50 (each 32×64, 18 px pitch, overlapping).
- Label: `textgenerator.text = "Time remaining" + RETURN`, fontStyle [bold, italic], fontSize 12, fixedLineSpace 0, color white → `3d_textsprite` at `point(72, 20)`.
```ts
// timer.stepFrame (per frame)
t = getTime(); if (t == 0 && !pOutOfTime) { pOutOfTime = 1; gameMode.outOfTime(); }
s = getTimeString(); for i: digit[i].setTexture(tex["char_" + s[i]]);
getTimeString() { t = getTime(); mm = t / 60 /*int div*/; ss = t - mm*60; return pad0(mm,2) + ":" + pad0(ss,2); } // "MM:SS"
stop() { pStopTime = getTime(); pStopped = 1; }
// chasetimer overrides (countdown):
new(gm) { super.new(gm); setTime(20); pLastOink = 10; }
setTime(T) { pStartTime = integer(simTime / 3); pTotalTime = T; }
getTime() {
if (pStopped) return pStopTime;
const t = Math.max(0, integer(-simTime / 3 + pStartTime + pTotalTime));
if (t < pLastOink) { pLastOink = t; sound(4).play({member: "alarm"}); } // beeps at 9,8,…,0
return t;
}
addTime(T) { pTotalTime += T; pLastOink = Math.min(10, pTotalTime); }
```
`subtractTime` exists in timer but references an undeclared property; unused.
### 6.4 pointsgoalcounter *(ParentScript 28)* (stunt, scavenger, supersonic)
- Uses textures `char_0..9`, `char_/`, `char_ ` (created by the timer).
- 11 overlays at `point(426 + ii*18, 50)`, ii = 1..11 → x = 444…624. ii 1–5 emissive red `rgb(255,0,0)` (score), ii 6 white ("/"), ii 7–11 green `rgb(0,255,0)` (goal).
- Label: `"Score / Goal" + RETURN`, word 1 ("Score") red, word 3 ("Goal") green, ("/" default white), bold italic 12, fixedLineSpace 0 → `3d_textsprite` at `point(552 - 18, 20)` = (534, 20).
- String: `padSpaceLeft(points, 5) + "/" + padSpaceRight(goal, 5)` e.g. `" 300/2500 "`.
- `addPoints(p)`: `pPoints = min(99999, pPoints + p)`; if `pPoints >= pGoal` and not yet reached → `pReachedGoal = 1`, digits 1–5 turn green, `gameMode.reachedGoal()`; then redraw. Initial goal 500 before `setGoal`.
### 6.5 pointscounter *(ParentScript 29)* (free driving, checkpoint chase)
- 5 overlays at `point(544 - 18 + ii*18, 50)` → x = 544, 562, 580, 598, 616; white.
- Label `"Points" + RETURN` white, bold italic 12 → at `point(582, 20)`.
- String: `padZeroLeft(points, 5)` e.g. "00120". `addPoints`: `min(99999, …)`, no goal.
### 6.6 textmessagemanager *(ParentScript 49)*
```ts
setText(text) {
if (pStopped) return;
gen.text = text; gen.line[1].fontSize = 24; gen.line[2].fontSize = 18; gen.line[3].fontSize = 12;
gen.fixedLineSpace = 0; gen.fontStyle = [bold, italic]; gen.color = white;
if (pCurrentSprite) pCurrentSprite.destroy(); // previous message vanishes instantly
pCurrentSprite = new 3d_textsprite(); pCurrentSprite.turnOn(); pCurrentSprite.setPosition(point(320, 400));
pCurrentSpriteStartedAt = now_ms;
}
stop() { pStopped = 1; } // freezes the final message on screen
stepFrame() { if (pCurrentSprite && now_ms > pCurrentSpriteStartedAt + 5000) pCurrentSprite = pCurrentSprite.fadeOutAndDestroy(); /* → VOID, sprite fades on its own */ }
destroy() { pCurrentSprite?.destroy(); actorList.remove(me); }
```
Messages fade in with the 3d_textsprite curve (§4.6), stay 5000 ms real time from creation, then fade out. Fonts in the text members: Arial / Verdana.
### 6.7 arrow *(ParentScript 42)*
```ts
new() {
member("arrow2").preload();
pmodel = p3d.cloneModelFromCastmember("pointarrow", "arrow", member("arrow2"));
pCarCam = p3d.camera[1];
pcamera = p3d.newCamera("arrowcam"); pcamera.rootNode = pmodel; pcamera.removeFromWorld(); pcamera.transform = identity;
pmodel.parent = pcamera;
pmodel.transform.position = (0, -12, -50); scale = (0.05,0.05,0.05); rotation = (-70, 0, 0);
pmodel.pointAtOrientation = [(0,1,0) /*front*/, (0,0,1) /*up*/];
pDummy = pmodel.clone("arrowdummy"); pDummy.transform.identity(); same pointAtOrientation; pDummy.parent = pcamera;
pTarget = vector(); actorList.append(me);
}
spriteReady() { sprite3d.addCamera(pcamera); rect = (0,0,640,480); pcamera.colorBuffer.clearAtRender = 0; }
stepFrame() { // per frame
pcamera.transform = pCarCam.transform; // follows main camera; own projection (default FOV)
pTarget.z = pmodel.worldPosition.z; // horizontal pointing
pDummy.pointAt(pTarget, pCarCam.transform.yAxis);
pmodel.transform.rotation = pDummy.transform.rotation; // replaces the initial -70° tilt
}
hide() { pmodel.visibility = #none; }
```
The arrow is drawn by its own camera after the HUD, at camera-space (0,-12,-50) (lower centre of the screen), its +Y axis pointing horizontally at the current checkpoint.
---
## 7. Audio
Director sound channels (volume 0–255; `#rateShift` is in **semitones**, rate = 2^(n/12); `loopCount: 0` = loop forever).
| Ch | Use | Volume | Script |
|---|---|---|---|
| 1 | engine (CarStart/CarLoop/CarEnd); also menu `buttonclick` | 25 (set by carsound.new and never restored → menu clicks at 25 after first race; default 255 before) | carsound, standardButton |
| 2 | steering servo (CarTurnLeft0n / CarTurnRight0n) | 20 | carsound |
| 3 | crashes | 125 (set right after play) | car.collision |
| 4 | stings (checkpoint, win, lose loop, hidden treasure) and countdown `alarm` (all share ch 4 and cut each other) | default | modes, chasetimer |
| 8 | music: `SSRMusicSplashScreen` (menus, loop), `musicloop1..5` (driving) | menus default/unchanged; driving fades to 75 | selectLevelManager, carsound, helpManager |
### 7.1 carsound *(ParentScript 22)*
```ts
new(car, wheels) {
pMusicLoops = [musicloop1..musicloop5]; pLeftLock = pRightLock = 0; pPlaying = 0; pShift = 1; pStopped = 0; pState = #middle;
if (!debugModus()) {
sound(1).volume = 25; sound(2).volume = 20;
sound(8).stop(); sound(8).play({member: random loop}); sound(8).fadeTo(75, 500);
}
}
update() { // per chunk
if (!debugModus() && sound(8).getPlaylist().count < 3 && !pStopped)
sound(8).queue({member: random loop}); // keeps 3 random loops queued (repeats allowed)
let throttle, l, r;
if (pStopped) throttle = l = r = 0;
else { throttle = keyPressed(126) || keyPressed(125); l = keyPressed(123); r = keyPressed(124); }
if (throttle && !pPlaying) {
const tStart = pCar.getRB().linearVelocity.length * 4 - 150; // ms into CarStart
sound(1).play({member: "CarStart", rateShift: pShift, startTime: Math.max(0, tStart)});
sound(1).queue({member: "CarLoop", loopCount: 0, rateShift: pShift});
pPlaying = 1;
} else if (pPlaying && !throttle) {
if (sound(1).member == member("carstart"))
sound(1).play({member: "carend", startTime: Math.max(0, member("carend").duration - sound(1).currentTime / 2 - 10)});
else sound(1).play({member: "CarEnd"});
pPlaying = 0;
}
if (l && pState != #left) { sound(2).play("carturnleft0" + random(3)); pState = #left; }
else if (r && pState != #right) { sound(2).play("carturnright0" + random(3)); pState = #right; }
if (!r && !l && pState != #middle) { // servo returns: opposite sound
sound(2).play(pState == #left ? "carturnright0" + random(3) : "carturnleft0" + random(3));
pState = #middle;
}
let tShift = 2 - (pWheels[1].pOnGround + pWheels[2].pOnGround) * 0.5 + 0.1; // both front down 1.1, one 1.6, none 2.1
if (pCar.pReversing) tShift = 0.2;
if (pShift != tShift && pPlaying) sound(1).play({member: "carloop", loopCount: 0, rateShift: tShift}); // abrupt restart
pShift = tShift;
}
stopDriving() { pStopped = 1; sound(8).fadeOut(500); }
destroy() { sound(1).stop(); sound(2).stop(); }
```
`random(3)` ∈ {1,2,3}. Engine pitch does **not** follow speed; only airborne front wheels / reversing change it. Speed only affects where CarStart begins.
### 7.2 Other sound events
- Title/help/select: `buttonclick` on ch1 at every mouseDown on any standardButton (incl. full-screen title/help backgrounds and disabled level buttons).
- `selectLevelManager.start`: ch8 `SSRMusicSplashScreen` loop.
- `helpManager.bgClicked`: `sound(8).fadeOut(500)` before the race.
- Checkpoint pass: ch4 `stingcheckpoint` (then possibly `stingwin` immediately on the same channel).
- Prize: ch4 `stinghiddentreasure`. Win: ch4 `stingwin`. Lose: ch4 `stingloseloop` (member name says loop; played once with default loopCount).
- Countdown: ch4 `alarm` once per second for remaining 9..0 (only when it drops below `pLastOink`).
- Crash: ch3, see §3.4.
- `s` toggles `the soundEnabled` (global mute) every chunk while held.
Sound files: 16 kHz mono 16-bit WAVs (`CarStart`, `CarLoop`, `CarEnd`, `CarTurnLeft01..03`, `CarTurnRight01..03`, `Crash1..2`, `crash3..8`, `Crash1..3_glass`, `Alarm`, `StingWin`, `StingCheckpoint`, `StingLoseLoop`, `StingHiddenTreasure`, `buttonclick`) and MP3 music (`SSRMusicSplashScreen`, `musicloop1..5`).
---
## 8. Lightmaps *(ParentScript 23 lightMapManager, MovieScript 24 UV_Lightmap_UVs, MovieScript 36 mCloneShader)*
### 8.1 Data shape — `getUVs_UV_Lightmap()`
A single Lingo propList literal (466 KB):
```
[ "<modelName>": [ #uvs: [ meshUVs_1, meshUVs_2, … ], #lightmap: "<lightmapName>" ], … ]
meshUVs_k = [ [u, v], [u, v], … ] // one [u,v] per entry of meshDeform.mesh[k].textureCoordinateList
```
- 102 models. Mesh index `k` = meshDeform mesh index = shader slot index (one mesh per shader/material in W3D).
- Lightmap names: `"001"`…`"007"`, `"MainA"`, `"MainB"` → bitmap members `<name>LightingMap` (512×512, `bitmaps/<name>LightingMap.png`).
- Models with `#uvs` but **no `#lightmap`**: `arrow`, `car`, `Cylinder02`, `Cylinder04`, `Cylinder05` (skipped by shader setup but still receive UVs in `applyUVs`).
- Lightmap → models:
- `001`: Lighting, Box03–Box18
- `002`: Dbody, Ramp12, Ramp48, ToyS105, ToyS111, ToyS112, ToyS114–ToyS118, Wood03, Ramp50–Ramp53
- `003`: Mddle Ramp, B01, Ramp25, Stairs, ToyS93, ToyS95–ToyS100, ToyS110, Ramplong06–Ramplong11, Wood02
- `004`: Ramp05, Ramp23, Ramplong01, ToyS58, Ramp24, Ramp34, Ramplong04, Ramplong05, Ramp40, Ramp41, ToyS80–ToyS88, Wood04, Ramplong12
- `005`: Ramp16, Big Ramp, BigRamp, Ramp36, Ramp37, Ramp38, Ramp46, Ramp47, ToyS89, ToyS90, ToyS92, Ramp49
- `006`: Box01, Ramp29, ToyS103, 2Ramp01, Object04
- `007`: Ramp08, Ramp44, ToyS104, Box02, Wood01
- `MainA`: MainA (9 meshes); `MainB`: MainB (9 meshes)
- Values are mostly in [0,1]; ~5 % are leftovers far outside (e.g. 9.995, 100.0, -734.04, 8547.34) — these are the model's *original* UVs for vertices that no mapped face touched (see 8.4) and should be harmless (unreferenced or clamped).
### 8.2 Indexing semantics (critical for remapping)
The data is **per-vertex, aligned with `meshDeform.mesh[k].vertexList` order** of each W3D mesh: the generator (`extractUVsFrom`, §8.4) starts from `textureCoordinateList`, and for each face `f = mesh.face[o]` (a list of 3 *vertex indices*) writes `uv[f[1]]`, `uv[f[2]]`, `uv[f[3]]`; in `constructBigLists` it also iterates `textureCoordinateList[o]` together with `vertexList[o]`. So the authors assumed `textureCoordinateList.count == vertexList.count` and index-aligned. At runtime `applyUVs` assigns the list to `mesh[k].textureLayer[2].textureCoordinateList` (the mesh's faces then use the same indices). To port: for each W3D model/mesh, take the W3D vertex order (and face index triplets) and build a second UV attribute (`uv2`/`uv1` in three.js) with `uv2[i] = meshUVs_k[i]`. If the glTF conversion re-ordered or split vertices, the remap must go through the original W3D vertex indices (keep a mapping from glTF vertex → W3D (mesh, vertex) index). V-axis convention: Director texture coordinates have v up (OpenGL style); three.js with `flipY=true` textures matches GL — verify on one model.
### 8.3 Runtime behaviour
`lightMapManager.new()` (in `drivingManager.start`, after `resetWorld`):
```ts
gJo = me;
// authoring only (runMode <> "Plugin"): UV script text from getPref("lightmapinfo"); import "<lm>LightingMap.bmp" files.
const tUVs = getUVs_UV_Lightmap();
for (model of member("3d").models) {
const e = tUVs[model.name]; if (!e || !e.lightmap) continue;
for (ii = 1 .. model.shaderList.count) {
model.shaderList[ii] = mCloneShader(member("3d"), model.shaderList[ii]); // per-model, per-slot clone "<name>_Clone", "<name>_Clone1", …
const lmName = e.lightmap + "LightingMap";
let tex = member("3d").texture(lmName)
?? newTexture(lmName, #fromCastMember, member(lmName)) with renderFormat #rgba8880; // shared per lightmap
shader.textureList[2] = tex; // layer 2, default textureMode (#none = use mesh UV layer 2), default blendFunction (#multiply)
shader.useDiffuseWithTexture = 1;
// (redundant) if textureList[1] = VOID then useDiffuseWithTexture = 1
if (shader.textureList[1].name == "DefaultTexture") shader.textureList[1] = VOID; // untextured surfaces: diffuse × lightmap
}
}
// hard-coded tweaks (on the ORIGINAL shaders by name, except D_Clone):
shader("column"): ambient rgb(0,0,0); diffuse rgb("111133"); specular rgb(5,5,5); useDiffuseWithTexture = 1
shader("Ex_Main_02"): ambient rgb(0,0,0); diffuse rgb("111133"); specular rgb(5,5,5); useDiffuseWithTexture = 1
shader("D_Clone"): ambient rgb(0,0,0); specular rgb(10,10,10); diffuse rgb("441111"); textureList[2] = VOID // lightmap removed
```
`mCloneShader` *(MovieScript 36)* copies, for `#standard`: ambient, diffuse, useDiffuseWithTexture, specular, emissive, shininess, blend, transparent, renderStyle, flat, and for every non-void texture layer m: textureList, textureModeList, wrapTransformList, textureTransformList, blendFunctionList, blendSourceList, blendConstantList, textureRepeatList. (Painter/newsprint/engraver branches unused.)
`doApplyUVs()` (from `drivingManager.firstFrame`):
```
save camera[1].worldPosition; camera[1].worldPosition = (0,0,50000); camera[1].pointAt(vector()); force a render (set the stageColor to the stageColor)
applyUVs(tUVs, 2)
camera[1].worldPosition = saved (rotation not restored; the chase camera fixes it next chunk)
```
`applyUVs(tUVs, channel=2)`:
```
for model of member("3d").models with tUVs[model.name]: // includes the 5 no-lightmap models
model.resource.lod.auto = 0; model.resource.lod.level = 100; model.addModifier(#meshDeform)
for k = 1 .. model.meshDeform.mesh.count:
uvs = tUVs[model.name].uvs[k]
while mesh[k].textureLayer.count < 2: mesh[k].textureLayer.add()
if model.shaderList[k].textureList[1] == VOID: // base texture was DefaultTexture
mesh[k].textureLayer[1].textureCoordinateList = uvs // layer 1 gets lightmap UVs too (log: "putting in bullshit uv's in first layer…")
mesh[k].textureLayer[2].textureCoordinateList = uvs
```
Port summary: every listed mesh gets `uv2 = lightmap UVs`; material = (base texture or diffuse colour) × lightmap texture (multiply), with `useDiffuseWithTexture` meaning the diffuse colour also modulates the base texture. `D_Clone` (first clone of shader `D`, presumably the `Dbody` material) has no lightmap and gets diffuse #441111. `column` and `Ex_Main_02` tweaks apply to the original shaders — models that were cloned no longer use them (only un-lightmapped users would be affected; see Open Questions).
### 8.4 Authoring-time generator (for reference only)
`extractUVsFrom(tUVSource)` mapped every face of member `geometry` to a face of the lightmap-unwrapped member (models `001..007`, `MainA`, `MainB`) by world-space vertex proximity (all 3 corners within ε, ε = 0.5 doubling on failure), and copied the source face's UVs onto the 3 vertex indices; models mapping onto two different lightmaps were skipped with an alert; unmapped entries equal to 0 were replaced by `[0,0]`. Output written as the `getUVs_<name>` movie script. `stepFrame` re-apply-on-Ctrl is dead code (`return` first).
---
## 9. UI screens
All screens are 640×480; elements centred at (320,240) unless noted.
- **Title** *(titleManager)*: `standardButton` over full-screen `Title-v8` (no Over/Down/Gray variants → same image). `start()`: `fadein`, `mach8_go("title")`. Click → `fadeout` → select level. (No music on the title screen; the first music is the splash on the select screen.)
- **Select level** *(selectLevelManager)*: `Background` (sprite "bg") + 5 `standardButton`s. Button members and their screen rects (sprite loc (320,240), rect = loc − regPoint, size from member; size re-fit per state):
| Button | Normal | Over | Down | Gray |
|---|---|---|---|---|
| free | `Free` (30,67)–(321,123) | `FreeOver` (30,65)–(321,124) | `FreeDown` (31,68)–(320,122) | `FreeGray` (30,67)–(321,123) |
| checkpoint | `Checkpoint` (30,150)–(321,207) | `CheckpointOver` (30,148)–(321,207) | `CheckpointDown` (31,151)–(320,206) | `CheckpointGray` (30,150)–(321,207) |
| stunt | `Stunt` (30,234)–(321,291) | `StuntOver` (30,233)–(321,292) | `StuntDown` (31,235)–(320,290) | `StuntGray` (30,234)–(321,291) |
| scavenger | `Scavenger` (30,318)–(321,375) | `ScavengerOver` (30,317)–(321,376) | `ScavengerDown` (31,319)–(320,374) | `ScavengerGray` (30,318)–(321,375) |
| supersonic | `Supersonic` (30,402)–(321,459) | `SupersonicOver` (30,401)–(321,460) | `SupersonicDown` (31,403)–(320,458) | `SupersonicGray` (30,402)–(321,459) |
States: normal; rollover on mouse enter; pressed (Down) on mouse down / re-entering while held; Gray when disabled (no hover/press response, but click sound still plays). Action fires on mouse up over the button after a press on it.
- **Help** *(helpManager)*: full-screen `Help-v5` standardButton; any click starts the chosen mode.
- **Loading**: frame `loading` shows sprite "text" (loc (173,206), 318×44; text "Initializing" per member data) over the stage colour; fadein → (blocking load) → fadeout.
- **Driving**: 3D sprite 640×480 + HUD (§6) + arrow; driving view appears abruptly after the loading fade-out.
- **Fades**: 1000 ms black fade in/out via a locZ-1000 full-screen shape; interface disabled during fade-out.
- FPS counter behaviour *(BehaviorScript 76)* exists (100-sample average, updates every 2000 ms into the `fps` field, text "NN Fps") but is not placed in the shipped score.
---
## 10. Input
| Key | Director test | Meaning | Where |
|---|---|---|---|
| ↑ | `keyPressed(126)` | accelerate (front wheels +6300) | wheel; engine sound (carsound) |
| ↓ | `keyPressed(125)` | brake (−10000) when forward speed ≥0; reverse (−4000) when <0; also counts as "throttle" for engine sound | wheel, carsound |
| ← | `keyPressed(123)` | steer left (+1°/chunk up to +5°) | wheel, carsound |
| → | `keyPressed(124)` | steer right (−1°/chunk down to −5°) | wheel, carsound |
| Space | `keyPressed(" ")` | hard brake on front wheels (−10000·sign(v) if \|v\|>10) and rear lateral grip ×0.3 | wheel |
| Esc | `keyPressed(53)` | quit race to level select (works during end screens) | gameTypeManager.havokUpdate |
| S | `keyPressed("s")` | toggle `the soundEnabled` (every chunk while held) | gameTypeManager.havokUpdate |
| 1 (held at race start) | `keyPressed("1")` | use camera_fromadistance (crashes in original: `gMainManager.getCar`) | drivingManager.start |
| Ctrl (held at movie start) | `the controlDown` | unlock all levels | selectLevelManager.new |
| Mouse | click | title / help: anywhere; select: buttons | GUILibrary |
Steering/input polling happens per chunk inside wheel.update; when `pStopped` all four arrow inputs read as 0.
---
## 11. Open questions / ambiguities
1. **Havok force point space.** `applyForceAtPoint` is fed car-local points (§3.8). Must determine the Xtra's real semantics (body-origin-relative vs COM-relative; rotated or not) by experiment; world-space interpretation is impossible given the spawn position.
2. **Effective gravity / world scale.** `havok.hke` contains gravity (0,0,−9.81) (offset ≈125609, after `default_subspace`); code multiplies by 0.2. Whether the Xtra applied a world-units scale (e.g. ×100) is unknown; this sets spring strength (`3*|g|/4*10`), jump arcs, and lateral grip (`3*pLoad`). With |g|=1.962 the drive force (6300 on mass 3) dwarfs gravity, so a scale factor is likely. Needs tuning against footage.
3. **Collision details list.** `t1[5]` (car crash sound threshold >1) and `info[5]` (stuntdetector max crash) — assumed impact/closing-velocity magnitude; exact meaning and units unknown. Also the semantics of `registerInterest` frequency 0 vs 1 and threshold 0/0.1.
4. **`pHavok.timeStep`** used in angular-distance integration: value after `step(dt*2.5, 7)` calls (probably the hke/default 1/60, or dt*2.5/7?). Determines stunt scores.
5. **Angular velocity units** (rad/s vs deg/s) in Director Havok — affects stunt scoring (`angularDistance > 1` threshold) and the collision clamp `tMax = 4`.
6. **`pCarRB.centerOfMass`**: local or world offset? It is added unrotated to `position`.
7. **Visual wheel ↔ mesh mapping**: relies on W3D child order of holypolycrap `Car`; glb shows Wheel2, Wheel1, Wheel3, Wheel4 — confirm W3D order (could make the front pair use the rear meshes or vice-versa). The 180° Y flip for indices 1–2 and the −3× visual steering sign must be validated visually.
8. **Car initial orientation**: inherited from the `car` model in 3d.w3d (not set in code); read it from the W3D.
9. **Director `modelResource("defaultModel")` geometry** used for shadow and overlays (assumed unit plane in XY facing +Z; `shaderList[2]` suggests a 2-mesh/double-sided resource). Also default `pointAtOrientation` for the shadow and the default FOV of `newCamera` (overlays, arrowcam; Director default believed 30°).
10. **Checkpoint group names**: code asks `"Checkpoint_" & i`, file has `checkpoint_1..10` (assume case-insensitive). Positions of `checkpoint_*` / `prizes_*` groups must be read from 3d.w3d (the glb export dropped groups).
11. **3d_textsprite fade** never reaches exactly 0 in floats → objects may never self-destroy; harmless in practice. Also text horizontal alignment within the 512-wide texture (generator member alignment) — check the `textgenerator` member.
12. **Timer digit alpha** construction (white texture, alpha from generator image) — confirm the digit glyph appears as opaque-on-transparent rather than inverted.
13. **Music volume after a race**: `carsound.stopDriving` / `helpManager` call `sound(8).fadeOut(500)`; the next `sound(8).play(SSRMusicSplashScreen)` does not reset volume — the splash music may be silent (volume 0) on return to the menu in the original. Decide whether to replicate.
14. **Shader tweaks on originals**: `column` and `Ex_Main_02` are tweaked by name after lightmapped models received clones; if every user of those shaders is lightmapped, the tweak is dead. `D_Clone` is only the *first* clone of `D`. Check which models use these materials (`D` likely = Dbody).
15. **Havok re-initialisation between races**: `pHavok.Initialize` is called again each race without `shutDown`; and a new `3d2` duplicate accumulates each race. Port: just rebuild the world per race.
16. **sign(0) = +1** in the stopped-car brake (`-10000*sign(vF)`) causes a backward push at exactly zero speed / jitter at rest after `stopDriving`. Replicate or damp.
17. **The static `car` proxy and `arrow` scene model in `3d2`** act as invisible raycast obstacles (wheels, camera, shadow) near (155.6,-77.5,40.4) and (357.7,269.4,259.7); visual-only `Box03`–`Box18` and `Lighting` are ray-hittable but have no Havok collision. Decide whether to replicate.
18. Supersonic `outOfTime` guard `pReachedGoal and pLaps > 1` is unreachable (laps cap at 1); free_driving `pOutOfTime` is a dead local; `tBraking`, `pRadius`, `pForceMultiplier`, `wheelrb` models, `pOldBehindDir`, `lingoRayCaster`, `prepareForRay` are dead.
19. Frame-rate dependence: steering ramp, camera lerp (0.1/chunk), odometer, star spin, angular-distance integration and the 3d_textsprite fade are per chunk/frame without dt. A faithful port should run the original 40 Hz frame clock with the exact 20 ms chunking of §2.
