@vgai/engine 0.5.21 → 0.5.23

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 (339) hide show
  1. package/README.md +6 -3
  2. package/dist/adapter/adapter-module.d.ts.map +1 -1
  3. package/dist/adapter/adapter-module.js +2 -4
  4. package/dist/adapter/adapter-surface.d.ts +1 -1
  5. package/dist/adapter/adapter-surface.js +1 -1
  6. package/dist/adapter/authoring.d.ts +19 -21
  7. package/dist/adapter/authoring.d.ts.map +1 -1
  8. package/dist/adapter/authoring.js +2 -2
  9. package/dist/adapter/binding.d.ts +341 -0
  10. package/dist/adapter/binding.d.ts.map +1 -0
  11. package/dist/adapter/binding.js +148 -0
  12. package/dist/adapter/colyseus-networking-adapter.d.ts.map +1 -1
  13. package/dist/adapter/colyseus-networking-adapter.js +23 -4
  14. package/dist/adapter/entrypoint-selection-readers.d.ts +24 -0
  15. package/dist/adapter/entrypoint-selection-readers.d.ts.map +1 -0
  16. package/dist/adapter/entrypoint-selection-readers.js +63 -0
  17. package/dist/adapter/entrypoint-selection-source.d.ts.map +1 -1
  18. package/dist/adapter/entrypoint-selection-source.js +1 -44
  19. package/dist/adapter/finders/scenes-from-entrypoint-selection.d.ts.map +1 -1
  20. package/dist/adapter/finders/scenes-from-entrypoint-selection.js +1 -52
  21. package/dist/adapter/host-context.d.ts +46 -15
  22. package/dist/adapter/host-context.d.ts.map +1 -1
  23. package/dist/adapter/host-context.js +2 -9
  24. package/dist/adapter/index.d.ts +11 -12
  25. package/dist/adapter/index.d.ts.map +1 -1
  26. package/dist/adapter/index.js +9 -10
  27. package/dist/adapter/ingest/game-contract.d.ts +0 -3
  28. package/dist/adapter/ingest/game-contract.d.ts.map +1 -1
  29. package/dist/adapter/ingest/game-contract.js +0 -3
  30. package/dist/adapter/ingest/scene-capture.d.ts +19 -23
  31. package/dist/adapter/ingest/scene-capture.d.ts.map +1 -1
  32. package/dist/adapter/ingest/scene-capture.js +143 -17
  33. package/dist/adapter/ingest/visible-capture-window.d.ts +34 -0
  34. package/dist/adapter/ingest/visible-capture-window.d.ts.map +1 -1
  35. package/dist/adapter/manifest-interpreter.d.ts +59 -0
  36. package/dist/adapter/manifest-interpreter.d.ts.map +1 -0
  37. package/dist/adapter/manifest-interpreter.js +50 -0
  38. package/dist/adapter/native-debug-module.d.ts +71 -3
  39. package/dist/adapter/native-debug-module.d.ts.map +1 -1
  40. package/dist/adapter/native-debug-module.js +146 -10
  41. package/dist/adapter/rapier-physics-adapter.d.ts +31 -1
  42. package/dist/adapter/rapier-physics-adapter.d.ts.map +1 -1
  43. package/dist/adapter/rapier-physics-adapter.js +50 -36
  44. package/dist/adapter/renderer-config.d.ts +3 -3
  45. package/dist/adapter/renderer-config.js +3 -3
  46. package/dist/adapter/root-adapter.d.ts +7 -7
  47. package/dist/adapter/root-adapter.js +4 -4
  48. package/dist/adapter/system-adapter.d.ts +45 -7
  49. package/dist/adapter/system-adapter.d.ts.map +1 -1
  50. package/dist/adapter/system-seam-contract.d.ts +3 -3
  51. package/dist/adapter/system-seam-contract.js +3 -3
  52. package/dist/adapter/system-slot.d.ts +168 -0
  53. package/dist/adapter/system-slot.d.ts.map +1 -0
  54. package/dist/adapter/system-slot.js +229 -0
  55. package/dist/adapter/transform.d.ts +11 -1
  56. package/dist/adapter/transform.d.ts.map +1 -1
  57. package/dist/ai/navigation.d.ts +17 -1
  58. package/dist/ai/navigation.d.ts.map +1 -1
  59. package/dist/ai/navigation.js +26 -5
  60. package/dist/animation/anim-graph-types.d.ts +11 -3
  61. package/dist/animation/anim-graph-types.d.ts.map +1 -1
  62. package/dist/animation/theatre-clock-binding.d.ts +4 -4
  63. package/dist/animation/theatre-clock-binding.js +4 -4
  64. package/dist/animation/xstate-animation-binding.d.ts.map +1 -1
  65. package/dist/animation/xstate-animation-binding.js +2 -2
  66. package/dist/animation/xstate-animation-meta.d.ts +13 -0
  67. package/dist/animation/xstate-animation-meta.d.ts.map +1 -1
  68. package/dist/animation/xstate-animation-meta.js +14 -0
  69. package/dist/asset-formats/index.d.ts +4 -7
  70. package/dist/asset-formats/index.d.ts.map +1 -1
  71. package/dist/asset-formats/index.js +4 -7
  72. package/dist/asset-formats/render-env.d.ts +5 -8
  73. package/dist/asset-formats/render-env.d.ts.map +1 -1
  74. package/dist/asset-formats/render-env.js +3 -6
  75. package/dist/asset-parse-error.d.ts +2 -5
  76. package/dist/asset-parse-error.d.ts.map +1 -1
  77. package/dist/asset-parse-error.js +2 -5
  78. package/dist/audio/bus-mixer.d.ts +101 -0
  79. package/dist/audio/bus-mixer.d.ts.map +1 -0
  80. package/dist/audio/bus-mixer.js +115 -0
  81. package/dist/canvas-react/index.d.ts +5 -12
  82. package/dist/canvas-react/index.d.ts.map +1 -1
  83. package/dist/canvas-react/index.js +5 -12
  84. package/dist/canvas-react/pixi-react-root-factory.d.ts +34 -21
  85. package/dist/canvas-react/pixi-react-root-factory.d.ts.map +1 -1
  86. package/dist/canvas-react/pixi-react-root-factory.js +274 -36
  87. package/dist/core/countdown-timer.d.ts +118 -0
  88. package/dist/core/countdown-timer.d.ts.map +1 -0
  89. package/dist/core/countdown-timer.js +137 -0
  90. package/dist/core/deferred-commands.d.ts +127 -0
  91. package/dist/core/deferred-commands.d.ts.map +1 -0
  92. package/dist/core/deferred-commands.js +132 -0
  93. package/dist/core/frame-pacing.d.ts +1 -1
  94. package/dist/core/frame-pacing.js +2 -2
  95. package/dist/core/game-loop.d.ts +1 -3
  96. package/dist/core/game-loop.d.ts.map +1 -1
  97. package/dist/core/game-loop.js +1 -3
  98. package/dist/core/seeded-random.d.ts +1 -1
  99. package/dist/core/sim-clock.d.ts +19 -8
  100. package/dist/core/sim-clock.d.ts.map +1 -1
  101. package/dist/core/sim-clock.js +18 -7
  102. package/dist/core/types.d.ts +1 -3
  103. package/dist/core/types.d.ts.map +1 -1
  104. package/dist/defaults.d.ts +6 -14
  105. package/dist/defaults.d.ts.map +1 -1
  106. package/dist/defaults.js +6 -14
  107. package/dist/dev/instruments.d.ts +38 -11
  108. package/dist/dev/instruments.d.ts.map +1 -1
  109. package/dist/dev/instruments.js +81 -11
  110. package/dist/dev/register-render-vitals.d.ts +1 -4
  111. package/dist/dev/register-render-vitals.d.ts.map +1 -1
  112. package/dist/dev/register-render-vitals.js +1 -4
  113. package/dist/dev/render-debug-adapter.d.ts +2 -3
  114. package/dist/dev/render-debug-adapter.d.ts.map +1 -1
  115. package/dist/dev/render-debug-adapter.js +2 -3
  116. package/dist/dev/render-vitals.d.ts +3 -3
  117. package/dist/dev/render-vitals.js +3 -3
  118. package/dist/dev/webgl-frame-capture.d.ts +1 -1
  119. package/dist/dev/webgl-frame-capture.js +1 -1
  120. package/dist/index.d.ts +2 -5
  121. package/dist/index.d.ts.map +1 -1
  122. package/dist/index.js +2 -4
  123. package/dist/input/host-pointer.d.ts +122 -0
  124. package/dist/input/host-pointer.d.ts.map +1 -0
  125. package/dist/input/host-pointer.js +160 -0
  126. package/dist/input/input-manager.d.ts +33 -5
  127. package/dist/input/input-manager.d.ts.map +1 -1
  128. package/dist/input/input-manager.js +48 -13
  129. package/dist/input/rebind-controller.d.ts.map +1 -1
  130. package/dist/input/rebind-controller.js +6 -8
  131. package/dist/input/schema.d.ts.map +1 -1
  132. package/dist/input/schema.js +56 -28
  133. package/dist/manifest/filename.d.ts.map +1 -1
  134. package/dist/manifest/filename.js +8 -12
  135. package/dist/manifest/load.d.ts +10 -10
  136. package/dist/manifest/load.d.ts.map +1 -1
  137. package/dist/manifest/load.js +9 -6
  138. package/dist/manifest/schema.d.ts +17 -21
  139. package/dist/manifest/schema.d.ts.map +1 -1
  140. package/dist/manifest/schema.js +45 -35
  141. package/dist/physics/collider-dimensions.d.ts +8 -12
  142. package/dist/physics/collider-dimensions.d.ts.map +1 -1
  143. package/dist/physics/collider-dimensions.js +8 -12
  144. package/dist/pixi/scene-capture.d.ts +11 -13
  145. package/dist/pixi/scene-capture.d.ts.map +1 -1
  146. package/dist/react/world-state.d.ts +1 -1
  147. package/dist/react/world-state.d.ts.map +1 -1
  148. package/dist/render/directional-shadow-fit.d.ts +88 -0
  149. package/dist/render/directional-shadow-fit.d.ts.map +1 -0
  150. package/dist/render/directional-shadow-fit.js +133 -0
  151. package/dist/render/environment-capture.d.ts +68 -0
  152. package/dist/render/environment-capture.d.ts.map +1 -0
  153. package/dist/render/environment-capture.js +90 -0
  154. package/dist/render/ibl-override-material.d.ts +97 -0
  155. package/dist/render/ibl-override-material.d.ts.map +1 -0
  156. package/dist/render/ibl-override-material.js +114 -0
  157. package/dist/render/light-camera-factory.d.ts +5 -7
  158. package/dist/render/light-camera-factory.d.ts.map +1 -1
  159. package/dist/render/light-camera-factory.js +8 -11
  160. package/dist/render/material-factory.js +1 -1
  161. package/dist/render/render-batch-system.d.ts.map +1 -1
  162. package/dist/render/render-features.d.ts +17 -6
  163. package/dist/render/render-features.d.ts.map +1 -1
  164. package/dist/render/render-features.js +0 -28
  165. package/dist/runtime/create-runtime.d.ts +34 -25
  166. package/dist/runtime/create-runtime.d.ts.map +1 -1
  167. package/dist/runtime/create-runtime.js +218 -93
  168. package/dist/runtime/debug-registry.d.ts +77 -8
  169. package/dist/runtime/debug-registry.d.ts.map +1 -1
  170. package/dist/runtime/debug-registry.js +3 -3
  171. package/dist/runtime/dev-build.d.ts +2 -2
  172. package/dist/runtime/dev-build.js +2 -2
  173. package/dist/runtime/game-input-seams.d.ts +51 -0
  174. package/dist/runtime/game-input-seams.d.ts.map +1 -0
  175. package/dist/runtime/game-input-seams.js +86 -0
  176. package/dist/runtime/game.d.ts +104 -40
  177. package/dist/runtime/game.d.ts.map +1 -1
  178. package/dist/runtime/game.js +119 -61
  179. package/dist/runtime/mount-game.d.ts +0 -16
  180. package/dist/runtime/mount-game.d.ts.map +1 -1
  181. package/dist/runtime/mount-game.js +26 -41
  182. package/dist/runtime/mount-manifest.d.ts +22 -14
  183. package/dist/runtime/mount-manifest.d.ts.map +1 -1
  184. package/dist/runtime/mount-manifest.js +22 -20
  185. package/dist/runtime/playtest.d.ts +22 -0
  186. package/dist/runtime/playtest.d.ts.map +1 -0
  187. package/dist/runtime/playtest.js +12 -0
  188. package/dist/runtime/state-bridge.d.ts +2 -3
  189. package/dist/runtime/state-bridge.d.ts.map +1 -1
  190. package/dist/runtime/state-bridge.js +1 -0
  191. package/dist/setup/setup-audio.d.ts +12 -0
  192. package/dist/setup/setup-audio.d.ts.map +1 -1
  193. package/dist/setup/setup-audio.js +9 -10
  194. package/dist/setup/setup-renderer.d.ts +2 -25
  195. package/dist/setup/setup-renderer.d.ts.map +1 -1
  196. package/dist/setup/setup-renderer.js +2 -31
  197. package/dist/world3d-react/index.d.ts +15 -25
  198. package/dist/world3d-react/index.d.ts.map +1 -1
  199. package/dist/world3d-react/index.js +15 -25
  200. package/dist/world3d-react/r3f-root-factory.d.ts +28 -34
  201. package/dist/world3d-react/r3f-root-factory.d.ts.map +1 -1
  202. package/dist/world3d-react/r3f-root-factory.js +403 -57
  203. package/dist/world3d-react/rapier-physics-bridge.d.ts +30 -17
  204. package/dist/world3d-react/rapier-physics-bridge.d.ts.map +1 -1
  205. package/dist/world3d-react/rapier-physics-bridge.js +143 -58
  206. package/package.json +1 -1
  207. package/schemas/engine-capabilities.json +16 -14
  208. package/schemas/vgai-project.schema.json +35 -26
  209. package/src/adapter/adapter-module.ts +2 -4
  210. package/src/adapter/adapter-surface.ts +1 -1
  211. package/src/adapter/authoring.ts +19 -21
  212. package/src/adapter/binding.ts +468 -0
  213. package/src/adapter/colyseus-networking-adapter.ts +20 -5
  214. package/src/adapter/entrypoint-selection-readers.ts +66 -0
  215. package/src/adapter/entrypoint-selection-source.ts +1 -40
  216. package/src/adapter/finders/scenes-from-entrypoint-selection.ts +1 -48
  217. package/src/adapter/host-context.ts +45 -15
  218. package/src/adapter/index.ts +23 -11
  219. package/src/adapter/ingest/game-contract.ts +0 -3
  220. package/src/adapter/ingest/scene-capture.ts +190 -39
  221. package/src/adapter/ingest/visible-capture-window.ts +35 -0
  222. package/src/adapter/manifest-interpreter.ts +64 -0
  223. package/src/adapter/native-debug-module.ts +248 -15
  224. package/src/adapter/rapier-physics-adapter.ts +76 -20
  225. package/src/adapter/renderer-config.ts +3 -3
  226. package/src/adapter/root-adapter.ts +7 -7
  227. package/src/adapter/system-adapter.ts +43 -8
  228. package/src/adapter/system-seam-contract.ts +3 -3
  229. package/src/adapter/system-slot.ts +291 -0
  230. package/src/adapter/transform.ts +18 -1
  231. package/src/ai/navigation.ts +30 -3
  232. package/src/animation/anim-graph-types.ts +11 -3
  233. package/src/animation/theatre-clock-binding.ts +4 -4
  234. package/src/animation/xstate-animation-binding.ts +2 -2
  235. package/src/animation/xstate-animation-meta.ts +16 -0
  236. package/src/asset-formats/index.ts +4 -7
  237. package/src/asset-formats/render-env.ts +3 -6
  238. package/src/asset-parse-error.ts +2 -5
  239. package/src/audio/bus-mixer.ts +161 -0
  240. package/src/canvas-react/index.ts +5 -21
  241. package/src/canvas-react/pixi-react-root-factory.tsx +301 -39
  242. package/src/core/countdown-timer.ts +188 -0
  243. package/src/core/deferred-commands.ts +174 -0
  244. package/src/core/frame-pacing.ts +2 -2
  245. package/src/core/game-loop.ts +1 -3
  246. package/src/core/seeded-random.ts +1 -1
  247. package/src/core/sim-clock.ts +19 -8
  248. package/src/core/types.ts +1 -3
  249. package/src/defaults.ts +6 -14
  250. package/src/dev/instruments.ts +108 -11
  251. package/src/dev/register-render-vitals.ts +1 -4
  252. package/src/dev/render-debug-adapter.ts +2 -3
  253. package/src/dev/render-vitals.ts +3 -3
  254. package/src/dev/webgl-frame-capture.ts +1 -1
  255. package/src/index.ts +2 -5
  256. package/src/input/host-pointer.ts +230 -0
  257. package/src/input/input-manager.ts +49 -13
  258. package/src/input/input-types.ts +1 -1
  259. package/src/input/rebind-controller.ts +6 -8
  260. package/src/input/schema.ts +216 -188
  261. package/src/manifest/filename.ts +8 -12
  262. package/src/manifest/load.ts +17 -13
  263. package/src/manifest/schema.ts +55 -47
  264. package/src/physics/collider-dimensions.ts +8 -12
  265. package/src/pixi/scene-capture.ts +12 -15
  266. package/src/react/world-state.tsx +1 -1
  267. package/src/render/directional-shadow-fit.ts +156 -0
  268. package/src/render/environment-capture.ts +102 -0
  269. package/src/render/ibl-override-material.ts +170 -0
  270. package/src/render/light-camera-factory.ts +8 -11
  271. package/src/render/material-factory.ts +1 -1
  272. package/src/render/render-batch-system.ts +14 -6
  273. package/src/render/render-features.ts +17 -35
  274. package/src/runtime/create-runtime.ts +270 -108
  275. package/src/runtime/debug-registry.ts +84 -8
  276. package/src/runtime/dev-build.ts +2 -2
  277. package/src/runtime/game-input-seams.ts +108 -0
  278. package/src/runtime/game.ts +231 -85
  279. package/src/runtime/mount-game.ts +31 -57
  280. package/src/runtime/mount-manifest.ts +47 -36
  281. package/src/runtime/playtest.ts +22 -0
  282. package/src/runtime/state-bridge.ts +3 -3
  283. package/src/setup/setup-audio.ts +21 -11
  284. package/src/setup/setup-renderer.ts +2 -61
  285. package/src/world3d-react/index.ts +15 -31
  286. package/src/world3d-react/r3f-root-factory.tsx +451 -60
  287. package/src/world3d-react/rapier-physics-bridge.tsx +154 -58
  288. package/dist/adapter/setup-three-root-adapter.d.ts +0 -86
  289. package/dist/adapter/setup-three-root-adapter.d.ts.map +0 -1
  290. package/dist/adapter/setup-three-root-adapter.js +0 -908
  291. package/dist/animation/clip-map.d.ts +0 -12
  292. package/dist/animation/clip-map.d.ts.map +0 -1
  293. package/dist/animation/clip-map.js +0 -31
  294. package/dist/asset-registry.d.ts +0 -38
  295. package/dist/asset-registry.d.ts.map +0 -1
  296. package/dist/asset-registry.js +0 -66
  297. package/dist/canvas-react/engine-bridge.d.ts +0 -45
  298. package/dist/canvas-react/engine-bridge.d.ts.map +0 -1
  299. package/dist/canvas-react/engine-bridge.js +0 -45
  300. package/dist/canvas-react/pixi-react-adapter.d.ts +0 -77
  301. package/dist/canvas-react/pixi-react-adapter.d.ts.map +0 -1
  302. package/dist/canvas-react/pixi-react-adapter.js +0 -294
  303. package/dist/canvas-react/world-context.d.ts +0 -98
  304. package/dist/canvas-react/world-context.d.ts.map +0 -1
  305. package/dist/canvas-react/world-context.js +0 -173
  306. package/dist/dev/debug-draw.d.ts +0 -24
  307. package/dist/dev/debug-draw.d.ts.map +0 -1
  308. package/dist/dev/debug-draw.js +0 -73
  309. package/dist/render/auto-batcher.d.ts +0 -34
  310. package/dist/render/auto-batcher.d.ts.map +0 -1
  311. package/dist/render/auto-batcher.js +0 -140
  312. package/dist/render/lod.d.ts +0 -13
  313. package/dist/render/lod.d.ts.map +0 -1
  314. package/dist/render/lod.js +0 -16
  315. package/dist/runtime/types.d.ts +0 -303
  316. package/dist/runtime/types.d.ts.map +0 -1
  317. package/dist/runtime/types.js +0 -1
  318. package/dist/world3d-react/engine-bridge.d.ts +0 -47
  319. package/dist/world3d-react/engine-bridge.d.ts.map +0 -1
  320. package/dist/world3d-react/engine-bridge.js +0 -73
  321. package/dist/world3d-react/r3f-adapter.d.ts +0 -58
  322. package/dist/world3d-react/r3f-adapter.d.ts.map +0 -1
  323. package/dist/world3d-react/r3f-adapter.js +0 -634
  324. package/dist/world3d-react/world-context.d.ts +0 -182
  325. package/dist/world3d-react/world-context.d.ts.map +0 -1
  326. package/dist/world3d-react/world-context.js +0 -235
  327. package/src/adapter/setup-three-root-adapter.ts +0 -1032
  328. package/src/animation/clip-map.ts +0 -34
  329. package/src/asset-registry.ts +0 -89
  330. package/src/canvas-react/engine-bridge.ts +0 -59
  331. package/src/canvas-react/pixi-react-adapter.tsx +0 -356
  332. package/src/canvas-react/world-context.ts +0 -253
  333. package/src/dev/debug-draw.ts +0 -80
  334. package/src/render/auto-batcher.ts +0 -168
  335. package/src/render/lod.ts +0 -17
  336. package/src/runtime/types.ts +0 -328
  337. package/src/world3d-react/engine-bridge.ts +0 -86
  338. package/src/world3d-react/r3f-adapter.tsx +0 -717
  339. package/src/world3d-react/world-context.ts +0 -358
