@the-inclusionist/engine 7.0.0 → 8.0.0-rc.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 (116) hide show
  1. package/README.md +5 -3
  2. package/app/css/style.css +22 -5
  3. package/app/public/vendor/fonts/fondamento-400-ext.woff2 +0 -0
  4. package/app/public/vendor/fonts/fondamento-400.woff2 +0 -0
  5. package/app/public/vendor/fonts/opendyslexic-400.woff2 +0 -0
  6. package/app/public/vendor/fonts/pressstart-400.woff2 +0 -0
  7. package/app/public/vendor/fonts.css +43 -2
  8. package/dist-pkg/boot/create-game.d.ts +76 -2
  9. package/dist-pkg/boot/create-game.js +271 -13
  10. package/dist-pkg/core/constants.d.ts +0 -28
  11. package/dist-pkg/core/constants.js +34 -17
  12. package/dist-pkg/core/contract.d.ts +39 -0
  13. package/dist-pkg/core/contract.js +36 -0
  14. package/dist-pkg/core/entity.d.ts +47 -4
  15. package/dist-pkg/core/layers.d.ts +18 -0
  16. package/dist-pkg/core/layers.js +18 -0
  17. package/dist-pkg/core/rng.js +14 -4
  18. package/dist-pkg/core/route.d.ts +42 -0
  19. package/dist-pkg/core/route.js +158 -0
  20. package/dist-pkg/core/state.d.ts +10 -1
  21. package/dist-pkg/core/state.js +13 -0
  22. package/dist-pkg/educational/adaptive-engine.d.ts +65 -0
  23. package/dist-pkg/educational/adaptive-engine.js +117 -0
  24. package/dist-pkg/educational/segment-bar.d.ts +96 -0
  25. package/dist-pkg/educational/segment-bar.js +89 -0
  26. package/dist-pkg/i18n/en.js +33 -0
  27. package/dist-pkg/i18n/es.js +33 -0
  28. package/dist-pkg/i18n/pt.js +46 -0
  29. package/dist-pkg/input/default-bindings.d.ts +40 -0
  30. package/dist-pkg/input/default-bindings.js +143 -9
  31. package/dist-pkg/input/gamepad.d.ts +18 -11
  32. package/dist-pkg/input/gamepad.js +75 -9
  33. package/dist-pkg/input/keyboard-runtime.d.ts +6 -8
  34. package/dist-pkg/input/keyboard-runtime.js +24 -5
  35. package/dist-pkg/input/keyboard.d.ts +10 -0
  36. package/dist-pkg/input/keyboard.js +41 -11
  37. package/dist-pkg/input/keydown.d.ts +27 -2
  38. package/dist-pkg/input/keydown.js +20 -6
  39. package/dist-pkg/input/latch-scope.d.ts +70 -0
  40. package/dist-pkg/input/latch-scope.js +110 -0
  41. package/dist-pkg/input/latch-store.d.ts +45 -0
  42. package/dist-pkg/input/latch-store.js +74 -0
  43. package/dist-pkg/input/origem-sintetica.d.ts +44 -0
  44. package/dist-pkg/input/origem-sintetica.js +60 -0
  45. package/dist-pkg/input/pointer.d.ts +61 -0
  46. package/dist-pkg/input/pointer.js +71 -0
  47. package/dist-pkg/input/state.d.ts +86 -1
  48. package/dist-pkg/input/state.js +128 -1
  49. package/dist-pkg/input/touch-bindings.d.ts +11 -2
  50. package/dist-pkg/input/touch-bindings.js +9 -3
  51. package/dist-pkg/input/touch.js +9 -1
  52. package/dist-pkg/input/transporte-em-uso.d.ts +101 -0
  53. package/dist-pkg/input/transporte-em-uso.js +130 -0
  54. package/dist-pkg/input/transports.d.ts +88 -2
  55. package/dist-pkg/input/transports.js +60 -5
  56. package/dist-pkg/input/vocabulary-migration.d.ts +18 -7
  57. package/dist-pkg/platform/audio-earcons.d.ts +29 -2
  58. package/dist-pkg/platform/audio-earcons.js +10 -0
  59. package/dist-pkg/platform/audio-nav.d.ts +1 -1
  60. package/dist-pkg/platform/audio-sonar.d.ts +149 -9
  61. package/dist-pkg/platform/audio-sonar.js +232 -21
  62. package/dist-pkg/platform/guide-intensity.d.ts +40 -0
  63. package/dist-pkg/platform/guide-intensity.js +73 -0
  64. package/dist-pkg/platform/storage.d.ts +30 -0
  65. package/dist-pkg/platform/storage.js +35 -0
  66. package/dist-pkg/platform/tts.js +16 -1
  67. package/dist-pkg/platform/voice-plan.d.ts +90 -0
  68. package/dist-pkg/platform/voice-plan.js +137 -0
  69. package/dist-pkg/render/draw.d.ts +1 -1
  70. package/dist-pkg/render/draw.js +15 -5
  71. package/dist-pkg/render/viz-axes.d.ts +17 -0
  72. package/dist-pkg/render/viz-axes.js +19 -0
  73. package/dist-pkg/render/viz-setters.d.ts +34 -2
  74. package/dist-pkg/render/viz-setters.js +189 -33
  75. package/dist-pkg/render/wheelchair-sprites.d.ts +11 -3
  76. package/dist-pkg/render/wheelchair-sprites.js +10 -4
  77. package/dist-pkg/ui/dom.js +20 -2
  78. package/dist-pkg/ui/fonts.d.ts +47 -1
  79. package/dist-pkg/ui/fonts.js +47 -9
  80. package/dist-pkg/ui/latch-refusal.d.ts +32 -0
  81. package/dist-pkg/ui/latch-refusal.js +60 -0
  82. package/dist-pkg/ui/layout.d.ts +32 -0
  83. package/dist-pkg/ui/layout.js +64 -1
  84. package/dist-pkg/ui/motion-scene.d.ts +41 -0
  85. package/dist-pkg/ui/motion-scene.js +76 -0
  86. package/dist-pkg/ui/panel-shell.d.ts +53 -0
  87. package/dist-pkg/ui/panel-shell.js +103 -0
  88. package/dist-pkg/ui/pause-icons.d.ts +134 -20
  89. package/dist-pkg/ui/pause-icons.js +307 -45
  90. package/dist-pkg/ui/reach-notice.js +8 -0
  91. package/dist-pkg/ui/settings-audio.d.ts +14 -1
  92. package/dist-pkg/ui/settings-audio.js +43 -3
  93. package/dist-pkg/ui/settings-controls.d.ts +66 -6
  94. package/dist-pkg/ui/settings-controls.js +179 -17
  95. package/dist-pkg/ui/settings-empathy.d.ts +15 -0
  96. package/dist-pkg/ui/settings-empathy.js +2 -0
  97. package/dist-pkg/ui/settings-motion.d.ts +25 -19
  98. package/dist-pkg/ui/settings-motion.js +32 -19
  99. package/dist-pkg/ui/settings-motor.d.ts +81 -3
  100. package/dist-pkg/ui/settings-motor.js +118 -9
  101. package/dist-pkg/ui/settings-typo.d.ts +32 -2
  102. package/dist-pkg/ui/settings-typo.js +64 -18
  103. package/dist-pkg/ui/settings-visual.d.ts +21 -1
  104. package/dist-pkg/ui/settings-visual.js +48 -4
  105. package/dist-pkg/ui/shell.d.ts +13 -3
  106. package/dist-pkg/ui/shell.js +3 -4
  107. package/dist-pkg/ui/simulation-refusal.d.ts +32 -0
  108. package/dist-pkg/ui/simulation-refusal.js +57 -0
  109. package/dist-pkg/ui/visual-axes-panel.d.ts +48 -0
  110. package/dist-pkg/ui/visual-axes-panel.js +95 -0
  111. package/dist-pkg/ui/webcam.js +6 -1
  112. package/docs/CREDITS.md +18 -0
  113. package/docs/LICENSES.md +12 -0
  114. package/package.json +24 -4
  115. package/app/public/vendor/fonts/greatvibes-400.woff2 +0 -0
  116. package/app/public/vendor/fonts/ufcook-700.woff2 +0 -0
