@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,606 @@
|
|
|
1
|
+
const KMH100 = 100 / 3.6;
|
|
2
|
+
const RAD_TO_DEG = 180 / Math.PI;
|
|
3
|
+
function input(throttle, brake, steer, handbrake = 0) {
|
|
4
|
+
return { throttle, brake, steer, handbrake };
|
|
5
|
+
}
|
|
6
|
+
function sideslipOf(step) {
|
|
7
|
+
return Math.hypot(step.forwardSpeed, step.lateralSpeed) < 1 ? 0 : Math.atan2(Math.abs(step.lateralSpeed), step.forwardSpeed);
|
|
8
|
+
}
|
|
9
|
+
function holdSpeed(target, current) {
|
|
10
|
+
const error = target - current;
|
|
11
|
+
return error >= 0 ? { throttle: Math.min(1, error * 0.6 + 0.25), brake: 0 } : { throttle: 0, brake: Math.min(1, -error * 0.3) };
|
|
12
|
+
}
|
|
13
|
+
function speedOf(subject) {
|
|
14
|
+
const [vx, vz] = subject.velocity();
|
|
15
|
+
return Math.hypot(vx, vz);
|
|
16
|
+
}
|
|
17
|
+
function launchTo(subject, speed, dt) {
|
|
18
|
+
let last = 0;
|
|
19
|
+
for (let i = 0; i < Math.ceil(60 / dt); i += 1) {
|
|
20
|
+
last = subject.tick(dt, input(1, 0, 0)).forwardSpeed;
|
|
21
|
+
if (last >= speed)
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
for (let i = 0; i < Math.ceil(0.5 / dt); i += 1) {
|
|
25
|
+
const hold = holdSpeed(speed, last);
|
|
26
|
+
last = subject.tick(dt, input(hold.throttle, hold.brake, 0)).forwardSpeed;
|
|
27
|
+
}
|
|
28
|
+
return last;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Drives fresh instances from `create` through fixed scenarios (launch, top speed, braking, a slow steer
|
|
32
|
+
* ramp, a step steer, a mid-corner lift-off, a handbrake pull, full throttle with full steer) and reports the feel metrics a test can
|
|
33
|
+
* assert. Deterministic: the same subject and options always produce the same report, so a tuning change
|
|
34
|
+
* shows up as a number moving, not an opinion.
|
|
35
|
+
* @capability handling-metrics measure a vehicle's feel as numbers — 0–100, braking, lateral g, turn-in, sideslip, spin
|
|
36
|
+
*/
|
|
37
|
+
export function measureHandling(create, options = {}) {
|
|
38
|
+
const dt = options.dt ?? 1 / 60;
|
|
39
|
+
const cornerSpeed = options.cornerSpeed ?? 25;
|
|
40
|
+
const stepSteer = options.stepSteer ?? 1;
|
|
41
|
+
const liftOffSteer = options.liftOffSteer ?? 0.3;
|
|
42
|
+
let zeroTo100 = Number.POSITIVE_INFINITY;
|
|
43
|
+
let topSpeed = 0;
|
|
44
|
+
{
|
|
45
|
+
const car = create();
|
|
46
|
+
for (let i = 1; i <= Math.ceil(40 / dt); i += 1) {
|
|
47
|
+
const step = car.tick(dt, input(1, 0, 0));
|
|
48
|
+
if (zeroTo100 === Number.POSITIVE_INFINITY && step.forwardSpeed >= KMH100)
|
|
49
|
+
zeroTo100 = i * dt;
|
|
50
|
+
topSpeed = step.forwardSpeed;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
let brake100To0 = Number.POSITIVE_INFINITY;
|
|
54
|
+
{
|
|
55
|
+
const car = create();
|
|
56
|
+
if (launchTo(car, KMH100, dt) >= KMH100 - 1) {
|
|
57
|
+
let distance = 0;
|
|
58
|
+
for (let i = 0; i < Math.ceil(20 / dt); i += 1) {
|
|
59
|
+
const step = car.tick(dt, input(0, 1, 0));
|
|
60
|
+
distance += Math.max(0, step.forwardSpeed) * dt;
|
|
61
|
+
if (step.forwardSpeed <= 0.3) {
|
|
62
|
+
brake100To0 = distance;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
let maxLateralG = 0;
|
|
69
|
+
{
|
|
70
|
+
const car = create();
|
|
71
|
+
let speed = launchTo(car, 20, dt);
|
|
72
|
+
let previous = car.velocity();
|
|
73
|
+
let filtered = 0;
|
|
74
|
+
for (let i = 0; i < Math.ceil(6 / dt); i += 1) {
|
|
75
|
+
const steer = Math.min(1, (i * dt) / 6);
|
|
76
|
+
const hold = holdSpeed(20, speed);
|
|
77
|
+
const step = car.tick(dt, input(hold.throttle, hold.brake, steer));
|
|
78
|
+
speed = step.forwardSpeed;
|
|
79
|
+
const velocity = car.velocity();
|
|
80
|
+
const ax = (velocity[0] - previous[0]) / dt;
|
|
81
|
+
const az = (velocity[1] - previous[1]) / dt;
|
|
82
|
+
previous = velocity;
|
|
83
|
+
const travel = Math.max(1e-6, Math.hypot(velocity[0], velocity[1]));
|
|
84
|
+
const lateral = Math.abs(ax * velocity[1] - az * velocity[0]) / travel;
|
|
85
|
+
filtered += (lateral - filtered) * 0.1;
|
|
86
|
+
if (sideslipOf(step) < 12 / RAD_TO_DEG)
|
|
87
|
+
maxLateralG = Math.max(maxLateralG, filtered / 9.81);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
let turnIn = Number.POSITIVE_INFINITY;
|
|
91
|
+
let stepPeakSideslipDeg = 0;
|
|
92
|
+
let stepSteadyYawRate = 0;
|
|
93
|
+
let stepSpeedRetained = 0;
|
|
94
|
+
let spun = false;
|
|
95
|
+
{
|
|
96
|
+
const car = create();
|
|
97
|
+
const entry = launchTo(car, cornerSpeed, dt);
|
|
98
|
+
const yaw = [];
|
|
99
|
+
let last = { forwardSpeed: entry, lateralSpeed: 0, yawRate: 0 };
|
|
100
|
+
for (let i = 0; i < Math.ceil(3 / dt); i += 1) {
|
|
101
|
+
last = car.tick(dt, input(0.35, 0, stepSteer));
|
|
102
|
+
yaw.push(Math.abs(last.yawRate));
|
|
103
|
+
stepPeakSideslipDeg = Math.max(stepPeakSideslipDeg, sideslipOf(last) * RAD_TO_DEG);
|
|
104
|
+
}
|
|
105
|
+
const window = yaw.slice(0, Math.ceil(1.5 / dt));
|
|
106
|
+
const peak = Math.max(...window);
|
|
107
|
+
const index = window.findIndex((value) => value >= peak * 0.9);
|
|
108
|
+
if (peak > 1e-3 && index >= 0)
|
|
109
|
+
turnIn = (index + 1) * dt;
|
|
110
|
+
stepSteadyYawRate = Math.abs(last.yawRate);
|
|
111
|
+
stepSpeedRetained = entry > 0 ? speedOf(car) / entry : 0;
|
|
112
|
+
spun = sideslipOf(last) > 60 / RAD_TO_DEG || last.forwardSpeed < 0;
|
|
113
|
+
}
|
|
114
|
+
let liftOffYawGain = 1;
|
|
115
|
+
{
|
|
116
|
+
const car = create();
|
|
117
|
+
let speed = launchTo(car, cornerSpeed, dt);
|
|
118
|
+
let yawBefore = 0;
|
|
119
|
+
for (let i = 0; i < Math.ceil(2.5 / dt); i += 1) {
|
|
120
|
+
const hold = holdSpeed(cornerSpeed, speed);
|
|
121
|
+
const step = car.tick(dt, input(hold.throttle, hold.brake, liftOffSteer));
|
|
122
|
+
speed = step.forwardSpeed;
|
|
123
|
+
yawBefore = Math.abs(step.yawRate);
|
|
124
|
+
}
|
|
125
|
+
let yawAfter = yawBefore;
|
|
126
|
+
for (let i = 0; i < Math.ceil(0.5 / dt); i += 1)
|
|
127
|
+
yawAfter = Math.abs(car.tick(dt, input(0, 0, liftOffSteer)).yawRate);
|
|
128
|
+
liftOffYawGain = yawBefore > 1e-3 ? yawAfter / yawBefore : 1;
|
|
129
|
+
}
|
|
130
|
+
let handbrakePeakSideslipDeg = 0;
|
|
131
|
+
let handbrakeRecoverySeconds = Number.POSITIVE_INFINITY;
|
|
132
|
+
{
|
|
133
|
+
const car = create();
|
|
134
|
+
launchTo(car, 20, dt);
|
|
135
|
+
const pullTicks = Math.ceil(0.6 / dt);
|
|
136
|
+
let settledSince = -1;
|
|
137
|
+
for (let i = 0; i < pullTicks + Math.ceil(3 / dt); i += 1) {
|
|
138
|
+
const pulling = i < pullTicks;
|
|
139
|
+
const step = car.tick(dt, input(0, 0, pulling ? 1 : 0, pulling ? 1 : 0));
|
|
140
|
+
const slip = sideslipOf(step) * RAD_TO_DEG;
|
|
141
|
+
if (i * dt < 1.5)
|
|
142
|
+
handbrakePeakSideslipDeg = Math.max(handbrakePeakSideslipDeg, slip);
|
|
143
|
+
if (pulling)
|
|
144
|
+
continue;
|
|
145
|
+
if (slip >= 5)
|
|
146
|
+
settledSince = -1;
|
|
147
|
+
else if (settledSince < 0)
|
|
148
|
+
settledSince = i;
|
|
149
|
+
if (settledSince >= 0 && (i - settledSince) * dt >= 0.3) {
|
|
150
|
+
handbrakeRecoverySeconds = (settledSince - pullTicks) * dt;
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
let powerSteerPeakSideslipDeg = 0;
|
|
156
|
+
let powerSteerSpun = false;
|
|
157
|
+
{
|
|
158
|
+
const car = create();
|
|
159
|
+
launchTo(car, 12, dt);
|
|
160
|
+
let last = { forwardSpeed: 0, lateralSpeed: 0, yawRate: 0 };
|
|
161
|
+
for (let i = 0; i < Math.ceil(2 / dt); i += 1) {
|
|
162
|
+
last = car.tick(dt, input(1, 0, 1));
|
|
163
|
+
powerSteerPeakSideslipDeg = Math.max(powerSteerPeakSideslipDeg, sideslipOf(last) * RAD_TO_DEG);
|
|
164
|
+
}
|
|
165
|
+
powerSteerSpun = sideslipOf(last) > 60 / RAD_TO_DEG || last.forwardSpeed < 0;
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
zeroTo100,
|
|
169
|
+
topSpeed,
|
|
170
|
+
brake100To0,
|
|
171
|
+
maxLateralG,
|
|
172
|
+
turnIn,
|
|
173
|
+
stepPeakSideslipDeg,
|
|
174
|
+
stepSteadyYawRate,
|
|
175
|
+
stepSpeedRetained,
|
|
176
|
+
spun,
|
|
177
|
+
liftOffYawGain,
|
|
178
|
+
handbrakePeakSideslipDeg,
|
|
179
|
+
handbrakeRecoverySeconds,
|
|
180
|
+
powerSteerPeakSideslipDeg,
|
|
181
|
+
powerSteerSpun,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Drives fresh sprung vehicles from `create` through a vertical kick, a hard stop, a steady corner and a
|
|
186
|
+
* launch-and-land, and reports how the body moves: how fast it settles, how much it dives and rolls, and
|
|
187
|
+
* whether a landing bounces.
|
|
188
|
+
* @capability ride-metrics measure a sprung vehicle's ride — settle time, dive, roll per g, landing bounce
|
|
189
|
+
*/
|
|
190
|
+
export function measureRide(create, options = {}) {
|
|
191
|
+
const dt = options.dt ?? 1 / 60;
|
|
192
|
+
const neutral = input(0, 0, 0);
|
|
193
|
+
let settleSeconds = Number.POSITIVE_INFINITY;
|
|
194
|
+
let heaveOvershoot = 0;
|
|
195
|
+
{
|
|
196
|
+
const car = create();
|
|
197
|
+
const rest = car.tick(dt, neutral).position[1];
|
|
198
|
+
car.applyImpulse(0, 0, 1);
|
|
199
|
+
let lastOutside = 0;
|
|
200
|
+
let rise = 0;
|
|
201
|
+
let drop = 0;
|
|
202
|
+
const ticks = Math.ceil(6 / dt);
|
|
203
|
+
for (let i = 1; i <= ticks; i += 1) {
|
|
204
|
+
const offset = car.tick(dt, neutral).position[1] - rest;
|
|
205
|
+
rise = Math.max(rise, offset);
|
|
206
|
+
drop = Math.max(drop, -offset);
|
|
207
|
+
if (Math.abs(offset) > 0.005)
|
|
208
|
+
lastOutside = i;
|
|
209
|
+
}
|
|
210
|
+
if (lastOutside < ticks)
|
|
211
|
+
settleSeconds = lastOutside * dt;
|
|
212
|
+
heaveOvershoot = rise > 0 ? drop / rise : 0;
|
|
213
|
+
}
|
|
214
|
+
let brakeDiveDeg = 0;
|
|
215
|
+
{
|
|
216
|
+
const car = create();
|
|
217
|
+
let speed = 0;
|
|
218
|
+
for (let i = 0; i < Math.ceil(30 / dt) && speed < 25; i += 1)
|
|
219
|
+
speed = car.tick(dt, input(1, 0, 0)).forwardSpeed;
|
|
220
|
+
for (let i = 0; i < Math.ceil(1.5 / dt); i += 1)
|
|
221
|
+
brakeDiveDeg = Math.max(brakeDiveDeg, car.tick(dt, input(0, 1, 0)).bodyPitch * RAD_TO_DEG);
|
|
222
|
+
}
|
|
223
|
+
let rollGradient = 0;
|
|
224
|
+
{
|
|
225
|
+
const car = create();
|
|
226
|
+
let speed = 0;
|
|
227
|
+
for (let i = 0; i < Math.ceil(30 / dt) && speed < 20; i += 1)
|
|
228
|
+
speed = car.tick(dt, input(1, 0, 0)).forwardSpeed;
|
|
229
|
+
let step = car.tick(dt, neutral);
|
|
230
|
+
for (let i = 0; i < Math.ceil(2.5 / dt); i += 1) {
|
|
231
|
+
const hold = holdSpeed(20, step.forwardSpeed);
|
|
232
|
+
step = car.tick(dt, input(hold.throttle, hold.brake, 0.5));
|
|
233
|
+
}
|
|
234
|
+
const g = Math.abs(step.lateralAccel) / 9.81;
|
|
235
|
+
rollGradient = g > 0.05 ? (Math.abs(step.bodyRoll) * RAD_TO_DEG) / g : 0;
|
|
236
|
+
}
|
|
237
|
+
let landingBounces = 0;
|
|
238
|
+
{
|
|
239
|
+
const car = create();
|
|
240
|
+
car.tick(dt, neutral);
|
|
241
|
+
car.applyImpulse(0, 0, 4);
|
|
242
|
+
let wasAirborne = false;
|
|
243
|
+
let landed = false;
|
|
244
|
+
for (let i = 0; i < Math.ceil(4 / dt); i += 1) {
|
|
245
|
+
const airborne = car.tick(dt, neutral).airborne;
|
|
246
|
+
if (wasAirborne && !airborne)
|
|
247
|
+
landed = true;
|
|
248
|
+
else if (landed && !wasAirborne && airborne)
|
|
249
|
+
landingBounces += 1;
|
|
250
|
+
wasAirborne = airborne;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return { settleSeconds, heaveOvershoot, brakeDiveDeg, rollGradient, landingBounces };
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Jumps a fresh vehicle from rest and holds full air input on each axis, reporting jump height and timing,
|
|
257
|
+
* double-jump height, and how fast the body rotates in the air.
|
|
258
|
+
* @capability air-metrics measure a vehicle's jumps and air control — apex height and time, double jump, air rotation rates
|
|
259
|
+
*/
|
|
260
|
+
export function measureAir(create, options = {}) {
|
|
261
|
+
const dt = options.dt ?? 1 / 60;
|
|
262
|
+
const neutral = input(0, 0, 0);
|
|
263
|
+
const flight = (secondJumpAtApex) => {
|
|
264
|
+
const car = create();
|
|
265
|
+
const rest = car.tick(dt, neutral).position[1];
|
|
266
|
+
car.jump();
|
|
267
|
+
let apex = 0;
|
|
268
|
+
let apexTime = 0;
|
|
269
|
+
let rising = true;
|
|
270
|
+
let last = rest;
|
|
271
|
+
for (let i = 1; i <= Math.ceil(4 / dt); i += 1) {
|
|
272
|
+
const y = car.tick(dt, neutral).position[1];
|
|
273
|
+
if (rising && y < last) {
|
|
274
|
+
rising = false;
|
|
275
|
+
if (secondJumpAtApex)
|
|
276
|
+
car.jump();
|
|
277
|
+
}
|
|
278
|
+
if (y - rest > apex) {
|
|
279
|
+
apex = y - rest;
|
|
280
|
+
apexTime = i * dt;
|
|
281
|
+
}
|
|
282
|
+
last = y;
|
|
283
|
+
}
|
|
284
|
+
return { apex, apexTime };
|
|
285
|
+
};
|
|
286
|
+
const single = flight(false);
|
|
287
|
+
const double = flight(true);
|
|
288
|
+
const rateAfter = (air, key) => {
|
|
289
|
+
const car = create();
|
|
290
|
+
car.tick(dt, neutral);
|
|
291
|
+
car.jump();
|
|
292
|
+
for (let i = 0; i < Math.ceil(0.4 / dt); i += 1)
|
|
293
|
+
car.tick(dt, neutral, { air });
|
|
294
|
+
return Math.abs(car.snapshot()[key]);
|
|
295
|
+
};
|
|
296
|
+
return {
|
|
297
|
+
jumpApexHeight: single.apex,
|
|
298
|
+
jumpApexTime: single.apexTime,
|
|
299
|
+
doubleJumpApexHeight: double.apex,
|
|
300
|
+
airPitchRate: rateAfter({ pitch: 1, yaw: 0, roll: 0 }, "pitchRate"),
|
|
301
|
+
airYawRate: rateAfter({ pitch: 0, yaw: 1, roll: 0 }, "yawRate"),
|
|
302
|
+
airRollRate: rateAfter({ pitch: 0, yaw: 0, roll: 1 }, "rollRate"),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function wrapAngle(angle) {
|
|
306
|
+
let a = angle;
|
|
307
|
+
while (a > Math.PI)
|
|
308
|
+
a -= 2 * Math.PI;
|
|
309
|
+
while (a < -Math.PI)
|
|
310
|
+
a += 2 * Math.PI;
|
|
311
|
+
return a;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Drives a fresh vehicle from `create` through a steer ramp, a skidpad and a slalom with simple
|
|
315
|
+
* deterministic drivers, and reports understeer gradient, skidpad g and the fastest clean slalom.
|
|
316
|
+
* @capability course-metrics measure a vehicle on driven tests — understeer gradient, skidpad g, slalom speed
|
|
317
|
+
*/
|
|
318
|
+
export function measureCourse(create, options) {
|
|
319
|
+
const dt = options.dt ?? 1 / 60;
|
|
320
|
+
const wheelbase = options.wheelbase;
|
|
321
|
+
const radius = options.skidpadRadius ?? 30;
|
|
322
|
+
const spacing = options.slalomSpacing ?? 18;
|
|
323
|
+
const offset = options.slalomOffset ?? 3.5;
|
|
324
|
+
let understeerGradient = Number.NaN;
|
|
325
|
+
{
|
|
326
|
+
const car = create();
|
|
327
|
+
let speed = launchTo(car, 20, dt);
|
|
328
|
+
let sum = 0;
|
|
329
|
+
let count = 0;
|
|
330
|
+
for (let i = 0; i < Math.ceil(8 / dt); i += 1) {
|
|
331
|
+
const steer = Math.min(1, (i * dt) / 8) * 0.5;
|
|
332
|
+
const hold = holdSpeed(20, speed);
|
|
333
|
+
const step = car.tick(dt, input(hold.throttle, hold.brake, steer));
|
|
334
|
+
speed = step.forwardSpeed;
|
|
335
|
+
const lateralG = Math.abs(step.yawRate * speed) / 9.81;
|
|
336
|
+
if (lateralG < 0.1 || lateralG > 0.4 || speed < 5 || sideslipOf(step) > 4 / RAD_TO_DEG)
|
|
337
|
+
continue;
|
|
338
|
+
const geometric = Math.atan((wheelbase * Math.abs(step.yawRate)) / speed);
|
|
339
|
+
sum += ((Math.abs(step.steerAngle) - geometric) * RAD_TO_DEG) / lateralG;
|
|
340
|
+
count += 1;
|
|
341
|
+
}
|
|
342
|
+
if (count > 0)
|
|
343
|
+
understeerGradient = sum / count;
|
|
344
|
+
}
|
|
345
|
+
let skidpadG = 0;
|
|
346
|
+
{
|
|
347
|
+
const car = create();
|
|
348
|
+
let speed = launchTo(car, 8, dt);
|
|
349
|
+
let target = 8;
|
|
350
|
+
let steer = 0;
|
|
351
|
+
for (let i = 0; i < Math.ceil(60 / dt); i += 1) {
|
|
352
|
+
target += 0.25 * dt;
|
|
353
|
+
const hold = holdSpeed(target, speed);
|
|
354
|
+
const step = car.tick(dt, input(hold.throttle, hold.brake, steer));
|
|
355
|
+
speed = step.forwardSpeed;
|
|
356
|
+
const wanted = speed / radius;
|
|
357
|
+
const actual = -step.yawRate;
|
|
358
|
+
steer = Math.max(0, Math.min(1, steer + 1.5 * (wanted - actual) * dt));
|
|
359
|
+
const holding = Math.abs(actual - wanted) < wanted * 0.03 && sideslipOf(step) < 12 / RAD_TO_DEG;
|
|
360
|
+
if (i * dt > 3 && holding)
|
|
361
|
+
skidpadG = Math.max(skidpadG, (speed * speed) / radius / 9.81);
|
|
362
|
+
if (steer >= 1 && actual < wanted * 0.9)
|
|
363
|
+
break;
|
|
364
|
+
if (sideslipOf(step) > 45 / RAD_TO_DEG)
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
let slalomSpeed = 0;
|
|
369
|
+
const lineX = (z) => offset * Math.sin((Math.PI * z) / spacing);
|
|
370
|
+
const cleanRun = (entry) => {
|
|
371
|
+
const car = create();
|
|
372
|
+
let speed = launchTo(car, entry, dt);
|
|
373
|
+
const start = car.pose().position;
|
|
374
|
+
let cone = 0;
|
|
375
|
+
for (let i = 0; i < Math.ceil(30 / dt); i += 1) {
|
|
376
|
+
const pose = car.pose();
|
|
377
|
+
const z = pose.position[2] - start[2];
|
|
378
|
+
const coneZ = spacing * (cone + 0.5);
|
|
379
|
+
if (z >= coneZ) {
|
|
380
|
+
const side = Math.sign(lineX(coneZ));
|
|
381
|
+
if ((pose.position[0] - start[0]) * side < 1.2)
|
|
382
|
+
return false;
|
|
383
|
+
cone += 1;
|
|
384
|
+
if (cone === 8)
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
const lookahead = Math.max(5, speed * 0.35);
|
|
388
|
+
const tx = start[0] + lineX(z + lookahead);
|
|
389
|
+
const tz = pose.position[2] + lookahead;
|
|
390
|
+
const bearing = Math.atan2(tx - pose.position[0], tz - pose.position[2]);
|
|
391
|
+
const error = wrapAngle(bearing - pose.heading);
|
|
392
|
+
const hold = holdSpeed(entry, speed);
|
|
393
|
+
const step = car.tick(dt, input(hold.throttle, hold.brake, Math.max(-1, Math.min(1, -error * 3))));
|
|
394
|
+
speed = step.forwardSpeed;
|
|
395
|
+
if (sideslipOf(step) > 30 / RAD_TO_DEG)
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
return false;
|
|
399
|
+
};
|
|
400
|
+
for (let entry = 8; entry <= 40; entry += 1) {
|
|
401
|
+
if (!cleanRun(entry))
|
|
402
|
+
break;
|
|
403
|
+
slalomSpeed = entry;
|
|
404
|
+
}
|
|
405
|
+
return { understeerGradient, skidpadG, slalomSpeed };
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Brings a fresh bike to `speed` and holds full steer, reporting how far and how fast it leans and how much it
|
|
409
|
+
* tips the other way first.
|
|
410
|
+
* @capability lean-metrics measure a motorcycle's lean — steady lean, time to lean, countersteer tip-in
|
|
411
|
+
*/
|
|
412
|
+
export function measureLean(create, options = {}) {
|
|
413
|
+
const dt = options.dt ?? 1 / 60;
|
|
414
|
+
const speed = options.speed ?? 20;
|
|
415
|
+
const bike = create();
|
|
416
|
+
let current = launchTo(bike, speed, dt);
|
|
417
|
+
const leans = [];
|
|
418
|
+
for (let i = 0; i < Math.ceil(2 / dt); i += 1) {
|
|
419
|
+
const hold = holdSpeed(speed, current);
|
|
420
|
+
const step = bike.tick(dt, input(hold.throttle, hold.brake, 1));
|
|
421
|
+
current = step.forwardSpeed;
|
|
422
|
+
leans.push(step.lean);
|
|
423
|
+
}
|
|
424
|
+
const steady = leans[leans.length - 1] ?? 0;
|
|
425
|
+
const index = leans.findIndex((lean) => Math.sign(lean) === Math.sign(steady) && Math.abs(lean) >= Math.abs(steady) * 0.9);
|
|
426
|
+
let counter = 0;
|
|
427
|
+
for (let i = 0; i < Math.min(leans.length, Math.ceil(0.4 / dt)); i += 1) {
|
|
428
|
+
if (Math.sign(leans[i]) === -Math.sign(steady))
|
|
429
|
+
counter = Math.max(counter, Math.abs(leans[i]));
|
|
430
|
+
}
|
|
431
|
+
return {
|
|
432
|
+
steadyLeanDeg: Math.abs(steady) * RAD_TO_DEG,
|
|
433
|
+
timeToLean: index < 0 ? Number.POSITIVE_INFINITY : (index + 1) * dt,
|
|
434
|
+
counterLeanDeg: counter * RAD_TO_DEG,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
function flightInput(throttle, pitch, roll, collective = 0) {
|
|
438
|
+
return { throttle, pitch: Math.max(-1, Math.min(1, pitch)), roll: Math.max(-1, Math.min(1, roll)), yaw: 0, collective };
|
|
439
|
+
}
|
|
440
|
+
function wrapPi(angle) {
|
|
441
|
+
let a = angle;
|
|
442
|
+
while (a > Math.PI)
|
|
443
|
+
a -= 2 * Math.PI;
|
|
444
|
+
while (a < -Math.PI)
|
|
445
|
+
a += 2 * Math.PI;
|
|
446
|
+
return a;
|
|
447
|
+
}
|
|
448
|
+
/** A PI loop on vertical speed toward a target altitude, with pitch-rate damping; returns the pitch command. */
|
|
449
|
+
function createAltitudeHold(targetY) {
|
|
450
|
+
let integral = 0;
|
|
451
|
+
return (step, dt) => {
|
|
452
|
+
const climbTarget = Math.max(-5, Math.min(5, 0.2 * (targetY - step.position[1])));
|
|
453
|
+
const error = climbTarget - step.velocity[1];
|
|
454
|
+
integral = Math.max(-1, Math.min(1, integral + 0.03 * error * dt));
|
|
455
|
+
return integral + 0.05 * error - 0.3 * step.pitchRate;
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
function bankHold(step, target) {
|
|
459
|
+
return 2 * wrapPi(target - step.bank) - 0.5 * step.rollRate;
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Flies fresh aircraft from `create` through fixed scenarios with simple deterministic autopilots: a full-aileron
|
|
463
|
+
* roll, banked turns at full throttle, an idle deceleration holding altitude, a full-throttle climb at cruise
|
|
464
|
+
* speed, a throttle step from idle, and a hands-off hover. `create` gets a spawn to pass straight into
|
|
465
|
+
* `createRigidAircraft`. Deterministic, so a tuning change shows up as a number moving.
|
|
466
|
+
* @capability flight-metrics measure an aircraft's feel as numbers — roll rate, sustained turn, stall speed, climb, throttle response, hover drift
|
|
467
|
+
*/
|
|
468
|
+
export function measureFlight(create, options = {}) {
|
|
469
|
+
const dt = options.dt ?? 1 / 60;
|
|
470
|
+
const cruise = options.cruiseSpeed ?? 60;
|
|
471
|
+
const probe = create({ position: [0, 0, 0], velocity: [0, 0, 0] });
|
|
472
|
+
const tuning = probe.tuning();
|
|
473
|
+
const rotor = tuning.rotor;
|
|
474
|
+
const steps = (seconds) => Math.ceil(seconds / dt);
|
|
475
|
+
if (rotor !== undefined) {
|
|
476
|
+
const altitude = options.altitude ?? 50;
|
|
477
|
+
const collective = options.hoverCollective ?? Math.min(1, (tuning.massKg * 9.81) / rotor.maxThrust);
|
|
478
|
+
const hovering = () => {
|
|
479
|
+
const aircraft = create({ position: [0, altitude, 0], velocity: [0, 0, 0] });
|
|
480
|
+
aircraft.restore({ ...aircraft.snapshot(), rotorSpeed: 1 });
|
|
481
|
+
return aircraft;
|
|
482
|
+
};
|
|
483
|
+
let rollRate = 0;
|
|
484
|
+
{
|
|
485
|
+
const aircraft = hovering();
|
|
486
|
+
for (let i = 0; i < steps(2); i += 1)
|
|
487
|
+
rollRate = Math.max(rollRate, Math.abs(aircraft.tick(dt, flightInput(1, 0, 1, collective)).rollRate));
|
|
488
|
+
}
|
|
489
|
+
let climbRate = 0;
|
|
490
|
+
{
|
|
491
|
+
const aircraft = hovering();
|
|
492
|
+
for (let i = 0; i < steps(5); i += 1)
|
|
493
|
+
climbRate = aircraft.tick(dt, flightInput(1, 0, 0, 1)).velocity[1];
|
|
494
|
+
}
|
|
495
|
+
let drift = 0;
|
|
496
|
+
{
|
|
497
|
+
const aircraft = hovering();
|
|
498
|
+
let step = aircraft.tick(dt, flightInput(1, 0, 0, collective));
|
|
499
|
+
for (let i = 1; i < steps(10); i += 1)
|
|
500
|
+
step = aircraft.tick(dt, flightInput(1, 0, 0, collective));
|
|
501
|
+
drift = Math.hypot(step.position[0], step.position[2]);
|
|
502
|
+
}
|
|
503
|
+
const response = throttleStep(() => {
|
|
504
|
+
const aircraft = create({ position: [0, altitude, 0], velocity: [0, 0, 0] });
|
|
505
|
+
aircraft.restore({ ...aircraft.snapshot(), rotorSpeed: 0 });
|
|
506
|
+
return aircraft;
|
|
507
|
+
}, (step) => (step.rotor?.speed ?? 0) ** 2, collective, dt);
|
|
508
|
+
return {
|
|
509
|
+
rollRateDeg: rollRate * RAD_TO_DEG,
|
|
510
|
+
sustainedTurnRateDeg: Number.NaN,
|
|
511
|
+
stallSpeed: Number.NaN,
|
|
512
|
+
climbRate,
|
|
513
|
+
throttleResponse: response,
|
|
514
|
+
hoverDriftMeters: drift,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
const altitude = options.altitude ?? 1000;
|
|
518
|
+
const cruising = () => create({ position: [0, altitude, 0], velocity: [0, 0, cruise] });
|
|
519
|
+
let rollRate = 0;
|
|
520
|
+
{
|
|
521
|
+
const aircraft = cruising();
|
|
522
|
+
for (let i = 0; i < steps(3); i += 1)
|
|
523
|
+
rollRate = Math.max(rollRate, Math.abs(aircraft.tick(dt, flightInput(0.5, 0, 1)).rollRate));
|
|
524
|
+
}
|
|
525
|
+
let sustained = 0;
|
|
526
|
+
for (let bankDeg = 10; bankDeg <= 85; bankDeg += 5) {
|
|
527
|
+
const aircraft = cruising();
|
|
528
|
+
const hold = createAltitudeHold(altitude);
|
|
529
|
+
const bank = bankDeg / RAD_TO_DEG;
|
|
530
|
+
let step = aircraft.tick(dt, flightInput(1, 0, 0));
|
|
531
|
+
let speedAt20 = 0;
|
|
532
|
+
let headingAt20 = 0;
|
|
533
|
+
let turned = 0;
|
|
534
|
+
let failed = false;
|
|
535
|
+
for (let i = 1; i < steps(30); i += 1) {
|
|
536
|
+
const previous = step.heading;
|
|
537
|
+
step = aircraft.tick(dt, flightInput(1, hold(step, dt), bankHold(step, bank)));
|
|
538
|
+
turned += wrapPi(step.heading - previous);
|
|
539
|
+
if (i === steps(20)) {
|
|
540
|
+
speedAt20 = step.airspeed;
|
|
541
|
+
headingAt20 = turned;
|
|
542
|
+
}
|
|
543
|
+
if (step.stalled || Math.abs(step.position[1] - altitude) > 60)
|
|
544
|
+
failed = true;
|
|
545
|
+
}
|
|
546
|
+
if (failed || step.airspeed < speedAt20 - 2)
|
|
547
|
+
continue;
|
|
548
|
+
sustained = Math.max(sustained, Math.abs(turned - headingAt20) / 10);
|
|
549
|
+
}
|
|
550
|
+
let stallSpeed = Number.NaN;
|
|
551
|
+
{
|
|
552
|
+
const aircraft = cruising();
|
|
553
|
+
const hold = createAltitudeHold(altitude);
|
|
554
|
+
let step = aircraft.tick(dt, flightInput(0, 0, 0));
|
|
555
|
+
for (let i = 0; i < steps(120); i += 1) {
|
|
556
|
+
step = aircraft.tick(dt, flightInput(0, hold(step, dt), bankHold(step, 0)));
|
|
557
|
+
if (step.stalled || step.velocity[1] < -3) {
|
|
558
|
+
stallSpeed = step.airspeed;
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
let climbRate = 0;
|
|
564
|
+
{
|
|
565
|
+
const aircraft = cruising();
|
|
566
|
+
let integral = 0;
|
|
567
|
+
let step = aircraft.tick(dt, flightInput(1, 0, 0));
|
|
568
|
+
let sum = 0;
|
|
569
|
+
for (let i = 1; i < steps(30); i += 1) {
|
|
570
|
+
// Speed above cruise pitches the nose up (trading it for climb); an attitude loop flies that pitch.
|
|
571
|
+
const error = step.airspeed - cruise;
|
|
572
|
+
integral = Math.max(-0.3, Math.min(0.6, integral + 0.005 * error * dt));
|
|
573
|
+
const pitchTarget = Math.max(-0.3, Math.min(0.8, integral + 0.02 * error));
|
|
574
|
+
step = aircraft.tick(dt, flightInput(1, 3 * (pitchTarget - step.pitch) - 0.5 * step.pitchRate, bankHold(step, 0)));
|
|
575
|
+
if (i >= steps(25))
|
|
576
|
+
sum += step.velocity[1];
|
|
577
|
+
}
|
|
578
|
+
climbRate = sum / (steps(30) - steps(25));
|
|
579
|
+
}
|
|
580
|
+
const response = tuning.engine === undefined
|
|
581
|
+
? Number.NaN
|
|
582
|
+
: throttleStep(() => {
|
|
583
|
+
const aircraft = cruising();
|
|
584
|
+
aircraft.restore({ ...aircraft.snapshot(), spool: 0 });
|
|
585
|
+
return aircraft;
|
|
586
|
+
}, (step) => step.thrust, 0, dt);
|
|
587
|
+
return {
|
|
588
|
+
rollRateDeg: rollRate * RAD_TO_DEG,
|
|
589
|
+
sustainedTurnRateDeg: sustained * RAD_TO_DEG,
|
|
590
|
+
stallSpeed,
|
|
591
|
+
climbRate,
|
|
592
|
+
throttleResponse: response,
|
|
593
|
+
hoverDriftMeters: Number.NaN,
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
function throttleStep(create, thrustOf, collective, dt) {
|
|
597
|
+
const trace = [];
|
|
598
|
+
const aircraft = create();
|
|
599
|
+
for (let i = 0; i < Math.ceil(20 / dt); i += 1)
|
|
600
|
+
trace.push(thrustOf(aircraft.tick(dt, flightInput(1, 0, 0, collective))));
|
|
601
|
+
const final = trace[trace.length - 1] ?? 0;
|
|
602
|
+
if (final <= 0)
|
|
603
|
+
return Number.POSITIVE_INFINITY;
|
|
604
|
+
const index = trace.findIndex((thrust) => thrust >= final * 0.9);
|
|
605
|
+
return index < 0 ? Number.POSITIVE_INFINITY : (index + 1) * dt;
|
|
606
|
+
}
|
|
@@ -37,7 +37,10 @@ export interface KinematicVehicleTuning {
|
|
|
37
37
|
hopGravity?: number;
|
|
38
38
|
/** Optional higher-fidelity powertrain. Omit to preserve the direct arcade acceleration model. */
|
|
39
39
|
powertrain?: KinematicPowertrainTuning;
|
|
40
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Optional bicycle-model steering with speed-sensitive lock and smoothed rack response.
|
|
42
|
+
* @deprecated Heading here is still commanded from steer, so it can't produce real understeer or oversteer. Use `createVehicleDynamics` (`physics/vehicleDynamics`) when handling matters.
|
|
43
|
+
*/
|
|
41
44
|
steering?: KinematicSteeringTuning;
|
|
42
45
|
/** Optional aerodynamic drag/downforce and electronic driver assists. */
|
|
43
46
|
dynamics?: KinematicDynamicsTuning;
|
|
@@ -46,11 +49,12 @@ export interface KinematicVehicleTuning {
|
|
|
46
49
|
* with force/mass dynamics — a tire friction budget, lateral weight transfer, and engine braking — so
|
|
47
50
|
* vehicles of different mass, drive/brake force, and center-of-gravity drive measurably differently.
|
|
48
51
|
* Omit to preserve the direct arcade acceleration model exactly.
|
|
52
|
+
* @deprecated The friction budget clamps lateral speed while heading keeps turning at the commanded rate, so a held steer at speed spins the car. Use `createVehicleDynamics` (`physics/vehicleDynamics`), whose yaw comes from tire forces.
|
|
49
53
|
*/
|
|
50
54
|
chassis?: KinematicChassisTuning;
|
|
51
55
|
/** Per-axis bindings for this vehicle — which actions drive throttle/brake/steer/handbrake. Unlisted axes keep car defaults (W/S/A/D/Space). */
|
|
52
56
|
bindings?: Partial<Record<"throttle" | "brake" | "steer" | "handbrake", AxisBinding>>;
|
|
53
|
-
/** Camera POV while this vehicle is piloted
|
|
57
|
+
/** Camera POV while this vehicle is piloted. Nothing applies it automatically: pass it to `ctx.camera.setChaseTuning` when the driver enters and `null` when they exit. */
|
|
54
58
|
camera?: ChaseCameraTuning | null;
|
|
55
59
|
}
|
|
56
60
|
/**
|
|
@@ -126,10 +130,15 @@ export interface KinematicVehicleOptions {
|
|
|
126
130
|
/**
|
|
127
131
|
* Veto or clamp a planar move before it commits — walls, closed gates, arena bounds. Given the
|
|
128
132
|
* attempted `from`→`to` on the XZ plane, return the destination actually allowed; velocity is
|
|
129
|
-
* rederived from the permitted displacement, so motion blocked on an axis stops on that axis
|
|
130
|
-
*
|
|
133
|
+
* rederived from the permitted displacement, so motion blocked on an axis stops on that axis.
|
|
134
|
+
* A result may carry `motion`, the permitted XZ displacement in world units before position
|
|
135
|
+
* recovery. Use it when the endpoint also depenetrates the body: recovery must not add velocity.
|
|
136
|
+
* Plain tuple results retain displacement-based velocity. Called once per tick; no purity required.
|
|
137
|
+
* Default: no clamp (the move always commits).
|
|
131
138
|
*/
|
|
132
|
-
clampMove?: (from: readonly [number, number], to: readonly [number, number]) => readonly [number, number]
|
|
139
|
+
clampMove?: (from: readonly [number, number], to: readonly [number, number]) => readonly [number, number] & {
|
|
140
|
+
readonly motion?: readonly [number, number];
|
|
141
|
+
};
|
|
133
142
|
}
|
|
134
143
|
/**
|
|
135
144
|
* Per-tick multipliers layered over the base tuning — the transient overrides games apply for one
|
|
@@ -184,6 +193,25 @@ export interface KinematicVehicleStep {
|
|
|
184
193
|
/** `true` while `airOffset > 0` — gate landing effects, air control, and trick scoring off this. */
|
|
185
194
|
airborne: boolean;
|
|
186
195
|
}
|
|
196
|
+
/** Serializable integrator state of a {@link KinematicVehicle}: what `restore` needs to resume bit-for-bit. */
|
|
197
|
+
export interface KinematicVehicleState {
|
|
198
|
+
x: number;
|
|
199
|
+
y: number;
|
|
200
|
+
z: number;
|
|
201
|
+
heading: number;
|
|
202
|
+
vx: number;
|
|
203
|
+
vz: number;
|
|
204
|
+
gearIndex: number;
|
|
205
|
+
rpm: number;
|
|
206
|
+
shiftRemaining: number;
|
|
207
|
+
steerAngle: number;
|
|
208
|
+
yawRate: number;
|
|
209
|
+
previousForwardSpeed: number;
|
|
210
|
+
bodyPitch: number;
|
|
211
|
+
bodyRoll: number;
|
|
212
|
+
airOffset: number;
|
|
213
|
+
verticalVelocity: number;
|
|
214
|
+
}
|
|
187
215
|
/**
|
|
188
216
|
* The pure-kinematic arcade car every racing game hand-rolled (#282.1): steer-yaw scaled by speed,
|
|
189
217
|
* throttle/brake acceleration, and a grip-curve lateral-slip bleed — no `PhysicsWorld`, no wheels,
|
|
@@ -212,8 +240,15 @@ export interface KinematicVehicle {
|
|
|
212
240
|
* would. For a change that lasts a single tick, pass {@link KinematicVehicleModifiers} instead.
|
|
213
241
|
*/
|
|
214
242
|
retune(next: KinematicVehicleTuning): void;
|
|
243
|
+
snapshot(): KinematicVehicleState;
|
|
244
|
+
restore(state: KinematicVehicleState): void;
|
|
215
245
|
resetTo(position: readonly [number, number, number], heading: number): void;
|
|
216
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* Arcade ground-vehicle sim (car, kart, bike) with throttle, brake, steer, drift and retunable handling.
|
|
249
|
+
*
|
|
250
|
+
* @capability kinematic-vehicle arcade car/kart driving physics with throttle, brake, steer and retunable handling
|
|
251
|
+
*/
|
|
217
252
|
export declare function createKinematicVehicle(initialTuning: KinematicVehicleTuning, options?: KinematicVehicleOptions): KinematicVehicle;
|
|
218
253
|
/** Default reverse drive force as a fraction of forward `engineForce` / `engineAccel`. */
|
|
219
254
|
export declare const DEFAULT_REVERSE_FORCE_SCALE = 0.48;
|