@the-inclusionist/engine 6.36.1

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 (267) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +107 -0
  3. package/app/css/style.css +622 -0
  4. package/app/public/vendor/fonts/andika-400-ext.woff2 +0 -0
  5. package/app/public/vendor/fonts/andika-400.woff2 +0 -0
  6. package/app/public/vendor/fonts/andika-700-ext.woff2 +0 -0
  7. package/app/public/vendor/fonts/andika-700.woff2 +0 -0
  8. package/app/public/vendor/fonts/atkinson-400-ext.woff2 +0 -0
  9. package/app/public/vendor/fonts/atkinson-400.woff2 +0 -0
  10. package/app/public/vendor/fonts/atkinson-700-ext.woff2 +0 -0
  11. package/app/public/vendor/fonts/atkinson-700.woff2 +0 -0
  12. package/app/public/vendor/fonts/atkinson-mono-400-ext.woff2 +0 -0
  13. package/app/public/vendor/fonts/atkinson-mono-400.woff2 +0 -0
  14. package/app/public/vendor/fonts/comicneue-400.woff2 +0 -0
  15. package/app/public/vendor/fonts/comicneue-700.woff2 +0 -0
  16. package/app/public/vendor/fonts/greatvibes-400.woff2 +0 -0
  17. package/app/public/vendor/fonts/inter-400.woff2 +0 -0
  18. package/app/public/vendor/fonts/inter-700.woff2 +0 -0
  19. package/app/public/vendor/fonts/lato-400.woff2 +0 -0
  20. package/app/public/vendor/fonts/lato-700.woff2 +0 -0
  21. package/app/public/vendor/fonts/lexend-400-ext.woff2 +0 -0
  22. package/app/public/vendor/fonts/lexend-400.woff2 +0 -0
  23. package/app/public/vendor/fonts/lexend-700-ext.woff2 +0 -0
  24. package/app/public/vendor/fonts/lexend-700.woff2 +0 -0
  25. package/app/public/vendor/fonts/literata-400.woff2 +0 -0
  26. package/app/public/vendor/fonts/literata-700.woff2 +0 -0
  27. package/app/public/vendor/fonts/newsreader-400.woff2 +0 -0
  28. package/app/public/vendor/fonts/newsreader-700.woff2 +0 -0
  29. package/app/public/vendor/fonts/opensans-400.woff2 +0 -0
  30. package/app/public/vendor/fonts/opensans-700.woff2 +0 -0
  31. package/app/public/vendor/fonts/pinyon-400.woff2 +0 -0
  32. package/app/public/vendor/fonts/quattro-400.woff2 +0 -0
  33. package/app/public/vendor/fonts/quattro-700.woff2 +0 -0
  34. package/app/public/vendor/fonts/sourcesans-400.woff2 +0 -0
  35. package/app/public/vendor/fonts/sourcesans-700.woff2 +0 -0
  36. package/app/public/vendor/fonts/sourceserif-400.woff2 +0 -0
  37. package/app/public/vendor/fonts/sourceserif-700.woff2 +0 -0
  38. package/app/public/vendor/fonts/ufcook-700.woff2 +0 -0
  39. package/app/public/vendor/fonts/ufmag-400.woff2 +0 -0
  40. package/app/public/vendor/fonts.css +88 -0
  41. package/dist-pkg/boot/create-game.d.ts +159 -0
  42. package/dist-pkg/boot/create-game.js +266 -0
  43. package/dist-pkg/core/a11y-sr.d.ts +4 -0
  44. package/dist-pkg/core/a11y-sr.js +20 -0
  45. package/dist-pkg/core/actions.d.ts +100 -0
  46. package/dist-pkg/core/actions.js +149 -0
  47. package/dist-pkg/core/anel.d.ts +17 -0
  48. package/dist-pkg/core/anel.js +31 -0
  49. package/dist-pkg/core/collision.d.ts +21 -0
  50. package/dist-pkg/core/collision.js +62 -0
  51. package/dist-pkg/core/constants.d.ts +84 -0
  52. package/dist-pkg/core/constants.js +64 -0
  53. package/dist-pkg/core/contract.d.ts +227 -0
  54. package/dist-pkg/core/contract.js +184 -0
  55. package/dist-pkg/core/dom-query.d.ts +9 -0
  56. package/dist-pkg/core/dom-query.js +25 -0
  57. package/dist-pkg/core/entity.d.ts +150 -0
  58. package/dist-pkg/core/entity.js +38 -0
  59. package/dist-pkg/core/escape-html.d.ts +16 -0
  60. package/dist-pkg/core/escape-html.js +32 -0
  61. package/dist-pkg/core/i18n.d.ts +71 -0
  62. package/dist-pkg/core/i18n.js +236 -0
  63. package/dist-pkg/core/layers.d.ts +39 -0
  64. package/dist-pkg/core/layers.js +71 -0
  65. package/dist-pkg/core/letter-grid.d.ts +55 -0
  66. package/dist-pkg/core/letter-grid.js +116 -0
  67. package/dist-pkg/core/loop.d.ts +17 -0
  68. package/dist-pkg/core/loop.js +36 -0
  69. package/dist-pkg/core/password.d.ts +34 -0
  70. package/dist-pkg/core/password.js +205 -0
  71. package/dist-pkg/core/rng.d.ts +20 -0
  72. package/dist-pkg/core/rng.js +61 -0
  73. package/dist-pkg/core/rotulo-acessivel.d.ts +17 -0
  74. package/dist-pkg/core/rotulo-acessivel.js +41 -0
  75. package/dist-pkg/core/run-state.d.ts +115 -0
  76. package/dist-pkg/core/run-state.js +40 -0
  77. package/dist-pkg/core/scenes.d.ts +68 -0
  78. package/dist-pkg/core/scenes.js +64 -0
  79. package/dist-pkg/core/screens.d.ts +16 -0
  80. package/dist-pkg/core/screens.js +28 -0
  81. package/dist-pkg/core/state.d.ts +138 -0
  82. package/dist-pkg/core/state.js +290 -0
  83. package/dist-pkg/core/tiles.d.ts +12 -0
  84. package/dist-pkg/core/tiles.js +61 -0
  85. package/dist-pkg/core/world.d.ts +4 -0
  86. package/dist-pkg/core/world.js +58 -0
  87. package/dist-pkg/educational/activities-registry.d.ts +47 -0
  88. package/dist-pkg/educational/activities-registry.js +95 -0
  89. package/dist-pkg/i18n/en.d.ts +3 -0
  90. package/dist-pkg/i18n/en.js +560 -0
  91. package/dist-pkg/i18n/es.d.ts +3 -0
  92. package/dist-pkg/i18n/es.js +558 -0
  93. package/dist-pkg/i18n/pt.d.ts +3 -0
  94. package/dist-pkg/i18n/pt.js +618 -0
  95. package/dist-pkg/input/default-bindings.d.ts +24 -0
  96. package/dist-pkg/input/default-bindings.js +122 -0
  97. package/dist-pkg/input/devices.d.ts +15 -0
  98. package/dist-pkg/input/devices.js +31 -0
  99. package/dist-pkg/input/edges.d.ts +58 -0
  100. package/dist-pkg/input/edges.js +49 -0
  101. package/dist-pkg/input/gamepad.d.ts +203 -0
  102. package/dist-pkg/input/gamepad.js +531 -0
  103. package/dist-pkg/input/keyboard-runtime.d.ts +61 -0
  104. package/dist-pkg/input/keyboard-runtime.js +83 -0
  105. package/dist-pkg/input/keyboard.d.ts +45 -0
  106. package/dist-pkg/input/keyboard.js +80 -0
  107. package/dist-pkg/input/keydown.d.ts +270 -0
  108. package/dist-pkg/input/keydown.js +381 -0
  109. package/dist-pkg/input/latch.d.ts +30 -0
  110. package/dist-pkg/input/latch.js +63 -0
  111. package/dist-pkg/input/pointer-space.d.ts +40 -0
  112. package/dist-pkg/input/pointer-space.js +51 -0
  113. package/dist-pkg/input/state.d.ts +11 -0
  114. package/dist-pkg/input/state.js +12 -0
  115. package/dist-pkg/input/touch-bindings.d.ts +210 -0
  116. package/dist-pkg/input/touch-bindings.js +365 -0
  117. package/dist-pkg/input/touch.d.ts +173 -0
  118. package/dist-pkg/input/touch.js +314 -0
  119. package/dist-pkg/input/transports.d.ts +90 -0
  120. package/dist-pkg/input/transports.js +96 -0
  121. package/dist-pkg/input/vocabulary-migration.d.ts +58 -0
  122. package/dist-pkg/input/vocabulary-migration.js +125 -0
  123. package/dist-pkg/platform/audio-ambient.d.ts +27 -0
  124. package/dist-pkg/platform/audio-ambient.js +89 -0
  125. package/dist-pkg/platform/audio-earcons.d.ts +24 -0
  126. package/dist-pkg/platform/audio-earcons.js +62 -0
  127. package/dist-pkg/platform/audio-jingles.d.ts +17 -0
  128. package/dist-pkg/platform/audio-jingles.js +54 -0
  129. package/dist-pkg/platform/audio-mixer.d.ts +14 -0
  130. package/dist-pkg/platform/audio-mixer.js +62 -0
  131. package/dist-pkg/platform/audio-nav.d.ts +51 -0
  132. package/dist-pkg/platform/audio-nav.js +99 -0
  133. package/dist-pkg/platform/audio-sonar.d.ts +71 -0
  134. package/dist-pkg/platform/audio-sonar.js +173 -0
  135. package/dist-pkg/platform/audio.d.ts +32 -0
  136. package/dist-pkg/platform/audio.js +191 -0
  137. package/dist-pkg/platform/interruptible-speech.d.ts +19 -0
  138. package/dist-pkg/platform/interruptible-speech.js +79 -0
  139. package/dist-pkg/platform/speech.d.ts +2 -0
  140. package/dist-pkg/platform/speech.js +35 -0
  141. package/dist-pkg/platform/storage.d.ts +87 -0
  142. package/dist-pkg/platform/storage.js +148 -0
  143. package/dist-pkg/platform/tts.d.ts +33 -0
  144. package/dist-pkg/platform/tts.js +142 -0
  145. package/dist-pkg/render/camera.d.ts +58 -0
  146. package/dist-pkg/render/camera.js +105 -0
  147. package/dist-pkg/render/canvas.d.ts +13 -0
  148. package/dist-pkg/render/canvas.js +26 -0
  149. package/dist-pkg/render/cenario-data.d.ts +127 -0
  150. package/dist-pkg/render/cenario-data.js +145 -0
  151. package/dist-pkg/render/city-tex.d.ts +63 -0
  152. package/dist-pkg/render/city-tex.js +228 -0
  153. package/dist-pkg/render/city-tiles.d.ts +11 -0
  154. package/dist-pkg/render/city-tiles.js +140 -0
  155. package/dist-pkg/render/crt.d.ts +19 -0
  156. package/dist-pkg/render/crt.js +92 -0
  157. package/dist-pkg/render/cvd-matrices.d.ts +35 -0
  158. package/dist-pkg/render/cvd-matrices.js +91 -0
  159. package/dist-pkg/render/draw.d.ts +149 -0
  160. package/dist-pkg/render/draw.js +221 -0
  161. package/dist-pkg/render/fx.d.ts +63 -0
  162. package/dist-pkg/render/fx.js +123 -0
  163. package/dist-pkg/render/hc-role-data.d.ts +14 -0
  164. package/dist-pkg/render/hc-role-data.js +24 -0
  165. package/dist-pkg/render/high-contrast.d.ts +109 -0
  166. package/dist-pkg/render/high-contrast.js +254 -0
  167. package/dist-pkg/render/lq-filter.d.ts +37 -0
  168. package/dist-pkg/render/lq-filter.js +94 -0
  169. package/dist-pkg/render/minimap.d.ts +11 -0
  170. package/dist-pkg/render/minimap.js +94 -0
  171. package/dist-pkg/render/parallax.d.ts +85 -0
  172. package/dist-pkg/render/parallax.js +158 -0
  173. package/dist-pkg/render/player-anim.d.ts +71 -0
  174. package/dist-pkg/render/player-anim.js +152 -0
  175. package/dist-pkg/render/port.d.ts +143 -0
  176. package/dist-pkg/render/port.js +29 -0
  177. package/dist-pkg/render/recycling-tex.d.ts +48 -0
  178. package/dist-pkg/render/recycling-tex.js +164 -0
  179. package/dist-pkg/render/scene-city.d.ts +77 -0
  180. package/dist-pkg/render/scene-city.js +181 -0
  181. package/dist-pkg/render/scene-parallax.d.ts +72 -0
  182. package/dist-pkg/render/scene-parallax.js +426 -0
  183. package/dist-pkg/render/scene-sky.d.ts +126 -0
  184. package/dist-pkg/render/scene-sky.js +295 -0
  185. package/dist-pkg/render/screen-pipeline.d.ts +134 -0
  186. package/dist-pkg/render/screen-pipeline.js +177 -0
  187. package/dist-pkg/render/set-cenario.d.ts +54 -0
  188. package/dist-pkg/render/set-cenario.js +103 -0
  189. package/dist-pkg/render/sprite-fx.d.ts +3 -0
  190. package/dist-pkg/render/sprite-fx.js +40 -0
  191. package/dist-pkg/render/textures.d.ts +75 -0
  192. package/dist-pkg/render/textures.js +240 -0
  193. package/dist-pkg/render/title-scene.d.ts +46 -0
  194. package/dist-pkg/render/title-scene.js +75 -0
  195. package/dist-pkg/render/viewports.d.ts +52 -0
  196. package/dist-pkg/render/viewports.js +185 -0
  197. package/dist-pkg/render/viz-axes.d.ts +98 -0
  198. package/dist-pkg/render/viz-axes.js +184 -0
  199. package/dist-pkg/render/viz-modes.d.ts +61 -0
  200. package/dist-pkg/render/viz-modes.js +74 -0
  201. package/dist-pkg/render/viz-setters.d.ts +130 -0
  202. package/dist-pkg/render/viz-setters.js +244 -0
  203. package/dist-pkg/render/weather.d.ts +96 -0
  204. package/dist-pkg/render/weather.js +171 -0
  205. package/dist-pkg/render/wheelchair-sprites.d.ts +24 -0
  206. package/dist-pkg/render/wheelchair-sprites.js +63 -0
  207. package/dist-pkg/render/world-tex.d.ts +18 -0
  208. package/dist-pkg/render/world-tex.js +87 -0
  209. package/dist-pkg/ui/activities-menu.d.ts +258 -0
  210. package/dist-pkg/ui/activities-menu.js +648 -0
  211. package/dist-pkg/ui/caa-sets.d.ts +39 -0
  212. package/dist-pkg/ui/caa-sets.js +65 -0
  213. package/dist-pkg/ui/changed-mark.d.ts +15 -0
  214. package/dist-pkg/ui/changed-mark.js +96 -0
  215. package/dist-pkg/ui/debug-panel.d.ts +67 -0
  216. package/dist-pkg/ui/debug-panel.js +173 -0
  217. package/dist-pkg/ui/dom.d.ts +24 -0
  218. package/dist-pkg/ui/dom.js +47 -0
  219. package/dist-pkg/ui/focus-trap.d.ts +71 -0
  220. package/dist-pkg/ui/focus-trap.js +99 -0
  221. package/dist-pkg/ui/fonts.d.ts +41 -0
  222. package/dist-pkg/ui/fonts.js +59 -0
  223. package/dist-pkg/ui/hud.d.ts +137 -0
  224. package/dist-pkg/ui/hud.js +229 -0
  225. package/dist-pkg/ui/item-announcement.d.ts +19 -0
  226. package/dist-pkg/ui/item-announcement.js +36 -0
  227. package/dist-pkg/ui/layout.d.ts +6 -0
  228. package/dist-pkg/ui/layout.js +54 -0
  229. package/dist-pkg/ui/loop-crash.d.ts +19 -0
  230. package/dist-pkg/ui/loop-crash.js +60 -0
  231. package/dist-pkg/ui/map-hub.d.ts +56 -0
  232. package/dist-pkg/ui/map-hub.js +138 -0
  233. package/dist-pkg/ui/menu-nav.d.ts +141 -0
  234. package/dist-pkg/ui/menu-nav.js +390 -0
  235. package/dist-pkg/ui/pause-icons.d.ts +327 -0
  236. package/dist-pkg/ui/pause-icons.js +620 -0
  237. package/dist-pkg/ui/reach-notice.d.ts +34 -0
  238. package/dist-pkg/ui/reach-notice.js +71 -0
  239. package/dist-pkg/ui/settings-audio.d.ts +139 -0
  240. package/dist-pkg/ui/settings-audio.js +544 -0
  241. package/dist-pkg/ui/settings-caa.d.ts +45 -0
  242. package/dist-pkg/ui/settings-caa.js +137 -0
  243. package/dist-pkg/ui/settings-controls.d.ts +100 -0
  244. package/dist-pkg/ui/settings-controls.js +152 -0
  245. package/dist-pkg/ui/settings-empathy.d.ts +62 -0
  246. package/dist-pkg/ui/settings-empathy.js +137 -0
  247. package/dist-pkg/ui/settings-motion.d.ts +95 -0
  248. package/dist-pkg/ui/settings-motion.js +282 -0
  249. package/dist-pkg/ui/settings-motor.d.ts +77 -0
  250. package/dist-pkg/ui/settings-motor.js +215 -0
  251. package/dist-pkg/ui/settings-panel.d.ts +76 -0
  252. package/dist-pkg/ui/settings-panel.js +187 -0
  253. package/dist-pkg/ui/settings-typo.d.ts +74 -0
  254. package/dist-pkg/ui/settings-typo.js +150 -0
  255. package/dist-pkg/ui/settings-visual.d.ts +111 -0
  256. package/dist-pkg/ui/settings-visual.js +252 -0
  257. package/dist-pkg/ui/shell.d.ts +239 -0
  258. package/dist-pkg/ui/shell.js +430 -0
  259. package/dist-pkg/ui/title.d.ts +36 -0
  260. package/dist-pkg/ui/title.js +60 -0
  261. package/dist-pkg/ui/vlibras.d.ts +11 -0
  262. package/dist-pkg/ui/vlibras.js +87 -0
  263. package/dist-pkg/ui/webcam.d.ts +7 -0
  264. package/dist-pkg/ui/webcam.js +88 -0
  265. package/docs/CREDITS.md +64 -0
  266. package/docs/LICENSES.md +132 -0
  267. package/package.json +115 -0
