lecodes-sdk 0.20.2 → 1.1.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.
Files changed (129) hide show
  1. package/dist/global.d.ts +35 -9
  2. package/dist/types/animate/tween/Animation.d.ts +69 -0
  3. package/dist/types/animate/tween/Timeline.d.ts +55 -0
  4. package/dist/types/animate/tween/animateValue.d.ts +27 -0
  5. package/dist/types/animate/tween/easing.d.ts +29 -0
  6. package/dist/types/animate/tween/spec.d.ts +178 -0
  7. package/dist/types/canvas/Canvas.d.ts +2 -0
  8. package/dist/types/g2/Node2D.d.ts +16 -0
  9. package/dist/types/g2/Sprite.d.ts +11 -1
  10. package/dist/types/gl/Camera.d.ts +15 -1
  11. package/dist/types/gl/DecalSet.d.ts +48 -3
  12. package/dist/types/gl/Foliage.d.ts +47 -0
  13. package/dist/types/gl/Geometry.d.ts +36 -0
  14. package/dist/types/gl/Light.d.ts +25 -7
  15. package/dist/types/gl/Lightmap.d.ts +90 -51
  16. package/dist/types/gl/Material.d.ts +32 -20
  17. package/dist/types/gl/Mesh.d.ts +7 -1
  18. package/dist/types/gl/Model.d.ts +41 -5
  19. package/dist/types/gl/Node.d.ts +18 -0
  20. package/dist/types/gl/Particles.d.ts +53 -1
  21. package/dist/types/gl/Scene.d.ts +21 -1
  22. package/dist/types/gl/animation/AnimationClip.d.ts +19 -0
  23. package/dist/types/gl/animation/Animator.d.ts +27 -0
  24. package/dist/types/gl/animation/DynamicBone.d.ts +184 -0
  25. package/dist/types/gl/animation/IK.d.ts +109 -0
  26. package/dist/types/gl/{Locomotion.d.ts → animation/Locomotion.d.ts} +6 -6
  27. package/dist/types/gl/animation/Warp.d.ts +2 -1
  28. package/dist/types/gl/animation/core.d.ts +35 -4
  29. package/dist/types/gl/{AudioSource.d.ts → audio/AudioSource.d.ts} +6 -6
  30. package/dist/types/gl/{AudioZone.d.ts → audio/AudioZone.d.ts} +4 -4
  31. package/dist/types/gl/{SceneAudio.d.ts → audio/SceneAudio.d.ts} +1 -1
  32. package/dist/types/gl/{NavAgent.d.ts → nav/NavAgent.d.ts} +4 -4
  33. package/dist/types/gl/{NavMesh.d.ts → nav/NavMesh.d.ts} +4 -4
  34. package/dist/types/gl/{CharacterController.d.ts → physics/CharacterController.d.ts} +5 -5
  35. package/dist/types/gl/{Physics.d.ts → physics/Physics.d.ts} +6 -5
  36. package/dist/types/gl/physics/Ragdoll.d.ts +161 -0
  37. package/dist/types/gl/{Shape.d.ts → physics/Shape.d.ts} +3 -3
  38. package/dist/types/gl/{Trigger.d.ts → physics/Trigger.d.ts} +2 -2
  39. package/dist/types/gl/{Terrain.d.ts → terrain/Terrain.d.ts} +10 -8
  40. package/dist/types/gl/{terrainMesh.d.ts → terrain/terrainMesh.d.ts} +1 -1
  41. package/dist/types/gl/vehicle/Vehicle.d.ts +300 -0
  42. package/dist/types/gl/vehicle/Wheel.d.ts +147 -0
  43. package/dist/types/inject.d.ts +35 -27
  44. package/dist/types/runtime/files.d.ts +24 -1
  45. package/dist/types/scene/defineScene.d.ts +50 -31
  46. package/dist/types/ui/UIButton.d.ts +3 -1
  47. package/dist/types/ui/UIInput.d.ts +5 -1
  48. package/dist/types/ui/UINode.d.ts +24 -24
  49. package/dist/types.json +1 -1
  50. package/package.json +1 -1
  51. package/prompts/README.md +142 -142
  52. package/prompts/core-design.md +27 -4
  53. package/prompts/core.md +35 -6
  54. package/prompts/select.ts +19 -4
  55. package/src/animate/tween/Animation.ts +378 -0
  56. package/src/animate/tween/Timeline.ts +175 -0
  57. package/src/animate/tween/animateValue.ts +100 -0
  58. package/src/animate/tween/easing.ts +172 -0
  59. package/src/animate/tween/spec.ts +479 -0
  60. package/src/bridges.d.ts +1760 -1481
  61. package/src/canvas/Canvas.ts +21 -0
  62. package/src/compile/__tests__/assetMacro.test.ts +26 -0
  63. package/src/compile/__tests__/compile.test.ts +11 -0
  64. package/src/compile/__tests__/detectEntry.test.ts +19 -0
  65. package/src/compile/__tests__/serverSplit.test.ts +27 -0
  66. package/src/compile/bundler.ts +34 -4
  67. package/src/compile/compileProject.ts +31 -1
  68. package/src/compile/detectEntry.ts +8 -3
  69. package/src/compile/header.ts +6 -3
  70. package/src/compile/index.ts +3 -0
  71. package/src/compile/sceneEditor.ts +42 -1
  72. package/src/compile/serverSplit.ts +9 -3
  73. package/src/g2/Node2D.ts +38 -0
  74. package/src/g2/Sprite.ts +20 -1
  75. package/src/gl/Camera.ts +34 -1
  76. package/src/gl/DecalSet.ts +132 -5
  77. package/src/gl/Foliage.ts +102 -0
  78. package/src/gl/Geometry.ts +109 -0
  79. package/src/gl/Light.ts +46 -16
  80. package/src/gl/Lightmap.ts +440 -249
  81. package/src/gl/Material.ts +69 -36
  82. package/src/gl/Mesh.ts +120 -102
  83. package/src/gl/Model.ts +167 -124
  84. package/src/gl/Node.ts +40 -1
  85. package/src/gl/Particles.ts +82 -5
  86. package/src/gl/Scene.ts +35 -2
  87. package/src/gl/animation/AnimationClip.ts +52 -0
  88. package/src/gl/animation/Animator.ts +42 -2
  89. package/src/gl/animation/DynamicBone.ts +482 -0
  90. package/src/gl/animation/IK.ts +214 -0
  91. package/src/gl/{Locomotion.ts → animation/Locomotion.ts} +7 -7
  92. package/src/gl/animation/Playback.ts +5 -4
  93. package/src/gl/animation/Warp.ts +5 -2
  94. package/src/gl/animation/core.ts +65 -4
  95. package/src/gl/{AudioSource.ts → audio/AudioSource.ts} +7 -7
  96. package/src/gl/{AudioZone.ts → audio/AudioZone.ts} +75 -75
  97. package/src/gl/{SceneAudio.ts → audio/SceneAudio.ts} +2 -2
  98. package/src/gl/{NavAgent.ts → nav/NavAgent.ts} +5 -5
  99. package/src/gl/{NavMesh.ts → nav/NavMesh.ts} +8 -8
  100. package/src/gl/{CharacterController.ts → physics/CharacterController.ts} +5 -5
  101. package/src/gl/{Physics.ts → physics/Physics.ts} +12 -5
  102. package/src/gl/physics/Ragdoll.ts +451 -0
  103. package/src/gl/{Shape.ts → physics/Shape.ts} +3 -3
  104. package/src/gl/{Trigger.ts → physics/Trigger.ts} +45 -45
  105. package/src/gl/{physicsEvents.ts → physics/physicsEvents.ts} +1 -1
  106. package/src/gl/{Terrain.ts → terrain/Terrain.ts} +14 -12
  107. package/src/gl/{terrainMesh.ts → terrain/terrainMesh.ts} +1 -1
  108. package/src/gl/vehicle/Vehicle.ts +666 -0
  109. package/src/gl/vehicle/Wheel.ts +290 -0
  110. package/src/inject.ts +236 -224
  111. package/src/runtime/files.ts +32 -2
  112. package/src/scene/defineScene.ts +92 -66
  113. package/src/scene/level.ts +2 -2
  114. package/src/ui/UIButton.ts +2 -2
  115. package/src/ui/UIInput.ts +3 -3
  116. package/src/ui/UINode.ts +61 -36
  117. package/dist/types/animate/animate.d.ts +0 -20
  118. package/dist/types/gl/Gearbox.d.ts +0 -86
  119. package/dist/types/gl/IK.d.ts +0 -53
  120. package/dist/types/gl/Ragdoll.d.ts +0 -86
  121. package/dist/types/gl/Vehicle.d.ts +0 -191
  122. package/dist/types/gl/Wheel.d.ts +0 -95
  123. package/src/animate/animate.ts +0 -238
  124. package/src/gl/Gearbox.ts +0 -212
  125. package/src/gl/IK.ts +0 -193
  126. package/src/gl/Ragdoll.ts +0 -270
  127. package/src/gl/Vehicle.ts +0 -473
  128. package/src/gl/Wheel.ts +0 -240
  129. /package/dist/types/gl/{physicsEvents.d.ts → physics/physicsEvents.d.ts} +0 -0
