@jgengine/core 0.11.0 → 0.12.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/CHANGELOG.md +26 -0
- package/dist/ai/populationDirector.d.ts +103 -0
- package/dist/ai/populationDirector.js +190 -0
- package/dist/ai/pursuit.d.ts +56 -0
- package/dist/ai/pursuit.js +53 -0
- package/dist/authoring.d.ts +1 -1
- package/dist/authoring.js +1 -1
- package/dist/combat.d.ts +1 -0
- package/dist/combat.js +1 -0
- package/dist/devtools/controls.d.ts +38 -0
- package/dist/devtools/controls.js +181 -0
- package/dist/devtools/devtools.d.ts +5 -197
- package/dist/devtools/devtools.js +38 -774
- package/dist/devtools/discover.d.ts +32 -0
- package/dist/devtools/discover.js +240 -0
- package/dist/devtools/fallbackSeams.d.ts +50 -0
- package/dist/devtools/fallbackSeams.js +111 -0
- package/dist/devtools/frame.d.ts +15 -0
- package/dist/devtools/frame.js +247 -0
- package/dist/devtools/logs.d.ts +15 -0
- package/dist/devtools/logs.js +86 -0
- package/dist/devtools/overrides.d.ts +16 -0
- package/dist/devtools/overrides.js +66 -0
- package/dist/devtools/types.d.ts +212 -0
- package/dist/devtools/types.js +1 -0
- package/dist/economy/auctionBook.d.ts +122 -0
- package/dist/economy/auctionBook.js +213 -0
- package/dist/economy/priceHistory.d.ts +44 -0
- package/dist/economy/priceHistory.js +89 -0
- package/dist/economy/unlockPoints.d.ts +92 -0
- package/dist/economy/unlockPoints.js +103 -0
- package/dist/editor/commandApply.d.ts +5 -0
- package/dist/editor/commandApply.js +755 -0
- package/dist/editor/commands.d.ts +48 -3
- package/dist/editor/commands.js +40 -754
- package/dist/editor/directives.d.ts +94 -0
- package/dist/editor/directives.js +206 -0
- package/dist/editor/document.d.ts +37 -2
- package/dist/editor/document.js +422 -6
- package/dist/editor/entityCatalog.d.ts +42 -0
- package/dist/editor/entityCatalog.js +101 -0
- package/dist/editor/environment.d.ts +74 -0
- package/dist/editor/environment.js +169 -0
- package/dist/editor/index.d.ts +26 -1
- package/dist/editor/index.js +8 -1
- package/dist/editor/minimap.d.ts +31 -0
- package/dist/editor/minimap.js +73 -0
- package/dist/editor/streamer.d.ts +66 -0
- package/dist/editor/streamer.js +68 -0
- package/dist/editor/types.d.ts +143 -0
- package/dist/editor/world.d.ts +136 -0
- package/dist/editor/world.js +303 -0
- package/dist/game/breeding.d.ts +161 -0
- package/dist/game/breeding.js +181 -0
- package/dist/game/cameraConfig.d.ts +451 -0
- package/dist/game/cameraConfig.js +1 -0
- package/dist/game/defineGame.d.ts +16 -9
- package/dist/game/defineGame.js +14 -6
- package/dist/game/defineSystem.d.ts +2 -0
- package/dist/game/defineSystem.js +2 -0
- package/dist/game/events.d.ts +28 -0
- package/dist/game/playableGame.d.ts +34 -435
- package/dist/game/playableGame.js +1 -1
- package/dist/game/progression.d.ts +35 -0
- package/dist/game/progression.js +19 -0
- package/dist/game/quest.js +1 -3
- package/dist/game/tribe.d.ts +184 -0
- package/dist/game/tribe.js +432 -0
- package/dist/gameplay.d.ts +3 -1
- package/dist/gameplay.js +3 -1
- package/dist/inventory/encumbrance.d.ts +69 -0
- package/dist/inventory/encumbrance.js +68 -0
- package/dist/item/durability.js +1 -3
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +24 -1
- package/dist/movement/movementModel.d.ts +15 -1
- package/dist/movement/movementModel.js +33 -15
- package/dist/movement/playerMovement.js +102 -2
- package/dist/multiplayer/combatSnapshot.js +13 -8
- package/dist/multiplayer/presenceModel.d.ts +2 -0
- package/dist/multiplayer/presenceModel.js +8 -0
- package/dist/physics/drivableVehicle.d.ts +2 -0
- package/dist/physics/drivableVehicle.js +1 -1
- package/dist/physics/flightDynamics.d.ts +82 -0
- package/dist/physics/flightDynamics.js +165 -0
- package/dist/physics/gravityField.d.ts +25 -0
- package/dist/physics/gravityField.js +41 -0
- package/dist/physics/kinematicVehicle.d.ts +87 -0
- package/dist/physics/kinematicVehicle.js +193 -11
- package/dist/physics/physicsWorld.d.ts +26 -0
- package/dist/physics/physicsWorld.js +109 -117
- package/dist/physics/vehicleObstacles.d.ts +44 -0
- package/dist/physics/vehicleObstacles.js +115 -0
- package/dist/random/rng.d.ts +4 -1
- package/dist/random/rng.js +4 -1
- package/dist/runtime/adapter.d.ts +13 -0
- package/dist/runtime/adapter.js +13 -0
- package/dist/runtime/context/combat.d.ts +51 -0
- package/dist/runtime/context/combat.js +130 -0
- package/dist/runtime/context/deathLoot.d.ts +30 -0
- package/dist/runtime/context/deathLoot.js +39 -0
- package/dist/runtime/context/player.d.ts +48 -0
- package/dist/runtime/context/player.js +125 -0
- package/dist/runtime/context/scene.d.ts +59 -0
- package/dist/runtime/context/scene.js +291 -0
- package/dist/runtime/context/snapshotCodecs.d.ts +12 -0
- package/dist/runtime/context/snapshotCodecs.js +26 -0
- package/dist/runtime/descriptors/baseline.js +26 -3
- package/dist/runtime/descriptors/features.d.ts +5 -0
- package/dist/runtime/descriptors/features.js +20 -2
- package/dist/runtime/gameContext.d.ts +2 -428
- package/dist/runtime/gameContext.js +88 -501
- package/dist/runtime/gameContextTypes.d.ts +467 -0
- package/dist/runtime/gameContextTypes.js +1 -0
- package/dist/runtime/headlessRunner.d.ts +4 -2
- package/dist/runtime/headlessRunner.js +1 -0
- package/dist/runtime/hostPolicy.d.ts +51 -0
- package/dist/runtime/hostPolicy.js +68 -0
- package/dist/runtime/hostedGameRunner.d.ts +3 -1
- package/dist/runtime/hostedGameRunner.js +1 -0
- package/dist/runtime/hostedWorldSession.d.ts +3 -1
- package/dist/runtime/hostedWorldSession.js +1 -0
- package/dist/runtime/worldSnapshot.d.ts +13 -1
- package/dist/runtime/worldSnapshot.js +15 -3
- package/dist/scene/assetCatalog.d.ts +5 -0
- package/dist/scene/assetSpace.js +1 -3
- package/dist/scene/authoredTriggers.d.ts +50 -0
- package/dist/scene/authoredTriggers.js +114 -0
- package/dist/scene/builtinSceneKinds.js +8 -3
- package/dist/scene/colliders.d.ts +58 -0
- package/dist/scene/colliders.js +108 -0
- package/dist/scene/collisionMesh.d.ts +94 -0
- package/dist/scene/collisionMesh.js +699 -0
- package/dist/scene/companion.d.ts +147 -0
- package/dist/scene/companion.js +193 -0
- package/dist/scene/entityStats.d.ts +7 -9
- package/dist/scene/entityStats.js +10 -14
- package/dist/scene/sceneKinds.d.ts +16 -0
- package/dist/scene/sceneKinds.js +4 -0
- package/dist/scene/sceneRaycast.js +15 -0
- package/dist/stats/spawnLevelStats.d.ts +110 -0
- package/dist/stats/spawnLevelStats.js +140 -0
- package/dist/stats/statPool.d.ts +70 -0
- package/dist/stats/statPool.js +59 -0
- package/dist/store/changeSignal.d.ts +3 -0
- package/dist/store/changeSignal.js +2 -0
- package/dist/store/observableKeyedStore.d.ts +2 -0
- package/dist/store/observableKeyedStore.js +1 -0
- package/dist/survival/decayMeter.js +1 -3
- package/dist/survival/regionHealth.js +1 -3
- package/dist/ui/hudLayout.d.ts +5 -0
- package/dist/ui/hudLayout.js +17 -1
- package/dist/world/authoredEntities.d.ts +29 -0
- package/dist/world/authoredEntities.js +29 -0
- package/dist/world/authoredObjects.d.ts +15 -1
- package/dist/world/authoredObjects.js +13 -1
- package/dist/world/authoredSpawn.d.ts +39 -0
- package/dist/world/authoredSpawn.js +37 -0
- package/dist/world/buildSockets.d.ts +152 -0
- package/dist/world/buildSockets.js +188 -0
- package/dist/world/cityBlocks.d.ts +152 -0
- package/dist/world/cityBlocks.js +670 -0
- package/dist/world/cityContent.d.ts +77 -0
- package/dist/world/cityContent.js +261 -0
- package/dist/world/cityGeometry.d.ts +114 -0
- package/dist/world/cityGeometry.js +573 -0
- package/dist/world/cityKind.d.ts +314 -0
- package/dist/world/cityKind.js +1578 -0
- package/dist/world/features.d.ts +18 -3
- package/dist/world/features.js +10 -1
- package/dist/world/grassKind.js +2 -2
- package/dist/world/markers.d.ts +50 -4
- package/dist/world/markers.js +49 -0
- package/dist/world/massing.js +1 -1
- package/dist/world/minimapBake.d.ts +78 -0
- package/dist/world/minimapBake.js +208 -0
- package/dist/world/minimapTrack.d.ts +17 -0
- package/dist/world/minimapTrack.js +22 -0
- package/dist/world/pathNetwork.d.ts +159 -0
- package/dist/world/pathNetwork.js +684 -0
- package/dist/world/place.d.ts +166 -0
- package/dist/world/place.js +96 -0
- package/dist/world/placeAsset.js +3 -1
- package/dist/world/poleLineKind.d.ts +40 -0
- package/dist/world/poleLineKind.js +97 -0
- package/dist/world/polyline.js +9 -2
- package/dist/world/resourceNode.d.ts +109 -0
- package/dist/world/resourceNode.js +140 -0
- package/dist/world/scatter.js +2 -20
- package/dist/world/streets.d.ts +29 -0
- package/dist/world/streets.js +55 -0
- package/dist/world/terraform.js +58 -88
- package/dist/world/terrain.d.ts +45 -0
- package/dist/world/terrain.js +78 -0
- package/dist/world.d.ts +14 -5
- package/dist/world.js +12 -3
- package/package.json +1 -1
- package/skills/game-design/SKILL.md +28 -0
- package/skills/game-design/agents/openai.yaml +4 -0
- package/skills/game-design/references/application-playbook.md +170 -0
- package/skills/game-design/references/field-guide.md +288 -0
- package/skills/jgengine/SKILL.md +7 -4
- package/skills/jgengine/api.md +172 -79
- package/skills/jgengine/capabilities.md +104 -2
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-combat/SKILL.md +5 -1
- package/skills/jgengine-combat/api.md +36 -0
- package/skills/jgengine-combat/capabilities.md +44 -0
- package/skills/jgengine-combat/recipes/portable-stat-pools.md +122 -0
- package/skills/jgengine-combat/reference.md +12 -2
- package/skills/jgengine-gameplay/api.md +152 -26
- package/skills/jgengine-gameplay/capabilities.md +78 -2
- package/skills/jgengine-gameplay/reference-systems.md +1 -5
- package/skills/jgengine-world/SKILL.md +13 -0
- package/skills/jgengine-world/api.md +419 -15
- package/skills/jgengine-world/capabilities.md +108 -2
- package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
- package/skills/jgengine-world/reference.md +43 -4
- package/skills/jgengine/reference-cartridge.md +0 -72
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { uniformGravity } from "./gravityField.js";
|
|
2
|
+
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
|
|
3
|
+
const length = (v) => Math.hypot(v[0], v[1], v[2]);
|
|
4
|
+
function axes(rotation) {
|
|
5
|
+
const [pitch, yaw, roll] = rotation;
|
|
6
|
+
const sp = Math.sin(pitch);
|
|
7
|
+
const cp = Math.cos(pitch);
|
|
8
|
+
const sy = Math.sin(yaw);
|
|
9
|
+
const cy = Math.cos(yaw);
|
|
10
|
+
const sr = Math.sin(roll);
|
|
11
|
+
const cr = Math.cos(roll);
|
|
12
|
+
const forward = [sy * cp, sp, cy * cp];
|
|
13
|
+
const flatRight = [cy, 0, -sy];
|
|
14
|
+
const flatUp = [-sy * sp, cp, -cy * sp];
|
|
15
|
+
return {
|
|
16
|
+
forward,
|
|
17
|
+
right: [flatRight[0] * cr + flatUp[0] * sr, flatUp[1] * sr, flatRight[2] * cr + flatUp[2] * sr],
|
|
18
|
+
up: [-flatRight[0] * sr + flatUp[0] * cr, flatUp[1] * cr, -flatRight[2] * sr + flatUp[2] * cr],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function dot(a, b) {
|
|
22
|
+
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
23
|
+
}
|
|
24
|
+
function wrapAngle(value) {
|
|
25
|
+
let angle = value;
|
|
26
|
+
while (angle > Math.PI)
|
|
27
|
+
angle -= Math.PI * 2;
|
|
28
|
+
while (angle < -Math.PI)
|
|
29
|
+
angle += Math.PI * 2;
|
|
30
|
+
return angle;
|
|
31
|
+
}
|
|
32
|
+
/** Six-degree-of-freedom arcade flight model for fixed-wing, helicopter, and VTOL aircraft.
|
|
33
|
+
* @capability flight-dynamics simulate fixed-wing, helicopter, and VTOL aircraft
|
|
34
|
+
*/
|
|
35
|
+
export function createAircraftDynamics(tuning, options = {}) {
|
|
36
|
+
let position = [...(options.position ?? [0, 0, 0])];
|
|
37
|
+
let velocity = [...(options.velocity ?? [0, 0, 0])];
|
|
38
|
+
let rotation = [...(options.rotation ?? [0, 0, 0])];
|
|
39
|
+
let angular = [0, 0, 0];
|
|
40
|
+
let rotor = 0;
|
|
41
|
+
let lastVerticalSpeed = velocity[1];
|
|
42
|
+
const gravity = Math.abs(tuning.gravity ?? 9.81);
|
|
43
|
+
const gravityField = options.gravityField ?? uniformGravity([0, -gravity, 0]);
|
|
44
|
+
const windAt = options.wind ?? (() => [0, 0, 0]);
|
|
45
|
+
const groundAt = options.groundHeight ?? (() => 0);
|
|
46
|
+
return {
|
|
47
|
+
tick(dt, input) {
|
|
48
|
+
const stepDt = clamp(dt, 0, 0.05);
|
|
49
|
+
const wind = windAt(position);
|
|
50
|
+
const gravityVector = gravityField.sample(position);
|
|
51
|
+
const gravityMagnitude = Math.max(0.001, length(gravityVector));
|
|
52
|
+
const relative = [velocity[0] - wind[0], velocity[1] - wind[1], velocity[2] - wind[2]];
|
|
53
|
+
const airspeed = length(relative);
|
|
54
|
+
const basis = axes(rotation);
|
|
55
|
+
const forwardSpeed = dot(relative, basis.forward);
|
|
56
|
+
const rightSpeed = dot(relative, basis.right);
|
|
57
|
+
const upSpeed = dot(relative, basis.up);
|
|
58
|
+
const angleOfAttack = Math.atan2(-upSpeed, Math.max(0.01, Math.abs(forwardSpeed)));
|
|
59
|
+
const speedAuthority = clamp(airspeed / Math.max(1, tuning.stallSpeed), 0.12, 1);
|
|
60
|
+
const rotorTarget = tuning.kind === "fixedWing" ? 0 : clamp(input.collective ?? input.throttle, 0, 1);
|
|
61
|
+
const rotorResponse = Math.max(0.01, tuning.rotorResponse ?? 3.5);
|
|
62
|
+
rotor += (rotorTarget - rotor) * (1 - Math.exp(-rotorResponse * stepDt));
|
|
63
|
+
const vectoring = tuning.kind === "vtol" ? clamp(input.vectoring ?? 1, 0, 1) : tuning.kind === "rotorcraft" ? 1 : 0;
|
|
64
|
+
const transition = tuning.kind === "vtol"
|
|
65
|
+
? clamp(Math.max(vectoring, 1 - airspeed / Math.max(1, tuning.vtolTransitionSpeed ?? 32)), 0, 1)
|
|
66
|
+
: vectoring;
|
|
67
|
+
const controlAuthority = transition + (1 - transition) * speedAuthority;
|
|
68
|
+
const rates = tuning.controls;
|
|
69
|
+
const targets = [
|
|
70
|
+
clamp(input.pitch, -1, 1) * rates.pitch * controlAuthority,
|
|
71
|
+
clamp(input.yaw, -1, 1) * rates.yaw * controlAuthority,
|
|
72
|
+
clamp(input.roll, -1, 1) * rates.roll * controlAuthority,
|
|
73
|
+
];
|
|
74
|
+
const response = 1 - Math.exp(-Math.max(0, rates.response) * stepDt);
|
|
75
|
+
angular[0] += (targets[0] - angular[0]) * response;
|
|
76
|
+
angular[1] += (targets[1] - angular[1]) * response;
|
|
77
|
+
angular[2] += (targets[2] - angular[2]) * response;
|
|
78
|
+
const damping = Math.max(0, 1 - rates.damping * stepDt);
|
|
79
|
+
angular[0] *= damping;
|
|
80
|
+
angular[1] *= damping;
|
|
81
|
+
angular[2] *= damping;
|
|
82
|
+
const stability = clamp(rates.stability * stepDt, 0, 1);
|
|
83
|
+
if (Math.abs(input.pitch) < 0.05)
|
|
84
|
+
angular[0] += -rotation[0] * stability;
|
|
85
|
+
if (Math.abs(input.roll) < 0.05)
|
|
86
|
+
angular[2] += -rotation[2] * stability;
|
|
87
|
+
rotation[0] = clamp(rotation[0] + angular[0] * stepDt, -Math.PI * 0.49, Math.PI * 0.49);
|
|
88
|
+
rotation[1] = wrapAngle(rotation[1] + angular[1] * stepDt);
|
|
89
|
+
rotation[2] = wrapAngle(rotation[2] + angular[2] * stepDt);
|
|
90
|
+
const updated = axes(rotation);
|
|
91
|
+
const throttle = clamp(input.throttle, 0, 1);
|
|
92
|
+
const afterburner = 1 + clamp(input.afterburner ?? 0, 0, 1) * ((tuning.afterburnerMultiplier ?? 1) - 1);
|
|
93
|
+
const thrustAccel = tuning.maxThrust * throttle * afterburner / Math.max(0.01, tuning.mass);
|
|
94
|
+
const hoverAccel = (tuning.hoverThrust ?? gravity * tuning.mass * 1.08) * rotor / Math.max(0.01, tuning.mass);
|
|
95
|
+
const ground = groundAt(position[0], position[2]);
|
|
96
|
+
const clearance = Math.max(0, position[1] - ground);
|
|
97
|
+
const groundEffectHeight = Math.max(0.1, tuning.groundEffectHeight ?? 8);
|
|
98
|
+
const groundEffect = transition > 0 ? 1 + clamp(1 - clearance / groundEffectHeight, 0, 1) * 0.18 : 1;
|
|
99
|
+
const liftFraction = clamp((Math.abs(forwardSpeed) / Math.max(0.1, tuning.stallSpeed)) ** 2, 0, 1.35);
|
|
100
|
+
const stallAngle = Math.PI * 0.22;
|
|
101
|
+
const aoaEfficiency = clamp(1 - Math.max(0, Math.abs(angleOfAttack) - stallAngle) / (Math.PI * 0.3), 0.08, 1);
|
|
102
|
+
const wingLift = tuning.lift * liftFraction * aoaEfficiency * (1 - transition);
|
|
103
|
+
const rotorLift = hoverAccel * groundEffect * transition;
|
|
104
|
+
const forwardThrust = thrustAccel * (1 - transition * 0.82);
|
|
105
|
+
const cyclicForward = transition * hoverAccel * Math.sin(rotation[0]) * 0.75;
|
|
106
|
+
const cyclicSide = -transition * hoverAccel * Math.sin(rotation[2]) * 0.75;
|
|
107
|
+
const dragScale = tuning.drag * (1 + clamp(input.airbrake ?? 0, 0, 1) * 4);
|
|
108
|
+
const speedRatio = airspeed / Math.max(1, tuning.maxSpeed);
|
|
109
|
+
const dragAccel = dragScale * airspeed * (1 + speedRatio * speedRatio);
|
|
110
|
+
const invAir = airspeed > 0.001 ? 1 / airspeed : 0;
|
|
111
|
+
velocity[0] += (updated.forward[0] * (forwardThrust + cyclicForward) + updated.right[0] * cyclicSide + updated.up[0] * wingLift - relative[0] * invAir * dragAccel - updated.right[0] * rightSpeed * tuning.sideDrag + updated.up[0] * rotorLift + gravityVector[0]) * stepDt;
|
|
112
|
+
velocity[1] += (updated.forward[1] * (forwardThrust + cyclicForward) + updated.right[1] * cyclicSide + updated.up[1] * wingLift - relative[1] * invAir * dragAccel - updated.right[1] * rightSpeed * tuning.sideDrag + updated.up[1] * rotorLift + gravityVector[1]) * stepDt;
|
|
113
|
+
velocity[2] += (updated.forward[2] * (forwardThrust + cyclicForward) + updated.right[2] * cyclicSide + updated.up[2] * wingLift - relative[2] * invAir * dragAccel - updated.right[2] * rightSpeed * tuning.sideDrag + updated.up[2] * rotorLift + gravityVector[2]) * stepDt;
|
|
114
|
+
const newSpeed = length(velocity);
|
|
115
|
+
if (newSpeed > tuning.maxSpeed * afterburner && newSpeed > 0) {
|
|
116
|
+
const scale = tuning.maxSpeed * afterburner / newSpeed;
|
|
117
|
+
velocity[0] *= scale;
|
|
118
|
+
velocity[1] *= scale;
|
|
119
|
+
velocity[2] *= scale;
|
|
120
|
+
}
|
|
121
|
+
position[0] += velocity[0] * stepDt;
|
|
122
|
+
position[1] += velocity[1] * stepDt;
|
|
123
|
+
position[2] += velocity[2] * stepDt;
|
|
124
|
+
const floor = ground + (tuning.groundClearance ?? 0.6);
|
|
125
|
+
let grounded = false;
|
|
126
|
+
if (position[1] < floor) {
|
|
127
|
+
position[1] = floor;
|
|
128
|
+
if (velocity[1] < 0)
|
|
129
|
+
velocity[1] *= -0.08;
|
|
130
|
+
velocity[0] *= 0.985;
|
|
131
|
+
velocity[2] *= 0.985;
|
|
132
|
+
grounded = true;
|
|
133
|
+
}
|
|
134
|
+
const verticalAcceleration = stepDt > 0 ? (velocity[1] - lastVerticalSpeed) / stepDt : 0;
|
|
135
|
+
lastVerticalSpeed = velocity[1];
|
|
136
|
+
return {
|
|
137
|
+
position: [...position],
|
|
138
|
+
velocity: [...velocity],
|
|
139
|
+
rotation: [...rotation],
|
|
140
|
+
forwardSpeed,
|
|
141
|
+
airspeed,
|
|
142
|
+
verticalSpeed: velocity[1],
|
|
143
|
+
angleOfAttack,
|
|
144
|
+
liftFraction,
|
|
145
|
+
stalled: transition < 0.5 && (Math.abs(forwardSpeed) < tuning.stallSpeed || aoaEfficiency < 0.35),
|
|
146
|
+
grounded,
|
|
147
|
+
gForce: (verticalAcceleration - gravityVector[1]) / gravityMagnitude,
|
|
148
|
+
};
|
|
149
|
+
},
|
|
150
|
+
pose: () => ({ position: [...position], rotation: [...rotation] }),
|
|
151
|
+
velocity: () => [...velocity],
|
|
152
|
+
resetTo(nextPosition, nextRotation = [0, 0, 0]) {
|
|
153
|
+
position = [...nextPosition];
|
|
154
|
+
velocity = [0, 0, 0];
|
|
155
|
+
rotation = [...nextRotation];
|
|
156
|
+
angular = [0, 0, 0];
|
|
157
|
+
rotor = 0;
|
|
158
|
+
lastVerticalSpeed = 0;
|
|
159
|
+
},
|
|
160
|
+
setVelocity(nextVelocity) {
|
|
161
|
+
velocity = [...nextVelocity];
|
|
162
|
+
lastVerticalSpeed = velocity[1];
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Three-dimensional acceleration vector sampled from a gravity field. */
|
|
2
|
+
export type GravityVector = readonly [number, number, number];
|
|
3
|
+
/** Position-dependent gravity source used by movement and vehicle simulations. */
|
|
4
|
+
export interface GravityField {
|
|
5
|
+
sample(position: GravityVector): GravityVector;
|
|
6
|
+
}
|
|
7
|
+
/** Center, strength, and falloff for radial gravity. */
|
|
8
|
+
export interface PointGravityOptions {
|
|
9
|
+
center: GravityVector;
|
|
10
|
+
strength: number;
|
|
11
|
+
falloff?: "constant" | "inverseSquare";
|
|
12
|
+
minRadius?: number;
|
|
13
|
+
}
|
|
14
|
+
/** Constant vector gravity for ordinary worlds, space stations, and sideways-gravity levels.
|
|
15
|
+
* @capability gravity-field sample configurable uniform, radial, or combined gravity
|
|
16
|
+
*/
|
|
17
|
+
export declare function uniformGravity(vector?: GravityVector): GravityField;
|
|
18
|
+
/** Radial gravity toward one center, with optional inverse-square falloff for planetary worlds.
|
|
19
|
+
* @capability gravity-field sample configurable uniform, radial, or combined gravity
|
|
20
|
+
*/
|
|
21
|
+
export declare function pointGravity(options: PointGravityOptions): GravityField;
|
|
22
|
+
/** Adds several gravity sources into one field.
|
|
23
|
+
* @capability gravity-field sample configurable uniform, radial, or combined gravity
|
|
24
|
+
*/
|
|
25
|
+
export declare function combineGravity(fields: readonly GravityField[]): GravityField;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** Constant vector gravity for ordinary worlds, space stations, and sideways-gravity levels.
|
|
2
|
+
* @capability gravity-field sample configurable uniform, radial, or combined gravity
|
|
3
|
+
*/
|
|
4
|
+
export function uniformGravity(vector = [0, -9.81, 0]) {
|
|
5
|
+
return { sample: () => vector };
|
|
6
|
+
}
|
|
7
|
+
/** Radial gravity toward one center, with optional inverse-square falloff for planetary worlds.
|
|
8
|
+
* @capability gravity-field sample configurable uniform, radial, or combined gravity
|
|
9
|
+
*/
|
|
10
|
+
export function pointGravity(options) {
|
|
11
|
+
const minRadius = Math.max(0.001, options.minRadius ?? 1);
|
|
12
|
+
return {
|
|
13
|
+
sample(position) {
|
|
14
|
+
const dx = options.center[0] - position[0];
|
|
15
|
+
const dy = options.center[1] - position[1];
|
|
16
|
+
const dz = options.center[2] - position[2];
|
|
17
|
+
const distance = Math.max(minRadius, Math.hypot(dx, dy, dz));
|
|
18
|
+
const scale = options.falloff === "inverseSquare" ? options.strength / (distance * distance) : options.strength;
|
|
19
|
+
return [dx / distance * scale, dy / distance * scale, dz / distance * scale];
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** Adds several gravity sources into one field.
|
|
24
|
+
* @capability gravity-field sample configurable uniform, radial, or combined gravity
|
|
25
|
+
*/
|
|
26
|
+
export function combineGravity(fields) {
|
|
27
|
+
return {
|
|
28
|
+
sample(position) {
|
|
29
|
+
let x = 0;
|
|
30
|
+
let y = 0;
|
|
31
|
+
let z = 0;
|
|
32
|
+
for (const field of fields) {
|
|
33
|
+
const vector = field.sample(position);
|
|
34
|
+
x += vector[0];
|
|
35
|
+
y += vector[1];
|
|
36
|
+
z += vector[2];
|
|
37
|
+
}
|
|
38
|
+
return [x, y, z];
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -12,6 +12,77 @@ export interface KinematicVehicleTuning {
|
|
|
12
12
|
handbrakeGrip: number;
|
|
13
13
|
/** Forward-speed decay per second when coasting (sleds, boats); default `0`. */
|
|
14
14
|
rollingResistance?: number;
|
|
15
|
+
/** Optional higher-fidelity powertrain. Omit to preserve the direct arcade acceleration model. */
|
|
16
|
+
powertrain?: KinematicPowertrainTuning;
|
|
17
|
+
/** Optional bicycle-model steering with speed-sensitive lock and smoothed rack response. */
|
|
18
|
+
steering?: KinematicSteeringTuning;
|
|
19
|
+
/** Optional aerodynamic drag/downforce and electronic driver assists. */
|
|
20
|
+
dynamics?: KinematicDynamicsTuning;
|
|
21
|
+
/**
|
|
22
|
+
* Optional mass-and-force chassis layer (#1051). When present it supersedes `engineAccel`/`brakeAccel`
|
|
23
|
+
* with force/mass dynamics — a tire friction budget, lateral weight transfer, and engine braking — so
|
|
24
|
+
* vehicles of different mass, drive/brake force, and center-of-gravity drive measurably differently.
|
|
25
|
+
* Omit to preserve the direct arcade acceleration model exactly.
|
|
26
|
+
*/
|
|
27
|
+
chassis?: KinematicChassisTuning;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Mass-and-force chassis layer (#1051); when present it supersedes `engineAccel`/`brakeAccel` with
|
|
31
|
+
* force/mass dynamics. Drive/brake become forces divided by `massKg`, a per-tick tire friction budget
|
|
32
|
+
* (`tireGrip` * grip curve * surface * downforce * m * g) is split lateral-first then longitudinal so
|
|
33
|
+
* hard slides and launches saturate, and `comHeight`/`trackWidth` set weight-transfer washout and body
|
|
34
|
+
* lean. Coasting decelerates from physical road load, not a per-vehicle constant: rolling resistance
|
|
35
|
+
* `μ_rr · m · g` always, plus engine braking `ENGINE_BRAKE_FRACTION · engineForce` routed through the
|
|
36
|
+
* current gear (so it strengthens as the car downshifts) whenever the throttle is lifted and a
|
|
37
|
+
* `powertrain` is configured. All fields are required; omit the whole block for the legacy model.
|
|
38
|
+
*/
|
|
39
|
+
export interface KinematicChassisTuning {
|
|
40
|
+
/** Total vehicle mass, kg (1200 compact … 11000 bus); divides every drive/brake force into accel. */
|
|
41
|
+
massKg: number;
|
|
42
|
+
/** Peak drive force at the wheels, N — scaled by the powertrain torque curve and throttle. Also sets engine-braking magnitude. */
|
|
43
|
+
engineForce: number;
|
|
44
|
+
/** Peak service-brake force, N; still modulated by the existing ABS logic. */
|
|
45
|
+
brakeForce: number;
|
|
46
|
+
/** Peak tire friction coefficient μ on an ideal surface (~0.9 street, 1.1 sport, 0.7 bus). */
|
|
47
|
+
tireGrip: number;
|
|
48
|
+
/** Center-of-mass height, m (0.45 sports car … 1.4 bus); drives weight transfer and body lean. */
|
|
49
|
+
comHeight: number;
|
|
50
|
+
/** Lateral wheel spacing (track width), m; larger resists lateral weight transfer. */
|
|
51
|
+
trackWidth: number;
|
|
52
|
+
}
|
|
53
|
+
/** Data-first gearbox and torque-curve tuning for a kinematic ground vehicle. */
|
|
54
|
+
export interface KinematicPowertrainTuning {
|
|
55
|
+
idleRpm: number;
|
|
56
|
+
redlineRpm: number;
|
|
57
|
+
shiftUpRpm: number;
|
|
58
|
+
shiftDownRpm: number;
|
|
59
|
+
shiftSeconds: number;
|
|
60
|
+
finalDrive: number;
|
|
61
|
+
wheelRadius: number;
|
|
62
|
+
gears: readonly number[];
|
|
63
|
+
torqueCurve: GripCurve;
|
|
64
|
+
}
|
|
65
|
+
/** Bicycle-model steering settings; all angles are radians. */
|
|
66
|
+
export interface KinematicSteeringTuning {
|
|
67
|
+
wheelbase: number;
|
|
68
|
+
maxAngle: number;
|
|
69
|
+
highSpeedAngle: number;
|
|
70
|
+
highSpeedAt: number;
|
|
71
|
+
response: number;
|
|
72
|
+
yawDamping?: number;
|
|
73
|
+
}
|
|
74
|
+
/** Aerodynamic and electronic-assist settings layered over tire grip. */
|
|
75
|
+
export interface KinematicDynamicsTuning {
|
|
76
|
+
aerodynamicDrag?: number;
|
|
77
|
+
downforce?: number;
|
|
78
|
+
tractionControl?: number;
|
|
79
|
+
abs?: number;
|
|
80
|
+
stabilityControl?: number;
|
|
81
|
+
bodyPitchFactor?: number;
|
|
82
|
+
bodyRollFactor?: number;
|
|
83
|
+
maxBodyPitch?: number;
|
|
84
|
+
maxBodyRoll?: number;
|
|
85
|
+
attitudeResponse?: number;
|
|
15
86
|
}
|
|
16
87
|
export interface KinematicVehicleOptions {
|
|
17
88
|
position?: readonly [number, number, number];
|
|
@@ -51,6 +122,22 @@ export interface KinematicVehicleStep {
|
|
|
51
122
|
slip: number;
|
|
52
123
|
/** The `surfaceFriction` sample used this tick (`< 1` = off the ideal surface). */
|
|
53
124
|
surface: number;
|
|
125
|
+
/** One-based forward gear, or `0` while reversing/neutral. */
|
|
126
|
+
gear: number;
|
|
127
|
+
rpm: number;
|
|
128
|
+
steerAngle: number;
|
|
129
|
+
yawRate: number;
|
|
130
|
+
longitudinalAcceleration: number;
|
|
131
|
+
tractionLimited: boolean;
|
|
132
|
+
absActive: boolean;
|
|
133
|
+
/**
|
|
134
|
+
* Demanded drive force exceeded the tire friction budget left after cornering this tick — a
|
|
135
|
+
* traction-limited launch (#1051). Always `false` without a `chassis` block; distinct from
|
|
136
|
+
* `tractionLimited`, which stays owned by the traction-control assist.
|
|
137
|
+
*/
|
|
138
|
+
wheelspin: boolean;
|
|
139
|
+
bodyPitch: number;
|
|
140
|
+
bodyRoll: number;
|
|
54
141
|
}
|
|
55
142
|
/**
|
|
56
143
|
* The pure-kinematic arcade car every racing game hand-rolled (#282.1): steer-yaw scaled by speed,
|
|
@@ -2,6 +2,7 @@ import { steerYaw } from "../movement/steering.js";
|
|
|
2
2
|
import { DEFAULT_GRIP_CURVE, sampleGripCurve } from "./vehicleBody.js";
|
|
3
3
|
export function createKinematicVehicle(tuning, options = {}) {
|
|
4
4
|
const grip = tuning.grip ?? DEFAULT_GRIP_CURVE;
|
|
5
|
+
const chassis = tuning.chassis;
|
|
5
6
|
const rollingResistance = tuning.rollingResistance ?? 0;
|
|
6
7
|
const surfaceFriction = options.surfaceFriction ?? (() => 1);
|
|
7
8
|
const dragAt = options.dragAt ?? (() => 0);
|
|
@@ -12,6 +13,14 @@ export function createKinematicVehicle(tuning, options = {}) {
|
|
|
12
13
|
let heading = options.heading ?? 0;
|
|
13
14
|
let vx = 0;
|
|
14
15
|
let vz = 0;
|
|
16
|
+
let gearIndex = 0;
|
|
17
|
+
let rpm = tuning.powertrain?.idleRpm ?? 0;
|
|
18
|
+
let shiftRemaining = 0;
|
|
19
|
+
let steerAngle = 0;
|
|
20
|
+
let yawRate = 0;
|
|
21
|
+
let previousForwardSpeed = 0;
|
|
22
|
+
let bodyPitch = 0;
|
|
23
|
+
let bodyRoll = 0;
|
|
15
24
|
function forward() {
|
|
16
25
|
return [Math.sin(heading), Math.cos(heading)];
|
|
17
26
|
}
|
|
@@ -22,18 +31,100 @@ export function createKinematicVehicle(tuning, options = {}) {
|
|
|
22
31
|
const turnRate = tuning.turnRate * (modifiers?.turnRateScale ?? 1);
|
|
23
32
|
const [fx0, fz0] = forward();
|
|
24
33
|
const speed = vx * fx0 + vz * fz0;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
34
|
+
if (tuning.steering === undefined) {
|
|
35
|
+
const steerScale = Math.min(1, Math.abs(speed) / tuning.turnSpeedRef);
|
|
36
|
+
const direction = speed >= 0 ? 1 : -1;
|
|
37
|
+
heading = steerYaw(heading, axis.steer * steerScale * direction, turnRate, dt);
|
|
38
|
+
yawRate = dt > 0 ? -axis.steer * steerScale * direction * turnRate : 0;
|
|
39
|
+
steerAngle = axis.steer;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
const steering = tuning.steering;
|
|
43
|
+
const speedBlend = Math.min(1, Math.abs(speed) / Math.max(0.001, steering.highSpeedAt));
|
|
44
|
+
const lock = steering.maxAngle + (steering.highSpeedAngle - steering.maxAngle) * speedBlend;
|
|
45
|
+
const targetAngle = axis.steer * lock * (modifiers?.turnRateScale ?? 1);
|
|
46
|
+
const response = 1 - Math.exp(-Math.max(0, steering.response) * Math.max(0, dt));
|
|
47
|
+
steerAngle += (targetAngle - steerAngle) * response;
|
|
48
|
+
const targetYaw = Math.abs(speed) < 0.05 ? 0 : -(speed / Math.max(0.1, steering.wheelbase)) * Math.tan(steerAngle);
|
|
49
|
+
const yawResponse = 1 - Math.exp(-Math.max(0, steering.yawDamping ?? 10) * Math.max(0, dt));
|
|
50
|
+
yawRate += (targetYaw - yawRate) * yawResponse;
|
|
51
|
+
heading += yawRate * dt;
|
|
52
|
+
}
|
|
28
53
|
const [fx, fz] = forward();
|
|
29
54
|
let accel = 0;
|
|
30
|
-
|
|
31
|
-
|
|
55
|
+
let tractionLimited = false;
|
|
56
|
+
let absActive = false;
|
|
57
|
+
let wheelspin = false;
|
|
58
|
+
// Powertrain state tracks wheel speed EVERY tick (not just under throttle): the engine keeps
|
|
59
|
+
// spinning with the wheels, so rpm, gear and downshifts stay live while coasting or braking. That
|
|
60
|
+
// is what makes engine braking below gear-dependent. Drive torque itself is still applied only on
|
|
61
|
+
// throttle; `gearLeverage` (current ratio / top-gear ratio) is `1` in top gear and grows in the
|
|
62
|
+
// low gears the car downshifts into as it slows.
|
|
63
|
+
let torqueFactor = 1;
|
|
64
|
+
let gearLeverage = 1;
|
|
65
|
+
if (tuning.powertrain !== undefined) {
|
|
66
|
+
const powertrain = tuning.powertrain;
|
|
67
|
+
shiftRemaining = Math.max(0, shiftRemaining - dt);
|
|
68
|
+
const ratio = powertrain.gears[Math.min(gearIndex, powertrain.gears.length - 1)] ?? 1;
|
|
69
|
+
const topGearRatio = powertrain.gears[powertrain.gears.length - 1] ?? ratio;
|
|
70
|
+
gearLeverage = topGearRatio > 0 ? Math.min(ENGINE_BRAKE_MAX_LEVERAGE, ratio / topGearRatio) : 1;
|
|
71
|
+
const wheelRpm = Math.abs(speed) / Math.max(0.01, powertrain.wheelRadius) * 60 / (Math.PI * 2);
|
|
72
|
+
rpm = Math.max(powertrain.idleRpm, Math.min(powertrain.redlineRpm, wheelRpm * ratio * powertrain.finalDrive));
|
|
73
|
+
if (shiftRemaining <= 0 && rpm >= powertrain.shiftUpRpm && gearIndex < powertrain.gears.length - 1) {
|
|
74
|
+
gearIndex += 1;
|
|
75
|
+
shiftRemaining = powertrain.shiftSeconds;
|
|
76
|
+
}
|
|
77
|
+
else if (shiftRemaining <= 0 && rpm <= powertrain.shiftDownRpm && gearIndex > 0) {
|
|
78
|
+
gearIndex -= 1;
|
|
79
|
+
shiftRemaining = powertrain.shiftSeconds;
|
|
80
|
+
}
|
|
81
|
+
const normalizedRpm = (rpm - powertrain.idleRpm) / Math.max(1, powertrain.redlineRpm - powertrain.idleRpm);
|
|
82
|
+
const torque = sampleGripCurve(powertrain.torqueCurve, normalizedRpm);
|
|
83
|
+
torqueFactor = torque * (shiftRemaining > 0 ? 0.32 : 1);
|
|
84
|
+
}
|
|
85
|
+
if (axis.throttle > 0 && speed < topSpeed) {
|
|
86
|
+
accel += chassis === undefined
|
|
87
|
+
? axis.throttle * engineAccel * torqueFactor
|
|
88
|
+
: (axis.throttle * chassis.engineForce * (modifiers?.accelScale ?? 1) * torqueFactor) / chassis.massKg;
|
|
89
|
+
}
|
|
32
90
|
if (axis.brake > 0) {
|
|
33
|
-
if (speed > 0.2)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
91
|
+
if (speed > 0.2) {
|
|
92
|
+
const abs = tuning.dynamics?.abs ?? 0;
|
|
93
|
+
const brakeScale = Math.max(0.15, 1 - abs * Math.max(0, Math.abs(speed) / Math.max(1, topSpeed) - 0.72));
|
|
94
|
+
absActive = brakeScale < 0.999;
|
|
95
|
+
accel -= chassis === undefined
|
|
96
|
+
? axis.brake * tuning.brakeAccel * brakeScale
|
|
97
|
+
: (axis.brake * chassis.brakeForce * brakeScale) / chassis.massKg;
|
|
98
|
+
}
|
|
99
|
+
else if (speed > -tuning.reverseSpeed) {
|
|
100
|
+
accel -= chassis === undefined
|
|
101
|
+
? axis.brake * engineAccel
|
|
102
|
+
: (axis.brake * chassis.engineForce * (modifiers?.accelScale ?? 1)) / chassis.massKg;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const traction = tuning.dynamics?.tractionControl ?? 0;
|
|
106
|
+
if (traction > 0 && Math.abs(speed) > 0.5) {
|
|
107
|
+
const lateral = Math.abs(-vx * fz + vz * fx);
|
|
108
|
+
const excess = Math.max(0, lateral / (Math.abs(speed) + 1) - 0.18);
|
|
109
|
+
if (excess > 0) {
|
|
110
|
+
accel *= Math.max(0.2, 1 - excess * traction * 3);
|
|
111
|
+
tractionLimited = true;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Coast resistance for the mass-and-force chassis (#1051) — derived from the vehicle's own weight,
|
|
115
|
+
// gravity, and gearing, never a flat "brake force" constant. Two weight-based terms:
|
|
116
|
+
// • rolling resistance F = μ_rr · m · g (always, the road-load floor)
|
|
117
|
+
// • engine braking F = μ_eb · m · g · gearLeverage (throttle lifted, in gear)
|
|
118
|
+
// Engine braking is routed through the CURRENT gear, so it strengthens as the car downshifts toward
|
|
119
|
+
// a stop — exactly like lifting off in a real drivetrain. Both are forces (∝ m·g) whose per-mass
|
|
120
|
+
// deceleration is weight-independent and gear-scaled; the per-tick decel is capped so coasting can
|
|
121
|
+
// never push the car backwards.
|
|
122
|
+
if (chassis !== undefined && dt > 0 && Math.abs(speed) > 1e-4) {
|
|
123
|
+
let resistDecel = ROLLING_RESISTANCE_COEFFICIENT * GRAVITY;
|
|
124
|
+
if (axis.throttle <= 0 && tuning.powertrain !== undefined) {
|
|
125
|
+
resistDecel += ENGINE_BRAKE_COEFFICIENT * GRAVITY * gearLeverage;
|
|
126
|
+
}
|
|
127
|
+
accel -= Math.sign(speed) * Math.min(resistDecel, Math.abs(speed) / dt);
|
|
37
128
|
}
|
|
38
129
|
vx += fx * accel * dt;
|
|
39
130
|
vz += fz * accel * dt;
|
|
@@ -42,11 +133,41 @@ export function createKinematicVehicle(tuning, options = {}) {
|
|
|
42
133
|
const surface = surfaceFriction(x, z);
|
|
43
134
|
const slip = Math.abs(lateralSpeed) / (Math.abs(forwardSpeed) + 1);
|
|
44
135
|
const handbrakeFactor = 1 - axis.handbrake * (1 - tuning.handbrakeGrip);
|
|
45
|
-
const
|
|
136
|
+
const downforce = 1 + (tuning.dynamics?.downforce ?? 0) * Math.min(1, Math.abs(forwardSpeed) / Math.max(1, topSpeed));
|
|
137
|
+
const gripValue = sampleGripCurve(grip, slip) * surface * handbrakeFactor * downforce;
|
|
46
138
|
const keep = Math.max(0, 1 - gripValue * tuning.gripStrength * dt);
|
|
47
|
-
const
|
|
139
|
+
const stability = tuning.dynamics?.stabilityControl ?? 0;
|
|
140
|
+
let keptLateral = lateralSpeed * keep * Math.max(0, 1 - stability * Math.abs(axis.steer) * dt);
|
|
141
|
+
if (chassis !== undefined) {
|
|
142
|
+
// Friction budget (#1051): total tire force F = μeff * m * g, with μeff = tireGrip * gripValue
|
|
143
|
+
// (grip curve * surface * handbrake * downforce). Lateral load transfer (|centripetal accel| *
|
|
144
|
+
// comHeight / (trackWidth * g), capped ~0.6) shrinks the lateral share so tall/narrow chassis wash
|
|
145
|
+
// out earlier. Lateral consumes its capped share first; only the remainder feeds drive/brake.
|
|
146
|
+
const capacity = chassis.tireGrip * gripValue * chassis.massKg * GRAVITY;
|
|
147
|
+
const transfer = Math.min(0.6, (Math.abs(yawRate * forwardSpeed) * chassis.comHeight) / (Math.max(0.1, chassis.trackWidth) * GRAVITY));
|
|
148
|
+
const lateralCapacity = capacity * (1 - 0.5 * transfer);
|
|
149
|
+
const reductionWanted = lateralSpeed - keptLateral;
|
|
150
|
+
const maxReduction = dt > 0 ? (lateralCapacity * dt) / chassis.massKg : 0;
|
|
151
|
+
let reductionActual = reductionWanted;
|
|
152
|
+
if (Math.abs(reductionWanted) > maxReduction) {
|
|
153
|
+
reductionActual = Math.sign(reductionWanted) * maxReduction;
|
|
154
|
+
keptLateral = lateralSpeed - reductionActual;
|
|
155
|
+
}
|
|
156
|
+
const lateralForceUsed = dt > 0 ? (chassis.massKg * Math.abs(reductionActual)) / dt : 0;
|
|
157
|
+
const remainingLong = Math.max(0, capacity - lateralForceUsed);
|
|
158
|
+
if (chassis.massKg * Math.abs(accel) > remainingLong) {
|
|
159
|
+
const actualAccel = (Math.sign(accel) * remainingLong) / chassis.massKg;
|
|
160
|
+
forwardSpeed -= (accel - actualAccel) * dt;
|
|
161
|
+
if (accel > 0 && axis.throttle > 0)
|
|
162
|
+
wheelspin = true;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
48
165
|
if (rollingResistance > 0)
|
|
49
166
|
forwardSpeed *= Math.max(0, 1 - rollingResistance * dt);
|
|
167
|
+
const aerodynamicDrag = tuning.dynamics?.aerodynamicDrag ?? 0;
|
|
168
|
+
if (aerodynamicDrag > 0) {
|
|
169
|
+
forwardSpeed *= 1 / (1 + aerodynamicDrag * Math.abs(forwardSpeed) * dt);
|
|
170
|
+
}
|
|
50
171
|
vx = fx * forwardSpeed - fz * keptLateral;
|
|
51
172
|
vz = fz * forwardSpeed + fx * keptLateral;
|
|
52
173
|
const drag = dragAt(x, z);
|
|
@@ -69,6 +190,28 @@ export function createKinematicVehicle(tuning, options = {}) {
|
|
|
69
190
|
x = toX;
|
|
70
191
|
z = toZ;
|
|
71
192
|
}
|
|
193
|
+
// Rest snap (#1051): parked with no throttle/brake/handbrake and sub-threshold motion → exact zero,
|
|
194
|
+
// killing residual creep and jitter. Zeroing previousForwardSpeed keeps the stop free of a pitch spike.
|
|
195
|
+
if (chassis !== undefined &&
|
|
196
|
+
axis.throttle <= 0 &&
|
|
197
|
+
axis.brake <= 0 &&
|
|
198
|
+
axis.handbrake <= 0 &&
|
|
199
|
+
Math.abs(vx * fx + vz * fz) < 0.2 &&
|
|
200
|
+
Math.abs(-vx * fz + vz * fx) < 0.2) {
|
|
201
|
+
vx = 0;
|
|
202
|
+
vz = 0;
|
|
203
|
+
forwardSpeed = 0;
|
|
204
|
+
previousForwardSpeed = 0;
|
|
205
|
+
}
|
|
206
|
+
const longitudinalAcceleration = dt > 0 ? (forwardSpeed - previousForwardSpeed) / dt : 0;
|
|
207
|
+
previousForwardSpeed = forwardSpeed;
|
|
208
|
+
const attitudeResponse = 1 - Math.exp(-Math.max(0, tuning.dynamics?.attitudeResponse ?? 7) * Math.max(0, dt));
|
|
209
|
+
// comHeight scales brake dive / throttle squat and cornering lean (#1051); `1` without a chassis.
|
|
210
|
+
const comScale = chassis !== undefined ? chassis.comHeight : 1;
|
|
211
|
+
const targetPitch = clampSigned(-longitudinalAcceleration * (tuning.dynamics?.bodyPitchFactor ?? 0) * comScale, tuning.dynamics?.maxBodyPitch ?? 0.12);
|
|
212
|
+
const targetRoll = clampSigned(-yawRate * forwardSpeed * (tuning.dynamics?.bodyRollFactor ?? 0) * comScale, tuning.dynamics?.maxBodyRoll ?? 0.16);
|
|
213
|
+
bodyPitch += (targetPitch - bodyPitch) * attitudeResponse;
|
|
214
|
+
bodyRoll += (targetRoll - bodyRoll) * attitudeResponse;
|
|
72
215
|
return {
|
|
73
216
|
position: [x, y, z],
|
|
74
217
|
heading,
|
|
@@ -76,6 +219,16 @@ export function createKinematicVehicle(tuning, options = {}) {
|
|
|
76
219
|
lateralSpeed: -vx * fz + vz * fx,
|
|
77
220
|
slip,
|
|
78
221
|
surface,
|
|
222
|
+
gear: speed < -0.2 ? 0 : gearIndex + 1,
|
|
223
|
+
rpm,
|
|
224
|
+
steerAngle,
|
|
225
|
+
yawRate,
|
|
226
|
+
longitudinalAcceleration,
|
|
227
|
+
tractionLimited,
|
|
228
|
+
absActive,
|
|
229
|
+
wheelspin,
|
|
230
|
+
bodyPitch,
|
|
231
|
+
bodyRoll,
|
|
79
232
|
};
|
|
80
233
|
},
|
|
81
234
|
pose: () => ({ position: [x, y, z], heading }),
|
|
@@ -91,6 +244,35 @@ export function createKinematicVehicle(tuning, options = {}) {
|
|
|
91
244
|
heading = nextHeading;
|
|
92
245
|
vx = 0;
|
|
93
246
|
vz = 0;
|
|
247
|
+
gearIndex = 0;
|
|
248
|
+
rpm = tuning.powertrain?.idleRpm ?? 0;
|
|
249
|
+
shiftRemaining = 0;
|
|
250
|
+
steerAngle = 0;
|
|
251
|
+
yawRate = 0;
|
|
252
|
+
previousForwardSpeed = 0;
|
|
253
|
+
bodyPitch = 0;
|
|
254
|
+
bodyRoll = 0;
|
|
94
255
|
},
|
|
95
256
|
};
|
|
96
257
|
}
|
|
258
|
+
/** Gravitational acceleration, m/s², for the chassis friction budget and weight-transfer ratio (#1051). */
|
|
259
|
+
const GRAVITY = 9.81;
|
|
260
|
+
/**
|
|
261
|
+
* Tyre rolling-resistance coefficient μ_rr (#1051). The always-present coast force is `μ_rr · m · g`
|
|
262
|
+
* (the road-load floor) — a little high vs. a real ~0.013 tyre so a lifted-off car settles to a stop
|
|
263
|
+
* in a game-reasonable distance rather than gliding for tens of seconds.
|
|
264
|
+
*/
|
|
265
|
+
const ROLLING_RESISTANCE_COEFFICIENT = 0.03;
|
|
266
|
+
/**
|
|
267
|
+
* Engine-braking drag as a weight fraction (#1051): the lift-off force is `μ_eb · m · g` routed through
|
|
268
|
+
* the current gear (`ratio / top-gear ratio`, capped by {@link ENGINE_BRAKE_MAX_LEVERAGE}). Like rolling
|
|
269
|
+
* resistance it scales with the vehicle's own weight and gravity — not a per-car constant — so every car
|
|
270
|
+
* settles with a consistent, firm lift-off deceleration that grows as it downshifts toward a stop, while
|
|
271
|
+
* always staying well short of the service brakes.
|
|
272
|
+
*/
|
|
273
|
+
const ENGINE_BRAKE_COEFFICIENT = 0.11;
|
|
274
|
+
/** Cap on low-gear engine-braking leverage so lifting off never bites as hard as the brake pedal. */
|
|
275
|
+
const ENGINE_BRAKE_MAX_LEVERAGE = 2.5;
|
|
276
|
+
function clampSigned(value, magnitude) {
|
|
277
|
+
return Math.max(-Math.abs(magnitude), Math.min(Math.abs(magnitude), value));
|
|
278
|
+
}
|
|
@@ -199,6 +199,7 @@ export declare class PhysicsWorld {
|
|
|
199
199
|
private jFreeCount;
|
|
200
200
|
private jointHigh;
|
|
201
201
|
private jointActiveCount;
|
|
202
|
+
private readonly anchorScratch;
|
|
202
203
|
private readonly bodyFree;
|
|
203
204
|
private bodyFreeCount;
|
|
204
205
|
private bodyHigh;
|
|
@@ -238,6 +239,13 @@ export declare class PhysicsWorld {
|
|
|
238
239
|
/** Move a joint's local anchor offset on A (world axes, no rotation applied by the solver). */
|
|
239
240
|
setJointAnchorA(id: number, x: number, y: number, z: number): void;
|
|
240
241
|
setJointRest(id: number, restLength: number): void;
|
|
242
|
+
/**
|
|
243
|
+
* Gather joint `id`'s two world anchor points into {@link anchorScratch} as [ax,ay,az,bx,by,bz]
|
|
244
|
+
* (A's local offset added to body A's centre, or world-origin when A is unset; B likewise, treating
|
|
245
|
+
* `anchorB` as a world point when B is unset). The single definition shared by {@link jointDistance},
|
|
246
|
+
* {@link readJointSegments}, and {@link solveJoints}.
|
|
247
|
+
*/
|
|
248
|
+
private anchorWorld;
|
|
241
249
|
private jointDistance;
|
|
242
250
|
/** Write [ax,ay,az,bx,by,bz] world endpoints per active joint into `out`; returns the joint count. */
|
|
243
251
|
readJointSegments(out: Float32Array): number;
|
|
@@ -268,10 +276,28 @@ export declare class PhysicsWorld {
|
|
|
268
276
|
getStats(): PhysicsStats;
|
|
269
277
|
private substep;
|
|
270
278
|
private constrainBounds;
|
|
279
|
+
/**
|
|
280
|
+
* Walk every broadphase cell body `i`'s AABB overlaps, invoking `visit(cell)` once per cell. The
|
|
281
|
+
* cell-range math (six `cellCoord` calls plus the row-major triple loop) lives here so the count
|
|
282
|
+
* and scatter passes of {@link buildGrid} share one definition. `visit` is a caller-hoisted closure
|
|
283
|
+
* (created once per `buildGrid`, not per body) so the hot loop stays allocation-free.
|
|
284
|
+
*/
|
|
285
|
+
private forEachBodyCell;
|
|
271
286
|
private buildGrid;
|
|
272
287
|
private cellOf;
|
|
273
288
|
private solve;
|
|
274
289
|
private resolve;
|
|
290
|
+
/**
|
|
291
|
+
* Apply an axial velocity impulse `jn` along unit axis (nX,nY,nZ) to a joint's two bodies: body A
|
|
292
|
+
* recoils (−) and body B advances (+), each scaled by its inverse mass. A world anchor (body index
|
|
293
|
+
* < 0) is skipped. Scalar args only — no allocation on the joint hot path.
|
|
294
|
+
*/
|
|
295
|
+
private applyAxialImpulse;
|
|
296
|
+
/**
|
|
297
|
+
* Positional (Baumgarte) twin of {@link applyAxialImpulse}: nudge the joint's two bodies along
|
|
298
|
+
* (nX,nY,nZ) by `corr` — body A advances (+), body B recoils (−), each scaled by its inverse mass.
|
|
299
|
+
*/
|
|
300
|
+
private applyAxialCorrection;
|
|
275
301
|
private solveJoints;
|
|
276
302
|
private updateSleep;
|
|
277
303
|
}
|