@vgai/engine 0.2.0 → 0.3.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 (541) hide show
  1. package/README.md +3 -1
  2. package/dist/adapter/authoring.d.ts +404 -0
  3. package/dist/adapter/authoring.d.ts.map +1 -0
  4. package/dist/adapter/authoring.js +22 -0
  5. package/dist/adapter/colyseus-networking-adapter.d.ts +43 -0
  6. package/dist/adapter/colyseus-networking-adapter.d.ts.map +1 -0
  7. package/dist/adapter/colyseus-networking-adapter.js +38 -0
  8. package/dist/adapter/first-party-systems.d.ts +38 -0
  9. package/dist/adapter/first-party-systems.d.ts.map +1 -0
  10. package/dist/adapter/first-party-systems.js +77 -0
  11. package/dist/adapter/game-adapter.d.ts +133 -0
  12. package/dist/adapter/game-adapter.d.ts.map +1 -0
  13. package/dist/adapter/game-adapter.js +10 -0
  14. package/dist/adapter/host-context.d.ts +73 -0
  15. package/dist/adapter/host-context.d.ts.map +1 -0
  16. package/dist/adapter/host-context.js +16 -0
  17. package/dist/adapter/index.d.ts +22 -0
  18. package/dist/adapter/index.d.ts.map +1 -0
  19. package/dist/adapter/index.js +15 -0
  20. package/dist/adapter/ingest/game-contract.d.ts +53 -0
  21. package/dist/adapter/ingest/game-contract.d.ts.map +1 -0
  22. package/dist/adapter/ingest/game-contract.js +30 -0
  23. package/dist/adapter/ingest/overlay-applier.d.ts +118 -0
  24. package/dist/adapter/ingest/overlay-applier.d.ts.map +1 -0
  25. package/dist/adapter/ingest/overlay-applier.js +132 -0
  26. package/dist/adapter/ingest/overlay-apply.d.ts +60 -0
  27. package/dist/adapter/ingest/overlay-apply.d.ts.map +1 -0
  28. package/dist/adapter/ingest/overlay-apply.js +101 -0
  29. package/dist/adapter/ingest/overlay-file.d.ts +79 -0
  30. package/dist/adapter/ingest/overlay-file.d.ts.map +1 -0
  31. package/dist/adapter/ingest/overlay-file.js +66 -0
  32. package/dist/adapter/ingest/overlay-report.d.ts +113 -0
  33. package/dist/adapter/ingest/overlay-report.d.ts.map +1 -0
  34. package/dist/adapter/ingest/overlay-report.js +103 -0
  35. package/dist/adapter/ingest/scene-capture.d.ts +97 -0
  36. package/dist/adapter/ingest/scene-capture.d.ts.map +1 -0
  37. package/dist/adapter/ingest/scene-capture.js +248 -0
  38. package/dist/adapter/ingest/upstream-pin.d.ts +47 -0
  39. package/dist/adapter/ingest/upstream-pin.d.ts.map +1 -0
  40. package/dist/adapter/ingest/upstream-pin.js +50 -0
  41. package/dist/adapter/loop-gate-report.d.ts +34 -0
  42. package/dist/adapter/loop-gate-report.d.ts.map +1 -0
  43. package/dist/adapter/loop-gate-report.js +37 -0
  44. package/dist/adapter/rapier-physics-adapter.d.ts +12 -0
  45. package/dist/adapter/rapier-physics-adapter.d.ts.map +1 -0
  46. package/dist/adapter/rapier-physics-adapter.js +46 -0
  47. package/dist/adapter/system-adapter.d.ts +133 -0
  48. package/dist/adapter/system-adapter.d.ts.map +1 -0
  49. package/dist/adapter/system-adapter.js +13 -0
  50. package/dist/adapter/transform.d.ts +18 -0
  51. package/dist/adapter/transform.d.ts.map +1 -0
  52. package/dist/adapter/transform.js +1 -0
  53. package/dist/adapter/vgai-scene-game-adapter.d.ts +116 -0
  54. package/dist/adapter/vgai-scene-game-adapter.d.ts.map +1 -0
  55. package/dist/adapter/vgai-scene-game-adapter.js +714 -0
  56. package/dist/adapter/world-kind.d.ts +30 -0
  57. package/dist/adapter/world-kind.d.ts.map +1 -0
  58. package/dist/adapter/world-kind.js +18 -0
  59. package/dist/ai/navigation.d.ts +64 -0
  60. package/dist/ai/navigation.d.ts.map +1 -0
  61. package/dist/ai/navigation.js +133 -0
  62. package/dist/animation/anim-graph-types.d.ts +25 -0
  63. package/dist/animation/anim-graph-types.d.ts.map +1 -0
  64. package/dist/animation/anim-graph-types.js +1 -0
  65. package/dist/animation/animation-clock.d.ts +213 -0
  66. package/dist/animation/animation-clock.d.ts.map +1 -0
  67. package/dist/animation/animation-clock.js +301 -0
  68. package/dist/animation/blend-node.d.ts +22 -0
  69. package/dist/animation/blend-node.d.ts.map +1 -0
  70. package/dist/animation/blend-node.js +97 -0
  71. package/dist/animation/camera-ownership.d.ts +250 -0
  72. package/dist/animation/camera-ownership.d.ts.map +1 -0
  73. package/dist/animation/camera-ownership.js +169 -0
  74. package/dist/animation/cinematic-cues.d.ts +200 -0
  75. package/dist/animation/cinematic-cues.d.ts.map +1 -0
  76. package/dist/animation/cinematic-cues.js +213 -0
  77. package/dist/animation/clip-map.d.ts +12 -0
  78. package/dist/animation/clip-map.d.ts.map +1 -0
  79. package/dist/animation/clip-map.js +37 -0
  80. package/dist/animation/gsap-registration.d.ts +119 -0
  81. package/dist/animation/gsap-registration.d.ts.map +1 -0
  82. package/dist/animation/gsap-registration.js +92 -0
  83. package/dist/animation/theatre-clock-binding.d.ts +89 -0
  84. package/dist/animation/theatre-clock-binding.d.ts.map +1 -0
  85. package/dist/animation/theatre-clock-binding.js +29 -0
  86. package/dist/animation/theatre-director.d.ts +242 -0
  87. package/dist/animation/theatre-director.d.ts.map +1 -0
  88. package/dist/animation/theatre-director.js +235 -0
  89. package/dist/animation/theatre-object-binding.d.ts +305 -0
  90. package/dist/animation/theatre-object-binding.d.ts.map +1 -0
  91. package/dist/animation/theatre-object-binding.js +351 -0
  92. package/dist/animation/xstate-animation-binding.d.ts +111 -0
  93. package/dist/animation/xstate-animation-binding.d.ts.map +1 -0
  94. package/dist/animation/xstate-animation-binding.js +307 -0
  95. package/dist/animation/xstate-animation-meta.d.ts +228 -0
  96. package/dist/animation/xstate-animation-meta.d.ts.map +1 -0
  97. package/dist/animation/xstate-animation-meta.js +221 -0
  98. package/dist/assets.d.ts +26 -0
  99. package/dist/assets.d.ts.map +1 -0
  100. package/dist/assets.js +55 -0
  101. package/dist/audio/index.d.ts +4 -0
  102. package/dist/audio/index.d.ts.map +1 -0
  103. package/dist/audio/index.js +24 -0
  104. package/dist/audio/tone-clock-binding.d.ts +75 -0
  105. package/dist/audio/tone-clock-binding.d.ts.map +1 -0
  106. package/dist/audio/tone-clock-binding.js +29 -0
  107. package/dist/audio/tone-context.d.ts +114 -0
  108. package/dist/audio/tone-context.d.ts.map +1 -0
  109. package/dist/audio/tone-context.js +39 -0
  110. package/dist/audio/tone-offline-render.d.ts +90 -0
  111. package/dist/audio/tone-offline-render.d.ts.map +1 -0
  112. package/dist/audio/tone-offline-render.js +56 -0
  113. package/dist/audio/wav-encode.d.ts +38 -0
  114. package/dist/audio/wav-encode.d.ts.map +1 -0
  115. package/dist/audio/wav-encode.js +97 -0
  116. package/dist/core/game-loop.d.ts +21 -0
  117. package/dist/core/game-loop.d.ts.map +1 -0
  118. package/dist/core/game-loop.js +130 -0
  119. package/dist/core/system-runner.d.ts +120 -0
  120. package/dist/core/system-runner.d.ts.map +1 -0
  121. package/dist/core/system-runner.js +286 -0
  122. package/dist/core/types.d.ts +58 -0
  123. package/dist/core/types.d.ts.map +1 -0
  124. package/dist/core/types.js +25 -0
  125. package/dist/data/data-asset.d.ts +101 -0
  126. package/dist/data/data-asset.d.ts.map +1 -0
  127. package/dist/data/data-asset.js +122 -0
  128. package/dist/data/data-check-core.d.ts +110 -0
  129. package/dist/data/data-check-core.d.ts.map +1 -0
  130. package/dist/data/data-check-core.js +178 -0
  131. package/dist/data/data-ref.d.ts +79 -0
  132. package/dist/data/data-ref.d.ts.map +1 -0
  133. package/dist/data/data-ref.js +130 -0
  134. package/dist/data/vite-plugin-data.d.ts +79 -0
  135. package/dist/data/vite-plugin-data.d.ts.map +1 -0
  136. package/dist/data/vite-plugin-data.js +217 -0
  137. package/dist/dev/console-bridge.d.ts +22 -0
  138. package/dist/dev/console-bridge.d.ts.map +1 -0
  139. package/dist/dev/console-bridge.js +71 -0
  140. package/dist/dev/debug-draw.d.ts +24 -0
  141. package/dist/dev/debug-draw.d.ts.map +1 -0
  142. package/dist/dev/debug-draw.js +73 -0
  143. package/dist/dev/logger.d.ts +30 -0
  144. package/dist/dev/logger.d.ts.map +1 -0
  145. package/dist/dev/logger.js +90 -0
  146. package/dist/ecs/component-manager.d.ts +189 -0
  147. package/dist/ecs/component-manager.d.ts.map +1 -0
  148. package/dist/ecs/component-manager.js +676 -0
  149. package/dist/ecs/game-component.d.ts +97 -0
  150. package/dist/ecs/game-component.d.ts.map +1 -0
  151. package/dist/ecs/game-component.js +80 -0
  152. package/dist/ecs/hmr-swap-report.d.ts +53 -0
  153. package/dist/ecs/hmr-swap-report.d.ts.map +1 -0
  154. package/dist/ecs/hmr-swap-report.js +55 -0
  155. package/dist/index.d.ts +37 -0
  156. package/dist/index.d.ts.map +1 -0
  157. package/dist/index.js +30 -0
  158. package/dist/input/input-manager.d.ts +596 -0
  159. package/dist/input/input-manager.d.ts.map +1 -0
  160. package/dist/input/input-manager.js +1564 -0
  161. package/dist/input/input-types.d.ts +171 -0
  162. package/dist/input/input-types.d.ts.map +1 -0
  163. package/dist/input/input-types.js +1 -0
  164. package/dist/input/prompt-labels.d.ts +21 -0
  165. package/dist/input/prompt-labels.d.ts.map +1 -0
  166. package/dist/input/prompt-labels.js +121 -0
  167. package/dist/input/rebind-controller.d.ts +59 -0
  168. package/dist/input/rebind-controller.d.ts.map +1 -0
  169. package/dist/input/rebind-controller.js +85 -0
  170. package/dist/input/schema.d.ts +16 -0
  171. package/dist/input/schema.d.ts.map +1 -0
  172. package/dist/input/schema.js +228 -0
  173. package/dist/loader.d.ts +43 -0
  174. package/dist/loader.d.ts.map +1 -0
  175. package/dist/loader.js +59 -0
  176. package/dist/manifest/index.d.ts +5 -0
  177. package/dist/manifest/index.d.ts.map +1 -0
  178. package/dist/manifest/index.js +8 -0
  179. package/dist/manifest/load-file.d.ts +4 -0
  180. package/dist/manifest/load-file.d.ts.map +1 -0
  181. package/dist/manifest/load-file.js +14 -0
  182. package/dist/manifest/load.d.ts +117 -0
  183. package/dist/manifest/load.d.ts.map +1 -0
  184. package/dist/manifest/load.js +255 -0
  185. package/dist/manifest/schema.d.ts +173 -0
  186. package/dist/manifest/schema.d.ts.map +1 -0
  187. package/dist/manifest/schema.js +302 -0
  188. package/dist/physics/collision-system.d.ts +41 -0
  189. package/dist/physics/collision-system.d.ts.map +1 -0
  190. package/dist/physics/collision-system.js +64 -0
  191. package/dist/physics/physics-registry.d.ts +45 -0
  192. package/dist/physics/physics-registry.d.ts.map +1 -0
  193. package/dist/physics/physics-registry.js +65 -0
  194. package/dist/physics/transform-writer.d.ts +16 -0
  195. package/dist/physics/transform-writer.d.ts.map +1 -0
  196. package/dist/physics/transform-writer.js +38 -0
  197. package/dist/physics/trigger-dispatch.d.ts +24 -0
  198. package/dist/physics/trigger-dispatch.d.ts.map +1 -0
  199. package/dist/physics/trigger-dispatch.js +80 -0
  200. package/dist/react/game-state.d.ts +105 -0
  201. package/dist/react/game-state.d.ts.map +1 -0
  202. package/dist/react/game-state.js +144 -0
  203. package/dist/react/use-data.d.ts +60 -0
  204. package/dist/react/use-data.d.ts.map +1 -0
  205. package/dist/react/use-data.js +96 -0
  206. package/dist/react/use-selection.d.ts +75 -0
  207. package/dist/react/use-selection.d.ts.map +1 -0
  208. package/dist/react/use-selection.js +90 -0
  209. package/dist/render/auto-batcher.d.ts +34 -0
  210. package/dist/render/auto-batcher.d.ts.map +1 -0
  211. package/dist/render/auto-batcher.js +140 -0
  212. package/dist/render/render-batch-system.d.ts +33 -0
  213. package/dist/render/render-batch-system.d.ts.map +1 -0
  214. package/dist/render/render-batch-system.js +230 -0
  215. package/dist/render/render-features.d.ts +47 -0
  216. package/dist/render/render-features.d.ts.map +1 -0
  217. package/dist/render/render-features.js +105 -0
  218. package/dist/render/render-settings.d.ts +36 -0
  219. package/dist/render/render-settings.d.ts.map +1 -0
  220. package/dist/render/render-settings.js +51 -0
  221. package/dist/runtime/create-runtime.d.ts +280 -0
  222. package/dist/runtime/create-runtime.d.ts.map +1 -0
  223. package/dist/runtime/create-runtime.js +795 -0
  224. package/dist/runtime/frame-selector-cache.d.ts +41 -0
  225. package/dist/runtime/frame-selector-cache.d.ts.map +1 -0
  226. package/dist/runtime/frame-selector-cache.js +65 -0
  227. package/dist/runtime/game.d.ts +433 -0
  228. package/dist/runtime/game.d.ts.map +1 -0
  229. package/dist/runtime/game.js +569 -0
  230. package/dist/runtime/input-router.d.ts +78 -0
  231. package/dist/runtime/input-router.d.ts.map +1 -0
  232. package/dist/runtime/input-router.js +171 -0
  233. package/dist/runtime/mount-game.d.ts +129 -0
  234. package/dist/runtime/mount-game.d.ts.map +1 -0
  235. package/dist/runtime/mount-game.js +152 -0
  236. package/dist/runtime/mount-manifest.d.ts +119 -0
  237. package/dist/runtime/mount-manifest.d.ts.map +1 -0
  238. package/dist/runtime/mount-manifest.js +204 -0
  239. package/dist/runtime/render-audio-control.d.ts +110 -0
  240. package/dist/runtime/render-audio-control.d.ts.map +1 -0
  241. package/dist/runtime/render-audio-control.js +111 -0
  242. package/dist/runtime/render-control.d.ts +291 -0
  243. package/dist/runtime/render-control.d.ts.map +1 -0
  244. package/dist/runtime/render-control.js +264 -0
  245. package/dist/runtime/render-seed.d.ts +57 -0
  246. package/dist/runtime/render-seed.d.ts.map +1 -0
  247. package/dist/runtime/render-seed.js +74 -0
  248. package/dist/runtime/scene-ui-bridge.d.ts +69 -0
  249. package/dist/runtime/scene-ui-bridge.d.ts.map +1 -0
  250. package/dist/runtime/scene-ui-bridge.js +23 -0
  251. package/dist/runtime/scene-ui-data.d.ts +13 -0
  252. package/dist/runtime/scene-ui-data.d.ts.map +1 -0
  253. package/dist/runtime/scene-ui-data.js +92 -0
  254. package/dist/runtime/state-bridge.d.ts +47 -0
  255. package/dist/runtime/state-bridge.d.ts.map +1 -0
  256. package/dist/runtime/state-bridge.js +53 -0
  257. package/dist/runtime/types.d.ts +179 -0
  258. package/dist/runtime/types.d.ts.map +1 -0
  259. package/dist/runtime/types.js +1 -0
  260. package/dist/scene/asset-loaders.d.ts +79 -0
  261. package/dist/scene/asset-loaders.d.ts.map +1 -0
  262. package/dist/scene/asset-loaders.js +141 -0
  263. package/dist/scene/asset-paths.d.ts +11 -0
  264. package/dist/scene/asset-paths.d.ts.map +1 -0
  265. package/dist/scene/asset-paths.js +108 -0
  266. package/dist/scene/asset-ref-check.d.ts +88 -0
  267. package/dist/scene/asset-ref-check.d.ts.map +1 -0
  268. package/dist/scene/asset-ref-check.js +214 -0
  269. package/dist/scene/asset-registry.d.ts +30 -0
  270. package/dist/scene/asset-registry.d.ts.map +1 -0
  271. package/dist/scene/asset-registry.js +50 -0
  272. package/dist/scene/collider-dimensions.d.ts +63 -0
  273. package/dist/scene/collider-dimensions.d.ts.map +1 -0
  274. package/dist/scene/collider-dimensions.js +98 -0
  275. package/dist/scene/component-registry.d.ts +17 -0
  276. package/dist/scene/component-registry.d.ts.map +1 -0
  277. package/dist/scene/component-registry.js +28 -0
  278. package/dist/scene/defaults.d.ts +252 -0
  279. package/dist/scene/defaults.d.ts.map +1 -0
  280. package/dist/scene/defaults.js +151 -0
  281. package/dist/scene/geometries/index.d.ts +8 -0
  282. package/dist/scene/geometries/index.d.ts.map +1 -0
  283. package/dist/scene/geometries/index.js +7 -0
  284. package/dist/scene/geometries/terrain.d.ts +2 -0
  285. package/dist/scene/geometries/terrain.d.ts.map +1 -0
  286. package/dist/scene/geometries/terrain.js +37 -0
  287. package/dist/scene/geometry-registry.d.ts +24 -0
  288. package/dist/scene/geometry-registry.d.ts.map +1 -0
  289. package/dist/scene/geometry-registry.js +20 -0
  290. package/dist/scene/instance-registry.d.ts +36 -0
  291. package/dist/scene/instance-registry.d.ts.map +1 -0
  292. package/dist/scene/instance-registry.js +55 -0
  293. package/dist/scene/instancers/grid.d.ts +2 -0
  294. package/dist/scene/instancers/grid.d.ts.map +1 -0
  295. package/dist/scene/instancers/grid.js +36 -0
  296. package/dist/scene/instancers/index.d.ts +8 -0
  297. package/dist/scene/instancers/index.d.ts.map +1 -0
  298. package/dist/scene/instancers/index.js +7 -0
  299. package/dist/scene/light-camera-factory.d.ts +23 -0
  300. package/dist/scene/light-camera-factory.d.ts.map +1 -0
  301. package/dist/scene/light-camera-factory.js +68 -0
  302. package/dist/scene/material-factory.d.ts +17 -0
  303. package/dist/scene/material-factory.d.ts.map +1 -0
  304. package/dist/scene/material-factory.js +197 -0
  305. package/dist/scene/material-registry.d.ts +40 -0
  306. package/dist/scene/material-registry.d.ts.map +1 -0
  307. package/dist/scene/material-registry.js +44 -0
  308. package/dist/scene/materials/index.d.ts +8 -0
  309. package/dist/scene/materials/index.d.ts.map +1 -0
  310. package/dist/scene/materials/index.js +7 -0
  311. package/dist/scene/materials/water.d.ts +2 -0
  312. package/dist/scene/materials/water.d.ts.map +1 -0
  313. package/dist/scene/materials/water.js +54 -0
  314. package/dist/scene/parse.d.ts +23 -0
  315. package/dist/scene/parse.d.ts.map +1 -0
  316. package/dist/scene/parse.js +179 -0
  317. package/dist/scene/particles-factory.d.ts +33 -0
  318. package/dist/scene/particles-factory.d.ts.map +1 -0
  319. package/dist/scene/particles-factory.js +282 -0
  320. package/dist/scene/scene-apply.d.ts +109 -0
  321. package/dist/scene/scene-apply.d.ts.map +1 -0
  322. package/dist/scene/scene-apply.js +326 -0
  323. package/dist/scene/scene-diff-schema.d.ts +285 -0
  324. package/dist/scene/scene-diff-schema.d.ts.map +1 -0
  325. package/dist/scene/scene-diff-schema.js +92 -0
  326. package/dist/scene/scene-diff-types.d.ts +40 -0
  327. package/dist/scene/scene-diff-types.d.ts.map +1 -0
  328. package/dist/scene/scene-diff-types.js +14 -0
  329. package/dist/scene/scene-loader.d.ts +177 -0
  330. package/dist/scene/scene-loader.d.ts.map +1 -0
  331. package/dist/scene/scene-loader.js +1213 -0
  332. package/dist/scene/scene-query.d.ts +11 -0
  333. package/dist/scene/scene-query.d.ts.map +1 -0
  334. package/dist/scene/scene-query.js +63 -0
  335. package/dist/scene/scene-types.d.ts +10 -0
  336. package/dist/scene/scene-types.d.ts.map +1 -0
  337. package/dist/scene/scene-types.js +8 -0
  338. package/dist/scene/scene-version.d.ts +26 -0
  339. package/dist/scene/scene-version.d.ts.map +1 -0
  340. package/dist/scene/scene-version.js +34 -0
  341. package/dist/scene/schema/animation.d.ts +8 -0
  342. package/dist/scene/schema/animation.d.ts.map +1 -0
  343. package/dist/scene/schema/animation.js +42 -0
  344. package/dist/scene/schema/audio.d.ts +13 -0
  345. package/dist/scene/schema/audio.d.ts.map +1 -0
  346. package/dist/scene/schema/audio.js +22 -0
  347. package/dist/scene/schema/camera.d.ts +28 -0
  348. package/dist/scene/schema/camera.d.ts.map +1 -0
  349. package/dist/scene/schema/camera.js +17 -0
  350. package/dist/scene/schema/collider.d.ts +40 -0
  351. package/dist/scene/schema/collider.d.ts.map +1 -0
  352. package/dist/scene/schema/collider.js +55 -0
  353. package/dist/scene/schema/entity-ref.d.ts +31 -0
  354. package/dist/scene/schema/entity-ref.d.ts.map +1 -0
  355. package/dist/scene/schema/entity-ref.js +61 -0
  356. package/dist/scene/schema/entity.d.ts +1036 -0
  357. package/dist/scene/schema/entity.d.ts.map +1 -0
  358. package/dist/scene/schema/entity.js +153 -0
  359. package/dist/scene/schema/environment.d.ts +425 -0
  360. package/dist/scene/schema/environment.d.ts.map +1 -0
  361. package/dist/scene/schema/environment.js +357 -0
  362. package/dist/scene/schema/index.d.ts +39 -0
  363. package/dist/scene/schema/index.d.ts.map +1 -0
  364. package/dist/scene/schema/index.js +20 -0
  365. package/dist/scene/schema/instances.d.ts +14 -0
  366. package/dist/scene/schema/instances.d.ts.map +1 -0
  367. package/dist/scene/schema/instances.js +28 -0
  368. package/dist/scene/schema/joint.d.ts +23 -0
  369. package/dist/scene/schema/joint.d.ts.map +1 -0
  370. package/dist/scene/schema/joint.js +23 -0
  371. package/dist/scene/schema/light.d.ts +38 -0
  372. package/dist/scene/schema/light.d.ts.map +1 -0
  373. package/dist/scene/schema/light.js +26 -0
  374. package/dist/scene/schema/material.d.ts +199 -0
  375. package/dist/scene/schema/material.d.ts.map +1 -0
  376. package/dist/scene/schema/material.js +99 -0
  377. package/dist/scene/schema/mesh.d.ts +48 -0
  378. package/dist/scene/schema/mesh.d.ts.map +1 -0
  379. package/dist/scene/schema/mesh.js +82 -0
  380. package/dist/scene/schema/particles.d.ts +2178 -0
  381. package/dist/scene/schema/particles.d.ts.map +1 -0
  382. package/dist/scene/schema/particles.js +321 -0
  383. package/dist/scene/schema/physics.d.ts +56 -0
  384. package/dist/scene/schema/physics.d.ts.map +1 -0
  385. package/dist/scene/schema/physics.js +37 -0
  386. package/dist/scene/schema/scene-file.d.ts +304 -0
  387. package/dist/scene/schema/scene-file.d.ts.map +1 -0
  388. package/dist/scene/schema/scene-file.js +262 -0
  389. package/dist/scene/schema/shadow.d.ts +28 -0
  390. package/dist/scene/schema/shadow.d.ts.map +1 -0
  391. package/dist/scene/schema/shadow.js +20 -0
  392. package/dist/scene/schema/spline.d.ts +25 -0
  393. package/dist/scene/schema/spline.d.ts.map +1 -0
  394. package/dist/scene/schema/spline.js +15 -0
  395. package/dist/scene/schema/tuples.d.ts +12 -0
  396. package/dist/scene/schema/tuples.d.ts.map +1 -0
  397. package/dist/scene/schema/tuples.js +14 -0
  398. package/dist/scene/schema/ui.d.ts +880 -0
  399. package/dist/scene/schema/ui.d.ts.map +1 -0
  400. package/dist/scene/schema/ui.js +506 -0
  401. package/dist/scene/user-data.d.ts +171 -0
  402. package/dist/scene/user-data.d.ts.map +1 -0
  403. package/dist/scene/user-data.js +137 -0
  404. package/dist/setup/setup-audio.d.ts +20 -0
  405. package/dist/setup/setup-audio.d.ts.map +1 -0
  406. package/dist/setup/setup-audio.js +41 -0
  407. package/dist/setup/setup-particles.d.ts +17 -0
  408. package/dist/setup/setup-particles.d.ts.map +1 -0
  409. package/dist/setup/setup-particles.js +16 -0
  410. package/dist/setup/setup-physics.d.ts +25 -0
  411. package/dist/setup/setup-physics.d.ts.map +1 -0
  412. package/dist/setup/setup-physics.js +51 -0
  413. package/dist/setup/setup-renderer.d.ts +69 -0
  414. package/dist/setup/setup-renderer.d.ts.map +1 -0
  415. package/dist/setup/setup-renderer.js +363 -0
  416. package/dist/tools/define-tool.d.ts +105 -0
  417. package/dist/tools/define-tool.d.ts.map +1 -0
  418. package/dist/tools/define-tool.js +82 -0
  419. package/dist/world2d/authoring-2d.d.ts +76 -0
  420. package/dist/world2d/authoring-2d.d.ts.map +1 -0
  421. package/dist/world2d/authoring-2d.js +176 -0
  422. package/dist/world2d/capture-to-scene2d.d.ts +5 -0
  423. package/dist/world2d/capture-to-scene2d.d.ts.map +1 -0
  424. package/dist/world2d/capture-to-scene2d.js +49 -0
  425. package/dist/world2d/collision-2d.d.ts +52 -0
  426. package/dist/world2d/collision-2d.d.ts.map +1 -0
  427. package/dist/world2d/collision-2d.js +70 -0
  428. package/dist/world2d/components-2d.d.ts +47 -0
  429. package/dist/world2d/components-2d.d.ts.map +1 -0
  430. package/dist/world2d/components-2d.js +77 -0
  431. package/dist/world2d/index.d.ts +34 -0
  432. package/dist/world2d/index.d.ts.map +1 -0
  433. package/dist/world2d/index.js +32 -0
  434. package/dist/world2d/ingest-iframe-2d.d.ts +82 -0
  435. package/dist/world2d/ingest-iframe-2d.d.ts.map +1 -0
  436. package/dist/world2d/ingest-iframe-2d.js +166 -0
  437. package/dist/world2d/ingest2d.d.ts +97 -0
  438. package/dist/world2d/ingest2d.d.ts.map +1 -0
  439. package/dist/world2d/ingest2d.js +40 -0
  440. package/dist/world2d/physics2d-registry.d.ts +24 -0
  441. package/dist/world2d/physics2d-registry.d.ts.map +1 -0
  442. package/dist/world2d/physics2d-registry.js +38 -0
  443. package/dist/world2d/pixi-game-adapter.d.ts +107 -0
  444. package/dist/world2d/pixi-game-adapter.d.ts.map +1 -0
  445. package/dist/world2d/pixi-game-adapter.js +211 -0
  446. package/dist/world2d/pixi-surface.d.ts +39 -0
  447. package/dist/world2d/pixi-surface.d.ts.map +1 -0
  448. package/dist/world2d/pixi-surface.js +47 -0
  449. package/dist/world2d/scene-capture-2d.d.ts +43 -0
  450. package/dist/world2d/scene-capture-2d.d.ts.map +1 -0
  451. package/dist/world2d/scene-capture-2d.js +92 -0
  452. package/dist/world2d/scene2d-loader.d.ts +27 -0
  453. package/dist/world2d/scene2d-loader.d.ts.map +1 -0
  454. package/dist/world2d/scene2d-loader.js +274 -0
  455. package/dist/world2d/schema/entity2d.d.ts +102 -0
  456. package/dist/world2d/schema/entity2d.d.ts.map +1 -0
  457. package/dist/world2d/schema/entity2d.js +119 -0
  458. package/dist/world2d/schema/physics2d.d.ts +46 -0
  459. package/dist/world2d/schema/physics2d.d.ts.map +1 -0
  460. package/dist/world2d/schema/physics2d.js +45 -0
  461. package/dist/world2d/schema/sprite.d.ts +42 -0
  462. package/dist/world2d/schema/sprite.d.ts.map +1 -0
  463. package/dist/world2d/schema/sprite.js +63 -0
  464. package/dist/world2d/schema/tilemap.d.ts +15 -0
  465. package/dist/world2d/schema/tilemap.d.ts.map +1 -0
  466. package/dist/world2d/schema/tilemap.js +19 -0
  467. package/dist/world2d/schema/tuples2d.d.ts +20 -0
  468. package/dist/world2d/schema/tuples2d.d.ts.map +1 -0
  469. package/dist/world2d/schema/tuples2d.js +19 -0
  470. package/dist/world2d/system-adapters-2d.d.ts +16 -0
  471. package/dist/world2d/system-adapters-2d.d.ts.map +1 -0
  472. package/dist/world2d/system-adapters-2d.js +37 -0
  473. package/dist/world2d/transform-writer-2d.d.ts +16 -0
  474. package/dist/world2d/transform-writer-2d.d.ts.map +1 -0
  475. package/dist/world2d/transform-writer-2d.js +22 -0
  476. package/dist/world2d/types.d.ts +53 -0
  477. package/dist/world2d/types.d.ts.map +1 -0
  478. package/dist/world2d/types.js +1 -0
  479. package/package.json +18 -4
  480. package/src/adapter/authoring.ts +10 -0
  481. package/src/adapter/first-party-systems.ts +23 -34
  482. package/src/adapter/index.ts +1 -3
  483. package/src/adapter/system-adapter.ts +15 -22
  484. package/src/adapter/vgai-scene-game-adapter.ts +5 -14
  485. package/src/animation/anim-graph-types.ts +12 -43
  486. package/src/animation/animation-clock.ts +479 -0
  487. package/src/animation/camera-ownership.ts +467 -0
  488. package/src/animation/cinematic-cues.ts +451 -0
  489. package/src/animation/clip-map.ts +41 -0
  490. package/src/animation/gsap-registration.ts +184 -0
  491. package/src/animation/theatre-clock-binding.ts +111 -0
  492. package/src/animation/theatre-director.ts +347 -0
  493. package/src/animation/theatre-object-binding.ts +661 -0
  494. package/src/animation/xstate-animation-binding.ts +398 -0
  495. package/src/animation/xstate-animation-meta.ts +282 -0
  496. package/src/audio/index.ts +39 -7
  497. package/src/audio/tone-clock-binding.ts +98 -0
  498. package/src/audio/tone-context.ts +129 -0
  499. package/src/audio/tone-offline-render.ts +167 -0
  500. package/src/audio/wav-encode.ts +119 -0
  501. package/src/core/game-loop.ts +17 -0
  502. package/src/core/types.ts +13 -0
  503. package/src/data/data-asset.ts +167 -0
  504. package/src/data/data-check-core.ts +242 -0
  505. package/src/data/data-ref.ts +145 -0
  506. package/src/data/vite-plugin-data.ts +290 -0
  507. package/src/index.ts +48 -0
  508. package/src/input/input-manager.ts +1206 -32
  509. package/src/input/input-types.ts +155 -3
  510. package/src/input/prompt-labels.ts +122 -0
  511. package/src/input/rebind-controller.ts +105 -0
  512. package/src/input/schema.ts +206 -52
  513. package/src/react/use-data.ts +114 -0
  514. package/src/react/use-selection.tsx +135 -0
  515. package/src/runtime/create-runtime.ts +30 -9
  516. package/src/runtime/render-audio-control.ts +168 -0
  517. package/src/runtime/render-control.ts +522 -0
  518. package/src/runtime/render-seed.ts +79 -0
  519. package/src/runtime/types.ts +0 -5
  520. package/src/scene/asset-loaders.ts +10 -36
  521. package/src/scene/asset-paths.ts +0 -2
  522. package/src/scene/asset-ref-check.ts +248 -0
  523. package/src/scene/parse.ts +133 -0
  524. package/src/scene/scene-loader.ts +81 -96
  525. package/src/scene/schema/animation.ts +30 -79
  526. package/src/scene/schema/entity.ts +20 -0
  527. package/src/scene/schema/index.ts +2 -12
  528. package/src/scene/user-data.ts +8 -9
  529. package/src/setup/setup-renderer.ts +9 -2
  530. package/src/tools/define-tool.ts +152 -0
  531. package/src/animation/anim-graph.ts +0 -406
  532. package/src/animation/anim-system.ts +0 -28
  533. package/src/animation/property-track.ts +0 -178
  534. package/src/animation/schema.ts +0 -204
  535. package/src/audio/ambient.ts +0 -300
  536. package/src/audio/impacts.ts +0 -212
  537. package/src/audio/movement.ts +0 -140
  538. package/src/audio/musical.ts +0 -200
  539. package/src/audio/ui-sounds.ts +0 -171
  540. package/src/audio/vehicle.ts +0 -235
  541. package/src/audio/weapons.ts +0 -152
