lecodes-sdk 0.20.2 → 1.0.0
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/dist/global.d.ts +17 -5
- package/dist/types/canvas/Canvas.d.ts +2 -0
- package/dist/types/gl/DecalSet.d.ts +48 -3
- package/dist/types/gl/Geometry.d.ts +12 -0
- package/dist/types/gl/Lightmap.d.ts +9 -0
- package/dist/types/gl/Material.d.ts +4 -0
- package/dist/types/gl/Mesh.d.ts +7 -1
- package/dist/types/gl/Model.d.ts +34 -0
- package/dist/types/gl/Particles.d.ts +13 -0
- package/dist/types/gl/Scene.d.ts +1 -1
- package/dist/types/gl/animation/DynamicBone.d.ts +173 -0
- package/dist/types/gl/{IK.d.ts → animation/IK.d.ts} +4 -4
- package/dist/types/gl/{Locomotion.d.ts → animation/Locomotion.d.ts} +6 -6
- package/dist/types/gl/{AudioSource.d.ts → audio/AudioSource.d.ts} +6 -6
- package/dist/types/gl/{AudioZone.d.ts → audio/AudioZone.d.ts} +4 -4
- package/dist/types/gl/{SceneAudio.d.ts → audio/SceneAudio.d.ts} +1 -1
- package/dist/types/gl/{NavAgent.d.ts → nav/NavAgent.d.ts} +4 -4
- package/dist/types/gl/{NavMesh.d.ts → nav/NavMesh.d.ts} +4 -4
- package/dist/types/gl/{CharacterController.d.ts → physics/CharacterController.d.ts} +5 -5
- package/dist/types/gl/{Physics.d.ts → physics/Physics.d.ts} +6 -5
- package/dist/types/gl/{Ragdoll.d.ts → physics/Ragdoll.d.ts} +4 -4
- package/dist/types/gl/{Shape.d.ts → physics/Shape.d.ts} +3 -3
- package/dist/types/gl/{Trigger.d.ts → physics/Trigger.d.ts} +2 -2
- package/dist/types/gl/{Terrain.d.ts → terrain/Terrain.d.ts} +6 -6
- package/dist/types/gl/{terrainMesh.d.ts → terrain/terrainMesh.d.ts} +1 -1
- package/dist/types/gl/vehicle/Vehicle.d.ts +300 -0
- package/dist/types/gl/vehicle/Wheel.d.ts +147 -0
- package/dist/types/inject.d.ts +28 -26
- package/dist/types/runtime/files.d.ts +24 -1
- package/dist/types/scene/defineScene.d.ts +10 -3
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/prompts/README.md +142 -142
- package/src/bridges.d.ts +1599 -1481
- package/src/canvas/Canvas.ts +21 -0
- package/src/compile/__tests__/compile.test.ts +11 -0
- package/src/compile/header.ts +6 -3
- package/src/compile/index.ts +1 -0
- package/src/compile/sceneEditor.ts +42 -1
- package/src/gl/DecalSet.ts +132 -5
- package/src/gl/Geometry.ts +348 -284
- package/src/gl/Lightmap.ts +32 -5
- package/src/gl/Material.ts +21 -0
- package/src/gl/Mesh.ts +120 -102
- package/src/gl/Model.ts +33 -1
- package/src/gl/Node.ts +1 -1
- package/src/gl/Particles.ts +21 -3
- package/src/gl/Scene.ts +1 -1
- package/src/gl/animation/DynamicBone.ts +459 -0
- package/src/gl/{IK.ts → animation/IK.ts} +4 -4
- package/src/gl/{Locomotion.ts → animation/Locomotion.ts} +7 -7
- package/src/gl/{AudioSource.ts → audio/AudioSource.ts} +7 -7
- package/src/gl/{AudioZone.ts → audio/AudioZone.ts} +75 -75
- package/src/gl/{SceneAudio.ts → audio/SceneAudio.ts} +2 -2
- package/src/gl/{NavAgent.ts → nav/NavAgent.ts} +5 -5
- package/src/gl/{NavMesh.ts → nav/NavMesh.ts} +8 -8
- package/src/gl/{CharacterController.ts → physics/CharacterController.ts} +5 -5
- package/src/gl/{Physics.ts → physics/Physics.ts} +12 -5
- package/src/gl/{Ragdoll.ts → physics/Ragdoll.ts} +8 -6
- package/src/gl/{Shape.ts → physics/Shape.ts} +3 -3
- package/src/gl/{Trigger.ts → physics/Trigger.ts} +45 -45
- package/src/gl/{physicsEvents.ts → physics/physicsEvents.ts} +1 -1
- package/src/gl/{Terrain.ts → terrain/Terrain.ts} +10 -10
- package/src/gl/{terrainMesh.ts → terrain/terrainMesh.ts} +1 -1
- package/src/gl/vehicle/Vehicle.ts +666 -0
- package/src/gl/vehicle/Wheel.ts +290 -0
- package/src/inject.ts +226 -224
- package/src/runtime/files.ts +32 -2
- package/src/scene/defineScene.ts +26 -10
- package/src/scene/level.ts +2 -2
- package/dist/types/gl/Gearbox.d.ts +0 -86
- package/dist/types/gl/Vehicle.d.ts +0 -191
- package/dist/types/gl/Wheel.d.ts +0 -95
- package/src/gl/Gearbox.ts +0 -212
- package/src/gl/Vehicle.ts +0 -473
- package/src/gl/Wheel.ts +0 -240
- /package/dist/types/gl/{physicsEvents.d.ts → physics/physicsEvents.d.ts} +0 -0
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
// A car, as an aspect on a 3D Node, backed by Jolt's wheeled-vehicle constraint (creator-physics).
|
|
2
|
+
// The node is the CHASSIS: it needs a Shape (its collision body) and must NOT also carry a Physics
|
|
3
|
+
// aspect — Vehicle owns its rigid body, the way CharacterController owns its controller.
|
|
4
|
+
//
|
|
5
|
+
// const car = new Node('car')
|
|
6
|
+
// .add(body, hubFL.aspect(Wheel), hubFR.aspect(Wheel), hubRL.aspect(Wheel), hubRR.aspect(Wheel))
|
|
7
|
+
// .aspect(Shape, { box: [0.9, 0.4, 2.1] })
|
|
8
|
+
// .aspect(Vehicle, { mass: 1200 })
|
|
9
|
+
//
|
|
10
|
+
// class Controls extends System<'controls'> {
|
|
11
|
+
// updateBefore() { // EARLY: this frame's step drives what the keys say
|
|
12
|
+
// const v = car.vehicle
|
|
13
|
+
// const fwd = (Input.key('KeyW') ? 1 : 0) - (Input.key('KeyS') ? 1 : 0)
|
|
14
|
+
// v.throttle = Math.abs(fwd)
|
|
15
|
+
// v.ratio = fwd < 0 ? -1 : 1 // the sign of the ratio IS the direction
|
|
16
|
+
// v.steer = (Input.key('KeyD') ? 1 : 0) - (Input.key('KeyA') ? 1 : 0)
|
|
17
|
+
// for (const w of v.wheels) w.brake = Input.key('Space') && !w.steer ? 4500 : 0
|
|
18
|
+
// }
|
|
19
|
+
// }
|
|
20
|
+
//
|
|
21
|
+
// This aspect is the car as HARDWARE: mass, engine, steering, aero, and the INPUTS a driver writes —
|
|
22
|
+
// `throttle`, `steer`, `steerForce`, `ratio`, `clutch` here, `brake` on each `Wheel`. Everything a
|
|
23
|
+
// DRIVER decides is the game's: which gear, when the clutch bites, whether the engine can stall,
|
|
24
|
+
// when the hands let go of the wheel in a slide, a gyro, ABS, a keyboard's pedal travel. All of
|
|
25
|
+
// those are a few lines over the sensors this class exposes (`rpm`, `speed`, `driftAngle`,
|
|
26
|
+
// `yawRate`, `steerAngle`, `steerTorque`, and per wheel `spin`, `slip`, `load`, the forces…), and
|
|
27
|
+
// the car-demo project carries a full driver as a reference. Anything per-wheel lives on the
|
|
28
|
+
// `Wheel` aspect of the wheel's own node (which is where Jolt keeps it too); Vehicle collects every
|
|
29
|
+
// Wheel under itself and groups them into axles by Z.
|
|
30
|
+
//
|
|
31
|
+
// The engine gets NUMBERS: the blob of settings (`_buildSettings`, live re-tunable) and one input
|
|
32
|
+
// vector per frame. It never shapes them — no taper, no rate limit, no gearbox, no idle governor.
|
|
33
|
+
//
|
|
34
|
+
// Chassis space is forward −Z / up +Y — the SDK's own convention, the one `node.forward` reports: if
|
|
35
|
+
// the model faces the other way, rotate the MODEL (a child), never the chassis node. Physics owns the
|
|
36
|
+
// chassis transform: drive it through the inputs, never per-frame `node.position` writes.
|
|
37
|
+
|
|
38
|
+
import { Aspect, _phaseStamp } from "../../core/Aspect"
|
|
39
|
+
import type { FieldMeta } from "../../core/fields"
|
|
40
|
+
import { Mat4 } from "../../math/mat4"
|
|
41
|
+
import { Vec3, cx, cy, cz, type Vec3Like } from "../../math/vec"
|
|
42
|
+
import { cw, type QuatLike } from "../../math/quat"
|
|
43
|
+
import { Gizmos } from "../../scene/gizmos"
|
|
44
|
+
import { Node } from "../Node"
|
|
45
|
+
import { DEFAULT_FRICTION, Physics } from "../physics/Physics"
|
|
46
|
+
import { Shape } from "../physics/Shape"
|
|
47
|
+
import { STATE_HEADER, SUSPENSION_MIN_FRACTION, WHEEL_STATE, Wheel, resolveTire, type Point } from "./Wheel"
|
|
48
|
+
|
|
49
|
+
/** Which axle the engine drives — the default for every wheel's own `driven` share. */
|
|
50
|
+
export type DriveLayout = "rear" | "front" | "all" | "none"
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* How an axle's two wheels share torque.
|
|
54
|
+
* - `lsd` — limited slip (Jolt's 1.4 ratio), the sane default.
|
|
55
|
+
* - `open` — fully open: a lifted or spinning wheel takes ALL the torque.
|
|
56
|
+
* - `locked` — both wheels forced to the same speed. Breaks traction predictably: the drift diff.
|
|
57
|
+
* A number is the raw max/min wheel-speed ratio (> 1; smaller is more locked).
|
|
58
|
+
*/
|
|
59
|
+
export type DifferentialMode = "lsd" | "open" | "locked"
|
|
60
|
+
|
|
61
|
+
export interface EngineConfig {
|
|
62
|
+
/** Peak torque in N·m (default 500). The single biggest "how fast is it" knob. */
|
|
63
|
+
torque?: number
|
|
64
|
+
/** Redline in RPM (default 6000) — Jolt's own clamp; a rev limiter's stutter is the game's. */
|
|
65
|
+
maxRpm?: number
|
|
66
|
+
/** Idle RPM (default 1000): the engine's floor. `0` = no floor — the engine can be dragged to a
|
|
67
|
+
* standstill and stall, and holding idle (a governor), cranking it, are the game's to write. */
|
|
68
|
+
idleRpm?: number
|
|
69
|
+
/**
|
|
70
|
+
* The engine's CHARACTER: points `{ x, y }` in NORMALISED units — x a fraction of the redline, y a
|
|
71
|
+
* fraction of `torque`, x ascending. Left out, Jolt's own broad curve is used.
|
|
72
|
+
*
|
|
73
|
+
* `[{ x: 0, y: 0.8 }, { x: 0.66, y: 1 }, { x: 1, y: 0.8 }]` is an ordinary petrol engine (peak two
|
|
74
|
+
* thirds up, falling to the redline); `[{ x: 0, y: 1 }, { x: 1, y: 1 }]` is flat, an electric motor.
|
|
75
|
+
*/
|
|
76
|
+
curve?: Point[]
|
|
77
|
+
/** Moment of inertia in kg·m² (default 0.5) — how eagerly it revs when the clutch is out. */
|
|
78
|
+
inertia?: number
|
|
79
|
+
/** Engine braking: angular damping of the engine itself (default 0.2). */
|
|
80
|
+
braking?: number
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The steering wheel as a BODY the tires push back on, simulated by the engine every sub-step. With
|
|
85
|
+
* it, `steer` is where the driver's hands AIM the wheel and `steerForce` how firmly they hold it;
|
|
86
|
+
* where the road wheels end up is physics:
|
|
87
|
+
*
|
|
88
|
+
* I·θ̈ = T_hand + T_align·(1 − powerSteering) + T_stop − (damping + endStop.damping·depth)·θ̇ − parkingFriction(v) − friction
|
|
89
|
+
*
|
|
90
|
+
* `T_align` is the tires' self-aligning torque (`steering.caster` / `trail` / `trailAtPeak` — the
|
|
91
|
+
* same torque `steerTorque` reports without a feedback body, for a force-feedback wheel). `T_hand`
|
|
92
|
+
* is the driver's arms, `steering.driver`. So: heavy at speed, light when the front lets go,
|
|
93
|
+
* self-centring on release, and in a slide a let-go wheel swings into opposite lock by itself.
|
|
94
|
+
* Without `feedback` there is no body: `steer` is the road wheels' angle as a fraction of `maxAngle`.
|
|
95
|
+
*/
|
|
96
|
+
export interface SteeringFeedback {
|
|
97
|
+
/** Moment of inertia of wheel + column + rack about the axis, kg·m² (default 1). Heavier = slower
|
|
98
|
+
* off the mark under the hands, but a wheel whipping into counter-steer carries further. */
|
|
99
|
+
inertia?: number
|
|
100
|
+
/** Viscous damping, N·m·s/rad (default 25): what keeps a let-go wheel from ringing about centre. */
|
|
101
|
+
damping?: number
|
|
102
|
+
/** The column's own dry friction, N·m, at any speed (default 8) — rack and bearings: the floor a
|
|
103
|
+
* free wheel's spin decays against, and what keeps a let-go wheel from creeping under a small torque. */
|
|
104
|
+
friction?: number
|
|
105
|
+
/** The contact patch's dry friction while (nearly) parked, N·m (default 40), fading out by 1.5 m/s
|
|
106
|
+
* — why a parked car is hard to steer, and why a wheel left turned stays turned. */
|
|
107
|
+
parkingFriction?: number
|
|
108
|
+
/** Power steering, 0..1 (default 0.5): the share of the tires' aligning torque the wheel never
|
|
109
|
+
* sees. It is NOT a gain on the angle: 0 is a kart (heavy, full of feel, a held key gives up its
|
|
110
|
+
* lock early at speed), 1 is a dead rim that neither centres nor counter-steers. */
|
|
111
|
+
powerSteering?: number
|
|
112
|
+
/** The END STOP before the lock — rubber, not a wall (default `{ band: 16, torque: 120, damping: 150 }`):
|
|
113
|
+
* over the last `band` degrees the rack pushes back toward centre with the SQUARE of the depth,
|
|
114
|
+
* `torque` N·m at the lock, and damps by `damping` N·m·s/rad × depth. A wheel let go on the stop
|
|
115
|
+
* eases back off it a few degrees; `band: 0` = the hard clamp only. */
|
|
116
|
+
endStop?: { band?: number, torque?: number, damping?: number }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The driver's ARMS on a `feedback` wheel — an actuator with numbers, because a stiff hand spring
|
|
120
|
+
* has to be integrated with the column every sub-step. WHAT the hands do (aim, hold, let go) is
|
|
121
|
+
* the game's, per frame, through `steer` and `steerForce`. */
|
|
122
|
+
export interface SteeringDriver {
|
|
123
|
+
/** The most torque the hands can put on the wheel, N·m (default 130). The ceiling on how much lock
|
|
124
|
+
* a held key keeps at speed against the tires' pull. */
|
|
125
|
+
maxTorque?: number
|
|
126
|
+
/** How stiff the arms are, N·m/rad of the gap between where they aim and where the wheel is (default 500). */
|
|
127
|
+
stiffness?: number
|
|
128
|
+
/** The fastest the hands turn the road wheels, deg/s (default 120): the hands cannot PUSH a wheel
|
|
129
|
+
* that already outruns them, they can still hold and brake it; a let-go wheel is uncapped. */
|
|
130
|
+
maxRate?: number
|
|
131
|
+
/** Seconds the hands' torque takes to build to `maxTorque` (default 0.1): muscles do not step. */
|
|
132
|
+
rampTime?: number
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The column's numbers as the engine gets them. Defaults = the car-demo's verified setup. */
|
|
136
|
+
const FEEDBACK_DEFAULTS = { inertia: 1.0, damping: 25, friction: 8, parkingFriction: 40, powerSteering: 0.5 }
|
|
137
|
+
const END_STOP_DEFAULTS = { band: 16, torque: 120, damping: 150 }
|
|
138
|
+
const DRIVER_DEFAULTS: Required<SteeringDriver> = { maxTorque: 130, stiffness: 500, maxRate: 120, rampTime: 0.1 }
|
|
139
|
+
|
|
140
|
+
export interface SteeringConfig {
|
|
141
|
+
/** Steering lock in degrees (default 35): what `steer = ±1` means. */
|
|
142
|
+
maxAngle?: number
|
|
143
|
+
/** The front axle's GEOMETRY, the source of the tires' self-aligning torque (`steerTorque`, and
|
|
144
|
+
* what a `feedback` wheel is pushed by): the mechanical caster's trail, metres (default 0.03)… */
|
|
145
|
+
caster?: number
|
|
146
|
+
/** …and the pneumatic trail, metres (default 0.025) — the patch's own lever, which collapses as the
|
|
147
|
+
* tire reaches its peak slip angle (the steering going light when the front lets go)… */
|
|
148
|
+
trail?: number
|
|
149
|
+
/** …to this fraction of itself at the peak (default 0.5). */
|
|
150
|
+
trailAtPeak?: number
|
|
151
|
+
/** Simulate the steering wheel as a body the tires push back on (`SteeringFeedback`). `true` = the
|
|
152
|
+
* defaults. Without it `steer` sets the road wheels' angle directly. */
|
|
153
|
+
feedback?: boolean | SteeringFeedback
|
|
154
|
+
/** The driver's arms on a `feedback` wheel (`SteeringDriver`); the defaults without it. */
|
|
155
|
+
driver?: SteeringDriver
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Aerodynamics. Both numbers are quoted the same way — **a fraction of the car's own weight at
|
|
160
|
+
* 30 m/s (108 km/h)** — and both grow with the SQUARE of speed from there, so `0.5` at 30 m/s is
|
|
161
|
+
* `2.0` at 60 m/s. One mental model for both, and no drag coefficients or frontal areas to invent.
|
|
162
|
+
*/
|
|
163
|
+
export interface AeroConfig {
|
|
164
|
+
/**
|
|
165
|
+
* Downforce (default 0 — a car with no wing). The knob that decides whether a car corners better
|
|
166
|
+
* or worse the faster it goes: tire grip is a constant, but the force a corner demands grows with
|
|
167
|
+
* v², so with no downforce every car runs out of road at speed. `0.6` roughly doubles a car's
|
|
168
|
+
* cornering at 150 km/h; a road car is nearer `0.1`, a race car well past `1`.
|
|
169
|
+
*
|
|
170
|
+
* It presses along the CHASSIS' own down axis (that is what keeps a car planted over a crest) and
|
|
171
|
+
* only while a wheel is on the ground — a car on its roof is never launched by its own wing.
|
|
172
|
+
*/
|
|
173
|
+
downforce?: number
|
|
174
|
+
/** Air resistance (default 0.03 — an ordinary car). What gives a car a top speed at all. */
|
|
175
|
+
drag?: number
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Blob v15 — see bridges.d.ts for the field-by-field layout. */
|
|
179
|
+
const BLOB_VERSION = 15
|
|
180
|
+
const BLOB_HEADER = 34
|
|
181
|
+
const BLOB_WHEEL_FIXED = 15
|
|
182
|
+
/** The input vector's fixed head: throttle, steer, steerForce, ratio, clutch — then one brake per wheel. */
|
|
183
|
+
const INPUT_FIXED = 5
|
|
184
|
+
const DIFFERENTIALS: Record<DifferentialMode, number> = { lsd: 1.4, open: 0, locked: 1 }
|
|
185
|
+
const GRAVITY = 9.81 // only used to place the wheels at their resting height
|
|
186
|
+
const AXLE_EPS = 0.35 // wheels within this many metres in Z are the same axle
|
|
187
|
+
const RAD2DEG = 180 / Math.PI
|
|
188
|
+
/** `driftAngle` fades in over this band of ground speed, m/s: under it a tire's slip angle is the
|
|
189
|
+
* noise of a patch that is not going anywhere. */
|
|
190
|
+
const DRIFT_SPEED_FROM = 1
|
|
191
|
+
const DRIFT_SPEED_FULL = 2
|
|
192
|
+
const clamp = (v: number, lo: number, hi: number): number => (v < lo ? lo : v > hi ? hi : v)
|
|
193
|
+
|
|
194
|
+
export class Vehicle extends Aspect<"vehicle", Node> {
|
|
195
|
+
static readonly aspect = "vehicle"
|
|
196
|
+
|
|
197
|
+
/** Chassis mass in kg (default 1200). Wheels are massless in this model. */
|
|
198
|
+
mass = 1200
|
|
199
|
+
/** Centre of mass height in chassis space (metres, negative = lower). Left out, it drops to halfway
|
|
200
|
+
* down the chassis shape — a car with its mass at the shape's centre tips over. */
|
|
201
|
+
centerOfMass?: number
|
|
202
|
+
/** Which axle the engine drives (default "rear") — the default for every wheel's own `driven`
|
|
203
|
+
* share: 1 on that axle, 0 elsewhere. `none` = no drive at all (a trailer, a pushed cart). */
|
|
204
|
+
driveAxle: DriveLayout = "rear"
|
|
205
|
+
/** Differential lock across each driven axle. `locked` is the drift setup — both wheels turn
|
|
206
|
+
* together, so the pair lets go as a pair. Live: re-configure the aspect to switch it mid-drive. */
|
|
207
|
+
differential: DifferentialMode | number = "lsd"
|
|
208
|
+
/**
|
|
209
|
+
* Anti-roll bars between each axle's wheels — what stops a car rolling onto its inside wheels in a
|
|
210
|
+
* fast corner. `true` (default) is a bar 0.01 as stiff as the wheel's own spring; a number is that
|
|
211
|
+
* ratio directly, and `0` / `false` removes the bars.
|
|
212
|
+
*
|
|
213
|
+
* **The numbers are far smaller than they look and the gain is steep** — Jolt's bar feeds
|
|
214
|
+
* `lengthDifference × stiffness × dt` as a bias into the suspension's own soft constraint, so 4%
|
|
215
|
+
* of the spring already flattens the body. Measured on a 1250 kg car: `0` → 1.3° of roll at 0.86 g,
|
|
216
|
+
* `0.01` → 0.6°, `0.15` → 0.2° (flat), `0.5` → the car HOPS.
|
|
217
|
+
*
|
|
218
|
+
* This is the knob for how much a car visibly LEANS, not for wheel lift: bars change how far the
|
|
219
|
+
* body tips, not how much load transfers. For lift, lower `centerOfMass`.
|
|
220
|
+
*/
|
|
221
|
+
antiRoll: boolean | number = true
|
|
222
|
+
/** Pitch/roll limit in degrees (default 60) before the constraint rights the car. 180 turns it off. */
|
|
223
|
+
maxTilt = 60
|
|
224
|
+
/** Friction of the CHASSIS body itself — how it slides when it lands on its roof or side. Nothing
|
|
225
|
+
* to do with the tires; those are the `Wheel`'s `tire` × the ground's friction. */
|
|
226
|
+
friction = DEFAULT_FRICTION
|
|
227
|
+
/** Strength of the clutch when fully engaged (default 10). Small values slip; large ones weld the
|
|
228
|
+
* engine to the wheels. `clutch` (the input) scales it 0..1. */
|
|
229
|
+
clutchStrength = 10
|
|
230
|
+
/** The clutch's torque CAPACITY, N·m (default 0 = none: Jolt's viscous clutch alone, torque =
|
|
231
|
+
* `clutchStrength` × slip speed, no ceiling — rigid at any real slip). Set it and the clutch
|
|
232
|
+
* SLIPS at this much torque × `clutch` — a dry clutch, emulated a physics step at a time: a
|
|
233
|
+
* launch can be feathered, and a dumped clutch in too high a gear drags the engine down instead
|
|
234
|
+
* of lurching. A road car's is ~1.5× the engine's peak torque. */
|
|
235
|
+
clutchCapacity = 0
|
|
236
|
+
|
|
237
|
+
engine: EngineConfig = { torque: 500, maxRpm: 6000, idleRpm: 1000, inertia: 0.5, braking: 0.2 }
|
|
238
|
+
steering: SteeringConfig = { maxAngle: 35 }
|
|
239
|
+
aero: AeroConfig = { downforce: 0, drag: 0.03 }
|
|
240
|
+
|
|
241
|
+
/** Scene-editor inspector: the flat, tunable half. The grouped objects are edited in code. */
|
|
242
|
+
static fields: FieldMeta<Vehicle> = {
|
|
243
|
+
mass: { min: 1, step: 10 },
|
|
244
|
+
driveAxle: { label: "Drive", options: [ "rear", "front", "all", "none" ] },
|
|
245
|
+
differential: { options: [ "lsd", "open", "locked" ] },
|
|
246
|
+
antiRoll: { label: "Anti-roll" },
|
|
247
|
+
maxTilt: { label: "Max tilt", min: 0, max: 180, step: 5 },
|
|
248
|
+
friction: { label: "Chassis friction", min: 0, max: 2, step: 0.05 },
|
|
249
|
+
clutchStrength: { label: "Clutch", min: 0.5, max: 100, step: 0.5 },
|
|
250
|
+
clutchCapacity: { label: "Clutch capacity", min: 0, step: 50 },
|
|
251
|
+
engine: { hidden: true },
|
|
252
|
+
steering: { hidden: true },
|
|
253
|
+
aero: { hidden: true },
|
|
254
|
+
}
|
|
255
|
+
static editor = { rebuild: true }
|
|
256
|
+
|
|
257
|
+
/** Driver systems tick at the default 0, so their inputs land in the SAME early pass, before this flush. */
|
|
258
|
+
protected override updateOrder = 100
|
|
259
|
+
|
|
260
|
+
private _id = 0
|
|
261
|
+
private _bodyId = 0
|
|
262
|
+
private _builtMass = 0
|
|
263
|
+
private _wheels: Wheel[] = []
|
|
264
|
+
private _state = new Float32Array(STATE_HEADER)
|
|
265
|
+
private _stateFrame = -1
|
|
266
|
+
private _throttle = 0
|
|
267
|
+
private _steer = 0
|
|
268
|
+
private _steerForce = 1
|
|
269
|
+
private _ratio = 1
|
|
270
|
+
private _clutch = 1
|
|
271
|
+
private _input = new Float32Array(INPUT_FIXED)
|
|
272
|
+
private _sent = new Float32Array(INPUT_FIXED)
|
|
273
|
+
private _dirty = true // an input write since the last send
|
|
274
|
+
private _retunePending = false
|
|
275
|
+
|
|
276
|
+
onAttach(): void {
|
|
277
|
+
if (!_creator.physicsHasSupport || !_creator.physicsHasSupport()) return
|
|
278
|
+
if (!_creator.vehicleCreate) {
|
|
279
|
+
console.warn("Vehicle needs a newer host (no vehicleCreate) — the car will not move")
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
const shape = this.node.get(Shape)
|
|
283
|
+
if (!shape) {
|
|
284
|
+
throw new Error("Vehicle requires a Shape aspect (the chassis collider) — add it first: node.aspect(Shape, { box: […] }).aspect(Vehicle, {…})")
|
|
285
|
+
}
|
|
286
|
+
if (shape._isTriangleMesh) {
|
|
287
|
+
throw new Error("Vehicle: the chassis cannot use Shape { mesh: true } (a triangle mesh can't be a dynamic body) — use { mesh: 'convex' } or a box")
|
|
288
|
+
}
|
|
289
|
+
if (this.node.has(Physics)) {
|
|
290
|
+
throw new Error("Vehicle owns its own rigid body — remove the Physics aspect from the chassis node (put `mass` on the Vehicle instead)")
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
this._wheels = this._collectWheels()
|
|
294
|
+
if (this._wheels.length === 0) {
|
|
295
|
+
throw new Error("Vehicle found no wheels — put a Wheel aspect on each wheel node under the chassis: hubFL.aspect(Wheel)")
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const shapeId = shape._claim() // the vehicle owns the shape; drop the pick-only body
|
|
299
|
+
this._id = _creator.vehicleCreate(this.node.id, shapeId, this._buildSettings())
|
|
300
|
+
if (!this._id) {
|
|
301
|
+
console.warn(`Vehicle: the engine refused the settings for "${this.node.name}" — check the wheel list`)
|
|
302
|
+
shape._recreatePickBody()
|
|
303
|
+
return
|
|
304
|
+
}
|
|
305
|
+
this._bodyId = _creator.vehicleBodyId ? _creator.vehicleBodyId(this._id) : 0
|
|
306
|
+
// The chassis body owns the node's transform, so route position writes to it (see Node._xf) —
|
|
307
|
+
// `car.position = p` places the car instead of being silently overwritten by the next sync. It is
|
|
308
|
+
// the WEAK form of the operation: it keeps the car's velocity and its roll, so a respawn still
|
|
309
|
+
// wants `reset(p)`, which also uprights it and clears engine and wheel spin.
|
|
310
|
+
this.node._xf = this._bodyId
|
|
311
|
+
this.node._xfKind = this._bodyId ? 2 : 0
|
|
312
|
+
if (this._bodyId && this.friction !== DEFAULT_FRICTION) {
|
|
313
|
+
_creator.physicsSetFriction?.(this._bodyId, this.friction)
|
|
314
|
+
}
|
|
315
|
+
this._builtMass = this.mass
|
|
316
|
+
this._state = new Float32Array(STATE_HEADER + this._wheels.length * WHEEL_STATE)
|
|
317
|
+
this._input = new Float32Array(INPUT_FIXED + this._wheels.length)
|
|
318
|
+
this._sent = new Float32Array(INPUT_FIXED + this._wheels.length)
|
|
319
|
+
this._dirty = true
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
onDetach(): void {
|
|
323
|
+
if (this._id) { _creator.vehicleDestroy(this._id); this._id = 0; this._bodyId = 0 }
|
|
324
|
+
for (const w of this._wheels) w._vehicle = null
|
|
325
|
+
this._wheels = []
|
|
326
|
+
this.node._xf = 0
|
|
327
|
+
this.node._xfKind = 0
|
|
328
|
+
this.node.get(Shape)?._recreatePickBody() // hands the routing back to the pick body
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Re-configuring an attached vehicle retunes it in place — the way a drift button switches the
|
|
333
|
+
* differential without the car so much as blinking:
|
|
334
|
+
*
|
|
335
|
+
* car.aspect(Vehicle, { differential: drift ? 'locked' : 'lsd' })
|
|
336
|
+
*
|
|
337
|
+
* Live: `differential`, `engine`, `steering`, `aero`, `maxTilt`, `friction`, `clutchStrength`,
|
|
338
|
+
* `clutchCapacity`, `antiRoll` (its stiffness, on axles that have a bar), and every `Wheel`'s
|
|
339
|
+
* `tire`. Everything else describes the car's STRUCTURE — `mass`, `centerOfMass`, which wheels
|
|
340
|
+
* exist and where, their suspension, the driven shares — and is baked into the constraint at
|
|
341
|
+
* attach; to change one of those, re-attach the aspect.
|
|
342
|
+
*/
|
|
343
|
+
onReconfigure(): void {
|
|
344
|
+
if (!this._id) return
|
|
345
|
+
if (this.mass !== this._builtMass) {
|
|
346
|
+
console.warn(`Vehicle: mass is fixed once the body exists (still ${this._builtMass} kg) — re-add the aspect to change it`)
|
|
347
|
+
this.mass = this._builtMass
|
|
348
|
+
}
|
|
349
|
+
this._retunePending = true
|
|
350
|
+
if (this._bodyId) _creator.physicsSetFriction?.(this._bodyId, this.friction)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** @internal A wheel's tire changed: re-send the tuning before the next step (one send however
|
|
354
|
+
* many wheels changed this frame). */
|
|
355
|
+
_retuneSoon(): void { this._retunePending = true }
|
|
356
|
+
|
|
357
|
+
/** Native vehicle id (0 until attached / no physics support). */
|
|
358
|
+
get id(): number { return this._id }
|
|
359
|
+
/** The chassis rigid body id (for `physicsApplyImpulse`-style calls). */
|
|
360
|
+
get bodyId(): number { return this._bodyId }
|
|
361
|
+
/** The wheels, front axle first, in the order the engine knows them. */
|
|
362
|
+
get wheels(): readonly Wheel[] { return this._wheels }
|
|
363
|
+
|
|
364
|
+
// --- inputs -----------------------------------------------------------------------------------
|
|
365
|
+
// Plain fields a driver writes; the early pass hands them to the engine as one vector before the
|
|
366
|
+
// step (`updateBefore`). Sticky: a value keeps applying until it is written again. Write them from
|
|
367
|
+
// a system's `updateBefore` and the very same physics step drives what the keys say.
|
|
368
|
+
|
|
369
|
+
/** Throttle 0..1 — the engine's pedal. No sign: the direction is `ratio`'s. */
|
|
370
|
+
get throttle(): number { return this._throttle }
|
|
371
|
+
set throttle(v: number) { this._throttle = clamp(v, 0, 1); this._dirty = true }
|
|
372
|
+
/** Steering −1..1. Without `steering.feedback`: the road wheels' angle as a fraction of `maxAngle`,
|
|
373
|
+
* applied as is (a taper with speed, a rate limit, are the game's). With it: where the hands AIM
|
|
374
|
+
* the wheel; the tires decide where the road wheels end up. */
|
|
375
|
+
get steer(): number { return this._steer }
|
|
376
|
+
set steer(v: number) { this._steer = clamp(v, -1, 1); this._dirty = true }
|
|
377
|
+
/** How firmly the hands hold a `feedback` wheel, 0..1 (default 1): the hands' torque is capped at
|
|
378
|
+
* `driver.maxTorque × steerForce`. 0 = let go — the tires alone have the wheel: back to centre in
|
|
379
|
+
* a grip corner, into opposite lock in a slide. Ignored without a feedback wheel. */
|
|
380
|
+
get steerForce(): number { return this._steerForce }
|
|
381
|
+
set steerForce(v: number) { this._steerForce = clamp(v, 0, 1); this._dirty = true }
|
|
382
|
+
/** The ONE gear ratio the engine runs (default 1): engine rpm = wheel rpm × ratio × the final
|
|
383
|
+
* drive (3.42). The SIGN is the direction — negative is reverse — and 0 is neutral. */
|
|
384
|
+
get ratio(): number { return this._ratio }
|
|
385
|
+
set ratio(v: number) { this._ratio = v; this._dirty = true }
|
|
386
|
+
/** Clutch engagement 0..1 (default 1 = home). 0 = out: the engine is free of the wheels. */
|
|
387
|
+
get clutch(): number { return this._clutch }
|
|
388
|
+
set clutch(v: number) { this._clutch = clamp(v, 0, 1); this._dirty = true }
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* EARLY phase: hand this frame's inputs to the engine, before the step. The vector is sent when
|
|
392
|
+
* anything changed — and every frame while a throttle, steer or brake is non-zero, because that is
|
|
393
|
+
* what keeps a driven car awake (an idle car with zero input sleeps). The pending re-tune (a
|
|
394
|
+
* config or tire change) goes first, so the step runs on the new numbers.
|
|
395
|
+
*/
|
|
396
|
+
protected updateBefore(): void {
|
|
397
|
+
if (!this._id) return
|
|
398
|
+
if (this._retunePending) {
|
|
399
|
+
this._retunePending = false
|
|
400
|
+
_creator.vehicleSetTuning?.(this._id, this._buildSettings())
|
|
401
|
+
}
|
|
402
|
+
const input = this._input
|
|
403
|
+
input[0] = this._throttle
|
|
404
|
+
input[1] = this._steer
|
|
405
|
+
input[2] = this._steerForce
|
|
406
|
+
input[3] = this._ratio
|
|
407
|
+
input[4] = this._clutch
|
|
408
|
+
let active = this._throttle !== 0 || this._steer !== 0
|
|
409
|
+
let changed = this._dirty
|
|
410
|
+
for (let i = 0; i < this._wheels.length; i++) {
|
|
411
|
+
const b = Math.max(0, this._wheels[i]!.brake)
|
|
412
|
+
if (b !== 0) active = true
|
|
413
|
+
if (b !== this._sent[INPUT_FIXED + i]) changed = true
|
|
414
|
+
input[INPUT_FIXED + i] = b
|
|
415
|
+
}
|
|
416
|
+
if (changed || active) {
|
|
417
|
+
_creator.vehicleSetInput(this._id, input)
|
|
418
|
+
this._sent.set(input)
|
|
419
|
+
this._dirty = false
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** Teleport upright and clear all motion (velocity, engine rpm, wheel spin). Defaults to the
|
|
424
|
+
* chassis node's current pose — `reset()` alone un-flips a car where it lies. The inputs are
|
|
425
|
+
* kept and re-sent on the next early pass. */
|
|
426
|
+
reset(position?: Vec3Like, rotation?: QuatLike): this {
|
|
427
|
+
if (!this._id) return this
|
|
428
|
+
const p = position ?? this.node.worldPosition
|
|
429
|
+
// No rotation given: keep the heading, drop the roll/pitch — the point of reset is to land upright.
|
|
430
|
+
const wq = this.node.worldQuaternion
|
|
431
|
+
const q: QuatLike = rotation ?? [ 0, wq.y, 0, wq.w ]
|
|
432
|
+
_creator.vehicleReset(this._id, cx(p), cy(p), cz(p), cx(q), cy(q), cz(q), cw(q))
|
|
433
|
+
this._stateFrame = -1
|
|
434
|
+
this._dirty = true // the engine zeroed its copy of the inputs
|
|
435
|
+
return this
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** Push the chassis (a boost pad, an explosion) — an instantaneous impulse in kg·m/s. */
|
|
439
|
+
applyImpulse(v: Vec3Like): this {
|
|
440
|
+
if (this._bodyId) _creator.physicsApplyImpulse(this._bodyId, cx(v), cy(v), cz(v))
|
|
441
|
+
return this
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// --- readouts (one native read per PHASE — early sees the pre-step state, late the settled one —
|
|
445
|
+
// shared by the car and every wheel; keyed on core/Aspect's _phaseStamp) -------------------
|
|
446
|
+
|
|
447
|
+
private _read(): Float32Array {
|
|
448
|
+
if (this._id && this._stateFrame !== _phaseStamp.value) {
|
|
449
|
+
_creator.vehicleGetState(this._id, this._state)
|
|
450
|
+
this._stateFrame = _phaseStamp.value
|
|
451
|
+
}
|
|
452
|
+
return this._state
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** @internal One value out of wheel `index`'s slice — see WHEEL_STATE in Wheel.ts. */
|
|
456
|
+
_wheelState(index: number, k: number): number {
|
|
457
|
+
const s = this._read()
|
|
458
|
+
const at = STATE_HEADER + index * WHEEL_STATE + k
|
|
459
|
+
return at < s.length ? s[at]! : 0
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** Speed along the car's forward axis in m/s — negative when reversing (× 3.6 for km/h). */
|
|
463
|
+
get speed(): number { return this._read()[0]! }
|
|
464
|
+
/** Engine RPM. */
|
|
465
|
+
get rpm(): number { return this._read()[1]! }
|
|
466
|
+
/** How many wheels touch the ground this frame. */
|
|
467
|
+
get wheelsOnGround(): number { return this._read()[2]! }
|
|
468
|
+
/** At least one wheel on the ground. */
|
|
469
|
+
get grounded(): boolean { return this._read()[2]! > 0 }
|
|
470
|
+
/** Chassis velocity in world space, m/s. */
|
|
471
|
+
get velocity(): Vec3 { const s = this._read(); return new Vec3(s[3]!, s[4]!, s[5]!) }
|
|
472
|
+
/** Chassis angular velocity in world space, rad/s. */
|
|
473
|
+
get angularVelocity(): Vec3 { const s = this._read(); return new Vec3(s[6]!, s[7]!, s[8]!) }
|
|
474
|
+
/** How fast the car is turning about the vertical, in degrees per second — positive turning RIGHT
|
|
475
|
+
* (the same sense as `steer`). The signal a counter-steer assist reads. */
|
|
476
|
+
get yawRate(): number { return -this._read()[7]! * RAD2DEG }
|
|
477
|
+
/**
|
|
478
|
+
* The car's slip angle in degrees: the angle between where the nose points and where the car is
|
|
479
|
+
* actually travelling, positive when it is travelling to the RIGHT of the nose. 0 going straight,
|
|
480
|
+
* ±180 backing up, and past ~10° the rear has stepped out — a drift.
|
|
481
|
+
*/
|
|
482
|
+
get slipAngle(): number {
|
|
483
|
+
const s = this._read()
|
|
484
|
+
const vx = s[3]!, vz = s[5]!
|
|
485
|
+
if (vx * vx + vz * vz < 0.25) return 0 // under 0.5 m/s the direction is noise
|
|
486
|
+
const f = this.node.forward
|
|
487
|
+
const along = f.x * vx + f.z * vz
|
|
488
|
+
const right = -f.z * vx + f.x * vz // chassis right is (−f.z, 0, f.x) for forward f
|
|
489
|
+
return Math.atan2(right, along) * RAD2DEG
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* The angle of the SLIDE, degrees, signed like `slipAngle` (positive = travelling to the right of
|
|
493
|
+
* the nose), −180..180: the angle between the car's axis and where its REAR AXLE travels — the
|
|
494
|
+
* mean of the non-steering wheels (every wheel if all of them steer), its velocity from the
|
|
495
|
+
* body's velocity and spin. Unlike `slipAngle` (the body's, at its centre) this is 0 on a clean
|
|
496
|
+
* corner at ANY speed: at walking pace and full lock the body already travels ~20° off its nose
|
|
497
|
+
* while the rear axle rolls straight along it. It is the rear stepping out — what a gyro comes in
|
|
498
|
+
* on, and what a drift meter should show — and unlike the tires' own `Wheel.slipAngle` it does
|
|
499
|
+
* not fold at 90°: a backward entry reads 110°, not 70°. The axis it is measured from is the one
|
|
500
|
+
* the DRIVETRAIN pushes along — the nose for a positive `ratio`, the tail for a negative one — so
|
|
501
|
+
* honest reversing reads 0, and a car spun round in a forward gear reads past 90°. 0 at a
|
|
502
|
+
* standstill: with no speed a direction is noise, so it fades in over walking pace,
|
|
503
|
+
* `DRIFT_SPEED_FROM`..`DRIFT_SPEED_FULL` of the axle's ground speed.
|
|
504
|
+
*/
|
|
505
|
+
get driftAngle(): number {
|
|
506
|
+
if (this._wheels.length === 0) return 0
|
|
507
|
+
// The rear axle in chassis space: the non-steering wheels' mean position (the hubs are the
|
|
508
|
+
// chassis' children, so a wheel node's position IS its chassis-local place).
|
|
509
|
+
let rx = 0, ry = 0, rz = 0, n = 0
|
|
510
|
+
for (const w of this._wheels) if (!w.steer) { const p = w.node.position; rx += p.x; ry += p.y; rz += p.z; n++ }
|
|
511
|
+
if (n === 0) for (const w of this._wheels) { const p = w.node.position; rx += p.x; ry += p.y; rz += p.z; n++ }
|
|
512
|
+
const r = this.node.quaternion.rotateVec3([ rx / n, ry / n, rz / n ])
|
|
513
|
+
// Its velocity: v + ω × r, in the ground plane.
|
|
514
|
+
const st = this._read()
|
|
515
|
+
const vx = st[3]!, vz = st[5]!, wx = st[6]!, wy = st[7]!, wz = st[8]!
|
|
516
|
+
const ax = vx + (wy * r.z - wz * r.y)
|
|
517
|
+
const az = vz + (wx * r.y - wy * r.x)
|
|
518
|
+
const ground = Math.hypot(ax, az)
|
|
519
|
+
const weight = clamp((ground - DRIFT_SPEED_FROM) / (DRIFT_SPEED_FULL - DRIFT_SPEED_FROM), 0, 1)
|
|
520
|
+
if (weight === 0) return 0
|
|
521
|
+
const f = this.node.forward
|
|
522
|
+
const dir = this._ratio < 0 ? -1 : 1
|
|
523
|
+
const along = dir * (f.x * ax + f.z * az) / ground
|
|
524
|
+
const right = dir * (-f.z * ax + f.x * az) / ground // chassis right is (−f.z, 0, f.x) for forward f
|
|
525
|
+
return Math.atan2(right, along) * RAD2DEG * weight
|
|
526
|
+
}
|
|
527
|
+
/** Where the steered wheels actually are, in degrees, right-positive: the angle `steer` set, or
|
|
528
|
+
* with `steering.feedback` where the column has come to. */
|
|
529
|
+
get steerAngle(): number { return this._read()[9]! }
|
|
530
|
+
/** The tires' self-aligning torque on the steering, N·m, positive = it pulls the wheel to the
|
|
531
|
+
* RIGHT — what the driver feels: heavy at speed, light when the front lets go, and the
|
|
532
|
+
* counter-steer in a slide. Reported with or without a `feedback` wheel: the signal a
|
|
533
|
+
* force-feedback steering wheel is driven by. */
|
|
534
|
+
get steerTorque(): number { return this._read()[10]! }
|
|
535
|
+
|
|
536
|
+
// --- setup ----------------------------------------------------------------------------------
|
|
537
|
+
|
|
538
|
+
/** Every Wheel under the chassis, ordered front axle first, with its axle group resolved. */
|
|
539
|
+
private _collectWheels(): Wheel[] {
|
|
540
|
+
const found: Wheel[] = []
|
|
541
|
+
this.node.traverse((n) => { const w = n.get(Wheel); if (w) found.push(w) })
|
|
542
|
+
if (found.length === 0) return found
|
|
543
|
+
|
|
544
|
+
const chassisInv = this.node.worldMatrix.invert()
|
|
545
|
+
const centres = found.map((w) => chassisInv.mul(w.node.worldMatrix).position)
|
|
546
|
+
|
|
547
|
+
// Axles: sort by Z (forward is −Z, so the front axle is the most NEGATIVE) and start a new group
|
|
548
|
+
// whenever the gap is more than AXLE_EPS.
|
|
549
|
+
const order = centres.map((_, i) => i).sort((a, b) => centres[a]!.z - centres[b]!.z)
|
|
550
|
+
const axleOf = new Array<number>(found.length).fill(0)
|
|
551
|
+
let axle = 0
|
|
552
|
+
let ref = centres[order[0]!]!.z
|
|
553
|
+
for (const i of order) {
|
|
554
|
+
if (Math.abs(centres[i]!.z - ref) > AXLE_EPS) { axle++; ref = centres[i]!.z }
|
|
555
|
+
axleOf[i] = found[i]!.axle ?? axle
|
|
556
|
+
}
|
|
557
|
+
const maxAxle = Math.max(...axleOf)
|
|
558
|
+
|
|
559
|
+
// Bind each wheel in front-to-back order, resolving what it did not say.
|
|
560
|
+
const wheels: Wheel[] = []
|
|
561
|
+
for (const i of order) {
|
|
562
|
+
const w = found[i]!
|
|
563
|
+
const a = axleOf[i]!
|
|
564
|
+
const measured = w._measure()
|
|
565
|
+
w.radius ??= measured.radius
|
|
566
|
+
w.width ??= measured.width
|
|
567
|
+
w.steer ??= a === 0
|
|
568
|
+
w.driven ??= this.driveAxle === "none" ? 0 : this.driveAxle === "all" ? 1 : (this.driveAxle === "front" ? a === 0 : a === maxAxle) ? 1 : 0
|
|
569
|
+
w.axle = a
|
|
570
|
+
const rest = centres[i]!
|
|
571
|
+
// Where the suspension hangs from, so the wheel CENTRE ends up where the author put it: the
|
|
572
|
+
// spring sags by g/(2πf)² under its own load (mass-independent), and that sag is what separates
|
|
573
|
+
// the resting length from full droop.
|
|
574
|
+
const travel = w.suspension.travel ?? 0.4
|
|
575
|
+
const stiffness = w.suspension.stiffness ?? 1.4
|
|
576
|
+
const sag = Math.min(GRAVITY / ((2 * Math.PI * stiffness) ** 2), travel * (1 - SUSPENSION_MIN_FRACTION))
|
|
577
|
+
const attach = new Vec3(rest.x, rest.y + (travel - sag), rest.z)
|
|
578
|
+
w._bind(this, wheels.length, rest, attach)
|
|
579
|
+
wheels.push(w)
|
|
580
|
+
}
|
|
581
|
+
return wheels
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/** Pack the whole configuration into the one blob the host ABI takes (see bridges.d.ts). */
|
|
585
|
+
private _buildSettings(): Float32Array {
|
|
586
|
+
const curve = this.engine.curve ?? []
|
|
587
|
+
const diff = typeof this.differential === "number"
|
|
588
|
+
? this.differential
|
|
589
|
+
: DIFFERENTIALS[this.differential] ?? DIFFERENTIALS.lsd
|
|
590
|
+
const tires = this._wheels.map((w) => resolveTire(w.tire))
|
|
591
|
+
const st = this.steering
|
|
592
|
+
const lock = Math.max(0, st.maxAngle ?? 35)
|
|
593
|
+
const fb = st.feedback === true ? {} : st.feedback || null
|
|
594
|
+
const stop = fb?.endStop ?? {}
|
|
595
|
+
const drv = st.driver ?? {}
|
|
596
|
+
|
|
597
|
+
let size = BLOB_HEADER + curve.length * 2
|
|
598
|
+
for (const t of tires) size += BLOB_WHEEL_FIXED + (t.side.length + t.forward.length) * 2
|
|
599
|
+
const out = new Float32Array(size)
|
|
600
|
+
out[0] = BLOB_VERSION
|
|
601
|
+
out[1] = this.mass
|
|
602
|
+
out[2] = this.centerOfMass === undefined ? 1 : 0
|
|
603
|
+
out[3] = this.centerOfMass ?? 0
|
|
604
|
+
out[4] = this.engine.torque ?? 500
|
|
605
|
+
out[5] = this.engine.maxRpm ?? 6000
|
|
606
|
+
out[6] = Math.max(0, this.engine.idleRpm ?? 1000)
|
|
607
|
+
out[7] = this.engine.inertia ?? 0.5
|
|
608
|
+
out[8] = this.engine.braking ?? 0.2
|
|
609
|
+
out[9] = this.clutchStrength
|
|
610
|
+
out[10] = diff
|
|
611
|
+
out[11] = this.antiRoll === true ? 0.01 : this.antiRoll === false ? 0 : Math.max(0, this.antiRoll)
|
|
612
|
+
out[12] = this.maxTilt
|
|
613
|
+
out[13] = Math.max(0, this.aero.downforce ?? 0)
|
|
614
|
+
out[14] = Math.max(0, this.aero.drag ?? 0.03)
|
|
615
|
+
out[15] = fb ? 1 : 0
|
|
616
|
+
out[16] = fb?.inertia ?? FEEDBACK_DEFAULTS.inertia
|
|
617
|
+
out[17] = fb?.damping ?? FEEDBACK_DEFAULTS.damping
|
|
618
|
+
out[18] = fb?.parkingFriction ?? FEEDBACK_DEFAULTS.parkingFriction
|
|
619
|
+
out[19] = st.caster ?? 0.03
|
|
620
|
+
out[20] = st.trail ?? 0.025
|
|
621
|
+
out[21] = st.trailAtPeak ?? 0.5
|
|
622
|
+
out[22] = drv.maxTorque ?? DRIVER_DEFAULTS.maxTorque
|
|
623
|
+
out[23] = drv.stiffness ?? DRIVER_DEFAULTS.stiffness
|
|
624
|
+
out[24] = fb?.powerSteering ?? FEEDBACK_DEFAULTS.powerSteering
|
|
625
|
+
out[25] = drv.maxRate ?? DRIVER_DEFAULTS.maxRate
|
|
626
|
+
out[26] = stop.band ?? END_STOP_DEFAULTS.band
|
|
627
|
+
out[27] = stop.torque ?? END_STOP_DEFAULTS.torque
|
|
628
|
+
out[28] = stop.damping ?? END_STOP_DEFAULTS.damping
|
|
629
|
+
out[29] = fb?.friction ?? FEEDBACK_DEFAULTS.friction
|
|
630
|
+
out[30] = drv.rampTime ?? DRIVER_DEFAULTS.rampTime
|
|
631
|
+
out[31] = Math.max(0, this.clutchCapacity)
|
|
632
|
+
out[32] = this._wheels.length
|
|
633
|
+
out[33] = curve.length
|
|
634
|
+
let at = BLOB_HEADER
|
|
635
|
+
for (const p of curve) { out[at++] = p.x; out[at++] = p.y }
|
|
636
|
+
this._wheels.forEach((w, i) => {
|
|
637
|
+
const t = tires[i]!
|
|
638
|
+
out[at++] = w._attach.x
|
|
639
|
+
out[at++] = w._attach.y
|
|
640
|
+
out[at++] = w._attach.z
|
|
641
|
+
out[at++] = w.radius ?? 0.35
|
|
642
|
+
out[at++] = w.width ?? 0.25
|
|
643
|
+
out[at++] = w.steer ? lock : 0
|
|
644
|
+
out[at++] = Math.max(0, w.driven ?? 0)
|
|
645
|
+
out[at++] = w.axle ?? 0
|
|
646
|
+
out[at++] = w.suspension.travel ?? 0.4
|
|
647
|
+
out[at++] = w.suspension.stiffness ?? 1.4
|
|
648
|
+
out[at++] = w.suspension.damping ?? 0.5
|
|
649
|
+
out[at++] = t.traction
|
|
650
|
+
out[at++] = t.circle
|
|
651
|
+
out[at++] = t.side.length
|
|
652
|
+
out[at++] = t.forward.length
|
|
653
|
+
for (const p of t.side) { out[at++] = p.x; out[at++] = p.y }
|
|
654
|
+
for (const p of t.forward) { out[at++] = p.x; out[at++] = p.y }
|
|
655
|
+
})
|
|
656
|
+
return out
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/** Edit mode: which way the chassis faces. Each wheel draws its own circle and travel range — see
|
|
660
|
+
* `Wheel.rebuild()`. Play mode never calls this. */
|
|
661
|
+
rebuild(): void {
|
|
662
|
+
const w = new Mat4(this.node.worldMatrix)
|
|
663
|
+
const P = (x: number, y: number, z: number): Vec3 => w.transformPoint([ x, y, z ])
|
|
664
|
+
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" })
|
|
665
|
+
}
|
|
666
|
+
}
|