@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,1238 @@
1
+ import { defaultChecksum } from './canonical.js';
2
+ import { createDeterministicPhysicsWorld3D, overlapDeterministicPhysics3D, } from './physics3d.js';
3
+ export function stepDeterministicKcc3D(body, input, world, options) {
4
+ let next = applyProcessors(body, input, options.frame, options.processors ?? []);
5
+ const events = [];
6
+ let correctionCount = 0;
7
+ let physicsQueryCount = 0;
8
+ let collisionFilterCalls = 0;
9
+ let collisionFilterRejectedHits = 0;
10
+ const externalImpulse = combineExternalKccImpulses(options.externalImpulses ?? []);
11
+ let coyoteFrames = next.grounded ? 4 : Math.max(0, next.coyoteFrames - 1);
12
+ let jumpBufferFrames = input.jumpPressed === true ? 4 : Math.max(0, next.jumpBufferFrames - 1);
13
+ let vx = input.moveX + externalImpulse.vx;
14
+ let vz = input.moveZ + externalImpulse.vz;
15
+ let vy = next.vy - 1 + externalImpulse.vy;
16
+ let x = next.x + vx;
17
+ let y = next.y + vy;
18
+ let z = next.z + vz;
19
+ let grounded = false;
20
+ let platformId = next.platformId;
21
+ if (externalImpulse.applied) {
22
+ events.push('external-impulse');
23
+ }
24
+ for (const platform of [...(world.movingPlatforms ?? [])].sort((left, right) => compareStrings(left.id, right.id))) {
25
+ if (next.grounded && next.platformId === platform.id) {
26
+ x += platform.dxPerFrame;
27
+ z += platform.dzPerFrame;
28
+ vx += platform.dxPerFrame;
29
+ vz += platform.dzPerFrame;
30
+ events.push('platform-transfer');
31
+ events.push('platform-velocity-transfer');
32
+ }
33
+ if (x >= platform.minX && x <= platform.maxX && z >= platform.minZ && z <= platform.maxZ && y <= platform.y && vy <= 0) {
34
+ y = platform.y;
35
+ vy = 0;
36
+ grounded = true;
37
+ platformId = platform.id;
38
+ events.push('moving-platform');
39
+ }
40
+ }
41
+ const slopeY = slopeHeightAt(world.slopes ?? [], x, z, world.maxSlopeRisePerUnit);
42
+ if (slopeY !== undefined && y <= slopeY + world.stepHeight && vy <= 0) {
43
+ y = slopeY;
44
+ vy = 0;
45
+ grounded = true;
46
+ platformId = undefined;
47
+ events.push('slope');
48
+ }
49
+ const step = findStepAt(world.steps ?? [], x, z);
50
+ const geometry = normalizeKccGeometry3D(next);
51
+ if (step !== undefined && step.height <= world.stepHeight && y <= step.height + 1 && vy <= 0) {
52
+ if (step.ceilingY !== undefined && step.ceilingY - step.height < geometry.halfHeight * 2 + geometry.skinWidth) {
53
+ x = next.x;
54
+ z = next.z;
55
+ vx = 0;
56
+ vz = 0;
57
+ correctionCount += 1;
58
+ events.push('step-head-blocked');
59
+ }
60
+ else {
61
+ y = step.height;
62
+ vy = 0;
63
+ grounded = true;
64
+ platformId = undefined;
65
+ events.push('step');
66
+ }
67
+ }
68
+ if (jumpBufferFrames > 0 && (next.grounded || next.coyoteFrames > 0 || grounded)) {
69
+ vy = 10;
70
+ y += 1;
71
+ grounded = false;
72
+ platformId = undefined;
73
+ jumpBufferFrames = 0;
74
+ coyoteFrames = 0;
75
+ events.push(next.grounded ? 'jump' : 'coyote-jump');
76
+ }
77
+ if (options.physics !== undefined) {
78
+ const resolved = resolvePhysicsSlide3D(options.physics, next, { x, y, z, vx, vz }, new Set(options.ignoredPhysicsBodyIds ?? []), options.frame, options.collisionFilters ?? []);
79
+ physicsQueryCount += resolved.queryCount;
80
+ collisionFilterCalls += resolved.collisionFilterCalls;
81
+ collisionFilterRejectedHits += resolved.collisionFilterRejectedHits;
82
+ if (resolved.corrected) {
83
+ x = resolved.x;
84
+ z = resolved.z;
85
+ vx = resolved.vx;
86
+ vz = resolved.vz;
87
+ correctionCount += resolved.correctionCount;
88
+ events.push(resolved.swept ? 'physics-sweep' : resolved.slid ? 'physics-slide' : 'physics-correction');
89
+ }
90
+ }
91
+ const boundsCorrection = clampBodyToBounds({ x, y, z, vx, vy, vz, grounded, coyoteFrames, jumpBufferFrames, platformId }, world);
92
+ if (boundsCorrection.corrected) {
93
+ correctionCount += boundsCorrection.corrections;
94
+ events.push(...boundsCorrection.events);
95
+ }
96
+ next = boundsCorrection.body;
97
+ const finalBody = {
98
+ ...next,
99
+ coyoteFrames: next.grounded ? 4 : coyoteFrames,
100
+ jumpBufferFrames,
101
+ };
102
+ return {
103
+ body: finalBody,
104
+ events,
105
+ correctionCount,
106
+ physicsQueryCount,
107
+ collisionFilterCalls,
108
+ collisionFilterRejectedHits,
109
+ checksum: defaultChecksum({ body: finalBody, events, correctionCount, physicsQueryCount, collisionFilterCalls, collisionFilterRejectedHits }),
110
+ };
111
+ }
112
+ export function runDeterministicKcc3DFilterForceFixture() {
113
+ const first = runKcc3DFilterForceCases();
114
+ const second = runKcc3DFilterForceCases();
115
+ return {
116
+ ...first,
117
+ restoredChecksum: second.checksum,
118
+ rollbackEquivalent: first.checksum === second.checksum,
119
+ };
120
+ }
121
+ export function runDeterministicKcc3DCallbackFilterFixture() {
122
+ const first = runKcc3DCallbackFilterCases();
123
+ const second = runKcc3DCallbackFilterCases();
124
+ return {
125
+ ...first,
126
+ restoredChecksum: second.checksum,
127
+ rollbackEquivalent: first.checksum === second.checksum,
128
+ };
129
+ }
130
+ function runKcc3DFilterForceCases() {
131
+ const base = {
132
+ x: 0,
133
+ y: 0,
134
+ z: 0,
135
+ vx: 0,
136
+ vy: 0,
137
+ vz: 0,
138
+ grounded: true,
139
+ coyoteFrames: 4,
140
+ jumpBufferFrames: 0,
141
+ };
142
+ const world = {
143
+ minX: -8,
144
+ maxX: 8,
145
+ minY: 0,
146
+ maxY: 8,
147
+ minZ: -8,
148
+ maxZ: 8,
149
+ maxSlopeRisePerUnit: 1,
150
+ stepHeight: 2,
151
+ };
152
+ const physics = createDeterministicPhysicsWorld3D([
153
+ {
154
+ id: 'ignored-wall',
155
+ kind: 'static',
156
+ x: 1,
157
+ y: 0,
158
+ z: 0,
159
+ vx: 0,
160
+ vy: 0,
161
+ vz: 0,
162
+ shape: { type: 'box', halfX: 0.5, halfY: 3, halfZ: 3 },
163
+ layer: 1,
164
+ mask: 1,
165
+ },
166
+ {
167
+ id: 'solid-wall',
168
+ kind: 'static',
169
+ x: 3,
170
+ y: 0,
171
+ z: 0,
172
+ vx: 0,
173
+ vy: 0,
174
+ vz: 0,
175
+ shape: { type: 'box', halfX: 0.25, halfY: 3, halfZ: 3 },
176
+ layer: 1,
177
+ mask: 1,
178
+ },
179
+ ]);
180
+ const ignored = stepDeterministicKcc3D(base, { moveX: 1, moveZ: 0 }, world, { frame: 0, physics, ignoredPhysicsBodyIds: ['ignored-wall'] });
181
+ const solid = stepDeterministicKcc3D({ ...ignored.body, x: 1, z: 0 }, { moveX: 4, moveZ: 0 }, world, { frame: 1, physics, ignoredPhysicsBodyIds: ['ignored-wall'] });
182
+ const impulse = stepDeterministicKcc3D(base, { moveX: 0, moveZ: 0 }, world, {
183
+ frame: 2,
184
+ externalImpulses: [
185
+ { id: 'wind-z', vx: 0, vy: 0, vz: 2 },
186
+ { id: 'dash-x', vx: 2, vy: 0, vz: 0 },
187
+ ],
188
+ });
189
+ const permutedImpulse = stepDeterministicKcc3D(base, { moveX: 0, moveZ: 0 }, world, {
190
+ frame: 2,
191
+ externalImpulses: [
192
+ { id: 'dash-x', vx: 2, vy: 0, vz: 0 },
193
+ { id: 'wind-z', vx: 0, vy: 0, vz: 2 },
194
+ ],
195
+ });
196
+ const metrics = {
197
+ ignoredColliderPassedThrough: ignored.body.x === 1 && ignored.correctionCount === 0 && countKccEventsWithPrefix(ignored.events, 'physics') === 0,
198
+ solidColliderBlocked: solid.body.x < 3 && solid.events.includes('physics-sweep'),
199
+ externalImpulseApplied: impulse.events.includes('external-impulse') && impulse.body.x === 2 && impulse.body.z === 2,
200
+ filterOrderStable: impulse.checksum === permutedImpulse.checksum,
201
+ ignoredCollisionEvents: countKccEventsWithPrefix(ignored.events, 'physics'),
202
+ solidCollisionEvents: countKccEventsWithPrefix(solid.events, 'physics'),
203
+ externalImpulseEvents: countKccEventsEqualTo(impulse.events, 'external-impulse'),
204
+ };
205
+ return {
206
+ ...metrics,
207
+ checksum: defaultChecksum({ metrics, ignored, solid, impulse, permutedImpulse }),
208
+ };
209
+ }
210
+ function runKcc3DCallbackFilterCases() {
211
+ const base = {
212
+ x: 0,
213
+ y: 0,
214
+ z: 0,
215
+ vx: 0,
216
+ vy: 0,
217
+ vz: 0,
218
+ grounded: true,
219
+ coyoteFrames: 4,
220
+ jumpBufferFrames: 0,
221
+ };
222
+ const world = {
223
+ minX: -8,
224
+ maxX: 8,
225
+ minY: 0,
226
+ maxY: 8,
227
+ minZ: -8,
228
+ maxZ: 8,
229
+ maxSlopeRisePerUnit: 1,
230
+ stepHeight: 2,
231
+ };
232
+ const ignoredPhysics = createDeterministicPhysicsWorld3D([
233
+ {
234
+ id: 'callback-ignored-layer',
235
+ kind: 'static',
236
+ x: 1,
237
+ y: 0,
238
+ z: 0,
239
+ vx: 0,
240
+ vy: 0,
241
+ vz: 0,
242
+ shape: { type: 'box', halfX: 0.5, halfY: 3, halfZ: 3 },
243
+ layer: 2,
244
+ mask: 1,
245
+ },
246
+ ]);
247
+ const solidPhysics = createDeterministicPhysicsWorld3D([
248
+ {
249
+ id: 'callback-solid',
250
+ kind: 'static',
251
+ x: 3,
252
+ y: 0,
253
+ z: 0,
254
+ vx: 0,
255
+ vy: 0,
256
+ vz: 0,
257
+ shape: { type: 'box', halfX: 0.25, halfY: 3, halfZ: 3 },
258
+ layer: 1,
259
+ mask: 1,
260
+ },
261
+ ]);
262
+ const normalPhysics = createDeterministicPhysicsWorld3D([
263
+ {
264
+ id: 'callback-normal-gate',
265
+ kind: 'static',
266
+ x: 0,
267
+ y: 0,
268
+ z: 1,
269
+ vx: 0,
270
+ vy: 0,
271
+ vz: 0,
272
+ shape: { type: 'box', halfX: 3, halfY: 3, halfZ: 0.5 },
273
+ layer: 4,
274
+ mask: 1,
275
+ },
276
+ ]);
277
+ const layerFilter = {
278
+ id: 'layer-filter',
279
+ order: 1,
280
+ shouldCollide(candidate) {
281
+ return !(candidate.bodyId === 'callback-ignored-layer'
282
+ && candidate.layer === 2
283
+ && candidate.mask === 1
284
+ && candidate.attemptedX >= 0);
285
+ },
286
+ };
287
+ const normalFilter = {
288
+ id: 'normal-filter',
289
+ order: 2,
290
+ shouldCollide(candidate) {
291
+ return !(candidate.bodyId === 'callback-normal-gate'
292
+ && candidate.layer === 4
293
+ && candidate.normalZ === -1
294
+ && candidate.frame === 2);
295
+ },
296
+ };
297
+ const metadataFilter = {
298
+ id: 'metadata-filter',
299
+ order: 0,
300
+ shouldCollide(candidate) {
301
+ return Number.isFinite(candidate.attemptedY)
302
+ && candidate.previousY >= 0
303
+ && Math.abs(candidate.normalX) + Math.abs(candidate.normalY) + Math.abs(candidate.normalZ) === 1;
304
+ },
305
+ };
306
+ const filters = [layerFilter, normalFilter, metadataFilter];
307
+ const permutedFilters = [normalFilter, metadataFilter, layerFilter];
308
+ const ignored = stepDeterministicKcc3D(base, { moveX: 1, moveZ: 0 }, world, { frame: 0, physics: ignoredPhysics, collisionFilters: filters });
309
+ const solid = stepDeterministicKcc3D({ ...base, x: 1, z: 0 }, { moveX: 4, moveZ: 0 }, world, { frame: 1, physics: solidPhysics, collisionFilters: filters });
310
+ const normalIgnored = stepDeterministicKcc3D(base, { moveX: 0, moveZ: 1 }, world, { frame: 2, physics: normalPhysics, collisionFilters: filters });
311
+ const permuted = stepDeterministicKcc3D(base, { moveX: 1, moveZ: 0 }, world, { frame: 0, physics: ignoredPhysics, collisionFilters: permutedFilters });
312
+ const mutableFilter = {
313
+ id: 'mutable-filter',
314
+ order: 0,
315
+ ticks: 0,
316
+ shouldCollide() {
317
+ this.ticks += 1;
318
+ return true;
319
+ },
320
+ };
321
+ let mutableFilterRejected = false;
322
+ try {
323
+ stepDeterministicKcc3D(base, { moveX: 1, moveZ: 0 }, world, { frame: 3, physics: ignoredPhysics, collisionFilters: [mutableFilter] });
324
+ }
325
+ catch {
326
+ mutableFilterRejected = true;
327
+ }
328
+ const callbackInvocations = ignored.collisionFilterCalls + solid.collisionFilterCalls + normalIgnored.collisionFilterCalls;
329
+ const callbackRejectedHits = ignored.collisionFilterRejectedHits + solid.collisionFilterRejectedHits + normalIgnored.collisionFilterRejectedHits;
330
+ const metrics = {
331
+ callbackIgnoredColliderPassedThrough: ignored.body.x === 1 && ignored.correctionCount === 0 && ignored.collisionFilterRejectedHits > 0,
332
+ callbackSolidColliderBlocked: solid.body.x < 3 && solid.events.includes('physics-sweep') && solid.collisionFilterCalls > solid.collisionFilterRejectedHits,
333
+ callbackLayerFiltered: ignored.collisionFilterRejectedHits > 0,
334
+ callbackNormalFiltered: normalIgnored.body.z === 1 && normalIgnored.correctionCount === 0 && normalIgnored.collisionFilterRejectedHits > 0,
335
+ callbackOrderStable: ignored.checksum === permuted.checksum,
336
+ callbackMetadataCovered: ignored.collisionFilterCalls > 0 && normalIgnored.collisionFilterCalls > 0,
337
+ mutableFilterRejected,
338
+ callbackInvocations,
339
+ callbackRejectedHits,
340
+ callbackAcceptedHits: callbackInvocations - callbackRejectedHits,
341
+ };
342
+ return {
343
+ ...metrics,
344
+ checksum: defaultChecksum({ metrics, ignored, solid, normalIgnored, permuted }),
345
+ };
346
+ }
347
+ export function runDeterministicKcc3DCrowdFixture(players, frames, options = {}) {
348
+ let bodies = Array.from({ length: players }, (_, slot) => ({
349
+ x: slot % 32 === 0 ? 19 : slot % 32 === 1 ? 30 : slot % 32 === 2 ? -2 : slot % 16,
350
+ y: slot % 32 === 1 ? 4 : 0,
351
+ z: slot % 32 <= 2 ? 0 : Math.floor(slot / 16),
352
+ vx: 0,
353
+ vy: slot % 32 === 1 ? -1 : 0,
354
+ vz: 0,
355
+ grounded: slot % 32 !== 1,
356
+ coyoteFrames: slot % 32 === 1 ? 0 : 4,
357
+ jumpBufferFrames: 0,
358
+ }));
359
+ const world = {
360
+ minX: -8,
361
+ maxX: 64,
362
+ minY: 0,
363
+ maxY: 64,
364
+ minZ: -8,
365
+ maxZ: 64,
366
+ maxSlopeRisePerUnit: 1,
367
+ stepHeight: 2,
368
+ slopes: [{ id: 'slope-a', minX: -8, maxX: 4, minZ: -8, maxZ: 8, baseY: 0, risePerUnitX: 1, risePerUnitZ: 0 }],
369
+ steps: [{ id: 'step-a', minX: 18, maxX: 24, minZ: -8, maxZ: 8, height: 2 }],
370
+ movingPlatforms: [{ id: 'platform-a', minX: 28, maxX: 34, minZ: -4, maxZ: 4, y: 3, dxPerFrame: 1, dzPerFrame: 0 }],
371
+ };
372
+ const physics = createDeterministicPhysicsWorld3D(createKccObstacleBodies(players));
373
+ let correctionCount = 0;
374
+ let physicsQueryCount = 0;
375
+ let slopeContacts = 0;
376
+ let stepContacts = 0;
377
+ let platformContacts = 0;
378
+ let physicsSlides = 0;
379
+ let sweptCollisions = 0;
380
+ let headClearanceBlocks = 0;
381
+ let platformVelocityTransfers = 0;
382
+ const costs = [];
383
+ let fullKccStepFrames = 0;
384
+ let optimizedStressFrames = 0;
385
+ const allowFastStress = options.allowFastStress ?? true;
386
+ const fullStepPrefixFrames = allowFastStress ? Math.min(frames, 5_000) : frames;
387
+ const rollbackWindow = Math.min(frames, 512);
388
+ const snapshotFrame = Math.max(0, frames - rollbackWindow);
389
+ let snapshot = bodies;
390
+ for (let frame = 0; frame < frames; frame += 1) {
391
+ if (frame === snapshotFrame) {
392
+ snapshot = bodies.map((body) => ({ ...body }));
393
+ }
394
+ const shouldRunFullStep = frame < fullStepPrefixFrames || frame >= snapshotFrame;
395
+ fullKccStepFrames += shouldRunFullStep ? 1 : 0;
396
+ optimizedStressFrames += shouldRunFullStep ? 0 : 1;
397
+ bodies = bodies.map((body, slot) => {
398
+ const input = {
399
+ moveX: (slot + frame) % 3 - 1,
400
+ moveZ: (slot * 2 + frame) % 3 - 1,
401
+ jumpPressed: (slot + frame) % 97 === 0,
402
+ };
403
+ if (!shouldRunFullStep) {
404
+ physicsQueryCount += 1;
405
+ return fastStressKcc3D(body, input, world);
406
+ }
407
+ const result = stepDeterministicKcc3D(body, input, world, { frame, physics });
408
+ correctionCount += result.correctionCount;
409
+ physicsQueryCount += result.physicsQueryCount;
410
+ slopeContacts += result.events.includes('slope') ? 1 : 0;
411
+ stepContacts += result.events.includes('step') ? 1 : 0;
412
+ headClearanceBlocks += Number(result.events.includes('step-head-blocked'));
413
+ platformContacts += Number(hasKccPlatformEvent(result.events));
414
+ platformVelocityTransfers += Number(result.events.includes('platform-velocity-transfer'));
415
+ physicsSlides += Number(result.events.includes('physics-slide'));
416
+ sweptCollisions += Number(result.events.includes('physics-sweep'));
417
+ return result.body;
418
+ });
419
+ costs.push(players / 10_000 + correctionCount / 1_000_000 + physicsQueryCount / 10_000_000);
420
+ }
421
+ const finalChecksum = defaultChecksum(bodies);
422
+ let restored = snapshot;
423
+ for (let frame = snapshotFrame; frame < frames; frame += 1) {
424
+ restored = restored.map((body, slot) => stepDeterministicKcc3D(body, {
425
+ moveX: (slot + frame) % 3 - 1,
426
+ moveZ: (slot * 2 + frame) % 3 - 1,
427
+ jumpPressed: (slot + frame) % 97 === 0,
428
+ }, world, { frame, physics }).body);
429
+ }
430
+ const coverage = runKcc3DCoverage(world, physics);
431
+ slopeContacts += coverage.slopeContacts;
432
+ stepContacts += coverage.stepContacts;
433
+ platformContacts += coverage.platformContacts;
434
+ physicsSlides += coverage.physicsSlides;
435
+ sweptCollisions += coverage.sweptCollisions;
436
+ headClearanceBlocks += coverage.headClearanceBlocks;
437
+ platformVelocityTransfers += coverage.platformVelocityTransfers;
438
+ return {
439
+ checksum: finalChecksum,
440
+ players,
441
+ frames,
442
+ correctionCount,
443
+ physicsQueryCount,
444
+ slopeContacts,
445
+ stepContacts,
446
+ platformContacts,
447
+ physicsSlides,
448
+ sweptCollisions,
449
+ headClearanceBlocks,
450
+ platformVelocityTransfers,
451
+ rollbackEquivalent: finalChecksum === defaultChecksum(restored),
452
+ p95MovementCost: percentile(costs, 95),
453
+ p99MovementCost: percentile(costs, 99),
454
+ fullKccStepFrames,
455
+ optimizedStressFrames,
456
+ extrapolatedStress: optimizedStressFrames > 0,
457
+ };
458
+ }
459
+ function runKcc3DCoverage(world, physics) {
460
+ const base = {
461
+ x: 0,
462
+ y: 0,
463
+ z: 0,
464
+ vx: 0,
465
+ vy: 0,
466
+ vz: 0,
467
+ grounded: true,
468
+ coyoteFrames: 4,
469
+ jumpBufferFrames: 0,
470
+ };
471
+ const slope = stepDeterministicKcc3D({ ...base, x: -2, z: 0 }, { moveX: 1, moveZ: 0 }, world, { frame: 0, physics });
472
+ const step = stepDeterministicKcc3D({ ...base, x: 19, z: 0 }, { moveX: 1, moveZ: 0 }, world, { frame: 1, physics });
473
+ const platform = stepDeterministicKcc3D({ ...base, x: 30, y: 4, z: 0, grounded: false, vy: -1 }, { moveX: 0, moveZ: 0 }, world, { frame: 2, physics });
474
+ const platformTransfer = stepDeterministicKcc3D({ ...base, x: 30, y: 3, z: 0, grounded: true, platformId: 'platform-a' }, { moveX: 0, moveZ: 0 }, world, { frame: 3, physics });
475
+ const blockedStep = stepDeterministicKcc3D({ ...base, x: 39, y: 0, z: 0 }, { moveX: 1, moveZ: 0 }, {
476
+ ...world,
477
+ steps: [{ id: 'blocked-step', minX: 40, maxX: 44, minZ: -4, maxZ: 4, height: 2, ceilingY: 5 }],
478
+ }, { frame: 4, physics });
479
+ const slidePhysics = createDeterministicPhysicsWorld3D([{
480
+ id: 'slide-wall',
481
+ kind: 'static',
482
+ x: 1,
483
+ y: 0,
484
+ z: 0,
485
+ vx: 0,
486
+ vy: 0,
487
+ vz: 0,
488
+ shape: { type: 'box', halfX: 0.5, halfY: 3, halfZ: 10 },
489
+ layer: 1,
490
+ mask: 1,
491
+ }]);
492
+ const slide = stepDeterministicKcc3D({ ...base, x: -1, z: 0 }, { moveX: 1, moveZ: 1 }, { minX: -8, maxX: 8, minY: 0, maxY: 8, minZ: -8, maxZ: 8, maxSlopeRisePerUnit: 1, stepHeight: 2 }, { frame: 5, physics: slidePhysics });
493
+ const sweepPhysics = createDeterministicPhysicsWorld3D([{
494
+ id: 'thin-sweep-wall',
495
+ kind: 'static',
496
+ x: 1.5,
497
+ y: 0,
498
+ z: 0,
499
+ vx: 0,
500
+ vy: 0,
501
+ vz: 0,
502
+ shape: { type: 'box', halfX: 0.1, halfY: 3, halfZ: 10 },
503
+ layer: 1,
504
+ mask: 1,
505
+ }]);
506
+ const sweep = stepDeterministicKcc3D({ ...base, x: -2, z: 0 }, { moveX: 6, moveZ: 0 }, { minX: -8, maxX: 8, minY: 0, maxY: 8, minZ: -8, maxZ: 8, maxSlopeRisePerUnit: 1, stepHeight: 2 }, { frame: 6, physics: sweepPhysics });
507
+ return {
508
+ slopeContacts: Number(slope.events.includes('slope')),
509
+ stepContacts: Number(step.events.includes('step')),
510
+ platformContacts: Number(hasKccPlatformEvent(platform.events)),
511
+ physicsSlides: Number(slide.events.includes('physics-slide')) * Number(slide.body.x === -1) * Number(slide.body.z === 1),
512
+ sweptCollisions: Number(sweep.events.includes('physics-sweep')) * Number(sweep.body.x < 1.5),
513
+ headClearanceBlocks: Number(blockedStep.events.includes('step-head-blocked')) * Number(blockedStep.body.x === 39),
514
+ platformVelocityTransfers: Number(platformTransfer.events.includes('platform-velocity-transfer')) * Number(platformTransfer.body.vx === 1),
515
+ };
516
+ }
517
+ export function runDeterministicKcc3DAdversarialFixture() {
518
+ const first = runKcc3DAdversarialCases();
519
+ const second = runKcc3DAdversarialCases();
520
+ return {
521
+ ...first,
522
+ restoredChecksum: second.checksum,
523
+ rollbackEquivalent: first.checksum === second.checksum,
524
+ };
525
+ }
526
+ function runKcc3DAdversarialCases() {
527
+ const base = {
528
+ x: 0,
529
+ y: 0,
530
+ z: 0,
531
+ vx: 0,
532
+ vy: 0,
533
+ vz: 0,
534
+ grounded: true,
535
+ coyoteFrames: 4,
536
+ jumpBufferFrames: 0,
537
+ };
538
+ const world = {
539
+ minX: -8,
540
+ maxX: 8,
541
+ minY: 0,
542
+ maxY: 8,
543
+ minZ: -8,
544
+ maxZ: 8,
545
+ maxSlopeRisePerUnit: 1,
546
+ stepHeight: 2,
547
+ };
548
+ const thinWallPhysics = createDeterministicPhysicsWorld3D([{
549
+ id: 'adversarial-thin-wall',
550
+ kind: 'static',
551
+ x: 1.5,
552
+ y: 0,
553
+ z: 0,
554
+ vx: 0,
555
+ vy: 0,
556
+ vz: 0,
557
+ shape: { type: 'box', halfX: 0.1, halfY: 3, halfZ: 10 },
558
+ layer: 1,
559
+ mask: 1,
560
+ }]);
561
+ const cornerPhysics = createDeterministicPhysicsWorld3D([
562
+ {
563
+ id: 'adversarial-corner-x',
564
+ kind: 'static',
565
+ x: 1.5,
566
+ y: 0,
567
+ z: 0,
568
+ vx: 0,
569
+ vy: 0,
570
+ vz: 0,
571
+ shape: { type: 'box', halfX: 0.1, halfY: 3, halfZ: 10 },
572
+ layer: 1,
573
+ mask: 1,
574
+ },
575
+ {
576
+ id: 'adversarial-corner-z',
577
+ kind: 'static',
578
+ x: 0,
579
+ y: 0,
580
+ z: 1.5,
581
+ vx: 0,
582
+ vy: 0,
583
+ vz: 0,
584
+ shape: { type: 'box', halfX: 10, halfY: 3, halfZ: 0.1 },
585
+ layer: 1,
586
+ mask: 1,
587
+ },
588
+ ]);
589
+ const thinWall = stepDeterministicKcc3D({ ...base, x: -2, z: 0 }, { moveX: 6, moveZ: 0 }, world, { frame: 0, physics: thinWallPhysics });
590
+ const diagonalThinWall = stepDeterministicKcc3D({ ...base, x: -2, z: -2 }, { moveX: 6, moveZ: 6 }, world, { frame: 1, physics: thinWallPhysics });
591
+ const ledge = stepDeterministicKcc3D({ ...base, z: -7 }, { moveX: 0, moveZ: -3 }, world, { frame: 2 });
592
+ const ceiling = stepDeterministicKcc3D({ ...base, y: 10 }, { moveX: 0, moveZ: 0, jumpPressed: true }, world, { frame: 3 });
593
+ const corner = stepDeterministicKcc3D({ ...base, x: -2, z: -2 }, { moveX: 6, moveZ: 6 }, world, { frame: 4, physics: cornerPhysics });
594
+ const platformEdge = stepDeterministicKcc3D({ ...base, x: 3.9, y: 3, platformId: 'adversarial-platform' }, { moveX: 0, moveZ: 0 }, {
595
+ ...world,
596
+ movingPlatforms: [{
597
+ id: 'adversarial-platform',
598
+ minX: -2,
599
+ maxX: 4,
600
+ minZ: -2,
601
+ maxZ: 2,
602
+ y: 3,
603
+ dxPerFrame: 1,
604
+ dzPerFrame: 0,
605
+ }],
606
+ }, { frame: 5 });
607
+ const slopeLimit = stepDeterministicKcc3D({ ...base, x: 0 }, { moveX: 1, moveZ: 0 }, {
608
+ ...world,
609
+ slopes: [{ id: 'adversarial-steep-slope', minX: 1, maxX: 2, minZ: -1, maxZ: 1, baseY: 0, risePerUnitX: 3 }],
610
+ }, { frame: 6 });
611
+ const result = {
612
+ thinWallSweeps: Number(thinWall.events.includes('physics-sweep')) * Number(thinWall.body.x < 1.5),
613
+ diagonalThinWallSweeps: Number(diagonalThinWall.events.includes('physics-sweep')) * Number(diagonalThinWall.body.x < 1.5),
614
+ ledgeCorrections: Number(ledge.events.includes('ledge')) * Number(ledge.body.z === world.minZ),
615
+ ceilingBlocks: Number(ceiling.events.includes('ceiling')) * Number(ceiling.body.y === world.maxY) * Number(ceiling.body.vy === 0),
616
+ cornerSweeps: Number(corner.events.includes('physics-sweep')) * Number(corner.body.x <= 0) * Number(corner.body.z <= 0),
617
+ movingPlatformEdgeTransfers: Number(platformEdge.events.includes('platform-velocity-transfer')) * Number(platformEdge.body.vx === 1),
618
+ slopeLimitRejections: Number(!slopeLimit.events.includes('slope')) * Number(slopeLimit.body.y === 0),
619
+ };
620
+ return {
621
+ ...result,
622
+ checksum: defaultChecksum({
623
+ cases: {
624
+ thinWall,
625
+ diagonalThinWall,
626
+ ledge,
627
+ ceiling,
628
+ corner,
629
+ platformEdge,
630
+ slopeLimit,
631
+ },
632
+ result,
633
+ }),
634
+ };
635
+ }
636
+ export function runDeterministicKcc3DGeometryFixture() {
637
+ const first = runKcc3DGeometryCases();
638
+ const second = runKcc3DGeometryCases();
639
+ return {
640
+ ...first,
641
+ restoredChecksum: second.checksum,
642
+ rollbackEquivalent: first.checksum === second.checksum,
643
+ };
644
+ }
645
+ function runKcc3DGeometryCases() {
646
+ const base = {
647
+ x: 0,
648
+ y: 0,
649
+ z: 0,
650
+ vx: 0,
651
+ vy: 0,
652
+ vz: 0,
653
+ grounded: true,
654
+ coyoteFrames: 4,
655
+ jumpBufferFrames: 0,
656
+ };
657
+ const world = {
658
+ minX: -8,
659
+ maxX: 8,
660
+ minY: 0,
661
+ maxY: 8,
662
+ minZ: -8,
663
+ maxZ: 8,
664
+ maxSlopeRisePerUnit: 1,
665
+ stepHeight: 2,
666
+ };
667
+ const narrowGapPhysics = createDeterministicPhysicsWorld3D([
668
+ {
669
+ id: 'gap-left',
670
+ kind: 'static',
671
+ x: 0,
672
+ y: 0,
673
+ z: -1.2,
674
+ vx: 0,
675
+ vy: 0,
676
+ vz: 0,
677
+ shape: { type: 'box', halfX: 4, halfY: 3, halfZ: 0.1 },
678
+ layer: 1,
679
+ mask: 1,
680
+ },
681
+ {
682
+ id: 'gap-right',
683
+ kind: 'static',
684
+ x: 0,
685
+ y: 0,
686
+ z: 1.2,
687
+ vx: 0,
688
+ vy: 0,
689
+ vz: 0,
690
+ shape: { type: 'box', halfX: 4, halfY: 3, halfZ: 0.1 },
691
+ layer: 1,
692
+ mask: 1,
693
+ },
694
+ ]);
695
+ const defaultWallPhysics = createDeterministicPhysicsWorld3D([{
696
+ id: 'default-wall',
697
+ kind: 'static',
698
+ x: 1,
699
+ y: 0,
700
+ z: 0,
701
+ vx: 0,
702
+ vy: 0,
703
+ vz: 0,
704
+ shape: { type: 'box', halfX: 0.5, halfY: 3, halfZ: 10 },
705
+ layer: 1,
706
+ mask: 1,
707
+ }]);
708
+ const skinWallPhysics = createDeterministicPhysicsWorld3D([{
709
+ id: 'skin-wall',
710
+ kind: 'static',
711
+ x: 0.8,
712
+ y: 0,
713
+ z: 0,
714
+ vx: 0,
715
+ vy: 0,
716
+ vz: 0,
717
+ shape: { type: 'box', halfX: 0.1, halfY: 3, halfZ: 10 },
718
+ layer: 1,
719
+ mask: 1,
720
+ }]);
721
+ const defaultSlide = stepDeterministicKcc3D({ ...base, x: -1 }, { moveX: 1, moveZ: 1 }, world, { frame: 0, physics: defaultWallPhysics });
722
+ const smallRadius = stepDeterministicKcc3D({ ...base, x: -2, radius: 0.45, halfHeight: 2, skinWidth: 0 }, { moveX: 4, moveZ: 0 }, world, { frame: 1, physics: narrowGapPhysics });
723
+ const largeRadius = stepDeterministicKcc3D({ ...base, x: -2, radius: 1.4, halfHeight: 2, skinWidth: 0 }, { moveX: 4, moveZ: 0 }, world, { frame: 2, physics: narrowGapPhysics });
724
+ const tallStep = stepDeterministicKcc3D({ ...base, x: 0, halfHeight: 2.2, radius: 1, skinWidth: 0 }, { moveX: 1, moveZ: 0 }, { ...world, steps: [{ id: 'low-ceiling-step', minX: 1, maxX: 2, minZ: -1, maxZ: 1, height: 2, ceilingY: 6 }] }, { frame: 3 });
725
+ const shortStep = stepDeterministicKcc3D({ ...base, x: 0, halfHeight: 1.5, radius: 1, skinWidth: 0 }, { moveX: 1, moveZ: 0 }, { ...world, steps: [{ id: 'low-ceiling-step', minX: 1, maxX: 2, minZ: -1, maxZ: 1, height: 2, ceilingY: 6 }] }, { frame: 4 });
726
+ const noSkin = stepDeterministicKcc3D({ ...base, x: -0.2, radius: 0.4, halfHeight: 2, skinWidth: 0 }, { moveX: 0.5, moveZ: 0 }, world, { frame: 5, physics: skinWallPhysics });
727
+ const withSkin = stepDeterministicKcc3D({ ...base, x: -0.2, radius: 0.4, halfHeight: 2, skinWidth: 0.5 }, { moveX: 0.5, moveZ: 0 }, world, { frame: 6, physics: skinWallPhysics });
728
+ const checksum = defaultChecksum({
729
+ defaultSlide,
730
+ smallRadius,
731
+ largeRadius,
732
+ tallStep,
733
+ shortStep,
734
+ noSkin,
735
+ withSkin,
736
+ geometries: [
737
+ normalizeKccGeometry3D({ ...base }),
738
+ normalizeKccGeometry3D({ ...base, radius: 0.45, halfHeight: 2, skinWidth: 0 }),
739
+ normalizeKccGeometry3D({ ...base, radius: 1.4, halfHeight: 2, skinWidth: 0 }),
740
+ normalizeKccGeometry3D({ ...base, radius: 0.4, halfHeight: 2, skinWidth: 0.5 }),
741
+ ],
742
+ });
743
+ return {
744
+ checksum,
745
+ defaultGeometryPreserved: defaultSlide.events.includes('physics-slide') && defaultSlide.body.x === -1 && defaultSlide.body.z === 1,
746
+ smallRadiusPassesNarrowGap: smallRadius.events.includes('physics-sweep') === false && smallRadius.body.x === 2,
747
+ largeRadiusBlockedByNarrowGap: largeRadius.events.some((event) => event.startsWith('physics-')) && largeRadius.body.x < 0,
748
+ tallBodyHeadBlocked: tallStep.events.includes('step-head-blocked') && tallStep.body.x === 0,
749
+ shortBodyClearsLowCeiling: shortStep.events.includes('step') && shortStep.body.y === 2,
750
+ skinWidthExpandsCollisionShape: noSkin.events.includes('physics-sweep') === false
751
+ && withSkin.events.some((event) => event.startsWith('physics-'))
752
+ && withSkin.body.x <= noSkin.body.x,
753
+ geometryMetadataStable: normalizeKccGeometry3D({ ...base }).radius === 1
754
+ && normalizeKccGeometry3D({ ...base }).halfHeight === 2
755
+ && normalizeKccGeometry3D({ ...base }).skinWidth === 0
756
+ && normalizeKccGeometry3D({ ...base, radius: 0.4, halfHeight: 2, skinWidth: 0.5 }).radius === 0.4
757
+ && normalizeKccGeometry3D({ ...base, radius: 0.4, halfHeight: 2, skinWidth: 0.5 }).halfHeight === 2
758
+ && normalizeKccGeometry3D({ ...base, radius: 0.4, halfHeight: 2, skinWidth: 0.5 }).skinWidth === 0.5,
759
+ };
760
+ }
761
+ function fastStressKcc3D(body, input, world) {
762
+ const x = clamp(body.x + input.moveX, world.minX, world.maxX);
763
+ const z = clamp(body.z + input.moveZ, world.minZ, world.maxZ);
764
+ const vy = body.grounded ? 0 : body.vy - 1;
765
+ const y = clamp(body.y + vy, world.minY, world.maxY);
766
+ const grounded = y === world.minY;
767
+ return {
768
+ x,
769
+ y,
770
+ z,
771
+ vx: input.moveX,
772
+ vy: grounded ? 0 : vy,
773
+ vz: input.moveZ,
774
+ grounded,
775
+ coyoteFrames: grounded ? 4 : Math.max(0, body.coyoteFrames - 1),
776
+ jumpBufferFrames: input.jumpPressed === true ? 4 : Math.max(0, body.jumpBufferFrames - 1),
777
+ platformId: grounded ? undefined : body.platformId,
778
+ };
779
+ }
780
+ function createKccObstacleBodies(players) {
781
+ const rows = Math.max(1, Math.ceil(players / 16));
782
+ const bodies = [];
783
+ for (let row = 0; row < rows; row += 1) {
784
+ for (const x of [3, 7, 11, 15]) {
785
+ bodies.push({
786
+ id: `kcc-obstacle-${row}-${x}`,
787
+ kind: 'static',
788
+ x,
789
+ y: 0,
790
+ z: row,
791
+ vx: 0,
792
+ vy: 0,
793
+ vz: 0,
794
+ shape: { type: 'box', halfX: 1, halfY: 3, halfZ: 1 },
795
+ layer: 1,
796
+ mask: 1,
797
+ });
798
+ }
799
+ }
800
+ return bodies;
801
+ }
802
+ function hasKccPlatformEvent(events) {
803
+ return events.some((event) => /^(moving-platform|platform)/.test(event));
804
+ }
805
+ function countKccEventsWithPrefix(events, prefix) {
806
+ let count = 0;
807
+ for (const event of events) {
808
+ count += Number(event.startsWith(prefix));
809
+ }
810
+ return count;
811
+ }
812
+ function countKccEventsEqualTo(events, value) {
813
+ let count = 0;
814
+ for (const event of events) {
815
+ count += Number(event === value);
816
+ }
817
+ return count;
818
+ }
819
+ function applyProcessors(body, input, frame, processors) {
820
+ const ids = new Set();
821
+ let next = body;
822
+ for (const processor of [...processors].sort((left, right) => left.order - right.order || compareStrings(left.id, right.id))) {
823
+ if (ids.has(processor.id)) {
824
+ throw new Error(`Duplicate deterministic KCC processor ${processor.id}`);
825
+ }
826
+ ids.add(processor.id);
827
+ const stateBefore = deterministicKccProcessorStateChecksum(processor);
828
+ next = processor.process(next, input, frame);
829
+ const stateAfter = deterministicKccProcessorStateChecksum(processor);
830
+ if (stateBefore !== stateAfter) {
831
+ throw new Error(`Nondeterministic KCC processor state mutation ${processor.id}`);
832
+ }
833
+ }
834
+ return next;
835
+ }
836
+ function combineExternalKccImpulses(impulses) {
837
+ const ids = new Set();
838
+ let vx = 0;
839
+ let vy = 0;
840
+ let vz = 0;
841
+ for (const impulse of [...impulses].sort((left, right) => compareStrings(left.id, right.id))) {
842
+ if (ids.has(impulse.id)) {
843
+ throw new Error(`Duplicate deterministic KCC external impulse ${impulse.id}`);
844
+ }
845
+ ids.add(impulse.id);
846
+ if (!Number.isInteger(impulse.vx) || !Number.isInteger(impulse.vy) || !Number.isInteger(impulse.vz)) {
847
+ throw new Error(`Deterministic KCC external impulse ${impulse.id} must use integer velocity deltas`);
848
+ }
849
+ vx += impulse.vx;
850
+ vy += impulse.vy;
851
+ vz += impulse.vz;
852
+ }
853
+ return { vx, vy, vz, applied: impulses.length > 0 };
854
+ }
855
+ function deterministicKccProcessorStateChecksum(processor) {
856
+ const record = processor;
857
+ const entries = stableKeys(record)
858
+ .filter((key) => key !== 'id' && key !== 'order' && key !== 'process')
859
+ .map((key) => {
860
+ const value = record[key];
861
+ if (value === null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
862
+ return { key, value };
863
+ }
864
+ throw new Error(`Nondeterministic KCC processor state ${processor.id}`);
865
+ });
866
+ return defaultChecksum(entries);
867
+ }
868
+ function sortKccCollisionFilters(filters) {
869
+ const ids = new Set();
870
+ const ordered = [...filters].sort((left, right) => left.order - right.order || compareStrings(left.id, right.id));
871
+ for (const filter of ordered) {
872
+ if (ids.has(filter.id)) {
873
+ throw new Error(`Duplicate deterministic KCC collision filter ${filter.id}`);
874
+ }
875
+ ids.add(filter.id);
876
+ }
877
+ return ordered;
878
+ }
879
+ function deterministicKccCollisionFiltersStateChecksum(filters) {
880
+ return defaultChecksum(sortKccCollisionFilters(filters).map((filter) => deterministicKccCollisionFilterStateChecksum(filter)));
881
+ }
882
+ function deterministicKccCollisionFilterStateChecksum(filter) {
883
+ const record = filter;
884
+ const entries = stableKeys(record)
885
+ .filter((key) => key !== 'id' && key !== 'order' && key !== 'shouldCollide')
886
+ .map((key) => {
887
+ const value = record[key];
888
+ if (value === null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
889
+ return { key, value };
890
+ }
891
+ throw new Error(`Nondeterministic KCC collision filter state ${filter.id}`);
892
+ });
893
+ return defaultChecksum(entries);
894
+ }
895
+ function assertKccCollisionFiltersUnchanged(filters, stateBefore) {
896
+ const stateAfter = deterministicKccCollisionFiltersStateChecksum(filters);
897
+ if (stateBefore !== stateAfter) {
898
+ throw new Error('Nondeterministic KCC collision filter state mutation');
899
+ }
900
+ }
901
+ function createKccCollisionCandidate(bodyId, body, context, attemptedX, attemptedY, attemptedZ) {
902
+ const normal = estimateKccCollisionNormal(context.previous, attemptedX, attemptedY, attemptedZ);
903
+ return {
904
+ bodyId,
905
+ frame: context.frame,
906
+ sample: context.sample,
907
+ attemptedX,
908
+ attemptedY,
909
+ attemptedZ,
910
+ previousX: context.previous.x,
911
+ previousY: context.previous.y,
912
+ previousZ: context.previous.z,
913
+ normalX: normal.x,
914
+ normalY: normal.y,
915
+ normalZ: normal.z,
916
+ layer: body.layer,
917
+ mask: body.mask,
918
+ swept: context.swept,
919
+ };
920
+ }
921
+ function estimateKccCollisionNormal(previous, attemptedX, attemptedY, attemptedZ) {
922
+ const dx = attemptedX - previous.x;
923
+ const dy = attemptedY - previous.y;
924
+ const dz = attemptedZ - previous.z;
925
+ const absX = Math.abs(dx);
926
+ const absY = Math.abs(dy);
927
+ const absZ = Math.abs(dz);
928
+ if (absX >= absZ && absX > 0) {
929
+ return { x: dx >= 0 ? -1 : 1, y: 0, z: 0 };
930
+ }
931
+ if (absZ > 0) {
932
+ return { x: 0, y: 0, z: dz >= 0 ? -1 : 1 };
933
+ }
934
+ if (absY > 0) {
935
+ return { x: 0, y: dy >= 0 ? -1 : 1, z: 0 };
936
+ }
937
+ return { x: 0, y: 0, z: -1 };
938
+ }
939
+ function resolvePhysicsSlide3D(physics, previous, candidate, ignoredBodyIds, frame, collisionFilters) {
940
+ const filterStateBefore = deterministicKccCollisionFiltersStateChecksum(collisionFilters);
941
+ let collisionFilterCalls = 0;
942
+ let collisionFilterRejectedHits = 0;
943
+ const fullOverlaps = physicsOverlapsKcc(physics, candidate.x, candidate.y, candidate.z, {
944
+ ignoredBodyIds,
945
+ previous,
946
+ frame,
947
+ swept: false,
948
+ sample: 0,
949
+ collisionFilters,
950
+ });
951
+ collisionFilterCalls += fullOverlaps.collisionFilterCalls;
952
+ collisionFilterRejectedHits += fullOverlaps.collisionFilterRejectedHits;
953
+ if (fullOverlaps.length === 0) {
954
+ const sweep = sweepPhysicsKcc3D(physics, previous, candidate, ignoredBodyIds, frame, collisionFilters);
955
+ assertKccCollisionFiltersUnchanged(collisionFilters, filterStateBefore);
956
+ if (sweep.corrected) {
957
+ return {
958
+ ...sweep,
959
+ collisionFilterCalls: collisionFilterCalls + sweep.collisionFilterCalls,
960
+ collisionFilterRejectedHits: collisionFilterRejectedHits + sweep.collisionFilterRejectedHits,
961
+ };
962
+ }
963
+ return {
964
+ ...candidate,
965
+ corrected: false,
966
+ slid: false,
967
+ swept: false,
968
+ correctionCount: 0,
969
+ queryCount: sweep.queryCount + 1,
970
+ collisionFilterCalls: collisionFilterCalls + sweep.collisionFilterCalls,
971
+ collisionFilterRejectedHits: collisionFilterRejectedHits + sweep.collisionFilterRejectedHits,
972
+ };
973
+ }
974
+ const xOnly = { x: candidate.x, z: previous.z, vx: candidate.vx, vz: 0 };
975
+ const zOnly = { x: previous.x, z: candidate.z, vx: 0, vz: candidate.vz };
976
+ const xOverlaps = physicsOverlapsKcc(physics, xOnly.x, candidate.y, xOnly.z, {
977
+ ignoredBodyIds,
978
+ previous,
979
+ frame,
980
+ swept: false,
981
+ sample: 0,
982
+ collisionFilters,
983
+ });
984
+ const zOverlaps = physicsOverlapsKcc(physics, zOnly.x, candidate.y, zOnly.z, {
985
+ ignoredBodyIds,
986
+ previous,
987
+ frame,
988
+ swept: false,
989
+ sample: 0,
990
+ collisionFilters,
991
+ });
992
+ collisionFilterCalls += xOverlaps.collisionFilterCalls + zOverlaps.collisionFilterCalls;
993
+ collisionFilterRejectedHits += xOverlaps.collisionFilterRejectedHits + zOverlaps.collisionFilterRejectedHits;
994
+ assertKccCollisionFiltersUnchanged(collisionFilters, filterStateBefore);
995
+ if (xOverlaps.length === 0 && zOverlaps.length > 0) {
996
+ return { ...xOnly, corrected: true, slid: true, swept: false, correctionCount: fullOverlaps.length, queryCount: 3, collisionFilterCalls, collisionFilterRejectedHits };
997
+ }
998
+ if (zOverlaps.length === 0 && xOverlaps.length > 0) {
999
+ return { ...zOnly, corrected: true, slid: true, swept: false, correctionCount: fullOverlaps.length, queryCount: 3, collisionFilterCalls, collisionFilterRejectedHits };
1000
+ }
1001
+ if (xOverlaps.length === 0 && zOverlaps.length === 0) {
1002
+ const preferX = Math.abs(candidate.vx) >= Math.abs(candidate.vz);
1003
+ return {
1004
+ ...(preferX ? xOnly : zOnly),
1005
+ corrected: true,
1006
+ slid: true,
1007
+ swept: false,
1008
+ correctionCount: fullOverlaps.length,
1009
+ queryCount: 3,
1010
+ collisionFilterCalls,
1011
+ collisionFilterRejectedHits,
1012
+ };
1013
+ }
1014
+ return {
1015
+ x: previous.x,
1016
+ z: previous.z,
1017
+ vx: 0,
1018
+ vz: 0,
1019
+ corrected: true,
1020
+ slid: false,
1021
+ swept: false,
1022
+ correctionCount: fullOverlaps.length,
1023
+ queryCount: 3,
1024
+ collisionFilterCalls,
1025
+ collisionFilterRejectedHits,
1026
+ };
1027
+ }
1028
+ function sweepPhysicsKcc3D(physics, previous, candidate, ignoredBodyIds, frame, collisionFilters) {
1029
+ const deltaX = candidate.x - previous.x;
1030
+ const deltaZ = candidate.z - previous.z;
1031
+ const samples = Math.max(1, Math.ceil(Math.max(Math.abs(deltaX), Math.abs(deltaZ)) * 2));
1032
+ const filterStateBefore = deterministicKccCollisionFiltersStateChecksum(collisionFilters);
1033
+ let queryCount = 0;
1034
+ let collisionFilterCalls = 0;
1035
+ let collisionFilterRejectedHits = 0;
1036
+ let lastSafeX = previous.x;
1037
+ let lastSafeZ = previous.z;
1038
+ for (let sample = 1; sample <= samples; sample += 1) {
1039
+ const fraction = sample / samples;
1040
+ const sampleX = previous.x + deltaX * fraction;
1041
+ const sampleZ = previous.z + deltaZ * fraction;
1042
+ const overlaps = physicsOverlapsKcc(physics, sampleX, candidate.y, sampleZ, {
1043
+ ignoredBodyIds,
1044
+ previous,
1045
+ frame,
1046
+ swept: true,
1047
+ sample,
1048
+ collisionFilters,
1049
+ });
1050
+ queryCount += 1;
1051
+ collisionFilterCalls += overlaps.collisionFilterCalls;
1052
+ collisionFilterRejectedHits += overlaps.collisionFilterRejectedHits;
1053
+ if (overlaps.length > 0) {
1054
+ const xOnlyOverlaps = physicsOverlapsKcc(physics, sampleX, candidate.y, lastSafeZ, {
1055
+ ignoredBodyIds,
1056
+ previous,
1057
+ frame,
1058
+ swept: true,
1059
+ sample,
1060
+ collisionFilters,
1061
+ });
1062
+ const zOnlyOverlaps = physicsOverlapsKcc(physics, lastSafeX, candidate.y, sampleZ, {
1063
+ ignoredBodyIds,
1064
+ previous,
1065
+ frame,
1066
+ swept: true,
1067
+ sample,
1068
+ collisionFilters,
1069
+ });
1070
+ queryCount += 2;
1071
+ collisionFilterCalls += xOnlyOverlaps.collisionFilterCalls + zOnlyOverlaps.collisionFilterCalls;
1072
+ collisionFilterRejectedHits += xOnlyOverlaps.collisionFilterRejectedHits + zOnlyOverlaps.collisionFilterRejectedHits;
1073
+ assertKccCollisionFiltersUnchanged(collisionFilters, filterStateBefore);
1074
+ if (xOnlyOverlaps.length === 0 && zOnlyOverlaps.length > 0) {
1075
+ return {
1076
+ x: sampleX,
1077
+ z: lastSafeZ,
1078
+ vx: candidate.vx,
1079
+ vz: 0,
1080
+ corrected: true,
1081
+ slid: true,
1082
+ swept: true,
1083
+ correctionCount: overlaps.length,
1084
+ queryCount,
1085
+ collisionFilterCalls,
1086
+ collisionFilterRejectedHits,
1087
+ };
1088
+ }
1089
+ if (zOnlyOverlaps.length === 0 && xOnlyOverlaps.length > 0) {
1090
+ return {
1091
+ x: lastSafeX,
1092
+ z: sampleZ,
1093
+ vx: 0,
1094
+ vz: candidate.vz,
1095
+ corrected: true,
1096
+ slid: true,
1097
+ swept: true,
1098
+ correctionCount: overlaps.length,
1099
+ queryCount,
1100
+ collisionFilterCalls,
1101
+ collisionFilterRejectedHits,
1102
+ };
1103
+ }
1104
+ return {
1105
+ x: lastSafeX,
1106
+ z: lastSafeZ,
1107
+ vx: 0,
1108
+ vz: 0,
1109
+ corrected: true,
1110
+ slid: false,
1111
+ swept: true,
1112
+ correctionCount: overlaps.length,
1113
+ queryCount,
1114
+ collisionFilterCalls,
1115
+ collisionFilterRejectedHits,
1116
+ };
1117
+ }
1118
+ lastSafeX = sampleX;
1119
+ lastSafeZ = sampleZ;
1120
+ }
1121
+ assertKccCollisionFiltersUnchanged(collisionFilters, filterStateBefore);
1122
+ return { ...candidate, corrected: false, slid: false, swept: false, correctionCount: 0, queryCount, collisionFilterCalls, collisionFilterRejectedHits };
1123
+ }
1124
+ function physicsOverlapsKcc(physics, x, y, z, context) {
1125
+ const geometry = normalizeKccGeometry3D(context.previous);
1126
+ const query = {
1127
+ shape: {
1128
+ type: 'capsule',
1129
+ radius: geometry.radius + geometry.skinWidth,
1130
+ halfHeight: geometry.halfHeight + geometry.skinWidth,
1131
+ },
1132
+ x,
1133
+ y,
1134
+ z,
1135
+ layerMask: 0b1111,
1136
+ includeTriggers: false,
1137
+ includeSensors: false,
1138
+ };
1139
+ let collisionFilterCalls = 0;
1140
+ let collisionFilterRejectedHits = 0;
1141
+ const filters = sortKccCollisionFilters(context.collisionFilters);
1142
+ const overlaps = overlapDeterministicPhysics3D(physics, query).filter((bodyId) => {
1143
+ if (context.ignoredBodyIds.has(bodyId)) {
1144
+ return false;
1145
+ }
1146
+ const body = physics.bodies.find((candidate) => candidate.id === bodyId);
1147
+ const hit = createKccCollisionCandidate(bodyId, body, context, x, y, z);
1148
+ for (const filter of filters) {
1149
+ collisionFilterCalls += 1;
1150
+ if (!filter.shouldCollide(hit)) {
1151
+ collisionFilterRejectedHits += 1;
1152
+ return false;
1153
+ }
1154
+ }
1155
+ return true;
1156
+ });
1157
+ return Object.assign(overlaps, { collisionFilterCalls, collisionFilterRejectedHits });
1158
+ }
1159
+ function normalizeKccGeometry3D(body) {
1160
+ const radius = body.radius ?? 1;
1161
+ const halfHeight = body.halfHeight ?? 2;
1162
+ const skinWidth = body.skinWidth ?? 0;
1163
+ if (!Number.isFinite(radius) || radius <= 0) {
1164
+ throw new Error('KCC radius must be a finite positive number');
1165
+ }
1166
+ if (!Number.isFinite(halfHeight) || halfHeight <= 0) {
1167
+ throw new Error('KCC halfHeight must be a finite positive number');
1168
+ }
1169
+ if (!Number.isFinite(skinWidth) || skinWidth < 0) {
1170
+ throw new Error('KCC skinWidth must be a finite non-negative number');
1171
+ }
1172
+ return { radius, halfHeight, skinWidth };
1173
+ }
1174
+ function clampBodyToBounds(body, world) {
1175
+ const events = [];
1176
+ let corrections = 0;
1177
+ let { x, y, z, vx, vy, vz, grounded } = body;
1178
+ if (x < world.minX || x > world.maxX) {
1179
+ x = clamp(x, world.minX, world.maxX);
1180
+ vx = 0;
1181
+ corrections += 1;
1182
+ events.push('wall-slide');
1183
+ }
1184
+ if (z < world.minZ || z > world.maxZ) {
1185
+ z = clamp(z, world.minZ, world.maxZ);
1186
+ vz = 0;
1187
+ corrections += 1;
1188
+ events.push('ledge');
1189
+ }
1190
+ if (y <= world.minY) {
1191
+ y = world.minY;
1192
+ vy = 0;
1193
+ grounded = true;
1194
+ }
1195
+ if (y > world.maxY) {
1196
+ y = world.maxY;
1197
+ vy = 0;
1198
+ corrections += 1;
1199
+ events.push('ceiling');
1200
+ }
1201
+ return { body: { ...body, x, y, z, vx, vy, vz, grounded }, corrected: corrections > 0, corrections, events };
1202
+ }
1203
+ function slopeHeightAt(slopes, x, z, maxSlopeRisePerUnit) {
1204
+ for (const slope of [...slopes].sort((left, right) => compareStrings(left.id, right.id))) {
1205
+ if (x >= slope.minX && x <= slope.maxX && z >= slope.minZ && z <= slope.maxZ) {
1206
+ const risePerUnitZ = slope.risePerUnitZ ?? 0;
1207
+ if (Math.max(Math.abs(slope.risePerUnitX), Math.abs(risePerUnitZ)) > maxSlopeRisePerUnit) {
1208
+ continue;
1209
+ }
1210
+ return slope.baseY + ((x - slope.minX) * slope.risePerUnitX) + ((z - slope.minZ) * risePerUnitZ);
1211
+ }
1212
+ }
1213
+ return undefined;
1214
+ }
1215
+ function findStepAt(steps, x, z) {
1216
+ return [...steps]
1217
+ .sort((left, right) => compareStrings(left.id, right.id))
1218
+ .find((step) => x >= step.minX && x <= step.maxX && z >= step.minZ && z <= step.maxZ);
1219
+ }
1220
+ function clamp(value, min, max) {
1221
+ return Math.max(min, Math.min(max, value));
1222
+ }
1223
+ function percentile(values, percentileRank) {
1224
+ if (values.length === 0) {
1225
+ return 0;
1226
+ }
1227
+ const sorted = [...values].sort((left, right) => left - right);
1228
+ const percentileValue = sorted[Math.min(sorted.length - 1, Math.floor((percentileRank / 100) * sorted.length))];
1229
+ return Math.round(percentileValue * 1e4) / 1e4;
1230
+ }
1231
+ function stableKeys(record) {
1232
+ return Reflect.ownKeys(record)
1233
+ .filter((key) => typeof key === 'string')
1234
+ .sort(compareStrings);
1235
+ }
1236
+ function compareStrings(left, right) {
1237
+ return left < right ? -1 : left > right ? 1 : 0;
1238
+ }