@the-inclusionist/engine 10.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 (528) hide show
  1. package/README.md +32 -1
  2. package/app/css/style.css +216 -42
  3. package/app/public/vendor/fonts-licences/NOTICE.txt +166 -0
  4. package/app/public/vendor/fonts-licences/OFL-1.1.txt +86 -0
  5. package/app/public/vendor/fonts.css +11 -1175
  6. package/dist-pkg/boot/create-game.d.ts +84 -33
  7. package/dist-pkg/boot/create-game.js +708 -307
  8. package/dist-pkg/core/a11y-sr.d.ts +1 -1
  9. package/dist-pkg/core/a11y-sr.js +3 -5
  10. package/dist-pkg/core/accommodations.d.ts +26 -8
  11. package/dist-pkg/core/accommodations.js +27 -10
  12. package/dist-pkg/core/actions.d.ts +39 -10
  13. package/dist-pkg/core/actions.js +28 -6
  14. package/dist-pkg/core/contract.d.ts +0 -2
  15. package/dist-pkg/core/contract.js +0 -2
  16. package/dist-pkg/core/i18n.d.ts +36 -71
  17. package/dist-pkg/core/i18n.js +131 -198
  18. package/dist-pkg/core/session-clock.d.ts +23 -0
  19. package/dist-pkg/core/session-clock.js +37 -0
  20. package/dist-pkg/i18n/en.js +44 -49
  21. package/dist-pkg/i18n/es.js +44 -49
  22. package/dist-pkg/i18n/pt.js +49 -54
  23. package/dist-pkg/input/gamepad.d.ts +42 -11
  24. package/dist-pkg/input/gamepad.js +50 -15
  25. package/dist-pkg/input/key-default.d.ts +30 -0
  26. package/dist-pkg/input/key-default.js +60 -0
  27. package/dist-pkg/input/keyboard-runtime.d.ts +0 -4
  28. package/dist-pkg/input/keyboard.d.ts +0 -1
  29. package/dist-pkg/input/keydown.d.ts +5 -11
  30. package/dist-pkg/input/keydown.js +3 -3
  31. package/dist-pkg/input/latch-edge.d.ts +10 -2
  32. package/dist-pkg/input/latch-edge.js +4 -3
  33. package/dist-pkg/input/latch-scope.d.ts +27 -22
  34. package/dist-pkg/input/latch-scope.js +25 -37
  35. package/dist-pkg/input/latch-store.d.ts +9 -12
  36. package/dist-pkg/input/latch-store.js +13 -18
  37. package/dist-pkg/input/latch-sync.d.ts +5 -4
  38. package/dist-pkg/input/latch-sync.js +5 -5
  39. package/dist-pkg/input/pad-reading.js +7 -2
  40. package/dist-pkg/input/pad-wizard.d.ts +4 -1
  41. package/dist-pkg/input/pad-wizard.js +60 -9
  42. package/dist-pkg/input/switch-scan.d.ts +29 -12
  43. package/dist-pkg/input/switch-scan.js +15 -2
  44. package/dist-pkg/input/touch-bindings.d.ts +3 -6
  45. package/dist-pkg/input/touch.d.ts +17 -2
  46. package/dist-pkg/input/touch.js +55 -17
  47. package/dist-pkg/input/transport-in-use.d.ts +5 -5
  48. package/dist-pkg/input/transport-in-use.js +5 -5
  49. package/dist-pkg/input/transports.d.ts +0 -9
  50. package/dist-pkg/input/transports.js +1 -1
  51. package/dist-pkg/input/virtual-controller.d.ts +39 -3
  52. package/dist-pkg/input/virtual-controller.js +32 -4
  53. package/dist-pkg/input/voice-map.d.ts +27 -11
  54. package/dist-pkg/input/voice-map.js +41 -43
  55. package/dist-pkg/platform/audio-ambient.d.ts +0 -1
  56. package/dist-pkg/platform/audio-ambient.js +3 -1
  57. package/dist-pkg/platform/audio-mixer.js +4 -9
  58. package/dist-pkg/platform/audio-sonar.d.ts +8 -94
  59. package/dist-pkg/platform/audio-sonar.js +22 -207
  60. package/dist-pkg/platform/audio.d.ts +10 -1
  61. package/dist-pkg/platform/audio.js +35 -2
  62. package/dist-pkg/platform/choose-by-voice.d.ts +70 -0
  63. package/dist-pkg/platform/choose-by-voice.js +157 -0
  64. package/dist-pkg/platform/font-library.d.ts +77 -0
  65. package/dist-pkg/platform/font-library.js +98 -0
  66. package/dist-pkg/platform/font-library.json +721 -0
  67. package/dist-pkg/platform/heavy-catalogue.d.ts +46 -0
  68. package/dist-pkg/platform/heavy-catalogue.js +47 -14
  69. package/dist-pkg/platform/heavy-mirror.d.ts +3 -9
  70. package/dist-pkg/platform/heavy-mirror.js +14 -17
  71. package/dist-pkg/platform/heavy.d.ts +40 -8
  72. package/dist-pkg/platform/heavy.js +177 -22
  73. package/dist-pkg/platform/interruptible-speech.d.ts +15 -8
  74. package/dist-pkg/platform/interruptible-speech.js +37 -13
  75. package/dist-pkg/platform/listener-scope.js +22 -13
  76. package/dist-pkg/platform/locale-host.d.ts +7 -3
  77. package/dist-pkg/platform/locale-host.js +13 -2
  78. package/dist-pkg/platform/reading-in-worker.d.ts +24 -0
  79. package/dist-pkg/platform/reading-in-worker.js +38 -0
  80. package/dist-pkg/platform/reading-model.d.ts +21 -0
  81. package/dist-pkg/platform/reading-model.js +25 -0
  82. package/dist-pkg/platform/reading-runtime.js +13 -8
  83. package/dist-pkg/platform/reading.d.ts +12 -1
  84. package/dist-pkg/platform/reading.js +9 -1
  85. package/dist-pkg/platform/speech-recognition.d.ts +10 -1
  86. package/dist-pkg/platform/speech-recognition.js +29 -9
  87. package/dist-pkg/platform/speech.d.ts +10 -1
  88. package/dist-pkg/platform/speech.js +24 -13
  89. package/dist-pkg/platform/tts.d.ts +37 -4
  90. package/dist-pkg/platform/tts.js +279 -57
  91. package/dist-pkg/platform/voice-listener.d.ts +6 -2
  92. package/dist-pkg/platform/voice-listener.js +1 -1
  93. package/dist-pkg/platform/voice-plan.d.ts +13 -0
  94. package/dist-pkg/platform/voice-plan.js +57 -2
  95. package/dist-pkg/platform/vosk-runtime.d.ts +25 -0
  96. package/dist-pkg/platform/vosk-runtime.js +59 -1
  97. package/dist-pkg/platform/vosk-vocabulary.d.ts +6 -0
  98. package/dist-pkg/platform/vosk-vocabulary.js +178 -0
  99. package/dist-pkg/render/crt.d.ts +11 -9
  100. package/dist-pkg/render/crt.js +6 -10
  101. package/dist-pkg/render/cvd-matrices.d.ts +5 -1
  102. package/dist-pkg/render/cvd-matrices.js +5 -1
  103. package/dist-pkg/render/high-contrast.d.ts +1 -2
  104. package/dist-pkg/render/high-contrast.js +5 -1
  105. package/dist-pkg/render/viz-axes.d.ts +9 -0
  106. package/dist-pkg/render/viz-axes.js +15 -2
  107. package/dist-pkg/ui/aac-sets.d.ts +29 -24
  108. package/dist-pkg/ui/aac-sets.js +22 -30
  109. package/dist-pkg/ui/audio-choices.d.ts +10 -5
  110. package/dist-pkg/ui/audio-choices.js +14 -10
  111. package/dist-pkg/ui/camera-control.d.ts +47 -0
  112. package/dist-pkg/ui/camera-control.js +35 -1
  113. package/dist-pkg/ui/declared-words.d.ts +21 -0
  114. package/dist-pkg/ui/declared-words.js +66 -0
  115. package/dist-pkg/ui/drawing-problems.js +4 -3
  116. package/dist-pkg/ui/eye-control.js +17 -17
  117. package/dist-pkg/ui/face-control.js +15 -15
  118. package/dist-pkg/ui/fonts.d.ts +42 -8
  119. package/dist-pkg/ui/fonts.js +58 -24
  120. package/dist-pkg/ui/footer-scroll-driver.d.ts +14 -0
  121. package/dist-pkg/ui/footer-scroll-driver.js +201 -0
  122. package/dist-pkg/ui/footer-scroll.d.ts +38 -0
  123. package/dist-pkg/ui/footer-scroll.js +55 -0
  124. package/dist-pkg/ui/game-options.d.ts +14 -9
  125. package/dist-pkg/ui/game-options.js +41 -21
  126. package/dist-pkg/ui/hand-control.js +15 -15
  127. package/dist-pkg/ui/help-panel.d.ts +19 -8
  128. package/dist-pkg/ui/help-panel.js +15 -5
  129. package/dist-pkg/ui/hud-bands.d.ts +29 -12
  130. package/dist-pkg/ui/hud-bands.js +63 -25
  131. package/dist-pkg/ui/hud-row.d.ts +34 -0
  132. package/dist-pkg/ui/hud-row.js +80 -0
  133. package/dist-pkg/ui/hud.d.ts +0 -3
  134. package/dist-pkg/ui/layout.js +5 -2
  135. package/dist-pkg/ui/libras-avatar-clip.d.ts +30 -0
  136. package/dist-pkg/ui/libras-avatar-clip.js +86 -0
  137. package/dist-pkg/ui/libras-avatar-load.d.ts +52 -0
  138. package/dist-pkg/ui/libras-avatar-load.js +134 -0
  139. package/dist-pkg/ui/libras-avatar-plan.d.ts +102 -0
  140. package/dist-pkg/ui/libras-avatar-plan.js +211 -0
  141. package/dist-pkg/ui/libras-avatar-player.d.ts +28 -0
  142. package/dist-pkg/ui/libras-avatar-player.js +226 -0
  143. package/dist-pkg/ui/libras-avatar-stage.d.ts +37 -0
  144. package/dist-pkg/ui/libras-avatar-stage.js +208 -0
  145. package/dist-pkg/ui/libras-glosses.d.ts +28 -0
  146. package/dist-pkg/ui/libras-glosses.js +161 -0
  147. package/dist-pkg/ui/menu-intent.d.ts +29 -0
  148. package/dist-pkg/ui/menu-intent.js +35 -1
  149. package/dist-pkg/ui/menu-items.d.ts +10 -0
  150. package/dist-pkg/ui/menu-items.js +29 -0
  151. package/dist-pkg/ui/menu-nav.d.ts +40 -4
  152. package/dist-pkg/ui/menu-nav.js +132 -47
  153. package/dist-pkg/ui/panel-widgets.d.ts +34 -0
  154. package/dist-pkg/ui/panel-widgets.js +86 -0
  155. package/dist-pkg/ui/pause-icons.d.ts +30 -14
  156. package/dist-pkg/ui/pause-icons.js +106 -45
  157. package/dist-pkg/ui/scan-overlay.d.ts +3 -3
  158. package/dist-pkg/ui/scan-overlay.js +11 -3
  159. package/dist-pkg/ui/screen-text.d.ts +25 -0
  160. package/dist-pkg/ui/screen-text.js +152 -0
  161. package/dist-pkg/ui/session-clock.d.ts +32 -0
  162. package/dist-pkg/ui/session-clock.js +67 -0
  163. package/dist-pkg/ui/settings-aac.d.ts +3 -1
  164. package/dist-pkg/ui/settings-aac.js +32 -13
  165. package/dist-pkg/ui/settings-audio.d.ts +10 -5
  166. package/dist-pkg/ui/settings-audio.js +101 -35
  167. package/dist-pkg/ui/settings-controls.d.ts +0 -4
  168. package/dist-pkg/ui/settings-mobility.d.ts +3 -6
  169. package/dist-pkg/ui/settings-mobility.js +4 -46
  170. package/dist-pkg/ui/settings-motion.js +2 -2
  171. package/dist-pkg/ui/settings-panel.d.ts +10 -0
  172. package/dist-pkg/ui/settings-panel.js +25 -1
  173. package/dist-pkg/ui/settings-typo.d.ts +5 -6
  174. package/dist-pkg/ui/settings-typo.js +20 -11
  175. package/dist-pkg/ui/settings-visual.js +2 -2
  176. package/dist-pkg/ui/shell.d.ts +1 -3
  177. package/dist-pkg/ui/shell.js +8 -4
  178. package/dist-pkg/ui/title.d.ts +2 -11
  179. package/dist-pkg/ui/title.js +39 -6
  180. package/dist-pkg/ui/top-band.d.ts +5 -3
  181. package/dist-pkg/ui/top-band.js +21 -25
  182. package/dist-pkg/ui/typo-choices.d.ts +3 -3
  183. package/dist-pkg/ui/typo-choices.js +9 -8
  184. package/dist-pkg/ui/vlibras.d.ts +71 -27
  185. package/dist-pkg/ui/vlibras.js +116 -44
  186. package/dist-pkg/ui/voice-control.d.ts +23 -1
  187. package/dist-pkg/ui/voice-control.js +149 -16
  188. package/dist-pkg/ui/voice-settings.d.ts +2 -3
  189. package/dist-pkg/ui/voice-settings.js +53 -3
  190. package/dist-pkg/ui/where-the-child-is.js +2 -2
  191. package/docs/CREDITS.md +122 -10
  192. package/docs/LICENSES.md +36 -7
  193. package/package.json +34 -9
  194. package/scripts/check-cartridge.mjs +78 -0
  195. package/scripts/game-build.d.mts +25 -0
  196. package/scripts/game-build.mjs +144 -0
  197. package/scripts/heavy-into-the-delivery.mjs +284 -40
  198. package/scripts/libras-avatar.json +680 -0
  199. package/scripts/libras-avatar.mjs +246 -0
  200. package/scripts/libras-glosses/gloss.py +95 -0
  201. package/scripts/libras-glosses/pyproject.toml +32 -0
  202. package/scripts/libras-glosses/uv.lock +781 -0
  203. package/scripts/libras-glosses.mjs +351 -0
  204. package/scripts/licences/OFL-1.1.txt +86 -0
  205. package/scripts/licences/UFL-1.0.txt +96 -0
  206. package/scripts/licences/fonts.mjs +389 -0
  207. package/scripts/licences/third-party.mjs +35 -21
  208. package/app/public/vendor/fonts/bitter-var-ext.woff2 +0 -0
  209. package/app/public/vendor/fonts/bitter-var.woff2 +0 -0
  210. package/app/public/vendor/fonts/bodonimoda-var-ext.woff2 +0 -0
  211. package/app/public/vendor/fonts/bodonimoda-var.woff2 +0 -0
  212. package/app/public/vendor/fonts/cat-abril_fatface-400-ext.woff2 +0 -0
  213. package/app/public/vendor/fonts/cat-abril_fatface-400.woff2 +0 -0
  214. package/app/public/vendor/fonts/cat-adlam_display-400-ext.woff2 +0 -0
  215. package/app/public/vendor/fonts/cat-adlam_display-400.woff2 +0 -0
  216. package/app/public/vendor/fonts/cat-agu_display-400-ext.woff2 +0 -0
  217. package/app/public/vendor/fonts/cat-agu_display-400.woff2 +0 -0
  218. package/app/public/vendor/fonts/cat-alfa_slab_one-400-ext.woff2 +0 -0
  219. package/app/public/vendor/fonts/cat-alfa_slab_one-400.woff2 +0 -0
  220. package/app/public/vendor/fonts/cat-are_you_serious-400-ext.woff2 +0 -0
  221. package/app/public/vendor/fonts/cat-are_you_serious-400.woff2 +0 -0
  222. package/app/public/vendor/fonts/cat-asimovian-400-ext.woff2 +0 -0
  223. package/app/public/vendor/fonts/cat-asimovian-400.woff2 +0 -0
  224. package/app/public/vendor/fonts/cat-atkinson_hyperlegible_next-400-ext.woff2 +0 -0
  225. package/app/public/vendor/fonts/cat-atkinson_hyperlegible_next-400.woff2 +0 -0
  226. package/app/public/vendor/fonts/cat-bakbak_one-400-ext.woff2 +0 -0
  227. package/app/public/vendor/fonts/cat-bakbak_one-400.woff2 +0 -0
  228. package/app/public/vendor/fonts/cat-bangers-400-ext.woff2 +0 -0
  229. package/app/public/vendor/fonts/cat-bangers-400.woff2 +0 -0
  230. package/app/public/vendor/fonts/cat-barriecito-400-ext.woff2 +0 -0
  231. package/app/public/vendor/fonts/cat-barriecito-400.woff2 +0 -0
  232. package/app/public/vendor/fonts/cat-berkshire_swash-400-ext.woff2 +0 -0
  233. package/app/public/vendor/fonts/cat-berkshire_swash-400.woff2 +0 -0
  234. package/app/public/vendor/fonts/cat-bevan-400-ext.woff2 +0 -0
  235. package/app/public/vendor/fonts/cat-bevan-400.woff2 +0 -0
  236. package/app/public/vendor/fonts/cat-bitcount-400-ext.woff2 +0 -0
  237. package/app/public/vendor/fonts/cat-bitcount-400.woff2 +0 -0
  238. package/app/public/vendor/fonts/cat-bowlby_one_sc-400-ext.woff2 +0 -0
  239. package/app/public/vendor/fonts/cat-bowlby_one_sc-400.woff2 +0 -0
  240. package/app/public/vendor/fonts/cat-bungee_shade-400-ext.woff2 +0 -0
  241. package/app/public/vendor/fonts/cat-bungee_shade-400.woff2 +0 -0
  242. package/app/public/vendor/fonts/cat-charm-400-ext.woff2 +0 -0
  243. package/app/public/vendor/fonts/cat-charm-400.woff2 +0 -0
  244. package/app/public/vendor/fonts/cat-charm-700-ext.woff2 +0 -0
  245. package/app/public/vendor/fonts/cat-charm-700.woff2 +0 -0
  246. package/app/public/vendor/fonts/cat-cherry_bomb_one-400-ext.woff2 +0 -0
  247. package/app/public/vendor/fonts/cat-cherry_bomb_one-400.woff2 +0 -0
  248. package/app/public/vendor/fonts/cat-chokokutai-400-ext.woff2 +0 -0
  249. package/app/public/vendor/fonts/cat-chokokutai-400.woff2 +0 -0
  250. package/app/public/vendor/fonts/cat-chonburi-400-ext.woff2 +0 -0
  251. package/app/public/vendor/fonts/cat-chonburi-400.woff2 +0 -0
  252. package/app/public/vendor/fonts/cat-climate_crisis-400-ext.woff2 +0 -0
  253. package/app/public/vendor/fonts/cat-climate_crisis-400.woff2 +0 -0
  254. package/app/public/vendor/fonts/cat-cookie-400.woff2 +0 -0
  255. package/app/public/vendor/fonts/cat-dela_gothic_one-400-ext.woff2 +0 -0
  256. package/app/public/vendor/fonts/cat-dela_gothic_one-400.woff2 +0 -0
  257. package/app/public/vendor/fonts/cat-fascinate_inline-400-ext.woff2 +0 -0
  258. package/app/public/vendor/fonts/cat-fascinate_inline-400.woff2 +0 -0
  259. package/app/public/vendor/fonts/cat-ga_maamli-400-ext.woff2 +0 -0
  260. package/app/public/vendor/fonts/cat-ga_maamli-400.woff2 +0 -0
  261. package/app/public/vendor/fonts/cat-itim-400-ext.woff2 +0 -0
  262. package/app/public/vendor/fonts/cat-itim-400.woff2 +0 -0
  263. package/app/public/vendor/fonts/cat-jacquard_24-400-ext.woff2 +0 -0
  264. package/app/public/vendor/fonts/cat-jacquard_24-400.woff2 +0 -0
  265. package/app/public/vendor/fonts/cat-kablammo-400-ext.woff2 +0 -0
  266. package/app/public/vendor/fonts/cat-kablammo-400.woff2 +0 -0
  267. package/app/public/vendor/fonts/cat-kalam-400-ext.woff2 +0 -0
  268. package/app/public/vendor/fonts/cat-kalam-400.woff2 +0 -0
  269. package/app/public/vendor/fonts/cat-kalam-700-ext.woff2 +0 -0
  270. package/app/public/vendor/fonts/cat-kalam-700.woff2 +0 -0
  271. package/app/public/vendor/fonts/cat-katibeh-400-ext.woff2 +0 -0
  272. package/app/public/vendor/fonts/cat-katibeh-400.woff2 +0 -0
  273. package/app/public/vendor/fonts/cat-lemonada-400-ext.woff2 +0 -0
  274. package/app/public/vendor/fonts/cat-lemonada-400.woff2 +0 -0
  275. package/app/public/vendor/fonts/cat-lilita_one-400-ext.woff2 +0 -0
  276. package/app/public/vendor/fonts/cat-lilita_one-400.woff2 +0 -0
  277. package/app/public/vendor/fonts/cat-lobster-400-ext.woff2 +0 -0
  278. package/app/public/vendor/fonts/cat-lobster-400.woff2 +0 -0
  279. package/app/public/vendor/fonts/cat-luckiest_guy-400-ext.woff2 +0 -0
  280. package/app/public/vendor/fonts/cat-luckiest_guy-400.woff2 +0 -0
  281. package/app/public/vendor/fonts/cat-marhey-400-ext.woff2 +0 -0
  282. package/app/public/vendor/fonts/cat-marhey-400.woff2 +0 -0
  283. package/app/public/vendor/fonts/cat-modak-400-ext.woff2 +0 -0
  284. package/app/public/vendor/fonts/cat-modak-400.woff2 +0 -0
  285. package/app/public/vendor/fonts/cat-momo_signature-400-ext.woff2 +0 -0
  286. package/app/public/vendor/fonts/cat-momo_signature-400.woff2 +0 -0
  287. package/app/public/vendor/fonts/cat-monofett-400-ext.woff2 +0 -0
  288. package/app/public/vendor/fonts/cat-monofett-400.woff2 +0 -0
  289. package/app/public/vendor/fonts/cat-monomakh-400-ext.woff2 +0 -0
  290. package/app/public/vendor/fonts/cat-monomakh-400.woff2 +0 -0
  291. package/app/public/vendor/fonts/cat-monoton-400-ext.woff2 +0 -0
  292. package/app/public/vendor/fonts/cat-monoton-400.woff2 +0 -0
  293. package/app/public/vendor/fonts/cat-nerko_one-400-ext.woff2 +0 -0
  294. package/app/public/vendor/fonts/cat-nerko_one-400.woff2 +0 -0
  295. package/app/public/vendor/fonts/cat-noto_sans_adlam-400-ext.woff2 +0 -0
  296. package/app/public/vendor/fonts/cat-noto_sans_adlam-400.woff2 +0 -0
  297. package/app/public/vendor/fonts/cat-noto_sans_arabic-400-ext.woff2 +0 -0
  298. package/app/public/vendor/fonts/cat-noto_sans_arabic-400.woff2 +0 -0
  299. package/app/public/vendor/fonts/cat-noto_sans_devanagari-400-ext.woff2 +0 -0
  300. package/app/public/vendor/fonts/cat-noto_sans_devanagari-400.woff2 +0 -0
  301. package/app/public/vendor/fonts/cat-noto_sans_hebrew-400-ext.woff2 +0 -0
  302. package/app/public/vendor/fonts/cat-noto_sans_hebrew-400.woff2 +0 -0
  303. package/app/public/vendor/fonts/cat-noto_sans_jp-400-ext.woff2 +0 -0
  304. package/app/public/vendor/fonts/cat-noto_sans_jp-400.woff2 +0 -0
  305. package/app/public/vendor/fonts/cat-noto_sans_khmer-400-ext.woff2 +0 -0
  306. package/app/public/vendor/fonts/cat-noto_sans_khmer-400.woff2 +0 -0
  307. package/app/public/vendor/fonts/cat-noto_sans_kr-400-ext.woff2 +0 -0
  308. package/app/public/vendor/fonts/cat-noto_sans_kr-400.woff2 +0 -0
  309. package/app/public/vendor/fonts/cat-noto_sans_sc-400-ext.woff2 +0 -0
  310. package/app/public/vendor/fonts/cat-noto_sans_sc-400.woff2 +0 -0
  311. package/app/public/vendor/fonts/cat-noto_sans_thai-400-ext.woff2 +0 -0
  312. package/app/public/vendor/fonts/cat-noto_sans_thai-400.woff2 +0 -0
  313. package/app/public/vendor/fonts/cat-ole-400-ext.woff2 +0 -0
  314. package/app/public/vendor/fonts/cat-ole-400.woff2 +0 -0
  315. package/app/public/vendor/fonts/cat-pacifico-400-ext.woff2 +0 -0
  316. package/app/public/vendor/fonts/cat-pacifico-400.woff2 +0 -0
  317. package/app/public/vendor/fonts/cat-patrick_hand-400-ext.woff2 +0 -0
  318. package/app/public/vendor/fonts/cat-patrick_hand-400.woff2 +0 -0
  319. package/app/public/vendor/fonts/cat-petit_formal_script-400-ext.woff2 +0 -0
  320. package/app/public/vendor/fonts/cat-petit_formal_script-400.woff2 +0 -0
  321. package/app/public/vendor/fonts/cat-pixelify_sans-400-ext.woff2 +0 -0
  322. package/app/public/vendor/fonts/cat-pixelify_sans-400.woff2 +0 -0
  323. package/app/public/vendor/fonts/cat-plaster-400-ext.woff2 +0 -0
  324. package/app/public/vendor/fonts/cat-plaster-400.woff2 +0 -0
  325. package/app/public/vendor/fonts/cat-playwrite_ar_guides-400-0.woff2 +0 -0
  326. package/app/public/vendor/fonts/cat-playwrite_at-400-0.woff2 +0 -0
  327. package/app/public/vendor/fonts/cat-playwrite_at_guides-400-0.woff2 +0 -0
  328. package/app/public/vendor/fonts/cat-playwrite_au_nsw-400-0.woff2 +0 -0
  329. package/app/public/vendor/fonts/cat-playwrite_au_nsw_guides-400-0.woff2 +0 -0
  330. package/app/public/vendor/fonts/cat-playwrite_au_qld-400-0.woff2 +0 -0
  331. package/app/public/vendor/fonts/cat-playwrite_au_qld_guides-400-0.woff2 +0 -0
  332. package/app/public/vendor/fonts/cat-playwrite_au_sa-400-0.woff2 +0 -0
  333. package/app/public/vendor/fonts/cat-playwrite_au_sa_guides-400-0.woff2 +0 -0
  334. package/app/public/vendor/fonts/cat-playwrite_au_tas-400-0.woff2 +0 -0
  335. package/app/public/vendor/fonts/cat-playwrite_au_tas_guides-400-0.woff2 +0 -0
  336. package/app/public/vendor/fonts/cat-playwrite_au_vic-400-0.woff2 +0 -0
  337. package/app/public/vendor/fonts/cat-playwrite_au_vic_guides-400-0.woff2 +0 -0
  338. package/app/public/vendor/fonts/cat-playwrite_be_vlg-400-0.woff2 +0 -0
  339. package/app/public/vendor/fonts/cat-playwrite_be_vlg_guides-400-0.woff2 +0 -0
  340. package/app/public/vendor/fonts/cat-playwrite_be_wal-400-0.woff2 +0 -0
  341. package/app/public/vendor/fonts/cat-playwrite_be_wal_guides-400-0.woff2 +0 -0
  342. package/app/public/vendor/fonts/cat-playwrite_br_guides-400-0.woff2 +0 -0
  343. package/app/public/vendor/fonts/cat-playwrite_ca_guides-400-0.woff2 +0 -0
  344. package/app/public/vendor/fonts/cat-playwrite_cl_guides-400-0.woff2 +0 -0
  345. package/app/public/vendor/fonts/cat-playwrite_co_guides-400-0.woff2 +0 -0
  346. package/app/public/vendor/fonts/cat-playwrite_cu_guides-400-0.woff2 +0 -0
  347. package/app/public/vendor/fonts/cat-playwrite_cz-400-0.woff2 +0 -0
  348. package/app/public/vendor/fonts/cat-playwrite_cz_guides-400-0.woff2 +0 -0
  349. package/app/public/vendor/fonts/cat-playwrite_de_grund-400-0.woff2 +0 -0
  350. package/app/public/vendor/fonts/cat-playwrite_de_grund_guides-400-0.woff2 +0 -0
  351. package/app/public/vendor/fonts/cat-playwrite_de_la-400-0.woff2 +0 -0
  352. package/app/public/vendor/fonts/cat-playwrite_de_la_guides-400-0.woff2 +0 -0
  353. package/app/public/vendor/fonts/cat-playwrite_de_sas-400-0.woff2 +0 -0
  354. package/app/public/vendor/fonts/cat-playwrite_de_sas_guides-400-0.woff2 +0 -0
  355. package/app/public/vendor/fonts/cat-playwrite_de_va-400-0.woff2 +0 -0
  356. package/app/public/vendor/fonts/cat-playwrite_de_va_guides-400-0.woff2 +0 -0
  357. package/app/public/vendor/fonts/cat-playwrite_dk_loopet-400-0.woff2 +0 -0
  358. package/app/public/vendor/fonts/cat-playwrite_dk_loopet_guides-400-0.woff2 +0 -0
  359. package/app/public/vendor/fonts/cat-playwrite_dk_uloopet-400-0.woff2 +0 -0
  360. package/app/public/vendor/fonts/cat-playwrite_dk_uloopet_guides-400-0.woff2 +0 -0
  361. package/app/public/vendor/fonts/cat-playwrite_es_deco_guides-400-0.woff2 +0 -0
  362. package/app/public/vendor/fonts/cat-playwrite_es_guides-400-0.woff2 +0 -0
  363. package/app/public/vendor/fonts/cat-playwrite_fr_moderne-400-0.woff2 +0 -0
  364. package/app/public/vendor/fonts/cat-playwrite_fr_moderne_guides-400-0.woff2 +0 -0
  365. package/app/public/vendor/fonts/cat-playwrite_fr_trad-400-0.woff2 +0 -0
  366. package/app/public/vendor/fonts/cat-playwrite_fr_trad_guides-400-0.woff2 +0 -0
  367. package/app/public/vendor/fonts/cat-playwrite_gb_j_guides-400-0.woff2 +0 -0
  368. package/app/public/vendor/fonts/cat-playwrite_gb_s_guides-400-0.woff2 +0 -0
  369. package/app/public/vendor/fonts/cat-playwrite_hr-400-0.woff2 +0 -0
  370. package/app/public/vendor/fonts/cat-playwrite_hr_guides-400-0.woff2 +0 -0
  371. package/app/public/vendor/fonts/cat-playwrite_hr_lijeva-400-0.woff2 +0 -0
  372. package/app/public/vendor/fonts/cat-playwrite_hr_lijeva_guides-400-0.woff2 +0 -0
  373. package/app/public/vendor/fonts/cat-playwrite_hu-400-0.woff2 +0 -0
  374. package/app/public/vendor/fonts/cat-playwrite_hu_guides-400-0.woff2 +0 -0
  375. package/app/public/vendor/fonts/cat-playwrite_id-400-0.woff2 +0 -0
  376. package/app/public/vendor/fonts/cat-playwrite_id_guides-400-0.woff2 +0 -0
  377. package/app/public/vendor/fonts/cat-playwrite_ie-400-0.woff2 +0 -0
  378. package/app/public/vendor/fonts/cat-playwrite_ie_guides-400-0.woff2 +0 -0
  379. package/app/public/vendor/fonts/cat-playwrite_in-400-0.woff2 +0 -0
  380. package/app/public/vendor/fonts/cat-playwrite_in_guides-400-0.woff2 +0 -0
  381. package/app/public/vendor/fonts/cat-playwrite_is-400-0.woff2 +0 -0
  382. package/app/public/vendor/fonts/cat-playwrite_is_guides-400-0.woff2 +0 -0
  383. package/app/public/vendor/fonts/cat-playwrite_it_moderna-400-0.woff2 +0 -0
  384. package/app/public/vendor/fonts/cat-playwrite_it_moderna_guides-400-0.woff2 +0 -0
  385. package/app/public/vendor/fonts/cat-playwrite_it_trad-400-0.woff2 +0 -0
  386. package/app/public/vendor/fonts/cat-playwrite_it_trad_guides-400-0.woff2 +0 -0
  387. package/app/public/vendor/fonts/cat-playwrite_mx_guides-400-0.woff2 +0 -0
  388. package/app/public/vendor/fonts/cat-playwrite_ng_modern-400-0.woff2 +0 -0
  389. package/app/public/vendor/fonts/cat-playwrite_ng_modern_guides-400-0.woff2 +0 -0
  390. package/app/public/vendor/fonts/cat-playwrite_nl-400-0.woff2 +0 -0
  391. package/app/public/vendor/fonts/cat-playwrite_nl_guides-400-0.woff2 +0 -0
  392. package/app/public/vendor/fonts/cat-playwrite_no-400-0.woff2 +0 -0
  393. package/app/public/vendor/fonts/cat-playwrite_no_guides-400-0.woff2 +0 -0
  394. package/app/public/vendor/fonts/cat-playwrite_nz-400-0.woff2 +0 -0
  395. package/app/public/vendor/fonts/cat-playwrite_nz_basic-400-0.woff2 +0 -0
  396. package/app/public/vendor/fonts/cat-playwrite_nz_basic_guides-400-0.woff2 +0 -0
  397. package/app/public/vendor/fonts/cat-playwrite_nz_guides-400-0.woff2 +0 -0
  398. package/app/public/vendor/fonts/cat-playwrite_pe_guides-400-0.woff2 +0 -0
  399. package/app/public/vendor/fonts/cat-playwrite_pl-400-0.woff2 +0 -0
  400. package/app/public/vendor/fonts/cat-playwrite_pl_guides-400-0.woff2 +0 -0
  401. package/app/public/vendor/fonts/cat-playwrite_pt_guides-400-0.woff2 +0 -0
  402. package/app/public/vendor/fonts/cat-playwrite_ro-400-0.woff2 +0 -0
  403. package/app/public/vendor/fonts/cat-playwrite_ro_guides-400-0.woff2 +0 -0
  404. package/app/public/vendor/fonts/cat-playwrite_sk-400-0.woff2 +0 -0
  405. package/app/public/vendor/fonts/cat-playwrite_sk_guides-400-0.woff2 +0 -0
  406. package/app/public/vendor/fonts/cat-playwrite_tz-400-0.woff2 +0 -0
  407. package/app/public/vendor/fonts/cat-playwrite_tz_guides-400-0.woff2 +0 -0
  408. package/app/public/vendor/fonts/cat-playwrite_us_modern_guides-400-0.woff2 +0 -0
  409. package/app/public/vendor/fonts/cat-playwrite_us_trad_guides-400-0.woff2 +0 -0
  410. package/app/public/vendor/fonts/cat-playwrite_vn-400-0.woff2 +0 -0
  411. package/app/public/vendor/fonts/cat-playwrite_vn_guides-400-0.woff2 +0 -0
  412. package/app/public/vendor/fonts/cat-playwrite_za-400-0.woff2 +0 -0
  413. package/app/public/vendor/fonts/cat-playwrite_za_guides-400-0.woff2 +0 -0
  414. package/app/public/vendor/fonts/cat-potta_one-400-ext.woff2 +0 -0
  415. package/app/public/vendor/fonts/cat-potta_one-400.woff2 +0 -0
  416. package/app/public/vendor/fonts/cat-ranchers-400-ext.woff2 +0 -0
  417. package/app/public/vendor/fonts/cat-ranchers-400.woff2 +0 -0
  418. package/app/public/vendor/fonts/cat-ranga-400-ext.woff2 +0 -0
  419. package/app/public/vendor/fonts/cat-ranga-400.woff2 +0 -0
  420. package/app/public/vendor/fonts/cat-ranga-700-ext.woff2 +0 -0
  421. package/app/public/vendor/fonts/cat-ranga-700.woff2 +0 -0
  422. package/app/public/vendor/fonts/cat-risque-400-ext.woff2 +0 -0
  423. package/app/public/vendor/fonts/cat-risque-400.woff2 +0 -0
  424. package/app/public/vendor/fonts/cat-rowdies-400-ext.woff2 +0 -0
  425. package/app/public/vendor/fonts/cat-rowdies-400.woff2 +0 -0
  426. package/app/public/vendor/fonts/cat-rowdies-700-ext.woff2 +0 -0
  427. package/app/public/vendor/fonts/cat-rowdies-700.woff2 +0 -0
  428. package/app/public/vendor/fonts/cat-ruge_boogie-400-ext.woff2 +0 -0
  429. package/app/public/vendor/fonts/cat-ruge_boogie-400.woff2 +0 -0
  430. package/app/public/vendor/fonts/cat-ruslan_display-400-ext.woff2 +0 -0
  431. package/app/public/vendor/fonts/cat-ruslan_display-400.woff2 +0 -0
  432. package/app/public/vendor/fonts/cat-sacramento-400-ext.woff2 +0 -0
  433. package/app/public/vendor/fonts/cat-sacramento-400.woff2 +0 -0
  434. package/app/public/vendor/fonts/cat-sancreek-400-ext.woff2 +0 -0
  435. package/app/public/vendor/fonts/cat-sancreek-400.woff2 +0 -0
  436. package/app/public/vendor/fonts/cat-sankofa_display-400-ext.woff2 +0 -0
  437. package/app/public/vendor/fonts/cat-sankofa_display-400.woff2 +0 -0
  438. package/app/public/vendor/fonts/cat-sedgwick_ave_display-400-ext.woff2 +0 -0
  439. package/app/public/vendor/fonts/cat-sedgwick_ave_display-400.woff2 +0 -0
  440. package/app/public/vendor/fonts/cat-shantell_sans-400-ext.woff2 +0 -0
  441. package/app/public/vendor/fonts/cat-shantell_sans-400.woff2 +0 -0
  442. package/app/public/vendor/fonts/cat-smokum-400-ext.woff2 +0 -0
  443. package/app/public/vendor/fonts/cat-smokum-400.woff2 +0 -0
  444. package/app/public/vendor/fonts/cat-solitreo-400-ext.woff2 +0 -0
  445. package/app/public/vendor/fonts/cat-solitreo-400.woff2 +0 -0
  446. package/app/public/vendor/fonts/cat-spicy_rice-400-ext.woff2 +0 -0
  447. package/app/public/vendor/fonts/cat-spicy_rice-400.woff2 +0 -0
  448. package/app/public/vendor/fonts/cat-sriracha-400-ext.woff2 +0 -0
  449. package/app/public/vendor/fonts/cat-sriracha-400.woff2 +0 -0
  450. package/app/public/vendor/fonts/cat-srisakdi-400-ext.woff2 +0 -0
  451. package/app/public/vendor/fonts/cat-srisakdi-400.woff2 +0 -0
  452. package/app/public/vendor/fonts/cat-srisakdi-700-ext.woff2 +0 -0
  453. package/app/public/vendor/fonts/cat-srisakdi-700.woff2 +0 -0
  454. package/app/public/vendor/fonts/cat-stix_two_math-400-0.woff2 +0 -0
  455. package/app/public/vendor/fonts/cat-tillana-400-ext.woff2 +0 -0
  456. package/app/public/vendor/fonts/cat-tillana-400.woff2 +0 -0
  457. package/app/public/vendor/fonts/cat-tillana-700-ext.woff2 +0 -0
  458. package/app/public/vendor/fonts/cat-tillana-700.woff2 +0 -0
  459. package/app/public/vendor/fonts/cat-titan_one-400-ext.woff2 +0 -0
  460. package/app/public/vendor/fonts/cat-titan_one-400.woff2 +0 -0
  461. package/app/public/vendor/fonts/cat-yatra_one-400-ext.woff2 +0 -0
  462. package/app/public/vendor/fonts/cat-yatra_one-400.woff2 +0 -0
  463. package/app/public/vendor/fonts/cat-yomogi-400-ext.woff2 +0 -0
  464. package/app/public/vendor/fonts/cat-yomogi-400.woff2 +0 -0
  465. package/app/public/vendor/fonts/dmserifdisplay-400-ext.woff2 +0 -0
  466. package/app/public/vendor/fonts/dmserifdisplay-400.woff2 +0 -0
  467. package/app/public/vendor/fonts/domine-var-ext.woff2 +0 -0
  468. package/app/public/vendor/fonts/domine-var.woff2 +0 -0
  469. package/app/public/vendor/fonts/fondamento-400-ext.woff2 +0 -0
  470. package/app/public/vendor/fonts/fondamento-400.woff2 +0 -0
  471. package/app/public/vendor/fonts/fraunces-var-ext.woff2 +0 -0
  472. package/app/public/vendor/fonts/fraunces-var.woff2 +0 -0
  473. package/app/public/vendor/fonts/fredoka-var-ext.woff2 +0 -0
  474. package/app/public/vendor/fonts/fredoka-var.woff2 +0 -0
  475. package/app/public/vendor/fonts/inter-400.woff2 +0 -0
  476. package/app/public/vendor/fonts/inter-700.woff2 +0 -0
  477. package/app/public/vendor/fonts/jakarta-var-ext.woff2 +0 -0
  478. package/app/public/vendor/fonts/jakarta-var.woff2 +0 -0
  479. package/app/public/vendor/fonts/lato-400.woff2 +0 -0
  480. package/app/public/vendor/fonts/lato-700.woff2 +0 -0
  481. package/app/public/vendor/fonts/literata-400.woff2 +0 -0
  482. package/app/public/vendor/fonts/literata-700.woff2 +0 -0
  483. package/app/public/vendor/fonts/lora-var-ext.woff2 +0 -0
  484. package/app/public/vendor/fonts/lora-var.woff2 +0 -0
  485. package/app/public/vendor/fonts/merriweather-var-ext.woff2 +0 -0
  486. package/app/public/vendor/fonts/merriweather-var.woff2 +0 -0
  487. package/app/public/vendor/fonts/newsreader-400.woff2 +0 -0
  488. package/app/public/vendor/fonts/newsreader-700.woff2 +0 -0
  489. package/app/public/vendor/fonts/notosans-var-ext.woff2 +0 -0
  490. package/app/public/vendor/fonts/notosans-var.woff2 +0 -0
  491. package/app/public/vendor/fonts/nunito-var-ext.woff2 +0 -0
  492. package/app/public/vendor/fonts/nunito-var.woff2 +0 -0
  493. package/app/public/vendor/fonts/opendyslexic-400.woff2 +0 -0
  494. package/app/public/vendor/fonts/opensans-400.woff2 +0 -0
  495. package/app/public/vendor/fonts/opensans-700.woff2 +0 -0
  496. package/app/public/vendor/fonts/pinyon-400.woff2 +0 -0
  497. package/app/public/vendor/fonts/playfair-var-ext.woff2 +0 -0
  498. package/app/public/vendor/fonts/playfair-var.woff2 +0 -0
  499. package/app/public/vendor/fonts/pressstart-400.woff2 +0 -0
  500. package/app/public/vendor/fonts/quattro-400.woff2 +0 -0
  501. package/app/public/vendor/fonts/quattro-700.woff2 +0 -0
  502. package/app/public/vendor/fonts/quicksand-var-ext.woff2 +0 -0
  503. package/app/public/vendor/fonts/quicksand-var.woff2 +0 -0
  504. package/app/public/vendor/fonts/robotoflex-var-ext.woff2 +0 -0
  505. package/app/public/vendor/fonts/robotoflex-var.woff2 +0 -0
  506. package/app/public/vendor/fonts/sora-var-ext.woff2 +0 -0
  507. package/app/public/vendor/fonts/sora-var.woff2 +0 -0
  508. package/app/public/vendor/fonts/sourcesans-400.woff2 +0 -0
  509. package/app/public/vendor/fonts/sourcesans-700.woff2 +0 -0
  510. package/app/public/vendor/fonts/sourceserif-400.woff2 +0 -0
  511. package/app/public/vendor/fonts/sourceserif-700.woff2 +0 -0
  512. package/app/public/vendor/fonts/spacegrotesk-var-ext.woff2 +0 -0
  513. package/app/public/vendor/fonts/spacegrotesk-var.woff2 +0 -0
  514. package/app/public/vendor/fonts/spectral-400-ext.woff2 +0 -0
  515. package/app/public/vendor/fonts/spectral-400.woff2 +0 -0
  516. package/app/public/vendor/fonts/spectral-700-ext.woff2 +0 -0
  517. package/app/public/vendor/fonts/spectral-700.woff2 +0 -0
  518. package/app/public/vendor/fonts/teachers-var-ext.woff2 +0 -0
  519. package/app/public/vendor/fonts/teachers-var.woff2 +0 -0
  520. package/app/public/vendor/fonts/ubuntu-400-ext.woff2 +0 -0
  521. package/app/public/vendor/fonts/ubuntu-400.woff2 +0 -0
  522. package/app/public/vendor/fonts/ubuntu-700-ext.woff2 +0 -0
  523. package/app/public/vendor/fonts/ubuntu-700.woff2 +0 -0
  524. package/app/public/vendor/fonts/ufmag-400.woff2 +0 -0
  525. package/dist-pkg/platform/guide-intensity.d.ts +0 -39
  526. package/dist-pkg/platform/guide-intensity.js +0 -71
  527. package/dist-pkg/ui/latch-refusal.d.ts +0 -33
  528. package/dist-pkg/ui/latch-refusal.js +0 -60
