@the-inclusionist/engine 9.0.0 → 11.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (532) hide show
  1. package/README.md +120 -78
  2. package/app/css/style.css +761 -377
  3. package/app/public/vendor/fonts/playwrite-cu.woff2 +0 -0
  4. package/app/public/vendor/fonts/playwrite-es-deco.woff2 +0 -0
  5. package/app/public/vendor/fonts/playwrite-es.woff2 +0 -0
  6. package/app/public/vendor/fonts/playwrite-gb-j.woff2 +0 -0
  7. package/app/public/vendor/fonts/playwrite-gb-s.woff2 +0 -0
  8. package/app/public/vendor/fonts/playwrite-pe.woff2 +0 -0
  9. package/app/public/vendor/fonts/playwrite-pt.woff2 +0 -0
  10. package/app/public/vendor/fonts-licences/NOTICE.txt +166 -0
  11. package/app/public/vendor/fonts-licences/OFL-1.1.txt +86 -0
  12. package/app/public/vendor/fonts.css +36 -84
  13. package/dist-pkg/boot/create-game.d.ts +499 -219
  14. package/dist-pkg/boot/create-game.js +3944 -562
  15. package/dist-pkg/core/a11y-sr.d.ts +25 -3
  16. package/dist-pkg/core/a11y-sr.js +38 -19
  17. package/dist-pkg/core/accessible-label.d.ts +17 -0
  18. package/dist-pkg/core/accessible-label.js +37 -0
  19. package/dist-pkg/core/accommodation-subjects.d.ts +26 -0
  20. package/dist-pkg/core/accommodation-subjects.js +32 -0
  21. package/dist-pkg/core/accommodations.d.ts +125 -0
  22. package/dist-pkg/core/accommodations.js +195 -0
  23. package/dist-pkg/core/actions.d.ts +108 -55
  24. package/dist-pkg/core/actions.js +122 -71
  25. package/dist-pkg/core/calm-mode.d.ts +31 -0
  26. package/dist-pkg/core/calm-mode.js +47 -0
  27. package/dist-pkg/core/camera-cycle.d.ts +15 -0
  28. package/dist-pkg/core/camera-cycle.js +7 -0
  29. package/dist-pkg/core/caption-duration.d.ts +15 -0
  30. package/dist-pkg/core/caption-duration.js +19 -0
  31. package/dist-pkg/core/cartridge-problems.d.ts +34 -0
  32. package/dist-pkg/core/cartridge-problems.js +53 -0
  33. package/dist-pkg/core/constants.d.ts +0 -54
  34. package/dist-pkg/core/constants.js +9 -76
  35. package/dist-pkg/core/contract.d.ts +142 -164
  36. package/dist-pkg/core/contract.js +222 -208
  37. package/dist-pkg/core/dom-query.d.ts +4 -4
  38. package/dist-pkg/core/dom-query.js +12 -20
  39. package/dist-pkg/core/entity.d.ts +62 -77
  40. package/dist-pkg/core/entity.js +18 -32
  41. package/dist-pkg/core/escape-html.d.ts +9 -10
  42. package/dist-pkg/core/escape-html.js +16 -20
  43. package/dist-pkg/core/flash-threshold.d.ts +17 -0
  44. package/dist-pkg/core/flash-threshold.js +113 -0
  45. package/dist-pkg/core/game-speed.d.ts +11 -0
  46. package/dist-pkg/core/game-speed.js +16 -0
  47. package/dist-pkg/core/genres.d.ts +20 -0
  48. package/dist-pkg/core/genres.js +44 -0
  49. package/dist-pkg/core/i18n.d.ts +71 -60
  50. package/dist-pkg/core/i18n.js +186 -200
  51. package/dist-pkg/core/loop.d.ts +17 -6
  52. package/dist-pkg/core/loop.js +26 -26
  53. package/dist-pkg/core/pause-icon-catalogue.d.ts +15 -0
  54. package/dist-pkg/core/pause-icon-catalogue.js +54 -0
  55. package/dist-pkg/core/ring.d.ts +17 -0
  56. package/dist-pkg/core/ring.js +29 -0
  57. package/dist-pkg/core/rng.d.ts +7 -12
  58. package/dist-pkg/core/rng.js +23 -53
  59. package/dist-pkg/core/route.d.ts +26 -26
  60. package/dist-pkg/core/route.js +126 -121
  61. package/dist-pkg/core/scenes.d.ts +37 -40
  62. package/dist-pkg/core/scenes.js +41 -43
  63. package/dist-pkg/core/screens.d.ts +4 -4
  64. package/dist-pkg/core/screens.js +11 -16
  65. package/dist-pkg/core/session-clock.d.ts +23 -0
  66. package/dist-pkg/core/session-clock.js +37 -0
  67. package/dist-pkg/core/setting-defaults.d.ts +61 -0
  68. package/dist-pkg/core/setting-defaults.js +66 -0
  69. package/dist-pkg/core/speech-rate.d.ts +29 -0
  70. package/dist-pkg/core/speech-rate.js +47 -0
  71. package/dist-pkg/core/state.d.ts +118 -118
  72. package/dist-pkg/core/state.js +322 -297
  73. package/dist-pkg/core/visual-cycles.d.ts +30 -0
  74. package/dist-pkg/core/visual-cycles.js +52 -0
  75. package/dist-pkg/core/visual-state.d.ts +13 -0
  76. package/dist-pkg/core/visual-state.js +6 -0
  77. package/dist-pkg/educational/activities-registry.d.ts +1 -1
  78. package/dist-pkg/educational/activities-registry.js +9 -8
  79. package/dist-pkg/educational/adaptive-engine.d.ts +11 -11
  80. package/dist-pkg/educational/adaptive-engine.js +17 -17
  81. package/dist-pkg/educational/segment-bar.d.ts +12 -12
  82. package/dist-pkg/educational/segment-bar.js +9 -9
  83. package/dist-pkg/i18n/en.js +324 -223
  84. package/dist-pkg/i18n/es.js +323 -223
  85. package/dist-pkg/i18n/pt.js +348 -226
  86. package/dist-pkg/input/default-bindings.d.ts +37 -38
  87. package/dist-pkg/input/default-bindings.js +127 -144
  88. package/dist-pkg/input/devices.d.ts +7 -7
  89. package/dist-pkg/input/devices.js +17 -16
  90. package/dist-pkg/input/edges.d.ts +23 -32
  91. package/dist-pkg/input/edges.js +23 -28
  92. package/dist-pkg/input/empathy-filter.d.ts +22 -0
  93. package/dist-pkg/input/empathy-filter.js +34 -0
  94. package/dist-pkg/input/face-map.d.ts +96 -0
  95. package/dist-pkg/input/face-map.js +255 -0
  96. package/dist-pkg/input/face-signals.d.ts +40 -0
  97. package/dist-pkg/input/face-signals.js +48 -0
  98. package/dist-pkg/input/gamepad.d.ts +195 -145
  99. package/dist-pkg/input/gamepad.js +383 -556
  100. package/dist-pkg/input/gaze-cycle.d.ts +64 -0
  101. package/dist-pkg/input/gaze-cycle.js +133 -0
  102. package/dist-pkg/input/gaze-relative.d.ts +68 -0
  103. package/dist-pkg/input/gaze-relative.js +142 -0
  104. package/dist-pkg/input/hand-map.d.ts +28 -0
  105. package/dist-pkg/input/hand-map.js +142 -0
  106. package/dist-pkg/input/input-cooldown.d.ts +17 -0
  107. package/dist-pkg/input/input-cooldown.js +39 -0
  108. package/dist-pkg/input/key-default.d.ts +30 -0
  109. package/dist-pkg/input/key-default.js +60 -0
  110. package/dist-pkg/input/keyboard-runtime.d.ts +15 -21
  111. package/dist-pkg/input/keyboard-runtime.js +23 -27
  112. package/dist-pkg/input/keyboard.d.ts +68 -63
  113. package/dist-pkg/input/keyboard.js +83 -105
  114. package/dist-pkg/input/keydown.d.ts +123 -137
  115. package/dist-pkg/input/keydown.js +284 -289
  116. package/dist-pkg/input/latch-edge.d.ts +33 -21
  117. package/dist-pkg/input/latch-edge.js +34 -35
  118. package/dist-pkg/input/latch-scope.d.ts +51 -50
  119. package/dist-pkg/input/latch-scope.js +61 -82
  120. package/dist-pkg/input/latch-store.d.ts +26 -30
  121. package/dist-pkg/input/latch-store.js +42 -50
  122. package/dist-pkg/input/latch-sync.d.ts +29 -29
  123. package/dist-pkg/input/latch-sync.js +25 -25
  124. package/dist-pkg/input/latch.d.ts +14 -17
  125. package/dist-pkg/input/latch.js +29 -37
  126. package/dist-pkg/input/pad-defaults.d.ts +13 -16
  127. package/dist-pkg/input/pad-defaults.js +21 -28
  128. package/dist-pkg/input/pad-reading.d.ts +76 -0
  129. package/dist-pkg/input/pad-reading.js +169 -0
  130. package/dist-pkg/input/pad-wizard.d.ts +99 -0
  131. package/dist-pkg/input/pad-wizard.js +256 -0
  132. package/dist-pkg/input/pointer-space.d.ts +19 -20
  133. package/dist-pkg/input/pointer-space.js +27 -32
  134. package/dist-pkg/input/pointer.d.ts +39 -40
  135. package/dist-pkg/input/pointer.js +42 -45
  136. package/dist-pkg/input/state.d.ts +101 -89
  137. package/dist-pkg/input/state.js +54 -134
  138. package/dist-pkg/input/switch-scan.d.ts +59 -0
  139. package/dist-pkg/input/switch-scan.js +67 -0
  140. package/dist-pkg/input/synthetic-source.d.ts +42 -0
  141. package/dist-pkg/input/synthetic-source.js +57 -0
  142. package/dist-pkg/input/touch-bindings.d.ts +127 -116
  143. package/dist-pkg/input/touch-bindings.js +158 -173
  144. package/dist-pkg/input/touch.d.ts +147 -72
  145. package/dist-pkg/input/touch.js +362 -126
  146. package/dist-pkg/input/transport-in-use.d.ts +107 -0
  147. package/dist-pkg/input/transport-in-use.js +130 -0
  148. package/dist-pkg/input/transports.d.ts +90 -117
  149. package/dist-pkg/input/transports.js +104 -106
  150. package/dist-pkg/input/virtual-controller.d.ts +82 -0
  151. package/dist-pkg/input/virtual-controller.js +69 -0
  152. package/dist-pkg/input/vocabulary-migration.d.ts +50 -49
  153. package/dist-pkg/input/vocabulary-migration.js +80 -79
  154. package/dist-pkg/input/voice-map.d.ts +59 -0
  155. package/dist-pkg/input/voice-map.js +123 -0
  156. package/dist-pkg/platform/audio-ambient.d.ts +13 -2
  157. package/dist-pkg/platform/audio-ambient.js +14 -14
  158. package/dist-pkg/platform/audio-earcons.d.ts +20 -19
  159. package/dist-pkg/platform/audio-earcons.js +51 -42
  160. package/dist-pkg/platform/audio-jingles.js +9 -10
  161. package/dist-pkg/platform/audio-mixer.d.ts +14 -2
  162. package/dist-pkg/platform/audio-mixer.js +31 -39
  163. package/dist-pkg/platform/audio-sonar.d.ts +49 -152
  164. package/dist-pkg/platform/audio-sonar.js +124 -302
  165. package/dist-pkg/platform/audio.d.ts +62 -19
  166. package/dist-pkg/platform/audio.js +193 -168
  167. package/dist-pkg/platform/choose-by-voice.d.ts +70 -0
  168. package/dist-pkg/platform/choose-by-voice.js +157 -0
  169. package/dist-pkg/platform/flash-sampler.d.ts +19 -0
  170. package/dist-pkg/platform/flash-sampler.js +79 -0
  171. package/dist-pkg/platform/font-library.d.ts +77 -0
  172. package/dist-pkg/platform/font-library.js +98 -0
  173. package/dist-pkg/platform/font-library.json +721 -0
  174. package/dist-pkg/platform/heavy-catalogue.d.ts +77 -0
  175. package/dist-pkg/platform/heavy-catalogue.js +256 -0
  176. package/dist-pkg/platform/heavy-mirror.d.ts +20 -0
  177. package/dist-pkg/platform/heavy-mirror.js +73 -0
  178. package/dist-pkg/platform/heavy.d.ts +136 -0
  179. package/dist-pkg/platform/heavy.js +324 -0
  180. package/dist-pkg/platform/interruptible-speech.d.ts +23 -16
  181. package/dist-pkg/platform/interruptible-speech.js +77 -54
  182. package/dist-pkg/platform/kokoro-port.d.ts +27 -0
  183. package/dist-pkg/platform/kokoro-port.js +50 -0
  184. package/dist-pkg/platform/kokoro-runtime.d.ts +20 -0
  185. package/dist-pkg/platform/kokoro-runtime.js +52 -0
  186. package/dist-pkg/platform/kokoro.d.ts +70 -0
  187. package/dist-pkg/platform/kokoro.js +133 -0
  188. package/dist-pkg/platform/listener-scope.d.ts +12 -0
  189. package/dist-pkg/platform/listener-scope.js +90 -0
  190. package/dist-pkg/platform/locale-host.d.ts +14 -0
  191. package/dist-pkg/platform/locale-host.js +45 -0
  192. package/dist-pkg/platform/microphone.d.ts +51 -0
  193. package/dist-pkg/platform/microphone.js +118 -0
  194. package/dist-pkg/platform/onnx-runtime.d.ts +34 -0
  195. package/dist-pkg/platform/onnx-runtime.js +42 -0
  196. package/dist-pkg/platform/reading-in-worker.d.ts +64 -0
  197. package/dist-pkg/platform/reading-in-worker.js +125 -0
  198. package/dist-pkg/platform/reading-model.d.ts +94 -0
  199. package/dist-pkg/platform/reading-model.js +272 -0
  200. package/dist-pkg/platform/reading-runtime.d.ts +24 -0
  201. package/dist-pkg/platform/reading-runtime.js +164 -0
  202. package/dist-pkg/platform/reading-worker.d.ts +41 -0
  203. package/dist-pkg/platform/reading-worker.js +58 -0
  204. package/dist-pkg/platform/reading.d.ts +80 -0
  205. package/dist-pkg/platform/reading.js +136 -0
  206. package/dist-pkg/platform/speech-recognition.d.ts +60 -0
  207. package/dist-pkg/platform/speech-recognition.js +104 -0
  208. package/dist-pkg/platform/speech.d.ts +23 -1
  209. package/dist-pkg/platform/speech.js +29 -18
  210. package/dist-pkg/platform/storage-keys.d.ts +96 -0
  211. package/dist-pkg/platform/storage-keys.js +106 -0
  212. package/dist-pkg/platform/storage.d.ts +46 -111
  213. package/dist-pkg/platform/storage.js +104 -169
  214. package/dist-pkg/platform/tts.d.ts +85 -39
  215. package/dist-pkg/platform/tts.js +447 -127
  216. package/dist-pkg/platform/vision-loop.d.ts +23 -0
  217. package/dist-pkg/platform/vision-loop.js +56 -0
  218. package/dist-pkg/platform/vision.d.ts +132 -0
  219. package/dist-pkg/platform/vision.js +117 -0
  220. package/dist-pkg/platform/voice-listener.d.ts +40 -0
  221. package/dist-pkg/platform/voice-listener.js +88 -0
  222. package/dist-pkg/platform/voice-plan.d.ts +15 -98
  223. package/dist-pkg/platform/voice-plan.js +56 -143
  224. package/dist-pkg/platform/vosk-runtime.d.ts +96 -0
  225. package/dist-pkg/platform/vosk-runtime.js +151 -0
  226. package/dist-pkg/platform/vosk-vocabulary.d.ts +6 -0
  227. package/dist-pkg/platform/vosk-vocabulary.js +178 -0
  228. package/dist-pkg/render/canvas.d.ts +13 -7
  229. package/dist-pkg/render/canvas.js +13 -13
  230. package/dist-pkg/render/crt.d.ts +48 -12
  231. package/dist-pkg/render/crt.js +78 -80
  232. package/dist-pkg/render/cvd-matrices.d.ts +22 -18
  233. package/dist-pkg/render/cvd-matrices.js +37 -38
  234. package/dist-pkg/render/hc-role-data.d.ts +7 -7
  235. package/dist-pkg/render/hc-role-data.js +14 -14
  236. package/dist-pkg/render/high-contrast.d.ts +67 -56
  237. package/dist-pkg/render/high-contrast.js +226 -217
  238. package/dist-pkg/render/low-vision-drawing.d.ts +5 -0
  239. package/dist-pkg/render/low-vision-drawing.js +46 -0
  240. package/dist-pkg/render/lq-filter.d.ts +32 -20
  241. package/dist-pkg/render/lq-filter.js +59 -65
  242. package/dist-pkg/render/port.d.ts +86 -88
  243. package/dist-pkg/render/port.js +17 -23
  244. package/dist-pkg/render/screen-pipeline.d.ts +42 -42
  245. package/dist-pkg/render/screen-pipeline.js +80 -87
  246. package/dist-pkg/render/sprite-fx.d.ts +2 -1
  247. package/dist-pkg/render/sprite-fx.js +16 -14
  248. package/dist-pkg/render/viewports.d.ts +20 -15
  249. package/dist-pkg/render/viewports.js +83 -114
  250. package/dist-pkg/render/viz-axes-labels.d.ts +47 -0
  251. package/dist-pkg/render/viz-axes-labels.js +88 -0
  252. package/dist-pkg/render/viz-axes.d.ts +89 -86
  253. package/dist-pkg/render/viz-axes.js +128 -116
  254. package/dist-pkg/render/viz-modes.d.ts +28 -29
  255. package/dist-pkg/render/viz-modes.js +40 -45
  256. package/dist-pkg/render/viz-refusal.d.ts +32 -0
  257. package/dist-pkg/render/viz-refusal.js +57 -0
  258. package/dist-pkg/render/viz-setters.d.ts +104 -66
  259. package/dist-pkg/render/viz-setters.js +182 -191
  260. package/dist-pkg/ui/aac-sets.d.ts +45 -0
  261. package/dist-pkg/ui/aac-sets.js +41 -0
  262. package/dist-pkg/ui/audio-choices.d.ts +68 -0
  263. package/dist-pkg/ui/audio-choices.js +98 -0
  264. package/dist-pkg/ui/audio-rows-that-apply.d.ts +12 -0
  265. package/dist-pkg/ui/audio-rows-that-apply.js +43 -0
  266. package/dist-pkg/ui/camera-control.d.ts +53 -0
  267. package/dist-pkg/ui/camera-control.js +48 -0
  268. package/dist-pkg/ui/changed-mark.d.ts +9 -8
  269. package/dist-pkg/ui/changed-mark.js +22 -53
  270. package/dist-pkg/ui/control-choices.d.ts +37 -0
  271. package/dist-pkg/ui/control-choices.js +84 -0
  272. package/dist-pkg/ui/debug-panel.d.ts +59 -43
  273. package/dist-pkg/ui/debug-panel.js +68 -69
  274. package/dist-pkg/ui/declared-words.d.ts +21 -0
  275. package/dist-pkg/ui/declared-words.js +66 -0
  276. package/dist-pkg/ui/dom.d.ts +10 -14
  277. package/dist-pkg/ui/dom.js +10 -47
  278. package/dist-pkg/ui/drawing-problems.d.ts +25 -0
  279. package/dist-pkg/ui/drawing-problems.js +94 -0
  280. package/dist-pkg/ui/eye-control.d.ts +36 -0
  281. package/dist-pkg/ui/eye-control.js +178 -0
  282. package/dist-pkg/ui/face-control.d.ts +25 -0
  283. package/dist-pkg/ui/face-control.js +183 -0
  284. package/dist-pkg/ui/focus-trap.d.ts +39 -49
  285. package/dist-pkg/ui/focus-trap.js +56 -65
  286. package/dist-pkg/ui/fonts.d.ts +157 -68
  287. package/dist-pkg/ui/fonts.js +305 -115
  288. package/dist-pkg/ui/footer-scroll-driver.d.ts +14 -0
  289. package/dist-pkg/ui/footer-scroll-driver.js +201 -0
  290. package/dist-pkg/ui/footer-scroll.d.ts +38 -0
  291. package/dist-pkg/ui/footer-scroll.js +55 -0
  292. package/dist-pkg/ui/game-options.d.ts +47 -0
  293. package/dist-pkg/ui/game-options.js +158 -0
  294. package/dist-pkg/ui/gaze-overlay.d.ts +64 -0
  295. package/dist-pkg/ui/gaze-overlay.js +165 -0
  296. package/dist-pkg/ui/hand-control.d.ts +25 -0
  297. package/dist-pkg/ui/hand-control.js +143 -0
  298. package/dist-pkg/ui/help-panel.d.ts +87 -0
  299. package/dist-pkg/ui/help-panel.js +242 -0
  300. package/dist-pkg/ui/hud-bands.d.ts +62 -0
  301. package/dist-pkg/ui/hud-bands.js +146 -0
  302. package/dist-pkg/ui/hud-row.d.ts +34 -0
  303. package/dist-pkg/ui/hud-row.js +80 -0
  304. package/dist-pkg/ui/hud.d.ts +73 -73
  305. package/dist-pkg/ui/hud.js +94 -98
  306. package/dist-pkg/ui/item-announcement.d.ts +13 -12
  307. package/dist-pkg/ui/item-announcement.js +25 -26
  308. package/dist-pkg/ui/layout.d.ts +103 -28
  309. package/dist-pkg/ui/layout.js +117 -99
  310. package/dist-pkg/ui/libras-avatar-clip.d.ts +30 -0
  311. package/dist-pkg/ui/libras-avatar-clip.js +86 -0
  312. package/dist-pkg/ui/libras-avatar-load.d.ts +52 -0
  313. package/dist-pkg/ui/libras-avatar-load.js +134 -0
  314. package/dist-pkg/ui/libras-avatar-plan.d.ts +102 -0
  315. package/dist-pkg/ui/libras-avatar-plan.js +211 -0
  316. package/dist-pkg/ui/libras-avatar-player.d.ts +28 -0
  317. package/dist-pkg/ui/libras-avatar-player.js +226 -0
  318. package/dist-pkg/ui/libras-avatar-stage.d.ts +37 -0
  319. package/dist-pkg/ui/libras-avatar-stage.js +208 -0
  320. package/dist-pkg/ui/libras-glosses.d.ts +28 -0
  321. package/dist-pkg/ui/libras-glosses.js +161 -0
  322. package/dist-pkg/ui/locale-flags.d.ts +16 -0
  323. package/dist-pkg/ui/locale-flags.js +30 -0
  324. package/dist-pkg/ui/loop-crash.d.ts +17 -17
  325. package/dist-pkg/ui/loop-crash.js +33 -62
  326. package/dist-pkg/ui/menu-intent.d.ts +61 -0
  327. package/dist-pkg/ui/menu-intent.js +88 -0
  328. package/dist-pkg/ui/menu-items.d.ts +20 -0
  329. package/dist-pkg/ui/menu-items.js +51 -0
  330. package/dist-pkg/ui/menu-nav.d.ts +98 -92
  331. package/dist-pkg/ui/menu-nav.js +398 -286
  332. package/dist-pkg/ui/mobility-choices.d.ts +26 -0
  333. package/dist-pkg/ui/mobility-choices.js +41 -0
  334. package/dist-pkg/ui/motion-choices.d.ts +31 -0
  335. package/dist-pkg/ui/motion-choices.js +68 -0
  336. package/dist-pkg/ui/motion-scene.d.ts +24 -19
  337. package/dist-pkg/ui/motion-scene.js +29 -61
  338. package/dist-pkg/ui/mount-panel.d.ts +81 -0
  339. package/dist-pkg/ui/mount-panel.js +61 -0
  340. package/dist-pkg/ui/panel-shell.d.ts +65 -31
  341. package/dist-pkg/ui/panel-shell.js +92 -66
  342. package/dist-pkg/ui/panel-widgets.d.ts +150 -0
  343. package/dist-pkg/ui/panel-widgets.js +294 -0
  344. package/dist-pkg/ui/pause-buttons.d.ts +35 -0
  345. package/dist-pkg/ui/pause-buttons.js +66 -0
  346. package/dist-pkg/ui/pause-icons.d.ts +261 -347
  347. package/dist-pkg/ui/pause-icons.js +671 -601
  348. package/dist-pkg/ui/pause-markup.d.ts +86 -0
  349. package/dist-pkg/ui/pause-markup.js +125 -0
  350. package/dist-pkg/ui/reach-notice.d.ts +22 -23
  351. package/dist-pkg/ui/reach-notice.js +55 -58
  352. package/dist-pkg/ui/scan-overlay.d.ts +23 -0
  353. package/dist-pkg/ui/scan-overlay.js +50 -0
  354. package/dist-pkg/ui/screen-text.d.ts +25 -0
  355. package/dist-pkg/ui/screen-text.js +152 -0
  356. package/dist-pkg/ui/session-clock.d.ts +32 -0
  357. package/dist-pkg/ui/session-clock.js +67 -0
  358. package/dist-pkg/ui/settings-aac.d.ts +70 -0
  359. package/dist-pkg/ui/settings-aac.js +208 -0
  360. package/dist-pkg/ui/settings-audio.d.ts +97 -114
  361. package/dist-pkg/ui/settings-audio.js +493 -425
  362. package/dist-pkg/ui/settings-controls.d.ts +53 -96
  363. package/dist-pkg/ui/settings-controls.js +143 -215
  364. package/dist-pkg/ui/settings-empathy.d.ts +39 -30
  365. package/dist-pkg/ui/settings-empathy.js +53 -60
  366. package/dist-pkg/ui/settings-mobility.d.ts +192 -0
  367. package/dist-pkg/ui/settings-mobility.js +332 -0
  368. package/dist-pkg/ui/settings-motion.d.ts +91 -70
  369. package/dist-pkg/ui/settings-motion.js +287 -198
  370. package/dist-pkg/ui/settings-panel.d.ts +58 -45
  371. package/dist-pkg/ui/settings-panel.js +189 -94
  372. package/dist-pkg/ui/settings-typo.d.ts +29 -78
  373. package/dist-pkg/ui/settings-typo.js +158 -129
  374. package/dist-pkg/ui/settings-visual.d.ts +29 -87
  375. package/dist-pkg/ui/settings-visual.js +240 -193
  376. package/dist-pkg/ui/shell.d.ts +112 -135
  377. package/dist-pkg/ui/shell.js +148 -217
  378. package/dist-pkg/ui/simulation-list.d.ts +27 -0
  379. package/dist-pkg/ui/simulation-list.js +76 -0
  380. package/dist-pkg/ui/simulation-over-the-world.d.ts +23 -0
  381. package/dist-pkg/ui/simulation-over-the-world.js +131 -0
  382. package/dist-pkg/ui/simulation-refusal.d.ts +5 -31
  383. package/dist-pkg/ui/simulation-refusal.js +8 -56
  384. package/dist-pkg/ui/switchable-control.d.ts +11 -0
  385. package/dist-pkg/ui/switchable-control.js +19 -0
  386. package/dist-pkg/ui/title.d.ts +3 -12
  387. package/dist-pkg/ui/title.js +49 -18
  388. package/dist-pkg/ui/top-band.d.ts +26 -0
  389. package/dist-pkg/ui/top-band.js +96 -0
  390. package/dist-pkg/ui/typo-choices.d.ts +71 -0
  391. package/dist-pkg/ui/typo-choices.js +85 -0
  392. package/dist-pkg/ui/visual-axes-panel.d.ts +11 -47
  393. package/dist-pkg/ui/visual-axes-panel.js +12 -94
  394. package/dist-pkg/ui/visual-choices.d.ts +77 -0
  395. package/dist-pkg/ui/visual-choices.js +107 -0
  396. package/dist-pkg/ui/vlibras.d.ts +77 -10
  397. package/dist-pkg/ui/vlibras.js +130 -81
  398. package/dist-pkg/ui/voice-control.d.ts +68 -0
  399. package/dist-pkg/ui/voice-control.js +276 -0
  400. package/dist-pkg/ui/voice-settings.d.ts +95 -0
  401. package/dist-pkg/ui/voice-settings.js +352 -0
  402. package/dist-pkg/ui/where-the-child-is.d.ts +31 -0
  403. package/dist-pkg/ui/where-the-child-is.js +70 -0
  404. package/docs/CREDITS.md +300 -44
  405. package/docs/LICENSES.md +189 -145
  406. package/package.json +51 -27
  407. package/scripts/check-cartridge.mjs +78 -0
  408. package/scripts/game-build.d.mts +25 -0
  409. package/scripts/game-build.mjs +144 -0
  410. package/scripts/heavy-into-the-delivery.mjs +388 -0
  411. package/scripts/libras-avatar.json +680 -0
  412. package/scripts/libras-avatar.mjs +246 -0
  413. package/scripts/libras-glosses/gloss.py +95 -0
  414. package/scripts/libras-glosses/pyproject.toml +32 -0
  415. package/scripts/libras-glosses/uv.lock +781 -0
  416. package/scripts/libras-glosses.mjs +351 -0
  417. package/scripts/licences/Apache-2.0.txt +176 -0
  418. package/scripts/licences/GPL-3.0.txt +674 -0
  419. package/scripts/licences/MIT.txt +17 -0
  420. package/scripts/licences/OFL-1.1.txt +86 -0
  421. package/scripts/licences/UFL-1.0.txt +96 -0
  422. package/scripts/licences/fonts.mjs +389 -0
  423. package/scripts/licences/third-party.mjs +301 -0
  424. package/scripts/licences/vosk-browser.NOTICE.txt +268 -0
  425. package/app/public/vendor/fonts/comicneue-400.woff2 +0 -0
  426. package/app/public/vendor/fonts/comicneue-700.woff2 +0 -0
  427. package/app/public/vendor/fonts/fondamento-400-ext.woff2 +0 -0
  428. package/app/public/vendor/fonts/fondamento-400.woff2 +0 -0
  429. package/app/public/vendor/fonts/inter-400.woff2 +0 -0
  430. package/app/public/vendor/fonts/inter-700.woff2 +0 -0
  431. package/app/public/vendor/fonts/lato-400.woff2 +0 -0
  432. package/app/public/vendor/fonts/lato-700.woff2 +0 -0
  433. package/app/public/vendor/fonts/literata-400.woff2 +0 -0
  434. package/app/public/vendor/fonts/literata-700.woff2 +0 -0
  435. package/app/public/vendor/fonts/newsreader-400.woff2 +0 -0
  436. package/app/public/vendor/fonts/newsreader-700.woff2 +0 -0
  437. package/app/public/vendor/fonts/opendyslexic-400.woff2 +0 -0
  438. package/app/public/vendor/fonts/opensans-400.woff2 +0 -0
  439. package/app/public/vendor/fonts/opensans-700.woff2 +0 -0
  440. package/app/public/vendor/fonts/pinyon-400.woff2 +0 -0
  441. package/app/public/vendor/fonts/pressstart-400.woff2 +0 -0
  442. package/app/public/vendor/fonts/quattro-400.woff2 +0 -0
  443. package/app/public/vendor/fonts/quattro-700.woff2 +0 -0
  444. package/app/public/vendor/fonts/sourcesans-400.woff2 +0 -0
  445. package/app/public/vendor/fonts/sourcesans-700.woff2 +0 -0
  446. package/app/public/vendor/fonts/sourceserif-400.woff2 +0 -0
  447. package/app/public/vendor/fonts/sourceserif-700.woff2 +0 -0
  448. package/app/public/vendor/fonts/ufmag-400.woff2 +0 -0
  449. package/dist-pkg/core/anel.d.ts +0 -17
  450. package/dist-pkg/core/anel.js +0 -31
  451. package/dist-pkg/core/collision.d.ts +0 -21
  452. package/dist-pkg/core/collision.js +0 -62
  453. package/dist-pkg/core/layers.d.ts +0 -63
  454. package/dist-pkg/core/layers.js +0 -95
  455. package/dist-pkg/core/letter-grid.d.ts +0 -55
  456. package/dist-pkg/core/letter-grid.js +0 -116
  457. package/dist-pkg/core/password.d.ts +0 -34
  458. package/dist-pkg/core/password.js +0 -205
  459. package/dist-pkg/core/rotulo-acessivel.d.ts +0 -17
  460. package/dist-pkg/core/rotulo-acessivel.js +0 -41
  461. package/dist-pkg/core/run-state.d.ts +0 -115
  462. package/dist-pkg/core/run-state.js +0 -40
  463. package/dist-pkg/core/tiles.d.ts +0 -12
  464. package/dist-pkg/core/tiles.js +0 -61
  465. package/dist-pkg/core/world.d.ts +0 -4
  466. package/dist-pkg/core/world.js +0 -58
  467. package/dist-pkg/input/origem-sintetica.d.ts +0 -44
  468. package/dist-pkg/input/origem-sintetica.js +0 -60
  469. package/dist-pkg/input/transporte-em-uso.d.ts +0 -101
  470. package/dist-pkg/input/transporte-em-uso.js +0 -130
  471. package/dist-pkg/platform/audio-nav.d.ts +0 -51
  472. package/dist-pkg/platform/audio-nav.js +0 -99
  473. package/dist-pkg/platform/guide-intensity.d.ts +0 -40
  474. package/dist-pkg/platform/guide-intensity.js +0 -73
  475. package/dist-pkg/platform/pesados-catalogo.d.ts +0 -14
  476. package/dist-pkg/platform/pesados-catalogo.js +0 -177
  477. package/dist-pkg/platform/pesados.d.ts +0 -31
  478. package/dist-pkg/platform/pesados.js +0 -75
  479. package/dist-pkg/platform/vozes-prontas.d.ts +0 -28
  480. package/dist-pkg/platform/vozes-prontas.js +0 -61
  481. package/dist-pkg/render/camera.d.ts +0 -58
  482. package/dist-pkg/render/camera.js +0 -105
  483. package/dist-pkg/render/cenario-data.d.ts +0 -127
  484. package/dist-pkg/render/cenario-data.js +0 -145
  485. package/dist-pkg/render/city-tex.d.ts +0 -63
  486. package/dist-pkg/render/city-tex.js +0 -228
  487. package/dist-pkg/render/city-tiles.d.ts +0 -11
  488. package/dist-pkg/render/city-tiles.js +0 -140
  489. package/dist-pkg/render/draw.d.ts +0 -149
  490. package/dist-pkg/render/draw.js +0 -231
  491. package/dist-pkg/render/fx.d.ts +0 -63
  492. package/dist-pkg/render/fx.js +0 -123
  493. package/dist-pkg/render/minimap.d.ts +0 -11
  494. package/dist-pkg/render/minimap.js +0 -94
  495. package/dist-pkg/render/parallax.d.ts +0 -85
  496. package/dist-pkg/render/parallax.js +0 -158
  497. package/dist-pkg/render/player-anim.d.ts +0 -71
  498. package/dist-pkg/render/player-anim.js +0 -152
  499. package/dist-pkg/render/recycling-tex.d.ts +0 -48
  500. package/dist-pkg/render/recycling-tex.js +0 -164
  501. package/dist-pkg/render/scene-city.d.ts +0 -77
  502. package/dist-pkg/render/scene-city.js +0 -181
  503. package/dist-pkg/render/scene-parallax.d.ts +0 -72
  504. package/dist-pkg/render/scene-parallax.js +0 -426
  505. package/dist-pkg/render/scene-sky.d.ts +0 -126
  506. package/dist-pkg/render/scene-sky.js +0 -295
  507. package/dist-pkg/render/set-cenario.d.ts +0 -54
  508. package/dist-pkg/render/set-cenario.js +0 -103
  509. package/dist-pkg/render/textures.d.ts +0 -75
  510. package/dist-pkg/render/textures.js +0 -240
  511. package/dist-pkg/render/title-scene.d.ts +0 -46
  512. package/dist-pkg/render/title-scene.js +0 -75
  513. package/dist-pkg/render/weather.d.ts +0 -96
  514. package/dist-pkg/render/weather.js +0 -171
  515. package/dist-pkg/render/wheelchair-sprites.d.ts +0 -32
  516. package/dist-pkg/render/wheelchair-sprites.js +0 -69
  517. package/dist-pkg/render/world-tex.d.ts +0 -18
  518. package/dist-pkg/render/world-tex.js +0 -87
  519. package/dist-pkg/ui/activities-menu.d.ts +0 -258
  520. package/dist-pkg/ui/activities-menu.js +0 -648
  521. package/dist-pkg/ui/caa-sets.d.ts +0 -39
  522. package/dist-pkg/ui/caa-sets.js +0 -65
  523. package/dist-pkg/ui/latch-refusal.d.ts +0 -32
  524. package/dist-pkg/ui/latch-refusal.js +0 -60
  525. package/dist-pkg/ui/map-hub.d.ts +0 -56
  526. package/dist-pkg/ui/map-hub.js +0 -138
  527. package/dist-pkg/ui/settings-caa.d.ts +0 -45
  528. package/dist-pkg/ui/settings-caa.js +0 -137
  529. package/dist-pkg/ui/settings-motor.d.ts +0 -175
  530. package/dist-pkg/ui/settings-motor.js +0 -342
  531. package/dist-pkg/ui/webcam.d.ts +0 -7
  532. package/dist-pkg/ui/webcam.js +0 -93
