@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
|
@@ -1,21 +1,45 @@
|
|
|
1
1
|
import type { ActionCodesMap } from "./actionBindings.js";
|
|
2
|
+
import type { AxisBinding } from "./axisInput.js";
|
|
3
|
+
import type { AxisShapeConfig } from "./axisShaper.js";
|
|
4
|
+
/** Serializable axis shaping a context may carry: {@link AxisShapeConfig} without the `scale` callback. */
|
|
5
|
+
export type ActionContextAxisShape = Omit<AxisShapeConfig, "scale">;
|
|
2
6
|
/** Named action-binding layer that may optionally expose lower layers. */
|
|
3
7
|
export interface ActionContext {
|
|
4
8
|
id: string;
|
|
5
9
|
codes: ActionCodesMap;
|
|
6
10
|
passthrough: boolean;
|
|
11
|
+
/** Axis bindings (by action name) this context brings, e.g. `steer` while driving; read with `activeAxes()`. */
|
|
12
|
+
axes?: Record<string, AxisBinding>;
|
|
13
|
+
/** Per-axis shaping this context brings; feed it to `createAxisShaper(...).retune` when the stack changes. */
|
|
14
|
+
shaping?: Record<string, ActionContextAxisShape>;
|
|
7
15
|
}
|
|
8
16
|
/** Serializable state for an action-context stack. */
|
|
9
17
|
export interface ActionContextStackSnapshot {
|
|
10
18
|
contexts: ActionContext[];
|
|
11
19
|
}
|
|
20
|
+
/** Axis bindings and shaping merged across the active contexts. */
|
|
21
|
+
export interface ActiveContextAxes {
|
|
22
|
+
bindings: Record<string, AxisBinding>;
|
|
23
|
+
shaping: Record<string, ActionContextAxisShape>;
|
|
24
|
+
}
|
|
12
25
|
/** Mutable layered action-map stack with snapshot and restore support. */
|
|
13
26
|
export interface ActionContextStack {
|
|
14
27
|
push(context: ActionContext): void;
|
|
15
28
|
pop(id: string): boolean;
|
|
16
29
|
active(): ActionCodesMap;
|
|
30
|
+
/** Axis bindings and shaping of the active contexts, merged top-down like {@link active}. */
|
|
31
|
+
activeAxes(): ActiveContextAxes;
|
|
32
|
+
/** Ids of the stacked contexts, bottom first. */
|
|
33
|
+
ids(): readonly string[];
|
|
34
|
+
/** Bumps on every push, pop and restore; cheap change detection for per-frame readers. */
|
|
35
|
+
version(): number;
|
|
36
|
+
/** Called after every push, pop and restore; returns an unsubscribe. The shell rebinds live input here. */
|
|
37
|
+
subscribe(listener: () => void): () => void;
|
|
17
38
|
snapshot(): ActionContextStackSnapshot;
|
|
18
39
|
restore(snapshot: ActionContextStackSnapshot): void;
|
|
19
40
|
}
|
|
20
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Creates a serializable stack of layered action maps for menus and gameplay modes.
|
|
43
|
+
* @capability action-contexts Swap bindings live for on-foot, driving, menu or build modes by pushing and popping layered action maps.
|
|
44
|
+
*/
|
|
21
45
|
export declare function createActionContextStack(): ActionContextStack;
|
|
@@ -12,45 +12,102 @@ function cloneCodes(codes) {
|
|
|
12
12
|
}
|
|
13
13
|
return result;
|
|
14
14
|
}
|
|
15
|
-
|
|
15
|
+
function cloneAxes(axes) {
|
|
16
|
+
const result = {};
|
|
17
|
+
for (const [axis, binding] of Object.entries(axes)) {
|
|
18
|
+
result[axis] = {
|
|
19
|
+
...binding,
|
|
20
|
+
positive: [...binding.positive],
|
|
21
|
+
...(binding.negative === undefined ? {} : { negative: [...binding.negative] }),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
function cloneShaping(shaping) {
|
|
27
|
+
const result = {};
|
|
28
|
+
for (const [axis, shape] of Object.entries(shaping)) {
|
|
29
|
+
result[axis] = {
|
|
30
|
+
...(shape.digital === undefined ? {} : { digital: { ...shape.digital } }),
|
|
31
|
+
...(shape.analog === undefined ? {} : { analog: { ...shape.analog } }),
|
|
32
|
+
...(shape.range === undefined ? {} : { range: { ...shape.range } }),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
function cloneContext(context) {
|
|
38
|
+
return {
|
|
39
|
+
id: context.id,
|
|
40
|
+
codes: cloneCodes(context.codes),
|
|
41
|
+
passthrough: context.passthrough,
|
|
42
|
+
...(context.axes === undefined ? {} : { axes: cloneAxes(context.axes) }),
|
|
43
|
+
...(context.shaping === undefined ? {} : { shaping: cloneShaping(context.shaping) }),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function mergeTopDown(contexts, pick) {
|
|
47
|
+
const merged = {};
|
|
48
|
+
for (let index = contexts.length - 1; index >= 0; index -= 1) {
|
|
49
|
+
const context = contexts[index];
|
|
50
|
+
const layer = pick(context);
|
|
51
|
+
if (layer !== undefined) {
|
|
52
|
+
for (const [key, value] of Object.entries(layer)) {
|
|
53
|
+
if (merged[key] === undefined)
|
|
54
|
+
merged[key] = value;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (!context.passthrough)
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
return merged;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Creates a serializable stack of layered action maps for menus and gameplay modes.
|
|
64
|
+
* @capability action-contexts Swap bindings live for on-foot, driving, menu or build modes by pushing and popping layered action maps.
|
|
65
|
+
*/
|
|
16
66
|
export function createActionContextStack() {
|
|
17
67
|
let contexts = [];
|
|
68
|
+
let version = 0;
|
|
69
|
+
const listeners = new Set();
|
|
70
|
+
const changed = () => {
|
|
71
|
+
version += 1;
|
|
72
|
+
for (const listener of [...listeners])
|
|
73
|
+
listener();
|
|
74
|
+
};
|
|
18
75
|
return {
|
|
19
76
|
push(context) {
|
|
20
|
-
contexts = [...contexts.filter((current) => current.id !== context.id),
|
|
21
|
-
|
|
22
|
-
codes: cloneCodes(context.codes),
|
|
23
|
-
passthrough: context.passthrough,
|
|
24
|
-
}];
|
|
77
|
+
contexts = [...contexts.filter((current) => current.id !== context.id), cloneContext(context)];
|
|
78
|
+
changed();
|
|
25
79
|
},
|
|
26
80
|
pop(id) {
|
|
27
81
|
const next = contexts.filter((context) => context.id !== id);
|
|
28
|
-
const
|
|
82
|
+
const removed = next.length !== contexts.length;
|
|
29
83
|
contexts = next;
|
|
30
|
-
|
|
84
|
+
if (removed)
|
|
85
|
+
changed();
|
|
86
|
+
return removed;
|
|
31
87
|
},
|
|
32
88
|
active() {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
89
|
+
return mergeTopDown(contexts, (context) => context.codes);
|
|
90
|
+
},
|
|
91
|
+
activeAxes() {
|
|
92
|
+
return {
|
|
93
|
+
bindings: mergeTopDown(contexts, (context) => context.axes),
|
|
94
|
+
shaping: mergeTopDown(contexts, (context) => context.shaping),
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
ids: () => contexts.map((context) => context.id),
|
|
98
|
+
version: () => version,
|
|
99
|
+
subscribe(listener) {
|
|
100
|
+
listeners.add(listener);
|
|
101
|
+
return () => {
|
|
102
|
+
listeners.delete(listener);
|
|
103
|
+
};
|
|
44
104
|
},
|
|
45
105
|
snapshot() {
|
|
46
|
-
return { contexts: contexts.map(
|
|
106
|
+
return { contexts: contexts.map(cloneContext) };
|
|
47
107
|
},
|
|
48
108
|
restore(snapshot) {
|
|
49
|
-
contexts = snapshot.contexts.map(
|
|
50
|
-
|
|
51
|
-
codes: cloneCodes(context.codes),
|
|
52
|
-
passthrough: context.passthrough,
|
|
53
|
-
}));
|
|
109
|
+
contexts = snapshot.contexts.map(cloneContext);
|
|
110
|
+
changed();
|
|
54
111
|
},
|
|
55
112
|
};
|
|
56
113
|
}
|
|
@@ -47,6 +47,10 @@ export declare class AxisChannel {
|
|
|
47
47
|
get value(): AxisInput;
|
|
48
48
|
sample(dt: number, isDown: (code: string) => boolean, pointer?: PointerAxisState | null): AxisInput;
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Car axes bound to raw key codes, for an {@link AxisChannel} sampled with a key-code `isDown`. `ctx.input.axis`
|
|
52
|
+
* samples action names instead, so bind it to the game's input actions (e.g. `moveForward`), not this map.
|
|
53
|
+
*/
|
|
50
54
|
export declare const DRIVE_AXIS_BINDINGS: AxisBindingMap;
|
|
51
55
|
export interface GenericAxisConfig<TAxes extends string> {
|
|
52
56
|
bindings: Record<TAxes, AxisBinding>;
|
package/dist/input/axisInput.js
CHANGED
|
@@ -86,6 +86,10 @@ export class AxisChannel {
|
|
|
86
86
|
return this.current;
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Car axes bound to raw key codes, for an {@link AxisChannel} sampled with a key-code `isDown`. `ctx.input.axis`
|
|
91
|
+
* samples action names instead, so bind it to the game's input actions (e.g. `moveForward`), not this map.
|
|
92
|
+
*/
|
|
89
93
|
export const DRIVE_AXIS_BINDINGS = {
|
|
90
94
|
throttle: { positive: ["KeyW", "ArrowUp"] },
|
|
91
95
|
brake: { positive: ["KeyS", "ArrowDown"] },
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { AxisBinding, AxisRange } from "./axisInput.js";
|
|
2
|
+
/**
|
|
3
|
+
* How one axis responds to one kind of source. Keys want ramps (a held key should feel like a pedal
|
|
4
|
+
* being pressed, not a switch); sticks want deadzone and curve but no lag.
|
|
5
|
+
*/
|
|
6
|
+
export interface AxisShapeProfile {
|
|
7
|
+
/** Inner deadzone `0..1`; the remaining travel is rescaled so output still starts at 0 (default `0`). */
|
|
8
|
+
deadzone?: number;
|
|
9
|
+
/** Raw magnitude at which output reaches full scale, `0..1` (default `1`). */
|
|
10
|
+
saturation?: number;
|
|
11
|
+
/** Response exponent applied after the deadzone: `1` linear, `>1` finer near centre (default `1`). */
|
|
12
|
+
curve?: number;
|
|
13
|
+
/** Units/s the output may move away from zero (default `Infinity`: no ramp). */
|
|
14
|
+
riseRate?: number;
|
|
15
|
+
/** Units/s the output may move back toward zero while the input keeps its sign (default `riseRate`). */
|
|
16
|
+
fallRate?: number;
|
|
17
|
+
/** Units/s the output moves when the input is released or reversed — self-centring (default `fallRate`). */
|
|
18
|
+
returnRate?: number;
|
|
19
|
+
}
|
|
20
|
+
/** Shaping for one named axis. */
|
|
21
|
+
export interface AxisShapeConfig {
|
|
22
|
+
/** Profile for keys and buttons (default: pass through). */
|
|
23
|
+
digital?: AxisShapeProfile;
|
|
24
|
+
/** Profile for sticks, triggers and other continuous sources (default: pass through). */
|
|
25
|
+
analog?: AxisShapeProfile;
|
|
26
|
+
/** Output range (default `[-1, 1]`; use `{ min: 0, max: 1 }` for pedals). */
|
|
27
|
+
range?: AxisRange;
|
|
28
|
+
/**
|
|
29
|
+
* Multiplier from a caller-supplied signal, applied to the target before ramping — speed-sensitive
|
|
30
|
+
* steering (`speed => 1 / (1 + speed / 30)`), weapon-sway-by-stamina, and similar. Default `1`.
|
|
31
|
+
*/
|
|
32
|
+
scale?: (signal: number) => number;
|
|
33
|
+
}
|
|
34
|
+
/** Config for {@link createAxisShaper}: one entry per axis. */
|
|
35
|
+
export interface AxisShaperConfig<TAxes extends string> {
|
|
36
|
+
axes: Record<TAxes, AxisShapeConfig>;
|
|
37
|
+
}
|
|
38
|
+
/** Per-sample options for {@link AxisShaper.shape}. */
|
|
39
|
+
export interface AxisShapeSample<TAxes extends string> {
|
|
40
|
+
/** Axes whose raw value came from an analog source this frame; unlisted axes use the digital profile. */
|
|
41
|
+
analog?: Partial<Record<TAxes, boolean>> | ReadonlySet<TAxes>;
|
|
42
|
+
/** Signal passed to each axis's `scale` callback (speed, stamina, zoom). */
|
|
43
|
+
signal?: number;
|
|
44
|
+
}
|
|
45
|
+
/** Shaped axis output, one number per axis. */
|
|
46
|
+
export type AxisValues<TAxes extends string> = Record<TAxes, number>;
|
|
47
|
+
/** Stateful per-axis response shaping between raw input and a sim. */
|
|
48
|
+
export interface AxisShaper<TAxes extends string> {
|
|
49
|
+
/** Shape one frame of raw axis values (as returned by `ctx.input.axis`) and return the shaped output. */
|
|
50
|
+
shape(dt: number, raw: Readonly<Record<TAxes, number>>, sample?: AxisShapeSample<TAxes>): AxisValues<TAxes>;
|
|
51
|
+
/** The last shaped output. */
|
|
52
|
+
value(): Readonly<AxisValues<TAxes>>;
|
|
53
|
+
/** Replace the shaping config (settings screen, per-vehicle feel) without resetting the current output. */
|
|
54
|
+
retune(next: AxisShaperConfig<TAxes>): void;
|
|
55
|
+
snapshot(): AxisValues<TAxes>;
|
|
56
|
+
restore(state: Readonly<AxisValues<TAxes>>): void;
|
|
57
|
+
/** Zero every axis. */
|
|
58
|
+
reset(): void;
|
|
59
|
+
}
|
|
60
|
+
/** Deadzone, saturation and curve applied to one raw value; sign is preserved. */
|
|
61
|
+
export declare function shapeAxisValue(raw: number, profile: AxisShapeProfile | undefined): number;
|
|
62
|
+
/**
|
|
63
|
+
* Creates an {@link AxisShaper}: deadzone, curve and ramp rates per axis, chosen per frame by whether the
|
|
64
|
+
* source was a key or a stick. It sits between `ctx.input.axis(...)` and any sim (a vehicle, an aircraft,
|
|
65
|
+
* aim), so a held key eases in and self-centres while a stick stays direct. Deterministic and serializable.
|
|
66
|
+
* @capability axis-shaping shape raw input per axis — deadzone, response curve, keyboard ramp, self-centring, speed-sensitive scaling
|
|
67
|
+
*/
|
|
68
|
+
export declare function createAxisShaper<TAxes extends string>(initial: AxisShaperConfig<TAxes>): AxisShaper<TAxes>;
|
|
69
|
+
/**
|
|
70
|
+
* Which axes an analog source is driving this frame: an axis counts as analog when any action bound to it
|
|
71
|
+
* has a published analog magnitude (`ctx.input.analog()`). Feed the result to {@link AxisShaper.shape}.
|
|
72
|
+
*/
|
|
73
|
+
export declare function analogAxes<TAxes extends string>(bindings: Readonly<Record<TAxes, AxisBinding>>, analog: Readonly<Record<string, number>> | null): Set<TAxes>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const FULL_RANGE = { min: -1, max: 1 };
|
|
2
|
+
function clampTo(value, range) {
|
|
3
|
+
return value < range.min ? range.min : value > range.max ? range.max : value;
|
|
4
|
+
}
|
|
5
|
+
/** Deadzone, saturation and curve applied to one raw value; sign is preserved. */
|
|
6
|
+
export function shapeAxisValue(raw, profile) {
|
|
7
|
+
if (profile === undefined)
|
|
8
|
+
return raw;
|
|
9
|
+
const deadzone = Math.min(0.99, Math.max(0, profile.deadzone ?? 0));
|
|
10
|
+
const saturation = Math.max(deadzone + 0.01, Math.min(1, profile.saturation ?? 1));
|
|
11
|
+
const magnitude = Math.abs(raw);
|
|
12
|
+
if (magnitude <= deadzone)
|
|
13
|
+
return 0;
|
|
14
|
+
const normalized = Math.min(1, (magnitude - deadzone) / (saturation - deadzone));
|
|
15
|
+
const curved = normalized ** Math.max(0.1, profile.curve ?? 1);
|
|
16
|
+
return Math.sign(raw) * curved;
|
|
17
|
+
}
|
|
18
|
+
function rateFor(current, target, profile) {
|
|
19
|
+
const rise = profile?.riseRate ?? Number.POSITIVE_INFINITY;
|
|
20
|
+
const fall = profile?.fallRate ?? rise;
|
|
21
|
+
const back = profile?.returnRate ?? fall;
|
|
22
|
+
if (target === 0 || (current !== 0 && Math.sign(target) !== Math.sign(current)))
|
|
23
|
+
return back;
|
|
24
|
+
return Math.abs(target) >= Math.abs(current) ? rise : fall;
|
|
25
|
+
}
|
|
26
|
+
function isAnalog(axis, analog) {
|
|
27
|
+
if (analog === undefined)
|
|
28
|
+
return false;
|
|
29
|
+
if (analog instanceof Set)
|
|
30
|
+
return analog.has(axis);
|
|
31
|
+
return analog[axis] === true;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Creates an {@link AxisShaper}: deadzone, curve and ramp rates per axis, chosen per frame by whether the
|
|
35
|
+
* source was a key or a stick. It sits between `ctx.input.axis(...)` and any sim (a vehicle, an aircraft,
|
|
36
|
+
* aim), so a held key eases in and self-centres while a stick stays direct. Deterministic and serializable.
|
|
37
|
+
* @capability axis-shaping shape raw input per axis — deadzone, response curve, keyboard ramp, self-centring, speed-sensitive scaling
|
|
38
|
+
*/
|
|
39
|
+
export function createAxisShaper(initial) {
|
|
40
|
+
let config = initial;
|
|
41
|
+
const names = Object.keys(config.axes);
|
|
42
|
+
const current = {};
|
|
43
|
+
for (const name of names)
|
|
44
|
+
current[name] = 0;
|
|
45
|
+
return {
|
|
46
|
+
shape(dt, raw, sample) {
|
|
47
|
+
const step = Math.max(0, dt);
|
|
48
|
+
for (const name of Object.keys(config.axes)) {
|
|
49
|
+
const axis = config.axes[name];
|
|
50
|
+
const range = axis.range ?? FULL_RANGE;
|
|
51
|
+
const profile = isAnalog(name, sample?.analog) ? axis.analog : axis.digital;
|
|
52
|
+
const scale = axis.scale === undefined ? 1 : axis.scale(sample?.signal ?? 0);
|
|
53
|
+
const target = clampTo(shapeAxisValue(raw[name] ?? 0, profile) * scale, range);
|
|
54
|
+
const from = current[name] ?? 0;
|
|
55
|
+
const rate = rateFor(from, target, profile);
|
|
56
|
+
let next;
|
|
57
|
+
if (!Number.isFinite(rate))
|
|
58
|
+
next = target;
|
|
59
|
+
else if (target !== 0 && from !== 0 && Math.sign(target) !== Math.sign(from)) {
|
|
60
|
+
// A reversal returns to centre at the return rate, then builds the other way at the rise rate.
|
|
61
|
+
const toZero = Math.min(Math.abs(from), rate * step);
|
|
62
|
+
const left = step - toZero / Math.max(rate, 1e-9);
|
|
63
|
+
next = Math.abs(from) > toZero ? from - Math.sign(from) * toZero : Math.sign(target) * Math.min(Math.abs(target), (profile?.riseRate ?? Number.POSITIVE_INFINITY) * left);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
const delta = target - from;
|
|
67
|
+
const move = rate * step;
|
|
68
|
+
next = Math.abs(delta) <= move ? target : from + Math.sign(delta) * move;
|
|
69
|
+
}
|
|
70
|
+
current[name] = clampTo(next, range);
|
|
71
|
+
}
|
|
72
|
+
return current;
|
|
73
|
+
},
|
|
74
|
+
value: () => current,
|
|
75
|
+
retune(next) {
|
|
76
|
+
config = next;
|
|
77
|
+
for (const name of Object.keys(next.axes))
|
|
78
|
+
current[name] ??= 0;
|
|
79
|
+
},
|
|
80
|
+
snapshot: () => ({ ...current }),
|
|
81
|
+
restore(state) {
|
|
82
|
+
Object.assign(current, state);
|
|
83
|
+
},
|
|
84
|
+
reset() {
|
|
85
|
+
for (const name of Object.keys(current))
|
|
86
|
+
current[name] = 0;
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Which axes an analog source is driving this frame: an axis counts as analog when any action bound to it
|
|
92
|
+
* has a published analog magnitude (`ctx.input.analog()`). Feed the result to {@link AxisShaper.shape}.
|
|
93
|
+
*/
|
|
94
|
+
export function analogAxes(bindings, analog) {
|
|
95
|
+
const out = new Set();
|
|
96
|
+
if (analog === null)
|
|
97
|
+
return out;
|
|
98
|
+
for (const name of Object.keys(bindings)) {
|
|
99
|
+
const binding = bindings[name];
|
|
100
|
+
const codes = binding.negative === undefined ? binding.positive : [...binding.positive, ...binding.negative];
|
|
101
|
+
if (codes.some((code) => analog[code] !== undefined))
|
|
102
|
+
out.add(name);
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
@@ -29,17 +29,51 @@ export interface GamepadDeadzone {
|
|
|
29
29
|
inner: number;
|
|
30
30
|
outer: number;
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Read-only view of one sampled gamepad. The browser `Gamepad` satisfies it structurally, so a poll
|
|
34
|
+
* loop can resolve `navigator.getGamepads()` entries without copying them.
|
|
35
|
+
*/
|
|
36
|
+
export interface GamepadSample {
|
|
37
|
+
readonly axes: ArrayLike<number>;
|
|
38
|
+
readonly buttons: ArrayLike<{
|
|
39
|
+
readonly pressed: boolean;
|
|
40
|
+
readonly value: number;
|
|
41
|
+
}>;
|
|
42
|
+
readonly connected: boolean;
|
|
43
|
+
}
|
|
32
44
|
/** Options for resolving one gamepad snapshot into action state. */
|
|
33
45
|
export interface ResolveGamepadFrameOptions {
|
|
34
46
|
deadzone: GamepadDeadzone;
|
|
47
|
+
/** Response exponent for sticks and analog buttons: `1` linear, `>1` finer near rest (default `1`). */
|
|
48
|
+
curve?: number;
|
|
49
|
+
/** Inner deadzone `0..1` for analog button values (triggers); below it the button reads 0 and is not held (default `0`). */
|
|
50
|
+
triggerDeadzone?: number;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Game-level pad feel read by the shell's gamepad poll. A number `deadzone` is the axial inner
|
|
54
|
+
* deadzone with a `0.95` outer edge.
|
|
55
|
+
*/
|
|
56
|
+
export interface GamepadFeelConfig {
|
|
57
|
+
/** Stick deadzone (default axial `0.12` inner, `0.95` outer). */
|
|
58
|
+
deadzone?: number | GamepadDeadzone;
|
|
59
|
+
/** Response exponent shared by sticks and triggers (default `1`). */
|
|
35
60
|
curve?: number;
|
|
61
|
+
/** Trigger inner deadzone `0..1` (default `0`). */
|
|
62
|
+
triggerDeadzone?: number;
|
|
36
63
|
}
|
|
64
|
+
/** Shell default stick deadzone. */
|
|
65
|
+
export declare const DEFAULT_GAMEPAD_DEADZONE: GamepadDeadzone;
|
|
66
|
+
/** Resolve a {@link GamepadFeelConfig} into the options {@link resolveGamepadFrame} takes. */
|
|
67
|
+
export declare function gamepadFeelOptions(config: GamepadFeelConfig | undefined): ResolveGamepadFrameOptions;
|
|
37
68
|
/** The digital and analog action state produced by a gamepad frame. */
|
|
38
69
|
export interface GamepadFrame {
|
|
39
70
|
held: string[];
|
|
40
71
|
analog: Record<string, number>;
|
|
41
72
|
}
|
|
42
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* Resolve one sampled gamepad into held actions and shaped analog action values. Sticks go through
|
|
75
|
+
* the deadzone and curve; analog buttons (triggers) through `triggerDeadzone` and the same curve.
|
|
76
|
+
* Pass `out` to reuse a frame across polls; it is cleared and returned.
|
|
43
77
|
* @capability gamepad-input Resolve sampled gamepad input into actions with deadzones and response curves.
|
|
44
78
|
*/
|
|
45
|
-
export declare function resolveGamepadFrame(snapshot:
|
|
79
|
+
export declare function resolveGamepadFrame(snapshot: GamepadSample, bindings: GamepadBindings, options: ResolveGamepadFrameOptions, out?: GamepadFrame): GamepadFrame;
|
|
@@ -7,63 +7,102 @@ export const GAMEPAD_GLYPH_SETS = {
|
|
|
7
7
|
};
|
|
8
8
|
/** Lowercase alias for consumers that prefer data-oriented naming. */
|
|
9
9
|
export const gamepadGlyphSets = GAMEPAD_GLYPH_SETS;
|
|
10
|
+
/** Shell default stick deadzone. */
|
|
11
|
+
export const DEFAULT_GAMEPAD_DEADZONE = { kind: "axial", inner: 0.12, outer: 0.95 };
|
|
12
|
+
/** Resolve a {@link GamepadFeelConfig} into the options {@link resolveGamepadFrame} takes. */
|
|
13
|
+
export function gamepadFeelOptions(config) {
|
|
14
|
+
const deadzone = config?.deadzone;
|
|
15
|
+
return {
|
|
16
|
+
deadzone: deadzone === undefined
|
|
17
|
+
? DEFAULT_GAMEPAD_DEADZONE
|
|
18
|
+
: typeof deadzone === "number"
|
|
19
|
+
? { kind: "axial", inner: deadzone, outer: DEFAULT_GAMEPAD_DEADZONE.outer }
|
|
20
|
+
: deadzone,
|
|
21
|
+
curve: config?.curve ?? 1,
|
|
22
|
+
triggerDeadzone: config?.triggerDeadzone ?? 0,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
10
25
|
function bindingCodes(codes) {
|
|
11
26
|
if (Array.isArray(codes))
|
|
12
27
|
return codes;
|
|
13
28
|
const modes = codes;
|
|
14
29
|
return [...(modes.hold ?? []), ...(modes.toggle ?? [])];
|
|
15
30
|
}
|
|
16
|
-
function
|
|
17
|
-
|
|
18
|
-
|
|
31
|
+
function clampUnit(value) {
|
|
32
|
+
return Number.isFinite(value) ? (value < -1 ? -1 : value > 1 ? 1 : value) : 0;
|
|
33
|
+
}
|
|
34
|
+
function rescale(magnitude, inner, outer, curve) {
|
|
35
|
+
if (magnitude <= inner)
|
|
19
36
|
return 0;
|
|
20
|
-
const normalized = magnitude >=
|
|
21
|
-
return
|
|
37
|
+
const normalized = magnitude >= outer ? 1 : (magnitude - inner) / (outer - inner);
|
|
38
|
+
return curve === 1 ? normalized : Math.pow(normalized, curve);
|
|
22
39
|
}
|
|
40
|
+
const axisCodes = new Map();
|
|
23
41
|
function readAxis(code) {
|
|
24
|
-
|
|
25
|
-
|
|
42
|
+
let parsed = axisCodes.get(code);
|
|
43
|
+
if (parsed === undefined) {
|
|
44
|
+
const match = /^padaxis:(\d+)([+-])$/.exec(code);
|
|
45
|
+
parsed = match === null ? null : { index: Number(match[1]), sign: match[2] === "+" ? 1 : -1 };
|
|
46
|
+
axisCodes.set(code, parsed);
|
|
47
|
+
}
|
|
48
|
+
return parsed;
|
|
26
49
|
}
|
|
27
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Resolve one sampled gamepad into held actions and shaped analog action values. Sticks go through
|
|
52
|
+
* the deadzone and curve; analog buttons (triggers) through `triggerDeadzone` and the same curve.
|
|
53
|
+
* Pass `out` to reuse a frame across polls; it is cleared and returned.
|
|
28
54
|
* @capability gamepad-input Resolve sampled gamepad input into actions with deadzones and response curves.
|
|
29
55
|
*/
|
|
30
|
-
export function resolveGamepadFrame(snapshot, bindings, options) {
|
|
56
|
+
export function resolveGamepadFrame(snapshot, bindings, options, out = { held: [], analog: {} }) {
|
|
57
|
+
out.held.length = 0;
|
|
58
|
+
for (const key in out.analog)
|
|
59
|
+
delete out.analog[key];
|
|
31
60
|
if (!snapshot.connected)
|
|
32
|
-
return
|
|
61
|
+
return out;
|
|
33
62
|
const inner = Math.max(0, options.deadzone.inner);
|
|
34
63
|
const outer = Math.max(inner + Number.EPSILON, options.deadzone.outer);
|
|
35
64
|
const curve = options.curve === undefined ? 1 : Math.max(Number.EPSILON, options.curve);
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
65
|
+
const triggerInner = Math.min(0.99, Math.max(0, options.triggerDeadzone ?? 0));
|
|
66
|
+
const radial = options.deadzone.kind === "radial";
|
|
67
|
+
let radialFactor = 0;
|
|
68
|
+
if (radial) {
|
|
69
|
+
let sumSquares = 0;
|
|
70
|
+
for (let index = 0; index < snapshot.axes.length; index += 1) {
|
|
71
|
+
const value = clampUnit(snapshot.axes[index]);
|
|
72
|
+
sumSquares += value * value;
|
|
73
|
+
}
|
|
74
|
+
const magnitude = Math.sqrt(sumSquares);
|
|
75
|
+
radialFactor = magnitude === 0 ? 0 : rescale(Math.min(1, magnitude), inner, outer, curve) / magnitude;
|
|
44
76
|
}
|
|
45
|
-
const
|
|
46
|
-
const analog = {};
|
|
47
|
-
for (const action of Object.keys(bindings)) {
|
|
77
|
+
for (const action in bindings) {
|
|
48
78
|
let actionAnalog = 0;
|
|
49
79
|
let isHeld = false;
|
|
50
80
|
for (const code of bindingCodes(bindings[action])) {
|
|
51
81
|
if (code.startsWith("pad:")) {
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
82
|
+
const button = snapshot.buttons[Number(code.slice(4))];
|
|
83
|
+
if (button === undefined)
|
|
84
|
+
continue;
|
|
85
|
+
const raw = Number.isFinite(button.value) ? Math.max(0, Math.min(1, button.value)) : 0;
|
|
86
|
+
const value = rescale(raw, triggerInner, 1, curve);
|
|
87
|
+
if (button.pressed && (value > 0 || raw === 0))
|
|
55
88
|
isHeld = true;
|
|
56
|
-
|
|
89
|
+
if (value > actionAnalog)
|
|
90
|
+
actionAnalog = value;
|
|
57
91
|
continue;
|
|
58
92
|
}
|
|
59
93
|
const axis = readAxis(code);
|
|
60
|
-
if (axis
|
|
61
|
-
|
|
94
|
+
if (axis === null)
|
|
95
|
+
continue;
|
|
96
|
+
const raw = clampUnit(snapshot.axes[axis.index] ?? 0);
|
|
97
|
+
const shaped = radial ? raw * radialFactor : Math.sign(raw) * rescale(Math.abs(raw), inner, outer, curve);
|
|
98
|
+
const value = shaped * axis.sign;
|
|
99
|
+
if (value > actionAnalog)
|
|
100
|
+
actionAnalog = value;
|
|
62
101
|
}
|
|
63
102
|
if (isHeld || actionAnalog > 0)
|
|
64
|
-
held.push(action);
|
|
103
|
+
out.held.push(action);
|
|
65
104
|
if (actionAnalog !== 0)
|
|
66
|
-
analog[action] = actionAnalog;
|
|
105
|
+
out.analog[action] = actionAnalog;
|
|
67
106
|
}
|
|
68
|
-
return
|
|
107
|
+
return out;
|
|
69
108
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Motor intensities, each `0..1`: `strong` is the low-frequency motor, `weak` the high-frequency one. */
|
|
2
|
+
export interface HapticLevel {
|
|
3
|
+
strong: number;
|
|
4
|
+
weak: number;
|
|
5
|
+
}
|
|
6
|
+
/** A decaying one-shot on a channel: starts at `strong`/`weak` and fades to zero over `ms`. */
|
|
7
|
+
export interface HapticPulse extends HapticLevel {
|
|
8
|
+
ms: number;
|
|
9
|
+
}
|
|
10
|
+
/** Options for {@link createHapticChannels}. */
|
|
11
|
+
export interface HapticChannelsOptions {
|
|
12
|
+
/** Scale applied to channels below the highest active priority (default `0.4`), so an impact cuts through the engine hum. */
|
|
13
|
+
duck?: number;
|
|
14
|
+
}
|
|
15
|
+
/** Serializable channel state. */
|
|
16
|
+
export interface HapticChannelsSnapshot {
|
|
17
|
+
duck: number;
|
|
18
|
+
channels: {
|
|
19
|
+
name: string;
|
|
20
|
+
strong: number;
|
|
21
|
+
weak: number;
|
|
22
|
+
priority: number;
|
|
23
|
+
remainingMs: number | null;
|
|
24
|
+
totalMs: number | null;
|
|
25
|
+
}[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Named continuous rumble channels for one player, mixed by priority each frame. Game code sets
|
|
29
|
+
* levels from its own telemetry (engine rpm, road surface, impacts); the shell mixes and drives the pad.
|
|
30
|
+
*/
|
|
31
|
+
export interface HapticChannels {
|
|
32
|
+
/** Hold a channel at a level until changed; `0`/`0` silences it. Higher `priority` (default `0`) ducks lower channels. */
|
|
33
|
+
set(channel: string, level: HapticLevel, priority?: number): void;
|
|
34
|
+
/** Start a fading one-shot on a channel, replacing what it held. */
|
|
35
|
+
pulse(channel: string, pulse: HapticPulse, priority?: number): void;
|
|
36
|
+
/** Silence one channel, or all of them. */
|
|
37
|
+
clear(channel?: string): void;
|
|
38
|
+
/** Advance pulses by `dt` seconds and return the mixed level. */
|
|
39
|
+
mix(dt: number): HapticLevel;
|
|
40
|
+
/** The last mixed level. */
|
|
41
|
+
level(): HapticLevel;
|
|
42
|
+
retune(options: HapticChannelsOptions): void;
|
|
43
|
+
snapshot(): HapticChannelsSnapshot;
|
|
44
|
+
restore(snapshot: HapticChannelsSnapshot): void;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Per-player haptic channels mixed by priority.
|
|
48
|
+
* @capability haptics Continuous gamepad rumble from named channels (engine, road, impact) mixed by priority, plus fading pulses.
|
|
49
|
+
*/
|
|
50
|
+
export declare function createHapticChannels(options?: HapticChannelsOptions): HapticChannels;
|