@@ -1,9 +1,10 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-or-later
2
2
  // input/voice-map — THE WORDS A CHILD SAYS, AND THE POSITION EACH ONE PRESSES (ADR-0204 erratum; issues #184, #190).
3
3
  //
4
- // This is the pure half of playing by voice: a table of words per language, a closed grammar built from it, and the rule that
5
- // decides WHEN a heard word becomes a press. Nothing here opens a microphone or loads a model — the same division that let the
6
- // gaze cycle be measured without a camera.
4
+ // This is the pure half of playing by voice: a table of words per language, a closed grammar built from it and the open menu's
5
+ // names, and what a heard phrase MEANS — a position, or an item by name (ADR-0194). WHEN a phrase is heard is the reader's
6
+ // (`platform/speech-recognition`). Nothing here opens a microphone or loads a model — the same division that let the gaze cycle
7
+ // be measured without a camera.
7
8
  //
8
9
  // 🎯 THE WORDS ARE THE DEV'S, checked by the Dev against what games use (racing R2 accelerate / L2 brake, shooters L2 aim / R2 fire)
9
10
  // and then run in the lab's bar protocol in the three languages: pt 23/23, es 24/24, en 24/24 detected, all rated «ok».
@@ -13,12 +14,13 @@
13
14
  // symbol table), so under that recogniser R1 answers only to «estibordo». It stays in the table because the browser's own