@@ -1,38 +1,4 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-or-later
2
- // ui/dom.ts — atalhos de seleção do DOM, usados em toda a UI (menus/HUD/pausa/quiz/opções). PURO no import:
3
- // só define as funções (não toca no DOM → importável em node). $ = querySelector; $$ = querySelectorAll
4
- // como Array. Genéricos: $<HTMLInputElement>('#x') já tipa o retorno. (Fase 2.27 / Tier 1)
5
- //
6
- // DEIXOU DE SER FOLHA no item 14, e de propósito: passou a importar `core/i18n` para que `toggleLabel` (o
7
- // texto do botão de alternância) more ao lado do `toggleBtn` (a classe e o `aria-pressed`). A propriedade que
8
- // importa — importável em node, sem I/O no import — continua valendo; o que se perdeu foi a contagem de zero
9
- // dependências, e o que se ganhou está escrito no comentário de `toggleLabel`.
10
- import { t } from '../core/i18n.js';
11
- // O PADRAO E `HTMLElement`, NAO `Element`, e a diferenca aparece em dezessete pontos de chamada: `.hidden`,
12
- // `.focus()` e `.value` nao existem em `Element`, e sao exatamente o que uma aplicacao faz com o que
13
- // seleciona. `Element` e o padrao do lib.dom porque `querySelector` tambem serve para SVG e MathML —
14
- // nenhum seletor deste projeto pega um desses (conferido). Quem precisar de um tipo mais estreito passa
15
- // o parametro: `$<HTMLSelectElement>('#pad-design')`.
16
- /**
17
- * ⚠️ RESOLVIDO POR `globalThis` E NÃO PELO GLOBAL CRU, e a diferença é entre devolver `null` e LANÇAR.
18
- *
19
- * `document.querySelector(...)` com `document` inexistente dá `ReferenceError` — não `undefined` —, e a
20
- * assinatura destas duas funções promete `T | null`. Uma consulta que lança onde promete `null` é um defeito
21
- * pela própria assinatura, e ele viajava longe: medido em 2026-09-08, o `core/a11y-sr.srAlert` chama o `$`
22
- * daqui, e o `createGame` chama o `srAlert` ao mostrar o aviso de alcance — logo bootar a engine contra um
23
- * documento INJECTADO (um iframe, um editor ao lado do jogo, um teste) rebentava o boot inteiro num anúncio.
24
- *
25
- * 📌 É o ACHADO 15 do `boot/create-game` outra vez, e sobreviveu pela mesma razão: enquanto toda raiz era um
26
- * `main.ts` num navegador, o global ERA o documento certo. `globalThis.document` é a mesma coisa onde ele
27
- * existe, e é `undefined` — em vez de explosão — onde não existe.
28
- *
29
- * ⚠️ E ELAS CONTINUAM A OLHAR PARA O GLOBAL, de propósito: quem precisa de consultar OUTRO documento injecta
30
- * o seu (`create-game` tem um `$` próprio ligado ao `doc` do hospedeiro, e o `ui/pause-icons` tem o
31
- * `docDaMontagem`). O que este conserto muda não é ONDE se procura — é o que acontece quando não há onde.
32
- */
33
- const docGlobal = () => globalThis.document;
34
- export const $ = (s) => docGlobal()?.querySelector(s) ?? null;
35
- export const $$ = (s) => [...(docGlobal()?.querySelectorAll(s) ?? [])];
36
2
  /**
37
3
  * Reflects an on/off state onto a toggle button: the visual class AND `aria-pressed`.
38
4
  * Lives here because the two must never drift apart — a button that looks pressed but does not
@@ -45,21 +11,18 @@ export function toggleBtn(b, on) {
45
11
  b.setAttribute('aria-pressed', String(on));
46
12
  }
47
13
  /**
48
- * O TEXTO do botão de alternância: '❚❚ Ligado' / '▶ Desligado'.
49
- *
50
- * Mora aqui pelo mesmo motivo do `toggleBtn` logo acima — os dois são as duas metades do mesmo gesto, e a
51
- * nota daquela função diz que elas "não podem se separar". Elas tinham se separado em TREZE cópias, em nove
52
- * arquivos, e dois deles declaravam um helper que se chamava compartilhado e servia um chamador só
53
- * (`settings-empathy.toggleLabel` e `settings-motor.onOffLabel`, ambos apagados agora).
14
+ * The TEXT of a toggle button: 'On' / 'Off' in English.
54
15
  *
55
- * O custo dessa dispersão não era estético: com o texto em treze lugares, traduzir o jogo exigiria achar os
56
- * treze, e esquecer um deixaria um botão em português no meio do inglês — sem erro, sem teste vermelho.
16
+ * It lives here for the same reason as `toggleBtn` just above — the two are halves of one gesture, and must not
17
+ * drift apart. A text copied into each panel is a text that translating the game has to find everywhere, and the
18
+ * one copy it misses leaves a button in the wrong language — with no error and no red test.
57
19
  */
