@vgai/engine 0.5.0 → 0.5.1

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 (538) hide show
  1. package/package.json +1 -1
  2. package/schemas/engine-capabilities.json +4 -4
  3. package/schemas/vgai-game.schema.json +2 -2
  4. package/src/adapter/authoring.ts +42 -0
  5. package/src/adapter/index.ts +3 -0
  6. package/src/adapter/ingest/overlay-apply.ts +50 -6
  7. package/src/adapter/ingest/scene-capture.ts +51 -2
  8. package/src/adapter/vgai-scene-game-adapter.ts +24 -0
  9. package/src/manifest/schema.ts +7 -3
  10. package/src/runtime/create-runtime.ts +97 -0
  11. package/src/runtime/game.ts +11 -1
  12. package/src/scene/asset-ref-check.ts +1 -1
  13. package/src/scene/scene-loader.ts +2 -2
  14. package/src/scene/user-data.ts +18 -0
  15. package/src/world3d-react/behavior.tsx +19 -11
  16. package/src/world3d-react/r3f-adapter.tsx +8 -0
  17. package/dist/adapter/adapter-surface.d.ts +0 -30
  18. package/dist/adapter/adapter-surface.d.ts.map +0 -1
  19. package/dist/adapter/adapter-surface.js +0 -18
  20. package/dist/adapter/authoring.d.ts +0 -461
  21. package/dist/adapter/authoring.d.ts.map +0 -1
  22. package/dist/adapter/authoring.js +0 -22
  23. package/dist/adapter/colyseus-networking-adapter.d.ts +0 -43
  24. package/dist/adapter/colyseus-networking-adapter.d.ts.map +0 -1
  25. package/dist/adapter/colyseus-networking-adapter.js +0 -38
  26. package/dist/adapter/first-party-systems.d.ts +0 -38
  27. package/dist/adapter/first-party-systems.d.ts.map +0 -1
  28. package/dist/adapter/first-party-systems.js +0 -77
  29. package/dist/adapter/game-adapter.d.ts +0 -133
  30. package/dist/adapter/game-adapter.d.ts.map +0 -1
  31. package/dist/adapter/game-adapter.js +0 -10
  32. package/dist/adapter/host-context.d.ts +0 -71
  33. package/dist/adapter/host-context.d.ts.map +0 -1
  34. package/dist/adapter/host-context.js +0 -16
  35. package/dist/adapter/index.d.ts +0 -22
  36. package/dist/adapter/index.d.ts.map +0 -1
  37. package/dist/adapter/index.js +0 -15
  38. package/dist/adapter/ingest/game-contract.d.ts +0 -53
  39. package/dist/adapter/ingest/game-contract.d.ts.map +0 -1
  40. package/dist/adapter/ingest/game-contract.js +0 -30
  41. package/dist/adapter/ingest/overlay-applier.d.ts +0 -118
  42. package/dist/adapter/ingest/overlay-applier.d.ts.map +0 -1
  43. package/dist/adapter/ingest/overlay-applier.js +0 -132
  44. package/dist/adapter/ingest/overlay-apply.d.ts +0 -60
  45. package/dist/adapter/ingest/overlay-apply.d.ts.map +0 -1
  46. package/dist/adapter/ingest/overlay-apply.js +0 -101
  47. package/dist/adapter/ingest/overlay-file.d.ts +0 -79
  48. package/dist/adapter/ingest/overlay-file.d.ts.map +0 -1
  49. package/dist/adapter/ingest/overlay-file.js +0 -66
  50. package/dist/adapter/ingest/overlay-report.d.ts +0 -113
  51. package/dist/adapter/ingest/overlay-report.d.ts.map +0 -1
  52. package/dist/adapter/ingest/overlay-report.js +0 -103
  53. package/dist/adapter/ingest/scene-capture.d.ts +0 -97
  54. package/dist/adapter/ingest/scene-capture.d.ts.map +0 -1
  55. package/dist/adapter/ingest/scene-capture.js +0 -248
  56. package/dist/adapter/ingest/upstream-pin.d.ts +0 -47
  57. package/dist/adapter/ingest/upstream-pin.d.ts.map +0 -1
  58. package/dist/adapter/ingest/upstream-pin.js +0 -50
  59. package/dist/adapter/loop-gate-report.d.ts +0 -34
  60. package/dist/adapter/loop-gate-report.d.ts.map +0 -1
  61. package/dist/adapter/loop-gate-report.js +0 -37
  62. package/dist/adapter/rapier-physics-adapter.d.ts +0 -12
  63. package/dist/adapter/rapier-physics-adapter.d.ts.map +0 -1
  64. package/dist/adapter/rapier-physics-adapter.js +0 -46
  65. package/dist/adapter/system-adapter.d.ts +0 -206
  66. package/dist/adapter/system-adapter.d.ts.map +0 -1
  67. package/dist/adapter/system-adapter.js +0 -13
  68. package/dist/adapter/transform.d.ts +0 -18
  69. package/dist/adapter/transform.d.ts.map +0 -1
  70. package/dist/adapter/transform.js +0 -1
  71. package/dist/adapter/vgai-scene-game-adapter.d.ts +0 -108
  72. package/dist/adapter/vgai-scene-game-adapter.d.ts.map +0 -1
  73. package/dist/adapter/vgai-scene-game-adapter.js +0 -762
  74. package/dist/ai/navigation.d.ts +0 -64
  75. package/dist/ai/navigation.d.ts.map +0 -1
  76. package/dist/ai/navigation.js +0 -133
  77. package/dist/animation/anim-graph-types.d.ts +0 -25
  78. package/dist/animation/anim-graph-types.d.ts.map +0 -1
  79. package/dist/animation/anim-graph-types.js +0 -1
  80. package/dist/animation/animation-clock.d.ts +0 -213
  81. package/dist/animation/animation-clock.d.ts.map +0 -1
  82. package/dist/animation/animation-clock.js +0 -301
  83. package/dist/animation/blend-node.d.ts +0 -22
  84. package/dist/animation/blend-node.d.ts.map +0 -1
  85. package/dist/animation/blend-node.js +0 -97
  86. package/dist/animation/camera-ownership.d.ts +0 -250
  87. package/dist/animation/camera-ownership.d.ts.map +0 -1
  88. package/dist/animation/camera-ownership.js +0 -169
  89. package/dist/animation/cinematic-cues.d.ts +0 -200
  90. package/dist/animation/cinematic-cues.d.ts.map +0 -1
  91. package/dist/animation/cinematic-cues.js +0 -213
  92. package/dist/animation/clip-map.d.ts +0 -12
  93. package/dist/animation/clip-map.d.ts.map +0 -1
  94. package/dist/animation/clip-map.js +0 -37
  95. package/dist/animation/gsap-registration.d.ts +0 -119
  96. package/dist/animation/gsap-registration.d.ts.map +0 -1
  97. package/dist/animation/gsap-registration.js +0 -92
  98. package/dist/animation/theatre-clock-binding.d.ts +0 -89
  99. package/dist/animation/theatre-clock-binding.d.ts.map +0 -1
  100. package/dist/animation/theatre-clock-binding.js +0 -29
  101. package/dist/animation/theatre-director.d.ts +0 -242
  102. package/dist/animation/theatre-director.d.ts.map +0 -1
  103. package/dist/animation/theatre-director.js +0 -235
  104. package/dist/animation/theatre-object-binding.d.ts +0 -305
  105. package/dist/animation/theatre-object-binding.d.ts.map +0 -1
  106. package/dist/animation/theatre-object-binding.js +0 -351
  107. package/dist/animation/xstate-animation-binding.d.ts +0 -56
  108. package/dist/animation/xstate-animation-binding.d.ts.map +0 -1
  109. package/dist/animation/xstate-animation-binding.js +0 -331
  110. package/dist/animation/xstate-animation-meta.d.ts +0 -266
  111. package/dist/animation/xstate-animation-meta.d.ts.map +0 -1
  112. package/dist/animation/xstate-animation-meta.js +0 -252
  113. package/dist/assets.d.ts +0 -26
  114. package/dist/assets.d.ts.map +0 -1
  115. package/dist/assets.js +0 -55
  116. package/dist/audio/index.d.ts +0 -4
  117. package/dist/audio/index.d.ts.map +0 -1
  118. package/dist/audio/index.js +0 -24
  119. package/dist/audio/tone-clock-binding.d.ts +0 -75
  120. package/dist/audio/tone-clock-binding.d.ts.map +0 -1
  121. package/dist/audio/tone-clock-binding.js +0 -29
  122. package/dist/audio/tone-context.d.ts +0 -114
  123. package/dist/audio/tone-context.d.ts.map +0 -1
  124. package/dist/audio/tone-context.js +0 -39
  125. package/dist/audio/tone-offline-render.d.ts +0 -90
  126. package/dist/audio/tone-offline-render.d.ts.map +0 -1
  127. package/dist/audio/tone-offline-render.js +0 -56
  128. package/dist/audio/wav-encode.d.ts +0 -38
  129. package/dist/audio/wav-encode.d.ts.map +0 -1
  130. package/dist/audio/wav-encode.js +0 -97
  131. package/dist/character/cloth-sim.d.ts +0 -179
  132. package/dist/character/cloth-sim.d.ts.map +0 -1
  133. package/dist/character/cloth-sim.js +0 -334
  134. package/dist/character/spring-chain.d.ts +0 -135
  135. package/dist/character/spring-chain.d.ts.map +0 -1
  136. package/dist/character/spring-chain.js +0 -165
  137. package/dist/core/game-loop.d.ts +0 -42
  138. package/dist/core/game-loop.d.ts.map +0 -1
  139. package/dist/core/game-loop.js +0 -167
  140. package/dist/core/seeded-random.d.ts +0 -69
  141. package/dist/core/seeded-random.d.ts.map +0 -1
  142. package/dist/core/seeded-random.js +0 -121
  143. package/dist/core/system-runner.d.ts +0 -120
  144. package/dist/core/system-runner.d.ts.map +0 -1
  145. package/dist/core/system-runner.js +0 -297
  146. package/dist/core/types.d.ts +0 -92
  147. package/dist/core/types.d.ts.map +0 -1
  148. package/dist/core/types.js +0 -25
  149. package/dist/data/data-asset.d.ts +0 -101
  150. package/dist/data/data-asset.d.ts.map +0 -1
  151. package/dist/data/data-asset.js +0 -122
  152. package/dist/data/data-check-core.d.ts +0 -110
  153. package/dist/data/data-check-core.d.ts.map +0 -1
  154. package/dist/data/data-check-core.js +0 -178
  155. package/dist/data/data-ref.d.ts +0 -79
  156. package/dist/data/data-ref.d.ts.map +0 -1
  157. package/dist/data/data-ref.js +0 -130
  158. package/dist/data/vite-plugin-data.d.ts +0 -79
  159. package/dist/data/vite-plugin-data.d.ts.map +0 -1
  160. package/dist/data/vite-plugin-data.js +0 -217
  161. package/dist/dev/console-bridge.d.ts +0 -22
  162. package/dist/dev/console-bridge.d.ts.map +0 -1
  163. package/dist/dev/console-bridge.js +0 -71
  164. package/dist/dev/debug-draw.d.ts +0 -24
  165. package/dist/dev/debug-draw.d.ts.map +0 -1
  166. package/dist/dev/debug-draw.js +0 -73
  167. package/dist/dev/logger.d.ts +0 -30
  168. package/dist/dev/logger.d.ts.map +0 -1
  169. package/dist/dev/logger.js +0 -90
  170. package/dist/dev/performance-profiler.d.ts +0 -60
  171. package/dist/dev/performance-profiler.d.ts.map +0 -1
  172. package/dist/dev/performance-profiler.js +0 -181
  173. package/dist/dev/webgl-gpu-timer.d.ts +0 -8
  174. package/dist/dev/webgl-gpu-timer.d.ts.map +0 -1
  175. package/dist/dev/webgl-gpu-timer.js +0 -52
  176. package/dist/ecs/component-manager.d.ts +0 -205
  177. package/dist/ecs/component-manager.d.ts.map +0 -1
  178. package/dist/ecs/component-manager.js +0 -706
  179. package/dist/ecs/game-component.d.ts +0 -146
  180. package/dist/ecs/game-component.d.ts.map +0 -1
  181. package/dist/ecs/game-component.js +0 -141
  182. package/dist/ecs/hmr-swap-report.d.ts +0 -53
  183. package/dist/ecs/hmr-swap-report.d.ts.map +0 -1
  184. package/dist/ecs/hmr-swap-report.js +0 -55
  185. package/dist/humanoid/bake.operation.d.ts +0 -119
  186. package/dist/humanoid/bake.operation.d.ts.map +0 -1
  187. package/dist/humanoid/bake.operation.js +0 -228
  188. package/dist/humanoid/body.d.ts +0 -101
  189. package/dist/humanoid/body.d.ts.map +0 -1
  190. package/dist/humanoid/body.js +0 -530
  191. package/dist/humanoid/clips.d.ts +0 -62
  192. package/dist/humanoid/clips.d.ts.map +0 -1
  193. package/dist/humanoid/clips.js +0 -121
  194. package/dist/humanoid/compose.d.ts +0 -75
  195. package/dist/humanoid/compose.d.ts.map +0 -1
  196. package/dist/humanoid/compose.js +0 -176
  197. package/dist/humanoid/generate.d.ts +0 -92
  198. package/dist/humanoid/generate.d.ts.map +0 -1
  199. package/dist/humanoid/generate.js +0 -124
  200. package/dist/humanoid/index.d.ts +0 -24
  201. package/dist/humanoid/index.d.ts.map +0 -1
  202. package/dist/humanoid/index.js +0 -18
  203. package/dist/humanoid/schema.d.ts +0 -35
  204. package/dist/humanoid/schema.d.ts.map +0 -1
  205. package/dist/humanoid/schema.js +0 -87
  206. package/dist/humanoid/skeleton.d.ts +0 -103
  207. package/dist/humanoid/skeleton.d.ts.map +0 -1
  208. package/dist/humanoid/skeleton.js +0 -289
  209. package/dist/index.d.ts +0 -37
  210. package/dist/index.d.ts.map +0 -1
  211. package/dist/index.js +0 -30
  212. package/dist/input/input-manager.d.ts +0 -945
  213. package/dist/input/input-manager.d.ts.map +0 -1
  214. package/dist/input/input-manager.js +0 -2207
  215. package/dist/input/input-types.d.ts +0 -174
  216. package/dist/input/input-types.d.ts.map +0 -1
  217. package/dist/input/input-types.js +0 -1
  218. package/dist/input/prompt-labels.d.ts +0 -21
  219. package/dist/input/prompt-labels.d.ts.map +0 -1
  220. package/dist/input/prompt-labels.js +0 -121
  221. package/dist/input/rebind-controller.d.ts +0 -59
  222. package/dist/input/rebind-controller.d.ts.map +0 -1
  223. package/dist/input/rebind-controller.js +0 -85
  224. package/dist/input/schema.d.ts +0 -16
  225. package/dist/input/schema.d.ts.map +0 -1
  226. package/dist/input/schema.js +0 -228
  227. package/dist/loader.d.ts +0 -43
  228. package/dist/loader.d.ts.map +0 -1
  229. package/dist/loader.js +0 -59
  230. package/dist/manifest/index.d.ts +0 -5
  231. package/dist/manifest/index.d.ts.map +0 -1
  232. package/dist/manifest/index.js +0 -8
  233. package/dist/manifest/load-file.d.ts +0 -4
  234. package/dist/manifest/load-file.d.ts.map +0 -1
  235. package/dist/manifest/load-file.js +0 -14
  236. package/dist/manifest/load.d.ts +0 -144
  237. package/dist/manifest/load.d.ts.map +0 -1
  238. package/dist/manifest/load.js +0 -273
  239. package/dist/manifest/schema.d.ts +0 -230
  240. package/dist/manifest/schema.d.ts.map +0 -1
  241. package/dist/manifest/schema.js +0 -394
  242. package/dist/physics/collision-system.d.ts +0 -41
  243. package/dist/physics/collision-system.d.ts.map +0 -1
  244. package/dist/physics/collision-system.js +0 -64
  245. package/dist/physics/physics-registry.d.ts +0 -45
  246. package/dist/physics/physics-registry.d.ts.map +0 -1
  247. package/dist/physics/physics-registry.js +0 -65
  248. package/dist/physics/transform-writer.d.ts +0 -16
  249. package/dist/physics/transform-writer.d.ts.map +0 -1
  250. package/dist/physics/transform-writer.js +0 -38
  251. package/dist/physics/trigger-dispatch.d.ts +0 -24
  252. package/dist/physics/trigger-dispatch.d.ts.map +0 -1
  253. package/dist/physics/trigger-dispatch.js +0 -80
  254. package/dist/react/game-state.d.ts +0 -156
  255. package/dist/react/game-state.d.ts.map +0 -1
  256. package/dist/react/game-state.js +0 -222
  257. package/dist/react/root-adapter.d.ts +0 -11
  258. package/dist/react/root-adapter.d.ts.map +0 -1
  259. package/dist/react/root-adapter.js +0 -41
  260. package/dist/react/unmanaged-root-detector.d.ts +0 -12
  261. package/dist/react/unmanaged-root-detector.d.ts.map +0 -1
  262. package/dist/react/unmanaged-root-detector.js +0 -45
  263. package/dist/react/use-data.d.ts +0 -60
  264. package/dist/react/use-data.d.ts.map +0 -1
  265. package/dist/react/use-data.js +0 -101
  266. package/dist/react/use-selection.d.ts +0 -75
  267. package/dist/react/use-selection.d.ts.map +0 -1
  268. package/dist/react/use-selection.js +0 -90
  269. package/dist/render/auto-batcher.d.ts +0 -34
  270. package/dist/render/auto-batcher.d.ts.map +0 -1
  271. package/dist/render/auto-batcher.js +0 -140
  272. package/dist/render/render-batch-system.d.ts +0 -33
  273. package/dist/render/render-batch-system.d.ts.map +0 -1
  274. package/dist/render/render-batch-system.js +0 -230
  275. package/dist/render/render-features.d.ts +0 -47
  276. package/dist/render/render-features.d.ts.map +0 -1
  277. package/dist/render/render-features.js +0 -105
  278. package/dist/render/render-settings.d.ts +0 -36
  279. package/dist/render/render-settings.d.ts.map +0 -1
  280. package/dist/render/render-settings.js +0 -51
  281. package/dist/runtime/create-runtime.d.ts +0 -249
  282. package/dist/runtime/create-runtime.d.ts.map +0 -1
  283. package/dist/runtime/create-runtime.js +0 -641
  284. package/dist/runtime/debug-bridge.d.ts +0 -257
  285. package/dist/runtime/debug-bridge.d.ts.map +0 -1
  286. package/dist/runtime/debug-bridge.js +0 -224
  287. package/dist/runtime/debug-registry.d.ts +0 -288
  288. package/dist/runtime/debug-registry.d.ts.map +0 -1
  289. package/dist/runtime/debug-registry.js +0 -513
  290. package/dist/runtime/frame-selector-cache.d.ts +0 -41
  291. package/dist/runtime/frame-selector-cache.d.ts.map +0 -1
  292. package/dist/runtime/frame-selector-cache.js +0 -65
  293. package/dist/runtime/game.d.ts +0 -496
  294. package/dist/runtime/game.d.ts.map +0 -1
  295. package/dist/runtime/game.js +0 -741
  296. package/dist/runtime/gameplay-rng-trap.d.ts +0 -48
  297. package/dist/runtime/gameplay-rng-trap.d.ts.map +0 -1
  298. package/dist/runtime/gameplay-rng-trap.js +0 -96
  299. package/dist/runtime/input-router.d.ts +0 -78
  300. package/dist/runtime/input-router.d.ts.map +0 -1
  301. package/dist/runtime/input-router.js +0 -171
  302. package/dist/runtime/mount-game.d.ts +0 -129
  303. package/dist/runtime/mount-game.d.ts.map +0 -1
  304. package/dist/runtime/mount-game.js +0 -152
  305. package/dist/runtime/mount-manifest.d.ts +0 -173
  306. package/dist/runtime/mount-manifest.d.ts.map +0 -1
  307. package/dist/runtime/mount-manifest.js +0 -297
  308. package/dist/runtime/render-audio-control.d.ts +0 -110
  309. package/dist/runtime/render-audio-control.d.ts.map +0 -1
  310. package/dist/runtime/render-audio-control.js +0 -111
  311. package/dist/runtime/render-control.d.ts +0 -291
  312. package/dist/runtime/render-control.d.ts.map +0 -1
  313. package/dist/runtime/render-control.js +0 -264
  314. package/dist/runtime/render-seed.d.ts +0 -57
  315. package/dist/runtime/render-seed.d.ts.map +0 -1
  316. package/dist/runtime/render-seed.js +0 -74
  317. package/dist/runtime/state-bridge.d.ts +0 -61
  318. package/dist/runtime/state-bridge.d.ts.map +0 -1
  319. package/dist/runtime/state-bridge.js +0 -63
  320. package/dist/runtime/types.d.ts +0 -253
  321. package/dist/runtime/types.d.ts.map +0 -1
  322. package/dist/runtime/types.js +0 -1
  323. package/dist/scene/asset-loaders.d.ts +0 -79
  324. package/dist/scene/asset-loaders.d.ts.map +0 -1
  325. package/dist/scene/asset-loaders.js +0 -141
  326. package/dist/scene/asset-paths.d.ts +0 -11
  327. package/dist/scene/asset-paths.d.ts.map +0 -1
  328. package/dist/scene/asset-paths.js +0 -108
  329. package/dist/scene/asset-ref-check.d.ts +0 -88
  330. package/dist/scene/asset-ref-check.d.ts.map +0 -1
  331. package/dist/scene/asset-ref-check.js +0 -214
  332. package/dist/scene/asset-registry.d.ts +0 -38
  333. package/dist/scene/asset-registry.d.ts.map +0 -1
  334. package/dist/scene/asset-registry.js +0 -66
  335. package/dist/scene/collider-dimensions.d.ts +0 -63
  336. package/dist/scene/collider-dimensions.d.ts.map +0 -1
  337. package/dist/scene/collider-dimensions.js +0 -98
  338. package/dist/scene/component-registry.d.ts +0 -22
  339. package/dist/scene/component-registry.d.ts.map +0 -1
  340. package/dist/scene/component-registry.js +0 -37
  341. package/dist/scene/defaults.d.ts +0 -252
  342. package/dist/scene/defaults.d.ts.map +0 -1
  343. package/dist/scene/defaults.js +0 -151
  344. package/dist/scene/geometries/index.d.ts +0 -8
  345. package/dist/scene/geometries/index.d.ts.map +0 -1
  346. package/dist/scene/geometries/index.js +0 -7
  347. package/dist/scene/geometries/terrain.d.ts +0 -2
  348. package/dist/scene/geometries/terrain.d.ts.map +0 -1
  349. package/dist/scene/geometries/terrain.js +0 -37
  350. package/dist/scene/geometry-registry.d.ts +0 -24
  351. package/dist/scene/geometry-registry.d.ts.map +0 -1
  352. package/dist/scene/geometry-registry.js +0 -20
  353. package/dist/scene/instance-registry.d.ts +0 -36
  354. package/dist/scene/instance-registry.d.ts.map +0 -1
  355. package/dist/scene/instance-registry.js +0 -55
  356. package/dist/scene/instancers/grid.d.ts +0 -2
  357. package/dist/scene/instancers/grid.d.ts.map +0 -1
  358. package/dist/scene/instancers/grid.js +0 -36
  359. package/dist/scene/instancers/index.d.ts +0 -8
  360. package/dist/scene/instancers/index.d.ts.map +0 -1
  361. package/dist/scene/instancers/index.js +0 -7
  362. package/dist/scene/light-camera-factory.d.ts +0 -23
  363. package/dist/scene/light-camera-factory.d.ts.map +0 -1
  364. package/dist/scene/light-camera-factory.js +0 -68
  365. package/dist/scene/material-factory.d.ts +0 -17
  366. package/dist/scene/material-factory.d.ts.map +0 -1
  367. package/dist/scene/material-factory.js +0 -197
  368. package/dist/scene/material-registry.d.ts +0 -40
  369. package/dist/scene/material-registry.d.ts.map +0 -1
  370. package/dist/scene/material-registry.js +0 -44
  371. package/dist/scene/materials/index.d.ts +0 -8
  372. package/dist/scene/materials/index.d.ts.map +0 -1
  373. package/dist/scene/materials/index.js +0 -7
  374. package/dist/scene/materials/water.d.ts +0 -2
  375. package/dist/scene/materials/water.d.ts.map +0 -1
  376. package/dist/scene/materials/water.js +0 -54
  377. package/dist/scene/parse.d.ts +0 -23
  378. package/dist/scene/parse.d.ts.map +0 -1
  379. package/dist/scene/parse.js +0 -179
  380. package/dist/scene/particles-factory.d.ts +0 -33
  381. package/dist/scene/particles-factory.d.ts.map +0 -1
  382. package/dist/scene/particles-factory.js +0 -282
  383. package/dist/scene/scene-apply.d.ts +0 -109
  384. package/dist/scene/scene-apply.d.ts.map +0 -1
  385. package/dist/scene/scene-apply.js +0 -370
  386. package/dist/scene/scene-diff-schema.d.ts +0 -285
  387. package/dist/scene/scene-diff-schema.d.ts.map +0 -1
  388. package/dist/scene/scene-diff-schema.js +0 -92
  389. package/dist/scene/scene-diff-types.d.ts +0 -40
  390. package/dist/scene/scene-diff-types.d.ts.map +0 -1
  391. package/dist/scene/scene-diff-types.js +0 -14
  392. package/dist/scene/scene-loader.d.ts +0 -167
  393. package/dist/scene/scene-loader.d.ts.map +0 -1
  394. package/dist/scene/scene-loader.js +0 -1211
  395. package/dist/scene/scene-query.d.ts +0 -11
  396. package/dist/scene/scene-query.d.ts.map +0 -1
  397. package/dist/scene/scene-query.js +0 -63
  398. package/dist/scene/scene-types.d.ts +0 -10
  399. package/dist/scene/scene-types.d.ts.map +0 -1
  400. package/dist/scene/scene-types.js +0 -8
  401. package/dist/scene/scene-version.d.ts +0 -26
  402. package/dist/scene/scene-version.d.ts.map +0 -1
  403. package/dist/scene/scene-version.js +0 -34
  404. package/dist/scene/schema/animation.d.ts +0 -8
  405. package/dist/scene/schema/animation.d.ts.map +0 -1
  406. package/dist/scene/schema/animation.js +0 -42
  407. package/dist/scene/schema/audio.d.ts +0 -13
  408. package/dist/scene/schema/audio.d.ts.map +0 -1
  409. package/dist/scene/schema/audio.js +0 -22
  410. package/dist/scene/schema/camera.d.ts +0 -28
  411. package/dist/scene/schema/camera.d.ts.map +0 -1
  412. package/dist/scene/schema/camera.js +0 -17
  413. package/dist/scene/schema/collider.d.ts +0 -40
  414. package/dist/scene/schema/collider.d.ts.map +0 -1
  415. package/dist/scene/schema/collider.js +0 -55
  416. package/dist/scene/schema/entity-ref.d.ts +0 -31
  417. package/dist/scene/schema/entity-ref.d.ts.map +0 -1
  418. package/dist/scene/schema/entity-ref.js +0 -61
  419. package/dist/scene/schema/entity.d.ts +0 -1036
  420. package/dist/scene/schema/entity.d.ts.map +0 -1
  421. package/dist/scene/schema/entity.js +0 -153
  422. package/dist/scene/schema/environment.d.ts +0 -425
  423. package/dist/scene/schema/environment.d.ts.map +0 -1
  424. package/dist/scene/schema/environment.js +0 -357
  425. package/dist/scene/schema/index.d.ts +0 -37
  426. package/dist/scene/schema/index.d.ts.map +0 -1
  427. package/dist/scene/schema/index.js +0 -19
  428. package/dist/scene/schema/instances.d.ts +0 -14
  429. package/dist/scene/schema/instances.d.ts.map +0 -1
  430. package/dist/scene/schema/instances.js +0 -28
  431. package/dist/scene/schema/joint.d.ts +0 -23
  432. package/dist/scene/schema/joint.d.ts.map +0 -1
  433. package/dist/scene/schema/joint.js +0 -23
  434. package/dist/scene/schema/light.d.ts +0 -38
  435. package/dist/scene/schema/light.d.ts.map +0 -1
  436. package/dist/scene/schema/light.js +0 -26
  437. package/dist/scene/schema/material.d.ts +0 -199
  438. package/dist/scene/schema/material.d.ts.map +0 -1
  439. package/dist/scene/schema/material.js +0 -103
  440. package/dist/scene/schema/mesh.d.ts +0 -48
  441. package/dist/scene/schema/mesh.d.ts.map +0 -1
  442. package/dist/scene/schema/mesh.js +0 -82
  443. package/dist/scene/schema/particles.d.ts +0 -2178
  444. package/dist/scene/schema/particles.d.ts.map +0 -1
  445. package/dist/scene/schema/particles.js +0 -321
  446. package/dist/scene/schema/physics.d.ts +0 -56
  447. package/dist/scene/schema/physics.d.ts.map +0 -1
  448. package/dist/scene/schema/physics.js +0 -37
  449. package/dist/scene/schema/scene-file.d.ts +0 -303
  450. package/dist/scene/schema/scene-file.d.ts.map +0 -1
  451. package/dist/scene/schema/scene-file.js +0 -256
  452. package/dist/scene/schema/shadow.d.ts +0 -28
  453. package/dist/scene/schema/shadow.d.ts.map +0 -1
  454. package/dist/scene/schema/shadow.js +0 -20
  455. package/dist/scene/schema/spline.d.ts +0 -25
  456. package/dist/scene/schema/spline.d.ts.map +0 -1
  457. package/dist/scene/schema/spline.js +0 -15
  458. package/dist/scene/schema/tuples.d.ts +0 -12
  459. package/dist/scene/schema/tuples.d.ts.map +0 -1
  460. package/dist/scene/schema/tuples.js +0 -14
  461. package/dist/scene/user-data.d.ts +0 -185
  462. package/dist/scene/user-data.d.ts.map +0 -1
  463. package/dist/scene/user-data.js +0 -144
  464. package/dist/setup/setup-audio.d.ts +0 -20
  465. package/dist/setup/setup-audio.d.ts.map +0 -1
  466. package/dist/setup/setup-audio.js +0 -41
  467. package/dist/setup/setup-particles.d.ts +0 -17
  468. package/dist/setup/setup-particles.d.ts.map +0 -1
  469. package/dist/setup/setup-particles.js +0 -16
  470. package/dist/setup/setup-physics.d.ts +0 -25
  471. package/dist/setup/setup-physics.d.ts.map +0 -1
  472. package/dist/setup/setup-physics.js +0 -51
  473. package/dist/setup/setup-renderer.d.ts +0 -69
  474. package/dist/setup/setup-renderer.d.ts.map +0 -1
  475. package/dist/setup/setup-renderer.js +0 -363
  476. package/dist/tools/define-tool.d.ts +0 -130
  477. package/dist/tools/define-tool.d.ts.map +0 -1
  478. package/dist/tools/define-tool.js +0 -96
  479. package/dist/world2d/authoring-2d.d.ts +0 -80
  480. package/dist/world2d/authoring-2d.d.ts.map +0 -1
  481. package/dist/world2d/authoring-2d.js +0 -195
  482. package/dist/world2d/capture-to-scene2d.d.ts +0 -5
  483. package/dist/world2d/capture-to-scene2d.d.ts.map +0 -1
  484. package/dist/world2d/capture-to-scene2d.js +0 -49
  485. package/dist/world2d/collision-2d.d.ts +0 -52
  486. package/dist/world2d/collision-2d.d.ts.map +0 -1
  487. package/dist/world2d/collision-2d.js +0 -70
  488. package/dist/world2d/components-2d.d.ts +0 -47
  489. package/dist/world2d/components-2d.d.ts.map +0 -1
  490. package/dist/world2d/components-2d.js +0 -77
  491. package/dist/world2d/index.d.ts +0 -34
  492. package/dist/world2d/index.d.ts.map +0 -1
  493. package/dist/world2d/index.js +0 -32
  494. package/dist/world2d/ingest-iframe-2d.d.ts +0 -82
  495. package/dist/world2d/ingest-iframe-2d.d.ts.map +0 -1
  496. package/dist/world2d/ingest-iframe-2d.js +0 -166
  497. package/dist/world2d/ingest2d.d.ts +0 -97
  498. package/dist/world2d/ingest2d.d.ts.map +0 -1
  499. package/dist/world2d/ingest2d.js +0 -40
  500. package/dist/world2d/physics2d-registry.d.ts +0 -24
  501. package/dist/world2d/physics2d-registry.d.ts.map +0 -1
  502. package/dist/world2d/physics2d-registry.js +0 -38
  503. package/dist/world2d/pixi-game-adapter.d.ts +0 -107
  504. package/dist/world2d/pixi-game-adapter.d.ts.map +0 -1
  505. package/dist/world2d/pixi-game-adapter.js +0 -210
  506. package/dist/world2d/pixi-surface.d.ts +0 -39
  507. package/dist/world2d/pixi-surface.d.ts.map +0 -1
  508. package/dist/world2d/pixi-surface.js +0 -47
  509. package/dist/world2d/scene-capture-2d.d.ts +0 -43
  510. package/dist/world2d/scene-capture-2d.d.ts.map +0 -1
  511. package/dist/world2d/scene-capture-2d.js +0 -92
  512. package/dist/world2d/scene2d-loader.d.ts +0 -27
  513. package/dist/world2d/scene2d-loader.d.ts.map +0 -1
  514. package/dist/world2d/scene2d-loader.js +0 -275
  515. package/dist/world2d/schema/entity2d.d.ts +0 -102
  516. package/dist/world2d/schema/entity2d.d.ts.map +0 -1
  517. package/dist/world2d/schema/entity2d.js +0 -119
  518. package/dist/world2d/schema/physics2d.d.ts +0 -46
  519. package/dist/world2d/schema/physics2d.d.ts.map +0 -1
  520. package/dist/world2d/schema/physics2d.js +0 -45
  521. package/dist/world2d/schema/sprite.d.ts +0 -42
  522. package/dist/world2d/schema/sprite.d.ts.map +0 -1
  523. package/dist/world2d/schema/sprite.js +0 -63
  524. package/dist/world2d/schema/tilemap.d.ts +0 -15
  525. package/dist/world2d/schema/tilemap.d.ts.map +0 -1
  526. package/dist/world2d/schema/tilemap.js +0 -19
  527. package/dist/world2d/schema/tuples2d.d.ts +0 -20
  528. package/dist/world2d/schema/tuples2d.d.ts.map +0 -1
  529. package/dist/world2d/schema/tuples2d.js +0 -19
  530. package/dist/world2d/system-adapters-2d.d.ts +0 -16
  531. package/dist/world2d/system-adapters-2d.d.ts.map +0 -1
  532. package/dist/world2d/system-adapters-2d.js +0 -37
  533. package/dist/world2d/transform-writer-2d.d.ts +0 -16
  534. package/dist/world2d/transform-writer-2d.d.ts.map +0 -1
  535. package/dist/world2d/transform-writer-2d.js +0 -22
  536. package/dist/world2d/types.d.ts +0 -59
  537. package/dist/world2d/types.d.ts.map +0 -1
  538. package/dist/world2d/types.js +0 -1
