lecodes-cli 0.18.0 → 0.18.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.js +2013 -564
- package/package.json +4 -4
- package/runtime/scene-harness.json +1 -1
- package/runtime/sdk/core/Aspect.ts +512 -255
- package/runtime/sdk/core/compWrite.ts +42 -0
- package/runtime/sdk/core/fields.ts +1 -1
- package/runtime/sdk/core/time.ts +81 -0
- package/runtime/sdk/g2/Camera2D.ts +8 -1
- package/runtime/sdk/g2/CharacterController2D.ts +253 -53
- package/runtime/sdk/g2/Node2D.ts +80 -10
- package/runtime/sdk/g2/OneWay2D.ts +66 -0
- package/runtime/sdk/g2/Physics2D.ts +240 -30
- package/runtime/sdk/g2/Scene2D.ts +33 -1
- package/runtime/sdk/g2/Shape2D.ts +218 -22
- package/runtime/sdk/g2/Trigger2D.ts +42 -12
- package/runtime/sdk/g2/groups2d.ts +106 -0
- package/runtime/sdk/g2/loop.ts +15 -4
- package/runtime/sdk/gl/Camera.ts +40 -1
- package/runtime/sdk/gl/CameraPlace.ts +52 -51
- package/runtime/sdk/gl/CharacterController.ts +184 -56
- package/runtime/sdk/gl/Gearbox.ts +212 -0
- package/runtime/sdk/gl/Geometry.ts +70 -9
- package/runtime/sdk/gl/Light.ts +64 -2
- package/runtime/sdk/gl/Lightmap.ts +179 -0
- package/runtime/sdk/gl/Material.ts +25 -0
- package/runtime/sdk/gl/Mesh.ts +6 -23
- package/runtime/sdk/gl/Model.ts +16 -2
- package/runtime/sdk/gl/Node.ts +119 -39
- package/runtime/sdk/gl/Physics.ts +75 -24
- package/runtime/sdk/gl/Scene.ts +161 -5
- package/runtime/sdk/gl/Shape.ts +42 -3
- package/runtime/sdk/gl/Trigger.ts +45 -50
- package/runtime/sdk/gl/Vehicle.ts +276 -322
- package/runtime/sdk/gl/Wheel.ts +240 -0
- package/runtime/sdk/gl/scenarios.ts +4 -30
- package/runtime/sdk/gl/state.ts +6 -6
- package/runtime/sdk/inject.ts +186 -171
- package/runtime/sdk/runtime/device.ts +102 -1
- package/runtime/sdk/scene/defineScene.ts +108 -53
- package/runtime/sdk/scene/gizmos.ts +31 -11
- package/runtime/sdk/scene/material.ts +188 -0
- package/runtime/sdk-types.json +1 -1
- package/runtime/sdk/compile/aspectMacro.ts +0 -42
- package/runtime/sdk/compile/assetIconMacro.ts +0 -384
- package/runtime/sdk/compile/assetMacro.ts +0 -45
- package/runtime/sdk/compile/assetName.ts +0 -50
- package/runtime/sdk/compile/bundler.ts +0 -252
- package/runtime/sdk/compile/compileProject.ts +0 -129
- package/runtime/sdk/compile/detectEntry.ts +0 -128
- package/runtime/sdk/compile/fontMacro.ts +0 -459
- package/runtime/sdk/compile/fontRegistry.ts +0 -78
- package/runtime/sdk/compile/header.ts +0 -67
- package/runtime/sdk/compile/index.ts +0 -85
- package/runtime/sdk/compile/libraryImports.ts +0 -52
- package/runtime/sdk/compile/liteMaterial.ts +0 -247
- package/runtime/sdk/compile/sceneEditor.ts +0 -78
- package/runtime/sdk/compile/serverSplit.ts +0 -233
- package/runtime/sdk/compile/serverTypes.ts +0 -227
- package/runtime/sdk/compile/sfnt.ts +0 -98
- package/runtime/sdk/compile/shaderTargets.ts +0 -42
- package/runtime/sdk/compile/sourcemap.ts +0 -25
|
@@ -2,28 +2,28 @@
|
|
|
2
2
|
// node is the CHASSIS: it needs a Shape (its collision body) and must NOT also carry a Physics aspect
|
|
3
3
|
// — Vehicle owns its rigid body, the way CharacterController owns its controller.
|
|
4
4
|
//
|
|
5
|
-
// const car = new Node(
|
|
5
|
+
// const car = new Node('car')
|
|
6
|
+
// .add(body, wheelFL.aspect(Wheel), wheelFR.aspect(Wheel),
|
|
7
|
+
// wheelRL.aspect(Wheel), wheelRR.aspect(Wheel))
|
|
6
8
|
// .aspect(Shape, { box: [0.9, 0.4, 2.1] })
|
|
7
|
-
// .aspect(Vehicle, { mass: 1200
|
|
8
|
-
// { node: wheelFL }, { node: wheelFR }, { node: wheelRL }, { node: wheelRR },
|
|
9
|
-
// ] })
|
|
9
|
+
// .aspect(Vehicle, { mass: 1200 })
|
|
10
10
|
//
|
|
11
11
|
// setLoop(() => {
|
|
12
|
-
// const gas = (Input.key(
|
|
13
|
-
// car.vehicle.drive(gas, (Input.key(
|
|
14
|
-
// hud.text = `${Math.round(car.vehicle.speed * 3.6)} km/h gear ${car.
|
|
12
|
+
// const gas = (Input.key('KeyW') ? 1 : 0) - (Input.key('KeyS') ? 1 : 0)
|
|
13
|
+
// car.vehicle.drive(gas, (Input.key('KeyD') ? 1 : 0) - (Input.key('KeyA') ? 1 : 0), 0, Input.key('Space') ? 1 : 0)
|
|
14
|
+
// hud.text = `${Math.round(car.vehicle.speed * 3.6)} km/h gear ${car.gearbox.gear}`
|
|
15
15
|
// })
|
|
16
16
|
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
// `node.forward` reports: if the model faces the other way, rotate the MODEL (a child), never the
|
|
22
|
-
// chassis node.
|
|
17
|
+
// This aspect is the car as a WHOLE: mass, engine, steering, aero. Anything per-wheel lives on the
|
|
18
|
+
// `Wheel` aspect of the wheel's own node (which is where Jolt keeps it too), and the gearbox is the
|
|
19
|
+
// `Gearbox` aspect — policy, not physics. Vehicle collects every Wheel under itself, groups them into
|
|
20
|
+
// axles by Z, and installs a default Gearbox if the node has none.
|
|
23
21
|
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
22
|
+
// Chassis space is forward −Z / up +Y — the SDK's own convention, the one `node.forward` reports: if
|
|
23
|
+
// the model faces the other way, rotate the MODEL (a child), never the chassis node. Physics owns the
|
|
24
|
+
// chassis transform: drive it with `drive()`, never per-frame `node.position` writes.
|
|
25
|
+
//
|
|
26
|
+
// Layering, and why it is where it is: docs/vehicle-rework-plan.md.
|
|
27
27
|
|
|
28
28
|
import { Aspect } from "../core/Aspect"
|
|
29
29
|
import type { FieldMeta } from "../core/fields"
|
|
@@ -32,204 +32,161 @@ import { Vec3, cx, cy, cz, type Vec3Like } from "../math/vec"
|
|
|
32
32
|
import { cw, type QuatLike } from "../math/quat"
|
|
33
33
|
import { Gizmos } from "../scene/gizmos"
|
|
34
34
|
import { glState } from "./state"
|
|
35
|
+
import { Gearbox } from "./Gearbox"
|
|
35
36
|
import { Node } from "./Node"
|
|
36
|
-
import { Physics } from "./Physics"
|
|
37
|
+
import { DEFAULT_FRICTION, Physics } from "./Physics"
|
|
37
38
|
import { Shape } from "./Shape"
|
|
39
|
+
import { STATE_HEADER, WHEEL_STATE, Wheel } from "./Wheel"
|
|
38
40
|
|
|
39
|
-
/** Which axle the engine drives.
|
|
41
|
+
/** Which axle the engine drives. A wheel's own `drive` overrides it. */
|
|
40
42
|
export type DriveLayout = "rear" | "front" | "all"
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
* scrapes, too large and it floats. */
|
|
51
|
-
radius?: number
|
|
52
|
-
/** Wheel width in metres (default 0.25). */
|
|
53
|
-
width?: number
|
|
54
|
-
/** Does this wheel steer? Default: the wheels on the front axle. */
|
|
55
|
-
steer?: boolean
|
|
56
|
-
/** Does the engine drive this wheel? Default: from the vehicle's `powered` layout. */
|
|
57
|
-
powered?: boolean
|
|
58
|
-
/** Foot brake on this wheel (default true). */
|
|
59
|
-
brake?: boolean
|
|
60
|
-
/** Handbrake on this wheel. Default: every non-steering wheel (locking the front wheels of a
|
|
61
|
-
* moving car is not a handbrake turn, it's a crash). */
|
|
62
|
-
handbrake?: boolean
|
|
63
|
-
/** Axle group, front to back. Derived from Z when omitted; set it for a car whose wheels are not
|
|
64
|
-
* in neat left/right pairs. Pairs an axle's wheels for the differential and the anti-roll bar. */
|
|
65
|
-
axle?: number
|
|
66
|
-
}
|
|
44
|
+
/**
|
|
45
|
+
* How an axle's two wheels share torque.
|
|
46
|
+
* - `lsd` — limited slip (Jolt's 1.4 ratio), the sane default.
|
|
47
|
+
* - `open` — fully open: a lifted or spinning wheel takes ALL the torque.
|
|
48
|
+
* - `locked` — both wheels forced to the same speed. Breaks traction predictably: the drift diff.
|
|
49
|
+
* A number is the raw max/min wheel-speed ratio (> 1; smaller is more locked).
|
|
50
|
+
*/
|
|
51
|
+
export type DifferentialMode = "lsd" | "open" | "locked"
|
|
67
52
|
|
|
68
|
-
export interface
|
|
69
|
-
/**
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
|
|
53
|
+
export interface EngineConfig {
|
|
54
|
+
/** Peak torque in N·m (default 500). The single biggest "how fast is it" knob. */
|
|
55
|
+
torque?: number
|
|
56
|
+
/** Redline in RPM (default 6000). */
|
|
57
|
+
maxRpm?: number
|
|
58
|
+
/** Idle RPM (default 1000) — the engine never falls below it. */
|
|
59
|
+
idleRpm?: number
|
|
60
|
+
/**
|
|
61
|
+
* The engine's CHARACTER: a flat list `[rpm, torque, rpm, torque, …]` in NORMALISED units — x is
|
|
62
|
+
* a fraction of the redline, y a fraction of `torque`. Left out, Jolt's own broad curve is used.
|
|
63
|
+
*
|
|
64
|
+
* `[0, 0.8, 0.66, 1, 1, 0.8]` is an ordinary petrol engine (peak two thirds up, falling to the
|
|
65
|
+
* redline); `[0, 1, 1, 1]` is flat, which is what an electric motor feels like.
|
|
66
|
+
*/
|
|
67
|
+
curve?: number[]
|
|
68
|
+
/** Moment of inertia in kg·m² (default 0.5) — how eagerly it revs when the clutch is out. */
|
|
69
|
+
inertia?: number
|
|
70
|
+
/** Engine braking: angular damping of the engine itself (default 0.2). */
|
|
71
|
+
braking?: number
|
|
75
72
|
}
|
|
76
73
|
|
|
77
74
|
export interface SteeringConfig {
|
|
78
|
-
/**
|
|
79
|
-
|
|
75
|
+
/** Steering lock in degrees at a standstill (default 35). */
|
|
76
|
+
lock?: number
|
|
77
|
+
/**
|
|
78
|
+
* The lock still available, in degrees, once the car reaches `speed` (default 10) — what stops a
|
|
79
|
+
* car over-driving its own front tires.
|
|
80
|
+
*
|
|
81
|
+
* A tire makes its peak grip at a few degrees of slip angle, and the steering angle a corner needs
|
|
82
|
+
* falls with the SQUARE of speed. Holding full lock at 100 km/h therefore asks the front for
|
|
83
|
+
* several times what it can give: the tires saturate, the car ploughs on, and the wheel stops
|
|
84
|
+
* answering. In between the lock eases on `(1 − v/speed)²`, which tracks the real curve closely.
|
|
85
|
+
* `0` turns the taper off — full lock at any speed.
|
|
86
|
+
*/
|
|
80
87
|
atSpeed?: number
|
|
81
88
|
/** The speed in m/s at which the lock has fallen to `atSpeed` (default 30 ≈ 108 km/h). */
|
|
82
89
|
speed?: number
|
|
83
|
-
/** How fast the front wheels may turn, in degrees per second (default 220).
|
|
90
|
+
/** How fast the front wheels may turn, in degrees per second (default 220). A keyboard asks for
|
|
91
|
+
* full lock in one frame, which no steering wheel can do, and that step alone snaps a tire past
|
|
92
|
+
* its peak. `0` = instantly. */
|
|
84
93
|
rate?: number
|
|
85
94
|
}
|
|
86
95
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
|
|
105
|
-
slipAngle: number
|
|
106
|
-
/** Longitudinal friction coefficient actually applied this step (curve value × ground friction). */
|
|
107
|
-
gripLong: number
|
|
108
|
-
/** Lateral friction coefficient actually applied this step — the cornering budget. */
|
|
109
|
-
gripLateral: number
|
|
110
|
-
/** Current suspension length in metres (short = compressed). */
|
|
111
|
-
suspension: number
|
|
112
|
-
/** Steer angle in degrees, positive to the right. */
|
|
113
|
-
steer: number
|
|
114
|
-
/** Wheel spin in rad/s (× radius ≈ the speed the tire is laying down). */
|
|
115
|
-
spin: number
|
|
96
|
+
/**
|
|
97
|
+
* Aerodynamics. Both numbers are quoted the same way — **a fraction of the car's own weight at
|
|
98
|
+
* 30 m/s (108 km/h)** — and both grow with the SQUARE of speed from there, so `0.5` at 30 m/s is
|
|
99
|
+
* `2.0` at 60 m/s. One mental model for both, and no drag coefficients or frontal areas to invent.
|
|
100
|
+
*/
|
|
101
|
+
export interface AeroConfig {
|
|
102
|
+
/**
|
|
103
|
+
* Downforce (default 0 — a car with no wing). The knob that decides whether a car corners better
|
|
104
|
+
* or worse the faster it goes: tire grip is a constant, but the force a corner demands grows with
|
|
105
|
+
* v², so with no downforce every car runs out of road at speed. `0.6` roughly doubles a car's
|
|
106
|
+
* cornering at 150 km/h; a road car is nearer `0.1`, a race car well past `1`.
|
|
107
|
+
*
|
|
108
|
+
* It presses along the CHASSIS' own down axis (that is what keeps a car planted over a crest) and
|
|
109
|
+
* only while a wheel is on the ground — a car on its roof is never launched by its own wing.
|
|
110
|
+
*/
|
|
111
|
+
downforce?: number
|
|
112
|
+
/** Air resistance (default 0.03 — an ordinary car). What gives a car a top speed at all. */
|
|
113
|
+
drag?: number
|
|
116
114
|
}
|
|
117
115
|
|
|
118
|
-
/**
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
* A number is the raw max/min wheel-speed ratio (> 1; smaller is more locked). */
|
|
123
|
-
export type DifferentialMode = "lsd" | "open" | "locked"
|
|
124
|
-
|
|
125
|
-
const DEFAULT_GEARS = [ 2.66, 1.78, 1.3, 1.0, 0.74 ]
|
|
126
|
-
/** Floats vehicleGetState writes per wheel — see the WheelState fields. */
|
|
127
|
-
const WHEEL_STATE = 8
|
|
128
|
-
/** Limited-slip ratios: `<= 0` tells the host "fully open". */
|
|
116
|
+
/** Blob v5 — see bridges.d.ts for the field-by-field layout. */
|
|
117
|
+
const BLOB_VERSION = 5
|
|
118
|
+
const BLOB_HEADER = 21
|
|
119
|
+
const BLOB_WHEEL = 15
|
|
129
120
|
const DIFFERENTIALS: Record<DifferentialMode, number> = { lsd: 1.4, open: 0, locked: 1 }
|
|
130
121
|
const GRAVITY = 9.81 // only used to place the wheels at their resting height
|
|
131
122
|
const AXLE_EPS = 0.35 // wheels within this many metres in Z are the same axle
|
|
132
123
|
const clamp = (v: number, lo: number, hi: number): number => (v < lo ? lo : v > hi ? hi : v)
|
|
133
124
|
|
|
134
|
-
/** A wheel's STRUCTURE, resolved once at attach: where it is, what it does. The tunable numbers
|
|
135
|
-
* (lock in degrees, brake torques) are applied in _buildSettings, so re-tuning picks them up. */
|
|
136
|
-
type ResolvedWheel = {
|
|
137
|
-
entityId: number
|
|
138
|
-
cx: number, cy: number, cz: number // wheel centre at rest, chassis space
|
|
139
|
-
radius: number
|
|
140
|
-
width: number
|
|
141
|
-
steers: boolean
|
|
142
|
-
powered: boolean
|
|
143
|
-
braked: boolean
|
|
144
|
-
handbraked: boolean
|
|
145
|
-
axle: number
|
|
146
|
-
}
|
|
147
|
-
|
|
148
125
|
export class Vehicle extends Aspect<"vehicle", Node> {
|
|
149
126
|
static readonly aspect = "vehicle"
|
|
150
127
|
|
|
151
128
|
/** Chassis mass in kg (default 1200). Wheels are massless in this model. */
|
|
152
129
|
mass = 1200
|
|
153
|
-
/**
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
130
|
+
/** Centre of mass height in chassis space (metres, negative = lower). Left out, it drops to halfway
|
|
131
|
+
* down the chassis shape — a car with its mass at the shape's centre tips over. */
|
|
132
|
+
centerOfMass?: number
|
|
133
|
+
/** Which axle the engine drives (default "rear"). A wheel's own `drive` overrides it. */
|
|
134
|
+
driveAxle: DriveLayout = "rear"
|
|
135
|
+
/** Differential lock across each driven axle. `locked` is the drift setup — both wheels turn
|
|
136
|
+
* together, so the pair lets go as a pair. Live: re-configure the aspect to switch it mid-drive. */
|
|
137
|
+
differential: DifferentialMode | number = "lsd"
|
|
160
138
|
/**
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
* actually needs falls with the SQUARE of speed. So holding full lock at 100 km/h asks the front
|
|
165
|
-
* for several times what it can give: the tires saturate, the car ploughs straight on, and the
|
|
166
|
-
* wheel stops answering. `atSpeed` is the lock left once the car reaches `speed`; in between it
|
|
167
|
-
* eases on `(1 − v/speed)²`, which tracks the real curve closely.
|
|
139
|
+
* Anti-roll bars between each axle's wheels — what stops a car rolling onto its inside wheels in a
|
|
140
|
+
* fast corner. `true` (default) is a bar 0.01 as stiff as the wheel's own spring; a number is that
|
|
141
|
+
* ratio directly, and `0` / `false` removes the bars.
|
|
168
142
|
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
143
|
+
* **The numbers are far smaller than they look and the gain is steep** — Jolt's bar feeds
|
|
144
|
+
* `lengthDifference × stiffness × dt` as a bias into the suspension's own soft constraint, so 4%
|
|
145
|
+
* of the spring already flattens the body. Measured on a 1250 kg car: `0` → 1.3° of roll at 0.86 g,
|
|
146
|
+
* `0.01` → 0.6°, `0.15` → 0.2° (flat), `0.5` → the car HOPS.
|
|
171
147
|
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
148
|
+
* This is the knob for how much a car visibly LEANS, not for wheel lift: bars change how far the
|
|
149
|
+
* body tips, not how much load transfers. For lift, lower `centerOfMass`.
|
|
174
150
|
*/
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Tire grip multiplier (default 1). Below 1 slides, above 1 sticks.
|
|
178
|
-
*
|
|
179
|
-
* This scales the tire's own friction curve. The grip a wheel ACTUALLY gets is combined with the
|
|
180
|
-
* ground it stands on — `sqrt(tire × groundFriction)` — and a body's friction defaults to 0.6, so
|
|
181
|
-
* a road you want a car to really corner on wants `Physics { friction: 1 }` (read
|
|
182
|
-
* `wheel(i).gripLateral` to see what the tires actually got).
|
|
183
|
-
*/
|
|
184
|
-
grip = 1
|
|
185
|
-
/** Friction of the CHASSIS body itself — how it slides when it lands on its roof or side.
|
|
186
|
-
* Nothing to do with the tires; those are `grip` × the ground's friction. */
|
|
187
|
-
chassisFriction = 0.6
|
|
188
|
-
/** Foot-brake torque per wheel in N·m (default 1500); the handbrake pulls 3× on its wheels. */
|
|
189
|
-
brakeTorque = 1500
|
|
190
|
-
/** Anti-roll bars between each axle's wheels (default on) — what keeps a car from tripping in a
|
|
191
|
-
* fast corner. */
|
|
192
|
-
antiRoll = true
|
|
151
|
+
antiRoll: boolean | number = true
|
|
193
152
|
/** Pitch/roll limit in degrees (default 60) before the constraint rights the car. 180 turns it off. */
|
|
194
153
|
maxTilt = 60
|
|
195
|
-
/**
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
centerOfMass?: number
|
|
208
|
-
|
|
209
|
-
/** Scene-editor inspector: the flat, tunable half. */
|
|
154
|
+
/** Friction of the CHASSIS body itself — how it slides when it lands on its roof or side. Nothing
|
|
155
|
+
* to do with the tires; those are the `Wheel`'s `tire.grip` × the ground's friction. */
|
|
156
|
+
friction = DEFAULT_FRICTION
|
|
157
|
+
/** Strength of the clutch when fully engaged (default 10). Small values slip; large ones weld the
|
|
158
|
+
* engine to the wheels. The `Gearbox` scales it 0..1 while shifting. */
|
|
159
|
+
clutchStrength = 10
|
|
160
|
+
|
|
161
|
+
engine: EngineConfig = { torque: 500, maxRpm: 6000, idleRpm: 1000, inertia: 0.5, braking: 0.2 }
|
|
162
|
+
steering: SteeringConfig = { lock: 35, atSpeed: 10, speed: 30, rate: 220 }
|
|
163
|
+
aero: AeroConfig = { downforce: 0, drag: 0.03 }
|
|
164
|
+
|
|
165
|
+
/** Scene-editor inspector: the flat, tunable half. The grouped objects are edited in code. */
|
|
210
166
|
static fields: FieldMeta<Vehicle> = {
|
|
211
167
|
mass: { min: 1, step: 10 },
|
|
212
|
-
|
|
213
|
-
maxSteer: { label: "Steering lock", min: 0, max: 80, step: 1 },
|
|
214
|
-
grip: { min: 0.1, max: 3, step: 0.05 },
|
|
215
|
-
chassisFriction: { label: "Chassis friction", min: 0, max: 2, step: 0.05 },
|
|
216
|
-
brakeTorque: { label: "Brake torque", min: 0, step: 50 },
|
|
217
|
-
maxTilt: { label: "Max tilt", min: 0, max: 180, step: 5 },
|
|
168
|
+
driveAxle: { label: "Drive", options: [ "rear", "front", "all" ] },
|
|
218
169
|
differential: { options: [ "lsd", "open", "locked" ] },
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
170
|
+
antiRoll: { label: "Anti-roll" },
|
|
171
|
+
maxTilt: { label: "Max tilt", min: 0, max: 180, step: 5 },
|
|
172
|
+
friction: { label: "Chassis friction", min: 0, max: 2, step: 0.05 },
|
|
173
|
+
clutchStrength: { label: "Clutch", min: 0.5, max: 100, step: 0.5 },
|
|
222
174
|
engine: { hidden: true },
|
|
223
175
|
steering: { hidden: true },
|
|
176
|
+
aero: { hidden: true },
|
|
224
177
|
}
|
|
225
178
|
static editor = { rebuild: true }
|
|
226
179
|
|
|
227
180
|
private _id = 0
|
|
228
181
|
private _bodyId = 0
|
|
229
182
|
private _builtMass = 0
|
|
230
|
-
private
|
|
231
|
-
private _state = new Float32Array(
|
|
183
|
+
private _wheels: Wheel[] = []
|
|
184
|
+
private _state = new Float32Array(STATE_HEADER)
|
|
232
185
|
private _stateFrame = -1
|
|
186
|
+
private _throttle = 0
|
|
187
|
+
private _steer = 0
|
|
188
|
+
private _brake = 0
|
|
189
|
+
private _handbrake = 0
|
|
233
190
|
|
|
234
191
|
onAttach(): void {
|
|
235
192
|
if (!_creator.physicsHasSupport || !_creator.physicsHasSupport()) return
|
|
@@ -247,26 +204,42 @@ export class Vehicle extends Aspect<"vehicle", Node> {
|
|
|
247
204
|
if (this.node.has(Physics)) {
|
|
248
205
|
throw new Error("Vehicle owns its own rigid body — remove the Physics aspect from the chassis node (put `mass` on the Vehicle instead)")
|
|
249
206
|
}
|
|
250
|
-
if (this.wheels.length === 0) throw new Error("Vehicle needs at least one wheel — `wheels: [{ node: … }, …]`")
|
|
251
207
|
|
|
252
|
-
this.
|
|
208
|
+
this._wheels = this._collectWheels()
|
|
209
|
+
if (this._wheels.length === 0) {
|
|
210
|
+
throw new Error("Vehicle found no wheels — put a Wheel aspect on each wheel node under the chassis: wheelFL.aspect(Wheel)")
|
|
211
|
+
}
|
|
212
|
+
|
|
253
213
|
const shapeId = shape._claim() // the vehicle owns the shape; drop the pick-only body
|
|
254
|
-
|
|
255
|
-
this._id = _creator.vehicleCreate(this.node.id, shapeId, this._buildSettings(), ids)
|
|
214
|
+
this._id = _creator.vehicleCreate(this.node.id, shapeId, this._buildSettings())
|
|
256
215
|
if (!this._id) {
|
|
257
216
|
console.warn(`Vehicle: the engine refused the settings for "${this.node.name}" — check the wheel list`)
|
|
258
217
|
shape._recreatePickBody()
|
|
259
218
|
return
|
|
260
219
|
}
|
|
261
220
|
this._bodyId = _creator.vehicleBodyId ? _creator.vehicleBodyId(this._id) : 0
|
|
262
|
-
|
|
221
|
+
// The chassis body owns the node's transform, so route position writes to it (see Node._xf) —
|
|
222
|
+
// `car.position = p` places the car instead of being silently overwritten by the next sync. It is
|
|
223
|
+
// the WEAK form of the operation: it keeps the car's velocity and its roll, so a respawn still
|
|
224
|
+
// wants `reset(p)`, which also uprights it and clears engine, gearbox and wheel spin.
|
|
225
|
+
this.node._xf = this._bodyId
|
|
226
|
+
this.node._xfKind = this._bodyId ? 2 : 0
|
|
227
|
+
if (this._bodyId && this.friction !== DEFAULT_FRICTION) {
|
|
228
|
+
_creator.physicsSetFriction?.(this._bodyId, this.friction)
|
|
229
|
+
}
|
|
263
230
|
this._builtMass = this.mass
|
|
264
|
-
this._state = new Float32Array(
|
|
231
|
+
this._state = new Float32Array(STATE_HEADER + this._wheels.length * WHEEL_STATE)
|
|
232
|
+
// A car with no gearbox still has to drive: install the default automatic.
|
|
233
|
+
if (!this.node.has(Gearbox)) this.node.aspect(Gearbox)
|
|
265
234
|
}
|
|
266
235
|
|
|
267
236
|
onDetach(): void {
|
|
268
237
|
if (this._id) { _creator.vehicleDestroy(this._id); this._id = 0; this._bodyId = 0 }
|
|
269
|
-
this.
|
|
238
|
+
for (const w of this._wheels) w._vehicle = null
|
|
239
|
+
this._wheels = []
|
|
240
|
+
this.node._xf = 0
|
|
241
|
+
this.node._xfKind = 0
|
|
242
|
+
this.node.get(Shape)?._recreatePickBody() // hands the routing back to the pick body
|
|
270
243
|
}
|
|
271
244
|
|
|
272
245
|
/**
|
|
@@ -275,41 +248,42 @@ export class Vehicle extends Aspect<"vehicle", Node> {
|
|
|
275
248
|
*
|
|
276
249
|
* car.aspect(Vehicle, { differential: drift ? 'locked' : 'lsd' })
|
|
277
250
|
*
|
|
278
|
-
* Live: `differential`, `
|
|
279
|
-
* `
|
|
280
|
-
* `
|
|
281
|
-
* at attach; to change one of those,
|
|
251
|
+
* Live: `differential`, `engine`, `steering`, `aero`, `maxTilt`, `friction`, `clutchStrength`, and
|
|
252
|
+
* every `Wheel`'s `tire.grip` and brake torques. Everything else describes the car's STRUCTURE —
|
|
253
|
+
* `mass`, `centerOfMass`, which wheels exist and where, their suspension, `driveAxle`, `antiRoll` — and
|
|
254
|
+
* is baked into the constraint at attach; to change one of those, re-attach the aspect.
|
|
282
255
|
*/
|
|
283
256
|
onReconfigure(): void {
|
|
284
257
|
if (!this._id) return
|
|
285
|
-
if (this.wheels.length !== this._resolved.length) {
|
|
286
|
-
console.warn("Vehicle: the wheel list can't change on a live car — remove and re-add the aspect")
|
|
287
|
-
return
|
|
288
|
-
}
|
|
289
258
|
if (this.mass !== this._builtMass) {
|
|
290
259
|
console.warn(`Vehicle: mass is fixed once the body exists (still ${this._builtMass} kg) — re-add the aspect to change it`)
|
|
291
260
|
this.mass = this._builtMass
|
|
292
261
|
}
|
|
293
262
|
_creator.vehicleSetTuning?.(this._id, this._buildSettings())
|
|
294
|
-
if (this._bodyId) _creator.physicsSetFriction?.(this._bodyId, this.
|
|
263
|
+
if (this._bodyId) _creator.physicsSetFriction?.(this._bodyId, this.friction)
|
|
295
264
|
}
|
|
296
265
|
|
|
297
266
|
/** Native vehicle id (0 until attached / no physics support). */
|
|
298
267
|
get id(): number { return this._id }
|
|
299
268
|
/** The chassis rigid-body id — the same handle the plain body calls take. */
|
|
300
269
|
get bodyId(): number { return this._bodyId }
|
|
270
|
+
/** The wheels, in blob order (front axle first). */
|
|
271
|
+
get wheels(): readonly Wheel[] { return this._wheels }
|
|
301
272
|
|
|
302
273
|
// --- driving --------------------------------------------------------------------------------
|
|
303
274
|
|
|
304
275
|
/**
|
|
305
276
|
* The driver's controls, all in [-1, 1] (brakes in [0, 1]). Sticky: the last values keep applying
|
|
306
|
-
* until the next call, so a held key does not need re-sending. `forward < 0`
|
|
307
|
-
*
|
|
277
|
+
* until the next call, so a held key does not need re-sending. `forward < 0` asks for reverse; the
|
|
278
|
+
* `Gearbox` engages it once the car has stopped.
|
|
308
279
|
*/
|
|
309
280
|
drive(forward: number, steer = 0, brake = 0, handbrake = 0): this {
|
|
310
281
|
if (this._id) {
|
|
311
|
-
|
|
312
|
-
|
|
282
|
+
this._throttle = clamp(forward, -1, 1)
|
|
283
|
+
this._steer = clamp(steer, -1, 1)
|
|
284
|
+
this._brake = clamp(brake, 0, 1)
|
|
285
|
+
this._handbrake = clamp(handbrake, 0, 1)
|
|
286
|
+
_creator.vehicleSetInput(this._id, this._throttle, this._steer, this._brake, this._handbrake)
|
|
313
287
|
}
|
|
314
288
|
return this
|
|
315
289
|
}
|
|
@@ -324,6 +298,8 @@ export class Vehicle extends Aspect<"vehicle", Node> {
|
|
|
324
298
|
const q: QuatLike = rotation ?? [ 0, wq.y, 0, wq.w ]
|
|
325
299
|
_creator.vehicleReset(this._id, cx(p), cy(p), cz(p), cx(q), cy(q), cz(q), cw(q))
|
|
326
300
|
this._stateFrame = -1
|
|
301
|
+
const box = this.node.get(Gearbox)
|
|
302
|
+
if (box) box.gear = 0
|
|
327
303
|
return this
|
|
328
304
|
}
|
|
329
305
|
|
|
@@ -333,7 +309,12 @@ export class Vehicle extends Aspect<"vehicle", Node> {
|
|
|
333
309
|
return this
|
|
334
310
|
}
|
|
335
311
|
|
|
336
|
-
|
|
312
|
+
/** @internal The Gearbox's latched pair: the one ratio the engine runs, and the clutch envelope. */
|
|
313
|
+
_setTransmission(ratio: number, clutch: number): void {
|
|
314
|
+
if (this._id) _creator.vehicleSetTransmission(this._id, ratio, clamp(clutch, 0, 1))
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// --- readouts (one native read per frame, shared by the car and every wheel) -------------------
|
|
337
318
|
|
|
338
319
|
private _read(): Float32Array {
|
|
339
320
|
if (this._id && this._stateFrame !== glState.frame) {
|
|
@@ -343,177 +324,150 @@ export class Vehicle extends Aspect<"vehicle", Node> {
|
|
|
343
324
|
return this._state
|
|
344
325
|
}
|
|
345
326
|
|
|
327
|
+
/** @internal One value out of wheel `index`'s slice — see WHEEL_STATE in Wheel.ts. */
|
|
328
|
+
_wheelState(index: number, k: number): number {
|
|
329
|
+
const s = this._read()
|
|
330
|
+
const at = STATE_HEADER + index * WHEEL_STATE + k
|
|
331
|
+
return at < s.length ? s[at]! : 0
|
|
332
|
+
}
|
|
333
|
+
|
|
346
334
|
/** Speed along the car's forward axis in m/s — negative when reversing (× 3.6 for km/h). */
|
|
347
335
|
get speed(): number { return this._read()[0]! }
|
|
348
336
|
/** Engine revolutions per minute. */
|
|
349
337
|
get rpm(): number { return this._read()[1]! }
|
|
350
|
-
/** Current gear: -1 reverse, 0 neutral, 1… forward. */
|
|
351
|
-
get gear(): number { return this._read()[2]! }
|
|
352
338
|
/** How many wheels are touching the ground. */
|
|
353
|
-
get wheelsOnGround(): number { return this._read()[
|
|
339
|
+
get wheelsOnGround(): number { return this._read()[2]! }
|
|
354
340
|
/** Any wheel on the ground — false while airborne. */
|
|
355
|
-
get grounded(): boolean { return this._read()[
|
|
341
|
+
get grounded(): boolean { return this._read()[2]! > 0 }
|
|
356
342
|
/** Chassis velocity in world units/s (fresh Vec3). */
|
|
357
|
-
get velocity(): Vec3 { const s = this._read(); return new Vec3(s[
|
|
358
|
-
/** The
|
|
343
|
+
get velocity(): Vec3 { const s = this._read(); return new Vec3(s[3]!, s[4]!, s[5]!) }
|
|
344
|
+
/** The last throttle handed to `drive()` — what the gearbox reads to know which way you want to go. */
|
|
345
|
+
get throttle(): number { return this._throttle }
|
|
346
|
+
/** The last steer input handed to `drive()`, [-1, 1]. */
|
|
347
|
+
get steerInput(): number { return this._steer }
|
|
348
|
+
/** The last foot-brake input handed to `drive()`, [0, 1] — the pedal, not the resulting force. */
|
|
349
|
+
get brake(): number { return this._brake }
|
|
350
|
+
/** The last handbrake input handed to `drive()`, [0, 1]. */
|
|
351
|
+
get handbrake(): number { return this._handbrake }
|
|
352
|
+
/** Current gear from the attached `Gearbox`: −1 reverse, 0 neutral, 1… forward. */
|
|
353
|
+
get gear(): number { return this.node.get(Gearbox)?.gear ?? 0 }
|
|
354
|
+
/** The steering lock, in degrees, the car is allowed at its current speed — the full lock at rest,
|
|
359
355
|
* falling to `steering.atSpeed`. The same curve the engine applies; useful on a HUD. */
|
|
360
356
|
get steerLock(): number {
|
|
357
|
+
const lock = this.steering.lock ?? 35
|
|
361
358
|
const atSpeed = Math.max(0, this.steering.atSpeed ?? 10)
|
|
362
|
-
if (atSpeed <= 0 || atSpeed >=
|
|
359
|
+
if (atSpeed <= 0 || atSpeed >= lock) return lock
|
|
363
360
|
const full = this.steering.speed ?? 30
|
|
364
361
|
const t = full > 0 ? Math.min(Math.abs(this.speed) / full, 1) : 1
|
|
365
|
-
return atSpeed + (
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/** One wheel's live state, by its index in `wheels`. */
|
|
369
|
-
wheel(index: number): WheelState {
|
|
370
|
-
const s = this._read()
|
|
371
|
-
const base = 7 + index * WHEEL_STATE
|
|
372
|
-
if (index < 0 || base + WHEEL_STATE > s.length) {
|
|
373
|
-
return { contact: false, slip: 0, slipAngle: 0, gripLong: 0, gripLateral: 0, suspension: 0, steer: 0, spin: 0 }
|
|
374
|
-
}
|
|
375
|
-
return {
|
|
376
|
-
contact: s[base] !== 0,
|
|
377
|
-
slip: s[base + 1]!,
|
|
378
|
-
slipAngle: s[base + 2]!,
|
|
379
|
-
gripLong: s[base + 3]!,
|
|
380
|
-
gripLateral: s[base + 4]!,
|
|
381
|
-
suspension: s[base + 5]!,
|
|
382
|
-
steer: s[base + 6]!,
|
|
383
|
-
spin: s[base + 7]!,
|
|
384
|
-
}
|
|
362
|
+
return atSpeed + (lock - atSpeed) * (1 - t) * (1 - t)
|
|
385
363
|
}
|
|
386
364
|
|
|
387
365
|
// --- setup ----------------------------------------------------------------------------------
|
|
388
366
|
|
|
389
|
-
/**
|
|
390
|
-
private
|
|
367
|
+
/** Every Wheel under the chassis, ordered front axle first, with its axle group resolved. */
|
|
368
|
+
private _collectWheels(): Wheel[] {
|
|
369
|
+
const found: Wheel[] = []
|
|
370
|
+
this.node.traverse((n) => { const w = n.get(Wheel); if (w) found.push(w) })
|
|
371
|
+
if (found.length === 0) return found
|
|
372
|
+
|
|
391
373
|
const chassisInv = this.node.worldMatrix.invert()
|
|
392
|
-
const centres =
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
return chassisInv.mul(node.worldMatrix).position
|
|
399
|
-
})
|
|
400
|
-
|
|
401
|
-
// Axles: sort by Z (front first) and start a new group whenever the gap is more than AXLE_EPS.
|
|
402
|
-
const axleOf = new Array<number>(this.wheels.length).fill(0)
|
|
403
|
-
// Forward is −Z, so the front axle is the most NEGATIVE z.
|
|
404
|
-
const byZ = centres.map((_, i) => i).sort((a, b) => centres[a]!.z - centres[b]!.z)
|
|
374
|
+
const centres = found.map((w) => chassisInv.mul(w.node.worldMatrix).position)
|
|
375
|
+
|
|
376
|
+
// Axles: sort by Z (forward is −Z, so the front axle is the most NEGATIVE) and start a new group
|
|
377
|
+
// whenever the gap is more than AXLE_EPS.
|
|
378
|
+
const order = centres.map((_, i) => i).sort((a, b) => centres[a]!.z - centres[b]!.z)
|
|
379
|
+
const axleOf = new Array<number>(found.length).fill(0)
|
|
405
380
|
let axle = 0
|
|
406
|
-
let ref = centres[
|
|
407
|
-
for (const i of
|
|
408
|
-
const w = this.wheels[i]!
|
|
381
|
+
let ref = centres[order[0]!]!.z
|
|
382
|
+
for (const i of order) {
|
|
409
383
|
if (Math.abs(centres[i]!.z - ref) > AXLE_EPS) { axle++; ref = centres[i]!.z }
|
|
410
|
-
axleOf[i] =
|
|
384
|
+
axleOf[i] = found[i]!.axle ?? axle
|
|
411
385
|
}
|
|
412
386
|
const maxAxle = Math.max(...axleOf)
|
|
413
387
|
|
|
414
|
-
|
|
388
|
+
// Bind each wheel in front-to-back order, resolving what it did not say.
|
|
389
|
+
const wheels: Wheel[] = []
|
|
390
|
+
for (const i of order) {
|
|
391
|
+
const w = found[i]!
|
|
415
392
|
const a = axleOf[i]!
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
private _wheelNode(w: WheelConfig): Node | null {
|
|
436
|
-
if (!w.node) return null
|
|
437
|
-
if (typeof w.node !== "string") return w.node
|
|
438
|
-
const found = this.node.bone(w.node)
|
|
439
|
-
if (!found) console.warn(`Vehicle: no node named "${w.node}" under "${this.node.name}" — that wheel will be invisible`)
|
|
440
|
-
return found
|
|
393
|
+
const measured = w._measure()
|
|
394
|
+
w.radius ??= measured.radius
|
|
395
|
+
w.width ??= measured.width
|
|
396
|
+
w.steer ??= a === 0
|
|
397
|
+
w.drive ??= this.driveAxle === "all" || (this.driveAxle === "front" ? a === 0 : a === maxAxle)
|
|
398
|
+
w.handbrake ??= !w.steer
|
|
399
|
+
w.axle = a
|
|
400
|
+
const rest = centres[i]!
|
|
401
|
+
// Where the suspension hangs from, so the wheel CENTRE ends up where the author put it: the
|
|
402
|
+
// spring sags by g/(2πf)² under its own load (mass-independent), and that sag is what separates
|
|
403
|
+
// the resting length from full droop.
|
|
404
|
+
const travel = w.suspension.travel ?? 0.4
|
|
405
|
+
const stiffness = w.suspension.stiffness ?? 1.4
|
|
406
|
+
const sag = Math.min(GRAVITY / ((2 * Math.PI * stiffness) ** 2), travel * 0.6)
|
|
407
|
+
const attach = new Vec3(rest.x, rest.y + (travel - sag), rest.z)
|
|
408
|
+
w._bind(this, wheels.length, rest, attach)
|
|
409
|
+
wheels.push(w)
|
|
410
|
+
}
|
|
411
|
+
return wheels
|
|
441
412
|
}
|
|
442
413
|
|
|
443
414
|
/** Pack the whole configuration into the one blob the host ABI takes (see bridges.d.ts). */
|
|
444
415
|
private _buildSettings(): Float32Array {
|
|
445
|
-
const
|
|
446
|
-
const
|
|
447
|
-
const damping = this.suspension.damping ?? 0.5
|
|
448
|
-
const maxRpm = this.engine.maxRpm ?? 6000
|
|
449
|
-
const gears = this.gears.length ? this.gears : DEFAULT_GEARS
|
|
450
|
-
// Where the suspension hangs from, so the wheel CENTRE ends up where the author put it: the
|
|
451
|
-
// spring sags by g/(2πf)² under its own load (mass-independent), and that sag is what separates
|
|
452
|
-
// the resting length from full droop.
|
|
453
|
-
const sag = Math.min(GRAVITY / ((2 * Math.PI * stiffness) ** 2), travel * 0.6)
|
|
454
|
-
const attachOffset = travel - sag
|
|
455
|
-
|
|
416
|
+
const curve = this.engine.curve ?? []
|
|
417
|
+
const curveCount = Math.floor(curve.length / 2)
|
|
456
418
|
const diff = typeof this.differential === "number"
|
|
457
419
|
? this.differential
|
|
458
420
|
: DIFFERENTIALS[this.differential] ?? DIFFERENTIALS.lsd
|
|
459
421
|
|
|
460
|
-
const out = new Float32Array(
|
|
461
|
-
out[0] =
|
|
422
|
+
const out = new Float32Array(BLOB_HEADER + curveCount * 2 + this._wheels.length * BLOB_WHEEL)
|
|
423
|
+
out[0] = BLOB_VERSION
|
|
462
424
|
out[1] = this.mass
|
|
463
425
|
out[2] = this.centerOfMass === undefined ? 1 : 0
|
|
464
426
|
out[3] = this.centerOfMass ?? 0
|
|
465
|
-
out[4] =
|
|
466
|
-
out[5] =
|
|
467
|
-
out[6] =
|
|
468
|
-
out[7] = this.engine.
|
|
469
|
-
out[8] =
|
|
470
|
-
out[9] =
|
|
471
|
-
out[10] =
|
|
472
|
-
out[11] =
|
|
473
|
-
out[12] = this.
|
|
474
|
-
out[13] = this.
|
|
475
|
-
out[14] = this.
|
|
476
|
-
out[15] =
|
|
477
|
-
out[16] = this.
|
|
478
|
-
out[17] =
|
|
479
|
-
out[18] = Math.max(0, this.
|
|
480
|
-
out[19] = this.
|
|
481
|
-
out[20] =
|
|
482
|
-
let at =
|
|
483
|
-
for (
|
|
484
|
-
for (const w of this.
|
|
485
|
-
out[at++] = w.
|
|
486
|
-
out[at++] = w.
|
|
487
|
-
out[at++] = w.
|
|
488
|
-
out[at++] = w.radius
|
|
489
|
-
out[at++] = w.width
|
|
490
|
-
out[at++] = w.
|
|
491
|
-
out[at++] = w.
|
|
492
|
-
out[at++] = w.
|
|
493
|
-
out[at++] = w.
|
|
494
|
-
out[at++] = w.axle
|
|
427
|
+
out[4] = this.engine.torque ?? 500
|
|
428
|
+
out[5] = this.engine.maxRpm ?? 6000
|
|
429
|
+
out[6] = this.engine.idleRpm ?? 1000
|
|
430
|
+
out[7] = this.engine.inertia ?? 0.5
|
|
431
|
+
out[8] = this.engine.braking ?? 0.2
|
|
432
|
+
out[9] = this.clutchStrength
|
|
433
|
+
out[10] = diff
|
|
434
|
+
out[11] = this.antiRoll === true ? 0.01 : this.antiRoll === false ? 0 : Math.max(0, this.antiRoll)
|
|
435
|
+
out[12] = this.maxTilt
|
|
436
|
+
out[13] = this.steering.lock ?? 35
|
|
437
|
+
out[14] = Math.max(0, this.steering.atSpeed ?? 10)
|
|
438
|
+
out[15] = this.steering.speed ?? 30
|
|
439
|
+
out[16] = Math.max(0, this.steering.rate ?? 220)
|
|
440
|
+
out[17] = Math.max(0, this.aero.downforce ?? 0)
|
|
441
|
+
out[18] = Math.max(0, this.aero.drag ?? 0.03)
|
|
442
|
+
out[19] = this._wheels.length
|
|
443
|
+
out[20] = curveCount
|
|
444
|
+
let at = BLOB_HEADER
|
|
445
|
+
for (let i = 0; i < curveCount * 2; i++) out[at++] = curve[i]!
|
|
446
|
+
for (const w of this._wheels) {
|
|
447
|
+
out[at++] = w._attach.x
|
|
448
|
+
out[at++] = w._attach.y
|
|
449
|
+
out[at++] = w._attach.z
|
|
450
|
+
out[at++] = w.radius ?? 0.35
|
|
451
|
+
out[at++] = w.width ?? 0.25
|
|
452
|
+
out[at++] = w.steer ? (this.steering.lock ?? 35) : 0
|
|
453
|
+
out[at++] = w.drive ? 1 : 0
|
|
454
|
+
out[at++] = w.brake ? w.brakeTorque : 0
|
|
455
|
+
out[at++] = w.handbrake ? w.brakeTorque * 3 : 0
|
|
456
|
+
out[at++] = w.axle ?? 0
|
|
457
|
+
out[at++] = w.suspension.travel ?? 0.4
|
|
458
|
+
out[at++] = w.suspension.stiffness ?? 1.4
|
|
459
|
+
out[at++] = w.suspension.damping ?? 0.5
|
|
460
|
+
out[at++] = w.tire.grip ?? 1
|
|
461
|
+
out[at++] = w.tire.curve === "arcade" ? 1 : 0
|
|
495
462
|
}
|
|
496
463
|
return out
|
|
497
464
|
}
|
|
498
465
|
|
|
499
|
-
/** Edit mode:
|
|
500
|
-
*
|
|
466
|
+
/** Edit mode: which way the chassis faces. Each wheel draws its own circle and travel range — see
|
|
467
|
+
* `Wheel.rebuild()`. Play mode never calls this. */
|
|
501
468
|
rebuild(): void {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
const world = new Mat4(this.node.worldMatrix)
|
|
505
|
-
const travel = this.suspension.travel ?? 0.3
|
|
506
|
-
const P = (x: number, y: number, z: number): Vec3 => world.transformPoint([ x, y, z ])
|
|
507
|
-
for (const w of resolved) {
|
|
508
|
-
const ring: Vec3[] = []
|
|
509
|
-
for (let i = 0; i < 24; i++) {
|
|
510
|
-
const t = (i / 24) * Math.PI * 2
|
|
511
|
-
ring.push(P(w.cx, w.cy + Math.sin(t) * w.radius, w.cz + Math.cos(t) * w.radius))
|
|
512
|
-
}
|
|
513
|
-
Gizmos.polyline(ring, { color: w.steers ? "#5b8ef0" : "#9aa3b2", closed: true })
|
|
514
|
-
Gizmos.line(P(w.cx, w.cy + travel * 0.6, w.cz), P(w.cx, w.cy - travel * 0.4, w.cz), { color: "#59606d" })
|
|
515
|
-
if (w.powered) Gizmos.cross(P(w.cx, w.cy, w.cz), w.radius * 0.35, { color: "#e0b25a" })
|
|
516
|
-
}
|
|
469
|
+
const w = new Mat4(this.node.worldMatrix)
|
|
470
|
+
const P = (x: number, y: number, z: number): Vec3 => w.transformPoint([ x, y, z ])
|
|
517
471
|
Gizmos.polyline([ P(0, 0, 0), P(0, 0, -1.2), P(-0.15, 0, -1.0), P(0, 0, -1.2), P(0.15, 0, -1.0) ], { color: "#ffffff" })
|
|
518
472
|
}
|
|
519
473
|
}
|