58
- export function toggleLabel(on) {
20
+ export function toggleLabel(t, on) {
59
21
  return t(on ? 'ui.toggle.on' : 'ui.toggle.off');
60
22
  }
61
- /** O `aria-label` da mesma alternância: '{alvo}: ligado'. Separado do rótulo visível porque o leitor de tela
62
- * precisa do NOME do alvo junto, e a tela não (o nome já está na linha ao lado do botão). */
63
- export function toggleAria(alvo, on) {
64
- return t(on ? 'ui.toggle.ariaOn' : 'ui.toggle.ariaOff', { alvo });
23
+ /** The `aria-label` of the same toggle: '{alvo}: on' (`{alvo}` is the dictionary's interpolation key). Separate from
24
+ * the visible label because a screen reader needs the NAME of the target with it, and the screen does not (the
25
+ * name is already on the row beside the button). */
26
+ export function toggleAria(t, target, on) {
27
+ return t(on ? 'ui.toggle.ariaOn' : 'ui.toggle.ariaOff', { alvo: target });
65
28
  }
@@ -0,0 +1,25 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ import { type Scale } from './layout.js';
3
+ export interface DrawingProblemsCtx {
4
+ /** `#game-region`, read at EVERY call — see the note at the top of this file. */
5
+ readonly region: () => HTMLElement | null;
6
+ /** The accessibility bar, or `null` where the game declined it. Read at every call, for the same reason. */
7
+ readonly bar: () => Element | null;
8
+ /** The scale the engine forced, or `null` before the first layout has run. */
9
+ readonly scale: () => Scale | null;
10
+ /** `window.getComputedStyle`, absent in a host that has none — then a node is counted rather than guessed about. */
11
+ readonly computedStyle?: (el: Element) => CSSStyleDeclaration;
12
+ }
13
+ /**
14
+ * ADR-0163 rule 4, second half: the text and the targets the CARTRIDGE draws under the floor, named — read when `problems` is.
15
+ * `null` when there is nothing to say, which is what a clean page answers.
16
+ */
17
+ export declare function drawnBelowTheFloor(ctx: DrawingProblemsCtx): string | null;
18
+ /**
19
+ * The nodes that PAINT over the accessibility bar's rectangle (ADR-0148 §3).
20
+ *
21
+ * 📏 ONLY WHAT PAINTS, and it was measured in `dist/quiz.html`: the quiz was accused at every boot because `#quiz-app` — a
22
+ * transparent box the size of the region, whose top padding IS the bar's room — intersects the bar's rectangle and draws
23
+ * nothing there. A node counts when it has text of its own, is a control or a medium, or paints a background.
24
+ */
25
+ export declare function barIntruderProblems(ctx: DrawingProblemsCtx): string[];
@@ -0,0 +1,94 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // ui/drawing-problems — WHAT A CARTRIDGE DREW THAT THE CHILD PAYS FOR, named (ADR-0163 rule 4, ADR-0148 §3; issue #203).
3
+ //
4
+ // Two questions the engine asks about its own region, and both answer with the sentence a `problems` line needs: text and
5
+ // targets drawn UNDER the floor, and the nodes drawn OVER the accessibility bar. They are one subject — the engine measuring
6
+ // what somebody else drew — and they lived inside `boot/create-game`, which is why this module exists: 📏 that file reached
7
+ // 2185 lines and 330 decision nodes (ADR-0221), and the debt is paid by SUBJECT, never by slicing.
8
+ //
9
+ // ⚠️ THE READERS ARE FUNCTIONS AND NOT VALUES, and that is the whole care in this file. `#game-region`, the bar and the applied
10
+ // scale are all things the root REPLACES: a `mount()` swaps the cartridge and the bar is built after the root is. Taking them
11
+ // as values here would freeze the first answer and report, forever, about a region that is no longer on the page — silently,
12
+ // because a stale element still answers `getBoundingClientRect()`.
13
+ import { barIntruders, belowFloor, minimumTarget } from './layout.js';
14
+ /** The engine's OWN nodes inside the region — their sizes are held by their own gates — and what is not drawn for the eye. */
15
+ const ENGINE_NODES = '#title-icons, .screen-pause, .overlay, #touch-controls, .rodape-da-tela, .pausa-rapida, #incl-parou, .sr-only, .hud-faixa, .hud-row';
16
+ /** What a finger is meant to hit; anything else has no target floor to fail. */
17
+ const TOUCH_TARGET = 'button, a[href], input:not([type="hidden"]), select, textarea, [role="button"], [tabindex]:not([tabindex="-1"])';
18
+ /**
19
+ * A node named the way a developer finds it: `tag#id.firstClass`.
20
+ *
21
+ * 📌 It is NOT exported, and the `exports-without-consumer` gate is what said so: exporting it would publish a name only this
22
+ * file uses, and an export with no consumer is surface somebody has to maintain with nobody needing it (ADR-0170 §3).
23
+ */
24
+ function nodeName(el) {
25
+ return el.tagName.toLowerCase() + (el.id ? `#${el.id}` : '')
26
+ + (el.className ? `.${String(el.className).trim().split(/\s+/)[0]}` : '');
27
+ }
28
+ /**
29
+ * ADR-0163 rule 4, second half: the text and the targets the CARTRIDGE draws under the floor, named — read when `problems` is.
30
+ * `null` when there is nothing to say, which is what a clean page answers.
31
+ */
32
+ export function drawnBelowTheFloor(ctx) {
33
+ const region = ctx.region();
34
+ const scale = ctx.scale();
35
+ if (!scale || !region || typeof region.querySelectorAll !== 'function' || !ctx.computedStyle)
36
+ return null;
37
+ const nodes = [...region.querySelectorAll('*')].map((el) => {
38
+ const b = el.getBoundingClientRect();
39
+ const hasText = [...el.childNodes].some((c) => c.nodeType === 3 && (c.textContent ?? '').trim() !== '');
40
+ return {
41
+ name: nodeName(el),
42
+ daEngine: el.closest(ENGINE_NODES) !== null,
43
+ fontPx: hasText && b.width > 0 && b.height > 0 ? parseFloat(ctx.computedStyle(el).fontSize) : null,
44
+ target: el.matches(TOUCH_TARGET) ? { w: b.width, h: b.height } : null,
45
+ };
46
+ });
47
+ const { text: smallText, targets: smallTargets } = belowFloor(nodes, scale.k);
48
+ if (!smallText.length && !smallTargets.length)
49
+ return null;
50
+ const k = minimumTarget(scale.k) / 22;
51
+ const parts = [
52
+ smallText.length ? `text under ${8 * k} px (${smallText.slice(0, 4).join(', ')})` : '',
53
+ smallTargets.length ? `targets under ${22 * k} px (${smallTargets.slice(0, 4).join(', ')})` : '',
54
+ ].filter(Boolean);
55
+ return `the cartridge draws ${parts.join(' and ')} in #game-region: a child with low vision or unsteady hands cannot `
56
+ + 'read or hit them — text and targets start at 16 and 44 px at 640×360 and grow with the scale (ADR-0163): size them '
57
+ + 'from `--ui-fs` and `--alvo-min`';
58
+ }
59
+ /**
60
+ * The nodes that PAINT over the accessibility bar's rectangle (ADR-0148 §3).
61
+ *
62
+ * 📏 ONLY WHAT PAINTS, and it was measured in `dist/quiz.html`: the quiz was accused at every boot because `#quiz-app` — a
63
+ * transparent box the size of the region, whose top padding IS the bar's room — intersects the bar's rectangle and draws
64
+ * nothing there. A node counts when it has text of its own, is a control or a medium, or paints a background.
65
+ */
66
+ export function barIntruderProblems(ctx) {
67
+ const region = ctx.region();
68
+ const bar = ctx.bar();
69
+ if (!bar || !region || typeof bar.getBoundingClientRect !== 'function')
70
+ return [];
71
+ const boxOf = (el) => {
72
+ const b = el.getBoundingClientRect();
73
+ return { x: b.x, y: b.y, w: b.width, h: b.height };
74
+ };
75
+ const paints = (el) => {
76
+ if ([...el.childNodes].some((c) => c.nodeType === 3 && (c.textContent ?? '').trim() !== ''))
77
+ return true;
78
+ if (/^(img|canvas|svg|video|input|button|select|textarea)$/i.test(el.tagName))
79
+ return true;
80
+ if (!ctx.computedStyle)
81
+ return true; // no way to tell: count it, as before
82
+ const cs = ctx.computedStyle(el);
83
+ return (cs.backgroundColor !== '' && cs.backgroundColor !== 'transparent' && cs.backgroundColor !== 'rgba(0, 0, 0, 0)')
84
+ || (cs.backgroundImage !== '' && cs.backgroundImage !== 'none');
85
+ };
86
+ const nodes = [...region.querySelectorAll('*')].filter((el) => paints(el)).map((el) => ({
87
+ name: nodeName(el),
88
+ box: boxOf(el),
89
+ // the engine's HUD bands and HUD row are placed by the ENGINE: if one reaches the bar, that is the engine's defect and
90
+ // not the game's
91
+ isBar: el === bar || bar.contains(el) || el.closest('.hud-faixa, .hud-row') !== null,
92
+ }));
93
+ return barIntruders(boxOf(bar), nodes);
94
+ }
@@ -0,0 +1,36 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ import type { Translate } from '../core/i18n.js';
3
+ import type { VirtualController } from '../input/virtual-controller.js';
4
+ import { type FaceTrackerLoad, type VisionDeps } from '../platform/vision.js';
5
+ import { type VisionLoopDeps } from '../platform/vision-loop.js';
6
+ import type { SwitchableControl } from './switchable-control.js';
7
+ /** A camera picture the tracker can read. */
8
+ export interface CameraFeed {
9
+ readonly frame: unknown;
10
+ ready(): boolean;
11
+ close(): void;
12
+ }
13
+ export interface EyeControlDeps {
14
+ /** Translates in the page's language — the root's translator (ADR-0232 D3). REQUIRED: text built from nowhere is a raw key. */
15
+ t: Translate;
16
+ readonly doc: Document;
17
+ /** Where the regions are drawn: `#game-region`. */
18
+ readonly region: HTMLElement;
19
+ /** The page's address, to reach `heavy/`. */
20
+ readonly base: string;
21
+ /** Whether a file is in the checked cache, handed to the tracker's loader (ADR-0232 D4: the root lends the cache). */
22
+ readonly hasFile: VisionDeps['hasFile'];
23
+ readonly loop: VisionLoopDeps;
24
+ /** Where the presses go: seat 0's virtual controller. */
25
+ readonly controller: VirtualController;
26
+ readonly say: (text: string) => void;
27
+ readonly alert: (text: string) => void;
28
+ /** A line for `problems`. */
29
+ readonly report: (line: string) => void;
30
+ /** Put the 📷 back to off: what cannot start must not show as on. */
31
+ readonly turnOff: () => void;
32
+ readonly loadTracker?: (deps: VisionDeps) => Promise<FaceTrackerLoad>;
33
+ readonly openFeed?: () => Promise<CameraFeed>;
34
+ }
35
+ export declare function videoFeed(doc: Document, media: MediaDevices): () => Promise<CameraFeed>;
36
+ export declare function createEyeControl(d: EyeControlDeps): SwitchableControl;
@@ -0,0 +1,178 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // ui/eye-control — PLAYING WITH THE EYES, PUT TOGETHER (ADR-0213; issues #194, #196).
3
+ //
4
+ // The pieces are each tested apart; this joins them for the root: the camera and the face tracker (platform/vision), a frame loop that
5
+ // cannot stop in silence (platform/vision-loop), the right eye's gaze read from its own rest (input/gaze-relative), the cycle that turns
6
+ // zones into actions (input/gaze-cycle), the presses on the virtual controller with the source `olhos` (input/virtual-controller, ADR-0111:
7
+ // never a disguised keyboard), and the regions drawn over the game
8
+ // (ui/gaze-overlay). The 📷 at its eyes position drives it (ADR-0215); any other position lets the camera go.
9
+ // · Nothing commands before the rest is measured: until then, and on a frame with no face or a head turning fast, the cycle is frozen.
10
+ // · A reading that re-centres itself cancels the gesture in hand.
11
+ // · Only the RIGHT eye is read (ADR-0213): the Dev's brain suppresses the left one, and a child's may too.
12
+ // · What cannot work is said to the child and written in `problems`, once: the files not on the device, a runtime that does not load,
13
+ // the camera refused, frames that stop or crawl (`ui/camera-control.startCameraReader` for the first three). A 📷 that is on and does nothing is the defect this module exists not to ship.
14
+ import { headPoseFromMatrix, scoresFromCategories, eyeGazeFromScores, bothEyesClosed } from '../input/face-signals.js';
15
+ import { createGazeReader, GAZE_DEFAULTS } from '../input/gaze-relative.js';
16
+ import { createGazeCycle } from '../input/gaze-cycle.js';
17
+ import { loadFaceTracker, openCamera, closeCamera } from '../platform/vision.js';
18
+ import { createVisionLoop } from '../platform/vision-loop.js';
19
+ import { drawGazeOverlay } from './gaze-overlay.js';
20
+ import { startCameraReader } from './camera-control.js';
21
+ // 📌 THE SHAPE IS THE FAMILY'S (ADR-0221 step 7f): what this module returns IS a SwitchableControl — following the 📷: on in
22
+ // the eyes position it opens the camera and the drawing, off it lets go of everything.
23
+ export function videoFeed(doc, media) {
24
+ return async () => {
25
+ const stream = await openCamera(media);
26
+ const video = doc.createElement('video');
27
+ video.muted = true;
28
+ video.playsInline = true;
29
+ video.srcObject = stream;
30
+ await video.play();
31
+ return { frame: video, ready: () => video.readyState >= 2, close: () => { video.pause(); video.srcObject = null; closeCamera(stream); } };
32
+ };
33
+ }
34
+ /** What one detection says: the blendshapes (none without a face), the head's pose, and the RIGHT eye's gaze. */
35
+ function readDetection(det) {
36
+ const cats = det?.faceBlendshapes?.[0]?.categories;
37
+ const scores = cats ? scoresFromCategories(cats) : null;
38
+ return { scores, pose: headPoseFromMatrix(det?.facialTransformationMatrixes?.[0]?.data), eye: scores ? eyeGazeFromScores(scores).right : null };
39
+ }
40
+ /** What the cycle is told: frozen while nothing can be trusted — no rest yet, no face, a head turning fast — and cancelled when
41
+ * the reader re-centres under the gaze, because what was held was never a decision. */
42
+ function cycleFrameOf(reading, scores) {
43
+ return {
44
+ zone: reading.zone,
45
+ eyesClosed: !!scores && bothEyesClosed(scores),
46
+ frozen: !reading.ready || !scores || reading.reason === 'head-moving',
47
+ cancel: reading.reason === 'recentring',
48
+ };
49
+ }
50
+ /** The reading, written on the drawing's own element: when a child's eyes are not answered, this says at which step it stops. */
51
+ function writeDiagnosis(canvas, reading, out, face, delegate) {
52
+ const f2 = (n) => (n === undefined ? '' : n.toFixed(2));
53
+ Object.assign(canvas.dataset, {
54
+ face: String(face), ready: String(reading.ready), reason: reading.reason ?? '', zone: reading.zone ?? '',
55
+ tremorH: f2(reading.tremor?.h), tremorV: f2(reading.tremor?.v), dh: f2(reading.displacement?.h), dv: f2(reading.displacement?.v),
56
+ stillMs: String(Math.round(reading.stillMs ?? 0)), armed: String(out.armed), preparing: String(out.preparing), preview: out.preview?.item ?? '',
57
+ delegate,
58
+ });
59
+ }
60
+ /** The regions over the game at the region's size, the middle counting down while the rest is measured, and the face's lines. */
61
+ function drawFrame(t, canvas, region, reading, out, det, lines) {
62
+ const w = canvas.width = region.clientWidth, h = canvas.height = region.clientHeight;
63
+ drawGazeOverlay(t, canvas.getContext('2d'), w, h, {
64
+ zone: reading.zone, armed: out.armed, preparing: out.preparing, preview: out.preview, restReady: reading.ready,
65
+ restLeftMs: reading.ready ? undefined : Math.max(0, GAZE_DEFAULTS.restMs - (reading.stillMs ?? 0)),
66
+ }, { face: det?.faceLandmarks?.[0] ? { landmarks: det.faceLandmarks[0], lines } : null });
67
+ }
68
+ export function createEyeControl(d) {
69
+ const { t } = d;
70
+ const loadTracker = d.loadTracker ?? loadFaceTracker;
71
+ const openFeed = d.openFeed ?? videoFeed(d.doc, d.doc.defaultView.navigator.mediaDevices);
72
+ const said = new Set();
73
+ const once = (kind, line, spoken) => {
74
+ if (!said.has(kind)) {
75
+ said.add(kind);
76
+ d.report(line);
77
+ }
78
+ d.alert(spoken);
79
+ };
80
+ let on = false, running = false, starting = null;
81
+ const current = () => on; // read through a call: `apply` awaits, and `on` can change meanwhile
82
+ let tracker = null, feed = null, canvas = null;
83
+ let pressed = null, wasReady = false;
84
+ let reader = createGazeReader(), cycle = createGazeCycle();
85
+ let loop = null;
86
+ const release = () => { if (pressed)
87
+ d.controller.release(pressed, 'olhos'); pressed = null; };
88
+ /** Only a change reaches the controller: the old action is let go, the new one pressed, from the eyes. */
89
+ const followPress = (next) => {
90
+ if (next === pressed)
91
+ return;
92
+ if (pressed)
93
+ d.controller.release(pressed, 'olhos');
94
+ if (next)
95
+ d.controller.press(next, 'olhos');
96
+ pressed = next;
97
+ };
98
+ const frame = (ms) => {
99
+ if (!tracker || !feed || !canvas || !feed.ready())
100
+ return;
101
+ const det = tracker.detect(feed.frame, ms);
102
+ const { scores, pose, eye } = readDetection(det);
103
+ const reading = reader(ms, { h: eye?.h, v: eye?.v, pose });
104
+ if (reading.ready && !wasReady)
105
+ d.say(t('sr.eyes.ready'));
106
+ wasReady = reading.ready;
107
+ const out = cycle(ms, cycleFrameOf(reading, scores));
108
+ followPress(out.pressed);
109
+ writeDiagnosis(canvas, reading, out, !!scores, tracker.delegate());
110
+ drawFrame(t, canvas, d.region, reading, out, det, tracker.eyeLines);
111
+ };
112
+ const health = (h, fps) => {
113
+ if (h === 'stalled')
114
+ once('stalled', 'eye control: a second without a camera frame — meanwhile the child\'s gaze commands nothing; keep the tab in front and the camera free', t('sr.eyes.stopped'));
115
+ if (h === 'slow')
116
+ once('slow', `eye control: ${fps} frames a second, under 8 — the reading misses movements and the child may not be answered; close other heavy tabs or use a lighter device`, t('sr.eyes.slow'));
117
+ };
118
+ const stop = () => {
119
+ loop?.stop();
120
+ loop = null;
121
+ release();
122
+ feed?.close();
123
+ feed = null;
124
+ tracker?.close();
125
+ tracker = null;
126
+ canvas?.remove();
127
+ canvas = null;
128
+ running = false;
129
+ wasReady = false;
130
+ };
131
+ /** Nothing started: said, written once, and the 📷 back to off — unless it already moved on, and then nobody is waiting (as the 👄). */
132
+ const failed = (kind, line, spoken) => {
133
+ if (!current())
134
+ return;
135
+ once(kind, line, spoken);
136
+ d.turnOff();
137
+ };
138
+ const start = async () => {
139
+ const opened = await startCameraReader({
140
+ subject: 'eye control', loses: 'play with the eyes',
141
+ spoken: { files: () => t('sr.eyes.needsInternet'), runtime: () => t('sr.eyes.failed'), camera: () => t('sr.eyes.noCamera') },
142
+ }, { load: () => loadTracker({ base: d.base, hasFile: d.hasFile }), openFeed, failed });
143
+ if (!opened)
144
+ return;
145
+ tracker = opened.tracker;
146
+ feed = opened.feed;
147
+ canvas = d.doc.createElement('canvas');
148
+ canvas.className = 'gaze-overlay';
149
+ canvas.setAttribute('aria-hidden', 'true');
150
+ Object.assign(canvas.style, { position: 'absolute', inset: '0', width: '100%', height: '100%', pointerEvents: 'none', zIndex: '5' });
151
+ d.region.appendChild(canvas);
152
+ reader = createGazeReader();
153
+ cycle = createGazeCycle();
154
+ pressed = null;
155
+ loop = createVisionLoop(d.loop, { onFrame: frame, onError: (e) => once('frame', `eye control: a frame failed (${String(e)}) — the loop goes on, but a repeated failure means the child is not being read`, t('sr.eyes.stopped')), onHealth: health });
156
+ running = true;
157
+ d.say(t('sr.eyes.lookMiddle'));
158
+ loop.start();
159
+ };
160
+ return {
161
+ async apply(next) {
162
+ on = next;
163
+ if (starting)
164
+ await starting;
165
+ if (!on) {
166
+ if (running)
167
+ stop();
168
+ return;
169
+ }
170
+ if (running)
171
+ return;
172
+ starting = start().finally(() => { starting = null; });
173
+ await starting;
174
+ if (!current() && running)
175
+ stop(); // turned off while the camera was opening
176
+ },
177
+ };
178
+ }
@@ -0,0 +1,25 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ import type { Translate } from '../core/i18n.js';
3
+ import type { VirtualController } from '../input/virtual-controller.js';
4
+ import { type FaceTrackerLoad, type VisionDeps } from '../platform/vision.js';
5
+ import { type VisionLoopDeps } from '../platform/vision-loop.js';
6
+ import type { CameraFeed } from './eye-control.js';
7
+ import type { SwitchableControl } from './switchable-control.js';
8
+ export interface FaceControlDeps {
9
+ /** Translates in the page's language — the root's translator (ADR-0232 D3). REQUIRED: text built from nowhere is a raw key. */
10
+ t: Translate;
11
+ readonly doc: Document;
12
+ readonly region: HTMLElement;
13
+ readonly base: string;
14
+ /** Whether a file is in the checked cache, handed to the tracker's loader (ADR-0232 D4: the root lends the cache). */
15
+ readonly hasFile: VisionDeps['hasFile'];
16
+ readonly loop: VisionLoopDeps;
17
+ readonly controller: VirtualController;
18
+ readonly say: (text: string) => void;
19
+ readonly alert: (text: string) => void;
20
+ readonly report: (line: string) => void;
21
+ readonly turnOff: () => void;
22
+ readonly openFeed: () => Promise<CameraFeed>;
23
+ readonly loadTracker?: (deps: VisionDeps) => Promise<FaceTrackerLoad>;
24
+ }
25
+ export declare function createFaceControl(d: FaceControlDeps): SwitchableControl;
@@ -0,0 +1,183 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // ui/face-control — PLAYING WITH THE FACE, PUT TOGETHER (ADR-0210 and its erratum, ADR-0212 §3; issue #191).
3
+ //
4
+ // The camera and the face tracker (platform/vision), the frame loop (platform/vision-loop), the Dev's face map (input/face-map), the
5
+ // presses on the virtual controller with the source `rosto` (input/virtual-controller, ADR-0111), and what is drawn over the game. The
6
+ // 📷 at its face position drives it (ADR-0215): it reads the face and draws the eyes, brows and lips — never the camera's picture
7
+ // (ADR-0212); any other position lets the camera go.
8
+ // · The rest measures itself (the Dev, 2026-09-16): the face still, looking at the middle, for 3 s in a row; until then the middle asks for
9
+ // it and nothing commands.
10
+ // · A frame with no face lets go of every held action: a face that left the camera is not holding anything.
11
+ // · What cannot start is said, written once in `problems`, and puts the 📷 back to off — the same promise as the eyes.
12
+ import { faceScoresFromCategories, headTurn, createFaceMapReader, createFaceRest } from '../input/face-map.js';
13
+ import { loadFaceTracker } from '../platform/vision.js';
14
+ import { createVisionLoop } from '../platform/vision-loop.js';
15
+ import { gazeFontPx } from './gaze-overlay.js';
16
+ import { startCameraReader } from './camera-control.js';
17
+ // 📌 THE SHAPE IS THE FAMILY'S (ADR-0221 step 7f): this module declares no `FaceControl` of its own — what it returns IS a
18
+ // `SwitchableControl`, and the compiler sees it.
19
+ const REST_MS = 3000;
20
+ /** The middle's request while the rest is measured, and the face lines, mirrored so they move the way the child moves. */
21
+ function draw(ctx, w, h, restLeftMs, lines, landmarks, say) {
22
+ ctx.clearRect(0, 0, w, h);
23
+ const f = gazeFontPx(w, h);
24
+ if (restLeftMs !== null) {
25
+ ctx.textAlign = 'center';
26
+ ctx.textBaseline = 'middle';
27
+ ctx.fillStyle = '#ffd23f';
28
+ ctx.shadowColor = '#000';
29
+ ctx.shadowBlur = 4;
30
+ ctx.font = `bold ${f}px system-ui`;
31
+ ctx.fillText(say('gaze.lookHere'), w / 2, h / 2 - f * 0.7);
32
+ ctx.font = `${f * 0.85}px system-ui`;
33
+ ctx.fillText(`${(restLeftMs / 1000).toFixed(1)} s`, w / 2, h / 2 + f * 0.8);
34
+ ctx.shadowBlur = 0;
35
+ }
36
+ if (lines && landmarks) {
37
+ ctx.strokeStyle = '#ffffff';
38
+ ctx.lineWidth = Math.max(2, f / 8);
39
+ ctx.shadowColor = '#000';
40
+ ctx.shadowBlur = 3;
41
+ for (const { start, end } of [...lines.eyes, ...lines.brows, ...lines.lips]) {
42
+ const a = landmarks[start], b = landmarks[end];
43
+ if (!a || !b)
44
+ continue;
45
+ ctx.beginPath();
46
+ ctx.moveTo((1 - a.x) * w, a.y * h);
47
+ ctx.lineTo((1 - b.x) * w, b.y * h);
48
+ ctx.stroke();
49
+ }
50
+ ctx.shadowBlur = 0;
51
+ }
52
+ }
53
+ export function createFaceControl(d) {
54
+ const { t } = d;
55
+ const loadTracker = d.loadTracker ?? loadFaceTracker;
56
+ const said = new Set();
57
+ const once = (kind, line, spoken) => { if (!said.has(kind)) {
58
+ said.add(kind);
59
+ d.report(line);
60
+ } d.alert(spoken); };
61
+ let on = false, running = false, starting = null;
62
+ const current = () => on;
63
+ let tracker = null, feed = null, canvas = null;
64
+ let loop = null;
65
+ let rest = null, measure = createFaceRest({ restMs: REST_MS }), read = null;
66
+ const held = new Set();
67
+ const releaseAll = () => { for (const a of held)
68
+ d.controller.release(a, 'rosto'); held.clear(); };
69
+ const frame = (ms) => {
70
+ if (!tracker || !feed || !canvas || !feed.ready())
71
+ return;
72
+ const det = tracker.detect(feed.frame, ms);
73
+ const cats = det?.faceBlendshapes?.[0]?.categories;
74
+ const landmarks = det?.faceLandmarks?.[0];
75
+ const w = canvas.width = d.region.clientWidth, h = canvas.height = d.region.clientHeight;
76
+ const g = canvas.getContext('2d');
77
+ if (!cats) {
78
+ releaseAll();
79
+ draw(g, w, h, rest ? null : REST_MS, null, undefined, t);
80
+ return;
81
+ }
82
+ const scores = faceScoresFromCategories(cats);
83
+ const restLeft = measureRest(ms, scores, landmarks);
84
+ if (read)
85
+ holdExactly(new Set(read(ms, scores, landmarks).held)); // nothing commands before the rest: no reader yet
86
+ draw(g, w, h, restLeft, tracker.faceLines, landmarks, t);
87
+ };
88
+ /**
89
+ * Until the rest is measured, measure it on this frame — the head counts too — and answer how long the middle is still asked
90
+ * for. Once it is measured: the reader is made, «ready» is said ONCE, and the answer is `null` from then on.
91
+ */
92
+ const measureRest = (ms, scores, landmarks) => {
93
+ if (rest)
94
+ return null;
95
+ const m = measure(ms, scores, headTurn(landmarks).x);
96
+ if (!m.rest)
97
+ return Math.max(0, REST_MS - m.stillMs);
98
+ rest = m.rest;
99
+ read = createFaceMapReader({ rest });
100
+ d.say(t('sr.face.ready'));
101
+ return null;
102
+ };
103
+ /** Hold exactly what the reading holds now: let go of what left it, press what joined it — once. */
104
+ const holdExactly = (now) => {
105
+ for (const a of held)
106
+ if (!now.has(a)) {
107
+ d.controller.release(a, 'rosto');
108
+ held.delete(a);
109
+ }
110
+ for (const a of now)
111
+ if (!held.has(a)) {
112
+ d.controller.press(a, 'rosto');
113
+ held.add(a);
114
+ }
115
+ };
116
+ const health = (hh, fps) => {
117
+ if (hh === 'stalled')
118
+ once('stalled', 'face control: a second without a camera frame — meanwhile the child\'s face commands nothing; keep the tab in front and the camera free', t('sr.face.stopped'));
119
+ if (hh === 'slow')
120
+ once('slow', `face control: ${fps} frames a second, under 8 — expressions are missed and the child may not be answered; close other heavy tabs or use a lighter device`, t('sr.face.slow'));
121
+ };
122
+ const stop = () => {
123
+ loop?.stop();
124
+ loop = null;
125
+ releaseAll();
126
+ feed?.close();
127
+ feed = null;
128
+ tracker?.close();
129
+ tracker = null;
130
+ canvas?.remove();
131
+ canvas = null;
132
+ running = false;
133
+ rest = null;
134
+ read = null;
135
+ };
136
+ /** Nothing started: said, written once, and the 📷 back to off — unless it already moved on, and then nobody is waiting (as the 👄). */
137
+ const failed = (kind, line, spoken) => {
138
+ if (!current())
139
+ return;
140
+ once(kind, line, spoken);
141
+ d.turnOff();
142
+ };
143
+ const start = async () => {
144
+ const opened = await startCameraReader({
145
+ subject: 'face control', loses: 'play with the face',
146
+ spoken: { files: () => t('sr.face.needsInternet'), runtime: () => t('sr.face.failed'), camera: () => t('sr.face.noCamera') },
147
+ }, { load: () => loadTracker({ base: d.base, hasFile: d.hasFile }), openFeed: d.openFeed, failed });
148
+ if (!opened)
149
+ return;
150
+ tracker = opened.tracker;
151
+ feed = opened.feed;
152
+ canvas = d.doc.createElement('canvas');
153
+ canvas.className = 'face-overlay';
154
+ canvas.setAttribute('aria-hidden', 'true');
155
+ Object.assign(canvas.style, { position: 'absolute', inset: '0', width: '100%', height: '100%', pointerEvents: 'none', zIndex: '5' });
156
+ d.region.appendChild(canvas);
157
+ rest = null;
158
+ read = null;
159
+ measure = createFaceRest({ restMs: REST_MS });
160
+ loop = createVisionLoop(d.loop, { onFrame: frame, onError: (e) => once('frame', `face control: a frame failed (${String(e)}) — the loop goes on, but a repeated failure means the child is not being read`, t('sr.face.stopped')), onHealth: health });
161
+ running = true;
162
+ d.say(t('sr.face.lookMiddle'));
163
+ loop.start();
164
+ };
165
+ return {
166
+ async apply(next) {
167
+ on = next;
168
+ if (starting)
169
+ await starting;
170
+ if (!on) {
171
+ if (running)
172
+ stop();
173
+ return;
174
+ }
175
+ if (running)
176
+ return;
177
+ starting = start().finally(() => { starting = null; });
178
+ await starting;
179
+ if (!current() && running)
180
+ stop();
181
+ },
182
+ };
183
+ }