@jgengine/core 0.11.0 → 0.13.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 +52 -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/effects.d.ts +11 -4
- package/dist/combat/effects.js +24 -17
- package/dist/combat/magazine.d.ts +13 -0
- package/dist/combat/magazine.js +27 -0
- package/dist/combat/vfxPresets.d.ts +176 -0
- package/dist/combat/vfxPresets.js +74 -0
- package/dist/combat/weaponFire.d.ts +114 -0
- package/dist/combat/weaponFire.js +96 -0
- package/dist/combat.d.ts +5 -2
- package/dist/combat.js +4 -1
- 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/devtools/urlFlags.d.ts +22 -0
- package/dist/devtools/urlFlags.js +62 -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/clipRoles.d.ts +51 -0
- package/dist/game/clipRoles.js +142 -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/partAnimation.d.ts +78 -0
- package/dist/game/partAnimation.js +147 -0
- package/dist/game/playableGame.d.ts +72 -440
- package/dist/game/playableGame.js +1 -1
- package/dist/game/progression.d.ts +35 -0
- package/dist/game/progression.js +22 -1
- 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/inventory/inventoryModel.d.ts +24 -8
- package/dist/inventory/inventoryModel.js +47 -8
- package/dist/item/durability.js +1 -3
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +48 -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 +131 -0
- package/dist/runtime/context/combatFx.js +15 -5
- 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 +60 -0
- package/dist/runtime/context/scene.js +329 -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 +133 -500
- package/dist/runtime/gameContextTypes.d.ts +500 -0
- package/dist/runtime/gameContextTypes.js +1 -0
- package/dist/runtime/headlessRunner.d.ts +17 -2
- package/dist/runtime/headlessRunner.js +15 -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 +7 -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 +79 -0
- package/dist/scene/colliders.js +170 -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 +19 -15
- 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/statModifiers.d.ts +16 -0
- package/dist/stats/statModifiers.js +30 -1
- package/dist/stats/statPool.d.ts +70 -0
- package/dist/stats/statPool.js +62 -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/ui/panelModel.d.ts +120 -0
- package/dist/ui/panelModel.js +168 -0
- package/dist/ui.d.ts +1 -0
- package/dist/ui.js +1 -0
- 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/buildingLots.d.ts +84 -0
- package/dist/world/buildingLots.js +119 -0
- package/dist/world/buildings.d.ts +6 -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/cityGenerator.d.ts +41 -0
- package/dist/world/cityGenerator.js +58 -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/environmentSummary.js +57 -1
- package/dist/world/features.d.ts +53 -5
- package/dist/world/features.js +15 -3
- 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/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/roads.d.ts +19 -2
- package/dist/world/roads.js +60 -3
- package/dist/world/scatter.js +2 -20
- package/dist/world/streetGenerator.d.ts +159 -0
- package/dist/world/streetGenerator.js +684 -0
- package/dist/world/streets.d.ts +103 -1
- package/dist/world/streets.js +273 -0
- package/dist/world/terraform.js +58 -88
- package/dist/world/terrain.d.ts +65 -1
- package/dist/world/terrain.js +119 -2
- 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 +8 -4
- package/skills/jgengine/api.md +182 -81
- package/skills/jgengine/capabilities.md +104 -2
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-combat/SKILL.md +19 -1
- package/skills/jgengine-combat/api.md +75 -2
- package/skills/jgengine-combat/capabilities.md +61 -1
- package/skills/jgengine-combat/recipes/portable-damage-effects.md +197 -0
- package/skills/jgengine-combat/recipes/portable-runtime-state.md +154 -0
- package/skills/jgengine-combat/recipes/portable-stat-pools.md +122 -0
- package/skills/jgengine-combat/recipes/portable-weapon-plumbing.md +156 -0
- package/skills/jgengine-combat/recipes/visible-attacks.md +81 -0
- package/skills/jgengine-combat/reference.md +33 -4
- package/skills/jgengine-gameplay/SKILL.md +8 -0
- package/skills/jgengine-gameplay/api.md +182 -26
- package/skills/jgengine-gameplay/capabilities.md +96 -2
- package/skills/jgengine-gameplay/recipes/portable-xp-leveling.md +168 -0
- package/skills/jgengine-gameplay/reference-systems.md +10 -5
- package/skills/jgengine-world/SKILL.md +13 -0
- package/skills/jgengine-world/api.md +453 -21
- package/skills/jgengine-world/capabilities.md +112 -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
|
@@ -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
|
}
|
|
@@ -95,6 +95,9 @@ export class PhysicsWorld {
|
|
|
95
95
|
jFreeCount = 0;
|
|
96
96
|
jointHigh = 0;
|
|
97
97
|
jointActiveCount = 0;
|
|
98
|
+
// Reused scratch for a joint's two world anchor points [ax,ay,az,bx,by,bz]; filled by anchorWorld
|
|
99
|
+
// so the gather is defined once and no per-call array is allocated on the joint hot path.
|
|
100
|
+
anchorScratch = new Float64Array(6);
|
|
98
101
|
// Removed bodies tombstone (FLAG_DEAD) rather than compact: `id` is the SoA slot itself, handed
|
|
99
102
|
// out to callers and stored raw in joints/game state, so a slot can never move once allocated.
|
|
100
103
|
// `bodyHigh` is the high-water mark of ever-used slots; `bodyFree` recycles dead ones for addBody.
|
|
@@ -313,18 +316,29 @@ export class PhysicsWorld {
|
|
|
313
316
|
setJointRest(id, restLength) {
|
|
314
317
|
this.jRest[id] = restLength;
|
|
315
318
|
}
|
|
316
|
-
|
|
319
|
+
/**
|
|
320
|
+
* Gather joint `id`'s two world anchor points into {@link anchorScratch} as [ax,ay,az,bx,by,bz]
|
|
321
|
+
* (A's local offset added to body A's centre, or world-origin when A is unset; B likewise, treating
|
|
322
|
+
* `anchorB` as a world point when B is unset). The single definition shared by {@link jointDistance},
|
|
323
|
+
* {@link readJointSegments}, and {@link solveJoints}.
|
|
324
|
+
*/
|
|
325
|
+
anchorWorld(id) {
|
|
326
|
+
const s = this.anchorScratch;
|
|
317
327
|
const bA = this.jBodyA[id];
|
|
318
328
|
const bB = this.jBodyB[id];
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
329
|
+
s[0] = (bA >= 0 ? this.posX[bA] : 0) + this.jAnchorAX[id];
|
|
330
|
+
s[1] = (bA >= 0 ? this.posY[bA] : 0) + this.jAnchorAY[id];
|
|
331
|
+
s[2] = (bA >= 0 ? this.posZ[bA] : 0) + this.jAnchorAZ[id];
|
|
332
|
+
s[3] = bB >= 0 ? this.posX[bB] + this.jAnchorBX[id] : this.jAnchorBX[id];
|
|
333
|
+
s[4] = bB >= 0 ? this.posY[bB] + this.jAnchorBY[id] : this.jAnchorBY[id];
|
|
334
|
+
s[5] = bB >= 0 ? this.posZ[bB] + this.jAnchorBZ[id] : this.jAnchorBZ[id];
|
|
335
|
+
}
|
|
336
|
+
jointDistance(id) {
|
|
337
|
+
this.anchorWorld(id);
|
|
338
|
+
const s = this.anchorScratch;
|
|
339
|
+
const dx = s[3] - s[0];
|
|
340
|
+
const dy = s[4] - s[1];
|
|
341
|
+
const dz = s[5] - s[2];
|
|
328
342
|
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
329
343
|
}
|
|
330
344
|
/** Write [ax,ay,az,bx,by,bz] world endpoints per active joint into `out`; returns the joint count. */
|
|
@@ -333,15 +347,15 @@ export class PhysicsWorld {
|
|
|
333
347
|
for (let id = 0; id < this.jointHigh; id += 1) {
|
|
334
348
|
if (this.jActive[id] !== 1)
|
|
335
349
|
continue;
|
|
336
|
-
|
|
337
|
-
const
|
|
350
|
+
this.anchorWorld(id);
|
|
351
|
+
const s = this.anchorScratch;
|
|
338
352
|
const o = n * 6;
|
|
339
|
-
out[o] =
|
|
340
|
-
out[o + 1] =
|
|
341
|
-
out[o + 2] =
|
|
342
|
-
out[o + 3] =
|
|
343
|
-
out[o + 4] =
|
|
344
|
-
out[o + 5] =
|
|
353
|
+
out[o] = s[0];
|
|
354
|
+
out[o + 1] = s[1];
|
|
355
|
+
out[o + 2] = s[2];
|
|
356
|
+
out[o + 3] = s[3];
|
|
357
|
+
out[o + 4] = s[4];
|
|
358
|
+
out[o + 5] = s[5];
|
|
345
359
|
n += 1;
|
|
346
360
|
}
|
|
347
361
|
return n;
|
|
@@ -609,6 +623,29 @@ export class PhysicsWorld {
|
|
|
609
623
|
this.velY[i] = this.velY[i] < rt ? 0 : -this.velY[i] * e;
|
|
610
624
|
}
|
|
611
625
|
}
|
|
626
|
+
/**
|
|
627
|
+
* Walk every broadphase cell body `i`'s AABB overlaps, invoking `visit(cell)` once per cell. The
|
|
628
|
+
* cell-range math (six `cellCoord` calls plus the row-major triple loop) lives here so the count
|
|
629
|
+
* and scatter passes of {@link buildGrid} share one definition. `visit` is a caller-hoisted closure
|
|
630
|
+
* (created once per `buildGrid`, not per body) so the hot loop stays allocation-free.
|
|
631
|
+
*/
|
|
632
|
+
forEachBodyCell(i, visit) {
|
|
633
|
+
const nx = this.nx;
|
|
634
|
+
const ny = this.ny;
|
|
635
|
+
const x0 = cellCoord(this.posX[i] - this.halfX[i], this.bounds.min[0], this.cellSize, this.nx);
|
|
636
|
+
const x1 = cellCoord(this.posX[i] + this.halfX[i], this.bounds.min[0], this.cellSize, this.nx);
|
|
637
|
+
const y0 = cellCoord(this.posY[i] - this.halfY[i], this.bounds.min[1], this.cellSize, this.ny);
|
|
638
|
+
const y1 = cellCoord(this.posY[i] + this.halfY[i], this.bounds.min[1], this.cellSize, this.ny);
|
|
639
|
+
const z0 = cellCoord(this.posZ[i] - this.halfZ[i], this.bounds.min[2], this.cellSize, this.nz);
|
|
640
|
+
const z1 = cellCoord(this.posZ[i] + this.halfZ[i], this.bounds.min[2], this.cellSize, this.nz);
|
|
641
|
+
for (let z = z0; z <= z1; z += 1) {
|
|
642
|
+
for (let y = y0; y <= y1; y += 1) {
|
|
643
|
+
const rowBase = (z * ny + y) * nx;
|
|
644
|
+
for (let x = x0; x <= x1; x += 1)
|
|
645
|
+
visit(rowBase + x);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
612
649
|
buildGrid() {
|
|
613
650
|
const n = this.bodyHigh;
|
|
614
651
|
const start = this.cellStart;
|
|
@@ -618,24 +655,13 @@ export class PhysicsWorld {
|
|
|
618
655
|
const i = this.awakeList[a];
|
|
619
656
|
this.bodyCell[i] = this.cellOf(i);
|
|
620
657
|
}
|
|
621
|
-
const
|
|
622
|
-
|
|
658
|
+
const countCell = (cell) => {
|
|
659
|
+
start[cell + 1] += 1;
|
|
660
|
+
};
|
|
623
661
|
for (let i = 0; i < n; i += 1) {
|
|
624
662
|
if ((this.flags[i] & FLAG_DEAD) !== 0)
|
|
625
663
|
continue;
|
|
626
|
-
|
|
627
|
-
const x1 = cellCoord(this.posX[i] + this.halfX[i], this.bounds.min[0], this.cellSize, this.nx);
|
|
628
|
-
const y0 = cellCoord(this.posY[i] - this.halfY[i], this.bounds.min[1], this.cellSize, this.ny);
|
|
629
|
-
const y1 = cellCoord(this.posY[i] + this.halfY[i], this.bounds.min[1], this.cellSize, this.ny);
|
|
630
|
-
const z0 = cellCoord(this.posZ[i] - this.halfZ[i], this.bounds.min[2], this.cellSize, this.nz);
|
|
631
|
-
const z1 = cellCoord(this.posZ[i] + this.halfZ[i], this.bounds.min[2], this.cellSize, this.nz);
|
|
632
|
-
for (let z = z0; z <= z1; z += 1) {
|
|
633
|
-
for (let y = y0; y <= y1; y += 1) {
|
|
634
|
-
const rowBase = (z * ny + y) * nx;
|
|
635
|
-
for (let x = x0; x <= x1; x += 1)
|
|
636
|
-
start[rowBase + x + 1] += 1;
|
|
637
|
-
}
|
|
638
|
-
}
|
|
664
|
+
this.forEachBodyCell(i, countCell);
|
|
639
665
|
}
|
|
640
666
|
for (let c = 0; c < this.numCells; c += 1) {
|
|
641
667
|
start[c + 1] += start[c];
|
|
@@ -645,24 +671,15 @@ export class PhysicsWorld {
|
|
|
645
671
|
if (total > this.sorted.length) {
|
|
646
672
|
this.sorted = new Int32Array(Math.max(total, this.sorted.length * 2));
|
|
647
673
|
}
|
|
674
|
+
let scatterBody = 0;
|
|
675
|
+
const scatterCell = (cell) => {
|
|
676
|
+
this.sorted[this.cursor[cell]++] = scatterBody;
|
|
677
|
+
};
|
|
648
678
|
for (let i = 0; i < n; i += 1) {
|
|
649
679
|
if ((this.flags[i] & FLAG_DEAD) !== 0)
|
|
650
680
|
continue;
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
const y0 = cellCoord(this.posY[i] - this.halfY[i], this.bounds.min[1], this.cellSize, this.ny);
|
|
654
|
-
const y1 = cellCoord(this.posY[i] + this.halfY[i], this.bounds.min[1], this.cellSize, this.ny);
|
|
655
|
-
const z0 = cellCoord(this.posZ[i] - this.halfZ[i], this.bounds.min[2], this.cellSize, this.nz);
|
|
656
|
-
const z1 = cellCoord(this.posZ[i] + this.halfZ[i], this.bounds.min[2], this.cellSize, this.nz);
|
|
657
|
-
for (let z = z0; z <= z1; z += 1) {
|
|
658
|
-
for (let y = y0; y <= y1; y += 1) {
|
|
659
|
-
const rowBase = (z * ny + y) * nx;
|
|
660
|
-
for (let x = x0; x <= x1; x += 1) {
|
|
661
|
-
const c = rowBase + x;
|
|
662
|
-
this.sorted[this.cursor[c]++] = i;
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
}
|
|
681
|
+
scatterBody = i;
|
|
682
|
+
this.forEachBodyCell(i, scatterCell);
|
|
666
683
|
}
|
|
667
684
|
}
|
|
668
685
|
cellOf(i) {
|
|
@@ -867,6 +884,39 @@ export class PhysicsWorld {
|
|
|
867
884
|
this.posY[j] += corr * imJ * nY;
|
|
868
885
|
this.posZ[j] += corr * imJ * nZ;
|
|
869
886
|
}
|
|
887
|
+
/**
|
|
888
|
+
* Apply an axial velocity impulse `jn` along unit axis (nX,nY,nZ) to a joint's two bodies: body A
|
|
889
|
+
* recoils (−) and body B advances (+), each scaled by its inverse mass. A world anchor (body index
|
|
890
|
+
* < 0) is skipped. Scalar args only — no allocation on the joint hot path.
|
|
891
|
+
*/
|
|
892
|
+
applyAxialImpulse(bA, bB, imA, imB, jn, nX, nY, nZ) {
|
|
893
|
+
if (bA >= 0) {
|
|
894
|
+
this.velX[bA] -= jn * imA * nX;
|
|
895
|
+
this.velY[bA] -= jn * imA * nY;
|
|
896
|
+
this.velZ[bA] -= jn * imA * nZ;
|
|
897
|
+
}
|
|
898
|
+
if (bB >= 0) {
|
|
899
|
+
this.velX[bB] += jn * imB * nX;
|
|
900
|
+
this.velY[bB] += jn * imB * nY;
|
|
901
|
+
this.velZ[bB] += jn * imB * nZ;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Positional (Baumgarte) twin of {@link applyAxialImpulse}: nudge the joint's two bodies along
|
|
906
|
+
* (nX,nY,nZ) by `corr` — body A advances (+), body B recoils (−), each scaled by its inverse mass.
|
|
907
|
+
*/
|
|
908
|
+
applyAxialCorrection(bA, bB, imA, imB, corr, nX, nY, nZ) {
|
|
909
|
+
if (bA >= 0) {
|
|
910
|
+
this.posX[bA] += corr * imA * nX;
|
|
911
|
+
this.posY[bA] += corr * imA * nY;
|
|
912
|
+
this.posZ[bA] += corr * imA * nZ;
|
|
913
|
+
}
|
|
914
|
+
if (bB >= 0) {
|
|
915
|
+
this.posX[bB] -= corr * imB * nX;
|
|
916
|
+
this.posY[bB] -= corr * imB * nY;
|
|
917
|
+
this.posZ[bB] -= corr * imB * nZ;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
870
920
|
solveJoints(dt) {
|
|
871
921
|
const beta = this.jointCorrection;
|
|
872
922
|
const slop = this.slop;
|
|
@@ -876,15 +926,11 @@ export class PhysicsWorld {
|
|
|
876
926
|
const kind = this.jKind[id];
|
|
877
927
|
const bA = this.jBodyA[id];
|
|
878
928
|
const bB = this.jBodyB[id];
|
|
879
|
-
|
|
880
|
-
const
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
const bz = bB >= 0 ? this.posZ[bB] + this.jAnchorBZ[id] : this.jAnchorBZ[id];
|
|
885
|
-
let dx = bx - ax;
|
|
886
|
-
let dy = by - ay;
|
|
887
|
-
let dz = bz - az;
|
|
929
|
+
this.anchorWorld(id);
|
|
930
|
+
const s = this.anchorScratch;
|
|
931
|
+
let dx = s[3] - s[0];
|
|
932
|
+
let dy = s[4] - s[1];
|
|
933
|
+
let dz = s[5] - s[2];
|
|
888
934
|
const imA = bA >= 0 ? this.invMass[bA] : 0;
|
|
889
935
|
const imB = bB >= 0 ? this.invMass[bB] : 0;
|
|
890
936
|
const imSum = imA + imB;
|
|
@@ -924,27 +970,9 @@ export class PhysicsWorld {
|
|
|
924
970
|
}
|
|
925
971
|
const rvn = rvx * nX + rvy * nY + rvz * nZ;
|
|
926
972
|
const jn = -rvn / imSum;
|
|
927
|
-
|
|
928
|
-
this.velX[bA] -= jn * imA * nX;
|
|
929
|
-
this.velY[bA] -= jn * imA * nY;
|
|
930
|
-
this.velZ[bA] -= jn * imA * nZ;
|
|
931
|
-
}
|
|
932
|
-
if (bB >= 0) {
|
|
933
|
-
this.velX[bB] += jn * imB * nX;
|
|
934
|
-
this.velY[bB] += jn * imB * nY;
|
|
935
|
-
this.velZ[bB] += jn * imB * nZ;
|
|
936
|
-
}
|
|
973
|
+
this.applyAxialImpulse(bA, bB, imA, imB, jn, nX, nY, nZ);
|
|
937
974
|
const corr = (dist * beta) / imSum;
|
|
938
|
-
|
|
939
|
-
this.posX[bA] += corr * imA * nX;
|
|
940
|
-
this.posY[bA] += corr * imA * nY;
|
|
941
|
-
this.posZ[bA] += corr * imA * nZ;
|
|
942
|
-
}
|
|
943
|
-
if (bB >= 0) {
|
|
944
|
-
this.posX[bB] -= corr * imB * nX;
|
|
945
|
-
this.posY[bB] -= corr * imB * nY;
|
|
946
|
-
this.posZ[bB] -= corr * imB * nZ;
|
|
947
|
-
}
|
|
975
|
+
this.applyAxialCorrection(bA, bB, imA, imB, corr, nX, nY, nZ);
|
|
948
976
|
}
|
|
949
977
|
else {
|
|
950
978
|
const rvl = Math.sqrt(rvx * rvx + rvy * rvy + rvz * rvz);
|
|
@@ -953,16 +981,7 @@ export class PhysicsWorld {
|
|
|
953
981
|
const nY = rvy / rvl;
|
|
954
982
|
const nZ = rvz / rvl;
|
|
955
983
|
const jn = -rvl / imSum;
|
|
956
|
-
|
|
957
|
-
this.velX[bA] -= jn * imA * nX;
|
|
958
|
-
this.velY[bA] -= jn * imA * nY;
|
|
959
|
-
this.velZ[bA] -= jn * imA * nZ;
|
|
960
|
-
}
|
|
961
|
-
if (bB >= 0) {
|
|
962
|
-
this.velX[bB] += jn * imB * nX;
|
|
963
|
-
this.velY[bB] += jn * imB * nY;
|
|
964
|
-
this.velZ[bB] += jn * imB * nZ;
|
|
965
|
-
}
|
|
984
|
+
this.applyAxialImpulse(bA, bB, imA, imB, jn, nX, nY, nZ);
|
|
966
985
|
}
|
|
967
986
|
}
|
|
968
987
|
continue;
|
|
@@ -994,16 +1013,7 @@ export class PhysicsWorld {
|
|
|
994
1013
|
jn = maxJ;
|
|
995
1014
|
else if (jn < -maxJ)
|
|
996
1015
|
jn = -maxJ;
|
|
997
|
-
|
|
998
|
-
this.velX[bA] -= jn * imA * nX;
|
|
999
|
-
this.velY[bA] -= jn * imA * nY;
|
|
1000
|
-
this.velZ[bA] -= jn * imA * nZ;
|
|
1001
|
-
}
|
|
1002
|
-
if (bB >= 0) {
|
|
1003
|
-
this.velX[bB] += jn * imB * nX;
|
|
1004
|
-
this.velY[bB] += jn * imB * nY;
|
|
1005
|
-
this.velZ[bB] += jn * imB * nZ;
|
|
1006
|
-
}
|
|
1016
|
+
this.applyAxialImpulse(bA, bB, imA, imB, jn, nX, nY, nZ);
|
|
1007
1017
|
continue;
|
|
1008
1018
|
}
|
|
1009
1019
|
if (kind !== JOINT_FIXED && kind !== JOINT_DISTANCE) {
|
|
@@ -1016,27 +1026,9 @@ export class PhysicsWorld {
|
|
|
1016
1026
|
this.wake(bB);
|
|
1017
1027
|
}
|
|
1018
1028
|
const jn = -rvn / imSum;
|
|
1019
|
-
|
|
1020
|
-
this.velX[bA] -= jn * imA * nX;
|
|
1021
|
-
this.velY[bA] -= jn * imA * nY;
|
|
1022
|
-
this.velZ[bA] -= jn * imA * nZ;
|
|
1023
|
-
}
|
|
1024
|
-
if (bB >= 0) {
|
|
1025
|
-
this.velX[bB] += jn * imB * nX;
|
|
1026
|
-
this.velY[bB] += jn * imB * nY;
|
|
1027
|
-
this.velZ[bB] += jn * imB * nZ;
|
|
1028
|
-
}
|
|
1029
|
+
this.applyAxialImpulse(bA, bB, imA, imB, jn, nX, nY, nZ);
|
|
1029
1030
|
const corr = (c * beta) / imSum;
|
|
1030
|
-
|
|
1031
|
-
this.posX[bA] += corr * imA * nX;
|
|
1032
|
-
this.posY[bA] += corr * imA * nY;
|
|
1033
|
-
this.posZ[bA] += corr * imA * nZ;
|
|
1034
|
-
}
|
|
1035
|
-
if (bB >= 0) {
|
|
1036
|
-
this.posX[bB] -= corr * imB * nX;
|
|
1037
|
-
this.posY[bB] -= corr * imB * nY;
|
|
1038
|
-
this.posZ[bB] -= corr * imB * nZ;
|
|
1039
|
-
}
|
|
1031
|
+
this.applyAxialCorrection(bA, bB, imA, imB, corr, nX, nY, nZ);
|
|
1040
1032
|
}
|
|
1041
1033
|
}
|
|
1042
1034
|
updateSleep() {
|