@thunderclap-engine/engine 0.0.0-stage → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (566) hide show
  1. package/CHANGELOG.md +390 -0
  2. package/LICENSE +21 -0
  3. package/README.md +333 -2
  4. package/dist/analytics/analytics.d.ts +80 -0
  5. package/dist/analytics/analytics.js +129 -0
  6. package/dist/analytics/index.d.ts +1 -0
  7. package/dist/analytics/index.js +1 -0
  8. package/dist/anim/aim.d.ts +43 -0
  9. package/dist/anim/aim.js +157 -0
  10. package/dist/anim/clip.d.ts +98 -0
  11. package/dist/anim/clip.js +282 -0
  12. package/dist/anim/compress.d.ts +65 -0
  13. package/dist/anim/compress.js +339 -0
  14. package/dist/anim/foot-ik.d.ts +74 -0
  15. package/dist/anim/foot-ik.js +241 -0
  16. package/dist/anim/graph.d.ts +153 -0
  17. package/dist/anim/graph.js +532 -0
  18. package/dist/anim/ik.d.ts +105 -0
  19. package/dist/anim/ik.js +435 -0
  20. package/dist/anim/index.d.ts +36 -0
  21. package/dist/anim/index.js +21 -0
  22. package/dist/anim/match.d.ts +66 -0
  23. package/dist/anim/match.js +153 -0
  24. package/dist/anim/mixer.d.ts +113 -0
  25. package/dist/anim/mixer.js +314 -0
  26. package/dist/anim/morph.d.ts +41 -0
  27. package/dist/anim/morph.js +94 -0
  28. package/dist/anim/pose.d.ts +74 -0
  29. package/dist/anim/pose.js +254 -0
  30. package/dist/anim/quat.d.ts +44 -0
  31. package/dist/anim/quat.js +187 -0
  32. package/dist/anim/retarget.d.ts +97 -0
  33. package/dist/anim/retarget.js +366 -0
  34. package/dist/anim/root-motion.d.ts +44 -0
  35. package/dist/anim/root-motion.js +224 -0
  36. package/dist/anim/skin-pose.d.ts +39 -0
  37. package/dist/anim/skin-pose.js +53 -0
  38. package/dist/anim/socket.d.ts +47 -0
  39. package/dist/anim/socket.js +84 -0
  40. package/dist/anim/spring-chain.d.ts +73 -0
  41. package/dist/anim/spring-chain.js +294 -0
  42. package/dist/assets/atlas.d.ts +90 -0
  43. package/dist/assets/atlas.js +212 -0
  44. package/dist/assets/baked.d.ts +33 -0
  45. package/dist/assets/baked.js +38 -0
  46. package/dist/assets/bundle.d.ts +88 -0
  47. package/dist/assets/bundle.js +464 -0
  48. package/dist/assets/cache.d.ts +56 -0
  49. package/dist/assets/cache.js +123 -0
  50. package/dist/assets/gltf-load.d.ts +252 -0
  51. package/dist/assets/gltf-load.js +661 -0
  52. package/dist/assets/gltf-skin.d.ts +36 -0
  53. package/dist/assets/gltf-skin.js +109 -0
  54. package/dist/assets/gltf.d.ts +706 -0
  55. package/dist/assets/gltf.js +1376 -0
  56. package/dist/assets/index.d.ts +20 -0
  57. package/dist/assets/index.js +10 -0
  58. package/dist/assets/ktx2.d.ts +71 -0
  59. package/dist/assets/ktx2.js +213 -0
  60. package/dist/assets/manifest.d.ts +79 -0
  61. package/dist/assets/manifest.js +7 -0
  62. package/dist/assets/meshopt.d.ts +23 -0
  63. package/dist/assets/meshopt.js +403 -0
  64. package/dist/audio/audio.d.ts +733 -0
  65. package/dist/audio/audio.js +2013 -0
  66. package/dist/audio/dsp.d.ts +61 -0
  67. package/dist/audio/dsp.js +159 -0
  68. package/dist/audio/duck.d.ts +44 -0
  69. package/dist/audio/duck.js +101 -0
  70. package/dist/audio/envelope.d.ts +53 -0
  71. package/dist/audio/envelope.js +106 -0
  72. package/dist/audio/index.d.ts +13 -0
  73. package/dist/audio/index.js +14 -0
  74. package/dist/audio/music.d.ts +113 -0
  75. package/dist/audio/music.js +548 -0
  76. package/dist/audio/occlusion.d.ts +55 -0
  77. package/dist/audio/occlusion.js +61 -0
  78. package/dist/audio/persist.d.ts +23 -0
  79. package/dist/audio/persist.js +27 -0
  80. package/dist/audio/recipes.d.ts +55 -0
  81. package/dist/audio/recipes.js +227 -0
  82. package/dist/audio/send.d.ts +41 -0
  83. package/dist/audio/send.js +118 -0
  84. package/dist/audio/sprites.d.ts +59 -0
  85. package/dist/audio/sprites.js +43 -0
  86. package/dist/audio/synth.d.ts +103 -0
  87. package/dist/audio/synth.js +197 -0
  88. package/dist/audio/variations.d.ts +25 -0
  89. package/dist/audio/variations.js +55 -0
  90. package/dist/audio/zones.d.ts +100 -0
  91. package/dist/audio/zones.js +167 -0
  92. package/dist/core/detached-surface.d.ts +44 -0
  93. package/dist/core/detached-surface.js +104 -0
  94. package/dist/core/events.d.ts +24 -0
  95. package/dist/core/events.js +43 -0
  96. package/dist/core/game.d.ts +409 -0
  97. package/dist/core/game.js +530 -0
  98. package/dist/core/hash.d.ts +24 -0
  99. package/dist/core/hash.js +49 -0
  100. package/dist/core/index.d.ts +17 -0
  101. package/dist/core/index.js +9 -0
  102. package/dist/core/loop.d.ts +113 -0
  103. package/dist/core/loop.js +185 -0
  104. package/dist/core/math.d.ts +58 -0
  105. package/dist/core/math.js +210 -0
  106. package/dist/core/noise.d.ts +40 -0
  107. package/dist/core/noise.js +247 -0
  108. package/dist/core/pool.d.ts +42 -0
  109. package/dist/core/pool.js +85 -0
  110. package/dist/core/random.d.ts +61 -0
  111. package/dist/core/random.js +199 -0
  112. package/dist/core/scene.d.ts +79 -0
  113. package/dist/core/scene.js +195 -0
  114. package/dist/core/timer.d.ts +66 -0
  115. package/dist/core/timer.js +286 -0
  116. package/dist/fx/billboard-wgsl.d.ts +29 -0
  117. package/dist/fx/billboard-wgsl.js +158 -0
  118. package/dist/fx/billboard.d.ts +249 -0
  119. package/dist/fx/billboard.js +702 -0
  120. package/dist/fx/debug-draw.d.ts +127 -0
  121. package/dist/fx/debug-draw.js +523 -0
  122. package/dist/fx/debug-wgsl.d.ts +7 -0
  123. package/dist/fx/debug-wgsl.js +32 -0
  124. package/dist/fx/decals-wgsl.d.ts +15 -0
  125. package/dist/fx/decals-wgsl.js +173 -0
  126. package/dist/fx/decals.d.ts +216 -0
  127. package/dist/fx/decals.js +607 -0
  128. package/dist/fx/effect.d.ts +129 -0
  129. package/dist/fx/effect.js +349 -0
  130. package/dist/fx/effects/hit-spark.json +48 -0
  131. package/dist/fx/effects/index.d.ts +8 -0
  132. package/dist/fx/effects/index.js +9 -0
  133. package/dist/fx/effects/magic-aura.json +47 -0
  134. package/dist/fx/effects/smoke-puff.json +34 -0
  135. package/dist/fx/emit-surface.d.ts +40 -0
  136. package/dist/fx/emit-surface.js +124 -0
  137. package/dist/fx/flicker.d.ts +30 -0
  138. package/dist/fx/flicker.js +42 -0
  139. package/dist/fx/index.d.ts +31 -0
  140. package/dist/fx/index.js +20 -0
  141. package/dist/fx/particles-wgsl.d.ts +57 -0
  142. package/dist/fx/particles-wgsl.js +828 -0
  143. package/dist/fx/particles.d.ts +566 -0
  144. package/dist/fx/particles.js +1533 -0
  145. package/dist/fx/random.d.ts +15 -0
  146. package/dist/fx/random.js +20 -0
  147. package/dist/fx/sky-wgsl.d.ts +23 -0
  148. package/dist/fx/sky-wgsl.js +127 -0
  149. package/dist/fx/sky.d.ts +96 -0
  150. package/dist/fx/sky.js +175 -0
  151. package/dist/fx/text-atlas.d.ts +143 -0
  152. package/dist/fx/text-atlas.js +327 -0
  153. package/dist/fx/trail-wgsl.d.ts +11 -0
  154. package/dist/fx/trail-wgsl.js +42 -0
  155. package/dist/fx/trail.d.ts +194 -0
  156. package/dist/fx/trail.js +555 -0
  157. package/dist/fx/water-wgsl.d.ts +34 -0
  158. package/dist/fx/water-wgsl.js +197 -0
  159. package/dist/fx/water.d.ts +176 -0
  160. package/dist/fx/water.js +371 -0
  161. package/dist/geom/collide/bvh.d.ts +151 -0
  162. package/dist/geom/collide/bvh.js +493 -0
  163. package/dist/geom/collide/debug.d.ts +14 -0
  164. package/dist/geom/collide/debug.js +74 -0
  165. package/dist/geom/collide/grid.d.ts +26 -0
  166. package/dist/geom/collide/grid.js +91 -0
  167. package/dist/geom/collide/index.d.ts +8 -0
  168. package/dist/geom/collide/index.js +8 -0
  169. package/dist/geom/collide/queries.d.ts +70 -0
  170. package/dist/geom/collide/queries.js +583 -0
  171. package/dist/geom/collide/serialize.d.ts +14 -0
  172. package/dist/geom/collide/serialize.js +145 -0
  173. package/dist/geom/collide/shapes.d.ts +161 -0
  174. package/dist/geom/collide/shapes.js +686 -0
  175. package/dist/geom/collide/slide.d.ts +66 -0
  176. package/dist/geom/collide/slide.js +375 -0
  177. package/dist/geom/collide/tile-slide.d.ts +88 -0
  178. package/dist/geom/collide/tile-slide.js +255 -0
  179. package/dist/geom/geometry.d.ts +95 -0
  180. package/dist/geom/geometry.js +322 -0
  181. package/dist/geom/heightfield.d.ts +72 -0
  182. package/dist/geom/heightfield.js +295 -0
  183. package/dist/geom/hull.d.ts +27 -0
  184. package/dist/geom/hull.js +279 -0
  185. package/dist/geom/index.d.ts +35 -0
  186. package/dist/geom/index.js +23 -0
  187. package/dist/geom/iso.d.ts +74 -0
  188. package/dist/geom/iso.js +89 -0
  189. package/dist/geom/lod.d.ts +29 -0
  190. package/dist/geom/lod.js +27 -0
  191. package/dist/geom/ops.d.ts +47 -0
  192. package/dist/geom/ops.js +285 -0
  193. package/dist/geom/primitives.d.ts +97 -0
  194. package/dist/geom/primitives.js +359 -0
  195. package/dist/geom/shapes.d.ts +43 -0
  196. package/dist/geom/shapes.js +88 -0
  197. package/dist/geom/simplify.d.ts +38 -0
  198. package/dist/geom/simplify.js +519 -0
  199. package/dist/geom/split.d.ts +37 -0
  200. package/dist/geom/split.js +413 -0
  201. package/dist/geom/sweep.d.ts +83 -0
  202. package/dist/geom/sweep.js +534 -0
  203. package/dist/geom/tangents.d.ts +16 -0
  204. package/dist/geom/tangents.js +124 -0
  205. package/dist/geom/triangulate.d.ts +19 -0
  206. package/dist/geom/triangulate.js +147 -0
  207. package/dist/host/default.d.ts +10 -0
  208. package/dist/host/default.js +10 -0
  209. package/dist/host/index.d.ts +4 -0
  210. package/dist/host/index.js +3 -0
  211. package/dist/host/types.d.ts +144 -0
  212. package/dist/host/types.js +1 -0
  213. package/dist/host/web-links.d.ts +38 -0
  214. package/dist/host/web-links.js +110 -0
  215. package/dist/host/web.d.ts +30 -0
  216. package/dist/host/web.js +216 -0
  217. package/dist/host/xr.d.ts +24 -0
  218. package/dist/host/xr.js +59 -0
  219. package/dist/i18n/i18n.d.ts +59 -0
  220. package/dist/i18n/i18n.js +241 -0
  221. package/dist/i18n/index.d.ts +3 -0
  222. package/dist/i18n/index.js +3 -0
  223. package/dist/i18n/message.d.ts +72 -0
  224. package/dist/i18n/message.js +369 -0
  225. package/dist/i18n/negotiate.d.ts +17 -0
  226. package/dist/i18n/negotiate.js +58 -0
  227. package/dist/index.d.ts +7 -0
  228. package/dist/index.js +1 -0
  229. package/dist/input/bindings.d.ts +31 -0
  230. package/dist/input/bindings.js +141 -0
  231. package/dist/input/clock.d.ts +44 -0
  232. package/dist/input/clock.js +49 -0
  233. package/dist/input/follow.d.ts +133 -0
  234. package/dist/input/follow.js +218 -0
  235. package/dist/input/gestures.d.ts +281 -0
  236. package/dist/input/gestures.js +662 -0
  237. package/dist/input/glyphs.d.ts +35 -0
  238. package/dist/input/glyphs.js +216 -0
  239. package/dist/input/gyro.d.ts +85 -0
  240. package/dist/input/gyro.js +140 -0
  241. package/dist/input/index.d.ts +19 -0
  242. package/dist/input/index.js +16 -0
  243. package/dist/input/input.d.ts +543 -0
  244. package/dist/input/input.js +1492 -0
  245. package/dist/input/look.d.ts +327 -0
  246. package/dist/input/look.js +681 -0
  247. package/dist/input/magnify.d.ts +177 -0
  248. package/dist/input/magnify.js +131 -0
  249. package/dist/input/pointer-dispatch.d.ts +138 -0
  250. package/dist/input/pointer-dispatch.js +180 -0
  251. package/dist/input/pointer-router.d.ts +204 -0
  252. package/dist/input/pointer-router.js +533 -0
  253. package/dist/input/pointer-sources.d.ts +164 -0
  254. package/dist/input/pointer-sources.js +306 -0
  255. package/dist/input/rays.d.ts +138 -0
  256. package/dist/input/rays.js +615 -0
  257. package/dist/input/record.d.ts +209 -0
  258. package/dist/input/record.js +601 -0
  259. package/dist/input/touch-zones.d.ts +147 -0
  260. package/dist/input/touch-zones.js +300 -0
  261. package/dist/input/types.d.ts +158 -0
  262. package/dist/input/types.js +1 -0
  263. package/dist/input/xr-source.d.ts +53 -0
  264. package/dist/input/xr-source.js +164 -0
  265. package/dist/nav/astar.d.ts +45 -0
  266. package/dist/nav/astar.js +182 -0
  267. package/dist/nav/corridor.d.ts +40 -0
  268. package/dist/nav/corridor.js +95 -0
  269. package/dist/nav/crowd.d.ts +94 -0
  270. package/dist/nav/crowd.js +624 -0
  271. package/dist/nav/flow.d.ts +61 -0
  272. package/dist/nav/flow.js +193 -0
  273. package/dist/nav/grid.d.ts +108 -0
  274. package/dist/nav/grid.js +210 -0
  275. package/dist/nav/heap.d.ts +22 -0
  276. package/dist/nav/heap.js +92 -0
  277. package/dist/nav/index.d.ts +37 -0
  278. package/dist/nav/index.js +19 -0
  279. package/dist/nav/mesh/areas.d.ts +44 -0
  280. package/dist/nav/mesh/areas.js +98 -0
  281. package/dist/nav/mesh/bake-options.d.ts +33 -0
  282. package/dist/nav/mesh/bake-options.js +76 -0
  283. package/dist/nav/mesh/contour.d.ts +22 -0
  284. package/dist/nav/mesh/contour.js +323 -0
  285. package/dist/nav/mesh/flow3.d.ts +35 -0
  286. package/dist/nav/mesh/flow3.js +229 -0
  287. package/dist/nav/mesh/links.d.ts +38 -0
  288. package/dist/nav/mesh/links.js +72 -0
  289. package/dist/nav/mesh/mesh-path.d.ts +152 -0
  290. package/dist/nav/mesh/mesh-path.js +643 -0
  291. package/dist/nav/mesh/mesh-reach.d.ts +78 -0
  292. package/dist/nav/mesh/mesh-reach.js +450 -0
  293. package/dist/nav/mesh/navmesh.d.ts +117 -0
  294. package/dist/nav/mesh/navmesh.js +647 -0
  295. package/dist/nav/mesh/polymesh.d.ts +24 -0
  296. package/dist/nav/mesh/polymesh.js +384 -0
  297. package/dist/nav/mesh/regions.d.ts +12 -0
  298. package/dist/nav/mesh/regions.js +367 -0
  299. package/dist/nav/mesh/serialize.d.ts +13 -0
  300. package/dist/nav/mesh/serialize.js +197 -0
  301. package/dist/nav/mesh/voxelize.d.ts +111 -0
  302. package/dist/nav/mesh/voxelize.js +552 -0
  303. package/dist/nav/obstacles.d.ts +75 -0
  304. package/dist/nav/obstacles.js +294 -0
  305. package/dist/nav/path.d.ts +59 -0
  306. package/dist/nav/path.js +177 -0
  307. package/dist/nav/query-queue.d.ts +46 -0
  308. package/dist/nav/query-queue.js +308 -0
  309. package/dist/nav/reach.d.ts +53 -0
  310. package/dist/nav/reach.js +148 -0
  311. package/dist/nav/steer.d.ts +36 -0
  312. package/dist/nav/steer.js +78 -0
  313. package/dist/platform/boot.d.ts +28 -0
  314. package/dist/platform/boot.js +43 -0
  315. package/dist/platform/device-mode.d.ts +40 -0
  316. package/dist/platform/device-mode.js +104 -0
  317. package/dist/platform/device.d.ts +31 -0
  318. package/dist/platform/device.js +91 -0
  319. package/dist/platform/environment.d.ts +42 -0
  320. package/dist/platform/environment.js +82 -0
  321. package/dist/platform/gpu.d.ts +40 -0
  322. package/dist/platform/gpu.js +57 -0
  323. package/dist/platform/index.d.ts +18 -0
  324. package/dist/platform/index.js +10 -0
  325. package/dist/platform/letterbox.d.ts +43 -0
  326. package/dist/platform/letterbox.js +57 -0
  327. package/dist/platform/links.d.ts +20 -0
  328. package/dist/platform/links.js +43 -0
  329. package/dist/platform/screens.d.ts +36 -0
  330. package/dist/platform/screens.js +138 -0
  331. package/dist/platform/surface.d.ts +72 -0
  332. package/dist/platform/surface.js +157 -0
  333. package/dist/platform/xr-fake.d.ts +168 -0
  334. package/dist/platform/xr-fake.js +434 -0
  335. package/dist/platform/xr-surface.d.ts +120 -0
  336. package/dist/platform/xr-surface.js +214 -0
  337. package/dist/platform/xr-types.d.ts +262 -0
  338. package/dist/post/ao.d.ts +12 -0
  339. package/dist/post/ao.js +136 -0
  340. package/dist/post/blend.d.ts +27 -0
  341. package/dist/post/blend.js +129 -0
  342. package/dist/post/depth-pass.d.ts +43 -0
  343. package/dist/post/depth-pass.js +118 -0
  344. package/dist/post/dof.d.ts +1 -0
  345. package/dist/post/dof.js +66 -0
  346. package/dist/post/exposure.d.ts +80 -0
  347. package/dist/post/exposure.js +322 -0
  348. package/dist/post/index.d.ts +12 -0
  349. package/dist/post/index.js +11 -0
  350. package/dist/post/lens-dirt.d.ts +13 -0
  351. package/dist/post/lens-dirt.js +91 -0
  352. package/dist/post/lut.d.ts +19 -0
  353. package/dist/post/lut.js +50 -0
  354. package/dist/post/motion-blur.d.ts +9 -0
  355. package/dist/post/motion-blur.js +78 -0
  356. package/dist/post/post-stack.d.ts +302 -0
  357. package/dist/post/post-stack.js +1137 -0
  358. package/dist/post/settings.d.ts +485 -0
  359. package/dist/post/settings.js +416 -0
  360. package/dist/post/shaders.d.ts +25 -0
  361. package/dist/post/shaders.js +410 -0
  362. package/dist/post/ssr.d.ts +16 -0
  363. package/dist/post/ssr.js +155 -0
  364. package/dist/post/taa.d.ts +74 -0
  365. package/dist/post/taa.js +235 -0
  366. package/dist/post/white-balance.d.ts +25 -0
  367. package/dist/post/white-balance.js +98 -0
  368. package/dist/render/bindings.d.ts +98 -0
  369. package/dist/render/bindings.js +281 -0
  370. package/dist/render/blend.d.ts +13 -0
  371. package/dist/render/blend.js +26 -0
  372. package/dist/render/camera-2d.d.ts +72 -0
  373. package/dist/render/camera-2d.js +92 -0
  374. package/dist/render/camera.d.ts +302 -0
  375. package/dist/render/camera.js +671 -0
  376. package/dist/render/color.d.ts +16 -0
  377. package/dist/render/color.js +41 -0
  378. package/dist/render/compressed.d.ts +104 -0
  379. package/dist/render/compressed.js +248 -0
  380. package/dist/render/diagnostics.d.ts +15 -0
  381. package/dist/render/diagnostics.js +43 -0
  382. package/dist/render/dynamic-resolution.d.ts +70 -0
  383. package/dist/render/dynamic-resolution.js +126 -0
  384. package/dist/render/fake-gpu.d.ts +204 -0
  385. package/dist/render/fake-gpu.js +426 -0
  386. package/dist/render/flipbook.d.ts +56 -0
  387. package/dist/render/flipbook.js +87 -0
  388. package/dist/render/frame.d.ts +278 -0
  389. package/dist/render/frame.js +174 -0
  390. package/dist/render/fullscreen.d.ts +54 -0
  391. package/dist/render/fullscreen.js +91 -0
  392. package/dist/render/gpu-timer.d.ts +73 -0
  393. package/dist/render/gpu-timer.js +186 -0
  394. package/dist/render/index.d.ts +53 -0
  395. package/dist/render/index.js +29 -0
  396. package/dist/render/instance-list.d.ts +185 -0
  397. package/dist/render/instance-list.js +547 -0
  398. package/dist/render/lights.d.ts +181 -0
  399. package/dist/render/lights.js +233 -0
  400. package/dist/render/lod.d.ts +54 -0
  401. package/dist/render/lod.js +52 -0
  402. package/dist/render/material.d.ts +165 -0
  403. package/dist/render/material.js +292 -0
  404. package/dist/render/mesh.d.ts +284 -0
  405. package/dist/render/mesh.js +472 -0
  406. package/dist/render/mipmap.d.ts +16 -0
  407. package/dist/render/mipmap.js +121 -0
  408. package/dist/render/parallax.d.ts +50 -0
  409. package/dist/render/parallax.js +47 -0
  410. package/dist/render/pbr-wgsl.d.ts +31 -0
  411. package/dist/render/pbr-wgsl.js +119 -0
  412. package/dist/render/ping-pong.d.ts +42 -0
  413. package/dist/render/ping-pong.js +74 -0
  414. package/dist/render/renderer.d.ts +741 -0
  415. package/dist/render/renderer.js +2486 -0
  416. package/dist/render/scene-pipelines.d.ts +44 -0
  417. package/dist/render/scene-pipelines.js +92 -0
  418. package/dist/render/scene-target.d.ts +170 -0
  419. package/dist/render/scene-target.js +450 -0
  420. package/dist/render/shadow.d.ts +74 -0
  421. package/dist/render/shadow.js +208 -0
  422. package/dist/render/skin.d.ts +112 -0
  423. package/dist/render/skin.js +240 -0
  424. package/dist/render/snapshot.d.ts +36 -0
  425. package/dist/render/snapshot.js +34 -0
  426. package/dist/render/standard.d.ts +162 -0
  427. package/dist/render/standard.js +263 -0
  428. package/dist/render/texture.d.ts +142 -0
  429. package/dist/render/texture.js +228 -0
  430. package/dist/render/tile-layer.d.ts +110 -0
  431. package/dist/render/tile-layer.js +304 -0
  432. package/dist/render/transform.d.ts +115 -0
  433. package/dist/render/transform.js +318 -0
  434. package/dist/render/wgsl.d.ts +131 -0
  435. package/dist/render/wgsl.js +528 -0
  436. package/dist/save/codec.d.ts +49 -0
  437. package/dist/save/codec.js +169 -0
  438. package/dist/save/index.d.ts +8 -0
  439. package/dist/save/index.js +8 -0
  440. package/dist/save/journal.d.ts +24 -0
  441. package/dist/save/journal.js +102 -0
  442. package/dist/save/migrate.d.ts +15 -0
  443. package/dist/save/migrate.js +37 -0
  444. package/dist/save/schema.d.ts +72 -0
  445. package/dist/save/schema.js +154 -0
  446. package/dist/save/settings.d.ts +28 -0
  447. package/dist/save/settings.js +59 -0
  448. package/dist/save/slots.d.ts +56 -0
  449. package/dist/save/slots.js +200 -0
  450. package/dist/save/storage.d.ts +35 -0
  451. package/dist/save/storage.js +62 -0
  452. package/dist/save/store.d.ts +114 -0
  453. package/dist/save/store.js +269 -0
  454. package/dist/testing/fake-host.d.ts +103 -0
  455. package/dist/testing/fake-host.js +163 -0
  456. package/dist/testing/fixed-clock.d.ts +25 -0
  457. package/dist/testing/fixed-clock.js +30 -0
  458. package/dist/testing/index.d.ts +4 -0
  459. package/dist/testing/index.js +5 -0
  460. package/dist/testing/test-game.d.ts +76 -0
  461. package/dist/testing/test-game.js +92 -0
  462. package/dist/tween/ease.d.ts +77 -0
  463. package/dist/tween/ease.js +192 -0
  464. package/dist/tween/index.d.ts +8 -0
  465. package/dist/tween/index.js +7 -0
  466. package/dist/tween/timeline.d.ts +78 -0
  467. package/dist/tween/timeline.js +269 -0
  468. package/dist/tween/tween.d.ts +168 -0
  469. package/dist/tween/tween.js +320 -0
  470. package/dist/ui/anchor.d.ts +206 -0
  471. package/dist/ui/anchor.js +307 -0
  472. package/dist/ui/caption.d.ts +122 -0
  473. package/dist/ui/caption.js +286 -0
  474. package/dist/ui/cursor.d.ts +105 -0
  475. package/dist/ui/cursor.js +261 -0
  476. package/dist/ui/dom.d.ts +30 -0
  477. package/dist/ui/dom.js +92 -0
  478. package/dist/ui/floats.d.ts +86 -0
  479. package/dist/ui/floats.js +260 -0
  480. package/dist/ui/hud.d.ts +198 -0
  481. package/dist/ui/hud.js +431 -0
  482. package/dist/ui/index.d.ts +37 -0
  483. package/dist/ui/index.js +34 -0
  484. package/dist/ui/inventory-card.d.ts +52 -0
  485. package/dist/ui/inventory-card.js +173 -0
  486. package/dist/ui/inventory-context.d.ts +29 -0
  487. package/dist/ui/inventory-context.js +125 -0
  488. package/dist/ui/inventory-drag.d.ts +63 -0
  489. package/dist/ui/inventory-drag.js +394 -0
  490. package/dist/ui/inventory-grid.d.ts +66 -0
  491. package/dist/ui/inventory-grid.js +625 -0
  492. package/dist/ui/inventory-radial.d.ts +39 -0
  493. package/dist/ui/inventory-radial.js +168 -0
  494. package/dist/ui/inventory.d.ts +128 -0
  495. package/dist/ui/inventory.js +154 -0
  496. package/dist/ui/layout.d.ts +356 -0
  497. package/dist/ui/layout.js +856 -0
  498. package/dist/ui/link.d.ts +64 -0
  499. package/dist/ui/link.js +155 -0
  500. package/dist/ui/loading.d.ts +40 -0
  501. package/dist/ui/loading.js +157 -0
  502. package/dist/ui/loupe.d.ts +74 -0
  503. package/dist/ui/loupe.js +164 -0
  504. package/dist/ui/menu-nav.d.ts +33 -0
  505. package/dist/ui/menu-nav.js +28 -0
  506. package/dist/ui/minimap-canvas.d.ts +145 -0
  507. package/dist/ui/minimap-canvas.js +460 -0
  508. package/dist/ui/notify.d.ts +40 -0
  509. package/dist/ui/notify.js +141 -0
  510. package/dist/ui/orb.d.ts +53 -0
  511. package/dist/ui/orb.js +304 -0
  512. package/dist/ui/overlay.d.ts +213 -0
  513. package/dist/ui/overlay.js +394 -0
  514. package/dist/ui/padnav.d.ts +53 -0
  515. package/dist/ui/padnav.js +146 -0
  516. package/dist/ui/perf.d.ts +130 -0
  517. package/dist/ui/perf.js +329 -0
  518. package/dist/ui/rotate-card.d.ts +54 -0
  519. package/dist/ui/rotate-card.js +97 -0
  520. package/dist/ui/stack.d.ts +128 -0
  521. package/dist/ui/stack.js +312 -0
  522. package/dist/ui/styles.d.ts +13 -0
  523. package/dist/ui/styles.js +200 -0
  524. package/dist/ui/text-scale.d.ts +67 -0
  525. package/dist/ui/text-scale.js +102 -0
  526. package/dist/ui/theme.d.ts +70 -0
  527. package/dist/ui/theme.js +98 -0
  528. package/dist/ui/toast.d.ts +23 -0
  529. package/dist/ui/toast.js +46 -0
  530. package/dist/ui/tooltip.d.ts +89 -0
  531. package/dist/ui/tooltip.js +333 -0
  532. package/dist/ui/widgets-2.d.ts +134 -0
  533. package/dist/ui/widgets-2.js +399 -0
  534. package/dist/ui/widgets-3.d.ts +91 -0
  535. package/dist/ui/widgets-3.js +212 -0
  536. package/dist/ui/widgets.d.ts +34 -0
  537. package/dist/ui/widgets.js +50 -0
  538. package/dist/ui/world-panel/index.d.ts +5 -0
  539. package/dist/ui/world-panel/index.js +5 -0
  540. package/dist/ui/world-panel/layout.d.ts +38 -0
  541. package/dist/ui/world-panel/layout.js +68 -0
  542. package/dist/ui/world-panel/paint.d.ts +96 -0
  543. package/dist/ui/world-panel/paint.js +136 -0
  544. package/dist/ui/world-panel/panel.d.ts +235 -0
  545. package/dist/ui/world-panel/panel.js +762 -0
  546. package/dist/ui/world-panel/wgsl.d.ts +10 -0
  547. package/dist/ui/world-panel/wgsl.js +52 -0
  548. package/dist/ui/world-panel/widgets.d.ts +207 -0
  549. package/dist/ui/world-panel/widgets.js +569 -0
  550. package/dist/voxel/chunks.d.ts +77 -0
  551. package/dist/voxel/chunks.js +149 -0
  552. package/dist/voxel/grid.d.ts +84 -0
  553. package/dist/voxel/grid.js +223 -0
  554. package/dist/voxel/index.d.ts +22 -0
  555. package/dist/voxel/index.js +7 -0
  556. package/dist/voxel/mesher-worker.d.ts +14 -0
  557. package/dist/voxel/mesher-worker.js +47 -0
  558. package/dist/voxel/mesher.d.ts +73 -0
  559. package/dist/voxel/mesher.js +306 -0
  560. package/dist/voxel/ray.d.ts +32 -0
  561. package/dist/voxel/ray.js +73 -0
  562. package/dist/voxel/wgsl.d.ts +16 -0
  563. package/dist/voxel/wgsl.js +28 -0
  564. package/dist/voxel/worker-client.d.ts +79 -0
  565. package/dist/voxel/worker-client.js +122 -0
  566. package/package.json +177 -3
