@the-inclusionist/engine 8.0.0-rc.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 (50) hide show
  1. package/app/public/vendor/fonts/playwrite-ar.woff2 +0 -0
  2. package/app/public/vendor/fonts/playwrite-br.woff2 +0 -0
  3. package/app/public/vendor/fonts/playwrite-ca.woff2 +0 -0
  4. package/app/public/vendor/fonts/playwrite-cl.woff2 +0 -0
  5. package/app/public/vendor/fonts/playwrite-co.woff2 +0 -0
  6. package/app/public/vendor/fonts/playwrite-mx.woff2 +0 -0
  7. package/app/public/vendor/fonts/playwrite-us-modern.woff2 +0 -0
  8. package/app/public/vendor/fonts/playwrite-us-trad.woff2 +0 -0
  9. package/app/public/vendor/fonts.css +21 -0
  10. package/dist-pkg/boot/create-game.d.ts +33 -4
  11. package/dist-pkg/boot/create-game.js +72 -11
  12. package/dist-pkg/core/contract.d.ts +60 -0
  13. package/dist-pkg/core/contract.js +42 -0
  14. package/dist-pkg/i18n/en.js +11 -0
  15. package/dist-pkg/i18n/es.js +11 -0
  16. package/dist-pkg/i18n/pt.js +14 -0
  17. package/dist-pkg/input/default-bindings.js +20 -12
  18. package/dist-pkg/input/gamepad.d.ts +15 -1
  19. package/dist-pkg/input/gamepad.js +30 -9
  20. package/dist-pkg/input/keyboard.d.ts +32 -0
  21. package/dist-pkg/input/keyboard.js +46 -6
  22. package/dist-pkg/input/keydown.d.ts +13 -0
  23. package/dist-pkg/input/keydown.js +9 -0
  24. package/dist-pkg/input/latch-edge.d.ts +28 -0
  25. package/dist-pkg/input/latch-edge.js +45 -0
  26. package/dist-pkg/input/latch-sync.d.ts +47 -0
  27. package/dist-pkg/input/latch-sync.js +43 -0
  28. package/dist-pkg/input/pad-defaults.d.ts +22 -0
  29. package/dist-pkg/input/pad-defaults.js +34 -0
  30. package/dist-pkg/input/touch-bindings.d.ts +10 -0
  31. package/dist-pkg/input/touch-bindings.js +6 -0
  32. package/dist-pkg/platform/pesados-catalogo.d.ts +14 -0
  33. package/dist-pkg/platform/pesados-catalogo.js +177 -0
  34. package/dist-pkg/platform/pesados.d.ts +31 -0
  35. package/dist-pkg/platform/pesados.js +75 -0
  36. package/dist-pkg/platform/voice-plan.d.ts +20 -1
  37. package/dist-pkg/platform/voice-plan.js +20 -1
  38. package/dist-pkg/platform/vozes-prontas.d.ts +28 -0
  39. package/dist-pkg/platform/vozes-prontas.js +61 -0
  40. package/dist-pkg/ui/fonts.d.ts +24 -0
  41. package/dist-pkg/ui/fonts.js +66 -1
  42. package/dist-pkg/ui/pause-icons.d.ts +36 -2
  43. package/dist-pkg/ui/pause-icons.js +9 -1
  44. package/dist-pkg/ui/settings-motor.d.ts +21 -1
  45. package/dist-pkg/ui/settings-motor.js +22 -4
  46. package/dist-pkg/ui/settings-typo.d.ts +18 -4
  47. package/dist-pkg/ui/settings-typo.js +16 -11
  48. package/docs/CREDITS.md +15 -12
  49. package/docs/LICENSES.md +170 -144
  50. package/package.json +2 -1
