@the-inclusionist/engine 7.0.1 → 8.0.0-rc.2

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 (136) 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/playwrite-ar.woff2 +0 -0
  7. package/app/public/vendor/fonts/playwrite-br.woff2 +0 -0
  8. package/app/public/vendor/fonts/playwrite-ca.woff2 +0 -0
  9. package/app/public/vendor/fonts/playwrite-cl.woff2 +0 -0
  10. package/app/public/vendor/fonts/playwrite-co.woff2 +0 -0
  11. package/app/public/vendor/fonts/playwrite-mx.woff2 +0 -0
  12. package/app/public/vendor/fonts/playwrite-us-modern.woff2 +0 -0
  13. package/app/public/vendor/fonts/playwrite-us-trad.woff2 +0 -0
  14. package/app/public/vendor/fonts/pressstart-400.woff2 +0 -0
  15. package/app/public/vendor/fonts.css +64 -2
  16. package/dist-pkg/boot/create-game.d.ts +107 -4
  17. package/dist-pkg/boot/create-game.js +337 -18
  18. package/dist-pkg/core/constants.d.ts +0 -28
  19. package/dist-pkg/core/constants.js +34 -17
  20. package/dist-pkg/core/contract.d.ts +99 -0
  21. package/dist-pkg/core/contract.js +78 -0
  22. package/dist-pkg/core/entity.d.ts +47 -4
  23. package/dist-pkg/core/layers.d.ts +18 -0
  24. package/dist-pkg/core/layers.js +18 -0
  25. package/dist-pkg/core/rng.js +14 -4
  26. package/dist-pkg/core/route.d.ts +42 -0
  27. package/dist-pkg/core/route.js +158 -0
  28. package/dist-pkg/core/state.d.ts +10 -1
  29. package/dist-pkg/core/state.js +13 -0
  30. package/dist-pkg/educational/adaptive-engine.d.ts +65 -0
  31. package/dist-pkg/educational/adaptive-engine.js +117 -0
  32. package/dist-pkg/educational/segment-bar.d.ts +96 -0
  33. package/dist-pkg/educational/segment-bar.js +89 -0
  34. package/dist-pkg/i18n/en.js +44 -0
  35. package/dist-pkg/i18n/es.js +44 -0
  36. package/dist-pkg/i18n/pt.js +60 -0
  37. package/dist-pkg/input/default-bindings.d.ts +40 -0
  38. package/dist-pkg/input/default-bindings.js +151 -9
  39. package/dist-pkg/input/gamepad.d.ts +33 -12
  40. package/dist-pkg/input/gamepad.js +103 -16
  41. package/dist-pkg/input/keyboard-runtime.d.ts +6 -8
  42. package/dist-pkg/input/keyboard-runtime.js +24 -5
  43. package/dist-pkg/input/keyboard.d.ts +42 -0
  44. package/dist-pkg/input/keyboard.js +87 -17
  45. package/dist-pkg/input/keydown.d.ts +40 -2
  46. package/dist-pkg/input/keydown.js +29 -6
  47. package/dist-pkg/input/latch-edge.d.ts +28 -0
  48. package/dist-pkg/input/latch-edge.js +45 -0
  49. package/dist-pkg/input/latch-scope.d.ts +70 -0
  50. package/dist-pkg/input/latch-scope.js +110 -0
  51. package/dist-pkg/input/latch-store.d.ts +45 -0
  52. package/dist-pkg/input/latch-store.js +74 -0
  53. package/dist-pkg/input/latch-sync.d.ts +47 -0
  54. package/dist-pkg/input/latch-sync.js +43 -0
  55. package/dist-pkg/input/origem-sintetica.d.ts +44 -0
  56. package/dist-pkg/input/origem-sintetica.js +60 -0
  57. package/dist-pkg/input/pad-defaults.d.ts +22 -0
  58. package/dist-pkg/input/pad-defaults.js +34 -0
  59. package/dist-pkg/input/pointer.d.ts +61 -0
  60. package/dist-pkg/input/pointer.js +71 -0
  61. package/dist-pkg/input/state.d.ts +86 -1
  62. package/dist-pkg/input/state.js +128 -1
  63. package/dist-pkg/input/touch-bindings.d.ts +21 -2
  64. package/dist-pkg/input/touch-bindings.js +15 -3
  65. package/dist-pkg/input/touch.js +9 -1
  66. package/dist-pkg/input/transporte-em-uso.d.ts +101 -0
  67. package/dist-pkg/input/transporte-em-uso.js +130 -0
  68. package/dist-pkg/input/transports.d.ts +88 -2
  69. package/dist-pkg/input/transports.js +60 -5
  70. package/dist-pkg/input/vocabulary-migration.d.ts +18 -7
  71. package/dist-pkg/platform/audio-earcons.d.ts +29 -2
  72. package/dist-pkg/platform/audio-earcons.js +10 -0
  73. package/dist-pkg/platform/audio-nav.d.ts +1 -1
  74. package/dist-pkg/platform/audio-sonar.d.ts +149 -9
  75. package/dist-pkg/platform/audio-sonar.js +232 -21
  76. package/dist-pkg/platform/guide-intensity.d.ts +40 -0
  77. package/dist-pkg/platform/guide-intensity.js +73 -0
  78. package/dist-pkg/platform/pesados-catalogo.d.ts +14 -0
  79. package/dist-pkg/platform/pesados-catalogo.js +177 -0
  80. package/dist-pkg/platform/pesados.d.ts +31 -0
  81. package/dist-pkg/platform/pesados.js +75 -0
  82. package/dist-pkg/platform/storage.d.ts +30 -0
  83. package/dist-pkg/platform/storage.js +35 -0
  84. package/dist-pkg/platform/tts.js +16 -1
  85. package/dist-pkg/platform/voice-plan.d.ts +109 -0
  86. package/dist-pkg/platform/voice-plan.js +156 -0
  87. package/dist-pkg/platform/vozes-prontas.d.ts +28 -0
  88. package/dist-pkg/platform/vozes-prontas.js +61 -0
  89. package/dist-pkg/render/draw.d.ts +1 -1
  90. package/dist-pkg/render/draw.js +15 -5
  91. package/dist-pkg/render/viz-axes.d.ts +17 -0
  92. package/dist-pkg/render/viz-axes.js +19 -0
  93. package/dist-pkg/render/viz-setters.d.ts +34 -2
  94. package/dist-pkg/render/viz-setters.js +189 -33
  95. package/dist-pkg/render/wheelchair-sprites.d.ts +11 -3
  96. package/dist-pkg/render/wheelchair-sprites.js +10 -4
  97. package/dist-pkg/ui/dom.js +20 -2
  98. package/dist-pkg/ui/fonts.d.ts +71 -1
  99. package/dist-pkg/ui/fonts.js +111 -8
  100. package/dist-pkg/ui/latch-refusal.d.ts +32 -0
  101. package/dist-pkg/ui/latch-refusal.js +60 -0
  102. package/dist-pkg/ui/layout.d.ts +32 -0
  103. package/dist-pkg/ui/layout.js +64 -1
  104. package/dist-pkg/ui/motion-scene.d.ts +41 -0
  105. package/dist-pkg/ui/motion-scene.js +76 -0
  106. package/dist-pkg/ui/panel-shell.d.ts +53 -0
  107. package/dist-pkg/ui/panel-shell.js +103 -0
  108. package/dist-pkg/ui/pause-icons.d.ts +168 -20
  109. package/dist-pkg/ui/pause-icons.js +315 -45
  110. package/dist-pkg/ui/reach-notice.js +8 -0
  111. package/dist-pkg/ui/settings-audio.d.ts +14 -1
  112. package/dist-pkg/ui/settings-audio.js +43 -3
  113. package/dist-pkg/ui/settings-controls.d.ts +66 -6
  114. package/dist-pkg/ui/settings-controls.js +179 -17
  115. package/dist-pkg/ui/settings-empathy.d.ts +15 -0
  116. package/dist-pkg/ui/settings-empathy.js +2 -0
  117. package/dist-pkg/ui/settings-motion.d.ts +25 -19
  118. package/dist-pkg/ui/settings-motion.js +32 -19
  119. package/dist-pkg/ui/settings-motor.d.ts +101 -3
  120. package/dist-pkg/ui/settings-motor.js +136 -9
  121. package/dist-pkg/ui/settings-typo.d.ts +49 -5
  122. package/dist-pkg/ui/settings-typo.js +74 -23
  123. package/dist-pkg/ui/settings-visual.d.ts +21 -1
  124. package/dist-pkg/ui/settings-visual.js +48 -4
  125. package/dist-pkg/ui/shell.d.ts +13 -3
  126. package/dist-pkg/ui/shell.js +3 -4
  127. package/dist-pkg/ui/simulation-refusal.d.ts +32 -0
  128. package/dist-pkg/ui/simulation-refusal.js +57 -0
  129. package/dist-pkg/ui/visual-axes-panel.d.ts +48 -0
  130. package/dist-pkg/ui/visual-axes-panel.js +95 -0
  131. package/dist-pkg/ui/webcam.js +6 -1
  132. package/docs/CREDITS.md +22 -1
  133. package/docs/LICENSES.md +170 -132
  134. package/package.json +25 -4
  135. package/app/public/vendor/fonts/greatvibes-400.woff2 +0 -0
  136. package/app/public/vendor/fonts/ufcook-700.woff2 +0 -0
