@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,117 +1,142 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-or-later
2
- // ui/pause-icons — the PER-SCREEN pause menu (`.screen-pause`) and the accessibility icon bar (`.pi-btn`).
3
- // Extracted VERBATIM from game.js (Estágio 4): PAUSE_ICONS, buildScreenPause, calmMode/applyCalm, iconAct,
4
- // iconLabel, reflectIconBtn, reflectPauseIcons and hasPrivateOutput.
2
+ // ui/pause-icons — the PER-SCREEN pause card (`.screen-pause`) and the accessibility quick bar (`.pi-btn`).
5
3
  //
6
- // WHY THE ICON BAR IS ITS OWN SLICE: the ten `.pi-btn` buttons are the ONLY place in the game where a state
7
- // owned by another subsystem (blind mode, TTS, Libras, TEA/reduced-motion, toggle-keys, contrast, CVD) is both
8
- // mutated AND read back as an `aria-label`. That round-trip — "the label must tell the truth about the state" —
9
- // is the whole reason `iconLabel` exists, and it is what the pure functions below make testable in node.
4
+ // WHY THE QUICK BAR IS ITS OWN MODULE: its buttons are the ONLY place where a state owned by another subsystem (blind
5
+ // mode, TTS, Libras, calm mode, the input mode, contrast, colour correction, camera, voice, speed, language) is both
6
+ // changed AND read back as an `aria-label`. That round-trip — «the label must tell the truth about the state» — is why
7
+ // `computeIconLabel` exists, and it is what the pure functions below make testable in node.
10
8
  //
11
- // `iconAct` IS A DISPATCHER (its ctx list is long by nature: seven subsystems, one button each), so it is
12
- // implemented here as a TABLE (`ICON_ACTS`) rather than the original if/else chain. Same order, same
13
- // guards, same announcements — only the shape changed.
9
+ // Each icon's action is a row of a TABLE (`ICON_ACTS`), not a chain of `if`s: the list of subsystems is long by nature,
10
+ // one button each.
14
11
  //
15
- // WHAT STAYS IN game.js (injected):
16
- // · `pauseActor` — read by the gamepad (input/gamepad.ts ctx) and the keyboard router, and by
17
- // openHelp()/openOptions(); this module only WRITES it (`setPauseActor`).
18
- // · `pauseActs` — the `.pm-btn` action table; every entry calls a panel that still lives in game.js
19
- // (openTypo/openAudio/openMovement/motion.open/openVisual/empathy.open/printMode/
20
- // quitGame/openHelp/setPhase/applyLetra/setQuizLevel/joinPlayer/fitsN/vpScreens).
21
- // Injected LAZILY (`getPauseActs`) because it is a `const` declared ~1200 lines below
22
- // the init site — an eager reference would hit its temporal dead zone.
23
- // · `vpPause` — the array of built pause screens; game.js rebuilds it in buildGameHud() and reads it
24
- // in setPhase/navPause/printMode/pauseSelect/__incl. Injected as a getter.
25
- // · `rm`/`saveRM` — the reduced-motion flags object, co-owned with ui/settings-motion (same reference).
26
- // · `PM_BTNS`/`QL_NAME` — owned by ui/activities-menu; injected, never copied.
27
- import { t } from '../core/i18n.js';
28
- import { CONTRAST_LEVELS } from './settings-visual.js';
29
- import { CURTO_DO_TEMA, CURTO_DA_CORRECAO } from './visual-axes-panel.js';
30
- import { proximoTema, proximaCorrecao, temAltoContraste, PADRAO, } from '../render/viz-axes.js';
31
- import { anunciarItem } from './item-announcement.js';
32
- import { rotuloAcessivel } from '../core/rotulo-acessivel.js';
33
- import { passoNoAnel } from '../core/anel.js'; // da FOLHA, e não de ui/menu-nav: ver a nota lá
34
- // LIGAÇÃO VIVA (ESM): o índice pode ser desligado no menu, e o valor aqui acompanha sem assinatura.
35
- import { menuIndexOn, DEFAULTS, setModoCegoValue } from '../core/state.js';
36
- // ⚠️ IMPORT DIRETO DE `platform/storage`, e não uma peça a mais no `ctx`, e a escolha é sobre quem pode
37
- // esquecer: `initPauseIcons` é chamado pela raiz de composição de CADA jogo, e um `store` injetado é um
38
- // campo que um consumidor pode omitir — e omiti-lo faria o nível TEA voltar a não persistir, em silêncio,
39
- // exactamente no jogo que se esqueceu. É a mesma forma que `ui/fonts` usa, e `ui/` depender de `platform/`
40
- // não inverte camada nenhuma.
41
- import * as store from '../platform/storage.js';
42
- import { definirAlternanciaDeMarcha } from './settings-motor.js';
43
- import { recusaDaAlternancia } from './latch-refusal.js';
44
- import { PM_BTNS, PM_OPTIONS_BTNS } from './activities-menu.js';
45
- import { CHAVES_DE_CENA, ANIMACOES_DO_PERSONAGEM, lerCenaGuardada, guardarCena } from './motion-scene.js';
12
+ // INJECTED, not owned here:
13
+ // · `setPauseActor` — who opened the card; the panels open scoped to that player. This module only WRITES it.
14
+ // · `getPauseActs` — the `.pm-btn` action table (`ui/shell` or the host's own), asked when an item is pressed, so it
15
+ // can be wired after this module.
16
+ // · the pause screens, asked through a getter: the host rebuilds them when the number of screens changes.
17
+ // · `rm`/`saveRM` — the reduced-motion flags, shared with ui/settings-motion (same reference).
18
+ // · `matchMedia` — the browser's media query, for the reduced-motion default when nothing is stored (ADR-0232).
19
+ // · `PM_BTNS` and the other lists of buttons are `ui/pause-buttons`', imported, never copied.
20
+ import { flagOf, nextLocale, LANGUAGE_NAME } from './locale-flags.js';
21
+ /*
22
+ * 🔴 THE TWO VISUAL CYCLES MOVED HOUSE to `core/visual-cycles` (ADR-0221, issue #203). They lived in TWO modules — the list of
23
+ * levels in the panel, the steps and the names here — and the thing that costs most to maintain is the ASYMMETRY between them,
24
+ * which only reads with both in sight. See the header there.
25
+ */
26
+ import { SHORT_THEME, SHORT_CORRECTION } from './visual-axes-panel.js';
27
+ /*
28
+ * 🔴 CALM MODE MOVED HOUSE (ADR-0221, issue #203). It is not about ICONS: it is about what a child who cannot bear noise needs
29
+ * the engine to silence, and the icon is only one of the surfaces she asks through. It lives in `core/calm-mode`, a leaf
30
+ * module — zero imports, zero DOM — which is why the destructive clamp of level 1 can be measured without mounting anything.
31
+ */
32
+ import { CALM_NAMES, CALM_AUDIO_CATS, nextCalmMode, sanitiseTeaLevel, calmAudioPlan, calmMotionPlan } from '../core/calm-mode.js';
33
+ /*
34
+ * 🔴 THE MARKUP MOVED HOUSE to `ui/pause-markup` (ADR-0221, issue #203). Building the strings and wiring the elements the
35
+ * browser makes from them are two jobs that do not need each other; what is left in this file is the second one. See the
36
+ * header there for why the icon catalogue had to leave before the markup could.
37
+ */
38
+ import { quickBarMarkup, screenPauseMarkup, } from './pause-markup.js';
39
+ /*
40
+ * 🔴 THE ICON CATALOGUE MOVED HOUSE to `core/pause-icon-catalogue` (ADR-0221, issue #203): WHICH icons exist and in what order
41
+ * is DATA, and this file is about what they DO. See the header there for why the data had to leave first.
42
+ */
43
+ import { PAUSE_ICONS, pauseIcon } from '../core/pause-icon-catalogue.js';
44
+ import { nextTheme, nextCorrection, hasHighContrast, DEFAULT_VISUAL, } from '../render/viz-axes.js';
45
+ import { announceItem } from './item-announcement.js';
46
+ import { keepInView } from './menu-items.js';
47
+ import { accessibleLabel } from '../core/accessible-label.js';
48
+ import { stepInRing } from '../core/ring.js'; // from the LEAF, not from ui/menu-nav: see the note there
49
+ // A LIVE BINDING (ESM): the index can be turned off in the menu, and the value here follows without a subscription.
50
+ // The stateless half (ADR-0232): the defaults, the reduced-motion question and the 📷 cycle are vocabulary, not the store.
51
+ import { DEFAULTS, defaultReducedMotion } from '../core/setting-defaults.js';
52
+ import { nextCameraControl } from '../core/camera-cycle.js';
53
+ /** The word for each position of the 📷 cycle (ADR-0215). */
54
+ const CAMERA_MODE_NAME = { off: 'state.off', hands: 'camera.hands', face: 'camera.face', eyes: 'camera.eyes' };
55
+ /**
56
+ * The word for each position. Private again since 2026-09-21: it was exported for the motor panel's row, and the Dev took that
57
+ * row out («Tire a linha de acessibilidade motora») — the icon is the only surface of this setting now.
58
+ */
59
+ const INPUT_MODE_NAME = { standard: 'input.standard', sticky: 'input.sticky', scan: 'input.scan' };
60
+ /**
61
+ * Which of the three is showing. The SCAN WINS over the latch on purpose: with one button there is nothing to hold, so a stored
62
+ * latch would otherwise make the icon claim a position the child is not in.
63
+ */
64
+ export function inputModeOf(s) {
65
+ return s.switchScan ? 'scan' : s.toggleMove ? 'sticky' : 'standard';
66
+ }
46
67
  /**
47
- * A LEGENDA de um ícone da barra de acessibilidade — uma função, e não três cópias da mesma expressão.
68
+ * The next position. A GAME THAT HOLDS NO KEY has nothing for the latch to hold, so the icon offers standard and one button:
69
+ * a cycle never stops where nothing would happen (ADR-0155), and a word that promised what this game cannot do would teach a
70
+ * child that her setting is broken (ADR-0106 §5).
48
71
  *
49
- * Ela é escrita em TRÊS momentos que parecem diferentes e são o mesmo: o cursor direcional pousa no ícone
50
- * (`ui/menu-nav`), o dedo o aciona, e o mouse ou o foco passa por cima. Enquanto eram três linhas soltas, o
51
- * índice do ADR-0044 teria de ser acrescentado em três lugares — e a chance de um ficar para trás é a mesma
52
- * que este repositório já pagou dezesseis vezes com o `DomQuery`.
72
+ * 📌 THE DEVICE TAKES NOTHING AWAY (ADR-0249): on eyes, face, gestures and speech the latch starts as the game answers
73
+ * `holdsKeys()`, and the child may turn it on or off like on any other device — so all three positions are offered there too.
74
+ */
75
+ function inputModeOrder(holdsKeys) {
76
+ return holdsKeys ? ['standard', 'sticky', 'scan'] : ['standard', 'scan'];
77
+ }
78
+ export function nextInputMode(m, holdsKeys) {
79
+ const order = inputModeOrder(holdsKeys);
80
+ const i = order.indexOf(m);
81
+ return order[(i < 0 ? 0 : i + 1) % order.length];
82
+ }
83
+ // The rule for applying an input mode is written in the icon's own action, where it happens: the ☝️ is the only surface of
84
+ // this setting.
85
+ import { nextGameSpeed } from '../core/game-speed.js';
86
+ import { KEYS } from '../platform/storage-keys.js';
87
+ import { setMoveLatch } from './settings-mobility.js';
88
+ import { PM_BTNS, PM_OPTIONS_BTNS, PM_GAME_BTNS } from './pause-buttons.js';
89
+ import { aacMenuLocked } from './aac-sets.js';
90
+ import { SCENE_KEYS, CHARACTER_ANIMATIONS, readStoredScene, storeScene } from './motion-scene.js';
91
+ /**
92
+ * Hover and focus on a bar's icons write its NAME in the bar's `.pause-icons-cap` and ask for its EXPLANATION.
53
93
  *
54
- * O `aria-label` já conta o ESTADO ("Alto contraste, ativado"): é ele que o `reflectIconBtn` reescreve a cada
55
- * mudança, e é por isso que a legenda o lê de volta em vez de recompor o texto por conta própria.
94
+ * Shared by the per-screen quick bars and the bar the engine mounts in `#title-icons`: two copies of this wiring is
95
+ * how one bar showed the name and the other did not — the Dev found the engine's bar silent on hover and on the
96
+ * cursor. Leaving an icon falls back to the CURSOR of the bar's mode when there is one (`.pi-sel`): it is the only
97
+ * thing saying where that cursor is.
56
98
  */
