@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
@@ -15,27 +15,20 @@
15
15
  * or the Pixi reconciler.
16
16
  *
17
17
  * Surface:
18
- * - `createPixiReactAdapter({ id, content })` — mount a `@pixi/react` tree as
19
- * a first-party `surface: "canvas"` root under the host's gated loop.
20
18
  * - `pixiReactRootFactory` / `resolveCanvasEntryAdapter` — what a canvas entry
21
- * module MEANS: a default-exported component (or an explicit `adapter`).
19
+ * module MEANS: a default-exported component, mounted as a first-party
20
+ * `surface: "canvas"` root under the host's gated loop. The world's vgai
21
+ * surface is its entry module's static exports
22
+ * (`export { debug, systems } from './commands'`), connected by the host at
23
+ * mount.
22
24
  * - `PixiPrimitive` / `adoptNow` — the canvas surface's `<primitive object={…}>`:
23
25
  * render a container the game already owns, and place a spawn in the
24
26
  * display tree ahead of the commit that renders it.
25
- * - `useGameContext()` — this root's `CanvasGameContext` (`input`, `debug`,
26
- * `random`, `clock`, `game`, `roots`) from inside any component in the tree.
27
- * Behavior in a canvas world is components + hooks, never a class under a
28
- * string key.
29
- * - `EngineBridge` / `EngineBridgeValue` — the context contract, exported for
30
- * adopters building their own bridge.
31
27
  *
32
28
  * The Pixi runtime itself is NOT re-exported here. A component reaches the live
33
29
  * `Application` (and therefore `stage`, `renderer`, `ticker`) with
34
30
  * `@pixi/react`'s own `useApplication()`, ticks with its `useTick()`, and loads
35
31
  * assets with Pixi's own `Assets` — the library's API, not a second one.
36
32
  */
37
- export { EngineBridge, useGameContext } from './engine-bridge';
38
33
  export { adoptNow, PixiPrimitive } from './pixi-primitive';
39
- export { createPixiReactAdapter, } from './pixi-react-adapter';
40
34
  export { pixiReactRootFactory, resolveCanvasEntryAdapter } from './pixi-react-root-factory';
41
- export { createCanvasRootContext, DEFAULT_INPUT_MAP_PATH, } from './world-context';
@@ -1,8 +1,6 @@
1
1
  /**
2
2
  * `pixiReactRootFactory` — mount a `canvas` root whose entry module
3
- * DEFAULT-EXPORTS a React component.
4
- *
5
- * A model asked for a 2D world writes:
3
+ * DEFAULT-EXPORTS a React component. This is the ONE first-party canvas mount:
6
4
  *
7
5
  * ```tsx
8
6
  * export default function World() {
@@ -10,11 +8,13 @@
10
8
  * }
11
9
  * ```
12
10
  *
13
- * It does NOT write `export const adapter = createPixiReactAdapter({ id, content })`.
14
- * That export is vgai ceremony wrapped around a component the author already
15
- * wrote. The `three` and `dom` roots both settled this convention read
16
- * `entryModule.default` and wrap it and this is the same contract for canvas
17
- * roots, so all three surfaces answer "what do I export?" the same way.
11
+ * The world is an ordinary `@pixi/react` app. Everything vgai-shaped stays at
12
+ * the entry module's STATIC surface (`export { debug, systems } from
13
+ * './commands'`) and on the host's side of the seam; no vgai runtime context
14
+ * ever enters the React tree. The host advances Pixi's real (never-started)
15
+ * ticker on GAME time, wires the game-scoped input seams from outside
16
+ * (`../runtime/game-input-seams.ts`), and installs `WorldProvider` off the
17
+ * Game handle so the lib-legal react doors resolve.
18
18
  *
19
19
  * ## Why this is NOT in `mount-game.ts`
20
20
  *
@@ -24,12 +24,28 @@
24
24
  * the opt-in module that owns those dependencies, so the factory lives here and
25
25
  * a project opts in with `registerAdapter('canvas', pixiReactRootFactory)`.
26
26
  *
27
- * ## Compatibility
27
+ * ## The loop contract
28
+ *
29
+ * `Application` is initialized with `autoStart: false` and `sharedTicker:
30
+ * false`, so it owns a native Pixi ticker that is NEVER started and therefore
31
+ * never requests an animation frame. `mounted.update(dt)` is the only thing
32
+ * that advances that ticker, and it feeds GAME time, not wall time — which is
33
+ * what makes the world freeze exactly when the host freezes it, and what makes
34
+ * `game.waitSimTime` work in a hidden tab (a hidden tab has no rAF; the engine
35
+ * loop's own catch-up still calls `update`, and Pixi has no independent clock
36
+ * to disagree with it).
28
37
  *
29
- * An `adapter` export still wins, so a game needing full control over its own
30
- * `RootAdapter<'canvas'>` keeps it.
38
+ * ## Component catalogue
39
+ *
40
+ * `@pixi/react` resolves `<pixiSprite>` etc. through a module-global catalogue
41
+ * that starts EMPTY — the first intrinsic in an un-extended tree throws
42
+ * "X is not part of the PIXI namespace! Did you forget to extend?". This
43
+ * module extends it with the whole `pixi.js` namespace once per mount
44
+ * (idempotent `Object.assign`). A world that adds its own classes
45
+ * (`@pixi/tilemap`, a custom `Container` subclass) calls `extend`/`useExtend`
46
+ * itself.
31
47
  */
