@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,11 +1,15 @@
|
|
|
1
|
+
import { createInputBuffer } from "../input/inputBuffer.js";
|
|
1
2
|
import { applyHorizontalImpulses, applyMotionImpulses } from "../runtime/motionIntents.js";
|
|
2
|
-
import { createCharacterController } from "./characterController.js";
|
|
3
|
+
import { createCharacterController, } from "./characterController.js";
|
|
3
4
|
import { groundFieldFor, hasEnvironmentTerrain, sampleSlope } from "../world/terrain.js";
|
|
4
|
-
import { advancePlayerMotion, constrainStepToAxis, createEmptyMovementKeys, createPlayerMotionState, DEFAULT_OBSTACLE_PLAYER_RADIUS, obstacleSupportHeight, resolveMovementIntent, resolveObstacleStep,
|
|
5
|
+
import { advancePlayerMotion, constrainStepToAxis, createEmptyMovementKeys, createPlayerMotionState, DEFAULT_OBSTACLE_PLAYER_RADIUS, DEFAULT_OBSTACLE_PLAYER_HEIGHT, motionStepSeconds, obstacleSupportHeight, resolveMovementIntent, resolveObstacleStep, resolveObstacleVerticalStep, snapPositionToGrid, } from "./movementModel.js";
|
|
5
6
|
import { advanceFreeFlight, createFreeFlightState, resolveFlightStep, resolveFreeFlightIntentFromInput, } from "./freeFlight.js";
|
|
6
7
|
import { solidObstaclesNear } from "./solidObstacles.js";
|
|
8
|
+
import { withPoseRollback } from "./poseState.js";
|
|
7
9
|
import { approachYaw, steerYaw } from "./steering.js";
|
|
10
|
+
import { resolveTerrainGradeStep } from "./terrainGrade.js";
|
|
8
11
|
import { advanceVoxelPlayer, createVoxelPlayerBody, } from "./voxelController.js";
|
|
12
|
+
const CONTROLLER_CONFIG_FIELDS = ["radius", "height", "stepHeight", "maxSlopeDeg", "skinWidth", "crouchHeight", "snapDistance", "maxSlides", "mask"];
|
|
9
13
|
const DEFAULT_TURN_SPEED = 2.4;
|
|
10
14
|
const DEFAULT_WALK_SPEED = 2;
|
|
11
15
|
const DEFAULT_SWIM_SPEED_MULTIPLIER = 0.65;
|
|
@@ -34,25 +38,41 @@ export function resolvePhysicsTuning(physics) {
|
|
|
34
38
|
}
|
|
35
39
|
/** Gather a game's collision/movement/physics/world config into a {@link PlayerMovementTuning} — call once per world; both the shell and a host pass the result to {@link stepPlayerMovement}. */
|
|
36
40
|
export function resolvePlayerMovementTuning(opts) {
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
const overrides = {
|
|
42
|
+
get gravityAcceleration() { return opts.physics?.gravity === undefined ? undefined : -opts.physics.gravity; },
|
|
43
|
+
get jumpVelocity() { return opts.physics?.jumpVelocity; },
|
|
44
|
+
get backpedalSpeedMultiplier() { return opts.movement?.backpedalMult; },
|
|
45
|
+
get groundAcceleration() { return opts.movement?.feel?.groundAcceleration; },
|
|
46
|
+
get airAcceleration() { return opts.movement?.feel?.airAcceleration; },
|
|
47
|
+
get groundFriction() { return opts.movement?.feel?.groundFriction; },
|
|
48
|
+
get runSpeedMultiplier() { return opts.movement?.feel?.runMultiplier; },
|
|
49
|
+
get crouchSpeedMultiplier() { return opts.movement?.feel?.crouchMultiplier; },
|
|
50
|
+
get jumpBufferMs() { return opts.movement?.feel?.jumpBufferMs; },
|
|
51
|
+
get coyoteMs() { return opts.movement?.feel?.coyoteMs; },
|
|
52
|
+
get jumpCutFactor() { return opts.movement?.feel?.jumpCutFactor; },
|
|
53
|
+
get apexGravityScale() { return opts.movement?.feel?.apexGravityScale; },
|
|
54
|
+
get apexSpeed() { return opts.movement?.feel?.apexSpeed; },
|
|
55
|
+
get fallGravityScale() { return opts.movement?.feel?.fallGravityScale; },
|
|
56
|
+
get landingRecoveryMs() { return opts.movement?.feel?.landingRecoveryMs; },
|
|
57
|
+
get landingSpeedScale() { return opts.movement?.feel?.landingSpeedScale; },
|
|
58
|
+
};
|
|
59
|
+
const defaultCapsule = {
|
|
60
|
+
radius: DEFAULT_OBSTACLE_PLAYER_RADIUS,
|
|
61
|
+
height: 1.8,
|
|
62
|
+
stepHeight: DEFAULT_PLAYER_STEP_HEIGHT,
|
|
63
|
+
};
|
|
64
|
+
const controller = {
|
|
65
|
+
get backend() { return opts.physics.backend; },
|
|
66
|
+
get capsule() { return opts.physics?.controller ?? defaultCapsule; },
|
|
67
|
+
};
|
|
40
68
|
return {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
backend: opts.physics.backend,
|
|
49
|
-
capsule: opts.physics.controller ?? {
|
|
50
|
-
radius: DEFAULT_OBSTACLE_PLAYER_RADIUS,
|
|
51
|
-
height: 1.8,
|
|
52
|
-
stepHeight: DEFAULT_PLAYER_STEP_HEIGHT,
|
|
53
|
-
},
|
|
54
|
-
},
|
|
55
|
-
}),
|
|
69
|
+
get collision() { return opts.collision; },
|
|
70
|
+
get movement() { return opts.movement; },
|
|
71
|
+
get physics() {
|
|
72
|
+
return opts.physics === undefined && opts.movement?.feel === undefined && opts.movement?.backpedalMult === undefined
|
|
73
|
+
? undefined : overrides;
|
|
74
|
+
},
|
|
75
|
+
get controller() { return opts.physics?.backend === undefined ? undefined : controller; },
|
|
56
76
|
ground: groundFieldFor(opts.world),
|
|
57
77
|
hasTerrain: hasEnvironmentTerrain(opts.world),
|
|
58
78
|
};
|
|
@@ -66,21 +86,190 @@ function storeFor(ctx) {
|
|
|
66
86
|
solids: { count: -1, set: new Set() },
|
|
67
87
|
};
|
|
68
88
|
stores.set(ctx, store);
|
|
89
|
+
const retained = store;
|
|
90
|
+
ctx.scene.entity.subscribeMembership(() => {
|
|
91
|
+
for (const [userId, state] of retained.players) {
|
|
92
|
+
if (state.liveEntityId !== null) {
|
|
93
|
+
if (ctx.scene.entity.get(state.liveEntityId) === null)
|
|
94
|
+
retained.players.delete(userId);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
const entityId = ctx.player.possession.active(userId);
|
|
98
|
+
if (ctx.scene.entity.get(entityId) !== null)
|
|
99
|
+
state.liveEntityId = entityId;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
});
|
|
69
103
|
}
|
|
70
104
|
return store;
|
|
71
105
|
}
|
|
72
|
-
function stateFor(store, userId) {
|
|
106
|
+
function stateFor(ctx, store, userId) {
|
|
73
107
|
let state = store.players.get(userId);
|
|
74
108
|
if (state === undefined) {
|
|
75
|
-
state = {
|
|
109
|
+
state = {
|
|
110
|
+
heading: 0,
|
|
111
|
+
facing: null,
|
|
112
|
+
voxelBody: null,
|
|
113
|
+
motion: null,
|
|
114
|
+
flight: null,
|
|
115
|
+
controller: null,
|
|
116
|
+
controllerConfig: null,
|
|
117
|
+
controllerJumpHeld: false,
|
|
118
|
+
jumpBuffer: null,
|
|
119
|
+
pendingController: null,
|
|
120
|
+
heightfieldHeight: null,
|
|
121
|
+
heightfieldEntityId: null,
|
|
122
|
+
liveEntityId: null,
|
|
123
|
+
entityId: null,
|
|
124
|
+
telemetry: null,
|
|
125
|
+
};
|
|
126
|
+
const entityId = ctx.player.possession.active(userId);
|
|
127
|
+
if (ctx.scene.entity.get(entityId) !== null)
|
|
128
|
+
state.liveEntityId = entityId;
|
|
76
129
|
store.players.set(userId, state);
|
|
77
130
|
}
|
|
78
131
|
return state;
|
|
79
132
|
}
|
|
133
|
+
function invalidateTelemetry(state) {
|
|
134
|
+
state.entityId = null;
|
|
135
|
+
state.telemetry = null;
|
|
136
|
+
}
|
|
137
|
+
function validateCollisionHeight(height) {
|
|
138
|
+
if (!Number.isFinite(height) || height <= 0) {
|
|
139
|
+
throw new RangeError("PlayerMovementConfig.collisionHeight must be finite and positive.");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function retuneController(state, config) {
|
|
143
|
+
const previous = state.controllerConfig;
|
|
144
|
+
if (previous !== null) {
|
|
145
|
+
let changed = false;
|
|
146
|
+
for (const field of CONTROLLER_CONFIG_FIELDS) {
|
|
147
|
+
if (previous[field] !== config[field]) {
|
|
148
|
+
changed = true;
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (!changed)
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const next = {
|
|
156
|
+
radius: config.radius,
|
|
157
|
+
height: config.height,
|
|
158
|
+
stepHeight: config.stepHeight,
|
|
159
|
+
maxSlopeDeg: config.maxSlopeDeg,
|
|
160
|
+
skinWidth: config.skinWidth,
|
|
161
|
+
crouchHeight: config.crouchHeight,
|
|
162
|
+
snapDistance: config.snapDistance,
|
|
163
|
+
maxSlides: config.maxSlides,
|
|
164
|
+
mask: config.mask,
|
|
165
|
+
};
|
|
166
|
+
state.controller.retune(next);
|
|
167
|
+
state.controllerConfig = next;
|
|
168
|
+
}
|
|
169
|
+
function positionAccepted(ctx, entityId, x, y, z) {
|
|
170
|
+
const actual = ctx.scene.entity.get(entityId)?.position;
|
|
171
|
+
return actual !== undefined && actual[0] === x && actual[1] === y && actual[2] === z;
|
|
172
|
+
}
|
|
173
|
+
function updateTelemetry(state, entityId, grounded, verticalVelocity, crouching, collisionHeight, requestedCollisionHeight, collisionHeightBlocked) {
|
|
174
|
+
state.entityId = entityId;
|
|
175
|
+
const telemetry = state.telemetry ??= { grounded, verticalVelocity, crouching };
|
|
176
|
+
telemetry.grounded = grounded;
|
|
177
|
+
telemetry.verticalVelocity = verticalVelocity;
|
|
178
|
+
telemetry.crouching = crouching;
|
|
179
|
+
if (collisionHeight === undefined) {
|
|
180
|
+
delete telemetry.collisionHeight;
|
|
181
|
+
delete telemetry.requestedCollisionHeight;
|
|
182
|
+
delete telemetry.collisionHeightBlocked;
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
telemetry.collisionHeight = collisionHeight;
|
|
186
|
+
telemetry.requestedCollisionHeight = requestedCollisionHeight;
|
|
187
|
+
telemetry.collisionHeightBlocked = collisionHeightBlocked;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Read the entity's last shared movement result through indexed possession ownership. Returns `null` before a
|
|
192
|
+
* step, after movement restore/forget, when a commit policy replaces the motor proposal, or for an entity not
|
|
193
|
+
* currently driven by this motor. Reuses one live view;
|
|
194
|
+
* custom movers supply their own animation parameters. Flight never reports ground contact or a walking crouch.
|
|
195
|
+
*
|
|
196
|
+
* @capability movement-telemetry read physical grounded, vertical velocity and crouch state without animation-store writes
|
|
197
|
+
*/
|
|
198
|
+
export function playerMovementTelemetry(ctx, entityId) {
|
|
199
|
+
const userId = ctx.player.possession.ownerOf(entityId) ?? entityId;
|
|
200
|
+
if (ctx.player.possession.active(userId) !== entityId || ctx.scene.entity.get(entityId) === null)
|
|
201
|
+
return null;
|
|
202
|
+
const state = stores.get(ctx)?.players.get(userId);
|
|
203
|
+
return state?.entityId === entityId ? state.telemetry : null;
|
|
204
|
+
}
|
|
80
205
|
/** One player's current heading (radians), integrated by {@link stepPlayerMovement} — the shell reads it back into its camera/aim yaw. */
|
|
81
206
|
export function playerMovementHeading(ctx, userId) {
|
|
82
207
|
return stores.get(ctx)?.players.get(userId)?.heading ?? 0;
|
|
83
208
|
}
|
|
209
|
+
function copyController(state) {
|
|
210
|
+
return { ...state, position: [...state.position], groundNormal: [...state.groundNormal] };
|
|
211
|
+
}
|
|
212
|
+
/** Copy one player's movement state for prediction, rollback or a save; `null` when the player has not moved yet. */
|
|
213
|
+
export function snapshotPlayerMovement(ctx, userId) {
|
|
214
|
+
const state = stores.get(ctx)?.players.get(userId);
|
|
215
|
+
if (state === undefined)
|
|
216
|
+
return null;
|
|
217
|
+
const controller = state.controller?.snapshot() ?? state.pendingController;
|
|
218
|
+
return {
|
|
219
|
+
heading: state.heading,
|
|
220
|
+
facing: state.facing,
|
|
221
|
+
voxelBody: state.voxelBody === null ? null : { ...state.voxelBody },
|
|
222
|
+
motion: state.motion === null ? null : { ...state.motion },
|
|
223
|
+
flight: state.flight === null ? null : { ...state.flight },
|
|
224
|
+
controller: controller === null ? null : copyController(controller),
|
|
225
|
+
controllerJumpHeld: state.controllerJumpHeld,
|
|
226
|
+
jumpBuffer: state.jumpBuffer?.snapshot() ?? null,
|
|
227
|
+
heightfieldHeight: state.heightfieldHeight,
|
|
228
|
+
heightfieldEntityId: state.heightfieldEntityId,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
/** Put a player's movement state back to a {@link snapshotPlayerMovement} copy. Pending state survives until first spawn; a driven actor's despawn clears it. */
|
|
232
|
+
export function restorePlayerMovement(ctx, userId, snapshot) {
|
|
233
|
+
const heightfieldHeight = snapshot.heightfieldHeight === undefined
|
|
234
|
+
? (snapshot.motion === null ? null : DEFAULT_OBSTACLE_PLAYER_HEIGHT) : snapshot.heightfieldHeight;
|
|
235
|
+
if (heightfieldHeight !== null)
|
|
236
|
+
validateCollisionHeight(heightfieldHeight);
|
|
237
|
+
const heightfieldEntityId = snapshot.heightfieldEntityId === undefined
|
|
238
|
+
? (heightfieldHeight === null ? null : ctx.player.possession.active(userId)) : snapshot.heightfieldEntityId;
|
|
239
|
+
if (heightfieldEntityId !== null && typeof heightfieldEntityId !== "string") {
|
|
240
|
+
throw new RangeError("PlayerMovementSnapshot.heightfieldEntityId must be a string or null.");
|
|
241
|
+
}
|
|
242
|
+
const state = stateFor(ctx, storeFor(ctx), userId);
|
|
243
|
+
const liveEntityId = ctx.player.possession.active(userId);
|
|
244
|
+
state.liveEntityId = ctx.scene.entity.get(liveEntityId) === null ? null : liveEntityId;
|
|
245
|
+
invalidateTelemetry(state);
|
|
246
|
+
state.heading = snapshot.heading;
|
|
247
|
+
state.facing = snapshot.facing;
|
|
248
|
+
state.voxelBody = snapshot.voxelBody === null ? null : { ...snapshot.voxelBody };
|
|
249
|
+
state.motion = snapshot.motion === null ? null : { ...snapshot.motion };
|
|
250
|
+
state.flight = snapshot.flight === null ? null : { ...snapshot.flight };
|
|
251
|
+
state.heightfieldHeight = heightfieldHeight;
|
|
252
|
+
state.heightfieldEntityId = heightfieldEntityId;
|
|
253
|
+
state.controllerJumpHeld = snapshot.controllerJumpHeld;
|
|
254
|
+
const jumpBuffer = snapshot.jumpBuffer ?? null;
|
|
255
|
+
if (jumpBuffer === null) {
|
|
256
|
+
state.jumpBuffer = null;
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
state.jumpBuffer ??= createInputBuffer({ windowMs: jumpBuffer.windowMs });
|
|
260
|
+
state.jumpBuffer.restore(jumpBuffer);
|
|
261
|
+
}
|
|
262
|
+
const controller = snapshot.controller === null ? null : copyController(snapshot.controller);
|
|
263
|
+
if (state.controller !== null && controller !== null) {
|
|
264
|
+
state.controller.restore(controller);
|
|
265
|
+
state.pendingController = null;
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
if (controller === null)
|
|
269
|
+
state.controller = null;
|
|
270
|
+
state.pendingController = controller;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
84
273
|
/** Drop a player's retained movement state (heading + kinematic body) — call on leave so a rejoin starts fresh instead of resuming stale velocity. */
|
|
85
274
|
export function forgetPlayerMovement(ctx, userId) {
|
|
86
275
|
stores.get(ctx)?.players.delete(userId);
|
|
@@ -148,27 +337,81 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitc
|
|
|
148
337
|
if (player.movement?.frozen === true)
|
|
149
338
|
return;
|
|
150
339
|
const store = storeFor(ctx);
|
|
151
|
-
const
|
|
340
|
+
const retainedState = store.players.get(userId);
|
|
341
|
+
const state = stateFor(ctx, store, userId);
|
|
342
|
+
let collisionHeight = DEFAULT_OBSTACLE_PLAYER_HEIGHT;
|
|
343
|
+
let requestedCollisionHeight = DEFAULT_OBSTACLE_PLAYER_HEIGHT;
|
|
344
|
+
let collisionHeightBlocked = false;
|
|
152
345
|
const held = new Set(input.held);
|
|
153
346
|
const isDown = (action) => held.has(action);
|
|
347
|
+
let nextHeading = state.heading;
|
|
154
348
|
if (heading !== undefined) {
|
|
155
|
-
|
|
349
|
+
nextHeading = heading;
|
|
156
350
|
}
|
|
157
351
|
else {
|
|
158
352
|
const turnInput = (isDown("turnRight") ? 1 : 0) - (isDown("turnLeft") ? 1 : 0);
|
|
159
353
|
const turnSpeed = tuning.movement?.turnSpeed ?? DEFAULT_TURN_SPEED;
|
|
160
354
|
if (turnInput !== 0)
|
|
161
|
-
|
|
355
|
+
nextHeading = steerYaw(nextHeading, turnInput, turnSpeed, dt);
|
|
162
356
|
}
|
|
163
|
-
const forwardX = Math.sin(
|
|
164
|
-
const forwardZ = Math.cos(
|
|
357
|
+
const forwardX = Math.sin(nextHeading);
|
|
358
|
+
const forwardZ = Math.cos(nextHeading);
|
|
165
359
|
const keys = createEmptyMovementKeys();
|
|
166
360
|
keys.w = isDown("moveForward");
|
|
167
361
|
keys.s = isDown("moveBack");
|
|
168
362
|
keys.a = isDown("moveLeft");
|
|
169
363
|
keys.d = isDown("moveRight");
|
|
170
|
-
|
|
171
|
-
|
|
364
|
+
let flightTuning;
|
|
365
|
+
try {
|
|
366
|
+
flightTuning = withPoseRollback(ctx.player.movement, playerId, () => {
|
|
367
|
+
keys.shift = isDown("sprint") && (tuning.movement?.canSprint?.(ctx) ?? true);
|
|
368
|
+
keys.space = isDown("jump");
|
|
369
|
+
keys.c = isDown("crouch") && ctx.player.movement.setPose(playerId, "crouch") === null;
|
|
370
|
+
if (!keys.c && tuning.controller === undefined) {
|
|
371
|
+
const currentPose = ctx.player.movement.getPose(playerId);
|
|
372
|
+
if (currentPose !== "prone")
|
|
373
|
+
ctx.player.movement.setPose(playerId, keys.shift ? "running" : "standing");
|
|
374
|
+
}
|
|
375
|
+
const flightTuning = flightTuningFor(tuning.movement, ctx);
|
|
376
|
+
if (flightTuning === null && tuning.controller === undefined && tuning.collision?.voxel !== true) {
|
|
377
|
+
const authoredHeight = tuning.movement?.collisionHeight;
|
|
378
|
+
requestedCollisionHeight = authoredHeight === undefined ? DEFAULT_OBSTACLE_PLAYER_HEIGHT : authoredHeight;
|
|
379
|
+
validateCollisionHeight(requestedCollisionHeight);
|
|
380
|
+
const previousHeight = state.heightfieldEntityId !== null && state.heightfieldEntityId !== playerId
|
|
381
|
+
? null : state.heightfieldHeight;
|
|
382
|
+
collisionHeight = previousHeight ?? requestedCollisionHeight;
|
|
383
|
+
if (tuning.movement?.collideObjects !== false &&
|
|
384
|
+
(previousHeight === null && authoredHeight !== undefined || requestedCollisionHeight > collisionHeight)) {
|
|
385
|
+
const swim = tuning.movement?.swim;
|
|
386
|
+
const swimEnabled = swim === true || (typeof swim === "object" && swim !== null);
|
|
387
|
+
const floating = swimEnabled && tuning.ground.waterLevel !== undefined &&
|
|
388
|
+
tuning.ground.sampleHeight(player.position[0], player.position[2]) < tuning.ground.waterLevel;
|
|
389
|
+
const groundedSupport = state.motion?.grounded !== false && (state.motion?.jumpOffset ?? 0) <= 0 && !floating;
|
|
390
|
+
const supportStep = groundedSupport ? tuning.movement?.stepHeight ?? DEFAULT_PLAYER_STEP_HEIGHT : 0;
|
|
391
|
+
const bounds = gatherMovementObstacles(ctx, player.position, 0, 0, 0, supportStep, Math.max(collisionHeight, requestedCollisionHeight));
|
|
392
|
+
// An authored terrain-relative spawn can rest a few centimetres inside a walkable road plate.
|
|
393
|
+
// Test the same support feet the grounded motor will accept, including the resulting headroom.
|
|
394
|
+
const support = groundedSupport
|
|
395
|
+
? obstacleSupportHeight(player.position[0], player.position[2], player.position[1], supportStep, bounds) : null;
|
|
396
|
+
const supportedPosition = support !== null && support > player.position[1]
|
|
397
|
+
? [player.position[0], support, player.position[2]] : player.position;
|
|
398
|
+
const overlap = resolveObstacleVerticalStep(supportedPosition, 0, bounds, DEFAULT_OBSTACLE_PLAYER_RADIUS, requestedCollisionHeight).initialOverlap;
|
|
399
|
+
if (overlap && previousHeight === null) {
|
|
400
|
+
throw new RangeError(`PlayerMovementConfig.collisionHeight ${requestedCollisionHeight} intersects blocking geometry at entity ${playerId}. Move the authored placement or choose a height that fits.`);
|
|
401
|
+
}
|
|
402
|
+
collisionHeightBlocked = overlap;
|
|
403
|
+
}
|
|
404
|
+
if (!collisionHeightBlocked)
|
|
405
|
+
collisionHeight = requestedCollisionHeight;
|
|
406
|
+
}
|
|
407
|
+
return flightTuning;
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
if (retainedState === undefined && store.players.get(userId) === state)
|
|
412
|
+
store.players.delete(userId);
|
|
413
|
+
throw error;
|
|
414
|
+
}
|
|
172
415
|
// A frame carrying analog magnitudes (virtual joystick, gamepad stick) walks at its deflection
|
|
173
416
|
// instead of slamming digital ±1 axes — the fix for "a slight stick tilt reads as a full strafe".
|
|
174
417
|
const analog = input.analog ?? null;
|
|
@@ -179,9 +422,14 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitc
|
|
|
179
422
|
right: (analog.moveRight ?? 0) - (analog.moveLeft ?? 0),
|
|
180
423
|
};
|
|
181
424
|
const intent = resolveMovementIntent(keys, true, analogMove);
|
|
182
|
-
const motionBatch = ctx.player.motionFor(userId).takePending();
|
|
183
425
|
const walkSpeed = player.movement?.walkSpeed ?? DEFAULT_WALK_SPEED;
|
|
184
|
-
|
|
426
|
+
if (flightTuning === null && tuning.controller === undefined && tuning.collision?.voxel !== true) {
|
|
427
|
+
state.heightfieldHeight = collisionHeight;
|
|
428
|
+
state.heightfieldEntityId = playerId;
|
|
429
|
+
}
|
|
430
|
+
state.liveEntityId = playerId;
|
|
431
|
+
state.heading = nextHeading;
|
|
432
|
+
const motionBatch = ctx.player.motionFor(userId).takePending();
|
|
185
433
|
if (flightTuning !== null) {
|
|
186
434
|
const value = (action) => input.analog?.[action] ?? (isDown(action) ? 1 : 0);
|
|
187
435
|
let flightIntent = resolveFreeFlightIntentFromInput(isDown, value, input.pointer, flightTuning.bindings);
|
|
@@ -219,7 +467,7 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitc
|
|
|
219
467
|
const cam = ctx.camera;
|
|
220
468
|
cam?.setChaseTuning(normalizedFlightTuning.camera ?? null);
|
|
221
469
|
}
|
|
222
|
-
const step = advanceFreeFlight(flightState, flightIntent, state.heading, pitch, dt, normalizedFlightTuning);
|
|
470
|
+
const step = advanceFreeFlight(flightState, flightIntent, state.heading, pitch, dt, normalizedFlightTuning, { authoritativeStep: tuning.authoritativeStep });
|
|
223
471
|
let stepX = step.stepX;
|
|
224
472
|
let stepY = step.stepY;
|
|
225
473
|
let stepZ = step.stepZ;
|
|
@@ -238,10 +486,12 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitc
|
|
|
238
486
|
flightState.vy = 0;
|
|
239
487
|
}
|
|
240
488
|
}
|
|
489
|
+
let motorProposalAccepted = true;
|
|
241
490
|
let nextX = player.position[0] + stepX;
|
|
242
491
|
let nextY = player.position[1] + stepY;
|
|
243
492
|
let nextZ = player.position[2] + stepZ;
|
|
244
493
|
if (motionBatch !== null && motionBatch.y !== null) {
|
|
494
|
+
motorProposalAccepted = motionBatch.y === nextY;
|
|
245
495
|
nextY = motionBatch.y;
|
|
246
496
|
}
|
|
247
497
|
if (tuning.movement?.beforeCommit !== undefined) {
|
|
@@ -254,6 +504,7 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitc
|
|
|
254
504
|
};
|
|
255
505
|
const replacement = tuning.movement.beforeCommit(frame);
|
|
256
506
|
if (replacement !== undefined) {
|
|
507
|
+
motorProposalAccepted = motorProposalAccepted && replacement[0] === nextX && replacement[1] === nextY && replacement[2] === nextZ;
|
|
257
508
|
nextX = replacement[0];
|
|
258
509
|
nextY = replacement[1];
|
|
259
510
|
nextZ = replacement[2];
|
|
@@ -264,6 +515,10 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitc
|
|
|
264
515
|
rotationY: resolveBodyFacing(state, flightIntent.moving, flightState.vx, flightState.vz, player.rotationY, tuning.movement?.turnSpeed, dt),
|
|
265
516
|
dt,
|
|
266
517
|
});
|
|
518
|
+
if (motorProposalAccepted && positionAccepted(ctx, playerId, nextX, nextY, nextZ))
|
|
519
|
+
updateTelemetry(state, playerId, false, flightState.vy, false);
|
|
520
|
+
else
|
|
521
|
+
invalidateTelemetry(state);
|
|
267
522
|
return;
|
|
268
523
|
}
|
|
269
524
|
if (flightTuning === null && state.flight !== null) {
|
|
@@ -275,7 +530,7 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitc
|
|
|
275
530
|
if (controller === null) {
|
|
276
531
|
controller = createCharacterController(tuning.controller.capsule);
|
|
277
532
|
state.controller = controller;
|
|
278
|
-
controller.restore({
|
|
533
|
+
controller.restore(state.pendingController ?? {
|
|
279
534
|
position: [player.position[0], player.position[1], player.position[2]],
|
|
280
535
|
verticalVelocity: 0,
|
|
281
536
|
grounded: true,
|
|
@@ -283,7 +538,9 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitc
|
|
|
283
538
|
groundBody: null,
|
|
284
539
|
crouching: false,
|
|
285
540
|
});
|
|
541
|
+
state.pendingController = null;
|
|
286
542
|
}
|
|
543
|
+
retuneController(state, tuning.controller.capsule);
|
|
287
544
|
const controllerState = controller.state();
|
|
288
545
|
if (Math.hypot(controllerState.position[0] - player.position[0], controllerState.position[1] - player.position[1], controllerState.position[2] - player.position[2]) > 1e-6) {
|
|
289
546
|
controller.restore({
|
|
@@ -293,43 +550,53 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitc
|
|
|
293
550
|
groundBody: null,
|
|
294
551
|
});
|
|
295
552
|
}
|
|
296
|
-
// Keep the existing input feel and horizontal impulse seam; the capsule owns vertical motion and collision.
|
|
297
553
|
let horizontal = state.motion;
|
|
298
554
|
if (horizontal === null) {
|
|
299
555
|
horizontal = createPlayerMotionState();
|
|
300
556
|
state.motion = horizontal;
|
|
301
557
|
}
|
|
558
|
+
controller.setCrouch(tuning.controller.backend, intent.crouching);
|
|
302
559
|
const grounded = controller.state().grounded;
|
|
303
560
|
horizontal.grounded = grounded;
|
|
304
|
-
horizontal.verticalVelocity =
|
|
561
|
+
horizontal.verticalVelocity = applyMotionImpulses(controller.state().verticalVelocity, motionBatch);
|
|
305
562
|
horizontal.jumpOffset = 0;
|
|
306
563
|
[horizontal.horizontalVelocityX, horizontal.horizontalVelocityZ] = applyHorizontalImpulses(horizontal.horizontalVelocityX, horizontal.horizontalVelocityZ, motionBatch);
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
564
|
+
const controllerIntent = {
|
|
565
|
+
...intent,
|
|
566
|
+
crouching: controller.state().crouching,
|
|
567
|
+
running: intent.running && !controller.state().crouching,
|
|
568
|
+
};
|
|
569
|
+
if (state.jumpBuffer === null) {
|
|
570
|
+
horizontal.jumpHeld = state.controllerJumpHeld;
|
|
571
|
+
state.jumpBuffer = createInputBuffer({ windowMs: tuning.physics?.jumpBufferMs ?? 0 });
|
|
572
|
+
}
|
|
573
|
+
const horizontalStep = advancePlayerMotion(horizontal, controllerIntent, forwardX, forwardZ, walkSpeed, dt, tuning.physics, { buffer: state.jumpBuffer, externalGrounding: true, authoritativeStep: tuning.authoritativeStep });
|
|
312
574
|
state.controllerJumpHeld = intent.jumping;
|
|
313
|
-
const current = controller.state();
|
|
314
575
|
controller.restore({
|
|
315
|
-
...
|
|
316
|
-
verticalVelocity:
|
|
576
|
+
...controller.state(),
|
|
577
|
+
verticalVelocity: horizontal.verticalVelocity,
|
|
578
|
+
grounded: horizontal.grounded,
|
|
317
579
|
});
|
|
318
580
|
const result = controller.move(tuning.controller.backend, {
|
|
319
581
|
motion: [horizontalStep.stepX, 0, horizontalStep.stepZ],
|
|
320
|
-
dt,
|
|
321
|
-
gravity: tuning.physics?.gravityAcceleration ?? MOVEMENT_TUNING.gravityAcceleration,
|
|
322
|
-
...(jumpPressed && !intent.crouching
|
|
323
|
-
? { jumpVelocity: tuning.physics?.jumpVelocity ?? MOVEMENT_TUNING.jumpVelocity }
|
|
324
|
-
: {}),
|
|
325
|
-
crouch: intent.crouching,
|
|
582
|
+
dt: motionStepSeconds(dt, tuning.authoritativeStep),
|
|
326
583
|
});
|
|
584
|
+
horizontal.verticalVelocity = controller.state().verticalVelocity;
|
|
585
|
+
horizontal.jumpOffset = 0;
|
|
586
|
+
if (!grounded && result.grounded)
|
|
587
|
+
horizontal.landedAtMs = horizontal.clockMs;
|
|
588
|
+
horizontal.wasAirborne = !result.grounded;
|
|
589
|
+
if (result.grounded)
|
|
590
|
+
horizontal.jumpRising = false;
|
|
591
|
+
ctx.player.movement.setPose(playerId, controller.state().crouching ? "crouch" : intent.running ? "running" : "standing");
|
|
592
|
+
let motorProposalAccepted = true;
|
|
327
593
|
let nextPosition = [
|
|
328
594
|
controller.state().position[0],
|
|
329
595
|
controller.state().position[1],
|
|
330
596
|
controller.state().position[2],
|
|
331
597
|
];
|
|
332
598
|
if (motionBatch !== null && motionBatch.y !== null) {
|
|
599
|
+
motorProposalAccepted = motionBatch.y === nextPosition[1];
|
|
333
600
|
nextPosition[1] = motionBatch.y;
|
|
334
601
|
controller.restore({ ...controller.state(), position: nextPosition });
|
|
335
602
|
}
|
|
@@ -343,6 +610,7 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitc
|
|
|
343
610
|
};
|
|
344
611
|
const replacement = tuning.movement.beforeCommit(frame);
|
|
345
612
|
if (replacement !== undefined) {
|
|
613
|
+
motorProposalAccepted = motorProposalAccepted && replacement[0] === nextPosition[0] && replacement[1] === nextPosition[1] && replacement[2] === nextPosition[2];
|
|
346
614
|
nextPosition = [replacement[0], replacement[1], replacement[2]];
|
|
347
615
|
controller.restore({ ...controller.state(), position: nextPosition });
|
|
348
616
|
}
|
|
@@ -353,6 +621,10 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitc
|
|
|
353
621
|
rotationY: resolveBodyFacing(state, intent.moving, horizontal.horizontalVelocityX, horizontal.horizontalVelocityZ, player.rotationY, tuning.movement?.turnSpeed, dt),
|
|
354
622
|
dt,
|
|
355
623
|
});
|
|
624
|
+
if (motorProposalAccepted && positionAccepted(ctx, playerId, nextPosition[0], nextPosition[1], nextPosition[2]))
|
|
625
|
+
updateTelemetry(state, playerId, controller.state().grounded, controller.state().verticalVelocity, controller.state().crouching);
|
|
626
|
+
else
|
|
627
|
+
invalidateTelemetry(state);
|
|
356
628
|
return;
|
|
357
629
|
}
|
|
358
630
|
if (tuning.collision?.voxel === true) {
|
|
@@ -375,7 +647,8 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitc
|
|
|
375
647
|
};
|
|
376
648
|
body.velocityY = applyMotionImpulses(body.velocityY, motionBatch);
|
|
377
649
|
[body.velocityX, body.velocityZ] = applyHorizontalImpulses(body.velocityX, body.velocityZ, motionBatch);
|
|
378
|
-
advanceVoxelPlayer(body, intent, forwardX, forwardZ, walkSpeed, dt, isSolid, dims, tuning.physics, tuning.hasTerrain ? (x, z) => tuning.ground.sampleHeight(x, z) : undefined);
|
|
650
|
+
advanceVoxelPlayer(body, intent, forwardX, forwardZ, walkSpeed, dt, isSolid, dims, tuning.physics, tuning.hasTerrain ? (x, z) => tuning.ground.sampleHeight(x, z) : undefined, { authoritativeStep: tuning.authoritativeStep });
|
|
651
|
+
const motorProposalAccepted = motionBatch === null || motionBatch.y === null || motionBatch.y === body.y;
|
|
379
652
|
if (motionBatch !== null && motionBatch.y !== null)
|
|
380
653
|
body.y = motionBatch.y;
|
|
381
654
|
ctx.scene.entity.setPose(playerId, {
|
|
@@ -383,6 +656,10 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitc
|
|
|
383
656
|
rotationY: resolveBodyFacing(state, intent.moving, body.velocityX, body.velocityZ, player.rotationY, tuning.movement?.turnSpeed, dt),
|
|
384
657
|
dt,
|
|
385
658
|
});
|
|
659
|
+
if (motorProposalAccepted && positionAccepted(ctx, playerId, body.x, body.y, body.z))
|
|
660
|
+
updateTelemetry(state, playerId, body.grounded, body.velocityY, intent.crouching);
|
|
661
|
+
else
|
|
662
|
+
invalidateTelemetry(state);
|
|
386
663
|
return;
|
|
387
664
|
}
|
|
388
665
|
let motion = state.motion;
|
|
@@ -401,10 +678,17 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitc
|
|
|
401
678
|
const submerged = swimEnabled &&
|
|
402
679
|
waterLevel !== undefined &&
|
|
403
680
|
tuning.ground.sampleHeight(player.position[0], player.position[2]) < waterLevel;
|
|
681
|
+
const jumpBufferMs = tuning.physics?.jumpBufferMs ?? 0;
|
|
682
|
+
let jumpBuffer = state.jumpBuffer;
|
|
683
|
+
if (jumpBuffer === null) {
|
|
684
|
+
jumpBuffer = createInputBuffer({ windowMs: jumpBufferMs });
|
|
685
|
+
state.jumpBuffer = jumpBuffer;
|
|
686
|
+
}
|
|
404
687
|
const motionOptions = submerged
|
|
405
|
-
? { speedScale: swimSpeedMultiplier, floating: true }
|
|
406
|
-
:
|
|
688
|
+
? { speedScale: swimSpeedMultiplier, floating: true, buffer: jumpBuffer, authoritativeStep: tuning.authoritativeStep }
|
|
689
|
+
: { buffer: jumpBuffer, externalGrounding: true, authoritativeStep: tuning.authoritativeStep };
|
|
407
690
|
const prevJumpOffset = motion.jumpOffset;
|
|
691
|
+
const groundedBeforeStep = motion.grounded;
|
|
408
692
|
const step = advancePlayerMotion(motion, intent, forwardX, forwardZ, walkSpeed, dt, tuning.physics, motionOptions);
|
|
409
693
|
// Airborne means "not resting on the surface below": any jump/impulse height before or after this
|
|
410
694
|
// frame's integration. Grounded-only forgiveness (step-up) and airborne-only landing key off it.
|
|
@@ -417,11 +701,14 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitc
|
|
|
417
701
|
stepZ = constrained.stepZ;
|
|
418
702
|
}
|
|
419
703
|
const stepHeight = tuning.movement?.stepHeight ?? DEFAULT_PLAYER_STEP_HEIGHT;
|
|
704
|
+
const verticalStep = airborne ? motion.jumpOffset - prevJumpOffset : 0;
|
|
705
|
+
const queryMinStepY = Math.min(0, verticalStep);
|
|
706
|
+
const queryMaxStepY = Math.max(stepHeight, verticalStep);
|
|
420
707
|
let obstacles = null;
|
|
421
708
|
if (tuning.movement?.collideObjects !== false) {
|
|
422
|
-
obstacles = gatherMovementObstacles(ctx, player.position, stepX, stepZ);
|
|
709
|
+
obstacles = gatherMovementObstacles(ctx, player.position, stepX, stepZ, queryMinStepY, queryMaxStepY, collisionHeight);
|
|
423
710
|
// While grounded, a box the player could simply step onto is a ledge, not a wall.
|
|
424
|
-
const resolved = resolveObstacleStep(player.position, stepX, stepZ, obstacles, DEFAULT_OBSTACLE_PLAYER_RADIUS, airborne ? 0 : stepHeight);
|
|
711
|
+
const resolved = resolveObstacleStep(player.position, stepX, stepZ, obstacles, DEFAULT_OBSTACLE_PLAYER_RADIUS, airborne ? 0 : stepHeight, collisionHeight);
|
|
425
712
|
stepX = resolved.stepX;
|
|
426
713
|
stepZ = resolved.stepZ;
|
|
427
714
|
}
|
|
@@ -444,46 +731,155 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitc
|
|
|
444
731
|
nextZ += slope.downhill[1] * slide;
|
|
445
732
|
}
|
|
446
733
|
}
|
|
447
|
-
const
|
|
734
|
+
const maxClimbGrade = tuning.movement?.maxClimbGrade;
|
|
735
|
+
// The full/X/Z candidates share four unique grade samples (origin and three endpoints), even
|
|
736
|
+
// when headroom rejects the grade resolver's first choice. Preserve the game sampler's budget.
|
|
737
|
+
const gradeSamples = maxClimbGrade === undefined ? null : [];
|
|
738
|
+
const gradeSampler = (x, z) => {
|
|
739
|
+
for (const sample of gradeSamples)
|
|
740
|
+
if (sample.x === x && sample.z === z)
|
|
741
|
+
return sample.height;
|
|
742
|
+
const authoredSampler = tuning.movement?.climbGradeHeight;
|
|
743
|
+
const height = authoredSampler === undefined ? tuning.ground.sampleHeight(x, z) : authoredSampler(x, z);
|
|
744
|
+
gradeSamples.push({ x, z, height });
|
|
745
|
+
return height;
|
|
746
|
+
};
|
|
747
|
+
if (maxClimbGrade !== undefined) {
|
|
748
|
+
const accepted = resolveTerrainGradeStep(gradeSampler, player.position, nextX - player.position[0], nextZ - player.position[2], maxClimbGrade);
|
|
749
|
+
nextX = player.position[0] + accepted.stepX;
|
|
750
|
+
nextZ = player.position[2] + accepted.stepZ;
|
|
751
|
+
}
|
|
448
752
|
// Blocking colliders are walkable surfaces: the effective ground under the player is the higher of
|
|
449
753
|
// the terrain and the tallest object top the player can stand on here. While grounded a top within
|
|
450
754
|
// stepHeight above the feet is stepped onto (matching the obstruction's ledge forgiveness); while
|
|
451
755
|
// airborne only tops at/below the feet catch, so a jump lands ON a crate instead of sinking inside
|
|
452
756
|
// it and being rubber-banded out by depenetration.
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
else {
|
|
470
|
-
nextY = Math.max(nextFeet, effectiveGround);
|
|
471
|
-
motion.jumpOffset = nextY - effectiveGround;
|
|
472
|
-
motion.grounded = false;
|
|
757
|
+
const absoluteHeightOverride = motionBatch?.y !== undefined && motionBatch.y !== null;
|
|
758
|
+
const terrainSamples = [];
|
|
759
|
+
const terrainHeightAt = (x, z) => {
|
|
760
|
+
for (const sample of terrainSamples)
|
|
761
|
+
if (sample.x === x && sample.z === z)
|
|
762
|
+
return sample.height;
|
|
763
|
+
const height = tuning.ground.sampleHeight(x, z);
|
|
764
|
+
terrainSamples.push({ x, z, height });
|
|
765
|
+
return height;
|
|
766
|
+
};
|
|
767
|
+
if (obstacles !== null && !absoluteHeightOverride) {
|
|
768
|
+
// A terrain-following style may accept a rise taller than stepHeight. Include those proposed
|
|
769
|
+
// full/X/Z body spans without querying the whole world or falling all the way to distant ground.
|
|
770
|
+
const rise = Math.max(terrainHeightAt(nextX, nextZ), terrainHeightAt(nextX, player.position[2]), terrainHeightAt(player.position[0], nextZ)) - player.position[1];
|
|
771
|
+
if (rise > queryMaxStepY) {
|
|
772
|
+
obstacles = gatherMovementObstacles(ctx, player.position, nextX - player.position[0], nextZ - player.position[2], queryMinStepY, rise, collisionHeight);
|
|
473
773
|
}
|
|
474
774
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
775
|
+
const validStartingBounds = obstacles === null || !resolveObstacleVerticalStep(player.position, 0, obstacles, DEFAULT_OBSTACLE_PLAYER_RADIUS, collisionHeight).initialOverlap;
|
|
776
|
+
const resolveCandidate = (x, z) => {
|
|
777
|
+
const terrainHeight = terrainHeightAt(x, z);
|
|
778
|
+
const supportY = obstacles === null ? null : obstacleSupportHeight(x, z, player.position[1], airborne ? 0 : stepHeight, obstacles);
|
|
779
|
+
const ground = supportY !== null && supportY > terrainHeight ? supportY : terrainHeight;
|
|
780
|
+
let y = ground;
|
|
781
|
+
let grounded = motion.grounded;
|
|
782
|
+
let velocity = motion.verticalVelocity;
|
|
783
|
+
let offset = motion.jumpOffset;
|
|
784
|
+
let rising = motion.jumpRising;
|
|
785
|
+
let blockedRise = false;
|
|
786
|
+
if (airborne) {
|
|
787
|
+
// Keep the arc in absolute space while the terrain/support changes below it.
|
|
788
|
+
const nextFeet = player.position[1] + (motion.jumpOffset - prevJumpOffset);
|
|
789
|
+
if (nextFeet <= ground && velocity <= 0) {
|
|
790
|
+
offset = 0;
|
|
791
|
+
velocity = 0;
|
|
792
|
+
grounded = true;
|
|
793
|
+
}
|
|
794
|
+
else {
|
|
795
|
+
y = Math.max(nextFeet, ground);
|
|
796
|
+
offset = y - ground;
|
|
797
|
+
grounded = false;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
else if (!submerged && player.position[1] - ground > stepHeight) {
|
|
801
|
+
y = player.position[1];
|
|
802
|
+
offset = y - ground;
|
|
803
|
+
velocity = 0;
|
|
804
|
+
grounded = false;
|
|
805
|
+
}
|
|
806
|
+
if (obstacles !== null && !absoluteHeightOverride) {
|
|
807
|
+
const vertical = resolveObstacleVerticalStep([x, player.position[1], z], y - player.position[1], obstacles, DEFAULT_OBSTACLE_PLAYER_RADIUS, collisionHeight);
|
|
808
|
+
if (vertical.hitCeiling) {
|
|
809
|
+
const clippedY = player.position[1] + vertical.stepY;
|
|
810
|
+
// A surface above the ceiling cannot be reached by snapping entirely through a thin roof.
|
|
811
|
+
blockedRise = ground > vertical.ceilingFeetY + vertical.ceilingTolerance;
|
|
812
|
+
y = Math.max(clippedY, ground);
|
|
813
|
+
offset = Math.max(0, y - ground);
|
|
814
|
+
velocity = 0;
|
|
815
|
+
rising = false;
|
|
816
|
+
grounded = y === ground;
|
|
817
|
+
}
|
|
818
|
+
else if (vertical.landed) {
|
|
819
|
+
y = player.position[1] + vertical.stepY;
|
|
820
|
+
offset = 0;
|
|
821
|
+
velocity = 0;
|
|
822
|
+
rising = false;
|
|
823
|
+
grounded = true;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
const clear = obstacles === null || absoluteHeightOverride || !validStartingBounds ||
|
|
827
|
+
(!blockedRise && !resolveObstacleVerticalStep([x, y, z], 0, obstacles, DEFAULT_OBSTACLE_PLAYER_RADIUS, collisionHeight).initialOverlap);
|
|
828
|
+
return { x, z, y, ground, grounded, velocity, offset, rising, clear };
|
|
829
|
+
};
|
|
830
|
+
let candidate = resolveCandidate(nextX, nextZ);
|
|
831
|
+
if (!candidate.clear) {
|
|
832
|
+
const gradeAllows = (x, z) => {
|
|
833
|
+
if (maxClimbGrade === undefined)
|
|
834
|
+
return true;
|
|
835
|
+
const accepted = resolveTerrainGradeStep(gradeSampler, player.position, x - player.position[0], z - player.position[2], maxClimbGrade);
|
|
836
|
+
return accepted.stepX === x - player.position[0] && accepted.stepZ === z - player.position[2];
|
|
837
|
+
};
|
|
838
|
+
const tryAxis = (x, z) => {
|
|
839
|
+
if (obstacles !== null) {
|
|
840
|
+
// The original Z sweep used post-X coordinates. An alternate axis must be swept again from
|
|
841
|
+
// the original pose; reject a changed endpoint rather than accepting an unsampled grade.
|
|
842
|
+
const swept = resolveObstacleStep(player.position, x - player.position[0], z - player.position[2], obstacles, DEFAULT_OBSTACLE_PLAYER_RADIUS, airborne ? 0 : stepHeight, collisionHeight);
|
|
843
|
+
if (swept.stepX !== x - player.position[0] || swept.stepZ !== z - player.position[2])
|
|
844
|
+
return false;
|
|
845
|
+
}
|
|
846
|
+
if (!gradeAllows(x, z))
|
|
847
|
+
return false;
|
|
848
|
+
const alternative = resolveCandidate(x, z);
|
|
849
|
+
if (!alternative.clear)
|
|
850
|
+
return false;
|
|
851
|
+
candidate = alternative;
|
|
852
|
+
return true;
|
|
853
|
+
};
|
|
854
|
+
const xAccepted = nextX !== player.position[0] && tryAxis(nextX, player.position[2]);
|
|
855
|
+
const zAccepted = !xAccepted && nextZ !== player.position[2] && tryAxis(player.position[0], nextZ);
|
|
856
|
+
if (!xAccepted && !zAccepted) {
|
|
857
|
+
candidate = resolveCandidate(player.position[0], player.position[2]);
|
|
858
|
+
if (!candidate.clear) {
|
|
859
|
+
// An inflated support edge cannot raise the body into a lintel. Retain the valid starting
|
|
860
|
+
// pose rather than forcing feet into support or repairing an illegal authored placement.
|
|
861
|
+
candidate.y = player.position[1];
|
|
862
|
+
candidate.grounded = groundedBeforeStep;
|
|
863
|
+
candidate.offset = prevJumpOffset;
|
|
864
|
+
candidate.velocity = groundedBeforeStep ? 0 : motion.verticalVelocity;
|
|
865
|
+
candidate.rising = groundedBeforeStep ? false : motion.jumpRising;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
485
868
|
}
|
|
869
|
+
nextX = candidate.x;
|
|
870
|
+
nextZ = candidate.z;
|
|
871
|
+
let nextY = candidate.y;
|
|
872
|
+
const effectiveGround = candidate.ground;
|
|
873
|
+
motion.grounded = candidate.grounded;
|
|
874
|
+
motion.verticalVelocity = candidate.velocity;
|
|
875
|
+
motion.jumpOffset = candidate.offset;
|
|
876
|
+
motion.jumpRising = candidate.rising;
|
|
877
|
+
let motorProposalAccepted = true;
|
|
878
|
+
if (!groundedBeforeStep && motion.grounded)
|
|
879
|
+
motion.landedAtMs = motion.clockMs;
|
|
880
|
+
motion.wasAirborne = !motion.grounded;
|
|
486
881
|
if (motionBatch !== null && motionBatch.y !== null) {
|
|
882
|
+
motorProposalAccepted = motionBatch.y === nextY;
|
|
487
883
|
nextY = motionBatch.y;
|
|
488
884
|
motion.jumpOffset = motionBatch.y - effectiveGround;
|
|
489
885
|
}
|
|
@@ -500,6 +896,7 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitc
|
|
|
500
896
|
};
|
|
501
897
|
const replacement = tuning.movement.beforeCommit(frame);
|
|
502
898
|
if (replacement !== undefined) {
|
|
899
|
+
motorProposalAccepted = motorProposalAccepted && replacement[0] === nextX && replacement[1] === nextY && replacement[2] === nextZ;
|
|
503
900
|
nextX = replacement[0];
|
|
504
901
|
nextY = replacement[1];
|
|
505
902
|
nextZ = replacement[2];
|
|
@@ -510,11 +907,20 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitc
|
|
|
510
907
|
rotationY: resolveBodyFacing(state, intent.moving, motion.horizontalVelocityX, motion.horizontalVelocityZ, player.rotationY, tuning.movement?.turnSpeed, dt),
|
|
511
908
|
dt,
|
|
512
909
|
});
|
|
910
|
+
if (motorProposalAccepted && positionAccepted(ctx, playerId, nextX, nextY, nextZ))
|
|
911
|
+
updateTelemetry(state, playerId, motion.grounded, motion.verticalVelocity, intent.crouching, collisionHeight, requestedCollisionHeight, collisionHeightBlocked);
|
|
912
|
+
else
|
|
913
|
+
invalidateTelemetry(state);
|
|
513
914
|
}
|
|
514
915
|
/**
|
|
515
916
|
* The player's blocking obstacles for this step, from the shared solid seam. A walking NPC calls
|
|
516
917
|
* `resolveWalkerStep` against the same query, so "solid" means one thing engine-wide.
|
|
517
918
|
*/
|
|
518
|
-
function gatherMovementObstacles(ctx, position, stepX, stepZ) {
|
|
519
|
-
|
|
919
|
+
function gatherMovementObstacles(ctx, position, stepX, stepZ, minStepY = 0, maxStepY = 0, playerHeight = DEFAULT_OBSTACLE_PLAYER_HEIGHT) {
|
|
920
|
+
if (!Number.isFinite(minStepY) || !Number.isFinite(maxStepY))
|
|
921
|
+
throw new RangeError("Movement obstacle query requires a finite vertical step.");
|
|
922
|
+
const minY = Math.min(0, minStepY);
|
|
923
|
+
const maxY = Math.max(0, maxStepY);
|
|
924
|
+
const queryPosition = minY < 0 ? [position[0], position[1] + minY, position[2]] : position;
|
|
925
|
+
return solidObstaclesNear(ctx, queryPosition, Math.abs(stepX) + DEFAULT_OBSTACLE_PLAYER_RADIUS, Math.abs(stepZ) + DEFAULT_OBSTACLE_PLAYER_RADIUS, playerHeight + maxY - minY);
|
|
520
926
|
}
|