package/README.md CHANGED
@@ -70,9 +70,11 @@ npm test # testes Vitest (node + browser via Playwright); npm run test
70
70
  `allow_failure: true` e estavam vermelhos havia semanas. O histórico o guarda; o cabeçalho do `ci.yml`
71
71
  documenta o porte linha a linha.
72
72
 
73
- - **CD** — Cloudflare Pages (plano gratuito). ⚠️ **A conexão git dele apontava para o GitLab, que agora está
74
- arquivado** — o deploy tem de ser reapontado para este repositório antes do próximo push valer publicação.
75
- Não dá para conferir isto de fora do painel da Cloudflare. A cada push na `main`:
73
+ - **CD** — ⚠️ **NÃO HÁ NENHUM**, hoje. Informado pelo Dev em 2026-09-07: **nenhum projeto do Cloudflare Pages
74
+ está conectado a repositório nenhum**. A ressalva anterior — «a conexão apontava para o GitLab, que agora
75
+ está arquivado» — descrevia um deploy desapontado; o estado atual é mais simples e mais grave de confundir:
76
+ **push não é publicação**, e `dist/` só chega a alguém por um passo manual.
77
+ A tabela abaixo fica como a RECEITA de quando houver conexão, e não como descrição do que existe:
76
78
 
77
79
  | Configuração | Valor |
78
80
  |---|---|
package/app/css/style.css CHANGED
@@ -51,6 +51,7 @@
51
51
  --z-menu:30000;
52
52
  --z-menu-max:39999;
53
53
  --z-transition:40000;
54
+ --z-skip-link:41000;
54
55
  --z-loop-crash:45000;
55
56
  --z-debug:90000;
56
57
  }
@@ -178,7 +179,9 @@ html,body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--font)
178
179
  (tolerância de 5px lógicos/lado) é cortado pelo .stage-wrap, e os menus rolam DENTRO dos cards. */
179
180
  body{display:flex;flex-direction:column;height:100vh;height:100dvh;overflow:hidden}