32
- import type { RootAdapter } from '../adapter/root-adapter';
48
+ import type { RootAdapter } from '../adapter';
33
49
  import type { AdapterSurfaceFactory } from '../runtime/mount-game';
34
50
  /**
35
51
  * What a canvas entry module MEANS, in one place.
@@ -37,20 +53,17 @@ import type { AdapterSurfaceFactory } from '../runtime/mount-game';
37
53
  * Two callers need this answer and must never disagree about it: the runtime
38
54
  * mount ({@link pixiReactRootFactory}, below) and the EDITOR, which resolves
39
55
  * the same entry for play mode and for the design-time layer. When only one
40
- * side knows about the default-export shape, a world plays fine and shows
41
- * "(no scene loaded)" in edit mode — the exact split the three lane already
42
- * paid for.
56
+ * side knows the shape, a world plays fine and shows "(no scene loaded)" in
57
+ * edit mode.
43
58
  *
44
- * Returns `null` when the module is neither shape — the callers differ on what
45
- * to do about that (the factory throws; the editor reports a mount failure on
46
- * that root's boundary node and leaves every sibling working).
59
+ * Returns `null` when the module has no default-exported component — the
60
+ * callers differ on what to do about that (the factory throws; the editor
61
+ * reports a mount failure on that root's boundary node and leaves every
62
+ * sibling working).
47
63
  */
48
64
  export declare function resolveCanvasEntryAdapter(entryModule: unknown, rootId: string): RootAdapter<'canvas'> | null;
49
65
  /**
50
66
  * Register with `registerAdapter('canvas', pixiReactRootFactory)`.
51
- *
52
- * Resolution order mirrors the three lane's (explicit adapter wins) and then
53
- * accepts the default-exported component.
54
67
  */
55
68
  export declare const pixiReactRootFactory: AdapterSurfaceFactory;
56
69
  //# sourceMappingURL=pixi-react-root-factory.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"pixi-react-root-factory.d.ts","sourceRoot":"","sources":["../../src/canvas-react/pixi-react-root-factory.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAUnE;;;;;;;;;;;;;GAaG;AACH,wBAAgB,yBAAyB,CACvC,WAAW,EAAE,OAAO,EACpB,MAAM,EAAE,MAAM,GACb,WAAW,CAAC,QAAQ,CAAC,GAAG,IAAI,CAc9B;AAED;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,EAAE,qBAQlC,CAAC"}