@@ -127,3 +127,24 @@
127
127
  @font-face{font-family:'Fondamento';font-style:normal;font-weight:400;font-display:swap;
128
128
  src:url('fonts/fondamento-400-ext.woff2') format('woff2');
129
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;}
130
+
131
+ /* ===== Playwrite — as OITO das Américas (ADR-0108 §2, issue #87 item 3) =====
132
+ * A mão que cada país ensina a escrever. Os EUA entram com AS DUAS que ensinam — escolher uma seria
133
+ * escolher pela criança. São variáveis no peso (100–400) e cada família é UM ficheiro: a Google não as
134
+ * corta por unicode-range, então não há metade `-ext` para carregar depois. SIL OFL 1.1, como as outras. */
135
+ @font-face{font-family:'Playwrite BR';font-style:normal;font-weight:100 400;font-display:swap;
136
+ src:url('fonts/playwrite-br.woff2') format('woff2');}
137
+ @font-face{font-family:'Playwrite US Trad';font-style:normal;font-weight:100 400;font-display:swap;
138
+ src:url('fonts/playwrite-us-trad.woff2') format('woff2');}
139
+ @font-face{font-family:'Playwrite US Modern';font-style:normal;font-weight:100 400;font-display:swap;
140
+ src:url('fonts/playwrite-us-modern.woff2') format('woff2');}
141
+ @font-face{font-family:'Playwrite CA';font-style:normal;font-weight:100 400;font-display:swap;
142
+ src:url('fonts/playwrite-ca.woff2') format('woff2');}
143
+ @font-face{font-family:'Playwrite MX';font-style:normal;font-weight:100 400;font-display:swap;
144
+ src:url('fonts/playwrite-mx.woff2') format('woff2');}
145
+ @font-face{font-family:'Playwrite AR';font-style:normal;font-weight:100 400;font-display:swap;
146
+ src:url('fonts/playwrite-ar.woff2') format('woff2');}
147
+ @font-face{font-family:'Playwrite CL';font-style:normal;font-weight:100 400;font-display:swap;
148
+ src:url('fonts/playwrite-cl.woff2') format('woff2');}
149
+ @font-face{font-family:'Playwrite CO';font-style:normal;font-weight:100 400;font-display:swap;
150
+ src:url('fonts/playwrite-co.woff2') format('woff2');}
@@ -11,6 +11,7 @@ import { type SettingsPanelApi } from '../ui/settings-panel.js';
11
11
  import { type MenuNavApi } from '../ui/menu-nav.js';
12
12
  import type { NavKeys } from '../input/edges.js';
13
13
  import { type KeyboardRuntime } from '../input/keyboard-runtime.js';
14
+ import { type RelatorioPesado } from '../platform/pesados.js';
14
15
  /** O que o jogo empresta do documento. Tudo opcional menos `doc`/`win`: o que faltar vira `problems`. */