@@ -32,38 +32,61 @@
32
32
  //
33
33
  // ========================= DECLINAR NÃO É MENTIR =========================
34
34
  // O quiz recusou o sonar e o pad em vez de inventar tiles e uma caixa de colisão falsos, e essa distinção é o
35
- // que separa um achado de um verde falso. Aqui ela vira TIPO: um jogo sem menu de pausa declara
36
- // `semMenuDePausa: true` em vez de devolver `null` de um getter e torcer. O que se declina fica registrado no
37
- // objeto devolvido, e um consumidor pode ser auditado pelo que recusou.
35
+ // que separa um achado de um verde falso. Aqui ela vira TIPO: um jogo que não tem uma peça declara-o num
36
+ // campo em vez de devolver `null` de um getter e torcer. O que se declina fica registrado no objeto devolvido,
37
+ // e um consumidor pode ser auditado pelo que recusou.
38
+ // ⚠️ E ESTE PARÁGRAFO NÃO NOMEIA NENHUM DOS CAMPOS, o que parece esquisito e é medido: o
39
+ // `tests/declinio-morto` conta MENÇÕES, incluindo as de comentário, e fá-lo de propósito — «falhar para o lado
40
+ // de vivo é a direcção certa deste erro», porque uma acusação falsa desliga um gate. A consequência é que usar
41
+ // um declínio como EXEMPLO em prosa o faz parecer lido, e um campo genuinamente morto deixa de ser acusado.
42
+ // 📌 O primeiro rascunho desta nota fez exactamente isso, e o crivo apanhou-o no mesmo minuto.
38
43
  //
39
44
  // ========================= O QUE ISTO AINDA NÃO FAZ, DITO AQUI E NÃO ESCONDIDO =========================
40
45
  // Não liga render, física, tiles nem o sonar — nada disso é de todo jogo, e o achado 9 mostra que o sonar hoje
41
46
  // exige seis coisas de plataforma. Não substitui o boot do `main.js`, que tem catorze anos de ordem própria.
42
47
  // O que ele cobre é o que o quiz provou ser IDÊNTICO em qualquer jogo: idioma, leitor de tela, mixer, voz,
43
48
  // pilha de diálogos, filtros de daltonismo, teclado remapeável e navegação de menu.
44
- import { initI18n } from '../core/i18n.js';
49
+ import { initI18n, idiomaPronto } from '../core/i18n.js';
50
+ import { entradaDe } from '../input/state.js';
45
51
  import { criarAvisoDeQueda } from '../ui/loop-crash.js';
46
52
  import { initFocusTrap, focaveisNoDom } from '../ui/focus-trap.js';
47
53
  import { mostrarAvisoDeAlcance } from '../ui/reach-notice.js';
48
54
  import { alcance, transportesPadrao } from '../input/transports.js';
49
- import { presetActions } from '../core/actions.js';
55
+ import { presetActions, ACTIONS } from '../core/actions.js';
50
56
  import { t } from '../core/i18n.js';
51
57
  import { srSay, srAlert } from '../core/a11y-sr.js';
58
+ import { initPauseIcons, iconsMarkup } from '../ui/pause-icons.js';
59
+ // O módulo INTEIRO: o on do barramento de eventos, para a barra montada continuar a dizer a verdade.
60
+ import * as state from '../core/state.js';
61
+ import { vlibrasOpen, toggleLibras } from '../ui/vlibras.js';
52
62
  import { conformanceProblems } from '../core/contract.js';
53
63
  import { criarPilha } from '../core/scenes.js';
54
64
  import { createTts } from '../platform/tts.js';
55
- import { ensureAC, catNode, audioOut, soundOn, volume, audioCat, initAudioMixer, tonePan, audioCtx } from '../platform/audio.js';
65
+ import { ensureAC, catNode, audioOut, soundOn, volume, audioCat, initAudioMixer, tonePan, audioCtx, setCatGain } from '../platform/audio.js';
56
66
  import { createAudioSonar } from '../platform/audio-sonar.js';
57
- import { VIZ_BY_KEY } from '../render/viz-modes.js';
67
+ // A raiz é a camada que PODE conhecer os dois eixos: `render/` está abaixo dela, e é dela a tarefa de
68
+ // responder ao `platform/audio-sonar`, que não pode importar daqui sem inverter uma aresta (#104).
69
+ import { ehCego, ehBaixaVisao, PADRAO } from '../render/viz-axes.js';
58
70
  import { OVERLAY_SCOPE_SELECTOR } from '../ui/settings-panel.js';
