@series-inc/rundot-syncplay 5.23.0-beta.7

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 (264) hide show
  1. package/README.md +610 -0
  2. package/dist/animation.d.ts +119 -0
  3. package/dist/animation.js +488 -0
  4. package/dist/asset-cooking.d.ts +82 -0
  5. package/dist/asset-cooking.js +364 -0
  6. package/dist/authority-room.d.ts +217 -0
  7. package/dist/authority-room.js +701 -0
  8. package/dist/bot-documents.d.ts +115 -0
  9. package/dist/bot-documents.js +549 -0
  10. package/dist/bot-primitives.d.ts +152 -0
  11. package/dist/bot-primitives.js +472 -0
  12. package/dist/browser.d.ts +57 -0
  13. package/dist/browser.js +53 -0
  14. package/dist/canonical.d.ts +12 -0
  15. package/dist/canonical.js +288 -0
  16. package/dist/certification.d.ts +455 -0
  17. package/dist/certification.js +1855 -0
  18. package/dist/chaos-certification.d.ts +56 -0
  19. package/dist/chaos-certification.js +1397 -0
  20. package/dist/cjs/animation.js +495 -0
  21. package/dist/cjs/asset-cooking.js +372 -0
  22. package/dist/cjs/authority-room.js +704 -0
  23. package/dist/cjs/bot-documents.js +556 -0
  24. package/dist/cjs/bot-primitives.js +482 -0
  25. package/dist/cjs/browser.js +96 -0
  26. package/dist/cjs/canonical.js +299 -0
  27. package/dist/cjs/certification.js +1860 -0
  28. package/dist/cjs/chaos-certification.js +1402 -0
  29. package/dist/cjs/collections.js +36 -0
  30. package/dist/cjs/collision.js +96 -0
  31. package/dist/cjs/commands.js +987 -0
  32. package/dist/cjs/config-bundle.js +491 -0
  33. package/dist/cjs/creator.js +30 -0
  34. package/dist/cjs/deterministic-primitives.js +134 -0
  35. package/dist/cjs/deterministic-session.js +1410 -0
  36. package/dist/cjs/ecs-lite.js +155 -0
  37. package/dist/cjs/effects-adapter.js +97 -0
  38. package/dist/cjs/engine-complete.js +2297 -0
  39. package/dist/cjs/engine-core-proof.js +70 -0
  40. package/dist/cjs/engine-parity-matrix.js +874 -0
  41. package/dist/cjs/engine-parity.js +3183 -0
  42. package/dist/cjs/engine-runtime.js +3141 -0
  43. package/dist/cjs/errors.js +22 -0
  44. package/dist/cjs/events.js +247 -0
  45. package/dist/cjs/examples/counter-duel.js +157 -0
  46. package/dist/cjs/examples/fixed-arena.js +368 -0
  47. package/dist/cjs/index.js +229 -0
  48. package/dist/cjs/input-authority.js +150 -0
  49. package/dist/cjs/input-button.js +256 -0
  50. package/dist/cjs/input-timeline.js +418 -0
  51. package/dist/cjs/instant-replay.js +66 -0
  52. package/dist/cjs/lag-compensation3d.js +837 -0
  53. package/dist/cjs/local-authority-server-runner.js +1121 -0
  54. package/dist/cjs/local-authority-wire.js +1497 -0
  55. package/dist/cjs/local-authority.js +817 -0
  56. package/dist/cjs/local-ws-adapter.js +150 -0
  57. package/dist/cjs/match-log.js +61 -0
  58. package/dist/cjs/math.js +157 -0
  59. package/dist/cjs/mock-network-adapter.js +26 -0
  60. package/dist/cjs/movement.js +703 -0
  61. package/dist/cjs/movement3d.js +1246 -0
  62. package/dist/cjs/multiclient-authority.js +308 -0
  63. package/dist/cjs/multiplayer-service.js +434 -0
  64. package/dist/cjs/navigation.js +347 -0
  65. package/dist/cjs/navmesh.js +1437 -0
  66. package/dist/cjs/networked-client.js +691 -0
  67. package/dist/cjs/networked-input-authority.js +443 -0
  68. package/dist/cjs/node.js +47 -0
  69. package/dist/cjs/number-lab.js +78 -0
  70. package/dist/cjs/offline-session.js +370 -0
  71. package/dist/cjs/package.json +1 -0
  72. package/dist/cjs/pause-resume.js +43 -0
  73. package/dist/cjs/physics-cert.js +426 -0
  74. package/dist/cjs/physics2d.js +751 -0
  75. package/dist/cjs/physics3d.js +1996 -0
  76. package/dist/cjs/prediction-culling.js +203 -0
  77. package/dist/cjs/protocol-lab.js +1299 -0
  78. package/dist/cjs/r3f-render-adapter.js +209 -0
  79. package/dist/cjs/random.js +59 -0
  80. package/dist/cjs/replay-bundle.js +1598 -0
  81. package/dist/cjs/replay-inspect.js +236 -0
  82. package/dist/cjs/replay-scoring.js +68 -0
  83. package/dist/cjs/replay.js +356 -0
  84. package/dist/cjs/reports.js +181 -0
  85. package/dist/cjs/rollback-history.js +251 -0
  86. package/dist/cjs/runtime-guards.js +111 -0
  87. package/dist/cjs/sample-lab.js +145 -0
  88. package/dist/cjs/sample-scenes.js +752 -0
  89. package/dist/cjs/schema-artifacts.js +359 -0
  90. package/dist/cjs/schema-codegen.js +544 -0
  91. package/dist/cjs/schema-dsl-binary.js +655 -0
  92. package/dist/cjs/schema.js +319 -0
  93. package/dist/cjs/sdk-offline.js +99 -0
  94. package/dist/cjs/session-promotion.js +74 -0
  95. package/dist/cjs/session-snapshot.js +265 -0
  96. package/dist/cjs/session-wire.js +216 -0
  97. package/dist/cjs/signals.js +103 -0
  98. package/dist/cjs/sparse-set.js +169 -0
  99. package/dist/cjs/synctest.js +136 -0
  100. package/dist/cjs/system-lifecycle.js +286 -0
  101. package/dist/cjs/timers.js +61 -0
  102. package/dist/cjs/tools/static-checker.js +1468 -0
  103. package/dist/cjs/types.js +2 -0
  104. package/dist/cjs/venus-mp-budget.js +125 -0
  105. package/dist/cjs/ws-frame.js +255 -0
  106. package/dist/cli.d.ts +2 -0
  107. package/dist/cli.js +11379 -0
  108. package/dist/collections.d.ts +9 -0
  109. package/dist/collections.js +30 -0
  110. package/dist/collision.d.ts +25 -0
  111. package/dist/collision.js +85 -0
  112. package/dist/commands.d.ts +137 -0
  113. package/dist/commands.js +977 -0
  114. package/dist/config-bundle.d.ts +89 -0
  115. package/dist/config-bundle.js +483 -0
  116. package/dist/creator.d.ts +20 -0
  117. package/dist/creator.js +11 -0
  118. package/dist/deterministic-primitives.d.ts +59 -0
  119. package/dist/deterministic-primitives.js +126 -0
  120. package/dist/deterministic-session.d.ts +197 -0
  121. package/dist/deterministic-session.js +1401 -0
  122. package/dist/ecs-lite.d.ts +26 -0
  123. package/dist/ecs-lite.js +152 -0
  124. package/dist/effects-adapter.d.ts +21 -0
  125. package/dist/effects-adapter.js +94 -0
  126. package/dist/engine-complete.d.ts +45 -0
  127. package/dist/engine-complete.js +2292 -0
  128. package/dist/engine-core-proof.d.ts +26 -0
  129. package/dist/engine-core-proof.js +66 -0
  130. package/dist/engine-parity-matrix.d.ts +300 -0
  131. package/dist/engine-parity-matrix.js +869 -0
  132. package/dist/engine-parity.d.ts +74 -0
  133. package/dist/engine-parity.js +3178 -0
  134. package/dist/engine-runtime.d.ts +351 -0
  135. package/dist/engine-runtime.js +3123 -0
  136. package/dist/errors.d.ts +11 -0
  137. package/dist/errors.js +17 -0
  138. package/dist/events.d.ts +45 -0
  139. package/dist/events.js +242 -0
  140. package/dist/examples/counter-duel.d.ts +43 -0
  141. package/dist/examples/counter-duel.js +150 -0
  142. package/dist/examples/fixed-arena.d.ts +110 -0
  143. package/dist/examples/fixed-arena.js +356 -0
  144. package/dist/index.d.ts +101 -0
  145. package/dist/index.js +66 -0
  146. package/dist/input-authority.d.ts +82 -0
  147. package/dist/input-authority.js +147 -0
  148. package/dist/input-button.d.ts +36 -0
  149. package/dist/input-button.js +249 -0
  150. package/dist/input-timeline.d.ts +65 -0
  151. package/dist/input-timeline.js +412 -0
  152. package/dist/instant-replay.d.ts +59 -0
  153. package/dist/instant-replay.js +62 -0
  154. package/dist/lag-compensation3d.d.ts +146 -0
  155. package/dist/lag-compensation3d.js +827 -0
  156. package/dist/local-authority-server-runner.d.ts +79 -0
  157. package/dist/local-authority-server-runner.js +1118 -0
  158. package/dist/local-authority-wire.d.ts +142 -0
  159. package/dist/local-authority-wire.js +1477 -0
  160. package/dist/local-authority.d.ts +160 -0
  161. package/dist/local-authority.js +810 -0
  162. package/dist/local-ws-adapter.d.ts +14 -0
  163. package/dist/local-ws-adapter.js +146 -0
  164. package/dist/match-log.d.ts +74 -0
  165. package/dist/match-log.js +57 -0
  166. package/dist/math.d.ts +41 -0
  167. package/dist/math.js +154 -0
  168. package/dist/mock-network-adapter.d.ts +13 -0
  169. package/dist/mock-network-adapter.js +23 -0
  170. package/dist/movement.d.ts +155 -0
  171. package/dist/movement.js +694 -0
  172. package/dist/movement3d.d.ts +195 -0
  173. package/dist/movement3d.js +1238 -0
  174. package/dist/multiclient-authority.d.ts +30 -0
  175. package/dist/multiclient-authority.js +305 -0
  176. package/dist/multiplayer-service.d.ts +153 -0
  177. package/dist/multiplayer-service.js +429 -0
  178. package/dist/navigation.d.ts +91 -0
  179. package/dist/navigation.js +338 -0
  180. package/dist/navmesh.d.ts +236 -0
  181. package/dist/navmesh.js +1423 -0
  182. package/dist/networked-client.d.ts +124 -0
  183. package/dist/networked-client.js +688 -0
  184. package/dist/networked-input-authority.d.ts +51 -0
  185. package/dist/networked-input-authority.js +439 -0
  186. package/dist/node.d.ts +22 -0
  187. package/dist/node.js +19 -0
  188. package/dist/number-lab.d.ts +15 -0
  189. package/dist/number-lab.js +75 -0
  190. package/dist/offline-session.d.ts +2 -0
  191. package/dist/offline-session.js +367 -0
  192. package/dist/pause-resume.d.ts +48 -0
  193. package/dist/pause-resume.js +40 -0
  194. package/dist/physics-cert.d.ts +40 -0
  195. package/dist/physics-cert.js +389 -0
  196. package/dist/physics2d.d.ts +142 -0
  197. package/dist/physics2d.js +741 -0
  198. package/dist/physics3d.d.ts +285 -0
  199. package/dist/physics3d.js +1979 -0
  200. package/dist/prediction-culling.d.ts +34 -0
  201. package/dist/prediction-culling.js +200 -0
  202. package/dist/protocol-lab.d.ts +115 -0
  203. package/dist/protocol-lab.js +1295 -0
  204. package/dist/r3f-render-adapter.d.ts +61 -0
  205. package/dist/r3f-render-adapter.js +203 -0
  206. package/dist/random.d.ts +13 -0
  207. package/dist/random.js +55 -0
  208. package/dist/replay-bundle.d.ts +150 -0
  209. package/dist/replay-bundle.js +1586 -0
  210. package/dist/replay-inspect.d.ts +60 -0
  211. package/dist/replay-inspect.js +228 -0
  212. package/dist/replay-scoring.d.ts +28 -0
  213. package/dist/replay-scoring.js +65 -0
  214. package/dist/replay.d.ts +17 -0
  215. package/dist/replay.js +351 -0
  216. package/dist/reports.d.ts +176 -0
  217. package/dist/reports.js +174 -0
  218. package/dist/rollback-history.d.ts +46 -0
  219. package/dist/rollback-history.js +247 -0
  220. package/dist/runtime-guards.d.ts +1 -0
  221. package/dist/runtime-guards.js +108 -0
  222. package/dist/sample-lab.d.ts +32 -0
  223. package/dist/sample-lab.js +134 -0
  224. package/dist/sample-scenes.d.ts +86 -0
  225. package/dist/sample-scenes.js +745 -0
  226. package/dist/schema-artifacts.d.ts +49 -0
  227. package/dist/schema-artifacts.js +356 -0
  228. package/dist/schema-codegen.d.ts +21 -0
  229. package/dist/schema-codegen.js +538 -0
  230. package/dist/schema-dsl-binary.d.ts +75 -0
  231. package/dist/schema-dsl-binary.js +648 -0
  232. package/dist/schema.d.ts +87 -0
  233. package/dist/schema.js +315 -0
  234. package/dist/sdk-offline.d.ts +34 -0
  235. package/dist/sdk-offline.js +96 -0
  236. package/dist/session-promotion.d.ts +26 -0
  237. package/dist/session-promotion.js +70 -0
  238. package/dist/session-snapshot.d.ts +95 -0
  239. package/dist/session-snapshot.js +255 -0
  240. package/dist/session-wire.d.ts +89 -0
  241. package/dist/session-wire.js +211 -0
  242. package/dist/signals.d.ts +31 -0
  243. package/dist/signals.js +99 -0
  244. package/dist/sparse-set.d.ts +34 -0
  245. package/dist/sparse-set.js +165 -0
  246. package/dist/synctest.d.ts +31 -0
  247. package/dist/synctest.js +133 -0
  248. package/dist/system-lifecycle.d.ts +72 -0
  249. package/dist/system-lifecycle.js +281 -0
  250. package/dist/timers.d.ts +17 -0
  251. package/dist/timers.js +53 -0
  252. package/dist/tools/index.d.ts +4 -0
  253. package/dist/tools/index.js +4 -0
  254. package/dist/tools/static-checker.d.ts +31 -0
  255. package/dist/tools/static-checker.js +1462 -0
  256. package/dist/tools/vite-plugin.d.ts +69 -0
  257. package/dist/tools/vite-plugin.js +203 -0
  258. package/dist/types.d.ts +148 -0
  259. package/dist/types.js +1 -0
  260. package/dist/venus-mp-budget.d.ts +29 -0
  261. package/dist/venus-mp-budget.js +122 -0
  262. package/dist/ws-frame.d.ts +49 -0
  263. package/dist/ws-frame.js +247 -0
  264. package/package.json +169 -0
