@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
|
@@ -2,7 +2,7 @@ import type { BodyHandle, BodyShape, PhysicsBackend, PhysicsVec3 } from "../phys
|
|
|
2
2
|
/** Capsule dimensions and the walk rules a controller applies; every field is retunable during play. */
|
|
3
3
|
export interface CharacterControllerConfig {
|
|
4
4
|
radius: number;
|
|
5
|
-
/** Standing height from feet to crown
|
|
5
|
+
/** Standing height from feet to crown, at least twice `radius`. */
|
|
6
6
|
height: number;
|
|
7
7
|
/** Height the feet may pop up to clear a ledge in one move. Default 0.35. */
|
|
8
8
|
stepHeight?: number;
|
|
@@ -10,7 +10,7 @@ export interface CharacterControllerConfig {
|
|
|
10
10
|
maxSlopeDeg?: number;
|
|
11
11
|
/** Gap kept between the capsule and any surface so casts never start inside geometry. Default 0.02. */
|
|
12
12
|
skinWidth?: number;
|
|
13
|
-
/** Height while crouched. Default `height * 0.6
|
|
13
|
+
/** Height while crouched, between the capsule diameter and standing height. Default max(`height * 0.6`, diameter). */
|
|
14
14
|
crouchHeight?: number;
|
|
15
15
|
/** How far below the feet the ground may be while still counting as grounded (stairs down, small bumps). Default 0.3. */
|
|
16
16
|
snapDistance?: number;
|
|
@@ -64,12 +64,15 @@ export interface CharacterController {
|
|
|
64
64
|
shape(): BodyShape;
|
|
65
65
|
/** Capsule center for the current state. */
|
|
66
66
|
center(): [number, number, number];
|
|
67
|
+
/** Set stance before resolving movement intent. Returns false only when standing lacks headroom. */
|
|
68
|
+
setCrouch(backend: PhysicsBackend, crouch: boolean, self?: BodyHandle): boolean;
|
|
67
69
|
move(backend: PhysicsBackend, input: CharacterMoveInput): CharacterMoveResult;
|
|
68
70
|
}
|
|
69
71
|
/**
|
|
70
72
|
* Collide-and-slide capsule controller over any {@link PhysicsBackend}: horizontal slide along walls, step-up over
|
|
71
73
|
* ledges, slope limit, ceiling test, crouch with headroom check, ground snapping, and moving-platform carry read from
|
|
72
74
|
* the ground body's velocity. Pure over the backend's `shapecast`/`overlap`; the caller owns input and gravity policy.
|
|
75
|
+
* Invalid dimensions or control ranges throw before configuration changes.
|
|
73
76
|
*
|
|
74
77
|
* @capability character-controller capsule walk over the physics backend — slide, step-up, slopes, crouch, platforms
|
|
75
78
|
*/
|
|
@@ -1,25 +1,47 @@
|
|
|
1
1
|
function resolve(config) {
|
|
2
|
-
|
|
2
|
+
const cfg = {
|
|
3
3
|
radius: config.radius,
|
|
4
4
|
height: config.height,
|
|
5
5
|
stepHeight: config.stepHeight ?? 0.35,
|
|
6
6
|
maxSlopeDeg: config.maxSlopeDeg ?? 50,
|
|
7
7
|
skinWidth: config.skinWidth ?? 0.02,
|
|
8
|
-
crouchHeight: config.crouchHeight ?? config.height * 0.6,
|
|
8
|
+
crouchHeight: config.crouchHeight ?? Math.max(config.radius * 2, config.height * 0.6),
|
|
9
9
|
snapDistance: config.snapDistance ?? 0.3,
|
|
10
10
|
maxSlides: config.maxSlides ?? 4,
|
|
11
11
|
mask: config.mask ?? 0xffffffff,
|
|
12
12
|
};
|
|
13
|
+
for (const field of ["radius", "height", "crouchHeight"]) {
|
|
14
|
+
if (!Number.isFinite(cfg[field]) || cfg[field] <= 0)
|
|
15
|
+
throw new RangeError(`CharacterController.${field} must be finite and greater than zero.`);
|
|
16
|
+
}
|
|
17
|
+
const diameter = cfg.radius * 2;
|
|
18
|
+
if (cfg.height < diameter)
|
|
19
|
+
throw new RangeError(`CharacterController.height must be at least the capsule diameter (${diameter}). Increase height or reduce radius.`);
|
|
20
|
+
if (cfg.crouchHeight < diameter)
|
|
21
|
+
throw new RangeError(`CharacterController.crouchHeight must be at least the capsule diameter (${diameter}). Increase crouchHeight or reduce radius.`);
|
|
22
|
+
if (cfg.crouchHeight > cfg.height)
|
|
23
|
+
throw new RangeError("CharacterController.crouchHeight must not exceed standing height.");
|
|
24
|
+
for (const field of ["stepHeight", "skinWidth", "snapDistance"]) {
|
|
25
|
+
if (!Number.isFinite(cfg[field]) || cfg[field] < 0)
|
|
26
|
+
throw new RangeError(`CharacterController.${field} must be finite and nonnegative.`);
|
|
27
|
+
}
|
|
28
|
+
if (!Number.isFinite(cfg.maxSlopeDeg) || cfg.maxSlopeDeg < 0 || cfg.maxSlopeDeg >= 90)
|
|
29
|
+
throw new RangeError("CharacterController.maxSlopeDeg must be finite and between 0 (inclusive) and 90 (exclusive).");
|
|
30
|
+
if (!Number.isInteger(cfg.maxSlides) || cfg.maxSlides < 1)
|
|
31
|
+
throw new RangeError("CharacterController.maxSlides must be a positive integer.");
|
|
32
|
+
return cfg;
|
|
13
33
|
}
|
|
14
34
|
/**
|
|
15
35
|
* Collide-and-slide capsule controller over any {@link PhysicsBackend}: horizontal slide along walls, step-up over
|
|
16
36
|
* ledges, slope limit, ceiling test, crouch with headroom check, ground snapping, and moving-platform carry read from
|
|
17
37
|
* the ground body's velocity. Pure over the backend's `shapecast`/`overlap`; the caller owns input and gravity policy.
|
|
38
|
+
* Invalid dimensions or control ranges throw before configuration changes.
|
|
18
39
|
*
|
|
19
40
|
* @capability character-controller capsule walk over the physics backend — slide, step-up, slopes, crouch, platforms
|
|
20
41
|
*/
|
|
21
42
|
export function createCharacterController(initial) {
|
|
22
|
-
let
|
|
43
|
+
let declaration = { ...initial };
|
|
44
|
+
let cfg = resolve(declaration);
|
|
23
45
|
let state = {
|
|
24
46
|
position: [0, 0, 0],
|
|
25
47
|
verticalVelocity: 0,
|
|
@@ -69,10 +91,26 @@ export function createCharacterController(initial) {
|
|
|
69
91
|
pos[2] += motion[2] * fraction;
|
|
70
92
|
return { fraction, hit };
|
|
71
93
|
}
|
|
94
|
+
function setCrouch(backend, crouch, self) {
|
|
95
|
+
if (crouch === state.crouching)
|
|
96
|
+
return true;
|
|
97
|
+
if (!crouch && backend.overlap({
|
|
98
|
+
shape: shapeFor(cfg.height),
|
|
99
|
+
position: centerFor(state.position, cfg.height),
|
|
100
|
+
mask: cfg.mask,
|
|
101
|
+
...(self === undefined ? {} : { exclude: self }),
|
|
102
|
+
}).length > 0)
|
|
103
|
+
return false;
|
|
104
|
+
state.crouching = crouch;
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
72
107
|
return {
|
|
73
108
|
config: () => cfg,
|
|
74
109
|
retune(next) {
|
|
75
|
-
|
|
110
|
+
const updated = { ...declaration, ...next };
|
|
111
|
+
const resolved = resolve(updated);
|
|
112
|
+
declaration = updated;
|
|
113
|
+
cfg = resolved;
|
|
76
114
|
},
|
|
77
115
|
state: () => state,
|
|
78
116
|
snapshot: () => ({
|
|
@@ -95,6 +133,7 @@ export function createCharacterController(initial) {
|
|
|
95
133
|
},
|
|
96
134
|
shape: () => shapeFor(currentHeight()),
|
|
97
135
|
center: () => centerFor(state.position, currentHeight()),
|
|
136
|
+
setCrouch,
|
|
98
137
|
move(backend, input) {
|
|
99
138
|
const { dt, self } = input;
|
|
100
139
|
const result = {
|
|
@@ -108,21 +147,8 @@ export function createCharacterController(initial) {
|
|
|
108
147
|
};
|
|
109
148
|
const start = [state.position[0], state.position[1], state.position[2]];
|
|
110
149
|
const pos = [start[0], start[1], start[2]];
|
|
111
|
-
if (input.crouch
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
else if (input.crouch === false && state.crouching) {
|
|
115
|
-
const standing = backend.overlap({
|
|
116
|
-
shape: shapeFor(cfg.height),
|
|
117
|
-
position: centerFor(pos, cfg.height),
|
|
118
|
-
mask: cfg.mask,
|
|
119
|
-
...(self === undefined ? {} : { exclude: self }),
|
|
120
|
-
});
|
|
121
|
-
if (standing.length === 0)
|
|
122
|
-
state.crouching = false;
|
|
123
|
-
else
|
|
124
|
-
result.crouchBlocked = true;
|
|
125
|
-
}
|
|
150
|
+
if (input.crouch !== undefined)
|
|
151
|
+
result.crouchBlocked = !setCrouch(backend, input.crouch, self);
|
|
126
152
|
const height = currentHeight();
|
|
127
153
|
let carry = [0, 0, 0];
|
|
128
154
|
if (state.grounded && state.groundBody !== null) {
|
|
@@ -145,7 +171,8 @@ export function createCharacterController(initial) {
|
|
|
145
171
|
if (!walkable(hit.normal)) {
|
|
146
172
|
result.hitWall = true;
|
|
147
173
|
slid[1] = 0;
|
|
148
|
-
if (
|
|
174
|
+
if (state.grounded && state.verticalVelocity <= 0 && input.jumpVelocity === undefined &&
|
|
175
|
+
cfg.stepHeight > 0 && !result.steppedUp && tryStepUp(backend, pos, height, remaining, self)) {
|
|
149
176
|
result.steppedUp = true;
|
|
150
177
|
result.hitWall = false;
|
|
151
178
|
break;
|
|
@@ -178,9 +205,9 @@ export function createCharacterController(initial) {
|
|
|
178
205
|
state.groundBody = verticalHit.body;
|
|
179
206
|
if (state.verticalVelocity < 0)
|
|
180
207
|
state.verticalVelocity = 0;
|
|
181
|
-
|
|
182
|
-
if (
|
|
183
|
-
pos[1] += cfg.skinWidth;
|
|
208
|
+
const initialGap = Math.max(0, verticalHit.toi * Math.abs(vertical));
|
|
209
|
+
if (initialGap < cfg.skinWidth)
|
|
210
|
+
pos[1] += cfg.skinWidth - initialGap;
|
|
184
211
|
}
|
|
185
212
|
else {
|
|
186
213
|
state.grounded = false;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AnalogMoveIntent, type CollisionObstacle, type MovementKeysState } from "./movementModel.js";
|
|
1
|
+
import { type AnalogMoveIntent, type CollisionObstacle, type MovementKeysState, type MotionFrameOptions } from "./movementModel.js";
|
|
2
2
|
import { type AxisBinding } from "../input/axisInput.js";
|
|
3
3
|
import type { PointerAxisState } from "../input/pointerAxis.js";
|
|
4
4
|
import type { ChaseCameraTuning } from "../runtime/cameraDirector.js";
|
|
@@ -91,7 +91,7 @@ export declare function resolveFreeFlightIntentFromInput(isDown: (action: string
|
|
|
91
91
|
* Horizontal uses yaw-relative strafe (A = left, D = right) — never roll/bank.
|
|
92
92
|
* Vertical is independent of pitch unless `alignWithLook` is true (spectator).
|
|
93
93
|
*/
|
|
94
|
-
export declare function advanceFreeFlight(state: FreeFlightState, intent: FreeFlightIntent, yaw: number, pitch: number | undefined, dt: number, tuning: FreeFlightTuning): FreeFlightStep;
|
|
94
|
+
export declare function advanceFreeFlight(state: FreeFlightState, intent: FreeFlightIntent, yaw: number, pitch: number | undefined, dt: number, tuning: FreeFlightTuning, options?: Pick<MotionFrameOptions, "authoritativeStep">): FreeFlightStep;
|
|
95
95
|
/** Resolve a flight step against world solids (XZ slide + Y clamp). */
|
|
96
96
|
export declare function resolveFlightStep(position: readonly [number, number, number], stepX: number, stepY: number, stepZ: number, obstacles: readonly CollisionObstacle[], radius?: number): {
|
|
97
97
|
stepX: number;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { motionStepSeconds } from "./movementModel.js";
|
|
2
2
|
import { DEFAULT_OBSTACLE_PLAYER_RADIUS } from "./movementModel.js";
|
|
3
3
|
import { sampleAxisBindings } from "../input/axisInput.js";
|
|
4
4
|
const ANALOG_EPSILON = 0.02;
|
|
@@ -50,8 +50,8 @@ export function resolveFreeFlightIntentFromInput(isDown, value, pointer, binding
|
|
|
50
50
|
* Horizontal uses yaw-relative strafe (A = left, D = right) — never roll/bank.
|
|
51
51
|
* Vertical is independent of pitch unless `alignWithLook` is true (spectator).
|
|
52
52
|
*/
|
|
53
|
-
export function advanceFreeFlight(state, intent, yaw, pitch, dt, tuning) {
|
|
54
|
-
const delta =
|
|
53
|
+
export function advanceFreeFlight(state, intent, yaw, pitch, dt, tuning, options) {
|
|
54
|
+
const delta = motionStepSeconds(dt, options?.authoritativeStep);
|
|
55
55
|
if (delta <= 0)
|
|
56
56
|
return { stepX: 0, stepY: 0, stepZ: 0 };
|
|
57
57
|
const speed = tuning.speed ?? 8;
|
|
@@ -40,6 +40,9 @@ export interface GlideModel {
|
|
|
40
40
|
step(dt: number, input?: GlideInput, externalVelocity?: GlideVec3): GlideStep;
|
|
41
41
|
pose(): GlideStep;
|
|
42
42
|
launch(position: GlideVec3, heading: number, initialSpeed?: number): void;
|
|
43
|
+
/** Plain JSON copy of the glider's position, velocity and heading. */
|
|
44
|
+
snapshot(): GlideStep;
|
|
45
|
+
restore(next: GlideStep): void;
|
|
43
46
|
}
|
|
44
47
|
/**
|
|
45
48
|
* Gliding/wingsuit descent control — lift, drag, and steering from a launch.
|
|
@@ -32,6 +32,12 @@ export function createGlideModel(config = {}) {
|
|
|
32
32
|
vz = Math.cos(heading) * initialSpeed;
|
|
33
33
|
},
|
|
34
34
|
pose: snapshot,
|
|
35
|
+
snapshot,
|
|
36
|
+
restore(next) {
|
|
37
|
+
[x, y, z] = next.position;
|
|
38
|
+
[vx, vy, vz] = next.velocity;
|
|
39
|
+
heading = next.heading;
|
|
40
|
+
},
|
|
35
41
|
step(dt, input = {}, externalVelocity) {
|
|
36
42
|
if (dt <= 0)
|
|
37
43
|
return snapshot();
|
|
@@ -28,6 +28,8 @@ export interface GrappleSwing {
|
|
|
28
28
|
fire(anchor: SwingVec3, from: SwingVec3): void;
|
|
29
29
|
release(): void;
|
|
30
30
|
state(): GrappleSwingState;
|
|
31
|
+
/** Puts a {@link GrappleSwing.state} back; `anchor: null` detaches. */
|
|
32
|
+
restore(next: GrappleSwingState): void;
|
|
31
33
|
/** Constrain one frame. `reeling` shortens the rope; returns the corrected position/velocity. */
|
|
32
34
|
step(position: SwingVec3, velocity: SwingVec3, dt: number, reeling?: boolean): GrappleSwingStep;
|
|
33
35
|
}
|
|
@@ -18,7 +18,11 @@ export function createGrappleSwing(config = {}) {
|
|
|
18
18
|
release() {
|
|
19
19
|
anchor = null;
|
|
20
20
|
},
|
|
21
|
-
state: () => ({ attached: anchor !== null, anchor, ropeLength }),
|
|
21
|
+
state: () => ({ attached: anchor !== null, anchor: anchor === null ? null : [anchor[0], anchor[1], anchor[2]], ropeLength }),
|
|
22
|
+
restore(next) {
|
|
23
|
+
anchor = next.anchor === null ? null : [next.anchor[0], next.anchor[1], next.anchor[2]];
|
|
24
|
+
ropeLength = next.ropeLength;
|
|
25
|
+
},
|
|
22
26
|
step(position, velocity, dt, reeling = false) {
|
|
23
27
|
if (anchor === null || dt <= 0)
|
|
24
28
|
return { position, velocity };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { InputBuffer } from "../input/inputBuffer.js";
|
|
1
2
|
/**
|
|
2
3
|
* Movement core (pure model).
|
|
3
4
|
*
|
|
@@ -83,6 +84,16 @@ export interface PlayerMotionState {
|
|
|
83
84
|
jumpOffset: number;
|
|
84
85
|
grounded: boolean;
|
|
85
86
|
jumpHeld: boolean;
|
|
87
|
+
/** Integrated time, ms; the clock jump buffering and coyote time read. */
|
|
88
|
+
clockMs: number;
|
|
89
|
+
/** `clockMs` of the last grounded frame; `null` after a jump spends it. */
|
|
90
|
+
groundedAtMs: number | null;
|
|
91
|
+
/** `clockMs` of the last landing; `null` before the first. */
|
|
92
|
+
landedAtMs: number | null;
|
|
93
|
+
/** Rising from the player's own jump, so releasing jump may cut it. */
|
|
94
|
+
jumpRising: boolean;
|
|
95
|
+
/** Ended the last frame in the air. */
|
|
96
|
+
wasAirborne: boolean;
|
|
86
97
|
}
|
|
87
98
|
/** @internal */
|
|
88
99
|
export declare function createPlayerMotionState(): PlayerMotionState;
|
|
@@ -100,6 +111,32 @@ export interface MovementTuningOverrides {
|
|
|
100
111
|
jumpVelocity?: number;
|
|
101
112
|
/** Fraction of walk speed while backpedalling; overrides {@link MOVEMENT_TUNING.backpedalSpeedMultiplier}. */
|
|
102
113
|
backpedalSpeedMultiplier?: number;
|
|
114
|
+
/** Ground velocity response, 1/s: how fast the body reaches its target velocity (default 26). */
|
|
115
|
+
groundAcceleration?: number;
|
|
116
|
+
/** Air velocity response, 1/s: air control (default 12; `0` locks the jump arc). */
|
|
117
|
+
airAcceleration?: number;
|
|
118
|
+
/** Ground stopping response with no input, 1/s (default 18; lower slides). */
|
|
119
|
+
groundFriction?: number;
|
|
120
|
+
/** Sprint speed as a multiple of walk speed (default 2.25). */
|
|
121
|
+
runSpeedMultiplier?: number;
|
|
122
|
+
/** Crouch speed as a multiple of walk speed (default 0.45). */
|
|
123
|
+
crouchSpeedMultiplier?: number;
|
|
124
|
+
/** A jump pressed up to this many ms before landing still fires on landing (default 0). Needs {@link MotionFrameOptions.buffer}. */
|
|
125
|
+
jumpBufferMs?: number;
|
|
126
|
+
/** A jump pressed up to this many ms after walking off a ledge still fires (default 0). */
|
|
127
|
+
coyoteMs?: number;
|
|
128
|
+
/** Rising speed kept when jump is released early, 0..1 (default 1: no cut). Lower makes a tap a hop. */
|
|
129
|
+
jumpCutFactor?: number;
|
|
130
|
+
/** Gravity multiplier near the peak, while vertical speed is under {@link MovementTuningOverrides.apexSpeed} (default 1). */
|
|
131
|
+
apexGravityScale?: number;
|
|
132
|
+
/** Vertical speed under which the apex gravity scale applies, m/s (default 1.5). */
|
|
133
|
+
apexSpeed?: number;
|
|
134
|
+
/** Gravity multiplier while falling (default 1). Above 1 drops faster than it rose. */
|
|
135
|
+
fallGravityScale?: number;
|
|
136
|
+
/** After landing, jumps wait and walk speed ramps back from {@link MovementTuningOverrides.landingSpeedScale} over this many ms (default 0). */
|
|
137
|
+
landingRecoveryMs?: number;
|
|
138
|
+
/** Walk speed share right after landing when `landingRecoveryMs` is set (default 0.5). */
|
|
139
|
+
landingSpeedScale?: number;
|
|
103
140
|
}
|
|
104
141
|
/**
|
|
105
142
|
* Per-frame dynamic modifiers for {@link advancePlayerMotion} — state the caller
|
|
@@ -107,10 +144,16 @@ export interface MovementTuningOverrides {
|
|
|
107
144
|
* fields leave the integrator at its default behavior.
|
|
108
145
|
*/
|
|
109
146
|
export interface MotionFrameOptions {
|
|
147
|
+
/** Integrate a caller-clamped simulation step in full, including scaled game time. Default false. */
|
|
148
|
+
authoritativeStep?: boolean;
|
|
110
149
|
/** Extra multiplier applied on top of the resolved target speed (e.g. swimming). Default 1. */
|
|
111
150
|
speedScale?: number;
|
|
112
151
|
/** Suppress gravity/jump integration and hold the avatar afloat this frame (e.g. swimming). Default false. */
|
|
113
152
|
floating?: boolean;
|
|
153
|
+
/** Buffers jump presses for `jumpBufferMs`. Without it a jump fires only on the press frame. */
|
|
154
|
+
buffer?: InputBuffer;
|
|
155
|
+
/** A collision controller supplies grounded state; integrate jump velocity without landing on the synthetic offset origin. */
|
|
156
|
+
externalGrounding?: boolean;
|
|
114
157
|
}
|
|
115
158
|
/**
|
|
116
159
|
* Advance one frame of avatar kinematics. Mutates `motion` (velocity, jump,
|
|
@@ -124,6 +167,8 @@ export interface MotionFrameOptions {
|
|
|
124
167
|
* @internal
|
|
125
168
|
*/
|
|
126
169
|
export declare function advancePlayerMotion(motion: PlayerMotionState, intent: MovementIntent, forwardX: number, forwardZ: number, baseSpeed: number, rawDeltaSeconds: number, tuning?: MovementTuningOverrides, options?: MotionFrameOptions): MovementFrameStep;
|
|
170
|
+
/** @internal Validate trusted simulation time or apply the standalone frame-stall clamp. */
|
|
171
|
+
export declare function motionStepSeconds(seconds: number, authoritativeStep?: boolean): number;
|
|
127
172
|
/** A placed scene object the walking player collides against as a circle-vs-AABB obstacle. */
|
|
128
173
|
export interface CollisionObstacle {
|
|
129
174
|
position: readonly [number, number, number];
|
|
@@ -144,6 +189,23 @@ export interface CollisionObstacle {
|
|
|
144
189
|
}
|
|
145
190
|
/** @internal Player radius the walking obstruction inflates obstacles by; callers gather within reach of it. */
|
|
146
191
|
export declare const DEFAULT_OBSTACLE_PLAYER_RADIUS = 0.3;
|
|
192
|
+
/** @internal Default feet-to-head span for heightfield obstruction; matches DEFAULT_VOXEL_DIMS.height. */
|
|
193
|
+
export declare const DEFAULT_OBSTACLE_PLAYER_HEIGHT = 1.8;
|
|
194
|
+
/**
|
|
195
|
+
* Sweep a heightfield walker's vertical step at its accepted XZ position against blocking bounds.
|
|
196
|
+
* The nearest crossed underside stops ascent; the nearest crossed top catches descent, including
|
|
197
|
+
* thin geometry crossed in one step. Compound boxes are relative to obstacle.position; offset is
|
|
198
|
+
* used only for a single AABB. Real starting intersections are reported separately, not repaired.
|
|
199
|
+
* @internal
|
|
200
|
+
*/
|
|
201
|
+
export declare function resolveObstacleVerticalStep(position: readonly [number, number, number], stepY: number, obstacles: readonly CollisionObstacle[], radius?: number, playerHeight?: number): {
|
|
202
|
+
stepY: number;
|
|
203
|
+
hitCeiling: boolean;
|
|
204
|
+
ceilingFeetY: number | null;
|
|
205
|
+
ceilingTolerance: number;
|
|
206
|
+
landed: boolean;
|
|
207
|
+
initialOverlap: boolean;
|
|
208
|
+
};
|
|
147
209
|
/**
|
|
148
210
|
* Resolve a horizontal step against nearby placed objects with classic
|
|
149
211
|
* axis-separated sliding (try X, then Z against the post-X position) so
|
|
@@ -159,7 +221,7 @@ export declare const DEFAULT_OBSTACLE_PLAYER_RADIUS = 0.3;
|
|
|
159
221
|
* tread is stepped up, not bumped into.
|
|
160
222
|
* @internal
|
|
161
223
|
*/
|
|
162
|
-
export declare function resolveObstacleStep(current: readonly [number, number, number], stepX: number, stepZ: number, obstacles: readonly CollisionObstacle[], playerRadius?: number, stepUpHeight?: number): MovementFrameStep;
|
|
224
|
+
export declare function resolveObstacleStep(current: readonly [number, number, number], stepX: number, stepZ: number, obstacles: readonly CollisionObstacle[], playerRadius?: number, stepUpHeight?: number, playerHeight?: number): MovementFrameStep;
|
|
163
225
|
/**
|
|
164
226
|
* Highest blocking-box top the player can stand on at `(x, z)`: among obstacles whose footprint
|
|
165
227
|
* (inflated by `playerRadius`, matching the blocking inflation so any horizontal contact that would
|
|
@@ -1,15 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Movement core (pure model).
|
|
3
|
-
*
|
|
4
|
-
* This module owns the raw key model, the pure translation from held keys
|
|
5
|
-
* into a frame-independent {@link MovementIntent}, and the kinematics
|
|
6
|
-
* integration. Keeping it framework-free means the kinematics controller,
|
|
7
|
-
* the keyboard handler and the on-screen touch D-pad all speak the same
|
|
8
|
-
* vocabulary, and the translation is unit testable without a renderer.
|
|
9
|
-
* DOM capture lives in `@jgengine/shell`'s GamePlayerShell (keyboard and
|
|
10
|
-
* touch controls) and its camera rigs (pointer lock, the per-frame look
|
|
11
|
-
* channel).
|
|
12
|
-
*/
|
|
13
1
|
/** @internal */
|
|
14
2
|
export function createEmptyMovementKeys() {
|
|
15
3
|
return { w: false, a: false, s: false, d: false, shift: false, control: false, c: false, space: false };
|
|
@@ -90,9 +78,9 @@ export const MOVEMENT_TUNING = {
|
|
|
90
78
|
function resolveTargetSpeed(intent, baseSpeed, tuning) {
|
|
91
79
|
const backpedal = tuning?.backpedalSpeedMultiplier ?? MOVEMENT_TUNING.backpedalSpeedMultiplier;
|
|
92
80
|
const speedMultiplier = intent.crouching
|
|
93
|
-
? MOVEMENT_TUNING.crouchSpeedMultiplier
|
|
81
|
+
? (tuning?.crouchSpeedMultiplier ?? MOVEMENT_TUNING.crouchSpeedMultiplier)
|
|
94
82
|
: intent.running
|
|
95
|
-
? MOVEMENT_TUNING.runSpeedMultiplier
|
|
83
|
+
? (tuning?.runSpeedMultiplier ?? MOVEMENT_TUNING.runSpeedMultiplier)
|
|
96
84
|
: intent.forward < 0
|
|
97
85
|
? backpedal
|
|
98
86
|
: 1;
|
|
@@ -107,6 +95,11 @@ export function createPlayerMotionState() {
|
|
|
107
95
|
jumpOffset: 0,
|
|
108
96
|
grounded: true,
|
|
109
97
|
jumpHeld: false,
|
|
98
|
+
clockMs: 0,
|
|
99
|
+
groundedAtMs: 0,
|
|
100
|
+
landedAtMs: null,
|
|
101
|
+
jumpRising: false,
|
|
102
|
+
wasAirborne: false,
|
|
110
103
|
};
|
|
111
104
|
}
|
|
112
105
|
/**
|
|
@@ -121,8 +114,20 @@ export function createPlayerMotionState() {
|
|
|
121
114
|
* @internal
|
|
122
115
|
*/
|
|
123
116
|
export function advancePlayerMotion(motion, intent, forwardX, forwardZ, baseSpeed, rawDeltaSeconds, tuning, options) {
|
|
124
|
-
const deltaSeconds =
|
|
125
|
-
const
|
|
117
|
+
const deltaSeconds = motionStepSeconds(rawDeltaSeconds, options?.authoritativeStep);
|
|
118
|
+
const now = (motion.clockMs ?? 0) + deltaSeconds * 1000;
|
|
119
|
+
motion.clockMs = now;
|
|
120
|
+
if (motion.grounded && motion.wasAirborne)
|
|
121
|
+
motion.landedAtMs = now;
|
|
122
|
+
const airborneAtStart = !motion.grounded;
|
|
123
|
+
const landingRecoveryMs = tuning?.landingRecoveryMs ?? 0;
|
|
124
|
+
const landedAtMs = motion.landedAtMs ?? null;
|
|
125
|
+
const recovery = landingRecoveryMs > 0 && motion.grounded && landedAtMs !== null && now - landedAtMs < landingRecoveryMs
|
|
126
|
+
? (now - landedAtMs) / landingRecoveryMs
|
|
127
|
+
: 1;
|
|
128
|
+
const landingSpeedScale = tuning?.landingSpeedScale ?? 0.5;
|
|
129
|
+
const recoverySpeedScale = landingSpeedScale + (1 - landingSpeedScale) * recovery;
|
|
130
|
+
const targetSpeed = resolveTargetSpeed(intent, baseSpeed, tuning) * (options?.speedScale ?? 1) * recoverySpeedScale;
|
|
126
131
|
const gravityAcceleration = tuning?.gravityAcceleration ?? MOVEMENT_TUNING.gravityAcceleration;
|
|
127
132
|
const jumpVelocity = tuning?.jumpVelocity ?? MOVEMENT_TUNING.jumpVelocity;
|
|
128
133
|
let targetVelocityX = 0;
|
|
@@ -161,48 +166,151 @@ export function advancePlayerMotion(motion, intent, forwardX, forwardZ, baseSpee
|
|
|
161
166
|
targetVelocityZ = 0;
|
|
162
167
|
}
|
|
163
168
|
}
|
|
164
|
-
const acceleration = motion.grounded
|
|
169
|
+
const acceleration = motion.grounded
|
|
170
|
+
? (tuning?.groundAcceleration ?? MOVEMENT_TUNING.groundAcceleration)
|
|
171
|
+
: (tuning?.airAcceleration ?? MOVEMENT_TUNING.airAcceleration);
|
|
165
172
|
const accelerationBlend = 1 - Math.exp(-acceleration * deltaSeconds);
|
|
166
173
|
motion.horizontalVelocityX += (targetVelocityX - motion.horizontalVelocityX) * accelerationBlend;
|
|
167
174
|
motion.horizontalVelocityZ += (targetVelocityZ - motion.horizontalVelocityZ) * accelerationBlend;
|
|
168
175
|
if (!intent.moving && motion.grounded) {
|
|
169
|
-
const friction = Math.exp(-MOVEMENT_TUNING.groundFriction * deltaSeconds);
|
|
176
|
+
const friction = Math.exp(-(tuning?.groundFriction ?? MOVEMENT_TUNING.groundFriction) * deltaSeconds);
|
|
170
177
|
motion.horizontalVelocityX *= friction;
|
|
171
178
|
motion.horizontalVelocityZ *= friction;
|
|
172
179
|
}
|
|
173
180
|
const floating = options?.floating === true;
|
|
174
181
|
const jumpPressed = intent.jumping;
|
|
175
|
-
if (
|
|
182
|
+
if (motion.grounded)
|
|
183
|
+
motion.groundedAtMs = now;
|
|
184
|
+
const coyoteMs = tuning?.coyoteMs ?? 0;
|
|
185
|
+
const groundedAtMs = motion.groundedAtMs ?? null;
|
|
186
|
+
const canJump = !intent.crouching &&
|
|
187
|
+
!floating &&
|
|
188
|
+
recovery >= 1 &&
|
|
189
|
+
(motion.grounded ||
|
|
190
|
+
(coyoteMs > 0 && motion.verticalVelocity <= 0 && groundedAtMs !== null && now - groundedAtMs <= coyoteMs));
|
|
191
|
+
const pressEdge = jumpPressed && !motion.jumpHeld;
|
|
192
|
+
const buffer = options?.buffer;
|
|
193
|
+
if (buffer !== undefined) {
|
|
194
|
+
if (pressEdge)
|
|
195
|
+
buffer.press("jump", now);
|
|
196
|
+
else if (!jumpPressed && motion.jumpHeld)
|
|
197
|
+
buffer.release("jump", now);
|
|
198
|
+
}
|
|
199
|
+
const jump = canJump && (buffer === undefined ? pressEdge : buffer.consume("jump", now, tuning?.jumpBufferMs ?? 0));
|
|
200
|
+
if (jump) {
|
|
176
201
|
motion.verticalVelocity = jumpVelocity;
|
|
177
202
|
motion.grounded = false;
|
|
203
|
+
motion.groundedAtMs = null;
|
|
204
|
+
motion.jumpRising = true;
|
|
205
|
+
}
|
|
206
|
+
else if (motion.jumpRising && !jumpPressed && motion.jumpHeld && motion.verticalVelocity > 0) {
|
|
207
|
+
motion.verticalVelocity *= Math.max(0, Math.min(1, tuning?.jumpCutFactor ?? 1));
|
|
208
|
+
motion.jumpRising = false;
|
|
178
209
|
}
|
|
179
210
|
motion.jumpHeld = jumpPressed;
|
|
180
211
|
if (floating) {
|
|
181
212
|
motion.verticalVelocity = 0;
|
|
182
213
|
motion.jumpOffset = 0;
|
|
183
214
|
motion.grounded = true;
|
|
215
|
+
motion.jumpRising = false;
|
|
184
216
|
}
|
|
185
217
|
else if (!motion.grounded || motion.verticalVelocity > 0) {
|
|
186
|
-
|
|
218
|
+
const apexSpeed = tuning?.apexSpeed ?? 1.5;
|
|
219
|
+
const gravityScale = Math.abs(motion.verticalVelocity) < apexSpeed
|
|
220
|
+
? (tuning?.apexGravityScale ?? 1)
|
|
221
|
+
: motion.verticalVelocity < 0
|
|
222
|
+
? (tuning?.fallGravityScale ?? 1)
|
|
223
|
+
: 1;
|
|
224
|
+
motion.verticalVelocity -= gravityAcceleration * gravityScale * deltaSeconds;
|
|
225
|
+
if (motion.verticalVelocity <= 0)
|
|
226
|
+
motion.jumpRising = false;
|
|
187
227
|
motion.jumpOffset += motion.verticalVelocity * deltaSeconds;
|
|
188
|
-
if (motion.jumpOffset <= 0) {
|
|
228
|
+
if (motion.jumpOffset <= 0 && options?.externalGrounding !== true) {
|
|
189
229
|
motion.jumpOffset = 0;
|
|
190
230
|
motion.verticalVelocity = 0;
|
|
191
231
|
motion.grounded = true;
|
|
192
232
|
}
|
|
193
233
|
}
|
|
234
|
+
if (airborneAtStart && motion.grounded)
|
|
235
|
+
motion.landedAtMs = now;
|
|
236
|
+
motion.wasAirborne = !motion.grounded;
|
|
194
237
|
return {
|
|
195
238
|
stepX: motion.horizontalVelocityX * deltaSeconds,
|
|
196
239
|
stepZ: motion.horizontalVelocityZ * deltaSeconds,
|
|
197
240
|
};
|
|
198
241
|
}
|
|
242
|
+
/** @internal Validate trusted simulation time or apply the standalone frame-stall clamp. */
|
|
243
|
+
export function motionStepSeconds(seconds, authoritativeStep = false) {
|
|
244
|
+
if (!Number.isFinite(seconds) || seconds < 0)
|
|
245
|
+
throw new RangeError("Movement step seconds must be finite and nonnegative");
|
|
246
|
+
return authoritativeStep ? seconds : Math.min(seconds, MOVEMENT_TUNING.maxFrameSeconds);
|
|
247
|
+
}
|
|
199
248
|
const DEFAULT_OBSTACLE_HALF_EXTENTS = [0.5, 0.5, 0.5];
|
|
200
249
|
/** @internal Player radius the walking obstruction inflates obstacles by; callers gather within reach of it. */
|
|
201
250
|
export const DEFAULT_OBSTACLE_PLAYER_RADIUS = 0.3;
|
|
202
|
-
/**
|
|
203
|
-
const
|
|
251
|
+
/** @internal Default feet-to-head span for heightfield obstruction; matches DEFAULT_VOXEL_DIMS.height. */
|
|
252
|
+
export const DEFAULT_OBSTACLE_PLAYER_HEIGHT = 1.8;
|
|
204
253
|
/** Extra nudge past a solid face when depenetrating so the escaped capsule lands just outside, not on, the box. */
|
|
205
254
|
const PENETRATION_EPSILON = 1e-3;
|
|
255
|
+
function verticalContactTolerance(feetY, headY, minY, maxY) {
|
|
256
|
+
return Number.EPSILON * 8 * Math.max(1, Math.abs(feetY), Math.abs(headY), Math.abs(minY), Math.abs(maxY));
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Sweep a heightfield walker's vertical step at its accepted XZ position against blocking bounds.
|
|
260
|
+
* The nearest crossed underside stops ascent; the nearest crossed top catches descent, including
|
|
261
|
+
* thin geometry crossed in one step. Compound boxes are relative to obstacle.position; offset is
|
|
262
|
+
* used only for a single AABB. Real starting intersections are reported separately, not repaired.
|
|
263
|
+
* @internal
|
|
264
|
+
*/
|
|
265
|
+
export function resolveObstacleVerticalStep(position, stepY, obstacles, radius = DEFAULT_OBSTACLE_PLAYER_RADIUS, playerHeight = DEFAULT_OBSTACLE_PLAYER_HEIGHT) {
|
|
266
|
+
if (!Number.isFinite(stepY))
|
|
267
|
+
throw new RangeError("Vertical obstacle step must be finite.");
|
|
268
|
+
if (!Number.isFinite(playerHeight) || playerHeight <= 0)
|
|
269
|
+
throw new RangeError("Obstacle player height must be finite and positive.");
|
|
270
|
+
const feetY = position[1];
|
|
271
|
+
const headY = feetY + playerHeight;
|
|
272
|
+
const nextFeet = feetY + stepY;
|
|
273
|
+
const nextHead = nextFeet + playerHeight;
|
|
274
|
+
const result = { stepY, hitCeiling: false, ceilingFeetY: null, ceilingTolerance: 0, landed: false, initialOverlap: false };
|
|
275
|
+
const consider = (minX, maxX, minY, maxY, minZ, maxZ) => {
|
|
276
|
+
if (position[0] <= minX - radius || position[0] >= maxX + radius || position[2] <= minZ - radius || position[2] >= maxZ + radius)
|
|
277
|
+
return;
|
|
278
|
+
const tolerance = verticalContactTolerance(feetY, headY, minY, maxY);
|
|
279
|
+
if (feetY < maxY - tolerance && headY > minY + tolerance) {
|
|
280
|
+
result.initialOverlap = true;
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (stepY > 0 && headY <= minY + tolerance && nextHead >= minY) {
|
|
284
|
+
const limitedStep = minY - playerHeight - tolerance - feetY;
|
|
285
|
+
if (limitedStep <= result.stepY) {
|
|
286
|
+
result.stepY = limitedStep;
|
|
287
|
+
// Keep the actual face limit separate from the tiny collision margin, so a support with
|
|
288
|
+
// exactly enough headroom remains valid at translated coordinates.
|
|
289
|
+
result.ceilingFeetY = minY - playerHeight;
|
|
290
|
+
result.ceilingTolerance = tolerance;
|
|
291
|
+
}
|
|
292
|
+
result.hitCeiling = true;
|
|
293
|
+
}
|
|
294
|
+
else if (stepY < 0 && feetY >= maxY - tolerance && nextFeet <= maxY) {
|
|
295
|
+
result.stepY = Math.max(result.stepY, maxY - feetY);
|
|
296
|
+
result.landed = true;
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
for (const obstacle of obstacles) {
|
|
300
|
+
const [x, y, z] = obstacle.position;
|
|
301
|
+
if (obstacle.boxes !== undefined && obstacle.boxes.length > 0) {
|
|
302
|
+
for (const box of obstacle.boxes)
|
|
303
|
+
consider(x + box.min[0], x + box.max[0], y + box.min[1], y + box.max[1], z + box.min[2], z + box.max[2]);
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
const half = obstacle.halfExtents ?? DEFAULT_OBSTACLE_HALF_EXTENTS;
|
|
307
|
+
const offset = obstacle.offset;
|
|
308
|
+
const cx = x + (offset?.[0] ?? 0), cy = y + (offset?.[1] ?? 0), cz = z + (offset?.[2] ?? 0);
|
|
309
|
+
consider(cx - half[0], cx + half[0], cy - half[1], cy + half[1], cz - half[2], cz + half[2]);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return result;
|
|
313
|
+
}
|
|
206
314
|
/**
|
|
207
315
|
* Swept clamp of a single-axis step against a box span. Entering from outside stops on the near
|
|
208
316
|
* face — including when the raw target would land *beyond* the far face (the old form returned the
|
|
@@ -242,16 +350,18 @@ function clampAxisStep(originalCoord, step, min, max) {
|
|
|
242
350
|
* tread is stepped up, not bumped into.
|
|
243
351
|
* @internal
|
|
244
352
|
*/
|
|
245
|
-
export function resolveObstacleStep(current, stepX, stepZ, obstacles, playerRadius = DEFAULT_OBSTACLE_PLAYER_RADIUS, stepUpHeight = 0) {
|
|
353
|
+
export function resolveObstacleStep(current, stepX, stepZ, obstacles, playerRadius = DEFAULT_OBSTACLE_PLAYER_RADIUS, stepUpHeight = 0, playerHeight = DEFAULT_OBSTACLE_PLAYER_HEIGHT) {
|
|
354
|
+
if (!Number.isFinite(playerHeight) || playerHeight <= 0)
|
|
355
|
+
throw new RangeError("Obstacle player height must be finite and positive.");
|
|
246
356
|
const feetY = current[1];
|
|
247
|
-
const headY = feetY +
|
|
357
|
+
const headY = feetY + playerHeight;
|
|
248
358
|
const reachX = Math.abs(stepX) + playerRadius;
|
|
249
359
|
const reachZ = Math.abs(stepZ) + playerRadius;
|
|
250
360
|
const nearby = [];
|
|
251
361
|
const considerBox = (minX, maxX, minY, maxY, minZ, maxZ) => {
|
|
252
362
|
// Vertical-span cull: a sub-box entirely above the head (walk under the lintel), below the feet,
|
|
253
363
|
// or low enough to step up onto (`stepUpHeight`) is skipped — walkable ledges don't wall.
|
|
254
|
-
if (maxY <= feetY + stepUpHeight || minY >= headY)
|
|
364
|
+
if (maxY <= feetY + stepUpHeight || minY >= headY - verticalContactTolerance(feetY, headY, minY, maxY))
|
|
255
365
|
return;
|
|
256
366
|
const centerX = (minX + maxX) / 2;
|
|
257
367
|
const centerZ = (minZ + maxZ) / 2;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { PhysicsConfig } from "../game/defineGame.js";
|
|
2
|
+
import type { PlayerMovementConfig } from "../game/playableGame.js";
|
|
3
|
+
/** The walk character {@link measureMovement} drives: the same fields a game passes to `defineGame`. */
|
|
4
|
+
export interface MovementProbeSubject {
|
|
5
|
+
/** `defineGame({ movement })`; `feel` and `backpedalMult` shape the walk. */
|
|
6
|
+
movement?: PlayerMovementConfig;
|
|
7
|
+
/** `defineGame({ physics })`; `gravity` and `jumpVelocity` shape the jump. */
|
|
8
|
+
physics?: PhysicsConfig;
|
|
9
|
+
/** The player entity's `movement.walkSpeed` (default `2`, the controller default). */
|
|
10
|
+
walkSpeed?: number;
|
|
11
|
+
}
|
|
12
|
+
/** Scenario settings for {@link measureMovement}; every field has a default. */
|
|
13
|
+
export interface MovementProbeOptions {
|
|
14
|
+
/** Fixed tick, s (default `1/60`). */
|
|
15
|
+
dt?: number;
|
|
16
|
+
/** Measure with sprint held (default `false`). */
|
|
17
|
+
sprint?: boolean;
|
|
18
|
+
/** How long jump is held for `tapJumpHeight`, s (default `0.1`). */
|
|
19
|
+
tapSeconds?: number;
|
|
20
|
+
}
|
|
21
|
+
/** Deterministic walk-feel metrics. `Infinity` means the target was never reached. */
|
|
22
|
+
export interface MovementReport {
|
|
23
|
+
/** Steady ground speed holding forward, m/s. */
|
|
24
|
+
topSpeed: number;
|
|
25
|
+
/** Standing start until speed reaches 90% of `topSpeed`, s. */
|
|
26
|
+
timeToTopSpeed: number;
|
|
27
|
+
/** Distance slid after releasing input at `topSpeed`, until speed is under 0.05 m/s, m. */
|
|
28
|
+
stopDistance: number;
|
|
29
|
+
/** Seconds from releasing input at `topSpeed` until speed is under 0.05 m/s. */
|
|
30
|
+
stopTime: number;
|
|
31
|
+
/** Seconds from reversing direction at `topSpeed` until speed along the new heading reaches 90% of `topSpeed`. */
|
|
32
|
+
turnAroundTime: number;
|
|
33
|
+
/** Peak height of a held standing jump, m. */
|
|
34
|
+
jumpHeight: number;
|
|
35
|
+
/** Peak height of a standing jump released after `tapSeconds`, m. Lower than `jumpHeight` when `jumpCutFactor` is set. */
|
|
36
|
+
tapJumpHeight: number;
|
|
37
|
+
/** Seconds from takeoff to the jump's peak. */
|
|
38
|
+
apexTime: number;
|
|
39
|
+
/** Seconds from takeoff to landing. */
|
|
40
|
+
airTime: number;
|
|
41
|
+
/** Sideways distance covered by strafing from the first airborne tick of a standing jump until landing, m. */
|
|
42
|
+
airControlReach: number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Drives a walk character through fixed scenarios (standing start, release to stop, reversal, a standing
|
|
46
|
+
* jump, a tapped jump, a strafed jump) on the same integrator `stepPlayerMovement` uses, and reports the feel metrics a
|
|
47
|
+
* test can assert. Deterministic: the same subject and options always produce the same report, so a feel
|
|
48
|
+
* change shows up as a number moving. Collision, terrain and swimming are out of scope; it measures flat ground.
|
|
49
|
+
* @capability movement-metrics measure a walking character's feel as numbers — time to top speed, stop distance, jump height and apex, air control, turn-around
|
|
50
|
+
*/
|
|
51
|
+
export declare function measureMovement(subject?: MovementProbeSubject, options?: MovementProbeOptions): MovementReport;
|