@@ -0,0 +1,137 @@
1
+ /**
2
+ * `Countdown` — a restartable interval the CALLER steps, with the two
3
+ * behaviours under a frame hitch that decide whether a game's event count is
4
+ * right.
5
+ *
6
+ * A respawn delay, a weapon cooldown, a wave spawner, a grace window: each is
7
+ * "count down from N seconds, tell me, maybe go again, and let me restart or
8
+ * stop you at any point". `core/sim-clock.ts` schedules a one-shot at an
9
+ * ABSOLUTE sim time — the right shape for "3 seconds from now, once" and the
10
+ * wrong one for a live interval that game code re-arms, reads
11
+ * ({@link Countdown.remaining} is what a HUD ring draws) and cancels.
12
+ *
13
+ * That gap used to be papered over in `sim-clock.ts`'s own header, which said
14
+ * an `after` re-arming itself was "three lines the game owns". Those three
15
+ * lines are wrong in a way nobody notices: re-arming schedules the next fire
16
+ * from the moment the callback RAN, so every long frame permanently lengthens
17
+ * the interval, and a game that hitches ten times has silently slowed its
18
+ * spawner. That is the drift {@link Countdown} exists to not have.
19
+ *
20
+ * ## It owns no clock, and that is the requirement rather than a shortcut
21
+ *
22
+ * There is no `setTimeout`, no `setInterval`, no `requestAnimationFrame` and no
23
+ * subscription anywhere in this module. A countdown advances only inside
24
+ * {@link Countdown.advance}, from seconds the caller already has — the fixed
25
+ * loop's own dt. This repo runs a fixed-step sim with display-rate
26
+ * presentation and can step a game deterministically; a countdown on wall-clock
27
+ * `setTimeout` would be a second, undeterministic timeline that no care
28
+ * downstream could re-sync. A countdown nobody advances does not fire —
29
+ * visibly, rather than drifting.
30
+ *
31
+ * ## The frame-hitch contract, which is the whole reason this is a module
32
+ *
33
+ * Both rules are about keeping the EVENT COUNT correct when `dt` is larger than
34
+ * the period, which is exactly when a game is already in trouble:
35
+ *
36
+ * - **Overshoot carries into the next period.** A 0.5 s repeating countdown
37
+ * advanced by 0.6 fires once and has 0.4 s left, not 0.5. Resetting to the
38
+ * full period instead would make one slow frame lengthen every interval
39
+ * after it — the drift above, one layer down.
40
+ * - **A single large step fires MORE THAN ONCE.** A 0.1 s countdown advanced
41
+ * by 0.35 fires three times and has 0.05 s left. Dropping the extras would
42
+ * silently slow a repeating countdown down whenever the frame was long, so a
43
+ * wave that should have spawned 3 enemies spawns 1 and the difficulty curve
44
+ * quietly depends on frame rate.
45
+ *
46
+ * Two consequences a caller must know about, both deliberate: `onElapsed` can
47
+ * be invoked several times within one `advance` call, and it runs INLINE
48
+ * (synchronously, inside `advance`) rather than on a microtask — so the caller
49
+ * always knows exactly which point of its frame the callback ran at.
50
+ *
51
+ * A one-shot countdown STOPS ITSELF BEFORE invoking `onElapsed`, so a callback
52
+ * that calls {@link Countdown.start} re-arms rather than fighting the stop.
53
+ * A callback that calls {@link Countdown.stop} ends the multi-fire loop
54
+ * immediately, which is what makes "fire once then disarm from inside the
55
+ * callback" work.
56
+ *
57
+ * ## Resource ownership
58
+ *
59
+ * **Owner:** whoever calls {@link createCountdown}. A countdown holds two
60
+ * numbers, a boolean and the `onElapsed` reference; it registers nothing,
61
+ * subscribes to nothing, and this module has no module-level state — so there
62
+ * is deliberately no `dispose()` and dropping the countdown drops everything it
63
+ * had. **Sharers:** none; a countdown is not shareable, because whoever
64
+ * advances it decides its timeline. **Teardown:** what DOES need unregistering
65
+ * is the countdown's slot in whatever set the owner advances each frame, and
66
+ * that is the owner's — the same way `SceneTree.addTimer` hands back a remover.
67
+ */
68
+ function assertDuration(value) {
69
+ if (!Number.isFinite(value) || value <= 0) {
70
+ throw new RangeError(`createCountdown: duration must be a finite, positive number of seconds, got ${String(value)}`);
71
+ }
72
+ }
73
+ /**
74
+ * Build a countdown.
75
+ *
76
+ * ```ts
77
+ * const wave = createCountdown({ duration: 8, repeats: true, autostart: true, onElapsed: spawnWave });
78
+ * useFrame((_, dt) => wave.advance(dt)); // the caller owns the step
79
+ * ```
80
+ */
81
+ export function createCountdown(options) {
82
+ const onElapsed = options.onElapsed;
83
+ const repeats = options.repeats === true;
84
+ let duration = options.duration ?? 1;
85
+ let remaining = 0;
86
+ let running = false;
87
+ assertDuration(duration);
88
+ const countdown = {
89
+ get duration() {
90
+ return duration;
91
+ },
92
+ get repeats() {
93
+ return repeats;
94
+ },
95
+ get remaining() {
96
+ return running ? remaining : 0;
97
+ },
98
+ get running() {
99
+ return running;
100
+ },
101
+ start(newDuration) {
102
+ if (newDuration !== undefined) {
103
+ assertDuration(newDuration);
104
+ duration = newDuration;
105
+ }
106
+ remaining = duration;
107
+ running = true;
108
+ },
109
+ stop() {
110
+ running = false;
111
+ remaining = 0;
112
+ },
113
+ advance(dt) {
114
+ if (!running)
115
+ return;
116
+ remaining -= dt;
117
+ // The loop is the multi-fire rule; `running` in the condition is what
118
+ // lets a callback's own stop() end it. Module header, frame-hitch.
119
+ while (running && remaining <= 0) {
120
+ if (!repeats) {
121
+ // Disarm BEFORE invoking, so a callback that calls start() re-arms
122
+ // instead of being immediately stopped by this branch.
123
+ running = false;
124
+ remaining = 0;
125
+ onElapsed();
126
+ return;
127
+ }
128
+ // Carry the overshoot rather than resetting to the full period.
129
+ remaining += duration;
130
+ onElapsed();
131
+ }
132
+ },
133
+ };
134
+ if (options.autostart === true)
135
+ countdown.start();
136
+ return countdown;
137
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * `DeferredCommands` — work the caller runs at ONE point it chooses, plus the
3
+ * subject-keyed disposal that has to land after it.
4
+ *
5
+ * The hazard is not exotic and it is not a foreign engine's: it is what a
6
+ * Rapier world does to anything that mutates it from inside a callback. Disable
7
+ * a collider while the solver is iterating, or free the node a collision
8
+ * handler was dispatched ON, and the failure is either a corrupt step or
9
+ * Rapier's own "recursive use of an object ... unsafe aliasing in rust". The
10
+ * remedy is always the same shape — write down WHAT to do, do it once the frame
11
+ * is out of anybody's way — and every caller that needs it was previously
12
+ * hand-rolling an array.
13
+ *
14
+ * ## The one point is the CALLER's, deliberately
15
+ *
16
+ * There is no `queueMicrotask`, no `setTimeout(0)`, no `requestAnimationFrame`
17
+ * and no subscription anywhere in this module. Whoever builds the queue decides
18
+ * where {@link DeferredCommands.drain} runs — after physics, at the tail of a
19
+ * world's update, wherever the frame's quiet moment actually is. A queue nobody
20
+ * drains simply does not run its work, which is a visible failure rather than a
21
+ * silent reordering onto a timeline the game does not control.
22
+ *
23
+ * ## This is NOT `SimClock`, and `after(0)` is not a substitute
24
+ *
25
+ * `core/sim-clock.ts` schedules on SIM TIME: `after(0, fn)` scheduled mid-frame
26
+ * does land in that frame's tail flush (`runtime/game.ts` calls `flush(simT)`
27
+ * after every phase of every world), so the resemblance is real and worth
28
+ * naming. Three things make it the wrong tool for deferral:
29
+ *
30
+ * - **A paused world never flushes.** `flush` is inside `runFrameImpl`'s
31
+ * `advanced` guard, so a game that pauses between the defer and the drain
32
+ * holds the work forever. End-of-frame deferral has to run on the frame,
33
+ * not on the clock.
34
+ * - **No subject identity.** Deferring destruction needs "this object, once",
35
+ * and needs the two questions that fall out of it —
36
+ * {@link DeferredCommands.isDisposePending} (a handler must be able to skip
37
+ * a subject that is already going away) and
38
+ * {@link DeferredCommands.wasDisposed} (an owner sweeping its own indexes
39
+ * after the drain). A time-ordered timer set answers neither.
40
+ * - **No same-drain re-entry.** A sim timer scheduled during a flush is
41
+ * deliberately never due in that flush; a deferred command that defers one
42
+ * more command must still land this frame, or the second write arrives a
43
+ * frame after the first and the two are no longer atomic.
44
+ *
45
+ * ## Ordering, and why disposal goes last
46
+ *
47
+ * {@link DeferredCommands.drain} runs every deferred command first, re-draining
48
+ * until the queue is empty, and only then disposes every pending subject. That
49
+ * order is not cosmetic: a deferred write to a subject that is ALSO queued for
50
+ * disposal must still happen — it is what the caller asked for — and disposing
51
+ * first would make that write operate on a freed handle. A subject queued twice
52
+ * is disposed once, and the FIRST thunk wins (every caller for one subject
53
+ * passes the same one).
54
+ *
55
+ * The re-drain is bounded by {@link MAX_DRAIN_PASSES}. A command that defers
56
+ * itself forever is a bug in the caller, and a bounded loop names it in a
57
+ * millisecond instead of hanging the frame.
58
+ *
59
+ * ## Why disposal carries its own thunk
60
+ *
61
+ * Disposal is the one thing here that is not surface-neutral. A `PIXI.Container`
62
+ * ends with `removeFromParent()` + `destroy({ children })`; a `THREE.Object3D`
63
+ * ends by detaching and disposing the geometries and materials underneath it;
64
+ * a physics-backed entity has a Rapier body to pull first. This queue does none
65
+ * of that — it takes the thunk from whoever owns the surface and runs it at the
66
+ * drain. So the ORDERING lives here once and the surface knowledge stays where
67
+ * the surface is, which is what lets one queue serve a 2D world and a 3D one.
68
+ *
69
+ * ## Resource ownership
70
+ *
71
+ * **Owner:** whoever calls {@link createDeferredCommands}, and that owner is
72
+ * also the only caller of `drain()` — a queue is per-DRAINER, not per-process,
73
+ * so two mounted games are two queues and neither can see the other's work.
74
+ * This module registers nothing anywhere: there is no game-scoped slot (unlike
75
+ * `sim-clock.ts`, which needs one because every world on a Game shares one
76
+ * clock) and no module-level state at all.
77
+ * **Sharers:** the queued subjects, by reference, and only until the drain that
78
+ * disposes them.
79
+ * **Teardown:** dropping the queue. There is deliberately no `dispose()` —
80
+ * nothing here holds a handle, a listener or a timer, and a queue dropped with
81
+ * work still in it has simply not run that work, which is what discarding a
82
+ * frame means.
83
+ */
84
+ /**
85
+ * How many times {@link DeferredCommands.drain} re-drains a queue that keeps
86
+ * refilling itself before declaring the caller's own logic non-terminating.
87
+ *
88
+ * A bound that correct code cannot reach (a deferred command that defers one
89
+ * more, ten deep) while catching the infinite case immediately.
90
+ */
91
+ export declare const MAX_DRAIN_PASSES = 16;
92
+ /** The queue. Build one per drainer with {@link createDeferredCommands}. */
93
+ export interface DeferredCommands {
94
+ /**
95
+ * Run `command` at the next {@link DeferredCommands.drain}.
96
+ *
97
+ * ```ts
98
+ * // inside a collision handler, where the solver still owns the world:
99
+ * frame.defer(() => collider.setSensor(true));
100
+ * ```
101
+ */
102
+ defer(command: () => void): void;
103
+ /**
104
+ * Dispose `subject` at the next drain, after every deferred command has run.
105
+ * Queuing the same subject twice disposes it once — the first `dispose`
106
+ * thunk wins.
107
+ */
108
+ deferDispose(subject: object, dispose: () => void): void;
109
+ /**
110
+ * Is `subject` queued for disposal and not yet disposed? Read by a handler
111
+ * that must skip a subject already on its way out.
112
+ */
113
+ isDisposePending(subject: object): boolean;
114
+ /**
115
+ * Has THIS queue already disposed `subject`? Read after a drain by an owner
116
+ * sweeping its own indexes (a group, a registry, a spatial bucket) — "am I
117
+ * destroyed" is a surface-specific question that a plain `Object3D` has no
118
+ * answer to, so the queue is asked instead.
119
+ */
120
+ wasDisposed(subject: object): boolean;
121
+ /** Run every deferred command (re-draining), then dispose every pending
122
+ * subject. Called once per frame, by the owner, at the point it chose. */
123
+ drain(): void;
124
+ }
125
+ /** Build one queue. See the module header for who owns it and who drains it. */
126
+ export declare function createDeferredCommands(): DeferredCommands;
127
+ //# sourceMappingURL=deferred-commands.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deferred-commands.d.ts","sourceRoot":"","sources":["../../src/core/deferred-commands.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkFG;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,KAAK,CAAC;AAEnC,4EAA4E;AAC5E,MAAM,WAAW,gBAAgB;IAC/B;;;;;;;OAOG;IACH,KAAK,CAAC,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IACjC;;;;OAIG;IACH,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IACzD;;;OAGG;IACH,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;IAC3C;;;;;OAKG;IACH,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;IACtC;+EAC2E;IAC3E,KAAK,IAAI,IAAI,CAAC;CACf;AAED,gFAAgF;AAChF,wBAAgB,sBAAsB,IAAI,gBAAgB,CA6CzD"}
@@ -0,0 +1,132 @@
1
+ /**
2
+ * `DeferredCommands` — work the caller runs at ONE point it chooses, plus the
3
+ * subject-keyed disposal that has to land after it.
4
+ *
5
+ * The hazard is not exotic and it is not a foreign engine's: it is what a
6
+ * Rapier world does to anything that mutates it from inside a callback. Disable
7
+ * a collider while the solver is iterating, or free the node a collision
8
+ * handler was dispatched ON, and the failure is either a corrupt step or
9
+ * Rapier's own "recursive use of an object ... unsafe aliasing in rust". The
10
+ * remedy is always the same shape — write down WHAT to do, do it once the frame
11
+ * is out of anybody's way — and every caller that needs it was previously
12
+ * hand-rolling an array.
13
+ *
14
+ * ## The one point is the CALLER's, deliberately
15
+ *
16
+ * There is no `queueMicrotask`, no `setTimeout(0)`, no `requestAnimationFrame`
17
+ * and no subscription anywhere in this module. Whoever builds the queue decides
18
+ * where {@link DeferredCommands.drain} runs — after physics, at the tail of a
19
+ * world's update, wherever the frame's quiet moment actually is. A queue nobody
20
+ * drains simply does not run its work, which is a visible failure rather than a
21
+ * silent reordering onto a timeline the game does not control.
22
+ *
23
+ * ## This is NOT `SimClock`, and `after(0)` is not a substitute
24
+ *
25
+ * `core/sim-clock.ts` schedules on SIM TIME: `after(0, fn)` scheduled mid-frame
26
+ * does land in that frame's tail flush (`runtime/game.ts` calls `flush(simT)`
27
+ * after every phase of every world), so the resemblance is real and worth
28
+ * naming. Three things make it the wrong tool for deferral:
29
+ *
30
+ * - **A paused world never flushes.** `flush` is inside `runFrameImpl`'s
31
+ * `advanced` guard, so a game that pauses between the defer and the drain
32
+ * holds the work forever. End-of-frame deferral has to run on the frame,
33
+ * not on the clock.
34
+ * - **No subject identity.** Deferring destruction needs "this object, once",
35
+ * and needs the two questions that fall out of it —
36
+ * {@link DeferredCommands.isDisposePending} (a handler must be able to skip
37
+ * a subject that is already going away) and
38
+ * {@link DeferredCommands.wasDisposed} (an owner sweeping its own indexes
39
+ * after the drain). A time-ordered timer set answers neither.
40
+ * - **No same-drain re-entry.** A sim timer scheduled during a flush is
41
+ * deliberately never due in that flush; a deferred command that defers one
42
+ * more command must still land this frame, or the second write arrives a
43
+ * frame after the first and the two are no longer atomic.
44
+ *
45
+ * ## Ordering, and why disposal goes last
46
+ *
47
+ * {@link DeferredCommands.drain} runs every deferred command first, re-draining
48
+ * until the queue is empty, and only then disposes every pending subject. That
49
+ * order is not cosmetic: a deferred write to a subject that is ALSO queued for
50
+ * disposal must still happen — it is what the caller asked for — and disposing
51
+ * first would make that write operate on a freed handle. A subject queued twice
52
+ * is disposed once, and the FIRST thunk wins (every caller for one subject
53
+ * passes the same one).
54
+ *
55
+ * The re-drain is bounded by {@link MAX_DRAIN_PASSES}. A command that defers
56
+ * itself forever is a bug in the caller, and a bounded loop names it in a
57
+ * millisecond instead of hanging the frame.
58
+ *
59
+ * ## Why disposal carries its own thunk
60
+ *
61
+ * Disposal is the one thing here that is not surface-neutral. A `PIXI.Container`
62
+ * ends with `removeFromParent()` + `destroy({ children })`; a `THREE.Object3D`
63
+ * ends by detaching and disposing the geometries and materials underneath it;
64
+ * a physics-backed entity has a Rapier body to pull first. This queue does none
65
+ * of that — it takes the thunk from whoever owns the surface and runs it at the
66
+ * drain. So the ORDERING lives here once and the surface knowledge stays where
67
+ * the surface is, which is what lets one queue serve a 2D world and a 3D one.
68
+ *
69
+ * ## Resource ownership
70
+ *
71
+ * **Owner:** whoever calls {@link createDeferredCommands}, and that owner is
72
+ * also the only caller of `drain()` — a queue is per-DRAINER, not per-process,
73
+ * so two mounted games are two queues and neither can see the other's work.
74
+ * This module registers nothing anywhere: there is no game-scoped slot (unlike
75
+ * `sim-clock.ts`, which needs one because every world on a Game shares one
76
+ * clock) and no module-level state at all.
77
+ * **Sharers:** the queued subjects, by reference, and only until the drain that
78
+ * disposes them.
79
+ * **Teardown:** dropping the queue. There is deliberately no `dispose()` —
80
+ * nothing here holds a handle, a listener or a timer, and a queue dropped with
81
+ * work still in it has simply not run that work, which is what discarding a
82
+ * frame means.
83
+ */
84
+ /**
85
+ * How many times {@link DeferredCommands.drain} re-drains a queue that keeps
86
+ * refilling itself before declaring the caller's own logic non-terminating.
87
+ *
88
+ * A bound that correct code cannot reach (a deferred command that defers one
89
+ * more, ten deep) while catching the infinite case immediately.
90
+ */
91
+ export const MAX_DRAIN_PASSES = 16;
92
+ /** Build one queue. See the module header for who owns it and who drains it. */
93
+ export function createDeferredCommands() {
94
+ let commands = [];
95
+ const doomed = new Map();
96
+ const disposed = new WeakSet();
97
+ return {
98
+ defer(command) {
99
+ commands.push(command);
100
+ },
101
+ deferDispose(subject, dispose) {
102
+ if (!doomed.has(subject))
103
+ doomed.set(subject, dispose);
104
+ },
105
+ isDisposePending(subject) {
106
+ return doomed.has(subject);
107
+ },
108
+ wasDisposed(subject) {
109
+ return disposed.has(subject);
110
+ },
111
+ drain() {
112
+ let passes = 0;
113
+ while (commands.length > 0) {
114
+ if (++passes > MAX_DRAIN_PASSES) {
115
+ throw new Error(`DeferredCommands.drain: still queueing more work after ${MAX_DRAIN_PASSES} passes. ` +
116
+ 'Something passed to defer() defers itself, so this drain would never terminate.');
117
+ }
118
+ const batch = commands;
119
+ commands = [];
120
+ for (const command of batch)
121
+ command();
122
+ }
123
+ // Subjects last, and only after every deferred command has run — see the
124
+ // module header's ordering section.
125
+ for (const [subject, dispose] of doomed) {
126
+ dispose();
127
+ disposed.add(subject);
128
+ }
129
+ doomed.clear();
130
+ },
131
+ };
132
+ }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Frame pacing — the accumulator/alpha arithmetic of the fixed-timestep loop,
3
- * extracted PURE (WO-P13, `docs/WORK.md` §2 P13).
3
+ * extracted PURE.
4
4
  *
