@vgai/engine 0.3.0 → 0.4.0-canary.20260715.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (559) hide show
  1. package/package.json +8 -2
  2. package/schemas/engine-api.json +124 -0
  3. package/schemas/engine-api.md +53 -0
  4. package/schemas/engine-capabilities.json +124 -0
  5. package/schemas/inputmap.schema.json +314 -0
  6. package/schemas/mat.schema.json +286 -0
  7. package/schemas/prefab.schema.json +10148 -0
  8. package/schemas/scn2d.schema.json +475 -0
  9. package/schemas/vgai-game.schema.json +383 -0
  10. package/schemas/vscn.schema.json +11007 -0
  11. package/src/adapter/{world-kind.ts → adapter-surface.ts} +6 -6
  12. package/src/adapter/authoring.ts +67 -0
  13. package/src/adapter/game-adapter.ts +8 -8
  14. package/src/adapter/host-context.ts +2 -4
  15. package/src/adapter/index.ts +3 -1
  16. package/src/adapter/system-adapter.ts +73 -0
  17. package/src/adapter/vgai-scene-game-adapter.ts +239 -180
  18. package/src/animation/xstate-animation-binding.ts +300 -262
  19. package/src/animation/xstate-animation-meta.ts +50 -13
  20. package/src/character/cloth-sim.ts +533 -0
  21. package/src/character/spring-chain.ts +307 -0
  22. package/src/core/game-loop.ts +41 -3
  23. package/src/core/seeded-random.ts +161 -0
  24. package/src/core/system-runner.ts +20 -3
  25. package/src/core/types.ts +37 -0
  26. package/src/data/data-asset.ts +1 -1
  27. package/src/dev/performance-profiler.ts +213 -0
  28. package/src/dev/webgl-gpu-timer.ts +53 -0
  29. package/src/ecs/component-manager.ts +45 -12
  30. package/src/ecs/game-component.ts +95 -11
  31. package/src/humanoid/bake.operation.ts +326 -0
  32. package/src/humanoid/body.ts +663 -0
  33. package/src/humanoid/clips.ts +149 -0
  34. package/src/humanoid/compose.ts +209 -0
  35. package/src/humanoid/generate.ts +189 -0
  36. package/src/humanoid/index.ts +36 -0
  37. package/src/humanoid/schema.ts +108 -0
  38. package/src/humanoid/skeleton.ts +345 -0
  39. package/src/index.ts +5 -5
  40. package/src/input/input-manager.ts +701 -22
  41. package/src/input/input-types.ts +5 -2
  42. package/src/manifest/index.ts +5 -5
  43. package/src/manifest/load.ts +125 -72
  44. package/src/manifest/schema.ts +362 -255
  45. package/src/react/game-state.tsx +135 -32
  46. package/src/react/root-adapter.tsx +49 -0
  47. package/src/react/unmanaged-root-detector.ts +66 -0
  48. package/src/react/use-data.ts +12 -2
  49. package/src/runtime/create-runtime.ts +100 -282
  50. package/src/runtime/debug-bridge.ts +483 -0
  51. package/src/runtime/debug-registry.ts +856 -0
  52. package/src/runtime/game.ts +342 -93
  53. package/src/runtime/gameplay-rng-trap.ts +134 -0
  54. package/src/runtime/input-router.ts +7 -7
  55. package/src/runtime/mount-game.ts +40 -38
  56. package/src/runtime/mount-manifest.ts +169 -37
  57. package/src/runtime/render-control.ts +13 -13
  58. package/src/runtime/render-seed.ts +1 -1
  59. package/src/runtime/state-bridge.ts +24 -10
  60. package/src/runtime/types.ts +110 -28
  61. package/src/scene/asset-registry.ts +22 -0
  62. package/src/scene/component-registry.ts +14 -3
  63. package/src/scene/defaults.ts +1 -0
  64. package/src/scene/light-camera-factory.ts +11 -3
  65. package/src/scene/scene-apply.ts +55 -4
  66. package/src/scene/scene-loader.ts +10 -27
  67. package/src/scene/scene-types.ts +0 -1
  68. package/src/scene/schema/index.ts +0 -34
  69. package/src/scene/schema/light.ts +16 -1
  70. package/src/scene/schema/material.ts +96 -91
  71. package/src/scene/schema/scene-file.ts +1 -7
  72. package/src/scene/user-data.ts +14 -1
  73. package/src/setup/setup-renderer.ts +1 -1
  74. package/src/tools/define-tool.ts +50 -11
  75. package/src/world2d/authoring-2d.ts +17 -1
  76. package/src/world2d/collision-2d.ts +1 -1
  77. package/src/world2d/pixi-game-adapter.ts +19 -17
  78. package/src/world2d/scene2d-loader.ts +1 -0
  79. package/src/world2d/types.ts +8 -2
  80. package/dist/adapter/authoring.d.ts +0 -404
  81. package/dist/adapter/authoring.d.ts.map +0 -1
  82. package/dist/adapter/authoring.js +0 -22
  83. package/dist/adapter/colyseus-networking-adapter.d.ts +0 -43
  84. package/dist/adapter/colyseus-networking-adapter.d.ts.map +0 -1
  85. package/dist/adapter/colyseus-networking-adapter.js +0 -38
  86. package/dist/adapter/first-party-systems.d.ts +0 -38
  87. package/dist/adapter/first-party-systems.d.ts.map +0 -1
  88. package/dist/adapter/first-party-systems.js +0 -77
  89. package/dist/adapter/game-adapter.d.ts +0 -133
  90. package/dist/adapter/game-adapter.d.ts.map +0 -1
  91. package/dist/adapter/game-adapter.js +0 -10
  92. package/dist/adapter/host-context.d.ts +0 -73
  93. package/dist/adapter/host-context.d.ts.map +0 -1
  94. package/dist/adapter/host-context.js +0 -16
  95. package/dist/adapter/index.d.ts +0 -22
  96. package/dist/adapter/index.d.ts.map +0 -1
  97. package/dist/adapter/index.js +0 -15
  98. package/dist/adapter/ingest/game-contract.d.ts +0 -53
  99. package/dist/adapter/ingest/game-contract.d.ts.map +0 -1
  100. package/dist/adapter/ingest/game-contract.js +0 -30
  101. package/dist/adapter/ingest/overlay-applier.d.ts +0 -118
  102. package/dist/adapter/ingest/overlay-applier.d.ts.map +0 -1
  103. package/dist/adapter/ingest/overlay-applier.js +0 -132
  104. package/dist/adapter/ingest/overlay-apply.d.ts +0 -60
  105. package/dist/adapter/ingest/overlay-apply.d.ts.map +0 -1
  106. package/dist/adapter/ingest/overlay-apply.js +0 -101
  107. package/dist/adapter/ingest/overlay-file.d.ts +0 -79
  108. package/dist/adapter/ingest/overlay-file.d.ts.map +0 -1
  109. package/dist/adapter/ingest/overlay-file.js +0 -66
  110. package/dist/adapter/ingest/overlay-report.d.ts +0 -113
  111. package/dist/adapter/ingest/overlay-report.d.ts.map +0 -1
  112. package/dist/adapter/ingest/overlay-report.js +0 -103
  113. package/dist/adapter/ingest/scene-capture.d.ts +0 -97
  114. package/dist/adapter/ingest/scene-capture.d.ts.map +0 -1
  115. package/dist/adapter/ingest/scene-capture.js +0 -248
  116. package/dist/adapter/ingest/upstream-pin.d.ts +0 -47
  117. package/dist/adapter/ingest/upstream-pin.d.ts.map +0 -1
  118. package/dist/adapter/ingest/upstream-pin.js +0 -50
  119. package/dist/adapter/loop-gate-report.d.ts +0 -34
  120. package/dist/adapter/loop-gate-report.d.ts.map +0 -1
  121. package/dist/adapter/loop-gate-report.js +0 -37
  122. package/dist/adapter/rapier-physics-adapter.d.ts +0 -12
  123. package/dist/adapter/rapier-physics-adapter.d.ts.map +0 -1
  124. package/dist/adapter/rapier-physics-adapter.js +0 -46
  125. package/dist/adapter/system-adapter.d.ts +0 -133
  126. package/dist/adapter/system-adapter.d.ts.map +0 -1
  127. package/dist/adapter/system-adapter.js +0 -13
  128. package/dist/adapter/transform.d.ts +0 -18
  129. package/dist/adapter/transform.d.ts.map +0 -1
  130. package/dist/adapter/transform.js +0 -1
  131. package/dist/adapter/vgai-scene-game-adapter.d.ts +0 -116
  132. package/dist/adapter/vgai-scene-game-adapter.d.ts.map +0 -1
  133. package/dist/adapter/vgai-scene-game-adapter.js +0 -714
  134. package/dist/adapter/world-kind.d.ts +0 -30
  135. package/dist/adapter/world-kind.d.ts.map +0 -1
  136. package/dist/adapter/world-kind.js +0 -18
  137. package/dist/ai/navigation.d.ts +0 -64
  138. package/dist/ai/navigation.d.ts.map +0 -1
  139. package/dist/ai/navigation.js +0 -133
  140. package/dist/animation/anim-graph-types.d.ts +0 -25
  141. package/dist/animation/anim-graph-types.d.ts.map +0 -1
  142. package/dist/animation/anim-graph-types.js +0 -1
  143. package/dist/animation/animation-clock.d.ts +0 -213
  144. package/dist/animation/animation-clock.d.ts.map +0 -1
  145. package/dist/animation/animation-clock.js +0 -301
  146. package/dist/animation/blend-node.d.ts +0 -22
  147. package/dist/animation/blend-node.d.ts.map +0 -1
  148. package/dist/animation/blend-node.js +0 -97
  149. package/dist/animation/camera-ownership.d.ts +0 -250
  150. package/dist/animation/camera-ownership.d.ts.map +0 -1
  151. package/dist/animation/camera-ownership.js +0 -169
  152. package/dist/animation/cinematic-cues.d.ts +0 -200
  153. package/dist/animation/cinematic-cues.d.ts.map +0 -1
  154. package/dist/animation/cinematic-cues.js +0 -213
  155. package/dist/animation/clip-map.d.ts +0 -12
  156. package/dist/animation/clip-map.d.ts.map +0 -1
  157. package/dist/animation/clip-map.js +0 -37
  158. package/dist/animation/gsap-registration.d.ts +0 -119
  159. package/dist/animation/gsap-registration.d.ts.map +0 -1
  160. package/dist/animation/gsap-registration.js +0 -92
  161. package/dist/animation/theatre-clock-binding.d.ts +0 -89
  162. package/dist/animation/theatre-clock-binding.d.ts.map +0 -1
  163. package/dist/animation/theatre-clock-binding.js +0 -29
  164. package/dist/animation/theatre-director.d.ts +0 -242
  165. package/dist/animation/theatre-director.d.ts.map +0 -1
  166. package/dist/animation/theatre-director.js +0 -235
  167. package/dist/animation/theatre-object-binding.d.ts +0 -305
  168. package/dist/animation/theatre-object-binding.d.ts.map +0 -1
  169. package/dist/animation/theatre-object-binding.js +0 -351
  170. package/dist/animation/xstate-animation-binding.d.ts +0 -111
  171. package/dist/animation/xstate-animation-binding.d.ts.map +0 -1
  172. package/dist/animation/xstate-animation-binding.js +0 -307
  173. package/dist/animation/xstate-animation-meta.d.ts +0 -228
  174. package/dist/animation/xstate-animation-meta.d.ts.map +0 -1
  175. package/dist/animation/xstate-animation-meta.js +0 -221
  176. package/dist/assets.d.ts +0 -26
  177. package/dist/assets.d.ts.map +0 -1
  178. package/dist/assets.js +0 -55
  179. package/dist/audio/index.d.ts +0 -4
  180. package/dist/audio/index.d.ts.map +0 -1
  181. package/dist/audio/index.js +0 -24
  182. package/dist/audio/tone-clock-binding.d.ts +0 -75
  183. package/dist/audio/tone-clock-binding.d.ts.map +0 -1
  184. package/dist/audio/tone-clock-binding.js +0 -29
  185. package/dist/audio/tone-context.d.ts +0 -114
  186. package/dist/audio/tone-context.d.ts.map +0 -1
  187. package/dist/audio/tone-context.js +0 -39
  188. package/dist/audio/tone-offline-render.d.ts +0 -90
  189. package/dist/audio/tone-offline-render.d.ts.map +0 -1
  190. package/dist/audio/tone-offline-render.js +0 -56
  191. package/dist/audio/wav-encode.d.ts +0 -38
  192. package/dist/audio/wav-encode.d.ts.map +0 -1
  193. package/dist/audio/wav-encode.js +0 -97
  194. package/dist/core/game-loop.d.ts +0 -21
  195. package/dist/core/game-loop.d.ts.map +0 -1
  196. package/dist/core/game-loop.js +0 -130
  197. package/dist/core/system-runner.d.ts +0 -120
  198. package/dist/core/system-runner.d.ts.map +0 -1
  199. package/dist/core/system-runner.js +0 -286
  200. package/dist/core/types.d.ts +0 -58
  201. package/dist/core/types.d.ts.map +0 -1
  202. package/dist/core/types.js +0 -25
  203. package/dist/data/data-asset.d.ts +0 -101
  204. package/dist/data/data-asset.d.ts.map +0 -1
  205. package/dist/data/data-asset.js +0 -122
  206. package/dist/data/data-check-core.d.ts +0 -110
  207. package/dist/data/data-check-core.d.ts.map +0 -1
  208. package/dist/data/data-check-core.js +0 -178
  209. package/dist/data/data-ref.d.ts +0 -79
  210. package/dist/data/data-ref.d.ts.map +0 -1
  211. package/dist/data/data-ref.js +0 -130
  212. package/dist/data/vite-plugin-data.d.ts +0 -79
  213. package/dist/data/vite-plugin-data.d.ts.map +0 -1
  214. package/dist/data/vite-plugin-data.js +0 -217
  215. package/dist/dev/console-bridge.d.ts +0 -22
  216. package/dist/dev/console-bridge.d.ts.map +0 -1
  217. package/dist/dev/console-bridge.js +0 -71
  218. package/dist/dev/debug-draw.d.ts +0 -24
  219. package/dist/dev/debug-draw.d.ts.map +0 -1
  220. package/dist/dev/debug-draw.js +0 -73
  221. package/dist/dev/logger.d.ts +0 -30
  222. package/dist/dev/logger.d.ts.map +0 -1
  223. package/dist/dev/logger.js +0 -90
  224. package/dist/ecs/component-manager.d.ts +0 -189
  225. package/dist/ecs/component-manager.d.ts.map +0 -1
  226. package/dist/ecs/component-manager.js +0 -676
  227. package/dist/ecs/game-component.d.ts +0 -97
  228. package/dist/ecs/game-component.d.ts.map +0 -1
  229. package/dist/ecs/game-component.js +0 -80
  230. package/dist/ecs/hmr-swap-report.d.ts +0 -53
  231. package/dist/ecs/hmr-swap-report.d.ts.map +0 -1
  232. package/dist/ecs/hmr-swap-report.js +0 -55
  233. package/dist/index.d.ts +0 -37
  234. package/dist/index.d.ts.map +0 -1
  235. package/dist/index.js +0 -30
  236. package/dist/input/input-manager.d.ts +0 -596
  237. package/dist/input/input-manager.d.ts.map +0 -1
  238. package/dist/input/input-manager.js +0 -1564
  239. package/dist/input/input-types.d.ts +0 -171
  240. package/dist/input/input-types.d.ts.map +0 -1
  241. package/dist/input/input-types.js +0 -1
  242. package/dist/input/prompt-labels.d.ts +0 -21
  243. package/dist/input/prompt-labels.d.ts.map +0 -1
  244. package/dist/input/prompt-labels.js +0 -121
  245. package/dist/input/rebind-controller.d.ts +0 -59
  246. package/dist/input/rebind-controller.d.ts.map +0 -1
  247. package/dist/input/rebind-controller.js +0 -85
  248. package/dist/input/schema.d.ts +0 -16
  249. package/dist/input/schema.d.ts.map +0 -1
  250. package/dist/input/schema.js +0 -228
  251. package/dist/loader.d.ts +0 -43
  252. package/dist/loader.d.ts.map +0 -1
  253. package/dist/loader.js +0 -59
  254. package/dist/manifest/index.d.ts +0 -5
  255. package/dist/manifest/index.d.ts.map +0 -1
  256. package/dist/manifest/index.js +0 -8
  257. package/dist/manifest/load-file.d.ts +0 -4
  258. package/dist/manifest/load-file.d.ts.map +0 -1
  259. package/dist/manifest/load-file.js +0 -14
  260. package/dist/manifest/load.d.ts +0 -117
  261. package/dist/manifest/load.d.ts.map +0 -1
  262. package/dist/manifest/load.js +0 -255
  263. package/dist/manifest/schema.d.ts +0 -173
  264. package/dist/manifest/schema.d.ts.map +0 -1
  265. package/dist/manifest/schema.js +0 -302
  266. package/dist/physics/collision-system.d.ts +0 -41
  267. package/dist/physics/collision-system.d.ts.map +0 -1
  268. package/dist/physics/collision-system.js +0 -64
  269. package/dist/physics/physics-registry.d.ts +0 -45
  270. package/dist/physics/physics-registry.d.ts.map +0 -1
  271. package/dist/physics/physics-registry.js +0 -65
  272. package/dist/physics/transform-writer.d.ts +0 -16
  273. package/dist/physics/transform-writer.d.ts.map +0 -1
  274. package/dist/physics/transform-writer.js +0 -38
  275. package/dist/physics/trigger-dispatch.d.ts +0 -24
  276. package/dist/physics/trigger-dispatch.d.ts.map +0 -1
  277. package/dist/physics/trigger-dispatch.js +0 -80
  278. package/dist/react/game-state.d.ts +0 -105
  279. package/dist/react/game-state.d.ts.map +0 -1
  280. package/dist/react/game-state.js +0 -144
  281. package/dist/react/use-data.d.ts +0 -60
  282. package/dist/react/use-data.d.ts.map +0 -1
  283. package/dist/react/use-data.js +0 -96
  284. package/dist/react/use-selection.d.ts +0 -75
  285. package/dist/react/use-selection.d.ts.map +0 -1
  286. package/dist/react/use-selection.js +0 -90
  287. package/dist/render/auto-batcher.d.ts +0 -34
  288. package/dist/render/auto-batcher.d.ts.map +0 -1
  289. package/dist/render/auto-batcher.js +0 -140
  290. package/dist/render/render-batch-system.d.ts +0 -33
  291. package/dist/render/render-batch-system.d.ts.map +0 -1
  292. package/dist/render/render-batch-system.js +0 -230
  293. package/dist/render/render-features.d.ts +0 -47
  294. package/dist/render/render-features.d.ts.map +0 -1
  295. package/dist/render/render-features.js +0 -105
  296. package/dist/render/render-settings.d.ts +0 -36
  297. package/dist/render/render-settings.d.ts.map +0 -1
  298. package/dist/render/render-settings.js +0 -51
  299. package/dist/runtime/create-runtime.d.ts +0 -280
  300. package/dist/runtime/create-runtime.d.ts.map +0 -1
  301. package/dist/runtime/create-runtime.js +0 -795
  302. package/dist/runtime/frame-selector-cache.d.ts +0 -41
  303. package/dist/runtime/frame-selector-cache.d.ts.map +0 -1
  304. package/dist/runtime/frame-selector-cache.js +0 -65
  305. package/dist/runtime/game.d.ts +0 -433
  306. package/dist/runtime/game.d.ts.map +0 -1
  307. package/dist/runtime/game.js +0 -569
  308. package/dist/runtime/input-router.d.ts +0 -78
  309. package/dist/runtime/input-router.d.ts.map +0 -1
  310. package/dist/runtime/input-router.js +0 -171
  311. package/dist/runtime/mount-game.d.ts +0 -129
  312. package/dist/runtime/mount-game.d.ts.map +0 -1
  313. package/dist/runtime/mount-game.js +0 -152
  314. package/dist/runtime/mount-manifest.d.ts +0 -119
  315. package/dist/runtime/mount-manifest.d.ts.map +0 -1
  316. package/dist/runtime/mount-manifest.js +0 -204
  317. package/dist/runtime/render-audio-control.d.ts +0 -110
  318. package/dist/runtime/render-audio-control.d.ts.map +0 -1
  319. package/dist/runtime/render-audio-control.js +0 -111
  320. package/dist/runtime/render-control.d.ts +0 -291
  321. package/dist/runtime/render-control.d.ts.map +0 -1
  322. package/dist/runtime/render-control.js +0 -264
  323. package/dist/runtime/render-seed.d.ts +0 -57
  324. package/dist/runtime/render-seed.d.ts.map +0 -1
  325. package/dist/runtime/render-seed.js +0 -74
  326. package/dist/runtime/scene-ui-bridge.d.ts +0 -69
  327. package/dist/runtime/scene-ui-bridge.d.ts.map +0 -1
  328. package/dist/runtime/scene-ui-bridge.js +0 -23
  329. package/dist/runtime/scene-ui-data.d.ts +0 -13
  330. package/dist/runtime/scene-ui-data.d.ts.map +0 -1
  331. package/dist/runtime/scene-ui-data.js +0 -92
  332. package/dist/runtime/state-bridge.d.ts +0 -47
  333. package/dist/runtime/state-bridge.d.ts.map +0 -1
  334. package/dist/runtime/state-bridge.js +0 -53
  335. package/dist/runtime/types.d.ts +0 -179
  336. package/dist/runtime/types.d.ts.map +0 -1
  337. package/dist/runtime/types.js +0 -1
  338. package/dist/scene/asset-loaders.d.ts +0 -79
  339. package/dist/scene/asset-loaders.d.ts.map +0 -1
  340. package/dist/scene/asset-loaders.js +0 -141
  341. package/dist/scene/asset-paths.d.ts +0 -11
  342. package/dist/scene/asset-paths.d.ts.map +0 -1
  343. package/dist/scene/asset-paths.js +0 -108
  344. package/dist/scene/asset-ref-check.d.ts +0 -88
  345. package/dist/scene/asset-ref-check.d.ts.map +0 -1
  346. package/dist/scene/asset-ref-check.js +0 -214
  347. package/dist/scene/asset-registry.d.ts +0 -30
  348. package/dist/scene/asset-registry.d.ts.map +0 -1
  349. package/dist/scene/asset-registry.js +0 -50
  350. package/dist/scene/collider-dimensions.d.ts +0 -63
  351. package/dist/scene/collider-dimensions.d.ts.map +0 -1
  352. package/dist/scene/collider-dimensions.js +0 -98
  353. package/dist/scene/component-registry.d.ts +0 -17
  354. package/dist/scene/component-registry.d.ts.map +0 -1
  355. package/dist/scene/component-registry.js +0 -28
  356. package/dist/scene/defaults.d.ts +0 -252
  357. package/dist/scene/defaults.d.ts.map +0 -1
  358. package/dist/scene/defaults.js +0 -151
  359. package/dist/scene/geometries/index.d.ts +0 -8
  360. package/dist/scene/geometries/index.d.ts.map +0 -1
  361. package/dist/scene/geometries/index.js +0 -7
  362. package/dist/scene/geometries/terrain.d.ts +0 -2
  363. package/dist/scene/geometries/terrain.d.ts.map +0 -1
  364. package/dist/scene/geometries/terrain.js +0 -37
  365. package/dist/scene/geometry-registry.d.ts +0 -24
  366. package/dist/scene/geometry-registry.d.ts.map +0 -1
  367. package/dist/scene/geometry-registry.js +0 -20
  368. package/dist/scene/instance-registry.d.ts +0 -36
  369. package/dist/scene/instance-registry.d.ts.map +0 -1
  370. package/dist/scene/instance-registry.js +0 -55
  371. package/dist/scene/instancers/grid.d.ts +0 -2
  372. package/dist/scene/instancers/grid.d.ts.map +0 -1
  373. package/dist/scene/instancers/grid.js +0 -36
  374. package/dist/scene/instancers/index.d.ts +0 -8
  375. package/dist/scene/instancers/index.d.ts.map +0 -1
  376. package/dist/scene/instancers/index.js +0 -7
  377. package/dist/scene/light-camera-factory.d.ts +0 -23
  378. package/dist/scene/light-camera-factory.d.ts.map +0 -1
  379. package/dist/scene/light-camera-factory.js +0 -68
  380. package/dist/scene/material-factory.d.ts +0 -17
  381. package/dist/scene/material-factory.d.ts.map +0 -1
  382. package/dist/scene/material-factory.js +0 -197
  383. package/dist/scene/material-registry.d.ts +0 -40
  384. package/dist/scene/material-registry.d.ts.map +0 -1
  385. package/dist/scene/material-registry.js +0 -44
  386. package/dist/scene/materials/index.d.ts +0 -8
  387. package/dist/scene/materials/index.d.ts.map +0 -1
  388. package/dist/scene/materials/index.js +0 -7
  389. package/dist/scene/materials/water.d.ts +0 -2
  390. package/dist/scene/materials/water.d.ts.map +0 -1
  391. package/dist/scene/materials/water.js +0 -54
  392. package/dist/scene/parse.d.ts +0 -23
  393. package/dist/scene/parse.d.ts.map +0 -1
  394. package/dist/scene/parse.js +0 -179
  395. package/dist/scene/particles-factory.d.ts +0 -33
  396. package/dist/scene/particles-factory.d.ts.map +0 -1
  397. package/dist/scene/particles-factory.js +0 -282
  398. package/dist/scene/scene-apply.d.ts +0 -109
  399. package/dist/scene/scene-apply.d.ts.map +0 -1
  400. package/dist/scene/scene-apply.js +0 -326
  401. package/dist/scene/scene-diff-schema.d.ts +0 -285
  402. package/dist/scene/scene-diff-schema.d.ts.map +0 -1
  403. package/dist/scene/scene-diff-schema.js +0 -92
  404. package/dist/scene/scene-diff-types.d.ts +0 -40
  405. package/dist/scene/scene-diff-types.d.ts.map +0 -1
  406. package/dist/scene/scene-diff-types.js +0 -14
  407. package/dist/scene/scene-loader.d.ts +0 -177
  408. package/dist/scene/scene-loader.d.ts.map +0 -1
  409. package/dist/scene/scene-loader.js +0 -1213
  410. package/dist/scene/scene-query.d.ts +0 -11
  411. package/dist/scene/scene-query.d.ts.map +0 -1
  412. package/dist/scene/scene-query.js +0 -63
  413. package/dist/scene/scene-types.d.ts +0 -10
  414. package/dist/scene/scene-types.d.ts.map +0 -1
  415. package/dist/scene/scene-types.js +0 -8
  416. package/dist/scene/scene-version.d.ts +0 -26
  417. package/dist/scene/scene-version.d.ts.map +0 -1
  418. package/dist/scene/scene-version.js +0 -34
  419. package/dist/scene/schema/animation.d.ts +0 -8
  420. package/dist/scene/schema/animation.d.ts.map +0 -1
  421. package/dist/scene/schema/animation.js +0 -42
  422. package/dist/scene/schema/audio.d.ts +0 -13
  423. package/dist/scene/schema/audio.d.ts.map +0 -1
  424. package/dist/scene/schema/audio.js +0 -22
  425. package/dist/scene/schema/camera.d.ts +0 -28
  426. package/dist/scene/schema/camera.d.ts.map +0 -1
  427. package/dist/scene/schema/camera.js +0 -17
  428. package/dist/scene/schema/collider.d.ts +0 -40
  429. package/dist/scene/schema/collider.d.ts.map +0 -1
  430. package/dist/scene/schema/collider.js +0 -55
  431. package/dist/scene/schema/entity-ref.d.ts +0 -31
  432. package/dist/scene/schema/entity-ref.d.ts.map +0 -1
  433. package/dist/scene/schema/entity-ref.js +0 -61
  434. package/dist/scene/schema/entity.d.ts +0 -1036
  435. package/dist/scene/schema/entity.d.ts.map +0 -1
  436. package/dist/scene/schema/entity.js +0 -153
  437. package/dist/scene/schema/environment.d.ts +0 -425
  438. package/dist/scene/schema/environment.d.ts.map +0 -1
  439. package/dist/scene/schema/environment.js +0 -357
  440. package/dist/scene/schema/index.d.ts +0 -39
  441. package/dist/scene/schema/index.d.ts.map +0 -1
  442. package/dist/scene/schema/index.js +0 -20
  443. package/dist/scene/schema/instances.d.ts +0 -14
  444. package/dist/scene/schema/instances.d.ts.map +0 -1
  445. package/dist/scene/schema/instances.js +0 -28
  446. package/dist/scene/schema/joint.d.ts +0 -23
  447. package/dist/scene/schema/joint.d.ts.map +0 -1
  448. package/dist/scene/schema/joint.js +0 -23
  449. package/dist/scene/schema/light.d.ts +0 -38
  450. package/dist/scene/schema/light.d.ts.map +0 -1
  451. package/dist/scene/schema/light.js +0 -26
  452. package/dist/scene/schema/material.d.ts +0 -199
  453. package/dist/scene/schema/material.d.ts.map +0 -1
  454. package/dist/scene/schema/material.js +0 -99
  455. package/dist/scene/schema/mesh.d.ts +0 -48
  456. package/dist/scene/schema/mesh.d.ts.map +0 -1
  457. package/dist/scene/schema/mesh.js +0 -82
  458. package/dist/scene/schema/particles.d.ts +0 -2178
  459. package/dist/scene/schema/particles.d.ts.map +0 -1
  460. package/dist/scene/schema/particles.js +0 -321
  461. package/dist/scene/schema/physics.d.ts +0 -56
  462. package/dist/scene/schema/physics.d.ts.map +0 -1
  463. package/dist/scene/schema/physics.js +0 -37
  464. package/dist/scene/schema/scene-file.d.ts +0 -304
  465. package/dist/scene/schema/scene-file.d.ts.map +0 -1
  466. package/dist/scene/schema/scene-file.js +0 -262
  467. package/dist/scene/schema/shadow.d.ts +0 -28
  468. package/dist/scene/schema/shadow.d.ts.map +0 -1
  469. package/dist/scene/schema/shadow.js +0 -20
  470. package/dist/scene/schema/spline.d.ts +0 -25
  471. package/dist/scene/schema/spline.d.ts.map +0 -1
  472. package/dist/scene/schema/spline.js +0 -15
  473. package/dist/scene/schema/tuples.d.ts +0 -12
  474. package/dist/scene/schema/tuples.d.ts.map +0 -1
  475. package/dist/scene/schema/tuples.js +0 -14
  476. package/dist/scene/schema/ui.d.ts +0 -880
  477. package/dist/scene/schema/ui.d.ts.map +0 -1
  478. package/dist/scene/schema/ui.js +0 -506
  479. package/dist/scene/user-data.d.ts +0 -171
  480. package/dist/scene/user-data.d.ts.map +0 -1
  481. package/dist/scene/user-data.js +0 -137
  482. package/dist/setup/setup-audio.d.ts +0 -20
  483. package/dist/setup/setup-audio.d.ts.map +0 -1
  484. package/dist/setup/setup-audio.js +0 -41
  485. package/dist/setup/setup-particles.d.ts +0 -17
  486. package/dist/setup/setup-particles.d.ts.map +0 -1
  487. package/dist/setup/setup-particles.js +0 -16
  488. package/dist/setup/setup-physics.d.ts +0 -25
  489. package/dist/setup/setup-physics.d.ts.map +0 -1
  490. package/dist/setup/setup-physics.js +0 -51
  491. package/dist/setup/setup-renderer.d.ts +0 -69
  492. package/dist/setup/setup-renderer.d.ts.map +0 -1
  493. package/dist/setup/setup-renderer.js +0 -363
  494. package/dist/tools/define-tool.d.ts +0 -105
  495. package/dist/tools/define-tool.d.ts.map +0 -1
  496. package/dist/tools/define-tool.js +0 -82
  497. package/dist/world2d/authoring-2d.d.ts +0 -76
  498. package/dist/world2d/authoring-2d.d.ts.map +0 -1
  499. package/dist/world2d/authoring-2d.js +0 -176
  500. package/dist/world2d/capture-to-scene2d.d.ts +0 -5
  501. package/dist/world2d/capture-to-scene2d.d.ts.map +0 -1
  502. package/dist/world2d/capture-to-scene2d.js +0 -49
  503. package/dist/world2d/collision-2d.d.ts +0 -52
  504. package/dist/world2d/collision-2d.d.ts.map +0 -1
  505. package/dist/world2d/collision-2d.js +0 -70
  506. package/dist/world2d/components-2d.d.ts +0 -47
  507. package/dist/world2d/components-2d.d.ts.map +0 -1
  508. package/dist/world2d/components-2d.js +0 -77
  509. package/dist/world2d/index.d.ts +0 -34
  510. package/dist/world2d/index.d.ts.map +0 -1
  511. package/dist/world2d/index.js +0 -32
  512. package/dist/world2d/ingest-iframe-2d.d.ts +0 -82
  513. package/dist/world2d/ingest-iframe-2d.d.ts.map +0 -1
  514. package/dist/world2d/ingest-iframe-2d.js +0 -166
  515. package/dist/world2d/ingest2d.d.ts +0 -97
  516. package/dist/world2d/ingest2d.d.ts.map +0 -1
  517. package/dist/world2d/ingest2d.js +0 -40
  518. package/dist/world2d/physics2d-registry.d.ts +0 -24
  519. package/dist/world2d/physics2d-registry.d.ts.map +0 -1
  520. package/dist/world2d/physics2d-registry.js +0 -38
  521. package/dist/world2d/pixi-game-adapter.d.ts +0 -107
  522. package/dist/world2d/pixi-game-adapter.d.ts.map +0 -1
  523. package/dist/world2d/pixi-game-adapter.js +0 -211
  524. package/dist/world2d/pixi-surface.d.ts +0 -39
  525. package/dist/world2d/pixi-surface.d.ts.map +0 -1
  526. package/dist/world2d/pixi-surface.js +0 -47
  527. package/dist/world2d/scene-capture-2d.d.ts +0 -43
  528. package/dist/world2d/scene-capture-2d.d.ts.map +0 -1
  529. package/dist/world2d/scene-capture-2d.js +0 -92
  530. package/dist/world2d/scene2d-loader.d.ts +0 -27
  531. package/dist/world2d/scene2d-loader.d.ts.map +0 -1
  532. package/dist/world2d/scene2d-loader.js +0 -274
  533. package/dist/world2d/schema/entity2d.d.ts +0 -102
  534. package/dist/world2d/schema/entity2d.d.ts.map +0 -1
  535. package/dist/world2d/schema/entity2d.js +0 -119
  536. package/dist/world2d/schema/physics2d.d.ts +0 -46
  537. package/dist/world2d/schema/physics2d.d.ts.map +0 -1
  538. package/dist/world2d/schema/physics2d.js +0 -45
  539. package/dist/world2d/schema/sprite.d.ts +0 -42
  540. package/dist/world2d/schema/sprite.d.ts.map +0 -1
  541. package/dist/world2d/schema/sprite.js +0 -63
  542. package/dist/world2d/schema/tilemap.d.ts +0 -15
  543. package/dist/world2d/schema/tilemap.d.ts.map +0 -1
  544. package/dist/world2d/schema/tilemap.js +0 -19
  545. package/dist/world2d/schema/tuples2d.d.ts +0 -20
  546. package/dist/world2d/schema/tuples2d.d.ts.map +0 -1
  547. package/dist/world2d/schema/tuples2d.js +0 -19
  548. package/dist/world2d/system-adapters-2d.d.ts +0 -16
  549. package/dist/world2d/system-adapters-2d.d.ts.map +0 -1
  550. package/dist/world2d/system-adapters-2d.js +0 -37
  551. package/dist/world2d/transform-writer-2d.d.ts +0 -16
  552. package/dist/world2d/transform-writer-2d.d.ts.map +0 -1
  553. package/dist/world2d/transform-writer-2d.js +0 -22
  554. package/dist/world2d/types.d.ts +0 -53
  555. package/dist/world2d/types.d.ts.map +0 -1
  556. package/dist/world2d/types.js +0 -1
  557. package/src/runtime/scene-ui-bridge.ts +0 -86
  558. package/src/runtime/scene-ui-data.ts +0 -119
  559. package/src/scene/schema/ui.ts +0 -602
