@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,1437 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cookDeterministicNavmesh = cookDeterministicNavmesh;
4
+ exports.cookImportedDeterministicNavmesh = cookImportedDeterministicNavmesh;
5
+ exports.findDeterministicNavmeshPath = findDeterministicNavmeshPath;
6
+ exports.findDeterministicNavmeshPointPath = findDeterministicNavmeshPointPath;
7
+ exports.locateDeterministicNavmeshPolygon = locateDeterministicNavmeshPolygon;
8
+ exports.smoothDeterministicNavmeshPath = smoothDeterministicNavmeshPath;
9
+ exports.stepDeterministicNavmeshAgents = stepDeterministicNavmeshAgents;
10
+ exports.stepDeterministicNavmeshOffMeshTraversal = stepDeterministicNavmeshOffMeshTraversal;
11
+ exports.runDeterministicNavmeshStress = runDeterministicNavmeshStress;
12
+ exports.runDeterministicNavmeshVelocityObstacleFixture = runDeterministicNavmeshVelocityObstacleFixture;
13
+ exports.runDeterministicNavmeshImportTraversalFixture = runDeterministicNavmeshImportTraversalFixture;
14
+ exports.runDeterministicNavmeshLayered3DFixture = runDeterministicNavmeshLayered3DFixture;
15
+ const canonical_js_1 = require("./canonical.js");
16
+ function cookDeterministicNavmesh(source) {
17
+ const polygons = [...source.polygons].map(normalizePolygon).sort(comparePolygons);
18
+ const polygonIds = new Set();
19
+ for (const polygon of polygons) {
20
+ if (polygonIds.has(polygon.id)) {
21
+ throw new Error(`Duplicate navmesh polygon ${polygon.id}`);
22
+ }
23
+ validatePolygonShape(polygon);
24
+ polygonIds.add(polygon.id);
25
+ }
26
+ const links = [...source.links.flatMap(expandLink)].sort(compareLinks);
27
+ const linkIds = new Set();
28
+ for (const link of links) {
29
+ if (!polygonIds.has(link.from) || !polygonIds.has(link.to)) {
30
+ throw new Error(`Navmesh link ${link.from}->${link.to} references a missing polygon`);
31
+ }
32
+ const linkId = `${link.from}->${link.to}`;
33
+ if (linkIds.has(linkId)) {
34
+ throw new Error(`Duplicate navmesh link ${linkId}`);
35
+ }
36
+ linkIds.add(linkId);
37
+ }
38
+ const regions = [...(source.regions ?? [])].sort((left, right) => compareStrings(left.id, right.id));
39
+ const regionIds = new Set();
40
+ for (const region of regions) {
41
+ if (regionIds.has(region.id)) {
42
+ throw new Error(`Duplicate navmesh region ${region.id}`);
43
+ }
44
+ regionIds.add(region.id);
45
+ }
46
+ for (const polygon of polygons) {
47
+ if (polygon.region !== undefined && !regionIds.has(polygon.region)) {
48
+ throw new Error(`Navmesh polygon ${polygon.id} references missing region ${polygon.region}`);
49
+ }
50
+ }
51
+ validatePolygonInteriorDisjoint(polygons);
52
+ const portalTable = createPortalTable(polygons, links);
53
+ const cooked = { id: source.id, polygons, links, regions, portalTable, spawnMarkers: [] };
54
+ const bytes = (0, canonical_js_1.canonicalStringify)(cooked);
55
+ return { ...cooked, bytes, hash: (0, canonical_js_1.defaultChecksum)(bytes) };
56
+ }
57
+ function cookImportedDeterministicNavmesh(source) {
58
+ const navmesh = cookDeterministicNavmesh({
59
+ id: source.id,
60
+ polygons: source.polygons.map((polygon) => ({
61
+ id: polygon.id,
62
+ vertices: polygon.vertices,
63
+ region: polygon.region,
64
+ cost: polygon.cost,
65
+ })),
66
+ links: source.links.map((link) => ({
67
+ from: link.from,
68
+ to: link.to,
69
+ cost: link.cost,
70
+ offMesh: link.offMesh,
71
+ bidirectional: link.bidirectional,
72
+ })),
73
+ regions: source.regions,
74
+ });
75
+ const polygonIds = new Set(navmesh.polygons.map((polygon) => polygon.id));
76
+ const spawnMarkers = [...(source.spawnMarkers ?? [])]
77
+ .map((marker) => ({
78
+ id: marker.id,
79
+ polygonId: marker.polygonId,
80
+ point: normalizePoint(marker.point),
81
+ }))
82
+ .sort((left, right) => compareStrings(left.id, right.id));
83
+ const markerIds = new Set();
84
+ for (const marker of spawnMarkers) {
85
+ if (markerIds.has(marker.id)) {
86
+ throw new Error(`Duplicate navmesh spawn marker ${marker.id}`);
87
+ }
88
+ markerIds.add(marker.id);
89
+ if (!polygonIds.has(marker.polygonId)) {
90
+ throw new Error(`Navmesh spawn marker ${marker.id} references missing polygon ${marker.polygonId}`);
91
+ }
92
+ const polygon = navmesh.polygons.find((candidate) => candidate.id === marker.polygonId);
93
+ if (!pointInPolygon3D(marker.point, polygon)) {
94
+ throw new Error(`Navmesh spawn marker ${marker.id} is outside polygon ${marker.polygonId}`);
95
+ }
96
+ }
97
+ const cooked = {
98
+ id: navmesh.id,
99
+ polygons: navmesh.polygons,
100
+ links: navmesh.links,
101
+ regions: navmesh.regions,
102
+ portalTable: navmesh.portalTable,
103
+ spawnMarkers,
104
+ };
105
+ const bytes = (0, canonical_js_1.canonicalStringify)(cooked);
106
+ return { ...cooked, bytes, hash: (0, canonical_js_1.defaultChecksum)(bytes) };
107
+ }
108
+ function findDeterministicNavmeshPath(navmesh, query) {
109
+ const polygons = new Map(navmesh.polygons.map((polygon) => [polygon.id, polygon]));
110
+ const disabledRegions = new Set([
111
+ ...navmesh.regions.filter((region) => region.enabled === false).map((region) => region.id),
112
+ ...(query.disabledRegions ?? []),
113
+ ]);
114
+ const blockedPolygons = new Set(query.blockedPolygons ?? []);
115
+ if (!polygons.has(query.start) || !polygons.has(query.goal)) {
116
+ return emptyPath();
117
+ }
118
+ if (isBlocked(polygons.get(query.start), disabledRegions, blockedPolygons) || isBlocked(polygons.get(query.goal), disabledRegions, blockedPolygons)) {
119
+ return emptyPath();
120
+ }
121
+ const open = [{ id: query.start, cost: 0, order: 0 }];
122
+ const cameFrom = new Map();
123
+ const costs = new Map([[query.start, 0]]);
124
+ const visited = new Set();
125
+ let order = 1;
126
+ while (open.length > 0) {
127
+ open.sort((left, right) => left.cost - right.cost || left.order - right.order);
128
+ const current = open.shift();
129
+ if (visited.has(current.id)) {
130
+ continue;
131
+ }
132
+ visited.add(current.id);
133
+ if (current.id === query.goal) {
134
+ const polygonPath = reconstructPath(cameFrom, current.id);
135
+ const points = smoothDeterministicNavmeshPath(navmesh, polygonPath);
136
+ return {
137
+ found: true,
138
+ polygons: polygonPath,
139
+ points,
140
+ offMeshTraversals: countOffMesh(navmesh, polygonPath),
141
+ visitedPolygons: visited.size,
142
+ pathHash: (0, canonical_js_1.defaultChecksum)({ polygonPath, points }),
143
+ };
144
+ }
145
+ for (const link of navmesh.links.filter((candidate) => candidate.from === current.id).sort(compareLinks)) {
146
+ const next = polygons.get(link.to);
147
+ if (next === undefined || isBlocked(next, disabledRegions, blockedPolygons)) {
148
+ continue;
149
+ }
150
+ const cost = current.cost + (link.cost ?? 1) + (next.cost ?? 1);
151
+ if (cost >= (costs.get(link.to) ?? Number.MAX_SAFE_INTEGER)) {
152
+ continue;
153
+ }
154
+ costs.set(link.to, cost);
155
+ cameFrom.set(link.to, current.id);
156
+ open.push({ id: link.to, cost, order });
157
+ order += 1;
158
+ }
159
+ }
160
+ return emptyPath();
161
+ }
162
+ function findDeterministicNavmeshPointPath(navmesh, query) {
163
+ const startPolygon = locateDeterministicNavmeshPolygon(navmesh, query.start, query.disabledRegions, query.blockedPolygons);
164
+ const goalPolygon = locateDeterministicNavmeshPolygon(navmesh, query.goal, query.disabledRegions, query.blockedPolygons);
165
+ if (startPolygon === undefined || goalPolygon === undefined) {
166
+ return emptyPath();
167
+ }
168
+ const path = findDeterministicNavmeshPath(navmesh, {
169
+ start: startPolygon.id,
170
+ goal: goalPolygon.id,
171
+ disabledRegions: query.disabledRegions,
172
+ blockedPolygons: query.blockedPolygons,
173
+ });
174
+ if (!path.found) {
175
+ return path;
176
+ }
177
+ const points = funnelDeterministicNavmeshPath(navmesh, path.polygons, query.start, query.goal);
178
+ return {
179
+ ...path,
180
+ startPolygonId: startPolygon.id,
181
+ goalPolygonId: goalPolygon.id,
182
+ points,
183
+ pathHash: (0, canonical_js_1.defaultChecksum)({ polygonPath: path.polygons, points, startPolygonId: startPolygon.id, goalPolygonId: goalPolygon.id }),
184
+ };
185
+ }
186
+ function locateDeterministicNavmeshPolygon(navmesh, point, disabledRegions = [], blockedPolygons = []) {
187
+ const disabled = new Set([
188
+ ...navmesh.regions.filter((region) => region.enabled === false).map((region) => region.id),
189
+ ...disabledRegions,
190
+ ]);
191
+ const blocked = new Set(blockedPolygons);
192
+ return navmesh.polygons
193
+ .filter((polygon) => !isBlocked(polygon, disabled, blocked))
194
+ .filter((polygon) => pointInPolygon3D(point, polygon))
195
+ .sort(comparePolygons)[0];
196
+ }
197
+ function smoothDeterministicNavmeshPath(navmesh, polygonIds) {
198
+ const polygons = new Map(navmesh.polygons.map((polygon) => [polygon.id, polygon]));
199
+ const points = polygonIds.map((id) => centroid(polygons.get(id)));
200
+ if (points.length <= 2) {
201
+ return points;
202
+ }
203
+ const smoothed = [points[0]];
204
+ for (let index = 1; index < points.length - 1; index += 1) {
205
+ const previous = smoothed[smoothed.length - 1];
206
+ const next = points[index + 1];
207
+ if ((previous.x === points[index].x && points[index].x === next.x) || (previous.y === points[index].y && points[index].y === next.y)) {
208
+ continue;
209
+ }
210
+ smoothed.push(points[index]);
211
+ }
212
+ smoothed.push(points[points.length - 1]);
213
+ return smoothed;
214
+ }
215
+ function stepDeterministicNavmeshAgents(navmesh, agents, options = {}) {
216
+ const blocked = new Set(options.movingObstaclePolygons ?? []);
217
+ let replans = 0;
218
+ let avoidanceCorrections = 0;
219
+ let waypointAdvances = 0;
220
+ let velocityObstacleAvoidanceCorrections = 0;
221
+ let agentVelocityObstacleAvoidanceCorrections = 0;
222
+ let agentAvoidancePriorityYields = 0;
223
+ let avoidanceSpeedReductions = 0;
224
+ let agentAvoidanceSpeedReductions = 0;
225
+ const polygons = new Map(navmesh.polygons.map((polygon) => [polygon.id, polygon]));
226
+ const disabledRegions = new Set([
227
+ ...navmesh.regions.filter((region) => region.enabled === false).map((region) => region.id),
228
+ ...(options.disabledRegions ?? []),
229
+ ]);
230
+ const orderedAgents = [...agents].sort((left, right) => compareStrings(left.id, right.id));
231
+ for (const agent of orderedAgents) {
232
+ validateNavmeshAgent(agent);
233
+ }
234
+ const agentById = new Map(orderedAgents.map((agent) => [agent.id, agent]));
235
+ const desiredByAgent = new Map(orderedAgents.map((agent) => [agent.id, desiredNextPolygon(navmesh, agent, blocked, options.disabledRegions)]));
236
+ const winnerByPolygon = new Map();
237
+ for (const agent of orderedAgents) {
238
+ const desired = desiredByAgent.get(agent.id);
239
+ const currentWinner = winnerByPolygon.get(desired);
240
+ if (currentWinner === undefined) {
241
+ winnerByPolygon.set(desired, agent.id);
242
+ continue;
243
+ }
244
+ const winner = agentById.get(currentWinner);
245
+ const priority = agent.avoidancePriority ?? 0;
246
+ const winnerPriority = winner.avoidancePriority ?? 0;
247
+ if (priority > winnerPriority || (priority === winnerPriority && compareStrings(agent.id, winner.id) < 0)) {
248
+ winnerByPolygon.set(desired, agent.id);
249
+ }
250
+ }
251
+ const advancedAgents = orderedAgents.map((agent) => {
252
+ const corridor = agent.corridor ?? findDeterministicNavmeshPath(navmesh, {
253
+ start: agent.polygonId,
254
+ goal: agent.targetPolygonId,
255
+ disabledRegions: options.disabledRegions,
256
+ blockedPolygons: [...blocked],
257
+ }).polygons;
258
+ const index = agent.corridorIndex ?? 0;
259
+ const nextPolygon = corridor[Math.min(index + 1, corridor.length - 1)] ?? agent.polygonId;
260
+ const nextPolygonData = polygons.get(nextPolygon);
261
+ if (nextPolygonData === undefined || isBlocked(nextPolygonData, disabledRegions, blocked)) {
262
+ replans += 1;
263
+ const path = findDeterministicNavmeshPath(navmesh, {
264
+ start: agent.polygonId,
265
+ goal: agent.targetPolygonId,
266
+ disabledRegions: options.disabledRegions,
267
+ blockedPolygons: [...blocked],
268
+ });
269
+ return { ...agent, corridor: path.polygons, corridorIndex: 0 };
270
+ }
271
+ if (winnerByPolygon.get(nextPolygon) !== agent.id) {
272
+ avoidanceCorrections += 1;
273
+ return { ...agent, corridor, corridorIndex: index };
274
+ }
275
+ const movingObstacleSpeedReduction = computeNavmeshVelocityObstacleSpeedReduction(agent, nextPolygonData, options.movingAvoidanceObstacles ?? []);
276
+ const agentSpeedReduction = computeNavmeshAgentVelocityObstacleSpeedReduction(agent, nextPolygonData, orderedAgents, desiredByAgent, polygons);
277
+ if (movingObstacleSpeedReduction.reduced) {
278
+ velocityObstacleAvoidanceCorrections += 1;
279
+ }
280
+ if (agentSpeedReduction.reduced) {
281
+ agentVelocityObstacleAvoidanceCorrections += 1;
282
+ agentAvoidancePriorityYields += agentSpeedReduction.yields;
283
+ agentAvoidanceSpeedReductions += agentSpeedReduction.reduction;
284
+ }
285
+ const totalSpeedReduction = Math.max(movingObstacleSpeedReduction.reduction, agentSpeedReduction.reduction);
286
+ avoidanceSpeedReductions += totalSpeedReduction;
287
+ const effectiveSpeed = totalSpeedReduction > 0
288
+ ? Math.max(0, (agent.speedPerFrame ?? 1000) - totalSpeedReduction)
289
+ : undefined;
290
+ const advanced = advanceNavmeshAgentWaypoint(agent, corridor, index, nextPolygon, nextPolygonData, options.frame ?? 0, effectiveSpeed);
291
+ waypointAdvances += advanced.advanced ? 1 : 0;
292
+ return advanced.agent;
293
+ });
294
+ const radiusAvoidance = applyRadiusAvoidance(advancedAgents);
295
+ avoidanceCorrections += radiusAvoidance.corrections;
296
+ avoidanceCorrections += velocityObstacleAvoidanceCorrections;
297
+ avoidanceCorrections += agentVelocityObstacleAvoidanceCorrections;
298
+ return {
299
+ agents: radiusAvoidance.agents,
300
+ replans,
301
+ avoidanceCorrections,
302
+ dynamicRegionToggles: options.disabledRegions?.length ?? 0,
303
+ obstacleCount: blocked.size,
304
+ waypointAdvances,
305
+ radiusAvoidanceCorrections: radiusAvoidance.corrections,
306
+ velocityObstacleAvoidanceCorrections,
307
+ agentVelocityObstacleAvoidanceCorrections,
308
+ agentAvoidancePriorityYields,
309
+ avoidanceSpeedReductions,
310
+ agentAvoidanceSpeedReductions,
311
+ checksum: (0, canonical_js_1.defaultChecksum)(radiusAvoidance.agents),
312
+ };
313
+ }
314
+ function stepDeterministicNavmeshOffMeshTraversal(navmesh, agent, state) {
315
+ const link = navmesh.links.find((candidate) => candidate.from === state.linkFrom && candidate.to === state.linkTo && candidate.offMesh === true);
316
+ if (link === undefined) {
317
+ throw new Error(`Off-mesh traversal ${state.linkFrom}->${state.linkTo} references missing off-mesh link`);
318
+ }
319
+ if (agent.id !== state.agentId) {
320
+ throw new Error(`Off-mesh traversal ${state.commandId} targets ${state.agentId} but received ${agent.id}`);
321
+ }
322
+ const polygons = new Map(navmesh.polygons.map((polygon) => [polygon.id, polygon]));
323
+ const target = polygons.get(state.linkTo);
324
+ if (target === undefined) {
325
+ throw new Error(`Off-mesh traversal ${state.commandId} references missing target polygon ${state.linkTo}`);
326
+ }
327
+ const events = [];
328
+ const elapsedFrames = state.elapsedFrames + 1;
329
+ let phase = state.phase;
330
+ if (phase === 'idle') {
331
+ phase = 'entering';
332
+ events.push('offmesh-enter');
333
+ }
334
+ else if (phase === 'entering') {
335
+ phase = 'traversing';
336
+ events.push('offmesh-traverse');
337
+ }
338
+ else if (phase === 'traversing' && elapsedFrames >= state.durationFrames - 1) {
339
+ phase = 'exiting';
340
+ events.push('offmesh-exit');
341
+ }
342
+ else if (phase === 'exiting' || elapsedFrames >= state.durationFrames) {
343
+ phase = 'complete';
344
+ events.push('offmesh-complete');
345
+ }
346
+ else {
347
+ events.push('offmesh-traverse');
348
+ }
349
+ const completed = phase === 'complete';
350
+ const nextAgent = completed
351
+ ? {
352
+ ...agent,
353
+ polygonId: state.linkTo,
354
+ ...centroid(target),
355
+ corridor: [state.linkTo],
356
+ corridorIndex: 0,
357
+ }
358
+ : { ...agent, corridor: [state.linkFrom, state.linkTo], corridorIndex: 0 };
359
+ const nextState = {
360
+ ...state,
361
+ phase,
362
+ elapsedFrames,
363
+ };
364
+ return {
365
+ state: nextState,
366
+ agent: nextAgent,
367
+ events,
368
+ checksum: (0, canonical_js_1.defaultChecksum)({ state: nextState, agent: nextAgent, events }),
369
+ };
370
+ }
371
+ function runDeterministicNavmeshStress(agents, frames) {
372
+ const navmesh = cookDeterministicNavmesh(createGridNavmeshSource(8, 8));
373
+ const polygonById = new Map(navmesh.polygons.map((polygon) => [polygon.id, polygon]));
374
+ const polygonCount = navmesh.polygons.length;
375
+ let currentAgents = Array.from({ length: agents }, (_, index) => ({
376
+ id: `a${index}`,
377
+ polygonId: `p${index % polygonCount}`,
378
+ targetPolygonId: `p${polygonCount - 1 - (index % polygonCount)}`,
379
+ ...centroid(polygonById.get(`p${index % polygonCount}`)),
380
+ speedPerFrame: 1000,
381
+ updateIntervalFrames: 1 + (index % 3),
382
+ stoppingDistance: 0,
383
+ avoidancePriority: index % 5,
384
+ }));
385
+ let replans = 0;
386
+ let avoidanceCorrections = 0;
387
+ let waypointAdvances = 0;
388
+ let radiusAvoidanceCorrections = 0;
389
+ let velocityObstacleAvoidanceCorrections = 0;
390
+ let agentVelocityObstacleAvoidanceCorrections = 0;
391
+ let agentAvoidancePriorityYields = 0;
392
+ let avoidanceSpeedReductions = 0;
393
+ let agentAvoidanceSpeedReductions = 0;
394
+ let snapshot = currentAgents;
395
+ for (let frame = 0; frame < frames; frame += 1) {
396
+ if (frame === Math.floor(frames / 2)) {
397
+ snapshot = currentAgents.map((agent) => (0, canonical_js_1.cloneCanonical)(agent));
398
+ }
399
+ const result = stepDeterministicNavmeshAgents(navmesh, currentAgents, {
400
+ frame,
401
+ disabledRegions: frame % 37 === 0 ? ['slow'] : [],
402
+ movingObstaclePolygons: [`p${frame % polygonCount}`],
403
+ movingAvoidanceObstacles: frame % 11 === 0 ? [{
404
+ id: `moving-obstacle-${frame % 3}`,
405
+ x: 0.5 + (frame % 5),
406
+ y: 0.5 + (frame % 7),
407
+ vx: frame % 2 === 0 ? 1 : -1,
408
+ vy: 0,
409
+ radius: 0.75,
410
+ predictionFrames: 1,
411
+ speedReductionPerFrame: 1000,
412
+ }] : [],
413
+ });
414
+ currentAgents = result.agents;
415
+ replans += result.replans;
416
+ avoidanceCorrections += result.avoidanceCorrections;
417
+ waypointAdvances += result.waypointAdvances;
418
+ radiusAvoidanceCorrections += result.radiusAvoidanceCorrections;
419
+ velocityObstacleAvoidanceCorrections += result.velocityObstacleAvoidanceCorrections;
420
+ agentVelocityObstacleAvoidanceCorrections += result.agentVelocityObstacleAvoidanceCorrections;
421
+ agentAvoidancePriorityYields += result.agentAvoidancePriorityYields;
422
+ avoidanceSpeedReductions += result.avoidanceSpeedReductions;
423
+ agentAvoidanceSpeedReductions += result.agentAvoidanceSpeedReductions;
424
+ }
425
+ const avoidanceCoverage = stepDeterministicNavmeshAgents(navmesh, [
426
+ {
427
+ id: 'low',
428
+ polygonId: 'p0',
429
+ targetPolygonId: 'p1',
430
+ ...centroid(polygonById.get('p0')),
431
+ speedPerFrame: 1000,
432
+ avoidancePriority: 0,
433
+ },
434
+ {
435
+ id: 'high',
436
+ polygonId: 'p2',
437
+ targetPolygonId: 'p1',
438
+ ...centroid(polygonById.get('p2')),
439
+ speedPerFrame: 1000,
440
+ avoidancePriority: 9,
441
+ },
442
+ ]);
443
+ avoidanceCorrections += avoidanceCoverage.avoidanceCorrections;
444
+ radiusAvoidanceCorrections += avoidanceCoverage.radiusAvoidanceCorrections;
445
+ const radiusCoverage = stepDeterministicNavmeshAgents(navmesh, [
446
+ {
447
+ id: 'near-a',
448
+ polygonId: 'p0',
449
+ targetPolygonId: 'p0',
450
+ x: 0.45,
451
+ y: 0.5,
452
+ speedPerFrame: 0,
453
+ radius: 0.2,
454
+ avoidancePriority: 0,
455
+ },
456
+ {
457
+ id: 'near-b',
458
+ polygonId: 'p0',
459
+ targetPolygonId: 'p0',
460
+ x: 0.55,
461
+ y: 0.5,
462
+ speedPerFrame: 0,
463
+ radius: 0.2,
464
+ avoidancePriority: 0,
465
+ },
466
+ ]);
467
+ radiusAvoidanceCorrections += radiusCoverage.radiusAvoidanceCorrections;
468
+ avoidanceCorrections += radiusCoverage.avoidanceCorrections;
469
+ const velocityObstacleCoverage = runDeterministicNavmeshVelocityObstacleFixture();
470
+ velocityObstacleAvoidanceCorrections += velocityObstacleCoverage.velocityObstacleAvoidanceCorrections;
471
+ avoidanceSpeedReductions += velocityObstacleCoverage.avoidanceSpeedReductions;
472
+ avoidanceCorrections += velocityObstacleCoverage.velocityObstacleAvoidanceCorrections;
473
+ const agentVelocityCoverage = stepDeterministicNavmeshAgents(navmesh, [
474
+ {
475
+ id: 'lower-priority-crossing',
476
+ polygonId: 'p0',
477
+ targetPolygonId: 'p63',
478
+ ...centroid(polygonById.get('p0')),
479
+ speedPerFrame: 1,
480
+ radius: 0.6,
481
+ avoidancePriority: 1,
482
+ },
483
+ {
484
+ id: 'higher-priority-crossing',
485
+ polygonId: 'p1',
486
+ targetPolygonId: 'p62',
487
+ ...centroid(polygonById.get('p1')),
488
+ speedPerFrame: 1,
489
+ radius: 0.6,
490
+ avoidancePriority: 9,
491
+ },
492
+ ]);
493
+ agentVelocityObstacleAvoidanceCorrections += agentVelocityCoverage.agentVelocityObstacleAvoidanceCorrections;
494
+ agentAvoidancePriorityYields += agentVelocityCoverage.agentAvoidancePriorityYields;
495
+ agentAvoidanceSpeedReductions += agentVelocityCoverage.agentAvoidanceSpeedReductions;
496
+ avoidanceSpeedReductions += agentVelocityCoverage.agentAvoidanceSpeedReductions;
497
+ avoidanceCorrections += agentVelocityCoverage.agentVelocityObstacleAvoidanceCorrections;
498
+ waypointAdvances += stepDeterministicNavmeshAgents(navmesh, [{
499
+ id: 'walker',
500
+ polygonId: 'p0',
501
+ targetPolygonId: 'p1',
502
+ ...centroid(polygonById.get('p0')),
503
+ speedPerFrame: 1000,
504
+ avoidancePriority: 0,
505
+ }]).waypointAdvances;
506
+ const checksum = (0, canonical_js_1.defaultChecksum)(currentAgents);
507
+ let restored = snapshot;
508
+ for (let frame = Math.floor(frames / 2); frame < frames; frame += 1) {
509
+ restored = stepDeterministicNavmeshAgents(navmesh, restored, {
510
+ frame,
511
+ disabledRegions: frame % 37 === 0 ? ['slow'] : [],
512
+ movingObstaclePolygons: [`p${frame % polygonCount}`],
513
+ movingAvoidanceObstacles: frame % 11 === 0 ? [{
514
+ id: `moving-obstacle-${frame % 3}`,
515
+ x: 0.5 + (frame % 5),
516
+ y: 0.5 + (frame % 7),
517
+ vx: frame % 2 === 0 ? 1 : -1,
518
+ vy: 0,
519
+ radius: 0.75,
520
+ predictionFrames: 1,
521
+ speedReductionPerFrame: 1000,
522
+ }] : [],
523
+ }).agents;
524
+ }
525
+ const pointPath = findDeterministicNavmeshPointPath(navmesh, {
526
+ start: { x: 0.5, y: 0.5 },
527
+ goal: { x: 7.5, y: 7.5 },
528
+ });
529
+ const located = locateDeterministicNavmeshPolygon(navmesh, { x: 0.5, y: 0.5 });
530
+ return {
531
+ checksum,
532
+ agents,
533
+ frames,
534
+ replans,
535
+ avoidanceCorrections,
536
+ waypointAdvances,
537
+ radiusAvoidanceCorrections,
538
+ velocityObstacleAvoidanceCorrections,
539
+ agentVelocityObstacleAvoidanceCorrections,
540
+ agentAvoidancePriorityYields,
541
+ avoidanceSpeedReductions,
542
+ agentAvoidanceSpeedReductions,
543
+ pathHash: findDeterministicNavmeshPath(navmesh, { start: 'p0', goal: `p${polygonCount - 1}` }).pathHash,
544
+ pointPathHash: pointPath.pathHash,
545
+ pointPathFound: pointPath.found,
546
+ pointLookupStable: located?.id === 'p0' && pointPath.startPolygonId === 'p0' && pointPath.goalPolygonId === `p${polygonCount - 1}`,
547
+ rollbackEquivalent: checksum === (0, canonical_js_1.defaultChecksum)(restored),
548
+ };
549
+ }
550
+ function runDeterministicNavmeshVelocityObstacleFixture() {
551
+ const first = runNavmeshVelocityObstacleCases(false);
552
+ const permuted = runNavmeshVelocityObstacleCases(true);
553
+ const restored = runNavmeshVelocityObstacleCases(false);
554
+ return {
555
+ ...first,
556
+ restoredChecksum: restored.checksum,
557
+ rollbackEquivalent: first.checksum === restored.checksum,
558
+ obstacleOrderStable: first.checksum === permuted.checksum,
559
+ };
560
+ }
561
+ function runNavmeshVelocityObstacleCases(permuteObstacles) {
562
+ const navmesh = cookDeterministicNavmesh(createGridNavmeshSource(2, 1));
563
+ const baseAgent = {
564
+ id: 'agent',
565
+ polygonId: 'p0',
566
+ targetPolygonId: 'p1',
567
+ x: 0.5,
568
+ y: 0.5,
569
+ speedPerFrame: 1,
570
+ radius: 0.2,
571
+ avoidancePriority: 0,
572
+ };
573
+ const movingObstacles = [
574
+ { id: 'crossing-a', x: 1.5, y: 0.5, vx: 0, vy: 0, radius: 0.5, predictionFrames: 1, speedReductionPerFrame: 1 },
575
+ { id: 'far-b', x: 8, y: 8, vx: 0, vy: 0, radius: 0.5, predictionFrames: 1, speedReductionPerFrame: 1 },
576
+ ];
577
+ const orderedObstacles = permuteObstacles ? [...movingObstacles].reverse() : movingObstacles;
578
+ const baseline = stepDeterministicNavmeshAgents(navmesh, [baseAgent], { frame: 0 });
579
+ const reduced = stepDeterministicNavmeshAgents(navmesh, [baseAgent], {
580
+ frame: 0,
581
+ movingAvoidanceObstacles: orderedObstacles,
582
+ });
583
+ const futurePredicted = stepDeterministicNavmeshAgents(navmesh, [baseAgent], {
584
+ frame: 0,
585
+ movingAvoidanceObstacles: [{
586
+ id: 'future-crossing',
587
+ x: -0.5,
588
+ y: 0.5,
589
+ vx: 2,
590
+ vy: 0,
591
+ radius: 0.5,
592
+ predictionFrames: 1,
593
+ speedReductionPerFrame: 1,
594
+ }],
595
+ });
596
+ const noFuturePrediction = stepDeterministicNavmeshAgents(navmesh, [baseAgent], {
597
+ frame: 0,
598
+ movingAvoidanceObstacles: [{
599
+ id: 'future-crossing',
600
+ x: -0.5,
601
+ y: 0.5,
602
+ vx: 2,
603
+ vy: 0,
604
+ radius: 0.5,
605
+ predictionFrames: 0,
606
+ speedReductionPerFrame: 1,
607
+ }],
608
+ });
609
+ const obstacleValidationRejected = throwsNavmeshImport(() => stepDeterministicNavmeshAgents(navmesh, [baseAgent], {
610
+ frame: 0,
611
+ movingAvoidanceObstacles: [{ id: 'bad-radius', x: 0, y: 0, vx: 0, vy: 0, radius: -1 }],
612
+ }));
613
+ const reducedAgent = reduced.agents[0];
614
+ const baselineAgent = baseline.agents[0];
615
+ const futureAgent = futurePredicted.agents[0];
616
+ const noFutureAgent = noFuturePrediction.agents[0];
617
+ const checksum = (0, canonical_js_1.defaultChecksum)({
618
+ baseline,
619
+ reduced,
620
+ futurePredicted,
621
+ noFuturePrediction,
622
+ obstacleValidationRejected,
623
+ });
624
+ return {
625
+ checksum,
626
+ movingObstacleSpeedReduction: baselineAgent?.polygonId === 'p1'
627
+ && reducedAgent?.polygonId === 'p0'
628
+ && reduced.velocityObstacleAvoidanceCorrections > 0
629
+ && reduced.avoidanceSpeedReductions > 0,
630
+ predictedFuturePositionAvoided: futureAgent?.polygonId === 'p0'
631
+ && noFutureAgent?.polygonId === 'p1'
632
+ && futurePredicted.velocityObstacleAvoidanceCorrections > 0,
633
+ authoredSpeedRestored: reducedAgent?.speedPerFrame === baseAgent.speedPerFrame,
634
+ obstacleValidationRejected,
635
+ velocityObstacleAvoidanceCorrections: reduced.velocityObstacleAvoidanceCorrections + futurePredicted.velocityObstacleAvoidanceCorrections,
636
+ avoidanceSpeedReductions: reduced.avoidanceSpeedReductions + futurePredicted.avoidanceSpeedReductions,
637
+ };
638
+ }
639
+ function runDeterministicNavmeshImportTraversalFixture() {
640
+ const source = createImportedArenaNavmeshSource();
641
+ const imported = cookImportedDeterministicNavmesh(source);
642
+ throwsNavmeshImport(() => cookImportedDeterministicNavmesh(source));
643
+ const permuted = cookImportedDeterministicNavmesh({
644
+ ...source,
645
+ polygons: [...source.polygons].reverse(),
646
+ links: [...source.links].reverse(),
647
+ regions: [...source.regions].reverse(),
648
+ spawnMarkers: [...source.spawnMarkers].reverse(),
649
+ });
650
+ const missingRegionRejected = throwsNavmeshImport(() => cookImportedDeterministicNavmesh({
651
+ ...source,
652
+ polygons: [{ ...source.polygons[0], region: 'missing' }, ...source.polygons.slice(1)],
653
+ }));
654
+ const missingLinkRejected = throwsNavmeshImport(() => cookImportedDeterministicNavmesh({
655
+ ...source,
656
+ links: [{ from: 'spawn', to: 'missing' }],
657
+ }));
658
+ const missingSpawnRejected = throwsNavmeshImport(() => cookImportedDeterministicNavmesh({
659
+ ...source,
660
+ spawnMarkers: [{ id: 'bad-spawn', polygonId: 'missing', point: { x: 0.5, y: 0.5 } }],
661
+ }));
662
+ const pointPath = findDeterministicNavmeshPointPath(imported, {
663
+ start: { x: 0.5, y: 0.5 },
664
+ goal: { x: 4.5, y: 0.5 },
665
+ });
666
+ const traversal = runOffMeshTraversalSequence(imported);
667
+ const restoredTraversal = runOffMeshTraversalSequence(imported);
668
+ const metrics = {
669
+ importedHash: imported.hash,
670
+ permutedHash: permuted.hash,
671
+ importOrderStable: imported.hash === permuted.hash,
672
+ portalCount: imported.portalTable.length,
673
+ spawnMarkerCount: imported.spawnMarkers.length,
674
+ missingRegionRejected,
675
+ missingLinkRejected,
676
+ missingSpawnRejected,
677
+ offMeshEnterEvents: traversal.events.filter((event) => event === 'offmesh-enter').length,
678
+ offMeshTraverseEvents: traversal.events.filter((event) => event === 'offmesh-traverse').length,
679
+ offMeshExitEvents: traversal.events.filter((event) => event === 'offmesh-exit').length,
680
+ offMeshCompleteEvents: traversal.events.filter((event) => event === 'offmesh-complete').length,
681
+ offMeshCommandIdStable: traversal.commandIds.every((id) => id === 'jump-gap-001'),
682
+ offMeshTraversalFrames: traversal.frames,
683
+ pointPathFound: pointPath.found,
684
+ pointPathHash: pointPath.pathHash,
685
+ };
686
+ return {
687
+ ...metrics,
688
+ checksum: (0, canonical_js_1.defaultChecksum)({ metrics, finalAgent: traversal.agent, finalState: traversal.state }),
689
+ restoredChecksum: (0, canonical_js_1.defaultChecksum)({
690
+ metrics,
691
+ finalAgent: restoredTraversal.agent,
692
+ finalState: restoredTraversal.state,
693
+ }),
694
+ rollbackEquivalent: (0, canonical_js_1.defaultChecksum)({ metrics, finalAgent: traversal.agent, finalState: traversal.state })
695
+ === (0, canonical_js_1.defaultChecksum)({ metrics, finalAgent: restoredTraversal.agent, finalState: restoredTraversal.state }),
696
+ };
697
+ }
698
+ function runDeterministicNavmeshLayered3DFixture() {
699
+ const source = createLayered3DNavmeshSource();
700
+ const cooked = cookDeterministicNavmesh(source);
701
+ const permuted = cookDeterministicNavmesh({
702
+ ...source,
703
+ polygons: [...source.polygons].reverse(),
704
+ links: [...source.links].reverse(),
705
+ regions: [...source.regions].reverse(),
706
+ });
707
+ const lowerLookup = locateDeterministicNavmeshPolygon(cooked, { x: 0.25, y: 0.25, z: 0 });
708
+ const upperLookup = locateDeterministicNavmeshPolygon(cooked, { x: 0.25, y: 0.25, z: 8 });
709
+ const lowerPath = findDeterministicNavmeshPointPath(cooked, {
710
+ start: { x: 0.25, y: 0.25, z: 0 },
711
+ goal: { x: 1.75, y: 0.25, z: 0 },
712
+ });
713
+ const upperPath = findDeterministicNavmeshPointPath(cooked, {
714
+ start: { x: 0.25, y: 0.25, z: 8 },
715
+ goal: { x: 1.75, y: 0.25, z: 8 },
716
+ });
717
+ const verticalPath = findDeterministicNavmeshPath(cooked, { start: 'lower-start', goal: 'upper-start' });
718
+ const stepped = stepDeterministicNavmeshAgents(cooked, [{
719
+ id: 'upper-agent',
720
+ polygonId: 'upper-start',
721
+ targetPolygonId: 'upper-goal',
722
+ x: 0.5,
723
+ y: 0.5,
724
+ z: 8,
725
+ speedPerFrame: 1000,
726
+ }], { frame: 0 });
727
+ const restored = stepDeterministicNavmeshAgents(cooked, [{
728
+ id: 'upper-agent',
729
+ polygonId: 'upper-start',
730
+ targetPolygonId: 'upper-goal',
731
+ x: 0.5,
732
+ y: 0.5,
733
+ z: 8,
734
+ speedPerFrame: 1000,
735
+ }], { frame: 0 });
736
+ const sameLayerOverlapRejected = throwsNavmeshImport(() => cookDeterministicNavmesh({
737
+ id: 'bad-layer-overlap',
738
+ regions: [{ id: 'same', enabled: true }],
739
+ polygons: [
740
+ { id: 'a', region: 'same', vertices: squareAt(0, 0, 0) },
741
+ { id: 'b', region: 'same', vertices: squareAt(0.25, 0.25, 0) },
742
+ ],
743
+ links: [],
744
+ }));
745
+ const metrics = {
746
+ cookedHash: cooked.hash,
747
+ permutedHash: permuted.hash,
748
+ cookOrderStable: cooked.hash === permuted.hash,
749
+ lowerLookupId: lowerLookup.id,
750
+ upperLookupId: upperLookup.id,
751
+ lowerPathHash: lowerPath.pathHash,
752
+ upperPathHash: upperPath.pathHash,
753
+ verticalOffMeshTraversals: verticalPath.offMeshTraversals,
754
+ stackedOverlapAccepted: cooked.polygons.length === 4,
755
+ sameLayerOverlapRejected,
756
+ layerDisambiguationStable: lowerLookup?.id === 'lower-start'
757
+ && upperLookup?.id === 'upper-start'
758
+ && lowerPath.startPolygonId === 'lower-start'
759
+ && lowerPath.goalPolygonId === 'lower-goal'
760
+ && upperPath.startPolygonId === 'upper-start'
761
+ && upperPath.goalPolygonId === 'upper-goal',
762
+ upperAgentZ: stepped.agents[0].z,
763
+ };
764
+ return {
765
+ ...metrics,
766
+ checksum: (0, canonical_js_1.defaultChecksum)({ metrics, agents: stepped.agents }),
767
+ restoredChecksum: (0, canonical_js_1.defaultChecksum)({ metrics, agents: restored.agents }),
768
+ rollbackEquivalent: (0, canonical_js_1.defaultChecksum)({ metrics, agents: stepped.agents })
769
+ === (0, canonical_js_1.defaultChecksum)({ metrics, agents: restored.agents }),
770
+ };
771
+ }
772
+ function desiredNextPolygon(navmesh, agent, blocked, disabledRegions) {
773
+ const corridor = agent.corridor ?? findDeterministicNavmeshPath(navmesh, {
774
+ start: agent.polygonId,
775
+ goal: agent.targetPolygonId,
776
+ disabledRegions,
777
+ blockedPolygons: [...blocked],
778
+ }).polygons;
779
+ const index = agent.corridorIndex ?? 0;
780
+ return corridor[Math.min(index + 1, corridor.length - 1)] ?? agent.polygonId;
781
+ }
782
+ function createPortalTable(polygons, links) {
783
+ const polygonById = new Map(polygons.map((polygon) => [polygon.id, polygon]));
784
+ return links.map((link) => {
785
+ const from = polygonById.get(link.from);
786
+ const to = polygonById.get(link.to);
787
+ const shared = sharedPortalMidpoint(from, to);
788
+ return {
789
+ from: link.from,
790
+ to: link.to,
791
+ midpoint: shared ?? centroid(to),
792
+ offMesh: link.offMesh === true,
793
+ };
794
+ }).sort((left, right) => compareStrings(left.from, right.from) || compareStrings(left.to, right.to));
795
+ }
796
+ function normalizePoint(point) {
797
+ return {
798
+ x: quantizeNavmeshCoordinate(point.x),
799
+ y: quantizeNavmeshCoordinate(point.y),
800
+ z: quantizeNavmeshCoordinate(point.z ?? 0),
801
+ };
802
+ }
803
+ function throwsNavmeshImport(fn) {
804
+ try {
805
+ fn();
806
+ return false;
807
+ }
808
+ catch {
809
+ return true;
810
+ }
811
+ }
812
+ function runOffMeshTraversalSequence(navmesh) {
813
+ let agent = {
814
+ id: 'runner',
815
+ polygonId: 'approach',
816
+ targetPolygonId: 'landing',
817
+ x: 2.5,
818
+ y: 0.5,
819
+ z: 0,
820
+ speedPerFrame: 1,
821
+ };
822
+ let state = {
823
+ agentId: 'runner',
824
+ linkFrom: 'approach',
825
+ linkTo: 'landing',
826
+ commandId: 'jump-gap-001',
827
+ phase: 'idle',
828
+ elapsedFrames: 0,
829
+ durationFrames: 5,
830
+ };
831
+ const events = [];
832
+ const commandIds = [];
833
+ for (let frame = 0; frame < 5; frame += 1) {
834
+ const step = stepDeterministicNavmeshOffMeshTraversal(navmesh, agent, state);
835
+ agent = step.agent;
836
+ state = step.state;
837
+ events.push(...step.events);
838
+ commandIds.push(state.commandId);
839
+ }
840
+ return { agent, state, events, commandIds, frames: 5 };
841
+ }
842
+ function createImportedArenaNavmeshSource() {
843
+ return {
844
+ id: 'imported-arena-navmesh',
845
+ regions: [
846
+ { id: 'main', enabled: true },
847
+ { id: 'jump', enabled: true },
848
+ ],
849
+ polygons: [
850
+ {
851
+ id: 'spawn',
852
+ region: 'main',
853
+ vertices: [{ x: 0, y: 0 }, { x: 1, y: 0 }, { x: 1, y: 1 }, { x: 0, y: 1 }],
854
+ },
855
+ {
856
+ id: 'approach',
857
+ region: 'main',
858
+ vertices: [{ x: 2, y: 0 }, { x: 3, y: 0 }, { x: 3, y: 1 }, { x: 2, y: 1 }],
859
+ },
860
+ {
861
+ id: 'landing',
862
+ region: 'jump',
863
+ vertices: [{ x: 4, y: 0 }, { x: 5, y: 0 }, { x: 5, y: 1 }, { x: 4, y: 1 }],
864
+ },
865
+ {
866
+ id: 'goal',
867
+ region: 'main',
868
+ vertices: [{ x: 6, y: 0 }, { x: 7, y: 0 }, { x: 7, y: 1 }, { x: 6, y: 1 }],
869
+ },
870
+ ],
871
+ links: [
872
+ { from: 'spawn', to: 'approach', bidirectional: true },
873
+ { from: 'approach', to: 'landing', offMesh: true, cost: 1 },
874
+ { from: 'landing', to: 'goal', bidirectional: true },
875
+ ],
876
+ spawnMarkers: [
877
+ { id: 'runner-spawn', polygonId: 'spawn', point: { x: 0.5, y: 0.5 } },
878
+ { id: 'goal-spawn', polygonId: 'goal', point: { x: 6.5, y: 0.5 } },
879
+ ],
880
+ };
881
+ }
882
+ function createLayered3DNavmeshSource() {
883
+ return {
884
+ id: 'layered-3d-navmesh',
885
+ regions: [
886
+ { id: 'lower', enabled: true },
887
+ { id: 'upper', enabled: true },
888
+ { id: 'vertical', enabled: true },
889
+ ],
890
+ polygons: [
891
+ { id: 'lower-start', region: 'lower', vertices: squareAt(0, 0, 0) },
892
+ { id: 'lower-goal', region: 'lower', vertices: squareAt(1, 0, 0) },
893
+ { id: 'upper-start', region: 'upper', vertices: squareAt(0, 0, 8) },
894
+ { id: 'upper-goal', region: 'upper', vertices: squareAt(1, 0, 8) },
895
+ ],
896
+ links: [
897
+ { from: 'lower-start', to: 'lower-goal', bidirectional: true },
898
+ { from: 'upper-start', to: 'upper-goal', bidirectional: true },
899
+ { from: 'lower-start', to: 'upper-start', offMesh: true, bidirectional: true, cost: 4 },
900
+ ],
901
+ };
902
+ }
903
+ function squareAt(x, y, z) {
904
+ return [
905
+ { x, y, z },
906
+ { x: x + 1, y, z },
907
+ { x: x + 1, y: y + 1, z },
908
+ { x, y: y + 1, z },
909
+ ];
910
+ }
911
+ function applyRadiusAvoidance(agents) {
912
+ const next = new Map(agents.map((agent) => [agent.id, { ...agent }]));
913
+ let corrections = 0;
914
+ const ordered = [...agents].sort(compareAgents);
915
+ for (let leftIndex = 0; leftIndex < ordered.length; leftIndex += 1) {
916
+ for (let rightIndex = leftIndex + 1; rightIndex < ordered.length; rightIndex += 1) {
917
+ const left = next.get(ordered[leftIndex].id);
918
+ const right = next.get(ordered[rightIndex].id);
919
+ if (left.polygonId !== right.polygonId) {
920
+ continue;
921
+ }
922
+ const minDistance = (left.radius ?? 0) + (right.radius ?? 0);
923
+ if (minDistance <= 0 || left.x === undefined || left.y === undefined || right.x === undefined || right.y === undefined) {
924
+ continue;
925
+ }
926
+ const dx = right.x - left.x;
927
+ const dy = right.y - left.y;
928
+ const distanceSquared = dx * dx + dy * dy;
929
+ if (distanceSquared >= minDistance * minDistance) {
930
+ continue;
931
+ }
932
+ const axisX = Math.abs(dx) >= Math.abs(dy);
933
+ const direction = axisX
934
+ ? dx === 0 ? 1 : Math.sign(dx)
935
+ : Math.sign(dy);
936
+ const distance = deterministicSqrt(distanceSquared);
937
+ const correction = quantizeNavmeshCoordinate((minDistance - distance) / 2);
938
+ if (axisX) {
939
+ next.set(left.id, { ...left, x: quantizeNavmeshCoordinate(left.x - direction * correction) });
940
+ next.set(right.id, { ...right, x: quantizeNavmeshCoordinate(right.x + direction * correction) });
941
+ }
942
+ else {
943
+ next.set(left.id, { ...left, y: quantizeNavmeshCoordinate(left.y - direction * correction) });
944
+ next.set(right.id, { ...right, y: quantizeNavmeshCoordinate(right.y + direction * correction) });
945
+ }
946
+ corrections += 1;
947
+ }
948
+ }
949
+ return { agents: [...next.values()].sort(compareAgents), corrections };
950
+ }
951
+ function computeNavmeshVelocityObstacleSpeedReduction(agent, nextPolygon, obstacles) {
952
+ if (agent.x === undefined || agent.y === undefined || obstacles.length === 0) {
953
+ return { reduced: false, reduction: 0 };
954
+ }
955
+ const agentRadius = agent.radius ?? 0;
956
+ const target = point3D(centroid(nextPolygon));
957
+ const speed = agent.speedPerFrame ?? 1000;
958
+ let reduction = 0;
959
+ for (const obstacle of [...obstacles].sort((left, right) => compareStrings(left.id, right.id))) {
960
+ validateMovingAvoidanceObstacle(obstacle);
961
+ const predictionFrames = obstacle.predictionFrames ?? 1;
962
+ const predicted = {
963
+ x: quantizeNavmeshCoordinate(obstacle.x + obstacle.vx * predictionFrames),
964
+ y: quantizeNavmeshCoordinate(obstacle.y + obstacle.vy * predictionFrames),
965
+ z: quantizeNavmeshCoordinate((obstacle.z ?? 0) + (obstacle.vz ?? 0) * predictionFrames),
966
+ };
967
+ const minDistance = agentRadius + obstacle.radius;
968
+ if (minDistance <= 0) {
969
+ continue;
970
+ }
971
+ const distanceSquared = distanceSquaredPointToSegment3D(predicted, { x: agent.x, y: agent.y, z: agent.z ?? 0 }, target);
972
+ if (distanceSquared > minDistance * minDistance) {
973
+ continue;
974
+ }
975
+ reduction = Math.max(reduction, obstacle.speedReductionPerFrame ?? speed);
976
+ }
977
+ return { reduced: reduction > 0, reduction };
978
+ }
979
+ function computeNavmeshAgentVelocityObstacleSpeedReduction(agent, nextPolygon, agents, desiredByAgent, polygons) {
980
+ if (agent.x === undefined || agent.y === undefined) {
981
+ return { reduced: false, reduction: 0, yields: 0 };
982
+ }
983
+ const speed = agent.speedPerFrame ?? 1000;
984
+ if (speed <= 0) {
985
+ return { reduced: false, reduction: 0, yields: 0 };
986
+ }
987
+ const start = { x: agent.x, y: agent.y, z: agent.z ?? 0 };
988
+ const target = point3D(centroid(nextPolygon));
989
+ const future = predictNavmeshAgentPoint(agent, target, speed);
990
+ let reduction = 0;
991
+ let yields = 0;
992
+ for (const other of agents) {
993
+ if (other.id === agent.id || other.x === undefined || other.y === undefined) {
994
+ continue;
995
+ }
996
+ if (!shouldYieldForNavmeshAgent(agent, other)) {
997
+ continue;
998
+ }
999
+ const otherNextPolygonId = desiredByAgent.get(other.id);
1000
+ const otherNextPolygon = polygons.get(otherNextPolygonId);
1001
+ if (otherNextPolygon === undefined) {
1002
+ continue;
1003
+ }
1004
+ const otherSpeed = other.speedPerFrame ?? 1000;
1005
+ const otherStart = { x: other.x, y: other.y, z: other.z ?? 0 };
1006
+ const otherTarget = point3D(centroid(otherNextPolygon));
1007
+ const otherFuture = predictNavmeshAgentPoint(other, otherTarget, otherSpeed);
1008
+ const minDistance = (agent.radius ?? 0) + (other.radius ?? 0);
1009
+ if (minDistance <= 0) {
1010
+ continue;
1011
+ }
1012
+ const minDistanceSquared = minDistance * minDistance;
1013
+ const crossingDistance = Math.min(distanceSquared3D(future, otherFuture), distanceSquaredPointToSegment3D(future, otherStart, otherTarget), distanceSquaredPointToSegment3D(otherFuture, start, target), distanceSquaredPointToSegment3D(start, otherStart, otherTarget));
1014
+ if (crossingDistance > minDistanceSquared) {
1015
+ continue;
1016
+ }
1017
+ reduction = Math.max(reduction, Math.max(1, Math.ceil(speed / 2)));
1018
+ yields += 1;
1019
+ }
1020
+ return { reduced: reduction > 0, reduction, yields };
1021
+ }
1022
+ function shouldYieldForNavmeshAgent(agent, other) {
1023
+ const priority = agent.avoidancePriority ?? 0;
1024
+ const otherPriority = other.avoidancePriority ?? 0;
1025
+ return priority < otherPriority || (priority === otherPriority && compareStrings(agent.id, other.id) > 0);
1026
+ }
1027
+ function predictNavmeshAgentPoint(agent, target, speed) {
1028
+ const start = { x: agent.x, y: agent.y, z: agent.z ?? 0 };
1029
+ const dx = target.x - start.x;
1030
+ const dy = target.y - start.y;
1031
+ const dz = target.z - start.z;
1032
+ const stepX = stepToward(dx, speed);
1033
+ const stepY = stepToward(dy, Math.max(0, speed - Math.abs(stepX)));
1034
+ const stepZ = stepToward(dz, Math.max(0, speed - Math.abs(stepX) - Math.abs(stepY)));
1035
+ return {
1036
+ x: quantizeNavmeshCoordinate(start.x + stepX),
1037
+ y: quantizeNavmeshCoordinate(start.y + stepY),
1038
+ z: quantizeNavmeshCoordinate(start.z + stepZ),
1039
+ };
1040
+ }
1041
+ function point3D(point) {
1042
+ return { x: point.x, y: point.y, z: point.z };
1043
+ }
1044
+ function distanceSquared3D(left, right) {
1045
+ return (left.x - right.x) ** 2 + (left.y - right.y) ** 2 + (left.z - right.z) ** 2;
1046
+ }
1047
+ function distanceSquaredPointToSegment3D(point, start, end) {
1048
+ const dx = end.x - start.x;
1049
+ const dy = end.y - start.y;
1050
+ const dz = end.z - start.z;
1051
+ const lengthSquared = dx * dx + dy * dy + dz * dz;
1052
+ if (lengthSquared === 0) {
1053
+ return (point.x - start.x) ** 2 + (point.y - start.y) ** 2 + (point.z - start.z) ** 2;
1054
+ }
1055
+ const t = Math.max(0, Math.min(1, ((point.x - start.x) * dx + (point.y - start.y) * dy + (point.z - start.z) * dz) / lengthSquared));
1056
+ const closest = {
1057
+ x: start.x + dx * t,
1058
+ y: start.y + dy * t,
1059
+ z: start.z + dz * t,
1060
+ };
1061
+ return (point.x - closest.x) ** 2 + (point.y - closest.y) ** 2 + (point.z - closest.z) ** 2;
1062
+ }
1063
+ function validateMovingAvoidanceObstacle(obstacle) {
1064
+ if (obstacle.id.length === 0) {
1065
+ throw new Error('Navmesh moving avoidance obstacle id must not be empty');
1066
+ }
1067
+ if (![obstacle.x, obstacle.y, obstacle.z ?? 0, obstacle.vx, obstacle.vy, obstacle.vz ?? 0, obstacle.radius].every(Number.isFinite)) {
1068
+ throw new Error(`Navmesh moving avoidance obstacle ${obstacle.id} has invalid numeric fields`);
1069
+ }
1070
+ if (obstacle.radius <= 0) {
1071
+ throw new Error(`Navmesh moving avoidance obstacle ${obstacle.id} has invalid radius`);
1072
+ }
1073
+ if (obstacle.predictionFrames !== undefined && (!Number.isInteger(obstacle.predictionFrames) || obstacle.predictionFrames < 0)) {
1074
+ throw new Error(`Navmesh moving avoidance obstacle ${obstacle.id} has invalid predictionFrames`);
1075
+ }
1076
+ if (obstacle.speedReductionPerFrame !== undefined && (!Number.isInteger(obstacle.speedReductionPerFrame) || obstacle.speedReductionPerFrame < 0)) {
1077
+ throw new Error(`Navmesh moving avoidance obstacle ${obstacle.id} has invalid speedReductionPerFrame`);
1078
+ }
1079
+ }
1080
+ function createGridNavmeshSource(width, height) {
1081
+ const polygons = [];
1082
+ const links = [];
1083
+ for (let y = 0; y < height; y += 1) {
1084
+ for (let x = 0; x < width; x += 1) {
1085
+ const id = `p${y * width + x}`;
1086
+ polygons.push({
1087
+ id,
1088
+ region: (x + y) % 7 === 0 ? 'slow' : undefined,
1089
+ vertices: [{ x, y }, { x: x + 1, y }, { x: x + 1, y: y + 1 }, { x, y: y + 1 }],
1090
+ });
1091
+ if (x + 1 < width) {
1092
+ links.push({ from: id, to: `p${y * width + x + 1}`, bidirectional: true });
1093
+ }
1094
+ if (y + 1 < height) {
1095
+ links.push({ from: id, to: `p${(y + 1) * width + x}`, bidirectional: true });
1096
+ }
1097
+ }
1098
+ }
1099
+ return { id: 'grid-navmesh', polygons, links, regions: [{ id: 'slow', enabled: true }] };
1100
+ }
1101
+ function normalizePolygon(polygon) {
1102
+ return { ...polygon, vertices: polygon.vertices.map((point) => (0, canonical_js_1.cloneCanonical)(point)) };
1103
+ }
1104
+ function validatePolygonShape(polygon) {
1105
+ if (polygon.vertices.length < 3) {
1106
+ throw new Error(`Navmesh polygon ${polygon.id} requires at least 3 vertices`);
1107
+ }
1108
+ const seen = new Set();
1109
+ for (const vertex of polygon.vertices) {
1110
+ if (!Number.isFinite(vertex.x) || !Number.isFinite(vertex.y) || !Number.isFinite(vertex.z ?? 0)) {
1111
+ throw new Error(`Navmesh polygon ${polygon.id} contains non-finite vertex`);
1112
+ }
1113
+ const key = `${quantizeNavmeshCoordinate(vertex.x)}:${quantizeNavmeshCoordinate(vertex.y)}:${quantizeNavmeshCoordinate(vertex.z ?? 0)}`;
1114
+ if (seen.has(key)) {
1115
+ throw new Error(`Navmesh polygon ${polygon.id} contains duplicate vertex ${key}`);
1116
+ }
1117
+ seen.add(key);
1118
+ }
1119
+ const area = polygonSignedArea2D(polygon.vertices);
1120
+ if (area === 0) {
1121
+ throw new Error(`Navmesh polygon ${polygon.id} has zero area`);
1122
+ }
1123
+ if (area < 0) {
1124
+ throw new Error(`Navmesh polygon ${polygon.id} must use counter-clockwise winding`);
1125
+ }
1126
+ validatePolygonConvexity(polygon);
1127
+ validatePolygonSelfIntersection(polygon);
1128
+ }
1129
+ function validatePolygonConvexity(polygon) {
1130
+ for (let index = 0; index < polygon.vertices.length; index += 1) {
1131
+ const previous = polygon.vertices[(index + polygon.vertices.length - 1) % polygon.vertices.length];
1132
+ const current = polygon.vertices[index];
1133
+ const next = polygon.vertices[(index + 1) % polygon.vertices.length];
1134
+ const turn = cross2D(previous, current, next);
1135
+ if (turn === 0) {
1136
+ throw new Error(`Navmesh polygon ${polygon.id} has collinear vertices`);
1137
+ }
1138
+ if (turn < 0) {
1139
+ throw new Error(`Navmesh polygon ${polygon.id} is concave`);
1140
+ }
1141
+ }
1142
+ }
1143
+ function validatePolygonSelfIntersection(polygon) {
1144
+ const edges = polygonEdges(polygon);
1145
+ for (let left = 0; left < edges.length; left += 1) {
1146
+ for (let right = left + 1; right < edges.length; right += 1) {
1147
+ if (edgesShareEndpointIndex(left, right, edges.length)) {
1148
+ continue;
1149
+ }
1150
+ if (segmentsProperlyIntersect2D(edges[left][0], edges[left][1], edges[right][0], edges[right][1])) {
1151
+ throw new Error(`Navmesh polygon ${polygon.id} self-intersects`);
1152
+ }
1153
+ }
1154
+ }
1155
+ }
1156
+ function validatePolygonInteriorDisjoint(polygons) {
1157
+ for (let left = 0; left < polygons.length; left += 1) {
1158
+ for (let right = left + 1; right < polygons.length; right += 1) {
1159
+ if (polygonZRangesAreDisjoint(polygons[left], polygons[right])) {
1160
+ continue;
1161
+ }
1162
+ if (polygonInteriorsOverlap(polygons[left], polygons[right])) {
1163
+ throw new Error(`Navmesh polygons ${polygons[left].id} and ${polygons[right].id} overlap`);
1164
+ }
1165
+ }
1166
+ }
1167
+ }
1168
+ function polygonInteriorsOverlap(left, right) {
1169
+ for (const [leftA, leftB] of polygonEdges(left)) {
1170
+ for (const [rightA, rightB] of polygonEdges(right)) {
1171
+ if (segmentsProperlyIntersect2D(leftA, leftB, rightA, rightB)) {
1172
+ return true;
1173
+ }
1174
+ }
1175
+ }
1176
+ return left.vertices.some((vertex) => pointStrictlyInsidePolygon2D(vertex, right))
1177
+ || right.vertices.some((vertex) => pointStrictlyInsidePolygon2D(vertex, left));
1178
+ }
1179
+ function expandLink(link) {
1180
+ if (link.bidirectional !== true) {
1181
+ return [link];
1182
+ }
1183
+ return [{ ...link, bidirectional: undefined }, { ...link, from: link.to, to: link.from, bidirectional: undefined }];
1184
+ }
1185
+ function isBlocked(polygon, disabledRegions, blockedPolygons) {
1186
+ return blockedPolygons.has(polygon.id) || (polygon.region !== undefined && disabledRegions.has(polygon.region));
1187
+ }
1188
+ function reconstructPath(cameFrom, id) {
1189
+ const path = [id];
1190
+ let current = id;
1191
+ while (cameFrom.has(current)) {
1192
+ current = cameFrom.get(current);
1193
+ path.push(current);
1194
+ }
1195
+ return path.reverse();
1196
+ }
1197
+ function countOffMesh(navmesh, path) {
1198
+ let count = 0;
1199
+ for (let index = 1; index < path.length; index += 1) {
1200
+ if (navmesh.links.find((link) => link.from === path[index - 1] && link.to === path[index])?.offMesh === true) {
1201
+ count += 1;
1202
+ }
1203
+ }
1204
+ return count;
1205
+ }
1206
+ function advanceNavmeshAgentWaypoint(agent, corridor, index, nextPolygon, target, frame, effectiveSpeed) {
1207
+ const nextIndex = Math.min(index + 1, corridor.length - 1);
1208
+ if (agent.x === undefined || agent.y === undefined) {
1209
+ return {
1210
+ agent: { ...agent, polygonId: nextPolygon, corridor, corridorIndex: nextIndex },
1211
+ advanced: nextPolygon !== agent.polygonId,
1212
+ };
1213
+ }
1214
+ const targetPoint = centroid(target);
1215
+ const updateInterval = agent.updateIntervalFrames ?? 1;
1216
+ if (updateInterval > 1 && frame % updateInterval !== 0) {
1217
+ return { agent: { ...agent, corridor, corridorIndex: index }, advanced: false };
1218
+ }
1219
+ const speed = effectiveSpeed ?? agent.speedPerFrame ?? 1000;
1220
+ const dx = targetPoint.x - agent.x;
1221
+ const dy = targetPoint.y - agent.y;
1222
+ const dz = targetPoint.z - (agent.z ?? 0);
1223
+ const distance = Math.abs(dx) + Math.abs(dy) + Math.abs(dz);
1224
+ const stoppingDistance = agent.stoppingDistance ?? 0;
1225
+ if (distance <= speed + stoppingDistance) {
1226
+ return {
1227
+ agent: { ...agent, ...targetPoint, polygonId: nextPolygon, corridor, corridorIndex: nextIndex },
1228
+ advanced: nextPolygon !== agent.polygonId,
1229
+ };
1230
+ }
1231
+ return {
1232
+ agent: {
1233
+ ...agent,
1234
+ x: agent.x + stepToward(dx, speed),
1235
+ y: agent.y + stepToward(dy, Math.max(0, speed - Math.abs(stepToward(dx, speed)))),
1236
+ z: (agent.z ?? 0) + stepToward(dz, Math.max(0, speed - Math.abs(stepToward(dx, speed)) - Math.abs(stepToward(dy, Math.max(0, speed - Math.abs(stepToward(dx, speed))))))),
1237
+ corridor,
1238
+ corridorIndex: index,
1239
+ },
1240
+ advanced: false,
1241
+ };
1242
+ }
1243
+ function stepToward(delta, budget) {
1244
+ if (delta === 0 || budget <= 0) {
1245
+ return 0;
1246
+ }
1247
+ return Math.sign(delta) * Math.min(Math.abs(delta), budget);
1248
+ }
1249
+ function validateNavmeshAgent(agent) {
1250
+ if (agent.speedPerFrame !== undefined && (!Number.isInteger(agent.speedPerFrame) || agent.speedPerFrame < 0)) {
1251
+ throw new Error(`Navmesh agent ${agent.id} has invalid speedPerFrame`);
1252
+ }
1253
+ }
1254
+ function centroid(polygon) {
1255
+ const sum = polygon.vertices.reduce((accumulator, point) => ({ x: accumulator.x + point.x, y: accumulator.y + point.y, z: accumulator.z + (point.z ?? 0) }), { x: 0, y: 0, z: 0 });
1256
+ return {
1257
+ x: quantizeNavmeshCoordinate(sum.x / polygon.vertices.length),
1258
+ y: quantizeNavmeshCoordinate(sum.y / polygon.vertices.length),
1259
+ z: quantizeNavmeshCoordinate(sum.z / polygon.vertices.length),
1260
+ };
1261
+ }
1262
+ function funnelDeterministicNavmeshPath(navmesh, polygonIds, start, goal) {
1263
+ if (polygonIds.length === 1) {
1264
+ return [start, goal];
1265
+ }
1266
+ const polygons = new Map(navmesh.polygons.map((polygon) => [polygon.id, polygon]));
1267
+ const corridor = polygonIds.map((id) => polygons.get(id));
1268
+ if (lineStaysInsideCorridor(start, goal, corridor)) {
1269
+ return [start, goal];
1270
+ }
1271
+ const points = [start];
1272
+ for (let index = 1; index < polygonIds.length - 1; index += 1) {
1273
+ const previous = polygons.get(polygonIds[index - 1]);
1274
+ const current = polygons.get(polygonIds[index]);
1275
+ const next = polygons.get(polygonIds[index + 1]);
1276
+ const portalIn = sharedPortalMidpoint(previous, current);
1277
+ const portalOut = sharedPortalMidpoint(current, next);
1278
+ if (portalIn === undefined || portalOut === undefined || !lineStaysInsidePolygon(portalIn, portalOut, current)) {
1279
+ points.push(centroid(current));
1280
+ }
1281
+ else {
1282
+ points.push(portalOut);
1283
+ }
1284
+ }
1285
+ points.push(goal);
1286
+ return points;
1287
+ }
1288
+ function lineStaysInsideCorridor(start, goal, polygons) {
1289
+ const sampleCount = Math.max(8, polygons.length * 4);
1290
+ for (let index = 0; index <= sampleCount; index += 1) {
1291
+ const t = index / sampleCount;
1292
+ const point = {
1293
+ x: quantizeNavmeshCoordinate(start.x + (goal.x - start.x) * t),
1294
+ y: quantizeNavmeshCoordinate(start.y + (goal.y - start.y) * t),
1295
+ z: quantizeNavmeshCoordinate((start.z ?? 0) + ((goal.z ?? 0) - (start.z ?? 0)) * t),
1296
+ };
1297
+ if (!polygons.some((polygon) => pointInPolygon3D(point, polygon))) {
1298
+ return false;
1299
+ }
1300
+ }
1301
+ return true;
1302
+ }
1303
+ function sharedPortalMidpoint(left, right) {
1304
+ const shared = left.vertices.filter((leftPoint) => right.vertices.some((rightPoint) => samePoint2D(leftPoint, rightPoint)));
1305
+ if (shared.length < 2) {
1306
+ return undefined;
1307
+ }
1308
+ const ordered = [...shared].sort((a, b) => compareStrings((0, canonical_js_1.canonicalStringify)(a), (0, canonical_js_1.canonicalStringify)(b)));
1309
+ return {
1310
+ x: (ordered[0].x + ordered[1].x) / 2,
1311
+ y: (ordered[0].y + ordered[1].y) / 2,
1312
+ z: ((ordered[0].z ?? 0) + (ordered[1].z ?? 0)) / 2,
1313
+ };
1314
+ }
1315
+ function pointInPolygon2D(point, polygon) {
1316
+ let inside = false;
1317
+ const x = point.x;
1318
+ const y = point.y;
1319
+ for (let i = 0, j = polygon.vertices.length - 1; i < polygon.vertices.length; j = i, i += 1) {
1320
+ const a = polygon.vertices[i];
1321
+ const b = polygon.vertices[j];
1322
+ if (pointOnSegment2D(point, a, b)) {
1323
+ return true;
1324
+ }
1325
+ const intersects = (a.y > y) !== (b.y > y)
1326
+ && x < ((b.x - a.x) * (y - a.y)) / (b.y - a.y) + a.x;
1327
+ if (intersects) {
1328
+ inside = !inside;
1329
+ }
1330
+ }
1331
+ return inside;
1332
+ }
1333
+ function pointInPolygon3D(point, polygon) {
1334
+ return pointInPolygon2D(point, polygon) && pointWithinPolygonZRange(point, polygon);
1335
+ }
1336
+ function pointWithinPolygonZRange(point, polygon) {
1337
+ const range = polygonZRange(polygon);
1338
+ const z = quantizeNavmeshCoordinate(point.z ?? 0);
1339
+ return z >= range.min && z <= range.max;
1340
+ }
1341
+ function pointOnSegment2D(point, a, b) {
1342
+ const cross = (point.y - a.y) * (b.x - a.x) - (point.x - a.x) * (b.y - a.y);
1343
+ if (cross !== 0) {
1344
+ return false;
1345
+ }
1346
+ return point.x >= Math.min(a.x, b.x) && point.x <= Math.max(a.x, b.x)
1347
+ && point.y >= Math.min(a.y, b.y) && point.y <= Math.max(a.y, b.y);
1348
+ }
1349
+ function pointStrictlyInsidePolygon2D(point, polygon) {
1350
+ return pointInPolygon2D(point, polygon)
1351
+ && !polygonEdges(polygon).some(([a, b]) => pointOnSegment2D(point, a, b));
1352
+ }
1353
+ function polygonSignedArea2D(points) {
1354
+ let doubleArea = 0;
1355
+ for (let index = 0; index < points.length; index += 1) {
1356
+ const current = points[index];
1357
+ const next = points[(index + 1) % points.length];
1358
+ doubleArea += current.x * next.y - next.x * current.y;
1359
+ }
1360
+ return quantizeNavmeshCoordinate(doubleArea / 2);
1361
+ }
1362
+ function polygonEdges(polygon) {
1363
+ return polygon.vertices.map((point, index) => [point, polygon.vertices[(index + 1) % polygon.vertices.length]]);
1364
+ }
1365
+ function edgesShareEndpointIndex(left, right, edgeCount) {
1366
+ return left === right
1367
+ || (left + 1) % edgeCount === right
1368
+ || (right + 1) % edgeCount === left;
1369
+ }
1370
+ function segmentsProperlyIntersect2D(a, b, c, d) {
1371
+ const left = cross2D(a, b, c);
1372
+ const right = cross2D(a, b, d);
1373
+ const top = cross2D(c, d, a);
1374
+ const bottom = cross2D(c, d, b);
1375
+ return left !== 0
1376
+ && right !== 0
1377
+ && top !== 0
1378
+ && bottom !== 0
1379
+ && (left > 0) !== (right > 0)
1380
+ && (top > 0) !== (bottom > 0);
1381
+ }
1382
+ function cross2D(a, b, c) {
1383
+ return quantizeNavmeshCoordinate((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x));
1384
+ }
1385
+ function lineStaysInsidePolygon(a, b, polygon) {
1386
+ const mid = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2, z: (pointZ(a) + pointZ(b)) / 2 };
1387
+ return pointInPolygon3D(mid, polygon);
1388
+ }
1389
+ function pointZ(point) {
1390
+ return point.z;
1391
+ }
1392
+ function samePoint2D(left, right) {
1393
+ return left.x === right.x && left.y === right.y && (left.z ?? 0) === (right.z ?? 0);
1394
+ }
1395
+ function quantizeNavmeshCoordinate(value) {
1396
+ // Pure integer rounding, not Number.prototype.toFixed — cross-engine
1397
+ // half-way divergence on a checksummed path (see physics2d quantizeDistance).
1398
+ const quantized = Math.round(value * 1e6) / 1e6;
1399
+ return quantized === 0 ? 0 : quantized;
1400
+ }
1401
+ function deterministicSqrt(value) {
1402
+ if (value <= 0) {
1403
+ return 0;
1404
+ }
1405
+ let estimate = value >= 1 ? value : 1;
1406
+ for (let iteration = 0; iteration < 8; iteration += 1) {
1407
+ estimate = (estimate + value / estimate) / 2;
1408
+ }
1409
+ return quantizeNavmeshCoordinate(estimate);
1410
+ }
1411
+ function emptyPath() {
1412
+ return { found: false, polygons: [], points: [], offMeshTraversals: 0, visitedPolygons: 0, pathHash: (0, canonical_js_1.defaultChecksum)([]) };
1413
+ }
1414
+ function comparePolygons(left, right) {
1415
+ return compareStrings(left.id, right.id);
1416
+ }
1417
+ function polygonZRangesAreDisjoint(left, right) {
1418
+ const leftRange = polygonZRange(left);
1419
+ const rightRange = polygonZRange(right);
1420
+ return leftRange.max < rightRange.min || rightRange.max < leftRange.min;
1421
+ }
1422
+ function polygonZRange(polygon) {
1423
+ const values = polygon.vertices.map((point) => quantizeNavmeshCoordinate(point.z ?? 0));
1424
+ return {
1425
+ min: Math.min(...values),
1426
+ max: Math.max(...values),
1427
+ };
1428
+ }
1429
+ function compareLinks(left, right) {
1430
+ return compareStrings(left.from, right.from) || compareStrings(left.to, right.to);
1431
+ }
1432
+ function compareAgents(left, right) {
1433
+ return compareStrings(left.id, right.id);
1434
+ }
1435
+ function compareStrings(left, right) {
1436
+ return left < right ? -1 : left > right ? 1 : 0;
1437
+ }