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.
- package/dist/global.d.ts +35 -9
- package/dist/types/animate/tween/Animation.d.ts +69 -0
- package/dist/types/animate/tween/Timeline.d.ts +55 -0
- package/dist/types/animate/tween/animateValue.d.ts +27 -0
- package/dist/types/animate/tween/easing.d.ts +29 -0
- package/dist/types/animate/tween/spec.d.ts +178 -0
- package/dist/types/canvas/Canvas.d.ts +2 -0
- package/dist/types/g2/Node2D.d.ts +16 -0
- package/dist/types/g2/Sprite.d.ts +11 -1
- package/dist/types/gl/Camera.d.ts +15 -1
- package/dist/types/gl/DecalSet.d.ts +48 -3
- package/dist/types/gl/Foliage.d.ts +47 -0
- package/dist/types/gl/Geometry.d.ts +36 -0
- package/dist/types/gl/Light.d.ts +25 -7
- package/dist/types/gl/Lightmap.d.ts +90 -51
- package/dist/types/gl/Material.d.ts +32 -20
- package/dist/types/gl/Mesh.d.ts +7 -1
- package/dist/types/gl/Model.d.ts +41 -5
- package/dist/types/gl/Node.d.ts +18 -0
- package/dist/types/gl/Particles.d.ts +53 -1
- package/dist/types/gl/Scene.d.ts +21 -1
- package/dist/types/gl/animation/AnimationClip.d.ts +19 -0
- package/dist/types/gl/animation/Animator.d.ts +27 -0
- package/dist/types/gl/animation/DynamicBone.d.ts +184 -0
- package/dist/types/gl/animation/IK.d.ts +109 -0
- package/dist/types/gl/{Locomotion.d.ts → animation/Locomotion.d.ts} +6 -6
- package/dist/types/gl/animation/Warp.d.ts +2 -1
- package/dist/types/gl/animation/core.d.ts +35 -4
- 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/physics/Ragdoll.d.ts +161 -0
- 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} +10 -8
- 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 +35 -27
- package/dist/types/runtime/files.d.ts +24 -1
- package/dist/types/scene/defineScene.d.ts +50 -31
- package/dist/types/ui/UIButton.d.ts +3 -1
- package/dist/types/ui/UIInput.d.ts +5 -1
- package/dist/types/ui/UINode.d.ts +24 -24
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/prompts/README.md +142 -142
- package/prompts/core-design.md +27 -4
- package/prompts/core.md +35 -6
- package/prompts/select.ts +19 -4
- package/src/animate/tween/Animation.ts +378 -0
- package/src/animate/tween/Timeline.ts +175 -0
- package/src/animate/tween/animateValue.ts +100 -0
- package/src/animate/tween/easing.ts +172 -0
- package/src/animate/tween/spec.ts +479 -0
- package/src/bridges.d.ts +1760 -1481
- package/src/canvas/Canvas.ts +21 -0
- package/src/compile/__tests__/assetMacro.test.ts +26 -0
- package/src/compile/__tests__/compile.test.ts +11 -0
- package/src/compile/__tests__/detectEntry.test.ts +19 -0
- package/src/compile/__tests__/serverSplit.test.ts +27 -0
- package/src/compile/bundler.ts +34 -4
- package/src/compile/compileProject.ts +31 -1
- package/src/compile/detectEntry.ts +8 -3
- package/src/compile/header.ts +6 -3
- package/src/compile/index.ts +3 -0
- package/src/compile/sceneEditor.ts +42 -1
- package/src/compile/serverSplit.ts +9 -3
- package/src/g2/Node2D.ts +38 -0
- package/src/g2/Sprite.ts +20 -1
- package/src/gl/Camera.ts +34 -1
- package/src/gl/DecalSet.ts +132 -5
- package/src/gl/Foliage.ts +102 -0
- package/src/gl/Geometry.ts +109 -0
- package/src/gl/Light.ts +46 -16
- package/src/gl/Lightmap.ts +440 -249
- package/src/gl/Material.ts +69 -36
- package/src/gl/Mesh.ts +120 -102
- package/src/gl/Model.ts +167 -124
- package/src/gl/Node.ts +40 -1
- package/src/gl/Particles.ts +82 -5
- package/src/gl/Scene.ts +35 -2
- package/src/gl/animation/AnimationClip.ts +52 -0
- package/src/gl/animation/Animator.ts +42 -2
- package/src/gl/animation/DynamicBone.ts +482 -0
- package/src/gl/animation/IK.ts +214 -0
- package/src/gl/{Locomotion.ts → animation/Locomotion.ts} +7 -7
- package/src/gl/animation/Playback.ts +5 -4
- package/src/gl/animation/Warp.ts +5 -2
- package/src/gl/animation/core.ts +65 -4
- 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/physics/Ragdoll.ts +451 -0
- 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} +14 -12
- 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 +236 -224
- package/src/runtime/files.ts +32 -2
- package/src/scene/defineScene.ts +92 -66
- package/src/scene/level.ts +2 -2
- package/src/ui/UIButton.ts +2 -2
- package/src/ui/UIInput.ts +3 -3
- package/src/ui/UINode.ts +61 -36
- package/dist/types/animate/animate.d.ts +0 -20
- package/dist/types/gl/Gearbox.d.ts +0 -86
- package/dist/types/gl/IK.d.ts +0 -53
- package/dist/types/gl/Ragdoll.d.ts +0 -86
- package/dist/types/gl/Vehicle.d.ts +0 -191
- package/dist/types/gl/Wheel.d.ts +0 -95
- package/src/animate/animate.ts +0 -238
- package/src/gl/Gearbox.ts +0 -212
- package/src/gl/IK.ts +0 -193
- package/src/gl/Ragdoll.ts +0 -270
- 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
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
|
-
}
|