1
+ {"version":3,"file":"pixi-react-root-factory.d.ts","sourceRoot":"","sources":["../../src/canvas-react/pixi-react-root-factory.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAMH,OAAO,KAAK,EAAsC,WAAW,EAAE,MAAM,YAAY,CAAC;AAKlF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAuQnE;;;;;;;;;;;;;GAaG;AACH,wBAAgB,yBAAyB,CACvC,WAAW,EAAE,OAAO,EACpB,MAAM,EAAE,MAAM,GACb,WAAW,CAAC,QAAQ,CAAC,GAAG,IAAI,CAI9B;AAED;;GAEG;AACH,eAAO,MAAM,oBAAoB,EAAE,qBAOlC,CAAC"}
@@ -1,8 +1,6 @@
1
1
  /**
2
2
  * `pixiReactRootFactory` — mount a `canvas` root whose entry module
3
- * DEFAULT-EXPORTS a React component.
4
- *
5
- * A model asked for a 2D world writes:
3
+ * DEFAULT-EXPORTS a React component. This is the ONE first-party canvas mount:
6
4
  *
7
5
  * ```tsx
8
6
  * export default function World() {
@@ -10,11 +8,13 @@
10
8
  * }
11
9
  * ```
12
10
  *
13
- * It does NOT write `export const adapter = createPixiReactAdapter({ id, content })`.
14
- * That export is vgai ceremony wrapped around a component the author already
15
- * wrote. The `three` and `dom` roots both settled this convention read
16
- * `entryModule.default` and wrap it and this is the same contract for canvas
17
- * roots, so all three surfaces answer "what do I export?" the same way.
11
+ * The world is an ordinary `@pixi/react` app. Everything vgai-shaped stays at
12
+ * the entry module's STATIC surface (`export { debug, systems } from
13
+ * './commands'`) and on the host's side of the seam; no vgai runtime context
14
+ * ever enters the React tree. The host advances Pixi's real (never-started)
15
+ * ticker on GAME time, wires the game-scoped input seams from outside
16
+ * (`../runtime/game-input-seams.ts`), and installs `WorldProvider` off the
17
+ * Game handle so the lib-legal react doors resolve.
18
18
  *
19
19
  * ## Why this is NOT in `mount-game.ts`
20
20
  *
@@ -24,54 +24,292 @@
24
24
  * the opt-in module that owns those dependencies, so the factory lives here and
25
25
  * a project opts in with `registerAdapter('canvas', pixiReactRootFactory)`.
26
26
  *
27
- * ## Compatibility
27
+ * ## The loop contract
28
+ *
29
+ * `Application` is initialized with `autoStart: false` and `sharedTicker:
30
+ * false`, so it owns a native Pixi ticker that is NEVER started and therefore
31
+ * never requests an animation frame. `mounted.update(dt)` is the only thing
32
+ * that advances that ticker, and it feeds GAME time, not wall time — which is
33
+ * what makes the world freeze exactly when the host freezes it, and what makes
34
+ * `game.waitSimTime` work in a hidden tab (a hidden tab has no rAF; the engine
35
+ * loop's own catch-up still calls `update`, and Pixi has no independent clock
36
+ * to disagree with it).
37
+ *
38
+ * ## Component catalogue
28
39
  *
29
- * An `adapter` export still wins, so a game needing full control over its own
30
- * `RootAdapter<'canvas'>` keeps it.
40
+ * `@pixi/react` resolves `<pixiSprite>` etc. through a module-global catalogue
41
+ * that starts EMPTY — the first intrinsic in an un-extended tree throws
42
+ * "X is not part of the PIXI namespace! Did you forget to extend?". This
43
+ * module extends it with the whole `pixi.js` namespace once per mount
44
+ * (idempotent `Object.assign`). A world that adds its own classes
45
+ * (`@pixi/tilemap`, a custom `Container` subclass) calls `extend`/`useExtend`
46
+ * itself.
47
+ */
48
+ import { createRoot, extend } from '@pixi/react';
49
+ import * as PIXI from 'pixi.js';
50
+ import { createElement, Fragment, useEffect, useLayoutEffect } from 'react';
51
+ import { WorldProvider } from '../react/world-state';
52
+ import { getDebugRegistry } from '../runtime/debug-registry';
53
+ import { DEFAULT_INPUT_MAP_PATH, wireGameInputSeams } from '../runtime/game-input-seams';
54
+ /** How long `mount()` waits for the tree's first commit before failing loudly
55
+ * rather than hanging (and wedging every root declared after this one, since
56
+ * roots mount sequentially). */
57
+ const FIRST_COMMIT_TIMEOUT_MS = 10_000;
58
+ /**
59
+ * `@pixi/react` keys its reconciler roots off the canvas element in a
60
+ * module-global map, and its own unmount path (which would clear that key) is
61
+ * not exported. So a canvas this lane has already mounted must never be handed
62
+ * to a second mount: `createRoot` would silently hand back the FIRST root,
63
+ * whose `Application` this lane already destroyed. Every host in this repo
64
+ * builds a fresh canvas per mount, so this only ever fires on a genuine bug —
65
+ * and it fires as a named error rather than as a dead world.
31
66
  */
32
- import { createElement } from 'react';
33
- import { createPixiReactAdapter } from './pixi-react-adapter';
67
+ const MOUNTED_CANVASES = new WeakSet();
68
+ /**
69
+ * Build the `RootAdapter<'canvas'>` for one default-exported Pixi world.
70
+ * Mounts through `@pixi/react`, gated entirely by the host's own loop and
71
+ * rendering into the host's own canvas — never a second canvas, never a
72
+ * second `requestAnimationFrame` loop.
73
+ */
74
+ function canvasWorldAdapter(id, component) {
75
+ const content = createElement(component);
76
+ return {
77
+ id,
78
+ async mount(host) {
79
+ // THE HOST WIRES THIS GAME'S INPUT FROM OUTSIDE THE TREE — the project's
80
+ // declared map (conventional path, optional: a brand-new project
81
+ // declares no actions yet) and this root's `game.input.*` seams on the
82
+ // debug registry. Actions must exist before any component's first tick
83
+ // reads them — wait for the (never-rejecting) load before the first
84
+ // commit.
85
+ const registry = host.game ? getDebugRegistry(host.game) : null;
86
+ if (registry) {
87
+ await wireGameInputSeams(host, registry, {
88
+ id,
89
+ inputMapPath: DEFAULT_INPUT_MAP_PATH,
90
+ optionalInputMap: true,
91
+ });
92
+ }
93
+ const canvas = host.canvas;
94
+ if (MOUNTED_CANVASES.has(canvas)) {
95
+ throw new Error(`canvas world "${id}": this lane has already mounted this canvas. @pixi/react keys ` +
96
+ 'its reconciler roots off the canvas element, so re-using one would resurrect a ' +
97
+ 'root whose Application is destroyed. Hand each mount a fresh canvas (every vgai ' +
98
+ 'host already does).');
99
+ }
100
+ MOUNTED_CANVASES.add(canvas);
101
+ // The catalogue is module-global and starts empty; this is idempotent.
102
+ extend(PIXI);
103
+ let resolveCommitted;
104
+ const committed = new Promise((resolve) => {
105
+ resolveCommitted = resolve;
106
+ });
107
+ let resolveEffectsReady;
108
+ const effectsReady = new Promise((resolve) => {
109
+ resolveEffectsReady = resolve;
110
+ });
111
+ let resolveUnmounted;
112
+ const unmounted = new Promise((resolve) => {
113
+ resolveUnmounted = resolve;
114
+ });
115
+ /**
116
+ * Signals the tree's FIRST COMMIT and its LAST. A layout effect,
117
+ * deliberately, and rendered as the LAST child, deliberately:
118
+ *
119
+ * - mount: layout effects run synchronously inside the commit, before
120
+ * any passive effect and before any tick can be scheduled;
121
+ * - unmount: React commits deletion effects in tree order, each node
122
+ * before its own children, so the LAST sibling's cleanup is the last
123
+ * to run. That is what makes this a reliable "every component has
124
+ * finished tearing down" signal — and it is load-bearing, because
125
+ * `useTick`'s own cleanup calls `ticker.remove()`. Destroying the
126
+ * Application before that cleanup lands throws inside React's commit
127
+ * on every teardown of any world with a tick hook.
128
+ */
129
+ function Lifecycle() {
130
+ useLayoutEffect(() => {
131
+ resolveCommitted();
132
+ return resolveUnmounted;
133
+ }, []);
134
+ // This component is the last sibling. Reaching its passive effect
135
+ // means the initial tree's provider/command effects have registered,
136
+ // so mount() can truthfully back Play's ack.
137
+ useEffect(resolveEffectsReady, []);
138
+ return null;
139
+ }
140
+ // `WorldProvider` rides `host.game` — the React seam every dom root
141
+ // already has, and the only way a capability hook can reach the ONE
142
+ // game-scoped registry the editor/`vgai eval` read. `host.game` is
143
+ // genuinely absent in bare/foreign hosts, so the provider is
144
+ // conditional and those hooks stay the inert no-ops they already
145
+ // document themselves to be.
146
+ const game = host.game;
147
+ const world = createElement(Fragment, null, content, createElement(Lifecycle));
148
+ const element = game ? createElement(WorldProvider, { game }, world) : world;
149
+ const root = createRoot(canvas);
150
+ // The host owns size, dpr, transparency and capture cost; the loop
151
+ // contract owns `autoStart`/`sharedTicker`.
152
+ const initOptions = {
153
+ antialias: true,
154
+ width: host.width,
155
+ height: host.height,
156
+ resolution: host.dpr ?? globalThis.devicePixelRatio ?? 1,
157
+ autoDensity: true,
158
+ // Stacked ABOVE a sibling root -> clear with alpha 0 so the layer below
159
+ // shows through (COMPOSITION-DESIGN D5 §1).
160
+ backgroundAlpha: host.transparent ? 0 : 1,
161
+ preserveDrawingBuffer: host.preserveDrawingBuffer ?? false,
162
+ autoStart: false,
163
+ sharedTicker: false,
164
+ };
165
+ // Install the reconcile-error listener BEFORE `root.render`: Pixi hands
166
+ // the Application back after `init()` but React can already have thrown
167
+ // during that same turn. Attaching after the await loses the real error
168
+ // and turns it into a misleading ten-second timeout.
169
+ let reportReconcileFailure;
170
+ const reconcileFailure = new Promise((resolve) => {
171
+ reportReconcileFailure = resolve;
172
+ });
173
+ const onError = (event) => {
174
+ reportReconcileFailure(`the Pixi tree crashed before its first commit — ${event.message} ` +
175
+ '(mount() fails loudly instead of hanging)');
176
+ };
177
+ globalThis.addEventListener?.('error', onError);
178
+ let app;
179
+ let timer;
180
+ try {
181
+ // `root.render` is async: it awaits `Application.init()` (which is what
182
+ // creates the renderer) and only then hands the tree to the reconciler.
183
+ // A crash inside init rejects here, which is the loud failure we want.
184
+ const renderedApp = await root.render(element, initOptions);
185
+ app = renderedApp;
186
+ // A reconcile-time crash (e.g. an un-extended component) surfaces as
187
+ // an uncaught window error and the first commit never lands. Race that
188
+ // exact signal against the commit and the last-resort ceiling.
189
+ const timeout = new Promise((resolve) => {
190
+ timer = setTimeout(() => resolve(`no first commit within ${FIRST_COMMIT_TIMEOUT_MS / 1000}s ` +
191
+ '(mount() fails loudly instead of hanging)'), FIRST_COMMIT_TIMEOUT_MS);
192
+ });
193
+ const failure = await Promise.race([
194
+ Promise.all([committed, effectsReady]).then(() => null),
195
+ reconcileFailure,
196
+ timeout,
197
+ ]);
198
+ if (failure !== null) {
199
+ throw new Error(`canvas world "${id}": ${failure}`);
200
+ }
201
+ }
202
+ catch (error) {
203
+ try {
204
+ app?.destroy(false, { children: true });
205
+ }
206
+ catch {
207
+ /* the world never fully came up; teardown failure must not mask why */
208
+ }
209
+ throw error;
210
+ }
211
+ finally {
212
+ if (timer !== undefined)
213
+ clearTimeout(timer);
214
+ globalThis.removeEventListener?.('error', onError);
215
+ }
216
+ // `app` is assigned before the first-commit race. Reaching here proves
217
+ // both initialization and commit succeeded, so narrow once for the live
218
+ // mounted-root methods below.
219
+ if (app === undefined) {
220
+ throw new Error(`canvas world "${id}": initialized without an Application`);
221
+ }
222
+ const mountedApp = app;
223
+ // Pin the ticker's clock to GAME time. `Ticker.lastTime` starts at -1, so
224
+ // without this the first `update()` would report a 1ms phantom delta on
225
+ // top of the real one. From here every `deltaMS` a `useTick` callback
226
+ // sees is exactly the `dt` the host handed us — no wall clock anywhere,
227
+ // which is what makes a paused world's resume frame ordinary rather than
228
+ // one giant catch-up step.
229
+ mountedApp.ticker.lastTime = 0;
230
+ let elapsedMs = 0;
231
+ let disposed = false;
232
+ // Adapter surface: the host's shared debug registry. The game's own
233
+ // capabilities arrive as the entry module's declared `systems`.
234
+ const systemAdapters = registry ? { debug: registry.adapter } : {};
235
+ const disposeComplete = unmounted.then(() => {
236
+ mountedApp.destroy(false, { children: true });
237
+ });
238
+ return {
239
+ kind: 'canvas',
240
+ // The reconciler's real root container — the same `PIXI.Container`
241
+ // `RootInstance.pixiStage()` hands the editor.
242
+ stage: mountedApp.stage,
243
+ drivesOwnLoop: false,
244
+ systems: systemAdapters,
245
+ disposeComplete,
246
+ update(dt) {
247
+ if (disposed)
248
+ return;
249
+ elapsedMs += dt * 1000;
250
+ // The real Pixi ticker remains the one scheduler `useTick` observes;
251
+ // the host only gates/advances it.
252
+ mountedApp.ticker.update(elapsedMs);
253
+ },
254
+ resize(width, height) {
255
+ if (disposed)
256
+ return;
257
+ // `autoDensity: true` makes Pixi re-stamp `canvas.style.width`/
258
+ // `.height` from this call, so the on-screen box follows the logical
259
+ // size with no explicit CSS write from the host.
260
+ mountedApp.renderer.resize(width, height);
261
+ },
262
+ dispose() {
263
+ if (disposed)
264
+ return;
265
+ disposed = true;
266
+ // Pixi's EventSystem owns document.pointermove + window.pointerup
267
+ // (and its event ticker's global listener). React's deletion commit
268
+ // is asynchronous, so waiting to destroy the Application leaves
269
+ // those listeners live when the host audits this game realm. Detach
270
+ // the input target synchronously; the later Application.destroy()
271
+ // repeats this idempotently after every useTick cleanup has landed.
272
+ mountedApp.renderer.events.setTargetElement(null);
273
+ // Rendering `null` children is the only unmount path `@pixi/react`
274
+ // exposes publicly (its `unmountRoot` helper is internal), and React
275
+ // commits that deletion on its own schedule — NOT synchronously. So
276
+ // the Application is destroyed on the `Lifecycle` cleanup signal
277
+ // instead of inline: `useTick`'s cleanup calls `ticker.remove()`, and
278
+ // a ticker destroyed before it lands throws inside React's commit.
279
+ // `removeView` stays false — the canvas belongs to the HOST.
280
+ void root.render(null, initOptions);
281
+ },
282
+ };
283
+ },
284
+ };
285
+ }
34
286
  /**
35
287
  * What a canvas entry module MEANS, in one place.
36
288
  *
37
289
  * Two callers need this answer and must never disagree about it: the runtime
38
290
  * mount ({@link pixiReactRootFactory}, below) and the EDITOR, which resolves
39
291
  * the same entry for play mode and for the design-time layer. When only one
40
- * side knows about the default-export shape, a world plays fine and shows
41
- * "(no scene loaded)" in edit mode — the exact split the three lane already
42
- * paid for.
292
+ * side knows the shape, a world plays fine and shows "(no scene loaded)" in
293
+ * edit mode.
43
294
  *
44
- * Returns `null` when the module is neither shape — the callers differ on what
45
- * to do about that (the factory throws; the editor reports a mount failure on
46
- * that root's boundary node and leaves every sibling working).
295
+ * Returns `null` when the module has no default-exported component — the
296
+ * callers differ on what to do about that (the factory throws; the editor
297
+ * reports a mount failure on that root's boundary node and leaves every
298
+ * sibling working).
47
299
  */
48
300
  export function resolveCanvasEntryAdapter(entryModule, rootId) {
49
301
  const mod = entryModule;
50
- if (mod?.adapter)
51
- return mod.adapter;
52
- if (typeof mod?.default === 'function') {
53
- return createPixiReactAdapter({
54
- id: rootId,
55
- content: createElement(mod.default),
56
- // The supported Canvas portfolio still consumes the compatibility
57
- // context. Removing it belongs to the tracked component-root migration;
58
- // disabling it here first made every current default-exported game fail
59
- // to mount before that migration had happened.
60
- });
61
- }
302
+ if (typeof mod?.default === 'function')
303
+ return canvasWorldAdapter(rootId, mod.default);
62
304
  return null;
63
305
  }
64
306
  /**
65
307
  * Register with `registerAdapter('canvas', pixiReactRootFactory)`.
66
- *
67
- * Resolution order mirrors the three lane's (explicit adapter wins) and then
68
- * accepts the default-exported component.
69
308
  */
70
309
  export const pixiReactRootFactory = (root, ctx) => {
71
310
  const adapter = resolveCanvasEntryAdapter(ctx.entryModule, root.id);
72
311
  if (adapter)
73
312
  return { kind: 'canvas', adapter };
74
313
  throw new Error(`pixiReactRootFactory: entry module "${root.entry ?? '(none)'}" for root "${root.id}" must ` +
75
- 'default-export a React component (`export default function World() { … }`) — or export ' +
76
- 'an `adapter` for full control.');
314
+ 'default-export a React component (`export default function World() { … }`).');
77
315
  };
