@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
@@ -10,7 +10,17 @@ import { t } from '../core/i18n.js';
10
10
  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
+ import { tabelaDoPad } from './pad-defaults.js';
13
14
  import { padCur, padPrevAct, padPrevStart, PAD_DEAD } from './state.js';
15
+ // ⚠️ O `oneButton` ENTRA POR IMPORT, e não pelo `ctx` — ao contrário de `input/keydown`, que o recebe por
16
+ // getter. A diferença não é de gosto: o `keydown` foi extraído quando o `oneButton` era um `let` do
17
+ // `game.js`, e a regra da casa manda o que o JOGO reatribui entrar por getter. Hoje ele é `core/state`,
18
+ // ou seja da ENGINE — e um getter obrigaria cada consumidor a lembrar-se de o passar.
19
+ //
20
+ // ⚠️ E ESQUECER É EXATAMENTE O DEFEITO QUE ISTO CONSERTA (issue #120). Um campo opcional no ctx faria a
21
+ // acomodação existir só nos jogos onde alguém se lembrou dela, que é o argumento M3 do ADR-0077 com outro
22
+ // substantivo. Binding vivo de `core/state`: não há por onde falhar.
23
+ import * as estadoDoJogo from '../core/state.js';
14
24
  import * as store from '../platform/storage.js';
