@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,430 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // ui/shell.ts — A CASCA: em que TELA o jogo está, e o que a tela liga e desliga ao trocar.
3
+ //
4
+ // Este módulo responde a UMA pergunta: "estamos no título, jogando ou pausados — e o que isso significa para o
5
+ // documento?". Ele é o irmão de cima de tudo o que já saiu: game/session.ts decide o que é uma RODADA,
6
+ // render/draw.ts decide o que é um QUADRO, e aqui se decide o que é uma TELA. Nada aqui sabe de física, de
7
+ // moeda ou de pixel; tudo aqui sabe de `hidden`, de foco, de mudo e do menu de pausa.
8
+ //
9
+ // O QUE VEIO, E DE ONDE (bloco "E14: shell — título/splash + pausa" do game.js)
10
+ // · `setPhase(p)` — a reação de UI à troca de fase. O VALOR continua em core/state.ts (setPhaseValue);
11
+ // o que mora aqui é só a consequência visível.
12
+ // · `pauseActs` — a tabela de ações dos `.pm-btn` do menu de pausa (por tela).
13
+ // · `pauseSelect()` — põe o 1º item (Continuar) selecionado em cada tela de pausa.
14
+ // · `printMode()` — esconde as pausas para ver a tela limpa; qualquer botão volta.
15
+ // · `togglePause()` — jogando ⇄ pausado.
16
+ // · `updateTitleLegend()`— a legenda de dispositivo do splash (2 linhas de "chips": movimento/pausa e os 4
17
+ // botões de ação, com o que está CONFIGURADO para o Jogador 1).
18
+ //
19
+ // POR QUE `setPhase` NÃO VIROU UMA MÁQUINA DE ESTADOS COM TABELA DE TRANSIÇÃO — E O QUE VIROU NO LUGAR
20
+ // O pedido do projeto era transformar a sequência de `if`s numa máquina de estados de verdade. Olhei e a
21
+ // resposta honesta é: metade sim, metade não, e a metade que vale não é a que o nome sugere.
22
+ // · NÃO vale a tabela de transição. São TRÊS estados e as nove transições são todas legais — de qualquer
23
+ // fase para qualquer fase, sem guarda, sem evento nomeado, sem hook de entrada/saída além do corpo que já
24
+ // está aqui. Uma `Map<[from,to], handler>` sobre isso não impede nenhum estado ilegal (não existe nenhum),
25
+ // não descreve nada que o código já não diga, e troca um bloco linear que se lê de cima a baixo por uma
26
+ // indireção que obriga a saltar. Seria cerimônia sobre três estados. Não fiz.
27
+ // · VALE, e muito, separar a DECISÃO do EFEITO. Os `if`s do original não são transições: são nove perguntas
28
+ // diferentes feitas à MESMA fase (`p!=='playing'`, `p!=='title'`, `p!=='paused'`, `p==='paused'`…),
29
+ // espalhadas no meio de nove escritas no DOM. Isso é uma PROJEÇÃO pura da fase disfarçada de sequência
30
+ // imperativa. Extraí a projeção — `phaseView(p)` devolve o registro completo do que a fase manda fazer, e
31
+ // `applyPhaseView` é a única parte que toca o documento. O ganho é concreto e não é estético: a decisão
32
+ // passa a ser testável no project `node`, sem DOM, sem PIXI e sem áudio, e um erro de sinal num `!==`
33
+ // (o tipo de erro que aqui deixa a11y quebrada em silêncio: overlay do título visível durante o jogo,
34
+ // áudio não silenciado na pausa) vira uma asserção em vez de um sintoma que só aparece jogando.
35
+ // É o mesmo movimento que ui/title.ts já fez com `computeTitleMenuView`, e por isso é o precedente da casa.
36
+ // · O único pedaço com MEMÓRIA (e portanto o único candidato legítimo a "estado") é a restauração dos
37
+ // controles de toque, que depende do `dataset.wasOn` gravado na pausa anterior. Esse virou
38
+ // `touchControlsPlan(...)`, uma função pura de (fase, wasOn, escondido, nº de telas) → (escondido', wasOn').
39
+ // Ver, logo abaixo, o defeito que essa separação tornou visível.
40
+ //
41
+ // O QUE FICOU DE FORA, E POR QUÊ
42
+ // · `fpsTick` — a fronteira listava, mas ele não é casca: é instrumentação de HUD (escreve `#hud-fps` e
43
+ // `#hud-fpsmin`), tem três contadores próprios de módulo e é chamado do laço a cada quadro, não na troca de
44
+ // tela. Trazê-lo para cá acoplaria o "em que tela estamos" ao "quantos quadros por segundo" sem nenhum
45
+ // parentesco. O precedente da casa já é esse: ui/layout.ts diz, no cabeçalho, "fpsTick/configureRender
46
+ // seguem no game.js (outro concern)". Não forcei; segue no game.js, e o lar natural dele, quando chegar a
47
+ // vez, é ui/hud.ts.
48
+ // · `padKind()` — NÃO extraí porque JÁ ESTÁ EXTRAÍDO: `input/touch.ts` exporta `padKind()` desde a Onda A,
49
+ // com o corpo idêntico ao do game.js, e o cabeçalho de lá registra que a cópia é órfã. A que sobrou no
50
+ // game.js é código morto duplicado (nenhum chamador — ver o relatório). O certo é apagá-la, não mudá-la
51
+ // de lugar mais uma vez.
52
+ // · `updateTitleLegend()` — a fronteira deixava em aberto se ela é de shell ou de menu-nav. É de SHELL, e a
53
+ // razão é simples: ela não navega nada. Não lê foco, não trata tecla, não anda entre itens; ela pinta o
54
+ // rodapé de UMA tela específica (o título) com a configuração de entrada vigente, e quem a chama é o
55
+ // próprio `setPhase('title')`. Se morasse em ui/menu-nav, a casca precisaria importar o módulo de
56
+ // navegação só para desenhar uma legenda, e a dependência apontaria para o lado errado (a casca chamando o
57
+ // teclado). Ficando aqui, ui/menu-nav não precisa saber que existe uma tela de título.
58
+ // · O `.pm-btn`/`.pi-btn` (markup e delegação de clique) é de ui/pause-icons.ts, que já os constrói e já
59
+ // consome esta tabela por `getPauseActs()`. Aqui está só a TABELA, não o botão.
60
+ // · Abrir/fechar os nove diálogos de configuração é de ui/settings-panel.ts + ui/settings-*.ts. `pauseActs`
61
+ // apenas os CHAMA, por injeção.
62
+ //
63
+ // INJEÇÃO E ORDEM DE BOOT (a armadilha desta etapa)
64
+ // · `phase` e `numPlayers` NÃO entram por getter, e isso é de propósito: eles deixaram de ser `let` do
65
+ // game.js na Fase 2 e hoje são bindings vivos de core/state.ts. Importá-los direto é exatamente o que
66
+ // game/session.ts e input/touch.ts já fazem — um getter aqui seria uma indireção sobre uma indireção.
67
+ // Quem AINDA é `let` do game.js entra por getter: `vpPause` (reatribuído por `buildGameHud`) e
68
+ // `pauseActor` (reatribuído por seis lugares, incluindo o ctx do gamepad).
69
+ // · TODA entrada de `pauseActs` é um callback, e não um valor, porque `initShell` precisa poder ser chamado
70
+ // no lugar do bloco E14 (linha ~1373 do game.js) enquanto quase tudo o que a tabela chama — `openTypo`,
71
+ // `openAudio`, `openMovement`, `openVisual`, `openHelp`, `quitGame`, `fitsN`, `joinPlayer` — são
72
+ // `function` içadas OU `const` declarados depois (`motor`, `motion`, `empathy`, `hud`, `selVizPlayer`).
73
+ // Com callbacks, a resolução acontece na CHAMADA (sempre pós-boot) e não na montagem do ctx.
74
+ // · `setPhase` é chamado de fora por game/session.ts, input/gamepad.ts, game/attract.ts e
75
+ // ui/activities-menu.ts, todos com o ctx montado ANTES do ponto de extração. Por isso o game.js deve
76
+ // manter um envelope `function setPhase(p){ shell.setPhase(p); }` — declaração de função, içada — em vez
77
+ // de trocar as quatro fiações. É o mesmo padrão já usado lá para `hideTouchControls`, `showTouchControls`,
78
+ // `restartGame`, `fitsN`, `joinPlayer` e `quitGame`.
79
+ // · O original protegia duas chamadas com `typeof pauseSelect==='function'` / `typeof reflectPauseIcons===
80
+ // 'function'`. Eram guardas de TDZ do tempo do monólito; aqui `pauseSelect` é função local (sempre
81
+ // definida) e `reflectPauseIcons` é injetada (sempre função). Verifiquei que `reflectPauseIcons` é `const`
82
+ // declarado no game.js MUITO antes de a primeira pausa acontecer, então as guardas nunca foram falsas em
83
+ // execução real — removê-las não muda comportamento, só tira ruído. Registrado aqui porque é a única
84
+ // linha que não é cópia literal.
85
+ //
86
+ // SEM I/O NO IMPORT: o corpo do módulo só declara dados e funções puras. Todo efeito passa por `initShell`.
87
+ // GUARDAS: cada consulta ao DOM passa por guarda de nulo, como manda a casa — é o que permite rodar no project
88
+ // `node` com um `$` falso que devolve `null`.
89
+ //
90
+ // Ver docs/5-Refactoring/plano-modularizacao-mapa.md (C3).
91
+ import { t } from '../core/i18n.js';
92
+ import { PAD_DESIGNS, PAD_GLYPH_SPOKEN } from '../input/devices.js'; // módulo-folha de DADOS (zero deps) — importado, não injetado
93
+ // UMA constante, e não um seletor repetido: com o submenu de opções (ADR-0044, item 5) o cartão de pausa passou
94
+ // a ter DUAS listas, e quem varrer `.pm-btn` cru enxerga também a que está escondida.
95
+ import { PM_ITENS_VISIVEIS } from './pause-icons.js';
96
+ // `FatosDaCena` mora em `core/scenes`, ao lado da pilha que os produz — é tipo de ENGINE, e precisa ser
97
+ // alcançável também por `game/`, que não pode importar de `ui/`. Ver o cabeçalho de lá.
98
+ /** Verbatim das nove perguntas que o `setPhase` do game.js fazia à fase, agora feitas de uma vez só. */
99
+ export function phaseView(f) {
100
+ return {
101
+ titleOverlayHidden: !f.telaDeTitulo,
102
+ pauseOverlayHidden: true,
103
+ screenPauseHidden: !f.menuDePausa,
104
+ masterMuted: !f.mundoRodando,
105
+ hideTouchControls: !f.mundoRodando,
106
+ pausePressed: f.menuDePausa,
107
+ focus: f.mundoRodando ? 'game-region' : f.menuDePausa ? 'pause-menu' : 'title-button',
108
+ };
109
+ }
110
+ /**
111
+ * O ÚNICO pedaço de `setPhase` com memória: esconder os controles de toque ao pausar (guardando que estavam
112
+ * ligados) e devolvê-los ao retomar. Pura, e por isso pinável sem navegador.
113
+ *
114
+ * O estado que chega aqui é o de ANTES de `hideTouchControls()` — quem chama lê primeiro e esconde depois.
115
+ * Já foi o contrário, e o efeito era que `wasOn` nunca era gravado (o plano via o pad como se já estivesse
116
+ * desligado): pausar no celular sumia com o direcional virtual e retomar não o devolvia.
117
+ */
118
+ export function touchControlsPlan(f, st, screens) {
119
+ if (f.menuDePausa) {
120
+ if (!st.hidden)
121
+ return { hidden: true, wasOn: true }; // guarda que estava ligado e esconde
122
+ return st; // já escondido: nada muda (nem `wasOn`)
123
+ }
124
+ if (f.mundoRodando) {
125
+ return { hidden: st.wasOn && screens <= 1 ? false : st.hidden, wasOn: false };
126
+ }
127
+ return { hidden: true, wasOn: false }; // título (ou qualquer cena que não seja jogo nem pausa): some e esquece
128
+ }
129
+ // ---------------------------------------------------------------------------------------------------------
130
+ // LEGENDA DO TÍTULO — a parte pura (montar as duas linhas de chips a partir dos glifos escolhidos)
131
+ // ---------------------------------------------------------------------------------------------------------
132
+ /** Um "chip" da legenda: o glifo (com cor de fundo opcional) seguido da palavra que ele significa. */
133
+ export function chip(txt, col, word) {
134
+ return `<span class="lg"><span class="lg-ico"${col ? ` style="background:${col}"` : ''}>${txt}</span>${word ? ' ' + word : ''}</span>`;
135
+ }
136
+ /**
137
+ * O nome FALADO de um glifo de controle. Glifo que já se lê passa intocado.
138
+ *
139
+ * Ver `PAD_GLYPH_SPOKEN` em `input/devices`: a tabela guarda CHAVES e não texto, porque ela é uma `const` de
140
+ * módulo avaliada uma vez no import — texto já resolvido congelaria o idioma no boot. Quem resolve é aqui, a
141
+ * cada chamada, com o idioma vigente naquele instante.
142
+ */
143
+ export function glifoFalado(g) {
144
+ const k = PAD_GLYPH_SPOKEN[g];
145
+ return k ? t(k) : g;
146
+ }
147
+ /**
148
+ * A LEGENDA DA PAUSA — duas camadas no mesmo lugar (ADR-0044, item 4).
149
+ *
150
+ * Ela dizia qual botão confirma e qual volta, e carregava `aria-hidden="true"` — ou seja, era invisível
151
+ * justamente para quem não pode ver o glifo. A XAG 106 manda narrar exatamente isto ("A to Select").
152
+ *
153
+ * Mas só tirar o atributo devolveria o ruído que provavelmente o motivou: um leitor de tela lê `✕` como
154
+ * "sinal de multiplicação". Então os CHIPS ficam visíveis e mudos, e ao lado nasce UMA frase só para leitor
155
+ * de tela, com os glifos já traduzidos em palavra. Duas leituras da mesma informação, cada uma no sentido
156
+ * que a alcança.
157
+ *
158
+ * Recebe os pares `[glifo, cor]` que `simNaoGlyphs` escolhe a partir do desenho do controle — a inversão
159
+ * PlayStation/Nintendo (onde "sim" é o botão 1) já vem decidida de lá.
160
+ */
161
+ export function pauseLegendHtml(sim, nao) {
162
+ const mudo = (g, palavra) => `<span class="lg" aria-hidden="true"><span class="lg-ico" style="background:${g[1]}">${g[0]}</span> ${palavra}</span>`;
163
+ const falada = t('menu.legendSpoken', { sim: glifoFalado(sim[0]), nao: glifoFalado(nao[0]) });
164
+ return mudo(sim, t('menu.yes')) + mudo(nao, t('menu.no')) + `<span class="sr-only">${falada}</span>`;
165
+ }
166
+ /**
167
+ * Linha 1 da legenda: direcional + START/Enter. Igual para toque e gamepad; o teclado sobrescreve os rótulos.
168
+ *
169
+ * As palavras vêm de `legend.*` e são resolvidas AQUI, a cada chamada — não numa tabela de módulo, que
170
+ * congelaria o idioma no boot. Registro CURTO de propósito: esta fileira fica embaixo de um glifo e não tem
171
+ * largura para o "Correr / interagir" que a lista de mapeamento usa (ver a nota em pt.ts).
172
+ */
173
+ export function legendRow1(dirTxt, pauseTxt) {
174
+ return chip(dirTxt, null, t('legend.move')) + chip(pauseTxt, null, t('legend.pause'));
175
+ }
176
+ /**
177
+ * Linha 2 da legenda: os botões de ação, na ordem dos glifos.
178
+ *
179
+ * ⚠️ AS PALAVRAS DEIXARAM DE ESTAR AQUI. Esta função dizia `t('legend.jump')`, `t('legend.run')` — o
180
+ * vocabulário da plataforma dentro de um módulo de engine, e a engine a afirmar que todo jogo tem pular,
181
+ * especial, correr e trocar, nessa ordem. Agora `rotulo` é perguntado ao jogo (a versão CURTA, ver
182
+ * `ActionWord.short`), e uma posição que o jogo não nomeia não vira ficha nenhuma.
183
+ */
184
+ export function legendRow2(g, rotulo) {
185
+ const ORDEM = ['action2', 'action3', 'action1', 'action4'];
186
+ return ORDEM.map((a) => {
187
+ const palavra = rotulo(a);
188
+ return palavra ? chip(g[a][0], g[a][1], palavra) : '';
189
+ }).join('');
190
+ }
191
+ /** O innerHTML final de `#title-legend`: duas `.lg-row`. */
192
+ export function legendHtml(l1, l2) {
193
+ return `<span class="lg-row">${l1}</span><span class="lg-row">${l2}</span>`;
194
+ }
195
+ /**
196
+ * Glifos de um pad FÍSICO. `layout` sai de `padLayoutFromId` (só quando `mapping === 'standard'`; fora do
197
+ * padrão é sempre 'generic'), e `custom` é o mapa do assistente (só consultado FORA do padrão). Verbatim:
198
+ * um botão sem entrada no mapa custom cai no índice default ('0'..'3'), e um índice que o design não conhece
199
+ * vira o par `[índice, '#3a4a6a']` — o cinza de fallback.
200
+ */
201
+ export function padActionGlyphs(layout, custom) {
202
+ const set = PAD_DESIGNS[layout] || PAD_DESIGNS.generic;
203
+ // O `typeof b === 'object'` não é cerimônia: o `PadMap` admite `boolean` além de `PadBinding` — é o
204
+ // sentinela `_skip: true` do assistente de mapeamento. As quatro chaves lidas aqui nunca são ele, então
205
+ // em execução nada muda; o que muda é que a leitura passa a PERGUNTAR em vez de supor.
206
+ const bOf = (k, def) => {
207
+ const b = custom && custom[k];
208
+ return b && typeof b === 'object' && typeof b.b === 'number' ? String(b.b) : def;
209
+ };
210
+ const gy = (k) => set[k] || [k, '#3a4a6a'];
211
+ return { action2: gy(bOf('action2', '0')), action3: gy(bOf('action3', '1')), action1: gy(bOf('action1', '2')), action4: gy(bOf('action4', '3')) };
212
+ }
213
+ /** Glifos do joystick VIRTUAL (toque): sempre o design 'generic' (0/1/2/3), sem mapa custom. */
214
+ export function touchActionGlyphs() {
215
+ return padActionGlyphs('generic', null);
216
+ }
217
+ /**
218
+ * Escolhe QUAL gamepad a legenda descreve: o do Jogador 1, se ele tiver um associado (`players[0].pad`);
219
+ * senão o primeiro conectado. Verbatim do laço do game.js — inclusive o detalhe de que, com `p1pad >= 0` e
220
+ * nenhum pad daquele índice presente, o resultado é `null` (a legenda cai no teclado) em vez de pegar outro.
221
+ */
222
+ export function pickLegendPad(pads, p1pad) {
223
+ let gp = null;
224
+ for (const g of pads) {
225
+ if (!g)
226
+ continue;
227
+ if (p1pad >= 0) {
228
+ if (g.index === p1pad) {
229
+ gp = g;
230
+ break;
231
+ }
232
+ }
233
+ else if (!gp)
234
+ gp = g;
235
+ }
236
+ return gp;
237
+ }
238
+ export function initShell(ctx) {
239
+ /* ===================== a legenda do título ===================== */
240
+ /** Linha 1+2 quando não há toque nem gamepad: as teclas REALMENTE configuradas do Jogador 1. */
241
+ function keyboardLegend() {
242
+ const m = ctx.kbFor(0);
243
+ const K = (a) => ctx.keyName((m[a] || [])[0] || '?');
244
+ const l1 = legendRow1(`${K('up')} ${K('left')} ${K('down')} ${K('right')}`, 'Enter');
245
+ const l2 = legendRow2({ action2: [K('action2'), null], action3: [K('action3'), null], action1: [K('action1'), null], action4: [K('action4'), null] }, ctx.rotuloCurto);
246
+ return [l1, l2];
247
+ }
248
+ function updateTitleLegend() {
249
+ const el = ctx.$('#title-legend');
250
+ if (!el)
251
+ return; // 2 LINHAS, com o que está CONFIGURADO p/ o jogador da tela
252
+ let l1, l2;
253
+ if (ctx.isTouchMode()) { // joystick VIRTUAL: 0/1/2/3 + START
254
+ l1 = legendRow1('✜', 'START');
255
+ l2 = legendRow2(touchActionGlyphs(), ctx.rotuloCurto);
256
+ }
257
+ else {
258
+ const p0 = ctx.getPlayers()[0];
259
+ const p1pad = p0 && typeof p0.pad === 'number' && p0.pad >= 0 ? p0.pad : -1;
260
+ const gp = pickLegendPad(ctx.getGamepads(), p1pad);
261
+ if (gp) { // joystick FÍSICO: design do modelo + mapa custom do wizard
262
+ const layout = gp.mapping === 'standard' ? ctx.padLayoutFromId(gp.id) : 'generic';
263
+ const custom = gp.mapping !== 'standard' ? ctx.padMapFor(gp.id) : null;
264
+ l1 = legendRow1('✜', 'START');
265
+ l2 = legendRow2(padActionGlyphs(layout, custom), ctx.rotuloCurto);
266
+ }
267
+ else {
268
+ [l1, l2] = keyboardLegend(); // TECLADO: teclas configuradas (remap respeitado)
269
+ }
270
+ }
271
+ el.innerHTML = legendHtml(l1, l2);
272
+ const w = ctx.$('#title-wait');
273
+ if (w)
274
+ w.hidden = ctx.getNumPlayers() <= 1; // MP: aviso "Aguarde o Jogador 1"
275
+ }
276
+ /* ===================== seleção e Print ===================== */
277
+ function pauseSelect() {
278
+ ctx.getPauseScreens().forEach((sp) => {
279
+ const items = [...sp.querySelectorAll(PM_ITENS_VISIVEIS)];
280
+ items.forEach((b) => b.classList.remove('pm-sel'));
281
+ if (items[0])
282
+ items[0].classList.add('pm-sel'); // 1º item (Continuar) selecionado em cada tela
283
+ });
284
+ }
285
+ function printMode() {
286
+ ctx.getPauseScreens().forEach((sp) => { sp.hidden = true; }); // vê a tela limpa; qualquer botão volta
287
+ const back = (e) => {
288
+ if (e && e.preventDefault) {
289
+ try {
290
+ e.preventDefault();
291
+ }
292
+ catch { /* noop */ }
293
+ }
294
+ ctx.win.removeEventListener('keydown', back, true);
295
+ ctx.win.removeEventListener('pointerdown', back, true);
296
+ if (ctx.fatosDaCena().menuDePausa) {
297
+ ctx.getPauseScreens().forEach((sp) => { sp.hidden = false; });
298
+ pauseSelect();
299
+ }
300
+ };
301
+ // 80ms de atraso: o próprio evento que ACIONOU o Print não pode ser o que o desfaz.
302
+ ctx.win.setTimeout(() => {
303
+ ctx.win.addEventListener('keydown', back, true);
304
+ ctx.win.addEventListener('pointerdown', back, true);
305
+ }, 80);
306
+ ctx.srSay(t('sr.print.on'));
307
+ }
308
+ /* ===================== a troca de fase ===================== */
309
+ /** A metade IMPURA: pega a projeção pronta e a carimba no documento. */
310
+ function applyPhaseView(v) {
311
+ const t = ctx.$('#title-overlay'), pa = ctx.$('#pause-overlay');
312
+ if (t)
313
+ t.hidden = v.titleOverlayHidden;
314
+ if (pa)
315
+ pa.hidden = v.pauseOverlayHidden; // pausa GLOBAL aposentada (Etapa 2): agora é uma por tela
316
+ ctx.getPauseScreens().forEach((sp) => { sp.hidden = v.screenPauseHidden; });
317
+ }
318
+ /** O estado do `#touch-controls` ANTES de qualquer coisa desta troca de fase mexer nele. */
319
+ function readTouchControls() {
320
+ const tc = ctx.$('#touch-controls');
321
+ return tc ? { hidden: tc.hidden, wasOn: tc.dataset.wasOn === '1' } : null;
322
+ }
323
+ /** A metade IMPURA do plano de toque: recebe o estado lido ANTES do hide e grava o plano de volta. */
324
+ function applyTouchControls(f, before) {
325
+ const tc = ctx.$('#touch-controls');
326
+ if (!tc || !before)
327
+ return;
328
+ const after = touchControlsPlan(f, before, ctx.getNumPlayers());
329
+ // Só escreve o que MUDOU — é o que torna o applier equivalente linha a linha ao original (que, no ramo
330
+ // 'paused' já-escondido, não toca em nada; e cujos `delete` nos outros ramos são no-op quando não havia flag).
331
+ if (after.hidden !== before.hidden)
332
+ tc.hidden = after.hidden;
333
+ if (after.wasOn !== before.wasOn) {
334
+ if (after.wasOn)
335
+ tc.dataset.wasOn = '1';
336
+ else
337
+ delete tc.dataset.wasOn;
338
+ }
339
+ }
340
+ function applyFocus(f) {
341
+ if (f === 'game-region') {
342
+ const gr = ctx.$('#game-region');
343
+ if (gr)
344
+ gr.focus();
345
+ return;
346
+ }
347
+ if (f === 'pause-menu') {
348
+ pauseSelect();
349
+ ctx.reflectPauseIcons();
350
+ return;
351
+ }
352
+ updateTitleLegend();
353
+ const b = ctx.$('#tm-main button');
354
+ if (b)
355
+ b.focus();
356
+ }
357
+ /**
358
+ * Projeta a cena do topo no documento. Chamada pela raiz DEPOIS de ela mexer na pilha — a casca não empilha
359
+ * nem desempilha, e é essa separação que faz o `Phase` sumir daqui: quem troca de cena sabe os nomes, quem
360
+ * projeta só precisa dos três fatos.
361
+ */
362
+ function aplicarCena() {
363
+ const f = ctx.fatosDaCena();
364
+ const v = phaseView(f);
365
+ // LER ANTES DE ESCONDER. Era aqui o defeito: `hideTouchControls()` roda logo abaixo e já põe `tc.hidden`
366
+ // em true, então o plano — que rodava depois — via o pad como se ele já estivesse desligado, nunca gravava
367
+ // o `wasOn`, e o ramo que o traz de volta ao retomar era inalcançável. No celular: pausar sumia com o
368
+ // direcional e retomar não o devolvia.
369
+ const antesDoHide = readTouchControls();
370
+ if (v.hideTouchControls)
371
+ ctx.hideTouchControls(); // menu ativo (título/pausa) = sem controle virtual
372
+ // GAG: na pausa, silencia TODO o som do jogo (loops de ambiente/chuva inclusive) — volta ao retomar.
373
+ ctx.setMasterMuted(v.masterMuted);
374
+ applyPhaseView(v);
375
+ applyTouchControls(f, antesDoHide); // o estado é o de ANTES do hide — ver o comentário acima
376
+ // ORDEM verbatim: o aria-pressed vem DEPOIS do bloco de toque.
377
+ //
378
+ // No TÍTULO o atributo SAI, em vez de virar `false`. Ali o botão significa "iniciar", e `aria-pressed`
379
+ // num botão que não alterna nada faz o leitor de tela anunciar um estado que não existe — pior que não
380
+ // anunciar nada. `titleOverlayHidden` é verdadeiro fora do título.
381
+ const pb = ctx.$('#touch-start');
382
+ if (pb) {
383
+ if (v.titleOverlayHidden)
384
+ pb.setAttribute('aria-pressed', String(v.pausePressed));
385
+ else
386
+ pb.removeAttribute('aria-pressed');
387
+ }
388
+ applyFocus(v.focus);
389
+ }
390
+ /* ===================== a tabela do menu de pausa ===================== */
391
+ // Ações do menu de pausa (compartilhadas pelos menus por tela). Ao abrir um submenu de a11y, escopa ao
392
+ // jogador que agiu (pauseActor) — o diálogo abre na aba dele.
393
+ const pauseActs = {
394
+ resume: () => ctx.retomarJogo(),
395
+ // O botão ABC era um CICLO de duas posições; virou a porta do menu de CAA (ADR-0028), onde a caixa da
396
+ // letra é uma escolha entre outras. Ele não sumiu — quem usava o atalho continua a um clique da escolha,
397
+ // em vez de ter de descobrir onde ela foi parar. A ação `letra` sumiu junto com o ciclo: um nome por coisa.
398
+ caa: () => ctx.openCaa(),
399
+ nivel: () => ctx.setQuizLevel(ctx.getQuizLevel() % 5 + 1, true), // L3: cicla 1..5
400
+ tipo: () => ctx.openTypo(),
401
+ // R-splash 2: só AUMENTA (nunca diminui); a tela nova ESPERA um botão do jogador entrar
402
+ addplayer: () => {
403
+ const n = ctx.getNumPlayers();
404
+ if (n >= 4) {
405
+ ctx.srAlert(t('sr.screens.maxPlayers'));
406
+ return;
407
+ }
408
+ if (!ctx.fitsN(n + 1)) {
409
+ ctx.srAlert(t('sr.screens.wontFitOneMore'));
410
+ return;
411
+ }
412
+ if (!ctx.joinPlayer(null))
413
+ return;
414
+ const p = ctx.getPlayers()[ctx.getNumPlayers() - 1];
415
+ p.waiting = true;
416
+ ctx.showWaitingBadge(p.i);
417
+ ctx.retomarJogo();
418
+ ctx.srAlert(t('sr.player.pressToJoin', { n: p.i + 1 }));
419
+ },
420
+ audio: () => ctx.openAudio(),
421
+ motora: () => { ctx.setMotorPlayer(ctx.getPauseActor()); ctx.openMovement(); },
422
+ anim: () => { ctx.setMotionPlayer(ctx.getPauseActor()); ctx.openMotion(); },
423
+ visual: () => { ctx.setSelVizPlayer(ctx.getPauseActor()); ctx.openVisual(); },
424
+ empatia: () => { ctx.setSelVizPlayer(ctx.getPauseActor()); ctx.openEmpathy(); },
425
+ print: () => printMode(),
426
+ quit: () => ctx.quitGame(),
427
+ ajuda: () => ctx.openHelp(),
428
+ };
429
+ return { aplicarCena, pauseSelect, printMode, updateTitleLegend, pauseActs };
430
+ }
@@ -0,0 +1,36 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ import type { DomQuery } from '../core/dom-query.js';
3
+ /** Minimal DOM-selector shape (matches ui/dom.ts's `$`). */
4
+ export type { DomQuery } from '../core/dom-query.js';
5
+ export type TitleMenuId = 'tm-main' | 'tm-alf' | 'tm-mat' | 'tm-tab' | 'tm-fr' | 'tm-cen';
6
+ /**
7
+ * The 6 submenus of the title screen, in the order game.js has always hidden/shown them.
8
+ *
9
+ * The ORDERED array is the source and the Set is derived from it, not the other way round. A Set carries no
10
+ * order in its type, so ui/activities-menu - which has to scan the submenus in order to find the visible one -
11
+ * had declared its own array of the same six ids, kept in step by discipline alone. One list now.
12
+ */
13
+ export declare const TITLE_MENU_IDS_ORDERED: readonly TitleMenuId[];
14
+ export declare const TITLE_MENU_IDS: ReadonlySet<TitleMenuId>;
15
+ export declare function isTitleMenuId(v: string): v is TitleMenuId;
16
+ export interface TitleCtx {
17
+ /** DOM selector (querySelector), injected — this module never reaches `document` globally. */
18
+ $: DomQuery;
19
+ }
20
+ export interface TitleApi {
21
+ /** Shows `which` and hides the other 5 submenus; toggles legend/title-block; focuses the first button. */
22
+ show: (which: TitleMenuId) => void;
23
+ }
24
+ export interface TitleMenuView {
25
+ /** The submenu that becomes visible. */
26
+ active: TitleMenuId;
27
+ /** hidden.get(id) === true means that submenu's `hidden` attribute should be set. */
28
+ hidden: ReadonlyMap<TitleMenuId, boolean>;
29
+ /** #title-legend is hidden everywhere except tm-main (submenus show their own description footer instead). */
30
+ legendHidden: boolean;
31
+ /** .title-block (the game's title art) shows only on tm-main; submenus render their own `.tm-title` heading. */
32
+ titleBlockDisplay: '' | 'none';
33
+ }
34
+ /** Pure view-model for showTitleMenu(which). Verbatim port of the old function's branching. */
35
+ export declare function computeTitleMenuView(which: TitleMenuId): TitleMenuView;
36
+ export declare function initTitle(ctx: TitleCtx): TitleApi;
@@ -0,0 +1,60 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // ui/title — title-screen submenu NAVIGATION (Estágio 4): extracted from game.js's showTitleMenu(). Shows one
3
+ // submenu (`tm-main`/`tm-alf`/`tm-mat`/`tm-tab`/`tm-fr`/`tm-cen`) and hides the rest, toggles the footer legend
4
+ // (only on `tm-main`) and the game-title block (only on `tm-main` — submenus show their own `.tm-title` instead),
5
+ // then focuses the first button of the now-visible submenu. DI via initTitle(ctx): only `$` (DOM selector,
6
+ // ui/dom.ts shape) is needed — the transition carries no message of its own (callers picked the srSay text
7
+ // per-transition in game.js; duplicating an announcement here would double-speak). Everything else that used to
8
+ // live under "menu inicial" in game.js — building the submenu markup (buildTitleMenus), the footer legend
9
+ // (updateTitleLegend), arrow-key focus traversal (navTitle/titleButtons) and the click/keydown wiring — belongs to
10
+ // other Estágio-4 slices and stays in game.js for now.
11
+ /**
12
+ * The 6 submenus of the title screen, in the order game.js has always hidden/shown them.
13
+ *
14
+ * The ORDERED array is the source and the Set is derived from it, not the other way round. A Set carries no
15
+ * order in its type, so ui/activities-menu - which has to scan the submenus in order to find the visible one -
16
+ * had declared its own array of the same six ids, kept in step by discipline alone. One list now.
17
+ */
18
+ export const TITLE_MENU_IDS_ORDERED = [
19
+ 'tm-main', 'tm-alf', 'tm-mat', 'tm-tab', 'tm-fr', 'tm-cen',
20
+ ];
21
+ export const TITLE_MENU_IDS = new Set(TITLE_MENU_IDS_ORDERED);
22
+ export function isTitleMenuId(v) {
23
+ return TITLE_MENU_IDS.has(v);
24
+ }
25
+ /** Pure view-model for showTitleMenu(which). Verbatim port of the old function's branching. */
26
+ export function computeTitleMenuView(which) {
27
+ const hidden = new Map();
28
+ for (const id of TITLE_MENU_IDS)
29
+ hidden.set(id, id !== which);
30
+ return {
31
+ active: which,
32
+ hidden,
33
+ legendHidden: which !== 'tm-main',
34
+ titleBlockDisplay: which === 'tm-main' ? '' : 'none',
35
+ };
36
+ }
37
+ // ---------------------------------------------------------------------------------------------
38
+ // DOM-facing (thin) — requires `document`/injected ctx
39
+ // ---------------------------------------------------------------------------------------------
40
+ export function initTitle(ctx) {
41
+ function show(which) {
42
+ const view = computeTitleMenuView(which);
43
+ for (const [id, hide] of view.hidden) {
44
+ const el = ctx.$('#' + id);
45
+ if (el)
46
+ el.hidden = hide;
47
+ }
48
+ const lg = ctx.$('#title-legend');
49
+ if (lg)
50
+ lg.hidden = view.legendHidden;
51
+ const tb = ctx.$('#title-overlay .title-block');
52
+ if (tb)
53
+ tb.style.display = view.titleBlockDisplay;
54
+ const activeEl = ctx.$('#' + which);
55
+ const b = activeEl && activeEl.querySelector('button');
56
+ if (b)
57
+ b.focus();
58
+ }
59
+ return { show };
60
+ }
@@ -0,0 +1,11 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ /** Estado do modo pessoa surda. Escolha da PESSOA, persistida — não inferência sobre um widget de terceiro. */
3
+ export declare let librasOpen: boolean;
4
+ export declare function setOnLibrasChange(fn: () => void): void;
5
+ export declare function vlibrasSay(text: string): void;
6
+ /** O modo pessoa surda está ligado? Lê o NOSSO estado — nunca mais a geometria do widget. */
7
+ export declare function vlibrasOpen(): boolean;
8
+ /** Liga/desliga o modo pessoa surda. Um toggle de verdade: o estado é nosso, então ele sempre alterna. */
9
+ export declare function toggleLibras(): void;
10
+ /** Mantido para o laço do jogo, que o chama a cada quadro. Já não decide nada — o estado é nosso. */
11
+ export declare function vlTick(): void;
@@ -0,0 +1,87 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // ui/vlibras.ts — MODO PESSOA SURDA (intérprete de Libras) — Estágio 4, Tier 1.
3
+ //
4
+ // DOIS DEFEITOS CONSERTADOS AQUI, e eles têm a mesma raiz.
5
+ //
6
+ // `librasOpen` era deduzido da GEOMETRIA do botão de acesso do VLibras: altura zero ou sem `offsetParent`
7
+ // significava "o painel está aberto". Isso funcionava enquanto o widget renderizava dentro do nosso
8
+ // `<div vw>`. Ele parou — hoje ele anexa `#vlibras-access-wrapper` direto no `<body>` —, e o que sobrou no
9
+ // nosso markup é um div VAZIO, de 747×0. Altura zero. Então o detector respondia "aberto" para sempre:
10
+ //
11
+ // 1. o layout reservava 380px à direita para um intérprete inexistente e empurrava o jogo para fora da
12
+ // tela (medido: canvas em `left: -136`);
13
+ // 2. o toggle não desligava, porque mandava um evento de fechar a um widget que não estava ali.
14
+ //
15
+ // (É a mesma raiz da falha do gate axe-core, consertada em 0657578: o VLibras mudou de lugar e três coisas
16
+ // que o localizavam pelo nosso markup pararam juntas, em silêncio.)
17
+ //
18
+ // O ESTADO AGORA É NOSSO. `librasOpen` é uma escolha da pessoa, persistida, e não a leitura de um retângulo
19
+ // de terceiro. Um modo de acessibilidade cujo estado é inferido da geometria de outra biblioteca é um modo
20
+ // que desliga sozinho quando essa biblioteca muda — e quem paga é quem depende dele.
21
+ //
22
+ // O QUE AINDA NÃO É: o Dev decidiu que o intérprete deve aparecer NA FRENTE da tela quando um áudio toca e
23
+ // sumir depois, sem clicar em nada e sem depender de clicar em textos. O widget do VLibras não faz isso (é um
24
+ // painel encaixado, que traduz o texto do elemento clicado), então essa é a razão de o pilar 2 do ADR-0010
25
+ // prever um motor próprio em zdog. Este arquivo para de empurrar a tela e passa a ter um toggle honesto; o
26
+ // intérprete sob demanda é trabalho à parte.
27
+ import { t } from '../core/i18n.js';
28
+ import * as store from '../platform/storage.js';
29
+ /** Estado do modo pessoa surda. Escolha da PESSOA, persistida — não inferência sobre um widget de terceiro. */
30
+ export let librasOpen = store.getBool('incl_libras', false);
31
+ let _vlOpen = false, _vlNode = null, _vlBusyUntil = 0, _vlNext = null;
32
+ let _onLibrasChange = () => { };
33
+ // Registra o reflow (layout) a rodar quando o painel abre/fecha. Chamado no boot do game.js.
34
+ export function setOnLibrasChange(fn) { _onLibrasChange = fn; }
35
+ // Fala em Libras: manda o texto ao intérprete (só quando o painel está aberto). Fila de 1 — fala nova substitui a pendente.
36
+ export function vlibrasSay(text) {
37
+ if (!_vlOpen || !text)
38
+ return;
39
+ const now = Date.now();
40
+ if (now < _vlBusyUntil) {
41
+ _vlNext = text;
42
+ return;
43
+ }
44
+ _vlBusyUntil = now + 4000;
45
+ if (!_vlNode) {
46
+ _vlNode = document.createElement('span');
47
+ _vlNode.setAttribute('aria-hidden', 'true');
48
+ _vlNode.style.cssText = 'position:fixed;left:0;top:0;width:1px;height:1px;overflow:hidden;opacity:.01;z-index:1;pointer-events:auto';
49
+ document.body.appendChild(_vlNode);
50
+ }
51
+ _vlNode.textContent = text;
52
+ try {
53
+ _vlNode.click();
54
+ }
55
+ catch (e) { /* noop */ }
56
+ setTimeout(() => { const nx = _vlNext; _vlNext = null; _vlBusyUntil = 0; if (nx)
57
+ vlibrasSay(nx); }, 4100);
58
+ }
59
+ const vwBtn = () => document.querySelector('[vw-access-button]');
60
+ /** O modo pessoa surda está ligado? Lê o NOSSO estado — nunca mais a geometria do widget. */
61
+ export function vlibrasOpen() { return librasOpen; }
62
+ /** Liga/desliga o modo pessoa surda. Um toggle de verdade: o estado é nosso, então ele sempre alterna. */
63
+ export function toggleLibras() {
64
+ librasOpen = !librasOpen;
65
+ _vlOpen = librasOpen;
66
+ store.setBool('incl_libras', librasOpen);
67
+ // Tentativa BEST-EFFORT de acordar o widget do VLibras, se ele estiver carregado. Falhar aqui não pode
68
+ // impedir o modo de ligar: o estado é a escolha da pessoa, o widget é só um tradutor possível para ela.
69
+ const b = vwBtn();
70
+ if (librasOpen && b) {
71
+ try {
72
+ b.click();
73
+ }
74
+ catch (e) { /* noop */ }
75
+ }
76
+ else if (!librasOpen) {
77
+ try {
78
+ window.dispatchEvent(new CustomEvent('vp-widget-close'));
79
+ }
80
+ catch (e) { /* noop */ }
81
+ }
82
+ _onLibrasChange();
83
+ if (librasOpen)
84
+ vlibrasSay(t('sr.libras.on'));
85
+ }
86
+ /** Mantido para o laço do jogo, que o chama a cada quadro. Já não decide nada — o estado é nosso. */
87
+ export function vlTick() { _vlOpen = librasOpen; }
@@ -0,0 +1,7 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ export declare let eyeMode: boolean;
3
+ export declare function setEyeMode(on: boolean): void;
4
+ export declare function onGaze(data: unknown): void;
5
+ export declare function startEyeControl(): void;
6
+ export declare function stopEyeControl(): void;
7
+ export declare function loadWebGazer(cb?: () => void): void;