5
5
  * `core/game-loop.ts` owns the browser side (rAF, `visibilitychange`,
6
6
  * `performance.now`); this module owns the arithmetic, so the interesting
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Frame pacing — the accumulator/alpha arithmetic of the fixed-timestep loop,
3
- * extracted PURE (WO-P13, `docs/WORK.md` §2 P13).
3
+ * extracted PURE.
4
4
  *
5
5
  * `core/game-loop.ts` owns the browser side (rAF, `visibilitychange`,
6
6
  * `performance.now`); this module owns the arithmetic, so the interesting
@@ -72,7 +72,7 @@ export function paceFrame(accumulator, rawDt, timeScale, limits) {
72
72
  // Also the accumulator's hard ceiling (see the spiral-of-death clamp below).
73
73
  const maxAccumulator = fixedDt * maxSubSteps;
74
74
  // Clamp large frame gaps (a slow frame, a debugger pause) BEFORE scaling by
75
- // timeScale — unchanged from the pre-WO-P13 loop.
75
+ // timeScale.
76
76
  const displayDt = Math.min(rawDt, maxAccumulator) * timeScale;
77
77
  let next = accumulator + displayDt;
78
78
  // Spiral-of-death guard: bound the accumulator regardless of timeScale or
@@ -1,9 +1,7 @@
1
1
  import type { GameLoopConfig, GameLoopLiveness } from './types';
2
2
  /**
3
3
  * Fixed-timestep game loop with accumulator pattern — SIM at a fixed rate,
4
- * PRESENTATION at the display's rate (WO-P13, `docs/WORK.md` §2 P13, which
5
- * flipped the former D1 "render is fixed-rate too" decision this comment used
6
- * to state).
4
+ * PRESENTATION at the display's rate.
7
5
  *
8
6
  * Per real (rAF) frame:
9
7
  *
@@ -1 +1 @@
1
- {"version":3,"file":"game-loop.d.ts","sourceRoot":"","sources":["../../src/core/game-loop.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAQhE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc;;;;IAuIjD;;;;;;;;;;;;OAYG;uBACa,gBAAgB;eASX,MAAM;IAe3B;;;;;;;OAOG;;EAKN"}
1
+ {"version":3,"file":"game-loop.d.ts","sourceRoot":"","sources":["../../src/core/game-loop.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAQhE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc;;;;IAuIjD;;;;;;;;;;;;OAYG;uBACa,gBAAgB;eASX,MAAM;IAe3B;;;;;;;OAOG;;EAKN"}
@@ -6,9 +6,7 @@ import { clampTimeScale, paceFrame, TIME_SCALE_RANGE } from './frame-pacing';
6
6
  const LOOP_STARVATION_MS = 1_000;
7
7
  /**
8
8
  * Fixed-timestep game loop with accumulator pattern — SIM at a fixed rate,
9
- * PRESENTATION at the display's rate (WO-P13, `docs/WORK.md` §2 P13, which
10
- * flipped the former D1 "render is fixed-rate too" decision this comment used
11
- * to state).
9
+ * PRESENTATION at the display's rate.
12
10
  *
13
11
  * Per real (rAF) frame:
14
12
  *
@@ -36,7 +36,7 @@ export declare const GAMEPLAY_STREAM = "gameplay";
36
36
  * `determinism.seededRandom` is declared, per T4.1's "no dead field" rule). */
37
37
  export declare const DEFAULT_SEEDED_RANDOM_SEED = 2654435769;
38
38
  /**
39
- * The `ctx.random` surface (`GameContext.random`, `runtime/types.ts`).
39
+ * A game-scoped random surface.
40
40
  * Callable (draws from the `'gameplay'` stream), plus:
41
41
  * - `stream(name)` — an independent named generator, `[0, 1)` floats, same
42
42
  * call signature as `Math.random`/the bare `SeededRandom` call itself.
@@ -93,10 +93,9 @@
93
93
  * controller cancels everything a `setup()` scheduled:
94
94
  *
95
95
  * ```ts
96
- * export async function setup(ctx: GameContext): Promise<GameCleanup> {
97
- * const ac = new AbortController();
98
- * ctx.clock.after(3, () => spawnWave(), { signal: ac.signal });
99
- * return { dispose: () => ac.abort() }; // cancels timers AND listeners
96
+ * const ac = new AbortController();
97
+ * clock.after(3, () => spawnWave(), { signal: ac.signal });
98
+ * // on dispose: ac.abort() cancels timers AND listeners
100
99
  * }
101
100
  * ```
102
101
  *
@@ -105,8 +104,20 @@
105
104
  *
106
105
  * ## Deliberately absent
107
106
  *
108
- * - **No `every`/`repeat`/interval.** Nothing needs one, and `after`
109
- * re-arming itself is three lines the game owns.
107
+ * - **No `every`/`repeat`/interval that is `core/countdown-timer.ts`.**
108
+ * This header used to say an `after` re-arming itself was "three lines the
109
+ * game owns"; it is three lines that DRIFT, because re-arming schedules the
110
+ * next fire from the moment the callback ran, so every long frame
111
+ * permanently lengthens the interval. A repeating interval needs
112
+ * overshoot carry and multi-fire on a big step to keep its event count
113
+ * right, and it is stepped by its owner rather than by the clock. Both
114
+ * belong to a countdown object, not to a sim-time one-shot.
115
+ * - **No end-of-frame command queue — that is
116
+ * `core/deferred-commands.ts`.** `after(0, fn)` looks like one (the flush
117
+ * below does run at the tail of the frame that scheduled it) and is not:
118
+ * it holds the work forever across a pause, it has no subject identity, and
119
+ * a command it schedules during a flush waits a whole extra frame. See that
120
+ * module's header for all three.
110
121
  * - **No fiber kernel, no coroutine emulation, no `task.spawn`.** Calling an
111
122
  * `async function` *is* `task.spawn` — the language already has it.
112
123
  * - **No wall clock.** This module reads no system timer and creates no
@@ -152,7 +163,7 @@ export interface SimClock {
152
163
  /**
153
164
  * The runtime-facing half — NOT for game code. Split off the public
154
165
  * {@link SimClock} the same way `runtime/game.ts` splits `GameInternal` off
155
- * `Game`: `GameContext.clock` is typed as `SimClock`, so game code cannot
166
+ * `Game`: game code only ever sees the public `SimClock`, so it cannot
156
167
  * reach `flush`/`dispose`.
157
168
  */
158
169
  export interface SimClockInternal extends SimClock {
@@ -193,7 +204,7 @@ export declare function registerSimClock(owner: object, clock: SimClockInternal)
193
204
  * Returns the INTERNAL view because this registry is engine-only — but a
194
205
  * caller that RESOLVES a clock here is not its owner and must not call
195
206
  * `dispose()` on it (module header, ownership). Game code never reaches it at
196
- * all: `GameContext.clock` is typed as the public {@link SimClock}, which has
207
+ * all: it only ever sees the public {@link SimClock}, which has
197
208
  * neither `flush` nor `dispose`, the same way `Game` hides `GameInternal`. */
198
209
  export declare function getSimClock(owner: object): SimClockInternal | null;
199
210
  //# sourceMappingURL=sim-clock.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sim-clock.d.ts","sourceRoot":"","sources":["../../src/core/sim-clock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiHG;AAEH,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAC;AAGpC,sFAAsF;AACtF,MAAM,WAAW,cAAc;IAC7B,4DAA4D;IAC5D,MAAM,IAAI,IAAI,CAAC;CAChB;AAED,iDAAiD;AACjD,MAAM,WAAW,kBAAkB;IACjC,8EAA8E;IAC9E,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;CAC3C;AAED,qEAAqE;AACrE,MAAM,WAAW,QAAQ;IACvB,wEAAwE;IACxE,GAAG,IAAI,MAAM,CAAC;IACd,2EAA2E;IAC3E,OAAO,IAAI,MAAM,CAAC;IAClB;;;;;OAKG;IACH,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjE;;;OAGG;IACH,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,IAAI,EAAE,IAAI,CAAC,EAAE,kBAAkB,GAAG,cAAc,CAAC;IAClF;;;;;OAKG;IACH,YAAY,CAAC,GAAG,EAAE,KAAK,CAAC,QAAQ,EAAE,OAAO,EAAE,MAAM,GAAG,cAAc,CAAC;CACpE;AAED;;;;;GAKG;AACH,MAAM,WAAW,gBAAiB,SAAQ,QAAQ;IAChD;;;;;OAKG;IACH,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B;;;;;OAKG;IACH,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,KAAK,CAAC,QAAQ,KAAK,IAAI,CAAC;CACjD;AAyBD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,eAAe,GAAG,gBAAgB,CAiJzE;AAWD,mFAAmF;AACnF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAE7E;AAED;;;;;;;;+EAQ+E;AAC/E,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CAElE"}
1
+ {"version":3,"file":"sim-clock.d.ts","sourceRoot":"","sources":["../../src/core/sim-clock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4HG;AAEH,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAC;AAGpC,sFAAsF;AACtF,MAAM,WAAW,cAAc;IAC7B,4DAA4D;IAC5D,MAAM,IAAI,IAAI,CAAC;CAChB;AAED,iDAAiD;AACjD,MAAM,WAAW,kBAAkB;IACjC,8EAA8E;IAC9E,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;CAC3C;AAED,qEAAqE;AACrE,MAAM,WAAW,QAAQ;IACvB,wEAAwE;IACxE,GAAG,IAAI,MAAM,CAAC;IACd,2EAA2E;IAC3E,OAAO,IAAI,MAAM,CAAC;IAClB;;;;;OAKG;IACH,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjE;;;OAGG;IACH,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,IAAI,EAAE,IAAI,CAAC,EAAE,kBAAkB,GAAG,cAAc,CAAC;IAClF;;;;;OAKG;IACH,YAAY,CAAC,GAAG,EAAE,KAAK,CAAC,QAAQ,EAAE,OAAO,EAAE,MAAM,GAAG,cAAc,CAAC;CACpE;AAED;;;;;GAKG;AACH,MAAM,WAAW,gBAAiB,SAAQ,QAAQ;IAChD;;;;;OAKG;IACH,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B;;;;;OAKG;IACH,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,KAAK,CAAC,QAAQ,KAAK,IAAI,CAAC;CACjD;AAyBD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,eAAe,GAAG,gBAAgB,CAiJzE;AAWD,mFAAmF;AACnF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAE7E;AAED;;;;;;;;+EAQ+E;AAC/E,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CAElE"}
@@ -93,10 +93,9 @@
93
93
  * controller cancels everything a `setup()` scheduled:
94
94
  *
95
95
  * ```ts
96
- * export async function setup(ctx: GameContext): Promise<GameCleanup> {
97
- * const ac = new AbortController();
98
- * ctx.clock.after(3, () => spawnWave(), { signal: ac.signal });
99
- * return { dispose: () => ac.abort() }; // cancels timers AND listeners
96
+ * const ac = new AbortController();
97
+ * clock.after(3, () => spawnWave(), { signal: ac.signal });
98
+ * // on dispose: ac.abort() cancels timers AND listeners
100
99
  * }
101
100
  * ```
102
101
  *
@@ -105,8 +104,20 @@
105
104
  *
106
105
  * ## Deliberately absent
107
106
  *
108
- * - **No `every`/`repeat`/interval.** Nothing needs one, and `after`
109
- * re-arming itself is three lines the game owns.
107
+ * - **No `every`/`repeat`/interval that is `core/countdown-timer.ts`.**
108
+ * This header used to say an `after` re-arming itself was "three lines the
109
+ * game owns"; it is three lines that DRIFT, because re-arming schedules the
110
+ * next fire from the moment the callback ran, so every long frame
111
+ * permanently lengthens the interval. A repeating interval needs
112
+ * overshoot carry and multi-fire on a big step to keep its event count
113
+ * right, and it is stepped by its owner rather than by the clock. Both
114
+ * belong to a countdown object, not to a sim-time one-shot.
115
+ * - **No end-of-frame command queue — that is
116
+ * `core/deferred-commands.ts`.** `after(0, fn)` looks like one (the flush
117
+ * below does run at the tail of the frame that scheduled it) and is not:
118
+ * it holds the work forever across a pause, it has no subject identity, and
119
+ * a command it schedules during a flush waits a whole extra frame. See that
120
+ * module's header for all three.
110
121
  * - **No fiber kernel, no coroutine emulation, no `task.spawn`.** Calling an
111
122
  * `async function` *is* `task.spawn` — the language already has it.
112
123
  * - **No wall clock.** This module reads no system timer and creates no
@@ -285,7 +296,7 @@ export function registerSimClock(owner, clock) {
285
296
  * Returns the INTERNAL view because this registry is engine-only — but a
286
297
  * caller that RESOLVES a clock here is not its owner and must not call
287
298
  * `dispose()` on it (module header, ownership). Game code never reaches it at
288
- * all: `GameContext.clock` is typed as the public {@link SimClock}, which has
299
+ * all: it only ever sees the public {@link SimClock}, which has
289
300
  * neither `flush` nor `dispose`, the same way `Game` hides `GameInternal`. */
290
301
  export function getSimClock(owner) {
291
302
  return clockByOwner.get(owner) ?? null;
@@ -56,9 +56,7 @@ export interface GameLoopConfig {
56
56
  */
57
57
  update: (dt: number) => void;
58
58
  /**
59
- * Called EXACTLY ONCE per real (rAF) frame — the PRESENTATION half
60
- * (WO-P13, `docs/WORK.md` §2 P13; this is the callback whose absence the
61
- * former D1 decision recorded).
59
+ * Called EXACTLY ONCE per real (rAF) frame — the PRESENTATION half.
62
60
  *
63
61
  * - `alpha` — `accumulator / fixedDt` after this frame's substeps were
64
62
  * consumed, in `[0, 1]`: how far presentation sits past the last
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,WAAW;;;;;;;;;CASd,CAAC;AAEX,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,OAAO,WAAW,CAAC,CAAC;AAE7E,+BAA+B;AAC/B,eAAO,MAAM,WAAW,EAAE,eAAe,EASxC,CAAC;AAEF,wDAAwD;AACxD,MAAM,MAAM,QAAQ,GAAG,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;AAE5C,MAAM,WAAW,aAAa;IAC5B,gEAAgE;IAChE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,iBAAiB;IAChC,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACvE,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACtE;AAED,yDAAyD;AACzD,MAAM,WAAW,SAAS;IACxB,uEAAuE;IACvE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,8CAA8C;IAC9C,KAAK,EAAE,eAAe,CAAC;IACvB,4EAA4E;IAC5E,IAAI,CAAC,IAAI,IAAI,CAAC;IACd,uBAAuB;IACvB,MAAM,EAAE,QAAQ,CAAC;IACjB,8DAA8D;IAC9D,OAAO,CAAC,IAAI,IAAI,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,gDAAgD;IAChD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;;;;OAUG;IACH,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B;;;;;;;;;;;;;;OAcG;IACH,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACpD;;;;;;;;;;OAUG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,cAAc,GAAG,SAAS,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,WAAW;;;;;;;;;CASd,CAAC;AAEX,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,OAAO,WAAW,CAAC,CAAC;AAE7E,+BAA+B;AAC/B,eAAO,MAAM,WAAW,EAAE,eAAe,EASxC,CAAC;AAEF,wDAAwD;AACxD,MAAM,MAAM,QAAQ,GAAG,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;AAE5C,MAAM,WAAW,aAAa;IAC5B,gEAAgE;IAChE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,iBAAiB;IAChC,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACvE,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACtE;AAED,yDAAyD;AACzD,MAAM,WAAW,SAAS;IACxB,uEAAuE;IACvE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,8CAA8C;IAC9C,KAAK,EAAE,eAAe,CAAC;IACvB,4EAA4E;IAC5E,IAAI,CAAC,IAAI,IAAI,CAAC;IACd,uBAAuB;IACvB,MAAM,EAAE,QAAQ,CAAC;IACjB,8DAA8D;IAC9D,OAAO,CAAC,IAAI,IAAI,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,gDAAgD;IAChD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;;;;OAUG;IACH,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B;;;;;;;;;;;;OAYG;IACH,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACpD;;;;;;;;;;OAUG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,cAAc,GAAG,SAAS,CAAC"}
@@ -1,20 +1,12 @@
1
1
  /**
2
2
  * Single source of truth for default values the engine's factories read.
3
3
  *
4
- * WO-8 deleted SEVEN groups `transform`, `entity`, `audio`, `shadow`,
5
- * `animation`, `fog`, `environment` because every one of their readers was a
6
- * `.vscn.json` consumer (`entity-factory.ts`, `scene-loader.ts`,
7
- * `scene-sync.ts`, `scene-serializer.ts`, the per-section inspectors) and
8
- * `defaults-source-scan.test.ts` reported them with ZERO `DEFAULTS.<group>`
9
- * readers anywhere in engine/editor src or tests. That guard offers two remedies
10
- * — wire the group to the factory that reads it, or delete it if truly unused —
11
- * and there is no surviving factory to wire these to: `transform` restated
12
- * THREE's own Object3D constructor values, `entity.locked`/ `pivot` were
13
- * descriptor-only fields, and `fog`/`environment.envMapIntensity`/ `shadow` were
14
- * the `.vscn` `environment` block that `render-env.ts` explicitly did NOT carry
15
- * forward. Keeping them would have been exactly the dead documentation T1.13
16
- * created this guard to prevent.
17
- *
4
+ * A group belongs here only while a FACTORY reads it. A default with no
5
+ * reader is dead documentation: it looks authoritative, nothing consults it,
6
+ * and it drifts from the value the code actually uses. Restating a library's
7
+ * own constructor values (THREE's `Object3D` transform, say) is the same
8
+ * mistake in a different shape — the library is already the source of truth.
9
+ * So when a factory goes, its group goes with it.
18
10
  */
19
11
  export declare const DEFAULTS: {
20
12
  readonly material: {
@@ -1 +1 @@
1
- {"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../src/defaults.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gDAkB0B,CAAC,MAAM,EAAE,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;kCA6BjB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6E5D,CAAC"}
1
+ {"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../src/defaults.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gDAkB0B,CAAC,MAAM,EAAE,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;kCA6BjB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6E5D,CAAC"}