59
71
  import { LOGICAL_W } from '../core/constants.js';
60
72
  import { initSettingsPanel } from '../ui/settings-panel.js';
61
73
  import { initMenuNav } from '../ui/menu-nav.js';
62
74
  import { initKeyboardRuntime } from '../input/keyboard-runtime.js';
63
- import { kb, initKB } from '../input/keyboard.js';
75
+ import { kb, initKB, registrarMapeamentoDoTeclado } from '../input/keyboard.js';
76
+ import { registrarMapeamentoDoPad } from '../input/pad-defaults.js';
77
+ import { baixarPesados } from '../platform/pesados.js';
64
78
  import { installCvdFilters } from '../render/cvd-matrices.js';
65
79
  /** Os ids que os painéis emprestados exigem do documento. Achado 6: sem eles o painel abre VAZIO, sem erro. */
66
80
  const MARCACAO_EXIGIDA = ['#game-region', '#sr-status', '#sr-alert'];
81
+ /**
82
+ * Onde a engine procura a barra de acessibilidade quando o jogo não declara `host.a11yBarHost`.
83
+ *
84
+ * ⚠️ NÃO ENTROU NA `MARCACAO_EXIGIDA` de propósito, e a diferença é de mensagem e não de rigor. Aquela lista
85
+ * produz «marcação ausente: #x», que é o que se diz de um id que o jogo esqueceu. Aqui o que falta não é um
86
+ * 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
87
+ * frase própria pode explicar O QUE se perde, e é isso que a torna útil a quem a lê pela primeira vez.
88
+ */
89
+ const SELETOR_BARRA_A11Y = '#title-icons';
67
90
  /**
68
91
  * Liga a engine para um jogo declarado.
69
92
  *
@@ -108,6 +131,11 @@ export function createGame(o) {
108
131
  getSoundOn: () => soundOn, getVolume: () => volume, getAudioCat: () => audioCat,
109
132
  carregarVozNeural: o.carregarVozNeural,
110
133
  });
134
+ if (!o.carregarVozNeural && !declines.semVozNeural) {
135
+ problems.push('sem voz neural: declare `carregarVozNeural` (uma linha — ver ADR-0094) ou `declines.semVozNeural`. '
136
+ + 'Sem ela a criança que não lê fica com a voz do sistema, que em Chromebook de escola pode não existir '
137
+ + 'em português');
138
+ }
111
139
  /**
112
140
  * O FILTRO DE VISÃO, aplicado ao MUNDO QUE O JOGO DECLAROU (ADR-0087).
113
141
  *
@@ -147,25 +175,236 @@ export function createGame(o) {
147
175
  const cvdFilters = installCvdFilters(o.host.cvdHost ?? null);
148
176
  if (!cvdFilters)
149
177
  problems.push('sem host de filtros (<svg>): a correção de daltonismo não foi montada');
178
+ // 4c. A BARRA DE ACESSIBILIDADE DA PRIMEIRA TELA. Ver a nota em `EngineHost.a11yBarHost`: cinco dos seis
179
+ // jogos do catálogo não têm nenhuma, e nada o dizia. Isto não a monta — diz que ela falta, que é o
180
+ // passo que tira o silêncio. A frase nomeia a saída, como as outras deste bloco fazem.
181
+ const a11yBar = o.host.a11yBarHost ?? $(SELETOR_BARRA_A11Y);
182
+ if (!a11yBar) {
183
+ 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`);
184
+ }
185
+ /*
186
+ * ⚠️ E AGORA A ENGINE MONTA-A (ADR-0106 §4, etapa 2). Até 2026-09-08 esta raiz só REPORTAVA a ausência, e o
187
+ * registo dizia porquê: «reportar não é oferecer — cinco jogos continuam sem barra até alguém agir na
188
+ * linha». A etapa 1 tirou dos sete campos acidentais a obrigação de virem do jogo, e a 3 deu lista padrão
189
+ * ao menu; com isso o `PauseIconsCtx` deixou de exigir seja o que for que esta raiz não saiba responder.
190
+ *
191
+ * ⚠️ NÃO É `buildQuickBar`, e a diferença tem dono: aquele põe `tabIndex = -1` nos botões porque durante a
192
+ * partida dez paradas de tabulação separam a criança do jogo (ADR-0044 item 7). Na primeira tela não se
193
+ * está a jogar, e tirar os ícones da ordem de tabulação ali seria escondê-los de quem navega por teclado —
194
+ * exactamente a pessoa para quem eles existem.
195
+ */
196
+ /**
197
+ * O LEITOR DO MODO CEGO — e ele tem de ler ONDE O ESCRITOR PADRÃO ESCREVE.
198
+ *
199
+ * 🔴 ESTAS DUAS METADES GANHARAM PADRÃO EM DIAS DIFERENTES E NÃO SE FALAVAM, o que produziu um defeito que
200
+ * nenhum teste podia ver. O escritor recebeu o padrão da engine na etapa 1b do ADR-0106
201
+ * (`ui/pause-icons` → `ctx.setModoCego ?? setModoCegoValue`), que grava no `core/state`. O leitor ficou com
202
+ * o `() => false` que já cá estava — uma CONSTANTE. Num jogo que não injecta `isBlindMode`:
203
+ *
204
+ * 1. a criança carrega no ícone → `setModoCego(!false)` → o modo LIGA de verdade;
205
+ * 2. o reflexo lê `false` → o ícone diz «desligado» e o anúncio diz o mesmo;
206
+ * 3. ela carrega outra vez → `setModoCegoValue(!false)` = `true` OUTRA VEZ → a guarda de igualdade do
207
+ * `core/state` devolve cedo → nada acontece.
208
+ *
209
+ * ⚠️ O modo cego ligava uma vez e NÃO HAVIA COMO DESLIGAR — um jogo que começa a descrever tudo em voz alta
210
+ * e não se cala, sem erro em lado nenhum. Para quem não depende dele, é o jogo a ficar inutilizável.
211
+ *
212
+ * 📌 UMA CONSTANTE E NÃO A EXPRESSÃO REPETIDA NOS DOIS SÍTIOS, porque a repetição É o defeito: duas
213
+ * respostas à mesma pergunta divergem, e foi assim que esta divergiu. `import * as state` dá ligação VIVA,
214
+ * então isto lê o valor de agora e não o do arranque.
215
+ */
216
+ const lerModoCego = o.isBlindMode ?? (() => state.modoCego);
217
+ const pauseIcons = initPauseIcons({
218
+ doc,
219
+ /*
220
+ * A RESPOSTA DO JOGO, lida da declaração (ADR-0115). Sem ela o ícone `altmove` não é montado.
221
+ *
222
+ * ⚠️ LIDA UMA VEZ, NO ARRANQUE, e a razão não é economia — é a criança. O campo é uma FUNÇÃO porque o
223
+ * ADR-0084 diz que um jogo muda de exigência entre fases, mas a COMPOSIÇÃO DA BARRA não pode mudar
224
+ * debaixo da mão de quem está a usá-la: um ícone que aparece e some entre fases é pior do que um que
225
+ * nunca esteve lá, e para quem navega por teclado desloca a ordem de tabulação a meio.
226
+ * 📌 Logo a leitura correcta do contrato é «este jogo segura teclas em ALGUMA fase» — um jogo que segura
227
+ * a pé e nada dentro de um veículo declara `true`, e o registo não disse isto porque a pergunta só
228
+ * aparece quando se monta a barra.
229
+ */
230
+ seguraTeclas: o.declaration.seguraTeclas(),
231
+ getPlayers: () => o.players ?? [],
232
+ getNumPlayers: () => (o.players ?? [null]).length,
233
+ srSay, srAlert,
234
+ // ⚠️ NÃO `instanceof HTMLElement`: esse é um GLOBAL DO NAVEGADOR, e lê-lo onde ele não existe LANÇA —
235
+ // não devolve falso. Escrito assim na etapa 2, fazia o `reflectPauseIcons` rebentar em qualquer ambiente
236
+ // sem DOM. É o mesmo erro de forma do ACHADO 15 no cabeçalho deste ficheiro: alcançar o global por baixo
237
+ // de quem injectou o documento. A pergunta certa é a mesma que o `barraUsavel` faz — sabe ser uma barra?
238
+ getA11yBars: () => (barraUsavel && a11yBar ? [a11yBar] : []),
239
+ getModoCego: lerModoCego,
240
+ getAudioCat: () => audioCat,
241
+ setCatGain,
242
+ /*
243
+ * ⚠️ QUEM RESPONDE PELO APARELHO EM USO É A RAIZ, e é aqui que o autómato do ADR-0109 ganha o primeiro
244
+ * leitor. `input/state.entradaDe(i)` devolve `PADRAO` para quem nunca produziu uma aresta, logo isto
245
+ * nunca é `undefined` e o ícone nunca escreve numa chave torta.
246
+ *
247
+ * 📌 E é a RAIZ que o passa, não o ícone que o importa: `ui/` a ler estado de módulo de `input/` seria
248
+ * uma aresta nova entre camadas para poupar um argumento. A composição é o trabalho deste ficheiro.
249
+ */
250
+ transporteEmUso: (i) => entradaDe(i).emUso,
251
+ reflectTtsPanel: () => { },
252
+ reflectTtsPanelEnabled: false,
253
+ isLibrasOn: vlibrasOpen,
254
+ toggleLibras,
255
+ });
256
+ /*
257
+ * ⚠️ O HOSPEDEIRO TEM DE SABER SER UMA BARRA, e perguntar isso não é zelo: `a11yBarHost` é `Element` no
258
+ * tipo, e um consumidor pode passar um duplo, um nó de outro documento, ou um elemento de um `<svg>`. Sem
259
+ * esta guarda, um objecto sem `addEventListener` derruba o BOOT INTEIRO — e derrubá-lo por causa da barra
260
+ * de acessibilidade seria tirar o jogo a toda a gente para não o dar a ninguém.
261
+ *
262
+ * Não sabendo, é `problems` como qualquer outra lacuna do hospedeiro: o consumidor lê e conserta.
263
+ */
264
+ const barraUsavel = !!a11yBar
265
+ && typeof a11yBar.addEventListener === 'function'
266
+ && 'innerHTML' in a11yBar;
267
+ if (a11yBar && !barraUsavel) {
268
+ problems.push('o elemento da barra de acessibilidade não aceita conteúdo nem clique: os ícones não foram montados');
269
+ }
270
+ if (a11yBar && barraUsavel) {
271
+ a11yBar.innerHTML = iconsMarkup(pauseIcons.iconesMontados);
272
+ a11yBar.addEventListener('click', (e) => {
273
+ const botao = e.target?.closest('.pi-btn');
274
+ if (!botao)
275
+ return;
276
+ pauseIcons.iconAct(botao.dataset.pi ?? '', 0);
277
+ pauseIcons.reflectIconsIn(a11yBar, 0);
278
+ // O anúncio lê o `aria-label` DEPOIS do reflexo, porque é ele que carrega o estado NOVO — anunciar
279
+ // antes diria o estado que a criança acabou de deixar.
280
+ srSay(botao.getAttribute('aria-label') ?? '');
281
+ });
282
+ pauseIcons.reflectIconsIn(a11yBar, 0);
283
+ /*
284
+ * ⚠️ E OUTRA VEZ QUANDO O IDIOMA DO ARRANQUE CHEGAR — sem isto a barra fica no idioma de RECUO.
285
+ *
286
+ * O `initI18n` aplica pt de forma síncrona (para a página nunca ficar em branco) e, se o idioma
287
+ * preferido for outro, PEDE a troca — que é assíncrona, porque en/es são chunks sob demanda. Esta
288
+ * marcação nasce nesse intervalo. 📏 Medido num navegador em 2026-09-08, com `lang="en"`: a barra
289
+ * servia cinco rótulos em inglês e três ainda em português, na mesma linha de ícones.
290
+ *
291
+ * 📌 O `idiomaPronto()` existe exactamente para isto, e o cabeçalho dele já descreve o defeito noutro
292
+ * lugar: «o `applyDom` conserta o markup ESTÁTICO, mas o que o JavaScript monta tinha capturado o
293
+ * texto de pt e ninguém reconstruía». A barra é a instância nova, criada quando a ENGINE passou a
294
+ * montá-la (ADR-0106 etapa 2).
295
+ *
296
+ * ⚠️ É O `idiomaPronto()` E NÃO O EVENTO `i18n:change`, de propósito: o evento é a troca de idioma EM
297
+ * EXECUÇÃO, e o próprio `core/i18n` declara essa pergunta como sendo do Dev («QUANDO a interface se
298
+ * reconstrói ao trocar de idioma em execução»). Isto responde só a pergunta do ARRANQUE, que aquele
299
+ * mesmo comentário diz não ter duas respostas.
300
+ */
301
+ void idiomaPronto().then(() => { pauseIcons.reflectIconsIn(a11yBar, 0); });
302
+ /*
303
+ * ⚠️ E ELA TEM DE CONTINUAR A DIZER A VERDADE quando o estado muda NOUTRO SÍTIO. O modo cego liga-se
304
+ * também pelo painel de áudio e pela simulação de empatia; sem esta assinatura, o ícone da barra ficaria
305
+ * a dizer «desligado» com `aria-pressed=false` depois de a criança o ter ligado — o controlo a mentir o
306
+ * estado, que é a família de defeito que o `reflectTTS` e o `#opt-modocego` já custaram a este projeto.
307
+ *
308
+ * 📌 SÓ O MODO CEGO, e a limitação é medida e não preguiça: dos ícones que esta raiz monta, ele é o ÚNICO
309
+ * cujo estado tem evento (`EventoDoJogo` tem `modoCego`; TTS, Libras, TEA e alternância não emitem nada).
310
+ * Os outros continuam a refletir-se ao clique, que é o caminho por onde hoje eles mudam.
311
+ */
312
+ state.on('modoCego', () => { pauseIcons.reflectIconsIn(a11yBar, 0); });
313
+ }
314
+ // 4d. QUEM ABRIU A PAUSA, quando há mais de um assento — o achado 3 da auditoria do `game-soccer`.
315
+ //
316
+ // ⚠️ O PAINEL DE CONTROLE É PARAMETRIZADO PELO ASSENTO: `render(selPlayer)` desenha as posições DAQUELE
317
+ // esquema, e não há selector de assento — o `#ctrl-players` é uma FRASE, não abas. Quem decide o assento é
318
+ // o consumidor, passando o ator da pausa: «edita o controle de quem abriu o menu».
319
+ //
320
+ // ⚠️ E É AQUI QUE ISTO FICA MUDO. O `setPauseActor` desta raiz é `() => {}` — literal, logo abaixo. Um jogo
321
+ // montado por `createGame` com dois assentos deixa a criança do SEGUNDO sem como remapear, e nada o diz.
322
+ // Não é a mesma coisa que declarar `semAtorDePausa`: essa é uma ausência declarada, e uma ausência
323
+ // declarada é uma escolha. Esta era uma ausência por omissão, que é a forma de defeito do ADR-0106 §2.
324
+ const assentos = (o.players ?? []).length;
325
+ if (assentos > 1 && !declines.semAtorDePausa) {
326
+ problems.push(`declarou ${assentos} jogadores e não registra o ator da pausa: o painel de controle edita sempre o `
327
+ + 'assento 0, então ninguém além do primeiro consegue remapear. Declare `declines.semAtorDePausa` se '
328
+ + 'for de propósito');
329
+ }
330
+ /*
331
+ * 4e. O CARTÃO DE PAUSA DA PRIMEIRA TELA — e isto fecha um LAÇO QUE ESTAVA ABERTO.
332
+ *
333
+ * 📏 MEDIDO EM 2026-09-08, nos seis jogos do catálogo local: `#vp-pause-0` é procurado por esta raiz (o
334
+ * `getPauseMenu` do `initMenuNav`, mais abaixo) e **NENHUM jogo o cria**. `git grep vp-pause` devolve zero
335
+ * em `game-platformer`, `game-soccer`, `pixi-15-puzzle`, `2048`, `whackwhack` e `game-chess`. Ou seja: a
336
+ * engine inventou uma convenção, procurou-a, não a achou, e concluiu em silêncio que nenhum jogo tem menu
337
+ * de pausa — que é a MESMA forma de defeito do ADR-0106 §2, desta vez cometida pela engine contra si mesma.
338
+ *
339
+ * Agora ela cria o que procura — e para TODO jogo, desde o ADR-0120 e outra vez desde o ADR-0122: o
340
+ * declínio saiu, porque a razão de ele existir foi construída fora por este mesmo ADR-0106, e porque a
341
+ * regra é do Dev — a pausa e os ícones de acessibilidade estão em todo jogo, logo são da engine.
342
+ */
343
+ const hospedeiroDaPausa = o.host.pauseHost ?? $('#game-region');
344
+ const pausaUsavel = !!hospedeiroDaPausa && typeof hospedeiroDaPausa.appendChild === 'function';
345
+ if (!pausaUsavel) {
346
+ problems.push('sem sítio para o menu de pausa: declare `host.pauseHost` ou tenha um #game-region que aceite filhos. '
347
+ + 'Sem ele a criança não alcança os ajustes durante a partida — e NÃO há como declinar: desde o '
348
+ + 'ADR-0122 a pausa é da engine em todo jogo, e o que este jogo declara é só ONDE ela cabe');
349
+ }
350
+ if (hospedeiroDaPausa && pausaUsavel) {
351
+ const cartao = pauseIcons.buildScreenPause(0);
352
+ // ⚠️ O ID É O QUE A PRÓPRIA ENGINE PROCURA, logo abaixo, no `getPauseMenu`. Montar sem o pôr deixaria o
353
+ // laço tão aberto como estava — o cartão existiria e a navegação de menu continuaria a não o achar.
354
+ cartao.id = 'vp-pause-0';
355
+ hospedeiroDaPausa.appendChild(cartao);
356
+ }
150
357
  // 4b. NAVEGAÇÃO SONORA. Só o contrato entra: nada de tile, caixa de colisão ou array de moedas.