180
181
  .sr-only{position:absolute!important;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
181
- .skip-link{position:absolute;left:8px;top:-60px;background:var(--accent);color:var(--accent-ink);padding:.6em 1em;border-radius:0 0 10px 10px;font-weight:700;z-index:100;transition:top .15s}
182
+ /* O `z-index` ADOTOU O SLOT em 2026-09-07. Era `100`, um literal que estava CERTO e cuja tabela estava
183
+ errada: o `Z` punha o skip-link em CAPTIONS(26000), abaixo dos modais. Ver `SKIP_LINK` em core/layers. */
184
+ .skip-link{position:absolute;left:8px;top:-60px;background:var(--accent);color:var(--accent-ink);padding:.6em 1em;border-radius:0 0 10px 10px;font-weight:700;z-index:var(--z-skip-link);transition:top .15s}
182
185
  .skip-link:focus{top:0}
183
186
  :focus-visible{outline:4px solid var(--focus);outline-offset:2px;border-radius:6px}
184
187
 
@@ -300,10 +303,24 @@ body:not(.dbg) .topbar{display:none} /* título da PÁGINA só em ?debug=true (o
300
303
  navegador, e sem esta linha as duas listas do cartão de pausa apareceriam empilhadas (ADR-0044, item 5). */
301
304
  .pause-menu[hidden]{display:none}
302
305
  /* 44 px NÃO é o mínimo da WCAG — o 2.5.8 pede 24×24, e o cartão já passava nisso com folga. 44 é a
303
- recomendação da Apple (HIG), e este projeto trata piso como piso. `min-height` e não `padding` porque a
304
- altura de antes (35,6 px) não estava declarada em lugar nenhum: ela RESULTAVA da soma de padding, fonte e
305
- entrelinha. Um alvo que ninguém escreveu é um alvo que ninguém defende. */
306
- .pm-btn{background:#1a2740;color:#eaf2f8;border:1px solid #3a4a6a;border-radius:10px;padding:.5rem .8rem;min-height:44px;display:flex;align-items:center;justify-content:center;font:inherit;font-weight:700;cursor:pointer;white-space:nowrap}
306
+ recomendação da Apple (HIG). `min-height` e não `padding` porque a altura de antes (35,6 px) não estava
307
+ declarada em lugar nenhum: ela RESULTAVA da soma de padding, fonte e entrelinha. Um alvo que ninguém
308
+ escreveu é um alvo que ninguém defende.
309
+
310
+ ⚠️ ESTA LINHA DIZIA TAMBÉM «e este projeto trata piso como piso», e essa metade morreu no ADR-0095: o
311
+ piso deixou de ser um número único e passou a ser a régua abaixo. Fica o resto, que continua a valer. */
312
+ /* ⚠️ O ALVO DO ITEM DE MENU SEGUE A RÉGUA DO ADR-0095, e não um literal.
313
+ Era `min-height:44px` cravado. O registro fixou que o alvo é função da ALTURA DA TELA — 24 px (WCAG
314
+ 2.5.8, AA) abaixo de 540, 34 a partir de 540, 44 (2.5.5, AAA) a partir de 720 — porque um alvo grande
315
+ que exige ROLAGEM para ser alcançado pode custar mais dedo do que um menor que está à vista.
316
+
317
+ ⚠️ E ELE USA `--alvo-min` E NÃO `--tap`, que são coisas diferentes: `--tap` é o tamanho PREFERIDO e
318
+ cresce com a escala do canvas (22 × k, nunca menos de 44); `--alvo-min` é o CHÃO por altura. Um botão
319
+ isolado pode ser o preferido; um item de LISTA, que tem de caber inteiro, usa o chão.
320
+
321
+ O `44px` do fallback é o comportamento de antes, para o caso de `layout()` não ter corrido — que era
322
+ exatamente o que acontecia no `quiz.html` até 07/09. */
323
+ .pm-btn{background:#1a2740;color:#eaf2f8;border:1px solid #3a4a6a;border-radius:10px;padding:.5rem .8rem;min-height:var(--alvo-min,44px);display:flex;align-items:center;justify-content:center;font:inherit;font-weight:700;cursor:pointer;white-space:nowrap}
307
324
  .pm-btn:hover,.pm-btn:focus,.pm-btn.pm-sel{background:#2a3a5e;border-color:#ffd23f;outline:2px solid #ffd23f;outline-offset:1px}
308
325
  /* Legenda sim/não: ícone do botão (conforme layout do controle) + a palavra. Sony/Nintendo invertem 0↔1. */
309
326
  .pause-legend{display:flex;gap:1.1rem;justify-content:center;margin:.7rem 0 .1rem;font-size:.82em;color:#c3cfe0}
@@ -80,9 +80,50 @@
80
80
  @font-face{font-family:'Newsreader';font-style:normal;font-weight:400;font-display:swap;src:url('fonts/newsreader-400.woff2') format('woff2');}
81
81
  @font-face{font-family:'Newsreader';font-style:normal;font-weight:700;font-display:swap;src:url('fonts/newsreader-700.woff2') format('woff2');}
82
82
  /* Manuscritas (caligráficas inglesas · blackletter alemãs · alfabetização) */
83
- @font-face{font-family:'Great Vibes';font-style:normal;font-weight:400;font-display:swap;src:url('fonts/greatvibes-400.woff2') format('woff2');}
84
83
  @font-face{font-family:'Pinyon Script';font-style:normal;font-weight:400;font-display:swap;src:url('fonts/pinyon-400.woff2') format('woff2');}
85
- @font-face{font-family:'UnifrakturCook';font-style:normal;font-weight:700;font-display:swap;src:url('fonts/ufcook-700.woff2') format('woff2');}
86
84
  @font-face{font-family:'UnifrakturMaguntia';font-style:normal;font-weight:400;font-display:swap;src:url('fonts/ufmag-400.woff2') format('woff2');}
87
85
  @font-face{font-family:'Comic Neue';font-style:normal;font-weight:400;font-display:swap;src:url('fonts/comicneue-400.woff2') format('woff2');}
88
86
  @font-face{font-family:'Comic Neue';font-style:normal;font-weight:700;font-display:swap;src:url('fonts/comicneue-700.woff2') format('woff2');}
87
+
88
+ /* ===== ARCADE (papel `jogo`: HUD, título, rótulos — NUNCA a face da interface) =====
89
+ ⚠️ SEM `unicode-range`, E É DELIBERADO. O Google Fonts serve esta família em CINCO ficheiros ordenados
90
+ por `unicode-range` **com o latino por ÚLTIMO**, e uma vendorização que pegue no primeiro `src:` traz o
91
+ subconjunto cirílico — um woff2 válido, corretamente declarado, corretamente carregado, e sem uma única
92
+ letra latina.
93
+
94
+ Isso aconteceu de verdade, no `SP-the-inclusionist-whackwhack`: o título saiu na fonte errada durante
95
+ QUATRO commits e todas as verificações disponíveis diziam que estava certo —
96
+ `document.fonts.check()` devolvia `true`, o `status` era `loaded`, o `getComputedStyle` mostrava a
97
+ família certa. As três comparam o NOME da família, uma string escrita duas vezes pela mesma mão; nenhuma
98
+ olha para dentro do ficheiro. E o CSS resolve por CODEPOINT, então uma face sem glifo para `U+0057` é
99
+ ignorada para aquele caractere em silêncio.
100
+
101
+ O ficheiro aqui é o subconjunto LATINO, copiado daquele repositório já verificado, e o gate que o mede
102
+ é `tests/fonte-arcade.browser.test.js` — pela LARGURA DE AVANÇO, que vem do glifo e portanto do ficheiro. */
103
+ @font-face{font-family:'Press Start 2P';font-style:normal;font-weight:400;font-display:swap;src:url('fonts/pressstart-400.woff2') format('woff2');}
104
+
105
+
106
+ /* ===== OpenDyslexic (ACESSIBILIDADE — oferecida, e SEM alegação de eficácia) =====
107
+ SIL OFL 1.1, confirmada na API do repositório oficial (antijingoist/opendyslexic) e não de memória — a
108
+ licença desta face JÁ MUDOU uma vez, e afirmar de cabeça seria afirmar o estado antigo.
109
+
110
+ ⚠️ ELA NÃO PROMETE LEITURA MELHOR, e isso é decisão registada. O `docs/game-design/typography.md` diz por
111
+ extenso: «Ofereça a Dyslexie e a OpenDyslexic apenas como escolha do usuário. A pesquisa não mostra ganho
112
+ de leitura com elas.» A descrição no menu fala do desenho — hastes pesadas na base —, nunca do efeito.
113
+
114
+ ⚠️ E ELA NÃO É SUBCONJUNTO: 100,9 KB do ficheiro compilado do próprio projeto, contra 23 KB da Fondamento
115
+ latina. É a face mais pesada do roster, e entra assim porque o upstream não publica subconjuntos; quem
116
+ quiser cortá-la depois faz por `pyftsubset`, e o gate de precache é quem reclama se o orçamento apertar. */
117
+ @font-face{font-family:'OpenDyslexic';font-style:normal;font-weight:400;font-display:swap;
118
+ src:url('fonts/opendyslexic-400.woff2') format('woff2');}
119
+
120
+ /* ===== Fondamento (CALIGRÁFICA — fora do menu, para dentro das atividades) =====
121
+ SIL OFL 1.1, do Google Fonts, nos dois subconjuntos que ele serve. Mínimo de 20 px fixado pelo Dev na
122
+ emenda do ADR-0012: abaixo disso a face deixa de ser DIFÍCIL e passa a ser ilegível, e as duas coisas são
123
+ diferentes — a dificuldade é o exercício, a ilegibilidade é a criança a desistir. */
124
+ @font-face{font-family:'Fondamento';font-style:normal;font-weight:400;font-display:swap;
125
+ src:url('fonts/fondamento-400.woff2') format('woff2');
126
+ unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;}
127
+ @font-face{font-family:'Fondamento';font-style:normal;font-weight:400;font-display:swap;
128
+ src:url('fonts/fondamento-400-ext.woff2') format('woff2');
129
+ unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;}
@@ -1,6 +1,7 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-or-later
2
2
  import { type Alcance, type Disponibilidade } from '../input/transports.js';
3
3
  import { type ActionPreset } from '../core/actions.js';
4
+ import type { KeyScheme } from '../core/entity.js';
4
5
  import { type GameDeclaration } from '../core/contract.js';
5
6
  import { type SceneStack } from '../core/scenes.js';
6
7
  import { createTts, type CarregarVozNeural } from '../platform/tts.js';
@@ -16,6 +17,41 @@ export interface EngineHost {
16
17
  readonly win: Window;
17
18
  /** Um `<svg>` vazio onde os seis filtros de daltonismo são montados em tempo de execução. */
18
19
  readonly cvdHost?: SVGElement | Element | null;
20
+ /**
21
+ * ONDE A BARRA DE ACESSIBILIDADE ENTRA, na PRIMEIRA tela do jogo.
22
+ *
23
+ * ⚠️ PEDIDO DO DEV, 2026-09-07: «o menu de pausa, o design do menu de pausa e os ícones de acessibilidade
24
+ * que aparecem no jogo desde a primeira tela devem ser oferecidos pela ENGINE e não pela programação do
25
+ * jogo. Todo jogo da engine inclusionist deve ter o mesmo menu de pausa e ícones de acessibilidade desde a
26
+ * primeira.»
27
+ *
28
+ * ⚠️ E A MEDIÇÃO DE 2026-09-08 MOSTROU QUE É UM ACHADO, e não uma arrumação: dos seis jogos do catálogo
29
+ * local, CINCO não têm barra de acessibilidade nenhuma — nem menu de pausa. `pixi-15-puzzle`, `game-chess`,
30
+ * `game-soccer`, `2048` e `whackwhack` não chamam `initPauseIcons` nem montam HUD, e o `createGame` nunca
31
+ * os montou por eles. O comentário do `ui/pause-icons` diz porquê sem o notar: «`initPauseIcons` é chamado
32
+ * pela raiz de composição de CADA jogo» — ou seja, cada jogo tinha de se lembrar, e cinco não se lembraram.
33
+ * A criança que depende do modo cego, do TTS ou do alto contraste abre esses cinco jogos e não tem por onde.
34
+ *
35
+ * Ausente, a engine procura `#title-icons` — o id que o jogo de plataforma usa desde sempre — e, não o
36
+ * achando, diz-o em `problems`.
37
+ *
38
+ * ✅ E DESDE 2026-09-08 ELA TAMBÉM **MONTA** (etapa 2 do ADR-0106 §4). Este parágrafo dizia «dizer não é
39
+ * montar, e a montagem é o passo seguinte»; o passo seguinte aconteceu. O que a destravou foram as etapas
40
+ * 1 e 3: nenhuma delas era sobre montar, e sem as duas o `PauseIconsCtx` exigia sete coisas que esta raiz
41
+ * não sabe responder por um jogo que não conhece.
42
+ *
43
+ * ⚠️ Achando o hospedeiro, a barra é escrita e fiada aqui. Não achando, continua a ser `problems` — porque
44
+ * a engine pode oferecer os ícones, mas não pode adivinhar ONDE eles cabem no desenho de um jogo alheio.
45
+ */
46
+ readonly a11yBarHost?: Element | null;
47
+ /**
48
+ * ONDE O CARTÃO DE PAUSA da primeira tela é pendurado. Ausente, a engine usa `#game-region`.
49
+ *
50
+ * ⚠️ Existe pela mesma razão do `a11yBarHost`: a engine pode OFERECER a pausa, mas não sabe onde ela cabe
51
+ * no desenho de um jogo alheio. Um jogo que não tem pausa nenhuma declara `declines.semMenuDePausa` —
52
+ * declinar é escolha registada, não ter é omissão, e o ADR-0106 §2 é inteiro sobre a diferença.
53
+ */
54
+ readonly pauseHost?: Element | null;
19
55
  }
20
56
  /**
21
57
  * O que este jogo NÃO tem. Declarado, e não deduzido de um getter que devolve null.
@@ -30,6 +66,20 @@ export interface Declinios {
30
66
  readonly semAssistenteDePad?: boolean;
31
67
  /** Sem "ator da pausa" — quem apertou o botão que abriu o menu. */
32
68
  readonly semAtorDePausa?: boolean;
69
+ /**
70
+ * Sem voz neural — este jogo não abre a porta do ADR-0094.
71
+ *
72
+ * ⚠️ EXISTE PORQUE A AUSÊNCIA ESTAVA A SER SILENCIOSA, e a medição de 2026-09-08 diz quanto: dos SEIS jogos
73
+ * do catálogo local, TRÊS declaram `carregarVozNeural` (platformer, 15-puzzle, 2048) e TRÊS não
74
+ * (`game-soccer`, `whackwhack`, `game-chess`). Nos três últimos não há voz neural nenhuma, e nada o dizia.
75
+ *
76
+ * ⚠️ E ISSO CONTRADIZ UMA PROMESSA ESCRITA. O ADR-0065 §3 diz que as vozes «fazem parte da engine, e não do
77
+ * jogo em si» e que um cartucho «não tem de saber que existe»; o ADR-0094 — com razão, e por 135 MB de WASM
78
+ * — passou a exigir UMA LINHA do jogo. As duas coisas podem ser verdade ao mesmo tempo (a engine é dona das
79
+ * VOZES, o jogo nomeia o FORNECEDOR), mas só se quem esquece a linha for avisado. Declinar é escolha; não
80
+ * declarar era omissão.
81
+ */
82
+ readonly semVozNeural?: boolean;
33
83
  }
34
84
  export interface CreateGameOptions {
35
85
  /** Os SETE CAMPOS (core/contract). É o que a pilha de acessibilidade lê, e a única coisa que ela lê. */
@@ -51,9 +101,12 @@ export interface CreateGameOptions {
51
101
  readonly comIndice?: () => boolean;
52
102
  readonly naBarraDe?: (i: number) => boolean;
53
103
  readonly navBar?: (i: number, k: NavKeys) => void;
54
- /** Jogadores para o teclado remapeável. `Pick<ControlledPlayer,'ctrl'>` — esquema de teclas e nada mais. */
104
+ /** Jogadores para o teclado remapeável. `Pick<ControlledPlayer,'ctrl'>` — esquema de teclas e nada mais.
105
+ * ⚠️ `KeyScheme` e não `Record<string, string[]>` desde a #118: era uma CÓPIA ESTRUTURAL do tipo, e uma
106
+ * cópia que ninguém obriga a concordar diverge — é a lição que o próprio `core/entity` abre a dizer, com
107
+ * o `DomQuery` (dezasseis cópias) e o `KeyScheme` (seis) como as contas já pagas. */
55
108
  readonly players?: {
56
- ctrl: Record<string, string[]>;
109
+ ctrl: KeyScheme;
57
110
  }[];
58
111
  /** Troca de fase, para quem tem fases. Ausente = não faz nada (o jogo sem fases não perde nada). */
59
112
  readonly setPhase?: (p: 'title' | 'playing' | 'paused') => void;
@@ -101,6 +154,27 @@ export interface CreateGameOptions {
101
154
  }
102
155
  export interface Engine {
103
156
  readonly declaration: GameDeclaration;
157
+ /**
158
+ * A PAUSA QUE ESTA RAIZ MONTOU — mostrar e esconder, sem o consumidor caçar id nenhum.
159
+ *
160
+ * ⚠️ EXISTE PORQUE MONTAR NÃO É MOSTRAR, e a etapa 2 do ADR-0106 tinha ficado a meio sem que nada o
161
+ * dissesse. O cartão nasce `hidden` (é assim que o `buildScreenPause` o entrega, e tem de ser: a pausa
162
+ * abre-se, não está aberta) e QUEM O REVELA é o `ui/shell`, por fase — que esta raiz **não monta**, de
163
+ * propósito: «não substitui o boot do main.js, que tem catorze anos de ordem própria».
164
+ *
165
+ * ⚠️ Sem estas duas, um jogo montado por `createGame` ficava com um cartão de pausa que NADA mostrava. Não
166
+ * é «o consumidor esqueceu-se»: não havia por onde, a não ser procurar `#vp-pause-0` no documento — que é
167
+ * exactamente o tipo de conhecimento que este ficheiro existe para não exigir.
168
+ *
169
+ * 📌 A engine OFERECE o mecanismo e não toma a fase. Quando abrir a pausa continua a ser do jogo, porque só
170
+ * ele sabe o que é estar a jogar; o que deixa de ser dele é saber COMO.
171
+ */
172
+ readonly pausa: {
173
+ /** Revela o cartão da tela `i` e refaz os itens — o §5 avaliado no instante em que ela abre. */
174
+ readonly mostrar: (i: number) => void;
175
+ /** Esconde-o outra vez. */
176
+ readonly esconder: (i: number) => void;
177
+ };
104
178
  readonly tts: ReturnType<typeof createTts>;
105
179
  readonly overlays: SettingsPanelApi;
106
180
  readonly nav: MenuNavApi;
@@ -41,20 +41,27 @@
41
41
  // exige seis coisas de plataforma. Não substitui o boot do `main.js`, que tem catorze anos de ordem própria.
42
42
  // O que ele cobre é o que o quiz provou ser IDÊNTICO em qualquer jogo: idioma, leitor de tela, mixer, voz,
43
43
  // pilha de diálogos, filtros de daltonismo, teclado remapeável e navegação de menu.
44
- import { initI18n } from '../core/i18n.js';
44
+ import { initI18n, idiomaPronto } from '../core/i18n.js';
45
+ import { entradaDe } from '../input/state.js';
45
46
  import { criarAvisoDeQueda } from '../ui/loop-crash.js';
46
47
  import { initFocusTrap, focaveisNoDom } from '../ui/focus-trap.js';
47
48
  import { mostrarAvisoDeAlcance } from '../ui/reach-notice.js';
48
49
  import { alcance, transportesPadrao } from '../input/transports.js';
49
- import { presetActions } from '../core/actions.js';
50
+ import { presetActions, ACTIONS } from '../core/actions.js';
50
51
  import { t } from '../core/i18n.js';
51
52
  import { srSay, srAlert } from '../core/a11y-sr.js';
53
+ import { initPauseIcons, iconsMarkup } from '../ui/pause-icons.js';
54
+ // O módulo INTEIRO: o on do barramento de eventos, para a barra montada continuar a dizer a verdade.
55
+ import * as state from '../core/state.js';
56
+ import { vlibrasOpen, toggleLibras } from '../ui/vlibras.js';
52
57
  import { conformanceProblems } from '../core/contract.js';
53
58
  import { criarPilha } from '../core/scenes.js';
54
59
  import { createTts } from '../platform/tts.js';
55
- import { ensureAC, catNode, audioOut, soundOn, volume, audioCat, initAudioMixer, tonePan, audioCtx } from '../platform/audio.js';
60
+ import { ensureAC, catNode, audioOut, soundOn, volume, audioCat, initAudioMixer, tonePan, audioCtx, setCatGain } from '../platform/audio.js';
56
61
  import { createAudioSonar } from '../platform/audio-sonar.js';
57
- import { VIZ_BY_KEY } from '../render/viz-modes.js';
62
+ // A raiz é a camada que PODE conhecer os dois eixos: `render/` está abaixo dela, e é dela a tarefa de
63
+ // responder ao `platform/audio-sonar`, que não pode importar daqui sem inverter uma aresta (#104).
64
+ import { ehCego, ehBaixaVisao, PADRAO } from '../render/viz-axes.js';
58
65
  import { OVERLAY_SCOPE_SELECTOR } from '../ui/settings-panel.js';
59
66
  import { LOGICAL_W } from '../core/constants.js';
60
67
  import { initSettingsPanel } from '../ui/settings-panel.js';
@@ -64,6 +71,15 @@ import { kb, initKB } from '../input/keyboard.js';
64
71
  import { installCvdFilters } from '../render/cvd-matrices.js';
65
72
  /** Os ids que os painéis emprestados exigem do documento. Achado 6: sem eles o painel abre VAZIO, sem erro. */
66
73
  const MARCACAO_EXIGIDA = ['#game-region', '#sr-status', '#sr-alert'];
74
+ /**
75
+ * Onde a engine procura a barra de acessibilidade quando o jogo não declara `host.a11yBarHost`.
76
+ *
77
+ * ⚠️ NÃO ENTROU NA `MARCACAO_EXIGIDA` de propósito, e a diferença é de mensagem e não de rigor. Aquela lista
78
+ * produz «marcação ausente: #x», que é o que se diz de um id que o jogo esqueceu. Aqui o que falta não é um
79
+ * id — é a barra inteira, e cinco jogos do catálogo não a têm porque ninguém lhes disse que a deviam ter. A
80
+ * frase própria pode explicar O QUE se perde, e é isso que a torna útil a quem a lê pela primeira vez.
81
+ */
82
+ const SELETOR_BARRA_A11Y = '#title-icons';
67
83
  /**
68
84
  * Liga a engine para um jogo declarado.
69
85
  *
@@ -108,6 +124,11 @@ export function createGame(o) {
108
124
  getSoundOn: () => soundOn, getVolume: () => volume, getAudioCat: () => audioCat,
109
125
  carregarVozNeural: o.carregarVozNeural,
110
126
  });
127
+ if (!o.carregarVozNeural && !declines.semVozNeural) {
128
+ problems.push('sem voz neural: declare `carregarVozNeural` (uma linha — ver ADR-0094) ou `declines.semVozNeural`. '
129
+ + 'Sem ela a criança que não lê fica com a voz do sistema, que em Chromebook de escola pode não existir '
130
+ + 'em português');
131
+ }
111
132
  /**
112
133
  * O FILTRO DE VISÃO, aplicado ao MUNDO QUE O JOGO DECLAROU (ADR-0087).
113
134
  *
@@ -147,25 +168,208 @@ export function createGame(o) {
147
168
  const cvdFilters = installCvdFilters(o.host.cvdHost ?? null);
148
169
  if (!cvdFilters)
149
170
  problems.push('sem host de filtros (<svg>): a correção de daltonismo não foi montada');
171
+ // 4c. A BARRA DE ACESSIBILIDADE DA PRIMEIRA TELA. Ver a nota em `EngineHost.a11yBarHost`: cinco dos seis
172
+ // jogos do catálogo não têm nenhuma, e nada o dizia. Isto não a monta — diz que ela falta, que é o
173
+ // passo que tira o silêncio. A frase nomeia a saída, como as outras deste bloco fazem.
174
+ const a11yBar = o.host.a11yBarHost ?? $(SELETOR_BARRA_A11Y);
175
+ if (!a11yBar) {
176
+ problems.push(`sem barra de acessibilidade na primeira tela: declare \`host.a11yBarHost\` ou ponha um ${SELETOR_BARRA_A11Y} no documento. Sem ela a criança não alcança modo cego, TTS, alto contraste nem Libras antes de começar`);
177
+ }
178
+ /*
179
+ * ⚠️ E AGORA A ENGINE MONTA-A (ADR-0106 §4, etapa 2). Até 2026-09-08 esta raiz só REPORTAVA a ausência, e o
180
+ * registo dizia porquê: «reportar não é oferecer — cinco jogos continuam sem barra até alguém agir na
181
+ * linha». A etapa 1 tirou dos sete campos acidentais a obrigação de virem do jogo, e a 3 deu lista padrão
182
+ * ao menu; com isso o `PauseIconsCtx` deixou de exigir seja o que for que esta raiz não saiba responder.
183
+ *
184
+ * ⚠️ NÃO É `buildQuickBar`, e a diferença tem dono: aquele põe `tabIndex = -1` nos botões porque durante a
185
+ * partida dez paradas de tabulação separam a criança do jogo (ADR-0044 item 7). Na primeira tela não se
186
+ * está a jogar, e tirar os ícones da ordem de tabulação ali seria escondê-los de quem navega por teclado —
187
+ * exactamente a pessoa para quem eles existem.
188
+ */
189
+ /**
190
+ * O LEITOR DO MODO CEGO — e ele tem de ler ONDE O ESCRITOR PADRÃO ESCREVE.
191
+ *
192
+ * 🔴 ESTAS DUAS METADES GANHARAM PADRÃO EM DIAS DIFERENTES E NÃO SE FALAVAM, o que produziu um defeito que
193
+ * nenhum teste podia ver. O escritor recebeu o padrão da engine na etapa 1b do ADR-0106
194
+ * (`ui/pause-icons` → `ctx.setModoCego ?? setModoCegoValue`), que grava no `core/state`. O leitor ficou com
195
+ * o `() => false` que já cá estava — uma CONSTANTE. Num jogo que não injecta `isBlindMode`:
196
+ *
197
+ * 1. a criança carrega no ícone → `setModoCego(!false)` → o modo LIGA de verdade;
198
+ * 2. o reflexo lê `false` → o ícone diz «desligado» e o anúncio diz o mesmo;
199
+ * 3. ela carrega outra vez → `setModoCegoValue(!false)` = `true` OUTRA VEZ → a guarda de igualdade do
200
+ * `core/state` devolve cedo → nada acontece.
201
+ *
202
+ * ⚠️ O modo cego ligava uma vez e NÃO HAVIA COMO DESLIGAR — um jogo que começa a descrever tudo em voz alta
203
+ * e não se cala, sem erro em lado nenhum. Para quem não depende dele, é o jogo a ficar inutilizável.
204
+ *
205
+ * 📌 UMA CONSTANTE E NÃO A EXPRESSÃO REPETIDA NOS DOIS SÍTIOS, porque a repetição É o defeito: duas
206
+ * respostas à mesma pergunta divergem, e foi assim que esta divergiu. `import * as state` dá ligação VIVA,
207
+ * então isto lê o valor de agora e não o do arranque.
208
+ */
209
+ const lerModoCego = o.isBlindMode ?? (() => state.modoCego);
210
+ const pauseIcons = initPauseIcons({
211
+ doc,
212
+ getPlayers: () => o.players ?? [],
213
+ getNumPlayers: () => (o.players ?? [null]).length,
214
+ srSay, srAlert,
215
+ // ⚠️ NÃO `instanceof HTMLElement`: esse é um GLOBAL DO NAVEGADOR, e lê-lo onde ele não existe LANÇA —
216
+ // não devolve falso. Escrito assim na etapa 2, fazia o `reflectPauseIcons` rebentar em qualquer ambiente
217
+ // sem DOM. É o mesmo erro de forma do ACHADO 15 no cabeçalho deste ficheiro: alcançar o global por baixo
218
+ // de quem injectou o documento. A pergunta certa é a mesma que o `barraUsavel` faz — sabe ser uma barra?
219
+ getA11yBars: () => (barraUsavel && a11yBar ? [a11yBar] : []),
220
+ getModoCego: lerModoCego,
221
+ getAudioCat: () => audioCat,
222
+ setCatGain,
223
+ /*
224
+ * ⚠️ QUEM RESPONDE PELO APARELHO EM USO É A RAIZ, e é aqui que o autómato do ADR-0109 ganha o primeiro
225
+ * leitor. `input/state.entradaDe(i)` devolve `PADRAO` para quem nunca produziu uma aresta, logo isto
226
+ * nunca é `undefined` e o ícone nunca escreve numa chave torta.
227
+ *
228
+ * 📌 E é a RAIZ que o passa, não o ícone que o importa: `ui/` a ler estado de módulo de `input/` seria
229
+ * uma aresta nova entre camadas para poupar um argumento. A composição é o trabalho deste ficheiro.
230
+ */
231
+ transporteEmUso: (i) => entradaDe(i).emUso,
232
+ reflectTtsPanel: () => { },
233
+ reflectTtsPanelEnabled: false,
234
+ isLibrasOn: vlibrasOpen,
235
+ toggleLibras,
236
+ });
237
+ /*
238
+ * ⚠️ O HOSPEDEIRO TEM DE SABER SER UMA BARRA, e perguntar isso não é zelo: `a11yBarHost` é `Element` no
239
+ * tipo, e um consumidor pode passar um duplo, um nó de outro documento, ou um elemento de um `<svg>`. Sem
240
+ * esta guarda, um objecto sem `addEventListener` derruba o BOOT INTEIRO — e derrubá-lo por causa da barra
241
+ * de acessibilidade seria tirar o jogo a toda a gente para não o dar a ninguém.
242
+ *
243
+ * Não sabendo, é `problems` como qualquer outra lacuna do hospedeiro: o consumidor lê e conserta.
244
+ */
245
+ const barraUsavel = !!a11yBar
246
+ && typeof a11yBar.addEventListener === 'function'
247
+ && 'innerHTML' in a11yBar;
248
+ if (a11yBar && !barraUsavel) {
249
+ problems.push('o elemento da barra de acessibilidade não aceita conteúdo nem clique: os ícones não foram montados');
250
+ }
251
+ if (a11yBar && barraUsavel) {
252
+ a11yBar.innerHTML = iconsMarkup(pauseIcons.iconesMontados);
253
+ a11yBar.addEventListener('click', (e) => {
254
+ const botao = e.target?.closest('.pi-btn');
255
+ if (!botao)
256
+ return;
257
+ pauseIcons.iconAct(botao.dataset.pi ?? '', 0);
258
+ pauseIcons.reflectIconsIn(a11yBar, 0);
259
+ // O anúncio lê o `aria-label` DEPOIS do reflexo, porque é ele que carrega o estado NOVO — anunciar
260
+ // antes diria o estado que a criança acabou de deixar.
261
+ srSay(botao.getAttribute('aria-label') ?? '');
262
+ });
263
+ pauseIcons.reflectIconsIn(a11yBar, 0);
264
+ /*
265
+ * ⚠️ E OUTRA VEZ QUANDO O IDIOMA DO ARRANQUE CHEGAR — sem isto a barra fica no idioma de RECUO.
266
+ *
267
+ * O `initI18n` aplica pt de forma síncrona (para a página nunca ficar em branco) e, se o idioma
268
+ * preferido for outro, PEDE a troca — que é assíncrona, porque en/es são chunks sob demanda. Esta
269
+ * marcação nasce nesse intervalo. 📏 Medido num navegador em 2026-09-08, com `lang="en"`: a barra
270
+ * servia cinco rótulos em inglês e três ainda em português, na mesma linha de ícones.
271
+ *
272
+ * 📌 O `idiomaPronto()` existe exactamente para isto, e o cabeçalho dele já descreve o defeito noutro
273
+ * lugar: «o `applyDom` conserta o markup ESTÁTICO, mas o que o JavaScript monta tinha capturado o
274
+ * texto de pt e ninguém reconstruía». A barra é a instância nova, criada quando a ENGINE passou a
275
+ * montá-la (ADR-0106 etapa 2).
276
+ *
277
+ * ⚠️ É O `idiomaPronto()` E NÃO O EVENTO `i18n:change`, de propósito: o evento é a troca de idioma EM
278
+ * EXECUÇÃO, e o próprio `core/i18n` declara essa pergunta como sendo do Dev («QUANDO a interface se
279
+ * reconstrói ao trocar de idioma em execução»). Isto responde só a pergunta do ARRANQUE, que aquele
280
+ * mesmo comentário diz não ter duas respostas.
281
+ */
282
+ void idiomaPronto().then(() => { pauseIcons.reflectIconsIn(a11yBar, 0); });
283
+ /*
284
+ * ⚠️ E ELA TEM DE CONTINUAR A DIZER A VERDADE quando o estado muda NOUTRO SÍTIO. O modo cego liga-se
285
+ * também pelo painel de áudio e pela simulação de empatia; sem esta assinatura, o ícone da barra ficaria
286
+ * a dizer «desligado» com `aria-pressed=false` depois de a criança o ter ligado — o controlo a mentir o
287
+ * estado, que é a família de defeito que o `reflectTTS` e o `#opt-modocego` já custaram a este projeto.
288
+ *
289
+ * 📌 SÓ O MODO CEGO, e a limitação é medida e não preguiça: dos ícones que esta raiz monta, ele é o ÚNICO
290
+ * cujo estado tem evento (`EventoDoJogo` tem `modoCego`; TTS, Libras, TEA e alternância não emitem nada).
291
+ * Os outros continuam a refletir-se ao clique, que é o caminho por onde hoje eles mudam.
292
+ */
293
+ state.on('modoCego', () => { pauseIcons.reflectIconsIn(a11yBar, 0); });
294
+ }
295
+ // 4d. QUEM ABRIU A PAUSA, quando há mais de um assento — o achado 3 da auditoria do `game-soccer`.
296
+ //
297
+ // ⚠️ O PAINEL DE CONTROLE É PARAMETRIZADO PELO ASSENTO: `render(selPlayer)` desenha as posições DAQUELE
298
+ // esquema, e não há selector de assento — o `#ctrl-players` é uma FRASE, não abas. Quem decide o assento é
299
+ // o consumidor, passando o ator da pausa: «edita o controle de quem abriu o menu».
300
+ //
301
+ // ⚠️ E É AQUI QUE ISTO FICA MUDO. O `setPauseActor` desta raiz é `() => {}` — literal, logo abaixo. Um jogo
302
+ // montado por `createGame` com dois assentos deixa a criança do SEGUNDO sem como remapear, e nada o diz.
303
+ // Não é a mesma coisa que declarar `semAtorDePausa`: essa é uma ausência declarada, e uma ausência
304
+ // declarada é uma escolha. Esta era uma ausência por omissão, que é a forma de defeito do ADR-0106 §2.
305
+ const assentos = (o.players ?? []).length;
306
+ if (assentos > 1 && !declines.semAtorDePausa) {
307
+ problems.push(`declarou ${assentos} jogadores e não registra o ator da pausa: o painel de controle edita sempre o `
308
+ + 'assento 0, então ninguém além do primeiro consegue remapear. Declare `declines.semAtorDePausa` se '
309
+ + 'for de propósito');
310
+ }
311
+ /*
312
+ * 4e. O CARTÃO DE PAUSA DA PRIMEIRA TELA — e isto fecha um LAÇO QUE ESTAVA ABERTO.
313
+ *
314
+ * 📏 MEDIDO EM 2026-09-08, nos seis jogos do catálogo local: `#vp-pause-0` é procurado por esta raiz (o
315
+ * `getPauseMenu` do `initMenuNav`, mais abaixo) e **NENHUM jogo o cria**. `git grep vp-pause` devolve zero
316
+ * em `game-platformer`, `game-soccer`, `pixi-15-puzzle`, `2048`, `whackwhack` e `game-chess`. Ou seja: a
317
+ * engine inventou uma convenção, procurou-a, não a achou, e concluiu em silêncio que nenhum jogo tem menu
318
+ * de pausa — que é a MESMA forma de defeito do ADR-0106 §2, desta vez cometida pela engine contra si mesma.
319
+ *
320
+ * Agora ela cria o que procura. Quem declina (`semMenuDePausa`) continua sem nada e sem acusação — declinar
321
+ * é escolha; não ter é omissão.
322
+ */
323
+ const hospedeiroDaPausa = declines.semMenuDePausa ? null : (o.host.pauseHost ?? $('#game-region'));
324
+ const pausaUsavel = !!hospedeiroDaPausa && typeof hospedeiroDaPausa.appendChild === 'function';
325
+ if (!declines.semMenuDePausa && !pausaUsavel) {
326
+ problems.push('sem sítio para o menu de pausa: declare `host.pauseHost` ou tenha um #game-region que aceite filhos. '
327
+ + 'Sem ele a criança não alcança os ajustes durante a partida, e `declines.semMenuDePausa` é como se diz '
328
+ + 'que isso é de propósito');
329
+ }
330
+ if (hospedeiroDaPausa && pausaUsavel) {
331
+ const cartao = pauseIcons.buildScreenPause(0);
332
+ // ⚠️ O ID É O QUE A PRÓPRIA ENGINE PROCURA, logo abaixo, no `getPauseMenu`. Montar sem o pôr deixaria o
333
+ // laço tão aberto como estava — o cartão existiria e a navegação de menu continuaria a não o achar.
334
+ cartao.id = 'vp-pause-0';
335
+ hospedeiroDaPausa.appendChild(cartao);
336
+ }
150
337
  // 4b. NAVEGAÇÃO SONORA. Só o contrato entra: nada de tile, caixa de colisão ou array de moedas.
151
338
  const sonar = createAudioSonar({
152
339
  topology: () => o.declaration.topology(),
153
340
  targetsOf: (i) => o.declaration.targetsOf(i),
154
341
  nameAt: (at) => o.declaration.nameAt(at),
342
+ // Campo 2 + o barramento do mixer: o que o GUIA CONTÍNUO precisa e o bipe não precisava (#84 item 2). O
343
+ // `roleAt` é o que deixa a rota contornar parede; o `catNode`/`audioOut`/`getVolume` são o que põem um
344
+ // grafo PERMANENTE no mesmo cursor de volume que todo o resto do áudio usa.
345
+ roleAt: (at) => o.declaration.roleAt(at),
155
346
  tonePan, srSay, narrate: (texto) => tts.narrate(texto),
156
- VIZ_BY_KEY, getModoCego: o.isBlindMode ?? (() => false), LOGICAL_W,
347
+ catNode, audioOut, getVolume: () => volume,
348
+ // ⚠️ A RESPOSTA, E NÃO A TABELA (#104). O `platform/audio-sonar` recebia o `VIZ_BY_KEY` e atravessava-o
349
+ // com `pl.viz`; ele deixou de saber o que é um modo visual, e quem responde é aqui — a raiz é a única
350
+ // camada que conhece os dois eixos E pode importar de `render/`.
351
+ visaoComprometida: (pl) => {
352
+ const v = pl.visual;
353
+ return !!v && (ehCego(v) || ehBaixaVisao(v));
354
+ },
355
+ getModoCego: lerModoCego, LOGICAL_W,
157
356
  // O jogador DERIVADO do foco: campo 4 respondendo "onde a criança está". Um jogo que não fornece lista
158
357
  // ainda tem sonar, e é isso que faz a pilha de acessibilidade não ser acessório.
159
358
  getPlayers: o.sonarPlayers ?? (() => {
160
359
  const f = o.declaration.focusOf(0);
161
- return f ? [{ i: 0, x: f.at.x, y: f.at.y, viz: 'normal' }] : [];
360
+ return f ? [{ i: 0, x: f.at.x, y: f.at.y, visual: PADRAO }] : [];
162
361
  }),
163
362
  getNumPlayers: () => (o.players ?? [null]).length,
164
363
  getAudioCtx: () => audioCtx, getSoundOn: () => soundOn, getAudioCat: () => audioCat,
165
364
  });
166
365
  // 5. Teclado remapeável — o melhor recorte da base (achado 11): esquema de teclas, sem mundo.
167
366
  initKB();
168
- const players = o.players ?? [{ ctrl: {} }];
367
+ // ⚠️ O ESQUEMA DE ARRANQUE ALCANÇA NADA, e diz isso com `null` em vez de com um objeto vazio (issue #118).
368
+ // Ele vive um instante — `assignControls()` logo abaixo substitui-o pelo esquema real —, mas enquanto vive
369
+ // é um `KeyScheme` como qualquer outro, e a única forma honesta de um esquema que não alcança nada é
370
+ // catorze ausências declaradas. Um `{}` fazia o tipo mentir sobre estar completo.
371
+ const semAlcance = Object.fromEntries(ACTIONS.map((a) => [a, null]));
372
+ const players = o.players ?? [{ ctrl: semAlcance }];
169
373
  const keyboard = initKeyboardRuntime({
170
374
  getKB: () => kb, getNumPlayers: () => players.length, getPlayers: () => players,
171
375
  });
@@ -182,10 +386,25 @@ export function createGame(o) {
182
386
  // que ele existe se vier desligado (a mesma razão de o modo cego nascer com TTS e sonar).
183
387
  comIndice: o.comIndice ?? (() => true),
184
388
  isNavigable: o.isNavigable ?? (() => true),
185
- // Um hospedeiro que nao tenha barra de acessibilidade responde "nunca" e nunca chama nada — o modo e'
186
- // opcional para o consumidor, obrigatorio para este jogo.
187
- naBarraDe: o.naBarraDe ?? (() => false),
188
- navBar: o.navBar ?? (() => { }),
389
+ /*
390
+ * ⚠️ ESTE PADRÃO ERA `() => false` / `() => {}`, E DESDE HOJE ISSO SERIA UM BURACO QUE EU ABRI. O
391
+ * comentário que estava aqui dizia «um hospedeiro que não tenha barra de acessibilidade responde nunca e
392
+ * nunca chama nada» — verdade até a etapa 2 do ADR-0106, quando esta raiz passou a MONTAR a barra.
393
+ *
394
+ * Com a barra montada e estes dois em no-op, ela existiria e **não se conseguiria navegar por teclado nem
395
+ * por controle**: alcançável só por ponteiro. Para uma criança cega, que navega por teclado, uma barra
396
+ * que ela não alcança é o mesmo que barra nenhuma — e é exactamente o «oferece o caminho e depois
397
+ * recusa-o» que o §5 do ADR-0106 proíbe.
398
+ *
399
+ * A engine responde com a SUA instância, que é a mesma que montou a barra. Quem injecta continua a mandar.
400
+ *
401
+ * ⚠️ E FICA UMA METADE POR LIGAR, dita aqui em vez de descoberta: o `navBar` do `ui/menu-nav` recebe
402
+ * `(i, k)` e não o terceiro argumento `temStart`, que é a borda do botão de pausa — a SEGUNDA saída do
403
+ * modo (ADR-0044 item 7). No cartucho ela chega por outra rota (o encaminhador do gamepad, `main.ts:1470`)
404
+ * que esta raiz ainda não monta. Logo: o direcional navega a barra; sair por START, por enquanto, não.
405
+ */
406
+ naBarraDe: o.naBarraDe ?? ((i) => pauseIcons.naBarraDe(i)),
407
+ navBar: o.navBar ?? ((i, k) => pauseIcons.navBar(i, k)),
189
408
  isCapturing: () => false,
190
409
  closePadWiz: () => { },
191
410
  whichPlayer: (code) => keyboard.whichPlayer(code),
@@ -243,9 +462,30 @@ export function createGame(o) {
243
462
  catch {
244
463
  return true;
245
464
  } },
465
+ /**
466
+ * O RATO (ADR-0112) — e a sonda é `any-pointer` de propósito, não `pointer`.
467
+ *
468
+ * ⚠️ `(pointer:fine)` descreve o ponteiro PRIMÁRIO, então um tablet com rato ligado responde «coarse» e
469
+ * o rato desaparecia — exactamente o aparelho que esta pergunta existe para achar. `any-pointer:fine` diz
470
+ * «ALGUM dos dispositivos apontadores é fino», que é a pergunta certa: para desenhar basta um.
471
+ *
472
+ * ⚠️ ELA ERRA, e a direcção do erro é a que se aceita: um stylus também responde `fine`, e é um ponteiro
473
+ * a sério — logo isso não é erro. O que pode faltar é um rato ligado depois do arranque, e por isso a
474
+ * sonda é uma FUNÇÃO, avaliada a cada pergunta, como as três acima.
475
+ */
476
+ rato: () => { try {
477
+ return win.matchMedia('(any-pointer:fine)').matches;
478
+ }
479
+ catch {
480
+ return false;
481
+ } },
246
482
  };
247
483
  const acoesDoJogo = o.preset ? presetActions(o.preset) : [];
248
- const alcanceAqui = alcance(transportesPadrao(disponibilidade), acoesDoJogo);
484
+ // O segundo eixo entra aqui, e vem do jogo (ADR-0104 §A): quantas posições ele segura ao mesmo tempo.
485
+ // ⚠️ O TERCEIRO EIXO ENTRA AQUI (ADR-0112), e vem do jogo tal como os outros dois. `?? false` e não um
486
+ // padrão inventado: o campo é opcional de propósito — ver a nota nele —, e a ausência significa «este jogo
487
+ // não desenha», que é a resposta certa para a esmagadora maioria dos trezentos.
488
+ const alcanceAqui = alcance(transportesPadrao(disponibilidade), acoesDoJogo, o.declaration.holdsAtOnce(), o.declaration.needsPointer?.() ?? false);
249
489
  // ⚠️ SÓ APARECE QUANDO HÁ O QUE DIZER. Um aviso que aparece sempre deixa de ser lido, e um jogo cujas ações
250
490
  // cabem no toque não tem nada a avisar — que é o caso comum e tem de continuar silencioso.
251
491
  if (acoesDoJogo.length) {
@@ -264,5 +504,23 @@ export function createGame(o) {
264
504
  criar: (tag) => doc.createElement(tag),
265
505
  narrar: (texto) => tts.narrate(texto),
266
506
  });
267
- return { declaration: o.declaration, tts, overlays, nav, keyboard, sonar, aplicarFiltroDeVisao, cenas: criarPilha(), cvdFilters, problems, declines, aoFalhar, alcance: alcanceAqui };
507
+ /*
508
+ * ⚠️ MOSTRAR REFAZ OS ITENS ANTES DE REVELAR, e a ordem é a regra: o §5 do ADR-0106 diz que a criança nunca
509
+ * vê um item que não acciona, e a tabela de acções deste jogo pode ter mudado desde a montagem. Revelar
510
+ * primeiro e refazer depois deixaria um piscar em que ela vê o que não pode usar.
511
+ */
512
+ const pausa = {
513
+ mostrar: (i) => {
514
+ pauseIcons.reflectPauseIcons();
515
+ const cartao = $(`#vp-pause-${i}`);
516
+ if (cartao)
517
+ cartao.hidden = false;
518
+ },
519
+ esconder: (i) => {
520
+ const cartao = $(`#vp-pause-${i}`);
521
+ if (cartao)
522
+ cartao.hidden = true;
523
+ },
524
+ };
525
+ return { declaration: o.declaration, pausa, tts, overlays, nav, keyboard, sonar, aplicarFiltroDeVisao, cenas: criarPilha(), cvdFilters, problems, declines, aoFalhar, alcance: alcanceAqui };
268
526
  }