@series-inc/rundot-syncplay 5.28.0-beta.2 → 5.28.0-beta.3

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.
@@ -101,6 +101,11 @@ export type DeterministicPhysicsJoint3D = {
101
101
  readonly targetZ: number;
102
102
  readonly maxForce: number;
103
103
  readonly damping?: number;
104
+ readonly targetRotation?: Quat3D;
105
+ readonly maxTorque?: number;
106
+ readonly angularDamping?: number;
107
+ readonly breakDistance?: number;
108
+ readonly breakImpulse?: number;
104
109
  };
105
110
  /** Build solver joint objects from joint definitions, seeded from the joint cache. */
106
111
  export declare function createSolverJoints(joints: readonly DeterministicPhysicsJoint3D[], indexById: Map<string, number>, jointCache: readonly SolverJointCacheEntry[]): SolverJointSolve[];
@@ -639,12 +639,33 @@ function grabJoint(joint, bodyIndex, warm) {
639
639
  let desiredVel = { x: 0, y: 0, z: 0 };
640
640
  const target = { x: joint.targetX, y: joint.targetY, z: joint.targetZ };
641
641
  const maxImpulse = joint.maxForce * 1;
642
+ const targetRotation = joint.targetRotation !== undefined ? normalizeQuat(joint.targetRotation) : undefined;
643
+ const maxTorque = joint.maxTorque ?? 0;
644
+ const angularDamping = joint.angularDamping ?? joint.damping ?? 1;
645
+ let desiredAngVel = { x: 0, y: 0, z: 0 };
646
+ let angMassMat;
647
+ let angMassDiag;
648
+ let broken = false;
649
+ let torqueImpulse = { x: 0, y: 0, z: 0 };
642
650
  return {
643
651
  bodyIndices: [bodyIndex],
644
652
  prepare(ctx) {
653
+ if (broken) {
654
+ return;
655
+ }
656
+ torqueImpulse = { x: 0, y: 0, z: 0 };
645
657
  const body = ctx.bodies[bodyIndex];
646
658
  r = jointLever(body, joint.anchor);
647
659
  const worldAnchor = addVec3(body.c, r);
660
+ if (joint.breakDistance !== undefined) {
661
+ const delta = subVec3(target, worldAnchor);
662
+ const d = solverSqrt(dotVec3(delta, delta));
663
+ if (d > joint.breakDistance) {
664
+ broken = true;
665
+ state.impulse = { x: 0, y: 0, z: 0 };
666
+ return;
667
+ }
668
+ }
648
669
  const imB = ctx.invMass[bodyIndex];
649
670
  const linear = mat3FromDiag(imB);
650
671
  const skewR = skewMat3(r);
@@ -653,8 +674,36 @@ function grabJoint(joint, bodyIndex, warm) {
653
674
  const damping = joint.damping ?? 1;
654
675
  desiredVel = scaleVec3(subVec3(target, worldAnchor), damping / ctx.dt);
655
676
  ctx.applyToBody(bodyIndex, r, state.impulse);
677
+ if (targetRotation !== undefined) {
678
+ const errorRotation = quatMultiply(quatConjugate(targetRotation), body.q);
679
+ const sign = errorRotation.w < 0 ? -1 : 1;
680
+ const error = {
681
+ x: 2 * sign * errorRotation.x,
682
+ y: 2 * sign * errorRotation.y,
683
+ z: 2 * sign * errorRotation.z,
684
+ };
685
+ desiredAngVel = scaleVec3(error, -angularDamping / ctx.dt);
686
+ const invI = ctx.invInertia[bodyIndex];
687
+ const fullInv = invertMat3(invI);
688
+ const isSingular = fullInv[0] === 0 && fullInv[4] === 0 && fullInv[8] === 0;
689
+ if (!isSingular) {
690
+ angMassMat = fullInv;
691
+ angMassDiag = undefined;
692
+ }
693
+ else {
694
+ angMassMat = undefined;
695
+ angMassDiag = {
696
+ x: invI[0] > 1e-9 ? 1 / invI[0] : 0,
697
+ y: invI[4] > 1e-9 ? 1 / invI[4] : 0,
698
+ z: invI[8] > 1e-9 ? 1 / invI[8] : 0,
699
+ };
700
+ }
701
+ }
656
702
  },
657
703
  iterate(ctx) {
704
+ if (broken) {
705
+ return;
706
+ }
658
707
  const body = ctx.bodies[bodyIndex];
659
708
  const currentVel = addVec3(body.v, crossVec3(body.w, r));
660
709
  const dImpulse = mulMat3Vec(kInv, subVec3(desiredVel, currentVel));
@@ -663,9 +712,36 @@ function grabJoint(joint, bodyIndex, warm) {
663
712
  const applied = subVec3(clamped, state.impulse);
664
713
  state.impulse = clamped;
665
714
  ctx.applyToBody(bodyIndex, r, applied);
715
+ if (targetRotation !== undefined && maxTorque > 0) {
716
+ const deltaW = subVec3(desiredAngVel, body.w);
717
+ const dTorque = angMassMat !== undefined
718
+ ? mulMat3Vec(angMassMat, deltaW)
719
+ : { x: deltaW.x * angMassDiag.x, y: deltaW.y * angMassDiag.y, z: deltaW.z * angMassDiag.z };
720
+ const targetTorque = addVec3(torqueImpulse, dTorque);
721
+ const clampedTorque = clampMagnitude(targetTorque, maxTorque * ctx.dt);
722
+ const appliedTorque = subVec3(clampedTorque, torqueImpulse);
723
+ torqueImpulse = clampedTorque;
724
+ ctx.applyAngularToBody(bodyIndex, appliedTorque);
725
+ }
726
+ },
727
+ postSubstep(_ctx) {
728
+ if (!broken) {
729
+ state.stepImpulse = addVec3(state.stepImpulse, state.impulse);
730
+ }
731
+ torqueImpulse = { x: 0, y: 0, z: 0 };
666
732
  },
667
733
  emitCache() {
668
- return { jointId: joint.id, impulseX: state.impulse.x, impulseY: state.impulse.y, impulseZ: state.impulse.z, axialImpulse: 0, hingeAngle: 0 };
734
+ const reaction2 = dotVec3(state.stepImpulse, state.stepImpulse);
735
+ const isBroken = broken || (joint.breakImpulse !== undefined && reaction2 > joint.breakImpulse * joint.breakImpulse);
736
+ return {
737
+ jointId: joint.id,
738
+ impulseX: isBroken ? 0 : state.impulse.x,
739
+ impulseY: isBroken ? 0 : state.impulse.y,
740
+ impulseZ: isBroken ? 0 : state.impulse.z,
741
+ axialImpulse: 0,
742
+ hingeAngle: 0,
743
+ ...(isBroken ? { broken: true } : {}),
744
+ };
669
745
  },
670
746
  };
671
747
  }
@@ -687,6 +763,33 @@ function validateJoint(joint) {
687
763
  if (!Number.isFinite(joint.targetX) || !Number.isFinite(joint.targetY) || !Number.isFinite(joint.targetZ)) {
688
764
  throw new Error(`physics3d joint ${joint.id}: target must be finite`);
689
765
  }
766
+ if (joint.damping !== undefined && (!Number.isFinite(joint.damping) || joint.damping < 0)) {
767
+ throw new Error(`physics3d joint ${joint.id}: damping must be non-negative`);
768
+ }
769
+ if (joint.targetRotation !== undefined) {
770
+ const target = joint.targetRotation;
771
+ if (!Number.isFinite(target.x) || !Number.isFinite(target.y) || !Number.isFinite(target.z) || !Number.isFinite(target.w)
772
+ || target.x * target.x + target.y * target.y + target.z * target.z + target.w * target.w === 0) {
773
+ throw new Error(`physics3d joint ${joint.id}: targetRotation must be a finite non-zero quaternion`);
774
+ }
775
+ if (joint.maxTorque === undefined || !Number.isFinite(joint.maxTorque) || joint.maxTorque <= 0) {
776
+ throw new Error(`physics3d joint ${joint.id}: maxTorque must be a positive finite number`);
777
+ }
778
+ }
779
+ else if (joint.maxTorque !== undefined) {
780
+ if (!Number.isFinite(joint.maxTorque) || joint.maxTorque <= 0) {
781
+ throw new Error(`physics3d joint ${joint.id}: maxTorque must be a positive finite number`);
782
+ }
783
+ }
784
+ if (joint.angularDamping !== undefined && (!Number.isFinite(joint.angularDamping) || joint.angularDamping < 0)) {
785
+ throw new Error(`physics3d joint ${joint.id}: angularDamping must be non-negative`);
786
+ }
787
+ if (joint.breakDistance !== undefined && (!Number.isFinite(joint.breakDistance) || joint.breakDistance <= 0)) {
788
+ throw new Error(`physics3d joint ${joint.id}: breakDistance must be positive`);
789
+ }
790
+ if (joint.breakImpulse !== undefined && (!Number.isFinite(joint.breakImpulse) || joint.breakImpulse <= 0)) {
791
+ throw new Error(`physics3d joint ${joint.id}: breakImpulse must be positive`);
792
+ }
690
793
  return;
691
794
  }
692
795
  assertFiniteVec(joint.anchorA, 'anchorA', joint.id);
@@ -222,6 +222,8 @@ export interface SolverJointContext {
222
222
  apply(a: number, b: number, rA: Point3D, rB: Point3D, impulse: Point3D): void;
223
223
  /** Angular-only impulse (no linear component), e.g. hinge motor/limit rows. */
224
224
  applyAngular(a: number, b: number, angularImpulse: Point3D): void;
225
+ /** Apply an angular impulse to a single body (grab joint orientation drive). */
226
+ applyAngularToBody(index: number, angularImpulse: Point3D): void;
225
227
  /** Apply an impulse to a single body at anchor `r` (grab / world-target joints). */
226
228
  applyToBody(index: number, r: Point3D, impulse: Point3D): void;
227
229
  /** Split-impulse position correction on pseudo-velocities (no real momentum). */
@@ -1685,6 +1685,13 @@ export function solveRigidStep(bodies, options) {
1685
1685
  B.w = addVec3(B.w, mulMat3Vec(invInertia[b], angularImpulse));
1686
1686
  }
1687
1687
  },
1688
+ applyAngularToBody: (index, angularImpulse) => {
1689
+ const body = bodies[index];
1690
+ if (!solid(body)) {
1691
+ return;
1692
+ }
1693
+ body.w = addVec3(body.w, mulMat3Vec(invInertia[index], angularImpulse));
1694
+ },
1688
1695
  applyToBody: (index, r, impulse) => {
1689
1696
  const body = bodies[index];
1690
1697
  if (!solid(body)) {
package/dist/runner.d.ts CHANGED
@@ -85,7 +85,17 @@ export interface SyncplayRunnerOfflineOptions<Input> {
85
85
  }
86
86
  export interface SyncplayRunnerSnapshot<RenderState> {
87
87
  readonly status: SyncplayRunnerStatus;
88
+ /**
89
+ * Discrete tick render state. Note: to prevent 30Hz display judder on
90
+ * high-refresh displays, prefer interpolatedRenderState or runner.getRenderState().
91
+ */
88
92
  readonly renderState?: Readonly<RenderState>;
93
+ /**
94
+ * Smooth presentation state interpolated between previous and current
95
+ * render states at the current `renderAlpha`. If no interpolator is
96
+ * configured or interpolation throws, falls back to `renderState`.
97
+ */
98
+ readonly interpolatedRenderState?: Readonly<RenderState>;
89
99
  readonly renderAlpha: number;
90
100
  readonly localSlot: number;
91
101
  readonly rollbackCount: number;
package/dist/runner.js CHANGED
@@ -68,9 +68,28 @@ export function createSyncplayRunner(config) {
68
68
  return localSession !== undefined ? localSession.currentFrame : 0;
69
69
  }
70
70
  function snapshot() {
71
+ let interpolatedRender;
72
+ if (currentRender !== undefined) {
73
+ if (previousRender !== undefined && config.presentation.interpolate !== undefined) {
74
+ try {
75
+ interpolatedRender = config.presentation.interpolate(previousRender, currentRender, renderAlpha);
76
+ }
77
+ catch {
78
+ interpolatedRender = currentRender;
79
+ }
80
+ }
81
+ else {
82
+ interpolatedRender = currentRender;
83
+ }
84
+ }
71
85
  return Object.freeze({
72
86
  status,
73
- ...(currentRender === undefined ? {} : { renderState: currentRender }),
87
+ ...(currentRender === undefined
88
+ ? {}
89
+ : {
90
+ renderState: currentRender,
91
+ interpolatedRenderState: interpolatedRender,
92
+ }),
74
93
  renderAlpha,
75
94
  localSlot,
76
95
  rollbackCount: networkClient?.rollbacks ?? 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@series-inc/rundot-syncplay",
3
- "version": "5.28.0-beta.2",
3
+ "version": "5.28.0-beta.3",
4
4
  "description": "Kinetix orchestration, rollback, replay, late-join, and deterministic networking for RUN.game",
5
5
  "repository": {
6
6
  "type": "git",