@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,99 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // ui/focus-trap.ts — A ARMADILHA DE FOCO. O segundo fio da issue #109, e o único dos três que não existia
3
+ // EM LADO NENHUM da engine: os outros dois eram construídos e deixados desligados, este nunca foi escrito.
4
+ // `ui/settings-panel.ts:17-19` diz isso por escrito — *"não instala armadilha de foco (o original também não
5
+ // tem)"*.
6
+ //
7
+ // ⚠️ E O DOCUMENTO JÁ PROMETE O CONTRÁRIO. Todo `.overlay__card` do `index.html` tem `aria-modal="true"`, o
8
+ // que diz à tecnologia assistiva que o resto da página está inerte. O Tab não concorda: ele sai do diálogo e
9
+ // entra no tabuleiro por baixo. É a pior forma de defeito de acessibilidade — não a ausência de uma promessa,
10
+ // mas uma promessa que o teclado desmente. Quem usa leitor de tela sai para um jogo cujo estado não consegue
11
+ // perceber, e não tem caminho de volta que perceba.
12
+ //
13
+ // ⚠️ SÓ INTERVÉM NAS BORDAS, e isso é decisão e não economia. Reimplementar a ordem de tabulação inteira
14
+ // significaria reproduzir regras que o navegador já acerta — `tabindex` positivo, ordem do DOM em `shadow
15
+ // root`, elementos que ficam focáveis por `contenteditable`. Uma armadilha que se mete no meio erra ao
16
+ // caminhar; uma que só fecha o ciclo nas pontas não tem como errar o meio, porque não o toca.
17
+ //
18
+ // A regra fica em UMA função pura (`proximoNaArmadilha`), testável no project node sem DOM nenhum. O que
19
+ // precisa de navegador é só ler quem está focado e chamar `.focus()`.
20
+ /**
21
+ * O que conta como focável. Deliberadamente SEM `[contenteditable]` e sem `tabindex` positivo: os dois
22
+ * existem, mas não aparecem em nenhum diálogo desta engine (conferido), e um seletor que promete mais do que
23
+ * foi verificado é um seletor que mente na próxima revisão.
24
+ */
25
+ export const SELETOR_FOCAVEL = [
26
+ 'a[href]',
27
+ 'button:not([disabled])',
28
+ 'input:not([disabled])',
29
+ 'select:not([disabled])',
30
+ 'textarea:not([disabled])',
31
+ '[tabindex]:not([tabindex="-1"])',
32
+ ].join(',');
33
+ /**
34
+ * Para onde o foco tem de ir quando o Tab está prestes a sair do diálogo — ou `null` quando não há nada a
35
+ * fazer e o navegador deve seguir sozinho.
36
+ *
37
+ * Os quatro casos, e o terceiro é a razão de isto existir:
38
+ *
39
+ * 1. lista VAZIA → `null`. Um diálogo sem nada focável é um defeito do diálogo, e prender o foco nele
40
+ * deixaria a criança sem saída nenhuma. Melhor deixar sair do que trancar num lugar mudo.
41
+ * 2. o foco está no ÚLTIMO e vai para a frente → volta ao primeiro; no PRIMEIRO e vai para trás → vai ao
42
+ * último. É o ciclo, que é o que «armadilha» quer dizer.
43
+ * 3. ⚠️ o foco está FORA da lista → traz de volta. É o caso que os outros três não cobrem: o foco pode já
44
+ * ter escapado antes de esta armadilha existir, ou por um clique no tabuleiro, ou porque o diálogo
45
+ * abriu sem focar nada. Sem ele a armadilha só funciona para quem já estava dentro.
46
+ * 4. o foco está no MEIO → `null`, e o navegador caminha. Ver o comentário do cabeçalho.
47
+ */
48
+ export function proximoNaArmadilha(focaveis, atual, paraTras) {
49
+ if (focaveis.length === 0)
50
+ return null;
51
+ const primeiro = focaveis[0];
52
+ const ultimo = focaveis[focaveis.length - 1];
53
+ const i = atual === null ? -1 : focaveis.indexOf(atual);
54
+ if (i < 0)
55
+ return paraTras ? ultimo : primeiro; // caso 3: o foco estava fora
56
+ if (!paraTras && i === focaveis.length - 1)
57
+ return primeiro;
58
+ if (paraTras && i === 0)
59
+ return ultimo;
60
+ return null; // caso 4: o meio é do navegador
61
+ }
62
+ export function initFocusTrap(ctx) {
63
+ function aoTeclar(e) {
64
+ if (e.key !== 'Tab')
65
+ return;
66
+ const dialogo = ctx.overlayDeCima();
67
+ if (!dialogo)
68
+ return; // sem diálogo aberto o Tab é do jogo, e tem de continuar a ser
69
+ const alvo = proximoNaArmadilha(ctx.focaveisDe(dialogo), ctx.focoAtual(), e.shiftKey);
70
+ if (!alvo)
71
+ return;
72
+ e.preventDefault();
73
+ alvo.focus();
74
+ }
75
+ return {
76
+ aoTeclar,
77
+ attach: () => ctx.win.addEventListener('keydown', aoTeclar, true),
78
+ detach: () => ctx.win.removeEventListener?.('keydown', aoTeclar, true),
79
+ };
80
+ }
81
+ /**
82
+ * Os focáveis de um contêiner, no navegador. Vive aqui e não na raiz para as duas raízes não escreverem duas
83
+ * versões que divergiriam em silêncio — uma prenderia o foco e a outra deixaria escapar, no mesmo produto.
84
+ *
85
+ * ⚠️ A VISIBILIDADE É POR `getClientRects()`, e não por `offsetParent`.
86
+ *
87
+ * ⚠️ E A PRIMEIRA VERSÃO DESTE COMENTÁRIO ESTAVA ERRADA, o que vale mais registrar do que apagar: ele dizia
88
+ * que `offsetParent` é `null` para «qualquer elemento em `position: fixed`». Não é — é `null` para o elemento
89
+ * FIXO EM SI, e os descendentes dele devolvem o próprio contêiner. O teste de navegador reprovou a afirmação,
90
+ * que é exatamente o que um teste tem de fazer com uma justificativa inventada.
91
+ *
92
+ * A razão verdadeira é mais simples e mais forte: `getClientRects()` responde *«isto desenha alguma caixa?»*,
93
+ * o que cobre `display:none` em QUALQUER ancestral, elementos de tamanho zero e o caso do contêiner fixo —
94
+ * sem que quem lê precise de saber em que casos `offsetParent` tem buracos.
95
+ */
96
+ export function focaveisNoDom(dentro) {
97
+ const todos = [...dentro.querySelectorAll(SELETOR_FOCAVEL)];
98
+ return todos.filter((el) => !el.hidden && el.getClientRects().length > 0);
99
+ }
@@ -0,0 +1,41 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ /**
3
+ * Uma fonte do catálogo. `fam` é o NOME DA FONTE — nome próprio, nunca traduzido. `d` guarda CHAVE i18n da
4
+ * descrição, não o texto: mesma decisão de `VIZ_MODES` e `RM_LABEL`, e pelo mesmo motivo — uma tabela de
5
+ * `const` com texto resolve uma vez, no import, e fica congelada no idioma do boot.
6
+ */
7
+ export type FontItem = {
8
+ k: string;
9
+ fam: string;
10
+ fb: string;
11
+ d?: string;
12
+ off?: string;
13
+ };
14
+ /** Um grupo do catálogo. `g` também guarda CHAVE ('font.group.sans'), pelo mesmo motivo. */
15
+ export type FontGroup = {
16
+ g: string;
17
+ items: FontItem[];
18
+ };
19
+ export declare const FONT_GROUPS: FontGroup[];
20
+ export declare const FONT_BY_KEY: Record<string, FontItem>;
21
+ /** Narrow store shape these need — lets a caller inject a fake without touching real storage. */
22
+ export interface FontStore {
23
+ get(key: string, fallback: string | null): string | null;
24
+ set(key: string, v: string): void;
25
+ }
26
+ export declare const FONT_KEY = "incl_font_k";
27
+ export declare const FONT_KEY_LEGACY = "incl_fonte";
28
+ /**
29
+ * A fonte de fábrica, com nome. Atkinson Hyperlegible foi desenhada pelo Braille Institute justamente para
30
+ * quem tem baixa visão — distingue as formas que mais se confundem (I/l/1, O/0). É por isso que ela é o padrão
31
+ * e não uma preferência estética, e é por isso que o "restaurar padrões" da tipografia (ADR-0028) volta para
32
+ * cá: o caminho de volta de um menu de acessibilidade tem que terminar na escolha mais legível, não numa
33
+ * qualquer. Usada em DOIS lugares — o fim da cadeia de boot logo abaixo e o reset do painel —, e uma constante
34
+ * porque duas cópias de um padrão são duas chances de o reset devolver algo que o jogo nunca usou.
35
+ */
36
+ export declare const DEFAULT_FONT_KEY = "atkinson";
37
+ /** Boot choice: validated persisted key (ignores .off fonts) -> legacy-key migration -> DEFAULT_FONT_KEY. */
38
+ export declare function resolveFontKey(s: FontStore): string;
39
+ export declare function persistFontKey(s: FontStore, k: string): void;
40
+ export declare function loadFontKey(): string;
41
+ export declare function saveFontKey(k: string): void;
@@ -0,0 +1,59 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // ui/fonts.ts — catálogo de fontes (dados) + índice por chave + carga/persistência da escolha. Módulo-folha
3
+ // (só depende de storage). A instância `fontKey` e o setGameFont (aplica família/espaçamento) ficam no game.js.
4
+ import * as store from '../platform/storage.js';
5
+ export const FONT_GROUPS = [
6
+ { g: 'font.group.sans', items: [
7
+ { k: 'atkinson', fam: 'Atkinson Hyperlegible', fb: 'sans', d: 'font.desc.atkinson' },
8
+ { k: 'lexend', fam: 'Lexend', fb: 'sans', d: 'font.desc.lexend' },
9
+ { k: 'quattro', fam: 'iA Writer Quattro', fb: 'sans', d: 'font.desc.quattro' },
10
+ { k: 'andika', fam: 'Andika', fb: 'sans', d: 'font.desc.andika' },
11
+ { k: 'sourcesans', fam: 'Source Sans 3', fb: 'sans' },
12
+ { k: 'inter', fam: 'Inter', fb: 'sans' },
13
+ { k: 'opensans', fam: 'Open Sans', fb: 'sans' },
14
+ { k: 'lato', fam: 'Lato', fb: 'sans' }
15
+ ] },
16
+ { g: 'font.group.serif', items: [
17
+ { k: 'literata', fam: 'Literata', fb: 'serif' },
18
+ { k: 'sourceserif', fam: 'Source Serif 4', fb: 'serif' },
19
+ { k: 'newsreader', fam: 'Newsreader', fb: 'serif' }
20
+ ] },
21
+ { g: 'font.group.hand', items: [
22
+ { k: 'greatvibes', fam: 'Great Vibes', fb: 'cursive', d: 'font.desc.greatvibes' },
23
+ { k: 'pinyon', fam: 'Pinyon Script', fb: 'cursive', d: 'font.desc.pinyon' },
24
+ { k: 'ufcook', fam: 'UnifrakturCook', fb: 'cursive', d: 'font.desc.ufcook' },
25
+ { k: 'ufmag', fam: 'UnifrakturMaguntia', fb: 'cursive', d: 'font.desc.ufmag' },
26
+ { k: 'comicneue', fam: 'Comic Neue', fb: 'cursive', d: 'font.desc.comicneue' },
27
+ { k: 'learningcurve', fam: 'Learning Curve', fb: 'cursive', d: 'font.desc.learningcurve', off: 'font.off.pending' },
28
+ { k: 'kindergarten', fam: 'Kindergarten Pro', fb: 'cursive', d: 'font.desc.kindergarten', off: 'font.off.negotiating' }
29
+ ] },
30
+ ];
31
+ export const FONT_BY_KEY = {};
32
+ FONT_GROUPS.forEach((g) => g.items.forEach((it) => { FONT_BY_KEY[it.k] = it; }));
33
+ export const FONT_KEY = 'incl_font_k';
34
+ export const FONT_KEY_LEGACY = 'incl_fonte'; // pre-Fase-2: 'alfabetizacao' | 'dislexia'
35
+ /**
36
+ * A fonte de fábrica, com nome. Atkinson Hyperlegible foi desenhada pelo Braille Institute justamente para
37
+ * quem tem baixa visão — distingue as formas que mais se confundem (I/l/1, O/0). É por isso que ela é o padrão
38
+ * e não uma preferência estética, e é por isso que o "restaurar padrões" da tipografia (ADR-0028) volta para
39
+ * cá: o caminho de volta de um menu de acessibilidade tem que terminar na escolha mais legível, não numa
40
+ * qualquer. Usada em DOIS lugares — o fim da cadeia de boot logo abaixo e o reset do painel —, e uma constante
41
+ * porque duas cópias de um padrão são duas chances de o reset devolver algo que o jogo nunca usou.
42
+ */
43
+ export const DEFAULT_FONT_KEY = 'atkinson';
44
+ /** Boot choice: validated persisted key (ignores .off fonts) -> legacy-key migration -> DEFAULT_FONT_KEY. */
45
+ export function resolveFontKey(s) {
46
+ const k = s.get(FONT_KEY, null);
47
+ if (k && FONT_BY_KEY[k] && !FONT_BY_KEY[k].off)
48
+ return k;
49
+ const leg = s.get(FONT_KEY_LEGACY, null);
50
+ if (leg === 'alfabetizacao')
51
+ return 'andika';
52
+ if (leg === 'dislexia')
53
+ return 'lexend';
54
+ return DEFAULT_FONT_KEY;
55
+ }
56
+ export function persistFontKey(s, k) { s.set(FONT_KEY, k); }
57
+ // Conveniencia sobre o storage real — mesma logica, sem duplicá-la.
58
+ export function loadFontKey() { return resolveFontKey(store); }
59
+ export function saveFontKey(k) { persistFontKey(store, k); }
@@ -0,0 +1,137 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ import type { PlayerView } from '../core/entity.js';
3
+ import type { Objective } from '../core/contract.js';
4
+ import type { DomQuery } from '../core/dom-query.js';
5
+ /** Minimal DOM-selector shape (matches ui/dom.ts's `$`). */
6
+ export type { DomQuery } from '../core/dom-query.js';
7
+ /** Só os campos do jogador que o HUD lê. Estrutural de propósito: o `players[]` real é `unknown[]` no core/state. */
8
+ /** O que o HUD lê do JOGADOR: o poder ativo e se ele desistiu. O progresso vem do `Objective`, não daqui —
9
+ * `collected` saiu da fatia, e com ele a última coisa que o HUD sabia sobre juntar objetos. */
10
+ export type HudPlayer = PlayerView<'activePower' | 'quit'>;
11
+ /** Grade de telas: 1 → 1×1, 2 → 2×1, 3-4 → 2×2 (a 3ª tela é centralizada na linha de baixo). */
12
+ export { screenGrid } from '../core/screens.js';
13
+ export type { ScreenGrid } from '../core/screens.js';
14
+ /** Retângulo da tela `i` em porcentagens de CSS, já prontas para `style.left/top/width/height`. */
15
+ export interface ScreenRect {
16
+ L: string;
17
+ T: string;
18
+ W: string;
19
+ H: string;
20
+ }
21
+ /**
22
+ * Posição/tamanho da tela `i` numa grade de `n` jogadores. Verbatim de screenRect(i) — que lia `numPlayers`
23
+ * global; aqui `n` é parâmetro (mesma conta, testável sem estado). Caso especial preservado: com 3 telas, a
24
+ * terceira é centralizada na linha de baixo, para casar com o posicionamento do render (configureRender).
25
+ */
26
+ export declare function screenRect(i: number, n: number): ScreenRect;
27
+ /** Quantas telas o HUD monta: ao menos uma, mesmo antes de `players[]` existir no boot. Verbatim (Math.max(1,…)). */
28
+ export declare function screenCount(n: number): number;
29
+ /** O texto que o leitor de tela ouve no contador. A MOLDURA é a chave; o NOME do objetivo atravessa por
30
+ * parâmetro — a regra do pilar 3 (ADR-0010), a mesma que o currículo segue. */
31
+ export declare const contadorLabel: (o: Objective) => string;
32
+ /**
33
+ * Markup do contador (objetivo na 1ª coluna, poder na 2ª).
34
+ *
35
+ * O objetivo entra INTEIRO — nome, quanto tem, quanto precisa — em vez de só o alvo numérico, e o ícone entra
36
+ * por injeção. É o que tira o desenho da moeda de dentro da engine: o HUD mostra o que o jogo declarou
37
+ * (campo 5 do contrato), sem saber se é moeda, palavra ou conta.
38
+ *
39
+ * O `aria-label` é a parte que não é renomeação: sem o nome do objetivo não havia o que dizer, e o contador
40
+ * era mudo para quem não vê a tela.
41
+ */
42
+ export declare function vphudHtml(objetivo: Objective, icone: string): string;
43
+ /**
44
+ * PÕE O NOME DECLARADO PELO JOGO NO RÓTULO DO LEITOR DE TELA — por atributo, nunca por markup (issue #106).
45
+ *
46
+ * ⚠️ ESTE É UM VETOR QUE A AUDITORIA DE 2026-08-26 NÃO TINHA COMO CONHECER, e vale dizer por quê: ela varreu
47
+ * por FONTE DE DADO e concluiu, com razão para a época, que nada de fora chegava a markup. Depois disso o
48
+ * contrato passou a existir (ADR-0030) e os jogos passaram a viver em REPOSITÓRIOS SEPARADOS, consumindo a
49
+ * engine como pacote (ADR-0083). O `name.text` do objetivo é texto de um jogo que esta árvore não revê.
50
+ *
51
+ * ⚠️ E ELE ENTRAVA NUM ATRIBUTO, que é o pior contexto dos dois: dentro de um elemento uma aspa é inofensiva,
52
+ * dentro de `aria-label="…"` ela FECHA o atributo e o que vem a seguir vira atributo — um `onmouseover` sem
53
+ * precisar de uma única tag.
54
+ *
55
+ * `setAttribute` escapa por construção, e é por isso que a resposta é «construir nós» e não «escapar à mão»:
56
+ * um escape esquecido não deixa rasto; um `setAttribute` esquecido tira o rótulo, e há caso a prendê-lo.
57
+ */
58
+ export declare function aplicarRotuloDoContador(vphud: Element | null, objetivo: Objective): void;
59
+ /** Markup do selo "aperte um botão para entrar" (tela criada em jogo, ainda sem dono). `i` é o índice 0-based. */
60
+ export declare function waitBadgeHtml(i: number): string;
61
+ /** Projeção do HUD de UMA tela: tudo que updateGameHud() escreve no DOM, sem tocar no DOM. */
62
+ export interface HudRowView {
63
+ /** Texto do contador: quanto o jogador tem, verbatim (sem formatação nem clamp). */
64
+ have: string;
65
+ /** O que o leitor de tela ouve no contador — reescrito junto com o número, senão ele ficaria falando o
66
+ * valor do primeiro quadro a partida inteira. É o defeito que um `aria-label` estático teria. */
67
+ label: string;
68
+ /** Rótulo curto do poder ativo, com o travessão como fallback de poder desconhecido/ausente. */
69
+ power: string;
70
+ /** `hidden` do selo "Jogo abandonado": escondido enquanto o jogador NÃO desistiu. */
71
+ quitHidden: boolean;
72
+ /** `style.visibility` do contador: quem desistiu vê tela preta com o selo, sem números. */
73
+ visibility: 'hidden' | 'visible';
74
+ }
75
+ /**
76
+ * Estado do HUD de um jogador. Verbatim do corpo de updateGameHud(), só que como valor.
77
+ * `powerShort` é o POWER_SHORT do game.js (injetado — a mesma FUNÇÃO que game/coin-spawning.ts já recebe).
78
+ * Função e não tabela: o texto depende do idioma ATUAL, e uma tabela lida no boot ficaria congelada nele.
79
+ */
80
+ export declare function hudRowView(p: HudPlayer, powerShort: (kind: string) => string, objetivo: Objective): HudRowView;
81
+ export interface HudCtx {
82
+ /** Quantos jogadores/telas. Estado de RODADA (ADR-0038): vem da instância que a raiz possui.
83
+ * Era `numPlayers`, um `let` de `core/state` importado como binding vivo — e um `let` de módulo
84
+ * é compartilhado por qualquer segundo jogo que a mesma página carregue (D13 do `demos`). */
85
+ getNumPlayers: () => number;
86
+ /** Os jogadores. Estado de RODADA, pelo mesmo motivo. `readonly unknown[]` porque cada consumidor
87
+ * estreita para a SUA fatia — o tipo real é do jogo, não da engine (ADR-0033). */
88
+ getPlayers: () => readonly unknown[];
89
+ /** Seletor DOM (forma do `$` de ui/dom.ts), injetado — o módulo nunca alcança `document` por global. */
90
+ $: DomQuery;
91
+ /**
92
+ * POWER_SHORT: rótulos curtos de poder mostrados no HUD. Fica no game.js porque game/coin-spawning.ts
93
+ * (showPower) também o recebe — injetar evita a 2ª cópia da tabela.
94
+ */
95
+ powerShort: (kind: string) => string;
96
+ /**
97
+ * O OBJETIVO do jogador `i` — campo 5 do contrato (`core/contract.Objective`): o nome do que se junta,
98
+ * quanto tem e quanto precisa. FUNÇÃO e não valor, porque `have` muda a cada quadro.
99
+ *
100
+ * Era `hudTarget: number`, e antes disso `COIN_TARGET` por importação. Cada passo tirou uma coisa que o HUD
101
+ * sabia sobre o jogo: primeiro a dependência, agora o assunto.
102
+ */
103
+ hudObjective: (playerIndex: number) => Objective;
104
+ /** O ícone do contador. Era o desenho da moeda cravado no markup da engine; é do jogo, como o nome. */
105
+ hudIcon: string;
106
+ /**
107
+ * Painel de pausa da tela `i`. NÃO é deste módulo (slice de pausa/ícones): entra por injeção e o HUD só
108
+ * anexa o retorno dentro da `.player-screen` correspondente.
109
+ */
110
+ buildScreenPause: (i: number) => HTMLElement;
111
+ /**
112
+ * A BARRA RÁPIDA de acessibilidade da tela `i` (ui/pause-icons `buildQuickBar`). Anexada como IRMÃ da
113
+ * `.screen-exp`, e não dentro dela: a barra é CONTROLE, e o modo empatia não a alcança (issue #82).
114
+ */
115
+ buildQuickBar: (i: number) => HTMLElement;
116
+ /**
117
+ * Chamado no FIM de buildGameHud() com os painéis de pausa recém-criados, em ordem de tela. É o gancho onde o
118
+ * game.js reatribui `vpPause` (binding local dele) e roda o que o original rodava depois do laço
119
+ * (applyLetra/renderPauseLegend). Opcional: sem ele o HUD monta igual, só não avisa ninguém.
120
+ */
121
+ onScreensBuilt?: (pausePanels: HTMLElement[]) => void;
122
+ /** As barras rápidas recém-montadas, na ordem das telas. A raiz guarda para o `getA11yBars`. */
123
+ onBarsBuilt?: (bars: HTMLElement[]) => void;
124
+ }
125
+ export interface HudApi {
126
+ /** (Re)monta `#game-hud`: uma `.player-screen` por jogador, com HUD, selo de abandono e painel de pausa. */
127
+ buildGameHud: () => void;
128
+ /** Reescreve moedas/poder e o selo de abandono de todas as telas montadas. Chamado a cada frame. */
129
+ updateGameHud: () => void;
130
+ /** Contêiner `.player-screen` da tela `i` (o quiz de MP e outros overlays por jogador penduram aqui). */
131
+ getScreen: (i: number) => HTMLElement | null;
132
+ /** Cria o selo `.vp-wait` na tela `i` (idempotente: não duplica se já existir). */
133
+ showWaitingBadge: (i: number) => void;
134
+ /** Remove o selo `.vp-wait` da tela `i` (o jogador entrou). No-op se a tela ou o selo não existirem. */
135
+ clearWaitingBadge: (i: number) => void;
136
+ }
137
+ export declare function initHud(ctx: HudCtx): HudApi;
@@ -0,0 +1,229 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // ui/hud.ts — HUD POR TELA + a infraestrutura de TELAS do jogo (Estágio 4). Duas responsabilidades coladas
3
+ // desde sempre no game.js: (a) a GRADE de `.player-screen` dentro de `#game-hud` — um contêiner por jogador,
4
+ // posicionado em %, que hospeda o HUD, o selo "jogo abandonado", o selo "aperte um botão para entrar"
5
+ // (`.vp-wait`), o menu de pausa daquele jogador e (em MP) o overlay de quiz dele; e (b) o CONTEÚDO do HUD —
6
+ // o OBJETIVO do jogador e o poder ativo — reescrito a cada frame por updateGameHud().
7
+ //
8
+ // ========================= O CONTADOR DEIXOU DE SER DE MOEDA (item 19) =========================
9
+ // O passo 4 do ADR-0027 matou a DEPENDÊNCIA (`vphudHtml(coinTarget = COIN_TARGET)` virou parâmetro
10
+ // obrigatório); o que sobrou era VOCABULÁRIO — o ícone cravado no markup, a classe `vphud-coins`, o campo
11
+ // `coins` do view-model. Nome não é seguido pelo compilador e não impede um pacote de se separar, e por isso
12
+ // a dívida era menor. Não era nula: ela dizia, em toda tela, que este HUD é de um jogo de plataforma.
13
+ //
14
+ // Agora o contador recebe um `Objective` — campo 5 de `core/contract` — e o ícone entra por injeção. E a
15
+ // mudança compra mais do que um nome: com `Objective.name` o contador GANHOU NOME ACESSÍVEL. Até aqui a
16
+ // criança cega ouvia o contador como "3 / 10", dois números sem substantivo — não havia o que falar porque a
17
+ // engine não sabia o nome do que se junta. Agora sabe, porque o jogo declara.
18
+ //
19
+ // O HUD é DOM SOBREPOSTO (não pixela: fica em alta definição sobre o canvas 320×180) — por isso vive aqui e
20
+ // não no render. O menu de pausa NÃO é deste módulo: `buildScreenPause(i)` entra por injeção e este módulo só
21
+ // anexa o retorno na tela certa e devolve os painéis prontos por `onScreensBuilt` (o game.js guarda `vpPause`
22
+ // e o `pauseActor`, que são do slice de pausa/ícones). Ver docs/5-Refactoring/plano-modularizacao-mapa.md.
23
+ //
24
+ // Sem I/O no import: `document` só aparece DENTRO das funções → o módulo é importável no project node, onde os
25
+ // testes exercitam só a metade PURA (screenGrid/screenRect/hudRowView/vphudHtml/waitBadgeHtml).
26
+ import { screenGrid } from '../core/screens.js';
27
+ import { t } from '../core/i18n.js';
28
+ // ---------------------------------------------------------------------------------------------
29
+ // Lógica PURA (nenhum `document`; testável no project node)
30
+ // ---------------------------------------------------------------------------------------------
31
+ /** Grade de telas: 1 → 1×1, 2 → 2×1, 3-4 → 2×2 (a 3ª tela é centralizada na linha de baixo). */
32
+ // A grade agora mora em core/screens (folha, sem dependências), porque o layout e o CRT precisam da MESMA
33
+ // conta e não têm o que fazer importando de um módulo de HUD. Reexportada aqui sob o nome de sempre.
34
+ export { screenGrid } from '../core/screens.js';
35
+ /**
36
+ * Posição/tamanho da tela `i` numa grade de `n` jogadores. Verbatim de screenRect(i) — que lia `numPlayers`
37
+ * global; aqui `n` é parâmetro (mesma conta, testável sem estado). Caso especial preservado: com 3 telas, a
38
+ * terceira é centralizada na linha de baixo, para casar com o posicionamento do render (configureRender).
39
+ */
40
+ export function screenRect(i, n) {
41
+ const { cols, rows } = screenGrid(n);
42
+ const col = i % cols, row = Math.floor(i / cols);
43
+ let colFrac = col / cols;
44
+ if (n === 3 && i === 2)
45
+ colFrac = (1 - 1 / cols) / 2;
46
+ return { L: (colFrac * 100) + '%', T: (row / rows * 100) + '%', W: (100 / cols) + '%', H: (100 / rows) + '%' };
47
+ }
48
+ /** Quantas telas o HUD monta: ao menos uma, mesmo antes de `players[]` existir no boot. Verbatim (Math.max(1,…)). */
49
+ export function screenCount(n) { return Math.max(1, n); }
50
+ /** O texto que o leitor de tela ouve no contador. A MOLDURA é a chave; o NOME do objetivo atravessa por
51
+ * parâmetro — a regra do pilar 3 (ADR-0010), a mesma que o currículo segue. */
52
+ export const contadorLabel = (o) => t('hud.contador', { have: String(o.have), need: String(o.need), nome: o.name.text });
53
+ /**
54
+ * Markup do contador (objetivo na 1ª coluna, poder na 2ª).
55
+ *
56
+ * O objetivo entra INTEIRO — nome, quanto tem, quanto precisa — em vez de só o alvo numérico, e o ícone entra
57
+ * por injeção. É o que tira o desenho da moeda de dentro da engine: o HUD mostra o que o jogo declarou
58
+ * (campo 5 do contrato), sem saber se é moeda, palavra ou conta.
59
+ *
60
+ * O `aria-label` é a parte que não é renomeação: sem o nome do objetivo não havia o que dizer, e o contador
61
+ * era mudo para quem não vê a tela.
62
+ */
63
+ export function vphudHtml(objetivo, icone) {
64
+ return '<span class="vphud-obj"><b class="vphud-ico">' + icone
65
+ + '</b> <b class="vphud-n">' + numero(objetivo.have) + '</b> / ' + numero(objetivo.need)
66
+ + '</span><span class="vphud-power"><b class="vphud-ico">✨</b> <span class="vphud-pw">—</span></span>';
67
+ }
68
+ /**
69
+ * PÕE O NOME DECLARADO PELO JOGO NO RÓTULO DO LEITOR DE TELA — por atributo, nunca por markup (issue #106).
70
+ *
71
+ * ⚠️ ESTE É UM VETOR QUE A AUDITORIA DE 2026-08-26 NÃO TINHA COMO CONHECER, e vale dizer por quê: ela varreu
72
+ * por FONTE DE DADO e concluiu, com razão para a época, que nada de fora chegava a markup. Depois disso o
73
+ * contrato passou a existir (ADR-0030) e os jogos passaram a viver em REPOSITÓRIOS SEPARADOS, consumindo a
74
+ * engine como pacote (ADR-0083). O `name.text` do objetivo é texto de um jogo que esta árvore não revê.
75
+ *
76
+ * ⚠️ E ELE ENTRAVA NUM ATRIBUTO, que é o pior contexto dos dois: dentro de um elemento uma aspa é inofensiva,
77
+ * dentro de `aria-label="…"` ela FECHA o atributo e o que vem a seguir vira atributo — um `onmouseover` sem
78
+ * precisar de uma única tag.
79
+ *
80
+ * `setAttribute` escapa por construção, e é por isso que a resposta é «construir nós» e não «escapar à mão»:
81
+ * um escape esquecido não deixa rasto; um `setAttribute` esquecido tira o rótulo, e há caso a prendê-lo.
82
+ */
83
+ export function aplicarRotuloDoContador(vphud, objetivo) {
84
+ vphud?.querySelector('.vphud-obj')?.setAttribute('aria-label', contadorLabel(objetivo));
85
+ }
86
+ /**
87
+ * Um número do jogo, coagido.
88
+ *
89
+ * ⚠️ `Objective.have` É `number` NO TIPO E O TIPO NÃO ATRAVESSA A FRONTEIRA DO PACOTE: um jogo em JavaScript
90
+ * puro, ou compilado de outra árvore, devolve o que quiser. Colar isso num template literal é a mesma
91
+ * categoria de defeito que o nome — só que mais fácil de esquecer, porque «é um número» está escrito no tipo.
92
+ * Um não-número vira `0`, que é falso mas inofensivo; deixar passar seria falso E perigoso.
93
+ */
94
+ function numero(v) {
95
+ return Number.isFinite(v) ? v : 0;
96
+ }
97
+ /** Markup do selo "aperte um botão para entrar" (tela criada em jogo, ainda sem dono). `i` é o índice 0-based. */
98
+ export function waitBadgeHtml(i) {
99
+ return '<div class="vphud-quit vp-wait">Jogador ' + (i + 1)
100
+ + ': aperte um botão do SEU teclado ou de um controle livre para entrar</div>';
101
+ }
102
+ /**
103
+ * Estado do HUD de um jogador. Verbatim do corpo de updateGameHud(), só que como valor.
104
+ * `powerShort` é o POWER_SHORT do game.js (injetado — a mesma FUNÇÃO que game/coin-spawning.ts já recebe).
105
+ * Função e não tabela: o texto depende do idioma ATUAL, e uma tabela lida no boot ficaria congelada nele.
106
+ */
107
+ export function hudRowView(p, powerShort, objetivo) {
108
+ return {
109
+ have: String(objetivo.have),
110
+ label: contadorLabel(objetivo),
111
+ // O `|| '—'` FICA, mesmo com o resolvedor já tratando desconhecido. Não é redundância: é a garantia de
112
+ // que o campo do poder NUNCA aparece em branco no HUD, e ela não pode depender de todo consumidor futuro
113
+ // lembrar de tratar o caso. Um teste meu ia perdê-la nesta mudança e reprovou por isso.
114
+ power: powerShort(p.activePower) || '—',
115
+ quitHidden: !p.quit,
116
+ visibility: p.quit ? 'hidden' : 'visible',
117
+ };
118
+ }
119
+ export function initHud(ctx) {
120
+ let gameHudEl = null;
121
+ let vpHudDom = [];
122
+ let vpQuitDom = [];
123
+ let vpScreens = [];
124
+ /** A sub-camada de EXPERIÊNCIA de cada tela — o que a empatia atrapalha. Ver `buildGameHud`. */
125
+ let vpExpDom = [];
126
+ function buildGameHud() {
127
+ if (!gameHudEl)
128
+ gameHudEl = ctx.$('#game-hud');
129
+ if (!gameHudEl)
130
+ return;
131
+ gameHudEl.innerHTML = '';
132
+ vpHudDom = [];
133
+ vpQuitDom = [];
134
+ vpScreens = [];
135
+ vpExpDom = [];
136
+ const panes = [];
137
+ const bars = [];
138
+ const n = ctx.getNumPlayers();
139
+ for (let i = 0; i < screenCount(n); i++) {
140
+ const r = screenRect(i, n);
141
+ const scr = document.createElement('div');
142
+ scr.className = 'player-screen';
143
+ scr.dataset.player = String(i);
144
+ scr.style.left = r.L;
145
+ scr.style.top = r.T;
146
+ scr.style.width = r.W;
147
+ scr.style.height = r.H;
148
+ // A TELA TEM DUAS SUB-CAMADAS, e a divisão é de PAPEL (issue #82, decisão do Dev):
149
+ //
150
+ // · EXPERIÊNCIA (`.screen-exp`) — HUD, selo de abandono e a atividade pedagógica do multi-tela. O
151
+ // modo de EMPATIA precisa atrapalhar aqui: é o prejuízo que a pessoa tem de sentir.
152
+ // · CONTROLE (o painel de pausa, irmão) — nunca é atingido por empatia. Simulação não é
153
+ // acessibilidade: é criar dificuldade onde a facilidade não existe. O que existe para DAR ACESSO
154
+ // — pausa, legenda, controle de toque — não pode ser degradado por ela.
155
+ //
156
+ // Elas são IRMÃS e não pai/filho porque `filter` de CSS desce para os descendentes e um filho não
157
+ // consegue cancelá-lo: com a pausa dentro da experiência, não haveria como isentá-la.
158
+ const exp = document.createElement('div');
159
+ exp.className = 'screen-exp';
160
+ scr.appendChild(exp);
161
+ vpExpDom.push(exp);
162
+ const d = document.createElement('div');
163
+ d.className = 'vphud';
164
+ d.innerHTML = vphudHtml(ctx.hudObjective(i), ctx.hudIcon);
165
+ aplicarRotuloDoContador(d, ctx.hudObjective(i)); // #106: o nome vem do JOGO — atributo, nunca markup
166
+ exp.appendChild(d);
167
+ vpHudDom.push(d);
168
+ const q = document.createElement('div');
169
+ q.className = 'vphud-quit';
170
+ q.hidden = true;
171
+ q.textContent = 'Jogo abandonado';
172
+ exp.appendChild(q);
173
+ vpQuitDom.push(q);
174
+ // A BARRA RÁPIDA entra entre a experiência e a pausa, e é IRMÃ das duas. Não vai DENTRO da
175
+ // `.screen-exp` porque `filter` de CSS desce para os descendentes e um filho não consegue cancelá-lo:
176
+ // ali dentro, o modo empatia degradaria justamente o que existe para dar acesso.
177
+ const bar = ctx.buildQuickBar(i);
178
+ scr.appendChild(bar);
179
+ bars.push(bar);
180
+ const sp = ctx.buildScreenPause(i);
181
+ scr.appendChild(sp);
182
+ panes.push(sp);
183
+ gameHudEl.appendChild(scr);
184
+ vpScreens.push(scr);
185
+ }
186
+ // O original terminava com `vpPause` preenchido e dois efeitos DEFENSIVOS (applyLetra/renderPauseLegend em
187
+ // try/catch, porque no 1º build do init LETRA/PAD_DESIGNS ainda estão em TDZ). Ambos são de OUTROS slices →
188
+ // saem por este gancho, com o try/catch preservado no game.js. Ver "chamada defensiva" no relatório.
189
+ ctx.onBarsBuilt?.(bars);
190
+ ctx.onScreensBuilt?.(panes);
191
+ }
192
+ function updateGameHud() {
193
+ for (let i = 0; i < vpHudDom.length; i++) {
194
+ const p = ctx.getPlayers()[i];
195
+ if (!p)
196
+ continue;
197
+ const d = vpHudDom[i];
198
+ const v = hudRowView(p, ctx.powerShort, ctx.hudObjective(i));
199
+ const n = d.querySelector('.vphud-n');
200
+ if (n)
201
+ n.textContent = v.have;
202
+ // O rótulo acessível acompanha o número. Escrevê-lo só na montagem deixaria o leitor de tela repetindo
203
+ // "0 de 10" a partida inteira — pior do que não ter rótulo, porque soa como informação.
204
+ const obj = d.querySelector('.vphud-obj');
205
+ if (obj)
206
+ obj.setAttribute('aria-label', v.label);
207
+ const pw = d.querySelector('.vphud-pw');
208
+ if (pw)
209
+ pw.textContent = v.power;
210
+ if (vpQuitDom[i])
211
+ vpQuitDom[i].hidden = v.quitHidden;
212
+ if (d)
213
+ d.style.visibility = v.visibility; // jogador que saiu: tela preta "jogo abandonado"
214
+ }
215
+ }
216
+ const getScreen = (i) => vpScreens[i] ?? null;
217
+ function showWaitingBadge(i) {
218
+ const scr = vpScreens[i];
219
+ if (scr && !scr.querySelector('.vp-wait'))
220
+ scr.insertAdjacentHTML('beforeend', waitBadgeHtml(i));
221
+ }
222
+ function clearWaitingBadge(i) {
223
+ const scr = vpScreens[i];
224
+ const w = scr && scr.querySelector('.vp-wait');
225
+ if (w)
226
+ w.remove();
227
+ }
228
+ return { buildGameHud, updateGameHud, getScreen, showWaitingBadge, clearWaitingBadge };
229
+ }
@@ -0,0 +1,19 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ export interface ItemDeMenu {
3
+ /** O que o item é. Espaço em branco de markup é normalizado aqui. */
4
+ rotulo: string;
5
+ /** O valor ou estado: "ativado", "38%", a palavra de exemplo do minijogo. Opcional. */
6
+ estado?: string;
7
+ /** Posição na lista, contada a partir de 1 — é o número que a criança ouve, não o índice do array. */
8
+ posicao: number;
9
+ /** Quantos itens a lista tem. */
10
+ total: number;
11
+ }
12
+ /**
13
+ * A frase que o item narra.
14
+ *
15
+ * A posição fora de faixa NÃO é anunciada, e é decisão, não guarda defensivo: um item filtrado por
16
+ * visibilidade sai da lista e deixa o índice de quem sobrou fora dela. "0 de 7" ensina uma geografia falsa
17
+ * do menu, e a criança confia nela — número errado é pior que número nenhum.
18
+ */
19
+ export declare function anunciarItem(item: ItemDeMenu, comIndice: boolean): string;
@@ -0,0 +1,36 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // ui/item-announcement — COMO UM ITEM DE MENU SE ANUNCIA (ADR-0044, item 3).
3
+ //
4
+ // A XAG 106 descreve a frase inteira — "Gamma, slider, 38%, 6 of 9" — e este módulo monta a parte que o jogo
5
+ // controla: RÓTULO, ESTADO e ÍNDICE, nessa ordem, com o índice no FIM.
6
+ //
7
+ // POR QUE O ÍNDICE VAI NO FIM. Quem varre um menu por escuta interrompe assim que reconhece o item — é o que
8
+ // a narração interrompível do item 2 passou a permitir. Com o número na frente, a contagem seria a única
9
+ // parte que SEMPRE daria tempo de ouvir, e o rótulo, a única que interessa, a que nunca chegaria.
10
+ //
11
+ // POR QUE ELE PODE SER DESLIGADO. Para quem já sabe o menu de cor, o número vira ruído em toda passagem. A
12
+ // XAG pede a opção explicitamente, e o princípio é o mesmo do resto do projeto: acessibilidade que não se
13
+ // pode desligar é imposição, não ajuste.
14
+ //
15
+ // ESTE MÓDULO NÃO TOCA O DOM. Quem chama separa as partes (o rótulo do botão, o sub-rótulo, o estado do
16
+ // alternador) e recebe uma frase. É o que permite que a regra seja a MESMA na abertura, na pausa e nas
17
+ // atividades sem que cada tela reinvente a pontuação.
18
+ import { t } from '../core/i18n.js';
19
+ /** Espaço em branco de markup (quebras de linha, indentação) vira UM espaço; pontas somem. */
20
+ const enxuto = (s) => (s || '').replace(/\s+/g, ' ').trim();
21
+ /**
22
+ * A frase que o item narra.
23
+ *
24
+ * A posição fora de faixa NÃO é anunciada, e é decisão, não guarda defensivo: um item filtrado por
25
+ * visibilidade sai da lista e deixa o índice de quem sobrou fora dela. "0 de 7" ensina uma geografia falsa
26
+ * do menu, e a criança confia nela — número errado é pior que número nenhum.
27
+ */
28
+ export function anunciarItem(item, comIndice) {
29
+ const valeIndice = comIndice && item.total >= 1 && item.posicao >= 1 && item.posicao <= item.total;
30
+ const partes = [
31
+ enxuto(item.rotulo),
32
+ enxuto(item.estado),
33
+ valeIndice ? t('sr.menu.index', { n: item.posicao, m: item.total }) : '',
34
+ ];
35
+ return partes.filter(Boolean).join(', ');
36
+ }
@@ -0,0 +1,6 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ /** Liga a contagem de jogadores. Chamado uma vez pela raiz, antes do primeiro `layout()`. */
3
+ export declare function initLayout(deps: {
4
+ numJogadores: () => number;
5
+ }): void;
6
+ export declare function layout(): void;