package/src/gl/Vehicle.ts DELETED
@@ -1,473 +0,0 @@
1
- // A car, as an aspect on a 3D Node, backed by Jolt's wheeled-vehicle constraint in creator-gl. The
2
- // node is the CHASSIS: it needs a Shape (its collision body) and must NOT also carry a Physics aspect
3
- // — Vehicle owns its rigid body, the way CharacterController owns its controller.
4
- //
5
- // const car = new Node('car')
6
- // .add(body, wheelFL.aspect(Wheel), wheelFR.aspect(Wheel),
7
- // wheelRL.aspect(Wheel), wheelRR.aspect(Wheel))
8
- // .aspect(Shape, { box: [0.9, 0.4, 2.1] })
9
- // .aspect(Vehicle, { mass: 1200 })
10
- //
11
- // setLoop(() => {
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
- // })
16
- //
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.
21
- //
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
-
28
- import { Aspect, _phaseStamp } from "../core/Aspect"
29
- import type { FieldMeta } from "../core/fields"
30
- import { Mat4 } from "../math/mat4"
31
- import { Vec3, cx, cy, cz, type Vec3Like } from "../math/vec"
32
- import { cw, type QuatLike } from "../math/quat"
33
- import { Gizmos } from "../scene/gizmos"
34
- import { Gearbox } from "./Gearbox"
35
- import { Node } from "./Node"
36
- import { DEFAULT_FRICTION, Physics } from "./Physics"
37
- import { Shape } from "./Shape"
38
- import { STATE_HEADER, WHEEL_STATE, Wheel } from "./Wheel"
39
-
40
- /** Which axle the engine drives. A wheel's own `drive` overrides it. */
41
- export type DriveLayout = "rear" | "front" | "all"
42
-
43
- /**
44
- * How an axle's two wheels share torque.
45
- * - `lsd` — limited slip (Jolt's 1.4 ratio), the sane default.
46
- * - `open` — fully open: a lifted or spinning wheel takes ALL the torque.
47
- * - `locked` — both wheels forced to the same speed. Breaks traction predictably: the drift diff.
48
- * A number is the raw max/min wheel-speed ratio (> 1; smaller is more locked).
49
- */
50
- export type DifferentialMode = "lsd" | "open" | "locked"
51
-
52
- export interface EngineConfig {
53
- /** Peak torque in N·m (default 500). The single biggest "how fast is it" knob. */
54
- torque?: number
55
- /** Redline in RPM (default 6000). */
56
- maxRpm?: number
57
- /** Idle RPM (default 1000) — the engine never falls below it. */
58
- idleRpm?: number
59
- /**
60
- * The engine's CHARACTER: a flat list `[rpm, torque, rpm, torque, …]` in NORMALISED units — x is
61
- * a fraction of the redline, y a fraction of `torque`. Left out, Jolt's own broad curve is used.
62
- *
63
- * `[0, 0.8, 0.66, 1, 1, 0.8]` is an ordinary petrol engine (peak two thirds up, falling to the
64
- * redline); `[0, 1, 1, 1]` is flat, which is what an electric motor feels like.
65
- */
66
- curve?: number[]
67
- /** Moment of inertia in kg·m² (default 0.5) — how eagerly it revs when the clutch is out. */
68
- inertia?: number
69
- /** Engine braking: angular damping of the engine itself (default 0.2). */
70
- braking?: number
71
- }
72
-
73
- export interface SteeringConfig {
74
- /** Steering lock in degrees at a standstill (default 35). */
75
- lock?: number
76
- /**
77
- * The lock still available, in degrees, once the car reaches `speed` (default 10) — what stops a
78
- * car over-driving its own front tires.
79
- *
80
- * A tire makes its peak grip at a few degrees of slip angle, and the steering angle a corner needs
81
- * falls with the SQUARE of speed. Holding full lock at 100 km/h therefore asks the front for
82
- * several times what it can give: the tires saturate, the car ploughs on, and the wheel stops
83
- * answering. In between the lock eases on `(1 − v/speed)²`, which tracks the real curve closely.
84
- * `0` turns the taper off — full lock at any speed.
85
- */
86
- atSpeed?: number
87
- /** The speed in m/s at which the lock has fallen to `atSpeed` (default 30 ≈ 108 km/h). */
88
- speed?: number
89
- /** How fast the front wheels may turn, in degrees per second (default 220). A keyboard asks for
90
- * full lock in one frame, which no steering wheel can do, and that step alone snaps a tire past
91
- * its peak. `0` = instantly. */
92
- rate?: number
93
- }
94
-
95
- /**
96
- * Aerodynamics. Both numbers are quoted the same way — **a fraction of the car's own weight at
97
- * 30 m/s (108 km/h)** — and both grow with the SQUARE of speed from there, so `0.5` at 30 m/s is
98
- * `2.0` at 60 m/s. One mental model for both, and no drag coefficients or frontal areas to invent.
99
- */
100
- export interface AeroConfig {
101
- /**
102
- * Downforce (default 0 — a car with no wing). The knob that decides whether a car corners better
103
- * or worse the faster it goes: tire grip is a constant, but the force a corner demands grows with
104
- * v², so with no downforce every car runs out of road at speed. `0.6` roughly doubles a car's
105
- * cornering at 150 km/h; a road car is nearer `0.1`, a race car well past `1`.
106
- *
107
- * It presses along the CHASSIS' own down axis (that is what keeps a car planted over a crest) and
108
- * only while a wheel is on the ground — a car on its roof is never launched by its own wing.
109
- */
110
- downforce?: number
111
- /** Air resistance (default 0.03 — an ordinary car). What gives a car a top speed at all. */
112
- drag?: number
113
- }
114
-
115
- /** Blob v5 — see bridges.d.ts for the field-by-field layout. */
116
- const BLOB_VERSION = 5
117
- const BLOB_HEADER = 21
118
- const BLOB_WHEEL = 15
119
- const DIFFERENTIALS: Record<DifferentialMode, number> = { lsd: 1.4, open: 0, locked: 1 }
120
- const GRAVITY = 9.81 // only used to place the wheels at their resting height
121
- const AXLE_EPS = 0.35 // wheels within this many metres in Z are the same axle
122
- const clamp = (v: number, lo: number, hi: number): number => (v < lo ? lo : v > hi ? hi : v)
123
-
124
- export class Vehicle extends Aspect<"vehicle", Node> {
125
- static readonly aspect = "vehicle"
126
-
127
- /** Chassis mass in kg (default 1200). Wheels are massless in this model. */
128
- mass = 1200
129
- /** Centre of mass height in chassis space (metres, negative = lower). Left out, it drops to halfway
130
- * down the chassis shape — a car with its mass at the shape's centre tips over. */
131
- centerOfMass?: number
132
- /** Which axle the engine drives (default "rear"). A wheel's own `drive` overrides it. */
133
- driveAxle: DriveLayout = "rear"
134
- /** Differential lock across each driven axle. `locked` is the drift setup — both wheels turn
135
- * together, so the pair lets go as a pair. Live: re-configure the aspect to switch it mid-drive. */
136
- differential: DifferentialMode | number = "lsd"
137
- /**
138
- * Anti-roll bars between each axle's wheels — what stops a car rolling onto its inside wheels in a
139
- * fast corner. `true` (default) is a bar 0.01 as stiff as the wheel's own spring; a number is that
140
- * ratio directly, and `0` / `false` removes the bars.
141
- *
142
- * **The numbers are far smaller than they look and the gain is steep** — Jolt's bar feeds
143
- * `lengthDifference × stiffness × dt` as a bias into the suspension's own soft constraint, so 4%
144
- * of the spring already flattens the body. Measured on a 1250 kg car: `0` → 1.3° of roll at 0.86 g,
145
- * `0.01` → 0.6°, `0.15` → 0.2° (flat), `0.5` → the car HOPS.
146
- *
147
- * This is the knob for how much a car visibly LEANS, not for wheel lift: bars change how far the
148
- * body tips, not how much load transfers. For lift, lower `centerOfMass`.
149
- */
150
- antiRoll: boolean | number = true
151
- /** Pitch/roll limit in degrees (default 60) before the constraint rights the car. 180 turns it off. */
152
- maxTilt = 60
153
- /** Friction of the CHASSIS body itself — how it slides when it lands on its roof or side. Nothing
154
- * to do with the tires; those are the `Wheel`'s `tire.grip` × the ground's friction. */
155
- friction = DEFAULT_FRICTION
156
- /** Strength of the clutch when fully engaged (default 10). Small values slip; large ones weld the
157
- * engine to the wheels. The `Gearbox` scales it 0..1 while shifting. */
158
- clutchStrength = 10
159
-
160
- engine: EngineConfig = { torque: 500, maxRpm: 6000, idleRpm: 1000, inertia: 0.5, braking: 0.2 }
161
- steering: SteeringConfig = { lock: 35, atSpeed: 10, speed: 30, rate: 220 }
162
- aero: AeroConfig = { downforce: 0, drag: 0.03 }
163
-
164
- /** Scene-editor inspector: the flat, tunable half. The grouped objects are edited in code. */
165
- static fields: FieldMeta<Vehicle> = {
166
- mass: { min: 1, step: 10 },
167
- driveAxle: { label: "Drive", options: [ "rear", "front", "all" ] },
168
- differential: { options: [ "lsd", "open", "locked" ] },
169
- antiRoll: { label: "Anti-roll" },
170
- maxTilt: { label: "Max tilt", min: 0, max: 180, step: 5 },
171
- friction: { label: "Chassis friction", min: 0, max: 2, step: 0.05 },
172
- clutchStrength: { label: "Clutch", min: 0.5, max: 100, step: 0.5 },
173
- engine: { hidden: true },
174
- steering: { hidden: true },
175
- aero: { hidden: true },
176
- }
177
- static editor = { rebuild: true }
178
-
179
- private _id = 0
180
- private _bodyId = 0
181
- private _builtMass = 0
182
- private _wheels: Wheel[] = []
183
- private _state = new Float32Array(STATE_HEADER)
184
- private _stateFrame = -1
185
- private _throttle = 0
186
- private _steer = 0
187
- private _brake = 0
188
- private _handbrake = 0
189
-
190
- onAttach(): void {
191
- if (!_creator.physicsHasSupport || !_creator.physicsHasSupport()) return
192
- if (!_creator.vehicleCreate) {
193
- console.warn("Vehicle needs a newer host (no vehicleCreate) — the car will not move")
194
- return
195
- }
196
- const shape = this.node.get(Shape)
197
- if (!shape) {
198
- throw new Error("Vehicle requires a Shape aspect (the chassis collider) — add it first: node.aspect(Shape, { box: […] }).aspect(Vehicle, {…})")
199
- }
200
- if (shape._isTriangleMesh) {
201
- 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")
202
- }
203
- if (this.node.has(Physics)) {
204
- throw new Error("Vehicle owns its own rigid body — remove the Physics aspect from the chassis node (put `mass` on the Vehicle instead)")
205
- }
206
-
207
- this._wheels = this._collectWheels()
208
- if (this._wheels.length === 0) {
209
- throw new Error("Vehicle found no wheels — put a Wheel aspect on each wheel node under the chassis: wheelFL.aspect(Wheel)")
210
- }
211
-
212
- const shapeId = shape._claim() // the vehicle owns the shape; drop the pick-only body
213
- this._id = _creator.vehicleCreate(this.node.id, shapeId, this._buildSettings())
214
- if (!this._id) {
215
- console.warn(`Vehicle: the engine refused the settings for "${this.node.name}" — check the wheel list`)
216
- shape._recreatePickBody()
217
- return
218
- }
219
- this._bodyId = _creator.vehicleBodyId ? _creator.vehicleBodyId(this._id) : 0
220
- // The chassis body owns the node's transform, so route position writes to it (see Node._xf) —
221
- // `car.position = p` places the car instead of being silently overwritten by the next sync. It is
222
- // the WEAK form of the operation: it keeps the car's velocity and its roll, so a respawn still
223
- // wants `reset(p)`, which also uprights it and clears engine, gearbox and wheel spin.
224
- this.node._xf = this._bodyId
225
- this.node._xfKind = this._bodyId ? 2 : 0
226
- if (this._bodyId && this.friction !== DEFAULT_FRICTION) {
227
- _creator.physicsSetFriction?.(this._bodyId, this.friction)
228
- }
229
- this._builtMass = this.mass
230
- this._state = new Float32Array(STATE_HEADER + this._wheels.length * WHEEL_STATE)
231
- // A car with no gearbox still has to drive: install the default automatic.
232
- if (!this.node.has(Gearbox)) this.node.aspect(Gearbox)
233
- }
234
-
235
- onDetach(): void {
236
- if (this._id) { _creator.vehicleDestroy(this._id); this._id = 0; this._bodyId = 0 }
237
- for (const w of this._wheels) w._vehicle = null
238
- this._wheels = []
239
- this.node._xf = 0
240
- this.node._xfKind = 0
241
- this.node.get(Shape)?._recreatePickBody() // hands the routing back to the pick body
242
- }
243
-
244
- /**
245
- * Re-configuring an attached vehicle retunes it in place — the way a drift button switches the
246
- * differential without the car so much as blinking:
247
- *
248
- * car.aspect(Vehicle, { differential: drift ? 'locked' : 'lsd' })
249
- *
250
- * Live: `differential`, `engine`, `steering`, `aero`, `maxTilt`, `friction`, `clutchStrength`, and
251
- * every `Wheel`'s `tire.grip` and brake torques. Everything else describes the car's STRUCTURE —
252
- * `mass`, `centerOfMass`, which wheels exist and where, their suspension, `driveAxle`, `antiRoll` — and
253
- * is baked into the constraint at attach; to change one of those, re-attach the aspect.
254
- */
255
- onReconfigure(): void {
256
- if (!this._id) return
257
- if (this.mass !== this._builtMass) {
258
- console.warn(`Vehicle: mass is fixed once the body exists (still ${this._builtMass} kg) — re-add the aspect to change it`)
259
- this.mass = this._builtMass
260
- }
261
- _creator.vehicleSetTuning?.(this._id, this._buildSettings())
262
- if (this._bodyId) _creator.physicsSetFriction?.(this._bodyId, this.friction)
263
- }
264
-
265
- /** Native vehicle id (0 until attached / no physics support). */
266
- get id(): number { return this._id }
267
- /** The chassis rigid-body id — the same handle the plain body calls take. */
268
- get bodyId(): number { return this._bodyId }
269
- /** The wheels, in blob order (front axle first). */
270
- get wheels(): readonly Wheel[] { return this._wheels }
271
-
272
- // --- driving --------------------------------------------------------------------------------
273
-
274
- /**
275
- * The driver's controls, all in [-1, 1] (brakes in [0, 1]). Sticky: the last values keep applying
276
- * until the next call, so a held key does not need re-sending. `forward < 0` asks for reverse; the
277
- * `Gearbox` engages it once the car has stopped.
278
- */
279
- drive(forward: number, steer = 0, brake = 0, handbrake = 0): this {
280
- if (this._id) {
281
- this._throttle = clamp(forward, -1, 1)
282
- this._steer = clamp(steer, -1, 1)
283
- this._brake = clamp(brake, 0, 1)
284
- this._handbrake = clamp(handbrake, 0, 1)
285
- _creator.vehicleSetInput(this._id, this._throttle, this._steer, this._brake, this._handbrake)
286
- }
287
- return this
288
- }
289
-
290
- /** Teleport upright and clear all motion (velocity, engine, gearbox, wheel spin). Defaults to the
291
- * chassis node's current pose — `reset()` alone un-flips a car where it lies. */
292
- reset(position?: Vec3Like, rotation?: QuatLike): this {
293
- if (!this._id) return this
294
- const p = position ?? this.node.worldPosition
295
- // No rotation given: keep the heading, drop the roll/pitch — the point of reset is to land upright.
296
- const wq = this.node.worldQuaternion
297
- const q: QuatLike = rotation ?? [ 0, wq.y, 0, wq.w ]
298
- _creator.vehicleReset(this._id, cx(p), cy(p), cz(p), cx(q), cy(q), cz(q), cw(q))
299
- this._stateFrame = -1
300
- const box = this.node.get(Gearbox)
301
- if (box) box.gear = 0
302
- return this
303
- }
304
-
305
- /** Push the chassis (a boost pad, an explosion) — an instantaneous impulse in kg·m/s. */
306
- applyImpulse(v: Vec3Like): this {
307
- if (this._bodyId) _creator.physicsApplyImpulse(this._bodyId, cx(v), cy(v), cz(v))
308
- return this
309
- }
310
-
311
- /** @internal The Gearbox's latched pair: the one ratio the engine runs, and the clutch envelope. */
312
- _setTransmission(ratio: number, clutch: number): void {
313
- if (this._id) _creator.vehicleSetTransmission(this._id, ratio, clamp(clutch, 0, 1))
314
- }
315
-
316
- // --- readouts (one native read per PHASE — early sees the pre-step state, late the settled one —
317
- // shared by the car and every wheel; keyed on core/Aspect's _phaseStamp) -------------------
318
-
319
- private _read(): Float32Array {
320
- if (this._id && this._stateFrame !== _phaseStamp.value) {
321
- _creator.vehicleGetState(this._id, this._state)
322
- this._stateFrame = _phaseStamp.value
323
- }
324
- return this._state
325
- }
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
-
334
- /** Speed along the car's forward axis in m/s — negative when reversing (× 3.6 for km/h). */
335
- get speed(): number { return this._read()[0]! }
336
- /** Engine revolutions per minute. */
337
- get rpm(): number { return this._read()[1]! }
338
- /** How many wheels are touching the ground. */
339
- get wheelsOnGround(): number { return this._read()[2]! }
340
- /** Any wheel on the ground — false while airborne. */
341
- get grounded(): boolean { return this._read()[2]! > 0 }
342
- /** Chassis velocity in world units/s (fresh Vec3). */
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,
355
- * falling to `steering.atSpeed`. The same curve the engine applies; useful on a HUD. */
356
- get steerLock(): number {
357
- const lock = this.steering.lock ?? 35
358
- const atSpeed = Math.max(0, this.steering.atSpeed ?? 10)
359
- if (atSpeed <= 0 || atSpeed >= lock) return lock
360
- const full = this.steering.speed ?? 30
361
- const t = full > 0 ? Math.min(Math.abs(this.speed) / full, 1) : 1
362
- return atSpeed + (lock - atSpeed) * (1 - t) * (1 - t)
363
- }
364
-
365
- // --- setup ----------------------------------------------------------------------------------
366
-
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
-
373
- const chassisInv = this.node.worldMatrix.invert()
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)
380
- let axle = 0
381
- let ref = centres[order[0]!]!.z
382
- for (const i of order) {
383
- if (Math.abs(centres[i]!.z - ref) > AXLE_EPS) { axle++; ref = centres[i]!.z }
384
- axleOf[i] = found[i]!.axle ?? axle
385
- }
386
- const maxAxle = Math.max(...axleOf)
387
-
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]!
392
- const a = axleOf[i]!
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
412
- }
413
-
414
- /** Pack the whole configuration into the one blob the host ABI takes (see bridges.d.ts). */
415
- private _buildSettings(): Float32Array {
416
- const curve = this.engine.curve ?? []
417
- const curveCount = Math.floor(curve.length / 2)
418
- const diff = typeof this.differential === "number"
419
- ? this.differential
420
- : DIFFERENTIALS[this.differential] ?? DIFFERENTIALS.lsd
421
-
422
- const out = new Float32Array(BLOB_HEADER + curveCount * 2 + this._wheels.length * BLOB_WHEEL)
423
- out[0] = BLOB_VERSION
424
- out[1] = this.mass
425
- out[2] = this.centerOfMass === undefined ? 1 : 0
426
- out[3] = this.centerOfMass ?? 0
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
462
- }
463
- return out
464
- }
465
-
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. */
468
- rebuild(): void {
469
- const w = new Mat4(this.node.worldMatrix)
470
- const P = (x: number, y: number, z: number): Vec3 => w.transformPoint([ x, y, z ])
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" })
472
- }
473
- }