@series-inc/rundot-syncplay 5.26.0-beta.2 → 5.26.0-beta.4
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/README.md +17 -0
- package/core/native/deterministic_math.hpp +4 -3
- package/core/native/fixed_math.cpp +741 -0
- package/core/native/fixed_math.hpp +111 -0
- package/core/native/kinetix-native-main.cpp +227 -88
- package/core/native/mobile/box3d_probe_scenario.cpp +101 -0
- package/core/native/mobile/box3d_probe_scenario.hpp +25 -0
- package/core/native/mobile/syncplay_native_probe.cpp +114 -0
- package/core/native/mobile/syncplay_native_probe_abi.h +46 -0
- package/core/physics.d.ts +48 -0
- package/core/scripts/build-mobile-native.mjs +101 -0
- package/core/scripts/build-native.mjs +15 -1
- package/core/scripts/build-wasm-fixed-math.mjs +58 -0
- package/core/scripts/run-box3d-benchmark.mjs +22 -124
- package/core/src/physics/box3d-probe.mjs +171 -0
- package/core/src/physics/index.mjs +156 -19
- package/core/src/physics/physics-wasm-module.generated.mjs +3 -3
- package/dist/browser.d.ts +2 -1
- package/dist/browser.js +1 -0
- package/dist/creator.d.ts +1 -0
- package/dist/creator.js +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/movement3d.d.ts +31 -0
- package/dist/movement3d.js +265 -30
- package/dist/physics3d-heightfield.d.ts +10 -0
- package/dist/physics3d-heightfield.js +33 -0
- package/dist/physics3d-hull.d.ts +14 -0
- package/dist/physics3d-hull.js +45 -0
- package/dist/physics3d.js +28 -1
- package/package.json +8 -4
- package/dist/prediction-culling.d.ts +0 -34
- package/dist/prediction-culling.js +0 -200
package/dist/browser.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ export { fbm2D, hash2, hashUint32, valueNoise1D, valueNoise2D } from './noise.js
|
|
|
27
27
|
export { circleCastDeterministicPhysics2D, createDeterministicPhysicsWorld2D, nearestDeterministicPhysicsHit2D, queryDeterministicPhysicsAabb2D, queryDeterministicPhysicsShapeOverlap2D, raycastAllDeterministicPhysics2D, raycastDeterministicPhysics2D, stepDeterministicPhysicsWorld2D } from './physics2d.js';
|
|
28
28
|
export { applyDeterministicImpulse3D, applyDeterministicImpulses3D, applyDeterministicWorldEdit3D, contactDetailsDeterministicPhysics3D, createDeterministicPhysicsWorld3D, createTrustedDeterministicPhysicsWorld3D, deriveInverseInertia, deterministicPhysicsBodyById3D, deterministicPhysicsCallback3D, overlapDeterministicPhysics3D, raycastDeterministicPhysics3D, raycastDeterministicPhysics3DDetailed, runDeterministicPhysics3DStress, setDeterministicBodyVelocity3D, shapeCastDeterministicPhysics3D, shapeCastDeterministicPhysics3DDetailed, stepDeterministicPhysicsWorld3D } from './physics3d.js';
|
|
29
29
|
export { cookDeterministicVoxelChunkCollider3D } from './physics3d-voxel.js';
|
|
30
|
+
export { cylinderHullVertices } from './physics3d-hull.js';
|
|
30
31
|
export { createDeterministicCosmeticPhysicsWorld3D, setDeterministicCosmeticPhysicsAuthoritativeBodyProxies3D, stepDeterministicCosmeticPhysicsWorld3D } from './cosmetic-physics3d.js';
|
|
31
32
|
export { stepDeterministicVehicle3D } from './physics3d-vehicle.js';
|
|
32
33
|
export { createDeterministicR3fRenderAdapter as createDeterministicR3fAdapter, createDeterministicR3fRenderAdapter, interpolateDeterministicRenderNumber, interpolateDeterministicRenderVec2 } from './r3f-render-adapter.js';
|
|
@@ -60,7 +61,7 @@ export type { DeterministicAuthoritativeLagCompensation3DShotContext, Determinis
|
|
|
60
61
|
export type { DeterministicLocalAuthoritySnapshotIdentity, DeterministicLocalAuthoritySnapshotTransfer, DeterministicLocalAuthorityWireEnvelope, DeterministicLocalAuthorityWireKind } from './local-authority-wire.js';
|
|
61
62
|
export type { DeterministicFixedQuaternion, DeterministicFixedVec2, DeterministicFixedVec3, DeterministicMath } from './math.js';
|
|
62
63
|
export type { DeterministicFpsAimState, DeterministicFpsInput, DeterministicKccBody, DeterministicKccInput, DeterministicKccStepResult, DeterministicLagCompensatedHitValidation, DeterministicLagCompensatedShot, DeterministicMovementCrowdMetrics, DeterministicMovementSimulationMetrics, DeterministicMovementStep, DeterministicMovementWorld, DeterministicMovingPlatform, DeterministicOneWayPlatform, DeterministicSlope, DeterministicTargetSnapshot, DeterministicTopDownBody, DeterministicTopDownBounds, DeterministicTopDownInput } from './movement.js';
|
|
63
|
-
export type { DeterministicKccBody3D, DeterministicKccCarrierPose3D, DeterministicKccCarryResult3D, DeterministicKccCarryState3D, DeterministicKccCollisionCandidate3D, DeterministicKccCollisionFilter3D, DeterministicKccExternalImpulse3D, DeterministicKccGeometry3D, DeterministicKccInput3D, DeterministicKccPhysicsImpulse3D, DeterministicKccProcessor3D, DeterministicKccStepResult3D, DeterministicKccWorld3D, DeterministicMovingPlatform3D, DeterministicSlope3D, DeterministicStep3D } from './movement3d.js';
|
|
64
|
+
export type { DeterministicKccBody3D, DeterministicKccCarrierPose3D, DeterministicKccCarryResult3D, DeterministicKccCarryState3D, DeterministicKccCollisionCandidate3D, DeterministicKccCollisionFilter3D, DeterministicKccExternalImpulse3D, DeterministicKccGeometry3D, DeterministicKccInput3D, DeterministicKccJumpTuning3D, DeterministicKccPhysicsImpulse3D, DeterministicKccProcessor3D, DeterministicKccStepResult3D, DeterministicKccWorld3D, DeterministicMovingPlatform3D, DeterministicSlope3D, DeterministicStep3D } from './movement3d.js';
|
|
64
65
|
export type { CookedDeterministicNavigationGraph, DeterministicFlowField, DeterministicNavEdge, DeterministicNavigationAgent, DeterministicNavigationGraphSource, DeterministicNavigationStepOptions, DeterministicNavigationStepResult, DeterministicNavNode, DeterministicNavRegion, DeterministicPathQuery, DeterministicPathResult, DeterministicTilemapSource } from './navigation.js';
|
|
65
66
|
export type { CookedDeterministicNavmesh, DeterministicNavmeshAgent, DeterministicNavmeshAgentStepResult, DeterministicNavmeshImportSource, DeterministicNavmeshLink, DeterministicNavmeshMovingAvoidanceObstacle, DeterministicNavmeshOffMeshTraversalState, DeterministicNavmeshOffMeshTraversalStep, DeterministicNavmeshPath, DeterministicNavmeshPoint, DeterministicNavmeshPolygon, DeterministicNavmeshSource } from './navmesh.js';
|
|
66
67
|
export type { ConfirmedSyncplayPresentationFrame, KinetixRuntimeFactory, NetworkedClientTransport, NetworkedSyncplayClient, NetworkedSyncplayClientInputDelayOptions, NetworkedSyncplayClientNetStats, NetworkedSyncplayClientOptions, NetworkedSyncplayClientPacingOptions, NetworkedSyncplayPreparationOptions, NetworkedSyncplaySessionDescriptor, NetworkedSyncplaySlotPresence, PreparedNetworkedSyncplayTransport, SyncplayPredictionMode } from './networked-client.js';
|
package/dist/browser.js
CHANGED
|
@@ -28,6 +28,7 @@ export { fbm2D, hash2, hashUint32, valueNoise1D, valueNoise2D } from './noise.js
|
|
|
28
28
|
export { circleCastDeterministicPhysics2D, createDeterministicPhysicsWorld2D, nearestDeterministicPhysicsHit2D, queryDeterministicPhysicsAabb2D, queryDeterministicPhysicsShapeOverlap2D, raycastAllDeterministicPhysics2D, raycastDeterministicPhysics2D, stepDeterministicPhysicsWorld2D } from './physics2d.js';
|
|
29
29
|
export { applyDeterministicImpulse3D, applyDeterministicImpulses3D, applyDeterministicWorldEdit3D, contactDetailsDeterministicPhysics3D, createDeterministicPhysicsWorld3D, createTrustedDeterministicPhysicsWorld3D, deriveInverseInertia, deterministicPhysicsBodyById3D, deterministicPhysicsCallback3D, overlapDeterministicPhysics3D, raycastDeterministicPhysics3D, raycastDeterministicPhysics3DDetailed, runDeterministicPhysics3DStress, setDeterministicBodyVelocity3D, shapeCastDeterministicPhysics3D, shapeCastDeterministicPhysics3DDetailed, stepDeterministicPhysicsWorld3D } from './physics3d.js';
|
|
30
30
|
export { cookDeterministicVoxelChunkCollider3D } from './physics3d-voxel.js';
|
|
31
|
+
export { cylinderHullVertices } from './physics3d-hull.js';
|
|
31
32
|
export { createDeterministicCosmeticPhysicsWorld3D, setDeterministicCosmeticPhysicsAuthoritativeBodyProxies3D, stepDeterministicCosmeticPhysicsWorld3D } from './cosmetic-physics3d.js';
|
|
32
33
|
export { stepDeterministicVehicle3D } from './physics3d-vehicle.js';
|
|
33
34
|
export { createDeterministicR3fRenderAdapter as createDeterministicR3fAdapter, createDeterministicR3fRenderAdapter, interpolateDeterministicRenderNumber, interpolateDeterministicRenderVec2 } from './r3f-render-adapter.js';
|
package/dist/creator.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export type { SyncplaySession, SyncplaySessionPolicy } from './sdk-session.js';
|
|
|
14
14
|
export type { InstantReplayInputFrame, InstantReplayResult, InstantReplayRunner, InstantReplayRunnerOptions, InstantReplayRunOptions, RunInstantReplayOptions, } from './instant-replay.js';
|
|
15
15
|
export { applyDeterministicImpulse3D, applyDeterministicImpulses3D, applyDeterministicWorldEdit3D, contactDetailsDeterministicPhysics3D, createDeterministicPhysicsWorld3D, deriveInverseInertia, deterministicPhysicsBodyById3D, deterministicPhysicsCallback3D, overlapDeterministicPhysics3D, raycastDeterministicPhysics3D, raycastDeterministicPhysics3DDetailed, runDeterministicPhysics3DStress, setDeterministicBodyVelocity3D, shapeCastDeterministicPhysics3D, shapeCastDeterministicPhysics3DDetailed, stepDeterministicPhysicsWorld3D, } from './physics3d.js';
|
|
16
16
|
export { cookDeterministicVoxelChunkCollider3D } from './physics3d-voxel.js';
|
|
17
|
+
export { cylinderHullVertices } from './physics3d-hull.js';
|
|
17
18
|
export { stepDeterministicVehicle3D } from './physics3d-vehicle.js';
|
|
18
19
|
export type { DeterministicContactCacheEntry3D, DeterministicJointCacheEntry3D, DeterministicPhysicsActivePair3D, DeterministicPhysicsAxisLock3D, DeterministicPhysicsBody3D, DeterministicPhysicsCallbackMask3D, DeterministicPhysicsCompoundChild3D, DeterministicPhysicsCompoundChildShape3D, DeterministicPhysicsConstraint3D, DeterministicPhysicsContactDetail3D, DeterministicPhysicsContactEvent3D, DeterministicPhysicsDetailedHit3D, DeterministicPhysicsHit3D, DeterministicPhysicsHullShape3D, DeterministicPhysicsImpulseApplication3D, DeterministicPhysicsOverlapQuery3D, DeterministicPhysicsPrimitiveShape3D, DeterministicPhysicsQuaternion3D, DeterministicPhysicsQueryOptions3D, DeterministicPhysicsRaycastQuery3D, DeterministicPhysicsScheduledQuery3D, DeterministicPhysicsShape3D, DeterministicPhysicsShapeCastQuery3D, DeterministicPhysicsStepResult3D, DeterministicPhysicsVec3, DeterministicPhysicsWorld3D, DeterministicPhysicsWorldEdit3D, } from './physics3d.js';
|
|
19
20
|
export type { DeterministicVoxelChunk3D, DeterministicVoxelCompoundShape3D } from './physics3d-voxel.js';
|
package/dist/creator.js
CHANGED
|
@@ -11,4 +11,5 @@ export { runSyncplaySynctest } from './synctest.js';
|
|
|
11
11
|
// stay off it — the first two are not requested, the third reads wall clock.
|
|
12
12
|
export { applyDeterministicImpulse3D, applyDeterministicImpulses3D, applyDeterministicWorldEdit3D, contactDetailsDeterministicPhysics3D, createDeterministicPhysicsWorld3D, deriveInverseInertia, deterministicPhysicsBodyById3D, deterministicPhysicsCallback3D, overlapDeterministicPhysics3D, raycastDeterministicPhysics3D, raycastDeterministicPhysics3DDetailed, runDeterministicPhysics3DStress, setDeterministicBodyVelocity3D, shapeCastDeterministicPhysics3D, shapeCastDeterministicPhysics3DDetailed, stepDeterministicPhysicsWorld3D, } from './physics3d.js';
|
|
13
13
|
export { cookDeterministicVoxelChunkCollider3D } from './physics3d-voxel.js';
|
|
14
|
+
export { cylinderHullVertices } from './physics3d-hull.js';
|
|
14
15
|
export { stepDeterministicVehicle3D } from './physics3d-vehicle.js';
|
package/dist/index.d.ts
CHANGED
|
@@ -30,6 +30,7 @@ export { fbm2D, hash2, hashUint32, valueNoise1D, valueNoise2D } from './noise.js
|
|
|
30
30
|
export { circleCastDeterministicPhysics2D, createDeterministicPhysicsWorld2D, nearestDeterministicPhysicsHit2D, queryDeterministicPhysicsAabb2D, queryDeterministicPhysicsShapeOverlap2D, raycastAllDeterministicPhysics2D, raycastDeterministicPhysics2D, stepDeterministicPhysicsWorld2D } from './physics2d.js';
|
|
31
31
|
export { applyDeterministicImpulse3D, applyDeterministicImpulses3D, applyDeterministicWorldEdit3D, contactDetailsDeterministicPhysics3D, createDeterministicPhysicsWorld3D, createTrustedDeterministicPhysicsWorld3D, deriveInverseInertia, deterministicPhysicsBodyById3D, deterministicPhysicsCallback3D, overlapDeterministicPhysics3D, raycastDeterministicPhysics3D, raycastDeterministicPhysics3DDetailed, runDeterministicPhysics3DStress, setDeterministicBodyVelocity3D, shapeCastDeterministicPhysics3D, shapeCastDeterministicPhysics3DDetailed, stepDeterministicPhysicsWorld3D } from './physics3d.js';
|
|
32
32
|
export { cookDeterministicVoxelChunkCollider3D } from './physics3d-voxel.js';
|
|
33
|
+
export { cylinderHullVertices } from './physics3d-hull.js';
|
|
33
34
|
export { runDeterministicPhysics3DDestructionCert } from './physics3d-destruction-cert.js';
|
|
34
35
|
export { createDeterministicCosmeticPhysicsWorld3D, setDeterministicCosmeticPhysicsAuthoritativeBodyProxies3D, stepDeterministicCosmeticPhysicsWorld3D } from './cosmetic-physics3d.js';
|
|
35
36
|
export { runDeterministicKickRecovery3DScenario, runDeterministicMassRatio3DScenario, runDeterministicPyramid3DScenario, runDeterministicStackingSuite3D, runDeterministicTower3DScenario } from './physics3d-stacking.js';
|
|
@@ -72,7 +73,7 @@ export type { DeterministicAuthoritativeLagCompensation3DShotContext, Determinis
|
|
|
72
73
|
export type { DeterministicLocalAuthoritySnapshotIdentity, DeterministicLocalAuthoritySnapshotTransfer, DeterministicLocalAuthorityWireEnvelope, DeterministicLocalAuthorityWireKind } from './local-authority-wire.js';
|
|
73
74
|
export type { DeterministicFixedQuaternion, DeterministicFixedVec2, DeterministicFixedVec3, DeterministicMath } from './math.js';
|
|
74
75
|
export type { DeterministicFpsAimState, DeterministicFpsInput, DeterministicKccBody, DeterministicKccInput, DeterministicKccStepResult, DeterministicMovementCrowdMetrics, DeterministicMovementSimulationMetrics, DeterministicMovementStep, DeterministicMovementWorld, DeterministicMovingPlatform, DeterministicOneWayPlatform, DeterministicSlope, DeterministicTargetSnapshot, DeterministicTopDownBody, DeterministicTopDownBounds, DeterministicTopDownInput } from './movement.js';
|
|
75
|
-
export type { DeterministicKccBody3D, DeterministicKccCarrierPose3D, DeterministicKccCarryResult3D, DeterministicKccCarryState3D, DeterministicKccCollisionCandidate3D, DeterministicKccCollisionFilter3D, DeterministicKccExternalImpulse3D, DeterministicKccGeometry3D, DeterministicKccInput3D, DeterministicKccPhysicsImpulse3D, DeterministicKccProcessor3D, DeterministicKccStepResult3D, DeterministicKccWorld3D, DeterministicMovingPlatform3D, DeterministicSlope3D, DeterministicStep3D } from './movement3d.js';
|
|
76
|
+
export type { DeterministicKccBody3D, DeterministicKccCarrierPose3D, DeterministicKccCarryResult3D, DeterministicKccCarryState3D, DeterministicKccCollisionCandidate3D, DeterministicKccCollisionFilter3D, DeterministicKccExternalImpulse3D, DeterministicKccGeometry3D, DeterministicKccInput3D, DeterministicKccJumpTuning3D, DeterministicKccPhysicsImpulse3D, DeterministicKccProcessor3D, DeterministicKccStepResult3D, DeterministicKccWorld3D, DeterministicMovingPlatform3D, DeterministicSlope3D, DeterministicStep3D } from './movement3d.js';
|
|
76
77
|
export type { CookedDeterministicNavigationGraph, DeterministicFlowField, DeterministicNavEdge, DeterministicNavigationAgent, DeterministicNavigationGraphSource, DeterministicNavigationStepOptions, DeterministicNavigationStepResult, DeterministicNavNode, DeterministicNavRegion, DeterministicPathQuery, DeterministicPathResult, DeterministicTilemapSource } from './navigation.js';
|
|
77
78
|
export type { CookedDeterministicNavmesh, DeterministicNavmeshAgent, DeterministicNavmeshAgentStepResult, DeterministicNavmeshImportSource, DeterministicNavmeshLink, DeterministicNavmeshMovingAvoidanceObstacle, DeterministicNavmeshOffMeshTraversalState, DeterministicNavmeshOffMeshTraversalStep, DeterministicNavmeshPath, DeterministicNavmeshPoint, DeterministicNavmeshPolygon, DeterministicNavmeshSource } from './navmesh.js';
|
|
78
79
|
export type { ConfirmedSyncplayPresentationFrame, KinetixRuntimeFactory, NetworkedClientTransport, NetworkedSyncplayClient, NetworkedSyncplayClientInputDelayOptions, NetworkedSyncplayClientNetStats, NetworkedSyncplayClientOptions, NetworkedSyncplayClientPacingOptions, NetworkedSyncplayPreparationOptions, NetworkedSyncplaySessionDescriptor, PreparedNetworkedSyncplayTransport, SyncplayPredictionMode } from './networked-client.js';
|
package/dist/index.js
CHANGED
|
@@ -31,6 +31,7 @@ export { fbm2D, hash2, hashUint32, valueNoise1D, valueNoise2D } from './noise.js
|
|
|
31
31
|
export { circleCastDeterministicPhysics2D, createDeterministicPhysicsWorld2D, nearestDeterministicPhysicsHit2D, queryDeterministicPhysicsAabb2D, queryDeterministicPhysicsShapeOverlap2D, raycastAllDeterministicPhysics2D, raycastDeterministicPhysics2D, stepDeterministicPhysicsWorld2D } from './physics2d.js';
|
|
32
32
|
export { applyDeterministicImpulse3D, applyDeterministicImpulses3D, applyDeterministicWorldEdit3D, contactDetailsDeterministicPhysics3D, createDeterministicPhysicsWorld3D, createTrustedDeterministicPhysicsWorld3D, deriveInverseInertia, deterministicPhysicsBodyById3D, deterministicPhysicsCallback3D, overlapDeterministicPhysics3D, raycastDeterministicPhysics3D, raycastDeterministicPhysics3DDetailed, runDeterministicPhysics3DStress, setDeterministicBodyVelocity3D, shapeCastDeterministicPhysics3D, shapeCastDeterministicPhysics3DDetailed, stepDeterministicPhysicsWorld3D } from './physics3d.js';
|
|
33
33
|
export { cookDeterministicVoxelChunkCollider3D } from './physics3d-voxel.js';
|
|
34
|
+
export { cylinderHullVertices } from './physics3d-hull.js';
|
|
34
35
|
export { runDeterministicPhysics3DDestructionCert } from './physics3d-destruction-cert.js';
|
|
35
36
|
export { createDeterministicCosmeticPhysicsWorld3D, setDeterministicCosmeticPhysicsAuthoritativeBodyProxies3D, stepDeterministicCosmeticPhysicsWorld3D } from './cosmetic-physics3d.js';
|
|
36
37
|
export { runDeterministicKickRecovery3DScenario, runDeterministicMassRatio3DScenario, runDeterministicPyramid3DScenario, runDeterministicStackingSuite3D, runDeterministicTower3DScenario } from './physics3d-stacking.js';
|
package/dist/movement3d.d.ts
CHANGED
|
@@ -24,6 +24,20 @@ export interface DeterministicKccInput3D {
|
|
|
24
24
|
readonly moveZ: number;
|
|
25
25
|
readonly jumpPressed?: boolean;
|
|
26
26
|
}
|
|
27
|
+
export interface DeterministicKccJumpTuning3D {
|
|
28
|
+
/** Upward velocity the jump sets, in units per frame. Default 10. */
|
|
29
|
+
readonly impulse?: number;
|
|
30
|
+
/**
|
|
31
|
+
* Immediate lift applied on the jump frame to unstick the capsule from the
|
|
32
|
+
* ground it is resting on. Defaults to `impulse / 10`, which keeps the
|
|
33
|
+
* historical 1-unit lift at the default impulse and scales with a retune.
|
|
34
|
+
*/
|
|
35
|
+
readonly liftOff?: number;
|
|
36
|
+
/** Frames a jump stays available after leaving the ground. Default 4. */
|
|
37
|
+
readonly coyoteFrames?: number;
|
|
38
|
+
/** Frames an early jump press is held for. Minimum 1, default 4. */
|
|
39
|
+
readonly bufferFrames?: number;
|
|
40
|
+
}
|
|
27
41
|
export interface DeterministicKccExternalImpulse3D {
|
|
28
42
|
readonly id: string;
|
|
29
43
|
readonly vx: number;
|
|
@@ -189,6 +203,23 @@ export declare function stepDeterministicKcc3D(body: DeterministicKccBody3D, inp
|
|
|
189
203
|
readonly maxPushImpulse?: number;
|
|
190
204
|
/** Enable standing on (and inheriting the velocity of) dynamic physics bodies. */
|
|
191
205
|
readonly standOnDynamicBodies?: boolean;
|
|
206
|
+
/**
|
|
207
|
+
* Ground the capsule on STATIC physics bodies, including heightfields.
|
|
208
|
+
* Off by default: without it the physics world only blocks horizontally and
|
|
209
|
+
* vertical support comes from `world.slopes` / `steps` / platforms / `minY`,
|
|
210
|
+
* so a streaming-terrain game had to rebuild slope geometry to stand on the
|
|
211
|
+
* same terrain it already streams as colliders. Opt-in so shipped games keep
|
|
212
|
+
* bit-identical checksums.
|
|
213
|
+
*/
|
|
214
|
+
readonly groundOnStaticBodies?: boolean;
|
|
215
|
+
/**
|
|
216
|
+
* Jump tuning, in the KCC's frame units. Gravity retunes through a processor
|
|
217
|
+
* because processors run before `vy -= 1`, but the jump impulse is applied
|
|
218
|
+
* AFTER them, so it cannot be scaled the same way — without these a game that
|
|
219
|
+
* retunes gravity must own jump, coyote and buffer itself. Every default
|
|
220
|
+
* reproduces the previous hardcoded behaviour bit-for-bit.
|
|
221
|
+
*/
|
|
222
|
+
readonly jump?: DeterministicKccJumpTuning3D;
|
|
192
223
|
}): DeterministicKccStepResult3D;
|
|
193
224
|
export interface DeterministicKccCarryState3D {
|
|
194
225
|
readonly bodyId: string;
|
package/dist/movement3d.js
CHANGED
|
@@ -1,7 +1,39 @@
|
|
|
1
1
|
import { defaultChecksum } from './canonical.js';
|
|
2
2
|
import { createDeterministicPhysicsWorld3D, overlapDeterministicPhysics3D, } from './physics3d.js';
|
|
3
|
-
import {
|
|
3
|
+
import { heightfieldLocalSurfaceY } from './physics3d-heightfield.js';
|
|
4
|
+
import { mulMat3Vec, quaternionFromQuarterTurns, quatMultiply, quatToMat3, rotatePointByQuat, solverSqrt } from './physics3d-shared.js';
|
|
5
|
+
function normalizeKccJumpTuning3D(tuning) {
|
|
6
|
+
const impulse = finiteKccJumpNumber(tuning?.impulse, 10, 'impulse');
|
|
7
|
+
return {
|
|
8
|
+
impulse,
|
|
9
|
+
liftOff: finiteKccJumpNumber(tuning?.liftOff, impulse / 10, 'liftOff'),
|
|
10
|
+
coyoteFrames: kccJumpFrameCount(tuning?.coyoteFrames, 'coyoteFrames', 0),
|
|
11
|
+
// A press arms the jump by writing this into jumpBufferFrames and the gate
|
|
12
|
+
// needs it > 0, so 0 would not mean "no buffering" — it would mean "never
|
|
13
|
+
// jump". 1 is the real floor: the press lives for the frame it happened on.
|
|
14
|
+
bufferFrames: kccJumpFrameCount(tuning?.bufferFrames, 'bufferFrames', 1),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function finiteKccJumpNumber(value, fallback, field) {
|
|
18
|
+
if (value === undefined) {
|
|
19
|
+
return fallback;
|
|
20
|
+
}
|
|
21
|
+
if (!Number.isFinite(value)) {
|
|
22
|
+
throw new Error(`KCC jump.${field} must be a finite number`);
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
function kccJumpFrameCount(value, field, minimum) {
|
|
27
|
+
if (value === undefined) {
|
|
28
|
+
return 4;
|
|
29
|
+
}
|
|
30
|
+
if (!Number.isInteger(value) || value < minimum) {
|
|
31
|
+
throw new Error(`KCC jump.${field} must be an integer >= ${minimum}, got ${value}`);
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
4
35
|
export function stepDeterministicKcc3D(body, input, world, options) {
|
|
36
|
+
const jump = normalizeKccJumpTuning3D(options.jump);
|
|
5
37
|
let next = applyProcessors(body, input, options.frame, options.processors ?? []);
|
|
6
38
|
const events = [];
|
|
7
39
|
let correctionCount = 0;
|
|
@@ -9,8 +41,8 @@ export function stepDeterministicKcc3D(body, input, world, options) {
|
|
|
9
41
|
let collisionFilterCalls = 0;
|
|
10
42
|
let collisionFilterRejectedHits = 0;
|
|
11
43
|
const externalImpulse = combineExternalKccImpulses(options.externalImpulses ?? []);
|
|
12
|
-
let coyoteFrames = next.grounded ?
|
|
13
|
-
let jumpBufferFrames = input.jumpPressed === true ?
|
|
44
|
+
let coyoteFrames = next.grounded ? jump.coyoteFrames : Math.max(0, next.coyoteFrames - 1);
|
|
45
|
+
let jumpBufferFrames = input.jumpPressed === true ? jump.bufferFrames : Math.max(0, next.jumpBufferFrames - 1);
|
|
14
46
|
let vx = input.moveX + externalImpulse.vx;
|
|
15
47
|
let vz = input.moveZ + externalImpulse.vz;
|
|
16
48
|
let vy = next.vy - 1 + externalImpulse.vy;
|
|
@@ -19,6 +51,7 @@ export function stepDeterministicKcc3D(body, input, world, options) {
|
|
|
19
51
|
let z = next.z + vz;
|
|
20
52
|
let grounded = false;
|
|
21
53
|
let platformId = next.platformId;
|
|
54
|
+
let platformClaimedThisFrame = false;
|
|
22
55
|
if (externalImpulse.applied) {
|
|
23
56
|
events.push('external-impulse');
|
|
24
57
|
}
|
|
@@ -36,6 +69,7 @@ export function stepDeterministicKcc3D(body, input, world, options) {
|
|
|
36
69
|
vy = 0;
|
|
37
70
|
grounded = true;
|
|
38
71
|
platformId = platform.id;
|
|
72
|
+
platformClaimedThisFrame = true;
|
|
39
73
|
events.push('moving-platform');
|
|
40
74
|
}
|
|
41
75
|
}
|
|
@@ -67,14 +101,66 @@ export function stepDeterministicKcc3D(body, input, world, options) {
|
|
|
67
101
|
}
|
|
68
102
|
}
|
|
69
103
|
if (jumpBufferFrames > 0 && (next.grounded || next.coyoteFrames > 0 || grounded)) {
|
|
70
|
-
vy =
|
|
71
|
-
y +=
|
|
104
|
+
vy = jump.impulse;
|
|
105
|
+
y += jump.liftOff;
|
|
72
106
|
grounded = false;
|
|
73
107
|
platformId = undefined;
|
|
74
108
|
jumpBufferFrames = 0;
|
|
75
109
|
coyoteFrames = 0;
|
|
76
110
|
events.push(next.grounded ? 'jump' : 'coyote-jump');
|
|
77
111
|
}
|
|
112
|
+
const previousFeetY = next.y - kccFeetOffset(geometry);
|
|
113
|
+
let groundedOnStatic = false;
|
|
114
|
+
let groundedOnDynamic = false;
|
|
115
|
+
// Resolved before the dynamic path so the higher of the two wins: a crate
|
|
116
|
+
// standing on a floor must not lose its platform to the floor underneath it.
|
|
117
|
+
const staticGround = options.physics !== undefined && options.groundOnStaticBodies === true && vy <= 0
|
|
118
|
+
? staticGroundUnderKcc(options.physics, x, y, z, previousFeetY, geometry, new Set(options.ignoredPhysicsBodyIds ?? []), world.stepHeight)
|
|
119
|
+
: undefined;
|
|
120
|
+
if (options.physics !== undefined && options.standOnDynamicBodies === true && vy <= 0) {
|
|
121
|
+
const ground = dynamicGroundUnderKcc(options.physics, x, y, z, geometry, new Set(options.ignoredPhysicsBodyIds ?? []));
|
|
122
|
+
if (ground !== undefined && (staticGround === undefined || ground.topY >= staticGround.topY)) {
|
|
123
|
+
groundedOnDynamic = true;
|
|
124
|
+
platformClaimedThisFrame = true;
|
|
125
|
+
y = ground.topY;
|
|
126
|
+
vy = 0;
|
|
127
|
+
grounded = true;
|
|
128
|
+
platformId = ground.bodyId;
|
|
129
|
+
// Carry the rider with the crate this frame (position delta) AND inherit its
|
|
130
|
+
// velocity, mirroring the kinematic moving-platform path — otherwise the
|
|
131
|
+
// rider lags one frame and slides off a moving crate.
|
|
132
|
+
x += ground.deltaX;
|
|
133
|
+
z += ground.deltaZ;
|
|
134
|
+
vx += ground.deltaX;
|
|
135
|
+
vz += ground.deltaZ;
|
|
136
|
+
events.push('stand-on-dynamic');
|
|
137
|
+
if (ground.deltaX !== 0 || ground.deltaZ !== 0) {
|
|
138
|
+
events.push('platform-velocity-transfer');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Applied BEFORE the horizontal slide: the slide resolves overlap horizontally
|
|
143
|
+
// only, so a capsule that fell into terrain would be shoved sideways across
|
|
144
|
+
// the surface instead of being stood on top of it. Skipped when a dynamic
|
|
145
|
+
// body already claimed a higher surface this frame.
|
|
146
|
+
// platformId carries over from the previous frame, so a rider that simply
|
|
147
|
+
// walked off keeps pointing at a platform it has left. Clear it once nothing
|
|
148
|
+
// has claimed it this frame.
|
|
149
|
+
if (options.standOnDynamicBodies === true && !platformClaimedThisFrame) {
|
|
150
|
+
platformId = undefined;
|
|
151
|
+
}
|
|
152
|
+
// `grounded` here means an earlier source (moving platform, slope, step, or a
|
|
153
|
+
// dynamic body) already claimed a surface this frame; static only wins if it
|
|
154
|
+
// is higher. platformId cannot be used for this — it carries over from the
|
|
155
|
+
// previous frame, so stepping off a crate onto terrain would skip grounding.
|
|
156
|
+
if (staticGround !== undefined && (!grounded || staticGround.topY > y)) {
|
|
157
|
+
platformId = undefined;
|
|
158
|
+
y = staticGround.topY;
|
|
159
|
+
vy = 0;
|
|
160
|
+
grounded = true;
|
|
161
|
+
groundedOnStatic = true;
|
|
162
|
+
events.push('static-ground');
|
|
163
|
+
}
|
|
78
164
|
// The KCC's intended (pre-slide) horizontal position and velocity — push
|
|
79
165
|
// impulses are computed here because the slide blocks the KCC off the body.
|
|
80
166
|
const pushCandidate = { x, z, vx, vz };
|
|
@@ -90,32 +176,41 @@ export function stepDeterministicKcc3D(body, input, world, options) {
|
|
|
90
176
|
vz = resolved.vz;
|
|
91
177
|
correctionCount += resolved.correctionCount;
|
|
92
178
|
events.push(resolved.swept ? 'physics-sweep' : resolved.slid ? 'physics-slide' : 'physics-correction');
|
|
179
|
+
// The slide moved the footprint, so the height sampled before it belongs
|
|
180
|
+
// to the old x/z. On sloped ground that stale height reads as penetration
|
|
181
|
+
// next frame and freezes movement along walls and ledges — re-sample.
|
|
182
|
+
// Attachment must follow the FINAL footprint. A slide can carry the
|
|
183
|
+
// capsule off the platform it was standing on (leaving it "grounded" in
|
|
184
|
+
// mid-air) or onto one it had missed.
|
|
185
|
+
if (groundedOnDynamic && (x !== pushCandidate.x || z !== pushCandidate.z)) {
|
|
186
|
+
const reattached = dynamicGroundUnderKcc(options.physics, x, y, z, geometry, new Set(options.ignoredPhysicsBodyIds ?? []));
|
|
187
|
+
if (reattached === undefined) {
|
|
188
|
+
grounded = false;
|
|
189
|
+
platformId = undefined;
|
|
190
|
+
groundedOnDynamic = false;
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
y = reattached.topY;
|
|
194
|
+
platformId = reattached.bodyId;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (groundedOnStatic && (x !== pushCandidate.x || z !== pushCandidate.z)) {
|
|
198
|
+
const reground = staticGroundUnderKcc(options.physics, x, y, z,
|
|
199
|
+
// The capsule has already landed, so this is a local correction from
|
|
200
|
+
// the landed feet — not the start-of-frame ones. Reusing those would
|
|
201
|
+
// reopen the fall-crossing window and let a slide under an overhang
|
|
202
|
+
// yank the capsule onto its roof.
|
|
203
|
+
y - kccFeetOffset(geometry), geometry, new Set(options.ignoredPhysicsBodyIds ?? []), world.stepHeight);
|
|
204
|
+
if (reground !== undefined) {
|
|
205
|
+
y = reground.topY;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
93
208
|
}
|
|
94
209
|
}
|
|
95
210
|
const geometryForInteraction = normalizeKccGeometry3D(next);
|
|
96
211
|
let physicsImpulses = [];
|
|
97
212
|
// Stand on a dynamic body directly beneath the feet: attach as a platform and
|
|
98
213
|
// inherit its per-frame delta (its velocity), mirroring the kinematic path.
|
|
99
|
-
if (options.physics !== undefined && options.standOnDynamicBodies === true && vy <= 0) {
|
|
100
|
-
const ground = dynamicGroundUnderKcc(options.physics, x, y, z, geometryForInteraction, new Set(options.ignoredPhysicsBodyIds ?? []));
|
|
101
|
-
if (ground !== undefined) {
|
|
102
|
-
y = ground.topY;
|
|
103
|
-
vy = 0;
|
|
104
|
-
grounded = true;
|
|
105
|
-
platformId = ground.bodyId;
|
|
106
|
-
// Carry the rider with the crate this frame (position delta) AND inherit its
|
|
107
|
-
// velocity, mirroring the kinematic moving-platform path — otherwise the
|
|
108
|
-
// rider lags one frame and slides off a moving crate.
|
|
109
|
-
x += ground.deltaX;
|
|
110
|
-
z += ground.deltaZ;
|
|
111
|
-
vx += ground.deltaX;
|
|
112
|
-
vz += ground.deltaZ;
|
|
113
|
-
events.push('stand-on-dynamic');
|
|
114
|
-
if (ground.deltaX !== 0 || ground.deltaZ !== 0) {
|
|
115
|
-
events.push('platform-velocity-transfer');
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
214
|
// Push the dynamic bodies the KCC walked into this frame.
|
|
120
215
|
if (options.physics !== undefined && (options.maxPushImpulse ?? 0) > 0) {
|
|
121
216
|
physicsImpulses = computeKccPushImpulses(options.physics, pushCandidate.x, y, pushCandidate.z, { vx: pushCandidate.vx, vz: pushCandidate.vz }, geometryForInteraction, options.maxPushImpulse ?? 0, new Set(options.ignoredPhysicsBodyIds ?? []));
|
|
@@ -123,7 +218,10 @@ export function stepDeterministicKcc3D(body, input, world, options) {
|
|
|
123
218
|
events.push('push-dynamic');
|
|
124
219
|
}
|
|
125
220
|
}
|
|
126
|
-
|
|
221
|
+
// Spread `next` first: the capsule's radius/halfHeight/skinWidth live on the
|
|
222
|
+
// body, and rebuilding it from a bare literal dropped them, so a caller's
|
|
223
|
+
// capsule silently reverted to the 1x2 default after a single step.
|
|
224
|
+
const boundsCorrection = clampBodyToBounds({ ...next, x, y, z, vx, vy, vz, grounded, coyoteFrames, jumpBufferFrames, platformId }, world);
|
|
127
225
|
if (boundsCorrection.corrected) {
|
|
128
226
|
correctionCount += boundsCorrection.corrections;
|
|
129
227
|
events.push(...boundsCorrection.events);
|
|
@@ -131,7 +229,7 @@ export function stepDeterministicKcc3D(body, input, world, options) {
|
|
|
131
229
|
next = boundsCorrection.body;
|
|
132
230
|
const finalBody = {
|
|
133
231
|
...next,
|
|
134
|
-
coyoteFrames: next.grounded ?
|
|
232
|
+
coyoteFrames: next.grounded ? jump.coyoteFrames : coyoteFrames,
|
|
135
233
|
jumpBufferFrames,
|
|
136
234
|
};
|
|
137
235
|
return {
|
|
@@ -247,7 +345,144 @@ function groundContactTopAt(body, x, z) {
|
|
|
247
345
|
}
|
|
248
346
|
return top;
|
|
249
347
|
}
|
|
250
|
-
|
|
348
|
+
if (shape.type === 'hull') {
|
|
349
|
+
// Not primitiveHalfExtent: that returns symmetric max(|y|) half-extents, so
|
|
350
|
+
// an asymmetric hull would snap the capsule above its real top and treat
|
|
351
|
+
// the mirrored side of the AABB as walkable. The stand-on height is max(y)
|
|
352
|
+
// of the rotated vertices, and the footprint is their true XZ range.
|
|
353
|
+
const m = quatToMat3(q);
|
|
354
|
+
let minX = Infinity;
|
|
355
|
+
let maxX = -Infinity;
|
|
356
|
+
let minZ = Infinity;
|
|
357
|
+
let maxZ = -Infinity;
|
|
358
|
+
let maxY = -Infinity;
|
|
359
|
+
for (const vertex of shape.vertices) {
|
|
360
|
+
const rotated = mulMat3Vec(m, vertex);
|
|
361
|
+
minX = Math.min(minX, rotated.x);
|
|
362
|
+
maxX = Math.max(maxX, rotated.x);
|
|
363
|
+
minZ = Math.min(minZ, rotated.z);
|
|
364
|
+
maxZ = Math.max(maxZ, rotated.z);
|
|
365
|
+
maxY = Math.max(maxY, rotated.y);
|
|
366
|
+
}
|
|
367
|
+
const localX = x - body.x;
|
|
368
|
+
const localZ = z - body.z;
|
|
369
|
+
if (localX < minX || localX > maxX || localZ < minZ || localZ > maxZ) {
|
|
370
|
+
return undefined;
|
|
371
|
+
}
|
|
372
|
+
return body.y + maxY;
|
|
373
|
+
}
|
|
374
|
+
if (shape.type === 'heightfield') {
|
|
375
|
+
// Heightfields are static and never rotate (the solver maps them to a mesh
|
|
376
|
+
// in body-local space), so local == world minus the body position.
|
|
377
|
+
const localY = heightfieldLocalSurfaceY(shape, x - body.x, z - body.z);
|
|
378
|
+
return localY === undefined ? undefined : body.y + localY;
|
|
379
|
+
}
|
|
380
|
+
return undefined; // mesh: arbitrary triangle soup, not a stand-on platform
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Highest static surface under the capsule within `snapDistance` of its feet.
|
|
384
|
+
*
|
|
385
|
+
* The overlap probe only narrows candidates (it rides the world's static query
|
|
386
|
+
* tree); the surface height itself comes from groundContactTopAt, so a capsule
|
|
387
|
+
* lands on the same triangle a projectile would hit.
|
|
388
|
+
*/
|
|
389
|
+
/**
|
|
390
|
+
* Separation held between a grounded capsule and the surface under it, so
|
|
391
|
+
* resting contact never reads as penetration. Fixed rather than derived because
|
|
392
|
+
* skinWidth defaults to 0; a plain double is bit-identical on every peer.
|
|
393
|
+
*/
|
|
394
|
+
const KCC_GROUND_CLEARANCE = 1e-4;
|
|
395
|
+
/**
|
|
396
|
+
* Extra vertical lift a round capsule bottom needs to rest ON a sloped surface
|
|
397
|
+
* instead of IN it.
|
|
398
|
+
*
|
|
399
|
+
* groundContactTopAt reports the height under the capsule CENTRE, but the
|
|
400
|
+
* bottom is a sphere, so on gradient `g` the contact sits uphill and a bare
|
|
401
|
+
* radius of lift buries the uphill flank by `radius * (sec θ - 1)`. Past ~1.7°
|
|
402
|
+
* that exceeds KCC_GROUND_CLEARANCE, every overlap probe (full, x-only AND
|
|
403
|
+
* z-only) hits, and resolvePhysicsSlide3D reverts the whole horizontal move.
|
|
404
|
+
*
|
|
405
|
+
* Sampled a bottom-sphere radius out: that is the footprint the sphere can
|
|
406
|
+
* touch, it reads a plane exactly at any angle, and it self-bounds the
|
|
407
|
+
* discontinuities groundContactTopAt has on some shapes (a compound's children
|
|
408
|
+
* can butt together at a height jump) — a jump of D reads as D/radius here
|
|
409
|
+
* instead of the skyward fling an epsilon probe would give. Per axis it takes
|
|
410
|
+
* the steeper of forward/backward so a sample straddling an edge cannot
|
|
411
|
+
* average its way to an under-lift and back into the freeze.
|
|
412
|
+
*/
|
|
413
|
+
function kccSlopeClearance(body, x, z, top, geometry) {
|
|
414
|
+
const step = geometry.radius + geometry.skinWidth;
|
|
415
|
+
const axisGradient = (deltaX, deltaZ) => {
|
|
416
|
+
const ahead = groundContactTopAt(body, x + deltaX, z + deltaZ);
|
|
417
|
+
const behind = groundContactTopAt(body, x - deltaX, z - deltaZ);
|
|
418
|
+
const forward = ahead === undefined ? 0 : Math.abs(ahead - top) / step;
|
|
419
|
+
const backward = behind === undefined ? 0 : Math.abs(top - behind) / step;
|
|
420
|
+
return Math.max(forward, backward);
|
|
421
|
+
};
|
|
422
|
+
const gradientX = axisGradient(step, 0);
|
|
423
|
+
const gradientZ = axisGradient(0, step);
|
|
424
|
+
const gradientSq = gradientX * gradientX + gradientZ * gradientZ;
|
|
425
|
+
if (gradientSq <= 0) {
|
|
426
|
+
return 0;
|
|
427
|
+
}
|
|
428
|
+
// radius + skinWidth is the bottom-sphere radius physicsOverlapsKcc probes with.
|
|
429
|
+
return (geometry.radius + geometry.skinWidth) * (solverSqrt(1 + gradientSq) - 1);
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Distance from the capsule's centre to the bottom of its collision shape.
|
|
433
|
+
* A capsule's vertical extent is halfHeight + radius (see primitiveHalfExtent),
|
|
434
|
+
* so grounding on `halfHeight` alone buries it by a radius and the horizontal
|
|
435
|
+
* slide then fights the penetration every frame — the capsule cannot walk.
|
|
436
|
+
* skinWidth keeps a gap so resting contact is not treated as overlap.
|
|
437
|
+
*/
|
|
438
|
+
function kccFeetOffset(geometry) {
|
|
439
|
+
// physicsOverlapsKcc probes with radius+skinWidth AND halfHeight+skinWidth,
|
|
440
|
+
// and counts exact touching as an overlap — at which point the slide reverts
|
|
441
|
+
// the whole horizontal move, so a capsule resting flush on terrain cannot
|
|
442
|
+
// walk. Rest one clearance above that probe instead of on the surface.
|
|
443
|
+
return geometry.halfHeight + geometry.radius + 2 * geometry.skinWidth + KCC_GROUND_CLEARANCE;
|
|
444
|
+
}
|
|
445
|
+
function staticGroundUnderKcc(physics, x, y, z, previousFeetY, geometry, ignoredBodyIds, snapDistance) {
|
|
446
|
+
const feetY = y - kccFeetOffset(geometry);
|
|
447
|
+
// Gravity is unbounded (vy -= 1 per frame), so a fast fall can clear the whole
|
|
448
|
+
// snap band in one step. Accept a surface the feet CROSSED this frame, not
|
|
449
|
+
// just one they ended near — otherwise terrain tunnels at terminal velocity.
|
|
450
|
+
// Requiring the frame to START at or above the surface is what keeps a capsule
|
|
451
|
+
// walking beneath a floating static box from being yanked onto its roof.
|
|
452
|
+
const fallDistance = Math.max(0, previousFeetY - feetY);
|
|
453
|
+
const reach = snapDistance + fallDistance;
|
|
454
|
+
const probe = {
|
|
455
|
+
shape: { type: 'sphere', radius: geometry.radius + reach },
|
|
456
|
+
x,
|
|
457
|
+
y: feetY,
|
|
458
|
+
z,
|
|
459
|
+
hitStatics: true,
|
|
460
|
+
hitDynamics: false,
|
|
461
|
+
hitKinematics: false,
|
|
462
|
+
};
|
|
463
|
+
const hitIds = new Set(overlapDeterministicPhysics3D(physics, probe));
|
|
464
|
+
const grounds = physics.bodies
|
|
465
|
+
.filter((body) => body.kind === 'static' && hitIds.has(body.id) && !ignoredBodyIds.has(body.id))
|
|
466
|
+
.map((body) => ({ body, top: groundContactTopAt(body, x, z) }))
|
|
467
|
+
.filter((candidate) => candidate.top !== undefined)
|
|
468
|
+
.map((candidate) => ({
|
|
469
|
+
...candidate,
|
|
470
|
+
lift: kccSlopeClearance(candidate.body, x, z, candidate.top, geometry),
|
|
471
|
+
}))
|
|
472
|
+
// The band is measured from where the capsule actually STANDS, so a slope
|
|
473
|
+
// lift bigger than snapDistance cannot drop it out of its own ground the
|
|
474
|
+
// next frame and leave it cycling between grounded and falling.
|
|
475
|
+
.filter((candidate) => feetY <= candidate.top + candidate.lift + snapDistance
|
|
476
|
+
&& previousFeetY >= candidate.top - snapDistance)
|
|
477
|
+
.sort((left, right) => right.top - left.top || compareStrings(left.body.id, right.body.id));
|
|
478
|
+
const ground = grounds[0];
|
|
479
|
+
if (ground === undefined) {
|
|
480
|
+
return undefined;
|
|
481
|
+
}
|
|
482
|
+
return {
|
|
483
|
+
bodyId: ground.body.id,
|
|
484
|
+
topY: ground.top + kccFeetOffset(geometry) + ground.lift,
|
|
485
|
+
};
|
|
251
486
|
}
|
|
252
487
|
/** World half-extents (x,y,z) of a primitive shape under rotation matrix `m`. */
|
|
253
488
|
function primitiveHalfExtent(shape, m) {
|
|
@@ -283,7 +518,7 @@ function primitiveHalfExtent(shape, m) {
|
|
|
283
518
|
}
|
|
284
519
|
/** A dynamic body directly under the KCC feet, with its per-frame delta. */
|
|
285
520
|
function dynamicGroundUnderKcc(physics, x, y, z, geometry, ignoredBodyIds) {
|
|
286
|
-
const feetY = y - geometry
|
|
521
|
+
const feetY = y - kccFeetOffset(geometry);
|
|
287
522
|
const probe = {
|
|
288
523
|
shape: { type: 'sphere', radius: geometry.radius },
|
|
289
524
|
x,
|
|
@@ -308,7 +543,7 @@ function dynamicGroundUnderKcc(physics, x, y, z, geometry, ignoredBodyIds) {
|
|
|
308
543
|
}
|
|
309
544
|
return {
|
|
310
545
|
bodyId: ground.body.id,
|
|
311
|
-
topY: ground.top + geometry
|
|
546
|
+
topY: ground.top + kccFeetOffset(geometry),
|
|
312
547
|
deltaX: ground.body.vx,
|
|
313
548
|
deltaZ: ground.body.vz,
|
|
314
549
|
};
|
|
@@ -14,4 +14,14 @@ export interface HeightfieldBounds3D {
|
|
|
14
14
|
readonly maxZ: number;
|
|
15
15
|
}
|
|
16
16
|
export declare function heightfieldLocalBounds(shape: HeightfieldShapeData3D): HeightfieldBounds3D;
|
|
17
|
+
/**
|
|
18
|
+
* Local surface height at a local (x, z), or undefined outside the field.
|
|
19
|
+
*
|
|
20
|
+
* MUST stay consistent with heightfieldLocalTriangles' cell split: a cell emits
|
|
21
|
+
* [a,b,c] and [d,c,b] with a=(col,row) b=(col,row+1) c=(col+1,row)
|
|
22
|
+
* d=(col+1,row+1), so the diagonal runs b–c and the first triangle is the
|
|
23
|
+
* u+v<=1 half. Sampling a different diagonal would let a character stand where
|
|
24
|
+
* a projectile passes through.
|
|
25
|
+
*/
|
|
26
|
+
export declare function heightfieldLocalSurfaceY(shape: HeightfieldShapeData3D, localX: number, localZ: number): number | undefined;
|
|
17
27
|
export declare function heightfieldLocalTriangles(shape: HeightfieldShapeData3D, bounds?: HeightfieldBounds3D): readonly (readonly [Point3D, Point3D, Point3D])[];
|
|
@@ -21,6 +21,39 @@ function vertex(shape, column, row) {
|
|
|
21
21
|
z: row * shape.scale.z,
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
|
+
function heightAt(shape, column, row) {
|
|
25
|
+
return shape.heights[row * shape.columns + column] * shape.scale.y;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Local surface height at a local (x, z), or undefined outside the field.
|
|
29
|
+
*
|
|
30
|
+
* MUST stay consistent with heightfieldLocalTriangles' cell split: a cell emits
|
|
31
|
+
* [a,b,c] and [d,c,b] with a=(col,row) b=(col,row+1) c=(col+1,row)
|
|
32
|
+
* d=(col+1,row+1), so the diagonal runs b–c and the first triangle is the
|
|
33
|
+
* u+v<=1 half. Sampling a different diagonal would let a character stand where
|
|
34
|
+
* a projectile passes through.
|
|
35
|
+
*/
|
|
36
|
+
export function heightfieldLocalSurfaceY(shape, localX, localZ) {
|
|
37
|
+
const maxX = (shape.columns - 1) * shape.scale.x;
|
|
38
|
+
const maxZ = (shape.rows - 1) * shape.scale.z;
|
|
39
|
+
if (localX < 0 || localX > maxX || localZ < 0 || localZ > maxZ) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
const columnFloat = localX / shape.scale.x;
|
|
43
|
+
const rowFloat = localZ / shape.scale.z;
|
|
44
|
+
const column = Math.min(shape.columns - 2, Math.floor(columnFloat));
|
|
45
|
+
const row = Math.min(shape.rows - 2, Math.floor(rowFloat));
|
|
46
|
+
const u = columnFloat - column;
|
|
47
|
+
const v = rowFloat - row;
|
|
48
|
+
const a = heightAt(shape, column, row);
|
|
49
|
+
const b = heightAt(shape, column, row + 1);
|
|
50
|
+
const c = heightAt(shape, column + 1, row);
|
|
51
|
+
if (u + v <= 1) {
|
|
52
|
+
return a + (b - a) * v + (c - a) * u;
|
|
53
|
+
}
|
|
54
|
+
const d = heightAt(shape, column + 1, row + 1);
|
|
55
|
+
return d + (c - d) * (1 - v) + (b - d) * (1 - u);
|
|
56
|
+
}
|
|
24
57
|
export function heightfieldLocalTriangles(shape, bounds) {
|
|
25
58
|
const minColumn = bounds === undefined ? 0 : Math.max(0, Math.floor(bounds.minX / shape.scale.x));
|
|
26
59
|
const maxColumn = bounds === undefined
|
package/dist/physics3d-hull.d.ts
CHANGED
|
@@ -51,3 +51,17 @@ export declare function closestPointOnHullLocal(point: Point3D, hull: SolverHull
|
|
|
51
51
|
readonly normal: Point3D;
|
|
52
52
|
readonly distance: number;
|
|
53
53
|
};
|
|
54
|
+
/**
|
|
55
|
+
* Vertices for a cylinder approximated as a regular prism, for trunks, stumps,
|
|
56
|
+
* logs, pillars and barrels.
|
|
57
|
+
*
|
|
58
|
+
* There is no cylinder primitive in Kinetix, the TypeScript solver, or the
|
|
59
|
+
* Box3D parity set — a cylinder is a convex hull, and hulls are at parity. The
|
|
60
|
+
* TypeScript hull builder accepts 4-16 vertices, so `sides` tops out at 8 (an
|
|
61
|
+
* octagonal prism, exactly 16). A capsule is cheaper but its rounded caps let a
|
|
62
|
+
* character slide off a flat top and change how a body rolls off an edge.
|
|
63
|
+
*
|
|
64
|
+
* Call this when building the world, not inside the step: the vertices are
|
|
65
|
+
* constant for a given (radius, halfHeight, sides).
|
|
66
|
+
*/
|
|
67
|
+
export declare function cylinderHullVertices(radius: number, halfHeight: number, sides?: number): readonly Point3D[];
|
package/dist/physics3d-hull.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* Numeric regime: add/mul/cmp plus `solverSqrt` for the final unit normals only.
|
|
11
11
|
* No transcendentals (banned in src/ by the static determinism checker).
|
|
12
12
|
*/
|
|
13
|
+
import { createDeterministicMath } from './math.js';
|
|
13
14
|
import { addVec3, crossVec3, dotVec3, scaleVec3, solverSqrt, subVec3, } from './physics3d-shared.js';
|
|
14
15
|
const PLANE_TOL = 1e-9;
|
|
15
16
|
const COPLANAR_TOL = 1e-6;
|
|
@@ -374,3 +375,47 @@ function closestPointOnSegmentLocal(point, a, b) {
|
|
|
374
375
|
t = t < 0 ? 0 : t > 1 ? 1 : t;
|
|
375
376
|
return addVec3(a, scaleVec3(ab, t));
|
|
376
377
|
}
|
|
378
|
+
/**
|
|
379
|
+
* Native Math.cos/Math.sin are not bit-identical across JS engines, and these
|
|
380
|
+
* vertices become checksummed collision geometry — two peers building the same
|
|
381
|
+
* trunk must agree exactly. sinTurns/cosTurns are pure integer arithmetic.
|
|
382
|
+
* 840 is divisible by every allowed side count, so each vertex lands on an
|
|
383
|
+
* exact turn boundary.
|
|
384
|
+
*/
|
|
385
|
+
const PRISM_TURN_SCALE = 840_000;
|
|
386
|
+
const prismMath = createDeterministicMath(PRISM_TURN_SCALE);
|
|
387
|
+
/**
|
|
388
|
+
* Vertices for a cylinder approximated as a regular prism, for trunks, stumps,
|
|
389
|
+
* logs, pillars and barrels.
|
|
390
|
+
*
|
|
391
|
+
* There is no cylinder primitive in Kinetix, the TypeScript solver, or the
|
|
392
|
+
* Box3D parity set — a cylinder is a convex hull, and hulls are at parity. The
|
|
393
|
+
* TypeScript hull builder accepts 4-16 vertices, so `sides` tops out at 8 (an
|
|
394
|
+
* octagonal prism, exactly 16). A capsule is cheaper but its rounded caps let a
|
|
395
|
+
* character slide off a flat top and change how a body rolls off an edge.
|
|
396
|
+
*
|
|
397
|
+
* Call this when building the world, not inside the step: the vertices are
|
|
398
|
+
* constant for a given (radius, halfHeight, sides).
|
|
399
|
+
*/
|
|
400
|
+
export function cylinderHullVertices(radius, halfHeight, sides = 8) {
|
|
401
|
+
if (!Number.isFinite(radius) || radius <= 0) {
|
|
402
|
+
throw new Error(`cylinder hull radius must be a finite positive number, received ${radius}`);
|
|
403
|
+
}
|
|
404
|
+
if (!Number.isFinite(halfHeight) || halfHeight <= 0) {
|
|
405
|
+
throw new Error(`cylinder hull halfHeight must be a finite positive number, received ${halfHeight}`);
|
|
406
|
+
}
|
|
407
|
+
if (!Number.isInteger(sides) || sides < 3 || sides > 8) {
|
|
408
|
+
throw new Error(`cylinder hull sides must be an integer in 3..8 (2 x sides must fit the 4-16 vertex hull limit), received ${sides}`);
|
|
409
|
+
}
|
|
410
|
+
const vertices = [];
|
|
411
|
+
for (let index = 0; index < sides; index += 1) {
|
|
412
|
+
// Turns, not radians: PRISM_TURN_SCALE is divisible by every allowed side
|
|
413
|
+
// count, so this is an exact integer and never rounds.
|
|
414
|
+
const turns = (PRISM_TURN_SCALE * index) / sides;
|
|
415
|
+
const x = (radius * prismMath.cosTurns(turns)) / PRISM_TURN_SCALE;
|
|
416
|
+
const z = (radius * prismMath.sinTurns(turns)) / PRISM_TURN_SCALE;
|
|
417
|
+
vertices.push({ x, y: halfHeight, z });
|
|
418
|
+
vertices.push({ x, y: -halfHeight, z });
|
|
419
|
+
}
|
|
420
|
+
return vertices;
|
|
421
|
+
}
|