@@ -0,0 +1,118 @@
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
+ /** A restartable countdown. Build one with {@link createCountdown}. */
69
+ export interface Countdown {
70
+ /** Seconds it counts from. Reflects the last {@link Countdown.start} that
71
+ * passed a duration. */
72
+ readonly duration: number;
73
+ /** Does it re-arm itself after firing, or stop? */
74
+ readonly repeats: boolean;
75
+ /** Seconds left, or 0 when it is not running. */
76
+ readonly remaining: number;
77
+ /** Is it counting? */
78
+ readonly running: boolean;
79
+ /**
80
+ * Start, or RESTART a running countdown from the top. Passing `duration`
81
+ * overwrites {@link Countdown.duration} for this and every later start.
82
+ *
83
+ * @throws RangeError if `duration` is not a finite positive number of
84
+ * seconds. A non-positive period cannot fire and reads as a hung game
85
+ * rather than a misconfigured one, so it refuses at the call site.
86
+ */
87
+ start(duration?: number): void;
88
+ /** Stop counting. Invokes nothing. */
89
+ stop(): void;
90
+ /**
91
+ * Advance by `dt` SECONDS, invoking `onElapsed` once per completed period —
92
+ * possibly several times in one call. See the module header's frame-hitch
93
+ * contract.
94
+ */
95
+ advance(dt: number): void;
96
+ }
97
+ /** What {@link createCountdown} needs. */
98
+ export interface CountdownOptions {
99
+ /** Seconds to count from. Default 1. */
100
+ readonly duration?: number;
101
+ /** Re-arm after firing instead of stopping. Default `false` (one-shot). */
102
+ readonly repeats?: boolean;
103
+ /** Begin counting immediately instead of waiting for a `start()`. Default
104
+ * `false`. */
105
+ readonly autostart?: boolean;
106
+ /** Invoked inline, once per completed period. */
107
+ readonly onElapsed: () => void;
108
+ }
109
+ /**
110
+ * Build a countdown.
111
+ *
112
+ * ```ts
113
+ * const wave = createCountdown({ duration: 8, repeats: true, autostart: true, onElapsed: spawnWave });
114
+ * useFrame((_, dt) => wave.advance(dt)); // the caller owns the step
115
+ * ```
116
+ */
117
+ export declare function createCountdown(options: CountdownOptions): Countdown;
118
+ //# sourceMappingURL=countdown-timer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"countdown-timer.d.ts","sourceRoot":"","sources":["../../src/core/countdown-timer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkEG;AAEH,uEAAuE;AACvE,MAAM,WAAW,SAAS;IACxB;6BACyB;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,mDAAmD;IACnD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,iDAAiD;IACjD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,sBAAsB;IACtB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B;;;;;;;OAOG;IACH,KAAK,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,sCAAsC;IACtC,IAAI,IAAI,IAAI,CAAC;IACb;;;;OAIG;IACH,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,0CAA0C;AAC1C,MAAM,WAAW,gBAAgB;IAC/B,wCAAwC;IACxC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,2EAA2E;IAC3E,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B;mBACe;IACf,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B,iDAAiD;IACjD,QAAQ,CAAC,SAAS,EAAE,MAAM,IAAI,CAAC;CAChC;AAUD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,SAAS,CA4DpE"}