151
358
  const sonar = createAudioSonar({
152
359
  topology: () => o.declaration.topology(),
153
360
  targetsOf: (i) => o.declaration.targetsOf(i),
154
361
  nameAt: (at) => o.declaration.nameAt(at),
362
+ // Campo 2 + o barramento do mixer: o que o GUIA CONTÍNUO precisa e o bipe não precisava (#84 item 2). O
363
+ // `roleAt` é o que deixa a rota contornar parede; o `catNode`/`audioOut`/`getVolume` são o que põem um
364
+ // grafo PERMANENTE no mesmo cursor de volume que todo o resto do áudio usa.
365
+ roleAt: (at) => o.declaration.roleAt(at),
155
366
  tonePan, srSay, narrate: (texto) => tts.narrate(texto),
156
- VIZ_BY_KEY, getModoCego: o.isBlindMode ?? (() => false), LOGICAL_W,
367
+ catNode, audioOut, getVolume: () => volume,
368
+ // ⚠️ A RESPOSTA, E NÃO A TABELA (#104). O `platform/audio-sonar` recebia o `VIZ_BY_KEY` e atravessava-o
369
+ // com `pl.viz`; ele deixou de saber o que é um modo visual, e quem responde é aqui — a raiz é a única
370
+ // camada que conhece os dois eixos E pode importar de `render/`.
371
+ visaoComprometida: (pl) => {
372
+ const v = pl.visual;
373
+ return !!v && (ehCego(v) || ehBaixaVisao(v));
374
+ },
375
+ getModoCego: lerModoCego, LOGICAL_W,
157
376
  // O jogador DERIVADO do foco: campo 4 respondendo "onde a criança está". Um jogo que não fornece lista
158
377
  // ainda tem sonar, e é isso que faz a pilha de acessibilidade não ser acessório.
159
378
  getPlayers: o.sonarPlayers ?? (() => {
160
379
  const f = o.declaration.focusOf(0);
161
- return f ? [{ i: 0, x: f.at.x, y: f.at.y, viz: 'normal' }] : [];
380
+ return f ? [{ i: 0, x: f.at.x, y: f.at.y, visual: PADRAO }] : [];
162
381
  }),
163
382
  getNumPlayers: () => (o.players ?? [null]).length,
164
383
  getAudioCtx: () => audioCtx, getSoundOn: () => soundOn, getAudioCat: () => audioCat,
165
384
  });
