@the-inclusionist/engine 7.0.1 → 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
@@ -168,6 +168,10 @@ const pt = {
168
168
  'touch.dir.cross': 'cruz (D-pad)',
169
169
  'touch.dir.stick': 'analógico',
170
170
  'sr.touch.slotSet': '{slot}: {acao}.',
171
+ // ⚠️ SEM O NOME DA POSIÇÃO, de propósito: é uma que este jogo não nomeia, e o id interno (`action3`)
172
+ // não pode chegar a uma criança (ADR-0074). O anúncio EXISTE na mesma — quem navega por ouvido precisa de
173
+ // saber que a escolha aterrou. Gémeo do `sr.ctrl.keyTakenHereUnnamed`, que resolveu o mesmo em `7742ac0`.
174
+ 'sr.touch.slotSetUnnamed': '{slot}: definido.',
171
175
  'sr.touch.dirSet': 'Direcional: {tipo}.',
172
176
  'sr.touch.presetChild': 'Controles no tamanho de mão de criança (6 a 12 anos).',
173
177
  'sr.touch.presetAdult': 'Controles no tamanho de mão de adulto.',
@@ -182,9 +186,17 @@ const pt = {
182
186
  'act.especial': 'Especial',
183
187
  'key.space': 'Espaço',
184
188
  'ctrl.change': 'Alterar',
189
+ 'ctrl.pressing': 'Pressione…',
190
+ 'ctrl.editingYours': 'Editando o seu controle — modo {modo}.',
191
+ 'ctrl.mode.one': '1 jogador',
192
+ 'ctrl.mode.many': '{n} jogadores',
185
193
  'ctrl.changeKeyAria': 'Alterar tecla de {acao} do Jogador {n}',
186
194
  'sr.ctrl.pressNewKey': 'Pressione a nova tecla para {acao} do Jogador {n}, ou Esc para cancelar.',
187
195
  'sr.ctrl.keyTaken': 'Essa tecla já é do Jogador {n}. Escolha outra, ou Esc para cancelar.',
196
+ 'sr.ctrl.keyTakenHere': 'Essa tecla já é de {acao}. Escolha outra, ou Esc para cancelar.',
197
+ // ⚠️ SEM O NOME DA POSIÇÃO, de propósito: ela é uma que este jogo não nomeia, e o id interno (`action2`)
198
+ // não pode chegar a uma criança (ADR-0074). A frase diz a verdade que interessa — a tecla está ocupada.
199
+ 'sr.ctrl.keyTakenHereUnnamed': 'Essa tecla já está em uso neste controle. Escolha outra, ou Esc para cancelar.',
188
200
  'sr.ctrl.reset': 'Controles restaurados ao padrão.',
189
201
  // Painel de áudio (#audio) — saídas por jogador, som/narração, bengala, motores de voz.
190
202
  'audio.playerN': 'Jogador {n}',
@@ -274,6 +286,11 @@ const pt = {
274
286
  'pad.glyph.circle': 'bola',
275
287
  'pad.glyph.square': 'quadrado',
276
288
  'pad.glyph.triangle': 'triângulo',
289
+ 'pad.wiz.step': '{n} de {total} — aperte: {acao}',
290
+ 'pad.wiz.mapped': 'Mapeados: {lista}',
291
+ 'pad.wiz.pressAny': 'Aperte QUALQUER botão no controle que deseja mapear.',
292
+ 'pad.wiz.detected': 'Controle novo detectado: {id}. O jogo pausou para você configurá-lo. SOLTE tudo para começar.',
293
+ 'pad.wiz.releaseAll': 'Controle: {id}. Agora SOLTE tudo.',
277
294
  'menu.legendSpoken': 'Botão {sim} para confirmar, botão {nao} para voltar.',
278
295
  'sr.menu.fractionsIntro': 'Soma e subtração de frações: escolha a notação e o tipo.',
279
296
  'sr.menu.keepOneNotation': 'Deixe ao menos uma notação ligada.',
@@ -302,6 +319,21 @@ const pt = {
302
319
  // conclui que o jogo está partido. Estas frases existem para ela saber ANTES, e o que fazer.
303
320
  'reach.titulo': 'Este jogo usa {pedidas} ações.',
304
321
  'reach.curto': 'O {transporte} tem {lugares} lugares — não chegam para todas.',
322
+ // ⚠️ Diz os DOIS números, porque é a diferença entre eles que a criança (ou quem a acompanha) pode
323
+ // resolver. «Falta alguma coisa» manda procurar sem dizer o quê; «segura 2 e este jogo pede 3» diz que a
324
+ // saída é outro controle, e diz porquê.
325
+ 'reach.naoSegura': 'O {transporte} segura {segura} botões de cada vez, e este jogo pede {pedidas} ao mesmo tempo.',
326
+ // ⚠️ A RECUSA DA SIMULAÇÃO (#104 / ADR-0076). Ela diz um FACTO SOBRE A DEMONSTRAÇÃO e o caminho de volta —
327
+ // nunca «desligue isso». Quem ligou o alto contraste ligou-o porque precisa dele para ver.
328
+ 'eixo.tema.titulo': 'Tema (contraste)',
329
+ 'eixo.tema.padrao': 'Tema padrão',
330
+ 'eixo.correcao.titulo': 'Correção de cor',
331
+ 'eixo.correcao.tricro': 'Visão tricromática',
332
+ 'viz.escolher': 'Selecionar',
333
+ 'viz.escolhido': '✓ Selecionado',
334
+ 'sim.indisponivel.tema': 'Para ver a simulação, o tema precisa estar no padrão: por cima do alto contraste ela mostraria o que o tema faz, e não o que a deficiência faz.',
335
+ 'sim.indisponivel.correcao': 'Para ver a simulação, a correção de cor precisa estar em visão tricromática: de uma tela já corrigida ela não mostraria nem a deficiência nem a correção.',
336
+ 'sim.indisponivel.ambos': 'Para ver a simulação, o tema e a correção de cor precisam estar no padrão: por cima de um ajuste, a demonstração mostra o ajuste e não a deficiência.',
305
337
  'reach.ligue': 'Ligue {saida} e você joga com todas.',
306
338
  // Quando NADA resolveria: mandar ligar um controle seria mandar procurar o que não conserta.
307
339
  'reach.semSaida': 'Nenhum controle deste aparelho alcança todas. Dá para jogar assim mesmo, mas algumas ações vão ficar sem lugar.',
@@ -379,6 +411,8 @@ const pt = {
379
411
  'sr.quiz.wellDone': 'Muito bem! {palavra}. {n} de 3.',
380
412
  'sr.quiz.correctSoFar': 'Acertou! {n} de 3 para ganhar a moeda.',
381
413
  'menu.restoreDefaults': 'Restaurar padrões deste menu',
414
+ 'menu.close': 'Fechar',
415
+ 'menu.typo': 'Tipografia',
382
416
  'sr.audio.reset': 'Acessibilidade auditiva restaurada aos padrões. Os outros menus não mudaram.',
383
417
  'sr.empathy.reset': 'Modo empatia restaurado aos padrões: simulações desligadas. Correções de daltonismo e os outros menus não mudaram.',
384
418
  'sr.motor.reset': 'Modo Fácil e movimento por alternância restaurados aos padrões. O controle pelos olhos e o mapeamento de teclas continuam como estavam.',
@@ -488,6 +522,13 @@ const pt = {
488
522
  'sr.motor.easyOff': 'Modo Fácil desligado.',
489
523
  'sr.motor.toggleMoveOn': 'Movimento por alternância ligado: toque a direção para andar sem segurar; toque de novo para parar; segure para ir mais rápido. O pulo não interrompe a caminhada.',
490
524
  'sr.motor.toggleMoveOff': 'Movimento por alternância desligado.',
525
+ // ⚠️ O MOTIVO É UM FACTO SOBRE O APARELHO, NUNCA UMA REPREENSÃO (ADR-0113 cláusula 3). Estas frases
526
+ // dizem por que o botão não responde; «não desligue isto» repreenderia uma criança por mexer num ajuste
527
+ // de que ela depende. QUATRO chaves e não uma com `{aparelho}`: «os gestos» é plural e «o olhar» não.
528
+ 'alt.exigida.olhos': 'O controle por olhar precisa das teclas de alternância para funcionar.',
529
+ 'alt.exigida.rosto': 'O controle por rosto precisa das teclas de alternância para funcionar.',
530
+ 'alt.exigida.gestos': 'Os gestos precisam das teclas de alternância para funcionar.',
531
+ 'alt.exigida.fala': 'O comando de voz precisa das teclas de alternância para funcionar.',
491
532
  'sr.motor.toggleRunOn': 'Alternância do correr ligada.',
492
533
  'sr.motor.toggleRunOff': 'Alternância do correr desligada.',
493
534
  'sr.physics.runLatchOn': 'Correndo.',
@@ -601,18 +642,23 @@ const pt = {
601
642
  // Os NOMES das fontes (Atkinson Hyperlegible, Lexend, …) NÃO entram aqui: são nomes próprios e ficam no
602
643
  // catálogo, como os nomes dos motores de voz. O que traduz é o nome do GRUPO e a descrição — que é o texto
603
644
  // que explica à criança (ou a quem a acompanha) POR QUE aquela fonte existe na lista.
645
+ 'font.grupo.rotulo': 'Família de letra',
604
646
  'font.group.sans': 'Sem serifa',
605
647
  'font.group.serif': 'Serifada',
606
648
  'font.group.hand': 'Manuscrita',
649
+ 'font.group.arcade': 'Arcade',
607
650
  'font.desc.atkinson': 'feita pelo Braille Institute para pessoas com baixa visão (padrão do jogo)',
608
651
  'font.desc.lexend': 'feita para reduzir stress visual e atender pessoas disléxicas (ativa o espaçamento extra)',
609
652
  'font.desc.quattro': 'criada para diminuir a fadiga visual de quem passa muito tempo na tela',
610
653
  'font.desc.andika': 'baseada na Sassoon; fruto de pesquisa sobre como crianças leem e escrevem',
654
+ 'font.desc.opendyslexic': 'Letras com a base mais pesada, para não virarem de cabeça para baixo ao ler.',
655
+ 'font.desc.fondamento': 'Caligráfica de pena, para as atividades de escrita à mão.',
611
656
  'font.desc.greatvibes': 'caligráfica inglesa',
612
657
  'font.desc.pinyon': 'caligráfica inglesa',
613
658
  'font.desc.ufcook': 'blackletter alemã',
614
659
  'font.desc.ufmag': 'blackletter alemã',
615
660
  'font.desc.comicneue': 'bola e bastão (alfabetização)',
661
+ 'font.desc.pressstart': 'pixel de 8 bits (HUD e título do jogo)',
616
662
  'font.desc.learningcurve': 'cursiva inglesa',
617
663
  'font.desc.kindergarten': 'cursiva brasileira',
618
664
  'font.off.pending': 'licença a confirmar — ainda não embarcada',
@@ -7,6 +7,35 @@ export type Binding<T> = T | null;
7
7
  * layout ABNT2/US e é a razão de nunca se usar `key` aqui.
8
8
  */
9
9
  export declare const KEYBOARD_SOLO: Readonly<Record<Action, Binding<readonly string[]>>>;
10
+ /**
11
+ * Teclado, esquema de DOIS JOGADORES — as quatorze posições para cada um (ADR-0096).
12
+ *
13
+ * ⚠️ O JOGADOR 1 AQUI NÃO É O `KEYBOARD_SOLO`, e a diferença é UMA e obrigatória: as SETAS saem dele. No solo
14
+ * elas são um segundo caminho para o direcional; em dupla são o direcional DO OUTRO. Deixá-las nos dois faria
15
+ * os dois bonecos andarem juntos — um defeito que não dá erro em lado nenhum e que só se vê jogando a dois.
16
+ * `conflitosEntreTabelas` existe por causa desta linha.
17
+ *
18
+ * ⚠️ E A GEOMETRIA DO JOGADOR 2 É A MESMA DO JOGADOR 1, TRANSPOSTA PARA O TECLADO NUMÉRICO — o que faz a
19
+ * memória muscular atravessar de um lado da mesa para o outro:
20
+ *
21
+ * 7 8 / * ombros na linha DE CIMA
22
+ * Y U I O 7 8 9 + gatilhos nas PONTAS da linha das ações
23
+ * J K 5 6
24
+ *
25
+ * action1 U ↔ Numpad8 (cima-esquerda) action4 I ↔ Numpad9 (cima-direita)
26
+ * action2 J ↔ Numpad5 (baixo-esquerda) action3 K ↔ Numpad6 (baixo-direita)
27
+ *
28
+ * O bloco `U I / J K` e o bloco `8 9 / 5 6` têm a MESMA forma, então a rotação de 45° que o cabeçalho deste
29
+ * ficheiro descreve para o Xbox vale igual para o jogador 2. Não é coincidência de teclado: é o que torna o
30
+ * padrão ensinável uma vez só.
31
+ *
32
+ * ⚠️ `Digit7`/`Digit8` (jogador 1) e `Numpad7` (jogador 2) SÃO TECLAS DIFERENTES, e é por isso que a
33
+ * especificação do Dev diz «alphanumeric» e «numeric» em tantas palavras. `KeyboardEvent.code` distingue-as
34
+ * sempre; `key` não — com Num Lock desligado o numérico chega como `ArrowUp`/`Home`, e um esquema lido por
35
+ * `key` juntaria o direcional do jogador 2 com as ações dele. Mais uma razão de este ficheiro só falar
36
+ * `code`.
37
+ */
38
+ export declare const KEYBOARD_DUO: readonly Readonly<Record<Action, Binding<readonly string[]>>>[];
10
39
  /**
11
40
  * Gamepad, mapa PADRÃO da Gamepad API (`mapping: "standard"`), que é o que um controle de Xbox reporta.
12
41
  * O número é o índice em `gamepad.buttons`.
@@ -20,5 +49,16 @@ export declare const GAMEPAD_STANDARD: Readonly<Record<Action, Binding<number>>>
20
49
  * ação dupla intermitente que ninguém consegue reproduzir de propósito.
21
50
  */
22
51
  export declare function bindingProblems<T>(tabela: Readonly<Record<Action, Binding<T | readonly T[]>>>): string[];
52
+ /**
53
+ * As teclas que DUAS OU MAIS tabelas reclamam para si. VAZIA quer dizer que os jogadores não se atropelam.
54
+ *
55
+ * ⚠️ ISTO É UM PROBLEMA DIFERENTE DO `bindingProblems`, E FOI POR ISSO QUE PRECISOU DE FUNÇÃO PRÓPRIA: aquele
56
+ * olha UMA tabela e apanha a mesma tecla em duas ações; este olha DUAS tabelas e apanha a mesma tecla em dois
57
+ * JOGADORES. Cada esquema do `KEYBOARD_DUO` passa no primeiro sozinho — e as setas, se ficassem nos dois,
58
+ * fariam os dois bonecos andarem juntos sem que nada reprovasse.
59
+ *
60
+ * A mensagem nomeia os dois donos, porque «tecla repetida» manda procurar o que a função já sabe.
61
+ */
62
+ export declare function conflitosEntreTabelas<T>(tabelas: readonly Readonly<Record<Action, Binding<T | readonly T[]>>>[]): string[];
23
63
  /** As ações que este transporte NÃO alcança. É o que uma tela de seleção precisa dizer ANTES de a criança começar. */
24
64
  export declare function unreachable<T>(tabela: Readonly<Record<Action, Binding<T | readonly T[]>>>): Action[];
@@ -6,10 +6,20 @@
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
- // ⚠️ NADA AQUI ESTÁ LIGADO AINDA. `input/gamepad` e `input/keyboard` continuam a falar os oito nomes de
10
- // plataforma (`jump`, `run`, `swap`, `especial`); a migração é a issue #103. Esta tabela é o destino dela,
11
- // escrita agora porque o Dev especificou o padrão — e uma especificação que fica no chat é uma decisão
12
- // perdida.
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:
12
+ //
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.
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.
13
23
  //
14
24
  // ========================= A SIMETRIA DO TECLADO, QUE NÃO É DECORAÇÃO =========================
15
25
  // O padrão que o Dev especificou apoia-se num bloco do QWERTY:
@@ -28,11 +38,18 @@
28
38
  // O gate deste ficheiro reprova tecla repetida, então o erro não teria passado de qualquer forma; o que a
29
39
  // simetria deu foi a tecla CERTA em vez de só a notícia de que havia uma errada.
30
40
  import { ACTIONS } from '../core/actions.js';
31
- // ⚠️ UMA TECLA QUE EXISTE HOJE E NÃO TEM LUGAR NESTA TABELA: `Space`. O esquema solo de `input/keyboard.ts`
32
- // tem `jump:['KeyJ','Space']` — a barra é um segundo atalho para o pulo desde sempre. A especificação do
33
- // padrão não a menciona, e pô-la em `action2` por conta própria seria decidir que o pulo mora ali: se o
34
- // preset da plataforma puser o pulo noutra posição, a barra segue o verbo errado, que é pior do que ela não
35
- // existir. Fica de fora, dita aqui, e é decisão do Dev — não de quem escreve a tabela.
41
+ // ⚠️ A `Space` ESTÁ EM `action2`, E ISTO É A HISTÓRIA DE COMO LÁ FOI PARAR — porque este bloco dizia o
42
+ // contrário e sobreviveu à decisão que o revogou.
43
+ //
44
+ // Ela ficou de fora da primeira versão desta tabela, e a razão estava certa: a barra é o segundo atalho do
45
+ // pulo desde sempre, a especificação do padrão não a mencionava, e pô-la em `action2` por conta própria
46
+ // seria decidir que o pulo mora ali. Se o preset da plataforma puser o pulo noutra posição, a barra segue o
47
+ // verbo errado — pior do que ela não existir.
48
+ //
49
+ // O Dev decidiu (ADR-0086 §2) que o pulo mora em `action2`, e a barra voltou. O texto acima continuou a
50
+ // dizer «fica de fora» durante todo esse tempo, ao lado de duas linhas que a declaram. Corrigido em
51
+ // 2026-09-07, na mesma passagem da issue #118 — que trata exactamente desta classe de erro: um comentário
52
+ // que descreve um estado que já passou.
36
53
  /**
37
54
  * Teclado, esquema SOLO. Códigos de `KeyboardEvent.code` — físicos, não a letra impressa, que muda com o
38
55
  * layout ABNT2/US e é a razão de nunca se usar `key` aqui.
@@ -58,6 +75,92 @@ export const KEYBOARD_SOLO = {
58
75
  start: ['KeyH', 'Enter'],
59
76
  select: ['KeyF'],
60
77
  };
78
+ /**
79
+ * Teclado, esquema de DOIS JOGADORES — as quatorze posições para cada um (ADR-0096).
80
+ *
81
+ * ⚠️ O JOGADOR 1 AQUI NÃO É O `KEYBOARD_SOLO`, e a diferença é UMA e obrigatória: as SETAS saem dele. No solo
82
+ * elas são um segundo caminho para o direcional; em dupla são o direcional DO OUTRO. Deixá-las nos dois faria
83
+ * os dois bonecos andarem juntos — um defeito que não dá erro em lado nenhum e que só se vê jogando a dois.
84
+ * `conflitosEntreTabelas` existe por causa desta linha.
85
+ *
86
+ * ⚠️ E A GEOMETRIA DO JOGADOR 2 É A MESMA DO JOGADOR 1, TRANSPOSTA PARA O TECLADO NUMÉRICO — o que faz a
87
+ * memória muscular atravessar de um lado da mesa para o outro:
88
+ *
89
+ * 7 8 / * ombros na linha DE CIMA
90
+ * Y U I O 7 8 9 + gatilhos nas PONTAS da linha das ações
91
+ * J K 5 6
92
+ *
93
+ * action1 U ↔ Numpad8 (cima-esquerda) action4 I ↔ Numpad9 (cima-direita)
94
+ * action2 J ↔ Numpad5 (baixo-esquerda) action3 K ↔ Numpad6 (baixo-direita)
95
+ *
96
+ * O bloco `U I / J K` e o bloco `8 9 / 5 6` têm a MESMA forma, então a rotação de 45° que o cabeçalho deste
97
+ * ficheiro descreve para o Xbox vale igual para o jogador 2. Não é coincidência de teclado: é o que torna o
98
+ * padrão ensinável uma vez só.
99
+ *
100
+ * ⚠️ `Digit7`/`Digit8` (jogador 1) e `Numpad7` (jogador 2) SÃO TECLAS DIFERENTES, e é por isso que a
101
+ * especificação do Dev diz «alphanumeric» e «numeric» em tantas palavras. `KeyboardEvent.code` distingue-as
102
+ * sempre; `key` não — com Num Lock desligado o numérico chega como `ArrowUp`/`Home`, e um esquema lido por
103
+ * `key` juntaria o direcional do jogador 2 com as ações dele. Mais uma razão de este ficheiro só falar
104
+ * `code`.
105
+ */
106
+ export const KEYBOARD_DUO = Object.freeze([
107
+ {
108
+ // JOGADOR 1 — a mão esquerda anda (WASD), a direita age (UIJK). Sem as setas: são do jogador 2.
109
+ up: ['KeyW'],
110
+ down: ['KeyS'],
111
+ left: ['KeyA'],
112
+ right: ['KeyD'],
113
+ action1: ['KeyU'],
114
+ action2: ['KeyJ', 'Space'],
115
+ action3: ['KeyK'],
116
+ action4: ['KeyI'],
117
+ leftShoulder: ['Digit7'],
118
+ leftTrigger: ['KeyY'],
119
+ rightShoulder: ['Digit8'],
120
+ rightTrigger: ['KeyO'],
121
+ start: ['KeyH', 'Enter'],
122
+ select: ['KeyF'],
123
+ },
124
+ {
125
+ // JOGADOR 2 — as setas andam, o teclado numérico age. `Enter` fica com o jogador 1; o `NumpadEnter` não
126
+ // entra aqui de propósito, porque `ui/menu-nav` já o usa como CONFIRMAR em qualquer menu (KEY_YES) e
127
+ // `input/keydown` registra, verbatim, que ele NÃO pausa. Dar-lhe um terceiro trabalho seria sobrepor.
128
+ up: ['ArrowUp'],
129
+ down: ['ArrowDown'],
130
+ left: ['ArrowLeft'],
131
+ right: ['ArrowRight'],
132
+ action1: ['Numpad8'],
133
+ action2: ['Numpad5'],
134
+ action3: ['Numpad6'],
135
+ action4: ['Numpad9'],
136
+ leftShoulder: ['NumpadDivide'], // a tecla `/` do bloco numérico
137
+ leftTrigger: ['Numpad7'],
138
+ rightShoulder: ['NumpadMultiply'], // a tecla `*`
139
+ rightTrigger: ['NumpadAdd'], // a tecla `+`
140
+ /**
141
+ * ⚠️ `ShiftRight` É A PORTA DE SAÍDA, E ELA FALTAVA (#122).
142
+ *
143
+ * Um Chromebook não tem bloco numérico, e um Chromebook é o hardware que o pilar 1 nomeia. Medido em
144
+ * 2026-09-07: das catorze posições deste assento, **DEZ só se alcançam pelo numpad** — as oito ações
145
+ * mais o `start` e o `select`. A criança da segunda cadeira anda pelas setas, não age em nada, e **não
146
+ * consegue abrir o menu para consertar**, porque a tecla que abre o menu está no mesmo bloco que falta.
147
+ *
148
+ * Isso é o que a auditoria chamou de «um padrão do qual a criança não escapa», e o conserto que ela pede
149
+ * é textual: *«tornar o próprio padrão remapeável, e não trocar as teclas que ele escolheu».* As teclas
150
+ * ficam — o layout do numpad é melhor onde ele existe, é um bloco físico sob uma mão, e não tira nada ao
151
+ * primeiro jogador. O que entra é UMA porta que todo teclado tem, para o remapeamento ser alcançável.
152
+ *
153
+ * ⚠️ E É SÓ NO `start`, de propósito: da pausa alcança-se a tela de remapeamento, e de lá TODAS as outras
154
+ * treze posições. Uma porta chega para escapar; cada padrão a mais é uma tecla tirada do bolo comum, e
155
+ * este teclado já é repartido por duas crianças.
156
+ *
157
+ * `ShiftRight` porque existe em todo o teclado, fica ao lado do bloco de setas (a mesma mão que já as
158
+ * usa), não é tecla de texto — e está LIVRE nas quatro tabelas, o que foi medido e não suposto.
159
+ */
160
+ start: ['Numpad1', 'ShiftRight'],
161
+ select: ['Numpad0'],
162
+ },
163
+ ]);
61
164
  /**
62
165
  * Gamepad, mapa PADRÃO da Gamepad API (`mapping: "standard"`), que é o que um controle de Xbox reporta.
63
166
  * O número é o índice em `gamepad.buttons`.
@@ -116,6 +219,37 @@ export function bindingProblems(tabela) {
116
219
  }
117
220
  return p;
118
221
  }
222
+ /**
223
+ * As teclas que DUAS OU MAIS tabelas reclamam para si. VAZIA quer dizer que os jogadores não se atropelam.
224
+ *
225
+ * ⚠️ ISTO É UM PROBLEMA DIFERENTE DO `bindingProblems`, E FOI POR ISSO QUE PRECISOU DE FUNÇÃO PRÓPRIA: aquele
226
+ * olha UMA tabela e apanha a mesma tecla em duas ações; este olha DUAS tabelas e apanha a mesma tecla em dois
227
+ * JOGADORES. Cada esquema do `KEYBOARD_DUO` passa no primeiro sozinho — e as setas, se ficassem nos dois,
228
+ * fariam os dois bonecos andarem juntos sem que nada reprovasse.
229
+ *
230
+ * A mensagem nomeia os dois donos, porque «tecla repetida» manda procurar o que a função já sabe.
231
+ */
232
+ export function conflitosEntreTabelas(tabelas) {
233
+ const p = [];
234
+ const dono = new Map();
235
+ tabelas.forEach((tabela, i) => {
236
+ for (const acao of ACTIONS) {
237
+ const v = tabela[acao];
238
+ if (v === null || v === undefined)
239
+ continue;
240
+ for (const item of (Array.isArray(v) ? v : [v])) {
241
+ const chave = String(item);
242
+ const aqui = `p${i + 1}.${acao}`;
243
+ const anterior = dono.get(chave);
244
+ if (anterior)
245
+ p.push(`cross: ${chave} is claimed by both ${anterior} and ${aqui}`);
246
+ else
247
+ dono.set(chave, aqui);
248
+ }
249
+ }
250
+ });
251
+ return p;
252
+ }
119
253
  /** As ações que este transporte NÃO alcança. É o que uma tela de seleção precisa dizer ANTES de a criança começar. */
120
254
  export function unreachable(tabela) {
121
255
  return ACTIONS.filter((a) => tabela[a] === null || tabela[a] === undefined);
@@ -56,21 +56,28 @@ export declare function bindActive(gp: PadLike, bd: PadBinding | null | undefine
56
56
  * custom caem de volta em stdDirs quando o binding do usuário não está ativo (D-pad/stick continuam vivos). */
57
57
  export declare function padActions(gp: PadLike, custom: PadMap | null): PadActions;
58
58
  /**
59
- * A ORDEM em que o assistente pergunta. SÓ a ordem.
59
+ * O MODO DE UM BOTÃO, APLICADO AO CONTROLE — a metade que faltava da empatia motora (issue #120).
60
60
  *
61
- * ⚠️ ELA CARREGAVA AS PALAVRAS, EM PORTUGUÊS CRU, DENTRO DA ENGINE: `['action2', 'PULAR']`,
62
- * `['action1', 'CORRER / INTERAGIR']`. Era o defeito do ADR-0074 na sua forma mais visível — não só
63
- * vocabulário de plataforma dentro do motor, mas vocabulário de plataforma NUM IDIOMA SÓ, à frente de uma
64
- * criança, num ficheiro que o pilar 3 obriga a ser localizável.
61
+ * ⚠️ ELE VALIA SÓ NO TECLADO. `input/keydown.ts:407` solta todas as outras teclas de jogo quando uma nova
62
+ * chega com o modo ligado; `pollPads` não tinha equivalente nenhum, e `grep oneButton` neste ficheiro
63
+ * devolvia zero. Uma criança que ligasse o modo e tivesse um controle na mão **não estava no modo** — sem
64
+ * erro, sem aviso, sem sintoma, porque as definições continuavam a dizer que estava ligado.
65
65
  *
66
- * A palavra vem agora de `ctx.rotuloDaAcao`, que o jogo fornece pelo seu preset. Aqui fica o que é mesmo da
67
- * engine: a sequência em que se pergunta — direções primeiro, ação depois, sistema no fim —, que é uma
68
- * decisão de ergonomia do assistente e não do jogo.
66
+ * ⚠️ AS DIREÇÕES CONTAM, e é isso que torna a regra fiel ao teclado: lá, `isGameKeyCode` inclui as teclas
67
+ * de `p.ctrl`, que são as quatro direções — andar e pular não coexistem. Um filtro que poupasse as
68
+ * direções seria mais confortável e estaria a simular outra deficiência.
69
69
  *
70
- * ⚠️ E UMA AÇÃO QUE O JOGO NÃO NOMEIA NÃO É PERGUNTADA. `wizPrompt` salta-a, porque um jogo que não usa a
71
- * posição não tem o que mapear nela — e perguntar produziria um passo mudo, ou pior, um passo a dizer
72
- * `action7` em voz alta.
70
+ * ⚠️ E A ESCOLHA DE QUEM SOBREVIVE É DIFERENTE DA DO TECLADO, POR NECESSIDADE. No teclado a chegada nova
71
+ * ganha, porque HÁ uma chegada: o evento diz qual é. Um controle é lido por SONDAGEM — o que chega é um
72
+ * retrato, sem ordem. Então mantém-se a que já valia, e só quando ela solta é que a próxima assume. É o
73
+ * que impede o botão de correr de ser cortado porque o polegar encostou noutro, e é a mesma leitura de
74
+ * «segurar» que o ADR-0077 dá.
75
+ *
76
+ * A POLÍTICA é a mesma do `keydown`; a IMPLEMENTAÇÃO não pode ser partilhada hoje porque as formas do
77
+ * estado diferem — lá é um `Set` de códigos de tecla, aqui é um retrato de booleanos por posição. Unificar
78
+ * as duas é trabalho à parte, e escrevê-lo aqui é a alternativa a fingir que não há duas.
73
79
  */
80
+ export declare function umBotaoPorVez(anterior: Readonly<Record<string, boolean | undefined>>, atual: PadActions, ligado: boolean): PadActions;
74
81
  export declare const PADWIZ_ORDER: readonly string[];
75
82
  export interface WizAnimDef {
76
83
  seq?: string[];
@@ -11,6 +11,15 @@ import { EDGE_BY_ACTION, edgeAllowed } from './edges.js';
11
11
  import { migrarMapaDeControle } from './vocabulary-migration.js';
12
12
  import { GAMEPAD_STANDARD } from './default-bindings.js';
13
13
  import { padCur, padPrevAct, padPrevStart, PAD_DEAD } from './state.js';
14
+ // ⚠️ O `oneButton` ENTRA POR IMPORT, e não pelo `ctx` — ao contrário de `input/keydown`, que o recebe por
15
+ // getter. A diferença não é de gosto: o `keydown` foi extraído quando o `oneButton` era um `let` do
16
+ // `game.js`, e a regra da casa manda o que o JOGO reatribui entrar por getter. Hoje ele é `core/state`,
17
+ // ou seja da ENGINE — e um getter obrigaria cada consumidor a lembrar-se de o passar.
18
+ //
19
+ // ⚠️ E ESQUECER É EXATAMENTE O DEFEITO QUE ISTO CONSERTA (issue #120). Um campo opcional no ctx faria a
20
+ // acomodação existir só nos jogos onde alguém se lembrou dela, que é o argumento M3 do ADR-0077 com outro
21
+ // substantivo. Binding vivo de `core/state`: não há por onde falhar.
22
+ import * as estadoDoJogo from '../core/state.js';
14
23
  import * as store from '../platform/storage.js';
15
24
  function bindingAt(map, key) {
16
25
  const v = map[key];
@@ -117,6 +126,56 @@ export function padActions(gp, custom) {
117
126
  * posição não tem o que mapear nela — e perguntar produziria um passo mudo, ou pior, um passo a dizer
118
127
  * `action7` em voz alta.
119
128
  */
129
+ /**
130
+ * AS POSIÇÕES QUE O MODO DE UM BOTÃO NÃO CORTA. Pausar é a SAÍDA, não uma jogada.
131
+ *
132
+ * ⚠️ Cortar o START prenderia a criança dentro da partida — é o mesmo raciocínio que põe «a saída
133
+ * primeiro» no ADR-0044 e que fez a armadilha de foco existir no ADR-0090. Uma acomodação que tranca não
134
+ * é acomodação. Os derivados (`_start`, `_pause`) também passam: eles descrevem o que a raiz faz com
135
+ * estas duas posições, não uma terceira.
136
+ */
137
+ const FORA_DO_CORTE = new Set(['start', 'select', '_start', '_pause']);
138
+ /**
139
+ * O MODO DE UM BOTÃO, APLICADO AO CONTROLE — a metade que faltava da empatia motora (issue #120).
140
+ *
141
+ * ⚠️ ELE VALIA SÓ NO TECLADO. `input/keydown.ts:407` solta todas as outras teclas de jogo quando uma nova
142
+ * chega com o modo ligado; `pollPads` não tinha equivalente nenhum, e `grep oneButton` neste ficheiro
143
+ * devolvia zero. Uma criança que ligasse o modo e tivesse um controle na mão **não estava no modo** — sem
144
+ * erro, sem aviso, sem sintoma, porque as definições continuavam a dizer que estava ligado.
145
+ *
146
+ * ⚠️ AS DIREÇÕES CONTAM, e é isso que torna a regra fiel ao teclado: lá, `isGameKeyCode` inclui as teclas
147
+ * de `p.ctrl`, que são as quatro direções — andar e pular não coexistem. Um filtro que poupasse as
148
+ * direções seria mais confortável e estaria a simular outra deficiência.
149
+ *
150
+ * ⚠️ E A ESCOLHA DE QUEM SOBREVIVE É DIFERENTE DA DO TECLADO, POR NECESSIDADE. No teclado a chegada nova
151
+ * ganha, porque HÁ uma chegada: o evento diz qual é. Um controle é lido por SONDAGEM — o que chega é um
152
+ * retrato, sem ordem. Então mantém-se a que já valia, e só quando ela solta é que a próxima assume. É o
153
+ * que impede o botão de correr de ser cortado porque o polegar encostou noutro, e é a mesma leitura de
154
+ * «segurar» que o ADR-0077 dá.
155
+ *
156
+ * A POLÍTICA é a mesma do `keydown`; a IMPLEMENTAÇÃO não pode ser partilhada hoje porque as formas do
157
+ * estado diferem — lá é um `Set` de códigos de tecla, aqui é um retrato de booleanos por posição. Unificar
158
+ * as duas é trabalho à parte, e escrevê-lo aqui é a alternativa a fingir que não há duas.
159
+ */
160
+ export function umBotaoPorVez(
161
+ // ⚠️ O ANTERIOR É TIPADO PELO QUE ESTA FUNÇÃO LÊ, e não por `PadActions`: o `padPrevAct[gi]` do laço é
162
+ // `PadState`, mais frouxo, e exigir a forma completa obrigaria o chamador a um molde que não descreve o
163
+ // que se passa aqui — só se pergunta «esta chave estava em baixo?».
164
+ anterior, atual, ligado) {
165
+ if (!ligado)
166
+ return atual;
167
+ const cortaveis = Object.keys(atual).filter((k) => !FORA_DO_CORTE.has(k));
168
+ const ativas = cortaveis.filter((k) => atual[k] === true);
169
+ if (ativas.length <= 1)
170
+ return atual;
171
+ // A que já valia tem prioridade; sem nenhuma, a primeira do retrato assume.
172
+ const mantida = ativas.find((k) => anterior[k] === true) ?? ativas[0];
173
+ const saida = { ...atual };
174
+ for (const k of ativas)
175
+ if (k !== mantida)
176
+ saida[k] = false;
177
+ return saida;
178
+ }
120
179
  export const PADWIZ_ORDER = [
121
180
  // Direções primeiro: são o que a criança encontra sem pensar, e acertar as quatro dá confiança para as
122
181
  // outras dez.
@@ -163,11 +222,15 @@ export function initGamepad(ctx) {
163
222
  }
164
223
  function actionsFor(gp) { return padActions(gp, padMapFor(gp.id)); }
165
224
  // ----- wizard: anúncio + demo animada (DOM-facing, thin) -----
166
- function wizSay(t) {
225
+ // ⚠️ O PARÂMETRO CHAMAVA-SE `t`, E ERA ELE QUE FECHAVA A PORTA. Dentro desta função o `t` do
226
+ // `core/i18n` estava sombreado, então traduzir uma frase aqui era impossível sem primeiro reparar no
227
+ // sombreamento — e não há erro nenhum a apontá-lo. Foi assim que cinco frases em português cru ficaram a
228
+ // falar dentro do motor (#123): não por decisão, por um nome.
229
+ function wizSay(frase) {
167
230
  const el = ctx.$('#padwiz-prompt');
168
231
  if (el)
169
- el.textContent = t;
170
- ctx.srSay(t);
232
+ el.textContent = frase;
233
+ ctx.srSay(frase);
171
234
  }
172
235
  function wizDemo(k) {
173
236
  const d = ctx.$('#padwiz-demo');
@@ -224,11 +287,12 @@ export function initGamepad(ctx) {
224
287
  return; // fechou ao avançar
225
288
  const acao = PADWIZ_ORDER[padWiz.step];
226
289
  const rotulo = ctx.rotuloDaAcao(acao);
227
- wizSay((padWiz.step + 1) + ' de ' + PADWIZ_ORDER.length + ' — aperte: ' + rotulo);
290
+ wizSay(t('pad.wiz.step', { n: padWiz.step + 1, total: PADWIZ_ORDER.length, acao: rotulo }));
228
291
  wizDemo(acao); // demonstração animada do que a ação FAZ
229
292
  const pr = ctx.$('#padwiz-progress');
293
+ // O travessão da lista vazia fica cru de propósito: é pontuação, não idioma.
230
294
  if (pr)
231
- pr.textContent = 'Mapeados: ' + (Object.keys(padWiz.map).join(' · ') || '—');
295
+ pr.textContent = t('pad.wiz.mapped', { lista: Object.keys(padWiz.map).join(' · ') || '—' });
232
296
  }
233
297
  function wizBind(bd) {
234
298
  if (!padWiz)
@@ -246,7 +310,7 @@ export function initGamepad(ctx) {
246
310
  ov.hidden = false;
247
311
  ctx.frontOverlay(ov);
248
312
  padWiz = { gi: -1, id: '', step: -1, base: null, map: {}, release: false, baseWait: false, axTrack: null, timer: null };
249
- wizSay('Aperte QUALQUER botão no controle que deseja mapear.');
313
+ wizSay(t('pad.wiz.pressAny'));
250
314
  wizDemo(null);
251
315
  const pr = ctx.$('#padwiz-progress');
252
316
  if (pr)
@@ -261,7 +325,7 @@ export function initGamepad(ctx) {
261
325
  ov.hidden = false;
262
326
  ctx.frontOverlay(ov);
263
327
  padWiz = { gi: gp.index, id: gp.id, step: -1, base: null, map: {}, release: false, baseWait: true, axTrack: null, timer: null };
264
- wizSay('Controle novo detectado: ' + gp.id + '. O jogo pausou para você configurá-lo. SOLTE tudo para começar.');
328
+ wizSay(t('pad.wiz.detected', { id: gp.id }));
265
329
  wizDemo(null);
266
330
  const pr = ctx.$('#padwiz-progress');
267
331
  if (pr)
@@ -316,7 +380,7 @@ export function initGamepad(ctx) {
316
380
  padWiz.gi = gp.index;
317
381
  padWiz.id = gp.id;
318
382
  padWiz.baseWait = true;
319
- wizSay('Controle: ' + gp.id + '. Agora SOLTE tudo.');
383
+ wizSay(t('pad.wiz.releaseAll', { id: gp.id }));
320
384
  break;
321
385
  }
322
386
  }
@@ -405,8 +469,10 @@ export function initGamepad(ctx) {
405
469
  openPadWizFor(gp);
406
470
  return;
407
471
  }
408
- const cur = actionsFor(gp);
409
472
  const prev = padPrevAct[gi] || {};
473
+ // ⚠️ A EMPATIA MOTORA APLICADA AO CONTROLE (issue #120). Sem esta linha, uma criança com o modo de
474
+ // um botão ligado e um pad na mão NÃO ESTAVA no modo — e nada em lado nenhum o dizia.
475
+ const cur = umBotaoPorVez(prev, actionsFor(gp), estadoDoJogo.oneButton);
410
476
  if (ctx.isTouchMode() && (cur.left || cur.right || cur.up || cur.down || cur.action2 || cur.action1 || cur.action4 || cur.action3 || cur._start)) {
411
477
  ctx.hideTouchControls(); // botão físico usado -> some o gamepad virtual (mesma regra do teclado)
412
478
  }
@@ -21,16 +21,14 @@ export interface KeyboardRuntimeCtx {
21
21
  /** The live players array (core/state.ts's `players`, mutated in place — never reassigned). */
22
22
  getPlayers(): KeyboardRuntimePlayer[];
23
23
  }
24
- /** Result of the old `applyControls()` mutation, as a value: `controls` + its per-action aliases (game.js's
25
- * KJUMP/KLEFT/KRIGHT/KUP/KDOWN/KRUN) + the flattened `GAME_KEYS` list. */
26
24
  export interface ControlsState {
27
25
  controls: KeyScheme;
28
- action2: string[];
29
- left: string[];
30
- right: string[];
31
- up: string[];
32
- down: string[];
33
- action1: string[];
26
+ action2: readonly string[];
27
+ left: readonly string[];
28
+ right: readonly string[];
29
+ up: readonly string[];
30
+ down: readonly string[];
31
+ action1: readonly string[];
34
32
  gameKeys: string[];
35
33
  }
36
34
  export interface KeyboardRuntime {
@@ -7,6 +7,11 @@
7
7
  // being reassigned here: imported bindings can't be reassigned from outside their own module, so the game.js
8
8
  // wrapper is the one that copies the returned fields onto its own `let`s (same trap `core/state.ts` documents
9
9
  // for `coins`/`players`).
10
+ import { ACTIONS } from '../core/actions.js';
11
+ /** Result of the old `applyControls()` mutation, as a value: `controls` + its per-action aliases (game.js's
12
+ * KJUMP/KLEFT/KRIGHT/KUP/KDOWN/KRUN) + the flattened `GAME_KEYS` list. */
13
+ /** A lista que uma posição SEM ALCANCE devolve. Congelada e partilhada: ninguém deve escrever nela. */
14
+ const VAZIO = Object.freeze([]);
10
15
  // ---------------------------------------------------------------------------------------------
11
16
  // Pure logic (no ctx, no `document` — directly testable with plain KeyScheme fixtures)
12
17
  // ---------------------------------------------------------------------------------------------
@@ -14,8 +19,12 @@
14
19
  * (mirrors ui/settings-controls.ts's keyUsedByOther: a Map built over the scheme, not a re-scan per query). */
15
20
  function buildActionIndex(scheme) {
16
21
  const index = new Map();
17
- for (const action in scheme) {
18
- for (const code of scheme[action]) {
22
+ // ⚠️ O LAÇO PASSOU A SER SOBRE `ACTIONS` E NÃO SOBRE AS CHAVES DO OBJETO (issue #118). São a mesma lista
23
+ // agora que o `KeyScheme` é fechado — mas percorrer `ACTIONS` diz QUAL é a lista, e um esquema que ganhe
24
+ // uma chave a mais por engano deixa de a ver. E o `?? []` é o que trata a AUSÊNCIA DECLARADA: um `null`
25
+ // não é um esquema partido, é um teclado que não alcança aquela posição.
26
+ for (const action of ACTIONS) {
27
+ for (const code of scheme[action] ?? []) {
19
28
  if (!index.has(code))
20
29
  index.set(code, action);
21
30
  }
@@ -65,12 +74,22 @@ export function initKeyboardRuntime(ctx) {
65
74
  function computeControlsState() {
66
75
  const kb = ctx.getKB();
67
76
  const controls = kb.solo; // alias do P1 — SEMPRE kb.solo, mesmo com numPlayers>1 (comportamento original; ver relato)
68
- const { action2, left, right, up, down, action1 } = controls;
77
+ // Os apelidos são LISTAS, nunca `null`: quem os lê faz `.includes(code)` sem perguntar. Uma posição que o
78
+ // esquema não alcança vira lista vazia aqui — «não alcança» e «alcança com zero teclas» valem o mesmo
79
+ // para quem só pergunta se a tecla está lá.
80
+ //
81
+ // ⚠️ E DEVOLVE A MESMA REFERÊNCIA, não uma cópia. A primeira versão fazia `[...]` e um caso reprovou por
82
+ // identidade — corretamente: o apelido é o alias do P1, e um teste que afirma «é o mesmo array» está a
83
+ // afirmar que ninguém interpôs uma cópia entre o esquema vivo e quem o lê. Copiar aqui não custaria nada
84
+ // hoje e passaria a custar no dia em que alguém mutasse a lista no lugar.
85
+ const lista = (a) => controls[a] ?? VAZIO;
86
+ const action2 = lista('action2'), left = lista('left'), right = lista('right');
87
+ const up = lista('up'), down = lista('down'), action1 = lista('action1');
69
88
  const gameKeySet = new Set();
70
89
  ctx.getPlayers().forEach((_, i) => {
71
90
  const scheme = kbFor(i);
72
- for (const action in scheme)
73
- for (const code of scheme[action])
91
+ for (const action of ACTIONS)
92
+ for (const code of scheme[action] ?? [])
74
93
  gameKeySet.add(code);
75
94
  });
76
95
  const gameKeys = gameKeySet.size ? [...gameKeySet] : [...action2, ...left, ...right, ...up, ...down];
@@ -20,6 +20,16 @@ export type KBDefaults = {
20
20
  p4: KeyScheme[];
21
21
  };
22
22
  export declare const KB_SCHEMES4: KeyScheme[];
23
+ /**
24
+ * ⚠️ AS DUAS TABELAS DE TECLADO PASSARAM A SER UMA (issue #118). O solo e a dupla já não são escritos aqui:
25
+ * são CÓPIAS VIVAS do que `input/default-bindings` declara, que é onde o ADR-0096 pôs a decisão.
26
+ *
27
+ * Antes eram duas listas paralelas com oito posições cada, e as oito «coincidiam» — menos uma. A `Space` do
28
+ * jogador 1 em dupla estava numa e não na outra, e o custo era concreto: `ui/webcam.ts` sintetiza `Space`
29
+ * para «olhar para cima = pular», então entrar um segundo jogador tirava o PULO de quem joga com os olhos e
30
+ * deixava o andar. Nada errava em voz alta. Derivar em vez de repetir torna essa divergência impossível de
31
+ * voltar a existir, em vez de a apanhar depois de acontecer.
32
+ */
23
33
  export declare const KB_DEFAULTS: KBDefaults;
24
34
  export declare function loadKB(): KBDefaults;
25
35
  export declare function saveKB(kb: KBDefaults): void;