@@ -1,2207 +0,0 @@
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
- * Thrown by `InputManager.setVirtualAction`/`tapVirtualAction` (Task 1.4,
22
- * ACCEPTANCE-DRIVER-BUILD-PLAN.md / SYNTHETIC-PLAYER-SPEC.md §3.2) for an
23
- * action name that was never `registerAction`/`loadMap`-ed. Carries a
24
- * machine-readable `code` and `data.registered` (every declared action name)
25
- * rather than requiring a caller to parse the message.
26
- */
27
- export class InputActionError extends Error {
28
- code = 'INPUT_ACTION_NOT_FOUND';
29
- data;
30
- constructor(action, method, registered) {
31
- super(`InputManager.${method}: unknown action "${action}". Registered actions: ` +
32
- (registered.length ? registered.join(', ') : '(none)'));
33
- this.name = 'InputActionError';
34
- this.data = { registered };
35
- }
36
- }
37
- /**
38
- * Thrown by `InputManager.scheduleActionAtTick` (D15/T-D15.5,
39
- * docs/D15-DETERMINISM-DESIGN.md §2.c) when the named `tick` has already
40
- * elapsed — scheduling only ever applies to the CURRENT or a future tick,
41
- * never one already serviced by `poll()`. Carries `data.currentTick` — the
42
- * NEXT tick `poll()` will service (not the tick that just elapsed) — so a
43
- * caller can retry by scheduling for `data.currentTick` or later.
44
- */
45
- export class InputTickError extends Error {
46
- code = 'TICK_ALREADY_PASSED';
47
- data;
48
- constructor(tick, currentTick) {
49
- super(`InputManager.scheduleActionAtTick: tick ${tick} has already passed (next tick to be ` +
50
- `serviced: ${currentTick}) — scheduling only applies to the current or a future tick`);
51
- this.name = 'InputTickError';
52
- this.data = { currentTick };
53
- }
54
- }
55
- /** Shallow equality for a resolved action value (boolean/number/Vector2) —
56
- * D15/T-D15.5's recording tap uses this to emit DELTAS only (the trace
57
- * format's "deltas only" line), never a full snapshot every tick. `prev`
58
- * is `undefined` for an action never recorded before (always "changed"). */
59
- function actionValuesEqual(prev, next) {
60
- if (prev === undefined)
61
- return false;
62
- if (typeof prev !== typeof next)
63
- return false;
64
- if (typeof prev === 'object' && typeof next === 'object') {
65
- return prev.x === next.x && prev.y === next.y;
66
- }
67
- return prev === next;
68
- }
69
- /** Cap on {@link InputManager}'s post-gate input-trace ring (D15/T-D15.5) —
70
- * mirrors `debug-registry.ts`'s own `RING_CAP` precedent for the debug event
71
- * ring: bounds memory for a recording session a caller forgot to stop. */
72
- const INPUT_TRACE_CAP = 2000;
73
- /**
74
- * Maps raw keyboard/mouse/gamepad events → named actions.
75
- *
76
- * Usage:
77
- * const input = new InputManager();
78
- * await input.loadMap('inputmaps/default.inputmap.json');
79
- * // In game loop:
80
- * input.poll(); // call at start of frame
81
- * if (input.isPressed('jump')) { ... }
82
- * if (input.isJustPressed('attack')) { ... }
83
- * input.endFrame(); // call at end of frame
84
- *
85
- * F1 (spec §12 "Define Typed Action Values") — typed action values:
86
- *
87
- * Every action DECLARES a value shape (`InputAction.valueType`, defaulted to
88
- * `'digital'` — see `input-types.ts`'s `ActionValueType`). Each shape has its
89
- * own typed getter, none of which return `any`:
90
- *
91
- * - `'digital'` — `isPressed`/`isJustPressed`/`isJustReleased` -> `boolean`
92
- * - `'scalar'` — `getScalar` -> `number`
93
- * - `'vector2'` — `getVector2` -> `Vector2` ({x,y})
94
- * - `'pointerDelta'` — `getPointerDelta` -> `Vector2`
95
- * - `'pointerPosition'` — `getPointerPosition` -> `Vector2`
96
- *
97
- * Reading an action through the WRONG getter (e.g. `getScalar` on a
98
- * `'digital'` action) throws — that's the type-safety enforcement point,
99
- * since actions are declared in data (JSON), not TS types, so there's no
100
- * compiler backstop otherwise. `readAction(name, expectedType)` is a generic
101
- * entry point over the same getters, typed via `ActionValueOf<T>`.
102
- *
103
- * Each typed getter has a `*Source` sibling (e.g. `getScalarSource`) that
104
- * returns `ActionValueSource | null` — which binding (and, for gamepad
105
- * bindings, which connected gamepad, or for injected test bindings, which
106
- * named source) produced the current value. That's the "device/source
107
- * metadata" AC.
108
- *
109
- * Deadzone + normalization:
110
- * - scalar: below `deadzone` magnitude -> 0; at/above it, rescaled from 0
111
- * (at the deadzone edge) to ±1 (at |raw| = 1), sign preserved
112
- * (`rescaleScalar`).
113
- * - vector2: below `deadzone` magnitude -> {0,0}; at/above it, direction is
114
- * preserved and magnitude is rescaled the same way, then the result is
115
- * clamped to the unit circle — magnitude never exceeds 1 even for a raw
116
- * diagonal input like {1,1} (`rescaleVector2`).
117
- *
118
- * Simultaneous-binding combine rules (documented here, spec §12 F1
119
- * "simultaneous bindings combine predictably"):
120
- * - digital: OR — any one satisfied binding makes the action pressed
121
- * (pre-existing behavior, unchanged).
122
- * - scalar: max-magnitude across contributing bindings' (deadzone-applied)
123
- * values, sign preserved; ties keep the first-listed binding
124
- * (deterministic given binding array order).
125
- * - vector2: sum contributing (deadzone-applied) {x,y} values component-
126
- * wise, then clamp the sum to the unit circle (never renormalized UP —
127
- * only ever scaled down if the sum exceeds magnitude 1).
128
- * - pointerDelta: sum contributing deltas (matches the existing
129
- * mouseDeltaX/Y + lookStick accumulation precedent below).
130
- * - pointerPosition: last-write-wins — the contributing source most
131
- * recently updated (by injection call order) wins; position isn't
132
- * additive so summing would be meaningless.
133
- *
134
- * Synthetic input injection (test hook, also the shape of F2's "injected
135
- * test input" backend): `injectAxis`/`injectVector2`/`injectPointerDelta`/
136
- * `injectPointerPosition` push a raw value into a named source, read back by
137
- * binding an action to `{ type: 'test_axis' | 'test_vector2' |
138
- * 'test_pointer_delta' | 'test_pointer_position', sourceId }` — the exact
139
- * same aggregation path (deadzone/normalize/combine) real device bindings
140
- * use.
141
- *
142
- * F2 (spec §12 "Complete Device Backends") completes the real-device side of
143
- * that same typed-value path:
144
- *
145
- * - `mouse_move` — un-rejected (schema.ts). Feeds `getPointerDelta` (raw
146
- * accumulated `mouseDeltaX/Y`, no deadzone) and `getVector2` (the same
147
- * delta, deadzone-rescaled + unit-circle-clamped).
148
- * - `gamepad_axis_pair` — un-rejected (schema.ts). Feeds `getVector2` with
149
- * the coupled `(xAxis, yAxis)` reading, deadzone-rescaled like a real
150
- * stick (default deadzone 0.15, matching `gamepad_axis`).
151
- * - `touch_button`/`touch_stick` — new binding kinds for touch/virtual
152
- * controls. `setTouchButton(sourceId, pressed)` feeds a digital action
153
- * (edge-tracked like `mouse_button`); `setTouchStick(sourceId, value)`
154
- * feeds a vector2 action (deadzone-rescaled like a gamepad stick). These
155
- * are the injectable entry points a real on-screen touch UI's own
156
- * touchstart/touchmove/touchend handlers would call — that DOM wiring is
157
- * a thin adapter at the edge (browser-verified separately, e.g. F4/e2e);
158
- * the mapping logic here is unit-tested headlessly with no DOM.
159
- * - Focus gating — `blur`/`focus` on `window` (guarded, real DOM only) now
160
- * drop input the same way `setEnabled(false)` does (flushing held
161
- * key/mouse/touch-button state) without touching the `enabled` flag the
162
- * editor drives — see `focused`/`inputActive` below. This is what stops
163
- * a stuck key/button when the user alt-tabs away mid-press.
164
- * - Pointer lock — `pointerlockchange` on `document` (guarded) tracks
165
- * `isPointerLocked()`; losing lock flushes mouse-button state (a click
166
- * held at the moment lock exits must not stick).
167
- * - Gamepad device identity — `getGamepadInfo(index)` exposes `{index, id,
168
- * mapping, connected}`; `ActionValueSource.gamepadId` (in addition to the
169
- * pre-existing `gamepadIndex`) is populated for every
170
- * gamepad_button/gamepad_axis/gamepad_axis_pair source.
171
- * - Disconnection/reconnection needs no new tracking: `poll()` already
172
- * re-reads `navigator.getGamepads()` every frame and every read (digital
173
- * edges, scalar/vector2 contributions) already skips a `null` gamepad
174
- * entry, so a disconnected gamepad's bound actions go neutral (and a
175
- * previously-held button correctly fires `isJustReleased`) with no
176
- * special-cased code — see the state-transition tests.
177
- *
178
- * F3 (spec §12 "Add Rebinding and Prompts") completes the input system with
179
- * rebinding, structural conflict detection, and device-prompt resolution:
180
- *
181
- * - Listing/mutating: `getBindings`/`setBindings`/`addBinding`/
182
- * `removeBinding`/`replaceBinding` — the same `this.actions` map F1/F2
183
- * already read, now with a public read + mutation surface (there was
184
- * previously none — every existing call site either loaded a map once or
185
- * read through the typed getters).
186
- * - Reset-to-defaults: `registerAction`/`loadMap`/`loadMapObject` snapshot
187
- * each action's bindings into `defaultBindings` (a deep clone) at
188
- * registration/load time; `resetBindings`/`resetAllBindings` restore from
189
- * that snapshot. Later rebinding calls do NOT touch the snapshot, so
190
- * "reset" always means "back to what was authored/loaded", not "back to
191
- * the last reset".
192
- * - Persist: `toInputMapFile()` serializes the live action set back into
193
- * the exact `.inputmap.json` shape (`InputMapFile`) `loadMap` reads;
194
- * `loadMapObject(data)` is `loadMap`'s synchronous, no-`fetch` sibling —
195
- * both funnel through the same `InputMapFileSchema`-validated
196
- * `applyParsedMap`. This is the round-trip seam: serialize, write/hand to
197
- * something else, reload, and rebound actions survive. It complements —
198
- * doesn't replace — the SDK's `project.inputMap.read/validate/update`
199
- * operations (B2, `packages/vgai-sdk/src/project/input-map-operations.ts`):
200
- * those are FILE-level (a CLI/agent editing the `.inputmap.json` on disk,
201
- * no running game involved); this is RUNTIME-level (a live game rebinding
202
- * while playing). Both validate through the identical
203
- * `InputMapFileSchema`, so a file B2 wrote loads here unchanged, and a
204
- * map this class serializes is a valid document for B2 to read back.
205
- * - Conflicts: `findConflicts(proposed, excludeActionName?)` scans every
206
- * OTHER registered action's bindings for one that's structurally
207
- * equivalent to `proposed` (see the private `bindingsCollide` — same
208
- * physical input identity: same key code, same mouse button, the same
209
- * gamepad button/axis/axis-pair, the same touch `sourceId`, etc.) and
210
- * returns the (possibly empty) list as plain `BindingConflict` data —
211
- * never throws. A rebinding UI calls this BEFORE calling
212
- * `addBinding`/`replaceBinding`/`setBindings` and decides what to do with
213
- * a non-empty result (block, warn, let the user steal the binding by
214
- * removing it from the other action first).
215
- * - Prompts: `getPrompt(actionName, device?, gamepadIndex?)` resolves a
216
- * `BindingPrompt` (display label + icon name) for whichever of the
217
- * action's bindings belongs to the requested `PromptDevice` family
218
- * (keyboard/mouse/gamepad/touch — `bindingDeviceFamily` in
219
- * `prompt-labels.ts`), using `getGamepadInfo` to prefer the standard
220
- * mapping's canonical glyph names (`'A'`, `'LB'`, ...) for gamepad
221
- * buttons and falling back to a raw index label (or a "disconnected"
222
- * label) when the pad isn't in standard mapping or isn't connected.
223
- * `device` is optional (F4 below) — omit it to resolve against
224
- * `getLastActiveDevice()` instead.
225
- *
226
- * F4 (spec §12 "Schema and Examples") closes F3's one open gap — a
227
- * current-ACTIVE-device tracker, so a prompt UI can switch its displayed
228
- * label as the player switches controllers instead of the caller having to
229
- * track that itself:
230
- *
231
- * - `getLastActiveDevice()` returns the `PromptDevice` family of whichever
232
- * input arrived most recently — keyboard on `keydown`, mouse on
233
- * `mousedown`/a real `mousemove`, gamepad on a button press or a stick
234
- * pushed meaningfully off-center (checked in `poll()`), touch on
235
- * `setTouchButton(_, true)` or `setTouchStick` pushed meaningfully
236
- * off-center. `null` before any input has arrived.
237
- * - `getPrompt(actionName)` (device omitted) resolves against it directly,
238
- * falling back to `'keyboard'` before any input has arrived — a HUD can
239
- * call it every frame with no extra bookkeeping and the label switches on
240
- * its own.
241
- */
242
- export class InputManager {
243
- actions = new Map();
244
- // F1 — each registered/loaded action's declared value shape (defaults to
245
- // 'digital' — see registerAction/loadMap). Kept as a sibling map rather than
246
- // folded into `actions` so the pre-F1 `Map<string, InputBinding[]>` shape
247
- // (and every existing direct read of it, e.g. in tests) stays unchanged.
248
- actionValueTypes = new Map();
249
- // F3 — a deep-cloned snapshot of each action's bindings AT REGISTRATION/LOAD
250
- // TIME (registerAction/loadMap/loadMapObject), restored by
251
- // resetBindings/resetAllBindings. Later rebinding calls (setBindings/
252
- // addBinding/removeBinding/replaceBinding) deliberately do not touch this —
253
- // "reset" always means "back to the originally authored/loaded defaults".
254
- defaultBindings = new Map();
255
- // F3 — the `version` field of the last `loadMap`/`loadMapObject`-ed
256
- // document, carried forward by `toInputMapFile()` so a round-trip
257
- // (serialize -> reload) preserves it. Defaults to 1 for a manager built
258
- // purely via `registerAction` (no file was ever loaded).
259
- loadedMapVersion = 1;
260
- keysDown = new Set();
261
- keysJustDown = new Set();
262
- keysJustUp = new Set();
263
- mouseButtons = new Set();
264
- mouseButtonsJustDown = new Set();
265
- mouseButtonsJustUp = new Set();
266
- mouseDeltaX = 0;
267
- mouseDeltaY = 0;
268
- gamepads = [];
269
- // Gamepad button edge tracking (P1.6c). Keys are `${gamepadIndex}:${buttonIndex}`.
270
- // `Prev` is last frame's pressed set; `Down` is this frame's; the just-down/up
271
- // sets are the per-frame diff computed in poll() and cleared in endFrame().
272
- gamepadButtonsDown = new Set();
273
- gamepadButtonsPrev = new Set();
274
- gamepadButtonsJustDown = new Set();
275
- gamepadButtonsJustUp = new Set();
276
- // Gamepad axis edge tracking: raw per-(gamepad,axis) values, this frame vs
277
- // last frame, so `gamepad_axis` bindings can compute a thresholded
278
- // (direction + deadzone) boolean for THIS frame and compare it against the
279
- // same threshold applied to last frame's raw value — giving real
280
- // isJustPressed/isJustReleased edges for an otherwise-continuous input.
281
- // Keys are `${gamepadIndex}:${axisIndex}`; `Prev` is carried forward in
282
- // endFrame(), mirroring the button edge-tracking above.
283
- gamepadAxesCurrent = new Map();
284
- gamepadAxesPrev = new Map();
285
- lookStickX = 0;
286
- lookStickY = 0;
287
- // F1 — injected-test-input raw value stores, keyed by caller-chosen
288
- // `sourceId` (see the class doc comment's "Synthetic input injection"
289
- // section). Axis/Vector2/pointer-position are LEVEL values (persist across
290
- // frames until changed, mirroring a real analog stick/gamepad axis —
291
- // there's no hardware poll to naturally refresh them each frame); pointer
292
- // delta is a per-frame accumulator (cleared in endFrame(), mirroring
293
- // mouseDeltaX/Y above).
294
- testAxisValues = new Map();
295
- testVector2Values = new Map();
296
- testPointerDeltaAccum = new Map();
297
- testPointerPositionValues = new Map();
298
- // Monotonic counter stamped onto each injectPointerPosition call so the
299
- // pointerPosition combine rule (last-write-wins) can tell which of several
300
- // contributing sources was written most recently within/across frames.
301
- pointerPositionSeq = 0;
302
- // F2 — touch/virtual-control state, keyed by caller-chosen `sourceId` (the
303
- // touch UI's own zone/knob id — see setTouchButton/setTouchStick). Buttons
304
- // mirror the mouse-button edge-tracking shape (Down/JustDown/JustUp sets);
305
- // sticks are LEVEL values like a gamepad axis (persist until changed).
306
- touchButtonsDown = new Set();
307
- touchButtonsJustDown = new Set();
308
- touchButtonsJustUp = new Set();
309
- touchStickValues = new Map();
310
- // F4 (spec §12 "Schema and Examples" — the F3 prompt-switching gap) — which
311
- // device family most recently produced REAL input, so a prompt UI can
312
- // switch its displayed label ('Space' -> 'A' -> a touch control's name) as
313
- // the player switches controllers, without the caller having to guess or
314
- // track this itself. Updated at the same points raw input arrives: keydown
315
- // (`'keyboard'`), mousedown/mousemove (`'mouse'`), a gamepad button press or
316
- // a stick pushed meaningfully past rest (`'gamepad'`, both detected in
317
- // `poll()` — a raw resting stick reports small non-zero noise on some
318
- // pads, so this deliberately checks a coarse magnitude, not "any non-zero
319
- // axis"), and a touch button press / stick pushed past rest
320
- // (`'touch'` — `setTouchButton`/`setTouchStick`). `null` until the very
321
- // first input of any kind arrives. See `getLastActiveDevice`/`getPrompt`.
322
- lastActiveDevice = null;
323
- /** Coarse "is this stick meaningfully off-center" check shared by the
324
- * gamepad and touch-stick active-device heuristics above — deliberately a
325
- * higher bar than a binding's own (often much smaller) configured
326
- * deadzone, since this is "did the player just grab this control", not
327
- * "does this binding's value count as non-zero". */
328
- static isStickActive(v) {
329
- return Math.hypot(v.x, v.y) > 0.3;
330
- }
331
- // Task 1.4 (ACCEPTANCE-DRIVER-BUILD-PLAN.md) — action-level virtual input
332
- // for a synthetic player, promoted from hollowstone's `VirtualInput`. Held
333
- // digitals OR into isPressed alongside binding contributions (like
334
- // touchButtonsDown above); tap queues, then is promoted to "active" for
335
- // exactly one poll()/endFrame() bracket (see poll()/endFrame() below,
336
- // mirroring hollowstone's queued/beginFrame contract); scalar/vector2 are
337
- // level values feeding the same max-magnitude/sum combine every real
338
- // binding uses (collectScalarContributions/collectVector2Contributions).
339
- virtualDigitalHeld = new Set();
340
- virtualDigitalTapQueued = new Set();
341
- virtualDigitalTapActive = new Set();
342
- /** pulse-runner friction #4 — the release-edge mirror of
343
- * `virtualDigitalTapActive`: a direct `setVirtualAction(action, false)` (or
344
- * `clearVirtualActions()`) on an action that WAS held writes here so
345
- * `isJustReleased` fires exactly once, mirroring real `onKeyUp`'s
346
- * immediate `keysJustUp` write (no queue/promotion needed — unlike the
347
- * press edge, nothing in `poll()` reassigns this set out from under a
348
- * write that lands before `poll()` runs). Cleared every `endFrame()`,
349
- * same bracket as every other just-* edge set. */
350
- virtualDigitalJustReleased = new Set();
351
- virtualScalarValues = new Map();
352
- virtualVector2Values = new Map();
353
- // D15/T-D15.5 (docs/D15-DETERMINISM-DESIGN.md §2.c) — tick-indexed input
354
- // scheduling + post-gate recording.
355
- //
356
- // `currentTick` is a FALLBACK, self-incrementing counter, used only when a
357
- // caller never tells `poll()` which tick it's servicing (every existing
358
- // bare/headless caller — tests, a mount with no `Game` shell behind it —
359
- // keeps this exact pre-existing behavior, zero regression). The REAL
360
- // per-world wiring (`vgai-scene-game-adapter.ts`) instead passes the
361
- // shared `Game`-level tick counter into every `poll(tick)` call — this is
362
- // deliberate: an InputManager-local counter drifts from the actual game
363
- // tick for a paused/frozen world (its `poll()` isn't called every game
364
- // tick, so a local increment-per-call counter undercounts) — see the
365
- // `lastServicedTick` gap-handling below, which structurally can't drift
366
- // because it's driven by whatever tick value the CALLER (ultimately the
367
- // shared `Game` tick) actually supplies.
368
- currentTick = 0;
369
- /** The last tick value `poll()` actually serviced, or `-1` before the
370
- * first call. Used to detect a GAP (this world's input phase didn't run
371
- * for one or more intervening ticks — e.g. frozen/paused while other
372
- * roots kept advancing the shared game tick) so any schedule entries
373
- * inside that gap can be dropped (with an observable event) instead of
374
- * silently rotting in {@link scheduledActions} forever. */
375
- lastServicedTick = -1;
376
- /** Pending `scheduleActionAtTick` actuations, grouped by their exact target
377
- * tick. Applied (and removed) at the START of that tick's `poll()` — see
378
- * `applyScheduledActionsForTick`. A tick whose input phase is skipped
379
- * entirely (this world paused/frozen, or a gap between two serviced
380
- * ticks) never reaches it — its scheduled entries are dropped, each
381
- * emitting one `'input.schedule.dropped'` debug event (`{tick, action}`)
382
- * via {@link setDebugEmit}'s sink if one is wired — NOT retried on a
383
- * later tick (mirrors `clearVirtualActions`-on-gate-close: no stuck
384
- * actuation ever silently fires late). */
385
- scheduledActions = new Map();
386
- /** Optional sink for the `'input.schedule.dropped'` debug event (`{tick,
387
- * action}`) emitted whenever a scheduled entry is discarded because its
388
- * target tick's input phase never ran. Wired by whoever constructs this
389
- * `InputManager` with access to a `DebugRegistry`
390
- * (`vgai-scene-game-adapter.ts`, the same seed spot as
391
- * `setVirtualInputTarget`/`setInputActionsSource`) — `null` (the
392
- * default) for a bare/headless `InputManager`, in which case a drop
393
- * stays silent (matching pre-D15/T-D15.5 behavior). */
394
- debugEmit = null;
395
- /** D15/T-D15.5 recording toggle — see {@link startInputRecording}. */
396
- inputRecording = false;
397
- /** The post-gate action-delta trace (§2.c's format sketch) — capped so an
398
- * accidentally-long recording session can't grow this unboundedly. */
399
- inputTrace = [];
400
- /** Last value recorded per action, so `recordPostGateTick` only ever
401
- * appends a DELTA (the format sketch's "deltas only" line), not a full
402
- * snapshot every tick. Cleared on `startInputRecording()`. */
403
- lastTraceSnapshot = new Map();
404
- disposed = false;
405
- /**
406
- * When false, all raw input is ignored and held/transient state is cleared.
407
- * The editor host drives this (T6.3, `packages/editor/src/play-mode.ts`) so
408
- * the running game only receives input while its viewport is the active,
409
- * focused surface — keystrokes typed into the editor (Scene tab, inspector
410
- * fields) must not leak into the game. A standalone game leaves this true
411
- * for its whole lifetime.
412
- */
413
- enabled = true;
414
- /**
415
- * F2 — real window focus, distinct from `enabled` (which is the editor's
416
- * explicit play-mode gate). `false` while the tab/window is blurred (real
417
- * `blur`/`focus` events — see the constructor). Held keyboard/mouse/touch
418
- * state is flushed on blur so a key/button held at alt-tab time doesn't
419
- * stick (the browser stops delivering keyup/mouseup while blurred).
420
- */
421
- focused = true;
422
- /** Both gates a standalone game must pass for input to be live (spec §12
423
- * F2 "focus gating"): the editor's explicit `enabled` flag AND real
424
- * window focus. Used everywhere `enabled` alone used to be checked. */
425
- get inputActive() {
426
- return this.enabled && this.focused;
427
- }
428
- /** #144 — the gate MACHINE input passes (virtual actions and
429
- * tick-scheduled actuations): the editor's explicit `enabled` flag ONLY.
430
- * Window focus deliberately does not participate: a synthetic player
431
- * drives the shared editor tab precisely while the human's focus is
432
- * elsewhere (their terminal, another window) — the first live in-editor
433
- * probe run froze mid-suite the moment the owner started typing to their
434
- * agent, which is the bug this gate split fixes. Real-device input keeps
435
- * the stricter `inputActive` (enabled AND focused) gate above. */
436
- get machineInputActive() {
437
- return this.enabled;
438
- }
439
- /** F2 — real pointer-lock state (`document.pointerLockElement`), tracked via
440
- * `pointerlockchange` (see the constructor). */
441
- pointerLocked = false;
442
- /** Element currently wired for click-to-pointer-lock, and its listener. */
443
- pointerLockElement = null;
444
- onPointerLockClick = () => {
445
- this.pointerLockElement?.requestPointerLock();
446
- };
447
- onPointerLockChange = () => {
448
- const doc = typeof document !== 'undefined' ? document : null;
449
- const wasLocked = this.pointerLocked;
450
- this.pointerLocked =
451
- !!this.pointerLockElement && doc?.pointerLockElement === this.pointerLockElement;
452
- // Losing lock mid-click must not leave a mouse button stuck down — the
453
- // browser may not deliver a mouseup when lock exits (e.g. Escape).
454
- if (wasLocked && !this.pointerLocked) {
455
- this.mouseButtons.clear();
456
- this.mouseButtonsJustDown.clear();
457
- this.mouseButtonsJustUp.clear();
458
- }
459
- };
460
- onKeyDown = (e) => {
461
- // Ignore game input while suspended, unfocused, or while the user is typing
462
- // into a text field (e.g. renaming an entity / editing an inspector value
463
- // in the editor).
464
- if (!this.inputActive || isTextEntryFocused())
465
- return;
466
- this.lastActiveDevice = 'keyboard';
467
- if (!this.keysDown.has(e.code)) {
468
- this.keysJustDown.add(e.code);
469
- }
470
- this.keysDown.add(e.code);
471
- };
472
- onKeyUp = (e) => {
473
- if (!this.inputActive)
474
- return;
475
- this.keysDown.delete(e.code);
476
- this.keysJustUp.add(e.code);
477
- };
478
- onMouseDown = (e) => {
479
- if (!this.inputActive)
480
- return;
481
- this.lastActiveDevice = 'mouse';
482
- this.mouseButtons.add(e.button);
483
- this.mouseButtonsJustDown.add(e.button);
484
- };
485
- onMouseUp = (e) => {
486
- if (!this.inputActive)
487
- return;
488
- this.mouseButtons.delete(e.button);
489
- this.mouseButtonsJustUp.add(e.button);
490
- };
491
- onMouseMove = (e) => {
492
- if (!this.inputActive)
493
- return;
494
- if (e.movementX !== 0 || e.movementY !== 0)
495
- this.lastActiveDevice = 'mouse';
496
- this.mouseDeltaX += e.movementX;
497
- this.mouseDeltaY += e.movementY;
498
- };
499
- /** F2 — real window blur: drop focus and flush held REAL-device state
500
- * (the browser stops delivering keyup/mouseup while blurred, so a key
501
- * held at alt-tab time would stick). #144: virtual-action state is
502
- * deliberately NOT flushed here — a bot's held action survives the human
503
- * looking away (see `machineInputActive`); only `setEnabled(false)`
504
- * clears it. */
505
- onWindowBlur = () => {
506
- this.focused = false;
507
- this.flushRealHeldState();
508
- };
509
- /** F2 — real window focus regain. No flush needed (there's nothing held to
510
- * flush — a fresh keydown/mousedown is required to register pressed
511
- * again, exactly like re-enabling via `setEnabled(true)`). */
512
- onWindowFocus = () => {
513
- this.focused = true;
514
- };
515
- /** Clear held/transient REAL-device state (keyboard/mouse/touch-button) —
516
- * shared by `setEnabled(false)` (via `flushHeldState`) and real blur
517
- * (`onWindowBlur`), so a key/button physically held when input is
518
- * suspended doesn't linger. Level values (gamepad axes, touch sticks,
519
- * injected test axes) are deliberately NOT flushed here — they are gated
520
- * at READ time instead (see `inputActive` checks in `poll()`/the
521
- * `collect*Contributions` methods), matching the pre-existing
522
- * gamepad-axis convention. */
523
- flushRealHeldState() {
524
- this.keysDown.clear();
525
- this.keysJustDown.clear();
526
- this.keysJustUp.clear();
527
- this.mouseButtons.clear();
528
- this.mouseButtonsJustDown.clear();
529
- this.mouseButtonsJustUp.clear();
530
- this.mouseDeltaX = 0;
531
- this.mouseDeltaY = 0;
532
- this.touchButtonsDown.clear();
533
- this.touchButtonsJustDown.clear();
534
- this.touchButtonsJustUp.clear();
535
- }
536
- /** The full flush `setEnabled(false)` performs: real-device state AND
537
- * virtual-action state. Task 1.4's rule ("a bot's held action must not
538
- * survive input being suspended") now applies only to the editor's
539
- * explicit gate — #144 moved real window blur onto `flushRealHeldState`
540
- * alone, so machine input survives the human's focus leaving the tab.
541
- * No release edge is manufactured here (`manufactureReleaseEdge: false`,
542
- * the default) — this is a SUSPEND, not a deliberate release, and mirrors
543
- * `flushRealHeldState`'s own silence (a key physically held at
544
- * suspend/blur time is wiped with no `keysJustUp`, "flush held state,
545
- * don't corrupt history", see `poll()`'s doc comment). */
546
- flushHeldState() {
547
- this.flushRealHeldState();
548
- this.clearVirtualActionState();
549
- }
550
- /** Shared by the public `clearVirtualActions()` and `flushHeldState()`
551
- * above — clears every virtual-action store (held digitals, queued/active
552
- * taps, scalar/vector2 values, the release-edge set itself).
553
- *
554
- * `manufactureReleaseEdge` (default `false`): when `true` — only
555
- * `clearVirtualActions()`'s own deliberate "let go" passes this — every
556
- * currently-held digital in `virtualDigitalHeld` first gets an
557
- * `isJustReleased` edge written to `virtualDigitalJustReleased`, mirroring
558
- * a real key-up (`onKeyUp` → `keysJustUp`). `flushHeldState()` (the
559
- * editor-gate suspend path) deliberately passes `false` — see its own doc
560
- * comment for why a suspend must stay silent. */
561
- clearVirtualActionState(options) {
562
- if (options?.manufactureReleaseEdge) {
563
- for (const action of this.virtualDigitalHeld) {
564
- this.virtualDigitalJustReleased.add(action);
565
- }
566
- }
567
- this.virtualDigitalHeld.clear();
568
- this.virtualDigitalTapQueued.clear();
569
- this.virtualDigitalTapActive.clear();
570
- this.virtualScalarValues.clear();
571
- this.virtualVector2Values.clear();
572
- }
573
- constructor() {
574
- window.addEventListener('keydown', this.onKeyDown);
575
- window.addEventListener('keyup', this.onKeyUp);
576
- window.addEventListener('mousedown', this.onMouseDown);
577
- window.addEventListener('mouseup', this.onMouseUp);
578
- window.addEventListener('mousemove', this.onMouseMove);
579
- window.addEventListener('blur', this.onWindowBlur);
580
- window.addEventListener('focus', this.onWindowFocus);
581
- // `document` isn't touched by any pre-F2 code path (the headless vitest
582
- // env stubs `window` only) — guard the same way `isTextEntryFocused` does
583
- // so tests that don't need pointer-lock coverage need no `document` stub.
584
- if (typeof document !== 'undefined' && typeof document.addEventListener === 'function') {
585
- document.addEventListener('pointerlockchange', this.onPointerLockChange);
586
- }
587
- }
588
- /**
589
- * Load action map from a .inputmap.json file. Validated via
590
- * `InputMapFileSchema` (T4.6) — a malformed input map throws a
591
- * `SceneParseError` naming the file, not a deep TypeError once the bad
592
- * data reaches `isPressed`/`isJustPressed`/`isJustReleased`.
593
- */
594
- async loadMap(url) {
595
- let data;
596
- try {
597
- const res = await fetch(resolveUrl(url));
598
- if (!res.ok) {
599
- throw new Error(`HTTP ${res.status} ${res.statusText}`);
600
- }
601
- const json = await res.json();
602
- const result = InputMapFileSchema.safeParse(json);
603
- if (!result.success)
604
- throw new SceneParseError(result.error.issues, url);
605
- data = result.data;
606
- }
607
- catch (err) {
608
- const message = `InputManager.loadMap: failed to load input map "${url}": ${err instanceof Error ? err.message : String(err)}`;
609
- console.error(message);
610
- throw err instanceof SceneParseError ? err : new Error(message);
611
- }
612
- this.applyParsedMap(data);
613
- }
614
- /**
615
- * F3 — `loadMap`'s synchronous, no-`fetch` sibling: apply an already-in-
616
- * memory (or freshly-deserialized) input-map DOCUMENT, validated through
617
- * the identical `InputMapFileSchema` (throws `SceneParseError` naming no
618
- * file, matching `loadMap`'s error shape minus the URL). This is the other
619
- * half of the F3 persist round-trip: `toInputMapFile()` serializes,
620
- * `loadMapObject` re-applies — no network/file I/O required, so it's also
621
- * how a headless test proves a rebind survives a save/reload cycle.
622
- */
623
- loadMapObject(data) {
624
- const result = InputMapFileSchema.safeParse(data);
625
- if (!result.success)
626
- throw new SceneParseError(result.error.issues);
627
- this.applyParsedMap(result.data);
628
- return result.data;
629
- }
630
- /** Shared apply step for `loadMap`/`loadMapObject`: copy each action's
631
- * bindings + declared valueType into the live maps, and snapshot the
632
- * bindings as that action's F3 reset-to-defaults baseline. */
633
- applyParsedMap(data) {
634
- this.loadedMapVersion = data.version;
635
- for (const [name, action] of Object.entries(data.actions)) {
636
- this.actions.set(name, action.bindings);
637
- // `InputActionSchema.valueType` has a Zod `.default('digital')`, so a
638
- // successfully-parsed action always carries one even when the source
639
- // JSON omitted it.
640
- this.actionValueTypes.set(name, action.valueType ?? 'digital');
641
- this.defaultBindings.set(name, structuredClone(action.bindings));
642
- }
643
- }
644
- /** Register an action programmatically. `valueType` declares the action's
645
- * value shape (F1) — defaults to `'digital'`, matching every call site
646
- * written before F1. F3 — also snapshots `bindings` as this action's
647
- * reset-to-defaults baseline (see `resetBindings`). */
648
- registerAction(name, bindings, valueType = 'digital') {
649
- this.actions.set(name, bindings);
650
- this.actionValueTypes.set(name, valueType);
651
- this.defaultBindings.set(name, structuredClone(bindings));
652
- }
653
- /** The declared value type for a registered/loaded action (F1) — `'digital'`
654
- * for an action that never specified one (including an unregistered/typo'd
655
- * name — harmless, since every typed getter already treats an unknown
656
- * action as inert). */
657
- getActionValueType(actionName) {
658
- return this.actionValueTypes.get(actionName) ?? 'digital';
659
- }
660
- /** Enforces the F1 type-safety AC: reading an action through a typed getter
661
- * whose value type doesn't match the action's DECLARED `valueType` throws,
662
- * naming both. Unknown actions (never registered/loaded) are exempt — they
663
- * already read as inert/zeroed by every getter, matching pre-F1 behavior
664
- * for a typo'd action name. */
665
- assertValueType(actionName, expected, method) {
666
- const actual = this.actionValueTypes.get(actionName);
667
- if (actual !== undefined && actual !== expected) {
668
- throw new Error(`InputManager.${method}: action "${actionName}" is declared valueType '${actual}', not ` +
669
- `'${expected}'. Typed reads must match an action's declared valueType (F1, spec §12) — ` +
670
- `use the getter for '${actual}' instead.`);
671
- }
672
- }
673
- /** The names of all registered actions (for the InputAdapter to enumerate). */
674
- actionNames() {
675
- return [...this.actions.keys()];
676
- }
677
- /**
678
- * Poll gamepads and virtual look stick (call at start of frame).
679
- *
680
- * `tick` (D15/T-D15.3/.5) — the GAME tick this call services. Pass the
681
- * live, shared `Game` tick counter here (`vgai-scene-game-adapter.ts` does,
682
- * via `DebugRegistry.getGameTick()`) so `scheduleActionAtTick`'s numbering
683
- * never drifts from the actual game tick — the bug an earlier revision of
684
- * this feature had: an InputManager-LOCAL counter, incremented once per
685
- * `poll()` call, undercounts for a paused/frozen world (its `poll()` isn't
686
- * called every game tick while frozen), so a schedule set against "tick
687
- * 500" could fire at the wrong wall-tick once the world resumed. Omit it
688
- * (every existing bare/headless caller does) to fall back to the
689
- * self-incrementing `currentTick` counter — unchanged pre-D15 behavior.
690
- */
691
- poll(tick) {
692
- const servicedTick = tick !== undefined ? tick : this.currentTick;
693
- // D15/T-D15.5 — a GAP: one or more ticks strictly between the last tick
694
- // this InputManager serviced and this one never ran its input phase at
695
- // all (this world was paused/frozen and skipped straight from tick A to
696
- // a later tick B > A+1 once stepped/resumed again — the frozen-world
697
- // scenario objection above). Any entries scheduled for a tick inside
698
- // that gap can never be honored — drop them (each emitting one
699
- // `'input.schedule.dropped'` event) rather than leaving them to rot in
700
- // `scheduledActions` forever. Guarded on `size > 0` so the overwhelmingly
701
- // common case (nothing ever scheduled) costs nothing, even across a
702
- // very large gap.
703
- if (this.scheduledActions.size > 0) {
704
- // Iterate the (tiny) schedule map, never the gap's tick RANGE — a
705
- // world resuming after a very long freeze (or one mounting late into
706
- // a game already millions of ticks in) must not pay O(gap) here.
707
- // Sorted so drop events still arrive in tick order.
708
- const gapped = [...this.scheduledActions.keys()]
709
- .filter((t) => t > this.lastServicedTick && t < servicedTick)
710
- .sort((a, b) => a - b);
711
- for (const t of gapped) {
712
- const skipped = this.scheduledActions.get(t);
713
- this.scheduledActions.delete(t);
714
- for (const entry of skipped) {
715
- this.debugEmit?.('input.schedule.dropped', { tick: t, action: entry.action });
716
- }
717
- }
718
- }
719
- this.lastServicedTick = servicedTick;
720
- this.currentTick = servicedTick + 1;
721
- if (!this.enabled) {
722
- // Input suspended by the editor's explicit gate (play stopped /
723
- // game-tab-inactive). #144: this branch no longer covers real window
724
- // blur — an enabled-but-blurred tick falls through so MACHINE input
725
- // (virtual actions, scheduled actuations) keeps flowing while the
726
- // human's focus is elsewhere; real devices are gated further down.
727
- // Unlike keyboard/mouse, gamepad state isn't DOM-event-driven —
728
- // `isPressed`/`isJustPressed`/`isJustReleased` read `this.gamepads`
729
- // (and the edge-tracked sets) directly, not a live poll of `navigator`.
730
- // So gate the READ here: leave `this.gamepads` empty so every
731
- // gamepad-bound check sees no gamepads and reports false. Deliberately
732
- // leave the edge-tracking sets
733
- // (gamepadButtons{Prev,Down,JustDown,JustUp}, gamepadAxes{Current,Prev})
734
- // untouched — they freeze at their last real values while disabled and
735
- // resume diffing against genuinely-last-observed hardware state once
736
- // re-enabled, so there's no phantom edge on re-enable (mirrors the
737
- // "flush held state, don't corrupt history" intent of setEnabled below).
738
- // D15/T-D15.5: a tick serviced while gated drops its OWN scheduled
739
- // entries too (never applied, never retried later) — same honesty line
740
- // `setVirtualAction`'s own gate already draws — each emitting the same
741
- // `'input.schedule.dropped'` event the gap-handling above does.
742
- const gatedDrop = this.scheduledActions.get(servicedTick);
743
- if (gatedDrop) {
744
- this.scheduledActions.delete(servicedTick);
745
- for (const entry of gatedDrop) {
746
- this.debugEmit?.('input.schedule.dropped', { tick: servicedTick, action: entry.action });
747
- }
748
- }
749
- this.gamepads = [];
750
- return;
751
- }
752
- // Task 1.4 — promote queued tapVirtualAction() calls to this frame's
753
- // "active" just-pressed set (hollowstone's queued/beginFrame contract,
754
- // ported onto this engine's existing fixed-step poll()/endFrame()
755
- // bracket instead of a bespoke beginFrame() — poll() already runs at the
756
- // start of the fixed step). Cleared in endFrame() below.
757
- this.virtualDigitalTapActive = this.virtualDigitalTapQueued;
758
- this.virtualDigitalTapQueued = new Set();
759
- // D15/T-D15.5 — apply any actuation scheduled for exactly this tick
760
- // (spec §2.c: "applied at the START of the target tick's input phase",
761
- // before any gameplay phase reads input — this still runs before every
762
- // OTHER phase this tick, `input` being the first system phase). Placed
763
- // AFTER the tap-queue promotion immediately above (not before) so a
764
- // scheduled digital `true` can add itself to `virtualDigitalTapActive`
765
- // (see `applyScheduledActionsForTick`) without that promotion's
766
- // reassignment wiping it out.
767
- this.applyScheduledActionsForTick(servicedTick);
768
- if (!this.focused) {
769
- // #144 — enabled but blurred: everything ABOVE this line is machine
770
- // input (tap promotion, scheduled actuations) and stays live; real
771
- // devices below gate exactly as the old combined branch did (no
772
- // gamepads presented, edge sets frozen at their last real values).
773
- // The post-gate trace still records — "what the sim actually
774
- // consumed" includes machine input delivered while blurred.
775
- this.gamepads = [];
776
- this.recordPostGateTick(servicedTick);
777
- return;
778
- }
779
- this.gamepads = navigator.getGamepads ? [...navigator.getGamepads()] : [];
780
- // Gamepad button edge detection: diff this frame's pressed set against the
781
- // previous frame's so isJustPressed/isJustReleased work for gamepads too.
782
- const current = new Set();
783
- for (let i = 0; i < this.gamepads.length; i++) {
784
- const gp = this.gamepads[i];
785
- if (!gp)
786
- continue;
787
- for (let b = 0; b < gp.buttons.length; b++) {
788
- if (gp.buttons[b]?.pressed)
789
- current.add(`${i}:${b}`);
790
- }
791
- }
792
- for (const key of current) {
793
- if (!this.gamepadButtonsPrev.has(key))
794
- this.gamepadButtonsJustDown.add(key);
795
- }
796
- for (const key of this.gamepadButtonsPrev) {
797
- if (!current.has(key))
798
- this.gamepadButtonsJustUp.add(key);
799
- }
800
- this.gamepadButtonsDown = current;
801
- // F4 — a button press is unambiguous real gamepad input.
802
- if (current.size > 0)
803
- this.lastActiveDevice = 'gamepad';
804
- // Snapshot this frame's raw axis values per (gamepad, axis) — independent
805
- // of any binding's direction/deadzone, mirroring the button snapshot
806
- // above. isPressed/isJustPressed/isJustReleased apply a binding's own
807
- // threshold to these raw values (current vs. `gamepadAxesPrev`) on read.
808
- const currentAxes = new Map();
809
- for (let i = 0; i < this.gamepads.length; i++) {
810
- const gp = this.gamepads[i];
811
- if (!gp)
812
- continue;
813
- for (let a = 0; a < gp.axes.length; a++) {
814
- const value = gp.axes[a] ?? 0;
815
- currentAxes.set(`${i}:${a}`, value);
816
- // F4 — a stick pushed meaningfully off-center is also unambiguous
817
- // real gamepad input (see `isStickActive`'s doc comment).
818
- if (Math.abs(value) > 0.3)
819
- this.lastActiveDevice = 'gamepad';
820
- }
821
- }
822
- this.gamepadAxesCurrent = currentAxes;
823
- // Inject look stick as continuous mouse delta (position → rate-of-rotation)
824
- if (this.lookStickX !== 0 || this.lookStickY !== 0) {
825
- const scale = 8;
826
- this.mouseDeltaX += this.lookStickX * scale;
827
- this.mouseDeltaY += this.lookStickY * scale;
828
- }
829
- // D15/T-D15.5 — tap the POST-GATE resolved values for this tick LAST
830
- // (every binding/virtual/scheduled contribution above has already been
831
- // folded in): "what the sim actually consumed", the honesty line the
832
- // design doc draws for record-now/replay-in-SP5 (§2.c).
833
- this.recordPostGateTick(servicedTick);
834
- }
835
- /**
836
- * D15/T-D15.5 — apply every actuation `scheduleActionAtTick` queued for
837
- * exactly `tick`, then forget them (a tick is serviced at most once).
838
- *
839
- * Digital `true` gets SPECIAL handling (the objection-1 fix): it promotes
840
- * the action into this tick's `virtualDigitalTapActive` set directly — the
841
- * same one-tick "active" pulse `tapVirtualAction` uses — so `isJustPressed`
842
- * reads true for EXACTLY this tick (false on the next, per `endFrame()`'s
843
- * clear) while `isPressed` keeps reading true afterward (still held) until
844
- * a later scheduled/explicit `false` releases it. This direct write is
845
- * safe here (unlike `applyVirtualActionValue`'s equivalent direct-path
846
- * case — see its doc comment) specifically because this method only ever
847
- * runs from inside `poll()`, AFTER `poll()`'s own tap-queue-promotion
848
- * reassignment of `virtualDigitalTapActive` has already happened for this
849
- * tick (see `poll()`'s own comment on why it's placed after) — so there's
850
- * no risk of this write being silently wiped by that reassignment.
851
- *
852
- * pulse-runner friction #4: a prior revision of this comment
853
- * used to say `setVirtualAction`'s plain write "never produces a
854
- * just-pressed edge at all" and that the edge fix landed ONLY here, for
855
- * this scheduled path — true at the time, and exactly the gap a blind
856
- * dogfood run (pulse-runner) hit through the direct `setVirtualAction`/
857
- * `holdFor` path. `applyVirtualActionValue` now manufactures the same kind
858
- * of edge for THAT path too (queued instead of written directly, for the
859
- * timing reason above) — this is no longer the only path that does.
860
- *
861
- * A digital `false` here simply releases the hold — no edge is
862
- * manufactured for it at a SCHEDULED tick (no caller needs a virtual
863
- * `isJustReleased` fired at an exact future tick today); the direct
864
- * `setVirtualAction(action, false)`/`clearVirtualActions()` paths DO
865
- * manufacture one now (see `applyVirtualActionValue`/
866
- * `clearVirtualActionState`). Scalar/vector2 values apply directly,
867
- * unchanged.
868
- */
869
- applyScheduledActionsForTick(tick) {
870
- const pending = this.scheduledActions.get(tick);
871
- if (!pending)
872
- return;
873
- this.scheduledActions.delete(tick);
874
- for (const entry of pending) {
875
- if (typeof entry.value === 'boolean') {
876
- if (entry.value) {
877
- this.virtualDigitalHeld.add(entry.action);
878
- this.virtualDigitalTapActive.add(entry.action);
879
- }
880
- else {
881
- this.virtualDigitalHeld.delete(entry.action);
882
- }
883
- }
884
- else {
885
- this.applyVirtualActionValue(entry.action, entry.value);
886
- }
887
- }
888
- }
889
- /** D15/T-D15.5 — append this tick's action-value DELTA to the recording
890
- * ring, iff a recording is active. Reads every declared digital/scalar/
891
- * vector2 action through the exact same typed getters gameplay code would
892
- * (`readAction`), so the trace reflects what a component reading these
893
- * actions THIS tick actually sees post-gate — never a raw/ungated value.
894
- * `pointerDelta`/`pointerPosition` actions are skipped (a different,
895
- * frame-accumulated shape, and not part of the virtual-action/schedule
896
- * surface this trace exists to prove).
897
- *
898
- * Two replay-affecting caveats a future SP5 consumer must know:
899
- * 1. A gated stretch never calls this at all (`poll()`'s gated branch
900
- * returns before reaching it) — no entries are recorded for ticks
901
- * input was suspended. Any change only OBSERVABLE once re-enabled (a
902
- * delta against `lastTraceSnapshot`) lands as a delta AT the re-enable
903
- * tick, not at whatever tick it actually happened.
904
- * 2. This ring is capped (`INPUT_TRACE_CAP`) and evicts via `shift()` once
905
- * it fills — dropping the OLDEST entry. Because entries are DELTAS,
906
- * evicting the earliest entries drops the base state later deltas were
907
- * relative to — this ring is not safe to replay from its start once it
908
- * has ever been capped. */
909
- recordPostGateTick(tick) {
910
- if (!this.inputRecording)
911
- return;
912
- const deltas = {};
913
- let changed = false;
914
- for (const name of this.actionNames()) {
915
- const type = this.getActionValueType(name);
916
- if (type !== 'digital' && type !== 'scalar' && type !== 'vector2')
917
- continue;
918
- const value = this.readAction(name, type);
919
- const prev = this.lastTraceSnapshot.get(name);
920
- if (actionValuesEqual(prev, value))
921
- continue;
922
- deltas[name] = value;
923
- this.lastTraceSnapshot.set(name, value);
924
- changed = true;
925
- }
926
- if (!changed)
927
- return;
928
- this.inputTrace.push({ tick, actions: deltas });
929
- if (this.inputTrace.length > INPUT_TRACE_CAP)
930
- this.inputTrace.shift();
931
- }
932
- /** Has a `gamepad_axis` binding's raw value crossed its direction+deadzone
933
- * threshold (positive: `value > deadzone`; negative: `value < -deadzone`)? */
934
- static axisThresholdMet(value, direction, deadzone) {
935
- return direction === 'positive' ? value > deadzone : value < -deadzone;
936
- }
937
- /** Is an action currently held down? */
938
- isPressed(actionName) {
939
- this.assertValueType(actionName, 'digital', 'isPressed');
940
- // Task 1.4 — a held virtual action, or a tap promoted to this frame's
941
- // "active" set, ORs into isPressed alongside binding contributions.
942
- if (this.virtualDigitalHeld.has(actionName) || this.virtualDigitalTapActive.has(actionName)) {
943
- return true;
944
- }
945
- const bindings = this.actions.get(actionName);
946
- if (!bindings)
947
- return false;
948
- for (const binding of bindings) {
949
- switch (binding.type) {
950
- case 'key':
951
- if (this.keysDown.has(binding.code))
952
- return true;
953
- break;
954
- case 'mouse_button':
955
- if (this.mouseButtons.has(binding.button))
956
- return true;
957
- break;
958
- case 'gamepad_button':
959
- for (const gp of this.gamepads) {
960
- if (gp?.buttons[binding.button]?.pressed)
961
- return true;
962
- }
963
- break;
964
- case 'gamepad_axis': {
965
- const dz = binding.deadzone ?? 0.15;
966
- for (const gp of this.gamepads) {
967
- if (!gp)
968
- continue;
969
- const val = gp.axes[binding.axis] ?? 0;
970
- if (InputManager.axisThresholdMet(val, binding.direction, dz))
971
- return true;
972
- }
973
- break;
974
- }
975
- case 'touch_button':
976
- if (this.touchButtonsDown.has(binding.sourceId))
977
- return true;
978
- break;
979
- }
980
- }
981
- return false;
982
- }
983
- /** Was an action pressed this frame (not held from previous)? */
984
- isJustPressed(actionName) {
985
- this.assertValueType(actionName, 'digital', 'isJustPressed');
986
- // Task 1.4 — a tapVirtualAction() queued on a prior frame reads
987
- // just-pressed for exactly the one poll()/endFrame() bracket it was
988
- // promoted into (see poll()/endFrame() below).
989
- if (this.virtualDigitalTapActive.has(actionName))
990
- return true;
991
- const bindings = this.actions.get(actionName);
992
- if (!bindings)
993
- return false;
994
- for (const binding of bindings) {
995
- switch (binding.type) {
996
- case 'key':
997
- if (this.keysJustDown.has(binding.code))
998
- return true;
999
- break;
1000
- case 'mouse_button':
1001
- if (this.mouseButtonsJustDown.has(binding.button))
1002
- return true;
1003
- break;
1004
- case 'gamepad_button':
1005
- // Edge-tracked in poll(): true only on the first frame the button
1006
- // transitions from up→down (across any connected gamepad).
1007
- for (let i = 0; i < this.gamepads.length; i++) {
1008
- if (this.gamepadButtonsJustDown.has(`${i}:${binding.button}`))
1009
- return true;
1010
- }
1011
- break;
1012
- case 'gamepad_axis': {
1013
- // Edge-tracked via raw axis snapshots (poll()): true only on the
1014
- // first frame the thresholded (direction+deadzone) boolean flips
1015
- // from off→on (across any connected gamepad).
1016
- const dz = binding.deadzone ?? 0.15;
1017
- for (let i = 0; i < this.gamepads.length; i++) {
1018
- const key = `${i}:${binding.axis}`;
1019
- const now = this.gamepadAxesCurrent.get(key) ?? 0;
1020
- const prev = this.gamepadAxesPrev.get(key) ?? 0;
1021
- const nowOn = InputManager.axisThresholdMet(now, binding.direction, dz);
1022
- const prevOn = InputManager.axisThresholdMet(prev, binding.direction, dz);
1023
- if (nowOn && !prevOn)
1024
- return true;
1025
- }
1026
- break;
1027
- }
1028
- case 'touch_button':
1029
- if (this.touchButtonsJustDown.has(binding.sourceId))
1030
- return true;
1031
- break;
1032
- }
1033
- }
1034
- return false;
1035
- }
1036
- /** Was an action released this frame (held last frame, up now)? */
1037
- isJustReleased(actionName) {
1038
- this.assertValueType(actionName, 'digital', 'isJustReleased');
1039
- // pulse-runner friction #4 — mirrors isJustPressed's virtual check above:
1040
- // a direct setVirtualAction(action, false) or clearVirtualActions() on a
1041
- // held action manufactures exactly one just-released edge here, matching
1042
- // a real key-up's keysJustUp.
1043
- if (this.virtualDigitalJustReleased.has(actionName))
1044
- return true;
1045
- const bindings = this.actions.get(actionName);
1046
- if (!bindings)
1047
- return false;
1048
- for (const binding of bindings) {
1049
- switch (binding.type) {
1050
- case 'key':
1051
- if (this.keysJustUp.has(binding.code))
1052
- return true;
1053
- break;
1054
- case 'mouse_button':
1055
- if (this.mouseButtonsJustUp.has(binding.button))
1056
- return true;
1057
- break;
1058
- case 'gamepad_button':
1059
- for (let i = 0; i < this.gamepads.length; i++) {
1060
- if (this.gamepadButtonsJustUp.has(`${i}:${binding.button}`))
1061
- return true;
1062
- }
1063
- break;
1064
- case 'gamepad_axis': {
1065
- // The mirror of isJustPressed: true only on the first frame the
1066
- // thresholded boolean flips from on→off.
1067
- const dz = binding.deadzone ?? 0.15;
1068
- for (let i = 0; i < this.gamepads.length; i++) {
1069
- const key = `${i}:${binding.axis}`;
1070
- const now = this.gamepadAxesCurrent.get(key) ?? 0;
1071
- const prev = this.gamepadAxesPrev.get(key) ?? 0;
1072
- const nowOn = InputManager.axisThresholdMet(now, binding.direction, dz);
1073
- const prevOn = InputManager.axisThresholdMet(prev, binding.direction, dz);
1074
- if (!nowOn && prevOn)
1075
- return true;
1076
- }
1077
- break;
1078
- }
1079
- case 'touch_button':
1080
- if (this.touchButtonsJustUp.has(binding.sourceId))
1081
- return true;
1082
- break;
1083
- }
1084
- }
1085
- return false;
1086
- }
1087
- /** Device/source metadata (F1 AC) for a digital action: which binding is
1088
- * CURRENTLY satisfying `isPressed`, or `null` if none is (including an
1089
- * unknown action). First-satisfied-binding-wins, mirroring isPressed's own
1090
- * iteration order. */
1091
- getDigitalSource(actionName) {
1092
- this.assertValueType(actionName, 'digital', 'getDigitalSource');
1093
- // #144 — mirror isPressed's own pre-gate virtual check: a machine-held
1094
- // action reports its source even while the window is blurred.
1095
- if (this.machineInputActive &&
1096
- (this.virtualDigitalHeld.has(actionName) || this.virtualDigitalTapActive.has(actionName))) {
1097
- return { bindingType: 'virtual' };
1098
- }
1099
- if (!this.inputActive)
1100
- return null;
1101
- const bindings = this.actions.get(actionName);
1102
- if (!bindings)
1103
- return null;
1104
- for (const binding of bindings) {
1105
- switch (binding.type) {
1106
- case 'key':
1107
- if (this.keysDown.has(binding.code))
1108
- return { bindingType: 'key' };
1109
- break;
1110
- case 'mouse_button':
1111
- if (this.mouseButtons.has(binding.button))
1112
- return { bindingType: 'mouse_button' };
1113
- break;
1114
- case 'gamepad_button':
1115
- for (let i = 0; i < this.gamepads.length; i++) {
1116
- const gp = this.gamepads[i];
1117
- if (gp?.buttons[binding.button]?.pressed) {
1118
- return { bindingType: 'gamepad_button', gamepadIndex: i, gamepadId: gp.id };
1119
- }
1120
- }
1121
- break;
1122
- case 'gamepad_axis': {
1123
- const dz = binding.deadzone ?? 0.15;
1124
- for (let i = 0; i < this.gamepads.length; i++) {
1125
- const gp = this.gamepads[i];
1126
- if (!gp)
1127
- continue;
1128
- const val = gp.axes[binding.axis] ?? 0;
1129
- if (InputManager.axisThresholdMet(val, binding.direction, dz)) {
1130
- return { bindingType: 'gamepad_axis', gamepadIndex: i, gamepadId: gp.id };
1131
- }
1132
- }
1133
- break;
1134
- }
1135
- case 'touch_button':
1136
- if (this.touchButtonsDown.has(binding.sourceId)) {
1137
- return { bindingType: 'touch_button', sourceId: binding.sourceId };
1138
- }
1139
- break;
1140
- }
1141
- }
1142
- return null;
1143
- }
1144
- // ==========================================================================
1145
- // F1 — typed scalar/vector2/pointer reads. See the class doc comment above
1146
- // for the deadzone/normalize/combine rules these implement.
1147
- // ==========================================================================
1148
- /** 1D deadzone rescale: magnitudes below `deadzone` become 0; magnitudes
1149
- * at/above it rescale from 0 (at the deadzone edge) to ±1 (at |raw| = 1),
1150
- * sign preserved. `raw` is clamped to [-1, 1] first so an out-of-range
1151
- * input can't overshoot past ±1 the other end. */
1152
- static rescaleScalar(raw, deadzone) {
1153
- const clamped = Math.max(-1, Math.min(1, raw));
1154
- const mag = Math.abs(clamped);
1155
- if (mag < deadzone)
1156
- return 0;
1157
- const rescaled = deadzone >= 1 ? 0 : (mag - deadzone) / (1 - deadzone);
1158
- return Math.sign(clamped) * Math.min(1, rescaled);
1159
- }
1160
- /** Radial deadzone + unit-circle normalization for a Vector2: magnitude
1161
- * below `deadzone` snaps to {0,0}; at/above it, direction is preserved and
1162
- * magnitude is rescaled the same way `rescaleScalar` rescales a 1D
1163
- * magnitude (never renormalized UP past 1 — only ever scaled down). */
1164
- static rescaleVector2(raw, deadzone) {
1165
- const mag = Math.hypot(raw.x, raw.y);
1166
- if (mag === 0 || mag < deadzone)
1167
- return { x: 0, y: 0 };
1168
- const rescaledMag = Math.min(1, deadzone >= 1 ? 0 : (mag - deadzone) / (1 - deadzone));
1169
- const scale = rescaledMag / mag;
1170
- return { x: raw.x * scale, y: raw.y * scale };
1171
- }
1172
- /** Clamp a Vector2's magnitude to at most 1 (unit circle), preserving
1173
- * direction — the second half of the vector2 "sum-then-clamp" combine
1174
- * rule (a diagonal sum like {0.8, 0.8} has magnitude > 1). */
1175
- static clampVector2(v) {
1176
- const mag = Math.hypot(v.x, v.y);
1177
- if (mag <= 1 || mag === 0)
1178
- return v;
1179
- return { x: v.x / mag, y: v.y / mag };
1180
- }
1181
- /** Every `scalar`-relevant binding's (deadzone-applied) contribution for
1182
- * `actionName`, one entry per binding that can produce a scalar value —
1183
- * `gamepad_axis` (real device, already implemented pre-F1 for digital
1184
- * thresholding — F1 additionally reads its raw signed value for scalar
1185
- * actions, ignoring the binding's `direction` field, which is a digital-
1186
- * only hint) and `test_axis` (F1's injected-test-input source). `mouse_move`
1187
- * deliberately does NOT feed scalar (spec §12 F2 — no natural single-axis
1188
- * selector field is defined for it; it feeds pointerDelta/vector2 only —
1189
- * see `collectPointerDeltaContributions`/`collectVector2Contributions`). */
1190
- collectScalarContributions(actionName) {
1191
- const out = [];
1192
- // Task 1.4 — a virtual scalar value joins the same max-magnitude combine
1193
- // real bindings do (computeScalar below). #144: it passes the MACHINE
1194
- // gate (enabled only), so a bot's held value reads through while the
1195
- // window is blurred; real bindings below keep the full focus gate.
1196
- const virtual = this.virtualScalarValues.get(actionName);
1197
- if (virtual !== undefined && this.machineInputActive) {
1198
- out.push({ value: virtual, source: { bindingType: 'virtual' } });
1199
- }
1200
- if (!this.inputActive)
1201
- return out;
1202
- const bindings = this.actions.get(actionName);
1203
- if (!bindings)
1204
- return out;
1205
- for (const binding of bindings) {
1206
- if (binding.type === 'gamepad_axis') {
1207
- const dz = binding.deadzone ?? 0.15;
1208
- for (let i = 0; i < this.gamepads.length; i++) {
1209
- const gp = this.gamepads[i];
1210
- if (!gp)
1211
- continue;
1212
- const raw = gp.axes[binding.axis] ?? 0;
1213
- out.push({
1214
- value: InputManager.rescaleScalar(raw, dz),
1215
- source: { bindingType: 'gamepad_axis', gamepadIndex: i, gamepadId: gp.id },
1216
- });
1217
- }
1218
- }
1219
- else if (binding.type === 'test_axis') {
1220
- const raw = this.testAxisValues.get(binding.sourceId) ?? 0;
1221
- out.push({
1222
- value: InputManager.rescaleScalar(raw, binding.deadzone ?? 0),
1223
- source: { bindingType: 'test_axis', sourceId: binding.sourceId },
1224
- });
1225
- }
1226
- }
1227
- return out;
1228
- }
1229
- computeScalar(actionName) {
1230
- this.assertValueType(actionName, 'scalar', 'getScalar');
1231
- let best = null;
1232
- for (const c of this.collectScalarContributions(actionName)) {
1233
- if (!best || Math.abs(c.value) > Math.abs(best.value))
1234
- best = c;
1235
- }
1236
- if (!best || best.value === 0)
1237
- return { value: 0, source: null };
1238
- return best;
1239
- }
1240
- /** Read a `'scalar'`-valueType action's current value: max-magnitude across
1241
- * contributing (deadzone-applied) bindings, sign preserved (combine rule —
1242
- * see the class doc comment). Throws if the action is declared a
1243
- * different valueType. */
1244
- getScalar(actionName) {
1245
- return this.computeScalar(actionName).value;
1246
- }
1247
- /** Device/source metadata for {@link getScalar} — which binding is
1248
- * currently driving the combined value, or `null` if every contribution is
1249
- * zero (including an unknown action). */
1250
- getScalarSource(actionName) {
1251
- return this.computeScalar(actionName).source;
1252
- }
1253
- /** `gamepad_axis_pair` contribution(s) for {@link collectVector2Contributions} — one entry
1254
- * per connected gamepad, deadzone-rescaled (default 0.15, matching `gamepad_axis`). Broken
1255
- * out to a helper so the per-gamepad loop doesn't add to the main dispatcher's complexity. */
1256
- contributeGamepadAxisPair(binding) {
1257
- const dz = binding.deadzone ?? 0.15;
1258
- const out = [];
1259
- for (let i = 0; i < this.gamepads.length; i++) {
1260
- const gp = this.gamepads[i];
1261
- if (!gp)
1262
- continue;
1263
- const raw = { x: gp.axes[binding.xAxis] ?? 0, y: gp.axes[binding.yAxis] ?? 0 };
1264
- out.push({
1265
- value: InputManager.rescaleVector2(raw, dz),
1266
- source: { bindingType: 'gamepad_axis_pair', gamepadIndex: i, gamepadId: gp.id },
1267
- });
1268
- }
1269
- return out;
1270
- }
1271
- /** Every `vector2`-relevant binding's (deadzone-applied) contribution:
1272
- * `test_vector2` (F1's injected-test-input source), plus F2's real-device
1273
- * cases — `gamepad_axis_pair` (see {@link contributeGamepadAxisPair}), `mouse_move` (the
1274
- * accumulated mouse delta, deadzone default 0 — raw pixel deltas have no natural "at rest"
1275
- * jitter floor the way an analog stick does), and `touch_stick` (a virtual on-screen stick,
1276
- * deadzone default 0). */
1277
- collectVector2Contributions(actionName) {
1278
- const out = [];
1279
- // Task 1.4 — a virtual vector2 value joins the same sum-then-clamp
1280
- // combine real bindings do (computeVector2 below). #144: machine gate
1281
- // only, same as the scalar collector above.
1282
- const virtual = this.virtualVector2Values.get(actionName);
1283
- if (virtual !== undefined && this.machineInputActive) {
1284
- out.push({ value: virtual, source: { bindingType: 'virtual' } });
1285
- }
1286
- if (!this.inputActive)
1287
- return out;
1288
- const bindings = this.actions.get(actionName);
1289
- if (!bindings)
1290
- return out;
1291
- for (const binding of bindings) {
1292
- switch (binding.type) {
1293
- case 'test_vector2': {
1294
- const raw = this.testVector2Values.get(binding.sourceId) ?? { x: 0, y: 0 };
1295
- out.push({
1296
- value: InputManager.rescaleVector2(raw, binding.deadzone ?? 0),
1297
- source: { bindingType: 'test_vector2', sourceId: binding.sourceId },
1298
- });
1299
- break;
1300
- }
1301
- case 'gamepad_axis_pair':
1302
- out.push(...this.contributeGamepadAxisPair(binding));
1303
- break;
1304
- case 'mouse_move': {
1305
- const raw = { x: this.mouseDeltaX, y: this.mouseDeltaY };
1306
- out.push({
1307
- value: InputManager.rescaleVector2(raw, binding.deadzone ?? 0),
1308
- source: { bindingType: 'mouse_move' },
1309
- });
1310
- break;
1311
- }
1312
- case 'touch_stick': {
1313
- const raw = this.touchStickValues.get(binding.sourceId) ?? { x: 0, y: 0 };
1314
- out.push({
1315
- value: InputManager.rescaleVector2(raw, binding.deadzone ?? 0),
1316
- source: { bindingType: 'touch_stick', sourceId: binding.sourceId },
1317
- });
1318
- break;
1319
- }
1320
- }
1321
- }
1322
- return out;
1323
- }
1324
- computeVector2(actionName) {
1325
- this.assertValueType(actionName, 'vector2', 'getVector2');
1326
- let sum = { x: 0, y: 0 };
1327
- let bestSource = null;
1328
- let bestMag = 0;
1329
- for (const c of this.collectVector2Contributions(actionName)) {
1330
- sum = { x: sum.x + c.value.x, y: sum.y + c.value.y };
1331
- const mag = Math.hypot(c.value.x, c.value.y);
1332
- if (mag > bestMag) {
1333
- bestMag = mag;
1334
- bestSource = c.source;
1335
- }
1336
- }
1337
- const clamped = InputManager.clampVector2(sum);
1338
- if (clamped.x === 0 && clamped.y === 0)
1339
- return { value: clamped, source: null };
1340
- return { value: clamped, source: bestSource };
1341
- }
1342
- /** Read a `'vector2'`-valueType action's current value: sum contributing
1343
- * (deadzone-applied) {x,y} values component-wise, then clamp to the unit
1344
- * circle (combine rule — see the class doc comment). Throws if the action
1345
- * is declared a different valueType. */
1346
- getVector2(actionName) {
1347
- return this.computeVector2(actionName).value;
1348
- }
1349
- /** Device/source metadata for {@link getVector2} — the contributing binding
1350
- * with the largest (pre-combine) magnitude, or `null` if the combined
1351
- * value is {0,0} (including an unknown action). */
1352
- getVector2Source(actionName) {
1353
- return this.computeVector2(actionName).source;
1354
- }
1355
- /** Every `pointerDelta`-relevant binding's contribution: `test_pointer_delta`
1356
- * (F1's injected-test-input source) and — F2 — `mouse_move`, reading the
1357
- * real accumulated `mouseDeltaX/Y` (the same accumulator `onMouseMove`
1358
- * fills and `getMouseDelta()` reads). No deadzone for either (deltas are
1359
- * not deadzoned). */
1360
- collectPointerDeltaContributions(actionName) {
1361
- if (!this.inputActive)
1362
- return [];
1363
- const bindings = this.actions.get(actionName);
1364
- if (!bindings)
1365
- return [];
1366
- const out = [];
1367
- for (const binding of bindings) {
1368
- if (binding.type === 'test_pointer_delta') {
1369
- const value = this.testPointerDeltaAccum.get(binding.sourceId) ?? { x: 0, y: 0 };
1370
- out.push({
1371
- value,
1372
- source: { bindingType: 'test_pointer_delta', sourceId: binding.sourceId },
1373
- });
1374
- }
1375
- else if (binding.type === 'mouse_move') {
1376
- out.push({
1377
- value: { x: this.mouseDeltaX, y: this.mouseDeltaY },
1378
- source: { bindingType: 'mouse_move' },
1379
- });
1380
- }
1381
- }
1382
- return out;
1383
- }
1384
- computePointerDelta(actionName) {
1385
- this.assertValueType(actionName, 'pointerDelta', 'getPointerDelta');
1386
- let sum = { x: 0, y: 0 };
1387
- let bestSource = null;
1388
- let bestMag = 0;
1389
- for (const c of this.collectPointerDeltaContributions(actionName)) {
1390
- sum = { x: sum.x + c.value.x, y: sum.y + c.value.y };
1391
- const mag = Math.hypot(c.value.x, c.value.y);
1392
- if (mag > bestMag) {
1393
- bestMag = mag;
1394
- bestSource = c.source;
1395
- }
1396
- }
1397
- if (sum.x === 0 && sum.y === 0)
1398
- return { value: sum, source: null };
1399
- return { value: sum, source: bestSource };
1400
- }
1401
- /** Read a `'pointerDelta'`-valueType action's this-frame movement: sum of
1402
- * contributing bindings' deltas (combine rule — see the class doc
1403
- * comment). Throws if the action is declared a different valueType. */
1404
- getPointerDelta(actionName) {
1405
- return this.computePointerDelta(actionName).value;
1406
- }
1407
- /** Device/source metadata for {@link getPointerDelta}. */
1408
- getPointerDeltaSource(actionName) {
1409
- return this.computePointerDelta(actionName).source;
1410
- }
1411
- /** Every `pointerPosition`-relevant binding's contribution, each tagged
1412
- * with the injection-order sequence number used to break ties (last-
1413
- * write-wins) — today only `test_pointer_position` (F1's injected-test-
1414
- * input source). */
1415
- collectPointerPositionContributions(actionName) {
1416
- if (!this.inputActive)
1417
- return [];
1418
- const bindings = this.actions.get(actionName);
1419
- if (!bindings)
1420
- return [];
1421
- const out = [];
1422
- for (const binding of bindings) {
1423
- if (binding.type === 'test_pointer_position') {
1424
- const entry = this.testPointerPositionValues.get(binding.sourceId);
1425
- if (entry) {
1426
- out.push({
1427
- value: entry.value,
1428
- seq: entry.seq,
1429
- source: { bindingType: 'test_pointer_position', sourceId: binding.sourceId },
1430
- });
1431
- }
1432
- }
1433
- }
1434
- return out;
1435
- }
1436
- computePointerPosition(actionName) {
1437
- this.assertValueType(actionName, 'pointerPosition', 'getPointerPosition');
1438
- const contributions = this.collectPointerPositionContributions(actionName);
1439
- if (contributions.length === 0)
1440
- return { value: { x: 0, y: 0 }, source: null };
1441
- let best = contributions[0];
1442
- for (const c of contributions) {
1443
- if (c.seq > best.seq)
1444
- best = c;
1445
- }
1446
- return { value: best.value, source: best.source };
1447
- }
1448
- /** Read a `'pointerPosition'`-valueType action's current absolute position:
1449
- * last-write-wins across contributing bindings (combine rule — see the
1450
- * class doc comment; position isn't additive, so it is never summed).
1451
- * Throws if the action is declared a different valueType. */
1452
- getPointerPosition(actionName) {
1453
- return this.computePointerPosition(actionName).value;
1454
- }
1455
- /** Device/source metadata for {@link getPointerPosition} — the most
1456
- * recently-written contributing source. */
1457
- getPointerPositionSource(actionName) {
1458
- return this.computePointerPosition(actionName).source;
1459
- }
1460
- /** Generic typed read (F1): dispatches to the matching typed getter based
1461
- * on `expectedType`, returning `ActionValueOf<T>` — a caller with a
1462
- * statically-known action-value-type map gets a fully-inferred, non-`any`
1463
- * return type. Throws the same mismatch error as the dedicated getters if
1464
- * the action's declared valueType disagrees with `expectedType`. */
1465
- readAction(actionName, expectedType) {
1466
- switch (expectedType) {
1467
- case 'digital':
1468
- return this.isPressed(actionName);
1469
- case 'scalar':
1470
- return this.getScalar(actionName);
1471
- case 'vector2':
1472
- return this.getVector2(actionName);
1473
- case 'pointerDelta':
1474
- return this.getPointerDelta(actionName);
1475
- case 'pointerPosition':
1476
- return this.getPointerPosition(actionName);
1477
- default:
1478
- throw new Error(`InputManager.readAction: unknown value type "${expectedType}"`);
1479
- }
1480
- }
1481
- /** Inject a synthetic scalar value for a named test source (F1's injected-
1482
- * test-input hook — also the shape of F2's "injected test input" backend).
1483
- * Bind an action to `{ type: 'test_axis', sourceId }` (`valueType:
1484
- * 'scalar'`) to read it back via {@link getScalar}. The value PERSISTS
1485
- * across frames (like a real analog stick held in position) until changed
1486
- * or cleared via {@link clearAxis}. */
1487
- injectAxis(sourceId, value) {
1488
- this.testAxisValues.set(sourceId, value);
1489
- }
1490
- /** Clear an injected scalar source (e.g. simulating an at-rest/disconnected
1491
- * device). */
1492
- clearAxis(sourceId) {
1493
- this.testAxisValues.delete(sourceId);
1494
- }
1495
- /** Inject a synthetic {x,y} value for a named test source. Bind an action
1496
- * to `{ type: 'test_vector2', sourceId }` (`valueType: 'vector2'`) to read
1497
- * it back via {@link getVector2}. Persists across frames until changed or
1498
- * cleared via {@link clearVector2}. */
1499
- injectVector2(sourceId, value) {
1500
- this.testVector2Values.set(sourceId, value);
1501
- }
1502
- /** Clear an injected Vector2 source. */
1503
- clearVector2(sourceId) {
1504
- this.testVector2Values.delete(sourceId);
1505
- }
1506
- /** Accumulate a synthetic pointer delta for a named test source — mirrors
1507
- * real mouse-move delta accumulation (`onMouseMove` above): multiple calls
1508
- * within the same frame sum, and the accumulator is cleared in
1509
- * {@link endFrame}. Bind an action to `{ type: 'test_pointer_delta',
1510
- * sourceId }` (`valueType: 'pointerDelta'`) to read it back via
1511
- * {@link getPointerDelta}. */
1512
- injectPointerDelta(sourceId, delta) {
1513
- const prev = this.testPointerDeltaAccum.get(sourceId) ?? { x: 0, y: 0 };
1514
- this.testPointerDeltaAccum.set(sourceId, { x: prev.x + delta.x, y: prev.y + delta.y });
1515
- }
1516
- /** Set a synthetic absolute pointer position for a named test source.
1517
- * Persists (last-write-wins across contributing sources on read — see the
1518
- * pointerPosition combine rule) until changed. Bind an action to `{ type:
1519
- * 'test_pointer_position', sourceId }` (`valueType: 'pointerPosition'`) to
1520
- * read it back via {@link getPointerPosition}. */
1521
- injectPointerPosition(sourceId, value) {
1522
- this.testPointerPositionValues.set(sourceId, { value, seq: ++this.pointerPositionSeq });
1523
- }
1524
- /** Whether input capture is currently enabled (see {@link setEnabled}). */
1525
- isEnabled() {
1526
- return this.enabled;
1527
- }
1528
- /**
1529
- * Enable/disable input capture. The editor host disables the running game's
1530
- * input while its viewport isn't focused so keystrokes don't leak in from
1531
- * the editor. Disabling clears all held/transient keyboard/mouse/touch-button
1532
- * state (via {@link flushHeldState}) so nothing sticks (e.g. a held movement
1533
- * key when you switch back to the Scene tab mid-flight) — gamepad state
1534
- * doesn't need clearing here since `poll()` itself stops reading it while
1535
- * disabled (see poll()'s doc comment).
1536
- */
1537
- setEnabled(enabled) {
1538
- if (this.enabled === enabled)
1539
- return;
1540
- this.enabled = enabled;
1541
- if (!enabled)
1542
- this.flushHeldState();
1543
- }
1544
- /** Whether the window/tab is currently focused (F2, spec §12 "focus
1545
- * gating") — `false` while blurred (real `blur`/`focus` events; see the
1546
- * constructor). Distinct from {@link isEnabled}, which is the editor's
1547
- * explicit play-mode gate. */
1548
- isFocused() {
1549
- return this.focused;
1550
- }
1551
- /** Whether the pointer is currently locked to {@link requestPointerLock}'s
1552
- * element (F2, spec §12 "pointer lock") — tracked via the real
1553
- * `pointerlockchange` event. */
1554
- isPointerLocked() {
1555
- return this.pointerLocked;
1556
- }
1557
- /** Device identity for a connected gamepad slot (F2, spec §12 "expose
1558
- * device identity" + "prefer the standard mapping"), or `null` if nothing
1559
- * is connected at `index` (including while input is disabled/unfocused,
1560
- * since `poll()` empties `this.gamepads` then). `mapping` is the Gamepad
1561
- * API's own `'standard'`/`''` value — bindings author button/axis indices
1562
- * assuming the W3C Standard Gamepad layout, so a non-`'standard'` mapping
1563
- * means those indices may not correspond as documented. */
1564
- getGamepadInfo(index) {
1565
- const gp = this.gamepads[index];
1566
- if (!gp)
1567
- return null;
1568
- return { index, id: gp.id, mapping: gp.mapping, connected: gp.connected };
1569
- }
1570
- /** Get mouse movement delta this frame */
1571
- getMouseDelta() {
1572
- return { x: this.mouseDeltaX, y: this.mouseDeltaY };
1573
- }
1574
- /**
1575
- * F4 (spec §12 "Schema and Examples" — the F3 prompt-switching gap): which
1576
- * device family most recently produced real input, or `null` before any
1577
- * input has arrived. A prompt UI polls this (or lets {@link getPrompt}
1578
- * consult it automatically by omitting `device`) to switch its displayed
1579
- * label as the player switches controllers — e.g. showing `'Space'` while
1580
- * they're on keyboard, then `'A'` the moment they touch a gamepad.
1581
- */
1582
- getLastActiveDevice() {
1583
- return this.lastActiveDevice;
1584
- }
1585
- // ==========================================================================
1586
- // F3 — rebinding: list/replace/add/remove/reset/persist. See the class doc
1587
- // comment above for the full design note.
1588
- // ==========================================================================
1589
- /** The current bindings for `actionName` (F3 AC "listed") — a defensive
1590
- * deep clone, so mutating the returned array/objects never affects the
1591
- * live binding list. `[]` for an unknown/never-registered action. */
1592
- getBindings(actionName) {
1593
- const bindings = this.actions.get(actionName);
1594
- return bindings ? structuredClone(bindings) : [];
1595
- }
1596
- /** Wholesale-replace `actionName`'s bindings array (F3 AC "replaced"). Does
1597
- * NOT touch the reset-to-defaults snapshot — see `resetBindings`. A no-op
1598
- * target for an action that was never registered is allowed (it simply
1599
- * registers one with `valueType: 'digital'`, matching `registerAction`'s
1600
- * own default) since a rebinding UI operates on actions it already knows
1601
- * about, but shouldn't have to special-case "brand new action" separately. */
1602
- setBindings(actionName, bindings) {
1603
- if (!this.actionValueTypes.has(actionName))
1604
- this.actionValueTypes.set(actionName, 'digital');
1605
- this.actions.set(actionName, structuredClone(bindings));
1606
- }
1607
- /** Append one binding to `actionName` (F3 AC "added"). */
1608
- addBinding(actionName, binding) {
1609
- const bindings = this.actions.get(actionName) ?? [];
1610
- this.actions.set(actionName, [...bindings, structuredClone(binding)]);
1611
- if (!this.actionValueTypes.has(actionName))
1612
- this.actionValueTypes.set(actionName, 'digital');
1613
- }
1614
- /** Remove the binding at `index` from `actionName` (F3 AC "removed"). A
1615
- * no-op if the action is unknown or `index` is out of range (rather than
1616
- * throwing) — mirrors every other getter's "unknown action is inert"
1617
- * convention, so a UI doesn't need a separate existence check first. */
1618
- removeBinding(actionName, index) {
1619
- const bindings = this.actions.get(actionName);
1620
- if (!bindings || index < 0 || index >= bindings.length)
1621
- return;
1622
- this.actions.set(actionName, [...bindings.slice(0, index), ...bindings.slice(index + 1)]);
1623
- }
1624
- /** Replace a single binding slot at `index` on `actionName` — the shape a
1625
- * rebinding UI actually wants ("rebind THIS control"), built on
1626
- * `getBindings`/`setBindings`. A no-op if the action is unknown or `index`
1627
- * is out of range. */
1628
- replaceBinding(actionName, index, binding) {
1629
- const bindings = this.actions.get(actionName);
1630
- if (!bindings || index < 0 || index >= bindings.length)
1631
- return;
1632
- const next = bindings.slice();
1633
- next[index] = structuredClone(binding);
1634
- this.actions.set(actionName, next);
1635
- }
1636
- /** Restore `actionName`'s bindings to the snapshot taken at
1637
- * `registerAction`/`loadMap`/`loadMapObject` time (F3 AC "reset"). A no-op
1638
- * for an action with no snapshot (never registered/loaded — there is no
1639
- * "default" to reset to). */
1640
- resetBindings(actionName) {
1641
- const defaults = this.defaultBindings.get(actionName);
1642
- if (!defaults)
1643
- return;
1644
- this.actions.set(actionName, structuredClone(defaults));
1645
- }
1646
- /** Reset every action that has a snapshot back to its registered/loaded
1647
- * defaults. Convenience batch form of `resetBindings`. */
1648
- resetAllBindings() {
1649
- for (const actionName of this.defaultBindings.keys())
1650
- this.resetBindings(actionName);
1651
- }
1652
- /**
1653
- * Whether `a` and `b` are the SAME physical input (F3's structural-conflict
1654
- * identity check — see `findConflicts`). Deliberately narrower than deep
1655
- * equality: `deadzone` never participates (two bindings on the same key
1656
- * with different deadzones are still the same physical control), and a
1657
- * `gamepad_axis`'s `direction` DOES participate — opposite directions of one
1658
- * physical axis (e.g. "accelerate" on positive, "brake" on negative) are a
1659
- * legitimate, non-conflicting pair, not a collision.
1660
- */
1661
- static bindingsCollide(a, b) {
1662
- if (a.type !== b.type)
1663
- return false;
1664
- switch (a.type) {
1665
- case 'key':
1666
- return a.code === b.code;
1667
- case 'mouse_button':
1668
- return a.button === b.button;
1669
- case 'mouse_move':
1670
- return true; // there is only one physical mouse-move source
1671
- case 'gamepad_button':
1672
- return a.button === b.button;
1673
- case 'gamepad_axis': {
1674
- const other = b;
1675
- return a.axis === other.axis && a.direction === other.direction;
1676
- }
1677
- case 'gamepad_axis_pair': {
1678
- const other = b;
1679
- return a.xAxis === other.xAxis && a.yAxis === other.yAxis;
1680
- }
1681
- case 'touch_button':
1682
- case 'touch_stick':
1683
- case 'test_axis':
1684
- case 'test_vector2':
1685
- case 'test_pointer_delta':
1686
- case 'test_pointer_position':
1687
- return a.sourceId === b.sourceId;
1688
- default:
1689
- return false;
1690
- }
1691
- }
1692
- /**
1693
- * Every EXISTING binding, on every OTHER registered action, that is
1694
- * structurally the same physical input as `proposed` (F3 AC "conflicts are
1695
- * detected and returned structurally") — never throws, always a plain
1696
- * (possibly empty) array a rebinding UI inspects before committing a
1697
- * rebind. `excludeActionName` is normally the action being rebound itself
1698
- * (so an action isn't reported as conflicting with its own current
1699
- * binding when re-proposing an unchanged value).
1700
- */
1701
- findConflicts(proposed, excludeActionName) {
1702
- const conflicts = [];
1703
- for (const [actionName, bindings] of this.actions) {
1704
- if (actionName === excludeActionName)
1705
- continue;
1706
- bindings.forEach((binding, bindingIndex) => {
1707
- if (InputManager.bindingsCollide(proposed, binding)) {
1708
- conflicts.push({ actionName, bindingIndex, binding: structuredClone(binding) });
1709
- }
1710
- });
1711
- }
1712
- return conflicts;
1713
- }
1714
- /**
1715
- * Serialize every registered/loaded action back into the `.inputmap.json`
1716
- * document shape (F3 AC "persisted") — the exact `InputMapFile` shape
1717
- * `loadMap`/`loadMapObject` read, so `manager.loadMapObject(manager.toInputMapFile())`
1718
- * on a fresh instance reproduces the same live bindings. `version` is
1719
- * carried forward from the last loaded map when known, else `1` (a
1720
- * manager built purely via `registerAction` calls, never `loadMap`, has no
1721
- * file-format version to preserve).
1722
- */
1723
- toInputMapFile() {
1724
- const actions = {};
1725
- for (const [name, bindings] of this.actions) {
1726
- actions[name] = {
1727
- valueType: this.actionValueTypes.get(name) ?? 'digital',
1728
- bindings: structuredClone(bindings),
1729
- };
1730
- }
1731
- return { version: this.loadedMapVersion, actions };
1732
- }
1733
- /**
1734
- * Resolve a display prompt (F3 AC: labels/icons like `'Space'`, `'A'`, or a
1735
- * touch control's name) for whichever of `actionName`'s bindings belongs to
1736
- * the requested `device` family. `null` when the action has no binding for
1737
- * that device (including an unknown action). `gamepadIndex` (default 0)
1738
- * selects which connected pad's `mapping`/connected state to consult for a
1739
- * `gamepad_*` binding — see `getGamepadInfo`.
1740
- *
1741
- * F4 — `device` is now OPTIONAL: omit it (or pass `undefined`) to resolve
1742
- * against {@link getLastActiveDevice} instead (falling back to `'keyboard'`
1743
- * before any input has arrived), so a prompt UI that wants "show whatever
1744
- * the player is actually holding right now" doesn't have to track the
1745
- * active device itself — it can just call `getPrompt(actionName)` every
1746
- * frame and the label switches on its own as the player changes controllers.
1747
- * Passing an explicit `device` (as every pre-F4 caller does) is unaffected.
1748
- */
1749
- getPrompt(actionName, device, gamepadIndex = 0) {
1750
- const resolvedDevice = device ?? this.lastActiveDevice ?? 'keyboard';
1751
- const bindings = this.actions.get(actionName);
1752
- if (!bindings)
1753
- return null;
1754
- const binding = bindings.find((b) => bindingDeviceFamily(b.type) === resolvedDevice);
1755
- if (!binding)
1756
- return null;
1757
- switch (binding.type) {
1758
- case 'key':
1759
- return {
1760
- device: 'keyboard',
1761
- bindingType: 'key',
1762
- label: keyLabel(binding.code),
1763
- icon: keyLabel(binding.code),
1764
- };
1765
- case 'mouse_button':
1766
- return {
1767
- device: 'mouse',
1768
- bindingType: 'mouse_button',
1769
- label: mouseButtonLabel(binding.button),
1770
- icon: 'mouse',
1771
- };
1772
- case 'mouse_move':
1773
- return { device: 'mouse', bindingType: 'mouse_move', label: 'Mouse', icon: 'mouse' };
1774
- case 'gamepad_button': {
1775
- const info = this.getGamepadInfo(gamepadIndex);
1776
- if (!info?.connected) {
1777
- return {
1778
- device: 'gamepad',
1779
- bindingType: 'gamepad_button',
1780
- label: 'Gamepad Disconnected',
1781
- icon: 'gamepad-off',
1782
- };
1783
- }
1784
- const label = info.mapping === 'standard'
1785
- ? (STANDARD_GAMEPAD_BUTTON_LABELS[binding.button] ?? `Button ${binding.button}`)
1786
- : `Button ${binding.button}`;
1787
- return { device: 'gamepad', bindingType: 'gamepad_button', label, icon: label };
1788
- }
1789
- case 'gamepad_axis': {
1790
- const info = this.getGamepadInfo(gamepadIndex);
1791
- if (!info?.connected) {
1792
- return {
1793
- device: 'gamepad',
1794
- bindingType: 'gamepad_axis',
1795
- label: 'Gamepad Disconnected',
1796
- icon: 'gamepad-off',
1797
- };
1798
- }
1799
- return {
1800
- device: 'gamepad',
1801
- bindingType: 'gamepad_axis',
1802
- label: gamepadAxisLabel(binding.axis),
1803
- icon: 'gamepad-stick',
1804
- };
1805
- }
1806
- case 'gamepad_axis_pair': {
1807
- const info = this.getGamepadInfo(gamepadIndex);
1808
- if (!info?.connected) {
1809
- return {
1810
- device: 'gamepad',
1811
- bindingType: 'gamepad_axis_pair',
1812
- label: 'Gamepad Disconnected',
1813
- icon: 'gamepad-off',
1814
- };
1815
- }
1816
- return {
1817
- device: 'gamepad',
1818
- bindingType: 'gamepad_axis_pair',
1819
- label: gamepadAxisPairLabel(binding.xAxis, binding.yAxis),
1820
- icon: 'gamepad-stick',
1821
- };
1822
- }
1823
- case 'touch_button':
1824
- return {
1825
- device: 'touch',
1826
- bindingType: 'touch_button',
1827
- label: binding.sourceId,
1828
- icon: 'touch',
1829
- };
1830
- case 'touch_stick':
1831
- return {
1832
- device: 'touch',
1833
- bindingType: 'touch_stick',
1834
- label: binding.sourceId,
1835
- icon: 'touch',
1836
- };
1837
- default:
1838
- return null;
1839
- }
1840
- }
1841
- /** Call at end of frame to clear per-frame state */
1842
- endFrame() {
1843
- this.keysJustDown.clear();
1844
- this.keysJustUp.clear();
1845
- this.mouseButtonsJustDown.clear();
1846
- this.mouseButtonsJustUp.clear();
1847
- // Carry this frame's gamepad pressed set forward as next frame's baseline,
1848
- // then clear the per-frame edge sets.
1849
- this.gamepadButtonsPrev = this.gamepadButtonsDown;
1850
- this.gamepadButtonsJustDown.clear();
1851
- this.gamepadButtonsJustUp.clear();
1852
- // Carry this frame's raw axis snapshot forward as next frame's baseline
1853
- // for gamepad_axis edge detection (isJustPressed/isJustReleased).
1854
- this.gamepadAxesPrev = this.gamepadAxesCurrent;
1855
- this.mouseDeltaX = 0;
1856
- this.mouseDeltaY = 0;
1857
- // F1 — pointerDelta is a per-frame accumulator, same shape as
1858
- // mouseDeltaX/Y above; axis/vector2/pointerPosition are level values and
1859
- // deliberately NOT reset here (they persist until changed/cleared).
1860
- this.testPointerDeltaAccum.clear();
1861
- // F2 — touch-button edges, same shape as the mouse-button edges above.
1862
- this.touchButtonsJustDown.clear();
1863
- this.touchButtonsJustUp.clear();
1864
- // Task 1.4 — a promoted tap is just-pressed for exactly one poll()/
1865
- // endFrame() bracket.
1866
- this.virtualDigitalTapActive.clear();
1867
- // Mirrors the line above for the release edge a direct
1868
- // `setVirtualAction(action, false)`/`clearVirtualActions()` manufactures
1869
- // (see `virtualDigitalJustReleased`'s doc comment) — just-released for
1870
- // exactly one poll()/endFrame() bracket too.
1871
- this.virtualDigitalJustReleased.clear();
1872
- }
1873
- /** Inject synthetic key state from touch controls */
1874
- setVirtualKey(code, pressed) {
1875
- if (pressed) {
1876
- if (!this.keysDown.has(code))
1877
- this.keysJustDown.add(code);
1878
- this.keysDown.add(code);
1879
- }
1880
- else {
1881
- if (this.keysDown.has(code))
1882
- this.keysJustUp.add(code);
1883
- this.keysDown.delete(code);
1884
- }
1885
- }
1886
- /** Set virtual look stick position (-1..1). Injected as mouse delta during poll(). */
1887
- setLookStick(x, y) {
1888
- this.lookStickX = x;
1889
- this.lookStickY = y;
1890
- }
1891
- /** Inject synthetic mouse delta (e.g. from touch controls) */
1892
- addMouseDelta(dx, dy) {
1893
- this.mouseDeltaX += dx;
1894
- this.mouseDeltaY += dy;
1895
- }
1896
- /**
1897
- * F2 (spec §12 "Complete Device Backends") — the touch/virtual-control
1898
- * digital backend. Set a named virtual button's (`sourceId` — a zone/knob
1899
- * id the touch UI chooses) pressed state; edge-tracked exactly like a real
1900
- * mouse button (`onMouseDown`/`onMouseUp` above) so `isJustPressed`/
1901
- * `isJustReleased` work for `{ type: 'touch_button', sourceId }` bindings.
1902
- * This is the injectable entry point a real on-screen button's own
1903
- * touchstart/touchend handler would call — that DOM wiring is a thin
1904
- * adapter at the edge (browser-verified separately); this method is the
1905
- * MAPPING logic, unit-tested headlessly with no DOM.
1906
- */
1907
- setTouchButton(sourceId, pressed) {
1908
- if (!this.inputActive)
1909
- return;
1910
- if (pressed) {
1911
- if (!this.touchButtonsDown.has(sourceId))
1912
- this.touchButtonsJustDown.add(sourceId);
1913
- this.touchButtonsDown.add(sourceId);
1914
- this.lastActiveDevice = 'touch'; // F4
1915
- }
1916
- else {
1917
- if (this.touchButtonsDown.has(sourceId))
1918
- this.touchButtonsJustUp.add(sourceId);
1919
- this.touchButtonsDown.delete(sourceId);
1920
- }
1921
- }
1922
- /**
1923
- * F2 — the touch/virtual-control analog backend. Set a named virtual
1924
- * stick's (`sourceId`) raw `{x,y}` position (e.g. -1..1 per axis, like a
1925
- * gamepad stick); read back via `getVector2` on a `{ type: 'touch_stick',
1926
- * sourceId }` binding, deadzone-rescaled the same way a gamepad stick is.
1927
- * Persists across frames (a level value, like a real analog stick held in
1928
- * position) until changed or cleared via {@link clearTouchStick}.
1929
- */
1930
- setTouchStick(sourceId, value) {
1931
- this.touchStickValues.set(sourceId, value);
1932
- // F4 — mirrors the gamepad-axis heuristic in poll() (see `isStickActive`).
1933
- if (InputManager.isStickActive(value))
1934
- this.lastActiveDevice = 'touch';
1935
- }
1936
- /** Clear a virtual stick's value (e.g. simulating the touch point lifting —
1937
- * the knob returns to center). */
1938
- clearTouchStick(sourceId) {
1939
- this.touchStickValues.delete(sourceId);
1940
- }
1941
- // ==========================================================================
1942
- // Task 1.4 (ACCEPTANCE-DRIVER-BUILD-PLAN.md / SYNTHETIC-PLAYER-SPEC.md §3.2)
1943
- // — action-level virtual input for a synthetic player. Promoted from
1944
- // hollowstone's proven `VirtualInput` (`bot/virtual-input.ts`): a bot drives
1945
- // NAMED ACTIONS (not raw devices), and its contributions join action
1946
- // resolution at exactly the same points binding contributions do (see
1947
- // isPressed/isJustPressed and collectScalarContributions/
1948
- // collectVector2Contributions above). #144 split the gates: virtual input
1949
- // passes `machineInputActive` (the editor's `enabled` flag only), while
1950
- // human input keeps the full `inputActive` (enabled AND focused) gate — a
1951
- // synthetic player must keep playing the shared editor tab while the
1952
- // human's focus is elsewhere. Unlike every typed getter above (which
1953
- // treats an unknown action as inert), these THROW on an unknown name — a
1954
- // synthetic-player caller needs a loud signal that the action it's driving
1955
- // doesn't exist, not a silently-ignored actuation.
1956
- // ==========================================================================
1957
- /** Existence check shared by `setVirtualAction`/`tapVirtualAction` — throws
1958
- * `InputActionError` (`code: 'INPUT_ACTION_NOT_FOUND'`, `data.registered`
1959
- * = every declared action name) for a name that was never
1960
- * `registerAction`/`loadMap`-ed. */
1961
- assertActionExists(actionName, method) {
1962
- if (!this.actionValueTypes.has(actionName)) {
1963
- throw new InputActionError(actionName, method, this.actionNames());
1964
- }
1965
- }
1966
- /** Which declared `ActionValueType` a `setVirtualAction` value shape
1967
- * corresponds to — the type-safety check mirrors every typed getter's
1968
- * `assertValueType` (F1). `pointerDelta`/`pointerPosition` actions aren't
1969
- * drivable through `setVirtualAction` (no boolean/number/Vector2 call
1970
- * shape maps to them) — use `injectPointerDelta`/`injectPointerPosition`. */
1971
- static virtualValueCategory(value) {
1972
- if (typeof value === 'boolean')
1973
- return 'digital';
1974
- if (typeof value === 'number')
1975
- return 'scalar';
1976
- return 'vector2';
1977
- }
1978
- /**
1979
- * Set a virtual value for `action`, OR'd/summed into the same action reads
1980
- * humans drive (see the section doc comment above). A `boolean` holds a
1981
- * digital action pressed until set `false`/cleared; a `number` or `{x,y}`
1982
- * is a level value combined via the action's own scalar max-magnitude /
1983
- * vector2 sum-then-clamp rule.
1984
- *
1985
- * A digital `false`→`true` transition manufactures a genuine `isJustPressed`
1986
- * edge, exactly one real key-down does (pulse-runner friction #4 —
1987
- * previously this write-half only ever held the action pressed with NO
1988
- * edge at all; every edge-triggered ability driven through this direct
1989
- * path was unprovable). Re-setting `true` while already held (auto-repeat)
1990
- * does NOT re-fire it — see {@link applyVirtualActionValue}. The mirror
1991
- * `true`→`false` transition manufactures an `isJustReleased` edge the same
1992
- * way a real key-up does.
1993
- *
1994
- * Throws `InputActionError` for an unregistered `action`, and the same
1995
- * valueType-mismatch error every typed getter throws (`assertValueType`)
1996
- * when `value`'s shape doesn't match the action's declared `valueType`.
1997
- *
1998
- * Gated (`setEnabled(false)` — play stopped or editor-gated; #144: window
1999
- * focus no longer gates machine input): writes NOTHING — matching
2000
- * `setTouchButton`'s write-gate precedent above — and returns
2001
- * `{delivered: false, reason}` rather than a silent no-op, so a probe/bot
2002
- * never mistakes a swallowed actuation for a delivered one.
2003
- */
2004
- setVirtualAction(action, value) {
2005
- this.assertActionExists(action, 'setVirtualAction');
2006
- const expected = InputManager.virtualValueCategory(value);
2007
- this.assertValueType(action, expected, 'setVirtualAction');
2008
- if (!this.machineInputActive) {
2009
- return { delivered: false, reason: 'input-gated (input disabled by the editor)' };
2010
- }
2011
- this.applyVirtualActionValue(action, value);
2012
- return { delivered: true };
2013
- }
2014
- /**
2015
- * The write half of {@link setVirtualAction} — extracted so D15/T-D15.5's
2016
- * `scheduleActionAtTick` can apply a non-boolean (scalar/vector2) queued
2017
- * actuation through the exact same code path (no gate check here: the two
2018
- * callers gate differently — `setVirtualAction` above checks
2019
- * `inputActive` before calling this; `applyScheduledActionsForTick` is
2020
- * only ever reached from inside `poll()` AFTER its own gate check has
2021
- * already returned early on a gated tick). `applyScheduledActionsForTick`
2022
- * still handles digital `true` itself rather than calling here (its
2023
- * one-tick edge must land EXACTLY at the scheduled tick — see its own doc
2024
- * comment) — this method's digital branch is `setVirtualAction`'s direct
2025
- * path, called at an arbitrary point relative to the poll()/endFrame()
2026
- * bracket.
2027
- *
2028
- * pulse-runner friction #4 — the digital branch used to be a bare held-flag
2029
- * write with no manufactured edge at all (the comment that used to sit
2030
- * here read: "A digital value is handled by `applyScheduledActionsForTick`
2031
- * itself (it needs the extra just-pressed edge a plain held-value write
2032
- * doesn't produce)" — i.e. the edge fix shipped ONLY for the scheduled
2033
- * path, never for this one, which is exactly the gap a blind dogfood run
2034
- * hit: `setVirtualAction('jump', true)` held the action down but
2035
- * `isJustPressed('jump')` never fired). Fixed here by reusing
2036
- * `tapVirtualAction`'s exact one-tick-pulse queue mechanism for this
2037
- * DIRECT case, specifically because this method's caller can land at any
2038
- * point relative to `poll()`: writing `virtualDigitalTapActive` directly
2039
- * here (as the scheduled path safely does, from inside `poll()` itself)
2040
- * would risk being silently wiped by `poll()`'s own unconditional
2041
- * `virtualDigitalTapActive = virtualDigitalTapQueued` reassignment,
2042
- * depending on timing. Queueing side-steps that hazard by construction —
2043
- * the same reason `tapVirtualAction` queues instead of writing directly. A
2044
- * redundant `true` while already held (`virtualDigitalHeld` already has
2045
- * `action`) does not queue again, matching a real key's auto-repeat never
2046
- * re-firing `isJustPressed`. The `false` branch mirrors real `onKeyUp`
2047
- * instead (immediate write, no queue needed — nothing in `poll()`
2048
- * reassigns `virtualDigitalJustReleased`), and only fires when the action
2049
- * was actually held (no released→released re-fire).
2050
- */
2051
- applyVirtualActionValue(action, value) {
2052
- switch (InputManager.virtualValueCategory(value)) {
2053
- case 'digital':
2054
- if (value) {
2055
- if (!this.virtualDigitalHeld.has(action))
2056
- this.virtualDigitalTapQueued.add(action);
2057
- this.virtualDigitalHeld.add(action);
2058
- }
2059
- else {
2060
- if (this.virtualDigitalHeld.has(action))
2061
- this.virtualDigitalJustReleased.add(action);
2062
- this.virtualDigitalHeld.delete(action);
2063
- }
2064
- break;
2065
- case 'scalar':
2066
- this.virtualScalarValues.set(action, value);
2067
- break;
2068
- default:
2069
- this.virtualVector2Values.set(action, value);
2070
- break;
2071
- }
2072
- }
2073
- /**
2074
- * One-frame press of a `'digital'` action (Task 1.4): queues `action` so it
2075
- * reads `isJustPressed`/`isPressed` true for EXACTLY the next `poll()`'s
2076
- * frame — promoted to the "active" set at the top of `poll()`, cleared in
2077
- * `endFrame()` alongside every other just-pressed edge set (see both
2078
- * above). Hollowstone's `queued`/`beginFrame` tap contract, ported onto
2079
- * this engine's existing fixed-step `poll()`/`endFrame()` bracket instead
2080
- * of a bespoke `beginFrame()` hook.
2081
- *
2082
- * Throws the same errors as {@link setVirtualAction} (unknown action;
2083
- * non-digital valueType). A gated tap does NOT queue — it must not fire on
2084
- * a later frame once input becomes active again — and returns
2085
- * `{delivered: false, reason}`.
2086
- */
2087
- tapVirtualAction(action) {
2088
- this.assertActionExists(action, 'tapVirtualAction');
2089
- this.assertValueType(action, 'digital', 'tapVirtualAction');
2090
- if (!this.machineInputActive) {
2091
- return { delivered: false, reason: 'input-gated (input disabled by the editor)' };
2092
- }
2093
- this.virtualDigitalTapQueued.add(action);
2094
- return { delivered: true };
2095
- }
2096
- /** Clear all virtual-action state — held digitals, queued/active taps, and
2097
- * scalar/vector2 values. `flushHeldState()` (the same flush
2098
- * `setEnabled(false)`/real blur apply to held keys/mouse/touch buttons)
2099
- * shares the underlying wipe, so a bot's held action never survives input
2100
- * being suspended — but only THIS public entry point (a deliberate "let
2101
- * go", the mirror of `setVirtualAction(action, false)` below) manufactures
2102
- * an `isJustReleased` edge for whatever was held; the suspend path stays
2103
- * silent (see `flushHeldState`'s own doc comment). */
2104
- clearVirtualActions() {
2105
- this.clearVirtualActionState({ manufactureReleaseEdge: true });
2106
- }
2107
- /**
2108
- * D15/T-D15.5 (`docs/D15-DETERMINISM-DESIGN.md` §2.c) — schedule a virtual
2109
- * actuation for exactly `tick`: applied at the START of that tick's
2110
- * `poll()` (composing with `runTicks` — a schedule for tick 500 fires
2111
- * exactly once the sim has been driven through tick 500, regardless of
2112
- * burst size). A digital `true` produces a genuine `isJustPressed` edge
2113
- * exactly at the target tick (not merely a held value — see
2114
- * `applyScheduledActionsForTick`).
2115
- *
2116
- * Throws `InputActionError` for an unregistered `action` and the same
2117
- * valueType-mismatch error every typed getter throws (`assertValueType`) —
2118
- * both checked EAGERLY, at schedule time, not deferred to the target tick.
2119
- * Throws `InputTickError` (`code: 'TICK_ALREADY_PASSED'`) when `tick` is
2120
- * strictly less than the tick `poll()` will service NEXT — scheduling only
2121
- * ever reaches the current or a future tick. A tick whose input phase
2122
- * never runs (this world paused/frozen at that point, or a multi-tick gap
2123
- * — see `poll()`) drops its scheduled entries rather than replaying them
2124
- * late — each drop emits one `'input.schedule.dropped'` debug event
2125
- * (`{tick, action}`) via {@link setDebugEmit}'s sink, if one is wired.
2126
- */
2127
- scheduleActionAtTick(tick, action, value) {
2128
- this.assertActionExists(action, 'scheduleActionAtTick');
2129
- const expected = InputManager.virtualValueCategory(value);
2130
- this.assertValueType(action, expected, 'scheduleActionAtTick');
2131
- if (tick < this.currentTick) {
2132
- throw new InputTickError(tick, this.currentTick);
2133
- }
2134
- const pending = this.scheduledActions.get(tick);
2135
- if (pending)
2136
- pending.push({ action, value });
2137
- else
2138
- this.scheduledActions.set(tick, [{ action, value }]);
2139
- }
2140
- /** Wire (or clear, via `null`) the sink `poll()` calls with
2141
- * `'input.schedule.dropped'` (`{tick, action}`) whenever it discards a
2142
- * scheduled entry for a tick whose input phase never ran. Called once by
2143
- * whoever constructs this `InputManager` with a `DebugRegistry` behind it
2144
- * (`vgai-scene-game-adapter.ts`), the same seed spot as
2145
- * `setVirtualInputTarget`/`setInputActionsSource` — never called directly
2146
- * by gameplay code. */
2147
- setDebugEmit(fn) {
2148
- this.debugEmit = fn;
2149
- }
2150
- /** D15/T-D15.5 — start (or restart) recording the post-gate action-delta
2151
- * trace: clears any prior trace/snapshot, so a fresh recording session
2152
- * never carries over stale deltas from an earlier one. */
2153
- startInputRecording() {
2154
- this.inputRecording = true;
2155
- this.inputTrace = [];
2156
- this.lastTraceSnapshot.clear();
2157
- }
2158
- /** D15/T-D15.5 — stop recording (the trace accumulated so far stays
2159
- * readable via {@link getInputTrace} until the next `startInputRecording`
2160
- * clears it). Idempotent. */
2161
- stopInputRecording() {
2162
- this.inputRecording = false;
2163
- }
2164
- /** D15/T-D15.5 — whether a recording is currently active. */
2165
- isInputRecording() {
2166
- return this.inputRecording;
2167
- }
2168
- /** D15/T-D15.5 — the recorded trace so far (a defensive copy), versioned
2169
- * per the design doc's format sketch (§2.c). SP5 (out of scope here) is
2170
- * the eventual consumer that replays this; this module only ever
2171
- * RECORDS. */
2172
- getInputTrace() {
2173
- return { version: 1, ticks: this.inputTrace.slice() };
2174
- }
2175
- /** Request pointer lock for FPS-style mouse control */
2176
- requestPointerLock(element) {
2177
- // Idempotent: never stack duplicate click listeners. Rewire if the element
2178
- // changed, and track it so dispose() can remove the listener.
2179
- if (this.pointerLockElement === element)
2180
- return;
2181
- if (this.pointerLockElement) {
2182
- this.pointerLockElement.removeEventListener('click', this.onPointerLockClick);
2183
- }
2184
- this.pointerLockElement = element;
2185
- element.addEventListener('click', this.onPointerLockClick);
2186
- }
2187
- /** Clean up event listeners */
2188
- dispose() {
2189
- if (this.disposed)
2190
- return;
2191
- this.disposed = true;
2192
- window.removeEventListener('keydown', this.onKeyDown);
2193
- window.removeEventListener('keyup', this.onKeyUp);
2194
- window.removeEventListener('mousedown', this.onMouseDown);
2195
- window.removeEventListener('mouseup', this.onMouseUp);
2196
- window.removeEventListener('mousemove', this.onMouseMove);
2197
- window.removeEventListener('blur', this.onWindowBlur);
2198
- window.removeEventListener('focus', this.onWindowFocus);
2199
- if (typeof document !== 'undefined' && typeof document.removeEventListener === 'function') {
2200
- document.removeEventListener('pointerlockchange', this.onPointerLockChange);
2201
- }
2202
- if (this.pointerLockElement) {
2203
- this.pointerLockElement.removeEventListener('click', this.onPointerLockClick);
2204
- this.pointerLockElement = null;
2205
- }
2206
- }
2207
- }