@woosh/meep-engine 2.146.0 → 2.147.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.
Files changed (68) hide show
  1. package/package.json +1 -1
  2. package/src/engine/control/first-person/FirstPersonPlayerController.d.ts +0 -11
  3. package/src/engine/control/first-person/FirstPersonPlayerController.d.ts.map +1 -1
  4. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts +8 -6
  5. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts.map +1 -1
  6. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.js +552 -551
  7. package/src/engine/control/first-person/abilities/LedgeGrab.d.ts +8 -3
  8. package/src/engine/control/first-person/abilities/LedgeGrab.d.ts.map +1 -1
  9. package/src/engine/control/first-person/abilities/LedgeGrab.js +213 -199
  10. package/src/engine/control/first-person/abilities/Mantle.d.ts.map +1 -1
  11. package/src/engine/control/first-person/abilities/Mantle.js +195 -188
  12. package/src/engine/control/first-person/abilities/WallRun.d.ts.map +1 -1
  13. package/src/engine/control/first-person/abilities/WallRun.js +183 -175
  14. package/src/engine/control/first-person/sensors/FirstPersonSensors.d.ts +9 -0
  15. package/src/engine/control/first-person/sensors/FirstPersonSensors.d.ts.map +1 -1
  16. package/src/engine/control/first-person/sensors/FirstPersonSensors.js +87 -77
  17. package/src/engine/control/first-person/sensors/FirstPersonSensorsSystem.d.ts +8 -0
  18. package/src/engine/control/first-person/sensors/FirstPersonSensorsSystem.d.ts.map +1 -1
  19. package/src/engine/control/first-person/sensors/FirstPersonSensorsSystem.js +229 -196
  20. package/src/engine/ecs/EntityManager.d.ts +34 -11
  21. package/src/engine/ecs/EntityManager.d.ts.map +1 -1
  22. package/src/engine/ecs/EntityManager.js +71 -42
  23. package/src/engine/interpolation/BinaryInterpolationAdapter.d.ts.map +1 -0
  24. package/src/engine/interpolation/Interpoland.d.ts +48 -0
  25. package/src/engine/interpolation/Interpoland.d.ts.map +1 -0
  26. package/src/engine/interpolation/Interpoland.js +49 -0
  27. package/src/engine/interpolation/Interpolated.d.ts +101 -0
  28. package/src/engine/interpolation/Interpolated.d.ts.map +1 -0
  29. package/src/engine/interpolation/Interpolated.js +149 -0
  30. package/src/engine/{network/sim → interpolation}/InterpolationLog.d.ts +1 -1
  31. package/src/engine/interpolation/InterpolationLog.d.ts.map +1 -0
  32. package/src/engine/{network/sim → interpolation}/InterpolationLog.js +2 -2
  33. package/src/engine/interpolation/InterpolationSystem.d.ts +116 -0
  34. package/src/engine/interpolation/InterpolationSystem.d.ts.map +1 -0
  35. package/src/engine/interpolation/InterpolationSystem.js +233 -0
  36. package/src/engine/interpolation/PoseInterpolationAdapter.d.ts +17 -0
  37. package/src/engine/interpolation/PoseInterpolationAdapter.d.ts.map +1 -0
  38. package/src/engine/interpolation/PoseInterpolationAdapter.js +61 -0
  39. package/src/engine/interpolation/TransformPoseSerializationAdapter.d.ts +35 -0
  40. package/src/engine/interpolation/TransformPoseSerializationAdapter.d.ts.map +1 -0
  41. package/src/engine/interpolation/TransformPoseSerializationAdapter.js +57 -0
  42. package/src/engine/interpolation/pose_interpoland.d.ts +18 -0
  43. package/src/engine/interpolation/pose_interpoland.d.ts.map +1 -0
  44. package/src/engine/interpolation/pose_interpoland.js +27 -0
  45. package/src/engine/network/NetworkSession.d.ts +2 -2
  46. package/src/engine/network/NetworkSession.d.ts.map +1 -1
  47. package/src/engine/network/NetworkSession.js +2 -2
  48. package/src/engine/network/adapters/QuaternionInterpolationAdapter.d.ts +1 -1
  49. package/src/engine/network/adapters/QuaternionInterpolationAdapter.d.ts.map +1 -1
  50. package/src/engine/network/adapters/QuaternionInterpolationAdapter.js +1 -1
  51. package/src/engine/network/adapters/TransformInterpolationAdapter.d.ts +1 -1
  52. package/src/engine/network/adapters/TransformInterpolationAdapter.d.ts.map +1 -1
  53. package/src/engine/network/adapters/TransformInterpolationAdapter.js +1 -1
  54. package/src/engine/network/adapters/Vector3InterpolationAdapter.d.ts +1 -1
  55. package/src/engine/network/adapters/Vector3InterpolationAdapter.d.ts.map +1 -1
  56. package/src/engine/network/adapters/Vector3InterpolationAdapter.js +1 -1
  57. package/src/engine/physics/INTEPOLATION_SYSTEM_PLAN.md +287 -0
  58. package/src/engine/physics/PLAN.md +10 -9
  59. package/src/engine/physics/ecs/PhysicsSystem.d.ts +64 -0
  60. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  61. package/src/engine/physics/ecs/PhysicsSystem.js +168 -0
  62. package/src/engine/physics/narrowphase/box_triangle_contact.js +15 -12
  63. package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
  64. package/src/engine/physics/narrowphase/narrowphase_step.js +28 -1
  65. package/src/engine/network/sim/BinaryInterpolationAdapter.d.ts.map +0 -1
  66. package/src/engine/network/sim/InterpolationLog.d.ts.map +0 -1
  67. /package/src/engine/{network/sim → interpolation}/BinaryInterpolationAdapter.d.ts +0 -0
  68. /package/src/engine/{network/sim → interpolation}/BinaryInterpolationAdapter.js +0 -0