14
15
  // recogniser has no closed vocabulary and does hear it — a word is dropped by the RECOGNISER that cannot hear it, never by this
15
16
  // table, which is the Dev's decision written down.
17
+ import { spokenText, createCommandReader } from '../platform/speech-recognition.js';
16
18
  const PT = {
17
19
  up: ['acima'], down: ['abaixo'], left: ['esquerda'], right: ['direita'],
18
20
  start: ['start'], select: ['select'],
19
21
  // 🔴 `ação` WITH THE CEDILLA AND THE TILDE, measured in a browser: written `acao`, the pt model answered "Ignoring word
20
22
  // missing in vocabulary: 'acao'" and the first position was MUTE — the child said the word and nothing happened. The
21
- // spelling here is the recogniser's VOCABULARY's; whoever compares no longer looks at accents (`wordsOf`), so spelling it
23
+ // spelling here is the recogniser's VOCABULARY's; whoever compares no longer looks at accents (the reader's key), so spelling it
22
24
  // right costs nothing on the hearing side. ⚠️ `boreste` is still outside the small model's vocabulary (ADR-0204 erratum)
23
25
  // and stays on the table on purpose: it is the Dev's word, and a larger model hears it.
24
26
  action1: ['ação'], action2: ['confirma', 'pega', 'ativar', 'pulo'], action3: ['voltar', 'solta', 'cancelar', 'especial'],
@@ -48,14 +50,6 @@ const BY_LANGUAGE = { pt: PT, es: ES, en: EN };
48
50
  export function voiceWordsFor(language) {
49
51
  return BY_LANGUAGE[language.split('-')[0].toLowerCase()] ?? PT;
50
52
  }
51
- /**
52
- * The words a sentence is made of, compared the way a recogniser writes them: no case, no accents, no punctuation.
53
- * The same rule the quiz's spoken answer uses — a recogniser that writes «Acima.» is not a child who said something else.
54
- */
55
- function wordsOf(sentence) {
56
- return sentence.normalize('NFD').replace(/[̀-ͯ]/gu, '').toLowerCase()
57
- .replace(/[^\p{L}\p{N}]+/gu, ' ').trim().split(' ').filter(Boolean);
58
- }
59
53
  /**
60
54
  * THE CLOSED GRAMMAR: every word the recogniser is allowed to return, and nothing else (ADR-0189, ADR-0193).
61
55
  *
@@ -63,7 +57,8 @@ function wordsOf(sentence) {
63
57
  * says something that is not a command: the lab measured free speech being pushed to the nearest word («configurações de
64
58
  * inclusão» → «quatro»). That is the cost of the closed list and the reason the words are short and unlike each other.
65
59
  * ⚠️ `extra` is what the MENU is showing right now (ADR-0194): saying the name of an item activates it, so those words join the
66
- * grammar while that menu is open and leave with it.
60
+ * grammar while that menu is open and leave with it — AS THE LANGUAGE WRITES THEM (§1, «as shown»), accents kept: stripped,
61
+ * «configurações» is not in the model's vocabulary and the name goes mute, the same defect «acao» was.
67
62
  */
68
63
  export function voiceGrammar(language, extra = []) {
69
64
  const all = new Set();
@@ -71,55 +66,58 @@ export function voiceGrammar(language, extra = []) {
71
66
  for (const p of words)
72
67
  all.add(p.toLowerCase());
73
68
  for (const e of extra) {
74
- const clean = wordsOf(e).join(' ');
69
+ const clean = spokenText(e);
75
70
  if (clean)
76
71
  all.add(clean);
77
72
  }
78
73
  return [...all];
79
74
  }
80
75
  /**
81
- * The rule that turns a growing partial into presses.
76
+ * The Dev's words and the open menu's names, READ BY `platform/speech-recognition.createCommandReader` — the one reader of what
77
+ * was heard (ADR-0194 §2–§3); this adds only what a word MEANS here, the position it presses.
82
78
  *
83
79
  * 🎯 A PARTIAL GROWS, AND ONLY WHAT IS NEW FIRES. «acima» then «acima abaixo» is a child who said two words, and the second
84
- * press must be `down`, not `up` all over again — so the words already answered for are counted, never re-read.
85
- * 📌 The tail is matched LONGEST FIRST because a position may answer to a phrase: «pick up» is one command in English, and
86
- * reading only the last word would make it `null` after having fired nothing.
80
+ * press must be `down`, not `up` all over again — the reader counts what it already answered for in each utterance.
87
81
  */
88
82
  export function createVoiceCommands(language) {
89
- const words = voiceWordsFor(language);
90
- const byPhrase = new Map();
91
- for (const [action, spoken] of Object.entries(words)) {
83
+ const actionOf = new Map();
84
+ for (const [action, spoken] of Object.entries(voiceWordsFor(language))) {
92
85
  for (const phrase of spoken)
93
- byPhrase.set(wordsOf(phrase).join(' '), action);
86
+ if (!actionOf.has(spokenText(phrase)))
87
+ actionOf.set(spokenText(phrase), action);
94
88
  }
95
- const longest = Math.max(1, ...[...byPhrase.keys()].map((f) => f.split(' ').length));
96
- let answered = 0;
89
+ const reader = createCommandReader([...actionOf.keys()]);
90
+ let names = [];
91
+ let utterance = 0;
97
92
  let previous = [];
93
+ const meaning = (heard) => heard.flatMap((h) => {
94
+ if (h.kind === 'palavra') {
95
+ const action = actionOf.get(h.word);
96
+ return action ? [{ kind: 'position', action }] : [];
97
+ }
98
+ const name = names.find((n) => spokenText(n) === h.name);
99
+ return name === undefined ? [] : [{ kind: 'item', name }];
100
+ });
98
101
  return {
102
+ items(n) { names = [...n]; reader.items(names); },
99
103
  partial(text) {
100
- const heard = wordsOf(text);
104
+ const heard = spokenText(text).split(' ').filter(Boolean);
101
105
  /*
102
106
  * 🔴 A NEW UTTERANCE IS ONE THAT DOES NOT CONTINUE THE LAST, and counting words is not enough to see it: a recogniser
103
- * starts a new partial without saying so, and «abaixo» after «acima abaixo» is the same LENGTH as what was already
104
- * answered. A case caught it — the child said a second command and nothing moved. What a partial of the same utterance
105
- * always is, is the previous one plus more.
107
+ * starts a new partial without saying so — and does whenever the grammar changes, because a new grammar is a new
108
+ * recogniser — and «abaixo» after «acima abaixo» is shorter than what was already answered. What a partial of the same
109
+ * utterance always is, is the previous one plus more.
106
110
  */
107
- const continues = previous.every((p, i) => heard[i] === p);
108
- if (!continues)
109
- answered = 0;
111
+ if (!previous.every((p, i) => heard[i] === p))
112
+ utterance += 1;
110
113
  previous = heard;
111
- if (heard.length <= answered)
112
- return null;
113
- for (let n = Math.min(longest, heard.length - answered); n >= 1; n--) {
114
- const action = byPhrase.get(heard.slice(heard.length - n).join(' '));
115
- if (action) {
116
- answered = heard.length;
117
- return action;
118
- }
119
- }
120
- answered = heard.length;
121
- return null;
114
+ return meaning(reader.read(utterance, text, false));
115
+ },
116
+ final(text) {
117
+ const done = meaning(reader.read(utterance, text, true));
118
+ utterance += 1;
119
+ previous = [];
120
+ return done;
122
121
  },
123
- reset() { answered = 0; previous = []; },
124
122
  };
125
123
  }
@@ -21,7 +21,6 @@ export interface AudioAmbientCtx {
21
21
  getAudioCtx: () => AudioContext | null;
22
22
  catNode: (cat: string) => AudioNode | null;
23
23
  audioOut: () => AudioNode | null;
24
- noiseBuffer: (ac: AudioContext) => AudioBuffer;
25
24
  getSoundOn: () => boolean;
26
25
  getVolume: () => number;
27
26
  getAudioCat: () => Record<string, {
@@ -5,6 +5,7 @@
5
5
  // updateAmbient() — per frame: builds the track once (lazily) and sets the WATER gain (nearness of what the game
6
6
  // calls 'water') and the RAIN gain (follows the rain level). Gated by audioCat.ambient.on.
7
7
  // thunder(inten) — a synthesised low rumble (low-passed noise) of variable intensity; called by the weather.
8
+ import { noiseBuffer } from './audio.js';
8
9
  export function createAudioAmbient(ctx) {
9
10
  let _ambient = null;
10
11
  function buildAmbient(ac) {
@@ -66,8 +67,9 @@ export function createAudioAmbient(ctx) {
66
67
  return;
67
68
  try {
68
69
  const vol = ctx.getVolume();
70
+ // the engine's own white noise (ADR-0258): a game could only ever hand back this same function
69
71
  const src = ac.createBufferSource();
70
- src.buffer = ctx.noiseBuffer(ac);
72
+ src.buffer = noiseBuffer(ac);
71
73
  src.loop = true;
72
74
  const bq = ac.createBiquadFilter();
73
75
  bq.type = 'lowpass';
@@ -12,8 +12,8 @@ export const AUDIO_CATS = [
12
12
  { k: 'earcons', lbl: 'audio.cat.earcons' },
13
13
  { k: 'tts', lbl: 'audio.cat.tts' },
14
14
  { k: 'sonar', lbl: 'audio.cat.sonar' },
15
- { k: 'guard', lbl: 'audio.cat.guard' },
16
- { k: 'guide', lbl: 'audio.cat.guide' },
15
+ // 🔴 `guard` and `guide` LEFT (ADR-0258, the Dev: «Saem.»): the engine plays nothing in either since the guide moved to the
16
+ // platformer (ADR-0257); a level stored for them stays in the child's storage and is read by nobody.
17
17
  ];
18
18
  /**
19
19
  * The categories born OFF, and the reason for each. A list with its reason written down, not a `k !== 'x'` hanging in an
@@ -22,14 +22,9 @@ export const AUDIO_CATS = [
22
22
  * · `tts` — a robotic voice irritates and overloads autistic people. Whoever needs it turns it on in the menu. (The
23
23
  * literacy voice is `gameSay()`, independent of this and always on.)
24
24
  *
25
- * · `guide` — the audio guide, off by the Dev's decision, a PROVISIONAL measure that should not become permanent
26
- * without someone reviewing it. The Dev's verdict on the old beacon: «um ping é a pior escolha possível, tenebroso
27
- * para quem tem TEA». It was not the FREQUENCY that was wrong — it was the beep, which the child who most needs cues
28
- * heard the whole game long. Its replacement, a sound that grows as one approaches along a walkable route (#84), is
29
- * the game's to decide; until someone reviews it, silence is the default, and a child who wants the guide can still
30
- * turn it on in the hearing menu.
25
+ * (`guide`, born off by the Dev's decision on the old beacon, left with its category — ADR-0258.)
31
26
  */
32
- const BORN_OFF = new Set(['tts', 'guide']);
27
+ const BORN_OFF = new Set(['tts']);
33
28
  /**
34
29
  * A category's FACTORY state. It has a name because two places need it: the boot's read (when nothing is stored) and
35
30
  * the hearing menu's "restore defaults" (ADR-0028). Writing `k !== 'tts'` and `0.8` in both would be the ownerless copy
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-or-later
2
- import { type Spot, type Topology, type Speakable, type Role } from '../core/contract.js';
2
+ import { type Spot, type Topology, type Speakable } from '../core/contract.js';
3
3
  import type { Translate } from '../core/i18n.js';
4
4
  export type SinkAC = AudioContext & {
5
5
  setSinkId?: (id: string) => Promise<void>;
@@ -33,51 +33,6 @@ export interface SonarPlayer extends PlayerAudioOut {
33
33
  readonly y: number;
34
34
  readonly audioSink?: string | null;
35
35
  }
36
- /** The guide's continuous graph: oscillator → low-pass → gain → pan → the `guide` category. */
37
- export interface LiveGuide {
38
- /** The context that built it — each `setTargetAtTime` takes its `currentTime` from it. */
39
- readonly ac: AudioContext;
40
- readonly osc: OscillatorNode;
41
- readonly filter: BiquadFilterNode;
42
- readonly gain: GainNode;
43
- /** `null` on an engine without `createStereoPanner` — the guide goes mono instead of not existing. */
44
- readonly panner: StereoPannerNode | null;
45
- /** Frames since the ROUTE was last recomputed. The route is costly; the sound cannot wait for it. */
46
- framesSinceRoute: number;
47
- /** The last measured steps. The intensity comes from here every frame. */
48
- steps: number;
49
- /** The last measured pan, for the same reason: it comes from the target, which is found only with the route. */
50
- pan: number;
51
- }
52
- /**
53
- * ⚠️ THE TIMBRE MUST HAVE HARMONICS, and it is a technical requirement, not taste.
54
- *
55
- * The axis of #84 item 2 is BRIGHTNESS, and brightness is a low-pass opening and closing. **A low-pass over a `sine` wave
56
- * does absolutely nothing**: a sine has nothing above its fundamental for the filter to cut, and the guide would be left
57
- * with a dead axis and only the volume working. The sawtooth is the richest of Web Audio's four waves — it has ALL the
58
- * harmonics — which is why it is the choice.
59
- *
60
- * And it solves, for free, the plan's other constraint: the sonar and the cane use `sine`. A different timbre was
61
- * required so they would not collide on one channel; here the different timbre IS the mechanism.
62
- */
63
- export declare const GUIDE_WAVE: OscillatorType;
64
- /**
65
- * How many frames between two route computations.
66
- *
67
- * ⚠️ THE ROUTE IS A BREADTH-FIRST SEARCH, and `__incl.update(dt)` counts FRAMES: running a BFS every frame on a platform
68
- * map spends the whole frame budget on the target device (Positivo/Chromebook, pillar 1). Twelve frames are ~0.2 s at
69
- * 60 fps — faster than a child takes a step, and the sound does not wait for them: the intensity is rewritten EVERY
70
- * frame, with the steps the last route left.
71
- */
72
- export declare const FRAMES_BETWEEN_ROUTES = 12;
73
- /**
74
- * The guide's base gain, before the intensity and the master volume multiply it.
75
- *
76
- * ⚠️ LOWER THAN THE BEEP IT REPLACES (0.11), and not by mistake: a sound that **never stops** is perceived as louder
77
- * than a transient of the same peak, and it tires by persistence rather than intensity — exactly what autism-support
78
- * mode exists not to do.
79
- */
80
- export declare const GUIDE_VOL = 0.06;
81
36
  /**
82
37
  * A player's DEDICATED OUTPUT, and this module OWNS it: it is here the two fields are BORN (`new AC()` + `createGain()`,
83
38
  * below). By ADR-0039 the owner declares where a field is born, and `core/entity` does not mention them — they are not
@@ -89,20 +44,6 @@ export declare const GUIDE_VOL = 0.06;
89
44
  export interface PlayerAudioOut {
90
45
  _ac?: SinkAC | null;
91
46
  _acOut?: GainNode | null;
92
- /**
93
- * This player's LIVE GUIDE GRAPH, and it lives HERE, beside the other two, for their reason (ADR-0039: the owner
94
- * declares where a field is born; ADR-0033: the engine's entity declares what the ENGINE owns, and an oscillator is
95
- * not). `null`/absent = quiet.
96
- *
97
- * ⚠️ IT HAS TO SURVIVE FRAMES, and that is where it differs from everything else in this file. The sonar and the old
98
- * beep created an oscillator, played it and threw it away; a CONTINUOUS presence (#84 item 2) is an oscillator that
99
- * STAYS, with the filter and gain moving under it. That is why it hangs on the player: there is nowhere else a
100
- * per-player thing lasts from one frame to the next.
101
- *
102
- * ⚠️ AND THAT IS WHY THERE IS A WAY TO STOP IT. A field that lasts is a field that leaks: with nobody stopping it,
103
- * turning the `guide` category off in the mixer would leave the sound playing.
104
- */
105
- _guide?: LiveGuide | null;
106
47
  }
107
48
  export interface PlayerCtxOut {
108
49
  ac: AudioContext;
@@ -117,33 +58,19 @@ export interface SonarCtx {
117
58
  targetsOf: (playerIndex: number) => readonly Spot[];
118
59
  /** Field 3: what the thing there is called. `null` = no name, and the announcement falls back to the generic. */
119
60
  nameAt: (at: Spot) => Speakable | null;
120
- /**
121
- * Field 2: what is at this spot. It is what `core/route` crosses (or not) to find the way.
122
- *
123
- * ⚠️ OPTIONAL HERE, and required in `GameDeclaration` — the difference is not carelessness. Making a `SonarCtx` field
124
- * required breaks every consumer that builds this ctx by hand, and the composition root (which really builds it) always
125
- * has it, because the declaration's validation requires it. Absent here, the guide does not go quiet: it falls back to
126
- * the straight-line distance, which is what it did before.
127
- */
128
- roleAt?: (at: Spot) => Role;
129
61
  tonePan: (freq: number, dur: number, cat: string, pan?: number | null, vol?: number, type?: OscillatorType, pc?: PlayerCtxOut | null) => void;
130
62
  srSay: (text: string) => void;
131
- narrate: (text: string) => void;
132
63
  /**
133
- * A mixer category's bus. The SAME shape the other audio modules receive — the guide cannot use `tonePan`, because
134
- * `tonePan` plays and forgets, and a continuous presence is a graph that STAYS.
135
- *
136
- * Optional for the reason of `roleAt`: whoever does not inject it falls back to `audioOut` and, lacking that, to the
137
- * `destination`. What is lost is the `guide` category's slider, not the sound.
64
+ * Speaks the sonar's words. `seat` is the player who pressed: a reading takes turns with the other players' and cuts that
65
+ * player's own (ADR-0234, errata 2026-09-26 — the root's narration does it; a ctx built by hand may ignore it).
138
66
  */
139
- catNode?: (cat: string) => AudioNode | null;
140
- /** The master node, `catNode`'s fallback. */
141
- audioOut?: () => AudioNode | null;
67
+ narrate: (text: string, seat: number) => void;
142
68
  /**
143
- * The master volume (0..1), which each synthesis multiplies by itself — `platform/audio`'s master gain is the pause's
144
- * mute, not the slider. Absent = 1: the guide sounds and ignores the slider. That is why the root injects it.
69
+ * WHAT IS ON THE SCREEN NOW, as text in reading order — `''` when there is none the engine can read (ADR-0234: «sonar do que
70
+ * está na tela»). With text, the sonar's WORDS are that text, read at the press; the tone still points at the nearest
71
+ * target. Without (a world drawn on a canvas, or no text), the words are the navigation sentence. Absent: always the latter.
145
72
  */
146
- getVolume?: () => number;
73
+ screenText?: () => string;
147
74
  /**
148
75
  * Is this child's sight impaired — simulated blindness or low vision?
149
76
  *
@@ -163,13 +90,7 @@ export interface SonarCtx {
163
90
  * is a candidate for the next major.
164
91
  */
165
92
  LOGICAL_W?: number;
166
- getPlayers: () => SonarPlayer[];
167
93
  getNumPlayers: () => number;
168
- getAudioCtx: () => AudioContext | null;
169
- getSoundOn: () => boolean;
170
- getAudioCat: () => Record<string, {
171
- on: boolean;
172
- }> | null;
173
94
  /**
174
95
  * Makes a NEW AudioContext, or `null` where the host has none: each player with an audio device of their own gets one, so
175
96
  * `setSinkId` can send that child's cues to that device. The root's maker, from `host.win` — the same one its `createAudio`
@@ -182,13 +103,6 @@ export interface AudioSonar {
182
103
  panFor: (wx: number, pl: SonarPlayer) => number;
183
104
  needsAudioCues: (pl: SonarPlayer) => boolean;
184
105
  sonar: (pl: SonarPlayer) => void;
185
- updateGuide: () => void;
186
106
  readonly sonarCount: number;
187
- /**
188
- * ⚠️ It counts FRAMES IN WHICH THE GUIDE SOUNDS (#84 item 2), not beeps: there is nothing discrete left to count.
189
- * Reading it to say "the guide is working" is right; reading it to say "it played three times" asks about something
190
- * that no longer exists.
191
- */
192
- readonly guideCount: number;
193
107
  }
194
108
  export declare function createAudioSonar(ctx: SonarCtx): AudioSonar;
@@ -9,7 +9,8 @@
9
9
  //
10
10
  // This is one of them: what navigates by SOUND — pointing at the nearest target, the looping beacon, the pan by relative
11
11
  // position and the question "does this child need audio cues?". The CANE and BLIND SWIMMING, which read tiles, floor and
12
- // hitbox, are the platformer's and live in its repository (ADR-0228, note CC), and the audio guide is its decision.
12
+ // hitbox, are the platformer's and live in its repository (ADR-0228, note CC). So is the continuous audio GUIDE, which
13
+ // is that game's decision: it left this module for the platformer's `platform/audio-guide` (ADR-0257, note DZ).
13
14
  //
14
15
  // ========================= WHAT REPLACED THE PLATFORMER THINGS =========================
15
16
  // None of them crossed over. The sonar used to ask the game for an array of coins and filter by taken/owner; now it
@@ -36,11 +37,6 @@
36
37
  // Nothing here touches `window`: a player's own context comes from the injected `newContext` (ADR-0232 D4). It runs in the
37
38
  // `node` project.
38
39
  import { distance, bearing } from '../core/contract.js';
39
- // THE GUIDE (#84 item 2) is made of these two and nothing else: the ROUTE says how many steps remain going round walls,
40
- // and the INTENSITY turns that number into brightness and volume. Neither touches Web Audio; the wiring — the only part
41
- // that does — is `updateGuide` below, which is why the design can be checked in `node`.
42
- import { routeTo } from '../core/route.js';
43
- import { guideIntensity, FAR_CUT, STEPS_TO_FLOOR } from './guide-intensity.js';
44
40
  /**
45
41
  * How many STEPS of the declared metric saturate the stereo. Beyond this, "to the right" is just to the right.
46
42
  *
@@ -62,55 +58,9 @@ export const PAN_PACES = 11;
62
58
  export function worldStep(shape) {
63
59
  return shape.kind === 'continuous' ? shape.unit : shape.kind === 'grid' ? 1 : 0;
64
60
  }
65
- /**
66
- * ⚠️ THE TIMBRE MUST HAVE HARMONICS, and it is a technical requirement, not taste.
67
- *
68
- * The axis of #84 item 2 is BRIGHTNESS, and brightness is a low-pass opening and closing. **A low-pass over a `sine` wave
69
- * does absolutely nothing**: a sine has nothing above its fundamental for the filter to cut, and the guide would be left
70
- * with a dead axis and only the volume working. The sawtooth is the richest of Web Audio's four waves — it has ALL the
71
- * harmonics — which is why it is the choice.
72
- *
73
- * And it solves, for free, the plan's other constraint: the sonar and the cane use `sine`. A different timbre was
74
- * required so they would not collide on one channel; here the different timbre IS the mechanism.
75
- */
76
- export const GUIDE_WAVE = 'sawtooth';
77
- /**
78
- * The guide's fundamental, fixed.
79
- *
80
- * ⚠️ FIXED ON PURPOSE: PITCH is already the sonar's language (`380 + 740 * near`, nearer = higher). If the guide also
81
- * rose in pitch, both would say the same thing through the same means, and whoever hears both at once could not tell them
82
- * apart. The guide says distance by brightness; the sonar, by pitch.
83
- */
84
- const GUIDE_HZ = 220;
85
- /**
86
- * How many frames between two route computations.
87
- *
88
- * ⚠️ THE ROUTE IS A BREADTH-FIRST SEARCH, and `__incl.update(dt)` counts FRAMES: running a BFS every frame on a platform
89
- * map spends the whole frame budget on the target device (Positivo/Chromebook, pillar 1). Twelve frames are ~0.2 s at
90
- * 60 fps — faster than a child takes a step, and the sound does not wait for them: the intensity is rewritten EVERY
91
- * frame, with the steps the last route left.
92
- */
93
- export const FRAMES_BETWEEN_ROUTES = 12;
94
- /**
95
- * The ceiling of spots for the guide's route. Well below `core/route`'s 4096, and that module says why: a cue every
96
- * frame tolerates far less than a calculation when a level loads. Hitting it returns `null`, which is "I cannot say" —
97
- * and the guide falls back to the straight line, which still sounds.
98
- */
99
- const ROUTE_BUDGET = 1024;
100
- /**
101
- * The guide's base gain, before the intensity and the master volume multiply it.
102
- *
103
- * ⚠️ LOWER THAN THE BEEP IT REPLACES (0.11), and not by mistake: a sound that **never stops** is perceived as louder
104
- * than a transient of the same peak, and it tires by persistence rather than intensity — exactly what autism-support
105
- * mode exists not to do.
106
- */
107
- export const GUIDE_VOL = 0.06;
108
- /** The `setTargetAtTime` time constant. Short enough to follow a step, long enough that a change is a glide and not a
109
- * stair — a stair on every route would be a beep again. */
110
- const GUIDE_TAU = 0.08;
111
61
  export function createAudioSonar(ctx) {
112
62
  const { t } = ctx;
113
- let _sonarCount = 0, _guideCount = 0;
63
+ let _sonarCount = 0;
114
64
  /** The player's AudioContext (for `setSinkId` on their device), or null → the global context. */
115
65
  function playerCtx(pl) {
116
66
  if (!pl || !pl.audioSink)
@@ -143,7 +93,8 @@ export function createAudioSonar(ctx) {
143
93
  return Math.max(-1, Math.min(1, (wx - pl.x) / (PAN_PACES * pace)));
144
94
  }
145
95
  /**
146
- * Impaired sight? The edge guard and the guide exist only when the answer is yes (or in blind mode).
96
+ * Impaired sight? A game's audio cues — its edge guard, its guide — ask this, and exist only when the answer is yes (or in
97
+ * blind mode).
147
98
  *
148
99
  * ⚠️ THE VISUAL HALF IS INJECTED (#104), and the module got SMALLER instead of migrating. It used to consult a table of
149
100
  * RENDER modes, walked with a render key, from inside `platform/`. The choice was: import `render/` (an edge the wrong
@@ -211,19 +162,31 @@ export function createAudioSonar(ctx) {
211
162
  * for the child in any genre, which is the contract's point.
212
163
  */
213
164
  const distanceKey = (d) => d < 4 ? 'sr.nav.veryClose' : d < 9 ? 'sr.nav.close' : 'sr.nav.far';
165
+ /**
166
+ * THE SONAR: a tone that points, and words (ADR-0234). The tone is the same with or without text on screen — pitch says how
167
+ * near the nearest target is, the pan which side, and a low tone that there is none. The WORDS are what is on the screen
168
+ * when there is text there, read now; only a screen with no text the engine can read gets the navigation sentence.
169
+ */
214
170
  function sonar(pl) {
215
171
  _sonarCount++;
216
172
  const pc = playerCtx(pl);
217
173
  const target = nearestSpot(pl);
174
+ if (target)
175
+ ctx.tonePan(380 + 740 * Math.max(0, 1 - target.d / 12), 0.16, 'sonar', panFor(target.at.x, pl), 0.26, 'sine', pc); // nearer = higher
176
+ else
177
+ ctx.tonePan(300, 0.2, 'sonar', 0, 0.2, 'sine', pc);
178
+ const onScreen = ctx.screenText?.() ?? '';
179
+ if (onScreen) {
180
+ ctx.srSay(onScreen);
181
+ ctx.narrate(onScreen, pl.i);
182
+ return;
183
+ }
218
184
  // A sonar that no longer knows what the target is cannot name it when there is NO target at all: it says "nothing
219
185
  // nearby", which is true in any genre.
220
186
  if (!target) {
221
- ctx.tonePan(300, 0.2, 'sonar', 0, 0.2, 'sine', pc);
222
187
  ctx.srSay(t('sr.nav.noTargetNear'));
223
188
  return;
224
189
  }
225
- const pan = panFor(target.at.x, pl), near = Math.max(0, 1 - target.d / 12);
226
- ctx.tonePan(380 + 740 * near, 0.16, 'sonar', pan, 0.26, 'sine', pc); // nearer = higher
227
190
  // The NAME comes from the game (field 3); the engine used to say "coin" because it only knew coins. The fallback is
228
191
  // for a game that declares a target without a name: "target" is better than a raw key.
229
192
  const name = ctx.nameAt(target.at);
@@ -234,158 +197,10 @@ export function createAudioSonar(ctx) {
234
197
  });
235
198
  const msg = (ctx.getNumPlayers() > 1 ? t('sr.player.prefix', { n: pl.i + 1 }) : '') + foundSentence;
236
199
  ctx.srSay(msg);
237
- ctx.narrate(msg);
238
- }
239
- /**
240
- * HOW MANY STEPS REMAIN, and the preferred answer is the one that goes round walls.
241
- *
242
- * ⚠️ BOTH ANSWERS ARE ALREADY IN THE SAME UNIT, which is the only reason this is a line and not a conversion: the route
243
- * counts steps by definition, and `distance()` returns steps in EVERY topology — it divides by the `unit` itself in the
244
- * continuous branch. Dividing again by the world step here was the mistake waiting to be made, and in a game with
245
- * `unit = 16` it would put the guide at full brightness forever — the defect #121 took out of `panFor`: mixing a world
246
- * ruler with a screen ruler.
247
- *
248
- * The route is lost two ways — a game that did not inject `roleAt`, and a budget run out — and both fall back to the
249
- * STRAIGHT LINE. ⚠️ It lies behind a wall (says "near" of a target that needs going round), which is why it is a
250
- * fallback and not a choice. But continuing to say it is strictly better than going quiet — quiet would claim there is
251
- * no target.
252
- */
253
- function stepsToTarget(pl, target) {
254
- const roleAt = ctx.roleAt;
255
- if (roleAt) {
256
- const path = routeTo({ topology: ctx.topology(), roleAt, budget: ROUTE_BUDGET }, { x: pl.x, y: pl.y }, [target.at]);
257
- if (path)
258
- return path.steps;
259
- }
260
- return target.d;
261
- }
262
- /** Lights this player's continuous graph. `null` = it could not (no context, or an engine without Web Audio). */
263
- function startGuide(pl) {
264
- const pc = playerCtx(pl);
265
- const ac = pc ? pc.ac : ctx.getAudioCtx();
266
- if (!ac)
267
- return null;
268
- try {
269
- const osc = ac.createOscillator(), lowpass = ac.createBiquadFilter(), level = ac.createGain();
270
- osc.type = GUIDE_WAVE;
271
- osc.frequency.value = GUIDE_HZ;
272
- lowpass.type = 'lowpass';
273
- lowpass.frequency.value = FAR_CUT; // born at the bottom of the scale and rising; born open would be a fright
274
- level.gain.value = 0; // and born quiet, so it does not click when it starts
275
- let migrated = level;
276
- let panner = null;
277
- if (ac.createStereoPanner) {
278
- panner = ac.createStereoPanner();
279
- level.connect(panner);
280
- migrated = panner;
281
- }
282
- osc.connect(lowpass).connect(level);
283
- migrated.connect(pc ? pc.out : (ctx.catNode?.('guide') || ctx.audioOut?.() || ac.destination));
284
- osc.start();
285
- // `framesSinceRoute` is born at the ceiling so the FIRST pass measures the route, instead of sounding for twelve
286
- // frames on invented steps.
287
- return { ac, osc, filter: lowpass, gain: level, panner, framesSinceRoute: FRAMES_BETWEEN_ROUTES, steps: STEPS_TO_FLOOR, pan: 0 };
288
- }
289
- catch (e) {
290
- return null;
291
- }
292
- }
293
- /**
294
- * Puts the graph out — and it HAS to be put out, because an oscillator that stays is the difference between this shape
295
- * and the old one. The beep died by itself; this plays until someone stops it. Without this, turning the `guide`
296
- * category off in the mixer would leave the sound playing, and changing visual mode would leave a second graph adding
297
- * to the first.
298
- *
299
- * It ramps down (does not cut) because a dry cut on a live oscillator is a click — a transient, precisely what this item
300
- * exists to take out of the child's ear.
301
- */
302
- function stopGuide(pl) {
303
- const g = pl._guide;
304
- if (!g)
305
- return;
306
- pl._guide = null;
307
- try {
308
- const audioNow = g.ac.currentTime;
309
- g.gain.gain.setTargetAtTime(0, audioNow, 0.05);
310
- g.osc.stop(audioNow + 0.3);
311
- }
312
- catch (e) { /* noop */ }
313
- }
314
- /**
315
- * THE CONTINUOUS PRESENCE, one frame at a time (#84 item 2).
316
- *
317
- * ⚠️ WHAT LEFT HERE WAS A BEEP, forever, whether or not the child moved or anything changed. The Dev's verdict: «um
318
- * ping é a pior escolha possível, tenebroso para quem tem TEA». What replaces it fires nothing — the sound is already
319
- * there, and changes brightness.
320
- *
321
- * ⚠️ AND GOING QUIET IS STILL A STATEMENT, with one meaning only: THERE IS NO TARGET. That is why "no target" puts the
322
- * graph out and "far" does not: `guide-intensity`'s floor (`FAR_VOL`) exists exactly so the child does not confuse "it
323
- * is far" with "there is nothing to find".
324
- */
325
- function updateGuide() {
326
- const audible = guideAudible();
327
- const vol = ctx.getVolume ? ctx.getVolume() : 1;
328
- for (const pl of ctx.getPlayers()) {
329
- if (!audible || !needsAudioCues(pl)) {
330
- stopGuide(pl);
331
- continue;
332
- }
333
- // no guide lit (no target, or a device that refused it), or the target gone: silence says «nothing to find»
334
- const g = liveGuide(pl);
335
- if (!g || !remeasure(pl, g)) {
336
- stopGuide(pl);
337
- continue;
338
- }
339
- glide(g, vol);
340
- _guideCount++;
341
- }
342
- }
343
- /** Can the guide be heard at all: the engine's audio started, the game's sound on, and a `guide` category that is on. */
344
- function guideAudible() {
345
- const cat = ctx.getAudioCat();
346
- return !!ctx.getAudioCtx() && ctx.getSoundOn() && !!cat && !!cat.guide && cat.guide.on;
347
- }
348
- /** This player's live guide — lit here only if there is a target to point to, and `null` where it cannot be lit. */
349
- function liveGuide(pl) {
350
- if (pl._guide)
351
- return pl._guide;
352
- // ⚠️ THE QUESTION "IS THERE A TARGET?" COMES BEFORE LIGHTING, and the gate insisted: with the graph born first, a
353
- // player with no target created an oscillator, measured the route, found nothing and put it out — SIXTY TIMES A
354
- // SECOND. The beep did not have this problem because nothing of it lasted; permanence brought it. `nearestSpot` is a
355
- // loop over `targetsOf`, not the BFS: asking every frame costs nothing when the list is empty, which is the case here.
356
- if (!nearestSpot(pl))
357
- return null;
358
- return (pl._guide = startGuide(pl));
359
- }
360
- /**
361
- * Every FRAMES_BETWEEN_ROUTES frames the route and the side are measured again — the BFS on a cadence, not sixty times a
362
- * second. Answers `false` when the target is gone.
363
- */
364
- function remeasure(pl, g) {
365
- if (++g.framesSinceRoute < FRAMES_BETWEEN_ROUTES)
366
- return true;
367
- g.framesSinceRoute = 0;
368
- const target = nearestSpot(pl);
369
- if (!target)
370
- return false;
371
- g.steps = stepsToTarget(pl, target);
372
- g.pan = panFor(target.at.x, pl);
373
- return true;
374
- }
375
- /** EVERY frame, not only when the route is new: this is what makes the change a glide. */
376
- function glide(g, vol) {
377
- const i = guideIntensity(g.steps);
378
- try {
379
- const audioNow = g.ac.currentTime;
380
- g.filter.frequency.setTargetAtTime(i.cutoff, audioNow, GUIDE_TAU);
381
- g.gain.gain.setTargetAtTime(GUIDE_VOL * i.volume * vol, audioNow, GUIDE_TAU);
382
- g.panner?.pan.setTargetAtTime(g.pan, audioNow, GUIDE_TAU);
383
- }
384
- catch (e) { /* noop */ }
200
+ ctx.narrate(msg, pl.i);
385
201
  }
386
202
  return {
387
- playerCtx, panFor, needsAudioCues, sonar, updateGuide,
203
+ playerCtx, panFor, needsAudioCues, sonar,
388
204
  get sonarCount() { return _sonarCount; },
389
- get guideCount() { return _guideCount; },
390
205
  };
391
206
  }