@@ -38,6 +38,70 @@ function isTextEntryFocused(): boolean {
38
38
  );
39
39
  }
40
40
 
41
+ /**
42
+ * Thrown by `InputManager.setVirtualAction`/`tapVirtualAction` (Task 1.4,
43
+ * ACCEPTANCE-DRIVER-BUILD-PLAN.md / SYNTHETIC-PLAYER-SPEC.md §3.2) for an
44
+ * action name that was never `registerAction`/`loadMap`-ed. Carries a
45
+ * machine-readable `code` and `data.registered` (every declared action name)
46
+ * rather than requiring a caller to parse the message.
47
+ */
48
+ export class InputActionError extends Error {
49
+ readonly code = 'INPUT_ACTION_NOT_FOUND' as const;
50
+ readonly data: { registered: string[] };
51
+
52
+ constructor(action: string, method: string, registered: string[]) {
53
+ super(
54
+ `InputManager.${method}: unknown action "${action}". Registered actions: ` +
55
+ (registered.length ? registered.join(', ') : '(none)'),
56
+ );
57
+ this.name = 'InputActionError';
58
+ this.data = { registered };
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Thrown by `InputManager.scheduleActionAtTick` (D15/T-D15.5,
64
+ * docs/D15-DETERMINISM-DESIGN.md §2.c) when the named `tick` has already
65
+ * elapsed — scheduling only ever applies to the CURRENT or a future tick,
66
+ * never one already serviced by `poll()`. Carries `data.currentTick` — the
67
+ * NEXT tick `poll()` will service (not the tick that just elapsed) — so a
68
+ * caller can retry by scheduling for `data.currentTick` or later.
69
+ */
70
+ export class InputTickError extends Error {
71
+ readonly code = 'TICK_ALREADY_PASSED' as const;
72
+ readonly data: { currentTick: number };
73
+
74
+ constructor(tick: number, currentTick: number) {
75
+ super(
76
+ `InputManager.scheduleActionAtTick: tick ${tick} has already passed (next tick to be ` +
77
+ `serviced: ${currentTick}) — scheduling only applies to the current or a future tick`,
78
+ );
79
+ this.name = 'InputTickError';
80
+ this.data = { currentTick };
81
+ }
82
+ }
83
+
84
+ /** Shallow equality for a resolved action value (boolean/number/Vector2) —
85
+ * D15/T-D15.5's recording tap uses this to emit DELTAS only (the trace
86
+ * format's "deltas only" line), never a full snapshot every tick. `prev`
87
+ * is `undefined` for an action never recorded before (always "changed"). */
88
+ function actionValuesEqual(
89
+ prev: boolean | number | { x: number; y: number } | undefined,
90
+ next: boolean | number | { x: number; y: number },
91
+ ): boolean {
92
+ if (prev === undefined) return false;
93
+ if (typeof prev !== typeof next) return false;
94
+ if (typeof prev === 'object' && typeof next === 'object') {
95
+ return prev.x === next.x && prev.y === next.y;
96
+ }
97
+ return prev === next;
98
+ }
99
+
100
+ /** Cap on {@link InputManager}'s post-gate input-trace ring (D15/T-D15.5) —
101
+ * mirrors `debug-registry.ts`'s own `RING_CAP` precedent for the debug event
102
+ * ring: bounds memory for a recording session a caller forgot to stop. */
103
+ const INPUT_TRACE_CAP = 2000;
104
+
41
105
  /**
42
106
  * Maps raw keyboard/mouse/gamepad events → named actions.
43
107
  *
@@ -296,6 +360,85 @@ export class InputManager {
296
360
  private static isStickActive(v: Vector2): boolean {
297
361
  return Math.hypot(v.x, v.y) > 0.3;
298
362
  }
363
+ // Task 1.4 (ACCEPTANCE-DRIVER-BUILD-PLAN.md) — action-level virtual input
364
+ // for a synthetic player, promoted from hollowstone's `VirtualInput`. Held
365
+ // digitals OR into isPressed alongside binding contributions (like
366
+ // touchButtonsDown above); tap queues, then is promoted to "active" for
367
+ // exactly one poll()/endFrame() bracket (see poll()/endFrame() below,
368
+ // mirroring hollowstone's queued/beginFrame contract); scalar/vector2 are
369
+ // level values feeding the same max-magnitude/sum combine every real
370
+ // binding uses (collectScalarContributions/collectVector2Contributions).
371
+ private virtualDigitalHeld = new Set<string>();
372
+ private virtualDigitalTapQueued = new Set<string>();
373
+ private virtualDigitalTapActive = new Set<string>();
374
+ /** pulse-runner friction #4 — the release-edge mirror of
375
+ * `virtualDigitalTapActive`: a direct `setVirtualAction(action, false)` (or
376
+ * `clearVirtualActions()`) on an action that WAS held writes here so
377
+ * `isJustReleased` fires exactly once, mirroring real `onKeyUp`'s
378
+ * immediate `keysJustUp` write (no queue/promotion needed — unlike the
379
+ * press edge, nothing in `poll()` reassigns this set out from under a
380
+ * write that lands before `poll()` runs). Cleared every `endFrame()`,
381
+ * same bracket as every other just-* edge set. */
382
+ private virtualDigitalJustReleased = new Set<string>();
383
+ private virtualScalarValues = new Map<string, number>();
384
+ private virtualVector2Values = new Map<string, Vector2>();
385
+ // D15/T-D15.5 (docs/D15-DETERMINISM-DESIGN.md §2.c) — tick-indexed input
386
+ // scheduling + post-gate recording.
387
+ //
388
+ // `currentTick` is a FALLBACK, self-incrementing counter, used only when a
389
+ // caller never tells `poll()` which tick it's servicing (every existing
390
+ // bare/headless caller — tests, a mount with no `Game` shell behind it —
391
+ // keeps this exact pre-existing behavior, zero regression). The REAL
392
+ // per-world wiring (`vgai-scene-game-adapter.ts`) instead passes the
393
+ // shared `Game`-level tick counter into every `poll(tick)` call — this is
394
+ // deliberate: an InputManager-local counter drifts from the actual game
395
+ // tick for a paused/frozen world (its `poll()` isn't called every game
396
+ // tick, so a local increment-per-call counter undercounts) — see the
397
+ // `lastServicedTick` gap-handling below, which structurally can't drift
398
+ // because it's driven by whatever tick value the CALLER (ultimately the
399
+ // shared `Game` tick) actually supplies.
400
+ private currentTick = 0;
401
+ /** The last tick value `poll()` actually serviced, or `-1` before the
402
+ * first call. Used to detect a GAP (this world's input phase didn't run
403
+ * for one or more intervening ticks — e.g. frozen/paused while other
404
+ * roots kept advancing the shared game tick) so any schedule entries
405
+ * inside that gap can be dropped (with an observable event) instead of
406
+ * silently rotting in {@link scheduledActions} forever. */
407
+ private lastServicedTick = -1;
408
+ /** Pending `scheduleActionAtTick` actuations, grouped by their exact target
409
+ * tick. Applied (and removed) at the START of that tick's `poll()` — see
410
+ * `applyScheduledActionsForTick`. A tick whose input phase is skipped
411
+ * entirely (this world paused/frozen, or a gap between two serviced
412
+ * ticks) never reaches it — its scheduled entries are dropped, each
413
+ * emitting one `'input.schedule.dropped'` debug event (`{tick, action}`)
414
+ * via {@link setDebugEmit}'s sink if one is wired — NOT retried on a
415
+ * later tick (mirrors `clearVirtualActions`-on-gate-close: no stuck
416
+ * actuation ever silently fires late). */
417
+ private scheduledActions = new Map<
418
+ number,
419
+ { action: string; value: boolean | number | { x: number; y: number } }[]
420
+ >();
421
+ /** Optional sink for the `'input.schedule.dropped'` debug event (`{tick,
422
+ * action}`) emitted whenever a scheduled entry is discarded because its
423
+ * target tick's input phase never ran. Wired by whoever constructs this
424
+ * `InputManager` with access to a `DebugRegistry`
425
+ * (`vgai-scene-game-adapter.ts`, the same seed spot as
426
+ * `setVirtualInputTarget`/`setInputActionsSource`) — `null` (the
427
+ * default) for a bare/headless `InputManager`, in which case a drop
428
+ * stays silent (matching pre-D15/T-D15.5 behavior). */
429
+ private debugEmit: ((event: string, detail?: unknown) => void) | null = null;
430
+ /** D15/T-D15.5 recording toggle — see {@link startInputRecording}. */
431
+ private inputRecording = false;
432
+ /** The post-gate action-delta trace (§2.c's format sketch) — capped so an
433
+ * accidentally-long recording session can't grow this unboundedly. */
434
+ private inputTrace: {
435
+ tick: number;
436
+ actions: Record<string, boolean | number | { x: number; y: number }>;
437
+ }[] = [];
438
+ /** Last value recorded per action, so `recordPostGateTick` only ever
439
+ * appends a DELTA (the format sketch's "deltas only" line), not a full
440
+ * snapshot every tick. Cleared on `startInputRecording()`. */
441
+ private lastTraceSnapshot = new Map<string, boolean | number | { x: number; y: number }>();
299
442
  private disposed = false;
300
443
  /**
301
444
  * When false, all raw input is ignored and held/transient state is cleared.
@@ -320,6 +463,17 @@ export class InputManager {
320
463
  private get inputActive(): boolean {
321
464
  return this.enabled && this.focused;
322
465
  }
466
+ /** #144 — the gate MACHINE input passes (virtual actions and
467
+ * tick-scheduled actuations): the editor's explicit `enabled` flag ONLY.
468
+ * Window focus deliberately does not participate: a synthetic player
469
+ * drives the shared editor tab precisely while the human's focus is
470
+ * elsewhere (their terminal, another window) — the first live in-editor
471
+ * probe run froze mid-suite the moment the owner started typing to their
472
+ * agent, which is the bug this gate split fixes. Real-device input keeps
473
+ * the stricter `inputActive` (enabled AND focused) gate above. */
474
+ private get machineInputActive(): boolean {
475
+ return this.enabled;
476
+ }
323
477
  /** F2 — real pointer-lock state (`document.pointerLockElement`), tracked via
324
478
  * `pointerlockchange` (see the constructor). */
325
479
  private pointerLocked = false;
@@ -380,12 +534,15 @@ export class InputManager {
380
534
  this.mouseDeltaY += e.movementY;
381
535
  };
382
536
 
383
- /** F2 — real window blur: drop focus and flush held state, mirroring
384
- * `setEnabled(false)`'s flush (see `flushHeldState`), without touching the
385
- * editor-driven `enabled` flag. */
537
+ /** F2 — real window blur: drop focus and flush held REAL-device state
538
+ * (the browser stops delivering keyup/mouseup while blurred, so a key
539
+ * held at alt-tab time would stick). #144: virtual-action state is
540
+ * deliberately NOT flushed here — a bot's held action survives the human
541
+ * looking away (see `machineInputActive`); only `setEnabled(false)`
542
+ * clears it. */
386
543
  private onWindowBlur = () => {
387
544
  this.focused = false;
388
- this.flushHeldState();
545
+ this.flushRealHeldState();
389
546
  };
390
547
 
391
548
  /** F2 — real window focus regain. No flush needed (there's nothing held to
@@ -395,14 +552,15 @@ export class InputManager {
395
552
  this.focused = true;
396
553
  };
397
554
 
398
- /** Clear all held/transient keyboard/mouse/touch-button state — shared by
399
- * `setEnabled(false)` and real blur (`onWindowBlur`), so a key/button
400
- * physically held when input is suspended doesn't linger. Level values
401
- * (gamepad axes, touch sticks, injected test axes) are deliberately NOT
402
- * flushed here — they are gated at READ time instead (see `inputActive`
403
- * checks in `poll()`/the `collect*Contributions` methods), matching the
404
- * pre-existing gamepad-axis convention. */
405
- private flushHeldState(): void {
555
+ /** Clear held/transient REAL-device state (keyboard/mouse/touch-button) —
556
+ * shared by `setEnabled(false)` (via `flushHeldState`) and real blur
557
+ * (`onWindowBlur`), so a key/button physically held when input is
558
+ * suspended doesn't linger. Level values (gamepad axes, touch sticks,
559
+ * injected test axes) are deliberately NOT flushed here — they are gated
560
+ * at READ time instead (see `inputActive` checks in `poll()`/the
561
+ * `collect*Contributions` methods), matching the pre-existing
562
+ * gamepad-axis convention. */
563
+ private flushRealHeldState(): void {
406
564
  this.keysDown.clear();
407
565
  this.keysJustDown.clear();
408
566
  this.keysJustUp.clear();
@@ -416,6 +574,45 @@ export class InputManager {
416
574
  this.touchButtonsJustUp.clear();
417
575
  }
418
576
 
577
+ /** The full flush `setEnabled(false)` performs: real-device state AND
578
+ * virtual-action state. Task 1.4's rule ("a bot's held action must not
579
+ * survive input being suspended") now applies only to the editor's
580
+ * explicit gate — #144 moved real window blur onto `flushRealHeldState`
581
+ * alone, so machine input survives the human's focus leaving the tab.
582
+ * No release edge is manufactured here (`manufactureReleaseEdge: false`,
583
+ * the default) — this is a SUSPEND, not a deliberate release, and mirrors
584
+ * `flushRealHeldState`'s own silence (a key physically held at
585
+ * suspend/blur time is wiped with no `keysJustUp`, "flush held state,
586
+ * don't corrupt history", see `poll()`'s doc comment). */
587
+ private flushHeldState(): void {
588
+ this.flushRealHeldState();
589
+ this.clearVirtualActionState();
590
+ }
591
+
592
+ /** Shared by the public `clearVirtualActions()` and `flushHeldState()`
593
+ * above — clears every virtual-action store (held digitals, queued/active
594
+ * taps, scalar/vector2 values, the release-edge set itself).
595
+ *
596
+ * `manufactureReleaseEdge` (default `false`): when `true` — only
597
+ * `clearVirtualActions()`'s own deliberate "let go" passes this — every
598
+ * currently-held digital in `virtualDigitalHeld` first gets an
599
+ * `isJustReleased` edge written to `virtualDigitalJustReleased`, mirroring
600
+ * a real key-up (`onKeyUp` → `keysJustUp`). `flushHeldState()` (the
601
+ * editor-gate suspend path) deliberately passes `false` — see its own doc
602
+ * comment for why a suspend must stay silent. */
603
+ private clearVirtualActionState(options?: { manufactureReleaseEdge: boolean }): void {
604
+ if (options?.manufactureReleaseEdge) {
605
+ for (const action of this.virtualDigitalHeld) {
606
+ this.virtualDigitalJustReleased.add(action);
607
+ }
608
+ }
609
+ this.virtualDigitalHeld.clear();
610
+ this.virtualDigitalTapQueued.clear();
611
+ this.virtualDigitalTapActive.clear();
612
+ this.virtualScalarValues.clear();
613
+ this.virtualVector2Values.clear();
614
+ }
615
+
419
616
  constructor() {
420
617
  window.addEventListener('keydown', this.onKeyDown);
421
618
  window.addEventListener('keyup', this.onKeyUp);
@@ -529,10 +726,56 @@ export class InputManager {
529
726
  return [...this.actions.keys()];
530
727
  }
531
728
 
532
- /** Poll gamepads and virtual look stick (call at start of frame) */
533
- poll() {
534
- if (!this.inputActive) {
535
- // Input suspended (editor-gated play mode, OR — F2 — real window blur).
729
+ /**
730
+ * Poll gamepads and virtual look stick (call at start of frame).
731
+ *
732
+ * `tick` (D15/T-D15.3/.5) — the GAME tick this call services. Pass the
733
+ * live, shared `Game` tick counter here (`vgai-scene-game-adapter.ts` does,
734
+ * via `DebugRegistry.getGameTick()`) so `scheduleActionAtTick`'s numbering
735
+ * never drifts from the actual game tick — the bug an earlier revision of
736
+ * this feature had: an InputManager-LOCAL counter, incremented once per
737
+ * `poll()` call, undercounts for a paused/frozen world (its `poll()` isn't
738
+ * called every game tick while frozen), so a schedule set against "tick
739
+ * 500" could fire at the wrong wall-tick once the world resumed. Omit it
740
+ * (every existing bare/headless caller does) to fall back to the
741
+ * self-incrementing `currentTick` counter — unchanged pre-D15 behavior.
742
+ */
743
+ poll(tick?: number) {
744
+ const servicedTick = tick !== undefined ? tick : this.currentTick;
745
+ // D15/T-D15.5 — a GAP: one or more ticks strictly between the last tick
746
+ // this InputManager serviced and this one never ran its input phase at
747
+ // all (this world was paused/frozen and skipped straight from tick A to
748
+ // a later tick B > A+1 once stepped/resumed again — the frozen-world
749
+ // scenario objection above). Any entries scheduled for a tick inside
750
+ // that gap can never be honored — drop them (each emitting one
751
+ // `'input.schedule.dropped'` event) rather than leaving them to rot in
752
+ // `scheduledActions` forever. Guarded on `size > 0` so the overwhelmingly
753
+ // common case (nothing ever scheduled) costs nothing, even across a
754
+ // very large gap.
755
+ if (this.scheduledActions.size > 0) {
756
+ // Iterate the (tiny) schedule map, never the gap's tick RANGE — a
757
+ // world resuming after a very long freeze (or one mounting late into
758
+ // a game already millions of ticks in) must not pay O(gap) here.
759
+ // Sorted so drop events still arrive in tick order.
760
+ const gapped = [...this.scheduledActions.keys()]
761
+ .filter((t) => t > this.lastServicedTick && t < servicedTick)
762
+ .sort((a, b) => a - b);
763
+ for (const t of gapped) {
764
+ const skipped = this.scheduledActions.get(t)!;
765
+ this.scheduledActions.delete(t);
766
+ for (const entry of skipped) {
767
+ this.debugEmit?.('input.schedule.dropped', { tick: t, action: entry.action });
768
+ }
769
+ }
770
+ }
771
+ this.lastServicedTick = servicedTick;
772
+ this.currentTick = servicedTick + 1;
773
+ if (!this.enabled) {
774
+ // Input suspended by the editor's explicit gate (play stopped /
775
+ // game-tab-inactive). #144: this branch no longer covers real window
776
+ // blur — an enabled-but-blurred tick falls through so MACHINE input
777
+ // (virtual actions, scheduled actuations) keeps flowing while the
778
+ // human's focus is elsewhere; real devices are gated further down.
536
779
  // Unlike keyboard/mouse, gamepad state isn't DOM-event-driven —
537
780
  // `isPressed`/`isJustPressed`/`isJustReleased` read `this.gamepads`
538
781
  // (and the edge-tracked sets) directly, not a live poll of `navigator`.
@@ -544,9 +787,49 @@ export class InputManager {
544
787
  // resume diffing against genuinely-last-observed hardware state once
545
788
  // re-enabled, so there's no phantom edge on re-enable (mirrors the
546
789
  // "flush held state, don't corrupt history" intent of setEnabled below).
790
+ // D15/T-D15.5: a tick serviced while gated drops its OWN scheduled
791
+ // entries too (never applied, never retried later) — same honesty line
792
+ // `setVirtualAction`'s own gate already draws — each emitting the same
793
+ // `'input.schedule.dropped'` event the gap-handling above does.
794
+ const gatedDrop = this.scheduledActions.get(servicedTick);
795
+ if (gatedDrop) {
796
+ this.scheduledActions.delete(servicedTick);
797
+ for (const entry of gatedDrop) {
798
+ this.debugEmit?.('input.schedule.dropped', { tick: servicedTick, action: entry.action });
799
+ }
800
+ }
547
801
  this.gamepads = [];
548
802
  return;
549
803
  }
804
+ // Task 1.4 — promote queued tapVirtualAction() calls to this frame's
805
+ // "active" just-pressed set (hollowstone's queued/beginFrame contract,
806
+ // ported onto this engine's existing fixed-step poll()/endFrame()
807
+ // bracket instead of a bespoke beginFrame() — poll() already runs at the
808
+ // start of the fixed step). Cleared in endFrame() below.
809
+ this.virtualDigitalTapActive = this.virtualDigitalTapQueued;
810
+ this.virtualDigitalTapQueued = new Set();
811
+ // D15/T-D15.5 — apply any actuation scheduled for exactly this tick
812
+ // (spec §2.c: "applied at the START of the target tick's input phase",
813
+ // before any gameplay phase reads input — this still runs before every
814
+ // OTHER phase this tick, `input` being the first system phase). Placed
815
+ // AFTER the tap-queue promotion immediately above (not before) so a
816
+ // scheduled digital `true` can add itself to `virtualDigitalTapActive`
817
+ // (see `applyScheduledActionsForTick`) without that promotion's
818
+ // reassignment wiping it out.
819
+ this.applyScheduledActionsForTick(servicedTick);
820
+
821
+ if (!this.focused) {
822
+ // #144 — enabled but blurred: everything ABOVE this line is machine
823
+ // input (tap promotion, scheduled actuations) and stays live; real
824
+ // devices below gate exactly as the old combined branch did (no
825
+ // gamepads presented, edge sets frozen at their last real values).
826
+ // The post-gate trace still records — "what the sim actually
827
+ // consumed" includes machine input delivered while blurred.
828
+ this.gamepads = [];
829
+ this.recordPostGateTick(servicedTick);
830
+ return;
831
+ }
832
+
550
833
  this.gamepads = navigator.getGamepads ? [...navigator.getGamepads()] : [];
551
834
 
552
835
  // Gamepad button edge detection: diff this frame's pressed set against the
@@ -593,6 +876,102 @@ export class InputManager {
593
876
  this.mouseDeltaX += this.lookStickX * scale;
594
877
  this.mouseDeltaY += this.lookStickY * scale;
595
878
  }
879
+ // D15/T-D15.5 — tap the POST-GATE resolved values for this tick LAST
880
+ // (every binding/virtual/scheduled contribution above has already been
881
+ // folded in): "what the sim actually consumed", the honesty line the
882
+ // design doc draws for record-now/replay-in-SP5 (§2.c).
883
+ this.recordPostGateTick(servicedTick);
884
+ }
885
+
886
+ /**
887
+ * D15/T-D15.5 — apply every actuation `scheduleActionAtTick` queued for
888
+ * exactly `tick`, then forget them (a tick is serviced at most once).
889
+ *
890
+ * Digital `true` gets SPECIAL handling (the objection-1 fix): it promotes
891
+ * the action into this tick's `virtualDigitalTapActive` set directly — the
892
+ * same one-tick "active" pulse `tapVirtualAction` uses — so `isJustPressed`
893
+ * reads true for EXACTLY this tick (false on the next, per `endFrame()`'s
894
+ * clear) while `isPressed` keeps reading true afterward (still held) until
895
+ * a later scheduled/explicit `false` releases it. This direct write is
896
+ * safe here (unlike `applyVirtualActionValue`'s equivalent direct-path
897
+ * case — see its doc comment) specifically because this method only ever
898
+ * runs from inside `poll()`, AFTER `poll()`'s own tap-queue-promotion
899
+ * reassignment of `virtualDigitalTapActive` has already happened for this
900
+ * tick (see `poll()`'s own comment on why it's placed after) — so there's
901
+ * no risk of this write being silently wiped by that reassignment.
902
+ *
903
+ * pulse-runner friction #4: a prior revision of this comment
904
+ * used to say `setVirtualAction`'s plain write "never produces a
905
+ * just-pressed edge at all" and that the edge fix landed ONLY here, for
906
+ * this scheduled path — true at the time, and exactly the gap a blind
907
+ * dogfood run (pulse-runner) hit through the direct `setVirtualAction`/
908
+ * `holdFor` path. `applyVirtualActionValue` now manufactures the same kind
909
+ * of edge for THAT path too (queued instead of written directly, for the
910
+ * timing reason above) — this is no longer the only path that does.
911
+ *
912
+ * A digital `false` here simply releases the hold — no edge is
913
+ * manufactured for it at a SCHEDULED tick (no caller needs a virtual
914
+ * `isJustReleased` fired at an exact future tick today); the direct
915
+ * `setVirtualAction(action, false)`/`clearVirtualActions()` paths DO
916
+ * manufacture one now (see `applyVirtualActionValue`/
917
+ * `clearVirtualActionState`). Scalar/vector2 values apply directly,
918
+ * unchanged.
919
+ */
920
+ private applyScheduledActionsForTick(tick: number): void {
921
+ const pending = this.scheduledActions.get(tick);
922
+ if (!pending) return;
923
+ this.scheduledActions.delete(tick);
924
+ for (const entry of pending) {
925
+ if (typeof entry.value === 'boolean') {
926
+ if (entry.value) {
927
+ this.virtualDigitalHeld.add(entry.action);
928
+ this.virtualDigitalTapActive.add(entry.action);
929
+ } else {
930
+ this.virtualDigitalHeld.delete(entry.action);
931
+ }
932
+ } else {
933
+ this.applyVirtualActionValue(entry.action, entry.value);
934
+ }
935
+ }
936
+ }
937
+
938
+ /** D15/T-D15.5 — append this tick's action-value DELTA to the recording
939
+ * ring, iff a recording is active. Reads every declared digital/scalar/
940
+ * vector2 action through the exact same typed getters gameplay code would
941
+ * (`readAction`), so the trace reflects what a component reading these
942
+ * actions THIS tick actually sees post-gate — never a raw/ungated value.
943
+ * `pointerDelta`/`pointerPosition` actions are skipped (a different,
944
+ * frame-accumulated shape, and not part of the virtual-action/schedule
945
+ * surface this trace exists to prove).
946
+ *
947
+ * Two replay-affecting caveats a future SP5 consumer must know:
948
+ * 1. A gated stretch never calls this at all (`poll()`'s gated branch
949
+ * returns before reaching it) — no entries are recorded for ticks
950
+ * input was suspended. Any change only OBSERVABLE once re-enabled (a
951
+ * delta against `lastTraceSnapshot`) lands as a delta AT the re-enable
952
+ * tick, not at whatever tick it actually happened.
953
+ * 2. This ring is capped (`INPUT_TRACE_CAP`) and evicts via `shift()` once
954
+ * it fills — dropping the OLDEST entry. Because entries are DELTAS,
955
+ * evicting the earliest entries drops the base state later deltas were
956
+ * relative to — this ring is not safe to replay from its start once it
957
+ * has ever been capped. */
958
+ private recordPostGateTick(tick: number): void {
959
+ if (!this.inputRecording) return;
960
+ const deltas: Record<string, boolean | number | { x: number; y: number }> = {};
961
+ let changed = false;
962
+ for (const name of this.actionNames()) {
963
+ const type = this.getActionValueType(name);
964
+ if (type !== 'digital' && type !== 'scalar' && type !== 'vector2') continue;
965
+ const value = this.readAction(name, type);
966
+ const prev = this.lastTraceSnapshot.get(name);
967
+ if (actionValuesEqual(prev, value)) continue;
968
+ deltas[name] = value;
969
+ this.lastTraceSnapshot.set(name, value);
970
+ changed = true;
971
+ }
972
+ if (!changed) return;
973
+ this.inputTrace.push({ tick, actions: deltas });
974
+ if (this.inputTrace.length > INPUT_TRACE_CAP) this.inputTrace.shift();
596
975
  }
597
976
 
598
977
  /** Has a `gamepad_axis` binding's raw value crossed its direction+deadzone
@@ -608,6 +987,11 @@ export class InputManager {
608
987
  /** Is an action currently held down? */
609
988
  isPressed(actionName: string): boolean {
610
989
  this.assertValueType(actionName, 'digital', 'isPressed');
990
+ // Task 1.4 — a held virtual action, or a tap promoted to this frame's
991
+ // "active" set, ORs into isPressed alongside binding contributions.
992
+ if (this.virtualDigitalHeld.has(actionName) || this.virtualDigitalTapActive.has(actionName)) {
993
+ return true;
994
+ }
611
995
  const bindings = this.actions.get(actionName);
612
996
  if (!bindings) return false;
613
997
 
@@ -644,6 +1028,10 @@ export class InputManager {
644
1028
  /** Was an action pressed this frame (not held from previous)? */
645
1029
  isJustPressed(actionName: string): boolean {
646
1030
  this.assertValueType(actionName, 'digital', 'isJustPressed');
1031
+ // Task 1.4 — a tapVirtualAction() queued on a prior frame reads
1032
+ // just-pressed for exactly the one poll()/endFrame() bracket it was
1033
+ // promoted into (see poll()/endFrame() below).
1034
+ if (this.virtualDigitalTapActive.has(actionName)) return true;
647
1035
  const bindings = this.actions.get(actionName);
648
1036
  if (!bindings) return false;
649
1037
 
@@ -688,6 +1076,11 @@ export class InputManager {
688
1076
  /** Was an action released this frame (held last frame, up now)? */
689
1077
  isJustReleased(actionName: string): boolean {
690
1078
  this.assertValueType(actionName, 'digital', 'isJustReleased');
1079
+ // pulse-runner friction #4 — mirrors isJustPressed's virtual check above:
1080
+ // a direct setVirtualAction(action, false) or clearVirtualActions() on a
1081
+ // held action manufactures exactly one just-released edge here, matching
1082
+ // a real key-up's keysJustUp.
1083
+ if (this.virtualDigitalJustReleased.has(actionName)) return true;
691
1084
  const bindings = this.actions.get(actionName);
692
1085
  if (!bindings) return false;
693
1086
 
@@ -732,6 +1125,14 @@ export class InputManager {
732
1125
  * iteration order. */
733
1126
  getDigitalSource(actionName: string): ActionValueSource | null {
734
1127
  this.assertValueType(actionName, 'digital', 'getDigitalSource');
1128
+ // #144 — mirror isPressed's own pre-gate virtual check: a machine-held
1129
+ // action reports its source even while the window is blurred.
1130
+ if (
1131
+ this.machineInputActive &&
1132
+ (this.virtualDigitalHeld.has(actionName) || this.virtualDigitalTapActive.has(actionName))
1133
+ ) {
1134
+ return { bindingType: 'virtual' };
1135
+ }
735
1136
  if (!this.inputActive) return null;
736
1137
  const bindings = this.actions.get(actionName);
737
1138
  if (!bindings) return null;
@@ -823,10 +1224,18 @@ export class InputManager {
823
1224
  private collectScalarContributions(
824
1225
  actionName: string,
825
1226
  ): { value: number; source: ActionValueSource }[] {
826
- if (!this.inputActive) return [];
827
- const bindings = this.actions.get(actionName);
828
- if (!bindings) return [];
829
1227
  const out: { value: number; source: ActionValueSource }[] = [];
1228
+ // Task 1.4 — a virtual scalar value joins the same max-magnitude combine
1229
+ // real bindings do (computeScalar below). #144: it passes the MACHINE
1230
+ // gate (enabled only), so a bot's held value reads through while the
1231
+ // window is blurred; real bindings below keep the full focus gate.
1232
+ const virtual = this.virtualScalarValues.get(actionName);
1233
+ if (virtual !== undefined && this.machineInputActive) {
1234
+ out.push({ value: virtual, source: { bindingType: 'virtual' } });
1235
+ }
1236
+ if (!this.inputActive) return out;
1237
+ const bindings = this.actions.get(actionName);
1238
+ if (!bindings) return out;
830
1239
  for (const binding of bindings) {
831
1240
  if (binding.type === 'gamepad_axis') {
832
1241
  const dz = binding.deadzone ?? 0.15;
@@ -904,10 +1313,17 @@ export class InputManager {
904
1313
  private collectVector2Contributions(
905
1314
  actionName: string,
906
1315
  ): { value: Vector2; source: ActionValueSource }[] {
907
- if (!this.inputActive) return [];
908
- const bindings = this.actions.get(actionName);
909
- if (!bindings) return [];
910
1316
  const out: { value: Vector2; source: ActionValueSource }[] = [];
1317
+ // Task 1.4 — a virtual vector2 value joins the same sum-then-clamp
1318
+ // combine real bindings do (computeVector2 below). #144: machine gate
1319
+ // only, same as the scalar collector above.
1320
+ const virtual = this.virtualVector2Values.get(actionName);
1321
+ if (virtual !== undefined && this.machineInputActive) {
1322
+ out.push({ value: virtual, source: { bindingType: 'virtual' } });
1323
+ }
1324
+ if (!this.inputActive) return out;
1325
+ const bindings = this.actions.get(actionName);
1326
+ if (!bindings) return out;
911
1327
  for (const binding of bindings) {
912
1328
  switch (binding.type) {
913
1329
  case 'test_vector2': {
@@ -1511,6 +1927,14 @@ export class InputManager {
1511
1927
  // F2 — touch-button edges, same shape as the mouse-button edges above.
1512
1928
  this.touchButtonsJustDown.clear();
1513
1929
  this.touchButtonsJustUp.clear();
1930
+ // Task 1.4 — a promoted tap is just-pressed for exactly one poll()/
1931
+ // endFrame() bracket.
1932
+ this.virtualDigitalTapActive.clear();
1933
+ // Mirrors the line above for the release edge a direct
1934
+ // `setVirtualAction(action, false)`/`clearVirtualActions()` manufactures
1935
+ // (see `virtualDigitalJustReleased`'s doc comment) — just-released for
1936
+ // exactly one poll()/endFrame() bracket too.
1937
+ this.virtualDigitalJustReleased.clear();
1514
1938
  }
1515
1939
 
1516
1940
  /** Inject synthetic key state from touch controls */
@@ -1579,6 +2003,261 @@ export class InputManager {
1579
2003
  this.touchStickValues.delete(sourceId);
1580
2004
  }
1581
2005
 
2006
+ // ==========================================================================
2007
+ // Task 1.4 (ACCEPTANCE-DRIVER-BUILD-PLAN.md / SYNTHETIC-PLAYER-SPEC.md §3.2)
2008
+ // — action-level virtual input for a synthetic player. Promoted from
2009
+ // hollowstone's proven `VirtualInput` (`bot/virtual-input.ts`): a bot drives
2010
+ // NAMED ACTIONS (not raw devices), and its contributions join action
2011
+ // resolution at exactly the same points binding contributions do (see
2012
+ // isPressed/isJustPressed and collectScalarContributions/
2013
+ // collectVector2Contributions above). #144 split the gates: virtual input
2014
+ // passes `machineInputActive` (the editor's `enabled` flag only), while
2015
+ // human input keeps the full `inputActive` (enabled AND focused) gate — a
2016
+ // synthetic player must keep playing the shared editor tab while the
2017
+ // human's focus is elsewhere. Unlike every typed getter above (which
2018
+ // treats an unknown action as inert), these THROW on an unknown name — a
2019
+ // synthetic-player caller needs a loud signal that the action it's driving
2020
+ // doesn't exist, not a silently-ignored actuation.
2021
+ // ==========================================================================
2022
+
2023
+ /** Existence check shared by `setVirtualAction`/`tapVirtualAction` — throws
2024
+ * `InputActionError` (`code: 'INPUT_ACTION_NOT_FOUND'`, `data.registered`
2025
+ * = every declared action name) for a name that was never
2026
+ * `registerAction`/`loadMap`-ed. */
2027
+ private assertActionExists(actionName: string, method: string): void {
2028
+ if (!this.actionValueTypes.has(actionName)) {
2029
+ throw new InputActionError(actionName, method, this.actionNames());
2030
+ }
2031
+ }
2032
+
2033
+ /** Which declared `ActionValueType` a `setVirtualAction` value shape
2034
+ * corresponds to — the type-safety check mirrors every typed getter's
2035
+ * `assertValueType` (F1). `pointerDelta`/`pointerPosition` actions aren't
2036
+ * drivable through `setVirtualAction` (no boolean/number/Vector2 call
2037
+ * shape maps to them) — use `injectPointerDelta`/`injectPointerPosition`. */
2038
+ private static virtualValueCategory(
2039
+ value: boolean | number | { x: number; y: number },
2040
+ ): ActionValueType {
2041
+ if (typeof value === 'boolean') return 'digital';
2042
+ if (typeof value === 'number') return 'scalar';
2043
+ return 'vector2';
2044
+ }
2045
+
2046
+ /**
2047
+ * Set a virtual value for `action`, OR'd/summed into the same action reads
2048
+ * humans drive (see the section doc comment above). A `boolean` holds a
2049
+ * digital action pressed until set `false`/cleared; a `number` or `{x,y}`
2050
+ * is a level value combined via the action's own scalar max-magnitude /
2051
+ * vector2 sum-then-clamp rule.
2052
+ *
2053
+ * A digital `false`→`true` transition manufactures a genuine `isJustPressed`
2054
+ * edge, exactly one real key-down does (pulse-runner friction #4 —
2055
+ * previously this write-half only ever held the action pressed with NO
2056
+ * edge at all; every edge-triggered ability driven through this direct
2057
+ * path was unprovable). Re-setting `true` while already held (auto-repeat)
2058
+ * does NOT re-fire it — see {@link applyVirtualActionValue}. The mirror
2059
+ * `true`→`false` transition manufactures an `isJustReleased` edge the same
2060
+ * way a real key-up does.
2061
+ *
2062
+ * Throws `InputActionError` for an unregistered `action`, and the same
2063
+ * valueType-mismatch error every typed getter throws (`assertValueType`)
2064
+ * when `value`'s shape doesn't match the action's declared `valueType`.
2065
+ *
2066
+ * Gated (`setEnabled(false)` — play stopped or editor-gated; #144: window
2067
+ * focus no longer gates machine input): writes NOTHING — matching
2068
+ * `setTouchButton`'s write-gate precedent above — and returns
2069
+ * `{delivered: false, reason}` rather than a silent no-op, so a probe/bot
2070
+ * never mistakes a swallowed actuation for a delivered one.
2071
+ */
2072
+ setVirtualAction(
2073
+ action: string,
2074
+ value: boolean | number | { x: number; y: number },
2075
+ ): { delivered: boolean; reason?: string } {
2076
+ this.assertActionExists(action, 'setVirtualAction');
2077
+ const expected = InputManager.virtualValueCategory(value);
2078
+ this.assertValueType(action, expected, 'setVirtualAction');
2079
+ if (!this.machineInputActive) {
2080
+ return { delivered: false, reason: 'input-gated (input disabled by the editor)' };
2081
+ }
2082
+ this.applyVirtualActionValue(action, value);
2083
+ return { delivered: true };
2084
+ }
2085
+
2086
+ /**
2087
+ * The write half of {@link setVirtualAction} — extracted so D15/T-D15.5's
2088
+ * `scheduleActionAtTick` can apply a non-boolean (scalar/vector2) queued
2089
+ * actuation through the exact same code path (no gate check here: the two
2090
+ * callers gate differently — `setVirtualAction` above checks
2091
+ * `inputActive` before calling this; `applyScheduledActionsForTick` is
2092
+ * only ever reached from inside `poll()` AFTER its own gate check has
2093
+ * already returned early on a gated tick). `applyScheduledActionsForTick`
2094
+ * still handles digital `true` itself rather than calling here (its
2095
+ * one-tick edge must land EXACTLY at the scheduled tick — see its own doc
2096
+ * comment) — this method's digital branch is `setVirtualAction`'s direct
2097
+ * path, called at an arbitrary point relative to the poll()/endFrame()
2098
+ * bracket.
2099
+ *
2100
+ * pulse-runner friction #4 — the digital branch used to be a bare held-flag
2101
+ * write with no manufactured edge at all (the comment that used to sit
2102
+ * here read: "A digital value is handled by `applyScheduledActionsForTick`
2103
+ * itself (it needs the extra just-pressed edge a plain held-value write
2104
+ * doesn't produce)" — i.e. the edge fix shipped ONLY for the scheduled
2105
+ * path, never for this one, which is exactly the gap a blind dogfood run
2106
+ * hit: `setVirtualAction('jump', true)` held the action down but
2107
+ * `isJustPressed('jump')` never fired). Fixed here by reusing
2108
+ * `tapVirtualAction`'s exact one-tick-pulse queue mechanism for this
2109
+ * DIRECT case, specifically because this method's caller can land at any
2110
+ * point relative to `poll()`: writing `virtualDigitalTapActive` directly
2111
+ * here (as the scheduled path safely does, from inside `poll()` itself)
2112
+ * would risk being silently wiped by `poll()`'s own unconditional
2113
+ * `virtualDigitalTapActive = virtualDigitalTapQueued` reassignment,
2114
+ * depending on timing. Queueing side-steps that hazard by construction —
2115
+ * the same reason `tapVirtualAction` queues instead of writing directly. A
2116
+ * redundant `true` while already held (`virtualDigitalHeld` already has
2117
+ * `action`) does not queue again, matching a real key's auto-repeat never
2118
+ * re-firing `isJustPressed`. The `false` branch mirrors real `onKeyUp`
2119
+ * instead (immediate write, no queue needed — nothing in `poll()`
2120
+ * reassigns `virtualDigitalJustReleased`), and only fires when the action
2121
+ * was actually held (no released→released re-fire).
2122
+ */
2123
+ private applyVirtualActionValue(
2124
+ action: string,
2125
+ value: boolean | number | { x: number; y: number },
2126
+ ): void {
2127
+ switch (InputManager.virtualValueCategory(value)) {
2128
+ case 'digital':
2129
+ if (value as boolean) {
2130
+ if (!this.virtualDigitalHeld.has(action)) this.virtualDigitalTapQueued.add(action);
2131
+ this.virtualDigitalHeld.add(action);
2132
+ } else {
2133
+ if (this.virtualDigitalHeld.has(action)) this.virtualDigitalJustReleased.add(action);
2134
+ this.virtualDigitalHeld.delete(action);
2135
+ }
2136
+ break;
2137
+ case 'scalar':
2138
+ this.virtualScalarValues.set(action, value as number);
2139
+ break;
2140
+ default:
2141
+ this.virtualVector2Values.set(action, value as { x: number; y: number });
2142
+ break;
2143
+ }
2144
+ }
2145
+
2146
+ /**
2147
+ * One-frame press of a `'digital'` action (Task 1.4): queues `action` so it
2148
+ * reads `isJustPressed`/`isPressed` true for EXACTLY the next `poll()`'s
2149
+ * frame — promoted to the "active" set at the top of `poll()`, cleared in
2150
+ * `endFrame()` alongside every other just-pressed edge set (see both
2151
+ * above). Hollowstone's `queued`/`beginFrame` tap contract, ported onto
2152
+ * this engine's existing fixed-step `poll()`/`endFrame()` bracket instead
2153
+ * of a bespoke `beginFrame()` hook.
2154
+ *
2155
+ * Throws the same errors as {@link setVirtualAction} (unknown action;
2156
+ * non-digital valueType). A gated tap does NOT queue — it must not fire on
2157
+ * a later frame once input becomes active again — and returns
2158
+ * `{delivered: false, reason}`.
2159
+ */
2160
+ tapVirtualAction(action: string): { delivered: boolean; reason?: string } {
2161
+ this.assertActionExists(action, 'tapVirtualAction');
2162
+ this.assertValueType(action, 'digital', 'tapVirtualAction');
2163
+ if (!this.machineInputActive) {
2164
+ return { delivered: false, reason: 'input-gated (input disabled by the editor)' };
2165
+ }
2166
+ this.virtualDigitalTapQueued.add(action);
2167
+ return { delivered: true };
2168
+ }
2169
+
2170
+ /** Clear all virtual-action state — held digitals, queued/active taps, and
2171
+ * scalar/vector2 values. `flushHeldState()` (the same flush
2172
+ * `setEnabled(false)`/real blur apply to held keys/mouse/touch buttons)
2173
+ * shares the underlying wipe, so a bot's held action never survives input
2174
+ * being suspended — but only THIS public entry point (a deliberate "let
2175
+ * go", the mirror of `setVirtualAction(action, false)` below) manufactures
2176
+ * an `isJustReleased` edge for whatever was held; the suspend path stays
2177
+ * silent (see `flushHeldState`'s own doc comment). */
2178
+ clearVirtualActions(): void {
2179
+ this.clearVirtualActionState({ manufactureReleaseEdge: true });
2180
+ }
2181
+
2182
+ /**
2183
+ * D15/T-D15.5 (`docs/D15-DETERMINISM-DESIGN.md` §2.c) — schedule a virtual
2184
+ * actuation for exactly `tick`: applied at the START of that tick's
2185
+ * `poll()` (composing with `runTicks` — a schedule for tick 500 fires
2186
+ * exactly once the sim has been driven through tick 500, regardless of
2187
+ * burst size). A digital `true` produces a genuine `isJustPressed` edge
2188
+ * exactly at the target tick (not merely a held value — see
2189
+ * `applyScheduledActionsForTick`).
2190
+ *
2191
+ * Throws `InputActionError` for an unregistered `action` and the same
2192
+ * valueType-mismatch error every typed getter throws (`assertValueType`) —
2193
+ * both checked EAGERLY, at schedule time, not deferred to the target tick.
2194
+ * Throws `InputTickError` (`code: 'TICK_ALREADY_PASSED'`) when `tick` is
2195
+ * strictly less than the tick `poll()` will service NEXT — scheduling only
2196
+ * ever reaches the current or a future tick. A tick whose input phase
2197
+ * never runs (this world paused/frozen at that point, or a multi-tick gap
2198
+ * — see `poll()`) drops its scheduled entries rather than replaying them
2199
+ * late — each drop emits one `'input.schedule.dropped'` debug event
2200
+ * (`{tick, action}`) via {@link setDebugEmit}'s sink, if one is wired.
2201
+ */
2202
+ scheduleActionAtTick(
2203
+ tick: number,
2204
+ action: string,
2205
+ value: boolean | number | { x: number; y: number },
2206
+ ): void {
2207
+ this.assertActionExists(action, 'scheduleActionAtTick');
2208
+ const expected = InputManager.virtualValueCategory(value);
2209
+ this.assertValueType(action, expected, 'scheduleActionAtTick');
2210
+ if (tick < this.currentTick) {
2211
+ throw new InputTickError(tick, this.currentTick);
2212
+ }
2213
+ const pending = this.scheduledActions.get(tick);
2214
+ if (pending) pending.push({ action, value });
2215
+ else this.scheduledActions.set(tick, [{ action, value }]);
2216
+ }
2217
+
2218
+ /** Wire (or clear, via `null`) the sink `poll()` calls with
2219
+ * `'input.schedule.dropped'` (`{tick, action}`) whenever it discards a
2220
+ * scheduled entry for a tick whose input phase never ran. Called once by
2221
+ * whoever constructs this `InputManager` with a `DebugRegistry` behind it
2222
+ * (`vgai-scene-game-adapter.ts`), the same seed spot as
2223
+ * `setVirtualInputTarget`/`setInputActionsSource` — never called directly
2224
+ * by gameplay code. */
2225
+ setDebugEmit(fn: ((event: string, detail?: unknown) => void) | null): void {
2226
+ this.debugEmit = fn;
2227
+ }
2228
+
2229
+ /** D15/T-D15.5 — start (or restart) recording the post-gate action-delta
2230
+ * trace: clears any prior trace/snapshot, so a fresh recording session
2231
+ * never carries over stale deltas from an earlier one. */
2232
+ startInputRecording(): void {
2233
+ this.inputRecording = true;
2234
+ this.inputTrace = [];
2235
+ this.lastTraceSnapshot.clear();
2236
+ }
2237
+
2238
+ /** D15/T-D15.5 — stop recording (the trace accumulated so far stays
2239
+ * readable via {@link getInputTrace} until the next `startInputRecording`
2240
+ * clears it). Idempotent. */
2241
+ stopInputRecording(): void {
2242
+ this.inputRecording = false;
2243
+ }
2244
+
2245
+ /** D15/T-D15.5 — whether a recording is currently active. */
2246
+ isInputRecording(): boolean {
2247
+ return this.inputRecording;
2248
+ }
2249
+
2250
+ /** D15/T-D15.5 — the recorded trace so far (a defensive copy), versioned
2251
+ * per the design doc's format sketch (§2.c). SP5 (out of scope here) is
2252
+ * the eventual consumer that replays this; this module only ever
2253
+ * RECORDS. */
2254
+ getInputTrace(): {
2255
+ version: 1;
2256
+ ticks: { tick: number; actions: Record<string, boolean | number | { x: number; y: number }> }[];
2257
+ } {
2258
+ return { version: 1, ticks: this.inputTrace.slice() };
2259
+ }
2260
+
1582
2261
  /** Request pointer lock for FPS-style mouse control */
1583
2262
  requestPointerLock(element: HTMLElement) {
1584
2263
  // Idempotent: never stack duplicate click listeners. Rewire if the element