@@ -105,15 +105,6 @@ export class EntityManager {
105
105
  */
106
106
  state = EntityManagerState.Initial;
107
107
 
108
- /**
109
- * Track remainders of simulation time for fixed step
110
- * Needed for accurate time keeping
111
- * @private
112
- * @readonly
113
- * @type {Map<System, number>}
114
- */
115
- systemAccumulatedFixedStepTime = new Map();
116
-
117
108
  /**
118
109
  * Value used to execute {@link System.fixedUpdate}
119
110
  * In seconds.
@@ -127,13 +118,35 @@ export class EntityManager {
127
118
  fixedUpdateStepSize = 0.016666666666;
128
119
 
129
120
  /**
130
- * How long can any given system run it's {@link System.fixedUpdate}, per simulation update
131
- * This is value allows us to avoid cases where {@link System.fixedUpdate} takes longer that its time step and causes a runaway freeze
132
- * In milliseconds.
121
+ * Wall-clock budget, in milliseconds, for the whole fixed-step catch-up loop
122
+ * within a single {@link simulate} call. Once a cycle's accumulated
123
+ * fixedUpdate work exceeds this, the loop stops early and the unconsumed time
124
+ * stays in the accumulator to be caught up later — this avoids a runaway
125
+ * freeze when {@link System.fixedUpdate} work outpaces its time step. (Now a
126
+ * per-cycle budget across all systems' lock-step steps; previously per-system.)
133
127
  * @type {number}
134
128
  */
135
129
  fixedUpdatePerSystemExecutionTimeLimit = 15;
136
130
 
131
+ /**
132
+ * Monotonic **simulation tick id** — the count of fixed steps the simulation
133
+ * has executed. Advances once per {@link fixedUpdateStepSize} step inside
134
+ * {@link simulate}. This is the hub-owned identity that producers (physics,
135
+ * network) stamp per-tick state with, and that render-rate consumers
136
+ * (interpolation) read as "the latest completed step". Pairs with
137
+ * {@link getFixedStepAlpha}.
138
+ * @type {number}
139
+ */
140
+ fixedStepTick = 0;
141
+
142
+ /**
143
+ * Canonical fixed-step time accumulator backing {@link fixedStepTick} and
144
+ * {@link getFixedStepAlpha}. Holds the sub-step remainder in `[0, step)`.
145
+ * @private
146
+ * @type {number}
147
+ */
148
+ __fixedStepAccumulator = 0;
149
+
137
150
  /**
138
151
  * Currently attached dataset.
139
152
  * Do not modify directly, instead use {@link attachDataset} and {@link detachDataset} respectively.
@@ -431,55 +444,75 @@ export class EntityManager {
431
444
  const system_count = systems.length;
432
445
 
433
446
  const fixed_step = this.fixedUpdateStepSize;
434
- const accumulatedTime = this.systemAccumulatedFixedStepTime;
435
447
 
436
448
  assert.notNaN(fixed_step, 'fixed_step');
437
449
  assert.greaterThan(fixed_step, 0, 'fixed_step must be greater than 0');
438
450
 
439
- for (let i = 0; i < system_count; i++) {
440
-
441
- const system = systems[i];
442
-
443
- // Perform fixed-step update
444
- if (system.fixedUpdate !== noop) {
445
- let accumulated_time = accumulatedTime.get(system) + time_delta_seconds;
446
-
447
- const t0 = performance.now();
448
-
449
- while (accumulated_time >= fixed_step) {
450
-
451
+ // Global fixed-step loop. Every system advances its fixedUpdate in
452
+ // LOCK-STEP on a single shared accumulator, and the simulation tick id is
453
+ // incremented once per step inside the loop — so all systems run the same
454
+ // step under the same `fixedStepTick`, and a producer can read
455
+ // `fixedStepTick` during its own fixedUpdate as the true current step.
456
+ // (All fixedUpdates for a step run before any update() — a clean
457
+ // sim-then-render phase split that also keeps every system on the same
458
+ // tick boundary for deterministic rewind/resim.)
459
+ this.__fixedStepAccumulator += time_delta_seconds;
460
+
461
+ const t0 = performance.now();
462
+ while (this.__fixedStepAccumulator >= fixed_step) {
463
+ this.fixedStepTick++;
464
+
465
+ for (let i = 0; i < system_count; i++) {
466
+ const system = systems[i];
467
+ if (system.fixedUpdate !== noop) {
451
468
  try {
452
- system.fixedUpdate(fixed_step)
469
+ system.fixedUpdate(fixed_step);
453
470
  } catch (e) {
454
471
  console.error(`Failed during fixedUpdate of system '${computeSystemName(system)}': `, e);
455
472
  }
456
-
457
- accumulated_time -= fixed_step;
458
-
459
- if (performance.now() - t0 > this.fixedUpdatePerSystemExecutionTimeLimit) {
460
- console.warn(`.fixedUpdate of system '${computeSystemName(system)}' is falling behind current clock due to slow execution. Retardation is done to avoid severe performance impact.`);
461
- break;
462
- }
463
473
  }
474
+ }
464
475
 
465
- // record whatever remains
466
- accumulatedTime.set(system, accumulated_time);
476
+ this.__fixedStepAccumulator -= fixed_step;
467
477
 
478
+ // Runaway guard: cap total fixed-step catch-up per simulate so a long
479
+ // stall (e.g. a backgrounded tab resuming) doesn't freeze the frame.
480
+ // The unconsumed remainder stays in the accumulator and is caught up
481
+ // over subsequent calls.
482
+ if (performance.now() - t0 > this.fixedUpdatePerSystemExecutionTimeLimit) {
483
+ console.warn('EntityManager.simulate: fixedUpdate is falling behind the clock; capping catch-up this cycle to avoid a freeze.');
484
+ break;
468
485
  }
486
+ }
469
487
 
488
+ // Variable-rate update pass — after all fixed steps for this cycle.
489
+ for (let i = 0; i < system_count; i++) {
490
+ const system = systems[i];
470
491
  if (system.update !== noop) {
471
-
472
492
  try {
473
493
  system.update(time_delta_seconds);
474
494
  } catch (e) {
475
495
  console.error(`Failed during update of system '${computeSystemName(system)}': `, e);
476
496
  }
477
-
478
497
  }
479
-
480
498
  }
481
499
  }
482
500
 
501
+ /**
502
+ * Fractional progress through the current fixed step, in `[0, 1)` — the
503
+ * leftover sim time that hasn't yet formed a whole {@link fixedUpdateStepSize}
504
+ * step, normalized by the step size. This is the blend parameter the
505
+ * render-time interpolation system pairs with {@link fixedStepTick} to
506
+ * interpolate between the last two completed steps: `alpha = 0` sits on the
507
+ * previous tick, `alpha → 1` approaches the latest.
508
+ *
509
+ * @returns {number} alpha in `[0, 1)`
510
+ */
511
+ getFixedStepAlpha() {
512
+ const alpha = this.__fixedStepAccumulator / this.fixedUpdateStepSize;
513
+ return alpha < 1 ? alpha : 1;
514
+ }
515
+
483
516
  /**
484
517
  * If the {@link EntityManager} is already started, the system will be started automatically before being added
485
518
  * @param {System} system
@@ -542,8 +575,6 @@ export class EntityManager {
542
575
  // link dependency components
543
576
  this.dataset?.registerManyComponentTypes(system.referenced_components);
544
577
 
545
- this.systemAccumulatedFixedStepTime.set(system, 0);
546
-
547
578
  this.on.systemAdded.send1(system);
548
579
 
549
580
  return result_promise;
@@ -590,8 +621,6 @@ export class EntityManager {
590
621
  });
591
622
  });
592
623
 
593
- this.systemAccumulatedFixedStepTime.delete(system);
594
-
595
624
  this.on.systemRemoved.send1(system);
596
625
 
597
626
  return true;
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BinaryInterpolationAdapter.d.ts","sourceRoot":"","sources":["../../../../src/engine/interpolation/BinaryInterpolationAdapter.js"],"names":[],"mappings":";;;;;gCAMU,MAAM;AANhB;;;;;;;GAOG;AACH;IACI,gEAAgE;;IAEhE,qEAAqE;;IAErE,+DAA+D;;GAEhE;AAEH;;;;;;;;;;;;;;;GAeG;AACH;IAEI,sDAAsD;IACtD,MADW,MAAM,CACe;IAEhC;;;;;;;;;;;;;OAaG;IACH,0EAJW,MAAM,iBACN,MAAM,KACN,MAAM,QAIhB;CACJ"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Descriptor for ONE thing the interpolation system blends for an entity.
3
+ *
4
+ * An entity carries an {@link Interpolated} component holding a LIST of these.
5
+ * Pure ECS allows at most one component instance of a given class per entity, so
6
+ * "interpolate the pose AND something else on the same entity" is expressed as a
7
+ * list of interpolands, not several `Interpolated` components.
8
+ *
9
+ * Each interpoland binds, for one interpolated quantity:
10
+ * - a stable `type_id` distinguishing it in the interpolation log under the
11
+ * entity's key (the log is indexed by `(key, type_id, tick)`);
12
+ * - the live ECS `component_class` the blended result is written into;
13
+ * - a `serialization_adapter` that encodes the live component into a snapshot
14
+ * and decodes a blended snapshot back into it; and
15
+ * - an `interpolation_adapter` that blends two snapshots at `t`.
16
+ *
17
+ * Interpolands are stateless and shareable — one instance per kind can be reused
18
+ * across every entity that interpolates that kind (e.g. a single shared pose
19
+ * interpoland for every physics body).
20
+ *
21
+ * @author Alex Goldring
22
+ * @copyright Company Named Limited (c) 2026
23
+ */
24
+ export class Interpoland {
25
+ /**
26
+ * @param {number} type_id stable id distinguishing this kind in the log
27
+ * @param {Function} component_class live component class written into
28
+ * @param {BinaryClassSerializationAdapter} serialization_adapter encodes /
29
+ * decodes the live component to / from a snapshot
30
+ * @param {BinaryInterpolationAdapter} interpolation_adapter blends two
31
+ * snapshots
32
+ */
33
+ constructor(type_id: number, component_class: Function, serialization_adapter: BinaryClassSerializationAdapter, interpolation_adapter: BinaryInterpolationAdapter);
34
+ /** @type {number} */
35
+ type_id: number;
36
+ /** @type {Function} */
37
+ component_class: Function;
38
+ /** @type {BinaryClassSerializationAdapter} */
39
+ serialization_adapter: BinaryClassSerializationAdapter;
40
+ /** @type {BinaryInterpolationAdapter} */
41
+ interpolation_adapter: BinaryInterpolationAdapter;
42
+ /**
43
+ * @readonly
44
+ * @type {boolean}
45
+ */
46
+ readonly isInterpoland: boolean;
47
+ }
48
+ //# sourceMappingURL=Interpoland.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Interpoland.d.ts","sourceRoot":"","sources":["../../../../src/engine/interpolation/Interpoland.js"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH;IACI;;;;;;;OAOG;IACH,qBAPW,MAAM,wIAgBhB;IARG,qBAAqB;IACrB,SADW,MAAM,CACK;IACtB,uBAAuB;IACvB,0BAAsC;IACtC,8CAA8C;IAC9C,uDAAkD;IAClD,yCAAyC;IACzC,kDAAkD;IAI1D;;;OAGG;IACH,wBAFU,OAAO,CAEkB;CANlC"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Descriptor for ONE thing the interpolation system blends for an entity.
3
+ *
4
+ * An entity carries an {@link Interpolated} component holding a LIST of these.
5
+ * Pure ECS allows at most one component instance of a given class per entity, so
6
+ * "interpolate the pose AND something else on the same entity" is expressed as a
7
+ * list of interpolands, not several `Interpolated` components.
8
+ *
9
+ * Each interpoland binds, for one interpolated quantity:
10
+ * - a stable `type_id` distinguishing it in the interpolation log under the
11
+ * entity's key (the log is indexed by `(key, type_id, tick)`);
12
+ * - the live ECS `component_class` the blended result is written into;
13
+ * - a `serialization_adapter` that encodes the live component into a snapshot
14
+ * and decodes a blended snapshot back into it; and
15
+ * - an `interpolation_adapter` that blends two snapshots at `t`.
16
+ *
17
+ * Interpolands are stateless and shareable — one instance per kind can be reused
18
+ * across every entity that interpolates that kind (e.g. a single shared pose
19
+ * interpoland for every physics body).
20
+ *
21
+ * @author Alex Goldring
22
+ * @copyright Company Named Limited (c) 2026
23
+ */
24
+ export class Interpoland {
25
+ /**
26
+ * @param {number} type_id stable id distinguishing this kind in the log
27
+ * @param {Function} component_class live component class written into
28
+ * @param {BinaryClassSerializationAdapter} serialization_adapter encodes /
29
+ * decodes the live component to / from a snapshot
30
+ * @param {BinaryInterpolationAdapter} interpolation_adapter blends two
31
+ * snapshots
32
+ */
33
+ constructor(type_id, component_class, serialization_adapter, interpolation_adapter) {
34
+ /** @type {number} */
35
+ this.type_id = type_id;
36
+ /** @type {Function} */
37
+ this.component_class = component_class;
38
+ /** @type {BinaryClassSerializationAdapter} */
39
+ this.serialization_adapter = serialization_adapter;
40
+ /** @type {BinaryInterpolationAdapter} */
41
+ this.interpolation_adapter = interpolation_adapter;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * @readonly
47
+ * @type {boolean}
48
+ */
49
+ Interpoland.prototype.isInterpoland = true;
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Sentinel for {@link Interpolated#key} before assignment. The
3
+ * {@link InterpolationSystem} replaces it with the entity id at link time.
4
+ * @type {number}
5
+ */
6
+ export const INTERPOLATION_KEY_UNSET: number;
7
+ /**
8
+ * Default {@link Interpolated#sourceId}: the local fixed-step timeline (the
9
+ * {@link InterpolationSystem}'s own log, blended on `fixedStepTick`). Every
10
+ * physics-driven body uses this. Other source ids are registered with the
11
+ * system (e.g. a network playout) and assigned to remote entities.
12
+ * @type {number}
13
+ */
14
+ export const INTERPOLATION_SOURCE_LOCAL: number;
15
+ /**
16
+ * Marker component opting an entity into render-time interpolation, and the
17
+ * cooperation point producers (physics, network) and the consumer
18
+ * ({@link InterpolationSystem}) share.
19
+ *
20
+ * Carries:
21
+ * - `key`: the integer identity this entity's snapshots are filed under in the
22
+ * interpolation log. Defaults to {@link INTERPOLATION_KEY_UNSET}; the
23
+ * {@link InterpolationSystem} assigns it the entity id at link time when
24
+ * unset — the common case needs no manual setup. The network layer can set it
25
+ * to the `network_id` instead, so a replicated entity keeps one identity
26
+ * across peers.
27
+ * - `interpolands`: the {@link Interpoland} kinds to blend for this entity (one
28
+ * pose interpoland in the common case; more for entities that smooth several
29
+ * quantities).
30
+ * - `snap`: when true, the next consume shows the latest snapshot exactly (no
31
+ * blend) and the flag clears — used to avoid sliding across a teleport.
32
+ *
33
+ * Transient render configuration: NOT serialized or replicated.
34
+ *
35
+ * @author Alex Goldring
36
+ * @copyright Company Named Limited (c) 2026
37
+ */
38
+ export class Interpolated {
39
+ /**
40
+ * Log key. {@link INTERPOLATION_KEY_UNSET} until the system assigns the
41
+ * entity id (or a producer assigns a network_id).
42
+ * @type {number}
43
+ */
44
+ key: number;
45
+ /**
46
+ * When true, the next consume snaps to the latest snapshot (no blend) and
47
+ * the flag clears. Set on teleport so the render doesn't slide across the jump.
48
+ * @type {boolean}
49
+ */
50
+ snap: boolean;
51
+ /**
52
+ * The interpoland kinds to blend for this entity.
53
+ * @type {Interpoland[]}
54
+ */
55
+ interpolands: Interpoland[];
56
+ /**
57
+ * Which interpolation timeline this entity is blended on — a plain id, NOT a
58
+ * resource or behavior (those live on the {@link InterpolationSystem}, keyed
59
+ * by this id). {@link INTERPOLATION_SOURCE_LOCAL} (the default) is the local
60
+ * fixed-step clock + the system's own log; a remote/networked entity carries
61
+ * the id of a source the network registered with the system (its
62
+ * render-delayed playout + log), so one render pass blends local and remote
63
+ * entities coherently.
64
+ * @type {number}
65
+ */
66
+ sourceId: number;
67
+ /**
68
+ * Value equality over the configurable state: the timeline ({@link sourceId})
69
+ * and the interpoland kinds (compared by their stable `type_id`). The
70
+ * system-assigned log {@link key} and the transient {@link snap} flag are
71
+ * excluded.
72
+ *
73
+ * @param {Interpolated} other
74
+ * @returns {boolean}
75
+ */
76
+ equals(other: Interpolated): boolean;
77
+ /**
78
+ * Hash over the same configurable state as {@link equals}. Equal components
79
+ * hash equal.
80
+ * @returns {number}
81
+ */
82
+ hash(): number;
83
+ /**
84
+ * Copy `other`'s state into this component. The interpoland list is
85
+ * duplicated as a new array sharing the (stateless, shared) interpoland
86
+ * descriptors; `key`, `snap`, and `sourceId` are copied verbatim.
87
+ *
88
+ * @param {Interpolated} other
89
+ * @returns {Interpolated} this
90
+ */
91
+ copy(other: Interpolated): Interpolated;
92
+ /**
93
+ * @readonly
94
+ * @type {boolean}
95
+ */
96
+ readonly isInterpolated: boolean;
97
+ }
98
+ export namespace Interpolated {
99
+ let typeName: string;
100
+ }
101
+ //# sourceMappingURL=Interpolated.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Interpolated.d.ts","sourceRoot":"","sources":["../../../../src/engine/interpolation/Interpolated.js"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,sCAFU,MAAM,CAE0B;AAE1C;;;;;;GAMG;AACH,yCAFU,MAAM,CAE4B;AAE5C;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH;IAEI;;;;OAIG;IACH,KAFU,MAAM,CAEc;IAE9B;;;;OAIG;IACH,MAFU,OAAO,CAEJ;IAEb;;;OAGG;IACH,cAFU,aAAa,CAEL;IAElB;;;;;;;;;OASG;IACH,UAFU,MAAM,CAEsB;IAEtC;;;;;;;;OAQG;IACH,cAHW,YAAY,GACV,OAAO,CAuBnB;IAED;;;;OAIG;IACH,QAFa,MAAM,CASlB;IAED;;;;;;;OAOG;IACH,YAHW,YAAY,GACV,YAAY,CAQxB;IASL;;;OAGG;IACH,yBAFU,OAAO,CAEoB;CAZpC;;kBAIS,MAAM"}
@@ -0,0 +1,149 @@
1
+ import { combine_hash } from "../../core/collection/array/combine_hash.js";
2
+
3
+ /**
4
+ * Sentinel for {@link Interpolated#key} before assignment. The
5
+ * {@link InterpolationSystem} replaces it with the entity id at link time.
6
+ * @type {number}
7
+ */
8
+ export const INTERPOLATION_KEY_UNSET = -1;
9
+
10
+ /**
11
+ * Default {@link Interpolated#sourceId}: the local fixed-step timeline (the
12
+ * {@link InterpolationSystem}'s own log, blended on `fixedStepTick`). Every
13
+ * physics-driven body uses this. Other source ids are registered with the
14
+ * system (e.g. a network playout) and assigned to remote entities.
15
+ * @type {number}
16
+ */
17
+ export const INTERPOLATION_SOURCE_LOCAL = 0;
18
+
19
+ /**
20
+ * Marker component opting an entity into render-time interpolation, and the
21
+ * cooperation point producers (physics, network) and the consumer
22
+ * ({@link InterpolationSystem}) share.
23
+ *
24
+ * Carries:
25
+ * - `key`: the integer identity this entity's snapshots are filed under in the
26
+ * interpolation log. Defaults to {@link INTERPOLATION_KEY_UNSET}; the
27
+ * {@link InterpolationSystem} assigns it the entity id at link time when
28
+ * unset — the common case needs no manual setup. The network layer can set it
29
+ * to the `network_id` instead, so a replicated entity keeps one identity
30
+ * across peers.
31
+ * - `interpolands`: the {@link Interpoland} kinds to blend for this entity (one
32
+ * pose interpoland in the common case; more for entities that smooth several
33
+ * quantities).
34
+ * - `snap`: when true, the next consume shows the latest snapshot exactly (no
35
+ * blend) and the flag clears — used to avoid sliding across a teleport.
36
+ *
37
+ * Transient render configuration: NOT serialized or replicated.
38
+ *
39
+ * @author Alex Goldring
40
+ * @copyright Company Named Limited (c) 2026
41
+ */
42
+ export class Interpolated {
43
+
44
+ /**
45
+ * Log key. {@link INTERPOLATION_KEY_UNSET} until the system assigns the
46
+ * entity id (or a producer assigns a network_id).
47
+ * @type {number}
48
+ */
49
+ key = INTERPOLATION_KEY_UNSET;
50
+
51
+ /**
52
+ * When true, the next consume snaps to the latest snapshot (no blend) and
53
+ * the flag clears. Set on teleport so the render doesn't slide across the jump.
54
+ * @type {boolean}
55
+ */
56
+ snap = false;
57
+
58
+ /**
59
+ * The interpoland kinds to blend for this entity.
60
+ * @type {Interpoland[]}
61
+ */
62
+ interpolands = [];
63
+
64
+ /**
65
+ * Which interpolation timeline this entity is blended on — a plain id, NOT a
66
+ * resource or behavior (those live on the {@link InterpolationSystem}, keyed
67
+ * by this id). {@link INTERPOLATION_SOURCE_LOCAL} (the default) is the local
68
+ * fixed-step clock + the system's own log; a remote/networked entity carries
69
+ * the id of a source the network registered with the system (its
70
+ * render-delayed playout + log), so one render pass blends local and remote
71
+ * entities coherently.
72
+ * @type {number}
73
+ */
74
+ sourceId = INTERPOLATION_SOURCE_LOCAL;
75
+
76
+ /**
77
+ * Value equality over the configurable state: the timeline ({@link sourceId})
78
+ * and the interpoland kinds (compared by their stable `type_id`). The
79
+ * system-assigned log {@link key} and the transient {@link snap} flag are
80
+ * excluded.
81
+ *
82
+ * @param {Interpolated} other
83
+ * @returns {boolean}
84
+ */
85
+ equals(other) {
86
+ if (other === this) {
87
+ return true;
88
+ }
89
+ if (other === null || other === undefined || other.isInterpolated !== true) {
90
+ return false;
91
+ }
92
+ if (this.sourceId !== other.sourceId) {
93
+ return false;
94
+ }
95
+ const a = this.interpolands;
96
+ const b = other.interpolands;
97
+ if (a.length !== b.length) {
98
+ return false;
99
+ }
100
+ for (let i = 0; i < a.length; i++) {
101
+ if (a[i].type_id !== b[i].type_id) {
102
+ return false;
103
+ }
104
+ }
105
+ return true;
106
+ }
107
+
108
+ /**
109
+ * Hash over the same configurable state as {@link equals}. Equal components
110
+ * hash equal.
111
+ * @returns {number}
112
+ */
113
+ hash() {
114
+ let h = this.sourceId | 0;
115
+ const ips = this.interpolands;
116
+ for (let i = 0; i < ips.length; i++) {
117
+ h = combine_hash(h, ips[i].type_id | 0);
118
+ }
119
+ return h;
120
+ }
121
+
122
+ /**
123
+ * Copy `other`'s state into this component. The interpoland list is
124
+ * duplicated as a new array sharing the (stateless, shared) interpoland
125
+ * descriptors; `key`, `snap`, and `sourceId` are copied verbatim.
126
+ *
127
+ * @param {Interpolated} other
128
+ * @returns {Interpolated} this
129
+ */
130
+ copy(other) {
131
+ this.key = other.key;
132
+ this.snap = other.snap;
133
+ this.sourceId = other.sourceId;
134
+ this.interpolands = other.interpolands.slice();
135
+ return this;
136
+ }
137
+ }
138
+
139
+ /**
140
+ * @readonly
141
+ * @type {string}
142
+ */
143
+ Interpolated.typeName = "Interpolated";
144
+
145
+ /**
146
+ * @readonly
147
+ * @type {boolean}
148
+ */
149
+ Interpolated.prototype.isInterpolated = true;
@@ -161,5 +161,5 @@ export class InterpolationLog {
161
161
  clear(): void;
162
162
  #private;
163
163
  }
164
- import { BinaryBuffer } from "../../../core/binary/BinaryBuffer.js";
164
+ import { BinaryBuffer } from "../../core/binary/BinaryBuffer.js";
165
165
  //# sourceMappingURL=InterpolationLog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InterpolationLog.d.ts","sourceRoot":"","sources":["../../../../src/engine/interpolation/InterpolationLog.js"],"names":[],"mappings":"AA6EA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH;IAkBI;;;;;OAKG;IACH,0DALW;QAAE,qBAAqB,CAAC,EAAE,MAAM,CAAC;QAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAAE,EAkGvE;IA9EG,+BAA+B;IAC/B,0BADqB,MAAM,CACiB;IAe5C,+BAA+B;IAC/B,2BADqB,MAAM,CACa;IA+D5C;;;;;;;;OAQG;IACH,iBAFW,MAAM,QAkEhB;IAED;;;;;;;;;;;;;OAaG;IACH,yBAJW,MAAM,WACN,MAAM,GACJ,YAAY,CAmCxB;IAED;;;;;OAKG;IACH,mBAsDC;IAED;;;;;;;OAOG;IACH,iBAeC;IAED;;;OAGG;IACH,eAHW,MAAM,GACJ,OAAO,CAInB;IAED;;;;;;;;;;;;;;OAcG;IACH,YAPW,MAAM,EAAE,GAAC,WAAW,cACpB,MAAM,QACN,MAAM,cACN,MAAM,WACN,MAAM,GACJ,OAAO,CA+BnB;IAED;;;;;;;;;;;;;;;;;;OAkBG;IACH,wBATW,YAAY,cACZ,MAAM,WACN,MAAM,UACN,MAAM,UACN,MAAM,KACN,MAAM,wCAEJ,OAAO,CAuBnB;IAED;;;OAGG;IACH,QAFa,MAAM,CAIlB;IAED;;;OAGG;IACH,eAFa,MAAM,CAIlB;IAED;;;OAGG;IACH,cASC;;CACJ;6BArkB4B,mCAAmC"}
@@ -1,5 +1,5 @@
1
- import { assert } from "../../../core/assert.js";
2
- import { BinaryBuffer } from "../../../core/binary/BinaryBuffer.js";
1
+ import { assert } from "../../core/assert.js";
2
+ import { BinaryBuffer } from "../../core/binary/BinaryBuffer.js";
3
3
 
4
4
  /** Words per record: [network_id, type_id, byte_offset, byte_length]. 4 × u32 = 16 B. */
5
5
  const RECORD_WORDS = 4;
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Render-time consumer of the interpolation log. Runs in {@link update} (the
3
+ * variable-rate, render-loop-driven cycle): for every {@link Interpolated}
4
+ * entity it blends each interpoland between the last two completed fixed ticks
5
+ * by the current fixed-step alpha and writes the result into the live target
6
+ * component (e.g. {@link Transform}). Because it writes the live component, all
7
+ * Transform-linked machinery (culling, particles, trails, attachments) renders
8
+ * the smoothed pose.
9
+ *
10
+ * Producers (physics, network) record authoritative per-tick snapshots into
11
+ * {@link log}; this system only consumes. Cost is O(entities × interpolands),
12
+ * and producers record only for things that moved, so the log stays sparse.
13
+ *
14
+ * For a LOCAL entity (the default), tick + alpha come from the simulation hub:
15
+ * `entityManager.fixedStepTick` is the latest completed fixed step and
16
+ * `entityManager.getFixedStepAlpha()` is the sub-step fraction, blending
17
+ * `(fixedStepTick - 1, fixedStepTick)` from this system's own log.
18
+ *
19
+ * An entity may instead carry a non-default {@link Interpolated#sourceId} naming
20
+ * a timeline registered via {@link registerSource} (the log + window provider
21
+ * live on the system, not the component) — e.g. a networked entity blended on
22
+ * the network's render-delayed playout. The same render pass then drives both
23
+ * local and remote entities, so a mixed scene is coherent; each source is
24
+ * sampled once per frame.
25
+ *
26
+ * @author Alex Goldring
27
+ * @copyright Company Named Limited (c) 2026
28
+ */
29
+ export class InterpolationSystem extends System<any, any, any, any, any> {
30
+ /**
31
+ * @param {{ buffer_capacity_bytes?: number, records_capacity?: number }} [options]
32
+ */
33
+ constructor({ buffer_capacity_bytes, records_capacity }?: {
34
+ buffer_capacity_bytes?: number;
35
+ records_capacity?: number;
36
+ });
37
+ dependencies: (typeof Interpolated)[];
38
+ components_used: ResourceAccessSpecification<typeof Interpolated>[];
39
+ /**
40
+ * Shared per-tick snapshot store. Producers record into it; this system
41
+ * blends from it.
42
+ * @type {InterpolationLog}
43
+ */
44
+ log: InterpolationLog;
45
+ /**
46
+ * Linked entities → their {@link Interpolated} component. Iterated each
47
+ * update.
48
+ * @private
49
+ * @type {Map<number, Interpolated>}
50
+ */
51
+ private __entities;
52
+ /**
53
+ * Reusable decode buffer for blended snapshots.
54
+ * @private
55
+ * @type {BinaryBuffer}
56
+ */
57
+ private __scratch;
58
+ /**
59
+ * Non-local timeline registry, keyed by `sourceId`. Each entry is the
60
+ * timeline's `{ log, sample() }` — its log to read and a per-frame window
61
+ * provider. Owners (e.g. the network layer) register here; the
62
+ * {@link Interpolated} component only carries the integer id.
63
+ * @private
64
+ * @type {Map<number, { log: InterpolationLog, sample: () => { tick_a: number, tick_b: number, t: number } }>}
65
+ */
66
+ private __sources;
67
+ /**
68
+ * Per-update cache of `sourceId → window`, so a source's playout is
69
+ * sampled once per frame, not once per entity. Cleared each update.
70
+ * @private
71
+ * @type {Map<number, { tick_a: number, tick_b: number, t: number }>}
72
+ */
73
+ private __source_windows;
74
+ /**
75
+ * Register a non-local interpolation timeline under `sourceId`. Entities
76
+ * carrying that id on their {@link Interpolated} component are blended from
77
+ * `log` on the window `sample()` returns (sampled once per render frame).
78
+ * Used by timeline owners — e.g. the network layer registers its
79
+ * render-delayed playout. `sourceId` must not be
80
+ * {@link INTERPOLATION_SOURCE_LOCAL} (reserved for the local clock).
81
+ *
82
+ * @param {number} sourceId
83
+ * @param {InterpolationLog} log
84
+ * @param {() => { tick_a: number, tick_b: number, t: number }} sample
85
+ */
86
+ registerSource(sourceId: number, log: InterpolationLog, sample: () => {
87
+ tick_a: number;
88
+ tick_b: number;
89
+ t: number;
90
+ }): void;
91
+ /**
92
+ * @param {EntityManager} entityManager
93
+ */
94
+ startup(entityManager: EntityManager): Promise<void>;
95
+ /**
96
+ * @param {Interpolated} interpolated
97
+ * @param {number} entity
98
+ */
99
+ link(interpolated: Interpolated, entity: number): void;
100
+ /**
101
+ * @param {Interpolated} interpolated
102
+ * @param {number} entity
103
+ */
104
+ unlink(interpolated: Interpolated, entity: number): void;
105
+ update(dt: any): void;
106
+ /**
107
+ * @readonly
108
+ * @type {boolean}
109
+ */
110
+ readonly isInterpolationSystem: boolean;
111
+ }
112
+ import { System } from "../ecs/System.js";
113
+ import { Interpolated } from "./Interpolated.js";
114
+ import { ResourceAccessSpecification } from "../../core/model/ResourceAccessSpecification.js";
115
+ import { InterpolationLog } from "./InterpolationLog.js";
116
+ //# sourceMappingURL=InterpolationSystem.d.ts.map