57
- export function legendaDoIcone(barra, el) {
58
- const icones = [...barra.querySelectorAll('.pi-btn')];
59
- // A regra "rótulo declarado vence" nasceu AQUI e valia só para os dez ícones. Virou `core/rotulo-acessivel`
60
- // e agora vale para o menu inicial e para a lista de pausa também — uma resposta para "como se chama este
61
- // controle", e não três.
62
- return anunciarItem({ rotulo: rotuloAcessivel(el), posicao: icones.indexOf(el) + 1, total: icones.length }, menuIndexOn);
99
+ export function wireBarCaption(bar, explain) {
100
+ const cap = bar.querySelector('.pause-icons-cap');
101
+ const captionFor = (b) => {
102
+ if (cap)
103
+ cap.textContent = accessibleLabel(b); // name and state only: «N de M» is spoken, never written (ADR-0167)
104
+ explain(b.dataset.pi ?? null);
105
+ };
106
+ const restoreCaption = () => {
107
+ const cursor = bar.querySelector('.pi-sel');
108
+ if (cursor) {
109
+ captionFor(cursor);
110
+ return;
111
+ }
112
+ if (cap)
113
+ cap.textContent = '';
114
+ explain(null);
115
+ };
116
+ bar.querySelectorAll('.pi-btn').forEach((b) => {
117
+ b.addEventListener('mouseenter', () => captionFor(b));
118
+ b.addEventListener('focus', () => captionFor(b));
119
+ b.addEventListener('mouseleave', restoreCaption);
120
+ b.addEventListener('blur', restoreCaption);
121
+ });
63
122
  }
64
- /** The accessibility shortcut bar at the top of every pause screen (and of the splash `#title-icons`).
65
- * Sound-bound icons (blind/TTS) require a private audio output; webcam/voice are still `soon`.
66
- * VERBATIM from game.js in order and behaviour; the names became i18n keys in the Fase-5 pass. */
67
- // `n` é a CHAVE i18n do nome do ícone (o emoji `e` não traduz — é o mesmo glifo em toda língua).
68
- export const PAUSE_ICONS = [
69
- { k: 'blind', e: '🦯', n: 'icon.blind' },
70
- { k: 'tts', e: '🗨️', n: 'icon.tts' },
71
- { k: 'libras', e: '🤟', n: 'icon.libras' },
72
- { k: 'tea', e: '🧩', n: 'icon.tea' },
73
- { k: 'altmove', e: '☝️', n: 'icon.altmove' }, // ☝️ e não 🦾: o gesto é UM DEDO tocando, que é o que a alternância pede (pedido do Dev)
74
- { k: 'contrast', e: '🌗', n: 'icon.contrast' },
75
- { k: 'cvd', e: '🚥', n: 'icon.cvd' },
76
- { k: 'face', e: '🧑', n: 'icon.face', soon: true },
77
- { k: 'eyes', e: '👀', n: 'icon.eyes', soon: true },
78
- { k: 'voice', e: '👄', n: 'icon.voice', soon: true },
79
- ];
80
- const ICON_BY_KEY = new Map(PAUSE_ICONS.map((ic) => [ic.k, ic]));
81
- export function pauseIcon(k) { return ICON_BY_KEY.get(k); }
82
- /** TEA cycle: 0 = normal · 1 = calmo (reduces) · 2 = silencioso (switches off). Never touches TTS/blind mode. */
83
- export const CALM_NAMES = ['calm.off', 'calm.quiet', 'calm.silent'];
84
123
  /**
85
- * O nível TEA guardado, saneado. Fora de 0..2 devolve o padrão — dado do navegador é dado de fora, e um
86
- * nível inventado escolheria `CALM_NAMES[3]`, que é `undefined`, e o anúncio ao leitor de tela sairia vazio.
124
+ * What an icon of the quick bar SAYS — one function, not three copies of the same expression. It is spoken at moments
125
+ * that look different and are the same (the cursor lands on the icon, a finger activates it), and the `aria-label` it
126
+ * reads already carries the STATE («Alto contraste, ativado»): `reflectIconBtn` rewrites it on every change, which is why
127
+ * this reads it back instead of rebuilding the text.
87
128
  */
88
- export function saneiaNivelTea(bruto) {
89
- return Number.isInteger(bruto) && bruto >= 0 && bruto < CALM_NAMES.length ? bruto : DEFAULTS.calmMode;
129
+ function iconCaption(t, barEl, el, menuIndexOn) {
130
+ const icons = [...barEl.querySelectorAll('.pi-btn')];
131
+ // «A declared label wins» is `core/accessible-label`'s rule, shared with the pause list and every menu: one answer to
132
+ // «what is this control called».
133
+ return announceItem(t, { label: accessibleLabel(el), position: icons.indexOf(el) + 1, total: icons.length }, menuIndexOn);
90
134
  }
91
- /** Lê o nível TEA do armazenamento. Chamado no `init`, nunca no import. */
92
- function lerNivelTea() {
93
- return saneiaNivelTea(store.getNum(store.KEYS.tea, DEFAULTS.calmMode));
135
+ /** Reads the calm level from storage, sanitised. Called in `init`, never on import. */
136
+ function readTeaLevel(store) {
137
+ return sanitiseTeaLevel(store.getNum(KEYS.tea, DEFAULTS.calmMode), DEFAULTS.calmMode);
94
138
  }
95
- /** The audio categories `applyCalm` governs. TTS/sonar/guarda/guia stay untouched — a calm player still needs them. */
96
- export const CALM_AUDIO_CATS = ['ambient', 'music', 'earcons', 'other', 'interact'];
97
- /** Colour-vision-deficiency cycle, in `player.viz` values. */
98
- export const CVD_SEQ = ['normal', 'fix-protan', 'fix-deuter', 'fix-tritan'];
99
- /** i18n keys of the CVD announcement names, indexed the same as CVD_SEQ.
100
- *
101
- * ⚠️ POSITION 0 IS `cvd.tricro` AND NOT `cvd.off`, AND THE TWO KEYS ARE NOT INTERCHANGEABLE.
102
- * This list names the four CHOICES of the cycle, so position 0 is a way of seeing — trichromatic
103
- * vision, the one that needs no correction — and it is said as one. `cvd.off` below is a FALLBACK
104
- * for `s.viz` values that are not corrections at all, and 13 of the 16 viz modes are exactly that:
105
- * the three simulations, the three contrast levels, the five low-vision modes and blind mode.
106
- * Announcing "trichromatic vision" there would have the software assert what the child sees, while
107
- * she is simulating not seeing it. The choice is named; the fallback is switched off. */
108
- export const CVD_NAMES = ['cvd.tricro', 'cvd.protan', 'cvd.deuter', 'cvd.tritan'];
109
- /** `player.viz` → i18n key of the label used by iconLabel (anything else falls back to the 'off' key). */
110
- export const CVD_LABELS = {
111
- 'fix-protan': 'cvd.protan', 'fix-deuter': 'cvd.deuter', 'fix-tritan': 'cvd.tritan',
112
- };
113
- /** A player has private output when nobody else is on the same sink. Single screen ⇒ always private.
114
- * Pure form of game.js's hasPrivateOutput (see the report: it had no other caller left). */
139
+ /** A player has private output when nobody else is on the same sink. Single screen ⇒ always private. */
115
140
  export function hasPrivateOutputIn(list, count, i) {
116
141
  if (count <= 1)
117
142
  return true;
@@ -120,234 +145,216 @@ export function hasPrivateOutputIn(list, count, i) {
120
145
  return false;
121
146
  return !list.some((q, j) => j !== i && q && q.audioSink === p.audioSink);
122
147
  }
123
- /** TEA cycle step. */
124
- export function nextCalmMode(cur) { return (cur + 1) % 3; }
125
- /** Next high-contrast level. An unlisted `viz` (e.g. a CVD filter) is treated as index 0 ⇒ jumps to 'hc-direto'. */
126
- export function nextContrast(cur) {
127
- let idx = CONTRAST_LEVELS.indexOf(cur);
128
- idx = idx < 0 ? 0 : idx;
129
- return CONTRAST_LEVELS[(idx + 1) % CONTRAST_LEVELS.length];
130
- }
131
- /** Next CVD filter. NOTE the asymmetry with nextContrast: an unlisted `viz` maps to index 1 ('fix-protan'),
132
- * not 0 — verbatim from game.js (`idx = idx<0 ? 1 : (idx+1)%seq.length`). */
133
- export function nextCvd(cur) {
134
- let idx = CVD_SEQ.indexOf(cur);
135
- idx = idx < 0 ? 1 : (idx + 1) % CVD_SEQ.length;
136
- return { idx, mode: CVD_SEQ[idx] };
137
- }
138
- /** What `applyCalm` does to ONE audio category, given the TEA level. Extracted so the (destructive) volume
139
- * clamp at level 1 is visible and testable — see the report. */
140
- export function calmAudioPlan(calmMode, vol) {
141
- if (calmMode === 0)
142
- return { on: true, vol };
143
- if (calmMode === 1)
144
- return { on: true, vol: Math.min(vol, 0.3) };
145
- return { on: false, vol };
146
- }
147
- /** What `applyCalm` does to the scene/character reduced-motion flags. */
148
- export function calmMotionPlan(calmMode) {
149
- return { sceneReduced: calmMode >= 1, charFrozen: calmMode === 2 };
150
- }
151
148
  /** The `aria-label` of one icon — it MUST reflect the current state, on/off or level. This is the whole
152
149
  * point of the function: a toggle that looks pressed but does not say so is invisible to a screen reader. */
153
- export function computeIconLabel(k, s) {
154
- const ic = ICON_BY_KEY.get(k);
150
+ export function computeIconLabel(t, k, s) {
151
+ const ic = pauseIcon(k);
155
152
  if (!ic)
156
153
  return '';
157
- // O estado vira SEMPRE um parâmetro (`{v}`), nunca uma concatenação: 'on'/'off' eram palavras inglesas
158
- // presas numa frase em português, e uma língua que anteponha o estado ao nome precisa do dicionário para
159
- // reordenar. `nomeDoIcone: estado` é a moldura; o estado é o conteúdo, e ele também é traduzido.
160
- if (ic.soon)
161
- return t('icon.soon', { nome: t(ic.n) });
162
- const rotulo = (v) => t('icon.state', { nome: t(ic.n), v: t(v) });
163
- if (k === 'blind')
164
- return rotulo(s.modoCego ? 'state.on' : 'state.off');
165
- if (k === 'tts')
166
- return rotulo(s.ttsOn ? 'state.on' : 'state.off');
167
- if (k === 'libras')
168
- return rotulo(s.librasOn ? 'state.on' : 'state.off');
169
- // TEA e daltonismo usam um nome CURTO aqui, diferente do nome do botão: o rótulo já diz o nível, e
170
- // repetir a lista de níveis do nome ("(calmo / silencioso)", "(protan/deutan/tritan)") a diria duas vezes.
171
- // Era assim antes da conversão, com o texto curto embutido — preservado, não reinventado.
172
- if (k === 'tea')
173
- return t('icon.state', { nome: t('icon.tea.short'), v: t(CALM_NAMES[s.calmMode]) });
174
- if (k === 'altmove')
175
- return rotulo(s.toggleMove ? 'state.on' : 'state.off');
176
- if (k === 'contrast')
177
- return rotulo(CURTO_DO_TEMA[s.visual.tema]);
178
- if (k === 'cvd')
179
- return t('icon.state', { nome: t('icon.cvd.short'), v: t(CURTO_DA_CORRECAO[s.visual.correcao]) });
180
- return t(ic.n);
154
+ const state = STATE_OF_ICON[k];
155
+ // The state is ALWAYS a parameter (`{v}`), never a concatenation: a language that puts the state before the name needs
156
+ // the dictionary to reorder it. «name: state» is the frame; the state is the content, and it is translated too.
157
+ return state ? t('icon.state', { nome: t(SHORT_NAME[k] ?? ic.n), v: state(t, s) }) : t(ic.n);
181
158
  }
182
- /** Pure form of reflectIconBtn's branching. A `soon` icon lands on all-false — it never claims to be on. */
159
+ const onOff = (t, on) => t(on ? 'state.on' : 'state.off');
160
+ /** What each icon with a state says after its name, already in the child's language. An icon not here has no state to say. */
161
+ const STATE_OF_ICON = {
162
+ blind: (t, s) => onOff(t, s.blindMode),
163
+ tts: (t, s) => onOff(t, s.ttsOn),
164
+ libras: (t, s) => onOff(t, s.librasOn),
165
+ tea: (t, s) => t(CALM_NAMES[s.calmMode]),
166
+ altmove: (t, s) => t(INPUT_MODE_NAME[inputModeOf(s)]),
167
+ contrast: (t, s) => t(SHORT_THEME[s.visual.tema]),
168
+ cvd: (t, s) => t(SHORT_CORRECTION[s.visual.correcao]),
169
+ camera: (t, s) => t(CAMERA_MODE_NAME[s.camera ?? 'off']),
170
+ voice: (t, s) => onOff(t, s.voice),
171
+ // a language's own name is not translated: «Español» reads the same in every interface
172
+ idioma: (_t, s) => LANGUAGE_NAME[(s.locale ?? 'pt')] ?? LANGUAGE_NAME.pt,
173
+ velocidade: (t, s) => t('icon.velocidade.valor', { pct: Math.round((s.speed ?? 1) * 100) }),
174
+ };
175
+ /**
176
+ * Calm mode and colour correction have a SHORT name key for the label, because the button's name once carried the list of
177
+ * levels («(calmo / silencioso)», «(protan/deutan/tritan)») and the label already says the level. ⚠️ In the three
178
+ * dictionaries the short key now equals the long one word for word: the distinction has no subject left, and merging the
179
+ * keys is a dictionary change.
180
+ */
181
+ const SHORT_NAME = { tea: 'icon.tea.short', cvd: 'icon.cvd.short' };
182
+ const ICON_VISUAL = Object.freeze({
183
+ blind: (s) => ({ on: s.blindMode, dis: !s.privateOutput }),
184
+ tts: (s) => ({ on: s.ttsOn, dis: !s.privateOutput || !!s.noVoice }),
185
+ libras: (s) => ({ on: s.librasOn }),
186
+ tea: (s) => ({ on: s.calmMode === 2, calm: s.calmMode === 1 }),
187
+ // ⚠️ AND IT IS NEVER GREYED OUT: every device may be in any of its positions (ADR-0249), and what a game cannot hold the
188
+ // CYCLE tells by not offering it — greying the icon would take the one-button scan away with it.
189
+ altmove: (s) => ({ on: inputModeOf(s) !== 'standard' }),
190
+ contrast: (s) => ({ on: hasHighContrast(s.visual) }),
191
+ velocidade: (s) => ({ on: (s.speed ?? 1) < 1 }),
192
+ camera: (s) => ({ on: (s.camera ?? 'off') !== 'off' }),
193
+ voice: (s) => ({ on: !!s.voice }),
194
+ // ⚠️ THE TWO-TONE BACKGROUND IS THIS ICON'S «ON» SIGNAL, and it reads the correction AXIS — so it still says the same when
195
+ // the high-contrast theme is on too, and the child's correction does not vanish from the icon that exists to show it.
196
+ cvd: (s) => ({ cvd: s.visual.correcao !== 'tricro' ? `pi-cvd-${s.visual.correcao}` : '' }),
197
+ });
198
+ /** At rest: an icon the table does not name shows nothing. */
199
+ const ICON_VISUAL_AT_REST = Object.freeze({ on: false, dis: false, calm: false, cvd: '', active: false });
200
+ /** Pure form of reflectIconBtn's branching. */
183
201
  export function computeIconVisual(k, s) {
184
- let on = false, dis = false, calm = false, cvd = '';
185
- if (k === 'blind') {
186
- on = s.modoCego;
187
- dis = !s.privateOutput;
188
- }
189
- else if (k === 'tts') {
190
- on = s.ttsOn;
191
- dis = !s.privateOutput;
192
- }
193
- else if (k === 'libras') {
194
- on = s.librasOn;
195
- }
196
- else if (k === 'tea') {
197
- on = s.calmMode === 2;
198
- calm = s.calmMode === 1;
199
- }
200
- else if (k === 'altmove') {
201
- on = s.toggleMove;
202
- dis = !!s.alternanciaExigida;
203
- }
204
- else if (k === 'contrast') {
205
- on = temAltoContraste(s.visual);
206
- }
207
- else if (k === 'cvd') {
208
- // ⚠️ O FUNDO DE DUAS CORES É O SINAL DE LIGADO deste ícone, e agora ele lê o EIXO da correção — que
209
- // continua a dizer o mesmo quando o tema também está ligado, coisa que a chave única não conseguia: com
210
- // `hc-direto-7` no campo, a correção da criança desaparecia do ícone que existe para a mostrar.
211
- if (s.visual.correcao !== 'tricro')
212
- cvd = 'pi-cvd-' + s.visual.correcao;
213
- }
214
- return { on, dis, calm, cvd, active: on || calm || !!cvd };
202
+ const v = { ...ICON_VISUAL_AT_REST, ...ICON_VISUAL[k]?.(s) };
203
+ // 📌 `active` is DERIVED, declared by no rule: it is the `aria-pressed`, and no icon should be able to say it is pressed
204
+ // without showing why.
205
+ return { ...v, active: v.on || v.calm || !!v.cvd };
215
206
  }
216
207
  /** The CSS classes reflectIconBtn clears before applying a fresh visual — in the original order. */
217
208
  export const ICON_STATE_CLASSES = ['pi-calm', 'pi-cvd-protan', 'pi-cvd-deuter', 'pi-cvd-tritan'];
218
- // --- markup (pure string builders; the DOM shell below just assigns them) ---
219
- /** One `.pi-btn`. `soon` icons get `.pi-soon` and the "under construction" suffix baked into the aria-label.
220
- * The label is the RESTING one: reflectIconBtn overwrites it with the stateful label as soon as the bar is
221
- * reflected. `ic.n` is an i18n key, so it must be resolved here too — the markup is rendered once at build
222
- * time and would otherwise ship the raw key to a screen reader. */
223
- export function iconBtnMarkup(ic) {
224
- return '<button class="pi-btn' + (ic.soon ? ' pi-soon' : '') + '" type="button" data-pi="' + ic.k +
225
- '" aria-label="' + (ic.soon ? t('icon.soon', { nome: t(ic.n) }) : t(ic.n)) + '">' + ic.e + '</button>';
226
- }
227
- export function iconesQueAccionam(escritores) {
228
- // ⚠️ POR ÍCONE, e não um booleano para os dois — e foi uma MUTAÇÃO SOBREVIVENTE que o mostrou. Com uma
229
- // única bandeira, `&&` e `||` produziam o mesmo resultado nos casos que eu tinha escrito, porque todos
230
- // tiravam os DOIS escritores. O `&&` escondia um ícone que FUNCIONA quando só um escritor falta, e o `||`
231
- // mostrava um que NÃO funciona. Os dois erram, em direcções opostas, e a pergunta certa nunca foi «este
232
- // jogo tem escritores visuais» — é «este ÍCONE tem quem o accione».
233
- // 📌 E o `altmove` entra pela MESMA porta, que é o achado: «este jogo segura teclas?» é a mesma pergunta
234
- // que «este ícone tem quem o accione», feita a um campo do contrato em vez de a um escritor injectado.
235
- // Um terceiro ramo, e não uma regra nova.
236
- return PAUSE_ICONS.filter((ic) => (ic.k === 'contrast' ? escritores.tema
237
- : ic.k === 'cvd' ? escritores.correcao
238
- : ic.k === 'altmove' ? escritores.seguraTeclas()
239
- : true));
240
- }
241
209
  /**
242
- * OS TRÊS ITENS QUE A ENGINE ACCIONA SOZINHA, e que por isso nunca dependem do `getPauseActs` de um jogo.
210
+ * WHO ACTIVATES EACH ICON — one question per icon, and an icon nothing locks is offered.
243
211
  *
244
- * 📏 Lidos do despacho, e não decididos aqui: `options`/`pmback` trocam qual lista está no cartão e
245
- * `acessibilidade` leva o cursor à barra rápida — os três são tratados neste módulo e voltam antes de a
246
- * tabela do jogo ser consultada.
212
+ * PER ICON, not one flag for the whole bar: with one flag, a single missing writer would either hide an icon that WORKS
213
+ * or show one that does NOT. The question is «does this ICON have something to activate it». A table, so adding an icon
214
+ * is adding a row.
247
215
  */
248
- export const ITENS_DA_ENGINE = new Set(['options', 'pmback', 'acessibilidade']);
216
+ const ICON_IS_ACTIONABLE = Object.freeze({
217
+ contrast: (w) => w.theme,
218
+ cvd: (w) => w.correction,
219
+ // «Does this game hold keys?» is the same question, asked of a contract field rather than an injected writer.
220
+ // AND A SECOND HALF (ADR-0218): «one button only» has a subject wherever the game declares a position to scan, so a game
221
+ // holding no key (the quiz demo) still has a ☝️. A game that declares nothing has none: a scan of nothing is the dead
222
+ // button of ADR-0106 §5 paid for in seconds.
223
+ altmove: (w) => w.holdsKeys() || (w.declaredPositions?.() ?? 0) > 0,
224
+ // The same question asked of the typography cycle (ADR-0149): the icon exists when someone knows how to walk it.
225
+ // `Boolean(...)` and not the raw field: it is optional, and the conversion is written rather than left to a `filter`.
226
+ tipografia: (w) => Boolean(w.typography),
227
+ // the hourglass exists where time runs by itself (ADR-0180): a turn game has nothing to slow
228
+ velocidade: (w) => Boolean(w.clock?.()),
229
+ camera: (w) => Boolean(w.camera),
230
+ // 👄 exists where there is a MICROPHONE to ask for, the same rule as the camera's (ADR-0106 §5)
231
+ voice: (w) => Boolean(w.microphone),
232
+ menu: (w) => Boolean(w.menus),
233
+ });
234
+ export function iconsThatAct(writers) {
235
+ // ABSENCE FROM THE TABLE MEANS YES: an icon no rule locks depends on nobody to work, and hiding it for lack of a row
236
+ // would take away from the child a way that exists.
237
+ return PAUSE_ICONS.filter((ic) => ICON_IS_ACTIONABLE[ic.k]?.(writers) ?? true);
238
+ }
249
239
  /**
250
- * OS ITENS DO MENU QUE ESTE JOGO CONSEGUE MESMO ACCIONAR (ADR-0106 §5).
251
- *
252
- * ⚠️ HOJE UM ITEM SEM ACÇÃO É UM BOTÃO MORTO, E EM SILÊNCIO. O despacho faz `const fn = acts[act]; if (fn)
253
- * fn();` — quem carrega num item que o jogo não implementou não recebe erro, não recebe anúncio, não recebe
254
- * nada. Para quem vê, parece que o clique falhou; para quem navega por leitor de tela, o menu leu-lhe um
255
- * item que não existe. É exactamente o que o §5 chama de pior do que a ausência: «uma barra que oferece um
256
- * caminho e depois o recusa ensina-lhe que o caminho não é para ela».
240
+ * THE ITEMS THE ENGINE ACTIVATES BY ITSELF, which therefore never depend on a game's `getPauseActs`. Read from the
241
+ * dispatch, not decided here: `options`, `opcoesdojogo` and `pmback` change which list is on the card, and
242
+ * `acessibilidade` takes the cursor to the quick bar — all four are handled in this module before the game's table is
243
+ * consulted. What belongs to the game is the CONTENT of the list `opcoesdojogo` opens.
244
+ */
245
+ const ENGINE_ITEMS = new Set(['options', 'opcoesdojogo', 'pmback', 'acessibilidade']);
246
+ /**
247
+ * Why a pause item is locked, in the child's words (ADR-0161). A key per item where the reason is particular to it —
248
+ * «Número de jogadores»: the GAME decides how many (ADR-0147) — and one general reason for the rest. Resolved at every
249
+ * refresh, so it follows the language of the moment the card opens.
257
250
  */
258
- export function itensQueAccionam(botoes, acts) {
259
- return botoes.filter((b) => ITENS_DA_ENGINE.has(b.act) || typeof acts[b.act] === 'function');
251
+ const OWN_REASONS = new Set(['ajuda', 'addplayer', 'opcoesdojogo', 'caa']);
252
+ function itemReason(t, act) {
253
+ return t(OWN_REASONS.has(act) ? `pause.motivo.${act}` : 'pause.motivo');
260
254
  }
261
255
  /**
262
- * A LISTA RAIZ, com uma regra a mais: `options` é uma PORTA, e uma porta para uma sala vazia também é um
263
- * botão morto.
264
- *
265
- * ⚠️ Esta é a parte que um filtro item-a-item não apanha. Se todos os painéis de ajuste forem filtrados —
266
- * um jogo que não monta nenhum —, o item `options` sobrevive (a engine acciona-o) e abre uma lista sem nada.
267
- * A criança atravessa uma porta e fica presa num submenu vazio, cuja única saída é o `pmback` que também
268
- * sumiu com ele.
256
+ * A door the ENGINE keeps locked whatever the game hands over. Today one: `caa`, the AAC menu, disabled until a
257
+ * pictogram set is licensed (ADR-0233 erratum). Its reason is only «menu disabled» — no licence explanation reaches the
258
+ * child — and a game's own `caa` action does not unlock it.
269
259
  */
270
- export function raizQueAcciona(raiz, opcoes, acts) {
271
- const opcoesVivas = itensQueAccionam(opcoes, acts).filter((b) => b.act !== 'pmback');
272
- const viva = itensQueAccionam(raiz, acts);
273
- return opcoesVivas.length > 0 ? viva : viva.filter((b) => b.act !== 'options');
260
+ function lockedByEngine(act) {
261
+ return act === 'caa' && aacMenuLocked();
274
262
  }
275
- /** The whole icon bar. Used by the pause screen AND by the splash `#title-icons` (which built the same string
276
- * by hand in game.js — that duplication dies with this export).
277
- * O parâmetro é ADITIVO e o padrão é a lista inteira: quem já chamava sem argumentos não muda de resultado. */
278
- export function iconsMarkup(icones = PAUSE_ICONS) {
279
- return icones.map(iconBtnMarkup).join('');
263
+ /**
264
+ * THE MENU ITEMS THIS GAME CAN ACTUALLY ACTIVATE. The others are not hidden: they stay on the card, LOCKED with their
265
+ * reason said (ADR-0161) — an item that does nothing when pressed, and says nothing, reads to a screen-reader user as an
266
+ * item that does not exist.
267
+ */
268
+ function itemsThatAct(buttons, acts) {
269
+ return buttons.filter((b) => !lockedByEngine(b.act) && (ENGINE_ITEMS.has(b.act) || typeof acts[b.act] === 'function'));
280
270
  }
281
- /** One `.pm-btn`. Dynamic labels (`letra`/`nivel`) are rendered eagerly and carry NO `data-i18n`, so
282
- * i18n.applyDom() cannot overwrite them. */
283
271
  /**
284
- * ⚠️ O RÓTULO DINÂMICO ENTRA PRONTO (item 19), e a mudança conserta DUAS coisas de uma vez.
285
- *
286
- * A linha era `'📚 Nível ' + level + ' · ' + qlName[level]` — e ela tinha dois defeitos que só se enxergam
287
- * juntos:
288
- *
289
- * 1. FRONTEIRA. `level` vinha de `core/state.quizLevel` e `qlName` de uma tabela do jogo. Um menu de pausa
290
- * da ENGINE montava o rótulo de uma atividade de alfabetização — conteúdo pedagógico, não mecânica.
291
- * 2. IDIOMA. "Nível" é pt-BR CRU dentro de um módulo de engine. O gate do item 14 vigia o `main.js` e não
292
- * alcança `ui/`, então esta linha atravessou a i18n inteira sem ser vista. Num build em inglês, o menu
293
- * de pausa de uma criança dizia "📚 Nível 2 · …".
294
- *
295
- * Agora o jogo entrega a frase montada (`dynLabel`), e a engine só a coloca no botão. O jogo é quem sabe o
296
- * que é um nível, quem sabe o nome dele e quem sabe em que idioma dizê-lo.
272
+ * THE ROOT LIST, with one more rule: `options` is a DOOR, and a door to an empty room acts no more than a dead button.
273
+ * An item-by-item filter misses this: the engine activates `options` itself, so it would pass and open a list with
274
+ * nothing in it.
297
275
  */
298
- export function pmBtnMarkup(b, dynLabel, tr) {
299
- const dyn = b.letra || b.nivel;
300
- const lbl = dynLabel(b) ?? (dyn ? (b.lbl ?? '') : tr('pause.' + b.act));
301
- return '<button class="pm-btn' + (b.letra ? ' pm-letra' : '') + (b.nivel ? ' pm-nivel' : '') +
302
- '" role="menuitem" type="button" data-act="' + b.act + '"' +
303
- (dyn ? '' : (' data-i18n="pause.' + b.act + '"')) + '>' + lbl + '</button>';
276
+ export function rootThatActs(rootEl, options, acts,
277
+ // An OPTIONAL fourth argument defaulting to the EMPTY list: a game that declares nothing of its own is exactly that
278
+ // case, so the default is also the right answer.
279
+ fromGame = []) {
280
+ const howManyAct = (bs) => itemsThatAct(bs, acts).filter((b) => b.act !== 'pmback').length;
281
+ let alive = itemsThatAct(rootEl, acts);
282
+ if (howManyAct(options) === 0)
283
+ alive = alive.filter((b) => b.act !== 'options');
284
+ // The same rule for the game's own door (ADR-0146): a game with nothing of its own gets no live «game options».
285
+ // ADR-0182: a door whose room the ENGINE draws from the cartridge's rows is live through its action, with no list behind it
286
+ if (howManyAct(fromGame) === 0 && typeof acts.opcoesdojogo !== 'function')
287
+ alive = alive.filter((b) => b.act !== 'opcoesdojogo');
288
+ return alive;
304
289
  }
305
290
  /**
306
- * Os itens navegáveis de um cartão de pausa — os da lista VISÍVEL, e só eles.
291
+ * The navigable items of a pause card — those of the VISIBLE list, and only those.
307
292
  *
308
- * Uma constante porque TRÊS módulos a consultam (a navegação em `ui/menu-nav`, a seleção inicial em
309
- * `ui/shell` e a troca de submenu aqui). Enquanto fosse `.pm-btn` escrito três vezes, bastaria um deles
310
- * esquecer o `:not([hidden])` para o anel atravessar para a lista invisível — e a criança ouviria itens de um
311
- * menu que não está na tela.
293
+ * One constant because THREE modules ask (navigation in `ui/menu-nav`, the first selection in `ui/shell` and the list
294
+ * switch here): if one of them forgot `:not([hidden])`, the ring would step into the invisible list and the child would
295
+ * hear items of a menu that is not on screen. `:not([hidden])` on the ITEM too: a hidden item must not be reached nor
296
+ * counted in «N de M» (a locked one is not hidden — it is reached, and says why).
312
297
  */
313
- export const PM_ITENS_VISIVEIS = '.pause-menu:not([hidden]) .pm-btn';
314
- /** O innerHTML de UMA `.pause-menu`: a lista, e só ela. */
315
- export function pauseMenuHtml(bs, sub, dynLabel, tr) {
316
- return '<div class="pause-menu" role="menu" data-sub="' + sub + '"' + (sub === 'raiz' ? '' : ' hidden') + '>' +
317
- bs.map((b) => pmBtnMarkup(b, dynLabel, tr)).join('') + '</div>';
318
- }
298
+ export const PM_VISIBLE_ITEMS = '.pause-menu:not([hidden]) .pm-btn:not([hidden])';
299
+ /**
300
+ * THE DOORS INSIDE THE CARD, and which list each opens. They do nothing to the game — they change which list is on
301
+ * screen — so they live here and not in the action table, which lives in `ui/shell` and does not know the card.
302
+ * `pmback` always returns to the ROOT, from either list. A table and not a chain of `if`s: a fourth list is one more
303
+ * row, not one more branch.
304
+ */
305
+ const DOOR_TO_LIST = Object.freeze({ options: 'opcoes', opcoesdojogo: 'jogo', pmback: 'raiz' });
306
+ /**
307
+ * The other direction of the same table: the item of the root that opens each list. It is where «back» puts the cursor
308
+ * (ADR-0130 rule 1), and the name a list is announced by (`ui/where-the-child-is`). The root has no door.
309
+ */
310
+ export const LIST_DOOR = Object.freeze({ raiz: null, opcoes: 'options', jogo: 'opcoesdojogo' });
319
311
  /**
320
- * Troca a lista visível de UM cartão de pausa, e põe o cursor no PRIMEIRO item da lista que entrou.
312
+ * Switches the visible list of ONE pause card, and puts the cursor on the FIRST item of the list that came in.
321
313
  *
322
- * Livre (e não um método do `init`) de propósito: `ui/menu-nav` precisa dela para o "não" voltar da lista de
323
- * opções à raiz, e não tem acesso às tabelas de botões. Como as duas listas já existem no markup, a troca é
324
- * só DOM — nada a re-renderizar, nada a injetar.
314
+ * Free-standing (not a method of `init`) on purpose: `ui/menu-nav` needs it for «no» to go back to the root, and it has no
315
+ * access to the button tables. Every list already exists in the markup, so the switch is DOM only.
325
316
  */
326
- export function mostrarSubmenuDaPausa(sp, sub) {
317
+ export function showPauseOptions(sp, sub) {
327
318
  sp.querySelectorAll('.pause-menu').forEach((m) => { m.hidden = m.dataset.sub !== sub; });
328
- const primeiro = sp.querySelector(PM_ITENS_VISIVEIS);
329
- sp.querySelectorAll('.pm-sel,.pi-sel').forEach((b) => b.classList.remove('pm-sel', 'pi-sel'));
330
- if (primeiro)
331
- primeiro.classList.add('pm-sel');
332
- return primeiro;
319
+ const first = sp.querySelector(PM_VISIBLE_ITEMS);
320
+ sp.querySelectorAll('.pi-sel').forEach((b) => b.classList.remove('pi-sel'));
321
+ markPauseItem(sp, first);
322
+ return first;
333
323
  }
334
324
  /**
335
- * A BARRA RÁPIDA DE ACESSIBILIDADE — dez alternadores e a legenda que os explica (ADR-0044, item 7).
336
- *
337
- * Saiu do cartão de pausa e passou a viver no HUD. O motivo é de uso, não de arrumação: é DURANTE a partida
338
- * que uma criança precisa mudar um ajuste que está a atrapalhando, e não depois de pausar. E o motivo
339
- * secundário é estrutural — sem ela, o cartão deixa de ter duas zonas e vira uma LISTA, o que é o que
340
- * finalmente autoriza o anel (a XAG 106 permite laço para menu linear e o proíbe para grade).
325
+ * PUTS THE CARD'S CURSOR ON `el` — the one place every move of it goes through, whatever moved it: the arrows and the pad
326
+ * (`ui/menu-nav`), a finger, a held press, a door between lists, «back». The card selects by class, not by browser focus,
327
+ * so nothing scrolls it by itself: an item past the visible part of a long list would be marked where the child cannot see
328
+ * it. So the list is scrolled HERE, inside the card, until the item is wholly in view (issue #134, `keepInView`).
329
+ */
330
+ export function markPauseItem(sp, el) {
331
+ if (!el)
332
+ return;
333
+ sp.querySelectorAll('.pm-sel').forEach((b) => b.classList.remove('pm-sel'));
334
+ el.classList.add('pm-sel');
335
+ keepInView(el);
336
+ }
337
+ /**
338
+ * «BACK» FROM A LIST OF THE CARD: the root comes in with the cursor on THE ITEM THAT OPENED the list being left, not on
339
+ * the root's first item (ADR-0130 rule 1). A child who went into «Opções», looked, and came back is where she was; landing
340
+ * on «Voltar» would send her walking the root again to find her place, and a child who cannot see the mark would not even
341
+ * know she had been moved.
341
342
  *
342
- * A LEGENDA VIAJA JUNTO. Ela é a dica que substitui, para quem não vê, o `title` que só o mouse revela;
343
- * deixá-la no cartão tornaria a barra do HUD muda.
343
+ * Opening the card is another question and keeps `showPauseOptions(sp, 'raiz')`: a card opens on item 1 (ADR-0158).
344
+ * A door that is gone or hidden leaves the cursor on the first item, which is all there is to return to.
344
345
  */
345
- export function quickBarMarkup(icones = PAUSE_ICONS) {
346
- return '<div class="pause-icons" role="group" aria-label="' + t('pause.iconBarAria') + '">' + iconsMarkup(icones) +
347
- '</div><p class="pause-icons-cap" aria-live="polite"></p>';
346
+ export function backToRoot(sp) {
347
+ const left = sp.querySelector('.pause-menu:not([hidden])')?.dataset.sub;
348
+ const first = showPauseOptions(sp, 'raiz');
349
+ const door = left ? LIST_DOOR[left] : null;
350
+ const opener = door ? [...sp.querySelectorAll(PM_VISIBLE_ITEMS)].find((b) => b.dataset.act === door) : undefined;
351
+ if (!opener || opener === first)
352
+ return first;
353
+ markPauseItem(sp, opener);
354
+ return opener;
348
355
  }
349
- export function acaoNaBarra(k, temStart) {
350
- if (temStart || k.no)
356
+ export function barAction(k, hasStart) {
357
+ if (hasStart || k.no)
351
358
  return 'sair';
352
359
  if (k.yes)
353
360
  return 'ativar';
@@ -355,130 +362,98 @@ export function acaoNaBarra(k, temStart) {
355
362
  return 'andar';
356
363
  return 'nada';
357
364
  }
358
- /** The full innerHTML of a `.screen-pause`. Pure — every input is a parameter. */
359
- export function screenPauseMarkup(o) {
360
- // O NOME ACESSÍVEL DO DIÁLOGO passa pelo dicionário. Era texto cru, e MEDIDO num jogo em inglês o efeito
361
- // era este: o título visível dizia "Paused" e o nome do diálogo, "Menu de pausa do jogador 1". Quem enxerga
362
- // lia em inglês; quem escuta recebia o menu anunciado em português — a mesma assimetria do item 4 do
363
- // ADR-0044, um nível acima. E `aria-label`, não `aria-labelledby`: o `<h2>` é rótulo VISUAL, e é por isso
364
- // que escondê-lo num quadro apertado não tira o nome do diálogo de quem escuta.
365
- return '<div class="pause-card" role="dialog" aria-modal="true" aria-label="' + t('pause.cardAria', { n: o.player + 1 }) + '">' +
366
- '<h2><span data-i18n="pause.title">' + o.t('pause.title') + '</span>' + (o.numPlayers > 1 ? ' · Jogador ' + (o.player + 1) : '') + '</h2>' +
367
- pauseMenuHtml(o.pmButtons, 'raiz', o.dynLabel, o.t) +
368
- pauseMenuHtml(o.optionsButtons, 'opcoes', o.dynLabel, o.t) +
369
- '<p class="pause-legend"></p></div>';
370
- }
371
365
  // ---------------------------------------------------------------------------------------------
372
366
  // DOM-facing shell
373
367
  // ---------------------------------------------------------------------------------------------
374
368
  export function initPauseIcons(ctx) {
375
- // ⚠️ O NÍVEL TEA PASSOU A PERSISTIR EM 2026-09-07 (issue #61). Este comentário dizia «deliberately NOT
376
- // persisted — verbatim: game.js never wrote it to storage», e o **verbatim** é o que o desqualificava como
377
- // decisão: foi PRESERVADO na extração do monólito, não escolhido. O ADR-0028 diz que todo menu persiste, e
378
- // este é um controlo de menu que vive na barra rápida.
369
+ const { t } = ctx.translator;
370
+ // THE CALM LEVEL PERSISTS (issue #61; ADR-0028: every menu setting persists). The child who uses the SILENT mode is the
371
+ // one for whom unexpected noise costs most, and a setting that is forgotten is a daily chore, not a setting.
379
372
  //
380
- // O custo de não persistir era da criança que mais precisa dele: quem usa o modo SILENCIOSO voltava a
381
- // pô-lo a cada sessão — e é para quem o barulho inesperado custa mais. Um ajuste que se esquece não é um
382
- // ajuste, é uma tarefa diária.
383
- //
384
- // ⚠️ LÊ NO INIT, NUNCA NO IMPORT: a regra vale para todo o projeto e aqui tem custo concreto — um teste que
385
- // importasse este módulo passaria a depender do `localStorage` do ambiente, e um nível herdado de outro
386
- // caso é uma falha que aparece longe da causa.
387
- let calmMode = lerNivelTea();
373
+ // READ IN INIT, NEVER ON IMPORT: a test importing this module would otherwise depend on the environment's storage, and a
374
+ // level inherited from another case is a failure that shows up far from its cause.
375
+ let calmMode = readTeaLevel(ctx.store);
388
376
  const P = () => ctx.getPlayers();
389
377
  /*
390
- * ⚠️ A ALTERNÂNCIA DE MARCHA PASSOU A TER PADRÃO DA ENGINE (ADR-0106 §4, etapa 1b). Quem injecta continua a
391
- * mandar; quem não injecta deixa de ficar sem ela — que era o caso dos cinco jogos sem barra.
392
- *
393
- * O `store` vem do import directo deste módulo e não do `ctx`, porque é assim que este ficheiro já persiste
394
- * o resto: uma chave injectada é um campo que um consumidor pode omitir, e omiti-la faria escrever num nome
395
- * torto.
378
+ * THE MOVEMENT LATCH HAS AN ENGINE DEFAULT (ADR-0106 §4): a host that injects one still rules; a host that does not is
379
+ * no longer left without it. The `store` is the page's, from the ctx (ADR-0232).
396
380
  */
397
381
  const setToggleMove = ctx.setToggleMove
398
- ?? ((i, on) => definirAlternanciaDeMarcha({
399
- players: P(), store, srSay: ctx.srSay, getNumPlayers: ctx.getNumPlayers,
400
- // ⚠️ ATRAVESSA, e não se resolve aqui: o ícone e o painel têm de escrever a MESMA coisa. Resolver
401
- // o aparelho num deles e não no outro é como duas superfícies da mesma engine passam a discordar.
402
- transporteEmUso: ctx.transporteEmUso,
382
+ ?? ((i, on) => setMoveLatch({
383
+ t, players: P(), store: ctx.store, srSay: ctx.srSay, getNumPlayers: ctx.getNumPlayers,
384
+ // Passed through, not resolved here: the icon and the panel must write the SAME thing, and resolving the
385
+ // device in one of them only is how two surfaces of the same engine come to disagree.
386
+ transportInUse: ctx.transportInUse,
403
387
  }, i, on));
404
388
  /*
405
- * ⚠️ O MODO CEGO IDEM, e aqui o padrão é literalmente o que o `core/state` já decidiu que um setter faz:
406
- * «grava, persiste, avisa» — e nada mais. Os efeitos de jogo (refazer os extras do nível) são REACÇÃO, e
407
- * quem reage assina `on('modoCego', …)`. O anúncio não se perde para quem não injecta: este ícone já diz
408
- * `sr.icon.blindOn`/`Off` por si, logo abaixo.
389
+ * BLIND MODE LIKEWISE: the default is exactly what `core/state` decided a setter does — write, persist, notify — and
390
+ * nothing more. Game effects are REACTIONS, subscribed with `on('blindMode', …)`. The announcement is not lost for a host
391
+ * that injects nothing: this icon says `sr.icon.blindOn`/`Off` itself, below.
409
392
  */
410
- const setModoCego = ctx.setModoCego ?? setModoCegoValue;
411
- /** O documento onde se constroi. Resolvido a cada uso, e por globalThis — em node o identificador
412
- * document nem existe, e um ?? sobre ele lançaria ReferenceError em vez de cair no padrão. */
413
- const docDaMontagem = () => ctx.doc ?? globalThis.document;
393
+ const writeBlindMode = ctx.setBlindMode ?? ((on) => { ctx.settings.setBlindModeValue(on); });
414
394
  /*
415
- * ⚠️ O CONTRASTE E A COR SÓ APARECEM SE HOUVER QUEM OS ESCREVA (ADR-0106 §5).
416
- *
417
- * 📏 Medido em 2026-09-08, e é o que separa este caso dos outros seis campos «acidentais»: eles eram estado
418
- * que um cartucho calhou de guardar, e a engine pôde reclamá-los. Estes precisam de um
419
- * `render/viz-setters`, cujo contexto pede **34 campos** do grafo de render de UM jogo — `parallaxLayers`,
420
- * `decoSprites`, `getPowerups`, `rebuildCoins`, `worldSprite`. O `createGame` não monta isso, e um quiz não
421
- * tem nada disso para montar. A engine não pode dar um padrão aqui; o que ela pode é não FINGIR.
395
+ * CONTRAST AND COLOUR ONLY APPEAR IF SOMEONE CAN WRITE THEM (ADR-0106 §5): the theme and the correction are written by
396
+ * the host (`setPlayerTheme`/`setPlayerCorrection`), and without a writer the engine does not pretend.
422
397
  *
423
- * Decidido uma vez, no arranque, e não a cada montagem de barra: o conjunto de escritores de um consumidor
424
- * não muda a meio de uma partida, e recalcular por tela faria as telas discordarem entre si.
398
+ * Decided once, at start, not per bar: a host's set of writers does not change mid-game, and recomputing per screen
399
+ * would let the screens disagree.
425
400
  */
426
- const iconesDoJogo = iconesQueAccionam({
427
- tema: Boolean(ctx.setTemaDoJogador),
428
- correcao: Boolean(ctx.setCorrecaoDoJogador),
429
- // 📌 Sem `Boolean(...)`: os dois de cima perguntam «existe escritor?» a um campo opcional; este é uma
430
- // RESPOSTA que o jogo deu, e envolvê-la faria um `undefined` de um ctx mal montado virar `false` —
431
- // esconder o controle em silêncio, que é metade do defeito que este campo existe para não cometer.
432
- seguraTeclas: ctx.seguraTeclas,
401
+ const gameIcons = iconsThatAct({
402
+ theme: Boolean(ctx.setPlayerTheme),
403
+ correction: Boolean(ctx.setPlayerCorrection),
404
+ // No `Boolean(...)`: the two above ask an optional field «is there a writer?»; this is an ANSWER the game gave, and
405
+ // wrapping it would turn a badly wired ctx's `undefined` into `false` — hiding the control silently.
406
+ holdsKeys: ctx.holdsKeys,
407
+ // the same question the scan asks (ADR-0218): how many positions this game would give it to offer
408
+ declaredPositions: ctx.declaredPositions,
409
+ typography: Boolean(ctx.cycleTypography),
410
+ // mounted when the root can answer the clock question; shown or hidden per cartridge in `reflectIconBtn` (ADR-0142)
411
+ clock: () => Boolean(ctx.clock),
412
+ camera: Boolean(ctx.camera),
413
+ microphone: Boolean(ctx.microphone),
414
+ menus: Boolean(ctx.openMenus),
433
415
  });
434
416
  /*
435
- * ⚠️ RESOLVIDOS UMA VEZ, no arranque, pela mesma razão do `settings-motion`: o `rm` é mutado in-place e
436
- * partilhado por REFERÊNCIA com quem desenha a cena, e resolvê-lo a cada uso criaria um objecto novo por
437
- * chamada — o interruptor deixaria de alcançar o desenho, sem erro nenhum.
438
- *
439
- * 📌 O `getPauseActs` fica FUNÇÃO e não valor, porque a laziness dele é a razão de ele existir assim: no
440
- * cartucho a tabela é um `const` declarado ~1200 linhas abaixo, e lê-la aqui cairia na zona morta temporal.
417
+ * RESOLVED ONCE, at start, as in `settings-motion`: `rm` is mutated in place and shared by REFERENCE with whoever draws
418
+ * the scene, and resolving it on each use would make a new object per call — the switch would stop reaching the drawing,
419
+ * with no error at all.
441
420
  */
442
- const rm = ctx.rm ?? lerCenaGuardada();
443
- const rmKeys = ctx.rmKeys ?? CHAVES_DE_CENA;
444
- const rmChar = ctx.rmChar ?? ANIMACOES_DO_PERSONAGEM;
445
- const saveRM = ctx.saveRM ?? (() => guardarCena(rm));
421
+ const rm = ctx.rm ?? readStoredScene(ctx.store, defaultReducedMotion(ctx.matchMedia));
422
+ const rmKeys = ctx.rmKeys ?? SCENE_KEYS;
423
+ const rmChar = ctx.rmChar ?? CHARACTER_ANIMATIONS;
424
+ const saveRM = ctx.saveRM ?? (() => storeScene(ctx.store, rm));
446
425
  /*
447
- * ⚠️ ESTES TRÊS SÃO LIDOS A CADA CHAMADA, e não resolvidos uma vez como o `rm` acima. A diferença é
448
- * deliberada e um teste apanhou-me a errá-la: congelar `ctx.getPauseActs` no arranque partiu um caso que
449
- * TROCA a tabela depois do `init` — e trocar depois é legítimo, porque a laziness deste campo existe
450
- * precisamente por a tabela chegar tarde. Um padrão não pode custar a ligação tardia que o campo tem.
451
- *
452
- * O `rm` é o contrário e por isso fica congelado: ali o que importa é a IDENTIDADE do objecto, partilhada
453
- * por referência com quem desenha a cena.
426
+ * THESE THREE ARE READ ON EACH CALL, not resolved once like `rm` above: a host may replace the action table after `init`
427
+ * (it can arrive late), and a default must not cost that late binding. `rm` is the opposite — what matters there is the
428
+ * object's IDENTITY, shared by reference with whoever draws the scene.
454
429
  *
455
- * A anotação de tipo é necessária: sem ela o padrão `() => ({})` infere `{}`, que não aceita indexação por
456
- * string, e o compilador passaria a recusar `acts[act]` — o despacho inteiro.
430
+ * The type annotation is needed: without it the default `() => ({})` infers `{}`, which does not accept string indexing,
431
+ * and the compiler would refuse `acts[act]` — the whole dispatch.
457
432
  */
458
433
  const dynLabel = (b) => (ctx.dynLabel ? ctx.dynLabel(b) : null);
459
434
  const getPauseActs = () => (ctx.getPauseActs ? ctx.getPauseActs() : {});
460
435
  const setPauseActor = (i) => { if (ctx.setPauseActor)
461
436
  ctx.setPauseActor(i); };
462
437
  function hasPrivateOutput(i) { return hasPrivateOutputIn(P(), ctx.getNumPlayers(), i); }
463
- /** A recusa da alternância para este jogador agora, ou `null`. Recalculada: o aparelho em uso muda. */
464
- function recusaAgora(i) { return ctx.transporteEmUso ? recusaDaAlternancia(ctx.transporteEmUso(i)) : null; }
465
438
  function iconState(i) {
466
439
  const p = P()[i] || {};
467
440
  const cat = ctx.getAudioCat();
468
441
  return {
469
- modoCego: ctx.getModoCego(),
442
+ blindMode: ctx.getBlindMode(),
470
443
  ttsOn: !!(cat && cat.tts && cat.tts.on),
471
444
  librasOn: ctx.isLibrasOn(),
472
445
  calmMode,
473
446
  toggleMove: !!p.toggleMove,
474
- // ⚠️ `DEFAULTS.viz` E NÃO `''` (issue #61). A cadeia vazia funcionava por ACIDENTE: não casa
475
- // `hc-direto` nem `fix-*`, então os dois ícones ficavam apagados pelo motivo certo por engano. O padrão
476
- // passou a ter nome em `core/state`, e `render/viz-modes` já declarava esse modo com `kind:'normal'` —
477
- // o que não faz nada. Dizer o padrão em vez de o deduzir é o que torna a marca do ADR-0029 possível
478
- // aqui, porque ela lê `DEFAULTS` e mais nada.
479
- visual: p.visual ?? PADRAO,
447
+ switchScan: ctx.settings.switchScan,
448
+ voice: ctx.settings.voiceControl,
449
+ // The NAMED default, not an empty value that only happened to match nothing: ADR-0029's changed-mark reads the
450
+ // default and nothing else.
451
+ visual: p.visual ?? DEFAULT_VISUAL,
452
+ speed: ctx.settings.gameSpeed,
453
+ camera: ctx.settings.cameraControl,
454
+ locale: ctx.translator.locale(),
480
455
  privateOutput: hasPrivateOutput(i),
481
- alternanciaExigida: recusaAgora(i) !== null,
456
+ noVoice: !!ctx.noVoice?.(),
482
457
  };
483
458
  }
484
459
  // --- TEA ---------------------------------------------------------------------------------
@@ -505,14 +480,15 @@ export function initPauseIcons(ctx) {
505
480
  }
506
481
  // --- icon actions (the dispatcher, as a table) ---------------------------------------------
507
482
  const ICON_ACTS = {
483
+ menu: (i) => { ctx.openMenus?.(i); },
508
484
  blind: () => {
509
- setModoCego(!ctx.getModoCego());
510
- ctx.srSay(t(ctx.getModoCego() ? 'sr.icon.blindOn' : 'sr.icon.blindOff'));
485
+ writeBlindMode(!ctx.getBlindMode());
486
+ ctx.srSay(t(ctx.getBlindMode() ? 'sr.icon.blindOn' : 'sr.icon.blindOff'));
511
487
  },
512
488
  tts: () => {
513
489
  const cat = ctx.getAudioCat();
514
490
  if (!cat || !cat.tts)
515
- return; // a guarda que `iconLabel` e `applyCalm` já tinham e esta ação não
491
+ return; // the same guard `iconLabel` and `applyCalm` have
516
492
  cat.tts.on = !cat.tts.on;
517
493
  ctx.setCatGain('tts');
518
494
  if (ctx.reflectTtsPanelEnabled)
@@ -525,66 +501,127 @@ export function initPauseIcons(ctx) {
525
501
  },
526
502
  tea: () => {
527
503
  calmMode = nextCalmMode(calmMode);
528
- store.set(store.KEYS.tea, calmMode); // ADR-0028: todo menu persiste. Ver a nota no `let` acima.
504
+ ctx.store.set(KEYS.tea, calmMode); // ADR-0028: every menu setting persists. See the note at the `let` above.
529
505
  applyCalm();
530
506
  ctx.srSay(t('sr.icon.tea', { v: t(CALM_NAMES[calmMode]) }));
531
507
  },
508
+ /*
509
+ * ☝️ CYCLES THREE READINGS OF A PRESS (ADR-0218): standard · no holding needed · one button only.
510
+ *
511
+ * 📌 ENTERING THE SCAN LEAVES THE LATCH WHERE IT IS, and leaving it turns the latch off. The scan wins in `inputModeOf`, so
512
+ * the position shown is never ambiguous; and because the latch writer announces by itself, it is called ONLY where it
513
+ * changes something — otherwise the child would hear «no holding needed, off» when what ended was the scan.
514
+ */
532
515
  altmove: (i) => {
533
516
  // verbatim: `players[i].toggleMove` with no `||{}` guard (unlike contrast/cvd below).
534
- setToggleMove(i, !P()[i].toggleMove);
517
+ const latched = !!P()[i].toggleMove;
518
+ const next = nextInputMode(inputModeOf({ toggleMove: latched, switchScan: ctx.settings.switchScan }), ctx.holdsKeys());
519
+ ctx.settings.setSwitchScanValue(next === 'scan');
520
+ // 📌 ENTERING THE SCAN LEAVES THE LATCH WHERE SHE PUT IT — the scan wins in `inputModeOf`, so the position shown is never
521
+ // ambiguous and coming back out returns her to the choice she had made. Leaving it writes the position she walked to.
522
+ // ⚠️ And the latch writer ANNOUNCES BY ITSELF, so it is called only where it changes something: otherwise the child would
523
+ // hear «não precisa segurar, desligado» when what ended was the scan.
524
+ // 📌 On every device, eyes and voice included (ADR-0249): the write goes under the transport in use, and that stored
525
+ // choice is what wins over the game's default the next time that transport presses.
526
+ if (next !== 'scan' && latched !== (next === 'sticky')) {
527
+ setToggleMove(i, next === 'sticky');
528
+ return;
529
+ }
530
+ ctx.srSay(t('sr.icon.inputMode', { v: t(INPUT_MODE_NAME[next]) }));
535
531
  },
536
- // ⚠️ OS DOIS ÍCONES DEIXARAM DE SE APAGAR UM AO OUTRO (#104). Eles SEMPRE ciclaram dentro do seu eixo —
537
- // `nextContrast` e `nextCvd` existem separados desde sempre —, mas escreviam os dois no mesmo campo, e
538
- // por isso mexer num zerava o outro. O snapshot dizia isso como se fosse desenho: «they overwrite each
539
- // other; that is by design». Agora cada um escreve no seu eixo e o outro fica onde estava.
540
- // ⚠️ AS DUAS GUARDAS NÃO SÃO CINTO E SUSPENSÓRIOS. Sem escritor, o ícone nem sequer é montado
541
- // (`iconesDoJogo`), então este ramo não deveria ser alcançável pela barra — mas `iconAct` é EXPORTADO e
542
- // qualquer consumidor pode chamá-lo por chave. Sem a guarda, essa chamada rebentaria; com ela, não faz
543
- // nada e não anuncia — que é o mesmo que dizer a verdade: este jogo não tem por onde.
532
+ // Each of the two icons writes its OWN axis (#104), so changing one leaves the other where it was.
533
+ // ⚠️ THE TWO GUARDS ARE NOT BELT AND BRACES. Without a writer the icon is not even mounted (`gameIcons`), so the bar
534
+ // cannot reach this branch — but `iconAct` is EXPORTED and any host can call it by key. Without the guard that call
535
+ // would throw; with it, nothing happens and nothing is announced — which is the truth: this game has no way to do it.
544
536
  contrast: (i) => {
545
- if (!ctx.setTemaDoJogador)
537
+ if (!ctx.setPlayerTheme)
546
538
  return;
547
- const v = proximoTema((P()[i] || {}).visual ?? PADRAO);
548
- ctx.setTemaDoJogador(i, v.tema);
549
- ctx.srSay(t('sr.visual.contrast', { v: t(CURTO_DO_TEMA[v.tema]) }));
539
+ const v = nextTheme((P()[i] || {}).visual ?? DEFAULT_VISUAL);
540
+ ctx.setPlayerTheme(i, v.tema);
541
+ ctx.srSay(t('sr.visual.contrast', { v: t(SHORT_THEME[v.tema]) }));
542
+ },
543
+ /*
544
+ * THE TYPOGRAPHY CYCLE (ADR-0149 §1): one press changes the letter CASE and the FACE at once. The guard is the one
545
+ * written at `contrast` above, for the same reason.
546
+ *
547
+ * It ANNOUNCES THE FACE, NOT THE POSITION: «position 3 of 5» tells nobody anything; the face's name is what the child
548
+ * recognises — the same reason ADR-0074 forbids `action2` from reaching a person.
549
+ */
550
+ tipografia: () => {
551
+ if (!ctx.cycleTypography)
552
+ return;
553
+ const face = ctx.cycleTypography();
554
+ if (face)
555
+ ctx.srSay(t('sr.typo.font', { fam: face }));
556
+ },
557
+ /*
558
+ * PLAYING THROUGH THE WEBCAM (ADR-0215) and BY SPEAKING (ADR-0189, issue #184): each writes the child's answer, one stored key.
559
+ *
560
+ * 🔴 A POSITION THAT STARTS A CONTROL IS ANNOUNCED BY THE CONTROL, NOT HERE. At this line nothing has started: the control
561
+ * starts after the write, asynchronously, and answers either way — «Pronto: já pode jogar…» when it is really on, or the
562
+ * reason assertively when it cannot start (and the answer goes back to off). Saying the position here told the child
563
+ * «Comando de voz: ligado» over a button that read «desligado», on every press where the microphone or the runtime failed.
564
+ * Off is true the moment it is written, so off is still said here.
565
+ */
566
+ camera: () => {
567
+ const v = nextCameraControl(ctx.settings.cameraControl);
568
+ ctx.settings.setCameraControlValue(v);
569
+ if (v === 'off')
570
+ ctx.srSay(t('sr.icon.camera', { v: t('state.off') }));
571
+ },
572
+ voice: () => {
573
+ const v = !ctx.settings.voiceControl;
574
+ ctx.settings.setVoiceControlValue(v);
575
+ if (!v)
576
+ ctx.srSay(t('sr.icon.voice', { v: t('state.off') }));
577
+ },
578
+ // THE LANGUAGE (the Dev, 2026-09-16): the next flag; `setLocale` stores it and every surface redraws on `i18n:change`. Said in the NEW
579
+ // language, once it has loaded.
580
+ idioma: () => {
581
+ const v = nextLocale(ctx.translator.locale());
582
+ void ctx.translator.setLocale(v).then(() => ctx.srSay(t('sr.icon.idioma', { v: LANGUAGE_NAME[v] })));
583
+ },
584
+ // THE GAME SPEED (ADR-0180): one step down, wrapping at 50%; stored, and felt on the next frame of `startLoop`.
585
+ velocidade: () => {
586
+ const v = nextGameSpeed(ctx.settings.gameSpeed);
587
+ ctx.settings.setGameSpeedValue(v);
588
+ ctx.srSay(t('sr.icon.velocidade', { pct: Math.round(v * 100) }));
550
589
  },
551
590
  cvd: (i) => {
552
- if (!ctx.setCorrecaoDoJogador)
591
+ if (!ctx.setPlayerCorrection)
553
592
  return;
554
- const v = proximaCorrecao((P()[i] || {}).visual ?? PADRAO);
555
- ctx.setCorrecaoDoJogador(i, v.correcao);
556
- ctx.srSay(t('sr.icon.cvd', { v: t(CURTO_DA_CORRECAO[v.correcao]) }));
593
+ const v = nextCorrection((P()[i] || {}).visual ?? DEFAULT_VISUAL);
594
+ ctx.setPlayerCorrection(i, v.correcao);
595
+ ctx.srSay(t('sr.icon.cvd', { v: t(SHORT_CORRECTION[v.correcao]) }));
557
596
  },
558
597
  };
559
598
  function iconAct(k, i) {
560
- const ic = ICON_BY_KEY.get(k);
561
- if (ic && ic.soon) {
562
- ctx.srAlert(t('sr.icon.underConstruction', { nome: t(ic.n) }));
599
+ // locked like the panel's row (ADR-0185): the same reason, said, and nothing turned on
600
+ if (k === 'tts' && ctx.noVoice?.()) {
601
+ ctx.srAlert(t('audio.semVoz'));
563
602
  return;
564
603
  }
565
604
  if ((k === 'blind' || k === 'tts') && !hasPrivateOutput(i)) {
566
605
  ctx.srAlert(t('sr.icon.needsPrivateOutput'));
567
606
  return;
568
607
  }
569
- // ⚠️ A MESMA RECUSA DO PAINEL, na mesma forma que as duas acima: DIZER e voltar. Aceitar o clique e
570
- // ignorá-lo é a outra metade do que o ADR-0076 proíbe, e aqui há um agravante — este ícone e o
571
- // `#opt-altmove` escrevem o MESMO valor, logo um a aceitar enquanto o outro recusa daria à criança dois
572
- // botões que discordam sobre o mesmo ajuste.
573
- if (k === 'altmove') {
574
- const recusa = recusaAgora(i);
575
- if (recusa) {
576
- ctx.srAlert(t(recusa.chave));
577
- return;
578
- }
579
- }
608
+ // No refusal for `altmove` (ADR-0249): no device locks the latch, and what a game cannot hold the CYCLE leaves out.
580
609
  const act = ICON_ACTS[k];
581
610
  if (act)
582
611
  act(i);
583
612
  }
584
613
  // --- reflection --------------------------------------------------------------------------
585
- function iconLabel(k, i) { return computeIconLabel(k, iconState(i)); }
614
+ function iconLabel(k, i) { return computeIconLabel(t, k, iconState(i)); }
586
615
  function reflectIconBtn(b, i) {
587
616
  const k = b.dataset.pi || '';
617
+ // the hourglass follows the CURRENT cartridge's clock (ADR-0180): a turn game mounted later hides it, a clock game shows it
618
+ if (k === 'velocidade')
619
+ b.hidden = !ctx.clock?.();
620
+ if (k === 'idioma') {
621
+ const flag = flagOf(ctx.translator.locale());
622
+ if (b.innerHTML !== flag)
623
+ b.innerHTML = flag;
624
+ }
588
625
  const st = iconState(i);
589
626
  const v = computeIconVisual(k, st);
590
627
  b.classList.remove(...ICON_STATE_CLASSES);
@@ -595,26 +632,44 @@ export function initPauseIcons(ctx) {
595
632
  b.classList.toggle('pi-on', v.on);
596
633
  b.classList.toggle('pi-dis', v.dis);
597
634
  /*
598
- * 🔴 A ISSUE #128, E ELA É DE UMA LINHA: `pi-dis` é CLASSE CSS. A criança que enxerga vê o ícone
599
- * apagado; a que navega por leitor de tela não recebe nada — o botão anuncia-se accionável e não
600
- * responde. `aria-disabled` espelha o mesmo facto para quem ouve.
601
- *
602
- * ⚠️ E `aria-disabled` e NÃO `disabled`: o segundo tira o botão da ordem de tabulação, e quem navega
603
- * por teclado deixaria de o alcançar — logo deixaria de poder ouvir POR QUE ele não responde. É a
604
- * mesma escolha que o `#opt-altmove` faz no painel, pela mesma razão.
635
+ * `pi-dis` is a CSS class (issue #128): a child who sees gets a greyed icon, a child on a screen reader gets nothing — the
636
+ * button announces itself actionable and does not respond. `aria-disabled` mirrors the same fact for whoever listens.
637
+ * `aria-disabled` and NOT `disabled`: the latter takes the button out of the tab order, and a keyboard user could no
638
+ * longer reach it — nor hear WHY it does not respond.
605
639
  */
606
640
  if (v.dis)
607
641
  b.setAttribute('aria-disabled', 'true');
608
642
  else
609
643
  b.removeAttribute('aria-disabled');
610
- b.setAttribute('aria-pressed', String(v.active));
611
- // ⚠️ TODO ÍCONE RECEBE RÓTULO, `soon` INCLUÍDO — e o guarda que aqui estava dizia por que não: «`soon`
612
- // buttons keep the label the markup gave them (same string)». A segunda metade continua certa (não há
613
- // estado a reportar), mas «mesma string» era verdade só enquanto a marcação e o reflexo corressem no
614
- // MESMO IDIOMA — e desde que a engine passou a montar a barra (ADR-0106 etapa 2) deixam de correr.
615
- // O `initI18n` carrega en/es de forma assíncrona; a marcação nasce em pt e só o reflexo a corrige.
616
- // 📌 Medido num navegador: cinco ícones em inglês e três ainda em «(em construção)», na mesma barra.
617
- b.setAttribute('aria-label', computeIconLabel(k, st));
644
+ // the ☰ opens something and holds no state: a pressed/unpressed button would announce a toggle
645
+ if (k === 'menu')
646
+ b.removeAttribute('aria-pressed');
647
+ else
648
+ b.setAttribute('aria-pressed', String(v.active));
649
+ // ⚠️ EVERY icon is relabelled here, including the ones that carry no state — because the markup and this reflection do not
650
+ // run in the same language. `initI18n` loads en/es asynchronously, so the markup is born in pt and only the reflection
651
+ // corrects it. 📌 Measured in a browser back when a guard skipped some of them: five icons in English and three still in
652
+ // Portuguese, on the same bar.
653
+ b.setAttribute('aria-label', computeIconLabel(t, k, st));
654
+ refreshCursorCaption(b);
655
+ }
656
+ /**
657
+ * THE NAME UNDER THE ROW FOLLOWS THE LABEL OF THE ICON UNDER THE CURSOR (ADR-0159 rule 10, ADR-0167). The caption is the
658
+ * visible half of what the cursor says, and the reflection is how a state changed ELSEWHERE reaches the icon — the 🦯, 📷
659
+ * and 👄 through the root's `stateOn`, the 🗣 through the mixer's `onCatChange` (ADR-0247). Written only on a move or a press,
660
+ * the caption kept the state the child had left while the label spoke the new one.
661
+ *
662
+ * Only the CURSOR's icon (`.pi-sel`, which lives only on the bars `getA11yBars` hands over): another icon's name under the
663
+ * row would move the cursor's only visible mark. And only when the text CHANGED: the per-screen bar's caption is a live
664
+ * region, and the same words rewritten are said again.
665
+ */
666
+ function refreshCursorCaption(b) {
667
+ if (!b.classList.contains('pi-sel'))
668
+ return;
669
+ const cap = ctx.getA11yBars().find((bar) => bar.contains(b))?.querySelector('.pause-icons-cap');
670
+ const text = accessibleLabel(b); // name and state only: «N de M» is spoken, never written (ADR-0167)
671
+ if (cap && cap.textContent !== text)
672
+ cap.textContent = text;
618
673
  }
619
674
  function reflectIconsIn(root, i) {
620
675
  if (!root)
@@ -622,111 +677,137 @@ export function initPauseIcons(ctx) {
622
677
  root.querySelectorAll('.pi-btn').forEach((b) => reflectIconBtn(b, i));
623
678
  }
624
679
  /**
625
- * Reflete os ícones de TODAS as telas. Varre as BARRAS e não mais os cartões de pausa: desde o item 7 do
626
- * ADR-0044 os ícones vivem no HUD, e um cartão de pausa não contém `.pi-btn` nenhum.
680
+ * THE CARDS THIS INSTANCE BUILT. Private: whoever built them keeps them, which asks nothing of any host — a ctx field
681
+ * would be one more thing every game had to remember to pass.
627
682
  */
628
- /**
629
- * OS CARTÕES QUE ESTA INSTÂNCIA CONSTRUIU. Privado, e é a resposta a um problema que eu próprio criei.
630
- *
631
- * ⚠️ O `PauseIconsCtx` tinha um `getPauseScreens` e eu removi-o hoje, com razão: tinha ZERO leitores. Agora
632
- * este módulo precisa de alcançar os cartões — e a saída certa NÃO é repor o campo. Quem os construiu foi
633
- * ele; guardar o que construiu não pede nada a consumidor nenhum, e um campo de ctx é mais uma coisa que
634
- * cada um dos 300 jogos teria de se lembrar de passar.
683
+ const cards = [];
684
+ /*
685
+ * `refreshPauseItems` LOCKS THE ITEMS THIS GAME CANNOT ACTIVATE — recomputed, not decided at start. The action table can
686
+ * arrive after the card is built (`getPauseActs` is asked when needed), so deciding at build time would lose for good
687
+ * every item whose action exists only later. The host calls `reflectPauseIcons()` when the pause opens and when a
688
+ * cartridge is mounted, so the card is right at the moment the child sees it.
635
689
  */
636
- const cartoes = [];
637
690
  /**
638
- * ESCONDE OS ITENS QUE ESTE JOGO NÃO CONSEGUE ACCIONAR — recalculado, e não decidido no arranque.
691
+ * THE CARD'S NAME — what is SEEN and what is HEARD — repainted in the current language.
639
692
  *
640
- * ⚠️ ESTA FUNÇÃO EXISTE POR UM DEFEITO DE TEMPO. O cartão era FILTRADO no `buildScreenPause`, e o
641
- * `getPauseActs` é um getter precisamente porque a tabela CHEGA TARDE — «`pauseActs` is a `const` declared
642
- * far below the init site», diz o próprio campo. Um consumidor que siga esse padrão documentado montava um
643
- * cartão sem os itens cuja acção só existiu depois do boot, e nunca mais os recuperava.
693
+ * The markup is born before an asynchronously loaded dictionary arrives, and an `aria-label` set once cannot be corrected
694
+ * later by `applyDom`, which only reaches `[data-i18n]` and `[data-i18n-aria]`. Nor would `data-i18n-aria` do: `applyDom`
695
+ * calls `t(k)` without parameters, and this key carries the seat number — the child would hear «Player {n} pause menu»,
696
+ * braces included. So it is repainted on each `reflectPauseIcons()`, i.e. when the pause opens.
644
697
  *
645
- * 📌 A avaliação passou para o ÚLTIMO instante possível: o `ui/shell` chama `reflectPauseIcons()` quando a
646
- * fase vira `pause-menu`, ou seja quando a pausa ABRE. O §5 continua respeitado — a criança nunca vê um
647
- * item que não acciona —, e agora também vê os que passaram a accionar.
698
+ * The one who lost was the one who LISTENS (ADR-0044 item 4): to a sighted child the card was entirely in the right
699
+ * language, and nothing looked wrong.
648
700
  */
649
- function refrescarItensDaPausa() {
701
+ function renameCard(cardEl, i) {
702
+ const card = cardEl.querySelector('.pause-card');
703
+ if (card)
704
+ card.setAttribute('aria-label', t('pause.cardAria', { n: i + 1 }));
705
+ const seat = cardEl.querySelector('h2 .pause-seat');
706
+ if (seat)
707
+ seat.textContent = ctx.getNumPlayers() > 1 ? t('pause.cardSeat', { n: i + 1 }) : '';
708
+ }
709
+ function refreshPauseItems() {
650
710
  const acts = getPauseActs();
651
- const raiz = ctx.pmButtons ?? PM_BTNS;
652
- const opcoes = ctx.optionsButtons ?? PM_OPTIONS_BTNS;
653
- const vivos = new Set([
654
- ...raizQueAcciona(raiz, opcoes, acts).map((b) => b.act),
655
- ...itensQueAccionam(opcoes, acts).map((b) => b.act),
711
+ const rootEl = ctx.pmButtons ?? PM_BTNS;
712
+ const options = ctx.optionsButtons ?? PM_OPTIONS_BTNS;
713
+ const fromGame = ctx.gameButtons ?? PM_GAME_BTNS;
714
+ const actingItems = new Set([
715
+ ...rootThatActs(rootEl, options, acts, fromGame).map((b) => b.act),
716
+ ...itemsThatAct(options, acts).map((b) => b.act),
717
+ ...itemsThatAct(fromGame, acts).map((b) => b.act),
656
718
  ]);
657
- // ⚠️ `filter(Boolean)`: os cartões são indexados por JOGADOR, e montar só a tela 2 deixa um buraco no
658
- // índice 0. Um `for…of` sobre array esparso entrega `undefined`, e foi o que rebentou à primeira.
659
- for (const cartao of cartoes.filter(Boolean)) {
660
- for (const btn of cartao.querySelectorAll('.pm-btn')) {
661
- // ⚠️ `hidden` e não `remove()`: reaparecer tem de ser possível, porque a tabela pode crescer outra vez
662
- // (um jogo que só liga «sair» depois da primeira fase). Remover seria decidir uma vez de novo.
663
- btn.hidden = !vivos.has(btn.dataset.act ?? '');
719
+ // An explicit index, not `filter(Boolean)`: the cards are indexed by PLAYER, building only screen 2 leaves a hole at
720
+ // index 0, and closing the hole would renumber the seats in the card's name. The `undefined` guard is written by hand.
721
+ for (let i = 0; i < cards.length; i++) {
722
+ const cardEl = cards[i];
723
+ if (!cardEl)
724
+ continue;
725
+ renameCard(cardEl, i);
726
+ for (const btn of cardEl.querySelectorAll('.pm-btn')) {
727
+ // LOCKED WITH ITS REASON, NOT HIDDEN (ADR-0161, superseding ADR-0106 §5 here): a card that changed shape from game
728
+ // to game hid from the child that an option exists. The cursor still stops on the item and it still counts;
729
+ // reaching or pressing it says why. And not `remove()`: the table can grow later.
730
+ const act = btn.dataset.act ?? '';
731
+ if (actingItems.has(act)) {
732
+ btn.removeAttribute('aria-disabled');
733
+ delete btn.dataset.motivo;
734
+ }
735
+ else {
736
+ btn.setAttribute('aria-disabled', 'true');
737
+ btn.dataset.motivo = itemReason(t, act);
738
+ }
664
739
  }
665
740
  }
666
741
  }
667
742
  function reflectPauseIcons() {
668
743
  ctx.getA11yBars().forEach((bar, i) => reflectIconsIn(bar, i));
669
- refrescarItensDaPausa();
744
+ refreshPauseItems();
670
745
  }
671
746
  // --- the pause screen ----------------------------------------------------------------------
672
747
  /**
673
- * Troca a lista visível E ANUNCIA o primeiro item da lista que entrou.
674
- *
675
- * O anúncio não é enfeite: quem não enxerga acabou de mudar de menu e o cursor pulou para outro lugar. Sem
676
- * a fala, a única pista de que a tela mudou seria o silêncio. O índice "N de M" vem junto (item 3), e é ele
677
- * que diz de quantos itens é a lista nova.
748
+ * Switches the visible list AND ANNOUNCES the first item of the list that came in. Not decoration: a child who cannot see
749
+ * has just changed menus and the cursor jumped; without speech the only clue would be silence. The «N de M» index comes
750
+ * with it (ADR-0044 item 3) and says how many items the new list has.
678
751
  */
679
- function anunciarLista(sp, sub) {
680
- const primeiro = mostrarSubmenuDaPausa(sp, sub);
681
- if (!primeiro)
752
+ function announceList(sp, sub) {
753
+ // «back» lands on the door that opened the list being left (ADR-0130 rule 1); any other door, on the first item
754
+ const cursor = sub === 'raiz' ? backToRoot(sp) : showPauseOptions(sp, sub);
755
+ if (!cursor)
682
756
  return;
683
- const itens = [...sp.querySelectorAll(PM_ITENS_VISIVEIS)];
684
- ctx.srSay(anunciarItem({ rotulo: primeiro.textContent || '', posicao: 1, total: itens.length }, menuIndexOn));
757
+ const items = [...sp.querySelectorAll(PM_VISIBLE_ITEMS)];
758
+ ctx.srSay(announceItem(t, { label: cursor.textContent || '', position: items.indexOf(cursor) + 1, total: items.length }, ctx.settings.menuIndexOn));
685
759
  }
686
- /* ===================== O MODO `accessibility` (ADR-0044, item 7) ===================== */
760
+ /* ===================== THE `accessibility` MODE (ADR-0044 item 7) ===================== */
687
761
  /**
688
- * Quem está com o direcional dirigindo a BARRA em vez do personagem.
689
- *
690
- * Vida de RODADA (ADR-0038): mora no closure desta instância, não é persistido, e some com a partida. Um
691
- * modo de entrada que sobrevivesse ao reinício seria a armadilha voltando pela porta dos fundos — a criança
692
- * abriria o jogo no dia seguinte e o personagem não andaria.
762
+ * Who has the d-pad steering the BAR instead of the character. ROUND lifetime (ADR-0038): it lives in this instance's
763
+ * closure, is not persisted, and goes with the round — an input mode that survived a restart would be the trap coming
764
+ * back: the child would open the game the next day and the character would not move.
693
765
  */
694
- const naBarra = new Set();
695
- /** O cursor da barra da tela `i`, ou o primeiro ícone quando ainda não há cursor. */
696
- function iconeSelecionado(bar) {
766
+ const onBar = new Set();
767
+ /** Screen `i`'s bar cursor, or the first icon when there is no cursor yet. */
768
+ function selectedIcon(bar) {
697
769
  return bar.querySelector('.pi-sel') || bar.querySelector('.pi-btn');
698
770
  }
699
- /** Põe o cursor num ícone, escreve a legenda e ANUNCIA — a legenda é o canal de quem não vê o ícone. */
700
- function selecionarIcone(bar, el) {
771
+ /** Puts the cursor on an icon, writes the caption and ANNOUNCES — speech is the channel of whoever cannot see the icon. */
772
+ function selectIcon(i, bar, el) {
701
773
  bar.querySelectorAll('.pi-sel').forEach((x) => x.classList.remove('pi-sel'));
702
774
  el.classList.add('pi-sel');
703
775
  const cap = bar.querySelector('.pause-icons-cap');
704
776
  if (cap)
705
- cap.textContent = legendaDoIcone(bar, el);
706
- ctx.srSay(legendaDoIcone(bar, el));
777
+ cap.textContent = accessibleLabel(el);
778
+ ctx.srSay(iconCaption(t, bar, el, ctx.settings.menuIndexOn)); // the spoken one carries the place (ADR-0167)
779
+ ctx.explainIcon?.(i, el.dataset.pi ?? null);
707
780
  }
708
781
  /**
709
- * ENTRA no modo: o direcional passa a dirigir a barra da tela `i`, e o jogo VOLTA a rodar.
782
+ * ENTERS the mode: the d-pad now steers screen `i`'s bar.
783
+ *
784
+ * It does NOT resume the game (ADR-0155): START is the QUICK PAUSE, the bar is used with the game FROZEN, and resuming on
785
+ * entry would unfreeze the world the instant the child asked it to stop. Whoever enters decides the phase.
710
786
  *
711
- * O anúncio diz como SAIR, e diz na hora de entrar. É a linha que desarma a armadilha que o próprio
712
- * ADR-0044 anotou como consequência negativa desta decisão: quem não enxerga aperta a direção, o personagem
713
- * não anda, e sem esta frase não há nada na tela que explique — porque a tela não é o canal dessa criança.
787
+ * The announcement says how to LEAVE, at the moment of entering — the line that disarms the trap ADR-0044 recorded as a
788
+ * negative consequence: a child who cannot see presses a direction, the character does not move, and nothing on screen
789
+ * explains why, because the screen is not that child's channel.
714
790
  */
715
- function entrarNaBarra(i) {
791
+ function enterBarMode(i) {
716
792
  const bar = ctx.getA11yBars()[i];
717
- const primeiro = bar && iconeSelecionado(bar);
718
- if (!bar || !primeiro)
793
+ const first = bar && selectedIcon(bar);
794
+ if (!bar || !first)
719
795
  return;
720
- naBarra.add(i);
721
- const acts = getPauseActs();
722
- if (acts.resume)
723
- acts.resume(); // volta à tela normal: o modo é para usar DURANTE a partida
796
+ onBar.add(i);
724
797
  ctx.srSay(t('sr.a11y.barEnter'));
725
- selecionarIcone(bar, primeiro);
798
+ selectIcon(i, bar, first);
726
799
  }
727
- /** SAI do modo e devolve o direcional ao personagem. Anuncia, porque a devolução também é informação. */
728
- function sairDaBarra(i) {
729
- if (!naBarra.delete(i))
800
+ /**
801
+ * LEAVES the mode and gives the d-pad back to the character. It announces, because giving it back is information too.
802
+ *
803
+ * `silent` is for leaving the bar for ANOTHER screen, not for the game — SELECT, which swaps the quick pause for the card
804
+ * (ADR-0155): «back to the game» said there would be a lie, with the card opening on top.
805
+ *
806
+ * `onLeaveBar` runs on EVERY exit — Back, START or whoever calls this — because the root must unfreeze the game by any
807
+ * door. An exit only one path knew would leave the child on the other back at the character in a stopped world.
808
+ */
809
+ function leaveBarMode(i, silent = false) {
810
+ if (!onBar.delete(i))
730
811
  return;
731
812
  const bar = ctx.getA11yBars()[i];
732
813
  if (bar) {
@@ -735,113 +816,116 @@ export function initPauseIcons(ctx) {
735
816
  if (cap)
736
817
  cap.textContent = '';
737
818
  }
738
- ctx.srSay(t('sr.a11y.barExit'));
819
+ ctx.explainIcon?.(i, null);
820
+ if (!silent)
821
+ ctx.srSay(t('sr.a11y.barExit'));
822
+ ctx.onLeaveBar?.(i, silent);
739
823
  }
740
- /** A tela `i` está com o direcional na barra? É o que o roteamento de entrada pergunta a cada quadro. */
741
- const naBarraDe = (i) => naBarra.has(i);
824
+ /** Is screen `i`'s d-pad on the bar? What input routing asks every frame. */
825
+ const isOnBar = (i) => onBar.has(i);
742
826
  /**
743
- * UM PASSO dentro do modo. `temStart` é a borda do botão que abre a pausa — a segunda saída.
827
+ * ONE STEP inside the mode. `hasStart` is the edge of the button that opens the pause — the second way out.
744
828
  *
745
- * A barra é uma fileira, então as QUATRO direções andam nela: para quem navega sem ver, "cima" numa lista
746
- * de uma linha só não pode ser um beco. E anda em ANEL, como todo menu do jogo desde o item 1.
829
+ * The bar is one row, so all FOUR directions move along it: for someone navigating without sight, «up» in a one-row list
830
+ * must not be a dead end. And it moves in a RING, like every menu of the game (ADR-0044 item 1).
747
831
  */
748
- function navBar(i, k, temStart = false) {
749
- if (!naBarra.has(i))
832
+ function navBar(i, k, hasStart = false) {
833
+ if (!onBar.has(i))
750
834
  return;
751
835
  const bar = ctx.getA11yBars()[i];
752
836
  if (!bar)
753
837
  return;
754
- const acao = acaoNaBarra(k, temStart);
755
- if (acao === 'sair') {
756
- sairDaBarra(i);
838
+ const action = barAction(k, hasStart);
839
+ if (action === 'sair') {
840
+ leaveBarMode(i);
757
841
  return;
758
842
  }
759
- const icones = [...bar.querySelectorAll('.pi-btn')];
760
- if (!icones.length)
843
+ const icons = [...bar.querySelectorAll('.pi-btn')];
844
+ if (!icons.length)
761
845
  return;
762
- const cur = iconeSelecionado(bar);
763
- const idx = cur ? icones.indexOf(cur) : 0;
764
- if (acao === 'ativar') {
765
- setPauseActor(i);
766
- if (cur)
767
- cur.click();
846
+ // never null here: the bar has an icon, and `selectedIcon` falls back to the first one
847
+ const cur = selectedIcon(bar);
848
+ // the click goes through the bar's own listener, which records who pressed it (`setPauseActor`)
849
+ if (action === 'ativar') {
850
+ cur.click();
768
851
  return;
769
852
  }
770
- if (acao === 'andar') {
771
- const d = (k.down || k.right) ? 1 : -1;
772
- selecionarIcone(bar, icones[passoNoAnel(icones.length, idx, d)]);
773
- }
853
+ if (action === 'andar')
854
+ selectIcon(i, bar, icons[stepInRing(icons.length, icons.indexOf(cur), (k.down || k.right) ? 1 : -1)]);
774
855
  }
775
856
  function buildScreenPause(i) {
776
- const sp = docDaMontagem().createElement('div');
857
+ const sp = ctx.doc.createElement('div');
777
858
  sp.className = 'screen-pause';
778
859
  sp.hidden = true;
779
860
  sp.dataset.player = String(i);
780
861
  /*
781
- * ⚠️ MONTA A LISTA INTEIRA E ESCONDE DEPOIS — e a versão anterior desta linha FILTRAVA aqui, o que estava
782
- * errado por uma razão de TEMPO. O comentário que estava neste sítio dizia «montar é o primeiro instante
783
- * em que a resposta existe»; não é. O `getPauseActs` é um getter precisamente porque a tabela chega
784
- * TARDE — «`pauseActs` is a `const` declared far below the init site», diz o próprio campo —, e o
785
- * `createGame` monta durante o próprio `createGame(...)`. Filtrar aqui apagava para sempre todo item cuja
786
- * acção só passou a existir depois do boot.
787
- *
788
- * Quem decide o que se VÊ é o `refrescarItensDaPausa`, a cada `reflectPauseIcons()` — que o `ui/shell`
789
- * dispara quando a fase vira `pause-menu`, ou seja quando a pausa ABRE. O §5 continua respeitado (a
790
- * criança nunca vê um item que não acciona) e agora também vê os que passaram a accionar.
862
+ * BUILDS THE WHOLE LIST, and `refreshPauseItems` decides which items act — here and on every `reflectPauseIcons()`.
863
+ * Filtering at build time would lose for good every item whose action exists only after the boot (see above).
791
864
  */
792
865
  sp.innerHTML = screenPauseMarkup({
793
866
  player: i,
794
867
  numPlayers: ctx.getNumPlayers(),
795
868
  pmButtons: ctx.pmButtons ?? PM_BTNS,
796
869
  optionsButtons: ctx.optionsButtons ?? PM_OPTIONS_BTNS,
870
+ gameButtons: ctx.gameButtons ?? PM_GAME_BTNS,
797
871
  dynLabel: dynLabel, t,
798
872
  });
799
- cartoes[i] = sp;
800
- refrescarItensDaPausa(); // o §5 vale já na montagem, e não só na primeira abertura
873
+ cards[i] = sp;
874
+ refreshPauseItems(); // the lock holds from the build, not only from the first opening
801
875
  sp.addEventListener('click', (e) => {
802
- const target = e.target;
803
- const b = target && target.closest('.pm-btn');
804
- if (b) {
805
- setPauseActor(i);
806
- const act = b.dataset.act || '';
807
- // NAVEGAÇÃO DENTRO DO CARTÃO fica aqui, e não na tabela de ações: `options` e `pmback` não fazem nada
808
- // ao jogo — trocam qual lista está na tela. A tabela vive em `ui/shell`, que não conhece este `sp`.
809
- if (act === 'options' || act === 'pmback') {
810
- anunciarLista(sp, act === 'options' ? 'opcoes' : 'raiz');
811
- return;
812
- }
813
- // `acessibilidade` leva o cursor à BARRA RÁPIDA. Enquanto ela mora dentro do cartão, "entrar no modo"
814
- // é pôr o cursor nela — e a saída continua sendo a saída da pausa, que é a mesma de sempre. Quando o
815
- // item 7 levar a barra para o HUD, esta linha o segue; o que o item SIGNIFICA não muda.
816
- if (act === 'acessibilidade') {
817
- entrarNaBarra(i);
818
- return;
819
- }
820
- const acts = getPauseActs();
821
- const fn = acts[act];
822
- if (fn)
823
- fn();
824
- return;
825
- }
876
+ const b = e.target?.closest('.pm-btn');
877
+ if (b)
878
+ pressPauseItem(i, sp, b);
826
879
  });
827
880
  return sp;
828
881
  }
882
+ /** One item of screen `i`'s pause card pressed — a locked one, a door between its lists, the quick bar, or an action. */
883
+ function pressPauseItem(i, sp, b) {
884
+ // 🔴 THE CURSOR GOES WHERE THE CHILD PRESSED (ADR-0130 rule 1). A finger or a mouse opens a panel without walking the
885
+ // arrows, and the mark stayed on the item the arrows had last reached — so «back» from the panel returned her to an
886
+ // item she never chose. The keyboard's press is already on its item; this is the pointer's half.
887
+ markPauseItem(sp, b);
888
+ // LOCKED (ADR-0161): pressing it SAYS the reason and does nothing — neither door nor action.
889
+ if (b.getAttribute('aria-disabled') === 'true') {
890
+ const reason = b.dataset.motivo ?? '';
891
+ ctx.srSay(reason);
892
+ ctx.explainItem?.(reason);
893
+ return;
894
+ }
895
+ setPauseActor(i);
896
+ const act = b.dataset.act || '';
897
+ // «Opções do jogo» with the cartridge's rows opens the engine's panel (ADR-0182), not the list a host may pass
898
+ const doorWithPanel = act === 'opcoesdojogo' && typeof getPauseActs().opcoesdojogo === 'function';
899
+ if (DOOR_TO_LIST[act] && !doorWithPanel) {
900
+ announceList(sp, DOOR_TO_LIST[act]);
901
+ return;
902
+ }
903
+ // `acessibilidade` takes the cursor to the QUICK BAR. The item is not in the engine's root (ADR-0151); only a list a
904
+ // GAME passes reaches it, and for that list the card closes and the bar is used — explicitly, since `enterBar` does
905
+ // not resume by itself (ADR-0155).
906
+ if (act === 'acessibilidade') {
907
+ getPauseActs().resume?.();
908
+ enterBarMode(i);
909
+ return;
910
+ }
911
+ const fn = getPauseActs()[act];
912
+ if (fn)
913
+ fn();
914
+ }
829
915
  /**
830
- * A BARRA RÁPIDA de uma tela: os dez alternadores, a legenda, e a fiação dos dois.
916
+ * One screen's QUICK BAR: the toggles, the caption, and their wiring.
831
917
  *
832
- * Ela é IRMÃ da `.screen-exp` e não filha, e isso é a decisão do #82 aplicada: a barra é CONTROLE, não
833
- * experiência. O modo empatia degrada a experiência de propósito — simulação é criar dificuldade onde a
834
- * facilidade não existe —, e degradar o que existe para DAR acesso seria o contrário do que ele serve.
918
+ * A SIBLING of `.screen-exp`, not a child (#82): the bar is CONTROL, not experience. Empathy mode degrades the experience
919
+ * on purpose — a simulation creates difficulty — and degrading what exists to GIVE access would be the opposite.
835
920
  */
836
921
  function buildQuickBar(i) {
837
- const bar = docDaMontagem().createElement('div');
922
+ const bar = ctx.doc.createElement('div');
838
923
  bar.className = 'screen-a11y';
839
924
  bar.dataset.player = String(i);
840
- bar.innerHTML = quickBarMarkup(iconesDoJogo);
841
- // FORA DA ORDEM DE TABULAÇÃO durante a partida (ADR-0044, item 7). Dez paradas entre a criança e o jogo
842
- // seria o preço de deixá-los lá — e o alcance por teclado não se perde: ele passa a ser o modo
843
- // `accessibility`, que se abre pela pausa. A barra do TÍTULO não é afetada: lá não se está jogando, e o
844
- // `tabindex` é posto AQUI, no elemento, e não no markup que as duas compartilham.
925
+ bar.innerHTML = quickBarMarkup(ctx.translator, gameIcons);
926
+ // OUT OF THE TAB ORDER during play (ADR-0044 item 7): a stop per icon between the child and the game would be the price
927
+ // of leaving them in, and keyboard reach is not lost — it becomes the `accessibility` mode, opened from the pause. The
928
+ // bar the root mounts in `#title-icons` is unaffected: the `tabindex` is set HERE, on the element, not in the shared markup.
845
929
  bar.querySelectorAll('.pi-btn').forEach((b) => { b.tabIndex = -1; });
846
930
  const cap = bar.querySelector('.pause-icons-cap');
847
931
  bar.addEventListener('click', (e) => {
@@ -852,39 +936,25 @@ export function initPauseIcons(ctx) {
852
936
  iconAct(ib.dataset.pi || '', i);
853
937
  reflectPauseIcons(); // must run BEFORE reading the label back — that is what makes the caption honest
854
938
  if (cap)
855
- cap.textContent = legendaDoIcone(bar, ib);
856
- });
857
- // Legenda = o `aria-label` do botão, para que passar o mouse ou focar diga a MESMA verdade que um leitor
858
- // de tela anunciaria. Uma fonte só para quem vê e para quem escuta.
859
- //
860
- // E ELA SOME AO SAIR. Antes ficava: a última explicação apontada permanecia por cima do jogo até alguém
861
- // apontar outra. Numa barra que agora vive na TELA DE JOGO isso é uma faixa de texto parada em cima da
862
- // partida — o Dev viu e disse o que é: explicação só enquanto o mouse estiver no botão.
863
- //
864
- // A EXCEÇÃO É O CURSOR DO MODO `accessibility`: quando ele está pousado num ícone, a legenda é a única
865
- // coisa que diz onde ele está, e apagá-la ao mexer o mouse cegaria o modo. Daí a pergunta pelo `.pi-sel`.
866
- const limpar = () => { if (cap && !bar.querySelector('.pi-sel'))
867
- cap.textContent = ''; };
868
- bar.querySelectorAll('.pi-btn').forEach((b) => {
869
- const show = () => { if (cap)
870
- cap.textContent = legendaDoIcone(bar, b); };
871
- b.addEventListener('mouseenter', show);
872
- b.addEventListener('focus', show);
873
- b.addEventListener('mouseleave', limpar);
874
- b.addEventListener('blur', limpar);
939
+ cap.textContent = accessibleLabel(ib);
875
940
  });
941
+ // Caption = the button's `aria-label`, so hovering or focusing says the SAME truth a screen reader would announce: one
942
+ // source for whoever sees and whoever listens. It GOES AWAY on leaving — a bar on the play screen must not leave a strip
943
+ // of text over the game (the Dev: an explanation only while the pointer is on the button). The exception is the
944
+ // `accessibility` mode's CURSOR: when it sits on an icon the caption is the only thing saying where it is, hence the
945
+ // `.pi-sel` check in `wireBarCaption`.
946
+ wireBarCaption(bar, (k) => ctx.explainIcon?.(i, k));
876
947
  return bar;
877
948
  }
878
949
  return {
879
- buildScreenPause, buildQuickBar, entrarNaBarra, sairDaBarra, naBarraDe, navBar,
880
- iconesMontados: iconesDoJogo,
950
+ buildScreenPause, buildQuickBar, enterBar: enterBarMode, leaveBar: leaveBarMode, onBar: isOnBar, navBar,
951
+ mountedIcons: gameIcons,
881
952
  iconAct, iconLabel, reflectIconBtn, reflectIconsIn, reflectPauseIcons,
882
- // ⚠️ O `setCalmMode` PERSISTE TAMBÉM, e sanea. Ele é a outra porta para o mesmo valor — se só o ciclo do
883
- // ícone gravasse, um nível posto por aqui sobreviveria à sessão e não ao fecho da aba, que é a metade
884
- // pior do defeito: o ajuste parece ter pegado e some depois.
953
+ // `setCalmMode` PERSISTS TOO, and sanitises: it is the other door to the same value, and a level set here that only
954
+ // lasted the session would look applied and then vanish.
885
955
  applyCalm,
886
956
  getCalmMode: () => calmMode,
887
- setCalmMode: (n) => { calmMode = saneiaNivelTea(n); store.set(store.KEYS.tea, calmMode); },
957
+ setCalmMode: (n) => { calmMode = sanitiseTeaLevel(n, DEFAULTS.calmMode); ctx.store.set(KEYS.tea, calmMode); },
888
958
  iconState,
889
959
  };
890
960
  }