15
25
  function bindingAt(map, key) {
16
26
  const v = map[key];
@@ -63,7 +73,7 @@ export function bindActive(gp, bd) {
63
73
  /** Ações do frame para este gamepad. `custom` = mapa salvo pelo wizard para este `gp.id` (null/`_skip` = usa o
64
74
  * mapa PADRÃO da Gamepad API "standard": 0=pulo · 1=especial · 2/5/7=correr · 3=troca · 9=START). Direções
65
75
  * custom caem de volta em stdDirs quando o binding do usuário não está ativo (D-pad/stick continuam vivos). */
66
- export function padActions(gp, custom) {
76
+ export function padActions(gp, custom, tabela = GAMEPAD_STANDARD) {
67
77
  if (custom && !custom._skip) {
68
78
  const A = (k) => bindActive(gp, bindingAt(custom, k));
69
79
  const sd = stdDirs(gp);
@@ -82,7 +92,12 @@ export function padActions(gp, custom) {
82
92
  // correr, enquanto a tabela declara R1 como `rightShoulder` e R2 como `rightTrigger`. O ADR-0086 registrou
83
93
  // esta mudança como o asterisco do seu «zero movimento»: nenhum VERBO muda de botão, mas `run` perde dois
84
94
  // dos seus três. Quem usava R1 para correr sente — e é o preço de os quatro ombros existirem.
85
- const B = GAMEPAD_STANDARD;
95
+ // 📌 A TABELA CHEGA POR PARÂMETRO desde o ADR-0115: é a fábrica da engine COM o padrão deste jogo por cima,
96
+ // resolvida em `input/pad-defaults`. O padrão da assinatura é o da engine, então quem chamava com dois
97
+ // argumentos continua a ler exactamente o que lia.
98
+ // ⚠️ E ela só decide neste ramo, que é o certo: o ramo de cima é o mapa que a CRIANÇA gravou no assistente,
99
+ // e o padrão de um jogo não se sobrepõe a uma escolha dela.
100
+ const B = tabela;
86
101
  const at = (a) => { const i = B[a]; return typeof i === 'number' ? b(i) : false; };
87
102
  return {
88
103
  left: sd.left, right: sd.right, up: sd.up, down: sd.down,
@@ -117,6 +132,56 @@ export function padActions(gp, custom) {
117
132
  * posição não tem o que mapear nela — e perguntar produziria um passo mudo, ou pior, um passo a dizer
118
133
  * `action7` em voz alta.
119
134
  */
135
+ /**
136
+ * AS POSIÇÕES QUE O MODO DE UM BOTÃO NÃO CORTA. Pausar é a SAÍDA, não uma jogada.
137
+ *
138
+ * ⚠️ Cortar o START prenderia a criança dentro da partida — é o mesmo raciocínio que põe «a saída
139
+ * primeiro» no ADR-0044 e que fez a armadilha de foco existir no ADR-0090. Uma acomodação que tranca não
140
+ * é acomodação. Os derivados (`_start`, `_pause`) também passam: eles descrevem o que a raiz faz com
141
+ * estas duas posições, não uma terceira.
142
+ */
143
+ const FORA_DO_CORTE = new Set(['start', 'select', '_start', '_pause']);
144
+ /**
145
+ * O MODO DE UM BOTÃO, APLICADO AO CONTROLE — a metade que faltava da empatia motora (issue #120).
146
+ *
147
+ * ⚠️ ELE VALIA SÓ NO TECLADO. `input/keydown.ts:407` solta todas as outras teclas de jogo quando uma nova
148
+ * chega com o modo ligado; `pollPads` não tinha equivalente nenhum, e `grep oneButton` neste ficheiro
149
+ * devolvia zero. Uma criança que ligasse o modo e tivesse um controle na mão **não estava no modo** — sem
150
+ * erro, sem aviso, sem sintoma, porque as definições continuavam a dizer que estava ligado.
151
+ *
152
+ * ⚠️ AS DIREÇÕES CONTAM, e é isso que torna a regra fiel ao teclado: lá, `isGameKeyCode` inclui as teclas
153
+ * de `p.ctrl`, que são as quatro direções — andar e pular não coexistem. Um filtro que poupasse as
154
+ * direções seria mais confortável e estaria a simular outra deficiência.
155
+ *
156
+ * ⚠️ E A ESCOLHA DE QUEM SOBREVIVE É DIFERENTE DA DO TECLADO, POR NECESSIDADE. No teclado a chegada nova
157
+ * ganha, porque HÁ uma chegada: o evento diz qual é. Um controle é lido por SONDAGEM — o que chega é um
158
+ * retrato, sem ordem. Então mantém-se a que já valia, e só quando ela solta é que a próxima assume. É o
159
+ * que impede o botão de correr de ser cortado porque o polegar encostou noutro, e é a mesma leitura de
160
+ * «segurar» que o ADR-0077 dá.
161
+ *
162
+ * A POLÍTICA é a mesma do `keydown`; a IMPLEMENTAÇÃO não pode ser partilhada hoje porque as formas do
163
+ * estado diferem — lá é um `Set` de códigos de tecla, aqui é um retrato de booleanos por posição. Unificar
164
+ * as duas é trabalho à parte, e escrevê-lo aqui é a alternativa a fingir que não há duas.
165
+ */
166
+ export function umBotaoPorVez(
167
+ // ⚠️ O ANTERIOR É TIPADO PELO QUE ESTA FUNÇÃO LÊ, e não por `PadActions`: o `padPrevAct[gi]` do laço é
168
+ // `PadState`, mais frouxo, e exigir a forma completa obrigaria o chamador a um molde que não descreve o
169
+ // que se passa aqui — só se pergunta «esta chave estava em baixo?».
170
+ anterior, atual, ligado) {
171
+ if (!ligado)
172
+ return atual;
173
+ const cortaveis = Object.keys(atual).filter((k) => !FORA_DO_CORTE.has(k));
174
+ const ativas = cortaveis.filter((k) => atual[k] === true);
175
+ if (ativas.length <= 1)
176
+ return atual;
177
+ // A que já valia tem prioridade; sem nenhuma, a primeira do retrato assume.
178
+ const mantida = ativas.find((k) => anterior[k] === true) ?? ativas[0];
179
+ const saida = { ...atual };
180
+ for (const k of ativas)
181
+ if (k !== mantida)
182
+ saida[k] = false;
183
+ return saida;
184
+ }
120
185
  export const PADWIZ_ORDER = [
121
186
  // Direções primeiro: são o que a criança encontra sem pensar, e acertar as quatro dá confiança para as
122
187
  // outras dez.
@@ -161,13 +226,17 @@ export function initGamepad(ctx) {
161
226
  }
162
227
  return _padMaps.get(id) ?? null;
163
228
  }
164
- function actionsFor(gp) { return padActions(gp, padMapFor(gp.id)); }
229
+ function actionsFor(gp, tabela) { return padActions(gp, padMapFor(gp.id), tabela); }
165
230
  // ----- wizard: anúncio + demo animada (DOM-facing, thin) -----
166
- function wizSay(t) {
231
+ // ⚠️ O PARÂMETRO CHAMAVA-SE `t`, E ERA ELE QUE FECHAVA A PORTA. Dentro desta função o `t` do
232
+ // `core/i18n` estava sombreado, então traduzir uma frase aqui era impossível sem primeiro reparar no
233
+ // sombreamento — e não há erro nenhum a apontá-lo. Foi assim que cinco frases em português cru ficaram a
234
+ // falar dentro do motor (#123): não por decisão, por um nome.
235
+ function wizSay(frase) {
167
236
  const el = ctx.$('#padwiz-prompt');
168
237
  if (el)
169
- el.textContent = t;
170
- ctx.srSay(t);
238
+ el.textContent = frase;
239
+ ctx.srSay(frase);
171
240
  }
172
241
  function wizDemo(k) {
173
242
  const d = ctx.$('#padwiz-demo');
@@ -224,11 +293,12 @@ export function initGamepad(ctx) {
224
293
  return; // fechou ao avançar
225
294
  const acao = PADWIZ_ORDER[padWiz.step];
226
295
  const rotulo = ctx.rotuloDaAcao(acao);
227
- wizSay((padWiz.step + 1) + ' de ' + PADWIZ_ORDER.length + ' — aperte: ' + rotulo);
296
+ wizSay(t('pad.wiz.step', { n: padWiz.step + 1, total: PADWIZ_ORDER.length, acao: rotulo }));
228
297
  wizDemo(acao); // demonstração animada do que a ação FAZ
229
298
  const pr = ctx.$('#padwiz-progress');
299
+ // O travessão da lista vazia fica cru de propósito: é pontuação, não idioma.
230
300
  if (pr)
231
- pr.textContent = 'Mapeados: ' + (Object.keys(padWiz.map).join(' · ') || '—');
301
+ pr.textContent = t('pad.wiz.mapped', { lista: Object.keys(padWiz.map).join(' · ') || '—' });
232
302
  }
233
303
  function wizBind(bd) {
234
304
  if (!padWiz)
@@ -246,7 +316,7 @@ export function initGamepad(ctx) {
246
316
  ov.hidden = false;
247
317
  ctx.frontOverlay(ov);
248
318
  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.');
319
+ wizSay(t('pad.wiz.pressAny'));
250
320
  wizDemo(null);
251
321
  const pr = ctx.$('#padwiz-progress');
252
322
  if (pr)
@@ -261,7 +331,7 @@ export function initGamepad(ctx) {
261
331
  ov.hidden = false;
262
332
  ctx.frontOverlay(ov);
263
333
  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.');
334
+ wizSay(t('pad.wiz.detected', { id: gp.id }));
265
335
  wizDemo(null);
266
336
  const pr = ctx.$('#padwiz-progress');
267
337
  if (pr)
@@ -316,7 +386,7 @@ export function initGamepad(ctx) {
316
386
  padWiz.gi = gp.index;
317
387
  padWiz.id = gp.id;
318
388
  padWiz.baseWait = true;
319
- wizSay('Controle: ' + gp.id + '. Agora SOLTE tudo.');
389
+ wizSay(t('pad.wiz.releaseAll', { id: gp.id }));
320
390
  break;
321
391
  }
322
392
  }
@@ -405,8 +475,18 @@ export function initGamepad(ctx) {
405
475
  openPadWizFor(gp);
406
476
  return;
407
477
  }
408
- const cur = actionsFor(gp);
409
478
  const prev = padPrevAct[gi] || {};
479
+ // ⚠️ O ASSENTO SUBIU PARA AQUI, e a razão é o ADR-0115: a tabela de botões deste jogo é declarada POR
480
+ // ASSENTO, e ela é lida dentro do `actionsFor`. Enquanto o `owner` só se resolvia lá em baixo, por ramo,
481
+ // a leitura acontecia antes de se saber de quem era o controle — e um padrão por assento chegava tarde.
482
+ // 📌 Os ramos abaixo passaram a usar esta constante em vez de recalcularem a mesma linha três vezes.
483
+ const players = ctx.getPlayers();
484
+ const owner = players.findIndex((p) => p.pad === gi);
485
+ // ⚠️ E O CONTROLE AINDA NÃO ATRIBUÍDO (`owner < 0`) LÊ O ASSENTO 0, e não «nenhum»: ele está a produzir
486
+ // arestas na tela do título, e um mapa vazio ali deixaria a criança sem como escolher o próprio jogo.
487
+ // A EMPATIA MOTORA APLICADA AO CONTROLE (issue #120). Sem esta linha, uma criança com o modo de
488
+ // um botão ligado e um pad na mão NÃO ESTAVA no modo — e nada em lado nenhum o dizia.
489
+ const cur = umBotaoPorVez(prev, actionsFor(gp, tabelaDoPad(ctx.getNumPlayers(), owner < 0 ? 0 : owner)), estadoDoJogo.oneButton);
410
490
  if (ctx.isTouchMode() && (cur.left || cur.right || cur.up || cur.down || cur.action2 || cur.action1 || cur.action4 || cur.action3 || cur._start)) {
411
491
  ctx.hideTouchControls(); // botão físico usado -> some o gamepad virtual (mesma regra do teclado)
412
492
  }
@@ -429,11 +509,9 @@ export function initGamepad(ctx) {
429
509
  // de propósito: numa cena que este módulo não conheça (um mapa, uma tela de resultados), o controle
430
510
  // deve navegar como no título — que é o comportamento seguro — em vez de não fazer nada.
431
511
  const rodando = ctx.mundoRodando(), pausado = ctx.menuDePausa();
432
- const players = ctx.getPlayers();
433
512
  if (!rodando && !pausado) {
434
513
  const k = { yes: edge('action2') || startEdge, no: edge('action3'), up: edge('up'), down: edge('down'), left: edge('left'), right: edge('right') };
435
514
  const any = k.yes || k.no || k.up || k.down || k.left || k.right;
436
- const owner = players.findIndex((p) => p.pad === gi);
437
515
  if (ctx.getNumPlayers() > 1 && owner > 0) {
438
516
  if (any)
439
517
  ctx.srSay(t('sr.title.waitP1'));
@@ -444,7 +522,6 @@ export function initGamepad(ctx) {
444
522
  continue;
445
523
  }
446
524
  if (pausado) {
447
- const owner = players.findIndex((p) => p.pad === gi);
448
525
  const pi = owner < 0 ? 0 : owner;
449
526
  if (pauseEdge) {
450
527
  ctx.retomar();
@@ -464,7 +541,6 @@ export function initGamepad(ctx) {
464
541
  continue;
465
542
  }
466
543
  if (rodando) {
467
- const owner = players.findIndex((p) => p.pad === gi);
468
544
  // ===================== O MODO `accessibility` (ADR-0044, item 7) =====================
469
545
  // Com o jogo ANDANDO, o direcional deste jogador dirige a BARRA RÁPIDA e não o personagem. Vem antes
470
546
  // de tudo o que é de jogo, porque enquanto o modo está ligado nada mais deste controle é de jogo.
@@ -519,10 +595,21 @@ export function initGamepad(ctx) {
519
595
  }
520
596
  // A tabela e a guarda do Fácil vêm de input/edges.ts, as MESMAS que keydown e touch usam. Antes eram
521
597
  // seis `if` à mão aqui, seis lá e seis no toque — e o do toque tinha esquecido o `!p.easy`.
598
+ let algumaAresta = false;
522
599
  for (const [act, flag] of EDGE_BY_ACTION) {
600
+ if (edge(act))
601
+ algumaAresta = true;
523
602
  if (edgeAllowed(act, p.easy) && edge(act))
524
603
  p[flag] = true;
525
604
  }
605
+ // 📌 A ARESTA DO CONTROLE, e ela conta MESMO QUANDO O FÁCIL A FILTRA (ADR-0113 cláusula 4): a
606
+ // criança carregou no botão — que a regra do Modo Fácil não levante a bandeira do jogo não muda
607
+ // o facto de o aparelho em uso ser este. Ler a mesma condição do `p[flag]` faria uma criança em
608
+ // Modo Fácil ficar com a alternância do teclado enquanto joga no controle.
609
+ // ⚠️ E É AQUI, no ramo de JOGO, e não nos de menu: `naBarraDe`, o título e a pausa são navegação, e
610
+ // a pergunta que isto alimenta — que alternância vale AGORA — é sobre jogar.
611
+ if (algumaAresta)
612
+ ctx.arestaDoJogador(owner, 'gamepad');
526
613
  }
527
614
  }
528
615
  }
@@ -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,7 +20,43 @@ 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;
34
+ /**
35
+ * O PADRÃO QUE O JOGO QUER, por número de jogadores e por assento (ADR-0115). Parcial: o que ele não disser
36
+ * fica como a fábrica da engine o deixou.
37
+ */
38
+ export type MapeamentoDoTeclado = (jogadores: number, assento: number) => Partial<KeyScheme> | null;
39
+ /**
40
+ * REGISTA O PADRÃO DO JOGO. Chamado uma vez pelo arranque (`boot/create-game`), a partir da declaração.
41
+ *
42
+ * 🔴 REGISTO E NÃO PARÂMETRO, e a razão é um defeito medido em vez de uma preferência. Há DOIS sítios que
43
+ * materializam padrões — `loadKB` e `resetKB` — e o segundo é chamado pelo painel de controles, que não tem a
44
+ * declaração do jogo à mão. Um parâmetro que o painel não passasse faria «restaurar padrões» devolver o mapa
45
+ * da ENGINE por cima do mapa do JOGO: a criança carrega no botão esperando voltar ao que o jogo lhe deu, e
46
+ * volta para outra coisa — num jogo cujo autor escolheu o layout por uma razão de acessibilidade, ela perde
47
+ * essa razão e nada o diz.
48
+ *
49
+ * 📌 É a mesma forma que o `kb` deste ficheiro já tem, e pela mesma justificação: o dono é evidente, e as
50
+ * funções que o gerem vivem todas aqui.
51
+ */
52
+ export declare function registrarMapeamentoDoTeclado(f: MapeamentoDoTeclado | null): void;
53
+ /**
54
+ * A FÁBRICA COM O PADRÃO DO JOGO POR CIMA — a **única** resolução, usada pelo `loadKB` E pelo `resetKB`.
55
+ *
56
+ * ⚠️ Uma função só, e é o ponto inteiro: enquanto eram duas cópias do `JSON.parse(JSON.stringify(...))`, a do
57
+ * `resetKB` não conhecia o jogo e a diferença só aparecia quando uma criança carregava em «restaurar».
58
+ */
59
+ export declare function fabricaComOJogo(): KBDefaults;
24
60
  export declare function loadKB(): KBDefaults;
25
61
  export declare function saveKB(kb: KBDefaults): void;
26
62
  /**
@@ -42,4 +78,10 @@ export declare function initKB(): KBDefaults;
42
78
  /** Troca o mapa inteiro. Só o "restaurar padrões" do painel de controles precisa disto — remapear uma tecla
43
79
  * MUTA o objeto, e reatribuir por engano faria as referências vivas apontarem para o mapa antigo. */
44
80
  export declare function setKB(next: KBDefaults): void;
81
+ /**
82
+ * «RESTAURAR PADRÕES» — e o padrão para onde ela volta é o DO JOGO, não o da engine (ADR-0115).
83
+ *
84
+ * 🔴 Esta linha era `JSON.parse(JSON.stringify(KB_DEFAULTS))`, e com o campo do jogo a existir isso passaria
85
+ * a apagar em silêncio o mapeamento que o jogo escolheu. A criança espera voltar ao que o jogo lhe deu.
86
+ */
45
87
  export declare function resetKB(): KBDefaults;
@@ -6,34 +6,98 @@
6
6
  // e o ADR-0086 §2 diz qual é para a plataforma. Esquemas por contagem de jogadores (solo/p2/p3/p4).
7
7
  // A INSTÂNCIA atual (KB) e o remap ficam no composition root — aqui só config/load/save/reset.
8
8
  import * as store from '../platform/storage.js';
9
+ // ⚠️ A DECLARAÇÃO DOS ESQUEMAS MORA LÁ (ADR-0096), e este ficheiro passou a derivá-los em vez de os repetir
10
+ // — é a união que a issue #118 pede. `default-bindings` é folha (só importa `core/actions`), então não há
11
+ // ciclo: quem depende é o ficheiro de persistência, e não o contrário.
12
+ import { KEYBOARD_SOLO, KEYBOARD_DUO } from './default-bindings.js';
9
13
  const CKEY = 'inclusionist.kbcontrols.v3';
14
+ /**
15
+ * AS SEIS POSIÇÕES QUE UM TECLADO PARTIDO NÃO ALCANÇA, declaradas como ausência (issue #118, decisão do Dev).
16
+ *
17
+ * ⚠️ `null` AQUI É UMA AFIRMAÇÃO, e é o que torna o `KeyScheme` fechado útil em vez de burocrático: quando o
18
+ * teclado é repartido por três ou quatro crianças, não há lugar físico para ombros, gatilhos, start e select
19
+ * de cada uma — o bloco de cada jogador tem oito teclas e acabou. Inventar teclas para preencher seria dar a
20
+ * cada criança um alcance que ela não tem, e o `ui/reach-notice` (#112) diria a coisa errada.
21
+ *
22
+ * O que `null` compra: o aviso de alcance pode dizer, ANTES de a criança começar, quais das ações do jogo o
23
+ * controlo dela não alcança — e o jogo pode decidir não usar essas posições no modo de quatro.
24
+ */
25
+ const SEM_ALCANCE_NO_TECLADO_PARTIDO = Object.freeze({
26
+ leftShoulder: null, leftTrigger: null, rightShoulder: null, rightTrigger: null, start: null, select: null,
27
+ });
10
28
  // 4 esquemas base p/ 3–4 jogadores (modos 3 e 4 têm esquemas SEPARADOS, p3 e p4, editáveis por jogador)
11
29
  export const KB_SCHEMES4 = [
12
- { left: ['KeyA'], right: ['KeyD'], up: ['KeyW'], down: ['KeyS'], action1: ['KeyZ'], action2: ['KeyX'], action4: ['KeyC'], action3: ['KeyV'] },
13
- { left: ['KeyJ'], right: ['KeyL'], up: ['KeyI'], down: ['KeyK'], action1: ['KeyM'], action2: ['Comma'], action4: ['Period'], action3: ['Semicolon', 'Slash'] },
14
- { left: ['ArrowLeft'], right: ['ArrowRight'], up: ['ArrowUp'], down: ['ArrowDown'], action1: ['Home'], action2: ['End'], action4: ['PageUp'], action3: ['PageDown'] },
15
- { left: ['Numpad4'], right: ['Numpad6'], up: ['Numpad8'], down: ['Numpad5'], action1: ['Numpad2'], action2: ['Numpad0'], action4: ['Numpad3'], action3: ['NumpadDecimal'] },
30
+ { left: ['KeyA'], right: ['KeyD'], up: ['KeyW'], down: ['KeyS'], action1: ['KeyZ'], action2: ['KeyX'], action4: ['KeyC'], action3: ['KeyV'], ...SEM_ALCANCE_NO_TECLADO_PARTIDO },
31
+ { left: ['KeyJ'], right: ['KeyL'], up: ['KeyI'], down: ['KeyK'], action1: ['KeyM'], action2: ['Comma'], action4: ['Period'], action3: ['Semicolon', 'Slash'], ...SEM_ALCANCE_NO_TECLADO_PARTIDO },
32
+ { left: ['ArrowLeft'], right: ['ArrowRight'], up: ['ArrowUp'], down: ['ArrowDown'], action1: ['Home'], action2: ['End'], action4: ['PageUp'], action3: ['PageDown'], ...SEM_ALCANCE_NO_TECLADO_PARTIDO },
33
+ { left: ['Numpad4'], right: ['Numpad6'], up: ['Numpad8'], down: ['Numpad5'], action1: ['Numpad2'], action2: ['Numpad0'], action4: ['Numpad3'], action3: ['NumpadDecimal'], ...SEM_ALCANCE_NO_TECLADO_PARTIDO },
16
34
  ];
35
+ /**
36
+ * Cópia PROFUNDA e MUTÁVEL de uma tabela declarada. O remapeamento escreve dentro do esquema vivo, então ele
37
+ * não pode partilhar objeto com a tabela de `input/default-bindings`, que é congelada e é a declaração.
38
+ */
39
+ const vivo = (t) => JSON.parse(JSON.stringify(t));
40
+ /**
41
+ * ⚠️ AS DUAS TABELAS DE TECLADO PASSARAM A SER UMA (issue #118). O solo e a dupla já não são escritos aqui:
42
+ * são CÓPIAS VIVAS do que `input/default-bindings` declara, que é onde o ADR-0096 pôs a decisão.
43
+ *
44
+ * Antes eram duas listas paralelas com oito posições cada, e as oito «coincidiam» — menos uma. A `Space` do
45
+ * jogador 1 em dupla estava numa e não na outra, e o custo era concreto: `ui/webcam.ts` sintetiza `Space`
46
+ * para «olhar para cima = pular», então entrar um segundo jogador tirava o PULO de quem joga com os olhos e
47
+ * deixava o andar. Nada errava em voz alta. Derivar em vez de repetir torna essa divergência impossível de
48
+ * voltar a existir, em vez de a apanhar depois de acontecer.
49
+ */
17
50
  export const KB_DEFAULTS = {
18
- // 1 jogador: WASD + setas; pulo J/Espaço; UJIK como na mão pequena do DOS. Sem Alt/AltGr/Ctrl/Shift.
19
- solo: { left: ['KeyA', 'ArrowLeft'], right: ['KeyD', 'ArrowRight'], up: ['KeyW', 'ArrowUp'], down: ['KeyS', 'ArrowDown'],
20
- action1: ['KeyU'], action2: ['KeyJ', 'Space'], action4: ['KeyI'], action3: ['KeyK'] },
21
- p2: [{ left: ['KeyA'], right: ['KeyD'], up: ['KeyW'], down: ['KeyS'], action1: ['KeyU'], action2: ['KeyJ'], action4: ['KeyI'], action3: ['KeyK'] },
22
- { left: ['ArrowLeft'], right: ['ArrowRight'], up: ['ArrowUp'], down: ['ArrowDown'], action1: ['Numpad8'], action2: ['Numpad5'], action4: ['Numpad9'], action3: ['Numpad6'] }],
23
- p3: JSON.parse(JSON.stringify(KB_SCHEMES4.slice(0, 3))), // modo 3 jogadores (independente do 4)
24
- p4: JSON.parse(JSON.stringify(KB_SCHEMES4)), // modo 4 jogadores
51
+ solo: vivo(KEYBOARD_SOLO),
52
+ p2: KEYBOARD_DUO.map(vivo),
53
+ p3: KB_SCHEMES4.slice(0, 3).map(vivo), // modo 3 jogadores (independente do 4)
54
+ p4: KB_SCHEMES4.map(vivo), // modo 4 jogadores
25
55
  };
26
56
  // dado salvo (parcial): sobrepõe os defaults; p34 é o formato ANTIGO (migra p/ p3+p4).
27
57
  // A FORMA vem de `vocabulary-migration`, que é quem a traduz — declarar aqui outra vez seria a
28
58
  // cópia que o `core/entity` passou o mês a eliminar.
29
59
  import { migrarSalvo } from './vocabulary-migration.js';
30
- // ⚠️ A MIGRAÇÃO DE VOCABULÁRIO MORA NOUTRO FICHEIRO, e a separação é deliberada:
31
- // `input/vocabulary-migration.ts` é o ÚNICO sítio da engine autorizado a dizer `jump`, porque traduzir o
32
- // nome antigo é a função dele. Deixá-la aqui punha o acoplamento num módulo que não é histórico, e o gate
33
- // `action-vocabulary-boundary` reprovou — corretamente. Ver o cabeçalho de lá para saber quando se apaga.
60
+ let mapeamentoDoJogo = null;
61
+ /**
62
+ * REGISTA O PADRÃO DO JOGO. Chamado uma vez pelo arranque (`boot/create-game`), a partir da declaração.
63
+ *
64
+ * 🔴 REGISTO E NÃO PARÂMETRO, e a razão é um defeito medido em vez de uma preferência. Há DOIS sítios que
65
+ * materializam padrões — `loadKB` e `resetKB` — e o segundo é chamado pelo painel de controles, que não tem a
66
+ * declaração do jogo à mão. Um parâmetro que o painel não passasse faria «restaurar padrões» devolver o mapa
67
+ * da ENGINE por cima do mapa do JOGO: a criança carrega no botão esperando voltar ao que o jogo lhe deu, e
68
+ * volta para outra coisa — num jogo cujo autor escolheu o layout por uma razão de acessibilidade, ela perde
69
+ * essa razão e nada o diz.
70
+ *
71
+ * 📌 É a mesma forma que o `kb` deste ficheiro já tem, e pela mesma justificação: o dono é evidente, e as
72
+ * funções que o gerem vivem todas aqui.
73
+ */
74
+ export function registrarMapeamentoDoTeclado(f) { mapeamentoDoJogo = f; }
75
+ /**
76
+ * A FÁBRICA COM O PADRÃO DO JOGO POR CIMA — a **única** resolução, usada pelo `loadKB` E pelo `resetKB`.
77
+ *
78
+ * ⚠️ Uma função só, e é o ponto inteiro: enquanto eram duas cópias do `JSON.parse(JSON.stringify(...))`, a do
79
+ * `resetKB` não conhecia o jogo e a diferença só aparecia quando uma criança carregava em «restaurar».
80
+ */
81
+ export function fabricaComOJogo() {
82
+ const d = JSON.parse(JSON.stringify(KB_DEFAULTS));
83
+ if (!mapeamentoDoJogo)
84
+ return d;
85
+ const aplicar = (alvo, jogadores, assento) => {
86
+ const parcial = mapeamentoDoJogo(jogadores, assento);
87
+ if (parcial)
88
+ Object.assign(alvo, parcial);
89
+ };
90
+ aplicar(d.solo, 1, 0);
91
+ d.p2.forEach((esq, i) => aplicar(esq, 2, i));
92
+ d.p3.forEach((esq, i) => aplicar(esq, 3, i));
93
+ d.p4.forEach((esq, i) => aplicar(esq, 4, i));
94
+ return d;
95
+ }
34
96
  // carrega os esquemas salvos SOBRE os defaults (com migração do dado antigo p34 → p3+p4)
35
97
  export function loadKB() {
36
- const d = JSON.parse(JSON.stringify(KB_DEFAULTS));
98
+ // ⚠️ A PRECEDÊNCIA É ESTA E ESTÁ ESCRITA UMA VEZ: fábrica da engine → padrão do JOGO → remapeamento da
99
+ // CRIANÇA. O que a criança gravou vem sempre por último, porque é a única das três que ela escolheu.
100
+ const d = fabricaComOJogo();
37
101
  // ⚠️ O DADO SALVO ATRAVESSA O TRADUTOR ANTES DE TOCAR NOS PADRÕES. Sem esta linha, um esquema gravado com
38
102
  // as chaves antigas (`run`, `jump`, `swap`, `especial`) seria fundido sobre defaults que já usam
39
103
  // `action1`..`action4`: o objeto ficaria com AS DUAS famílias de chaves, os transportes leriam só as novas,
@@ -77,4 +141,10 @@ export function initKB() { kb = loadKB(); return kb; }
77
141
  /** Troca o mapa inteiro. Só o "restaurar padrões" do painel de controles precisa disto — remapear uma tecla
78
142
  * MUTA o objeto, e reatribuir por engano faria as referências vivas apontarem para o mapa antigo. */
79
143
  export function setKB(next) { kb = next; }
80
- export function resetKB() { store.remove(CKEY); return JSON.parse(JSON.stringify(KB_DEFAULTS)); }
144
+ /**
145
+ * «RESTAURAR PADRÕES» — e o padrão para onde ela volta é o DO JOGO, não o da engine (ADR-0115).
146
+ *
147
+ * 🔴 Esta linha era `JSON.parse(JSON.stringify(KB_DEFAULTS))`, e com o campo do jogo a existir isso passaria
148
+ * a apagar em silêncio o mapeamento que o jogo escolheu. A criança espera voltar ao que o jogo lhe deu.
149
+ */
150
+ export function resetKB() { store.remove(CKEY); return fabricaComOJogo(); }
@@ -9,6 +9,14 @@ export interface KeydownEventLike {
9
9
  altKey: boolean;
10
10
  ctrlKey: boolean;
11
11
  preventDefault(): void;
12
+ /**
13
+ * O navegador viu a pessoa carregar? (ADR-0109) — OPCIONAL, e a opcionalidade é a decisão.
14
+ *
15
+ * ⚠️ Torná-lo obrigatório partiria todos os duplos de teste que já existem, e partiria-os por uma razão
16
+ * falsa: eles descrevem a decisão de teclado, que não depende disto. O que a ausência significa está
17
+ * escrito no `input/origem-sintetica` — «não afirmei nada», cuja resposta é `undefined` e não `'teclado'`.
18
+ */
19
+ isTrusted?: boolean;
12
20
  }
13
21
  /** `keyup` só precisa do código. */
14
22
  export interface KeyupEventLike {
@@ -174,6 +182,7 @@ export declare function isEasyShortcut(code: string, s: KeydownSnapshot): boolea
174
182
  export declare function titleNavOf(code: string, s: KeydownSnapshot, action2: boolean): TitleNav;
175
183
  import type { EventTargetLike } from './touch-bindings.js';
176
184
  import type { DomQuery } from '../core/dom-query.js';
185
+ import type { Transporte } from './transporte-em-uso.js';
177
186
  export { hasNavIntent as hasTitleIntent } from './edges.js';
178
187
  /**
179
188
  * Quem é o dono do modal que esta tecla comanda? Devolve a POSIÇÃO no array, ou -1.
@@ -219,8 +228,37 @@ export interface KeydownCtx {
219
228
  getPlayers: () => readonly KeydownPlayer[];
220
229
  /** `kbRuntime.controlsState()` — memorizado do lado de lá; uma chamada por tecla, como no original. */
221
230
  getControls: () => ControlsSnapshot;
222
- /** `keys` de input/state.ts: `const` mutado in place → entra por VALOR (é sempre o mesmo objeto). */
223
- heldKeys: Set<string>;
231
+ /**
232
+ * O PAR DE `input/state`, E NÃO O CONJUNTO CRU (ADR-0109) — o mesmo corte que o `input/touch-bindings` fez.
233
+ *
234
+ * ⚠️ `ReadonlySet` e não `Set`, e a diferença É a razão de o par existir: LER o conjunto nunca foi o
235
+ * problema; ESCREVER nele apagava a origem. Com o tipo assim, uma escrita crua deixa de compilar — o crivo
236
+ * do inventário passa a ter o compilador do lado dele, em vez de ser a única coisa a segurar a linha.
237
+ */
238
+ readonly heldKeys: ReadonlySet<string>;
239
+ /** Uma tecla foi segurada, e sabe-se por quem. */
240
+ marcarTecla: (code: string, origem: Transporte) => void;
241
+ /**
242
+ * Uma tecla foi segurada e NÃO se sabe por quem — o evento sintético que ninguém assinou.
243
+ *
244
+ * ⚠️ Está no ctx a par das outras duas de propósito: se fosse importada, um consumidor não teria como ver
245
+ * que ela existe, e é justamente ele quem produz os eventos que caem aqui.
246
+ */
247
+ marcarTeclaSemOrigem: (code: string) => void;
248
+ /**
249
+ * ESTA ARESTA É DESTE JOGADOR, E VEIO DAQUI (ADR-0113 cláusula 4, issue #127) — `input/state.arestaDoJogador`.
250
+ *
251
+ * 🔴 CAMPO OBRIGATÓRIO, e a medição é a razão: em 2026-09-09 o autómato do ADR-0109 tinha ZERO alimentadores
252
+ * em produção, logo `entradaDe(i).emUso` respondia `teclado` a toda a gente — para sempre, e sem erro
253
+ * nenhum. Com isso, a recusa da cláusula 3 nunca dispara: a criança que joga por webcam consegue desligar a
254
+ * alternância de que a entrada dela depende, e nada o diz.
255
+ *
256
+ * ⚠️ E É AQUI QUE ELE VALE, e não no teclado que já é o padrão: o evento sintético que a webcam despacha
257
+ * chega carimbado (`input/origem-sintetica`), então é por esta linha que `olhos`/`rosto`/`gestos`/`fala`
258
+ * passam a ser o transporte em uso. Uma tecla premida a sério devolve o teclado, que é a regra 3 do ADR-0109.
259
+ */
260
+ arestaDoJogador: (jogador: number, origem: Transporte) => void;
261
+ soltarTecla: (code: string) => void;
224
262
  /** `let oneButton` do game.js (empatia motora) → getter. */
225
263
  isOneButton: () => boolean;
226
264
  actionOf: (code: string, playerIndex: number) => string | null;
@@ -113,7 +113,9 @@ export function isJumpKey(code, s) {
113
113
  * jogador. NÃO inclui os atalhos do Fácil (o original também não: por isso `easyKey` é somado à parte). */
114
114
  export function isGameKeyCode(code, s) {
115
115
  return s.controls.gameKeys.includes(code)
116
- || s.players.some((p) => !!p.ctrl && Object.values(p.ctrl).some((arr) => arr.includes(code)));
116
+ // `arr &&` porque uma posição sem alcance é `null` desde a #118 — e `null.includes` seria uma exceção
117
+ // no caminho do teclado, ou seja, o jogo a parar de responder a qualquer tecla.
118
+ || s.players.some((p) => !!p.ctrl && Object.values(p.ctrl).some((arr) => !!arr && arr.includes(code)));
117
119
  }
118
120
  /** Os atalhos de acessibilidade do Fácil só existem SOLO (`numPlayers<=1`) e só para o Jogador 1. */
119
121
  export function isEasyShortcut(code, s) {
@@ -138,6 +140,10 @@ export function titleNavOf(code, s, action2) {
138
140
  }
139
141
  /** Alguma intenção foi expressa? Definição única em input/edges; aqui só o nome que este módulo sempre teve. */
140
142
  import { hasNavIntent as hasTitleIntent } from './edges.js';
143
+ // ⚠️ IMPORTADA E NÃO INJECTADA, ao contrário dos três escritores logo abaixo, e a linha que separa os dois é
144
+ // esta: `origemDoEvento` é uma função PURA do evento — não toca estado nenhum que o cartucho possua. Os
145
+ // escritores tocam o `input/state`, que é mutado in-place e partilhado, e é por isso que continuam a entrar.
146
+ import { origemDoEvento } from './origem-sintetica.js';
141
147
  export { hasNavIntent as hasTitleIntent } from './edges.js';
142
148
  /**
143
149
  * Quem é o dono do modal que esta tecla comanda? Devolve a POSIÇÃO no array, ou -1.
@@ -287,7 +293,7 @@ export function initKeydown(ctx) {
287
293
  };
288
294
  }
289
295
  /** A metade IMPURA: pega a decisão pronta e a carimba no mundo. */
290
- function apply(d, code) {
296
+ function apply(d, code, origem) {
291
297
  switch (d.kind) {
292
298
  case 'overlay':
293
299
  if (d.closeId)
@@ -354,8 +360,23 @@ export function initKeydown(ctx) {
354
360
  p[edge] = true;
355
361
  }
356
362
  for (const k of d.releaseKeys)
357
- ctx.heldKeys.delete(k);
358
- ctx.heldKeys.add(code);
363
+ ctx.soltarTecla(k);
364
+ // ⚠️ A ORIGEM CHEGA DO EVENTO E NÃO É INVENTADA AQUI (ADR-0109). `origem` é `undefined` só para o
365
+ // evento sintético que ninguém assinou — e nesse caso a porta estreita APAGA a entrada anterior, em
366
+ // vez de deixar a tecla herdar de quem a segurou da última vez. Ver `marcarTeclaSemOrigem`.
367
+ if (origem)
368
+ ctx.marcarTecla(code, origem);
369
+ else
370
+ ctx.marcarTeclaSemOrigem(code);
371
+ // 📌 A ARESTA ALIMENTA O AUTÓMATO NO MESMO PONTO E SOB A MESMA CONDIÇÃO em que a origem é gravada na
372
+ // tecla — origem desconhecida não é aresta de aparelho nenhum, e inventar-lhe `teclado` faria uma
373
+ // tecla do toque desligar a alternância de quem joga por olhar, sem erro e no meio da partida.
374
+ // ⚠️ Tecla genérica (sem dono) conta para o jogador 1, que é a mesma convenção do `ui/menu-nav`: quem
375
+ // carrega numa tecla que não é de assento nenhum está a jogar no primeiro assento.
376
+ if (origem) {
377
+ const dono = ctx.whichPlayer(code);
378
+ ctx.arestaDoJogador(dono < 0 ? 0 : dono, origem);
379
+ }
359
380
  return;
360
381
  }
361
382
  }
@@ -370,9 +391,11 @@ export function initKeydown(ctx) {
370
391
  const d = decideKeydown(e, snapshot());
371
392
  if (d.preventDefault)
372
393
  e.preventDefault();
373
- apply(d, e.code);
394
+ apply(d, e.code, origemDoEvento(e));
374
395
  }
375
- function onKeyup(e) { ctx.heldKeys.delete(e.code); }
396
+ // ⚠️ SOLTA NOS DOIS. Um `keys.delete` cru deixava a origem para trás, e um mapa que descreve teclas que já
397
+ // ninguém segura responde à alternância com o aparelho errado — sem erro, e só na aresta seguinte.
398
+ function onKeyup(e) { ctx.soltarTecla(e.code); }
376
399
  function attach() {
377
400
  ctx.win.addEventListener('keydown', onKeydown);
378
401
  ctx.win.addEventListener('keyup', onKeyup);