@jgengine/core 0.18.0 → 0.19.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 +265 -2
- package/dist/ai/decisionGraph.d.ts +96 -0
- package/dist/ai/decisionGraph.js +212 -0
- package/dist/ai/groupAssist.d.ts +10 -0
- package/dist/ai/groupAssist.js +11 -0
- package/dist/ai/mobBrainGraph.d.ts +3 -0
- package/dist/ai/mobBrainGraph.js +25 -0
- package/dist/ai/pursuit.d.ts +1 -1
- package/dist/ai/pursuit.js +1 -1
- package/dist/ai/tacticalQueries.d.ts +30 -0
- package/dist/ai/tacticalQueries.js +45 -0
- package/dist/ai/threat.d.ts +8 -0
- package/dist/ai/threat.js +10 -0
- package/dist/anim/animGraph.d.ts +155 -0
- package/dist/anim/animGraph.js +414 -0
- 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/ikSolver.d.ts +51 -0
- package/dist/anim/ikSolver.js +124 -0
- package/dist/anim/locomotionGraph.d.ts +35 -0
- package/dist/anim/locomotionGraph.js +62 -0
- package/dist/assets/spriteAtlas.d.ts +21 -0
- package/dist/assets/spriteAtlas.js +103 -0
- package/dist/audio/audioFalloff.d.ts +12 -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/combat/abilityKit.d.ts +1 -0
- package/dist/combat/abilityKit.js +1 -0
- package/dist/combat/death.d.ts +7 -0
- 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/devtools/warnOnce.d.ts +8 -0
- package/dist/devtools/warnOnce.js +18 -0
- package/dist/economy/currency.d.ts +16 -2
- package/dist/economy/currency.js +55 -14
- package/dist/economy/wallet.d.ts +5 -4
- package/dist/economy/wallet.js +8 -4
- package/dist/editor/commandApply.js +1 -0
- package/dist/editor/commands.d.ts +4 -1
- package/dist/editor/document.d.ts +2 -1
- package/dist/editor/document.js +92 -1
- package/dist/editor/environment.d.ts +3 -0
- package/dist/editor/environment.js +24 -0
- package/dist/editor/index.d.ts +4 -2
- package/dist/editor/index.js +1 -1
- package/dist/editor/types.d.ts +20 -0
- package/dist/faction/reputation.d.ts +6 -0
- package/dist/faction/reputation.js +11 -0
- package/dist/game/cameraConfig.d.ts +59 -1
- package/dist/game/chat.d.ts +22 -1
- package/dist/game/chat.js +48 -19
- package/dist/game/controlGate.d.ts +6 -0
- package/dist/game/controlGate.js +30 -1
- package/dist/game/defineGame.d.ts +10 -1
- package/dist/game/events.d.ts +14 -1
- package/dist/game/lootTable.d.ts +4 -0
- package/dist/game/lootTable.js +16 -0
- package/dist/game/pixelPerfect.d.ts +25 -0
- package/dist/game/pixelPerfect.js +29 -0
- package/dist/game/playableGame.d.ts +167 -6
- package/dist/game/systemRuntime.js +6 -1
- 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/input/actionBindings.d.ts +17 -2
- package/dist/input/actionBindings.js +24 -11
- package/dist/input/actionContexts.d.ts +45 -0
- package/dist/input/actionContexts.js +113 -0
- 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 +79 -0
- package/dist/input/gamepadModel.js +108 -0
- 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/item/affix.d.ts +6 -0
- package/dist/item/affix.js +6 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +196 -1
- package/dist/movement/characterController.d.ts +76 -0
- package/dist/movement/characterController.js +216 -0
- package/dist/movement/freeFlight.d.ts +119 -0
- package/dist/movement/freeFlight.js +294 -0
- 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 +39 -0
- package/dist/movement/movementModel.js +62 -19
- package/dist/movement/movementProbe.d.ts +51 -0
- package/dist/movement/movementProbe.js +119 -0
- package/dist/movement/playerMovement.d.ts +30 -3
- package/dist/movement/playerMovement.js +304 -7
- package/dist/movement/serverStep.d.ts +5 -0
- package/dist/movement/serverStep.js +6 -0
- package/dist/movement/solidObstacles.d.ts +39 -8
- package/dist/movement/solidObstacles.js +71 -6
- package/dist/movement/voxelController.js +7 -7
- package/dist/multiplayer/poseSyncGate.d.ts +3 -1
- package/dist/multiplayer/poseSyncGate.js +9 -1
- package/dist/multiplayer/presenceContract.d.ts +6 -0
- package/dist/multiplayer/presenceModel.d.ts +2 -0
- package/dist/multiplayer/presenceModel.js +11 -0
- package/dist/nav/navFromEnvironment.d.ts +9 -3
- package/dist/nav/navFromEnvironment.js +18 -12
- package/dist/nav/navMesh.d.ts +74 -0
- package/dist/nav/navMesh.js +570 -0
- package/dist/physics/aircraftDynamics.d.ts +390 -0
- package/dist/physics/aircraftDynamics.js +704 -0
- 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 +29 -4
- package/dist/physics/flightDynamics.js +28 -5
- package/dist/physics/handlingProbe.d.ts +249 -0
- package/dist/physics/handlingProbe.js +606 -0
- package/dist/physics/kinematicVehicle.d.ts +37 -2
- package/dist/physics/kinematicVehicle.js +15 -0
- package/dist/physics/physicsBackend.d.ts +203 -0
- package/dist/physics/physicsBackend.js +49 -0
- package/dist/physics/physicsBackendConformance.d.ts +27 -0
- package/dist/physics/physicsBackendConformance.js +151 -0
- package/dist/physics/physicsWorldBackend.d.ts +45 -0
- package/dist/physics/physicsWorldBackend.js +518 -0
- package/dist/physics/vehicleBackendLink.d.ts +49 -0
- package/dist/physics/vehicleBackendLink.js +102 -0
- package/dist/physics/vehicleDynamics.d.ts +374 -0
- package/dist/physics/vehicleDynamics.js +737 -0
- package/dist/physics/worldColliders.d.ts +13 -0
- package/dist/physics/worldColliders.js +128 -0
- package/dist/puzzle/cellGrid.d.ts +15 -9
- package/dist/puzzle/cellGrid.js +13 -9
- package/dist/random/rng.d.ts +12 -1
- package/dist/random/rng.js +20 -3
- package/dist/render/environment.d.ts +23 -0
- package/dist/render/environment.js +1 -0
- package/dist/render/lookPreset.d.ts +19 -6
- package/dist/render/lookPreset.js +56 -4
- package/dist/render/postProcessing.d.ts +40 -1
- package/dist/render/postProcessing.js +1 -1
- package/dist/render/sprite2d.d.ts +38 -0
- package/dist/render/sprite2d.js +40 -0
- package/dist/runtime/adapter.d.ts +20 -1
- package/dist/runtime/adapter.js +8 -1
- package/dist/runtime/cameraDirector.d.ts +17 -3
- package/dist/runtime/cameraDirector.js +25 -0
- package/dist/runtime/commandInput.d.ts +52 -0
- package/dist/runtime/commandInput.js +71 -0
- package/dist/runtime/commandRunner.d.ts +39 -7
- package/dist/runtime/commandRunner.js +35 -5
- package/dist/runtime/context/combat.d.ts +1 -0
- package/dist/runtime/context/combat.js +2 -1
- package/dist/runtime/context/deathLoot.d.ts +2 -0
- package/dist/runtime/context/deathLoot.js +1 -0
- package/dist/runtime/gameContext.js +18 -1
- package/dist/runtime/gameContextTypes.d.ts +33 -7
- package/dist/runtime/gameRuntime.d.ts +23 -5
- package/dist/runtime/gameRuntime.js +21 -4
- package/dist/runtime/headlessRunner.d.ts +10 -0
- package/dist/runtime/headlessRunner.js +26 -8
- package/dist/runtime/hostPolicy.d.ts +3 -2
- package/dist/runtime/hostPolicy.js +8 -10
- package/dist/runtime/hostedGameRunner.js +34 -6
- package/dist/runtime/hostedWorldSession.d.ts +12 -16
- package/dist/runtime/hostedWorldSession.js +29 -6
- package/dist/runtime/hostedWorldStore.d.ts +20 -0
- package/dist/runtime/hostedWorldStore.js +1 -0
- package/dist/runtime/inputRecorder.d.ts +30 -0
- package/dist/runtime/inputRecorder.js +59 -0
- package/dist/runtime/inputSnapshot.d.ts +14 -0
- package/dist/runtime/inputSnapshot.js +11 -0
- package/dist/runtime/localPlayers.d.ts +73 -0
- package/dist/runtime/localPlayers.js +119 -0
- package/dist/runtime/poseInterpolation.d.ts +36 -0
- package/dist/runtime/poseInterpolation.js +91 -0
- package/dist/runtime/prediction.d.ts +45 -0
- package/dist/runtime/prediction.js +62 -0
- package/dist/runtime/simContext.d.ts +45 -0
- package/dist/runtime/simContext.js +65 -0
- package/dist/runtime/simLoop.d.ts +52 -0
- package/dist/runtime/simLoop.js +72 -0
- package/dist/runtime/simSnapshot.d.ts +32 -0
- package/dist/runtime/simSnapshot.js +60 -0
- package/dist/runtime/snapshot.d.ts +8 -0
- package/dist/runtime/snapshotBuffer.d.ts +33 -0
- package/dist/runtime/snapshotBuffer.js +58 -0
- package/dist/runtime/transport.d.ts +41 -2
- package/dist/runtime/transport.js +7 -1
- package/dist/runtime/worldChunks.d.ts +2 -0
- package/dist/runtime/worldChunks.js +14 -0
- package/dist/runtime/worldProjection.d.ts +9 -0
- package/dist/runtime/worldProjection.js +17 -1
- package/dist/runtime/worldSnapshot.d.ts +6 -0
- package/dist/runtime/worldSnapshot.js +3 -0
- package/dist/scene/behaviorRuntime.d.ts +29 -1
- package/dist/scene/behaviorRuntime.js +71 -4
- package/dist/scene/behaviors.d.ts +12 -1
- package/dist/scene/builtinSceneKinds.js +10 -0
- package/dist/scene/sceneKinds.d.ts +9 -0
- package/dist/scene/vehicleSeat.d.ts +5 -1
- package/dist/scene/vehicleSeat.js +5 -1
- 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 +70 -0
- package/dist/sensor/perception.js +189 -0
- 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 +21 -0
- package/dist/settings/graphicsProfile.js +48 -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/eventMeter.d.ts +1 -1
- package/dist/stats/eventMeter.js +1 -1
- package/dist/time/accrueSince.d.ts +10 -0
- package/dist/time/accrueSince.js +9 -0
- package/dist/time/rateWindow.d.ts +16 -0
- package/dist/time/rateWindow.js +14 -0
- package/dist/time/serverTick.d.ts +21 -3
- package/dist/time/serverTick.js +14 -13
- package/dist/vfx/feedbackMixer.d.ts +61 -0
- package/dist/vfx/feedbackMixer.js +106 -0
- package/dist/vfx/particleDirector.d.ts +68 -0
- package/dist/vfx/particleDirector.js +81 -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/world/authoredSolids.d.ts +46 -0
- package/dist/world/authoredSolids.js +75 -0
- package/dist/world/buildingGenerator.js +2 -2
- package/dist/world/buildingKit.d.ts +3 -1
- package/dist/world/buildings.d.ts +46 -2
- package/dist/world/buildings.js +53 -3
- package/dist/world/cityContent.js +103 -25
- package/dist/world/cityKind.d.ts +139 -0
- package/dist/world/cityKind.js +367 -16
- package/dist/world/features.d.ts +56 -3
- package/dist/world/features.js +13 -2
- package/dist/world/footprintGrid.d.ts +7 -0
- package/dist/world/footprintGrid.js +16 -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/placement.d.ts +7 -0
- package/dist/world/placement.js +2 -0
- package/dist/world/placementController.d.ts +1 -1
- package/dist/world/placementController.js +7 -0
- package/dist/world/raceCircuit.d.ts +168 -0
- package/dist/world/raceCircuit.js +797 -0
- package/dist/world/terraform.d.ts +8 -0
- package/dist/world/terraform.js +7 -0
- package/dist/world/terrain.d.ts +5 -1
- package/dist/world/terrain.js +5 -0
- package/dist/world/territory.d.ts +97 -0
- package/dist/world/territory.js +182 -0
- package/dist/world/walls.d.ts +7 -0
- package/dist/world/walls.js +7 -0
- package/dist/world/worldSolids.d.ts +84 -0
- package/dist/world/worldSolids.js +240 -0
- package/dist/world.d.ts +5 -3
- package/dist/world.js +4 -2
- package/package.json +1 -1
- package/skills/ce-handoff/SKILL.md +2 -2
- package/skills/game-design/SKILL.md +11 -1
- package/skills/jgengine/SKILL.md +9 -5
- package/skills/jgengine/api.md +205 -101
- package/skills/jgengine/capabilities.md +56 -0
- package/skills/jgengine/recipes/minimal-game.md +3 -0
- package/skills/jgengine-combat/SKILL.md +5 -0
- package/skills/jgengine-combat/api.md +138 -103
- package/skills/jgengine-combat/capabilities.md +29 -1
- package/skills/jgengine-combat/recipes/weapon-feel.md +50 -0
- package/skills/jgengine-gameplay/SKILL.md +10 -0
- package/skills/jgengine-gameplay/api.md +501 -453
- package/skills/jgengine-gameplay/capabilities.md +36 -0
- package/skills/jgengine-gameplay/reference-systems.md +4 -0
- package/skills/jgengine-world/SKILL.md +21 -3
- package/skills/jgengine-world/api.md +1172 -625
- package/skills/jgengine-world/capabilities.md +229 -3
- package/skills/jgengine-world/recipes/character-animation.md +39 -0
- package/skills/jgengine-world/recipes/controls.md +44 -0
- package/skills/jgengine-world/recipes/flight-feel.md +76 -0
- package/skills/jgengine-world/recipes/npc-senses.md +43 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +85 -0
- package/skills/jgengine-world/reference.md +31 -2
- package/skills/jgengine-world/visibility.md +112 -0
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { createInputBuffer } from "../input/inputBuffer.js";
|
|
1
2
|
import { applyHorizontalImpulses, applyMotionImpulses } from "../runtime/motionIntents.js";
|
|
3
|
+
import { createCharacterController, } from "./characterController.js";
|
|
2
4
|
import { groundFieldFor, hasEnvironmentTerrain, sampleSlope } from "../world/terrain.js";
|
|
3
|
-
import { advancePlayerMotion, constrainStepToAxis, createEmptyMovementKeys, createPlayerMotionState, DEFAULT_OBSTACLE_PLAYER_RADIUS, obstacleSupportHeight, resolveMovementIntent, resolveObstacleStep, snapPositionToGrid, } from "./movementModel.js";
|
|
5
|
+
import { advancePlayerMotion, constrainStepToAxis, createEmptyMovementKeys, createPlayerMotionState, DEFAULT_OBSTACLE_PLAYER_RADIUS, obstacleSupportHeight, resolveMovementIntent, resolveObstacleStep, snapPositionToGrid, MOVEMENT_TUNING, } from "./movementModel.js";
|
|
6
|
+
import { advanceFreeFlight, createFreeFlightState, resolveFlightStep, resolveFreeFlightIntentFromInput, } from "./freeFlight.js";
|
|
4
7
|
import { solidObstaclesNear } from "./solidObstacles.js";
|
|
5
8
|
import { approachYaw, steerYaw } from "./steering.js";
|
|
6
9
|
import { advanceVoxelPlayer, createVoxelPlayerBody, } from "./voxelController.js";
|
|
@@ -34,11 +37,40 @@ export function resolvePhysicsTuning(physics) {
|
|
|
34
37
|
export function resolvePlayerMovementTuning(opts) {
|
|
35
38
|
const physics = resolvePhysicsTuning(opts.physics);
|
|
36
39
|
const backpedal = opts.movement?.backpedalMult;
|
|
37
|
-
const
|
|
40
|
+
const feel = opts.movement?.feel;
|
|
41
|
+
const feelOverrides = {
|
|
42
|
+
...(backpedal === undefined ? {} : { backpedalSpeedMultiplier: backpedal }),
|
|
43
|
+
...(feel?.groundAcceleration === undefined ? {} : { groundAcceleration: feel.groundAcceleration }),
|
|
44
|
+
...(feel?.airAcceleration === undefined ? {} : { airAcceleration: feel.airAcceleration }),
|
|
45
|
+
...(feel?.groundFriction === undefined ? {} : { groundFriction: feel.groundFriction }),
|
|
46
|
+
...(feel?.runMultiplier === undefined ? {} : { runSpeedMultiplier: feel.runMultiplier }),
|
|
47
|
+
...(feel?.crouchMultiplier === undefined ? {} : { crouchSpeedMultiplier: feel.crouchMultiplier }),
|
|
48
|
+
...(feel?.jumpBufferMs === undefined ? {} : { jumpBufferMs: feel.jumpBufferMs }),
|
|
49
|
+
...(feel?.coyoteMs === undefined ? {} : { coyoteMs: feel.coyoteMs }),
|
|
50
|
+
...(feel?.jumpCutFactor === undefined ? {} : { jumpCutFactor: feel.jumpCutFactor }),
|
|
51
|
+
...(feel?.apexGravityScale === undefined ? {} : { apexGravityScale: feel.apexGravityScale }),
|
|
52
|
+
...(feel?.apexSpeed === undefined ? {} : { apexSpeed: feel.apexSpeed }),
|
|
53
|
+
...(feel?.fallGravityScale === undefined ? {} : { fallGravityScale: feel.fallGravityScale }),
|
|
54
|
+
...(feel?.landingRecoveryMs === undefined ? {} : { landingRecoveryMs: feel.landingRecoveryMs }),
|
|
55
|
+
...(feel?.landingSpeedScale === undefined ? {} : { landingSpeedScale: feel.landingSpeedScale }),
|
|
56
|
+
};
|
|
57
|
+
const overrides = Object.keys(feelOverrides).length === 0 ? physics : { ...(physics ?? {}), ...feelOverrides };
|
|
38
58
|
return {
|
|
39
59
|
...(opts.collision === undefined ? {} : { collision: opts.collision }),
|
|
40
60
|
...(opts.movement === undefined ? {} : { movement: opts.movement }),
|
|
41
61
|
...(overrides === undefined ? {} : { physics: overrides }),
|
|
62
|
+
...(opts.physics?.backend === undefined
|
|
63
|
+
? {}
|
|
64
|
+
: {
|
|
65
|
+
controller: {
|
|
66
|
+
backend: opts.physics.backend,
|
|
67
|
+
capsule: opts.physics.controller ?? {
|
|
68
|
+
radius: DEFAULT_OBSTACLE_PLAYER_RADIUS,
|
|
69
|
+
height: 1.8,
|
|
70
|
+
stepHeight: DEFAULT_PLAYER_STEP_HEIGHT,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
}),
|
|
42
74
|
ground: groundFieldFor(opts.world),
|
|
43
75
|
hasTerrain: hasEnvironmentTerrain(opts.world),
|
|
44
76
|
};
|
|
@@ -58,7 +90,17 @@ function storeFor(ctx) {
|
|
|
58
90
|
function stateFor(store, userId) {
|
|
59
91
|
let state = store.players.get(userId);
|
|
60
92
|
if (state === undefined) {
|
|
61
|
-
state = {
|
|
93
|
+
state = {
|
|
94
|
+
heading: 0,
|
|
95
|
+
facing: null,
|
|
96
|
+
voxelBody: null,
|
|
97
|
+
motion: null,
|
|
98
|
+
flight: null,
|
|
99
|
+
controller: null,
|
|
100
|
+
controllerJumpHeld: false,
|
|
101
|
+
jumpBuffer: null,
|
|
102
|
+
pendingController: null,
|
|
103
|
+
};
|
|
62
104
|
store.players.set(userId, state);
|
|
63
105
|
}
|
|
64
106
|
return state;
|
|
@@ -67,6 +109,54 @@ function stateFor(store, userId) {
|
|
|
67
109
|
export function playerMovementHeading(ctx, userId) {
|
|
68
110
|
return stores.get(ctx)?.players.get(userId)?.heading ?? 0;
|
|
69
111
|
}
|
|
112
|
+
function copyController(state) {
|
|
113
|
+
return { ...state, position: [...state.position], groundNormal: [...state.groundNormal] };
|
|
114
|
+
}
|
|
115
|
+
/** Copy one player's movement state for prediction, rollback or a save; `null` when the player has not moved yet. */
|
|
116
|
+
export function snapshotPlayerMovement(ctx, userId) {
|
|
117
|
+
const state = stores.get(ctx)?.players.get(userId);
|
|
118
|
+
if (state === undefined)
|
|
119
|
+
return null;
|
|
120
|
+
const controller = state.controller?.snapshot() ?? state.pendingController;
|
|
121
|
+
return {
|
|
122
|
+
heading: state.heading,
|
|
123
|
+
facing: state.facing,
|
|
124
|
+
voxelBody: state.voxelBody === null ? null : { ...state.voxelBody },
|
|
125
|
+
motion: state.motion === null ? null : { ...state.motion },
|
|
126
|
+
flight: state.flight === null ? null : { ...state.flight },
|
|
127
|
+
controller: controller === null ? null : copyController(controller),
|
|
128
|
+
controllerJumpHeld: state.controllerJumpHeld,
|
|
129
|
+
jumpBuffer: state.jumpBuffer?.snapshot() ?? null,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/** Put a player's movement state back to a {@link snapshotPlayerMovement} copy, so the next {@link stepPlayerMovement} replays from there. */
|
|
133
|
+
export function restorePlayerMovement(ctx, userId, snapshot) {
|
|
134
|
+
const state = stateFor(storeFor(ctx), userId);
|
|
135
|
+
state.heading = snapshot.heading;
|
|
136
|
+
state.facing = snapshot.facing;
|
|
137
|
+
state.voxelBody = snapshot.voxelBody === null ? null : { ...snapshot.voxelBody };
|
|
138
|
+
state.motion = snapshot.motion === null ? null : { ...snapshot.motion };
|
|
139
|
+
state.flight = snapshot.flight === null ? null : { ...snapshot.flight };
|
|
140
|
+
state.controllerJumpHeld = snapshot.controllerJumpHeld;
|
|
141
|
+
const jumpBuffer = snapshot.jumpBuffer ?? null;
|
|
142
|
+
if (jumpBuffer === null) {
|
|
143
|
+
state.jumpBuffer = null;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
state.jumpBuffer ??= createInputBuffer({ windowMs: jumpBuffer.windowMs });
|
|
147
|
+
state.jumpBuffer.restore(jumpBuffer);
|
|
148
|
+
}
|
|
149
|
+
const controller = snapshot.controller === null ? null : copyController(snapshot.controller);
|
|
150
|
+
if (state.controller !== null && controller !== null) {
|
|
151
|
+
state.controller.restore(controller);
|
|
152
|
+
state.pendingController = null;
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
if (controller === null)
|
|
156
|
+
state.controller = null;
|
|
157
|
+
state.pendingController = controller;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
70
160
|
/** Drop a player's retained movement state (heading + kinematic body) — call on leave so a rejoin starts fresh instead of resuming stale velocity. */
|
|
71
161
|
export function forgetPlayerMovement(ctx, userId) {
|
|
72
162
|
stores.get(ctx)?.players.delete(userId);
|
|
@@ -90,15 +180,41 @@ function resolveBodyFacing(state, moving, velocityX, velocityZ, fallbackYaw, tur
|
|
|
90
180
|
state.facing = next;
|
|
91
181
|
return next;
|
|
92
182
|
}
|
|
183
|
+
function flightTuningFor(movement, ctx) {
|
|
184
|
+
const cfg = movement?.flight;
|
|
185
|
+
if (cfg === undefined || cfg === false)
|
|
186
|
+
return null;
|
|
187
|
+
if (cfg === true)
|
|
188
|
+
return { mode: "creative" };
|
|
189
|
+
if (typeof cfg === "object") {
|
|
190
|
+
if (cfg.canFly !== undefined && !cfg.canFly(ctx))
|
|
191
|
+
return null;
|
|
192
|
+
return {
|
|
193
|
+
mode: cfg.mode ?? "creative",
|
|
194
|
+
speed: cfg.speed,
|
|
195
|
+
verticalSpeed: cfg.verticalSpeed,
|
|
196
|
+
sprintMultiplier: cfg.sprintMultiplier,
|
|
197
|
+
acceleration: cfg.acceleration,
|
|
198
|
+
gravity: cfg.gravity,
|
|
199
|
+
thrust: cfg.thrust,
|
|
200
|
+
alignWithLook: cfg.alignWithLook,
|
|
201
|
+
collide: cfg.collide,
|
|
202
|
+
bindings: cfg.bindings,
|
|
203
|
+
camera: cfg.camera ?? null,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
93
208
|
/**
|
|
94
209
|
* Integrate one player's movement for a tick from their held-input frame and commit the pose — the single
|
|
95
210
|
* genre-agnostic controller both the shell (its local player) and a host (each connected player in `onTick`) call,
|
|
96
211
|
* so single-player and server-authoritative movement are identical. Reads the player's controlled entity, terrain,
|
|
97
212
|
* scene solids, and pending motion impulses; writes the entity pose via `setPose`. Retains heading + kinematic body
|
|
98
213
|
* per `userId` on the `ctx`. Pass `heading` to override the internally-integrated yaw (the shell owns yaw for its
|
|
99
|
-
* camera); omit it and the controller turns from the frame's `turnLeft`/`turnRight` actions.
|
|
214
|
+
* camera); omit it and the controller turns from the frame's `turnLeft`/`turnRight` actions. Pass `pitch` for
|
|
215
|
+
* 6DOF spectator flight when `alignWithLook` is set.
|
|
100
216
|
*/
|
|
101
|
-
export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading) {
|
|
217
|
+
export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitch) {
|
|
102
218
|
const playerId = ctx.player.possession.active(userId);
|
|
103
219
|
const player = ctx.scene.entity.get(playerId);
|
|
104
220
|
if (player === null)
|
|
@@ -141,6 +257,181 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading) {
|
|
|
141
257
|
const intent = resolveMovementIntent(keys, true, analogMove);
|
|
142
258
|
const motionBatch = ctx.player.motionFor(userId).takePending();
|
|
143
259
|
const walkSpeed = player.movement?.walkSpeed ?? DEFAULT_WALK_SPEED;
|
|
260
|
+
const flightTuning = flightTuningFor(tuning.movement, ctx);
|
|
261
|
+
if (flightTuning !== null) {
|
|
262
|
+
const value = (action) => input.analog?.[action] ?? (isDown(action) ? 1 : 0);
|
|
263
|
+
let flightIntent = resolveFreeFlightIntentFromInput(isDown, value, input.pointer, flightTuning.bindings);
|
|
264
|
+
if (tuning.movement?.canSprint !== undefined && !tuning.movement.canSprint(ctx)) {
|
|
265
|
+
flightIntent = { ...flightIntent, sprint: false };
|
|
266
|
+
}
|
|
267
|
+
let flightState = state.flight;
|
|
268
|
+
if (flightState === null) {
|
|
269
|
+
flightState = createFreeFlightState();
|
|
270
|
+
state.flight = flightState;
|
|
271
|
+
}
|
|
272
|
+
if (motionBatch !== null) {
|
|
273
|
+
flightState.vy = applyMotionImpulses(flightState.vy, motionBatch);
|
|
274
|
+
const [hx, hz] = applyHorizontalImpulses(flightState.vx, flightState.vz, motionBatch);
|
|
275
|
+
flightState.vx = hx;
|
|
276
|
+
flightState.vz = hz;
|
|
277
|
+
}
|
|
278
|
+
const normalizedFlightTuning = {
|
|
279
|
+
mode: flightTuning.mode,
|
|
280
|
+
speed: flightTuning.speed,
|
|
281
|
+
verticalSpeed: flightTuning.verticalSpeed,
|
|
282
|
+
sprintMultiplier: flightTuning.sprintMultiplier,
|
|
283
|
+
acceleration: flightTuning.acceleration,
|
|
284
|
+
gravity: flightTuning.gravity,
|
|
285
|
+
thrust: flightTuning.thrust,
|
|
286
|
+
alignWithLook: flightTuning.alignWithLook,
|
|
287
|
+
collide: flightTuning.collide,
|
|
288
|
+
bindings: flightTuning.bindings,
|
|
289
|
+
camera: flightTuning.camera,
|
|
290
|
+
};
|
|
291
|
+
if (normalizedFlightTuning.collide === undefined) {
|
|
292
|
+
normalizedFlightTuning.collide = normalizedFlightTuning.mode === "creative" || normalizedFlightTuning.mode === "hover";
|
|
293
|
+
}
|
|
294
|
+
if (normalizedFlightTuning.camera !== undefined) {
|
|
295
|
+
const cam = ctx.camera;
|
|
296
|
+
cam?.setChaseTuning(normalizedFlightTuning.camera ?? null);
|
|
297
|
+
}
|
|
298
|
+
const step = advanceFreeFlight(flightState, flightIntent, state.heading, pitch, dt, normalizedFlightTuning);
|
|
299
|
+
let stepX = step.stepX;
|
|
300
|
+
let stepY = step.stepY;
|
|
301
|
+
let stepZ = step.stepZ;
|
|
302
|
+
if (normalizedFlightTuning.collide !== false && tuning.movement?.collideObjects !== false) {
|
|
303
|
+
const obstacles = gatherMovementObstacles(ctx, player.position, stepX, stepZ);
|
|
304
|
+
const verticalResolved = resolveFlightStep(player.position, stepX, stepY, stepZ, obstacles, DEFAULT_OBSTACLE_PLAYER_RADIUS);
|
|
305
|
+
stepY = verticalResolved.stepY;
|
|
306
|
+
const xzResolved = resolveObstacleStep(player.position, stepX, stepZ, obstacles, DEFAULT_OBSTACLE_PLAYER_RADIUS, 0);
|
|
307
|
+
stepX = xzResolved.stepX;
|
|
308
|
+
stepZ = xzResolved.stepZ;
|
|
309
|
+
const ground = tuning.ground.sampleHeight(player.position[0] + stepX, player.position[2] + stepZ);
|
|
310
|
+
const nextY = player.position[1] + stepY;
|
|
311
|
+
if (nextY < ground + 0.2) {
|
|
312
|
+
stepY = ground + 0.2 - player.position[1];
|
|
313
|
+
if (flightState.vy < 0)
|
|
314
|
+
flightState.vy = 0;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
let nextX = player.position[0] + stepX;
|
|
318
|
+
let nextY = player.position[1] + stepY;
|
|
319
|
+
let nextZ = player.position[2] + stepZ;
|
|
320
|
+
if (motionBatch !== null && motionBatch.y !== null) {
|
|
321
|
+
nextY = motionBatch.y;
|
|
322
|
+
}
|
|
323
|
+
if (tuning.movement?.beforeCommit !== undefined) {
|
|
324
|
+
const frame = {
|
|
325
|
+
entityId: playerId,
|
|
326
|
+
current: player.position,
|
|
327
|
+
next: [nextX, nextY, nextZ],
|
|
328
|
+
dt,
|
|
329
|
+
ctx,
|
|
330
|
+
};
|
|
331
|
+
const replacement = tuning.movement.beforeCommit(frame);
|
|
332
|
+
if (replacement !== undefined) {
|
|
333
|
+
nextX = replacement[0];
|
|
334
|
+
nextY = replacement[1];
|
|
335
|
+
nextZ = replacement[2];
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
ctx.scene.entity.setPose(playerId, {
|
|
339
|
+
position: [nextX, nextY, nextZ],
|
|
340
|
+
rotationY: resolveBodyFacing(state, flightIntent.moving, flightState.vx, flightState.vz, player.rotationY, tuning.movement?.turnSpeed, dt),
|
|
341
|
+
dt,
|
|
342
|
+
});
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (flightTuning === null && state.flight !== null) {
|
|
346
|
+
const cam = ctx.camera;
|
|
347
|
+
cam?.setChaseTuning(null);
|
|
348
|
+
}
|
|
349
|
+
if (tuning.controller !== undefined) {
|
|
350
|
+
let controller = state.controller;
|
|
351
|
+
if (controller === null) {
|
|
352
|
+
controller = createCharacterController(tuning.controller.capsule);
|
|
353
|
+
state.controller = controller;
|
|
354
|
+
controller.restore(state.pendingController ?? {
|
|
355
|
+
position: [player.position[0], player.position[1], player.position[2]],
|
|
356
|
+
verticalVelocity: 0,
|
|
357
|
+
grounded: true,
|
|
358
|
+
groundNormal: [0, 1, 0],
|
|
359
|
+
groundBody: null,
|
|
360
|
+
crouching: false,
|
|
361
|
+
});
|
|
362
|
+
state.pendingController = null;
|
|
363
|
+
}
|
|
364
|
+
const controllerState = controller.state();
|
|
365
|
+
if (Math.hypot(controllerState.position[0] - player.position[0], controllerState.position[1] - player.position[1], controllerState.position[2] - player.position[2]) > 1e-6) {
|
|
366
|
+
controller.restore({
|
|
367
|
+
...controllerState,
|
|
368
|
+
position: [player.position[0], player.position[1], player.position[2]],
|
|
369
|
+
grounded: false,
|
|
370
|
+
groundBody: null,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
// Keep the existing input feel and horizontal impulse seam; the capsule owns vertical motion and collision.
|
|
374
|
+
let horizontal = state.motion;
|
|
375
|
+
if (horizontal === null) {
|
|
376
|
+
horizontal = createPlayerMotionState();
|
|
377
|
+
state.motion = horizontal;
|
|
378
|
+
}
|
|
379
|
+
const grounded = controller.state().grounded;
|
|
380
|
+
horizontal.grounded = grounded;
|
|
381
|
+
horizontal.verticalVelocity = 0;
|
|
382
|
+
horizontal.jumpOffset = 0;
|
|
383
|
+
[horizontal.horizontalVelocityX, horizontal.horizontalVelocityZ] = applyHorizontalImpulses(horizontal.horizontalVelocityX, horizontal.horizontalVelocityZ, motionBatch);
|
|
384
|
+
const jumpPressed = intent.jumping && !state.controllerJumpHeld;
|
|
385
|
+
const horizontalStep = advancePlayerMotion(horizontal, { ...intent, jumping: false }, forwardX, forwardZ, walkSpeed, dt, tuning.physics);
|
|
386
|
+
horizontal.grounded = grounded;
|
|
387
|
+
horizontal.verticalVelocity = 0;
|
|
388
|
+
horizontal.jumpOffset = 0;
|
|
389
|
+
state.controllerJumpHeld = intent.jumping;
|
|
390
|
+
const current = controller.state();
|
|
391
|
+
controller.restore({
|
|
392
|
+
...current,
|
|
393
|
+
verticalVelocity: applyMotionImpulses(current.verticalVelocity, motionBatch),
|
|
394
|
+
});
|
|
395
|
+
const result = controller.move(tuning.controller.backend, {
|
|
396
|
+
motion: [horizontalStep.stepX, 0, horizontalStep.stepZ],
|
|
397
|
+
dt,
|
|
398
|
+
gravity: tuning.physics?.gravityAcceleration ?? MOVEMENT_TUNING.gravityAcceleration,
|
|
399
|
+
...(jumpPressed && !intent.crouching
|
|
400
|
+
? { jumpVelocity: tuning.physics?.jumpVelocity ?? MOVEMENT_TUNING.jumpVelocity }
|
|
401
|
+
: {}),
|
|
402
|
+
crouch: intent.crouching,
|
|
403
|
+
});
|
|
404
|
+
let nextPosition = [
|
|
405
|
+
controller.state().position[0],
|
|
406
|
+
controller.state().position[1],
|
|
407
|
+
controller.state().position[2],
|
|
408
|
+
];
|
|
409
|
+
if (motionBatch !== null && motionBatch.y !== null) {
|
|
410
|
+
nextPosition[1] = motionBatch.y;
|
|
411
|
+
controller.restore({ ...controller.state(), position: nextPosition });
|
|
412
|
+
}
|
|
413
|
+
if (tuning.movement?.beforeCommit !== undefined) {
|
|
414
|
+
const frame = {
|
|
415
|
+
entityId: playerId,
|
|
416
|
+
current: player.position,
|
|
417
|
+
next: nextPosition,
|
|
418
|
+
dt,
|
|
419
|
+
ctx,
|
|
420
|
+
};
|
|
421
|
+
const replacement = tuning.movement.beforeCommit(frame);
|
|
422
|
+
if (replacement !== undefined) {
|
|
423
|
+
nextPosition = [replacement[0], replacement[1], replacement[2]];
|
|
424
|
+
controller.restore({ ...controller.state(), position: nextPosition });
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
horizontal.grounded = result.grounded;
|
|
428
|
+
ctx.scene.entity.setPose(playerId, {
|
|
429
|
+
position: nextPosition,
|
|
430
|
+
rotationY: resolveBodyFacing(state, intent.moving, horizontal.horizontalVelocityX, horizontal.horizontalVelocityZ, player.rotationY, tuning.movement?.turnSpeed, dt),
|
|
431
|
+
dt,
|
|
432
|
+
});
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
144
435
|
if (tuning.collision?.voxel === true) {
|
|
145
436
|
let body = state.voxelBody;
|
|
146
437
|
if (body === null) {
|
|
@@ -187,9 +478,15 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading) {
|
|
|
187
478
|
const submerged = swimEnabled &&
|
|
188
479
|
waterLevel !== undefined &&
|
|
189
480
|
tuning.ground.sampleHeight(player.position[0], player.position[2]) < waterLevel;
|
|
481
|
+
const jumpBufferMs = tuning.physics?.jumpBufferMs ?? 0;
|
|
482
|
+
let jumpBuffer = state.jumpBuffer;
|
|
483
|
+
if (jumpBuffer === null) {
|
|
484
|
+
jumpBuffer = createInputBuffer({ windowMs: jumpBufferMs });
|
|
485
|
+
state.jumpBuffer = jumpBuffer;
|
|
486
|
+
}
|
|
190
487
|
const motionOptions = submerged
|
|
191
|
-
? { speedScale: swimSpeedMultiplier, floating: true }
|
|
192
|
-
:
|
|
488
|
+
? { speedScale: swimSpeedMultiplier, floating: true, buffer: jumpBuffer }
|
|
489
|
+
: { buffer: jumpBuffer };
|
|
193
490
|
const prevJumpOffset = motion.jumpOffset;
|
|
194
491
|
const step = advancePlayerMotion(motion, intent, forwardX, forwardZ, walkSpeed, dt, tuning.physics, motionOptions);
|
|
195
492
|
// Airborne means "not resting on the surface below": any jump/impulse height before or after this
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { GameContext } from "../runtime/gameContext.js";
|
|
2
|
+
import type { InputFrame } from "../runtime/inputSnapshot.js";
|
|
3
|
+
import { type PlayerMovementTuning } from "./playerMovement.js";
|
|
4
|
+
/** Advance one authoritative player's movement using the same controller as the shell. */
|
|
5
|
+
export declare function serverStep(ctx: GameContext, userId: string, frame: InputFrame, dt: number, tuning?: PlayerMovementTuning): void;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { groundFieldFor } from "../world/terrain.js";
|
|
2
|
+
import { stepPlayerMovement } from "./playerMovement.js";
|
|
3
|
+
/** Advance one authoritative player's movement using the same controller as the shell. */
|
|
4
|
+
export function serverStep(ctx, userId, frame, dt, tuning) {
|
|
5
|
+
stepPlayerMovement(ctx, userId, frame, dt, tuning ?? { ground: groundFieldFor(), hasTerrain: false });
|
|
6
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { GameContext } from "../runtime/gameContext.js";
|
|
2
2
|
import { resolveColliders, type ResolvedCollider } from "../scene/colliders.js";
|
|
3
3
|
import type { EntityPosition } from "../scene/entityStore.js";
|
|
4
|
+
import type { WorldSolid } from "../world/worldSolids.js";
|
|
4
5
|
import { type CollisionObstacle } from "./movementModel.js";
|
|
5
6
|
/** Largest blocking-collider extent in the scene, split so tower height cannot inflate the XZ query. */
|
|
6
7
|
export interface ObstacleReach {
|
|
@@ -11,8 +12,8 @@ export interface ObstacleReach {
|
|
|
11
12
|
}
|
|
12
13
|
/**
|
|
13
14
|
* The slice of the scene's object store this query needs. Narrow on purpose: the scene context wires
|
|
14
|
-
* its own store here while it is still being constructed,
|
|
15
|
-
*
|
|
15
|
+
* its own store here while it is still being constructed, `ctx.scene.object` satisfies it as-is, and a
|
|
16
|
+
* pure rules package with no `GameContext` to import can satisfy it from its own object list.
|
|
16
17
|
*/
|
|
17
18
|
export interface SolidObstacleSource {
|
|
18
19
|
list(): readonly {
|
|
@@ -27,12 +28,12 @@ export interface SolidObstacleSource {
|
|
|
27
28
|
}[];
|
|
28
29
|
collidersOf(instanceId: string): Parameters<typeof resolveColliders>[0] | null;
|
|
29
30
|
}
|
|
30
|
-
/** Mutable reach cache; one per source, invalidated by object count.
|
|
31
|
+
/** Mutable reach cache; one per source, invalidated by object count. */
|
|
31
32
|
export interface ObstacleReachCache {
|
|
32
33
|
count: number;
|
|
33
34
|
value: ObstacleReach;
|
|
34
35
|
}
|
|
35
|
-
/** A fresh {@link ObstacleReachCache} for a caller that owns its own source.
|
|
36
|
+
/** A fresh {@link ObstacleReachCache} for a caller that owns its own source. */
|
|
36
37
|
export declare function createObstacleReachCache(): ObstacleReachCache;
|
|
37
38
|
/**
|
|
38
39
|
* Largest blocking reach in the scene, split into horizontal vs vertical and hard-capped. Cached per
|
|
@@ -41,7 +42,7 @@ export declare function createObstacleReachCache(): ObstacleReachCache;
|
|
|
41
42
|
* @internal
|
|
42
43
|
*/
|
|
43
44
|
export declare function solidObstacleReach(ctx: GameContext): ObstacleReach;
|
|
44
|
-
/** {@link solidObstacleReach} against a bare source and caller-owned cache.
|
|
45
|
+
/** {@link solidObstacleReach} against a bare source and caller-owned cache. */
|
|
45
46
|
export declare function sourceObstacleReach(source: SolidObstacleSource, cache: ObstacleReachCache): ObstacleReach;
|
|
46
47
|
/** One blocking collider as a {@link CollisionObstacle}: a compound `boxes` obstacle for a mesh collider
|
|
47
48
|
* carrying a small sub-box set (openings), else a single half-extents/offset AABB. Yaw is applied by
|
|
@@ -49,9 +50,16 @@ export declare function sourceObstacleReach(source: SolidObstacleSource, cache:
|
|
|
49
50
|
* never under-covers.
|
|
50
51
|
* @internal */
|
|
51
52
|
export declare function obstacleFromCollider(collider: ResolvedCollider, position: EntityPosition, rotationY: number): CollisionObstacle;
|
|
53
|
+
/**
|
|
54
|
+
* A {@link WorldSolid} as a {@link CollisionObstacle}. Axis-aligned solids are one box; a yawed solid is
|
|
55
|
+
* cut into strips along its long axis, each fitted with an AABB, so a turned building does not block
|
|
56
|
+
* the street beside it the way a single enclosing box would.
|
|
57
|
+
* @capability solid-obstacles the one blocking-geometry query the player resolver and NPC movers share
|
|
58
|
+
*/
|
|
59
|
+
export declare function obstacleFromSolid(solid: WorldSolid): CollisionObstacle;
|
|
52
60
|
/**
|
|
53
61
|
* Every blocking physical obstacle overlapping the box `position` ± `reachX`/`reachZ`, read from the
|
|
54
|
-
* scene's resolved colliders.
|
|
62
|
+
* scene's resolved colliders and `ctx.world.solids`.
|
|
55
63
|
*
|
|
56
64
|
* This is the engine's single answer to "what is solid here". The player resolver, a walking NPC, and
|
|
57
65
|
* anything else that has to stop at a wall read it rather than deriving their own set — three
|
|
@@ -61,7 +69,7 @@ export declare function obstacleFromCollider(collider: ResolvedCollider, positio
|
|
|
61
69
|
* @capability solid-obstacles the one blocking-geometry query the player resolver and NPC movers share
|
|
62
70
|
*/
|
|
63
71
|
export declare function solidObstaclesNear(ctx: GameContext, position: EntityPosition, reachX: number, reachZ: number, height?: number): CollisionObstacle[];
|
|
64
|
-
/** {@link solidObstaclesNear} against a bare source and an already-resolved reach.
|
|
72
|
+
/** {@link solidObstaclesNear} against a bare source and an already-resolved reach. */
|
|
65
73
|
export declare function sourceObstaclesNear(source: SolidObstacleSource, reach: ObstacleReach, position: EntityPosition, reachX: number, reachZ: number, height?: number): CollisionObstacle[];
|
|
66
74
|
/**
|
|
67
75
|
* Slide a walker's horizontal step against the same solid geometry that stops the player, returning the
|
|
@@ -71,6 +79,11 @@ export declare function sourceObstaclesNear(source: SolidObstacleSource, reach:
|
|
|
71
79
|
* Bounded by the same broadphase as the player's gather: one `inBox` query sized to this step, never a
|
|
72
80
|
* full-world scan, so it stays affordable per NPC per tick.
|
|
73
81
|
*
|
|
82
|
+
* The sliding is already axis-separated: X and Z are resolved independently inside, so a caller that
|
|
83
|
+
* also splits its own movement by axis must hand this exactly one axis per call (`stepZ: 0`, then
|
|
84
|
+
* `stepX: 0`). Running an axis-split pass over a step this function already resolved double-cuts it,
|
|
85
|
+
* and the composition is wrong in either order.
|
|
86
|
+
*
|
|
74
87
|
* @capability walker-solid-step slide an NPC's step against the geometry that blocks the player
|
|
75
88
|
*/
|
|
76
89
|
export declare function resolveWalkerStep(ctx: GameContext, position: EntityPosition, stepX: number, stepZ: number, options?: {
|
|
@@ -80,7 +93,25 @@ export declare function resolveWalkerStep(ctx: GameContext, position: EntityPosi
|
|
|
80
93
|
stepX: number;
|
|
81
94
|
stepZ: number;
|
|
82
95
|
};
|
|
83
|
-
/**
|
|
96
|
+
/**
|
|
97
|
+
* {@link resolveWalkerStep} with no `GameContext`: gather plus slide against a bare
|
|
98
|
+
* {@link SolidObstacleSource} and a caller-owned {@link ObstacleReachCache}.
|
|
99
|
+
*
|
|
100
|
+
* This is the entry point for rules that live in a pure package — no React, no three.js, no engine
|
|
101
|
+
* context to import. Hold one cache per source for the life of that source (`createObstacleReachCache()`)
|
|
102
|
+
* rather than making a fresh one per step; it is what keeps the reach scan off the per-step path.
|
|
103
|
+
* The same axis-separation note as {@link resolveWalkerStep} applies.
|
|
104
|
+
*
|
|
105
|
+
* @capability walker-solid-step-source slide a step against solid geometry from a pure rules package
|
|
106
|
+
*/
|
|
107
|
+
export declare function resolveSourceWalkerStep(source: SolidObstacleSource, cache: ObstacleReachCache, position: EntityPosition, stepX: number, stepZ: number, options?: {
|
|
108
|
+
radius?: number;
|
|
109
|
+
stepUpHeight?: number;
|
|
110
|
+
}): {
|
|
111
|
+
stepX: number;
|
|
112
|
+
stepZ: number;
|
|
113
|
+
};
|
|
114
|
+
/** {@link resolveWalkerStep} against an already-gathered obstacle set. */
|
|
84
115
|
export declare function slideStep(position: EntityPosition, stepX: number, stepZ: number, obstacles: readonly CollisionObstacle[], radius?: number, stepUpHeight?: number): {
|
|
85
116
|
stepX: number;
|
|
86
117
|
stepZ: number;
|
|
@@ -26,9 +26,13 @@ const OBSTACLE_VERTICAL_CAP = 8;
|
|
|
26
26
|
* multiply narrowphase cost without changing the blocked footprint.
|
|
27
27
|
*/
|
|
28
28
|
const MOVEMENT_MESH_BOX_BUDGET = 12;
|
|
29
|
+
/** Most AABB strips an oriented world solid is sliced into, bounding the corner overhang a yawed box leaves. */
|
|
30
|
+
const SOLID_MAX_STRIPS = 8;
|
|
31
|
+
/** Yaw within this of a quarter turn is treated as axis-aligned (radians). */
|
|
32
|
+
const AXIS_ALIGNED_EPSILON = 1e-4;
|
|
29
33
|
/** Feet-to-head span a walker is tested over — matches the player's, so both agree on what a wall is. */
|
|
30
34
|
const WALKER_HEIGHT = 1.8;
|
|
31
|
-
/** A fresh {@link ObstacleReachCache} for a caller that owns its own source.
|
|
35
|
+
/** A fresh {@link ObstacleReachCache} for a caller that owns its own source. */
|
|
32
36
|
export function createObstacleReachCache() {
|
|
33
37
|
return { count: -1, value: { horizontal: OBSTACLE_MAX_HALF_EXTENT, vertical: OBSTACLE_VERTICAL_FLOOR } };
|
|
34
38
|
}
|
|
@@ -42,7 +46,7 @@ const reachCache = perContext(() => createObstacleReachCache());
|
|
|
42
46
|
export function solidObstacleReach(ctx) {
|
|
43
47
|
return sourceObstacleReach(ctx.scene.object, reachCache(ctx));
|
|
44
48
|
}
|
|
45
|
-
/** {@link solidObstacleReach} against a bare source and caller-owned cache.
|
|
49
|
+
/** {@link solidObstacleReach} against a bare source and caller-owned cache. */
|
|
46
50
|
export function sourceObstacleReach(source, cache) {
|
|
47
51
|
const objects = source.list();
|
|
48
52
|
if (cache.count === objects.length)
|
|
@@ -141,9 +145,40 @@ export function obstacleFromCollider(collider, position, rotationY) {
|
|
|
141
145
|
halfExtents: [Math.abs(hx * cos) + Math.abs(hz * sin), hy, Math.abs(hx * sin) + Math.abs(hz * cos)],
|
|
142
146
|
};
|
|
143
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* A {@link WorldSolid} as a {@link CollisionObstacle}. Axis-aligned solids are one box; a yawed solid is
|
|
150
|
+
* cut into strips along its long axis, each fitted with an AABB, so a turned building does not block
|
|
151
|
+
* the street beside it the way a single enclosing box would.
|
|
152
|
+
* @capability solid-obstacles the one blocking-geometry query the player resolver and NPC movers share
|
|
153
|
+
*/
|
|
154
|
+
export function obstacleFromSolid(solid) {
|
|
155
|
+
const [hx, hy, hz] = solid.halfExtents;
|
|
156
|
+
const yaw = solid.rotationY ?? 0;
|
|
157
|
+
const quarter = yaw / (Math.PI / 2);
|
|
158
|
+
const nearest = Math.round(quarter);
|
|
159
|
+
if (Math.abs(quarter - nearest) < AXIS_ALIGNED_EPSILON) {
|
|
160
|
+
const swapped = Math.abs(nearest) % 2 === 1;
|
|
161
|
+
return { position: solid.center, halfExtents: swapped ? [hz, hy, hx] : [hx, hy, hz] };
|
|
162
|
+
}
|
|
163
|
+
const alongX = hx >= hz;
|
|
164
|
+
const long = alongX ? hx : hz;
|
|
165
|
+
const short = Math.max(alongX ? hz : hx, 1e-6);
|
|
166
|
+
const strips = Math.min(SOLID_MAX_STRIPS, Math.max(1, Math.ceil(long / short)));
|
|
167
|
+
const cos = Math.cos(yaw);
|
|
168
|
+
const sin = Math.sin(yaw);
|
|
169
|
+
const boxes = [];
|
|
170
|
+
for (let i = 0; i < strips; i += 1) {
|
|
171
|
+
const from = -long + (2 * long * i) / strips;
|
|
172
|
+
const to = from + (2 * long) / strips;
|
|
173
|
+
const boxMin = alongX ? [from, -hy, -hz] : [-hx, -hy, from];
|
|
174
|
+
const boxMax = alongX ? [to, hy, hz] : [hx, hy, to];
|
|
175
|
+
boxes.push(yawExpandLocalBox(boxMin, boxMax, cos, sin));
|
|
176
|
+
}
|
|
177
|
+
return { position: solid.center, boxes };
|
|
178
|
+
}
|
|
144
179
|
/**
|
|
145
180
|
* Every blocking physical obstacle overlapping the box `position` ± `reachX`/`reachZ`, read from the
|
|
146
|
-
* scene's resolved colliders.
|
|
181
|
+
* scene's resolved colliders and `ctx.world.solids`.
|
|
147
182
|
*
|
|
148
183
|
* This is the engine's single answer to "what is solid here". The player resolver, a walking NPC, and
|
|
149
184
|
* anything else that has to stop at a wall read it rather than deriving their own set — three
|
|
@@ -153,9 +188,16 @@ export function obstacleFromCollider(collider, position, rotationY) {
|
|
|
153
188
|
* @capability solid-obstacles the one blocking-geometry query the player resolver and NPC movers share
|
|
154
189
|
*/
|
|
155
190
|
export function solidObstaclesNear(ctx, position, reachX, reachZ, height = WALKER_HEIGHT) {
|
|
156
|
-
|
|
191
|
+
const obstacles = sourceObstaclesNear(ctx.scene.object, solidObstacleReach(ctx), position, reachX, reachZ, height);
|
|
192
|
+
const solids = ctx.world.solids;
|
|
193
|
+
if (solids.count() === 0)
|
|
194
|
+
return obstacles;
|
|
195
|
+
for (const solid of solids.inBox([position[0] - reachX, position[1] - height, position[2] - reachZ], [position[0] + reachX, position[1] + 2 * height, position[2] + reachZ])) {
|
|
196
|
+
obstacles.push(obstacleFromSolid(solid));
|
|
197
|
+
}
|
|
198
|
+
return obstacles;
|
|
157
199
|
}
|
|
158
|
-
/** {@link solidObstaclesNear} against a bare source and an already-resolved reach.
|
|
200
|
+
/** {@link solidObstaclesNear} against a bare source and an already-resolved reach. */
|
|
159
201
|
export function sourceObstaclesNear(source, reach, position, reachX, reachZ, height = WALKER_HEIGHT) {
|
|
160
202
|
const min = [
|
|
161
203
|
position[0] - reachX - reach.horizontal,
|
|
@@ -191,6 +233,11 @@ export function sourceObstaclesNear(source, reach, position, reachX, reachZ, hei
|
|
|
191
233
|
* Bounded by the same broadphase as the player's gather: one `inBox` query sized to this step, never a
|
|
192
234
|
* full-world scan, so it stays affordable per NPC per tick.
|
|
193
235
|
*
|
|
236
|
+
* The sliding is already axis-separated: X and Z are resolved independently inside, so a caller that
|
|
237
|
+
* also splits its own movement by axis must hand this exactly one axis per call (`stepZ: 0`, then
|
|
238
|
+
* `stepX: 0`). Running an axis-split pass over a step this function already resolved double-cuts it,
|
|
239
|
+
* and the composition is wrong in either order.
|
|
240
|
+
*
|
|
194
241
|
* @capability walker-solid-step slide an NPC's step against the geometry that blocks the player
|
|
195
242
|
*/
|
|
196
243
|
export function resolveWalkerStep(ctx, position, stepX, stepZ, options = {}) {
|
|
@@ -200,7 +247,25 @@ export function resolveWalkerStep(ctx, position, stepX, stepZ, options = {}) {
|
|
|
200
247
|
const obstacles = solidObstaclesNear(ctx, position, Math.abs(stepX) + radius, Math.abs(stepZ) + radius);
|
|
201
248
|
return slideStep(position, stepX, stepZ, obstacles, radius, options.stepUpHeight ?? 0);
|
|
202
249
|
}
|
|
203
|
-
/**
|
|
250
|
+
/**
|
|
251
|
+
* {@link resolveWalkerStep} with no `GameContext`: gather plus slide against a bare
|
|
252
|
+
* {@link SolidObstacleSource} and a caller-owned {@link ObstacleReachCache}.
|
|
253
|
+
*
|
|
254
|
+
* This is the entry point for rules that live in a pure package — no React, no three.js, no engine
|
|
255
|
+
* context to import. Hold one cache per source for the life of that source (`createObstacleReachCache()`)
|
|
256
|
+
* rather than making a fresh one per step; it is what keeps the reach scan off the per-step path.
|
|
257
|
+
* The same axis-separation note as {@link resolveWalkerStep} applies.
|
|
258
|
+
*
|
|
259
|
+
* @capability walker-solid-step-source slide a step against solid geometry from a pure rules package
|
|
260
|
+
*/
|
|
261
|
+
export function resolveSourceWalkerStep(source, cache, position, stepX, stepZ, options = {}) {
|
|
262
|
+
if (stepX === 0 && stepZ === 0)
|
|
263
|
+
return { stepX, stepZ };
|
|
264
|
+
const radius = options.radius ?? DEFAULT_OBSTACLE_PLAYER_RADIUS;
|
|
265
|
+
const obstacles = sourceObstaclesNear(source, sourceObstacleReach(source, cache), position, Math.abs(stepX) + radius, Math.abs(stepZ) + radius);
|
|
266
|
+
return slideStep(position, stepX, stepZ, obstacles, radius, options.stepUpHeight ?? 0);
|
|
267
|
+
}
|
|
268
|
+
/** {@link resolveWalkerStep} against an already-gathered obstacle set. */
|
|
204
269
|
export function slideStep(position, stepX, stepZ, obstacles, radius = DEFAULT_OBSTACLE_PLAYER_RADIUS, stepUpHeight = 0) {
|
|
205
270
|
if (obstacles.length === 0)
|
|
206
271
|
return { stepX, stepZ };
|
|
@@ -97,7 +97,7 @@ function supportTopWithin(x, z, fromY, toY, isSolid, dims) {
|
|
|
97
97
|
}
|
|
98
98
|
return best;
|
|
99
99
|
}
|
|
100
|
-
function targetHorizontalVelocity(intent, forwardX, forwardZ, baseSpeed) {
|
|
100
|
+
function targetHorizontalVelocity(intent, forwardX, forwardZ, baseSpeed, tuning) {
|
|
101
101
|
if (!intent.moving)
|
|
102
102
|
return { x: 0, z: 0 };
|
|
103
103
|
let fx = forwardX;
|
|
@@ -120,9 +120,9 @@ function targetHorizontalVelocity(intent, forwardX, forwardZ, baseSpeed) {
|
|
|
120
120
|
if (magSq < 1e-6)
|
|
121
121
|
return { x: 0, z: 0 };
|
|
122
122
|
const speedMultiplier = intent.crouching
|
|
123
|
-
? MOVEMENT_TUNING.crouchSpeedMultiplier
|
|
123
|
+
? (tuning?.crouchSpeedMultiplier ?? MOVEMENT_TUNING.crouchSpeedMultiplier)
|
|
124
124
|
: intent.running
|
|
125
|
-
? MOVEMENT_TUNING.runSpeedMultiplier
|
|
125
|
+
? (tuning?.runSpeedMultiplier ?? MOVEMENT_TUNING.runSpeedMultiplier)
|
|
126
126
|
: 1;
|
|
127
127
|
const targetSpeed = baseSpeed * MOVEMENT_TUNING.walkSpeedMultiplier * speedMultiplier;
|
|
128
128
|
const scale = targetSpeed / Math.sqrt(magSq);
|
|
@@ -176,15 +176,15 @@ export function advanceVoxelPlayer(body, intent, forwardX, forwardZ, baseSpeed,
|
|
|
176
176
|
const dt = Math.min(rawDeltaSeconds, MOVEMENT_TUNING.maxFrameSeconds);
|
|
177
177
|
const gravityAcceleration = tuning?.gravityAcceleration ?? MOVEMENT_TUNING.gravityAcceleration;
|
|
178
178
|
const jumpVelocity = tuning?.jumpVelocity ?? MOVEMENT_TUNING.jumpVelocity;
|
|
179
|
-
const target = targetHorizontalVelocity(intent, forwardX, forwardZ, baseSpeed);
|
|
179
|
+
const target = targetHorizontalVelocity(intent, forwardX, forwardZ, baseSpeed, tuning);
|
|
180
180
|
const acceleration = body.grounded
|
|
181
|
-
? MOVEMENT_TUNING.groundAcceleration
|
|
182
|
-
: MOVEMENT_TUNING.airAcceleration;
|
|
181
|
+
? (tuning?.groundAcceleration ?? MOVEMENT_TUNING.groundAcceleration)
|
|
182
|
+
: (tuning?.airAcceleration ?? MOVEMENT_TUNING.airAcceleration);
|
|
183
183
|
const accelerationBlend = 1 - Math.exp(-acceleration * dt);
|
|
184
184
|
body.velocityX += (target.x - body.velocityX) * accelerationBlend;
|
|
185
185
|
body.velocityZ += (target.z - body.velocityZ) * accelerationBlend;
|
|
186
186
|
if (!intent.moving && body.grounded) {
|
|
187
|
-
const friction = Math.exp(-MOVEMENT_TUNING.groundFriction * dt);
|
|
187
|
+
const friction = Math.exp(-(tuning?.groundFriction ?? MOVEMENT_TUNING.groundFriction) * dt);
|
|
188
188
|
body.velocityX *= friction;
|
|
189
189
|
body.velocityZ *= friction;
|
|
190
190
|
}
|
|
@@ -16,4 +16,6 @@ export interface PoseSyncTuning {
|
|
|
16
16
|
export interface PoseSyncGate {
|
|
17
17
|
evaluate(pose: PlayerPose, nowMs: number): boolean;
|
|
18
18
|
}
|
|
19
|
-
|
|
19
|
+
/** Default ten-hertz client pose gate with a five-second heartbeat. */
|
|
20
|
+
export declare const DEFAULT_POSE_SYNC_TUNING: PoseSyncTuning;
|
|
21
|
+
export declare function createPoseSyncGate(tuning?: PoseSyncTuning): PoseSyncGate;
|