@jgengine/core 0.18.1 → 0.21.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 +504 -7
- package/dist/ai/decisionGraph.d.ts +50 -4
- package/dist/ai/decisionGraph.js +159 -23
- package/dist/ai/flock.d.ts +1 -1
- package/dist/ai/flock.js +7 -4
- package/dist/ai/groupAssist.d.ts +10 -0
- package/dist/ai/groupAssist.js +11 -0
- package/dist/ai/pursuit.d.ts +1 -1
- package/dist/ai/pursuit.js +1 -1
- package/dist/ai/threat.d.ts +8 -0
- package/dist/ai/threat.js +10 -0
- package/dist/anim/animGraph.d.ts +43 -1
- package/dist/anim/animGraph.js +326 -10
- package/dist/anim/boneTexture.d.ts +55 -0
- package/dist/anim/boneTexture.js +43 -0
- package/dist/anim/footPlacement.d.ts +57 -0
- package/dist/anim/footPlacement.js +67 -0
- package/dist/anim/locomotionGraph.d.ts +18 -1
- package/dist/anim/locomotionGraph.js +42 -3
- package/dist/audio/audioFalloff.d.ts +6 -0
- package/dist/audio/doppler.d.ts +21 -0
- package/dist/audio/doppler.js +32 -0
- package/dist/audio/engineLayers.d.ts +83 -0
- package/dist/audio/engineLayers.js +115 -0
- package/dist/audio/voiceAllocator.d.ts +46 -0
- package/dist/audio/voiceAllocator.js +76 -0
- package/dist/combat/abilityKit.d.ts +20 -0
- package/dist/combat/abilityKit.js +41 -4
- package/dist/combat/death.d.ts +11 -3
- package/dist/combat/death.js +3 -3
- package/dist/combat/magazine.d.ts +9 -0
- package/dist/combat/magazine.js +24 -2
- package/dist/combat/projectiles.d.ts +94 -2
- package/dist/combat/projectiles.js +282 -61
- package/dist/combat/regenShield.d.ts +14 -0
- package/dist/combat/regenShield.js +23 -7
- package/dist/combat/shotOrigin.d.ts +6 -0
- package/dist/combat/shotOrigin.js +6 -0
- package/dist/combat/weaponFeedback.d.ts +36 -0
- package/dist/combat/weaponFeedback.js +42 -0
- package/dist/combat/weaponHandling.d.ts +161 -0
- package/dist/combat/weaponHandling.js +202 -0
- package/dist/combat/weaponPresentation.d.ts +115 -0
- package/dist/combat/weaponPresentation.js +96 -0
- package/dist/crafting/recipeCatalog.d.ts +16 -0
- package/dist/crafting/recipeCatalog.js +55 -0
- package/dist/devtools/warnOnce.d.ts +8 -0
- package/dist/devtools/warnOnce.js +18 -0
- package/dist/economy/auctionBook.d.ts +1 -1
- package/dist/economy/auctionBook.js +11 -1
- package/dist/economy/listingBook.d.ts +1 -1
- package/dist/economy/listingBook.js +11 -1
- package/dist/economy/resourceLedger.js +3 -3
- package/dist/economy/shopStock.js +7 -6
- package/dist/economy/techTree.js +7 -6
- package/dist/economy/unlockPoints.js +3 -6
- package/dist/editor/commandApply.js +89 -16
- package/dist/editor/commands.d.ts +38 -1
- package/dist/editor/commands.js +320 -3
- package/dist/editor/creatorStorage.d.ts +59 -0
- package/dist/editor/creatorStorage.js +164 -0
- package/dist/editor/document.d.ts +4 -0
- package/dist/editor/document.js +119 -18
- package/dist/editor/environment.js +8 -0
- package/dist/editor/index.d.ts +2 -0
- package/dist/editor/liveSync.d.ts +4 -1
- package/dist/editor/liveSync.js +26 -10
- package/dist/editor/materialAuthoring.d.ts +14 -0
- package/dist/editor/materialAuthoring.js +38 -0
- package/dist/editor/modelNodeVisibility.d.ts +11 -0
- package/dist/editor/modelNodeVisibility.js +29 -0
- package/dist/editor/movementCatalog.d.ts +90 -0
- package/dist/editor/movementCatalog.js +144 -0
- package/dist/editor/simulation.d.ts +60 -0
- package/dist/editor/simulation.js +202 -0
- package/dist/editor/staticPrefab.d.ts +17 -0
- package/dist/editor/staticPrefab.js +51 -0
- package/dist/editor/types.d.ts +15 -0
- package/dist/faction/reputation.d.ts +6 -0
- package/dist/faction/reputation.js +11 -0
- package/dist/game/cameraConfig.d.ts +54 -2
- package/dist/game/clipRoles.d.ts +2 -1
- package/dist/game/clipRoles.js +7 -3
- package/dist/game/contentValidation.d.ts +69 -0
- package/dist/game/contentValidation.js +118 -0
- package/dist/game/controlGate.d.ts +8 -0
- package/dist/game/controlGate.js +43 -6
- package/dist/game/defineGame.d.ts +32 -3
- package/dist/game/dialogueGraph.d.ts +20 -0
- package/dist/game/dialogueGraph.js +107 -0
- package/dist/game/events.d.ts +11 -0
- package/dist/game/keyValueStore.d.ts +17 -4
- package/dist/game/keyValueStore.js +35 -23
- package/dist/game/lootModifiers.d.ts +17 -0
- package/dist/game/lootModifiers.js +26 -0
- package/dist/game/lootTable.d.ts +4 -0
- package/dist/game/lootTable.js +16 -0
- package/dist/game/narrativeAuthoring.d.ts +67 -0
- package/dist/game/narrativeAuthoring.js +122 -0
- package/dist/game/playableGame.d.ts +134 -35
- package/dist/game/quest.d.ts +15 -0
- package/dist/game/quest.js +209 -63
- package/dist/game/questCatalog.d.ts +50 -0
- package/dist/game/questCatalog.js +205 -0
- package/dist/game/saveStore.d.ts +6 -5
- package/dist/game/saveStore.js +57 -57
- package/dist/game/systemRuntime.d.ts +2 -2
- package/dist/game/systemRuntime.js +84 -33
- package/dist/game/toasts.d.ts +8 -0
- package/dist/game/toasts.js +7 -0
- package/dist/game/vfxInstance.d.ts +9 -0
- package/dist/game/vfxInstance.js +12 -1
- package/dist/game/viewports.d.ts +43 -0
- package/dist/game/viewports.js +46 -0
- package/dist/game/worldItem.d.ts +5 -1
- package/dist/game/worldItem.js +0 -1
- package/dist/gameplay.d.ts +3 -0
- package/dist/gameplay.js +3 -0
- package/dist/input/actionBindings.d.ts +15 -1
- package/dist/input/actionBindings.js +14 -3
- package/dist/input/actionContexts.d.ts +25 -1
- package/dist/input/actionContexts.js +82 -25
- package/dist/input/axisInput.d.ts +4 -0
- package/dist/input/axisInput.js +4 -0
- package/dist/input/axisShaper.d.ts +73 -0
- package/dist/input/axisShaper.js +105 -0
- package/dist/input/gamepadModel.d.ts +36 -2
- package/dist/input/gamepadModel.js +69 -30
- package/dist/input/haptics.d.ts +50 -0
- package/dist/input/haptics.js +74 -0
- package/dist/input/inputBuffer.d.ts +40 -0
- package/dist/input/inputBuffer.js +59 -0
- package/dist/input/lookChannel.d.ts +10 -0
- package/dist/input/lookChannel.js +6 -0
- package/dist/input/pointer.d.ts +2 -0
- package/dist/input/pointer.js +4 -0
- package/dist/interaction/promptRegistry.d.ts +5 -4
- package/dist/interaction/promptRegistry.js +8 -2
- package/dist/inventory/inventoryModel.js +9 -6
- package/dist/inventory/shapedGrid.d.ts +48 -9
- package/dist/inventory/shapedGrid.js +69 -10
- package/dist/item/affix.d.ts +6 -0
- package/dist/item/affix.js +6 -0
- package/dist/item/durability.js +4 -0
- package/dist/item/itemInstanceRegistry.d.ts +32 -1
- package/dist/item/itemInstanceRegistry.js +111 -7
- package/dist/item/itemgen.d.ts +1 -1
- package/dist/item/itemgen.js +1 -1
- package/dist/item/modularItem.js +7 -1
- package/dist/material/appearanceSignals.d.ts +53 -0
- package/dist/material/appearanceSignals.js +47 -0
- package/dist/material/hairCards.d.ts +47 -0
- package/dist/material/hairCards.js +136 -0
- package/dist/material/materialAsset.d.ts +162 -0
- package/dist/material/materialAsset.js +382 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +444 -1
- package/dist/movement/characterController.d.ts +5 -2
- package/dist/movement/characterController.js +50 -23
- package/dist/movement/freeFlight.d.ts +2 -2
- package/dist/movement/freeFlight.js +3 -3
- package/dist/movement/glideModel.d.ts +3 -0
- package/dist/movement/glideModel.js +6 -0
- package/dist/movement/grappleSwing.d.ts +2 -0
- package/dist/movement/grappleSwing.js +5 -1
- package/dist/movement/movementModel.d.ts +63 -1
- package/dist/movement/movementModel.js +136 -26
- package/dist/movement/movementProbe.d.ts +51 -0
- package/dist/movement/movementProbe.js +119 -0
- package/dist/movement/playerMovement.d.ts +48 -2
- package/dist/movement/playerMovement.js +493 -87
- package/dist/movement/poseState.d.ts +2 -0
- package/dist/movement/poseState.js +29 -1
- package/dist/movement/solidObstacles.d.ts +11 -1
- package/dist/movement/solidObstacles.js +48 -3
- package/dist/movement/terrainGrade.d.ts +13 -0
- package/dist/movement/terrainGrade.js +33 -0
- package/dist/movement/voxelController.d.ts +2 -2
- package/dist/movement/voxelController.js +10 -10
- package/dist/nav/navFromEnvironment.d.ts +9 -3
- package/dist/nav/navFromEnvironment.js +18 -12
- package/dist/nav/navGrid.d.ts +19 -1
- package/dist/nav/navGrid.js +26 -10
- package/dist/nav/navMesh.d.ts +43 -2
- package/dist/nav/navMesh.js +534 -128
- package/dist/nav/solidRoute.d.ts +50 -0
- package/dist/nav/solidRoute.js +144 -0
- package/dist/physics/aircraftDynamics.d.ts +390 -0
- package/dist/physics/aircraftDynamics.js +704 -0
- package/dist/physics/ballisticSweep.d.ts +10 -3
- package/dist/physics/ballisticSweep.js +53 -29
- package/dist/physics/boatDynamics.d.ts +116 -0
- package/dist/physics/boatDynamics.js +160 -0
- package/dist/physics/drivableVehicle.d.ts +22 -8
- package/dist/physics/drivableVehicle.js +4 -2
- package/dist/physics/flightDynamics.d.ts +21 -4
- package/dist/physics/flightDynamics.js +28 -5
- package/dist/physics/forceVolume.d.ts +41 -0
- package/dist/physics/forceVolume.js +88 -0
- package/dist/physics/handlingProbe.d.ts +249 -0
- package/dist/physics/handlingProbe.js +606 -0
- package/dist/physics/kinematicVehicle.d.ts +40 -5
- package/dist/physics/kinematicVehicle.js +17 -2
- package/dist/physics/physicsBackend.d.ts +12 -0
- package/dist/physics/physicsBackendConformance.js +34 -0
- package/dist/physics/physicsWorldBackend.d.ts +1 -0
- package/dist/physics/physicsWorldBackend.js +52 -3
- package/dist/physics/spatialGrid.d.ts +1 -1
- package/dist/physics/spatialGrid.js +8 -9
- package/dist/physics/vehicleBackendLink.d.ts +49 -0
- package/dist/physics/vehicleBackendLink.js +102 -0
- package/dist/physics/vehicleDynamics.d.ts +375 -0
- package/dist/physics/vehicleDynamics.js +737 -0
- package/dist/physics/vehicleObstacles.d.ts +3 -2
- package/dist/physics/vehicleObstacles.js +29 -5
- package/dist/physics/worldColliders.d.ts +1 -1
- package/dist/physics/worldColliders.js +35 -3
- package/dist/puzzle/cellGrid.d.ts +15 -9
- package/dist/puzzle/cellGrid.js +13 -9
- package/dist/render/lookPreset.d.ts +9 -2
- package/dist/render/lookPreset.js +29 -2
- package/dist/render/postProcessing.d.ts +30 -0
- package/dist/render/postProcessing.js +1 -1
- package/dist/runtime/cameraDirector.d.ts +54 -3
- package/dist/runtime/cameraDirector.js +187 -0
- package/dist/runtime/commandDispatch.d.ts +14 -0
- package/dist/runtime/commandDispatch.js +19 -0
- package/dist/runtime/commandInput.d.ts +37 -0
- package/dist/runtime/commandInput.js +58 -0
- package/dist/runtime/commandRunner.d.ts +27 -0
- package/dist/runtime/commandRunner.js +31 -3
- package/dist/runtime/context/combat.d.ts +12 -4
- package/dist/runtime/context/combat.js +179 -11
- package/dist/runtime/context/deathLoot.d.ts +11 -8
- package/dist/runtime/context/deathLoot.js +17 -16
- package/dist/runtime/context/scene.d.ts +2 -0
- package/dist/runtime/context/scene.js +13 -21
- package/dist/runtime/descriptors/baseline.js +34 -3
- package/dist/runtime/descriptors/features.d.ts +2 -1
- package/dist/runtime/descriptors/features.js +18 -0
- package/dist/runtime/gameContext.js +86 -13
- package/dist/runtime/gameContextTypes.d.ts +33 -3
- package/dist/runtime/gameRuntime.d.ts +3 -1
- package/dist/runtime/gameRuntime.js +4 -0
- package/dist/runtime/headlessRunner.d.ts +3 -1
- package/dist/runtime/headlessRunner.js +11 -11
- package/dist/runtime/hostedGameRunner.d.ts +24 -1
- package/dist/runtime/hostedGameRunner.js +156 -10
- package/dist/runtime/hostedWorldSession.d.ts +17 -5
- package/dist/runtime/hostedWorldSession.js +154 -16
- package/dist/runtime/hostedWorldStore.d.ts +2 -2
- package/dist/runtime/inputRecorder.d.ts +1 -1
- package/dist/runtime/inputRecorder.js +21 -1
- package/dist/runtime/inputSnapshot.d.ts +22 -4
- package/dist/runtime/inputSnapshot.js +41 -3
- package/dist/runtime/localPlayers.d.ts +73 -0
- package/dist/runtime/localPlayers.js +119 -0
- package/dist/runtime/runtimeSave.d.ts +10 -6
- package/dist/runtime/runtimeSave.js +11 -5
- package/dist/runtime/simContext.d.ts +8 -3
- package/dist/runtime/simContext.js +7 -3
- package/dist/runtime/simSnapshot.js +2 -0
- package/dist/runtime/transport.d.ts +9 -0
- package/dist/runtime/transport.js +7 -1
- package/dist/runtime/worldReplication.d.ts +2 -0
- package/dist/runtime/worldReplication.js +1 -1
- package/dist/scene/assetCatalog.d.ts +2 -0
- package/dist/scene/behaviorRuntime.d.ts +38 -7
- package/dist/scene/behaviorRuntime.js +293 -30
- package/dist/scene/behaviors.d.ts +35 -2
- package/dist/scene/behaviors.js +29 -0
- package/dist/scene/entityStore.d.ts +14 -3
- package/dist/scene/entityStore.js +45 -9
- package/dist/scene/mount.d.ts +9 -0
- package/dist/scene/mount.js +35 -0
- package/dist/scene/possession.d.ts +2 -0
- package/dist/scene/possession.js +21 -1
- package/dist/scene/sceneKinds.d.ts +9 -0
- package/dist/scene/sceneRaycast.d.ts +7 -0
- package/dist/scene/sceneRaycast.js +55 -0
- package/dist/scene/spatial.d.ts +20 -1
- package/dist/scene/spatial.js +94 -2
- package/dist/scene/vehicleSeat.d.ts +41 -3
- package/dist/scene/vehicleSeat.js +94 -3
- package/dist/sensor/concealment.d.ts +6 -0
- package/dist/sensor/concealment.js +8 -0
- package/dist/sensor/freezeMonitor.d.ts +6 -0
- package/dist/sensor/freezeMonitor.js +8 -0
- package/dist/sensor/perception.d.ts +11 -1
- package/dist/sensor/perception.js +57 -10
- package/dist/sensor/recordingBuffer.d.ts +3 -0
- package/dist/sensor/recordingBuffer.js +8 -0
- package/dist/session/roundState.d.ts +4 -0
- package/dist/session/roundState.js +11 -0
- package/dist/settings/frameRateLimit.d.ts +16 -0
- package/dist/settings/frameRateLimit.js +37 -0
- package/dist/settings/graphicsProfile.d.ts +2 -0
- package/dist/settings/graphicsProfile.js +12 -0
- package/dist/settings/graphicsSettings.d.ts +40 -0
- package/dist/settings/graphicsSettings.js +81 -0
- package/dist/settings/settingsModel.d.ts +9 -1
- package/dist/settings/settingsModel.js +10 -0
- package/dist/stats/accumulatorMeter.d.ts +10 -0
- package/dist/stats/accumulatorMeter.js +6 -0
- package/dist/stats/eventMeter.d.ts +10 -3
- package/dist/stats/eventMeter.js +6 -3
- package/dist/store/defineStore.d.ts +3 -1
- package/dist/store/defineStore.js +3 -1
- package/dist/store/observableKeyedStore.d.ts +1 -1
- package/dist/store/observableKeyedStore.js +8 -6
- package/dist/time/simClock.d.ts +6 -2
- package/dist/time/simClock.js +5 -0
- package/dist/vfx/feedbackMixer.d.ts +61 -0
- package/dist/vfx/feedbackMixer.js +106 -0
- package/dist/vfx/particleDirector.d.ts +58 -6
- package/dist/vfx/particleDirector.js +113 -22
- package/dist/vfx/particles.d.ts +68 -5
- package/dist/vfx/particles.js +284 -32
- package/dist/visibility/assetStreaming.d.ts +21 -3
- package/dist/visibility/assetStreaming.js +108 -24
- package/dist/visibility/settings.d.ts +8 -2
- package/dist/visibility/settings.js +3 -0
- package/dist/visibility/simulationCulling.d.ts +8 -1
- package/dist/visibility/simulationCulling.js +7 -0
- package/dist/visibility/spatialIndex.d.ts +21 -0
- package/dist/visibility/spatialIndex.js +16 -0
- package/dist/visibility/visibilitySystem.js +1 -0
- package/dist/work/expedition.d.ts +110 -0
- package/dist/work/expedition.js +195 -0
- package/dist/work/staffedStation.d.ts +29 -0
- package/dist/work/staffedStation.js +27 -0
- package/dist/world/authoredAnimation.d.ts +38 -0
- package/dist/world/authoredAnimation.js +99 -0
- package/dist/world/authoredObjects.d.ts +24 -4
- package/dist/world/authoredObjects.js +43 -16
- package/dist/world/authoredSimulation.d.ts +74 -0
- package/dist/world/authoredSimulation.js +302 -0
- package/dist/world/authoredSolids.d.ts +46 -0
- package/dist/world/authoredSolids.js +75 -0
- package/dist/world/authoredWeather.d.ts +120 -0
- package/dist/world/authoredWeather.js +314 -0
- package/dist/world/buildingGenerator.js +2 -2
- package/dist/world/buildingKit.d.ts +11 -4
- package/dist/world/buildingKit.js +21 -8
- package/dist/world/buildings.d.ts +34 -2
- package/dist/world/buildings.js +11 -1
- package/dist/world/cityKind.d.ts +7 -0
- package/dist/world/cityKind.js +52 -1
- package/dist/world/environmentForces.d.ts +21 -0
- package/dist/world/environmentForces.js +42 -0
- package/dist/world/features.d.ts +32 -5
- package/dist/world/features.js +6 -0
- package/dist/world/footprintGrid.d.ts +62 -0
- package/dist/world/footprintGrid.js +217 -0
- package/dist/world/interiors.d.ts +1 -0
- package/dist/world/interiors.js +5 -3
- package/dist/world/lod.d.ts +6 -0
- package/dist/world/lod.js +8 -0
- package/dist/world/pathTerrain.d.ts +1 -1
- package/dist/world/terraform.d.ts +9 -1
- package/dist/world/terraform.js +53 -18
- package/dist/world/walls.d.ts +7 -0
- package/dist/world/walls.js +7 -0
- package/dist/world/weather.d.ts +17 -0
- package/dist/world/weather.js +116 -20
- package/dist/world/worldSolids.d.ts +95 -0
- package/dist/world/worldSolids.js +351 -0
- package/dist/world.d.ts +6 -6
- package/dist/world.js +5 -5
- package/package.json +1 -1
- package/skills/ce-handoff/SKILL.md +2 -2
- package/skills/game-design/SKILL.md +2 -2
- package/skills/jgengine/SKILL.md +28 -6
- package/skills/jgengine/api.md +139 -99
- package/skills/jgengine/capabilities.md +28 -0
- package/skills/jgengine/recipes/minimal-game.md +10 -2
- package/skills/jgengine-combat/SKILL.md +13 -2
- package/skills/jgengine-combat/api.md +150 -103
- package/skills/jgengine-combat/capabilities.md +29 -1
- package/skills/jgengine-combat/recipes/portable-runtime-state.md +60 -6
- package/skills/jgengine-combat/recipes/portable-weapon-plumbing.md +37 -3
- package/skills/jgengine-combat/recipes/weapon-feel.md +50 -0
- package/skills/jgengine-combat/reference.md +24 -2
- package/skills/jgengine-gameplay/SKILL.md +63 -0
- package/skills/jgengine-gameplay/api.md +583 -463
- package/skills/jgengine-gameplay/capabilities.md +92 -0
- package/skills/jgengine-gameplay/recipes/device-save-status.md +46 -0
- package/skills/jgengine-gameplay/recipes/expeditions.md +108 -0
- package/skills/jgengine-gameplay/recipes/narrative-authoring.md +76 -0
- package/skills/jgengine-gameplay/recipes/paged-item-storage.md +101 -0
- package/skills/jgengine-gameplay/recipes/quest-authoring.md +63 -0
- package/skills/jgengine-gameplay/recipes/shaped-inventory.md +50 -0
- package/skills/jgengine-gameplay/reference-content-validation.md +74 -0
- package/skills/jgengine-gameplay/reference-systems.md +123 -0
- package/skills/jgengine-world/SKILL.md +25 -4
- package/skills/jgengine-world/api.md +1045 -655
- package/skills/jgengine-world/capabilities.md +246 -9
- package/skills/jgengine-world/recipes/character-animation.md +62 -0
- package/skills/jgengine-world/recipes/controls.md +48 -0
- package/skills/jgengine-world/recipes/flight-feel.md +76 -0
- package/skills/jgengine-world/recipes/npc-senses.md +55 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +87 -0
- package/skills/jgengine-world/reference.md +180 -4
- package/skills/jgengine-world/visibility.md +139 -0
|
@@ -0,0 +1,737 @@
|
|
|
1
|
+
import { sampleGripCurve } from "./vehicleBody.js";
|
|
2
|
+
const GRAVITY = 9.81;
|
|
3
|
+
const AIR_DENSITY = 1.225;
|
|
4
|
+
/** Below this speed slip angles use it as the longitudinal reference, avoiding the atan singularity at rest. */
|
|
5
|
+
const SLIP_REFERENCE_SPEED = 2;
|
|
6
|
+
const DEFAULT_SUBSTEP = 1 / 240;
|
|
7
|
+
function tireShape(tire) {
|
|
8
|
+
const slide = Math.min(0.999, Math.max(0.01, tire.slideGrip));
|
|
9
|
+
const c = 1 + (2 / Math.PI) * Math.acos(slide);
|
|
10
|
+
const b = Math.tan(Math.PI / (2 * c)) / Math.max(0.005, tire.peakSlipAngle);
|
|
11
|
+
return { b, c };
|
|
12
|
+
}
|
|
13
|
+
function lateralCurve(shape, alpha) {
|
|
14
|
+
return Math.sin(shape.c * Math.atan(shape.b * alpha));
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Creates a {@link VehicleDynamics}.
|
|
18
|
+
* @capability vehicle-dynamics force-based car handling — tires, load transfer, drivetrain, assists — tuned with physical numbers
|
|
19
|
+
*/
|
|
20
|
+
export function createVehicleDynamics(initialTuning, options = {}) {
|
|
21
|
+
let tuning = initialTuning;
|
|
22
|
+
let frontShape = tireShape(tuning.front);
|
|
23
|
+
let rearShape = tireShape(tuning.rear ?? tuning.front);
|
|
24
|
+
const surfaceFriction = options.surfaceFriction ?? (() => 1);
|
|
25
|
+
const clampMove = options.clampMove;
|
|
26
|
+
const spawnGround = options.position?.[1] ?? 0;
|
|
27
|
+
const groundAt = options.groundHeight ?? (() => spawnGround);
|
|
28
|
+
const state = freshState(tuning, options.position ?? [0, 0, 0], options.heading ?? 0);
|
|
29
|
+
settleOnGround();
|
|
30
|
+
/** Spawn resting on the terrain: pitch, roll and height fitted to the ground under the four wheels. */
|
|
31
|
+
function settleOnGround() {
|
|
32
|
+
const suspension = tuning.suspension;
|
|
33
|
+
if (suspension === undefined)
|
|
34
|
+
return;
|
|
35
|
+
const L = Math.max(0.5, tuning.wheelbase);
|
|
36
|
+
const a = L * (1 - clamp01(tuning.frontWeight));
|
|
37
|
+
const b = L - a;
|
|
38
|
+
const half = Math.max(0.15, tuning.trackWidth / 2);
|
|
39
|
+
const [fx, fz] = forwardOf(state.heading);
|
|
40
|
+
const ground = (along, lat) => groundAt(state.x + fx * along - fz * lat, state.z + fz * along + fx * lat);
|
|
41
|
+
const fl = ground(a, -half);
|
|
42
|
+
const fr = ground(a, half);
|
|
43
|
+
const rl = ground(-b, -half);
|
|
44
|
+
const rr = ground(-b, half);
|
|
45
|
+
const front = (fl + fr) / 2;
|
|
46
|
+
const rear = (rl + rr) / 2;
|
|
47
|
+
state.bodyPitch = -Math.atan2(front - rear, L);
|
|
48
|
+
state.bodyRoll = Math.atan2((fr + rr) / 2 - (fl + rl) / 2, 2 * half);
|
|
49
|
+
state.y = rear + (front - rear) * (b / L) + suspension.rideHeight;
|
|
50
|
+
state.vy = 0;
|
|
51
|
+
state.pitchRate = 0;
|
|
52
|
+
state.rollRate = 0;
|
|
53
|
+
const sags = staticSags(tuning, suspension);
|
|
54
|
+
for (let i = 0; i < 4; i += 1)
|
|
55
|
+
state.compression[i] = sags[i];
|
|
56
|
+
}
|
|
57
|
+
function baseHeight() {
|
|
58
|
+
return tuning.suspension === undefined ? state.y : state.y - tuning.suspension.rideHeight;
|
|
59
|
+
}
|
|
60
|
+
function tick(dt, input, modifiers) {
|
|
61
|
+
const maxSub = Math.max(1 / 2000, tuning.maxSubstep ?? DEFAULT_SUBSTEP);
|
|
62
|
+
const steps = dt > 0 ? Math.max(1, Math.ceil(dt / maxSub - 1e-9)) : 0;
|
|
63
|
+
const h = steps > 0 ? dt / steps : 0;
|
|
64
|
+
const telemetry = emptyTelemetry();
|
|
65
|
+
const surface = surfaceFriction(state.x, state.z);
|
|
66
|
+
telemetry.airborne = state.airborne;
|
|
67
|
+
for (let i = 0; i < steps; i += 1)
|
|
68
|
+
substep(h, input, modifiers, surface, telemetry);
|
|
69
|
+
if (steps === 0)
|
|
70
|
+
substep(0, input, modifiers, surface, telemetry);
|
|
71
|
+
const [fx, fz] = forwardOf(state.heading);
|
|
72
|
+
const forwardSpeed = state.vx * fx + state.vz * fz;
|
|
73
|
+
const lateralSpeed = -state.vx * fz + state.vz * fx;
|
|
74
|
+
return {
|
|
75
|
+
position: [state.x, baseHeight(), state.z],
|
|
76
|
+
heading: state.heading,
|
|
77
|
+
forwardSpeed,
|
|
78
|
+
lateralSpeed,
|
|
79
|
+
yawRate: state.yawRate,
|
|
80
|
+
steerAngle: -state.steerAngle,
|
|
81
|
+
sideslip: telemetry.sideslip,
|
|
82
|
+
slipAngleFront: telemetry.slipFront,
|
|
83
|
+
slipAngleRear: telemetry.slipRear,
|
|
84
|
+
frontSaturation: telemetry.satFront,
|
|
85
|
+
rearSaturation: telemetry.satRear,
|
|
86
|
+
lateralAccel: state.lateralAccel,
|
|
87
|
+
longitudinalAccel: state.longitudinalAccel,
|
|
88
|
+
frontLoad: telemetry.loadFront,
|
|
89
|
+
rearLoad: telemetry.loadRear,
|
|
90
|
+
surface,
|
|
91
|
+
gear: state.reversing ? -1 : state.gearIndex + 1,
|
|
92
|
+
rpm: state.rpm,
|
|
93
|
+
engineLoad: telemetry.engineLoad,
|
|
94
|
+
wheelspin: telemetry.wheelspin,
|
|
95
|
+
wheelLock: telemetry.wheelLock,
|
|
96
|
+
absActive: telemetry.abs,
|
|
97
|
+
tractionLimited: telemetry.traction,
|
|
98
|
+
stabilityActive: telemetry.stability,
|
|
99
|
+
bodyPitch: state.bodyPitch,
|
|
100
|
+
bodyRoll: state.bodyRoll,
|
|
101
|
+
airOffset: tuning.suspension === undefined ? 0 : Math.max(0, baseHeight() - groundAt(state.x, state.z)),
|
|
102
|
+
airborne: state.airborne,
|
|
103
|
+
landingSpeed: telemetry.landingSpeed,
|
|
104
|
+
wheelLoads: telemetry.wheelLoads,
|
|
105
|
+
lean: tuning.lean === undefined ? 0 : balanceLean(state.lateralAccel),
|
|
106
|
+
wheelie: telemetry.wheelie,
|
|
107
|
+
stoppie: telemetry.stoppie,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function substep(h, input, modifiers, surface, out) {
|
|
111
|
+
const t = tuning;
|
|
112
|
+
const m = t.massKg;
|
|
113
|
+
const L = Math.max(0.5, t.wheelbase);
|
|
114
|
+
const a = L * (1 - clamp01(t.frontWeight));
|
|
115
|
+
const b = L - a;
|
|
116
|
+
const iz = Math.max(1, (t.yawInertiaIndex ?? 1) * m * a * b);
|
|
117
|
+
const heading = state.heading;
|
|
118
|
+
const [fx, fz] = forwardOf(heading);
|
|
119
|
+
const u = state.vx * fx + state.vz * fz;
|
|
120
|
+
const v = -state.vx * fz + state.vz * fx;
|
|
121
|
+
const r = state.yawRate;
|
|
122
|
+
const speed = Math.hypot(state.vx, state.vz);
|
|
123
|
+
const throttle = clamp01(input.throttle);
|
|
124
|
+
const brakeInput = clamp01(input.brake);
|
|
125
|
+
const handbrake = clamp01(input.handbrake);
|
|
126
|
+
const steerInput = Math.max(-1, Math.min(1, input.steer)) * (modifiers?.steerScale ?? 1);
|
|
127
|
+
if (t.reverseOnBrake ?? true) {
|
|
128
|
+
if (!state.reversing && brakeInput > 0.1 && throttle <= 0.05 && u < 0.5)
|
|
129
|
+
state.reversing = true;
|
|
130
|
+
else if (state.reversing && (throttle > 0.1 || u > 0.5))
|
|
131
|
+
state.reversing = false;
|
|
132
|
+
}
|
|
133
|
+
const wantReverseDrive = state.reversing && brakeInput > 0;
|
|
134
|
+
// Rack: requested angle is in "heading-positive" sign, the opposite of AxisInput.steer.
|
|
135
|
+
const steering = t.steering;
|
|
136
|
+
const lockBlend = Math.min(1, Math.abs(u) / Math.max(0.1, steering.highSpeedAt));
|
|
137
|
+
const lock = steering.maxAngle + (steering.highSpeedAngle - steering.maxAngle) * lockBlend;
|
|
138
|
+
const sideslipNow = Math.abs(u) > 1 ? Math.atan2(v, Math.abs(u)) : 0;
|
|
139
|
+
// Caster: the wheels trail the velocity, so at `selfAlign` they steer into a slide by that share of β.
|
|
140
|
+
const align = (steering.selfAlign ?? 0) * -sideslipNow * Math.sign(u || 1);
|
|
141
|
+
let target = Math.max(-steering.maxAngle, Math.min(steering.maxAngle, -steerInput * lock + align));
|
|
142
|
+
const leanTuning = t.lean;
|
|
143
|
+
if (leanTuning !== undefined) {
|
|
144
|
+
// A rider cannot hold more lean than the tires can balance: tan(lean) is the lateral g it needs.
|
|
145
|
+
const gripLean = Math.atan(t.front.peakGrip * surface * (modifiers?.gripScale ?? 1));
|
|
146
|
+
const wanted = steerInput * Math.min(leanTuning.maxLean, gripLean);
|
|
147
|
+
const leanStep = Math.max(0, leanTuning.leanRate) * h;
|
|
148
|
+
const leanChange = Math.max(-leanStep, Math.min(leanStep, wanted - state.lean));
|
|
149
|
+
state.lean += leanChange;
|
|
150
|
+
const speed = Math.abs(u);
|
|
151
|
+
const direct = leanTuning.directSteerBelow ?? 3;
|
|
152
|
+
// Balanced turn: the lean sets the yaw rate the bike should hold (g·tan(lean)/v). The bar is that turn's
|
|
153
|
+
// geometric angle plus a trim the rider feeds back from the yaw error, which covers the tire slip.
|
|
154
|
+
const wantedYaw = (-GRAVITY * Math.tan(state.lean)) / Math.max(speed, 1) * Math.sign(u || 1);
|
|
155
|
+
if (speed >= direct)
|
|
156
|
+
state.leanTrim += (wantedYaw - r) * 3 * h;
|
|
157
|
+
else
|
|
158
|
+
state.leanTrim = 0;
|
|
159
|
+
const trimLimit = Math.min(steering.maxAngle, t.front.peakSlipAngle * 1.5);
|
|
160
|
+
state.leanTrim = Math.max(-trimLimit, Math.min(trimLimit, state.leanTrim));
|
|
161
|
+
const geometric = Math.atan((L * wantedYaw) / Math.max(speed, 1));
|
|
162
|
+
const counter = h > 0 ? ((leanTuning.countersteer ?? 0.012) * leanChange) / h : 0;
|
|
163
|
+
const blend = Math.min(1, speed / Math.max(0.1, direct));
|
|
164
|
+
target = Math.max(-steering.maxAngle, Math.min(steering.maxAngle, (geometric + state.leanTrim + counter) * blend + -steerInput * steering.maxAngle * (1 - blend) + align));
|
|
165
|
+
}
|
|
166
|
+
const toward = target - state.steerAngle;
|
|
167
|
+
const returning = Math.sign(toward) !== Math.sign(state.steerAngle) && state.steerAngle !== 0;
|
|
168
|
+
const rate = returning ? (steering.returnRate ?? steering.rate) : steering.rate;
|
|
169
|
+
const maxMove = Math.max(0, rate) * h;
|
|
170
|
+
state.steerAngle += Math.max(-maxMove, Math.min(maxMove, toward));
|
|
171
|
+
const delta = state.steerAngle;
|
|
172
|
+
const aero = t.aero;
|
|
173
|
+
const q = 0.5 * AIR_DENSITY * u * u;
|
|
174
|
+
const downforce = q * (aero?.downforceArea ?? 0);
|
|
175
|
+
const downFront = aero?.downforceFront ?? 0.45;
|
|
176
|
+
const weight = m * GRAVITY;
|
|
177
|
+
const rsf = t.rollStiffnessFront ?? 0.5;
|
|
178
|
+
const suspension = t.suspension;
|
|
179
|
+
let loadFront;
|
|
180
|
+
let loadRear;
|
|
181
|
+
let transferFront;
|
|
182
|
+
let transferRear;
|
|
183
|
+
let grounded = true;
|
|
184
|
+
let slopeLong = 0;
|
|
185
|
+
let slopeLat = 0;
|
|
186
|
+
if (suspension === undefined) {
|
|
187
|
+
const transferLong = (m * state.longitudinalAccel * t.comHeight) / L;
|
|
188
|
+
const rawFront = weight * (b / L) - transferLong + downforce * downFront;
|
|
189
|
+
const rawRear = weight * (a / L) + transferLong + downforce * (1 - downFront);
|
|
190
|
+
if (rawFront <= 0)
|
|
191
|
+
out.wheelie = true;
|
|
192
|
+
if (rawRear <= 0)
|
|
193
|
+
out.stoppie = true;
|
|
194
|
+
loadFront = Math.max(0, rawFront);
|
|
195
|
+
loadRear = Math.max(0, rawRear);
|
|
196
|
+
// A single-track vehicle leans instead of shifting load across an axle.
|
|
197
|
+
const transferLat = t.lean !== undefined ? 0 : (m * Math.abs(state.lateralAccel) * t.comHeight) / Math.max(0.3, t.trackWidth);
|
|
198
|
+
transferFront = transferLat * rsf;
|
|
199
|
+
transferRear = transferLat * (1 - rsf);
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
const wheel = springForces(suspension, a, b, h, rsf);
|
|
203
|
+
loadFront = wheel[0] + wheel[1];
|
|
204
|
+
loadRear = wheel[2] + wheel[3];
|
|
205
|
+
transferFront = Math.abs(wheel[0] - wheel[1]) / 2;
|
|
206
|
+
transferRear = Math.abs(wheel[2] - wheel[3]) / 2;
|
|
207
|
+
grounded = loadFront + loadRear > 0;
|
|
208
|
+
out.wheelLoads = [wheel[0], wheel[1], wheel[2], wheel[3]];
|
|
209
|
+
if (grounded) {
|
|
210
|
+
// Gravity along the terrain: the tires hold the car to the surface, so the downhill share acts in-plane.
|
|
211
|
+
const e = 0.5;
|
|
212
|
+
const gx = (groundAt(state.x + e, state.z) - groundAt(state.x - e, state.z)) / (2 * e);
|
|
213
|
+
const gz = (groundAt(state.x, state.z + e) - groundAt(state.x, state.z - e)) / (2 * e);
|
|
214
|
+
const norm = Math.sqrt(1 + gx * gx + gz * gz);
|
|
215
|
+
const worldX = (-weight * gx) / norm;
|
|
216
|
+
const worldZ = (-weight * gz) / norm;
|
|
217
|
+
slopeLong = worldX * fx + worldZ * fz;
|
|
218
|
+
slopeLat = -worldX * fz + worldZ * fx;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const sens = t.loadSensitivity ?? 0.2;
|
|
222
|
+
const gripScale = surface * (modifiers?.gripScale ?? 1);
|
|
223
|
+
const muFront = t.front.peakGrip * gripScale * loadSensitivityFactor(transferFront, loadFront, sens);
|
|
224
|
+
const rearTire = t.rear ?? t.front;
|
|
225
|
+
const muRear = rearTire.peakGrip * gripScale * loadSensitivityFactor(transferRear, loadRear, sens);
|
|
226
|
+
const capFront = muFront * loadFront;
|
|
227
|
+
const hbGrip = 1 - handbrake * (1 - (t.handbrakeGrip ?? 0.35));
|
|
228
|
+
const capRear = muRear * loadRear;
|
|
229
|
+
// Axle-point velocities in the car frame: front sits `a` ahead, rear `b` behind.
|
|
230
|
+
const uF = u;
|
|
231
|
+
const vF = v - a * r;
|
|
232
|
+
const vR = v + b * r;
|
|
233
|
+
const cosD = Math.cos(delta);
|
|
234
|
+
const sinD = Math.sin(delta);
|
|
235
|
+
const wheelLong = uF * cosD - vF * sinD;
|
|
236
|
+
const wheelLat = uF * sinD + vF * cosD;
|
|
237
|
+
const slipFront = Math.atan2(wheelLat, Math.max(SLIP_REFERENCE_SPEED, Math.abs(wheelLong)));
|
|
238
|
+
const slipRear = Math.atan2(vR, Math.max(SLIP_REFERENCE_SPEED, Math.abs(u)));
|
|
239
|
+
let drive = driveForce(throttle, wantReverseDrive ? brakeInput : 0, u, h, out) * (modifiers?.driveScale ?? 1);
|
|
240
|
+
// A governor cuts drive like a limiter: forcing speed down after the fact would leave full drive
|
|
241
|
+
// consuming the tire budget, and a car pinned at its cap could not turn.
|
|
242
|
+
if (t.speedLimit !== undefined && drive > 0)
|
|
243
|
+
drive *= clamp01((t.speedLimit - u) / 0.5);
|
|
244
|
+
let brakeTotal = 0;
|
|
245
|
+
if (!wantReverseDrive && brakeInput > 0 && Math.abs(u) > 0.05)
|
|
246
|
+
brakeTotal = brakeInput * t.brakeForce * (modifiers?.brakeScale ?? 1);
|
|
247
|
+
const brakeFrontShare = t.brakeFront ?? 0.65;
|
|
248
|
+
const assists = t.assists;
|
|
249
|
+
const driveFrontShare = clamp01(t.driveFront);
|
|
250
|
+
let longFront = drive * driveFrontShare;
|
|
251
|
+
let longRear = drive * (1 - driveFrontShare);
|
|
252
|
+
let brakeF = brakeTotal * brakeFrontShare;
|
|
253
|
+
let brakeR = brakeTotal * (1 - brakeFrontShare);
|
|
254
|
+
// ABS and TC act on combined slip like the real systems: the longitudinal limit is what the friction
|
|
255
|
+
// ellipse leaves after the cornering force the axle is already asking for.
|
|
256
|
+
const frontLatUse = Math.abs(lateralCurve(frontShape, slipFront));
|
|
257
|
+
const rearLatUse = Math.abs(lateralCurve(rearShape, slipRear)) * hbGrip;
|
|
258
|
+
const longRoomFront = capFront * Math.sqrt(Math.max(0, 1 - frontLatUse * frontLatUse));
|
|
259
|
+
const longRoomRear = capRear * Math.sqrt(Math.max(0, 1 - rearLatUse * rearLatUse));
|
|
260
|
+
const abs = assists?.abs ?? 0;
|
|
261
|
+
if (abs > 0) {
|
|
262
|
+
const limitFront = longRoomFront * (1 - 0.2 * abs);
|
|
263
|
+
const limitRear = longRoomRear * (1 - 0.2 * abs);
|
|
264
|
+
if (brakeF > limitFront) {
|
|
265
|
+
brakeF = limitFront + (brakeF - limitFront) * (1 - abs);
|
|
266
|
+
out.abs = true;
|
|
267
|
+
}
|
|
268
|
+
if (brakeR > limitRear) {
|
|
269
|
+
brakeR = limitRear + (brakeR - limitRear) * (1 - abs);
|
|
270
|
+
out.abs = true;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const tc = assists?.tractionControl ?? 0;
|
|
274
|
+
if (tc > 0) {
|
|
275
|
+
const limitFront = longRoomFront * (1 - 0.35 * tc);
|
|
276
|
+
const limitRear = longRoomRear * (1 - 0.35 * tc);
|
|
277
|
+
if (Math.abs(longFront) > limitFront) {
|
|
278
|
+
longFront = Math.sign(longFront) * (limitFront + (Math.abs(longFront) - limitFront) * (1 - tc));
|
|
279
|
+
out.traction = true;
|
|
280
|
+
}
|
|
281
|
+
if (Math.abs(longRear) > limitRear) {
|
|
282
|
+
longRear = Math.sign(longRear) * (limitRear + (Math.abs(longRear) - limitRear) * (1 - tc));
|
|
283
|
+
out.traction = true;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// Pitch-over limits: the front lifts once m·ax·h exceeds its static moment (weight·b plus aero), the rear
|
|
287
|
+
// under braking once it exceeds weight·a. Springs overshoot that static limit as the body pitches, so a
|
|
288
|
+
// sprung vehicle also backs off in proportion to how light the axle already is.
|
|
289
|
+
const antiWheelie = assists?.antiWheelie ?? 0;
|
|
290
|
+
const driveSum = longFront + longRear;
|
|
291
|
+
if (antiWheelie > 0 && driveSum > 0 && u >= 0) {
|
|
292
|
+
const lightFront = suspension === undefined ? 1 : clamp01(loadFront / (0.3 * weight * (b / L)));
|
|
293
|
+
const liftDrive = ((weight * b + downforce * downFront * L) / t.comHeight) * (1 - 0.15 * antiWheelie) * lightFront;
|
|
294
|
+
if (driveSum > liftDrive) {
|
|
295
|
+
const scale = (liftDrive + (driveSum - liftDrive) * (1 - antiWheelie)) / driveSum;
|
|
296
|
+
longFront *= scale;
|
|
297
|
+
longRear *= scale;
|
|
298
|
+
out.traction = true;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const antiStoppie = assists?.antiStoppie ?? 0;
|
|
302
|
+
const brakeSum = brakeF + brakeR;
|
|
303
|
+
if (antiStoppie > 0 && brakeSum > 0) {
|
|
304
|
+
const lightRear = suspension === undefined ? 1 : clamp01(loadRear / (0.3 * weight * (a / L)));
|
|
305
|
+
const liftBrake = ((weight * a + downforce * (1 - downFront) * L) / t.comHeight) * (1 - 0.15 * antiStoppie) * lightRear;
|
|
306
|
+
if (brakeSum > liftBrake) {
|
|
307
|
+
const scale = (liftBrake + (brakeSum - liftBrake) * (1 - antiStoppie)) / brakeSum;
|
|
308
|
+
brakeF *= scale;
|
|
309
|
+
brakeR *= scale;
|
|
310
|
+
out.abs = true;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const direction = Math.sign(u) || 1;
|
|
314
|
+
// A locked rear (handbrake) drags at sliding friction and gives up most of its cornering force.
|
|
315
|
+
const hbDrag = handbrake * capRear * rearTire.slideGrip * 0.9;
|
|
316
|
+
let fxFront = longFront - direction * brakeF;
|
|
317
|
+
let fxRear = longRear - direction * (brakeR + hbDrag);
|
|
318
|
+
const front = combine(fxFront, capFront, frontShape, slipFront, 1, out, "front", Math.abs(longFront) > 0);
|
|
319
|
+
const rear = combine(fxRear, capRear, rearShape, slipRear, hbGrip, out, "rear", Math.abs(longRear) > 0);
|
|
320
|
+
fxFront = front.fx;
|
|
321
|
+
fxRear = rear.fx;
|
|
322
|
+
let fyFront = front.fy;
|
|
323
|
+
let fyRear = rear.fy;
|
|
324
|
+
// Cap lateral forces so one substep cannot push an axle's slip velocity past zero at low speed.
|
|
325
|
+
if (h > 0) {
|
|
326
|
+
const mFront = m * (b / L);
|
|
327
|
+
const mRear = m * (a / L);
|
|
328
|
+
const maxFront = (mFront * Math.abs(wheelLat)) / h;
|
|
329
|
+
const maxRear = (mRear * Math.abs(vR)) / h;
|
|
330
|
+
if (Math.abs(fyFront) > maxFront)
|
|
331
|
+
fyFront = Math.sign(fyFront) * maxFront;
|
|
332
|
+
if (Math.abs(fyRear) > maxRear)
|
|
333
|
+
fyRear = Math.sign(fyRear) * maxRear;
|
|
334
|
+
}
|
|
335
|
+
// Car-frame forces (x forward, y along the lateral axis used by `v`).
|
|
336
|
+
const frontLat = fyFront * cosD - fxFront * sinD;
|
|
337
|
+
let forceLong = fxFront * cosD + fyFront * sinD + fxRear + (modifiers?.thrust ?? 0) + slopeLong;
|
|
338
|
+
const forceLat = frontLat + fyRear + slopeLat;
|
|
339
|
+
const drag = q * (aero?.dragArea ?? 0) * Math.sign(u);
|
|
340
|
+
const rollingLoad = suspension === undefined ? weight : loadFront + loadRear;
|
|
341
|
+
const rolling = (aero?.rollingResistance ?? 0.015) * rollingLoad * Math.sign(u) * Math.min(1, Math.abs(u) / 0.5);
|
|
342
|
+
forceLong -= drag + rolling;
|
|
343
|
+
// Lateral force ahead of the CG yaws the heading negative in this frame, force behind it positive.
|
|
344
|
+
let yawMoment = -a * frontLat + b * fyRear;
|
|
345
|
+
const stab = assists?.stability ?? 0;
|
|
346
|
+
if (stab > 0 && grounded && Math.abs(u) > 3) {
|
|
347
|
+
const allowed = assists?.maxSideslip ?? 0.2;
|
|
348
|
+
const excess = Math.abs(sideslipNow) - allowed;
|
|
349
|
+
if (excess > 0) {
|
|
350
|
+
yawMoment += -Math.sign(sideslipNow) * Math.sign(u) * stab * excess * 4 * iz * Math.max(1, Math.abs(r) + 1);
|
|
351
|
+
yawMoment += -stab * r * iz * 0.6 * Math.min(1, excess * 4);
|
|
352
|
+
out.stability = true;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
let du = forceLong / m - r * v;
|
|
356
|
+
const dv = forceLat / m + r * u;
|
|
357
|
+
// Brakes and rolling resistance cannot reverse the car inside one substep (a slope can).
|
|
358
|
+
if (h > 0 && throttle <= 0 && !wantReverseDrive && (modifiers?.thrust ?? 0) === 0 && slopeLong === 0) {
|
|
359
|
+
const next = u + du * h;
|
|
360
|
+
if (Math.sign(next) !== Math.sign(u) && u !== 0)
|
|
361
|
+
du = -u / h;
|
|
362
|
+
}
|
|
363
|
+
const dr = yawMoment / iz;
|
|
364
|
+
let nu = u + du * h;
|
|
365
|
+
let nv = v + dv * h;
|
|
366
|
+
let nr = r + dr * h;
|
|
367
|
+
const slopePull = Math.hypot(slopeLong, slopeLat);
|
|
368
|
+
const holds = slopePull <= (aero?.rollingResistance ?? 0.015) * weight * 2 || brakeInput > 0 || handbrake > 0;
|
|
369
|
+
if (h > 0 && grounded && holds && speed < 0.3 && throttle <= 0 && brakeInput <= 0 && Math.abs(modifiers?.thrust ?? 0) === 0) {
|
|
370
|
+
nu *= Math.max(0, 1 - 6 * h);
|
|
371
|
+
nv *= Math.max(0, 1 - 6 * h);
|
|
372
|
+
nr *= Math.max(0, 1 - 6 * h);
|
|
373
|
+
}
|
|
374
|
+
// Near rest, held brakes grip statically: they cancel motion up to what the braked axles can hold,
|
|
375
|
+
// so a car parks on a slope instead of creeping at whatever speed a damped slide settles to.
|
|
376
|
+
if (h > 0 && grounded && Math.abs(nu) < 0.5 && !wantReverseDrive && throttle <= 0) {
|
|
377
|
+
const holdForce = handbrake * capRear * rearTire.slideGrip + brakeInput * Math.min(t.brakeForce, capFront + capRear);
|
|
378
|
+
const hold = (holdForce / m) * h;
|
|
379
|
+
if (hold > 0)
|
|
380
|
+
nu = Math.abs(nu) <= hold ? 0 : nu - Math.sign(nu) * hold;
|
|
381
|
+
}
|
|
382
|
+
const reverseCap = t.reverseSpeed ?? 8;
|
|
383
|
+
if (nu < -reverseCap)
|
|
384
|
+
nu = -reverseCap;
|
|
385
|
+
state.heading = heading + nr * h;
|
|
386
|
+
const [nfx, nfz] = forwardOf(state.heading);
|
|
387
|
+
let nvx = nfx * nu - nfz * nv;
|
|
388
|
+
let nvz = nfz * nu + nfx * nv;
|
|
389
|
+
const toX = state.x + nvx * h;
|
|
390
|
+
const toZ = state.z + nvz * h;
|
|
391
|
+
if (clampMove !== undefined && h > 0) {
|
|
392
|
+
const allowed = clampMove([state.x, state.z], [toX, toZ]);
|
|
393
|
+
nvx = (allowed.motion?.[0] ?? allowed[0] - state.x) / h;
|
|
394
|
+
nvz = (allowed.motion?.[1] ?? allowed[1] - state.z) / h;
|
|
395
|
+
state.x = allowed[0];
|
|
396
|
+
state.z = allowed[1];
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
state.x = toX;
|
|
400
|
+
state.z = toZ;
|
|
401
|
+
}
|
|
402
|
+
state.vx = nvx;
|
|
403
|
+
state.vz = nvz;
|
|
404
|
+
state.yawRate = nr;
|
|
405
|
+
const response = suspension === undefined ? 1 - Math.exp(-(t.suspensionResponse ?? 8) * h) : 1;
|
|
406
|
+
const accelLong = h > 0 ? du + r * v : 0;
|
|
407
|
+
const accelLat = h > 0 ? dv - r * u : 0;
|
|
408
|
+
state.longitudinalAccel += (accelLong - state.longitudinalAccel) * response;
|
|
409
|
+
// The rider's lean is the displayed roll, so it keeps the roll lag springs would otherwise supply.
|
|
410
|
+
const responseLat = t.lean !== undefined ? 1 - Math.exp(-(t.suspensionResponse ?? 8) * h) : response;
|
|
411
|
+
state.lateralAccel += (accelLat - state.lateralAccel) * responseLat;
|
|
412
|
+
if (suspension === undefined) {
|
|
413
|
+
const rollK = t.rollStiffness ?? (m * GRAVITY * t.comHeight) / 0.05;
|
|
414
|
+
const pitchK = t.pitchStiffness ?? (m * GRAVITY * t.comHeight) / 0.035;
|
|
415
|
+
state.bodyRoll = t.lean !== undefined ? -balanceLean(state.lateralAccel) : (m * state.lateralAccel * t.comHeight) / rollK;
|
|
416
|
+
state.bodyPitch = (-m * state.longitudinalAccel * t.comHeight) / pitchK;
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
if (!grounded && t.air !== undefined)
|
|
420
|
+
steerInAir(t.air, input, modifiers?.air, h);
|
|
421
|
+
integrateBody(suspension, a, b, h, downforce, grounded, out);
|
|
422
|
+
}
|
|
423
|
+
out.sideslip = Math.abs(nu) > 1 ? Math.atan2(nv, Math.abs(nu)) : 0;
|
|
424
|
+
out.slipFront = slipFront;
|
|
425
|
+
out.slipRear = slipRear;
|
|
426
|
+
out.loadFront = loadFront;
|
|
427
|
+
out.loadRear = loadRear;
|
|
428
|
+
}
|
|
429
|
+
const corner = [0, 0, 0, 0];
|
|
430
|
+
/** Spring + damper + anti-roll force at each corner from the current body pose; updates stored compression. */
|
|
431
|
+
function springForces(suspension, a, b, h, rsf) {
|
|
432
|
+
const t = tuning;
|
|
433
|
+
const half = Math.max(0.15, t.trackWidth / 2);
|
|
434
|
+
const [fx, fz] = forwardOf(state.heading);
|
|
435
|
+
const sags = staticSags(t, suspension);
|
|
436
|
+
// A leaning single-track vehicle rolls about its tire contacts, so its lean never compresses one side's springs.
|
|
437
|
+
const roll = t.lean !== undefined ? 0 : state.bodyRoll;
|
|
438
|
+
for (let i = 0; i < 4; i += 1) {
|
|
439
|
+
const along = i < 2 ? a : -b;
|
|
440
|
+
const lat = i % 2 === 0 ? -half : half;
|
|
441
|
+
const px = state.x + fx * along - fz * lat;
|
|
442
|
+
const pz = state.z + fz * along + fx * lat;
|
|
443
|
+
const attach = state.y - Math.sin(state.bodyPitch) * along + Math.sin(roll) * lat;
|
|
444
|
+
const c = groundAt(px, pz) + suspension.rideHeight + sags[i] - attach;
|
|
445
|
+
const rate = h > 0 ? (c - state.compression[i]) / h : 0;
|
|
446
|
+
state.compression[i] = c;
|
|
447
|
+
let force = 0;
|
|
448
|
+
if (c > 0) {
|
|
449
|
+
const damping = rate >= 0 ? suspension.damperRate : (suspension.reboundRate ?? suspension.damperRate * 1.5);
|
|
450
|
+
force = suspension.springRate * c + damping * rate;
|
|
451
|
+
// The bump stop is stiff and lossy: it stops the travel without handing the landing back as a bounce.
|
|
452
|
+
if (c > suspension.travel)
|
|
453
|
+
force += suspension.springRate * 20 * (c - suspension.travel) + Math.max(0, rate) * suspension.damperRate * 6;
|
|
454
|
+
}
|
|
455
|
+
corner[i] = Math.max(0, force);
|
|
456
|
+
}
|
|
457
|
+
const antiRoll = t.lean !== undefined ? 0 : (suspension.antiRoll ?? 0);
|
|
458
|
+
if (antiRoll > 0) {
|
|
459
|
+
for (let axle = 0; axle < 2; axle += 1) {
|
|
460
|
+
const left = axle * 2;
|
|
461
|
+
if (corner[left] <= 0 || corner[left + 1] <= 0)
|
|
462
|
+
continue;
|
|
463
|
+
const share = axle === 0 ? rsf : 1 - rsf;
|
|
464
|
+
const shift = (antiRoll * share * state.bodyRoll) / (2 * half);
|
|
465
|
+
corner[left] = Math.max(0, corner[left] + shift);
|
|
466
|
+
corner[left + 1] = Math.max(0, corner[left + 1] - shift);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return corner;
|
|
470
|
+
}
|
|
471
|
+
function steerInAir(air, input, explicit, h) {
|
|
472
|
+
const pitch = clampUnit(explicit?.pitch ?? clamp01(input.throttle) - clamp01(input.brake));
|
|
473
|
+
const yaw = clampUnit(explicit?.yaw ?? input.steer);
|
|
474
|
+
const roll = clampUnit(explicit?.roll ?? 0);
|
|
475
|
+
const damping = air.damping ?? 1.5;
|
|
476
|
+
const maxRate = air.maxRate ?? 5.5;
|
|
477
|
+
state.pitchRate = driveRate(state.pitchRate, pitch * air.pitchAccel, damping, maxRate, h);
|
|
478
|
+
state.yawRate = driveRate(state.yawRate, -yaw * air.yawAccel, damping, maxRate, h);
|
|
479
|
+
state.rollRate = driveRate(state.rollRate, roll * air.rollAccel, damping, maxRate, h);
|
|
480
|
+
}
|
|
481
|
+
/** Heave, pitch and roll from the corner forces found this substep plus the tires' inertial moments. */
|
|
482
|
+
function integrateBody(suspension, a, b, h, downforce, grounded, out) {
|
|
483
|
+
const t = tuning;
|
|
484
|
+
const m = t.massKg;
|
|
485
|
+
const half = Math.max(0.15, t.trackWidth / 2);
|
|
486
|
+
let lift = 0;
|
|
487
|
+
let pitchMoment = 0;
|
|
488
|
+
let rollMoment = 0;
|
|
489
|
+
for (let i = 0; i < 4; i += 1) {
|
|
490
|
+
const along = i < 2 ? a : -b;
|
|
491
|
+
const lat = i % 2 === 0 ? -half : half;
|
|
492
|
+
lift += corner[i];
|
|
493
|
+
pitchMoment -= corner[i] * along;
|
|
494
|
+
rollMoment += corner[i] * lat;
|
|
495
|
+
}
|
|
496
|
+
if (grounded) {
|
|
497
|
+
pitchMoment -= m * state.longitudinalAccel * t.comHeight;
|
|
498
|
+
rollMoment += m * state.lateralAccel * t.comHeight;
|
|
499
|
+
}
|
|
500
|
+
const pitchInertia = Math.max(1, (suspension.pitchInertiaIndex ?? 0.9) * m * a * b);
|
|
501
|
+
const rollInertia = Math.max(1, 0.6 * m * half * half);
|
|
502
|
+
state.vy += ((lift - downforce) / m - GRAVITY) * h;
|
|
503
|
+
state.y += state.vy * h;
|
|
504
|
+
state.pitchRate += (pitchMoment / pitchInertia) * h;
|
|
505
|
+
state.rollRate += (rollMoment / rollInertia) * h;
|
|
506
|
+
state.bodyPitch += state.pitchRate * h;
|
|
507
|
+
state.bodyRoll += state.rollRate * h;
|
|
508
|
+
if (t.lean !== undefined) {
|
|
509
|
+
state.bodyRoll = -balanceLean(state.lateralAccel);
|
|
510
|
+
state.rollRate = 0;
|
|
511
|
+
}
|
|
512
|
+
if (state.airborne && grounded)
|
|
513
|
+
out.landingSpeed = Math.max(out.landingSpeed, -state.vy);
|
|
514
|
+
state.airborne = !grounded;
|
|
515
|
+
if (!grounded)
|
|
516
|
+
state.airTime += h;
|
|
517
|
+
else if (state.vy <= 0.5) {
|
|
518
|
+
// Still-compressed springs right after a jump read as ground contact; only a settled car has landed.
|
|
519
|
+
state.jumpsUsed = 0;
|
|
520
|
+
state.airTime = 0;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
function driveForce(throttle, reverse, u, h, out) {
|
|
524
|
+
const pt = tuning.powertrain;
|
|
525
|
+
if (pt.kind === "direct") {
|
|
526
|
+
out.engineLoad = Math.max(throttle, reverse);
|
|
527
|
+
if (reverse > 0)
|
|
528
|
+
return -reverse * pt.maxForce * (pt.reverseScale ?? 0.5);
|
|
529
|
+
if (throttle <= 0)
|
|
530
|
+
return Math.abs(u) > 0.2 ? -Math.sign(u) * (pt.coastForce ?? pt.maxForce * 0.08) : 0;
|
|
531
|
+
const powerLimited = pt.maxPower / Math.max(1, Math.abs(u));
|
|
532
|
+
return throttle * Math.min(pt.maxForce, powerLimited);
|
|
533
|
+
}
|
|
534
|
+
state.shiftRemaining = Math.max(0, state.shiftRemaining - h);
|
|
535
|
+
const eff = pt.efficiency ?? 0.85;
|
|
536
|
+
const ratio = state.reversing ? pt.reverseGear : (pt.gears[state.gearIndex] ?? 1);
|
|
537
|
+
const wheelRpm = (Math.abs(u) / Math.max(0.05, pt.wheelRadius)) * (60 / (2 * Math.PI));
|
|
538
|
+
const coupled = wheelRpm * ratio * pt.finalDrive;
|
|
539
|
+
const pedal = Math.max(throttle, reverse);
|
|
540
|
+
const launch = pt.launchRpm ?? pt.idleRpm + (pt.redlineRpm - pt.idleRpm) * 0.35;
|
|
541
|
+
const slipRpm = pt.idleRpm + (launch - pt.idleRpm) * pedal;
|
|
542
|
+
const targetRpm = Math.min(pt.redlineRpm, Math.max(coupled, slipRpm, pt.idleRpm));
|
|
543
|
+
const rpmResponse = 1 - Math.exp(-18 * h);
|
|
544
|
+
state.rpm += (targetRpm - state.rpm) * rpmResponse;
|
|
545
|
+
if (!state.reversing && state.shiftRemaining <= 0) {
|
|
546
|
+
if (coupled >= pt.shiftUpRpm && state.gearIndex < pt.gears.length - 1 && pedal > 0) {
|
|
547
|
+
state.gearIndex += 1;
|
|
548
|
+
state.shiftRemaining = pt.shiftSeconds;
|
|
549
|
+
}
|
|
550
|
+
else if (coupled <= pt.shiftDownRpm && state.gearIndex > 0) {
|
|
551
|
+
state.gearIndex -= 1;
|
|
552
|
+
state.shiftRemaining = pt.shiftSeconds;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
const normalized = (state.rpm - pt.idleRpm) / Math.max(1, pt.redlineRpm - pt.idleRpm);
|
|
556
|
+
const limiter = coupled >= pt.redlineRpm ? 0 : 1;
|
|
557
|
+
const shifting = state.shiftRemaining > 0 ? 0.2 : 1;
|
|
558
|
+
const toWheel = (ratio * pt.finalDrive * eff) / Math.max(0.05, pt.wheelRadius);
|
|
559
|
+
out.engineLoad = pedal * limiter * shifting;
|
|
560
|
+
if (pedal > 0) {
|
|
561
|
+
const torque = pt.peakTorque * sampleGripCurve(pt.torqueCurve, normalized) * pedal * limiter * shifting;
|
|
562
|
+
return (reverse > 0 ? -1 : 1) * torque * toWheel;
|
|
563
|
+
}
|
|
564
|
+
if (Math.abs(u) < 0.5)
|
|
565
|
+
return 0;
|
|
566
|
+
return -Math.sign(u) * (pt.engineBrakeTorque ?? pt.peakTorque * 0.12) * toWheel;
|
|
567
|
+
}
|
|
568
|
+
return {
|
|
569
|
+
tick,
|
|
570
|
+
pose: () => ({ position: [state.x, baseHeight(), state.z], heading: state.heading }),
|
|
571
|
+
velocity: () => [state.vx, state.vz],
|
|
572
|
+
scaleVelocity(factor) {
|
|
573
|
+
state.vx *= factor;
|
|
574
|
+
state.vz *= factor;
|
|
575
|
+
state.yawRate *= factor;
|
|
576
|
+
},
|
|
577
|
+
jump() {
|
|
578
|
+
const jump = tuning.jump;
|
|
579
|
+
if (jump === undefined || tuning.suspension === undefined)
|
|
580
|
+
return false;
|
|
581
|
+
if (!state.airborne && state.jumpsUsed === 0) {
|
|
582
|
+
state.vy = Math.max(state.vy, 0) + jump.speed;
|
|
583
|
+
state.jumpsUsed = 1;
|
|
584
|
+
return true;
|
|
585
|
+
}
|
|
586
|
+
const count = jump.count ?? 1;
|
|
587
|
+
const inWindow = state.airTime <= (jump.window ?? Number.POSITIVE_INFINITY);
|
|
588
|
+
if (state.jumpsUsed >= count || !inWindow)
|
|
589
|
+
return false;
|
|
590
|
+
// An air jump always takes off from a first jump (or a ledge) and counts as one used either way.
|
|
591
|
+
state.jumpsUsed = Math.max(state.jumpsUsed, 1) + 1;
|
|
592
|
+
state.vy = Math.max(state.vy, 0) + jump.speed;
|
|
593
|
+
return true;
|
|
594
|
+
},
|
|
595
|
+
applyAngularImpulse(pitch, yaw, roll) {
|
|
596
|
+
state.pitchRate += pitch;
|
|
597
|
+
state.yawRate += yaw;
|
|
598
|
+
state.rollRate += roll;
|
|
599
|
+
},
|
|
600
|
+
applyImpulse(dvx, dvz, dvy = 0) {
|
|
601
|
+
state.vx += dvx;
|
|
602
|
+
state.vz += dvz;
|
|
603
|
+
if (tuning.suspension !== undefined)
|
|
604
|
+
state.vy += dvy;
|
|
605
|
+
},
|
|
606
|
+
retune(next) {
|
|
607
|
+
tuning = next;
|
|
608
|
+
frontShape = tireShape(next.front);
|
|
609
|
+
rearShape = tireShape(next.rear ?? next.front);
|
|
610
|
+
if (next.powertrain.kind === "gearbox") {
|
|
611
|
+
state.gearIndex = Math.min(state.gearIndex, next.powertrain.gears.length - 1);
|
|
612
|
+
}
|
|
613
|
+
},
|
|
614
|
+
tuning: () => tuning,
|
|
615
|
+
snapshot: () => ({ ...state, compression: [...state.compression] }),
|
|
616
|
+
restore(next) {
|
|
617
|
+
Object.assign(state, next, { compression: [...next.compression] });
|
|
618
|
+
},
|
|
619
|
+
resetTo(position, heading) {
|
|
620
|
+
Object.assign(state, freshState(tuning, position, heading));
|
|
621
|
+
settleOnGround();
|
|
622
|
+
},
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
function emptyTelemetry() {
|
|
626
|
+
return {
|
|
627
|
+
sideslip: 0,
|
|
628
|
+
slipFront: 0,
|
|
629
|
+
slipRear: 0,
|
|
630
|
+
satFront: 0,
|
|
631
|
+
satRear: 0,
|
|
632
|
+
loadFront: 0,
|
|
633
|
+
loadRear: 0,
|
|
634
|
+
engineLoad: 0,
|
|
635
|
+
wheelspin: false,
|
|
636
|
+
wheelLock: false,
|
|
637
|
+
abs: false,
|
|
638
|
+
traction: false,
|
|
639
|
+
stability: false,
|
|
640
|
+
airborne: false,
|
|
641
|
+
landingSpeed: 0,
|
|
642
|
+
wheelLoads: [0, 0, 0, 0],
|
|
643
|
+
wheelie: false,
|
|
644
|
+
stoppie: false,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
/** Static spring compression at each corner that holds the car level at `rideHeight`. */
|
|
648
|
+
function staticSags(tuning, suspension) {
|
|
649
|
+
const front = clamp01(tuning.frontWeight);
|
|
650
|
+
const perFront = (tuning.massKg * GRAVITY * front) / 2 / Math.max(1, suspension.springRate);
|
|
651
|
+
const perRear = (tuning.massKg * GRAVITY * (1 - front)) / 2 / Math.max(1, suspension.springRate);
|
|
652
|
+
return [perFront, perFront, perRear, perRear];
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Friction ellipse for one axle: longitudinal demand is clamped to the budget first, then lateral force gets
|
|
656
|
+
* what is left. Driving or braking hard therefore costs cornering grip on that axle, which is what turns
|
|
657
|
+
* throttle into power oversteer on a RWD car and brake into rotation on corner entry.
|
|
658
|
+
*/
|
|
659
|
+
function combine(demandX, capacity, shape, slip, lateralScale, out, axle, driven) {
|
|
660
|
+
if (capacity <= 0)
|
|
661
|
+
return { fx: 0, fy: 0 };
|
|
662
|
+
let fx = demandX;
|
|
663
|
+
if (Math.abs(fx) > capacity) {
|
|
664
|
+
fx = Math.sign(fx) * capacity;
|
|
665
|
+
if (driven && demandX > 0)
|
|
666
|
+
out.wheelspin = true;
|
|
667
|
+
else
|
|
668
|
+
out.wheelLock = true;
|
|
669
|
+
}
|
|
670
|
+
const remaining = Math.sqrt(Math.max(0, 1 - (fx / capacity) ** 2));
|
|
671
|
+
const fy = -capacity * lateralScale * remaining * lateralCurve(shape, slip);
|
|
672
|
+
const saturation = Math.max(Math.hypot(fx, fy) / capacity, Math.abs(slip) / peakAngle(shape));
|
|
673
|
+
if (axle === "front")
|
|
674
|
+
out.satFront = Math.max(out.satFront, saturation);
|
|
675
|
+
else
|
|
676
|
+
out.satRear = Math.max(out.satRear, saturation);
|
|
677
|
+
return { fx, fy };
|
|
678
|
+
}
|
|
679
|
+
function peakAngle(shape) {
|
|
680
|
+
return Math.tan(Math.PI / (2 * shape.c)) / shape.b;
|
|
681
|
+
}
|
|
682
|
+
function loadSensitivityFactor(transfer, load, sensitivity) {
|
|
683
|
+
if (load <= 0)
|
|
684
|
+
return 1;
|
|
685
|
+
const ratio = Math.min(1, transfer / load);
|
|
686
|
+
return Math.max(0.5, 1 - sensitivity * ratio * ratio);
|
|
687
|
+
}
|
|
688
|
+
function forwardOf(heading) {
|
|
689
|
+
return [Math.sin(heading), Math.cos(heading)];
|
|
690
|
+
}
|
|
691
|
+
function clamp01(value) {
|
|
692
|
+
return Math.max(0, Math.min(1, value));
|
|
693
|
+
}
|
|
694
|
+
function freshState(tuning, position, heading) {
|
|
695
|
+
return {
|
|
696
|
+
x: position[0],
|
|
697
|
+
y: position[1],
|
|
698
|
+
z: position[2],
|
|
699
|
+
heading,
|
|
700
|
+
vx: 0,
|
|
701
|
+
vz: 0,
|
|
702
|
+
yawRate: 0,
|
|
703
|
+
steerAngle: 0,
|
|
704
|
+
gearIndex: 0,
|
|
705
|
+
reversing: false,
|
|
706
|
+
shiftRemaining: 0,
|
|
707
|
+
rpm: tuning.powertrain.kind === "gearbox" ? tuning.powertrain.idleRpm : 0,
|
|
708
|
+
longitudinalAccel: 0,
|
|
709
|
+
lateralAccel: 0,
|
|
710
|
+
bodyPitch: 0,
|
|
711
|
+
bodyRoll: 0,
|
|
712
|
+
vy: 0,
|
|
713
|
+
pitchRate: 0,
|
|
714
|
+
rollRate: 0,
|
|
715
|
+
compression: [0, 0, 0, 0],
|
|
716
|
+
airborne: false,
|
|
717
|
+
lean: 0,
|
|
718
|
+
leanTrim: 0,
|
|
719
|
+
jumpsUsed: 0,
|
|
720
|
+
airTime: 0,
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
/** The lean that balances a lateral acceleration: a bike leans until gravity's moment matches the corner's. */
|
|
724
|
+
function balanceLean(lateralAccel) {
|
|
725
|
+
return Math.atan(lateralAccel / GRAVITY);
|
|
726
|
+
}
|
|
727
|
+
function clampUnit(value) {
|
|
728
|
+
return Math.max(-1, Math.min(1, value));
|
|
729
|
+
}
|
|
730
|
+
/** Angular rate driven by input acceleration and slowed by damping, capped at `maxRate` only while input pushes past it. */
|
|
731
|
+
function driveRate(rate, accel, damping, maxRate, h) {
|
|
732
|
+
let next = rate + (accel - damping * rate) * h;
|
|
733
|
+
if (Math.abs(next) > maxRate && Math.sign(accel) === Math.sign(next) && Math.abs(next) > Math.abs(rate)) {
|
|
734
|
+
next = Math.sign(next) * Math.max(maxRate, Math.abs(rate));
|
|
735
|
+
}
|
|
736
|
+
return next;
|
|
737
|
+
}
|