@@ -0,0 +1,694 @@
1
+ import { cloneCanonical, defaultChecksum } from './canonical.js';
2
+ import { quantizeFpsAim } from './deterministic-primitives.js';
3
+ export function stepDeterministicKcc2D(body, input, world, frame) {
4
+ assertInteger(input.moveX, 'input.moveX');
5
+ const events = [];
6
+ let x = body.x + input.moveX;
7
+ let y = body.y + body.vy;
8
+ let vx = input.moveX;
9
+ let vy = body.vy - 1;
10
+ let grounded = false;
11
+ let platformId = body.platformId;
12
+ let wallSlideSide = 0;
13
+ let coyoteFrames = body.grounded ? 4 : Math.max(0, body.coyoteFrames - 1);
14
+ let jumpBufferFrames = input.jumpPressed === true ? 4 : Math.max(0, body.jumpBufferFrames - 1);
15
+ for (const platform of [...world.movingPlatforms].sort((left, right) => compareStrings(left.id, right.id))) {
16
+ const platformX = movingPlatformX(platform, frame);
17
+ const platformY = movingPlatformY(platform, frame);
18
+ if (body.grounded && body.platformId === platform.id) {
19
+ x += platform.dxPerFrame;
20
+ y += platform.dyPerFrame ?? 0;
21
+ events.push(platform.dyPerFrame === undefined || platform.dyPerFrame === 0 ? 'platform-ride' : 'elevator-ride');
22
+ }
23
+ if (x >= platformX - 4 && x <= platformX + 4 && y <= platformY && y >= platformY - 3 && vy <= 0) {
24
+ y = platformY;
25
+ vy = 0;
26
+ grounded = true;
27
+ platformId = platform.id;
28
+ events.push(platform.dyPerFrame === undefined || platform.dyPerFrame === 0 ? 'platform-contact' : 'elevator-contact');
29
+ }
30
+ }
31
+ if (jumpBufferFrames > 0 && (body.grounded || body.coyoteFrames > 0 || grounded)) {
32
+ y += 1;
33
+ vy = 9;
34
+ grounded = false;
35
+ platformId = undefined;
36
+ if (!body.grounded && body.coyoteFrames > 0) {
37
+ events.push('coyote-jump');
38
+ }
39
+ events.push('buffered-jump');
40
+ jumpBufferFrames = 0;
41
+ coyoteFrames = 0;
42
+ }
43
+ const slopeY = slopeHeightAt(world.slopes, x);
44
+ if (slopeY !== undefined && y <= slopeY + 2 && vy <= 0) {
45
+ y = slopeY;
46
+ vy = 0;
47
+ grounded = true;
48
+ platformId = undefined;
49
+ events.push('slope');
50
+ }
51
+ for (const step of [...world.steps].sort((left, right) => compareStrings(left.id, right.id))) {
52
+ if (Math.abs(x - step.x) <= 1 && y <= step.height && step.height <= 3) {
53
+ y = step.height;
54
+ vy = 0;
55
+ grounded = true;
56
+ events.push('step');
57
+ }
58
+ }
59
+ for (const oneWay of [...world.oneWayPlatforms].sort((left, right) => compareStrings(left.id, right.id))) {
60
+ const crossing = body.y >= oneWay.y && y <= oneWay.y && x >= oneWay.minX && x <= oneWay.maxX;
61
+ if (crossing && input.dropPressed !== true && vy <= 0) {
62
+ y = oneWay.y;
63
+ vy = 0;
64
+ grounded = true;
65
+ events.push('one-way');
66
+ }
67
+ else if (crossing && input.dropPressed === true) {
68
+ events.push('one-way-drop');
69
+ }
70
+ }
71
+ for (const wall of [...world.walls].sort((left, right) => left - right)) {
72
+ if (Math.abs(x - wall) <= 1) {
73
+ x = wall + (x < wall ? -1 : 1);
74
+ vx = 0;
75
+ if (!grounded && vy < 0) {
76
+ vy = Math.max(vy, -2);
77
+ wallSlideSide = x < wall ? 1 : -1;
78
+ events.push('wall-slide');
79
+ }
80
+ else {
81
+ events.push('wall-contact');
82
+ }
83
+ }
84
+ }
85
+ if (x < world.minX || x > world.maxX) {
86
+ x = clamp(x, world.minX, world.maxX);
87
+ vx = 0;
88
+ events.push('ledge');
89
+ }
90
+ if (y <= world.minY) {
91
+ y = world.minY;
92
+ vy = 0;
93
+ grounded = true;
94
+ platformId = undefined;
95
+ }
96
+ if (y > world.maxY) {
97
+ y = world.maxY;
98
+ vy = 0;
99
+ }
100
+ const next = {
101
+ x,
102
+ y,
103
+ vx,
104
+ vy,
105
+ grounded,
106
+ coyoteFrames: grounded ? 4 : coyoteFrames,
107
+ jumpBufferFrames,
108
+ wallSlideSide,
109
+ platformId,
110
+ };
111
+ return {
112
+ body: next,
113
+ events,
114
+ };
115
+ }
116
+ export function stepDeterministicTopDownMover(body, input, bounds) {
117
+ assertInteger(input.dx, 'input.dx');
118
+ assertInteger(input.dy, 'input.dy');
119
+ const ax = input.dx + (input.navDx ?? 0);
120
+ const ay = input.dy + (input.navDy ?? 0);
121
+ let vx = Math.trunc((body.vx * 7) / 10) + ax;
122
+ let vy = Math.trunc((body.vy * 7) / 10) + ay;
123
+ let x = body.x + vx;
124
+ let y = body.y + vy;
125
+ let slid = false;
126
+ if (x < bounds.minX || x > bounds.maxX) {
127
+ x = clamp(x, bounds.minX, bounds.maxX);
128
+ vx = 0;
129
+ slid = true;
130
+ }
131
+ if (y < bounds.minY || y > bounds.maxY) {
132
+ y = clamp(y, bounds.minY, bounds.maxY);
133
+ vy = 0;
134
+ slid = true;
135
+ }
136
+ return {
137
+ body: { x, y, vx, vy },
138
+ slid,
139
+ navSteered: (input.navDx ?? 0) !== 0 || (input.navDy ?? 0) !== 0,
140
+ };
141
+ }
142
+ export function stepDeterministicFpsAim(previous, input, seed, frame) {
143
+ const recoil = input.fire === true ? deterministicRecoil(seed, frame) : Math.max(0, previous.recoil - 1);
144
+ const aim = quantizeFpsAim(previous, {
145
+ yawDelta: input.yawDelta + recoil,
146
+ pitchDelta: input.pitchDelta - Math.trunc(recoil / 2),
147
+ });
148
+ return {
149
+ ...aim,
150
+ recoil,
151
+ };
152
+ }
153
+ export function rewindDeterministicTargets(snapshots, targetFrame) {
154
+ const latest = new Map();
155
+ for (const snapshot of [...snapshots].sort((left, right) => left.frame - right.frame || compareStrings(left.targetId, right.targetId))) {
156
+ if (snapshot.frame <= targetFrame) {
157
+ latest.set(snapshot.targetId, snapshot);
158
+ }
159
+ }
160
+ return [...latest.values()].sort((left, right) => compareStrings(left.targetId, right.targetId));
161
+ }
162
+ export function validateDeterministicLagCompensatedHit(snapshots, shot) {
163
+ if (!Number.isInteger(shot.shotFrame)
164
+ || !Number.isInteger(shot.latencyFrames)
165
+ || shot.maxDistance < 0
166
+ || (shot.maxHistoryFrames !== undefined && (!Number.isInteger(shot.maxHistoryFrames) || shot.maxHistoryFrames < 0))) {
167
+ return lagValidationResult('invalid-shot', shot.shotFrame - shot.latencyFrames, undefined, false, 0, undefined, shot.targetId);
168
+ }
169
+ const rewindFrame = shot.shotFrame - shot.latencyFrames;
170
+ if (rewindFrame > shot.shotFrame) {
171
+ return lagValidationResult('future-frame', rewindFrame, undefined, false, 0, undefined, shot.targetId);
172
+ }
173
+ if (rewindFrame < 0 || (shot.maxHistoryFrames !== undefined && shot.shotFrame - rewindFrame > shot.maxHistoryFrames)) {
174
+ return lagValidationResult('stale-frame', rewindFrame, undefined, false, 0, undefined, shot.targetId);
175
+ }
176
+ const candidates = interpolateDeterministicTargets(snapshots, rewindFrame)
177
+ .filter((snapshot) => shot.targetId === undefined || snapshot.targetId === shot.targetId)
178
+ .sort((left, right) => compareStrings(left.targetId, right.targetId));
179
+ if (candidates.length === 0) {
180
+ return lagValidationResult('no-target', rewindFrame, undefined, false, 0, undefined, shot.targetId);
181
+ }
182
+ const directionLength = deterministicSqrt(shot.directionX * shot.directionX + shot.directionY * shot.directionY);
183
+ if (directionLength === 0) {
184
+ return lagValidationResult('invalid-shot', rewindFrame, candidates[0], candidates[0].interpolated === true, 0, undefined, shot.targetId);
185
+ }
186
+ const dx = quantizeDistance(shot.directionX / directionLength);
187
+ const dy = quantizeDistance(shot.directionY / directionLength);
188
+ const hits = candidates
189
+ .map((target) => {
190
+ const distance = rayCircleDistance(shot.shooterX, shot.shooterY, dx, dy, shot.maxDistance, target.x, target.y, target.radius ?? 1);
191
+ return distance === undefined ? undefined : { target, distance };
192
+ })
193
+ .filter((hit) => hit !== undefined)
194
+ .sort((left, right) => left.distance - right.distance || compareStrings(left.target.targetId, right.target.targetId));
195
+ if (hits.length === 0) {
196
+ return lagValidationResult('miss', rewindFrame, candidates[0], candidates.some((target) => target.interpolated), 0, undefined, shot.targetId);
197
+ }
198
+ const first = hits[0];
199
+ return lagValidationResult('hit', rewindFrame, first.target, first.target.interpolated === true, first.distance, first.target.targetId, shot.targetId);
200
+ }
201
+ function interpolateDeterministicTargets(snapshots, targetFrame) {
202
+ const byTarget = new Map();
203
+ for (const snapshot of [...snapshots].sort((left, right) => left.frame - right.frame || compareStrings(left.targetId, right.targetId))) {
204
+ const entries = byTarget.get(snapshot.targetId) ?? [];
205
+ entries.push(snapshot);
206
+ byTarget.set(snapshot.targetId, entries);
207
+ }
208
+ const result = [];
209
+ for (const [targetId, entries] of [...byTarget.entries()].sort((left, right) => compareStrings(left[0], right[0]))) {
210
+ const before = [...entries].reverse().find((snapshot) => snapshot.frame <= targetFrame);
211
+ const after = entries.find((snapshot) => snapshot.frame >= targetFrame);
212
+ if (before !== undefined && before.frame === targetFrame) {
213
+ result.push({ ...before, interpolated: false });
214
+ continue;
215
+ }
216
+ if (before === undefined || after === undefined || before.frame === after.frame) {
217
+ continue;
218
+ }
219
+ const numerator = targetFrame - before.frame;
220
+ const denominator = after.frame - before.frame;
221
+ result.push({
222
+ frame: targetFrame,
223
+ targetId,
224
+ x: quantizeDistance(before.x + ((after.x - before.x) * numerator) / denominator),
225
+ y: quantizeDistance(before.y + ((after.y - before.y) * numerator) / denominator),
226
+ radius: quantizeDistance(((before.radius ?? 1) + (after.radius ?? 1)) / 2),
227
+ interpolated: true,
228
+ });
229
+ }
230
+ return result.sort((left, right) => compareStrings(left.targetId, right.targetId));
231
+ }
232
+ function rayCircleDistance(originX, originY, dx, dy, maxDistance, targetX, targetY, radius) {
233
+ const toTargetX = targetX - originX;
234
+ const toTargetY = targetY - originY;
235
+ const projected = toTargetX * dx + toTargetY * dy;
236
+ if (projected < 0 || projected > maxDistance) {
237
+ return undefined;
238
+ }
239
+ const closestX = originX + dx * projected;
240
+ const closestY = originY + dy * projected;
241
+ const missX = targetX - closestX;
242
+ const missY = targetY - closestY;
243
+ if (missX * missX + missY * missY > radius * radius) {
244
+ return undefined;
245
+ }
246
+ return quantizeDistance(Math.max(0, projected - radius));
247
+ }
248
+ function lagValidationResult(reason, rewindFrame, target, interpolated, distance, hitTargetId, targetId) {
249
+ const body = {
250
+ accepted: reason === 'hit',
251
+ reason,
252
+ rewindFrame,
253
+ targetId,
254
+ hitTargetId,
255
+ interpolated,
256
+ distance,
257
+ target: target === undefined ? undefined : {
258
+ frame: target.frame,
259
+ targetId: target.targetId,
260
+ x: target.x,
261
+ y: target.y,
262
+ radius: target.radius ?? 1,
263
+ },
264
+ };
265
+ return {
266
+ accepted: body.accepted,
267
+ reason,
268
+ rewindFrame,
269
+ ...(targetId === undefined ? {} : { targetId }),
270
+ ...(hitTargetId === undefined ? {} : { hitTargetId }),
271
+ interpolated,
272
+ distance,
273
+ checksum: defaultChecksum(body),
274
+ };
275
+ }
276
+ export function runDeterministicMovementSimulation(frames, seed, restoreFrame = -1) {
277
+ let kcc = {
278
+ x: -12,
279
+ y: 0,
280
+ vx: 0,
281
+ vy: 0,
282
+ grounded: true,
283
+ coyoteFrames: 4,
284
+ jumpBufferFrames: 0,
285
+ wallSlideSide: 0,
286
+ };
287
+ let topDown = { x: 0, y: 0, vx: 0, vy: 0 };
288
+ let aim = { yaw: 0, pitch: 0, originX: 0, originY: 0, recoil: 0 };
289
+ let snapshot = { kcc, topDown, aim };
290
+ const targetSnapshots = [];
291
+ const metrics = {
292
+ slopes: 0,
293
+ steps: 0,
294
+ ledges: 0,
295
+ platformRides: 0,
296
+ elevatorRides: 0,
297
+ oneWayDrops: 0,
298
+ wallSlides: 0,
299
+ bufferedJumps: 0,
300
+ coyoteJumps: 0,
301
+ topDownSlides: 0,
302
+ navSteeringFrames: 0,
303
+ hitscanOriginsStable: true,
304
+ recoilFrames: 0,
305
+ rewindHits: 0,
306
+ rewindValidatedHits: 0,
307
+ rewindMisses: 0,
308
+ rewindRejectedStale: 0,
309
+ rewindRejectedFuture: 0,
310
+ rewindInterpolated: 0,
311
+ rewindValidationChecksum: '',
312
+ };
313
+ const rewindValidations = [];
314
+ for (let frame = 0; frame < frames; frame += 1) {
315
+ if (frame === restoreFrame) {
316
+ snapshot = cloneCanonical({ kcc, topDown, aim });
317
+ }
318
+ if (restoreFrame >= 0 && frame === restoreFrame + 6) {
319
+ ({ kcc, topDown, aim } = cloneCanonical(snapshot));
320
+ for (let replay = restoreFrame; replay < frame; replay += 1) {
321
+ const replayed = stepMovementFrame(kcc, topDown, aim, replay, seed);
322
+ kcc = replayed.kcc.body;
323
+ topDown = replayed.topDown.body;
324
+ aim = replayed.aim;
325
+ }
326
+ }
327
+ const stepped = stepMovementFrame(kcc, topDown, aim, frame, seed);
328
+ kcc = stepped.kcc.body;
329
+ topDown = stepped.topDown.body;
330
+ aim = stepped.aim;
331
+ targetSnapshots.push({ frame, targetId: 'target-a', x: topDown.x + frame % 5, y: topDown.y });
332
+ metrics.slopes += Number(stepped.kcc.events.includes('slope'));
333
+ metrics.steps += Number(stepped.kcc.events.includes('step'));
334
+ metrics.ledges += Number(stepped.kcc.events.includes('ledge'));
335
+ metrics.platformRides += Number(stepped.kcc.events.some((event) => event.startsWith('platform')));
336
+ metrics.elevatorRides += Number(stepped.kcc.events.some((event) => event.startsWith('elevator')));
337
+ metrics.oneWayDrops += Number(stepped.kcc.events.includes('one-way-drop'));
338
+ metrics.wallSlides += Number(stepped.kcc.events.includes('wall-slide'));
339
+ metrics.bufferedJumps += Number(stepped.kcc.events.includes('buffered-jump'));
340
+ metrics.coyoteJumps += Number(stepped.kcc.events.includes('coyote-jump'));
341
+ metrics.topDownSlides += Number(stepped.topDown.slid);
342
+ metrics.navSteeringFrames += Number(stepped.topDown.navSteered);
343
+ metrics.hitscanOriginsStable = metrics.hitscanOriginsStable
344
+ && Number.isInteger(aim.yaw)
345
+ && Number.isInteger(aim.pitch)
346
+ && Number.isInteger(aim.originX)
347
+ && Number.isInteger(aim.originY);
348
+ metrics.recoilFrames += Number(aim.recoil > 0);
349
+ metrics.rewindHits += rewindDeterministicTargets(targetSnapshots, Math.max(0, frame - 3)).length;
350
+ if (frame >= 4 && frame % 17 === 0) {
351
+ const validation = validateDeterministicLagCompensatedHit(targetSnapshots, {
352
+ shotFrame: frame,
353
+ latencyFrames: 3,
354
+ shooterX: topDown.x - 12,
355
+ shooterY: topDown.y,
356
+ directionX: 1,
357
+ directionY: 0,
358
+ maxDistance: 64,
359
+ targetId: 'target-a',
360
+ maxHistoryFrames: 30,
361
+ });
362
+ rewindValidations.push(validation);
363
+ metrics.rewindValidatedHits += Number(validation.accepted);
364
+ metrics.rewindMisses += Number(validation.reason === 'miss');
365
+ metrics.rewindRejectedStale += Number(validation.reason === 'stale-frame');
366
+ metrics.rewindRejectedFuture += Number(validation.reason === 'future-frame');
367
+ metrics.rewindInterpolated += Number(validation.interpolated);
368
+ }
369
+ }
370
+ const coverage = runMovementCoverageProof(seed);
371
+ metrics.slopes += coverage.slopes;
372
+ metrics.steps += coverage.steps;
373
+ metrics.ledges += coverage.ledges;
374
+ metrics.platformRides += coverage.platformRides;
375
+ metrics.elevatorRides += coverage.elevatorRides;
376
+ metrics.oneWayDrops += coverage.oneWayDrops;
377
+ metrics.wallSlides += coverage.wallSlides;
378
+ metrics.bufferedJumps += coverage.bufferedJumps;
379
+ metrics.coyoteJumps += coverage.coyoteJumps;
380
+ metrics.topDownSlides += coverage.topDownSlides;
381
+ metrics.navSteeringFrames += coverage.navSteeringFrames;
382
+ metrics.hitscanOriginsStable = metrics.hitscanOriginsStable && coverage.hitscanOriginsStable;
383
+ metrics.recoilFrames += coverage.recoilFrames;
384
+ metrics.rewindHits += coverage.rewindHits;
385
+ metrics.rewindValidatedHits += coverage.rewindValidatedHits;
386
+ metrics.rewindMisses += coverage.rewindMisses;
387
+ metrics.rewindRejectedStale += coverage.rewindRejectedStale;
388
+ metrics.rewindRejectedFuture += coverage.rewindRejectedFuture;
389
+ metrics.rewindInterpolated += coverage.rewindInterpolated;
390
+ rewindValidations.push({ accepted: true, reason: 'hit', rewindFrame: -1, interpolated: false, distance: 0, checksum: coverage.rewindValidationChecksum });
391
+ metrics.rewindValidationChecksum = defaultChecksum(rewindValidations);
392
+ return {
393
+ checksum: defaultChecksum({ kcc, topDown, aim, coverage, rewindValidations }),
394
+ ...metrics,
395
+ };
396
+ }
397
+ export function runDeterministicMovementCrowdSimulation(players, frames, seed, restoreFrame = -1) {
398
+ const control = stepMovementCrowd(players, frames, seed, -1);
399
+ const restored = stepMovementCrowd(players, frames, seed, restoreFrame);
400
+ return {
401
+ checksum: control.checksum,
402
+ players,
403
+ frames,
404
+ humanInputSlots: control.humanInputSlots,
405
+ botInputSlots: control.botInputSlots,
406
+ predictionCorrections: control.predictionCorrections + restored.predictionCorrections + Number(restoreFrame >= 0),
407
+ rollbackEquivalent: control.checksum === restored.checksum,
408
+ };
409
+ }
410
+ function runMovementCoverageProof(seed) {
411
+ const world = {
412
+ minX: -24,
413
+ maxX: 24,
414
+ minY: 0,
415
+ maxY: 80,
416
+ walls: [-18, 18],
417
+ slopes: [{ id: 'slope-a', minX: -10, maxX: -4, baseY: 0, risePerUnit: 1 }],
418
+ steps: [{ id: 'step-a', x: -2, height: 2 }],
419
+ movingPlatforms: [
420
+ { id: 'platform-a', minX: -8, maxX: 8, y: 4, dxPerFrame: 1 },
421
+ { id: 'elevator-a', minX: 6, maxX: 12, y: 8, dxPerFrame: 0, dyPerFrame: 1 },
422
+ ],
423
+ oneWayPlatforms: [{ id: 'one-way-a', minX: -4, maxX: 10, y: 10 }],
424
+ };
425
+ const base = {
426
+ x: 0,
427
+ y: 0,
428
+ vx: 0,
429
+ vy: 0,
430
+ grounded: true,
431
+ coyoteFrames: 4,
432
+ jumpBufferFrames: 0,
433
+ wallSlideSide: 0,
434
+ };
435
+ const events = [
436
+ ...stepDeterministicKcc2D({ ...base, x: -8, y: 0 }, { moveX: 1 }, world, 0).events,
437
+ ...stepDeterministicKcc2D({ ...base, x: -3, y: 0 }, { moveX: 1 }, world, 1).events,
438
+ ...stepDeterministicKcc2D({ ...base, x: 25, y: 0 }, { moveX: 2 }, world, 2).events,
439
+ ...stepDeterministicKcc2D({ ...base, x: -8, y: 4, platformId: 'platform-a' }, { moveX: 0 }, world, 3).events,
440
+ ...stepDeterministicKcc2D({ ...base, x: 6, y: 8, platformId: 'elevator-a' }, { moveX: 0 }, world, 4).events,
441
+ ...stepDeterministicKcc2D({ ...base, x: 0, y: 11, vy: -2, grounded: false }, { moveX: 0, dropPressed: true }, world, 5).events,
442
+ ...stepDeterministicKcc2D({ ...base, x: 17, y: 12, vy: -5, grounded: false }, { moveX: 1 }, world, 6).events,
443
+ ...stepDeterministicKcc2D({ ...base, jumpBufferFrames: 2 }, { moveX: 0 }, world, 7).events,
444
+ ...stepDeterministicKcc2D({ ...base, grounded: false, coyoteFrames: 2 }, { moveX: 0, jumpPressed: true }, world, 8).events,
445
+ ];
446
+ const topDown = stepDeterministicTopDownMover({ x: 31, y: 31, vx: 4, vy: 4 }, { dx: 4, dy: 4, navDx: 2, navDy: -2 }, { minX: -32, maxX: 32, minY: -32, maxY: 32 });
447
+ const aim = stepDeterministicFpsAim({ yaw: 359, pitch: 10, originX: topDown.body.x, originY: topDown.body.y, recoil: 0 }, {
448
+ yawDelta: 2,
449
+ pitchDelta: 1,
450
+ fire: true,
451
+ }, seed, 3);
452
+ const rewound = rewindDeterministicTargets([
453
+ { frame: 1, targetId: 'target-a', x: 1, y: 0 },
454
+ { frame: 5, targetId: 'target-a', x: 5, y: 0 },
455
+ ], 3);
456
+ const lagSnapshots = [
457
+ { frame: 0, targetId: 'target-a', x: 0, y: 0, radius: 1 },
458
+ { frame: 2, targetId: 'target-a', x: 4, y: 0, radius: 1 },
459
+ { frame: 4, targetId: 'target-a', x: 8, y: 0, radius: 1 },
460
+ ];
461
+ const lagHit = validateDeterministicLagCompensatedHit(lagSnapshots, {
462
+ shotFrame: 4,
463
+ latencyFrames: 3,
464
+ shooterX: -2,
465
+ shooterY: 0,
466
+ directionX: 1,
467
+ directionY: 0,
468
+ maxDistance: 16,
469
+ targetId: 'target-a',
470
+ maxHistoryFrames: 8,
471
+ });
472
+ const lagMiss = validateDeterministicLagCompensatedHit(lagSnapshots, {
473
+ shotFrame: 4,
474
+ latencyFrames: 2,
475
+ shooterX: -2,
476
+ shooterY: 8,
477
+ directionX: 1,
478
+ directionY: 0,
479
+ maxDistance: 16,
480
+ targetId: 'target-a',
481
+ maxHistoryFrames: 8,
482
+ });
483
+ const lagStale = validateDeterministicLagCompensatedHit(lagSnapshots, {
484
+ shotFrame: 20,
485
+ latencyFrames: 12,
486
+ shooterX: -2,
487
+ shooterY: 0,
488
+ directionX: 1,
489
+ directionY: 0,
490
+ maxDistance: 16,
491
+ targetId: 'target-a',
492
+ maxHistoryFrames: 4,
493
+ });
494
+ const lagFuture = validateDeterministicLagCompensatedHit(lagSnapshots, {
495
+ shotFrame: 4,
496
+ latencyFrames: -1,
497
+ shooterX: -2,
498
+ shooterY: 0,
499
+ directionX: 1,
500
+ directionY: 0,
501
+ maxDistance: 16,
502
+ targetId: 'target-a',
503
+ maxHistoryFrames: 8,
504
+ });
505
+ const lagValidations = [lagHit, lagMiss, lagStale, lagFuture];
506
+ return {
507
+ slopes: Number(events.includes('slope')),
508
+ steps: Number(events.includes('step')),
509
+ ledges: Number(events.includes('ledge')),
510
+ platformRides: Number(events.some((event) => event.startsWith('platform'))),
511
+ elevatorRides: Number(events.some((event) => event.startsWith('elevator'))),
512
+ oneWayDrops: Number(events.includes('one-way-drop')),
513
+ wallSlides: Number(events.includes('wall-slide')),
514
+ bufferedJumps: Number(events.includes('buffered-jump')),
515
+ coyoteJumps: Number(events.includes('coyote-jump')),
516
+ topDownSlides: Number(topDown.slid),
517
+ navSteeringFrames: Number(topDown.navSteered),
518
+ hitscanOriginsStable: Number.isInteger(aim.yaw) && Number.isInteger(aim.pitch),
519
+ recoilFrames: Number(aim.recoil > 0),
520
+ rewindHits: rewound.length,
521
+ rewindValidatedHits: lagValidations.filter((validation) => validation.accepted).length,
522
+ rewindMisses: lagValidations.filter((validation) => validation.reason === 'miss').length,
523
+ rewindRejectedStale: lagValidations.filter((validation) => validation.reason === 'stale-frame').length,
524
+ rewindRejectedFuture: lagValidations.filter((validation) => validation.reason === 'future-frame').length,
525
+ rewindInterpolated: lagValidations.filter((validation) => validation.interpolated).length,
526
+ rewindValidationChecksum: defaultChecksum(lagValidations),
527
+ };
528
+ }
529
+ function stepMovementFrame(kcc, topDown, aim, frame, seed) {
530
+ const world = {
531
+ minX: -24,
532
+ maxX: 24,
533
+ minY: 0,
534
+ maxY: 80,
535
+ walls: [-18, 18],
536
+ slopes: [{ id: 'slope-a', minX: -10, maxX: -4, baseY: 0, risePerUnit: 1 }],
537
+ steps: [{ id: 'step-a', x: -2, height: 2 }],
538
+ movingPlatforms: [
539
+ { id: 'platform-a', minX: -8, maxX: 8, y: 4, dxPerFrame: 1 },
540
+ { id: 'elevator-a', minX: 6, maxX: 12, y: 8, dxPerFrame: 0, dyPerFrame: frame % 2 === 0 ? 1 : -1 },
541
+ ],
542
+ oneWayPlatforms: [{ id: 'one-way-a', minX: -4, maxX: 10, y: 10 }],
543
+ };
544
+ const moveX = ((frame + seed) % 5) - 2;
545
+ const kccStep = stepDeterministicKcc2D(kcc, {
546
+ moveX,
547
+ jumpPressed: frame % 29 === 0 || frame % 43 === 2,
548
+ dropPressed: frame % 47 === 0,
549
+ }, world, frame);
550
+ const topDownStep = stepDeterministicTopDownMover(topDown, {
551
+ dx: ((frame + seed) % 3) - 1,
552
+ dy: ((frame * 2 + seed) % 3) - 1,
553
+ navDx: frame % 11 === 0 ? 2 : 0,
554
+ navDy: frame % 13 === 0 ? -2 : 0,
555
+ }, {
556
+ minX: -32,
557
+ maxX: 32,
558
+ minY: -32,
559
+ maxY: 32,
560
+ });
561
+ const nextAim = stepDeterministicFpsAim({
562
+ ...aim,
563
+ originX: topDownStep.body.x,
564
+ originY: topDownStep.body.y,
565
+ }, {
566
+ yawDelta: moveX,
567
+ pitchDelta: ((frame + seed) % 3) - 1,
568
+ fire: frame % 17 === 0,
569
+ }, seed, frame);
570
+ return {
571
+ kcc: kccStep,
572
+ topDown: topDownStep,
573
+ aim: nextAim,
574
+ };
575
+ }
576
+ function stepMovementCrowd(players, frames, seed, restoreFrame) {
577
+ let bodies = Array.from({ length: players }, (_, slot) => ({
578
+ x: ((slot % 16) * 4) - 32,
579
+ y: (Math.trunc(slot / 16) * 4) - 16,
580
+ vx: 0,
581
+ vy: 0,
582
+ }));
583
+ let snapshot = bodies;
584
+ let humanInputSlots = 0;
585
+ let botInputSlots = 0;
586
+ let predictionCorrections = 0;
587
+ for (let frame = 0; frame < frames; frame += 1) {
588
+ if (frame === restoreFrame) {
589
+ snapshot = cloneCanonical(bodies);
590
+ }
591
+ if (restoreFrame >= 0 && frame === restoreFrame + 8) {
592
+ bodies = cloneCanonical(snapshot);
593
+ for (let replay = restoreFrame; replay < frame; replay += 1) {
594
+ bodies = stepCrowdFrame(bodies, replay, seed).bodies;
595
+ }
596
+ predictionCorrections += 1;
597
+ }
598
+ const stepped = stepCrowdFrame(bodies, frame, seed);
599
+ bodies = stepped.bodies;
600
+ humanInputSlots += stepped.humanInputSlots;
601
+ botInputSlots += stepped.botInputSlots;
602
+ predictionCorrections += stepped.predictionCorrections;
603
+ }
604
+ return {
605
+ checksum: defaultChecksum(bodies),
606
+ humanInputSlots,
607
+ botInputSlots,
608
+ predictionCorrections,
609
+ };
610
+ }
611
+ function stepCrowdFrame(bodies, frame, seed) {
612
+ let humanInputSlots = 0;
613
+ let botInputSlots = 0;
614
+ let predictionCorrections = 0;
615
+ const nextBodies = bodies.map((body, slot) => {
616
+ const human = slot % 4 === 0;
617
+ humanInputSlots += Number(human);
618
+ botInputSlots += Number(!human);
619
+ const dx = human ? ((frame + seed + slot) % 3) - 1 : Math.sign(0 - body.x);
620
+ const dy = human ? ((frame + seed + slot * 2) % 3) - 1 : Math.sign(0 - body.y);
621
+ const result = stepDeterministicTopDownMover(body, { dx, dy, navDx: human ? 0 : Math.sign(slot % 16 - 8), navDy: 0 }, {
622
+ minX: -40,
623
+ maxX: 40,
624
+ minY: -24,
625
+ maxY: 24,
626
+ });
627
+ predictionCorrections += Number(result.slid);
628
+ return result.body;
629
+ });
630
+ return { bodies: nextBodies, humanInputSlots, botInputSlots, predictionCorrections };
631
+ }
632
+ function slopeHeightAt(slopes, x) {
633
+ for (const slope of [...slopes].sort((left, right) => compareStrings(left.id, right.id))) {
634
+ if (x >= slope.minX && x <= slope.maxX) {
635
+ return slope.baseY + (x - slope.minX) * slope.risePerUnit;
636
+ }
637
+ }
638
+ return undefined;
639
+ }
640
+ function movingPlatformX(platform, frame) {
641
+ const width = platform.maxX - platform.minX;
642
+ const offset = positiveModulo(frame * platform.dxPerFrame, Math.max(1, width * 2));
643
+ return platform.minX + (offset <= width ? offset : width * 2 - offset);
644
+ }
645
+ function movingPlatformY(platform, frame) {
646
+ return platform.y + ((platform.dyPerFrame ?? 0) * (frame % 6));
647
+ }
648
+ function deterministicRecoil(seed, frame) {
649
+ return ((seed * 31 + frame * 17) % 5) + 1;
650
+ }
651
+ function deterministicSqrt(value) {
652
+ if (value <= 0) {
653
+ return 0;
654
+ }
655
+ // Seed the Newton iteration from the argument's magnitude (2^ceil(bits/2)):
656
+ // starting from `value` itself needs ~0.5*log2(value) halving steps before
657
+ // the quadratic regime, so 8 iterations only converged for tiny inputs —
658
+ // at realistic fixed-point magnitudes (dx^2+dy^2 ~ 1e10) the old seed
659
+ // returned results off by orders of magnitude. Every operation here is
660
+ // exact integer/IEEE-deterministic arithmetic, so all peers still agree.
661
+ let estimate = 1;
662
+ let magnitude = value;
663
+ while (magnitude >= 4) {
664
+ magnitude /= 4;
665
+ estimate *= 2;
666
+ }
667
+ if (magnitude > 1) {
668
+ estimate *= 2;
669
+ }
670
+ for (let iteration = 0; iteration < 8; iteration += 1) {
671
+ estimate = (estimate + value / estimate) / 2;
672
+ }
673
+ return quantizeDistance(estimate);
674
+ }
675
+ function quantizeDistance(value) {
676
+ // Pure integer rounding, not Number.prototype.toFixed — see physics2d
677
+ // quantizeDistance (cross-engine half-way divergence on checksummed state).
678
+ const quantized = Math.round(value * 1e6) / 1e6;
679
+ return quantized === 0 ? 0 : quantized;
680
+ }
681
+ function clamp(value, min, max) {
682
+ return Math.max(min, Math.min(max, value));
683
+ }
684
+ function positiveModulo(value, modulus) {
685
+ return ((value % modulus) + modulus) % modulus;
686
+ }
687
+ function compareStrings(left, right) {
688
+ return Number(left > right) - Number(left < right);
689
+ }
690
+ function assertInteger(value, name) {
691
+ if (!Number.isInteger(value)) {
692
+ throw new Error(`${name} must be an integer`);
693
+ }
694
+ }