15
16
  export interface EngineHost {
16
17
  readonly doc: Document;
@@ -48,8 +49,9 @@ export interface EngineHost {
48
49
  * ONDE O CARTÃO DE PAUSA da primeira tela é pendurado. Ausente, a engine usa `#game-region`.
49
50
  *
50
51
  * ⚠️ 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.
52
+ * no desenho de um jogo alheio. 📌 E desde o ADR-0120 ONDE já não é SE: a pausa deixou de ser declinável, e
53
+ * o que sobra deste campo é o lugar. Um hospedeiro que não aceite filhos vira linha de `problems`, que é a
54
+ * diferença entre a engine não saber e a engine calar-se.
53
55
  */
54
56
  readonly pauseHost?: Element | null;
55
57
  }
@@ -60,8 +62,6 @@ export interface EngineHost {
60
62
  * "pausado" para navegar os próprios menus, porque a engine não tinha por onde ouvir "eu não tenho fases".
61
63
  */
62
64
  export interface Declinios {
63
- /** Sem menu de pausa por tela (um quiz não tem). */
64
- readonly semMenuDePausa?: boolean;
65
65
  /** Sem assistente de mapeamento de controle. */
66
66
  readonly semAssistenteDePad?: boolean;
67
67
  /** Sem "ator da pausa" — quem apertou o botão que abriu o menu. */
@@ -144,6 +144,35 @@ export interface CreateGameOptions {
144
144
  * painel de áudio deixa de OFERECER o motor neural, em vez de o oferecer e nunca o carregar.
145
145
  */
146
146
  readonly carregarVozNeural?: CarregarVozNeural;
147
+ /**
148
+ * BAIXAR AS COISAS PESADAS NO PRIMEIRO CARREGAMENTO? Padrão **sim** (ADR-0110 (b), ADR-0116, ADR-0119).
149
+ *
150
+ * As quatro vozes neurais são ~241 MB e descem em SEGUNDO PLANO, uma de cada vez, sem bloquear o jogo: a
151
+ * criança joga enquanto elas chegam, e o que não pode acontecer é ela voltar no segundo dia, sem rede, e
152
+ * descobrir que a voz nunca foi buscada. O pilar 8 é «primeiro dia ONLINE, depois offline-first», e o
153
+ * ADR-0116 tirou a contradição que travava isto — instalar já é um acto de rede.
154
+ *
155
+ * ⚠️ PÔR `false` É PARA QUEM TEM RAZÃO PARA O FAZER, e a razão que já existe é um TESTE: um caso que monte
156
+ * o arranque num navegador de verdade não pode disparar 241 MB contra o Hugging Face. Um jogo em produção
157
+ * que o desligue está a decidir que a criança dele fica sem voz neural offline.
158
+ *
159
+ * 📌 E o ADR-0117 diz que quem devia pagar isto uma vez é a PLATAFORMA, não cada cartucho — a Cache Storage
160
+ * é particionada por origem, e num site só os 241 MB descem uma vez para todos os jogos. Enquanto a
161
+ * plataforma não os pede, é o jogo que os pede: melhor descer duas vezes do que nunca.
162
+ */
163
+ readonly baixarPesados?: boolean;
164
+ /**
165
+ * O QUE ACONTECEU COM CADA COISA PESADA, à medida que acontece. Ausente = ninguém está a ver.
166
+ *
167
+ * ⚠️ É AQUI E NÃO EM `problems` porque a descarga é de FUNDO: `problems` é devolvido sincronamente pelo
168
+ * `createGame`, e uma linha que chegue depois disso entra num vector que o leitor já leu. O ADR-0110 pede
169
+ * que uma busca falhada seja REPORTADA — reportar é ter um canal que existe quando a notícia chega, e não
170
+ * empurrar para uma lista que já foi entregue.
171
+ *
172
+ * 📌 A engine não inventa superfície nenhuma com isto: quem sabe onde cabe «faltam 241 MB» na tela de um
173
+ * jogo é o jogo. `pesoPorBaixar(relatorio)` dá o número para a frase.
174
+ */
175
+ readonly aoProgredirPesados?: (r: RelatorioPesado) => void;
147
176
  /**
148
177
  * Como se descobre que cada transporte está aqui. Ausente = a engine pergunta ao aparelho.
149
178
  *
@@ -32,9 +32,14 @@
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
@@ -67,7 +72,9 @@ import { LOGICAL_W } from '../core/constants.js';
67
72
  import { initSettingsPanel } from '../ui/settings-panel.js';
68
73
  import { initMenuNav } from '../ui/menu-nav.js';
69
74
  import { initKeyboardRuntime } from '../input/keyboard-runtime.js';
70
- 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';
71
78
  import { installCvdFilters } from '../render/cvd-matrices.js';
72
79
  /** Os ids que os painéis emprestados exigem do documento. Achado 6: sem eles o painel abre VAZIO, sem erro. */
73
80
  const MARCACAO_EXIGIDA = ['#game-region', '#sr-status', '#sr-alert'];
@@ -209,6 +216,18 @@ export function createGame(o) {
209
216
  const lerModoCego = o.isBlindMode ?? (() => state.modoCego);
210
217
  const pauseIcons = initPauseIcons({
211
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(),
212
231
  getPlayers: () => o.players ?? [],
213
232
  getNumPlayers: () => (o.players ?? [null]).length,
214
233
  srSay, srAlert,
@@ -317,15 +336,16 @@ export function createGame(o) {
317
336
  * engine inventou uma convenção, procurou-a, não a achou, e concluiu em silêncio que nenhum jogo tem menu
318
337
  * de pausa — que é a MESMA forma de defeito do ADR-0106 §2, desta vez cometida pela engine contra si mesma.
319
338
  *
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.
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.
322
342
  */
323
- const hospedeiroDaPausa = declines.semMenuDePausa ? null : (o.host.pauseHost ?? $('#game-region'));
343
+ const hospedeiroDaPausa = o.host.pauseHost ?? $('#game-region');
324
344
  const pausaUsavel = !!hospedeiroDaPausa && typeof hospedeiroDaPausa.appendChild === 'function';
325
- if (!declines.semMenuDePausa && !pausaUsavel) {
345
+ if (!pausaUsavel) {
326
346
  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');
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');
329
349
  }
330
350
  if (hospedeiroDaPausa && pausaUsavel) {
331
351
  const cartao = pauseIcons.buildScreenPause(0);
@@ -363,6 +383,21 @@ export function createGame(o) {
363
383
  getAudioCtx: () => audioCtx, getSoundOn: () => soundOn, getAudioCat: () => audioCat,
364
384
  });
365
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);
366
401
  initKB();
367
402
  // ⚠️ O ESQUEMA DE ARRANQUE ALCANÇA NADA, e diz isso com `null` em vez de com um objeto vazio (issue #118).
368
403
  // Ele vive um instante — `assignControls()` logo abaixo substitui-o pelo esquema real —, mas enquanto vive
@@ -378,7 +413,7 @@ export function createGame(o) {
378
413
  const nav = initMenuNav({
379
414
  $, getActiveElement: () => doc.activeElement,
380
415
  topVisibleOverlay: overlays.topVisibleOverlay, closeById: overlays.closeById,
381
- getPauseMenu: declines.semMenuDePausa ? () => null : (i) => $(`#vp-pause-${i}`),
416
+ getPauseMenu: (i) => $(`#vp-pause-${i}`),
382
417
  setPhase: o.setPhase ?? (() => { }),
383
418
  setPauseActor: () => { },
384
419
  srSay,
@@ -522,5 +557,31 @@ export function createGame(o) {
522
557
  cartao.hidden = true;
523
558
  },
524
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
+ }
525
586
  return { declaration: o.declaration, pausa, tts, overlays, nav, keyboard, sonar, aplicarFiltroDeVisao, cenas: criarPilha(), cvdFilters, problems, declines, aoFalhar, alcance: alcanceAqui };
526
587
  }
@@ -1,4 +1,5 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ import type { Action } from './actions.js';
2
3
  /** Gênero gramatical do nome. `n` = neutro/indefinido (o pt-BR usa o masculino como default nesse caso). */
3
4
  export type Gender = 'm' | 'f' | 'n';
4
5
  /** Um nome que pode ser FALADO (leitor de tela) e SINALIZADO (Libras, que traduz o mesmo texto). */
@@ -194,6 +195,28 @@ export interface GameDeclaration {
194
195
  * em silêncio, que é o defeito que o ADR-0084 nomeou.
195
196
  */
196
197
  holdsAtOnce(): number;
198
+ /**
199
+ * ESTE JOGO SEGURA ALGUMA TECLA? — e a resposta não é derivável de mais nada. (ADR-0115.)
200
+ *
201
+ * 🔴 A ALTERNÂNCIA EXISTE PARA UMA CRIANÇA CONCRETA: quem não consegue MANTER uma tecla premida carrega uma
202
+ * vez para andar e outra para parar. Num jogo onde nada se segura — um quiz, um tabuleiro, um puzzle de
203
+ * peças — não há nada a travar, e o controle passa a ser uma opção que **não faz nada**. A criança abre o
204
+ * menu de acessibilidade, liga o ajuste de que depende, e não acontece nada: ela aprende que o ajuste está
205
+ * partido. É o botão morto que o ADR-0106 §5 proíbe.
206
+ *
207
+ * ⚠️ E O `holdsAtOnce` ACIMA NÃO RESPONDE ISTO, o que foi o achado que obrigou a este campo: ele conta
208
+ * POSIÇÕES SIMULTÂNEAS e recusa zero, porque zero faria a aritmética do alcance passar por vacuidade. O
209
+ * `consumer-quiz` declara **1 sem segurar coisa nenhuma**. «Um de cada vez» e «um SEGURADO» são o mesmo
210
+ * número, e toda decisão a jusante vinha a ler um número que responde a outra pergunta.
211
+ *
212
+ * ⚠️ OBRIGATÓRIO, e a obrigatoriedade É a decisão — a mesma do `holdsAtOnce`, pela mesma frase do Dev:
213
+ * «não declarar é ter a acessibilidade programada no controle pro sorte». Um campo opcional faria um jogo
214
+ * que ESQUECE a linha perder a alternância em silêncio, e quem paga é a criança com dificuldade motora.
215
+ *
216
+ * 📌 FUNÇÃO e não valor, pelo ADR-0084: um jogo muda de exigência entre fases. A pé segura-se uma direcção;
217
+ * o mesmo jogo dentro de um veículo pode não segurar nada.
218
+ */
219
+ seguraTeclas(): boolean;
197
220
  /**
198
221
  * ESTE JOGO PRECISA DE UM PONTEIRO — posição contínua? (ADR-0112.)
199
222
  *
@@ -213,6 +236,43 @@ export interface GameDeclaration {
213
236
  * FUNÇÃO e não valor, pela mesma razão das outras: uma actividade pode desenhar numa fase e não noutra.
214
237
  */
215
238
  needsPointer?(): boolean;
239
+ /**
240
+ * O MAPEAMENTO DE TECLADO QUE ESTE JOGO QUER — por número de jogadores e por assento (ADR-0115).
241
+ *
242
+ * A precedência é a que o registo pede, e ela cabe entre duas linhas que já existiam no `input/keyboard`:
243
+ * **fábrica da engine → padrão do JOGO → remapeamento da CRIANÇA.** Devolver `null` (ou não declarar) deixa
244
+ * a fábrica da engine intacta, que é o comportamento de sempre.
245
+ *
246
+ * PARCIAL de propósito: um jogo que só queira trocar o `action1` troca o `action1`. A fusão já existe — é o
247
+ * `Object.assign` que sobrepõe o dado guardado —, então esta é mais uma camada no mesmo sítio e não uma
248
+ * segunda forma de fundir.
249
+ *
250
+ * ⚠️ OPCIONAL, e aqui, ao contrário do `seguraTeclas`, o silêncio tem um lado seguro: sem declaração o jogo
251
+ * fica com a fábrica da engine, que é jogável e é o que ele já tem hoje. Não há lado errado na ausência.
252
+ *
253
+ * ⚠️ E LEVA O ASSENTO porque o teclado de dois jogadores não é o de um: as setas mudam de dono, e um padrão
254
+ * que não soubesse o assento daria as mesmas teclas a duas crianças. `jogadores` é 1, 2, 3 ou 4; `assento` é
255
+ * o índice dentro desse arranjo.
256
+ *
257
+ * 📌 O irmão do CONTROLE é o campo logo abaixo, e chegou um commit depois: o obstáculo era que o assento
258
+ * ainda não se conhecia no ponto em que a tabela de botões é lida, e a saída foi subi-lo no laço.
259
+ */
260
+ mapeamentoDoTeclado?(jogadores: number, assento: number): Partial<Record<Action, readonly string[] | null>> | null;
261
+ /**
262
+ * O MAPEAMENTO DE BOTÕES QUE ESTE JOGO QUER NO CONTROLE — mesma pergunta, outro aparelho (ADR-0115).
263
+ *
264
+ * Índices de botão da Gamepad API «standard», parciais: `{ action1: 3 }` troca só essa. `null` num botão diz
265
+ * «esta posição não existe neste jogo», que é diferente de a deixar na fábrica.
266
+ *
267
+ * ⚠️ A PRECEDÊNCIA TEM UMA DIFERENÇA DE SÍTIO QUE VALE SABER: no teclado, o que a criança remapeou é uma
268
+ * camada POR CIMA desta; no controle, o mapa que ela gravou no assistente é um RAMO inteiro — se ele existe,
269
+ * este padrão não é consultado. Nos dois casos ela ganha, que é o que importa.
270
+ *
271
+ * 📌 E leva o assento pela mesma razão do teclado, ainda que por um caminho diferente: dois controles são
272
+ * dois aparelhos, mas o JOGO pode querer arranjos distintos por assento (o guarda-redes e o atacante não
273
+ * fazem o mesmo).
274
+ */
275
+ mapeamentoDoPad?(jogadores: number, assento: number): Partial<Record<Action, number | null>> | null;
216
276
  readonly tick: TickOwner;
217
277
  /** O papel do que está em `at`. É o campo 2, e é o que substitui `roleOf`. */
218
278
  roleAt(at: Spot): Role;
@@ -1,6 +1,11 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-or-later
2
2
  // core/contract — OS SETE CAMPOS. A interface que o ADR-0030 escolheu como o eixo da engine.
3
3
  //
4
+ // ⚠️ ESTE FICHEIRO NÃO TINHA IMPORT NENHUM, e o único que ganhou é `import type` — apagado na compilação, logo
5
+ // o módulo publicado continua sem dependência de execução. O que entra é o VOCABULÁRIO das catorze posições
6
+ // (ADR-0074), e ele tinha de entrar: um campo que fala de mapeamento e escrevesse as suas próprias chaves
7
+ // seria a segunda cópia da união que o `core/actions` existe para ser a primeira.
8
+ //
4
9
  // ========================= O QUE ISTO É, E O QUE NÃO É =========================
5
10
  // Não é um framework nem uma classe-base. É a ÚNICA coisa que a pilha de acessibilidade sabe sobre um jogo.
6
11
  // O ADR-0027 mediu que as três funções que MAIS pareciam genéricas eram as três mais amarradas à plataforma —
@@ -126,6 +131,17 @@ export function conformanceProblems(d) {
126
131
  p.push('holdsAtOnce: must be an integer >= 1 - a game that holds nothing cannot be played');
127
132
  }
128
133
  }
134
+ // ⚠️ E ESTA É A OUTRA PERGUNTA, que o número acima parecia responder e não responde (ADR-0115). A mensagem
135
+ // diz o que a ausência CUSTA, e não só o que falta: sem ela, um jogo que nada segura oferece um controle de
136
+ // acessibilidade que não faz nada, e um que segura tudo pode não o oferecer a quem depende dele.
137
+ if (typeof d.seguraTeclas !== 'function') {
138
+ p.push('seguraTeclas: missing - declare whether any key is HELD in this game (latching is offered only where something can be held, and a game that holds nothing must not show a control that does nothing)');
139
+ }
140
+ else if (typeof d.seguraTeclas() !== 'boolean') {
141
+ // Um valor não-booleano seria truthy e ofereceria a alternância a toda a gente — o mesmo defeito
142
+ // silencioso que o `needsPointer` recusa logo abaixo, e pela mesma razão.
143
+ p.push('seguraTeclas: must return a boolean - a non-boolean is truthy and would offer latching in a game where nothing is held');
144
+ }
129
145
  // ⚠️ OPCIONAL, MAS NÃO IMPUNE. Ausente é a resposta `false` e não é problema — ver a nota no campo. O que
130
146
  // se recusa é declará-lo MAL: um `needsPointer: true` (valor em vez de função) seria sempre verdadeiro por
131
147
  // ser um objecto, e um que devolvesse `'sim'` também. Nos dois casos o jogo julgaria ter declarado, o
@@ -144,6 +160,32 @@ export function conformanceProblems(d) {
144
160
  p.push('needsPointer: must return a boolean - a non-boolean would be truthy and refuse devices this game can actually use');
145
161
  }
146
162
  }
163
+ // ⚠️ E O MESMO PARA O MAPEAMENTO, com uma razão própria: aqui um valor em vez de uma função não seria um
164
+ // erro barulhento, seria um mapeamento SILENCIOSAMENTE ignorado — a fábrica da engine ficava, e a criança
165
+ // jogava com um teclado que o autor do jogo julga ter mudado. E devolver algo que não é objecto nem `null`
166
+ // atravessaria o `Object.assign` sem escrever nada, que é a mesma ausência com outra roupa.
167
+ if (d.mapeamentoDoTeclado !== undefined) {
168
+ if (typeof d.mapeamentoDoTeclado !== 'function') {
169
+ p.push('mapeamentoDoTeclado: must be a function - write `mapeamentoDoTeclado: (jogadores, assento) => ({ action1: ["KeyQ"] })`, because the keyboard of two players is not the keyboard of one');
170
+ }
171
+ else {
172
+ const m = d.mapeamentoDoTeclado(1, 0);
173
+ if (m !== null && (typeof m !== 'object' || Array.isArray(m))) {
174
+ p.push('mapeamentoDoTeclado: must return an object or null - anything else is merged into nothing, and this game keeps the engine factory while its author believes otherwise');
175
+ }
176
+ }
177
+ }
178
+ if (d.mapeamentoDoPad !== undefined) {
179
+ if (typeof d.mapeamentoDoPad !== 'function') {
180
+ p.push('mapeamentoDoPad: must be a function - write `mapeamentoDoPad: (jogadores, assento) => ({ action1: 3 })`, because two seats may want different arrangements');
181
+ }
182
+ else {
183
+ const m = d.mapeamentoDoPad(1, 0);
184
+ if (m !== null && (typeof m !== 'object' || Array.isArray(m))) {
185
+ p.push('mapeamentoDoPad: must return an object or null - anything else is merged into nothing, and this game keeps the engine factory while its author believes otherwise');
186
+ }
187
+ }
188
+ }
147
189
  if (d.tick !== 'player' && d.tick !== 'clock')
148
190
  p.push('tick: must be "player" or "clock"');
149
191
  for (const f of ['roleAt', 'nameAt', 'focusOf', 'objectiveOf', 'targetsOf']) {
@@ -580,6 +580,17 @@ const en = {
580
580
  'font.desc.andika': 'based on Sassoon; the fruit of research into how children read and write',
581
581
  'font.desc.opendyslexic': 'Letters weighted at the bottom, so they do not flip upside down as you read.',
582
582
  'font.desc.fondamento': 'Pen calligraphy, for the handwriting activities.',
583
+ 'font.desc.pw.br': 'School cursive taught in Brazil.',
584
+ 'font.desc.pw.ustrad': 'School cursive taught in the USA — traditional.',
585
+ 'font.desc.pw.usmod': 'School cursive taught in the USA — modern.',
586
+ 'font.desc.pw.ca': 'School cursive taught in Canada.',
587
+ 'font.desc.pw.mx': 'School cursive taught in Mexico.',
588
+ 'font.desc.pw.ar': 'School cursive taught in Argentina.',
589
+ 'font.desc.pw.cl': 'School cursive taught in Chile.',
590
+ 'font.desc.pw.co': 'School cursive taught in Colombia.',
591
+ 'font.desc.ronde': 'French ronde, the joined handwriting taught at school.',
592
+ 'font.off.ronde': 'Install one of these three on the device: Ronde Script, OPTIFrench-Script or Merveille. '
593
+ + 'They are free for personal use, which is why they cannot ship inside the game.',
583
594
  'font.desc.greatvibes': 'English calligraphy',
584
595
  'font.desc.pinyon': 'English calligraphy',
585
596
  'font.desc.ufcook': 'German blackletter',
@@ -578,6 +578,17 @@ const es = {
578
578
  'font.desc.andika': 'basada en la Sassoon; fruto de la investigación sobre cómo leen y escriben los niños',
579
579
  'font.desc.opendyslexic': 'Letras con la base más pesada, para que no se den vuelta al leer.',
580
580
  'font.desc.fondamento': 'Caligráfica de pluma, para las actividades de escritura a mano.',
581
+ 'font.desc.pw.br': 'Cursiva escolar de Brasil.',
582
+ 'font.desc.pw.ustrad': 'Cursiva escolar de EE. UU. — tradicional.',
583
+ 'font.desc.pw.usmod': 'Cursiva escolar de EE. UU. — moderna.',
584
+ 'font.desc.pw.ca': 'Cursiva escolar de Canadá.',
585
+ 'font.desc.pw.mx': 'Cursiva escolar de México.',
586
+ 'font.desc.pw.ar': 'Cursiva escolar de Argentina.',
587
+ 'font.desc.pw.cl': 'Cursiva escolar de Chile.',
588
+ 'font.desc.pw.co': 'Cursiva escolar de Colombia.',
589
+ 'font.desc.ronde': 'Ronde francesa, la letra ligada que se enseña en la escuela.',
590
+ 'font.off.ronde': 'Instale en el dispositivo una de estas tres: Ronde Script, OPTIFrench-Script o Merveille. '
591
+ + 'Son gratuitas para uso personal, y por eso no pueden venir dentro del juego.',
581
592
  'font.desc.greatvibes': 'caligráfica inglesa',
582
593
  'font.desc.pinyon': 'caligráfica inglesa',
583
594
  'font.desc.ufcook': 'blackletter alemana',
@@ -653,6 +653,20 @@ const pt = {
653
653
  'font.desc.andika': 'baseada na Sassoon; fruto de pesquisa sobre como crianças leem e escrevem',
654
654
  'font.desc.opendyslexic': 'Letras com a base mais pesada, para não virarem de cabeça para baixo ao ler.',
655
655
  'font.desc.fondamento': 'Caligráfica de pena, para as atividades de escrita à mão.',
656
+ 'font.desc.pw.br': 'Cursiva escolar do Brasil.',
657
+ 'font.desc.pw.ustrad': 'Cursiva escolar dos EUA — tradicional.',
658
+ 'font.desc.pw.usmod': 'Cursiva escolar dos EUA — moderna.',
659
+ 'font.desc.pw.ca': 'Cursiva escolar do Canadá.',
660
+ 'font.desc.pw.mx': 'Cursiva escolar do México.',
661
+ 'font.desc.pw.ar': 'Cursiva escolar da Argentina.',
662
+ 'font.desc.pw.cl': 'Cursiva escolar do Chile.',
663
+ 'font.desc.pw.co': 'Cursiva escolar da Colômbia.',
664
+ 'font.desc.ronde': 'Ronde francesa, a letra de mão que se ensina na escola.',
665
+ // ⚠️ AS TRÊS PELO NOME, e não «uma fonte ronde» (ADR-0108 §4): um adulto não consegue agir sobre uma
666
+ // categoria. A frase existe para ser executável — abrir o navegador, procurar UM destes três nomes,
667
+ // instalar. Nomear a categoria seria a mesma linha morta que este catálogo já removeu duas vezes.
668
+ 'font.off.ronde': 'Instale no aparelho uma destas três: Ronde Script, OPTIFrench-Script ou Merveille. '
669
+ + 'Elas são gratuitas para uso pessoal, e por isso não podem vir dentro do jogo.',
656
670
  'font.desc.greatvibes': 'caligráfica inglesa',
657
671
  'font.desc.pinyon': 'caligráfica inglesa',
658
672
  'font.desc.ufcook': 'blackletter alemã',
@@ -6,20 +6,28 @@
6
6
  // físicos — L1, R2, X, A — são BINDING e vivem no transporte, não no vocabulário. Este é o ficheiro onde eles
7
7
  // vivem. Um transporte novo (fala, olhar, toque) traz a sua tabela e não toca em `core/actions`.
8
8
  //
9
- // ⚠️ METADE DISTO ESTÁ LIGADA, e a linha que dizia o contrário morreu com a issue #103 (fechada em 06/09).
10
- // Ela dizia «NADA AQUI ESTÁ LIGADO AINDA … a migração é a issue #103», e era verdade no dia em que foi
11
- // escrita. Hoje:
9
+ // ✅ AS TRÊS ESTÃO LIGADAS, e esta nota já mentiu duas vezes — cada versão dela era verdade no dia em que foi
10
+ // escrita e deixou de ser sem ninguém a corrigir. A primeira dizia «NADA AQUI ESTÁ LIGADO AINDA» (morreu com a
11
+ // issue #103, 06/09); a segunda dizia que só o gamepad estava, e morreu com a issue #118, que uniu as duas
12
+ // tabelas de teclado. 📏 Medido em 2026-09-09:
12
13
  //
13
- // · `GAMEPAD_STANDARD` — LIGADO. `input/gamepad.ts:129` lê os índices desta tabela em vez de literais, e a
14
- // ligação apanhou uma discordância real: `action1` corria em X, R1 e R2 enquanto a tabela declarava R1 e
15
- // R2 como ombro e gatilho.
16
- // · `KEYBOARD_SOLO` / `KEYBOARD_DUO` — NÃO LIGADOS. O jogo continua a usar o `KB_DEFAULTS` de
17
- // `input/keyboard.ts`, que declara OITO das quatorze posições. Unir as duas é a issue #118, e o que
18
- // impede a união hoje não é trabalho: é decidir se `KeyScheme` deixa de ser um `Record` aberto, e onde
19
- // moram as seis posições que faltam num teclado dividido por quatro.
14
+ // · `GAMEPAD_STANDARD` — `input/gamepad.ts:129` lê os índices desta tabela em vez de literais, e a ligação
15
+ // apanhou uma discordância real: `action1` corria em X, R1 e R2 enquanto a tabela declarava R1 e R2 como
16
+ // ombro e gatilho.
17
+ // · `KEYBOARD_SOLO` / `KEYBOARD_DUO` — `input/keyboard.ts:73` constrói o `KB_DEFAULTS` a partir delas
18
+ // (`vivo(KEYBOARD_SOLO)` e `KEYBOARD_DUO.map(vivo)`). Deixaram de ser duas listas paralelas: são a MESMA
19
+ // decisão, derivada, e por isso a divergência de antes não pode voltar por esquecimento.
20
20
  //
21
- // ⚠️ ENQUANTO AS DUAS EXISTIREM, ELAS NÃO PODEM DIVERGIR EM SILÊNCIO, e `tests/teclado-duas-tabelas` é quem
22
- // o garante. Nasceu vermelho: a `Space` do jogador 1 em dupla estava numa e não na outra.
21
+ // ⚠️ E A DERIVAÇÃO É POR CÓPIA PROFUNDA (`vivo` faz JSON round-trip), em DUAS camadas — verificado antes de
22
+ // escrito, porque a primeira versão desta linha dizia mais do que é verdade. `input/keyboard.ts` copia estas
23
+ // tabelas para o `KB_DEFAULTS`, e copia OUTRA VEZ para o `kb` mutável. O remapeamento da criança muta o `kb`
24
+ // («remapear uma tecla MUTA o objeto», diz o `setKB`), logo ele já não alcançaria isto nem sem a primeira
25
+ // cópia. O que a primeira camada compra é o resto: `KB_DEFAULTS` é exportado, e sem ela qualquer consumidor
26
+ // que lhe escrevesse dentro alterava o padrão de fábrica desta tabela para toda a gente.
27
+ //
28
+ // 📌 A LIÇÃO QUE ESTA NOTA CARREGA AGORA É SOBRE SI PRÓPRIA: um comentário que descreve estado de ligação
29
+ // apodrece a cada entrega. Este diz a DATA da medição, para que a próxima pessoa saiba contra o que a
30
+ // comparar em vez de acreditar.
23
31
  //
24
32
  // ========================= A SIMETRIA DO TECLADO, QUE NÃO É DECORAÇÃO =========================
25
33
  // O padrão que o Dev especificou apoia-se num bloco do QWERTY:
@@ -1,5 +1,6 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-or-later
2
2
  import type { PlayerView } from '../core/entity.js';
3
+ import { type TabelaDoPad } from './pad-defaults.js';
3
4
  export interface PadButtonLike {
4
5
  pressed: boolean;
5
6
  }
@@ -54,7 +55,7 @@ export declare function bindActive(gp: PadLike, bd: PadBinding | null | undefine
54
55
  /** Ações do frame para este gamepad. `custom` = mapa salvo pelo wizard para este `gp.id` (null/`_skip` = usa o
55
56
  * mapa PADRÃO da Gamepad API "standard": 0=pulo · 1=especial · 2/5/7=correr · 3=troca · 9=START). Direções
56
57
  * custom caem de volta em stdDirs quando o binding do usuário não está ativo (D-pad/stick continuam vivos). */
57
- export declare function padActions(gp: PadLike, custom: PadMap | null): PadActions;
58
+ export declare function padActions(gp: PadLike, custom: PadMap | null, tabela?: TabelaDoPad): PadActions;
58
59
  /**
59
60
  * O MODO DE UM BOTÃO, APLICADO AO CONTROLE — a metade que faltava da empatia motora (issue #120).
60
61
  *
@@ -170,6 +171,19 @@ export interface GamepadCtx {
170
171
  navPause: (menu: HTMLElement, playerIndex: number, k: NavKeys) => void;
171
172
  /** Qual jogador abre o submenu de a11y em seguida (game.js's `pauseActor`). */
172
173
  setPauseActor: (playerIndex: number) => void;
174
+ /**
175
+ * ESTA ARESTA É DESTE JOGADOR, E VEIO DO CONTROLE (ADR-0113 cláusula 4, issue #127).
176
+ *
177
+ * 🔴 OBRIGATÓRIO, e a razão foi medida em 2026-09-09: `input/state.arestaDoJogador` tinha ZERO chamadores
178
+ * em produção, logo `entradaDe(i).emUso` respondia `teclado` a toda a gente — e a alternância lida era a do
179
+ * teclado mesmo com o controle na mão. 📌 Passe `criarArestaComAlternancia(() => players)` de
180
+ * `input/latch-edge`, e não o cru: é ela que também resolve a alternância deste aparelho no jogador.
181
+ *
182
+ * ⚠️ O gamepad era o ÚNICO transporte que sobrevivia identificável sem isto — ele nunca passou pelo
183
+ * conjunto de teclas, passa por `padCur` —, e é exactamente por isso que a falta aqui era invisível: o
184
+ * módulo sabe de que controle veio a aresta, e o autómato não.
185
+ */
186
+ arestaDoJogador: (jogador: number, origem: 'gamepad') => void;
173
187
  /**
174
188
  * MODAL do PRÓPRIO jogador: a engine entrega a INTENÇÃO, o jogo decide (ADR-0033).
175
189
  *