@@ -0,0 +1,1492 @@
1
+ import { Emitter } from '../core/events.js';
2
+ import { bindingMatchesDevice, glyphForBinding, guessPadLayout, } from './glyphs.js';
3
+ import { createPointerDispatcher, POINTER_PRIORITY, } from './pointer-dispatch.js';
4
+ const EMPTY_BINDINGS = [];
5
+ /**
6
+ * Shape a stick (x, y) through a deadzone into `out` and return it (ws/input-5). Pure and
7
+ * allocation-free; `Input.stick` reads four actions through it and `Look` reads the pad sticks
8
+ * and the touch stick through it. See `StickOptions` for the shapes.
9
+ */
10
+ export function shapeStick(x, y, out, opts) {
11
+ const dz = opts?.deadzone ?? 0.15;
12
+ const shape = opts?.shape ?? 'scaledRadial';
13
+ const curve = opts?.curve ?? 1;
14
+ const clamp = opts?.clamp ?? true;
15
+ if (shape === 'axial') {
16
+ let ax = Math.abs(x) <= dz ? 0 : x;
17
+ let ay = Math.abs(y) <= dz ? 0 : y;
18
+ if (curve !== 1) {
19
+ ax = Math.sign(ax) * Math.abs(ax) ** curve;
20
+ ay = Math.sign(ay) * Math.abs(ay) ** curve;
21
+ }
22
+ const m = Math.hypot(ax, ay);
23
+ if (clamp && m > 1) {
24
+ ax /= m;
25
+ ay /= m;
26
+ }
27
+ out[0] = ax;
28
+ out[1] = ay;
29
+ return out;
30
+ }
31
+ const m = Math.hypot(x, y);
32
+ if (m <= dz || m === 0) {
33
+ out[0] = 0;
34
+ out[1] = 0;
35
+ return out;
36
+ }
37
+ const mag = clamp ? Math.min(1, m) : m;
38
+ let len;
39
+ if (shape === 'radial')
40
+ len = mag;
41
+ else
42
+ len = dz >= 1 ? 0 : (mag - dz) / (1 - dz);
43
+ if (curve !== 1)
44
+ len **= curve;
45
+ const k = len / m;
46
+ out[0] = x * k;
47
+ out[1] = y * k;
48
+ return out;
49
+ }
50
+ /** Default `holdMs` for `trigger: 'hold'`. */
51
+ export const DEFAULT_HOLD_MS = 400;
52
+ /** Default `doubleTapMs` for `trigger: 'doubleTap'`. */
53
+ export const DEFAULT_DOUBLE_TAP_MS = 300;
54
+ const DEFAULT_AXIS_THRESHOLD = 0.5;
55
+ const DEFAULT_BUTTON_THRESHOLD = 0.5;
56
+ /** Most repeat taps queued per key/button. */
57
+ const MAX_QUEUED_TAPS = 8;
58
+ const XR_BUTTONS = ['trigger', 'squeeze', 'pinch'];
59
+ function newTriggerState() {
60
+ return { wasDown: false, downTicks: 0, lastDownTick: -1, latched: false };
61
+ }
62
+ const NON_TEXT_INPUTS = new Set([
63
+ 'button',
64
+ 'submit',
65
+ 'reset',
66
+ 'checkbox',
67
+ 'radio',
68
+ 'image',
69
+ 'range',
70
+ 'color',
71
+ 'file',
72
+ ]);
73
+ /** True for elements where keys type text (text inputs, textareas, selects, contenteditable). */
74
+ export function isTextEntryTarget(target) {
75
+ const el = target;
76
+ if (!el || typeof el.tagName !== 'string')
77
+ return false;
78
+ if (el.isContentEditable)
79
+ return true;
80
+ switch (el.tagName) {
81
+ case 'TEXTAREA':
82
+ case 'SELECT':
83
+ return true;
84
+ case 'INPUT':
85
+ return !NON_TEXT_INPUTS.has((el.type ?? '').toLowerCase());
86
+ }
87
+ const role = el.getAttribute?.('role');
88
+ return role === 'textbox' || role === 'searchbox' || role === 'combobox';
89
+ }
90
+ const BUTTON_INPUTS = new Set(['button', 'submit', 'reset', 'image']);
91
+ /**
92
+ * True for plain activatable controls (buttons, links, `summary`, button-type inputs,
93
+ * `role=button|link|menuitem`) whose own keys are only Enter and Space.
94
+ */
95
+ export function isButtonTarget(target) {
96
+ const el = target;
97
+ if (!el || typeof el.tagName !== 'string')
98
+ return false;
99
+ switch (el.tagName) {
100
+ case 'BUTTON':
101
+ case 'A':
102
+ case 'SUMMARY':
103
+ return true;
104
+ case 'INPUT':
105
+ return BUTTON_INPUTS.has((el.type ?? '').toLowerCase());
106
+ }
107
+ const role = el.getAttribute?.('role');
108
+ return role === 'button' || role === 'link' || role === 'menuitem';
109
+ }
110
+ function defaultGamepads() {
111
+ try {
112
+ return typeof navigator !== 'undefined' && navigator.getGamepads ? navigator.getGamepads() : [];
113
+ }
114
+ catch {
115
+ // Permissions policy can forbid the Gamepad API inside iframes.
116
+ return [];
117
+ }
118
+ }
119
+ /** True for elements that own their keyboard input (text fields, buttons, links, …). */
120
+ export function isInteractiveTarget(target) {
121
+ const el = target;
122
+ if (!el || typeof el.tagName !== 'string')
123
+ return false;
124
+ if (el.isContentEditable)
125
+ return true;
126
+ switch (el.tagName) {
127
+ case 'INPUT':
128
+ case 'TEXTAREA':
129
+ case 'SELECT':
130
+ case 'BUTTON':
131
+ case 'A':
132
+ case 'SUMMARY':
133
+ return true;
134
+ }
135
+ const role = el.getAttribute?.('role');
136
+ return role === 'button' || role === 'textbox' || role === 'slider' || role === 'menuitem';
137
+ }
138
+ /**
139
+ * Action-mapped input. Feed it raw events (via `attach` or the low-level methods), call
140
+ * `update()` once per simulation tick, then query actions.
141
+ *
142
+ * ```ts
143
+ * const input = new Input({ confirm: [{ key: 'Enter' }, { key: 'Space' }, { padButton: 0 }],
144
+ * cancel: [{ key: 'Escape' }, { padButton: 1 }] });
145
+ * input.attach({ pointerTarget: canvas });
146
+ * input.setPointerTransform(platform.clientToLogical);
147
+ * // each tick:
148
+ * input.update();
149
+ * if (input.pressed('confirm')) …
150
+ * ```
151
+ *
152
+ * `A` is the full action union. The base map may leave some out (ws/input-12): an action only a
153
+ * pushed context binds (`new Input<'jump' | 'back'>({ jump }, …)`, then
154
+ * `pushContext('menu', { bindings: { back } })`) is typed everywhere and reads inactive until a
155
+ * context binds it. `bindings()` lists the actions bound so far.
156
+ */
157
+ export class Input {
158
+ pointer = {
159
+ x: 0,
160
+ y: 0,
161
+ dx: 0,
162
+ dy: 0,
163
+ inside: false,
164
+ down: false,
165
+ wheelX: 0,
166
+ wheelY: 0,
167
+ type: 'mouse',
168
+ };
169
+ blocked;
170
+ /** Pad button naming for `glyphFor` (`InputOptions.padLayout`); `null` guesses from the pad's `id`. */
171
+ padLayout;
172
+ /** Ms per tick (`InputOptions.tickMs`). */
173
+ tickMs;
174
+ /** The recorder capturing this input's feed (`createInputRecorder`), until `stop()`. */
175
+ recorder = null;
176
+ /** The playback feeding this input (`createInputPlayback`), until `detach()`. */
177
+ playback = null;
178
+ /** Feed hooks in install order (`onFeed`). Read-only for callers. */
179
+ feedHooks = [];
180
+ /** The base bindings (below every context). */
181
+ map;
182
+ /** The bindings in force: `map` with the context stack resolved over it (`map` when empty). */
183
+ active;
184
+ /** Pushed contexts, bottom first. */
185
+ stack = [];
186
+ /** Actions a context added that the base map lacks (`pushContext`); bound to nothing below it. */
187
+ extraActions = false;
188
+ /** `stack`'s names, bottom first (the `contexts` getter). */
189
+ contextNames = [];
190
+ /** The latest press per action (`buffered`, `consume`). */
191
+ presses = new Map();
192
+ /** True while `replayPointer` dispatches: the feed consumer then leaves the raw state alone. */
193
+ injecting = false;
194
+ states = new Map();
195
+ triggers = new Map();
196
+ events = new Emitter();
197
+ deviceValue = 'keyboard';
198
+ /** Ticks since construction (the trigger clock). */
199
+ tick = 0;
200
+ listenState = null;
201
+ /** Last client-space pointer position fed through `pointerAt` (the orbit drag's coordinates). */
202
+ clientX = 0;
203
+ clientY = 0;
204
+ claimFn = (_id, device) => this.linkedLook?.claimsPointer(device) ?? false;
205
+ getGamepads;
206
+ transform = null;
207
+ // Raw state.
208
+ keysDown = new Set();
209
+ keysTapped = new Set();
210
+ buttonsDown = new Set();
211
+ buttonsTapped = new Set();
212
+ /** Extra taps queued per key/button (see `InputOptions.queueTaps`). */
213
+ keyRepeats = new Map();
214
+ buttonRepeats = new Map();
215
+ queueTaps;
216
+ globalKeys = new Set();
217
+ buttonKeys = new Set();
218
+ wheelAccX = 0;
219
+ wheelAccY = 0;
220
+ prevX = 0;
221
+ prevY = 0;
222
+ boundKeys = new Set();
223
+ boundMouse = new Set();
224
+ boundWheel = false;
225
+ pads = [];
226
+ /** `this.pads`, bound once, for a linked look (`Look.padsFromInput`). */
227
+ readTickPads = () => this.pads;
228
+ /** Keys and mouse buttons that were down or tapped on the last tick. */
229
+ prevKeys = new Set();
230
+ prevButtons = new Set();
231
+ detachFn = null;
232
+ linkedGestures = null;
233
+ linkedPointers = null;
234
+ linkedLook = null;
235
+ attached = null;
236
+ dispatcherValue = null;
237
+ updatedFns = [];
238
+ actionSources = [];
239
+ actionReader = {
240
+ bound: (action) => this.states.has(action),
241
+ pressed: (action) => this.states.get(action)?.pressed ?? false,
242
+ };
243
+ constructor(bindings, opts = {}) {
244
+ this.map = bindings;
245
+ this.active = bindings;
246
+ this.getGamepads = opts.getGamepads ?? defaultGamepads;
247
+ this.blocked = opts.blocked ?? (() => false);
248
+ this.queueTaps = opts.queueTaps ?? true;
249
+ this.tickMs = opts.tickMs ?? 1000 / 60;
250
+ this.padLayout = opts.padLayout ?? null;
251
+ if (opts.globalKeys)
252
+ this.setGlobalKeys(opts.globalKeys);
253
+ if (opts.buttonKeys)
254
+ this.setButtonKeys(opts.buttonKeys);
255
+ this.setBindings(bindings);
256
+ if (opts.gestures)
257
+ this.setGestures(opts.gestures);
258
+ if (opts.pointers)
259
+ this.setPointers(opts.pointers);
260
+ if (opts.look)
261
+ this.setLook(opts.look);
262
+ }
263
+ /**
264
+ * Device that produced the most recent input (updated on real events: keys, pointer events,
265
+ * pad activity, XR button presses). Useful for showing the right button prompts; `on('device')`
266
+ * fires when it changes.
267
+ */
268
+ get lastDevice() {
269
+ return this.deviceValue;
270
+ }
271
+ set lastDevice(device) {
272
+ if (device === this.deviceValue)
273
+ return;
274
+ this.deviceValue = device;
275
+ this.events.emit('device', device);
276
+ }
277
+ /** Subscribe to an `InputEvents` event; returns the unsubscriber. */
278
+ on(type, fn) {
279
+ return this.events.on(type, fn);
280
+ }
281
+ /**
282
+ * Link gestures (or unlink with `null`). Linked gestures share this input's `blocked()` and
283
+ * pointer transform, attach to the same pointer target, and advance in `update()` before the
284
+ * actions are read, so `{ gesture }` bindings work. Don't call `gestures.update()` yourself.
285
+ */
286
+ setGestures(gestures) {
287
+ if (this.linkedGestures === gestures)
288
+ return;
289
+ if (this.linkedGestures && this.attached)
290
+ this.linkedGestures.detach();
291
+ this.linkedGestures = gestures;
292
+ if (!gestures)
293
+ return;
294
+ gestures.blocked = () => this.blocked();
295
+ gestures.setPointerTransform(this.transform);
296
+ this.linkedPointers?.setGestures(gestures);
297
+ this.wireClaim();
298
+ if (this.attached) {
299
+ gestures.attach({
300
+ pointerTarget: this.attached.pointerTarget,
301
+ disableTouchScroll: this.attached.disableTouchScroll,
302
+ });
303
+ }
304
+ }
305
+ /** The linked gestures (`setGestures`), or `null`. */
306
+ get gestures() {
307
+ return this.linkedGestures;
308
+ }
309
+ /**
310
+ * Link a pointer router (or unlink with `null`). A linked router shares this input's
311
+ * `blocked()`, pointer transform (client → camera screen units) and gestures (the magnify aim),
312
+ * attaches to the same pointer target, and is polled in `update()` before the actions are
313
+ * read, so `{ pointer }` and `{ xr }` bindings work; after them its `select` and `secondary`
314
+ * events follow the actions named by `selectAction` / `secondaryAction` when bound. Don't call
315
+ * `router.update()` yourself.
316
+ */
317
+ setPointers(router) {
318
+ if (this.linkedPointers === router)
319
+ return;
320
+ if (this.linkedPointers && this.attached)
321
+ this.linkedPointers.detach();
322
+ this.linkedPointers = router;
323
+ if (!router)
324
+ return;
325
+ router.blocked = () => this.blocked();
326
+ router.setPointerTransform(this.transform);
327
+ router.setGestures(this.linkedGestures);
328
+ if (this.attached)
329
+ router.attach(this.attached.pointerTarget, this.dispatcherValue ?? undefined);
330
+ }
331
+ /** The linked pointer router (`setPointers`), or `null`. */
332
+ get pointers() {
333
+ return this.linkedPointers;
334
+ }
335
+ /**
336
+ * Link look controls (or unlink with `null`). Linked, they share this input's `blocked()`, read
337
+ * their move keys from its raw key state, attach to the same pointer target and advance in
338
+ * `update()`, so `look.look` and `look.move` are this tick's. Don't call `look.update()` yourself.
339
+ */
340
+ setLook(look) {
341
+ if (this.linkedLook === look)
342
+ return;
343
+ if (this.linkedLook) {
344
+ if (this.attached)
345
+ this.linkedLook.detach();
346
+ this.linkedLook.keyHeld = null;
347
+ this.linkedLook.orbitFromInput = false;
348
+ this.linkedLook.pointerTransform = null;
349
+ this.linkedLook.padsFromInput = null;
350
+ }
351
+ this.linkedLook = look;
352
+ this.wireClaim();
353
+ if (!look)
354
+ return;
355
+ look.blocked = () => this.blocked();
356
+ look.keyHeld = (code) => this.keysDown.has(code);
357
+ // Orbit drags come through this input's own pointer feed, so recordings capture them.
358
+ look.orbitFromInput = true;
359
+ look.pointerTransform = this.transform;
360
+ // Its pad stick reads this tick's pads, recorded and replayed with the rest (ws/input-9).
361
+ look.padsFromInput = this.readTickPads;
362
+ if (this.attached)
363
+ look.attach(this.attached.pointerTarget, this.dispatcherValue ?? undefined);
364
+ }
365
+ /**
366
+ * The pointer dispatcher `attach` built (one DOM listener set shared by the feed, gestures,
367
+ * look, router and touch zones), or `null` while detached.
368
+ */
369
+ get dispatcher() {
370
+ return this.dispatcherValue;
371
+ }
372
+ /**
373
+ * Run `fn` at the end of every `update()`, after gestures, look, router and triggers have
374
+ * advanced (same tick, registration order): the place to poll a world panel or read
375
+ * `pressed()` for the frame. Returns the remover.
376
+ */
377
+ onUpdated(fn) {
378
+ this.updatedFns.push(fn);
379
+ return () => {
380
+ const i = this.updatedFns.indexOf(fn);
381
+ if (i >= 0)
382
+ this.updatedFns.splice(i, 1);
383
+ };
384
+ }
385
+ /** Add an `ActionSource` (touch zones). Returns the remover. */
386
+ addActionSource(src) {
387
+ this.actionSources.push(src);
388
+ return () => {
389
+ const i = this.actionSources.indexOf(src);
390
+ if (i >= 0)
391
+ this.actionSources.splice(i, 1);
392
+ };
393
+ }
394
+ /** `value(positive) - value(negative)`, -1..1: a move axis from two actions. */
395
+ axis(negative, positive) {
396
+ return this.value(positive) - this.value(negative);
397
+ }
398
+ /**
399
+ * Gives the linked gestures the look's pointer claim (an orbit drag is never a pan) when the
400
+ * gestures have no claim of their own; takes it back when the look unlinks.
401
+ */
402
+ wireClaim() {
403
+ const g = this.linkedGestures;
404
+ if (!g)
405
+ return;
406
+ if (this.linkedLook) {
407
+ if (!g.claim || g.claim === this.claimFn)
408
+ g.claim = this.claimFn;
409
+ }
410
+ else if (g.claim === this.claimFn)
411
+ g.claim = null;
412
+ }
413
+ /** The linked look controls (`setLook`), or `null`. */
414
+ get look() {
415
+ return this.linkedLook;
416
+ }
417
+ /**
418
+ * Replace the base bindings (below every pushed context). Action states and the input buffer
419
+ * reset (use `rebind` to change one action and keep the rest). The map may leave out actions
420
+ * of `A` (ws/input-12): a pushed context binds those.
421
+ */
422
+ setBindings(bindings) {
423
+ this.map = bindings;
424
+ this.states.clear();
425
+ this.triggers.clear();
426
+ this.presses.clear();
427
+ this.keyRepeats.clear();
428
+ this.buttonRepeats.clear();
429
+ for (const action of Object.keys(bindings)) {
430
+ this.states.set(action, {
431
+ pressed: false,
432
+ held: false,
433
+ released: false,
434
+ value: 0,
435
+ heldTicks: 0,
436
+ holdProgress: 0,
437
+ });
438
+ this.presses.set(action, { tick: -1, used: false });
439
+ }
440
+ this.extraActions = false;
441
+ for (const c of this.stack)
442
+ this.addContextActions(c.bindings);
443
+ this.active = this.resolveContexts();
444
+ for (const action of this.states.keys()) {
445
+ this.triggers.set(action, this.active[action].map(newTriggerState));
446
+ }
447
+ this.rebuildBound();
448
+ }
449
+ /**
450
+ * Replace one action's bindings, keeping every other action's state (ws/input-3). Rebinding
451
+ * while an input is held is safe: the old action sees `released` next tick, and a new binding
452
+ * whose input is already down reads inactive until it lifts and goes down again.
453
+ *
454
+ * With contexts pushed (ws/input-5) it writes into the top context (the action becomes one that
455
+ * context binds); `context` names another pushed context instead, `null` the base map.
456
+ */
457
+ rebind(action, list, context) {
458
+ if (!this.states.has(action))
459
+ throw new Error(`unknown action "${action}"`);
460
+ const entry = this.contextEntry(context);
461
+ if (entry)
462
+ entry.bindings = { ...entry.bindings, [action]: list };
463
+ else
464
+ this.map = { ...this.map, [action]: list };
465
+ // The action's trigger state is always rebuilt (latched), as before contexts.
466
+ this.applyActive(action);
467
+ }
468
+ /**
469
+ * The bindings in force (what `serializeBindings` saves): the base map with the pushed
470
+ * contexts resolved over it. `context` reads one context's own bindings over the ones below
471
+ * it as if it were on top (`null`: the base map).
472
+ */
473
+ bindings(context) {
474
+ if (context === undefined)
475
+ return this.active;
476
+ if (context === null)
477
+ return this.map;
478
+ const i = this.contextIndex(context);
479
+ if (i < 0)
480
+ throw new Error(`no context "${context}"`);
481
+ return this.resolveContexts(i);
482
+ }
483
+ // ---------------------------------------------------------------- contexts
484
+ /**
485
+ * Push an action context (ws/input-5): a menu, gameplay, a vehicle, each with its own bindings.
486
+ * The top context's bindings win; `pass` says what happens to the actions it does not bind
487
+ * (default `'unbound'`: they fall through). Pushing a name already on the stack moves it to the
488
+ * top with the new options. Every action whose bindings in force change resets its hold
489
+ * progress, tap counts and buffered press, and a binding whose input is already down reads
490
+ * inactive until it lifts, so a held key never fires across the change. Returns the remover
491
+ * (it removes this push only).
492
+ */
493
+ pushContext(name, opts = {}) {
494
+ const bindings = (opts.bindings ?? {});
495
+ this.addContextActions(bindings);
496
+ const old = this.contextIndex(name);
497
+ if (old >= 0)
498
+ this.stack.splice(old, 1);
499
+ const entry = { name, bindings, pass: opts.pass ?? 'unbound' };
500
+ this.stack.push(entry);
501
+ this.contextNames = this.stack.map((c) => c.name);
502
+ const hooks = this.feedHooks;
503
+ for (let i = 0; i < hooks.length; i++)
504
+ hooks[i].context?.(name, true);
505
+ this.applyActive(null);
506
+ this.events.emit('context', name);
507
+ return () => {
508
+ const i = this.stack.indexOf(entry);
509
+ if (i >= 0)
510
+ this.removeContextAt(i);
511
+ };
512
+ }
513
+ /** Remove the topmost context named `name`. Returns false when none is pushed. */
514
+ popContext(name) {
515
+ const i = this.contextIndex(name);
516
+ if (i < 0)
517
+ return false;
518
+ this.removeContextAt(i);
519
+ return true;
520
+ }
521
+ /** The top context's name, `null` with none pushed. */
522
+ get context() {
523
+ return this.stack[this.stack.length - 1]?.name ?? null;
524
+ }
525
+ /** The pushed context names, bottom first. One array, rebuilt when the stack changes. */
526
+ get contexts() {
527
+ return this.contextNames;
528
+ }
529
+ /** Shorthand for `on('context', fn)`: the top context changed. Returns the remover. */
530
+ onContextChange(fn) {
531
+ return this.events.on('context', fn);
532
+ }
533
+ removeContextAt(i) {
534
+ const before = this.context;
535
+ const [entry] = this.stack.splice(i, 1);
536
+ this.contextNames = this.stack.map((c) => c.name);
537
+ const hooks = this.feedHooks;
538
+ for (let h = 0; h < hooks.length; h++)
539
+ hooks[h].context?.(entry.name, false);
540
+ this.applyActive(null);
541
+ const now = this.context;
542
+ if (now !== before)
543
+ this.events.emit('context', now);
544
+ }
545
+ contextIndex(name) {
546
+ for (let i = this.stack.length - 1; i >= 0; i--) {
547
+ if (this.stack[i].name === name)
548
+ return i;
549
+ }
550
+ return -1;
551
+ }
552
+ /** The context `rebind` writes to: `undefined` the top one (or none), `null` none, else by name. */
553
+ contextEntry(context) {
554
+ if (context === null)
555
+ return null;
556
+ if (context === undefined)
557
+ return this.stack[this.stack.length - 1] ?? null;
558
+ const i = this.contextIndex(context);
559
+ if (i < 0)
560
+ throw new Error(`no context "${context}"`);
561
+ return this.stack[i];
562
+ }
563
+ /** Give a context's actions the base map lacks a state (bound to nothing below the context). */
564
+ addContextActions(bindings) {
565
+ for (const action of Object.keys(bindings)) {
566
+ if (this.states.has(action))
567
+ continue;
568
+ this.states.set(action, {
569
+ pressed: false,
570
+ held: false,
571
+ released: false,
572
+ value: 0,
573
+ heldTicks: 0,
574
+ holdProgress: 0,
575
+ });
576
+ this.presses.set(action, { tick: -1, used: false });
577
+ this.triggers.set(action, []);
578
+ this.extraActions = true;
579
+ }
580
+ }
581
+ /** The bindings in force with contexts `0..top` on the stack (all of them by default). */
582
+ resolveContexts(top = this.stack.length - 1) {
583
+ if (top < 0 && !this.extraActions)
584
+ return this.map;
585
+ const out = {};
586
+ for (const action of this.states.keys())
587
+ out[action] = this.resolveAction(action, top);
588
+ return out;
589
+ }
590
+ resolveAction(action, i) {
591
+ if (i < 0)
592
+ return this.map[action] ?? EMPTY_BINDINGS;
593
+ const c = this.stack[i];
594
+ const own = c.bindings[action];
595
+ if (own)
596
+ return c.pass === 'all' ? [...own, ...this.resolveAction(action, i - 1)] : own;
597
+ return c.pass === 'none' ? EMPTY_BINDINGS : this.resolveAction(action, i - 1);
598
+ }
599
+ /**
600
+ * Recompute the bindings in force; every action whose list changed (element by element), and
601
+ * `force` too, gets fresh trigger state, latched where its input is down, and loses its
602
+ * buffered press.
603
+ */
604
+ applyActive(force) {
605
+ const prev = this.active;
606
+ const next = this.resolveContexts();
607
+ this.active = next;
608
+ for (const action of this.states.keys()) {
609
+ const list = next[action];
610
+ if (action !== force && sameList(prev[action], list))
611
+ continue;
612
+ const ts = list.map(newTriggerState);
613
+ for (let i = 0; i < list.length; i++) {
614
+ if (this.rawValue(list[i]) > 0)
615
+ ts[i].latched = true;
616
+ }
617
+ this.triggers.set(action, ts);
618
+ if (action !== force) {
619
+ const b = this.presses.get(action);
620
+ if (b)
621
+ b.tick = -1;
622
+ }
623
+ }
624
+ this.rebuildBound();
625
+ }
626
+ // ---------------------------------------------------------------- buffer
627
+ /**
628
+ * True if `action` went `pressed` within the last `windowMs` (counted in ticks:
629
+ * `ceil(windowMs / tickMs)`, this tick included) and that press was not `consume`d
630
+ * (ws/input-5). A press a few frames early still lands: `if (input.buffered('jump', 100) &&
631
+ * grounded) { input.consume('jump'); jump(); }`. Deterministic under replay.
632
+ */
633
+ buffered(action, windowMs) {
634
+ const b = this.presses.get(action);
635
+ if (!b || b.tick < 0 || b.used)
636
+ return false;
637
+ return this.tick - b.tick <= Math.ceil(windowMs / this.tickMs);
638
+ }
639
+ /**
640
+ * Mark `action`'s latest press used: `buffered` no longer returns it and `pressed` reads false
641
+ * for the rest of this tick. Returns whether there was an unused press. Feed hooks see it, so a
642
+ * recording carries it.
643
+ */
644
+ consume(action) {
645
+ const b = this.presses.get(action);
646
+ if (!b)
647
+ return false;
648
+ const hooks = this.feedHooks;
649
+ for (let i = 0; i < hooks.length; i++)
650
+ hooks[i].consume?.(action);
651
+ const st = this.states.get(action);
652
+ st.pressed = false;
653
+ if (b.tick < 0 || b.used)
654
+ return false;
655
+ b.used = true;
656
+ return true;
657
+ }
658
+ // ---------------------------------------------------------------- sticks
659
+ /**
660
+ * Read four actions as a stick into `out` (`[x, y]`, the caller's; returned) through
661
+ * `shapeStick` (ws/input-5): x is `value(posX) - value(negX)`, y `value(posY) - value(negY)`.
662
+ * Keys, pad axes (bind them with `threshold: 0` for an analog read) and touch-zone sticks all
663
+ * go through it; diagonal keys read length 1. Allocation-free.
664
+ */
665
+ stick(negX, posX, negY, posY, out, opts) {
666
+ const x = this.value(posX) - this.value(negX);
667
+ const y = this.value(posY) - this.value(negY);
668
+ return shapeStick(x, y, out, opts);
669
+ }
670
+ /**
671
+ * The aim angle of a stick read (`Math.atan2(y, x)`, radians), `null` when it is at rest
672
+ * (inside the deadzone). Pass the array `stick` filled.
673
+ */
674
+ stickAngle(out) {
675
+ if (out[0] === 0 && out[1] === 0)
676
+ return null;
677
+ return Math.atan2(out[1], out[0]);
678
+ }
679
+ rebuildBound() {
680
+ this.boundKeys = new Set();
681
+ this.boundMouse = new Set();
682
+ this.boundWheel = false;
683
+ for (const action of this.states.keys()) {
684
+ for (const b of this.active[action])
685
+ this.noteBound(b);
686
+ }
687
+ }
688
+ noteBound(b) {
689
+ if ('key' in b)
690
+ this.boundKeys.add(b.key);
691
+ else if ('mouse' in b)
692
+ this.boundMouse.add(b.mouse);
693
+ else if ('wheel' in b)
694
+ this.boundWheel = true;
695
+ else if ('chord' in b)
696
+ for (const m of b.chord)
697
+ this.noteBound(m);
698
+ }
699
+ /**
700
+ * Resolve with the next input the player presses (ws/input-3): a key as `{ key }`, a mouse
701
+ * or pen button as `{ mouse }`, a pad button that was up when the listen began as
702
+ * `{ padButton }`, an XR button as `{ xr }`. Escape, a `timeoutMs` elapsing or a newer
703
+ * `listen` resolve `null`. The captured input is swallowed (no action sees it). Keys typed
704
+ * into text fields never reach `Input`, so they are not captured either. The caller rebinds:
705
+ * `const b = await input.listen('jump'); if (b) input.rebind('jump', [b]);`.
706
+ */
707
+ listen(action, opts = {}) {
708
+ this.listenState?.resolve(null);
709
+ return new Promise((resolve) => {
710
+ const baseline = new Set();
711
+ this.pads.forEach((p, pi) => {
712
+ if (!p || p.connected === false)
713
+ return;
714
+ p.buttons.forEach((b, bi) => {
715
+ if (b.pressed)
716
+ baseline.add(pi * 64 + bi);
717
+ });
718
+ });
719
+ const ticks = opts.timeoutMs === undefined ? Infinity : Math.ceil(opts.timeoutMs / this.tickMs);
720
+ this.listenState = { action, resolve, deadline: this.tick + ticks, baseline };
721
+ });
722
+ }
723
+ /** The action a `listen` is waiting for, or `null`. */
724
+ get listening() {
725
+ return this.listenState?.action ?? null;
726
+ }
727
+ settleListen(b) {
728
+ const l = this.listenState;
729
+ if (!l)
730
+ return;
731
+ this.listenState = null;
732
+ l.resolve(b);
733
+ }
734
+ /** Replace the global keys (see `InputOptions.globalKeys`). */
735
+ setGlobalKeys(codes) {
736
+ this.globalKeys = new Set(codes);
737
+ }
738
+ /** Replace the button keys (see `InputOptions.buttonKeys`). */
739
+ setButtonKeys(codes) {
740
+ this.buttonKeys = new Set(codes);
741
+ }
742
+ /**
743
+ * The prompt glyph for `action` on `device` (default `lastDevice`): the first binding that
744
+ * device produces, else the action's first binding; `null` for an unbound action. Pad labels
745
+ * follow `padLayout`, guessed from the first connected pad's `id` when unset. Reads the
746
+ * bindings in force, so the top context's (ws/input-5).
747
+ */
748
+ glyphFor(action, device = this.deviceValue) {
749
+ const list = this.active[action];
750
+ if (!list || list.length === 0)
751
+ return null;
752
+ const b = list.find((x) => bindingMatchesDevice(x, device)) ?? list[0];
753
+ return glyphForBinding(b, this.padLayout ?? guessPadLayout(this.firstPad()?.id));
754
+ }
755
+ firstPad(index) {
756
+ if (index !== undefined) {
757
+ const p = this.pads[index];
758
+ return p && p.connected !== false ? p : null;
759
+ }
760
+ for (const p of this.pads)
761
+ if (p && p.connected !== false)
762
+ return p;
763
+ return null;
764
+ }
765
+ /** True when `pad` (or any connected pad) has a vibration actuator `rumble` can drive. */
766
+ rumbleSupported(pad) {
767
+ return this.actuatorPad(pad) !== null;
768
+ }
769
+ actuatorPad(index) {
770
+ if (index !== undefined) {
771
+ const p = this.firstPad(index);
772
+ return p?.vibrationActuator ? p : null;
773
+ }
774
+ for (const p of this.pads)
775
+ if (p && p.connected !== false && p.vibrationActuator)
776
+ return p;
777
+ return null;
778
+ }
779
+ /**
780
+ * Play a dual-rumble effect on `pad` (default: the first connected pad that can). Returns
781
+ * true when an actuator took it; a no-op (false) otherwise, so games call it freely. Feed hooks
782
+ * see every call, so a recording carries the rumbles for playback to assert.
783
+ */
784
+ rumble(opts = {}) {
785
+ const strong = clamp01(opts.strong ?? 1);
786
+ const weak = clamp01(opts.weak ?? 1);
787
+ const ms = Math.max(0, opts.ms ?? 100);
788
+ const hooks = this.feedHooks;
789
+ for (let i = 0; i < hooks.length; i++)
790
+ hooks[i].rumble?.(strong, weak, ms, opts.pad);
791
+ this.events.emit('rumble', opts);
792
+ const p = this.actuatorPad(opts.pad);
793
+ if (!p?.vibrationActuator)
794
+ return false;
795
+ try {
796
+ const r = p.vibrationActuator.playEffect('dual-rumble', {
797
+ duration: ms,
798
+ startDelay: 0,
799
+ strongMagnitude: strong,
800
+ weakMagnitude: weak,
801
+ });
802
+ r?.catch?.(() => { });
803
+ return true;
804
+ }
805
+ catch {
806
+ return false;
807
+ }
808
+ }
809
+ /**
810
+ * Install a feed hook: it sees every `keyDown`, `keyUp`, `buttonDown`, `buttonUp`, `wheel`,
811
+ * `pointerAt` and `releaseAll` call (DOM or direct) and the pads read in `update()`, and may
812
+ * substitute those pads. Returns the uninstaller. Used by `createInputRecorder` and
813
+ * `createInputPlayback`; hooks never change how bindings are evaluated.
814
+ */
815
+ onFeed(hook) {
816
+ this.feedHooks.push(hook);
817
+ return () => {
818
+ const i = this.feedHooks.indexOf(hook);
819
+ if (i >= 0)
820
+ this.feedHooks.splice(i, 1);
821
+ };
822
+ }
823
+ /** Install the client → game-space mapping (platform owns letterboxing). `null` = identity relative to the target. */
824
+ setPointerTransform(fn) {
825
+ this.transform = fn;
826
+ this.linkedGestures?.setPointerTransform(fn);
827
+ this.linkedPointers?.setPointerTransform(fn);
828
+ this.dispatcherValue?.setTransform(fn);
829
+ if (this.linkedLook)
830
+ this.linkedLook.pointerTransform = fn;
831
+ }
832
+ // ---------------------------------------------------------------- queries
833
+ pressed(action) {
834
+ return this.states.get(action)?.pressed ?? false;
835
+ }
836
+ held(action) {
837
+ return this.states.get(action)?.held ?? false;
838
+ }
839
+ released(action) {
840
+ return this.states.get(action)?.released ?? false;
841
+ }
842
+ value(action) {
843
+ return this.states.get(action)?.value ?? 0;
844
+ }
845
+ state(action) {
846
+ const st = this.states.get(action);
847
+ if (!st)
848
+ throw new Error(`unknown action "${action}"`);
849
+ return st;
850
+ }
851
+ /** True if any bound action was pressed this tick. */
852
+ anyPressed() {
853
+ for (const st of this.states.values())
854
+ if (st.pressed)
855
+ return true;
856
+ return false;
857
+ }
858
+ /** Raw key state by `KeyboardEvent.code`, ignoring bindings and blocking. */
859
+ keyHeld(code) {
860
+ return this.keysDown.has(code);
861
+ }
862
+ // ---------------------------------------------------------------- tick
863
+ /** Advance one tick: poll gamepads, compute edges, clear per-tick accumulators. */
864
+ update() {
865
+ // The pads are read first, so a linked look turns from the same snapshot the action map
866
+ // reads and a recorder writes down (ws/input-9), and a playback's replace both.
867
+ let pads = this.getGamepads();
868
+ const hooks = this.feedHooks;
869
+ for (let i = 0; i < hooks.length; i++) {
870
+ const src = hooks[i].padSource;
871
+ if (src)
872
+ pads = src(pads);
873
+ }
874
+ this.pads = pads;
875
+ this.linkedGestures?.update();
876
+ this.linkedLook?.update();
877
+ this.linkedPointers?.poll();
878
+ const blocked = this.blocked();
879
+ for (let i = 0; i < hooks.length; i++)
880
+ pads = hooks[i].gamepads(pads);
881
+ this.pads = pads;
882
+ this.tick++;
883
+ let padActive = false;
884
+ if (this.listenState)
885
+ this.pollListen();
886
+ const sources = this.actionSources;
887
+ for (let i = 0; i < sources.length; i++)
888
+ sources[i].update?.();
889
+ for (const [action, st] of this.states) {
890
+ const v = this.evaluate(action, this.active[action], this.triggers.get(action));
891
+ if (v.pad)
892
+ padActive = true;
893
+ const active = v.value > 0;
894
+ if (blocked) {
895
+ // Report nothing. A key held through the block must not fire `pressed` afterwards,
896
+ // so remember it as held internally via heldTicks > 0.
897
+ st.pressed = false;
898
+ st.released = false;
899
+ st.value = 0;
900
+ st.held = false;
901
+ st.holdProgress = 0;
902
+ st.heldTicks = active ? Math.max(1, st.heldTicks) : 0;
903
+ continue;
904
+ }
905
+ const was = st.heldTicks > 0;
906
+ st.pressed = active && (!was || v.fresh);
907
+ st.released = !active && was && st.held;
908
+ st.held = active;
909
+ st.value = v.value;
910
+ st.holdProgress = v.hold;
911
+ st.heldTicks = active ? st.heldTicks + 1 : 0;
912
+ if (st.pressed) {
913
+ const b = this.presses.get(action);
914
+ b.tick = this.tick;
915
+ b.used = false;
916
+ }
917
+ }
918
+ if (padActive)
919
+ this.lastDevice = 'gamepad';
920
+ const r = this.linkedPointers;
921
+ if (r && (r.xrFired('trigger') || r.xrFired('squeeze') || r.xrFired('pinch'))) {
922
+ this.lastDevice = 'xr';
923
+ }
924
+ if (blocked) {
925
+ this.keyRepeats.clear();
926
+ this.buttonRepeats.clear();
927
+ }
928
+ else {
929
+ consumeRepeats(this.keyRepeats, this.keysTapped);
930
+ consumeRepeats(this.buttonRepeats, this.buttonsTapped);
931
+ }
932
+ rememberActive(this.prevKeys, this.keysDown, this.keysTapped);
933
+ rememberActive(this.prevButtons, this.buttonsDown, this.buttonsTapped);
934
+ this.keysTapped.clear();
935
+ this.buttonsTapped.clear();
936
+ this.pointer.wheelX = this.wheelAccX;
937
+ this.pointer.wheelY = this.wheelAccY;
938
+ this.wheelAccX = 0;
939
+ this.wheelAccY = 0;
940
+ this.pointer.dx = this.pointer.x - this.prevX;
941
+ this.pointer.dy = this.pointer.y - this.prevY;
942
+ this.prevX = this.pointer.x;
943
+ this.prevY = this.pointer.y;
944
+ this.linkedPointers?.resolve(this.actionReader);
945
+ const fns = this.updatedFns;
946
+ for (let i = 0; i < fns.length; i++)
947
+ fns[i]();
948
+ }
949
+ /** A pending `listen`: pad buttons newly down, XR presses, the timeout. Keys and mouse come via the feed. */
950
+ pollListen() {
951
+ const l = this.listenState;
952
+ if (this.tick > l.deadline) {
953
+ this.settleListen(null);
954
+ return;
955
+ }
956
+ for (let pi = 0; pi < this.pads.length; pi++) {
957
+ const p = this.pads[pi];
958
+ if (!p || p.connected === false)
959
+ continue;
960
+ for (let bi = 0; bi < p.buttons.length; bi++) {
961
+ if (p.buttons[bi].pressed && !l.baseline.has(pi * 64 + bi)) {
962
+ this.lastDevice = 'gamepad';
963
+ this.settleListen({ padButton: bi });
964
+ return;
965
+ }
966
+ }
967
+ }
968
+ const r = this.linkedPointers;
969
+ if (r) {
970
+ for (const xr of XR_BUTTONS) {
971
+ if (r.xrFired(xr)) {
972
+ this.lastDevice = 'xr';
973
+ this.settleListen({ xr });
974
+ return;
975
+ }
976
+ }
977
+ }
978
+ }
979
+ evalOut = { value: 0, pad: false, fresh: false, hold: 0 };
980
+ /**
981
+ * Fold a list of bindings into one value: the largest raw value after each binding's trigger
982
+ * (`press` passes it through, `hold` gates it on `holdMs` of ticks down, `doubleTap` passes a
983
+ * second down edge within `doubleTapMs`). `fresh` marks a new press while already active,
984
+ * `hold` the furthest hold progress, `pad` that a pad produced the value.
985
+ */
986
+ evaluate(action, list, ts) {
987
+ const out = this.evalOut;
988
+ out.value = 0;
989
+ out.pad = false;
990
+ out.fresh = false;
991
+ out.hold = 0;
992
+ const sources = this.actionSources;
993
+ for (let i = 0; i < sources.length; i++) {
994
+ const src = sources[i];
995
+ const v = src.value(action);
996
+ if (v > out.value)
997
+ out.value = v;
998
+ if (v > 0 && src.fresh?.(action))
999
+ out.fresh = true;
1000
+ }
1001
+ for (let i = 0; i < list.length; i++) {
1002
+ const b = list[i];
1003
+ const t = ts[i];
1004
+ const freshBefore = out.fresh;
1005
+ out.fresh = false;
1006
+ let v = this.rawValue(b);
1007
+ const rawFresh = out.fresh;
1008
+ out.fresh = freshBefore;
1009
+ const down = v > 0;
1010
+ const edge = down && (!t.wasDown || rawFresh);
1011
+ t.wasDown = down;
1012
+ if (t.latched) {
1013
+ // Installed by `rebind` while its input was down: silent until a fresh press.
1014
+ if (!down)
1015
+ t.latched = false;
1016
+ t.downTicks = 0;
1017
+ continue;
1018
+ }
1019
+ if (b.trigger === undefined || b.trigger === 'press') {
1020
+ if (rawFresh)
1021
+ out.fresh = true;
1022
+ }
1023
+ else if (b.trigger === 'hold') {
1024
+ const need = Math.max(1, Math.ceil((b.holdMs ?? DEFAULT_HOLD_MS) / this.tickMs));
1025
+ t.downTicks = down ? (edge ? 1 : t.downTicks + 1) : 0;
1026
+ const progress = Math.min(1, t.downTicks / need);
1027
+ if (progress > out.hold)
1028
+ out.hold = progress;
1029
+ if (t.downTicks < need)
1030
+ v = 0;
1031
+ else if (t.downTicks === need)
1032
+ out.fresh = true;
1033
+ }
1034
+ else if (b.trigger === 'doubleTap') {
1035
+ const window = Math.max(1, Math.ceil((b.doubleTapMs ?? DEFAULT_DOUBLE_TAP_MS) / this.tickMs));
1036
+ if (edge && t.lastDownTick >= 0 && this.tick - t.lastDownTick <= window) {
1037
+ t.lastDownTick = -1;
1038
+ out.fresh = true;
1039
+ }
1040
+ else {
1041
+ if (edge)
1042
+ t.lastDownTick = this.tick;
1043
+ v = 0;
1044
+ }
1045
+ }
1046
+ if (v > out.value)
1047
+ out.value = v;
1048
+ }
1049
+ return out;
1050
+ }
1051
+ /** One binding's raw value before its trigger; sets `evalOut.fresh`/`pad` as a side effect. */
1052
+ rawValue(b) {
1053
+ const out = this.evalOut;
1054
+ let v = 0;
1055
+ if ('key' in b) {
1056
+ const tapped = this.keysTapped.has(b.key);
1057
+ // A queued repeat counts once the tick holding its first tap has passed.
1058
+ const queued = !tapped && (this.keyRepeats.get(b.key) ?? 0) > 0;
1059
+ // Released and pressed again since last tick: a new press, not a hold.
1060
+ if (queued || (tapped && this.prevKeys.has(b.key)))
1061
+ out.fresh = true;
1062
+ v = this.keysDown.has(b.key) || tapped || queued ? 1 : 0;
1063
+ }
1064
+ else if ('mouse' in b) {
1065
+ const tapped = this.buttonsTapped.has(b.mouse);
1066
+ const queued = !tapped && (this.buttonRepeats.get(b.mouse) ?? 0) > 0;
1067
+ if (queued || (tapped && this.prevButtons.has(b.mouse)))
1068
+ out.fresh = true;
1069
+ v = this.buttonsDown.has(b.mouse) || tapped || queued ? 1 : 0;
1070
+ }
1071
+ else if ('wheel' in b) {
1072
+ v = Math.sign(this.wheelAccY) === b.wheel ? 1 : 0;
1073
+ }
1074
+ else if ('padButton' in b) {
1075
+ v = this.padValue(b.pad, (p) => {
1076
+ const btn = p.buttons[b.padButton];
1077
+ if (!btn)
1078
+ return 0;
1079
+ const t = b.threshold ?? DEFAULT_BUTTON_THRESHOLD;
1080
+ return btn.pressed || btn.value >= t ? Math.max(btn.value, btn.pressed ? 1 : 0) : 0;
1081
+ });
1082
+ if (v > 0)
1083
+ out.pad = true;
1084
+ }
1085
+ else if ('pointer' in b) {
1086
+ const r = this.linkedPointers;
1087
+ // A press shorter than a tick still holds for one tick (the sources keep it).
1088
+ if (r?.held(b.pointer)) {
1089
+ v = 1;
1090
+ if (r.fired(b.pointer))
1091
+ out.fresh = true;
1092
+ }
1093
+ }
1094
+ else if ('xr' in b) {
1095
+ const r = this.linkedPointers;
1096
+ if (r?.xrHeld(b.xr, b.hand)) {
1097
+ v = 1;
1098
+ if (r.xrFired(b.xr, b.hand))
1099
+ out.fresh = true;
1100
+ }
1101
+ }
1102
+ else if ('gesture' in b) {
1103
+ const g = this.linkedGestures;
1104
+ if (g?.active(b.gesture)) {
1105
+ v = 1;
1106
+ // A tap on consecutive ticks is a new press each time.
1107
+ if (g.fired(b.gesture))
1108
+ out.fresh = true;
1109
+ }
1110
+ }
1111
+ else if ('chord' in b) {
1112
+ // Every member down, in any order. Members' own triggers are ignored.
1113
+ v = b.chord.length > 0 ? 1 : 0;
1114
+ const fresh = out.fresh;
1115
+ for (const m of b.chord) {
1116
+ if (this.rawValue(m) <= 0) {
1117
+ v = 0;
1118
+ break;
1119
+ }
1120
+ }
1121
+ out.fresh = fresh;
1122
+ }
1123
+ else {
1124
+ v = this.padValue(b.pad, (p) => {
1125
+ const raw = (p.axes[b.padAxis] ?? 0) * b.dir;
1126
+ const t = b.threshold ?? DEFAULT_AXIS_THRESHOLD;
1127
+ return raw >= t ? Math.min(1, raw) : 0;
1128
+ });
1129
+ if (v > 0)
1130
+ out.pad = true;
1131
+ }
1132
+ return v;
1133
+ }
1134
+ padValue(index, read) {
1135
+ if (index !== undefined) {
1136
+ const p = this.pads[index];
1137
+ return p && p.connected !== false ? read(p) : 0;
1138
+ }
1139
+ let best = 0;
1140
+ for (const p of this.pads) {
1141
+ if (!p || p.connected === false)
1142
+ continue;
1143
+ const v = read(p);
1144
+ if (v > best)
1145
+ best = v;
1146
+ }
1147
+ return best;
1148
+ }
1149
+ // ---------------------------------------------------------------- low-level feed
1150
+ // Public so tests, replays and automated playtests can drive input without a DOM.
1151
+ keyDown(code) {
1152
+ const hooks = this.feedHooks;
1153
+ for (let i = 0; i < hooks.length; i++)
1154
+ hooks[i].keyDown(code);
1155
+ this.lastDevice = 'keyboard';
1156
+ if (this.listenState) {
1157
+ this.settleListen(code === 'Escape' ? null : { key: code });
1158
+ return;
1159
+ }
1160
+ if (!this.keysDown.has(code)) {
1161
+ if (this.queueTaps && (this.keysTapped.has(code) || this.keyRepeats.has(code))) {
1162
+ queueRepeat(this.keyRepeats, code);
1163
+ }
1164
+ else {
1165
+ this.keysTapped.add(code);
1166
+ }
1167
+ }
1168
+ this.keysDown.add(code);
1169
+ }
1170
+ keyUp(code) {
1171
+ const hooks = this.feedHooks;
1172
+ for (let i = 0; i < hooks.length; i++)
1173
+ hooks[i].keyUp(code);
1174
+ this.keysDown.delete(code);
1175
+ }
1176
+ buttonDown(button) {
1177
+ const hooks = this.feedHooks;
1178
+ for (let i = 0; i < hooks.length; i++)
1179
+ hooks[i].buttonDown(button);
1180
+ if (this.listenState) {
1181
+ this.settleListen({ mouse: button });
1182
+ return;
1183
+ }
1184
+ if (button === 0 && this.pointer.type !== 'touch') {
1185
+ this.linkedLook?.orbitDown(this.clientX, this.clientY);
1186
+ }
1187
+ if (!this.buttonsDown.has(button)) {
1188
+ if (this.queueTaps && (this.buttonsTapped.has(button) || this.buttonRepeats.has(button))) {
1189
+ queueRepeat(this.buttonRepeats, button);
1190
+ }
1191
+ else {
1192
+ this.buttonsTapped.add(button);
1193
+ }
1194
+ }
1195
+ this.buttonsDown.add(button);
1196
+ if (button === 0)
1197
+ this.pointer.down = true;
1198
+ }
1199
+ buttonUp(button) {
1200
+ const hooks = this.feedHooks;
1201
+ for (let i = 0; i < hooks.length; i++)
1202
+ hooks[i].buttonUp(button);
1203
+ this.buttonsDown.delete(button);
1204
+ if (button === 0) {
1205
+ this.pointer.down = false;
1206
+ this.linkedLook?.orbitUp();
1207
+ }
1208
+ }
1209
+ wheel(dx, dy) {
1210
+ const hooks = this.feedHooks;
1211
+ for (let i = 0; i < hooks.length; i++)
1212
+ hooks[i].wheel(dx, dy);
1213
+ this.wheelAccX += dx;
1214
+ this.wheelAccY += dy;
1215
+ }
1216
+ /** Set the pointer position from client coordinates (runs the transform). */
1217
+ pointerAt(clientX, clientY, type = 'mouse') {
1218
+ const hooks = this.feedHooks;
1219
+ for (let i = 0; i < hooks.length; i++)
1220
+ hooks[i].pointerAt(clientX, clientY, type);
1221
+ const p = this.transform ? this.transform(clientX, clientY) : { x: clientX, y: clientY };
1222
+ this.pointer.type = type;
1223
+ this.lastDevice = type;
1224
+ this.clientX = clientX;
1225
+ this.clientY = clientY;
1226
+ if (type !== 'touch')
1227
+ this.linkedLook?.orbitMove(clientX, clientY);
1228
+ if (p) {
1229
+ this.pointer.x = p.x;
1230
+ this.pointer.y = p.y;
1231
+ this.pointer.inside = true;
1232
+ }
1233
+ else {
1234
+ this.pointer.inside = false;
1235
+ }
1236
+ }
1237
+ /** Release every key and button (focus loss, tab hidden). */
1238
+ releaseAll() {
1239
+ const hooks = this.feedHooks;
1240
+ for (let i = 0; i < hooks.length; i++)
1241
+ hooks[i].releaseAll();
1242
+ this.keysDown.clear();
1243
+ this.buttonsDown.clear();
1244
+ this.keyRepeats.clear();
1245
+ this.buttonRepeats.clear();
1246
+ this.pointer.down = false;
1247
+ this.linkedLook?.orbitUp();
1248
+ }
1249
+ /**
1250
+ * An overlay control fired (ws/input-12): a button the player clicked, tapped or activated
1251
+ * with Enter or the pad, named by its stable id (`controlId` in ui). Feed hooks see it, so a
1252
+ * recording carries it in its `ui` lane on the tick it happened; nothing else changes. The
1253
+ * control's own handler has already run. `createGame` calls it for every overlay control.
1254
+ */
1255
+ control(id) {
1256
+ const hooks = this.feedHooks;
1257
+ for (let i = 0; i < hooks.length; i++)
1258
+ hooks[i].control?.(id);
1259
+ }
1260
+ /**
1261
+ * Replay an overlay control's activation (ws/input-12): emits `'control'` with `id`, whose
1262
+ * listener (`createGame`: `Overlay.activate`) fires the control. `createInputPlayback` calls it
1263
+ * for the recording's `ui` lane, before that tick's `update()`, so the handler runs on the
1264
+ * tick it ran live. Feed hooks do not see it.
1265
+ */
1266
+ replayControl(id) {
1267
+ this.events.emit('control', id);
1268
+ }
1269
+ // ---------------------------------------------------------------- DOM
1270
+ /**
1271
+ * Listen to the DOM. Keyboard: only bound keys are `preventDefault`ed, and never when a
1272
+ * modifier (Ctrl/Meta/Alt) is held, the target is a text field/button/link, input is
1273
+ * blocked, or the document lacks focus. That keeps browser shortcuts working and avoids
1274
+ * scrolling hijacks inside the itch.io iframe.
1275
+ */
1276
+ attach(opts) {
1277
+ this.detach();
1278
+ const target = opts.pointerTarget;
1279
+ const win = target.ownerDocument.defaultView ?? window;
1280
+ const doc = target.ownerDocument;
1281
+ const keyTarget = opts.keyTarget ?? win;
1282
+ const prevTouchAction = target.style.touchAction;
1283
+ if (opts.disableTouchScroll !== false)
1284
+ target.style.touchAction = 'none';
1285
+ const onKeyDown = (ev) => {
1286
+ const e = ev;
1287
+ if (isInteractiveTarget(e.target)) {
1288
+ // Global keys still count on buttons/links (never in text entry); the control keeps
1289
+ // the default action.
1290
+ const passes = (this.globalKeys.has(e.code) && !isTextEntryTarget(e.target)) ||
1291
+ (this.buttonKeys.has(e.code) && isButtonTarget(e.target));
1292
+ if (!e.repeat && passes)
1293
+ this.keyDown(e.code);
1294
+ return;
1295
+ }
1296
+ if (!e.repeat)
1297
+ this.keyDown(e.code);
1298
+ if (this.boundKeys.has(e.code) &&
1299
+ !e.ctrlKey &&
1300
+ !e.metaKey &&
1301
+ !e.altKey &&
1302
+ !this.blocked() &&
1303
+ doc.hasFocus()) {
1304
+ e.preventDefault();
1305
+ }
1306
+ };
1307
+ const onKeyUp = (ev) => {
1308
+ this.keyUp(ev.code);
1309
+ };
1310
+ const dispatcher = createPointerDispatcher(target, {
1311
+ transform: this.transform,
1312
+ hoverWhileClaimed: opts.hoverWhileClaimed,
1313
+ bounds: opts.bounds,
1314
+ });
1315
+ this.dispatcherValue = dispatcher;
1316
+ // The feed sees every pointer, claimed or not, so recordings stay complete and identical.
1317
+ // Hooks see every record (the recording's `ptr` lane); while `replayPointer` dispatches, the
1318
+ // raw state is left alone (the playback feeds it from the `p` and `b` lanes).
1319
+ const hooks = this.feedHooks;
1320
+ const notePointer = (phase, p) => {
1321
+ for (let i = 0; i < hooks.length; i++)
1322
+ hooks[i].pointer?.(phase, p);
1323
+ return this.injecting;
1324
+ };
1325
+ const consumer = {
1326
+ pointerDown: (p) => {
1327
+ if (notePointer('down', p))
1328
+ return;
1329
+ if (!p.primary && p.device === 'touch')
1330
+ return;
1331
+ this.pointerAt(p.x, p.y, p.device);
1332
+ this.buttonDown(p.button);
1333
+ // Clicking the canvas should give the iframe keyboard focus.
1334
+ if (!doc.hasFocus())
1335
+ win.focus();
1336
+ },
1337
+ pointerMove: (p) => {
1338
+ if (notePointer('move', p))
1339
+ return;
1340
+ if (!p.primary)
1341
+ return;
1342
+ this.pointerAt(p.x, p.y, p.device);
1343
+ },
1344
+ pointerUp: (p) => {
1345
+ if (notePointer('up', p))
1346
+ return;
1347
+ if (!p.primary && p.device === 'touch')
1348
+ return;
1349
+ this.buttonUp(p.button);
1350
+ },
1351
+ pointerCancel: (p) => {
1352
+ if (notePointer('cancel', p))
1353
+ return;
1354
+ if (p.primary)
1355
+ this.buttonUp(0);
1356
+ },
1357
+ pointerLeave: (p) => {
1358
+ if (notePointer('leave', p))
1359
+ return;
1360
+ if (p.device === 'mouse')
1361
+ this.pointer.inside = false;
1362
+ },
1363
+ cancelAll: () => {
1364
+ for (let i = 0; i < hooks.length; i++)
1365
+ hooks[i].cancelPointers?.();
1366
+ if (!this.injecting)
1367
+ this.releaseAll();
1368
+ },
1369
+ };
1370
+ const removeConsumer = dispatcher.add(consumer, POINTER_PRIORITY.feed, true);
1371
+ const onWheel = (e) => {
1372
+ // deltaMode 1 = lines, 2 = pages; normalise roughly to pixels.
1373
+ const k = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? 400 : 1;
1374
+ this.wheel(e.deltaX * k, e.deltaY * k);
1375
+ if (this.boundWheel && !this.blocked())
1376
+ e.preventDefault();
1377
+ };
1378
+ const onContextMenu = (e) => {
1379
+ if (this.boundMouse.has(2) && !this.blocked())
1380
+ e.preventDefault();
1381
+ };
1382
+ keyTarget.addEventListener('keydown', onKeyDown);
1383
+ keyTarget.addEventListener('keyup', onKeyUp);
1384
+ target.addEventListener('wheel', onWheel, { passive: false });
1385
+ target.addEventListener('contextmenu', onContextMenu);
1386
+ this.attached = opts;
1387
+ this.linkedGestures?.attach({
1388
+ pointerTarget: target,
1389
+ disableTouchScroll: opts.disableTouchScroll,
1390
+ dispatcher,
1391
+ });
1392
+ this.linkedPointers?.attach(target, dispatcher);
1393
+ this.linkedLook?.attach(target, dispatcher);
1394
+ this.events.emit('attach', dispatcher);
1395
+ this.detachFn = () => {
1396
+ this.events.emit('detach', dispatcher);
1397
+ this.linkedGestures?.detach();
1398
+ this.linkedPointers?.detach();
1399
+ this.linkedLook?.detach();
1400
+ this.attached = null;
1401
+ keyTarget.removeEventListener('keydown', onKeyDown);
1402
+ keyTarget.removeEventListener('keyup', onKeyUp);
1403
+ target.removeEventListener('wheel', onWheel);
1404
+ target.removeEventListener('contextmenu', onContextMenu);
1405
+ removeConsumer();
1406
+ dispatcher.destroy();
1407
+ this.dispatcherValue = null;
1408
+ target.style.touchAction = prevTouchAction;
1409
+ };
1410
+ }
1411
+ /**
1412
+ * Dispatch a recorded pointer event on the attached pointer target (ws/input-5), so every
1413
+ * dispatcher consumer (gestures, look, touch zones, the router) sees it as it saw the original;
1414
+ * the feed's own key/button/pointer state is left to the playback's `b` and `p` lanes.
1415
+ * `createInputPlayback` calls it for the recording's `ptr` lane. `age` (ws/input-7) is how
1416
+ * many ms ago the event happened on the consumers' clocks (`PointerRecord.age`; negative is
1417
+ * ahead); omitted, it happened now. Returns false (and does nothing) while detached or
1418
+ * without `PointerEvent`.
1419
+ */
1420
+ replayPointer(phase, id, device, clientX, clientY, button, primary, age = 0) {
1421
+ const d = this.dispatcherValue;
1422
+ if (!d || typeof PointerEvent !== 'function')
1423
+ return false;
1424
+ const type = phase === 'down'
1425
+ ? 'pointerdown'
1426
+ : phase === 'move'
1427
+ ? 'pointermove'
1428
+ : phase === 'up'
1429
+ ? 'pointerup'
1430
+ : phase === 'cancel'
1431
+ ? 'pointercancel'
1432
+ : 'pointerleave';
1433
+ const e = new PointerEvent(type, {
1434
+ pointerId: id,
1435
+ pointerType: device,
1436
+ isPrimary: primary,
1437
+ button,
1438
+ clientX,
1439
+ clientY,
1440
+ });
1441
+ this.injecting = true;
1442
+ d.replayAge = age;
1443
+ try {
1444
+ d.target.dispatchEvent(e);
1445
+ }
1446
+ finally {
1447
+ this.injecting = false;
1448
+ d.replayAge = null;
1449
+ }
1450
+ return true;
1451
+ }
1452
+ detach() {
1453
+ this.detachFn?.();
1454
+ this.detachFn = null;
1455
+ this.releaseAll();
1456
+ }
1457
+ }
1458
+ /** Same length and the same binding objects in order. */
1459
+ function sameList(a, b) {
1460
+ if (a === b)
1461
+ return true;
1462
+ if (!a || a.length !== b.length)
1463
+ return false;
1464
+ for (let i = 0; i < a.length; i++)
1465
+ if (a[i] !== b[i])
1466
+ return false;
1467
+ return true;
1468
+ }
1469
+ function clamp01(v) {
1470
+ return Math.min(1, Math.max(0, v));
1471
+ }
1472
+ function queueRepeat(repeats, k) {
1473
+ repeats.set(k, Math.min(MAX_QUEUED_TAPS, (repeats.get(k) ?? 0) + 1));
1474
+ }
1475
+ /** One queued repeat per key is used up each tick, except on the tick of its first tap. */
1476
+ function rememberActive(prev, down, tapped) {
1477
+ prev.clear();
1478
+ for (const k of down)
1479
+ prev.add(k);
1480
+ for (const k of tapped)
1481
+ prev.add(k);
1482
+ }
1483
+ function consumeRepeats(repeats, tappedNow) {
1484
+ for (const [k, n] of repeats) {
1485
+ if (tappedNow.has(k))
1486
+ continue;
1487
+ if (n <= 1)
1488
+ repeats.delete(k);
1489
+ else
1490
+ repeats.set(k, n - 1);
1491
+ }
1492
+ }