@@ -0,0 +1,1564 @@
1
+ import { resolveUrl } from '../loader';
2
+ import { SceneParseError } from '../scene/parse';
3
+ import { bindingDeviceFamily, gamepadAxisLabel, gamepadAxisPairLabel, keyLabel, mouseButtonLabel, STANDARD_GAMEPAD_BUTTON_LABELS, } from './prompt-labels';
4
+ import { InputMapFileSchema } from './schema';
5
+ /**
6
+ * True when a text-entry element is focused, so game input shouldn't also fire
7
+ * (e.g. typing into an editor inspector field / renaming an entity). Cheap guard
8
+ * that holds even when the game viewport is the active tab.
9
+ */
10
+ function isTextEntryFocused() {
11
+ const el = typeof document !== 'undefined' ? document.activeElement : null;
12
+ if (!el)
13
+ return false;
14
+ const tag = el.tagName;
15
+ return (tag === 'INPUT' ||
16
+ tag === 'TEXTAREA' ||
17
+ tag === 'SELECT' ||
18
+ el.isContentEditable === true);
19
+ }
20
+ /**
21
+ * Maps raw keyboard/mouse/gamepad events → named actions.
22
+ *
23
+ * Usage:
24
+ * const input = new InputManager();
25
+ * await input.loadMap('inputmaps/default.inputmap.json');
26
+ * // In game loop:
27
+ * input.poll(); // call at start of frame
28
+ * if (input.isPressed('jump')) { ... }
29
+ * if (input.isJustPressed('attack')) { ... }
30
+ * input.endFrame(); // call at end of frame
31
+ *
32
+ * F1 (spec §12 "Define Typed Action Values") — typed action values:
33
+ *
34
+ * Every action DECLARES a value shape (`InputAction.valueType`, defaulted to
35
+ * `'digital'` — see `input-types.ts`'s `ActionValueType`). Each shape has its
36
+ * own typed getter, none of which return `any`:
37
+ *
38
+ * - `'digital'` — `isPressed`/`isJustPressed`/`isJustReleased` -> `boolean`
39
+ * - `'scalar'` — `getScalar` -> `number`
40
+ * - `'vector2'` — `getVector2` -> `Vector2` ({x,y})
41
+ * - `'pointerDelta'` — `getPointerDelta` -> `Vector2`
42
+ * - `'pointerPosition'` — `getPointerPosition` -> `Vector2`
43
+ *
44
+ * Reading an action through the WRONG getter (e.g. `getScalar` on a
45
+ * `'digital'` action) throws — that's the type-safety enforcement point,
46
+ * since actions are declared in data (JSON), not TS types, so there's no
47
+ * compiler backstop otherwise. `readAction(name, expectedType)` is a generic
48
+ * entry point over the same getters, typed via `ActionValueOf<T>`.
49
+ *
50
+ * Each typed getter has a `*Source` sibling (e.g. `getScalarSource`) that
51
+ * returns `ActionValueSource | null` — which binding (and, for gamepad
52
+ * bindings, which connected gamepad, or for injected test bindings, which
53
+ * named source) produced the current value. That's the "device/source
54
+ * metadata" AC.
55
+ *
56
+ * Deadzone + normalization:
57
+ * - scalar: below `deadzone` magnitude -> 0; at/above it, rescaled from 0
58
+ * (at the deadzone edge) to ±1 (at |raw| = 1), sign preserved
59
+ * (`rescaleScalar`).
60
+ * - vector2: below `deadzone` magnitude -> {0,0}; at/above it, direction is
61
+ * preserved and magnitude is rescaled the same way, then the result is
62
+ * clamped to the unit circle — magnitude never exceeds 1 even for a raw
63
+ * diagonal input like {1,1} (`rescaleVector2`).
64
+ *
65
+ * Simultaneous-binding combine rules (documented here, spec §12 F1
66
+ * "simultaneous bindings combine predictably"):
67
+ * - digital: OR — any one satisfied binding makes the action pressed
68
+ * (pre-existing behavior, unchanged).
69
+ * - scalar: max-magnitude across contributing bindings' (deadzone-applied)
70
+ * values, sign preserved; ties keep the first-listed binding
71
+ * (deterministic given binding array order).
72
+ * - vector2: sum contributing (deadzone-applied) {x,y} values component-
73
+ * wise, then clamp the sum to the unit circle (never renormalized UP —
74
+ * only ever scaled down if the sum exceeds magnitude 1).
75
+ * - pointerDelta: sum contributing deltas (matches the existing
76
+ * mouseDeltaX/Y + lookStick accumulation precedent below).
77
+ * - pointerPosition: last-write-wins — the contributing source most
78
+ * recently updated (by injection call order) wins; position isn't
79
+ * additive so summing would be meaningless.
80
+ *
81
+ * Synthetic input injection (test hook, also the shape of F2's "injected
82
+ * test input" backend): `injectAxis`/`injectVector2`/`injectPointerDelta`/
83
+ * `injectPointerPosition` push a raw value into a named source, read back by
84
+ * binding an action to `{ type: 'test_axis' | 'test_vector2' |
85
+ * 'test_pointer_delta' | 'test_pointer_position', sourceId }` — the exact
86
+ * same aggregation path (deadzone/normalize/combine) real device bindings
87
+ * use.
88
+ *
89
+ * F2 (spec §12 "Complete Device Backends") completes the real-device side of
90
+ * that same typed-value path:
91
+ *
92
+ * - `mouse_move` — un-rejected (schema.ts). Feeds `getPointerDelta` (raw
93
+ * accumulated `mouseDeltaX/Y`, no deadzone) and `getVector2` (the same
94
+ * delta, deadzone-rescaled + unit-circle-clamped).
95
+ * - `gamepad_axis_pair` — un-rejected (schema.ts). Feeds `getVector2` with
96
+ * the coupled `(xAxis, yAxis)` reading, deadzone-rescaled like a real
97
+ * stick (default deadzone 0.15, matching `gamepad_axis`).
98
+ * - `touch_button`/`touch_stick` — new binding kinds for touch/virtual
99
+ * controls. `setTouchButton(sourceId, pressed)` feeds a digital action
100
+ * (edge-tracked like `mouse_button`); `setTouchStick(sourceId, value)`
101
+ * feeds a vector2 action (deadzone-rescaled like a gamepad stick). These
102
+ * are the injectable entry points a real on-screen touch UI's own
103
+ * touchstart/touchmove/touchend handlers would call — that DOM wiring is
104
+ * a thin adapter at the edge (browser-verified separately, e.g. F4/e2e);
105
+ * the mapping logic here is unit-tested headlessly with no DOM.
106
+ * - Focus gating — `blur`/`focus` on `window` (guarded, real DOM only) now
107
+ * drop input the same way `setEnabled(false)` does (flushing held
108
+ * key/mouse/touch-button state) without touching the `enabled` flag the
109
+ * editor drives — see `focused`/`inputActive` below. This is what stops
110
+ * a stuck key/button when the user alt-tabs away mid-press.
111
+ * - Pointer lock — `pointerlockchange` on `document` (guarded) tracks
112
+ * `isPointerLocked()`; losing lock flushes mouse-button state (a click
113
+ * held at the moment lock exits must not stick).
114
+ * - Gamepad device identity — `getGamepadInfo(index)` exposes `{index, id,
115
+ * mapping, connected}`; `ActionValueSource.gamepadId` (in addition to the
116
+ * pre-existing `gamepadIndex`) is populated for every
117
+ * gamepad_button/gamepad_axis/gamepad_axis_pair source.
118
+ * - Disconnection/reconnection needs no new tracking: `poll()` already
119
+ * re-reads `navigator.getGamepads()` every frame and every read (digital
120
+ * edges, scalar/vector2 contributions) already skips a `null` gamepad
121
+ * entry, so a disconnected gamepad's bound actions go neutral (and a
122
+ * previously-held button correctly fires `isJustReleased`) with no
123
+ * special-cased code — see the state-transition tests.
124
+ *
125
+ * F3 (spec §12 "Add Rebinding and Prompts") completes the input system with
126
+ * rebinding, structural conflict detection, and device-prompt resolution:
127
+ *
128
+ * - Listing/mutating: `getBindings`/`setBindings`/`addBinding`/
129
+ * `removeBinding`/`replaceBinding` — the same `this.actions` map F1/F2
130
+ * already read, now with a public read + mutation surface (there was
131
+ * previously none — every existing call site either loaded a map once or
132
+ * read through the typed getters).
133
+ * - Reset-to-defaults: `registerAction`/`loadMap`/`loadMapObject` snapshot
134
+ * each action's bindings into `defaultBindings` (a deep clone) at
135
+ * registration/load time; `resetBindings`/`resetAllBindings` restore from
136
+ * that snapshot. Later rebinding calls do NOT touch the snapshot, so
137
+ * "reset" always means "back to what was authored/loaded", not "back to
138
+ * the last reset".
139
+ * - Persist: `toInputMapFile()` serializes the live action set back into
140
+ * the exact `.inputmap.json` shape (`InputMapFile`) `loadMap` reads;
141
+ * `loadMapObject(data)` is `loadMap`'s synchronous, no-`fetch` sibling —
142
+ * both funnel through the same `InputMapFileSchema`-validated
143
+ * `applyParsedMap`. This is the round-trip seam: serialize, write/hand to
144
+ * something else, reload, and rebound actions survive. It complements —
145
+ * doesn't replace — the SDK's `project.inputMap.read/validate/update`
146
+ * operations (B2, `packages/vgai-sdk/src/project/input-map-operations.ts`):
147
+ * those are FILE-level (a CLI/agent editing the `.inputmap.json` on disk,
148
+ * no running game involved); this is RUNTIME-level (a live game rebinding
149
+ * while playing). Both validate through the identical
150
+ * `InputMapFileSchema`, so a file B2 wrote loads here unchanged, and a
151
+ * map this class serializes is a valid document for B2 to read back.
152
+ * - Conflicts: `findConflicts(proposed, excludeActionName?)` scans every
153
+ * OTHER registered action's bindings for one that's structurally
154
+ * equivalent to `proposed` (see the private `bindingsCollide` — same
155
+ * physical input identity: same key code, same mouse button, the same
156
+ * gamepad button/axis/axis-pair, the same touch `sourceId`, etc.) and
157
+ * returns the (possibly empty) list as plain `BindingConflict` data —
158
+ * never throws. A rebinding UI calls this BEFORE calling
159
+ * `addBinding`/`replaceBinding`/`setBindings` and decides what to do with
160
+ * a non-empty result (block, warn, let the user steal the binding by
161
+ * removing it from the other action first).
162
+ * - Prompts: `getPrompt(actionName, device?, gamepadIndex?)` resolves a
163
+ * `BindingPrompt` (display label + icon name) for whichever of the
164
+ * action's bindings belongs to the requested `PromptDevice` family
165
+ * (keyboard/mouse/gamepad/touch — `bindingDeviceFamily` in
166
+ * `prompt-labels.ts`), using `getGamepadInfo` to prefer the standard
167
+ * mapping's canonical glyph names (`'A'`, `'LB'`, ...) for gamepad
168
+ * buttons and falling back to a raw index label (or a "disconnected"
169
+ * label) when the pad isn't in standard mapping or isn't connected.
170
+ * `device` is optional (F4 below) — omit it to resolve against
171
+ * `getLastActiveDevice()` instead.
172
+ *
173
+ * F4 (spec §12 "Schema and Examples") closes F3's one open gap — a
174
+ * current-ACTIVE-device tracker, so a prompt UI can switch its displayed
175
+ * label as the player switches controllers instead of the caller having to
176
+ * track that itself:
177
+ *
178
+ * - `getLastActiveDevice()` returns the `PromptDevice` family of whichever
179
+ * input arrived most recently — keyboard on `keydown`, mouse on
180
+ * `mousedown`/a real `mousemove`, gamepad on a button press or a stick
181
+ * pushed meaningfully off-center (checked in `poll()`), touch on
182
+ * `setTouchButton(_, true)` or `setTouchStick` pushed meaningfully
183
+ * off-center. `null` before any input has arrived.
184
+ * - `getPrompt(actionName)` (device omitted) resolves against it directly,
185
+ * falling back to `'keyboard'` before any input has arrived — a HUD can
186
+ * call it every frame with no extra bookkeeping and the label switches on
187
+ * its own.
188
+ */
189
+ export class InputManager {
190
+ actions = new Map();
191
+ // F1 — each registered/loaded action's declared value shape (defaults to
192
+ // 'digital' — see registerAction/loadMap). Kept as a sibling map rather than
193
+ // folded into `actions` so the pre-F1 `Map<string, InputBinding[]>` shape
194
+ // (and every existing direct read of it, e.g. in tests) stays unchanged.
195
+ actionValueTypes = new Map();
196
+ // F3 — a deep-cloned snapshot of each action's bindings AT REGISTRATION/LOAD
197
+ // TIME (registerAction/loadMap/loadMapObject), restored by
198
+ // resetBindings/resetAllBindings. Later rebinding calls (setBindings/
199
+ // addBinding/removeBinding/replaceBinding) deliberately do not touch this —
200
+ // "reset" always means "back to the originally authored/loaded defaults".
201
+ defaultBindings = new Map();
202
+ // F3 — the `version` field of the last `loadMap`/`loadMapObject`-ed
203
+ // document, carried forward by `toInputMapFile()` so a round-trip
204
+ // (serialize -> reload) preserves it. Defaults to 1 for a manager built
205
+ // purely via `registerAction` (no file was ever loaded).
206
+ loadedMapVersion = 1;
207
+ keysDown = new Set();
208
+ keysJustDown = new Set();
209
+ keysJustUp = new Set();
210
+ mouseButtons = new Set();
211
+ mouseButtonsJustDown = new Set();
212
+ mouseButtonsJustUp = new Set();
213
+ mouseDeltaX = 0;
214
+ mouseDeltaY = 0;
215
+ gamepads = [];
216
+ // Gamepad button edge tracking (P1.6c). Keys are `${gamepadIndex}:${buttonIndex}`.
217
+ // `Prev` is last frame's pressed set; `Down` is this frame's; the just-down/up
218
+ // sets are the per-frame diff computed in poll() and cleared in endFrame().
219
+ gamepadButtonsDown = new Set();
220
+ gamepadButtonsPrev = new Set();
221
+ gamepadButtonsJustDown = new Set();
222
+ gamepadButtonsJustUp = new Set();
223
+ // Gamepad axis edge tracking: raw per-(gamepad,axis) values, this frame vs
224
+ // last frame, so `gamepad_axis` bindings can compute a thresholded
225
+ // (direction + deadzone) boolean for THIS frame and compare it against the
226
+ // same threshold applied to last frame's raw value — giving real
227
+ // isJustPressed/isJustReleased edges for an otherwise-continuous input.
228
+ // Keys are `${gamepadIndex}:${axisIndex}`; `Prev` is carried forward in
229
+ // endFrame(), mirroring the button edge-tracking above.
230
+ gamepadAxesCurrent = new Map();
231
+ gamepadAxesPrev = new Map();
232
+ lookStickX = 0;
233
+ lookStickY = 0;
234
+ // F1 — injected-test-input raw value stores, keyed by caller-chosen
235
+ // `sourceId` (see the class doc comment's "Synthetic input injection"
236
+ // section). Axis/Vector2/pointer-position are LEVEL values (persist across
237
+ // frames until changed, mirroring a real analog stick/gamepad axis —
238
+ // there's no hardware poll to naturally refresh them each frame); pointer
239
+ // delta is a per-frame accumulator (cleared in endFrame(), mirroring
240
+ // mouseDeltaX/Y above).
241
+ testAxisValues = new Map();
242
+ testVector2Values = new Map();
243
+ testPointerDeltaAccum = new Map();
244
+ testPointerPositionValues = new Map();
245
+ // Monotonic counter stamped onto each injectPointerPosition call so the
246
+ // pointerPosition combine rule (last-write-wins) can tell which of several
247
+ // contributing sources was written most recently within/across frames.
248
+ pointerPositionSeq = 0;
249
+ // F2 — touch/virtual-control state, keyed by caller-chosen `sourceId` (the
250
+ // touch UI's own zone/knob id — see setTouchButton/setTouchStick). Buttons
251
+ // mirror the mouse-button edge-tracking shape (Down/JustDown/JustUp sets);
252
+ // sticks are LEVEL values like a gamepad axis (persist until changed).
253
+ touchButtonsDown = new Set();
254
+ touchButtonsJustDown = new Set();
255
+ touchButtonsJustUp = new Set();
256
+ touchStickValues = new Map();
257
+ // F4 (spec §12 "Schema and Examples" — the F3 prompt-switching gap) — which
258
+ // device family most recently produced REAL input, so a prompt UI can
259
+ // switch its displayed label ('Space' -> 'A' -> a touch control's name) as
260
+ // the player switches controllers, without the caller having to guess or
261
+ // track this itself. Updated at the same points raw input arrives: keydown
262
+ // (`'keyboard'`), mousedown/mousemove (`'mouse'`), a gamepad button press or
263
+ // a stick pushed meaningfully past rest (`'gamepad'`, both detected in
264
+ // `poll()` — a raw resting stick reports small non-zero noise on some
265
+ // pads, so this deliberately checks a coarse magnitude, not "any non-zero
266
+ // axis"), and a touch button press / stick pushed past rest
267
+ // (`'touch'` — `setTouchButton`/`setTouchStick`). `null` until the very
268
+ // first input of any kind arrives. See `getLastActiveDevice`/`getPrompt`.
269
+ lastActiveDevice = null;
270
+ /** Coarse "is this stick meaningfully off-center" check shared by the
271
+ * gamepad and touch-stick active-device heuristics above — deliberately a
272
+ * higher bar than a binding's own (often much smaller) configured
273
+ * deadzone, since this is "did the player just grab this control", not
274
+ * "does this binding's value count as non-zero". */
275
+ static isStickActive(v) {
276
+ return Math.hypot(v.x, v.y) > 0.3;
277
+ }
278
+ disposed = false;
279
+ /**
280
+ * When false, all raw input is ignored and held/transient state is cleared.
281
+ * The editor host drives this (T6.3, `packages/editor/src/play-mode.ts`) so
282
+ * the running game only receives input while its viewport is the active,
283
+ * focused surface — keystrokes typed into the editor (Scene tab, inspector
284
+ * fields) must not leak into the game. A standalone game leaves this true
285
+ * for its whole lifetime.
286
+ */
287
+ enabled = true;
288
+ /**
289
+ * F2 — real window focus, distinct from `enabled` (which is the editor's
290
+ * explicit play-mode gate). `false` while the tab/window is blurred (real
291
+ * `blur`/`focus` events — see the constructor). Held keyboard/mouse/touch
292
+ * state is flushed on blur so a key/button held at alt-tab time doesn't
293
+ * stick (the browser stops delivering keyup/mouseup while blurred).
294
+ */
295
+ focused = true;
296
+ /** Both gates a standalone game must pass for input to be live (spec §12
297
+ * F2 "focus gating"): the editor's explicit `enabled` flag AND real
298
+ * window focus. Used everywhere `enabled` alone used to be checked. */
299
+ get inputActive() {
300
+ return this.enabled && this.focused;
301
+ }
302
+ /** F2 — real pointer-lock state (`document.pointerLockElement`), tracked via
303
+ * `pointerlockchange` (see the constructor). */
304
+ pointerLocked = false;
305
+ /** Element currently wired for click-to-pointer-lock, and its listener. */
306
+ pointerLockElement = null;
307
+ onPointerLockClick = () => {
308
+ this.pointerLockElement?.requestPointerLock();
309
+ };
310
+ onPointerLockChange = () => {
311
+ const doc = typeof document !== 'undefined' ? document : null;
312
+ const wasLocked = this.pointerLocked;
313
+ this.pointerLocked =
314
+ !!this.pointerLockElement && doc?.pointerLockElement === this.pointerLockElement;
315
+ // Losing lock mid-click must not leave a mouse button stuck down — the
316
+ // browser may not deliver a mouseup when lock exits (e.g. Escape).
317
+ if (wasLocked && !this.pointerLocked) {
318
+ this.mouseButtons.clear();
319
+ this.mouseButtonsJustDown.clear();
320
+ this.mouseButtonsJustUp.clear();
321
+ }
322
+ };
323
+ onKeyDown = (e) => {
324
+ // Ignore game input while suspended, unfocused, or while the user is typing
325
+ // into a text field (e.g. renaming an entity / editing an inspector value
326
+ // in the editor).
327
+ if (!this.inputActive || isTextEntryFocused())
328
+ return;
329
+ this.lastActiveDevice = 'keyboard';
330
+ if (!this.keysDown.has(e.code)) {
331
+ this.keysJustDown.add(e.code);
332
+ }
333
+ this.keysDown.add(e.code);
334
+ };
335
+ onKeyUp = (e) => {
336
+ if (!this.inputActive)
337
+ return;
338
+ this.keysDown.delete(e.code);
339
+ this.keysJustUp.add(e.code);
340
+ };
341
+ onMouseDown = (e) => {
342
+ if (!this.inputActive)
343
+ return;
344
+ this.lastActiveDevice = 'mouse';
345
+ this.mouseButtons.add(e.button);
346
+ this.mouseButtonsJustDown.add(e.button);
347
+ };
348
+ onMouseUp = (e) => {
349
+ if (!this.inputActive)
350
+ return;
351
+ this.mouseButtons.delete(e.button);
352
+ this.mouseButtonsJustUp.add(e.button);
353
+ };
354
+ onMouseMove = (e) => {
355
+ if (!this.inputActive)
356
+ return;
357
+ if (e.movementX !== 0 || e.movementY !== 0)
358
+ this.lastActiveDevice = 'mouse';
359
+ this.mouseDeltaX += e.movementX;
360
+ this.mouseDeltaY += e.movementY;
361
+ };
362
+ /** F2 — real window blur: drop focus and flush held state, mirroring
363
+ * `setEnabled(false)`'s flush (see `flushHeldState`), without touching the
364
+ * editor-driven `enabled` flag. */
365
+ onWindowBlur = () => {
366
+ this.focused = false;
367
+ this.flushHeldState();
368
+ };
369
+ /** F2 — real window focus regain. No flush needed (there's nothing held to
370
+ * flush — a fresh keydown/mousedown is required to register pressed
371
+ * again, exactly like re-enabling via `setEnabled(true)`). */
372
+ onWindowFocus = () => {
373
+ this.focused = true;
374
+ };
375
+ /** Clear all held/transient keyboard/mouse/touch-button state — shared by
376
+ * `setEnabled(false)` and real blur (`onWindowBlur`), so a key/button
377
+ * physically held when input is suspended doesn't linger. Level values
378
+ * (gamepad axes, touch sticks, injected test axes) are deliberately NOT
379
+ * flushed here — they are gated at READ time instead (see `inputActive`
380
+ * checks in `poll()`/the `collect*Contributions` methods), matching the
381
+ * pre-existing gamepad-axis convention. */
382
+ flushHeldState() {
383
+ this.keysDown.clear();
384
+ this.keysJustDown.clear();
385
+ this.keysJustUp.clear();
386
+ this.mouseButtons.clear();
387
+ this.mouseButtonsJustDown.clear();
388
+ this.mouseButtonsJustUp.clear();
389
+ this.mouseDeltaX = 0;
390
+ this.mouseDeltaY = 0;
391
+ this.touchButtonsDown.clear();
392
+ this.touchButtonsJustDown.clear();
393
+ this.touchButtonsJustUp.clear();
394
+ }
395
+ constructor() {
396
+ window.addEventListener('keydown', this.onKeyDown);
397
+ window.addEventListener('keyup', this.onKeyUp);
398
+ window.addEventListener('mousedown', this.onMouseDown);
399
+ window.addEventListener('mouseup', this.onMouseUp);
400
+ window.addEventListener('mousemove', this.onMouseMove);
401
+ window.addEventListener('blur', this.onWindowBlur);
402
+ window.addEventListener('focus', this.onWindowFocus);
403
+ // `document` isn't touched by any pre-F2 code path (the headless vitest
404
+ // env stubs `window` only) — guard the same way `isTextEntryFocused` does
405
+ // so tests that don't need pointer-lock coverage need no `document` stub.
406
+ if (typeof document !== 'undefined' && typeof document.addEventListener === 'function') {
407
+ document.addEventListener('pointerlockchange', this.onPointerLockChange);
408
+ }
409
+ }
410
+ /**
411
+ * Load action map from a .inputmap.json file. Validated via
412
+ * `InputMapFileSchema` (T4.6) — a malformed input map throws a
413
+ * `SceneParseError` naming the file, not a deep TypeError once the bad
414
+ * data reaches `isPressed`/`isJustPressed`/`isJustReleased`.
415
+ */
416
+ async loadMap(url) {
417
+ let data;
418
+ try {
419
+ const res = await fetch(resolveUrl(url));
420
+ if (!res.ok) {
421
+ throw new Error(`HTTP ${res.status} ${res.statusText}`);
422
+ }
423
+ const json = await res.json();
424
+ const result = InputMapFileSchema.safeParse(json);
425
+ if (!result.success)
426
+ throw new SceneParseError(result.error.issues, url);
427
+ data = result.data;
428
+ }
429
+ catch (err) {
430
+ const message = `InputManager.loadMap: failed to load input map "${url}": ${err instanceof Error ? err.message : String(err)}`;
431
+ console.error(message);
432
+ throw err instanceof SceneParseError ? err : new Error(message);
433
+ }
434
+ this.applyParsedMap(data);
435
+ }
436
+ /**
437
+ * F3 — `loadMap`'s synchronous, no-`fetch` sibling: apply an already-in-
438
+ * memory (or freshly-deserialized) input-map DOCUMENT, validated through
439
+ * the identical `InputMapFileSchema` (throws `SceneParseError` naming no
440
+ * file, matching `loadMap`'s error shape minus the URL). This is the other
441
+ * half of the F3 persist round-trip: `toInputMapFile()` serializes,
442
+ * `loadMapObject` re-applies — no network/file I/O required, so it's also
443
+ * how a headless test proves a rebind survives a save/reload cycle.
444
+ */
445
+ loadMapObject(data) {
446
+ const result = InputMapFileSchema.safeParse(data);
447
+ if (!result.success)
448
+ throw new SceneParseError(result.error.issues);
449
+ this.applyParsedMap(result.data);
450
+ return result.data;
451
+ }
452
+ /** Shared apply step for `loadMap`/`loadMapObject`: copy each action's
453
+ * bindings + declared valueType into the live maps, and snapshot the
454
+ * bindings as that action's F3 reset-to-defaults baseline. */
455
+ applyParsedMap(data) {
456
+ this.loadedMapVersion = data.version;
457
+ for (const [name, action] of Object.entries(data.actions)) {
458
+ this.actions.set(name, action.bindings);
459
+ // `InputActionSchema.valueType` has a Zod `.default('digital')`, so a
460
+ // successfully-parsed action always carries one even when the source
461
+ // JSON omitted it.
462
+ this.actionValueTypes.set(name, action.valueType ?? 'digital');
463
+ this.defaultBindings.set(name, structuredClone(action.bindings));
464
+ }
465
+ }
466
+ /** Register an action programmatically. `valueType` declares the action's
467
+ * value shape (F1) — defaults to `'digital'`, matching every call site
468
+ * written before F1. F3 — also snapshots `bindings` as this action's
469
+ * reset-to-defaults baseline (see `resetBindings`). */
470
+ registerAction(name, bindings, valueType = 'digital') {
471
+ this.actions.set(name, bindings);
472
+ this.actionValueTypes.set(name, valueType);
473
+ this.defaultBindings.set(name, structuredClone(bindings));
474
+ }
475
+ /** The declared value type for a registered/loaded action (F1) — `'digital'`
476
+ * for an action that never specified one (including an unregistered/typo'd
477
+ * name — harmless, since every typed getter already treats an unknown
478
+ * action as inert). */
479
+ getActionValueType(actionName) {
480
+ return this.actionValueTypes.get(actionName) ?? 'digital';
481
+ }
482
+ /** Enforces the F1 type-safety AC: reading an action through a typed getter
483
+ * whose value type doesn't match the action's DECLARED `valueType` throws,
484
+ * naming both. Unknown actions (never registered/loaded) are exempt — they
485
+ * already read as inert/zeroed by every getter, matching pre-F1 behavior
486
+ * for a typo'd action name. */
487
+ assertValueType(actionName, expected, method) {
488
+ const actual = this.actionValueTypes.get(actionName);
489
+ if (actual !== undefined && actual !== expected) {
490
+ throw new Error(`InputManager.${method}: action "${actionName}" is declared valueType '${actual}', not ` +
491
+ `'${expected}'. Typed reads must match an action's declared valueType (F1, spec §12) — ` +
492
+ `use the getter for '${actual}' instead.`);
493
+ }
494
+ }
495
+ /** The names of all registered actions (for the InputAdapter to enumerate). */
496
+ actionNames() {
497
+ return [...this.actions.keys()];
498
+ }
499
+ /** Poll gamepads and virtual look stick (call at start of frame) */
500
+ poll() {
501
+ if (!this.inputActive) {
502
+ // Input suspended (editor-gated play mode, OR — F2 — real window blur).
503
+ // Unlike keyboard/mouse, gamepad state isn't DOM-event-driven —
504
+ // `isPressed`/`isJustPressed`/`isJustReleased` read `this.gamepads`
505
+ // (and the edge-tracked sets) directly, not a live poll of `navigator`.
506
+ // So gate the READ here: leave `this.gamepads` empty so every
507
+ // gamepad-bound check sees no gamepads and reports false. Deliberately
508
+ // leave the edge-tracking sets
509
+ // (gamepadButtons{Prev,Down,JustDown,JustUp}, gamepadAxes{Current,Prev})
510
+ // untouched — they freeze at their last real values while disabled and
511
+ // resume diffing against genuinely-last-observed hardware state once
512
+ // re-enabled, so there's no phantom edge on re-enable (mirrors the
513
+ // "flush held state, don't corrupt history" intent of setEnabled below).
514
+ this.gamepads = [];
515
+ return;
516
+ }
517
+ this.gamepads = navigator.getGamepads ? [...navigator.getGamepads()] : [];
518
+ // Gamepad button edge detection: diff this frame's pressed set against the
519
+ // previous frame's so isJustPressed/isJustReleased work for gamepads too.
520
+ const current = new Set();
521
+ for (let i = 0; i < this.gamepads.length; i++) {
522
+ const gp = this.gamepads[i];
523
+ if (!gp)
524
+ continue;
525
+ for (let b = 0; b < gp.buttons.length; b++) {
526
+ if (gp.buttons[b]?.pressed)
527
+ current.add(`${i}:${b}`);
528
+ }
529
+ }
530
+ for (const key of current) {
531
+ if (!this.gamepadButtonsPrev.has(key))
532
+ this.gamepadButtonsJustDown.add(key);
533
+ }
534
+ for (const key of this.gamepadButtonsPrev) {
535
+ if (!current.has(key))
536
+ this.gamepadButtonsJustUp.add(key);
537
+ }
538
+ this.gamepadButtonsDown = current;
539
+ // F4 — a button press is unambiguous real gamepad input.
540
+ if (current.size > 0)
541
+ this.lastActiveDevice = 'gamepad';
542
+ // Snapshot this frame's raw axis values per (gamepad, axis) — independent
543
+ // of any binding's direction/deadzone, mirroring the button snapshot
544
+ // above. isPressed/isJustPressed/isJustReleased apply a binding's own
545
+ // threshold to these raw values (current vs. `gamepadAxesPrev`) on read.
546
+ const currentAxes = new Map();
547
+ for (let i = 0; i < this.gamepads.length; i++) {
548
+ const gp = this.gamepads[i];
549
+ if (!gp)
550
+ continue;
551
+ for (let a = 0; a < gp.axes.length; a++) {
552
+ const value = gp.axes[a] ?? 0;
553
+ currentAxes.set(`${i}:${a}`, value);
554
+ // F4 — a stick pushed meaningfully off-center is also unambiguous
555
+ // real gamepad input (see `isStickActive`'s doc comment).
556
+ if (Math.abs(value) > 0.3)
557
+ this.lastActiveDevice = 'gamepad';
558
+ }
559
+ }
560
+ this.gamepadAxesCurrent = currentAxes;
561
+ // Inject look stick as continuous mouse delta (position → rate-of-rotation)
562
+ if (this.lookStickX !== 0 || this.lookStickY !== 0) {
563
+ const scale = 8;
564
+ this.mouseDeltaX += this.lookStickX * scale;
565
+ this.mouseDeltaY += this.lookStickY * scale;
566
+ }
567
+ }
568
+ /** Has a `gamepad_axis` binding's raw value crossed its direction+deadzone
569
+ * threshold (positive: `value > deadzone`; negative: `value < -deadzone`)? */
570
+ static axisThresholdMet(value, direction, deadzone) {
571
+ return direction === 'positive' ? value > deadzone : value < -deadzone;
572
+ }
573
+ /** Is an action currently held down? */
574
+ isPressed(actionName) {
575
+ this.assertValueType(actionName, 'digital', 'isPressed');
576
+ const bindings = this.actions.get(actionName);
577
+ if (!bindings)
578
+ return false;
579
+ for (const binding of bindings) {
580
+ switch (binding.type) {
581
+ case 'key':
582
+ if (this.keysDown.has(binding.code))
583
+ return true;
584
+ break;
585
+ case 'mouse_button':
586
+ if (this.mouseButtons.has(binding.button))
587
+ return true;
588
+ break;
589
+ case 'gamepad_button':
590
+ for (const gp of this.gamepads) {
591
+ if (gp?.buttons[binding.button]?.pressed)
592
+ return true;
593
+ }
594
+ break;
595
+ case 'gamepad_axis': {
596
+ const dz = binding.deadzone ?? 0.15;
597
+ for (const gp of this.gamepads) {
598
+ if (!gp)
599
+ continue;
600
+ const val = gp.axes[binding.axis] ?? 0;
601
+ if (InputManager.axisThresholdMet(val, binding.direction, dz))
602
+ return true;
603
+ }
604
+ break;
605
+ }
606
+ case 'touch_button':
607
+ if (this.touchButtonsDown.has(binding.sourceId))
608
+ return true;
609
+ break;
610
+ }
611
+ }
612
+ return false;
613
+ }
614
+ /** Was an action pressed this frame (not held from previous)? */
615
+ isJustPressed(actionName) {
616
+ this.assertValueType(actionName, 'digital', 'isJustPressed');
617
+ const bindings = this.actions.get(actionName);
618
+ if (!bindings)
619
+ return false;
620
+ for (const binding of bindings) {
621
+ switch (binding.type) {
622
+ case 'key':
623
+ if (this.keysJustDown.has(binding.code))
624
+ return true;
625
+ break;
626
+ case 'mouse_button':
627
+ if (this.mouseButtonsJustDown.has(binding.button))
628
+ return true;
629
+ break;
630
+ case 'gamepad_button':
631
+ // Edge-tracked in poll(): true only on the first frame the button
632
+ // transitions from up→down (across any connected gamepad).
633
+ for (let i = 0; i < this.gamepads.length; i++) {
634
+ if (this.gamepadButtonsJustDown.has(`${i}:${binding.button}`))
635
+ return true;
636
+ }
637
+ break;
638
+ case 'gamepad_axis': {
639
+ // Edge-tracked via raw axis snapshots (poll()): true only on the
640
+ // first frame the thresholded (direction+deadzone) boolean flips
641
+ // from off→on (across any connected gamepad).
642
+ const dz = binding.deadzone ?? 0.15;
643
+ for (let i = 0; i < this.gamepads.length; i++) {
644
+ const key = `${i}:${binding.axis}`;
645
+ const now = this.gamepadAxesCurrent.get(key) ?? 0;
646
+ const prev = this.gamepadAxesPrev.get(key) ?? 0;
647
+ const nowOn = InputManager.axisThresholdMet(now, binding.direction, dz);
648
+ const prevOn = InputManager.axisThresholdMet(prev, binding.direction, dz);
649
+ if (nowOn && !prevOn)
650
+ return true;
651
+ }
652
+ break;
653
+ }
654
+ case 'touch_button':
655
+ if (this.touchButtonsJustDown.has(binding.sourceId))
656
+ return true;
657
+ break;
658
+ }
659
+ }
660
+ return false;
661
+ }
662
+ /** Was an action released this frame (held last frame, up now)? */
663
+ isJustReleased(actionName) {
664
+ this.assertValueType(actionName, 'digital', 'isJustReleased');
665
+ const bindings = this.actions.get(actionName);
666
+ if (!bindings)
667
+ return false;
668
+ for (const binding of bindings) {
669
+ switch (binding.type) {
670
+ case 'key':
671
+ if (this.keysJustUp.has(binding.code))
672
+ return true;
673
+ break;
674
+ case 'mouse_button':
675
+ if (this.mouseButtonsJustUp.has(binding.button))
676
+ return true;
677
+ break;
678
+ case 'gamepad_button':
679
+ for (let i = 0; i < this.gamepads.length; i++) {
680
+ if (this.gamepadButtonsJustUp.has(`${i}:${binding.button}`))
681
+ return true;
682
+ }
683
+ break;
684
+ case 'gamepad_axis': {
685
+ // The mirror of isJustPressed: true only on the first frame the
686
+ // thresholded boolean flips from on→off.
687
+ const dz = binding.deadzone ?? 0.15;
688
+ for (let i = 0; i < this.gamepads.length; i++) {
689
+ const key = `${i}:${binding.axis}`;
690
+ const now = this.gamepadAxesCurrent.get(key) ?? 0;
691
+ const prev = this.gamepadAxesPrev.get(key) ?? 0;
692
+ const nowOn = InputManager.axisThresholdMet(now, binding.direction, dz);
693
+ const prevOn = InputManager.axisThresholdMet(prev, binding.direction, dz);
694
+ if (!nowOn && prevOn)
695
+ return true;
696
+ }
697
+ break;
698
+ }
699
+ case 'touch_button':
700
+ if (this.touchButtonsJustUp.has(binding.sourceId))
701
+ return true;
702
+ break;
703
+ }
704
+ }
705
+ return false;
706
+ }
707
+ /** Device/source metadata (F1 AC) for a digital action: which binding is
708
+ * CURRENTLY satisfying `isPressed`, or `null` if none is (including an
709
+ * unknown action). First-satisfied-binding-wins, mirroring isPressed's own
710
+ * iteration order. */
711
+ getDigitalSource(actionName) {
712
+ this.assertValueType(actionName, 'digital', 'getDigitalSource');
713
+ if (!this.inputActive)
714
+ return null;
715
+ const bindings = this.actions.get(actionName);
716
+ if (!bindings)
717
+ return null;
718
+ for (const binding of bindings) {
719
+ switch (binding.type) {
720
+ case 'key':
721
+ if (this.keysDown.has(binding.code))
722
+ return { bindingType: 'key' };
723
+ break;
724
+ case 'mouse_button':
725
+ if (this.mouseButtons.has(binding.button))
726
+ return { bindingType: 'mouse_button' };
727
+ break;
728
+ case 'gamepad_button':
729
+ for (let i = 0; i < this.gamepads.length; i++) {
730
+ const gp = this.gamepads[i];
731
+ if (gp?.buttons[binding.button]?.pressed) {
732
+ return { bindingType: 'gamepad_button', gamepadIndex: i, gamepadId: gp.id };
733
+ }
734
+ }
735
+ break;
736
+ case 'gamepad_axis': {
737
+ const dz = binding.deadzone ?? 0.15;
738
+ for (let i = 0; i < this.gamepads.length; i++) {
739
+ const gp = this.gamepads[i];
740
+ if (!gp)
741
+ continue;
742
+ const val = gp.axes[binding.axis] ?? 0;
743
+ if (InputManager.axisThresholdMet(val, binding.direction, dz)) {
744
+ return { bindingType: 'gamepad_axis', gamepadIndex: i, gamepadId: gp.id };
745
+ }
746
+ }
747
+ break;
748
+ }
749
+ case 'touch_button':
750
+ if (this.touchButtonsDown.has(binding.sourceId)) {
751
+ return { bindingType: 'touch_button', sourceId: binding.sourceId };
752
+ }
753
+ break;
754
+ }
755
+ }
756
+ return null;
757
+ }
758
+ // ==========================================================================
759
+ // F1 — typed scalar/vector2/pointer reads. See the class doc comment above
760
+ // for the deadzone/normalize/combine rules these implement.
761
+ // ==========================================================================
762
+ /** 1D deadzone rescale: magnitudes below `deadzone` become 0; magnitudes
763
+ * at/above it rescale from 0 (at the deadzone edge) to ±1 (at |raw| = 1),
764
+ * sign preserved. `raw` is clamped to [-1, 1] first so an out-of-range
765
+ * input can't overshoot past ±1 the other end. */
766
+ static rescaleScalar(raw, deadzone) {
767
+ const clamped = Math.max(-1, Math.min(1, raw));
768
+ const mag = Math.abs(clamped);
769
+ if (mag < deadzone)
770
+ return 0;
771
+ const rescaled = deadzone >= 1 ? 0 : (mag - deadzone) / (1 - deadzone);
772
+ return Math.sign(clamped) * Math.min(1, rescaled);
773
+ }
774
+ /** Radial deadzone + unit-circle normalization for a Vector2: magnitude
775
+ * below `deadzone` snaps to {0,0}; at/above it, direction is preserved and
776
+ * magnitude is rescaled the same way `rescaleScalar` rescales a 1D
777
+ * magnitude (never renormalized UP past 1 — only ever scaled down). */
778
+ static rescaleVector2(raw, deadzone) {
779
+ const mag = Math.hypot(raw.x, raw.y);
780
+ if (mag === 0 || mag < deadzone)
781
+ return { x: 0, y: 0 };
782
+ const rescaledMag = Math.min(1, deadzone >= 1 ? 0 : (mag - deadzone) / (1 - deadzone));
783
+ const scale = rescaledMag / mag;
784
+ return { x: raw.x * scale, y: raw.y * scale };
785
+ }
786
+ /** Clamp a Vector2's magnitude to at most 1 (unit circle), preserving
787
+ * direction — the second half of the vector2 "sum-then-clamp" combine
788
+ * rule (a diagonal sum like {0.8, 0.8} has magnitude > 1). */
789
+ static clampVector2(v) {
790
+ const mag = Math.hypot(v.x, v.y);
791
+ if (mag <= 1 || mag === 0)
792
+ return v;
793
+ return { x: v.x / mag, y: v.y / mag };
794
+ }
795
+ /** Every `scalar`-relevant binding's (deadzone-applied) contribution for
796
+ * `actionName`, one entry per binding that can produce a scalar value —
797
+ * `gamepad_axis` (real device, already implemented pre-F1 for digital
798
+ * thresholding — F1 additionally reads its raw signed value for scalar
799
+ * actions, ignoring the binding's `direction` field, which is a digital-
800
+ * only hint) and `test_axis` (F1's injected-test-input source). `mouse_move`
801
+ * deliberately does NOT feed scalar (spec §12 F2 — no natural single-axis
802
+ * selector field is defined for it; it feeds pointerDelta/vector2 only —
803
+ * see `collectPointerDeltaContributions`/`collectVector2Contributions`). */
804
+ collectScalarContributions(actionName) {
805
+ if (!this.inputActive)
806
+ return [];
807
+ const bindings = this.actions.get(actionName);
808
+ if (!bindings)
809
+ return [];
810
+ const out = [];
811
+ for (const binding of bindings) {
812
+ if (binding.type === 'gamepad_axis') {
813
+ const dz = binding.deadzone ?? 0.15;
814
+ for (let i = 0; i < this.gamepads.length; i++) {
815
+ const gp = this.gamepads[i];
816
+ if (!gp)
817
+ continue;
818
+ const raw = gp.axes[binding.axis] ?? 0;
819
+ out.push({
820
+ value: InputManager.rescaleScalar(raw, dz),
821
+ source: { bindingType: 'gamepad_axis', gamepadIndex: i, gamepadId: gp.id },
822
+ });
823
+ }
824
+ }
825
+ else if (binding.type === 'test_axis') {
826
+ const raw = this.testAxisValues.get(binding.sourceId) ?? 0;
827
+ out.push({
828
+ value: InputManager.rescaleScalar(raw, binding.deadzone ?? 0),
829
+ source: { bindingType: 'test_axis', sourceId: binding.sourceId },
830
+ });
831
+ }
832
+ }
833
+ return out;
834
+ }
835
+ computeScalar(actionName) {
836
+ this.assertValueType(actionName, 'scalar', 'getScalar');
837
+ let best = null;
838
+ for (const c of this.collectScalarContributions(actionName)) {
839
+ if (!best || Math.abs(c.value) > Math.abs(best.value))
840
+ best = c;
841
+ }
842
+ if (!best || best.value === 0)
843
+ return { value: 0, source: null };
844
+ return best;
845
+ }
846
+ /** Read a `'scalar'`-valueType action's current value: max-magnitude across
847
+ * contributing (deadzone-applied) bindings, sign preserved (combine rule —
848
+ * see the class doc comment). Throws if the action is declared a
849
+ * different valueType. */
850
+ getScalar(actionName) {
851
+ return this.computeScalar(actionName).value;
852
+ }
853
+ /** Device/source metadata for {@link getScalar} — which binding is
854
+ * currently driving the combined value, or `null` if every contribution is
855
+ * zero (including an unknown action). */
856
+ getScalarSource(actionName) {
857
+ return this.computeScalar(actionName).source;
858
+ }
859
+ /** `gamepad_axis_pair` contribution(s) for {@link collectVector2Contributions} — one entry
860
+ * per connected gamepad, deadzone-rescaled (default 0.15, matching `gamepad_axis`). Broken
861
+ * out to a helper so the per-gamepad loop doesn't add to the main dispatcher's complexity. */
862
+ contributeGamepadAxisPair(binding) {
863
+ const dz = binding.deadzone ?? 0.15;
864
+ const out = [];
865
+ for (let i = 0; i < this.gamepads.length; i++) {
866
+ const gp = this.gamepads[i];
867
+ if (!gp)
868
+ continue;
869
+ const raw = { x: gp.axes[binding.xAxis] ?? 0, y: gp.axes[binding.yAxis] ?? 0 };
870
+ out.push({
871
+ value: InputManager.rescaleVector2(raw, dz),
872
+ source: { bindingType: 'gamepad_axis_pair', gamepadIndex: i, gamepadId: gp.id },
873
+ });
874
+ }
875
+ return out;
876
+ }
877
+ /** Every `vector2`-relevant binding's (deadzone-applied) contribution:
878
+ * `test_vector2` (F1's injected-test-input source), plus F2's real-device
879
+ * cases — `gamepad_axis_pair` (see {@link contributeGamepadAxisPair}), `mouse_move` (the
880
+ * accumulated mouse delta, deadzone default 0 — raw pixel deltas have no natural "at rest"
881
+ * jitter floor the way an analog stick does), and `touch_stick` (a virtual on-screen stick,
882
+ * deadzone default 0). */
883
+ collectVector2Contributions(actionName) {
884
+ if (!this.inputActive)
885
+ return [];
886
+ const bindings = this.actions.get(actionName);
887
+ if (!bindings)
888
+ return [];
889
+ const out = [];
890
+ for (const binding of bindings) {
891
+ switch (binding.type) {
892
+ case 'test_vector2': {
893
+ const raw = this.testVector2Values.get(binding.sourceId) ?? { x: 0, y: 0 };
894
+ out.push({
895
+ value: InputManager.rescaleVector2(raw, binding.deadzone ?? 0),
896
+ source: { bindingType: 'test_vector2', sourceId: binding.sourceId },
897
+ });
898
+ break;
899
+ }
900
+ case 'gamepad_axis_pair':
901
+ out.push(...this.contributeGamepadAxisPair(binding));
902
+ break;
903
+ case 'mouse_move': {
904
+ const raw = { x: this.mouseDeltaX, y: this.mouseDeltaY };
905
+ out.push({
906
+ value: InputManager.rescaleVector2(raw, binding.deadzone ?? 0),
907
+ source: { bindingType: 'mouse_move' },
908
+ });
909
+ break;
910
+ }
911
+ case 'touch_stick': {
912
+ const raw = this.touchStickValues.get(binding.sourceId) ?? { x: 0, y: 0 };
913
+ out.push({
914
+ value: InputManager.rescaleVector2(raw, binding.deadzone ?? 0),
915
+ source: { bindingType: 'touch_stick', sourceId: binding.sourceId },
916
+ });
917
+ break;
918
+ }
919
+ }
920
+ }
921
+ return out;
922
+ }
923
+ computeVector2(actionName) {
924
+ this.assertValueType(actionName, 'vector2', 'getVector2');
925
+ let sum = { x: 0, y: 0 };
926
+ let bestSource = null;
927
+ let bestMag = 0;
928
+ for (const c of this.collectVector2Contributions(actionName)) {
929
+ sum = { x: sum.x + c.value.x, y: sum.y + c.value.y };
930
+ const mag = Math.hypot(c.value.x, c.value.y);
931
+ if (mag > bestMag) {
932
+ bestMag = mag;
933
+ bestSource = c.source;
934
+ }
935
+ }
936
+ const clamped = InputManager.clampVector2(sum);
937
+ if (clamped.x === 0 && clamped.y === 0)
938
+ return { value: clamped, source: null };
939
+ return { value: clamped, source: bestSource };
940
+ }
941
+ /** Read a `'vector2'`-valueType action's current value: sum contributing
942
+ * (deadzone-applied) {x,y} values component-wise, then clamp to the unit
943
+ * circle (combine rule — see the class doc comment). Throws if the action
944
+ * is declared a different valueType. */
945
+ getVector2(actionName) {
946
+ return this.computeVector2(actionName).value;
947
+ }
948
+ /** Device/source metadata for {@link getVector2} — the contributing binding
949
+ * with the largest (pre-combine) magnitude, or `null` if the combined
950
+ * value is {0,0} (including an unknown action). */
951
+ getVector2Source(actionName) {
952
+ return this.computeVector2(actionName).source;
953
+ }
954
+ /** Every `pointerDelta`-relevant binding's contribution: `test_pointer_delta`
955
+ * (F1's injected-test-input source) and — F2 — `mouse_move`, reading the
956
+ * real accumulated `mouseDeltaX/Y` (the same accumulator `onMouseMove`
957
+ * fills and `getMouseDelta()` reads). No deadzone for either (deltas are
958
+ * not deadzoned). */
959
+ collectPointerDeltaContributions(actionName) {
960
+ if (!this.inputActive)
961
+ return [];
962
+ const bindings = this.actions.get(actionName);
963
+ if (!bindings)
964
+ return [];
965
+ const out = [];
966
+ for (const binding of bindings) {
967
+ if (binding.type === 'test_pointer_delta') {
968
+ const value = this.testPointerDeltaAccum.get(binding.sourceId) ?? { x: 0, y: 0 };
969
+ out.push({
970
+ value,
971
+ source: { bindingType: 'test_pointer_delta', sourceId: binding.sourceId },
972
+ });
973
+ }
974
+ else if (binding.type === 'mouse_move') {
975
+ out.push({
976
+ value: { x: this.mouseDeltaX, y: this.mouseDeltaY },
977
+ source: { bindingType: 'mouse_move' },
978
+ });
979
+ }
980
+ }
981
+ return out;
982
+ }
983
+ computePointerDelta(actionName) {
984
+ this.assertValueType(actionName, 'pointerDelta', 'getPointerDelta');
985
+ let sum = { x: 0, y: 0 };
986
+ let bestSource = null;
987
+ let bestMag = 0;
988
+ for (const c of this.collectPointerDeltaContributions(actionName)) {
989
+ sum = { x: sum.x + c.value.x, y: sum.y + c.value.y };
990
+ const mag = Math.hypot(c.value.x, c.value.y);
991
+ if (mag > bestMag) {
992
+ bestMag = mag;
993
+ bestSource = c.source;
994
+ }
995
+ }
996
+ if (sum.x === 0 && sum.y === 0)
997
+ return { value: sum, source: null };
998
+ return { value: sum, source: bestSource };
999
+ }
1000
+ /** Read a `'pointerDelta'`-valueType action's this-frame movement: sum of
1001
+ * contributing bindings' deltas (combine rule — see the class doc
1002
+ * comment). Throws if the action is declared a different valueType. */
1003
+ getPointerDelta(actionName) {
1004
+ return this.computePointerDelta(actionName).value;
1005
+ }
1006
+ /** Device/source metadata for {@link getPointerDelta}. */
1007
+ getPointerDeltaSource(actionName) {
1008
+ return this.computePointerDelta(actionName).source;
1009
+ }
1010
+ /** Every `pointerPosition`-relevant binding's contribution, each tagged
1011
+ * with the injection-order sequence number used to break ties (last-
1012
+ * write-wins) — today only `test_pointer_position` (F1's injected-test-
1013
+ * input source). */
1014
+ collectPointerPositionContributions(actionName) {
1015
+ if (!this.inputActive)
1016
+ return [];
1017
+ const bindings = this.actions.get(actionName);
1018
+ if (!bindings)
1019
+ return [];
1020
+ const out = [];
1021
+ for (const binding of bindings) {
1022
+ if (binding.type === 'test_pointer_position') {
1023
+ const entry = this.testPointerPositionValues.get(binding.sourceId);
1024
+ if (entry) {
1025
+ out.push({
1026
+ value: entry.value,
1027
+ seq: entry.seq,
1028
+ source: { bindingType: 'test_pointer_position', sourceId: binding.sourceId },
1029
+ });
1030
+ }
1031
+ }
1032
+ }
1033
+ return out;
1034
+ }
1035
+ computePointerPosition(actionName) {
1036
+ this.assertValueType(actionName, 'pointerPosition', 'getPointerPosition');
1037
+ const contributions = this.collectPointerPositionContributions(actionName);
1038
+ if (contributions.length === 0)
1039
+ return { value: { x: 0, y: 0 }, source: null };
1040
+ let best = contributions[0];
1041
+ for (const c of contributions) {
1042
+ if (c.seq > best.seq)
1043
+ best = c;
1044
+ }
1045
+ return { value: best.value, source: best.source };
1046
+ }
1047
+ /** Read a `'pointerPosition'`-valueType action's current absolute position:
1048
+ * last-write-wins across contributing bindings (combine rule — see the
1049
+ * class doc comment; position isn't additive, so it is never summed).
1050
+ * Throws if the action is declared a different valueType. */
1051
+ getPointerPosition(actionName) {
1052
+ return this.computePointerPosition(actionName).value;
1053
+ }
1054
+ /** Device/source metadata for {@link getPointerPosition} — the most
1055
+ * recently-written contributing source. */
1056
+ getPointerPositionSource(actionName) {
1057
+ return this.computePointerPosition(actionName).source;
1058
+ }
1059
+ /** Generic typed read (F1): dispatches to the matching typed getter based
1060
+ * on `expectedType`, returning `ActionValueOf<T>` — a caller with a
1061
+ * statically-known action-value-type map gets a fully-inferred, non-`any`
1062
+ * return type. Throws the same mismatch error as the dedicated getters if
1063
+ * the action's declared valueType disagrees with `expectedType`. */
1064
+ readAction(actionName, expectedType) {
1065
+ switch (expectedType) {
1066
+ case 'digital':
1067
+ return this.isPressed(actionName);
1068
+ case 'scalar':
1069
+ return this.getScalar(actionName);
1070
+ case 'vector2':
1071
+ return this.getVector2(actionName);
1072
+ case 'pointerDelta':
1073
+ return this.getPointerDelta(actionName);
1074
+ case 'pointerPosition':
1075
+ return this.getPointerPosition(actionName);
1076
+ default:
1077
+ throw new Error(`InputManager.readAction: unknown value type "${expectedType}"`);
1078
+ }
1079
+ }
1080
+ /** Inject a synthetic scalar value for a named test source (F1's injected-
1081
+ * test-input hook — also the shape of F2's "injected test input" backend).
1082
+ * Bind an action to `{ type: 'test_axis', sourceId }` (`valueType:
1083
+ * 'scalar'`) to read it back via {@link getScalar}. The value PERSISTS
1084
+ * across frames (like a real analog stick held in position) until changed
1085
+ * or cleared via {@link clearAxis}. */
1086
+ injectAxis(sourceId, value) {
1087
+ this.testAxisValues.set(sourceId, value);
1088
+ }
1089
+ /** Clear an injected scalar source (e.g. simulating an at-rest/disconnected
1090
+ * device). */
1091
+ clearAxis(sourceId) {
1092
+ this.testAxisValues.delete(sourceId);
1093
+ }
1094
+ /** Inject a synthetic {x,y} value for a named test source. Bind an action
1095
+ * to `{ type: 'test_vector2', sourceId }` (`valueType: 'vector2'`) to read
1096
+ * it back via {@link getVector2}. Persists across frames until changed or
1097
+ * cleared via {@link clearVector2}. */
1098
+ injectVector2(sourceId, value) {
1099
+ this.testVector2Values.set(sourceId, value);
1100
+ }
1101
+ /** Clear an injected Vector2 source. */
1102
+ clearVector2(sourceId) {
1103
+ this.testVector2Values.delete(sourceId);
1104
+ }
1105
+ /** Accumulate a synthetic pointer delta for a named test source — mirrors
1106
+ * real mouse-move delta accumulation (`onMouseMove` above): multiple calls
1107
+ * within the same frame sum, and the accumulator is cleared in
1108
+ * {@link endFrame}. Bind an action to `{ type: 'test_pointer_delta',
1109
+ * sourceId }` (`valueType: 'pointerDelta'`) to read it back via
1110
+ * {@link getPointerDelta}. */
1111
+ injectPointerDelta(sourceId, delta) {
1112
+ const prev = this.testPointerDeltaAccum.get(sourceId) ?? { x: 0, y: 0 };
1113
+ this.testPointerDeltaAccum.set(sourceId, { x: prev.x + delta.x, y: prev.y + delta.y });
1114
+ }
1115
+ /** Set a synthetic absolute pointer position for a named test source.
1116
+ * Persists (last-write-wins across contributing sources on read — see the
1117
+ * pointerPosition combine rule) until changed. Bind an action to `{ type:
1118
+ * 'test_pointer_position', sourceId }` (`valueType: 'pointerPosition'`) to
1119
+ * read it back via {@link getPointerPosition}. */
1120
+ injectPointerPosition(sourceId, value) {
1121
+ this.testPointerPositionValues.set(sourceId, { value, seq: ++this.pointerPositionSeq });
1122
+ }
1123
+ /** Whether input capture is currently enabled (see {@link setEnabled}). */
1124
+ isEnabled() {
1125
+ return this.enabled;
1126
+ }
1127
+ /**
1128
+ * Enable/disable input capture. The editor host disables the running game's
1129
+ * input while its viewport isn't focused so keystrokes don't leak in from
1130
+ * the editor. Disabling clears all held/transient keyboard/mouse/touch-button
1131
+ * state (via {@link flushHeldState}) so nothing sticks (e.g. a held movement
1132
+ * key when you switch back to the Scene tab mid-flight) — gamepad state
1133
+ * doesn't need clearing here since `poll()` itself stops reading it while
1134
+ * disabled (see poll()'s doc comment).
1135
+ */
1136
+ setEnabled(enabled) {
1137
+ if (this.enabled === enabled)
1138
+ return;
1139
+ this.enabled = enabled;
1140
+ if (!enabled)
1141
+ this.flushHeldState();
1142
+ }
1143
+ /** Whether the window/tab is currently focused (F2, spec §12 "focus
1144
+ * gating") — `false` while blurred (real `blur`/`focus` events; see the
1145
+ * constructor). Distinct from {@link isEnabled}, which is the editor's
1146
+ * explicit play-mode gate. */
1147
+ isFocused() {
1148
+ return this.focused;
1149
+ }
1150
+ /** Whether the pointer is currently locked to {@link requestPointerLock}'s
1151
+ * element (F2, spec §12 "pointer lock") — tracked via the real
1152
+ * `pointerlockchange` event. */
1153
+ isPointerLocked() {
1154
+ return this.pointerLocked;
1155
+ }
1156
+ /** Device identity for a connected gamepad slot (F2, spec §12 "expose
1157
+ * device identity" + "prefer the standard mapping"), or `null` if nothing
1158
+ * is connected at `index` (including while input is disabled/unfocused,
1159
+ * since `poll()` empties `this.gamepads` then). `mapping` is the Gamepad
1160
+ * API's own `'standard'`/`''` value — bindings author button/axis indices
1161
+ * assuming the W3C Standard Gamepad layout, so a non-`'standard'` mapping
1162
+ * means those indices may not correspond as documented. */
1163
+ getGamepadInfo(index) {
1164
+ const gp = this.gamepads[index];
1165
+ if (!gp)
1166
+ return null;
1167
+ return { index, id: gp.id, mapping: gp.mapping, connected: gp.connected };
1168
+ }
1169
+ /** Get mouse movement delta this frame */
1170
+ getMouseDelta() {
1171
+ return { x: this.mouseDeltaX, y: this.mouseDeltaY };
1172
+ }
1173
+ /**
1174
+ * F4 (spec §12 "Schema and Examples" — the F3 prompt-switching gap): which
1175
+ * device family most recently produced real input, or `null` before any
1176
+ * input has arrived. A prompt UI polls this (or lets {@link getPrompt}
1177
+ * consult it automatically by omitting `device`) to switch its displayed
1178
+ * label as the player switches controllers — e.g. showing `'Space'` while
1179
+ * they're on keyboard, then `'A'` the moment they touch a gamepad.
1180
+ */
1181
+ getLastActiveDevice() {
1182
+ return this.lastActiveDevice;
1183
+ }
1184
+ // ==========================================================================
1185
+ // F3 — rebinding: list/replace/add/remove/reset/persist. See the class doc
1186
+ // comment above for the full design note.
1187
+ // ==========================================================================
1188
+ /** The current bindings for `actionName` (F3 AC "listed") — a defensive
1189
+ * deep clone, so mutating the returned array/objects never affects the
1190
+ * live binding list. `[]` for an unknown/never-registered action. */
1191
+ getBindings(actionName) {
1192
+ const bindings = this.actions.get(actionName);
1193
+ return bindings ? structuredClone(bindings) : [];
1194
+ }
1195
+ /** Wholesale-replace `actionName`'s bindings array (F3 AC "replaced"). Does
1196
+ * NOT touch the reset-to-defaults snapshot — see `resetBindings`. A no-op
1197
+ * target for an action that was never registered is allowed (it simply
1198
+ * registers one with `valueType: 'digital'`, matching `registerAction`'s
1199
+ * own default) since a rebinding UI operates on actions it already knows
1200
+ * about, but shouldn't have to special-case "brand new action" separately. */
1201
+ setBindings(actionName, bindings) {
1202
+ if (!this.actionValueTypes.has(actionName))
1203
+ this.actionValueTypes.set(actionName, 'digital');
1204
+ this.actions.set(actionName, structuredClone(bindings));
1205
+ }
1206
+ /** Append one binding to `actionName` (F3 AC "added"). */
1207
+ addBinding(actionName, binding) {
1208
+ const bindings = this.actions.get(actionName) ?? [];
1209
+ this.actions.set(actionName, [...bindings, structuredClone(binding)]);
1210
+ if (!this.actionValueTypes.has(actionName))
1211
+ this.actionValueTypes.set(actionName, 'digital');
1212
+ }
1213
+ /** Remove the binding at `index` from `actionName` (F3 AC "removed"). A
1214
+ * no-op if the action is unknown or `index` is out of range (rather than
1215
+ * throwing) — mirrors every other getter's "unknown action is inert"
1216
+ * convention, so a UI doesn't need a separate existence check first. */
1217
+ removeBinding(actionName, index) {
1218
+ const bindings = this.actions.get(actionName);
1219
+ if (!bindings || index < 0 || index >= bindings.length)
1220
+ return;
1221
+ this.actions.set(actionName, [...bindings.slice(0, index), ...bindings.slice(index + 1)]);
1222
+ }
1223
+ /** Replace a single binding slot at `index` on `actionName` — the shape a
1224
+ * rebinding UI actually wants ("rebind THIS control"), built on
1225
+ * `getBindings`/`setBindings`. A no-op if the action is unknown or `index`
1226
+ * is out of range. */
1227
+ replaceBinding(actionName, index, binding) {
1228
+ const bindings = this.actions.get(actionName);
1229
+ if (!bindings || index < 0 || index >= bindings.length)
1230
+ return;
1231
+ const next = bindings.slice();
1232
+ next[index] = structuredClone(binding);
1233
+ this.actions.set(actionName, next);
1234
+ }
1235
+ /** Restore `actionName`'s bindings to the snapshot taken at
1236
+ * `registerAction`/`loadMap`/`loadMapObject` time (F3 AC "reset"). A no-op
1237
+ * for an action with no snapshot (never registered/loaded — there is no
1238
+ * "default" to reset to). */
1239
+ resetBindings(actionName) {
1240
+ const defaults = this.defaultBindings.get(actionName);
1241
+ if (!defaults)
1242
+ return;
1243
+ this.actions.set(actionName, structuredClone(defaults));
1244
+ }
1245
+ /** Reset every action that has a snapshot back to its registered/loaded
1246
+ * defaults. Convenience batch form of `resetBindings`. */
1247
+ resetAllBindings() {
1248
+ for (const actionName of this.defaultBindings.keys())
1249
+ this.resetBindings(actionName);
1250
+ }
1251
+ /**
1252
+ * Whether `a` and `b` are the SAME physical input (F3's structural-conflict
1253
+ * identity check — see `findConflicts`). Deliberately narrower than deep
1254
+ * equality: `deadzone` never participates (two bindings on the same key
1255
+ * with different deadzones are still the same physical control), and a
1256
+ * `gamepad_axis`'s `direction` DOES participate — opposite directions of one
1257
+ * physical axis (e.g. "accelerate" on positive, "brake" on negative) are a
1258
+ * legitimate, non-conflicting pair, not a collision.
1259
+ */
1260
+ static bindingsCollide(a, b) {
1261
+ if (a.type !== b.type)
1262
+ return false;
1263
+ switch (a.type) {
1264
+ case 'key':
1265
+ return a.code === b.code;
1266
+ case 'mouse_button':
1267
+ return a.button === b.button;
1268
+ case 'mouse_move':
1269
+ return true; // there is only one physical mouse-move source
1270
+ case 'gamepad_button':
1271
+ return a.button === b.button;
1272
+ case 'gamepad_axis': {
1273
+ const other = b;
1274
+ return a.axis === other.axis && a.direction === other.direction;
1275
+ }
1276
+ case 'gamepad_axis_pair': {
1277
+ const other = b;
1278
+ return a.xAxis === other.xAxis && a.yAxis === other.yAxis;
1279
+ }
1280
+ case 'touch_button':
1281
+ case 'touch_stick':
1282
+ case 'test_axis':
1283
+ case 'test_vector2':
1284
+ case 'test_pointer_delta':
1285
+ case 'test_pointer_position':
1286
+ return a.sourceId === b.sourceId;
1287
+ default:
1288
+ return false;
1289
+ }
1290
+ }
1291
+ /**
1292
+ * Every EXISTING binding, on every OTHER registered action, that is
1293
+ * structurally the same physical input as `proposed` (F3 AC "conflicts are
1294
+ * detected and returned structurally") — never throws, always a plain
1295
+ * (possibly empty) array a rebinding UI inspects before committing a
1296
+ * rebind. `excludeActionName` is normally the action being rebound itself
1297
+ * (so an action isn't reported as conflicting with its own current
1298
+ * binding when re-proposing an unchanged value).
1299
+ */
1300
+ findConflicts(proposed, excludeActionName) {
1301
+ const conflicts = [];
1302
+ for (const [actionName, bindings] of this.actions) {
1303
+ if (actionName === excludeActionName)
1304
+ continue;
1305
+ bindings.forEach((binding, bindingIndex) => {
1306
+ if (InputManager.bindingsCollide(proposed, binding)) {
1307
+ conflicts.push({ actionName, bindingIndex, binding: structuredClone(binding) });
1308
+ }
1309
+ });
1310
+ }
1311
+ return conflicts;
1312
+ }
1313
+ /**
1314
+ * Serialize every registered/loaded action back into the `.inputmap.json`
1315
+ * document shape (F3 AC "persisted") — the exact `InputMapFile` shape
1316
+ * `loadMap`/`loadMapObject` read, so `manager.loadMapObject(manager.toInputMapFile())`
1317
+ * on a fresh instance reproduces the same live bindings. `version` is
1318
+ * carried forward from the last loaded map when known, else `1` (a
1319
+ * manager built purely via `registerAction` calls, never `loadMap`, has no
1320
+ * file-format version to preserve).
1321
+ */
1322
+ toInputMapFile() {
1323
+ const actions = {};
1324
+ for (const [name, bindings] of this.actions) {
1325
+ actions[name] = {
1326
+ valueType: this.actionValueTypes.get(name) ?? 'digital',
1327
+ bindings: structuredClone(bindings),
1328
+ };
1329
+ }
1330
+ return { version: this.loadedMapVersion, actions };
1331
+ }
1332
+ /**
1333
+ * Resolve a display prompt (F3 AC: labels/icons like `'Space'`, `'A'`, or a
1334
+ * touch control's name) for whichever of `actionName`'s bindings belongs to
1335
+ * the requested `device` family. `null` when the action has no binding for
1336
+ * that device (including an unknown action). `gamepadIndex` (default 0)
1337
+ * selects which connected pad's `mapping`/connected state to consult for a
1338
+ * `gamepad_*` binding — see `getGamepadInfo`.
1339
+ *
1340
+ * F4 — `device` is now OPTIONAL: omit it (or pass `undefined`) to resolve
1341
+ * against {@link getLastActiveDevice} instead (falling back to `'keyboard'`
1342
+ * before any input has arrived), so a prompt UI that wants "show whatever
1343
+ * the player is actually holding right now" doesn't have to track the
1344
+ * active device itself — it can just call `getPrompt(actionName)` every
1345
+ * frame and the label switches on its own as the player changes controllers.
1346
+ * Passing an explicit `device` (as every pre-F4 caller does) is unaffected.
1347
+ */
1348
+ getPrompt(actionName, device, gamepadIndex = 0) {
1349
+ const resolvedDevice = device ?? this.lastActiveDevice ?? 'keyboard';
1350
+ const bindings = this.actions.get(actionName);
1351
+ if (!bindings)
1352
+ return null;
1353
+ const binding = bindings.find((b) => bindingDeviceFamily(b.type) === resolvedDevice);
1354
+ if (!binding)
1355
+ return null;
1356
+ switch (binding.type) {
1357
+ case 'key':
1358
+ return {
1359
+ device: 'keyboard',
1360
+ bindingType: 'key',
1361
+ label: keyLabel(binding.code),
1362
+ icon: keyLabel(binding.code),
1363
+ };
1364
+ case 'mouse_button':
1365
+ return {
1366
+ device: 'mouse',
1367
+ bindingType: 'mouse_button',
1368
+ label: mouseButtonLabel(binding.button),
1369
+ icon: 'mouse',
1370
+ };
1371
+ case 'mouse_move':
1372
+ return { device: 'mouse', bindingType: 'mouse_move', label: 'Mouse', icon: 'mouse' };
1373
+ case 'gamepad_button': {
1374
+ const info = this.getGamepadInfo(gamepadIndex);
1375
+ if (!info?.connected) {
1376
+ return {
1377
+ device: 'gamepad',
1378
+ bindingType: 'gamepad_button',
1379
+ label: 'Gamepad Disconnected',
1380
+ icon: 'gamepad-off',
1381
+ };
1382
+ }
1383
+ const label = info.mapping === 'standard'
1384
+ ? (STANDARD_GAMEPAD_BUTTON_LABELS[binding.button] ?? `Button ${binding.button}`)
1385
+ : `Button ${binding.button}`;
1386
+ return { device: 'gamepad', bindingType: 'gamepad_button', label, icon: label };
1387
+ }
1388
+ case 'gamepad_axis': {
1389
+ const info = this.getGamepadInfo(gamepadIndex);
1390
+ if (!info?.connected) {
1391
+ return {
1392
+ device: 'gamepad',
1393
+ bindingType: 'gamepad_axis',
1394
+ label: 'Gamepad Disconnected',
1395
+ icon: 'gamepad-off',
1396
+ };
1397
+ }
1398
+ return {
1399
+ device: 'gamepad',
1400
+ bindingType: 'gamepad_axis',
1401
+ label: gamepadAxisLabel(binding.axis),
1402
+ icon: 'gamepad-stick',
1403
+ };
1404
+ }
1405
+ case 'gamepad_axis_pair': {
1406
+ const info = this.getGamepadInfo(gamepadIndex);
1407
+ if (!info?.connected) {
1408
+ return {
1409
+ device: 'gamepad',
1410
+ bindingType: 'gamepad_axis_pair',
1411
+ label: 'Gamepad Disconnected',
1412
+ icon: 'gamepad-off',
1413
+ };
1414
+ }
1415
+ return {
1416
+ device: 'gamepad',
1417
+ bindingType: 'gamepad_axis_pair',
1418
+ label: gamepadAxisPairLabel(binding.xAxis, binding.yAxis),
1419
+ icon: 'gamepad-stick',
1420
+ };
1421
+ }
1422
+ case 'touch_button':
1423
+ return {
1424
+ device: 'touch',
1425
+ bindingType: 'touch_button',
1426
+ label: binding.sourceId,
1427
+ icon: 'touch',
1428
+ };
1429
+ case 'touch_stick':
1430
+ return {
1431
+ device: 'touch',
1432
+ bindingType: 'touch_stick',
1433
+ label: binding.sourceId,
1434
+ icon: 'touch',
1435
+ };
1436
+ default:
1437
+ return null;
1438
+ }
1439
+ }
1440
+ /** Call at end of frame to clear per-frame state */
1441
+ endFrame() {
1442
+ this.keysJustDown.clear();
1443
+ this.keysJustUp.clear();
1444
+ this.mouseButtonsJustDown.clear();
1445
+ this.mouseButtonsJustUp.clear();
1446
+ // Carry this frame's gamepad pressed set forward as next frame's baseline,
1447
+ // then clear the per-frame edge sets.
1448
+ this.gamepadButtonsPrev = this.gamepadButtonsDown;
1449
+ this.gamepadButtonsJustDown.clear();
1450
+ this.gamepadButtonsJustUp.clear();
1451
+ // Carry this frame's raw axis snapshot forward as next frame's baseline
1452
+ // for gamepad_axis edge detection (isJustPressed/isJustReleased).
1453
+ this.gamepadAxesPrev = this.gamepadAxesCurrent;
1454
+ this.mouseDeltaX = 0;
1455
+ this.mouseDeltaY = 0;
1456
+ // F1 — pointerDelta is a per-frame accumulator, same shape as
1457
+ // mouseDeltaX/Y above; axis/vector2/pointerPosition are level values and
1458
+ // deliberately NOT reset here (they persist until changed/cleared).
1459
+ this.testPointerDeltaAccum.clear();
1460
+ // F2 — touch-button edges, same shape as the mouse-button edges above.
1461
+ this.touchButtonsJustDown.clear();
1462
+ this.touchButtonsJustUp.clear();
1463
+ }
1464
+ /** Inject synthetic key state from touch controls */
1465
+ setVirtualKey(code, pressed) {
1466
+ if (pressed) {
1467
+ if (!this.keysDown.has(code))
1468
+ this.keysJustDown.add(code);
1469
+ this.keysDown.add(code);
1470
+ }
1471
+ else {
1472
+ if (this.keysDown.has(code))
1473
+ this.keysJustUp.add(code);
1474
+ this.keysDown.delete(code);
1475
+ }
1476
+ }
1477
+ /** Set virtual look stick position (-1..1). Injected as mouse delta during poll(). */
1478
+ setLookStick(x, y) {
1479
+ this.lookStickX = x;
1480
+ this.lookStickY = y;
1481
+ }
1482
+ /** Inject synthetic mouse delta (e.g. from touch controls) */
1483
+ addMouseDelta(dx, dy) {
1484
+ this.mouseDeltaX += dx;
1485
+ this.mouseDeltaY += dy;
1486
+ }
1487
+ /**
1488
+ * F2 (spec §12 "Complete Device Backends") — the touch/virtual-control
1489
+ * digital backend. Set a named virtual button's (`sourceId` — a zone/knob
1490
+ * id the touch UI chooses) pressed state; edge-tracked exactly like a real
1491
+ * mouse button (`onMouseDown`/`onMouseUp` above) so `isJustPressed`/
1492
+ * `isJustReleased` work for `{ type: 'touch_button', sourceId }` bindings.
1493
+ * This is the injectable entry point a real on-screen button's own
1494
+ * touchstart/touchend handler would call — that DOM wiring is a thin
1495
+ * adapter at the edge (browser-verified separately); this method is the
1496
+ * MAPPING logic, unit-tested headlessly with no DOM.
1497
+ */
1498
+ setTouchButton(sourceId, pressed) {
1499
+ if (!this.inputActive)
1500
+ return;
1501
+ if (pressed) {
1502
+ if (!this.touchButtonsDown.has(sourceId))
1503
+ this.touchButtonsJustDown.add(sourceId);
1504
+ this.touchButtonsDown.add(sourceId);
1505
+ this.lastActiveDevice = 'touch'; // F4
1506
+ }
1507
+ else {
1508
+ if (this.touchButtonsDown.has(sourceId))
1509
+ this.touchButtonsJustUp.add(sourceId);
1510
+ this.touchButtonsDown.delete(sourceId);
1511
+ }
1512
+ }
1513
+ /**
1514
+ * F2 — the touch/virtual-control analog backend. Set a named virtual
1515
+ * stick's (`sourceId`) raw `{x,y}` position (e.g. -1..1 per axis, like a
1516
+ * gamepad stick); read back via `getVector2` on a `{ type: 'touch_stick',
1517
+ * sourceId }` binding, deadzone-rescaled the same way a gamepad stick is.
1518
+ * Persists across frames (a level value, like a real analog stick held in
1519
+ * position) until changed or cleared via {@link clearTouchStick}.
1520
+ */
1521
+ setTouchStick(sourceId, value) {
1522
+ this.touchStickValues.set(sourceId, value);
1523
+ // F4 — mirrors the gamepad-axis heuristic in poll() (see `isStickActive`).
1524
+ if (InputManager.isStickActive(value))
1525
+ this.lastActiveDevice = 'touch';
1526
+ }
1527
+ /** Clear a virtual stick's value (e.g. simulating the touch point lifting —
1528
+ * the knob returns to center). */
1529
+ clearTouchStick(sourceId) {
1530
+ this.touchStickValues.delete(sourceId);
1531
+ }
1532
+ /** Request pointer lock for FPS-style mouse control */
1533
+ requestPointerLock(element) {
1534
+ // Idempotent: never stack duplicate click listeners. Rewire if the element
1535
+ // changed, and track it so dispose() can remove the listener.
1536
+ if (this.pointerLockElement === element)
1537
+ return;
1538
+ if (this.pointerLockElement) {
1539
+ this.pointerLockElement.removeEventListener('click', this.onPointerLockClick);
1540
+ }
1541
+ this.pointerLockElement = element;
1542
+ element.addEventListener('click', this.onPointerLockClick);
1543
+ }
1544
+ /** Clean up event listeners */
1545
+ dispose() {
1546
+ if (this.disposed)
1547
+ return;
1548
+ this.disposed = true;
1549
+ window.removeEventListener('keydown', this.onKeyDown);
1550
+ window.removeEventListener('keyup', this.onKeyUp);
1551
+ window.removeEventListener('mousedown', this.onMouseDown);
1552
+ window.removeEventListener('mouseup', this.onMouseUp);
1553
+ window.removeEventListener('mousemove', this.onMouseMove);
1554
+ window.removeEventListener('blur', this.onWindowBlur);
1555
+ window.removeEventListener('focus', this.onWindowFocus);
1556
+ if (typeof document !== 'undefined' && typeof document.removeEventListener === 'function') {
1557
+ document.removeEventListener('pointerlockchange', this.onPointerLockChange);
1558
+ }
1559
+ if (this.pointerLockElement) {
1560
+ this.pointerLockElement.removeEventListener('click', this.onPointerLockClick);
1561
+ this.pointerLockElement = null;
1562
+ }
1563
+ }
1564
+ }