166
385
  // 5. Teclado remapeável — o melhor recorte da base (achado 11): esquema de teclas, sem mundo.
386
+ //
387
+ // ⚠️ O PADRÃO DO JOGO REGISTA-SE ANTES DO `initKB()`, e a ordem é a regra: quem lê o disco já tem de saber
388
+ // qual é a fábrica sobre a qual o dado da criança se sobrepõe (ADR-0115). Registar depois deixaria o
389
+ // primeiro arranque com a fábrica da ENGINE e o segundo com a do jogo — a pior espécie de defeito, porque
390
+ // desaparece quando alguém vai ver.
391
+ // 📌 E o registo aceita `null`, que é o que um jogo sem opinião produz: fica a fábrica da engine.
392
+ registrarMapeamentoDoTeclado(o.declaration.mapeamentoDoTeclado
393
+ ? (jogadores, assento) => o.declaration.mapeamentoDoTeclado(jogadores, assento)
394
+ : null);
395
+ // ⚠️ E O DO CONTROLE REGISTA-SE AQUI AINDA QUE ESTA RAIZ NÃO MONTE GAMEPAD NENHUM. Não é descuido: quem
396
+ // chama `initGamepad` é o cartucho, e é exactamente por isso que o registo não pode viver lá — seria mais
397
+ // um campo que um jogo pode esquecer, e esquecê-lo devolve o mapa da ENGINE a quem declarou outro, calado.
398
+ registrarMapeamentoDoPad(o.declaration.mapeamentoDoPad
399
+ ? (jogadores, assento) => o.declaration.mapeamentoDoPad(jogadores, assento)
400
+ : null);
167
401
  initKB();