@@ -0,0 +1,620 @@
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.
5
+ //
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.
10
+ //
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.
14
+ //
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, CONTRAST_LABELS } from './settings-visual.js';
29
+ import { anunciarItem } from './item-announcement.js';
30
+ import { rotuloAcessivel } from '../core/rotulo-acessivel.js';
31
+ import { passoNoAnel } from '../core/anel.js'; // da FOLHA, e não de ui/menu-nav: ver a nota lá
32
+ // LIGAÇÃO VIVA (ESM): o índice pode ser desligado no menu, e o valor aqui acompanha sem assinatura.
33
+ import { menuIndexOn } from '../core/state.js';
34
+ /**
35
+ * A LEGENDA de um ícone da barra de acessibilidade — uma função, e não três cópias da mesma expressão.
36
+ *
37
+ * Ela é escrita em TRÊS momentos que parecem diferentes e são o mesmo: o cursor direcional pousa no ícone
38
+ * (`ui/menu-nav`), o dedo o aciona, e o mouse ou o foco passa por cima. Enquanto eram três linhas soltas, o
39
+ * índice do ADR-0044 teria de ser acrescentado em três lugares — e a chance de um ficar para trás é a mesma
40
+ * que este repositório já pagou dezesseis vezes com o `DomQuery`.
41
+ *
42
+ * O `aria-label` já conta o ESTADO ("Alto contraste, ativado"): é ele que o `reflectIconBtn` reescreve a cada
43
+ * mudança, e é por isso que a legenda o lê de volta em vez de recompor o texto por conta própria.
44
+ */
45
+ export function legendaDoIcone(barra, el) {
46
+ const icones = [...barra.querySelectorAll('.pi-btn')];
47
+ // A regra "rótulo declarado vence" nasceu AQUI e valia só para os dez ícones. Virou `core/rotulo-acessivel`
48
+ // e agora vale para o menu inicial e para a lista de pausa também — uma resposta para "como se chama este
49
+ // controle", e não três.
50
+ return anunciarItem({ rotulo: rotuloAcessivel(el), posicao: icones.indexOf(el) + 1, total: icones.length }, menuIndexOn);
51
+ }
52
+ /** The accessibility shortcut bar at the top of every pause screen (and of the splash `#title-icons`).
53
+ * Sound-bound icons (blind/TTS) require a private audio output; webcam/voice are still `soon`.
54
+ * VERBATIM from game.js in order and behaviour; the names became i18n keys in the Fase-5 pass. */
55
+ // `n` é a CHAVE i18n do nome do ícone (o emoji `e` não traduz — é o mesmo glifo em toda língua).
56
+ export const PAUSE_ICONS = [
57
+ { k: 'blind', e: '🦯', n: 'icon.blind' },
58
+ { k: 'tts', e: '🗨️', n: 'icon.tts' },
59
+ { k: 'libras', e: '🤟', n: 'icon.libras' },
60
+ { k: 'tea', e: '🧩', n: 'icon.tea' },
61
+ { k: 'altmove', e: '☝️', n: 'icon.altmove' }, // ☝️ e não 🦾: o gesto é UM DEDO tocando, que é o que a alternância pede (pedido do Dev)
62
+ { k: 'contrast', e: '🌗', n: 'icon.contrast' },
63
+ { k: 'cvd', e: '🚥', n: 'icon.cvd' },
64
+ { k: 'face', e: '🧑', n: 'icon.face', soon: true },
65
+ { k: 'eyes', e: '👀', n: 'icon.eyes', soon: true },
66
+ { k: 'voice', e: '👄', n: 'icon.voice', soon: true },
67
+ ];
68
+ const ICON_BY_KEY = new Map(PAUSE_ICONS.map((ic) => [ic.k, ic]));
69
+ export function pauseIcon(k) { return ICON_BY_KEY.get(k); }
70
+ /** TEA cycle: 0 = normal · 1 = calmo (reduces) · 2 = silencioso (switches off). Never touches TTS/blind mode. */
71
+ export const CALM_NAMES = ['calm.off', 'calm.quiet', 'calm.silent'];
72
+ /** The audio categories `applyCalm` governs. TTS/sonar/guarda/guia stay untouched — a calm player still needs them. */
73
+ export const CALM_AUDIO_CATS = ['ambient', 'music', 'earcons', 'other', 'interact'];
74
+ /** Colour-vision-deficiency cycle, in `player.viz` values. */
75
+ export const CVD_SEQ = ['normal', 'fix-protan', 'fix-deuter', 'fix-tritan'];
76
+ /** i18n keys of the CVD announcement names, indexed the same as CVD_SEQ.
77
+ *
78
+ * ⚠️ POSITION 0 IS `cvd.tricro` AND NOT `cvd.off`, AND THE TWO KEYS ARE NOT INTERCHANGEABLE.
79
+ * This list names the four CHOICES of the cycle, so position 0 is a way of seeing — trichromatic
80
+ * vision, the one that needs no correction — and it is said as one. `cvd.off` below is a FALLBACK
81
+ * for `s.viz` values that are not corrections at all, and 13 of the 16 viz modes are exactly that:
82
+ * the three simulations, the three contrast levels, the five low-vision modes and blind mode.
83
+ * Announcing "trichromatic vision" there would have the software assert what the child sees, while
84
+ * she is simulating not seeing it. The choice is named; the fallback is switched off. */
85
+ export const CVD_NAMES = ['cvd.tricro', 'cvd.protan', 'cvd.deuter', 'cvd.tritan'];
86
+ /** `player.viz` → i18n key of the label used by iconLabel (anything else falls back to the 'off' key). */
87
+ export const CVD_LABELS = {
88
+ 'fix-protan': 'cvd.protan', 'fix-deuter': 'cvd.deuter', 'fix-tritan': 'cvd.tritan',
89
+ };
90
+ /** A player has private output when nobody else is on the same sink. Single screen ⇒ always private.
91
+ * Pure form of game.js's hasPrivateOutput (see the report: it had no other caller left). */
92
+ export function hasPrivateOutputIn(list, count, i) {
93
+ if (count <= 1)
94
+ return true;
95
+ const p = list[i];
96
+ if (!p || !p.audioSink)
97
+ return false;
98
+ return !list.some((q, j) => j !== i && q && q.audioSink === p.audioSink);
99
+ }
100
+ /** TEA cycle step. */
101
+ export function nextCalmMode(cur) { return (cur + 1) % 3; }
102
+ /** Next high-contrast level. An unlisted `viz` (e.g. a CVD filter) is treated as index 0 ⇒ jumps to 'hc-direto'. */
103
+ export function nextContrast(cur) {
104
+ let idx = CONTRAST_LEVELS.indexOf(cur);
105
+ idx = idx < 0 ? 0 : idx;
106
+ return CONTRAST_LEVELS[(idx + 1) % CONTRAST_LEVELS.length];
107
+ }
108
+ /** Next CVD filter. NOTE the asymmetry with nextContrast: an unlisted `viz` maps to index 1 ('fix-protan'),
109
+ * not 0 — verbatim from game.js (`idx = idx<0 ? 1 : (idx+1)%seq.length`). */
110
+ export function nextCvd(cur) {
111
+ let idx = CVD_SEQ.indexOf(cur);
112
+ idx = idx < 0 ? 1 : (idx + 1) % CVD_SEQ.length;
113
+ return { idx, mode: CVD_SEQ[idx] };
114
+ }
115
+ /** What `applyCalm` does to ONE audio category, given the TEA level. Extracted so the (destructive) volume
116
+ * clamp at level 1 is visible and testable — see the report. */
117
+ export function calmAudioPlan(calmMode, vol) {
118
+ if (calmMode === 0)
119
+ return { on: true, vol };
120
+ if (calmMode === 1)
121
+ return { on: true, vol: Math.min(vol, 0.3) };
122
+ return { on: false, vol };
123
+ }
124
+ /** What `applyCalm` does to the scene/character reduced-motion flags. */
125
+ export function calmMotionPlan(calmMode) {
126
+ return { sceneReduced: calmMode >= 1, charFrozen: calmMode === 2 };
127
+ }
128
+ /** The `aria-label` of one icon — it MUST reflect the current state, on/off or level. This is the whole
129
+ * point of the function: a toggle that looks pressed but does not say so is invisible to a screen reader. */
130
+ export function computeIconLabel(k, s) {
131
+ const ic = ICON_BY_KEY.get(k);
132
+ if (!ic)
133
+ return '';
134
+ // O estado vira SEMPRE um parâmetro (`{v}`), nunca uma concatenação: 'on'/'off' eram palavras inglesas
135
+ // presas numa frase em português, e uma língua que anteponha o estado ao nome precisa do dicionário para
136
+ // reordenar. `nomeDoIcone: estado` é a moldura; o estado é o conteúdo, e ele também é traduzido.
137
+ if (ic.soon)
138
+ return t('icon.soon', { nome: t(ic.n) });
139
+ const rotulo = (v) => t('icon.state', { nome: t(ic.n), v: t(v) });
140
+ if (k === 'blind')
141
+ return rotulo(s.modoCego ? 'state.on' : 'state.off');
142
+ if (k === 'tts')
143
+ return rotulo(s.ttsOn ? 'state.on' : 'state.off');
144
+ if (k === 'libras')
145
+ return rotulo(s.librasOn ? 'state.on' : 'state.off');
146
+ // TEA e daltonismo usam um nome CURTO aqui, diferente do nome do botão: o rótulo já diz o nível, e
147
+ // repetir a lista de níveis do nome ("(calmo / silencioso)", "(protan/deutan/tritan)") a diria duas vezes.
148
+ // Era assim antes da conversão, com o texto curto embutido — preservado, não reinventado.
149
+ if (k === 'tea')
150
+ return t('icon.state', { nome: t('icon.tea.short'), v: t(CALM_NAMES[s.calmMode]) });
151
+ if (k === 'altmove')
152
+ return rotulo(s.toggleMove ? 'state.on' : 'state.off');
153
+ if (k === 'contrast')
154
+ return rotulo(CONTRAST_LABELS[s.viz] || 'contrast.off');
155
+ if (k === 'cvd')
156
+ return t('icon.state', { nome: t('icon.cvd.short'), v: t(CVD_LABELS[s.viz] || 'cvd.off') });
157
+ return t(ic.n);
158
+ }
159
+ /** Pure form of reflectIconBtn's branching. A `soon` icon lands on all-false — it never claims to be on. */
160
+ export function computeIconVisual(k, s) {
161
+ let on = false, dis = false, calm = false, cvd = '';
162
+ if (k === 'blind') {
163
+ on = s.modoCego;
164
+ dis = !s.privateOutput;
165
+ }
166
+ else if (k === 'tts') {
167
+ on = s.ttsOn;
168
+ dis = !s.privateOutput;
169
+ }
170
+ else if (k === 'libras') {
171
+ on = s.librasOn;
172
+ }
173
+ else if (k === 'tea') {
174
+ on = s.calmMode === 2;
175
+ calm = s.calmMode === 1;
176
+ }
177
+ else if (k === 'altmove') {
178
+ on = s.toggleMove;
179
+ }
180
+ else if (k === 'contrast') {
181
+ on = /^hc-direto/.test(s.viz || '');
182
+ }
183
+ else if (k === 'cvd') {
184
+ if (s.viz === 'fix-protan')
185
+ cvd = 'pi-cvd-protan';
186
+ else if (s.viz === 'fix-deuter')
187
+ cvd = 'pi-cvd-deuter';
188
+ else if (s.viz === 'fix-tritan')
189
+ cvd = 'pi-cvd-tritan';
190
+ }
191
+ return { on, dis, calm, cvd, active: on || calm || !!cvd };
192
+ }
193
+ /** The CSS classes reflectIconBtn clears before applying a fresh visual — in the original order. */
194
+ export const ICON_STATE_CLASSES = ['pi-calm', 'pi-cvd-protan', 'pi-cvd-deuter', 'pi-cvd-tritan'];
195
+ // --- markup (pure string builders; the DOM shell below just assigns them) ---
196
+ /** One `.pi-btn`. `soon` icons get `.pi-soon` and the "under construction" suffix baked into the aria-label.
197
+ * The label is the RESTING one: reflectIconBtn overwrites it with the stateful label as soon as the bar is
198
+ * reflected. `ic.n` is an i18n key, so it must be resolved here too — the markup is rendered once at build
199
+ * time and would otherwise ship the raw key to a screen reader. */
200
+ export function iconBtnMarkup(ic) {
201
+ return '<button class="pi-btn' + (ic.soon ? ' pi-soon' : '') + '" type="button" data-pi="' + ic.k +
202
+ '" aria-label="' + (ic.soon ? t('icon.soon', { nome: t(ic.n) }) : t(ic.n)) + '">' + ic.e + '</button>';
203
+ }
204
+ /** The whole icon bar. Used by the pause screen AND by the splash `#title-icons` (which built the same string
205
+ * by hand in game.js — that duplication dies with this export). */
206
+ export function iconsMarkup() { return PAUSE_ICONS.map(iconBtnMarkup).join(''); }
207
+ /** One `.pm-btn`. Dynamic labels (`letra`/`nivel`) are rendered eagerly and carry NO `data-i18n`, so
208
+ * i18n.applyDom() cannot overwrite them. */
209
+ /**
210
+ * ⚠️ O RÓTULO DINÂMICO ENTRA PRONTO (item 19), e a mudança conserta DUAS coisas de uma vez.
211
+ *
212
+ * A linha era `'📚 Nível ' + level + ' · ' + qlName[level]` — e ela tinha dois defeitos que só se enxergam
213
+ * juntos:
214
+ *
215
+ * 1. FRONTEIRA. `level` vinha de `core/state.quizLevel` e `qlName` de uma tabela do jogo. Um menu de pausa
216
+ * da ENGINE montava o rótulo de uma atividade de alfabetização — conteúdo pedagógico, não mecânica.
217
+ * 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
218
+ * alcança `ui/`, então esta linha atravessou a i18n inteira sem ser vista. Num build em inglês, o menu
219
+ * de pausa de uma criança dizia "📚 Nível 2 · …".
220
+ *
221
+ * Agora o jogo entrega a frase montada (`dynLabel`), e a engine só a coloca no botão. O jogo é quem sabe o
222
+ * que é um nível, quem sabe o nome dele e quem sabe em que idioma dizê-lo.
223
+ */
224
+ export function pmBtnMarkup(b, dynLabel, tr) {
225
+ const dyn = b.letra || b.nivel;
226
+ const lbl = dynLabel(b) ?? (dyn ? (b.lbl ?? '') : tr('pause.' + b.act));
227
+ return '<button class="pm-btn' + (b.letra ? ' pm-letra' : '') + (b.nivel ? ' pm-nivel' : '') +
228
+ '" role="menuitem" type="button" data-act="' + b.act + '"' +
229
+ (dyn ? '' : (' data-i18n="pause.' + b.act + '"')) + '>' + lbl + '</button>';
230
+ }
231
+ /**
232
+ * Os itens navegáveis de um cartão de pausa — os da lista VISÍVEL, e só eles.
233
+ *
234
+ * Uma constante porque TRÊS módulos a consultam (a navegação em `ui/menu-nav`, a seleção inicial em
235
+ * `ui/shell` e a troca de submenu aqui). Enquanto fosse `.pm-btn` escrito três vezes, bastaria um deles
236
+ * esquecer o `:not([hidden])` para o anel atravessar para a lista invisível — e a criança ouviria itens de um
237
+ * menu que não está na tela.
238
+ */
239
+ export const PM_ITENS_VISIVEIS = '.pause-menu:not([hidden]) .pm-btn';
240
+ /** O innerHTML de UMA `.pause-menu`: a lista, e só ela. */
241
+ export function pauseMenuHtml(bs, sub, dynLabel, tr) {
242
+ return '<div class="pause-menu" role="menu" data-sub="' + sub + '"' + (sub === 'raiz' ? '' : ' hidden') + '>' +
243
+ bs.map((b) => pmBtnMarkup(b, dynLabel, tr)).join('') + '</div>';
244
+ }
245
+ /**
246
+ * Troca a lista visível de UM cartão de pausa, e põe o cursor no PRIMEIRO item da lista que entrou.
247
+ *
248
+ * 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
249
+ * opções à raiz, e não tem acesso às tabelas de botões. Como as duas listas já existem no markup, a troca é
250
+ * só DOM — nada a re-renderizar, nada a injetar.
251
+ */
252
+ export function mostrarSubmenuDaPausa(sp, sub) {
253
+ sp.querySelectorAll('.pause-menu').forEach((m) => { m.hidden = m.dataset.sub !== sub; });
254
+ const primeiro = sp.querySelector(PM_ITENS_VISIVEIS);
255
+ sp.querySelectorAll('.pm-sel,.pi-sel').forEach((b) => b.classList.remove('pm-sel', 'pi-sel'));
256
+ if (primeiro)
257
+ primeiro.classList.add('pm-sel');
258
+ return primeiro;
259
+ }
260
+ /**
261
+ * A BARRA RÁPIDA DE ACESSIBILIDADE — dez alternadores e a legenda que os explica (ADR-0044, item 7).
262
+ *
263
+ * Saiu do cartão de pausa e passou a viver no HUD. O motivo é de uso, não de arrumação: é DURANTE a partida
264
+ * que uma criança precisa mudar um ajuste que está a atrapalhando, e não depois de pausar. E o motivo
265
+ * secundário é estrutural — sem ela, o cartão deixa de ter duas zonas e vira uma LISTA, o que é o que
266
+ * finalmente autoriza o anel (a XAG 106 permite laço para menu linear e o proíbe para grade).
267
+ *
268
+ * A LEGENDA VIAJA JUNTO. Ela é a dica que substitui, para quem não vê, o `title` que só o mouse revela;
269
+ * deixá-la no cartão tornaria a barra do HUD muda.
270
+ */
271
+ export function quickBarMarkup() {
272
+ return '<div class="pause-icons" role="group" aria-label="' + t('pause.iconBarAria') + '">' + iconsMarkup() +
273
+ '</div><p class="pause-icons-cap" aria-live="polite"></p>';
274
+ }
275
+ export function acaoNaBarra(k, temStart) {
276
+ if (temStart || k.no)
277
+ return 'sair';
278
+ if (k.yes)
279
+ return 'ativar';
280
+ if (k.up || k.down || k.left || k.right)
281
+ return 'andar';
282
+ return 'nada';
283
+ }
284
+ /** The full innerHTML of a `.screen-pause`. Pure — every input is a parameter. */
285
+ export function screenPauseMarkup(o) {
286
+ // O NOME ACESSÍVEL DO DIÁLOGO passa pelo dicionário. Era texto cru, e MEDIDO num jogo em inglês o efeito
287
+ // era este: o título visível dizia "Paused" e o nome do diálogo, "Menu de pausa do jogador 1". Quem enxerga
288
+ // lia em inglês; quem escuta recebia o menu anunciado em português — a mesma assimetria do item 4 do
289
+ // ADR-0044, um nível acima. E `aria-label`, não `aria-labelledby`: o `<h2>` é rótulo VISUAL, e é por isso
290
+ // que escondê-lo num quadro apertado não tira o nome do diálogo de quem escuta.
291
+ return '<div class="pause-card" role="dialog" aria-modal="true" aria-label="' + t('pause.cardAria', { n: o.player + 1 }) + '">' +
292
+ '<h2><span data-i18n="pause.title">' + o.t('pause.title') + '</span>' + (o.numPlayers > 1 ? ' · Jogador ' + (o.player + 1) : '') + '</h2>' +
293
+ pauseMenuHtml(o.pmButtons, 'raiz', o.dynLabel, o.t) +
294
+ pauseMenuHtml(o.optionsButtons, 'opcoes', o.dynLabel, o.t) +
295
+ '<p class="pause-legend"></p></div>';
296
+ }
297
+ // ---------------------------------------------------------------------------------------------
298
+ // DOM-facing shell
299
+ // ---------------------------------------------------------------------------------------------
300
+ export function initPauseIcons(ctx) {
301
+ // TEA level. It lives HERE (game.js's `let calmMode` had no other reader) and is deliberately NOT persisted
302
+ // — verbatim: game.js never wrote it to storage, even though applyCalm() persists `rm` as a side effect.
303
+ let calmMode = 0;
304
+ const P = () => ctx.getPlayers();
305
+ function hasPrivateOutput(i) { return hasPrivateOutputIn(P(), ctx.getNumPlayers(), i); }
306
+ function iconState(i) {
307
+ const p = P()[i] || {};
308
+ const cat = ctx.getAudioCat();
309
+ return {
310
+ modoCego: ctx.getModoCego(),
311
+ ttsOn: !!(cat && cat.tts && cat.tts.on),
312
+ librasOn: ctx.isLibrasOn(),
313
+ calmMode,
314
+ toggleMove: !!p.toggleMove,
315
+ viz: p.viz || '',
316
+ privateOutput: hasPrivateOutput(i),
317
+ };
318
+ }
319
+ // --- TEA ---------------------------------------------------------------------------------
320
+ function applyCalm() {
321
+ const plan = calmMotionPlan(calmMode);
322
+ for (const k of ctx.rmKeys)
323
+ ctx.rm[k] = plan.sceneReduced;
324
+ ctx.saveRM();
325
+ // "silencioso" freezes the character too. Iterates the WHOLE players array (not just numPlayers) — verbatim.
326
+ for (const p of P())
327
+ for (const c of ctx.rmChar)
328
+ p[c.prop] = plan.charFrozen;
329
+ const cat = ctx.getAudioCat();
330
+ for (const k of CALM_AUDIO_CATS) {
331
+ const c = cat && cat[k];
332
+ if (!c)
333
+ continue;
334
+ const next = calmAudioPlan(calmMode, c.vol);
335
+ c.on = next.on;
336
+ c.vol = next.vol;
337
+ ctx.setCatGain(k);
338
+ }
339
+ // TTS/sonar/guarda/guia stay intact on purpose: TEA is about noise, not about losing navigation.
340
+ }
341
+ // --- icon actions (the dispatcher, as a table) ---------------------------------------------
342
+ const ICON_ACTS = {
343
+ blind: () => {
344
+ ctx.setModoCego(!ctx.getModoCego());
345
+ ctx.srSay(t(ctx.getModoCego() ? 'sr.icon.blindOn' : 'sr.icon.blindOff'));
346
+ },
347
+ tts: () => {
348
+ const cat = ctx.getAudioCat();
349
+ if (!cat || !cat.tts)
350
+ return; // a guarda que `iconLabel` e `applyCalm` já tinham e esta ação não
351
+ cat.tts.on = !cat.tts.on;
352
+ ctx.setCatGain('tts');
353
+ if (ctx.reflectTtsPanelEnabled)
354
+ ctx.reflectTtsPanel();
355
+ ctx.srSay(t(cat.tts.on ? 'sr.audio.ttsOn' : 'sr.audio.ttsOff'));
356
+ },
357
+ libras: () => {
358
+ ctx.toggleLibras();
359
+ ctx.srSay(t(ctx.isLibrasOn() ? 'sr.icon.librasOn' : 'sr.icon.librasOff'));
360
+ },
361
+ tea: () => {
362
+ calmMode = nextCalmMode(calmMode);
363
+ applyCalm();
364
+ ctx.srSay(t('sr.icon.tea', { v: t(CALM_NAMES[calmMode]) }));
365
+ },
366
+ altmove: (i) => {
367
+ // verbatim: `players[i].toggleMove` with no `||{}` guard (unlike contrast/cvd below).
368
+ ctx.setToggleMove(i, !P()[i].toggleMove);
369
+ },
370
+ contrast: (i) => {
371
+ const nx = nextContrast((P()[i] || {}).viz);
372
+ ctx.setPlayerViz(i, nx);
373
+ ctx.srSay(t('sr.visual.contrast', { v: t(CONTRAST_LABELS[nx] || 'contrast.off') }));
374
+ },
375
+ cvd: (i) => {
376
+ const nx = nextCvd((P()[i] || {}).viz);
377
+ ctx.setPlayerViz(i, nx.mode);
378
+ ctx.srSay(t('sr.icon.cvd', { v: t(CVD_NAMES[nx.idx]) }));
379
+ },
380
+ };
381
+ function iconAct(k, i) {
382
+ const ic = ICON_BY_KEY.get(k);
383
+ if (ic && ic.soon) {
384
+ ctx.srAlert(t('sr.icon.underConstruction', { nome: t(ic.n) }));
385
+ return;
386
+ }
387
+ if ((k === 'blind' || k === 'tts') && !hasPrivateOutput(i)) {
388
+ ctx.srAlert(t('sr.icon.needsPrivateOutput'));
389
+ return;
390
+ }
391
+ const act = ICON_ACTS[k];
392
+ if (act)
393
+ act(i);
394
+ }
395
+ // --- reflection --------------------------------------------------------------------------
396
+ function iconLabel(k, i) { return computeIconLabel(k, iconState(i)); }
397
+ function reflectIconBtn(b, i) {
398
+ const k = b.dataset.pi || '';
399
+ const st = iconState(i);
400
+ const v = computeIconVisual(k, st);
401
+ b.classList.remove(...ICON_STATE_CLASSES);
402
+ if (v.calm)
403
+ b.classList.add('pi-calm');
404
+ if (v.cvd)
405
+ b.classList.add(v.cvd);
406
+ b.classList.toggle('pi-on', v.on);
407
+ b.classList.toggle('pi-dis', v.dis);
408
+ b.setAttribute('aria-pressed', String(v.active));
409
+ // `soon` buttons keep the label the markup gave them (same string) — no state to report.
410
+ if (!(ICON_BY_KEY.get(k) || {}).soon)
411
+ b.setAttribute('aria-label', computeIconLabel(k, st));
412
+ }
413
+ function reflectIconsIn(root, i) {
414
+ if (!root)
415
+ return;
416
+ root.querySelectorAll('.pi-btn').forEach((b) => reflectIconBtn(b, i));
417
+ }
418
+ /**
419
+ * Reflete os ícones de TODAS as telas. Varre as BARRAS e não mais os cartões de pausa: desde o item 7 do
420
+ * ADR-0044 os ícones vivem no HUD, e um cartão de pausa não contém `.pi-btn` nenhum.
421
+ */
422
+ function reflectPauseIcons() {
423
+ ctx.getA11yBars().forEach((bar, i) => reflectIconsIn(bar, i));
424
+ }
425
+ // --- the pause screen ----------------------------------------------------------------------
426
+ /**
427
+ * Troca a lista visível E ANUNCIA o primeiro item da lista que entrou.
428
+ *
429
+ * O anúncio não é enfeite: quem não enxerga acabou de mudar de menu e o cursor pulou para outro lugar. Sem
430
+ * 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
431
+ * que diz de quantos itens é a lista nova.
432
+ */
433
+ function anunciarLista(sp, sub) {
434
+ const primeiro = mostrarSubmenuDaPausa(sp, sub);
435
+ if (!primeiro)
436
+ return;
437
+ const itens = [...sp.querySelectorAll(PM_ITENS_VISIVEIS)];
438
+ ctx.srSay(anunciarItem({ rotulo: primeiro.textContent || '', posicao: 1, total: itens.length }, menuIndexOn));
439
+ }
440
+ /* ===================== O MODO `accessibility` (ADR-0044, item 7) ===================== */
441
+ /**
442
+ * Quem está com o direcional dirigindo a BARRA em vez do personagem.
443
+ *
444
+ * Vida de RODADA (ADR-0038): mora no closure desta instância, não é persistido, e some com a partida. Um
445
+ * modo de entrada que sobrevivesse ao reinício seria a armadilha voltando pela porta dos fundos — a criança
446
+ * abriria o jogo no dia seguinte e o personagem não andaria.
447
+ */
448
+ const naBarra = new Set();
449
+ /** O cursor da barra da tela `i`, ou o primeiro ícone quando ainda não há cursor. */
450
+ function iconeSelecionado(bar) {
451
+ return bar.querySelector('.pi-sel') || bar.querySelector('.pi-btn');
452
+ }
453
+ /** Põe o cursor num ícone, escreve a legenda e ANUNCIA — a legenda é o canal de quem não vê o ícone. */
454
+ function selecionarIcone(bar, el) {
455
+ bar.querySelectorAll('.pi-sel').forEach((x) => x.classList.remove('pi-sel'));
456
+ el.classList.add('pi-sel');
457
+ const cap = bar.querySelector('.pause-icons-cap');
458
+ if (cap)
459
+ cap.textContent = legendaDoIcone(bar, el);
460
+ ctx.srSay(legendaDoIcone(bar, el));
461
+ }
462
+ /**
463
+ * ENTRA no modo: o direcional passa a dirigir a barra da tela `i`, e o jogo VOLTA a rodar.
464
+ *
465
+ * O anúncio diz como SAIR, e diz na hora de entrar. É a linha que desarma a armadilha que o próprio
466
+ * ADR-0044 anotou como consequência negativa desta decisão: quem não enxerga aperta a direção, o personagem
467
+ * 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.
468
+ */
469
+ function entrarNaBarra(i) {
470
+ const bar = ctx.getA11yBars()[i];
471
+ const primeiro = bar && iconeSelecionado(bar);
472
+ if (!bar || !primeiro)
473
+ return;
474
+ naBarra.add(i);
475
+ const acts = ctx.getPauseActs();
476
+ if (acts.resume)
477
+ acts.resume(); // volta à tela normal: o modo é para usar DURANTE a partida
478
+ ctx.srSay(t('sr.a11y.barEnter'));
479
+ selecionarIcone(bar, primeiro);
480
+ }
481
+ /** SAI do modo e devolve o direcional ao personagem. Anuncia, porque a devolução também é informação. */
482
+ function sairDaBarra(i) {
483
+ if (!naBarra.delete(i))
484
+ return;
485
+ const bar = ctx.getA11yBars()[i];
486
+ if (bar) {
487
+ bar.querySelectorAll('.pi-sel').forEach((x) => x.classList.remove('pi-sel'));
488
+ const cap = bar.querySelector('.pause-icons-cap');
489
+ if (cap)
490
+ cap.textContent = '';
491
+ }
492
+ ctx.srSay(t('sr.a11y.barExit'));
493
+ }
494
+ /** A tela `i` está com o direcional na barra? É o que o roteamento de entrada pergunta a cada quadro. */
495
+ const naBarraDe = (i) => naBarra.has(i);
496
+ /**
497
+ * UM PASSO dentro do modo. `temStart` é a borda do botão que abre a pausa — a segunda saída.
498
+ *
499
+ * A barra é uma fileira, então as QUATRO direções andam nela: para quem navega sem ver, "cima" numa lista
500
+ * de uma linha só não pode ser um beco. E anda em ANEL, como todo menu do jogo desde o item 1.
501
+ */
502
+ function navBar(i, k, temStart = false) {
503
+ if (!naBarra.has(i))
504
+ return;
505
+ const bar = ctx.getA11yBars()[i];
506
+ if (!bar)
507
+ return;
508
+ const acao = acaoNaBarra(k, temStart);
509
+ if (acao === 'sair') {
510
+ sairDaBarra(i);
511
+ return;
512
+ }
513
+ const icones = [...bar.querySelectorAll('.pi-btn')];
514
+ if (!icones.length)
515
+ return;
516
+ const cur = iconeSelecionado(bar);
517
+ const idx = cur ? icones.indexOf(cur) : 0;
518
+ if (acao === 'ativar') {
519
+ ctx.setPauseActor(i);
520
+ if (cur)
521
+ cur.click();
522
+ return;
523
+ }
524
+ if (acao === 'andar') {
525
+ const d = (k.down || k.right) ? 1 : -1;
526
+ selecionarIcone(bar, icones[passoNoAnel(icones.length, idx, d)]);
527
+ }
528
+ }
529
+ function buildScreenPause(i) {
530
+ const sp = document.createElement('div');
531
+ sp.className = 'screen-pause';
532
+ sp.hidden = true;
533
+ sp.dataset.player = String(i);
534
+ sp.innerHTML = screenPauseMarkup({
535
+ player: i, numPlayers: ctx.getNumPlayers(), pmButtons: ctx.pmButtons, optionsButtons: ctx.optionsButtons,
536
+ dynLabel: ctx.dynLabel, t,
537
+ });
538
+ sp.addEventListener('click', (e) => {
539
+ const target = e.target;
540
+ const b = target && target.closest('.pm-btn');
541
+ if (b) {
542
+ ctx.setPauseActor(i);
543
+ const act = b.dataset.act || '';
544
+ // NAVEGAÇÃO DENTRO DO CARTÃO fica aqui, e não na tabela de ações: `options` e `pmback` não fazem nada
545
+ // ao jogo — trocam qual lista está na tela. A tabela vive em `ui/shell`, que não conhece este `sp`.
546
+ if (act === 'options' || act === 'pmback') {
547
+ anunciarLista(sp, act === 'options' ? 'opcoes' : 'raiz');
548
+ return;
549
+ }
550
+ // `acessibilidade` leva o cursor à BARRA RÁPIDA. Enquanto ela mora dentro do cartão, "entrar no modo"
551
+ // é pôr o cursor nela — e a saída continua sendo a saída da pausa, que é a mesma de sempre. Quando o
552
+ // item 7 levar a barra para o HUD, esta linha o segue; o que o item SIGNIFICA não muda.
553
+ if (act === 'acessibilidade') {
554
+ entrarNaBarra(i);
555
+ return;
556
+ }
557
+ const acts = ctx.getPauseActs();
558
+ const fn = acts[act];
559
+ if (fn)
560
+ fn();
561
+ return;
562
+ }
563
+ });
564
+ return sp;
565
+ }
566
+ /**
567
+ * A BARRA RÁPIDA de uma tela: os dez alternadores, a legenda, e a fiação dos dois.
568
+ *
569
+ * Ela é IRMÃ da `.screen-exp` e não filha, e isso é a decisão do #82 aplicada: a barra é CONTROLE, não
570
+ * experiência. O modo empatia degrada a experiência de propósito — simulação é criar dificuldade onde a
571
+ * facilidade não existe —, e degradar o que existe para DAR acesso seria o contrário do que ele serve.
572
+ */
573
+ function buildQuickBar(i) {
574
+ const bar = document.createElement('div');
575
+ bar.className = 'screen-a11y';
576
+ bar.dataset.player = String(i);
577
+ bar.innerHTML = quickBarMarkup();
578
+ // FORA DA ORDEM DE TABULAÇÃO durante a partida (ADR-0044, item 7). Dez paradas entre a criança e o jogo
579
+ // seria o preço de deixá-los lá — e o alcance por teclado não se perde: ele passa a ser o modo
580
+ // `accessibility`, que se abre pela pausa. A barra do TÍTULO não é afetada: lá não se está jogando, e o
581
+ // `tabindex` é posto AQUI, no elemento, e não no markup que as duas compartilham.
582
+ bar.querySelectorAll('.pi-btn').forEach((b) => { b.tabIndex = -1; });
583
+ const cap = bar.querySelector('.pause-icons-cap');
584
+ bar.addEventListener('click', (e) => {
585
+ const ib = e.target?.closest('.pi-btn');
586
+ if (!ib)
587
+ return;
588
+ ctx.setPauseActor(i);
589
+ iconAct(ib.dataset.pi || '', i);
590
+ reflectPauseIcons(); // must run BEFORE reading the label back — that is what makes the caption honest
591
+ if (cap)
592
+ cap.textContent = legendaDoIcone(bar, ib);
593
+ });
594
+ // Legenda = o `aria-label` do botão, para que passar o mouse ou focar diga a MESMA verdade que um leitor
595
+ // de tela anunciaria. Uma fonte só para quem vê e para quem escuta.
596
+ //
597
+ // E ELA SOME AO SAIR. Antes ficava: a última explicação apontada permanecia por cima do jogo até alguém
598
+ // apontar outra. Numa barra que agora vive na TELA DE JOGO isso é uma faixa de texto parada em cima da
599
+ // partida — o Dev viu e disse o que é: explicação só enquanto o mouse estiver no botão.
600
+ //
601
+ // A EXCEÇÃO É O CURSOR DO MODO `accessibility`: quando ele está pousado num ícone, a legenda é a única
602
+ // coisa que diz onde ele está, e apagá-la ao mexer o mouse cegaria o modo. Daí a pergunta pelo `.pi-sel`.
603
+ const limpar = () => { if (cap && !bar.querySelector('.pi-sel'))
604
+ cap.textContent = ''; };
605
+ bar.querySelectorAll('.pi-btn').forEach((b) => {
606
+ const show = () => { if (cap)
607
+ cap.textContent = legendaDoIcone(bar, b); };
608
+ b.addEventListener('mouseenter', show);
609
+ b.addEventListener('focus', show);
610
+ b.addEventListener('mouseleave', limpar);
611
+ b.addEventListener('blur', limpar);
612
+ });
613
+ return bar;
614
+ }
615
+ return {
616
+ buildScreenPause, buildQuickBar, entrarNaBarra, sairDaBarra, naBarraDe, navBar,
617
+ iconAct, iconLabel, reflectIconBtn, reflectIconsIn, reflectPauseIcons,
618
+ applyCalm, getCalmMode: () => calmMode, setCalmMode: (n) => { calmMode = n; }, iconState,
619
+ };
620
+ }
@@ -0,0 +1,34 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ import type { Alcance } from '../input/transports.js';
3
+ /** `core/i18n.t` — injetado para o núcleo continuar puro e para o teste poder ver as chaves cruas. */
4
+ export type Tradutor = (chave: string, params?: Record<string, string | number>) => string;
5
+ /**
6
+ * AS FRASES DO AVISO, na ordem em que são lidas. Vazio = não há nada a dizer, e é o caso comum.
7
+ *
8
+ * ⚠️ A ÚLTIMA LINHA TEM DUAS FORMAS, e a diferença entre elas é a diferença entre ajudar e mentir:
9
+ *
10
+ * · há transporte que serviria se ligado → «ligue um controle», que é acionável.
11
+ * · não há nenhum → dizer «ligue um controle» seria mandar a criança procurar uma coisa que não resolve.
12
+ * Nesse caso o problema é do JOGO, que pede mais posições do que qualquer transporte deste aparelho
13
+ * oferece, e a frase honesta é outra.
14
+ */
15
+ export declare function linhasDoAviso(a: Alcance, t: Tradutor): string[];
16
+ export interface ReachNoticeCtx {
17
+ /** `querySelector` do documento deste jogo. */
18
+ procurar: (sel: string) => HTMLElement | null;
19
+ /** `document.createElement`. Injetado como tudo o mais que toca o documento. */
20
+ criar: (tag: string) => HTMLElement;
21
+ t: Tradutor;
22
+ /** Anúncio assertivo. Uma criança cega tem de OUVIR isto — ela não vai ver o cartão. */
23
+ srAlert: (texto: string) => void;
24
+ }
25
+ /** O id do cartão. Estável porque o teste e a folha de estilo o procuram. */
26
+ export declare const REACH_NOTICE_ID = "reach-notice";
27
+ /**
28
+ * Mostra o aviso, se houver o que dizer. Devolve `true` quando mostrou.
29
+ *
30
+ * ⚠️ MONTA POR NÓS E NÃO POR `innerHTML`. As frases passam por `t()` e um dicionário é conteúdo que muda sem
31
+ * passar por revisão de código — é exatamente a fronteira que a issue #106 mapeia. `textContent` fecha a
32
+ * questão sem precisar de escapar nada.
33
+ */
34
+ export declare function mostrarAvisoDeAlcance(ctx: ReachNoticeCtx, a: Alcance): boolean;