168
- const players = o.players ?? [{ ctrl: {} }];
402
+ // ⚠️ O ESQUEMA DE ARRANQUE ALCANÇA NADA, e diz isso com `null` em vez de com um objeto vazio (issue #118).
403
+ // Ele vive um instante — `assignControls()` logo abaixo substitui-o pelo esquema real —, mas enquanto vive
404
+ // é um `KeyScheme` como qualquer outro, e a única forma honesta de um esquema que não alcança nada é
405
+ // catorze ausências declaradas. Um `{}` fazia o tipo mentir sobre estar completo.
406
+ const semAlcance = Object.fromEntries(ACTIONS.map((a) => [a, null]));
407
+ const players = o.players ?? [{ ctrl: semAlcance }];
169
408
  const keyboard = initKeyboardRuntime({
170
409
  getKB: () => kb, getNumPlayers: () => players.length, getPlayers: () => players,
171
410
  });
@@ -174,7 +413,7 @@ export function createGame(o) {
174
413
  const nav = initMenuNav({
175
414
  $, getActiveElement: () => doc.activeElement,
176
415
  topVisibleOverlay: overlays.topVisibleOverlay, closeById: overlays.closeById,
177
- getPauseMenu: declines.semMenuDePausa ? () => null : (i) => $(`#vp-pause-${i}`),
416
+ getPauseMenu: (i) => $(`#vp-pause-${i}`),
178
417
  setPhase: o.setPhase ?? (() => { }),
179
418
  setPauseActor: () => { },
180
419
  srSay,
@@ -182,10 +421,25 @@ export function createGame(o) {
182
421
  // que ele existe se vier desligado (a mesma razão de o modo cego nascer com TTS e sonar).
183
422
  comIndice: o.comIndice ?? (() => true),
184
423
  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 ?? (() => { }),
424
+ /*
425
+ * ⚠️ ESTE PADRÃO ERA `() => false` / `() => {}`, E DESDE HOJE ISSO SERIA UM BURACO QUE EU ABRI. O
426
+ * comentário que estava aqui dizia «um hospedeiro que não tenha barra de acessibilidade responde nunca e
427
+ * nunca chama nada» — verdade até a etapa 2 do ADR-0106, quando esta raiz passou a MONTAR a barra.
428
+ *
429
+ * Com a barra montada e estes dois em no-op, ela existiria e **não se conseguiria navegar por teclado nem
430
+ * por controle**: alcançável só por ponteiro. Para uma criança cega, que navega por teclado, uma barra
431
+ * que ela não alcança é o mesmo que barra nenhuma — e é exactamente o «oferece o caminho e depois
432
+ * recusa-o» que o §5 do ADR-0106 proíbe.
433
+ *
434
+ * A engine responde com a SUA instância, que é a mesma que montou a barra. Quem injecta continua a mandar.
435
+ *
436
+ * ⚠️ E FICA UMA METADE POR LIGAR, dita aqui em vez de descoberta: o `navBar` do `ui/menu-nav` recebe
437
+ * `(i, k)` e não o terceiro argumento `temStart`, que é a borda do botão de pausa — a SEGUNDA saída do
438
+ * modo (ADR-0044 item 7). No cartucho ela chega por outra rota (o encaminhador do gamepad, `main.ts:1470`)
439
+ * que esta raiz ainda não monta. Logo: o direcional navega a barra; sair por START, por enquanto, não.
440
+ */
441
+ naBarraDe: o.naBarraDe ?? ((i) => pauseIcons.naBarraDe(i)),
442
+ navBar: o.navBar ?? ((i, k) => pauseIcons.navBar(i, k)),
189
443
  isCapturing: () => false,
190
444
  closePadWiz: () => { },
191
445
  whichPlayer: (code) => keyboard.whichPlayer(code),
@@ -243,9 +497,30 @@ export function createGame(o) {
243
497
  catch {
244
498
  return true;
245
499
  } },
500
+ /**
501
+ * O RATO (ADR-0112) — e a sonda é `any-pointer` de propósito, não `pointer`.
502
+ *
503
+ * ⚠️ `(pointer:fine)` descreve o ponteiro PRIMÁRIO, então um tablet com rato ligado responde «coarse» e
504
+ * o rato desaparecia — exactamente o aparelho que esta pergunta existe para achar. `any-pointer:fine` diz
505
+ * «ALGUM dos dispositivos apontadores é fino», que é a pergunta certa: para desenhar basta um.
506
+ *
507
+ * ⚠️ ELA ERRA, e a direcção do erro é a que se aceita: um stylus também responde `fine`, e é um ponteiro
508
+ * a sério — logo isso não é erro. O que pode faltar é um rato ligado depois do arranque, e por isso a
509
+ * sonda é uma FUNÇÃO, avaliada a cada pergunta, como as três acima.
510
+ */
511
+ rato: () => { try {
512
+ return win.matchMedia('(any-pointer:fine)').matches;
513
+ }
514
+ catch {
515
+ return false;
516
+ } },
246
517
  };
247
518
  const acoesDoJogo = o.preset ? presetActions(o.preset) : [];
248
- const alcanceAqui = alcance(transportesPadrao(disponibilidade), acoesDoJogo);
519
+ // O segundo eixo entra aqui, e vem do jogo (ADR-0104 §A): quantas posições ele segura ao mesmo tempo.
520
+ // ⚠️ O TERCEIRO EIXO ENTRA AQUI (ADR-0112), e vem do jogo tal como os outros dois. `?? false` e não um
521
+ // padrão inventado: o campo é opcional de propósito — ver a nota nele —, e a ausência significa «este jogo
522
+ // não desenha», que é a resposta certa para a esmagadora maioria dos trezentos.
523
+ const alcanceAqui = alcance(transportesPadrao(disponibilidade), acoesDoJogo, o.declaration.holdsAtOnce(), o.declaration.needsPointer?.() ?? false);
249
524
  // ⚠️ SÓ APARECE QUANDO HÁ O QUE DIZER. Um aviso que aparece sempre deixa de ser lido, e um jogo cujas ações
250
525
  // cabem no toque não tem nada a avisar — que é o caso comum e tem de continuar silencioso.
251
526
  if (acoesDoJogo.length) {
@@ -264,5 +539,49 @@ export function createGame(o) {
264
539
  criar: (tag) => doc.createElement(tag),
265
540
  narrar: (texto) => tts.narrate(texto),
266
541
  });
267
- return { declaration: o.declaration, tts, overlays, nav, keyboard, sonar, aplicarFiltroDeVisao, cenas: criarPilha(), cvdFilters, problems, declines, aoFalhar, alcance: alcanceAqui };
542
+ /*
543
+ * ⚠️ MOSTRAR REFAZ OS ITENS ANTES DE REVELAR, e a ordem é a regra: o §5 do ADR-0106 diz que a criança nunca
544
+ * vê um item que não acciona, e a tabela de acções deste jogo pode ter mudado desde a montagem. Revelar
545
+ * primeiro e refazer depois deixaria um piscar em que ela vê o que não pode usar.
546
+ */
547
+ const pausa = {
548
+ mostrar: (i) => {
549
+ pauseIcons.reflectPauseIcons();
550
+ const cartao = $(`#vp-pause-${i}`);
551
+ if (cartao)
552
+ cartao.hidden = false;
553
+ },
554
+ esconder: (i) => {
555
+ const cartao = $(`#vp-pause-${i}`);
556
+ if (cartao)
557
+ cartao.hidden = true;
558
+ },
559
+ };
560
+ /*
561
+ * AS COISAS PESADAS COMEÇAM A DESCER AQUI, e a linha é deliberadamente a ÚLTIMA coisa do arranque.
562
+ *
563
+ * ⚠️ SEM `await`. O arranque não espera por 241 MB — se esperasse, a primeira tela de uma escola com 3G
564
+ * ficaria em branco durante minutos e a criança concluiria que o jogo não abre. O `catch` vazio é a mesma
565
+ * regra escrita duas vezes: uma falha de rede aqui não pode derrubar um jogo que hoje nem usa a voz.
566
+ *
567
+ * 🔴 E O RELATÓRIO NÃO VAI PARA `problems`, embora a primeira versão o fizesse. Duas razões medidas, e a
568
+ * primeira é a que importa:
569
+ *
570
+ * 1. **CHEGA DEPOIS DE O LEITOR SE IR EMBORA.** `problems` é devolvido na linha abaixo, sincronamente; a
571
+ * descarga é de fundo, logo TODA linha dela entraria num vector que o consumidor já leu. Quem faz
572
+ * `if (motor.problems.length) …` não veria nada, e quem o lesse mais tarde veria uma lista que cresceu
573
+ * depois do arranque. Um relatório que chega depois do leitor não é um relatório — é a forma exacta do
574
+ * `srSay` a escrever onde não havia `#sr-status`.
575
+ * 2. **AFOGAVA O QUE SE PODE RESOLVER.** Sem rede — uma escola sem rede, que é o alvo e não a excepção —
576
+ * são OITO falhas a empurrar para uma lista que o ADR-0106 §2 construiu para dizer o que FALTA NO
577
+ * HOSPEDEIRO. A criança perde a barra de acessibilidade e a linha que o diz fica em nono lugar.
578
+ *
579
+ * 📌 O canal certo é o que a própria função já tem: `aoProgredir`, entregue a quem chama. Um consumidor que
580
+ * queira mostrar «faltam 241 MB» ou «a voz não desceu» tem por onde; a engine não inventa uma superfície.
581
+ */
582
+ if (o.baixarPesados !== false) {
583
+ void baixarPesados({ aoProgredir: o.aoProgredirPesados })
584
+ .catch(() => { });
585
+ }
586
+ return { declaration: o.declaration, pausa, tts, overlays, nav, keyboard, sonar, aplicarFiltroDeVisao, cenas: criarPilha(), cvdFilters, problems, declines, aoFalhar, alcance: alcanceAqui };
268
587
  }
@@ -1,23 +1,5 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-or-later
2
2
  export declare const LOGICAL_W = 320, LOGICAL_H = 180, TILE = 16;
3
- export declare const COIN_TARGET = 10;
4
- export declare const TUNE: {
5
- jumpVel: number;
6
- waterJump: number;
7
- waterJumpRun: number;
8
- waterStrokeFrames: number;
9
- trampBase: number;
10
- trampMax: number;
11
- gravity: number;
12
- hWalk: number;
13
- hRun: number;
14
- climbSpeed: number;
15
- maxFall: number;
16
- waterMaxFall: number;
17
- hTurbo: number;
18
- ultraJumpVel: number;
19
- };
20
- export declare const JUMP_BASE: number;
21
3
  export declare const ANIM: {
22
4
  walkHold: number;
23
5
  runHold: number;
@@ -53,18 +35,8 @@ export type TileType = {
53
35
  export declare const TILE_TYPES: Record<number, TileType>;
54
36
  /** Machuca ao encostar (lava). */
55
37
  export declare const ehPerigo: (t: number) => boolean;
56
- /** Água: nada-se dentro, e o pulo funciona lá. */
57
- export declare const ehAgua: (t: number) => boolean;
58
- /** Escada: sobe e desce. */
59
- export declare const ehEscada: (t: number) => boolean;
60
38
  /** Trampolim: arremessa para cima. */
61
39
  export declare const ehTrampolim: (t: number) => boolean;
62
- /** Portão: barra até a chave abrir. */
63
- export declare const ehPortao: (t: number) => boolean;
64
- /** A chave que abre o portão. */
65
- export declare const ehChave: (t: number) => boolean;
66
- /** Ar de região secreta: o que a escuridão cobre até alguém entrar. */
67
- export declare const ehSecreto: (t: number) => boolean;
68
40
  export declare const TILE_COLOR: {
69
41
  0: string;
70
42
  1: string;
@@ -3,13 +3,22 @@
3
3
  // TUNE é objeto mutável (o painel de debug ajusta propriedades), mas NUNCA reatribuído → o import const
4
4
  // funciona (mutar propriedade é ok; reatribuir é que quebraria). Ver docs/plano-modularizacao.md.
5
5
  export const LOGICAL_W = 320, LOGICAL_H = 180, TILE = 16;
6
- export const COIN_TARGET = 10;
7
- export const TUNE = {
8
- jumpVel: 3.5, waterJump: 3.5, waterJumpRun: 4, waterStrokeFrames: 30,
9
- trampBase: 5, trampMax: 8, gravity: 0.15, hWalk: 2, hRun: 3, climbSpeed: 1.5,
10
- maxFall: 7, waterMaxFall: 3, hTurbo: 4.5, ultraJumpVel: 10, // E12: power-ups (valores do José)
11
- };
12
- export const JUMP_BASE = TUNE.jumpVel * Math.sqrt(8 / 5); // ~4.43 (altura confortável)
6
+ // ⚠️ `COIN_TARGET` e `TUNE` MUDARAM DE CASA EM 2026-09-07 — foram para `game/tuning.ts` do `game-platformer`
7
+ // (issue #63, etapa B, decisão do Dev). Tinham ZERO importadores dentro desta árvore e TODOS do outro lado
8
+ // da fronteira do pacote: não eram código morto, eram a superfície pública que o cartucho consumia.
9
+ //
10
+ // Uma engine que exporta a GRAVIDADE e a META DE MOEDAS está a decidir que todo jogo é uma plataforma de
11
+ // coletar coisas. `TUNE` é gravidade, velocidade de nado e curso do trampolim — a física DAQUELE jogo; um
12
+ // quiz não tem gravidade. `COIN_TARGET` é o objetivo, que é conteúdo.
13
+ //
14
+ // A etapa C (o segundo consumidor) é o que autorizou o corte: catorze achados, e nenhum reclama estes dois
15
+ // para a engine. O `ui/debug-panel` continua a afinar o `TUNE` ao vivo — ele sempre o recebeu por INJEÇÃO
16
+ // (`TUNE: Tune` no ctx), nunca por importação, e é por isso que a mudança não lhe custou nada.
17
+ // ⚠️ `JUMP_BASE` SAIU EM 2026-09-07 (issue #63, etapa B, decisão do Dev). Não tinha um único consumidor:
18
+ // nem na engine, nem no `game-platformer`, nem nos testes — fora UM, que verificava que
19
+ // `JUMP_BASE === jumpVel * sqrt(8/5)`, ou seja **reafirmava a própria definição**. Um teste que não pode
20
+ // falhar por motivo que importe, a manter viva uma constante que ninguém usa. A própria issue #63 já o
21
+ // nomeava no seu «achado solto».
13
22
  // E15: cadência de animação (ticks por quadro) — regulável ao vivo no painel ?debug=true. Como TUNE, é objeto
14
23
  // mutável (o debug ajusta propriedades) mas NUNCA reatribuído → import const funciona. andar 6; correr 8 (~8fps,
15
24
  // pedido do José); idle 20; swim 24; cling 10; escada 8; flavor ~6s.
@@ -46,18 +55,26 @@ export const TILE_TYPES = {
46
55
  const prop = (t, k) => !!TILE_TYPES[t]?.[k];
47
56
  /** Machuca ao encostar (lava). */
48
57
  export const ehPerigo = (t) => prop(t, 'hazard');
49
- /** Água: nada-se dentro, e o pulo funciona lá. */
50
- export const ehAgua = (t) => prop(t, 'water');
51
- /** Escada: sobe e desce. */
52
- export const ehEscada = (t) => prop(t, 'ladder');
53
58
  /** Trampolim: arremessa para cima. */
54
59
  export const ehTrampolim = (t) => prop(t, 'tramp');
55
- /** Portão: barra até a chave abrir. */
56
- export const ehPortao = (t) => prop(t, 'gate');
57
- /** A chave que abre o portão. */
58
- export const ehChave = (t) => prop(t, 'key');
59
- /** Ar de região secreta: o que a escuridão cobre até alguém entrar. */
60
- export const ehSecreto = (t) => prop(t, 'secreto');
60
+ // ⚠️ `ehAgua`, `ehEscada`, `ehPortao` e `ehSecreto` MUDARAM DE CASA EM 2026-09-07 — foram para
61
+ // `game/tile-flags.ts` do `game-platformer` (issue #63, etapa B). Tinham zero importadores aqui dentro.
62
+ //
63
+ // E as duas que ficaram — `ehPerigo` e `ehTrampolim` — ficaram por um motivo MEDIDO, não por simetria: a
64
+ // engine lê-as, e não por geometria. O `isSolidType` de `core/collision` torna perigo e trampolim SÓLIDOS
65
+ // no modo cadeira de rodas, e perigo sólido no modo cego. É uma regra de ACESSIBILIDADE — a criança em
66
+ // cadeira de rodas não cai no fosso — e essa é da engine.
67
+ //
68
+ // A engine precisa de saber que um tile é PERIGO; não precisa de saber que ele é ESCADA.
69
+ //
70
+ // ⚠️ O QUE FICA POR DECIDIR, e está registado na #63: a `TILE_TYPES` acima continua a carregar as bandeiras
71
+ // `water`, `ladder`, `gate`, `key` e `secreto`, que **nenhum módulo desta árvore lê**. Enquanto elas aqui
72
+ // estiverem, o jogo faz perguntas sobre uma tabela que a engine publica. O fim honesto é a tabela mudar de
73
+ // casa e o jogo declarar os papéis pelo `core/contract` (`roleOf`) — que é o mecanismo que já existe para
74
+ // isso. Não é execução: é a fronteira seguinte, e ela é decisão do Dev.
75
+ // ⚠️ `ehChave` SAIU EM 2026-09-07, pelo mesmo motivo e no mesmo passo que a `JUMP_BASE`: zero consumidores
76
+ // em lado nenhum — engine, testes e cartucho. Os outros predicados de tile ficam por enquanto porque o
77
+ // `game-platformer` os importa; estes dois não eram fronteira mal cortada, eram peso morto.
61
78
  export const TILE_COLOR = {
62
79
  0: '#0a0a14', 1: '#241f38', 2: '#6b6480', 3: '#2f6fae', 4: '#8a5a2b', 5: '#34e29b', 6: '#3a3a46',
63
80
  7: '#7fdcff', 8: '#ffd23f', 9: '#ff5b3a', 10: '#9a8a6f', 11: '#ffe06a', 12: '#3a86ff', 13: '#8a5cff', 14: '#ff6fae',