@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
@@ -1,4 +1,5 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ import type { Action } from './actions.js';
2
3
  /** Gênero gramatical do nome. `n` = neutro/indefinido (o pt-BR usa o masculino como default nesse caso). */
3
4
  export type Gender = 'm' | 'f' | 'n';
4
5
  /** Um nome que pode ser FALADO (leitor de tela) e SINALIZADO (Libras, que traduz o mesmo texto). */
@@ -174,6 +175,104 @@ export interface GameDeclaration {
174
175
  * o esquecimento passar como se fosse escolha.
175
176
  */
176
177
  world(): WorldScope;
178
+ /**
179
+ * QUANTAS POSIÇÕES ESTE JOGO PRECISA DE SEGURAR AO MESMO TEMPO. Correr + andar + pular são TRÊS; um quiz é
180
+ * UM. (ADR-0104 §A.)
181
+ *
182
+ * ⚠️ É UM EIXO DIFERENTE DE «QUANTAS AÇÕES», e é por não serem o mesmo que existia um ponto cego onde o
183
+ * aviso nunca disparava. Medido: a plataforma declara NOVE ações e o controle de tela tem NOVE lugares,
184
+ * então o `alcance().ok` era verdadeiro e o cartão da #112 nunca aparecia — mas correr, andar e pular ao
185
+ * mesmo tempo são três dedos, e num telemóvel de dois a criança simplesmente não consegue, sem nada em
186
+ * lado nenhum a dizer porquê. Alcançar uma ação e segurá-la junto com outra são perguntas distintas.
187
+ *
188
+ * ⚠️ OBRIGATÓRIO, e a obrigatoriedade É a decisão, na frase do Dev: «os 300 jogos precisam declarar sim!
189
+ * Não declarar é ter a acessibilidade programada no controle pro sorte». Um campo opcional é respondido
190
+ * por SILÊNCIO, e aqui o silêncio decide pela criança — decide-o quem não pensou no assunto. Trezentos
191
+ * jogos a responder deliberadamente é o custo; acessibilidade por sorte é a alternativa.
192
+ *
193
+ * FUNÇÃO e não valor, pela mesma razão que a `topology`: um jogo com fases troca de exigência entre elas —
194
+ * uma fase a pé pede três, a mesma fase num veículo pode pedir uma. Um valor memorizado ficaria defasado
195
+ * em silêncio, que é o defeito que o ADR-0084 nomeou.
196
+ */
197
+ holdsAtOnce(): number;
198
+ /**
199
+ * ESTE JOGO SEGURA ALGUMA TECLA? — e a resposta não é derivável de mais nada. (ADR-0115.)
200
+ *
201
+ * 🔴 A ALTERNÂNCIA EXISTE PARA UMA CRIANÇA CONCRETA: quem não consegue MANTER uma tecla premida carrega uma
202
+ * vez para andar e outra para parar. Num jogo onde nada se segura — um quiz, um tabuleiro, um puzzle de
203
+ * peças — não há nada a travar, e o controle passa a ser uma opção que **não faz nada**. A criança abre o
204
+ * menu de acessibilidade, liga o ajuste de que depende, e não acontece nada: ela aprende que o ajuste está
205
+ * partido. É o botão morto que o ADR-0106 §5 proíbe.
206
+ *
207
+ * ⚠️ E O `holdsAtOnce` ACIMA NÃO RESPONDE ISTO, o que foi o achado que obrigou a este campo: ele conta
208
+ * POSIÇÕES SIMULTÂNEAS e recusa zero, porque zero faria a aritmética do alcance passar por vacuidade. O
209
+ * `consumer-quiz` declara **1 sem segurar coisa nenhuma**. «Um de cada vez» e «um SEGURADO» são o mesmo
210
+ * número, e toda decisão a jusante vinha a ler um número que responde a outra pergunta.
211
+ *
212
+ * ⚠️ OBRIGATÓRIO, e a obrigatoriedade É a decisão — a mesma do `holdsAtOnce`, pela mesma frase do Dev:
213
+ * «não declarar é ter a acessibilidade programada no controle pro sorte». Um campo opcional faria um jogo
214
+ * que ESQUECE a linha perder a alternância em silêncio, e quem paga é a criança com dificuldade motora.
215
+ *
216
+ * 📌 FUNÇÃO e não valor, pelo ADR-0084: um jogo muda de exigência entre fases. A pé segura-se uma direcção;
217
+ * o mesmo jogo dentro de um veículo pode não segurar nada.
218
+ */
219
+ seguraTeclas(): boolean;
220
+ /**
221
+ * ESTE JOGO PRECISA DE UM PONTEIRO — posição contínua? (ADR-0112.)
222
+ *
223
+ * Um jogo de desenho precisa; um quiz não. Declarar faz um aparelho sem ponteiro RECUSAR-SE antes de a
224
+ * criança começar, em vez de ela descobrir a meio do primeiro traço.
225
+ *
226
+ * ⚠️ OPCIONAL, E A DIFERENÇA PARA O `holdsAtOnce` LOGO ACIMA É DELIBERADA — copiar a obrigatoriedade dele
227
+ * seria aplicar uma regra cuja premissa não se sustenta aqui. O `holdsAtOnce` é obrigatório porque não tem
228
+ * padrão seguro E porque falha INVISIVELMENTE a quem escreve o jogo: ele tem teclado completo, o jogo corre,
229
+ * e quem descobre o defeito é a criança no telemóvel de dois dedos. Este tem padrão seguro (`false`) e falha
230
+ * VISIVELMENTE — um jogo de desenho que se esqueça de o declarar é inoperável no próprio aparelho de quem o
231
+ * escreve, porque ele também precisaria do ponteiro para o experimentar.
232
+ *
233
+ * Obrigar trezentos jogos a escrever `needsPointer: () => false` cobraria o preço do `holdsAtOnce` sem o
234
+ * motivo dele.
235
+ *
236
+ * FUNÇÃO e não valor, pela mesma razão das outras: uma actividade pode desenhar numa fase e não noutra.
237
+ */
238
+ needsPointer?(): boolean;
239
+ /**
240
+ * O MAPEAMENTO DE TECLADO QUE ESTE JOGO QUER — por número de jogadores e por assento (ADR-0115).
241
+ *
242
+ * A precedência é a que o registo pede, e ela cabe entre duas linhas que já existiam no `input/keyboard`:
243
+ * **fábrica da engine → padrão do JOGO → remapeamento da CRIANÇA.** Devolver `null` (ou não declarar) deixa
244
+ * a fábrica da engine intacta, que é o comportamento de sempre.
245
+ *
246
+ * PARCIAL de propósito: um jogo que só queira trocar o `action1` troca o `action1`. A fusão já existe — é o
247
+ * `Object.assign` que sobrepõe o dado guardado —, então esta é mais uma camada no mesmo sítio e não uma
248
+ * segunda forma de fundir.
249
+ *
250
+ * ⚠️ OPCIONAL, e aqui, ao contrário do `seguraTeclas`, o silêncio tem um lado seguro: sem declaração o jogo
251
+ * fica com a fábrica da engine, que é jogável e é o que ele já tem hoje. Não há lado errado na ausência.
252
+ *
253
+ * ⚠️ E LEVA O ASSENTO porque o teclado de dois jogadores não é o de um: as setas mudam de dono, e um padrão
254
+ * que não soubesse o assento daria as mesmas teclas a duas crianças. `jogadores` é 1, 2, 3 ou 4; `assento` é
255
+ * o índice dentro desse arranjo.
256
+ *
257
+ * 📌 O irmão do CONTROLE é o campo logo abaixo, e chegou um commit depois: o obstáculo era que o assento
258
+ * ainda não se conhecia no ponto em que a tabela de botões é lida, e a saída foi subi-lo no laço.
259
+ */
260
+ mapeamentoDoTeclado?(jogadores: number, assento: number): Partial<Record<Action, readonly string[] | null>> | null;
261
+ /**
262
+ * O MAPEAMENTO DE BOTÕES QUE ESTE JOGO QUER NO CONTROLE — mesma pergunta, outro aparelho (ADR-0115).
263
+ *
264
+ * Índices de botão da Gamepad API «standard», parciais: `{ action1: 3 }` troca só essa. `null` num botão diz
265
+ * «esta posição não existe neste jogo», que é diferente de a deixar na fábrica.
266
+ *
267
+ * ⚠️ A PRECEDÊNCIA TEM UMA DIFERENÇA DE SÍTIO QUE VALE SABER: no teclado, o que a criança remapeou é uma
268
+ * camada POR CIMA desta; no controle, o mapa que ela gravou no assistente é um RAMO inteiro — se ele existe,
269
+ * este padrão não é consultado. Nos dois casos ela ganha, que é o que importa.
270
+ *
271
+ * 📌 E leva o assento pela mesma razão do teclado, ainda que por um caminho diferente: dois controles são
272
+ * dois aparelhos, mas o JOGO pode querer arranjos distintos por assento (o guarda-redes e o atacante não
273
+ * fazem o mesmo).
274
+ */
275
+ mapeamentoDoPad?(jogadores: number, assento: number): Partial<Record<Action, number | null>> | null;
177
276
  readonly tick: TickOwner;
178
277
  /** O papel do que está em `at`. É o campo 2, e é o que substitui `roleOf`. */
179
278
  roleAt(at: Spot): Role;
@@ -1,6 +1,11 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-or-later
2
2
  // core/contract — OS SETE CAMPOS. A interface que o ADR-0030 escolheu como o eixo da engine.
3
3
  //
4
+ // ⚠️ ESTE FICHEIRO NÃO TINHA IMPORT NENHUM, e o único que ganhou é `import type` — apagado na compilação, logo
5
+ // o módulo publicado continua sem dependência de execução. O que entra é o VOCABULÁRIO das catorze posições
6
+ // (ADR-0074), e ele tinha de entrar: um campo que fala de mapeamento e escrevesse as suas próprias chaves
7
+ // seria a segunda cópia da união que o `core/actions` existe para ser a primeira.
8
+ //
4
9
  // ========================= O QUE ISTO É, E O QUE NÃO É =========================
5
10
  // Não é um framework nem uma classe-base. É a ÚNICA coisa que a pilha de acessibilidade sabe sobre um jogo.
6
11
  // O ADR-0027 mediu que as três funções que MAIS pareciam genéricas eram as três mais amarradas à plataforma —
@@ -108,6 +113,79 @@ export function conformanceProblems(d) {
108
113
  else if (w.kind !== 'none')
109
114
  p.push('world: unknown kind');
110
115
  }
116
+ // ⚠️ A MENSAGEM NOMEIA A SAÍDA, como as outras quatro fazem. Um jogo que não declara isto não recebe um
117
+ // padrão — recebe uma frase que diz o que perguntar a si próprio, porque a resposta é do jogo e de mais
118
+ // ninguém. (ADR-0104 §A.)
119
+ if (typeof d.holdsAtOnce !== 'function') {
120
+ // ⚠️ A MENSAGEM NÃO NOMEIA GÊNERO, e o gate de fronteira cobrou-o: a primeira escrita dizia «run+walk+jump
121
+ // is 3, a quiz is 1» e o `engine-boundary` reprovou a palavra «quiz» em linha de CÓDIGO da engine. Ele
122
+ // tinha razão, e a frase ficou melhor: descreve a FORMA da pergunta, que serve aos 300 jogos, em vez de
123
+ // dois exemplos que servem a dois.
124
+ p.push('holdsAtOnce: missing - declare how many positions are held AT ONCE (three if three fingers must press together, one if commands arrive one at a time)');
125
+ }
126
+ else {
127
+ const n = d.holdsAtOnce();
128
+ if (!Number.isInteger(n) || n < 1) {
129
+ // Zero não é «não usa controle»: um jogo que não segura posição nenhuma não é jogável, e devolver zero
130
+ // faria a aritmética do alcance passar por vacuidade — o mesmo defeito que o `reachable` recusa.
131
+ p.push('holdsAtOnce: must be an integer >= 1 - a game that holds nothing cannot be played');
132
+ }
133
+ }
134
+ // ⚠️ E ESTA É A OUTRA PERGUNTA, que o número acima parecia responder e não responde (ADR-0115). A mensagem
135
+ // diz o que a ausência CUSTA, e não só o que falta: sem ela, um jogo que nada segura oferece um controle de
136
+ // acessibilidade que não faz nada, e um que segura tudo pode não o oferecer a quem depende dele.
137
+ if (typeof d.seguraTeclas !== 'function') {
138
+ p.push('seguraTeclas: missing - declare whether any key is HELD in this game (latching is offered only where something can be held, and a game that holds nothing must not show a control that does nothing)');
139
+ }
140
+ else if (typeof d.seguraTeclas() !== 'boolean') {
141
+ // Um valor não-booleano seria truthy e ofereceria a alternância a toda a gente — o mesmo defeito
142
+ // silencioso que o `needsPointer` recusa logo abaixo, e pela mesma razão.
143
+ p.push('seguraTeclas: must return a boolean - a non-boolean is truthy and would offer latching in a game where nothing is held');
144
+ }
145
+ // ⚠️ OPCIONAL, MAS NÃO IMPUNE. Ausente é a resposta `false` e não é problema — ver a nota no campo. O que
146
+ // se recusa é declará-lo MAL: um `needsPointer: true` (valor em vez de função) seria sempre verdadeiro por
147
+ // ser um objecto, e um que devolvesse `'sim'` também. Nos dois casos o jogo julgaria ter declarado, o
148
+ // alcance leria uma coisa diferente do que ele quis dizer, e ninguém saberia — que é o defeito silencioso
149
+ // que esta função inteira existe para não deixar acontecer.
150
+ if (d.needsPointer !== undefined) {
151
+ if (typeof d.needsPointer !== 'function') {
152
+ // ⚠️ MENSAGENS SEM A PALAVRA `as`, e o motivo merece uma linha porque volta a morder: o detector de prosa
153
+ // pt-BR do `engine-i18n` casa palavras funcionais isoladas, e `as` é artigo plural em português. Uma
154
+ // mensagem INGLESA que diga «declare it as …» é contada como texto cru e faz o tecto do módulo subir.
155
+ // O cabeçalho daquele gate já admite a aproximação («senão 'mode' casa 'de'»); reescrever a frase custa
156
+ // nada e afrouxar o detector custaria a razão de ele existir.
157
+ p.push('needsPointer: must be a function - write `needsPointer: () => true`, because a game may draw in one phase and not in another');
158
+ }
159
+ else if (typeof d.needsPointer() !== 'boolean') {
160
+ p.push('needsPointer: must return a boolean - a non-boolean would be truthy and refuse devices this game can actually use');
161
+ }
162
+ }
163
+ // ⚠️ E O MESMO PARA O MAPEAMENTO, com uma razão própria: aqui um valor em vez de uma função não seria um
164
+ // erro barulhento, seria um mapeamento SILENCIOSAMENTE ignorado — a fábrica da engine ficava, e a criança
165
+ // jogava com um teclado que o autor do jogo julga ter mudado. E devolver algo que não é objecto nem `null`
166
+ // atravessaria o `Object.assign` sem escrever nada, que é a mesma ausência com outra roupa.
167
+ if (d.mapeamentoDoTeclado !== undefined) {
168
+ if (typeof d.mapeamentoDoTeclado !== 'function') {
169
+ p.push('mapeamentoDoTeclado: must be a function - write `mapeamentoDoTeclado: (jogadores, assento) => ({ action1: ["KeyQ"] })`, because the keyboard of two players is not the keyboard of one');
170
+ }
171
+ else {
172
+ const m = d.mapeamentoDoTeclado(1, 0);
173
+ if (m !== null && (typeof m !== 'object' || Array.isArray(m))) {
174
+ p.push('mapeamentoDoTeclado: must return an object or null - anything else is merged into nothing, and this game keeps the engine factory while its author believes otherwise');
175
+ }
176
+ }
177
+ }
178
+ if (d.mapeamentoDoPad !== undefined) {
179
+ if (typeof d.mapeamentoDoPad !== 'function') {
180
+ p.push('mapeamentoDoPad: must be a function - write `mapeamentoDoPad: (jogadores, assento) => ({ action1: 3 })`, because two seats may want different arrangements');
181
+ }
182
+ else {
183
+ const m = d.mapeamentoDoPad(1, 0);
184
+ if (m !== null && (typeof m !== 'object' || Array.isArray(m))) {
185
+ p.push('mapeamentoDoPad: must return an object or null - anything else is merged into nothing, and this game keeps the engine factory while its author believes otherwise');
186
+ }
187
+ }
188
+ }
111
189
  if (d.tick !== 'player' && d.tick !== 'clock')
112
190
  p.push('tick: must be "player" or "clock"');
113
191
  for (const f of ['roleAt', 'nameAt', 'focusOf', 'objectiveOf', 'targetsOf']) {
@@ -1,6 +1,23 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-or-later
2
- /** Ação → lista de códigos físicos (`KeyA`, `ArrowLeft`…). Fonte única: estava triplicado em input/. */
3
- export type KeyScheme = Record<string, string[]>;
2
+ import type { Action } from './actions.js';
3
+ import type { VisualState } from '../render/viz-axes.js';
4
+ /**
5
+ * Ação → lista de códigos físicos (`KeyA`, `ArrowLeft`…). Fonte única: estava triplicado em input/.
6
+ *
7
+ * ⚠️ FECHADO EM `Action` DESDE 2026-09-07 (issue #118, decisão do Dev), e era `Record<string, string[]>`.
8
+ * Enquanto foi aberto, **faltar uma posição não dava erro de compilação** — e foi assim que o esquema de
9
+ * dupla ficou com oito das quatorze, e que a `Space` do jogador 1 desapareceu sem que nada apitasse. O
10
+ * defeito não era de digitação: era de o tipo aceitar um esquema incompleto como se fosse completo.
11
+ *
12
+ * ⚠️ E `null` NÃO É BURACO — é AUSÊNCIA DECLARADA, e é a metade que dá sentido a fechar o tipo. Um teclado
13
+ * partido por quatro pode não ter lugar físico para ombros e gatilhos; dizer `null` afirma isso, e é o que o
14
+ * aviso de alcance (`ui/reach-notice`, issue #112) lê para dizer à criança, ANTES de ela começar, quais das
15
+ * ações do jogo o controlo dela não alcança. Inventar teclas para preencher seria mentir-lhe em silêncio.
16
+ *
17
+ * Quem consome tem de tratar o `null`: `input/keyboard-runtime`, `input/touch-bindings` e
18
+ * `ui/settings-controls` fazem-no, e é isso que impede um `null` de virar um `undefined.includes`.
19
+ */
20
+ export type KeyScheme = Record<Action, readonly string[] | null>;
4
21
  /** Lados da ventosa-aranha: direita, esquerda, teto, chão. */
5
22
  export type ClingSide = 'R' | 'L' | 'U' | 'D';
6
23
  /**
@@ -76,6 +93,26 @@ export interface Player {
76
93
  ctrl: KeyScheme | null;
77
94
  /** Índice do gamepad, ou -1 quando o jogador não tem controle físico. */
78
95
  pad: number;
96
+ /**
97
+ * O ESTADO VISUAL desta criança, em DOIS EIXOS mais a simulação (ADR-0076, issue #104).
98
+ *
99
+ * ⚠️ `import type`, e a seta aponta para cima. `core/` não deveria importar de `render/`, e o tipo do
100
+ * ESTADO discutivelmente devia morar aqui em vez de lá. Fica assim de propósito e declarado: é só tipo,
101
+ * apagado em execução, sem aresta em runtime — e mover o módulo a meio da migração seria churn no ficheiro
102
+ * mais partilhado do repositório. Se `viz-axes` se dividir um dia, o `VisualState` vem para `core/` e as
103
+ * funções de renderização ficam lá.
104
+ */
105
+ visual: VisualState;
106
+ /**
107
+ * @deprecated ⚠️ O ESPELHO LEGADO, e ele morre nesta migração. Enquanto os dois existirem, `setPlayerViz`
108
+ * escreve os DOIS e um gate exige que nunca discordem — é o que deixa cada leitor migrar sozinho, com a
109
+ * árvore verde entre cada passo, em vez de uma passagem única onde não há onde parar.
110
+ *
111
+ * ⚠️ E ELE NÃO CONSEGUE EXPRIMIR O QUE A #104 EXISTE PARA PERMITIR: uma criança com `hc7` E `fix-deuter` ao
112
+ * mesmo tempo não tem chave única que a descreva. Por isso o espelho só sobrevive enquanto os controles
113
+ * ainda escrevem um valor de cada vez; assim que eles passam a escrever por eixo, ele deixa de poder
114
+ * acompanhar e sai.
115
+ */
79
116
  viz: string;
80
117
  easy: boolean;
81
118
  toggleMove: boolean;
@@ -124,9 +161,15 @@ export interface Player {
124
161
  alfWins?: number;
125
162
  /** ui/settings-audio, ui/pause-icons: saída de áudio própria; `null`/ausente = compartilhada. */
126
163
  audioSink?: string | null;
127
- /** platform/audio-nav: temporizadores do sonar de parede e do guia. */
164
+ /**
165
+ * platform/audio-nav: temporizador do sonar de parede.
166
+ *
167
+ * ⚠️ `guideT` SAIU em 2026-09-07 (#84 item 2), e a ausência é a notícia: ele contava os 48 quadros entre
168
+ * dois bipes do guia, e não há mais bipe nenhum para temporizar. O que o substituiu — o grafo de áudio vivo
169
+ * — não é da entidade da engine e por isso não nasce aqui: mora em `PlayerAudioOut`, ao lado do `_ac`, pela
170
+ * mesma regra que o parágrafo acima explica.
171
+ */
128
172
  wnT?: number;
129
- guideT?: number;
130
173
  }
131
174
  /**
132
175
  * Um jogador DEPOIS de `assignControls` — o `ctrl` deixou de ser `null`.
@@ -32,6 +32,24 @@ export declare const Z: {
32
32
  readonly MENU: 30000;
33
33
  readonly MENU_MAX: 39999;
34
34
  readonly TRANSITION: 40000;
35
+ /**
36
+ * ⚠️ A SAÍDA DE EMERGÊNCIA, E ELA FICA ACIMA DA TRANSIÇÃO (decisão do Dev, 2026-09-07).
37
+ *
38
+ * Este comentário dizia que o *skip-link* mora em `CAPTIONS` (26000) — e isso deixava-o ABAIXO de
39
+ * `MENU` (30000). Um «pular para o conteúdo» que um modal cobre não é alcançável, o que é a WCAG 2.4.1 ao
40
+ * contrário: o mecanismo que existe para atravessar blocos repetidos passa a ser mais um bloco.
41
+ * O gate do `z-order-css` já registava o defeito com todas as letras — «o `Z` ERRA, não o CSS» —, e ficou
42
+ * assim durante semanas porque nada o obrigava a mudar.
43
+ *
44
+ * ⚠️ E É ACIMA DA `TRANSITION`, não entre o menu e ela. O Dev pediu 41000 e o motivo é o que sobra depois
45
+ * de tudo o resto falhar: a saída não pode ficar atrás de nada que a pessoa não controla, e um fade de
46
+ * troca de fase é precisamente isso. O custo — o link aparecer por cima de uma animação — é nulo na
47
+ * prática, porque ele só é visível AO FOCO (`top:-60px` até `:focus`), e porque durante a transição já não
48
+ * se tabula: o foco sai da tela antiga e só reaparece na seguinte (ver a regra de foco no ADR).
49
+ *
50
+ * Só o aviso de laço morto (ADR-0054) e o painel de desenvolvimento ficam acima dela.
51
+ */
52
+ readonly SKIP_LINK: 41000;
35
53
  /**
36
54
  * O LAÇO MORREU (ADR-0054). Acima do `TRANSITION` de propósito: quando o quadro lança, o que estiver na
37
55
  * tela — menu aberto, fade a meio, mensagem de fase — está congelado e deixou de importar. O aviso tem de
@@ -49,6 +49,24 @@ export const Z = {
49
49
  MENU: 30000, // menus — RANGE reservado 30000–39999 (níveis aninhados: +1000 por nível)
50
50
  MENU_MAX: 39999,
51
51
  TRANSITION: 40000, // fade/wipe de troca de fase (cobre tudo)
52
+ /**
53
+ * ⚠️ A SAÍDA DE EMERGÊNCIA, E ELA FICA ACIMA DA TRANSIÇÃO (decisão do Dev, 2026-09-07).
54
+ *
55
+ * Este comentário dizia que o *skip-link* mora em `CAPTIONS` (26000) — e isso deixava-o ABAIXO de
56
+ * `MENU` (30000). Um «pular para o conteúdo» que um modal cobre não é alcançável, o que é a WCAG 2.4.1 ao
57
+ * contrário: o mecanismo que existe para atravessar blocos repetidos passa a ser mais um bloco.
58
+ * O gate do `z-order-css` já registava o defeito com todas as letras — «o `Z` ERRA, não o CSS» —, e ficou
59
+ * assim durante semanas porque nada o obrigava a mudar.
60
+ *
61
+ * ⚠️ E É ACIMA DA `TRANSITION`, não entre o menu e ela. O Dev pediu 41000 e o motivo é o que sobra depois
62
+ * de tudo o resto falhar: a saída não pode ficar atrás de nada que a pessoa não controla, e um fade de
63
+ * troca de fase é precisamente isso. O custo — o link aparecer por cima de uma animação — é nulo na
64
+ * prática, porque ele só é visível AO FOCO (`top:-60px` até `:focus`), e porque durante a transição já não
65
+ * se tabula: o foco sai da tela antiga e só reaparece na seguinte (ver a regra de foco no ADR).
66
+ *
67
+ * Só o aviso de laço morto (ADR-0054) e o painel de desenvolvimento ficam acima dela.
68
+ */
69
+ SKIP_LINK: 41000,
52
70
  /**
53
71
  * O LAÇO MORREU (ADR-0054). Acima do `TRANSITION` de propósito: quando o quadro lança, o que estiver na
54
72
  * tela — menu aberto, fade a meio, mensagem de fase — está congelado e deixou de importar. O aviso tem de
@@ -42,10 +42,20 @@ export const createRng = (semente = SEMENTE_PADRAO) => {
42
42
  const reseed = (s) => { _seed = s >>> 0; };
43
43
  return { rnd, randInt, shuffle, reseed };
44
44
  };
45
- // ⚠️ A CORRENTE DO JOGO PRÓPRIO DA ENGINE, e SÓ dela. Existe porque `game/` ainda vive aqui dentro
46
- // (issue #111: o cartucho que ainda não saiu, ADR-0036/ADR-0083). Quando `game/` sair, isto sai com ele.
47
- // UM CONSUMIDOR EXTERNO NÃO DEVE IMPORTAR ESTES QUATRO — são estado partilhado, que é o defeito que a
48
- // fábrica acima conserta. Faça `createRng(suaSemente)`.
45
+ // ⚠️ ESTE BLOCO TINHA DATA DE MORTE ESCRITA, E ELA NÃO CHEGOU — A PREMISSA É QUE MORREU.
46
+ // Dizia: «Existe porque `game/` ainda vive aqui dentro (issue #111). Quando `game/` sair, isto sai com ele.»
47
+ // O `game/` saiu em 2026-09-07 — e saiu para OUTRO REPOSITÓRIO, onde continua a consumir estes quatro pelo
48
+ // pacote. Medido no dia: `@the-inclusionist/game-platformer` importa `rnd`, `randInt` e `shuffle` em OITO
49
+ // módulos (`quiz`, `physics`, `life`, `traffic`, `fractions`, `coins`, `coin-spawning`,
50
+ // `literacy-distractors`) mais o `main.ts`.
51
+ //
52
+ // Ou seja, o que se previu como «sai junto» virou o oposto: saindo, o cartucho tornou-os SUPERFÍCIE
53
+ // PÚBLICA de um pacote publicado. Removê-los agora é quebra de contrato maior, não limpeza.
54
+ //
55
+ // ⚠️ E A ADVERTÊNCIA CONTINUA VERDADEIRA, o que é o incómodo: são estado partilhado de módulo, que é
56
+ // exatamente o defeito que a fábrica acima conserta. Um segundo jogo na mesma página divide esta corrente
57
+ // com o primeiro. O caminho de saída é o consumidor passar a `createRng(suaSemente)` — e isso é migração
58
+ // dele, com aviso e um major, não uma remoção unilateral daqui.
49
59
  const _padrao = createRng(SEMENTE_PADRAO);
50
60
  export const reseed = _padrao.reseed;
51
61
  export const rnd = _padrao.rnd;
@@ -0,0 +1,42 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ import type { Role, Spot, Topology } from './contract.js';
3
+ /**
4
+ * Os papéis que uma rota atravessa. Leitura do `core/contract`, com o `hazard` de fora por decisão declarada
5
+ * no cabeçalho — e `goal` de fora porque ele entra pela regra do último passo, não pela do meio.
6
+ */
7
+ export declare const PAPEIS_ATRAVESSAVEIS: ReadonlySet<Role>;
8
+ /** Este papel deixa passar? */
9
+ export declare function atravessavel(papel: Role): boolean;
10
+ export interface RouteCtx {
11
+ /** Campo 1 do contrato. A métrica decide QUANTOS vizinhos um ponto tem. */
12
+ readonly topology: Topology;
13
+ /** Campo 2 do contrato: o que há neste ponto. Obrigatório na `GameDeclaration`, logo sempre disponível. */
14
+ readonly roleAt: (at: Spot) => Role;
15
+ /**
16
+ * Teto de pontos visitados. Estourou → `null`, que é «não sei».
17
+ *
18
+ * 4096 é ~64×64 numa grade e cobre com folga os tabuleiros que existem hoje; num mapa de plataforma grande
19
+ * ele corta antes de a busca custar um quadro. O número é PARÂMETRO porque o custo aceitável é de quem
20
+ * chama: uma pista por quadro tolera muito menos do que um cálculo ao carregar a fase.
21
+ */
22
+ readonly orcamento?: number;
23
+ }
24
+ export interface Rota {
25
+ /** O PRÓXIMO ponto a pisar — a um passo de onde se está. É isto que uma pista aponta. */
26
+ readonly proximo: Spot;
27
+ /** Qual dos alvos a rota alcançou. Pode não ser o mais próximo em linha reta, e é esse o ponto. */
28
+ readonly ate: Spot;
29
+ /** Quantos passos ao longo do caminho. ⚠️ NÃO é `distance()`, que mede a reta que atravessa parede. */
30
+ readonly passos: number;
31
+ }
32
+ /**
33
+ * A rota de `de` até o mais próximo ALCANÇÁVEL dos `alvos` — largura primeiro, sobre o que se atravessa.
34
+ *
35
+ * `null` quando não há alvo, quando nenhum é alcançável, quando a topologia não tem espaço (`hotspots`), ou
36
+ * quando o orçamento estourou. Os quatro casos são o mesmo para quem chama: **não sei dizer por onde**.
37
+ *
38
+ * ⚠️ «MAIS PRÓXIMO» AQUI É AO LONGO DO CAMINHO, e é a diferença inteira. Um alvo a três células em linha reta
39
+ * do outro lado de uma parede está mais LONGE do que um a oito células por um corredor aberto — e é o segundo
40
+ * que a criança consegue alcançar.
41
+ */
42
+ export declare function rotaAte(ctx: RouteCtx, de: Spot, alvos: readonly Spot[]): Rota | null;
@@ -0,0 +1,158 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // core/route — POR ONDE SE VAI ATÉ LÁ, e não só onde é lá (#84, item 1).
3
+ //
4
+ // ========================= O DEFEITO QUE ISTO EXISTE PARA CONSERTAR =========================
5
+ // A issue #84 escreve-o em duas linhas, e elas são o módulo inteiro:
6
+ //
7
+ // «Sem isso, qualquer pista aponta em LINHA RETA para o alvo, e a linha reta atravessa parede: hoje
8
+ // `alvoMaisProximo()` mede distância pela métrica declarada e `panFor()` só compara `x`, então o guia
9
+ // manda a criança andar para dentro de um bloco sólido e ela não tem como saber por quê.»
10
+ //
11
+ // ⚠️ E ELA NÃO TEM MESMO COMO SABER. Uma criança que vê ignora uma seta que aponta para uma parede sem sequer
12
+ // reparar que a ignorou. Quem depende da pista faz o que ela diz — e depois faz outra vez, porque a pista
13
+ // continua a dizer o mesmo. A pista errada é pior do que pista nenhuma: pista nenhuma deixa-a explorar.
14
+ //
15
+ // ========================= O QUE É «ATRAVESSÁVEL», E POR QUE NÃO É INVENÇÃO MINHA =========================
16
+ // A #84 pede «as direções por onde há AR OU ÁGUA». Essas duas palavras já estão no contrato, e cada `Role`
17
+ // diz de si mesmo se se atravessa — o conjunto abaixo é uma LEITURA do `core/contract`, não uma decisão nova:
18
+ //
19
+ // free «atravessável e sem significado próprio» → o ar
20
+ // water «atravessa-se nadando» → a água
21
+ // climb «muda-se de altura interagindo com isto» → a escada: é POR ELA que se sobe
22
+ // key «satisfaz um gate» → um objeto pousado, não uma barreira
23
+ // structure «cenário: chão, parede, o que sustenta» → NÃO
24
+ // gate «barra até uma condição» → NÃO: barrar é o que ele faz
25
+ // hazard «machuca ao encostar» → NÃO, e é a única onde eu escolhi
26
+ // goal «o que a rodada pede» → caso à parte, ver `alvo` abaixo
27
+ //
28
+ // ⚠️ O `hazard` É A ESCOLHA, e declaro-a: a métrica dele não diz se se atravessa, diz o que custa. Rota que
29
+ // passa por espinho é rota que manda a criança levar dano — e no modo cego e no modo cadeirante o próprio
30
+ // `core/collision.isSolidType` já o trata como sólido, por decisão de acessibilidade. Encaminhar por cima
31
+ // dele contradiria a camada que existe para a proteger. Um jogo em que o espinho seja passagem obrigatória
32
+ // terá de dizê-lo por outro meio; hoje nenhum diz.
33
+ //
34
+ // ========================= O ALVO É SEMPRE PISÁVEL, E O CAMINHO NÃO =========================
35
+ // Um `goal` pode estar declarado numa célula que não se atravessa (uma bandeira dentro de um portão). Recusar
36
+ // entrar nela faria a rota nunca chegar a lado nenhum. A regra é assimétrica de propósito: **o último passo é
37
+ // sempre permitido; os do meio obedecem ao conjunto.** É a diferença entre «entrar no portão» e «atravessar o
38
+ // portão para continuar do outro lado».
39
+ //
40
+ // ========================= O ORÇAMENTO, QUE O PRÓPRIO CONTRATO PEDIU =========================
41
+ // ⚠️ `core/contract` avisa, na sua secção 5: «num espaço CONTÍNUO não há como enumerar os pontos, e num mapa
42
+ // grande enumerar seria caro por quadro». Este módulo não pode ignorar esse aviso só porque é conveniente.
43
+ // Por isso tem TETO de células visitadas e devolve `null` ao estourá-lo. `null` quer dizer **«não sei»**, e
44
+ // não «não há caminho» — quem chamar tem de tratar os dois iguais, que é o que uma pista honesta faz: cala-se.
45
+ //
46
+ // E não importa nada além do contrato: é lógica pura, aferida no project `node`.
47
+ import { distance } from './contract.js';
48
+ /**
49
+ * Os papéis que uma rota atravessa. Leitura do `core/contract`, com o `hazard` de fora por decisão declarada
50
+ * no cabeçalho — e `goal` de fora porque ele entra pela regra do último passo, não pela do meio.
51
+ */
52
+ export const PAPEIS_ATRAVESSAVEIS = new Set(['free', 'water', 'climb', 'key']);
53
+ /** Este papel deixa passar? */
54
+ export function atravessavel(papel) {
55
+ return PAPEIS_ATRAVESSAVEIS.has(papel);
56
+ }
57
+ const ORCAMENTO_PADRAO = 4096;
58
+ /** Chave de um ponto na fila. Arredondada, porque no contínuo os pontos nascem de somas de `passo`. */
59
+ const chave = (s, casas) => s.x.toFixed(casas) + '|' + s.y.toFixed(casas) + '|' + (s.z ?? 0).toFixed(casas);
60
+ /**
61
+ * Os deslocamentos de UM passo, na métrica declarada.
62
+ *
63
+ * ⚠️ `free` (L², espaço contínuo sem passo discreto) NÃO TEM VIZINHOS — e fingir que tem é a aproximação que
64
+ * este módulo faz e declara: ele anda numa GRELHA de lado `unit`, nas oito direções. A alternativa seria não
65
+ * responder nada num jogo de plataforma, que é justamente o gênero da issue. Quem ler uma rota `free` está a
66
+ * ler uma amostragem, não uma trajetória — e é por isso que `passos` é uma contagem de células e não uma
67
+ * medida física.
68
+ */
69
+ function vizinhos(topo, passo) {
70
+ if (topo.kind === 'hotspots')
71
+ return [];
72
+ const dims = topo.size.length;
73
+ const ortogonal = topo.move === 'orthogonal';
74
+ const fora = [];
75
+ const eixos = [-1, 0, 1];
76
+ for (const dx of eixos)
77
+ for (const dy of eixos) {
78
+ for (const dz of dims > 2 ? eixos : [0]) {
79
+ const n = Math.abs(dx) + Math.abs(dy) + Math.abs(dz);
80
+ if (n === 0)
81
+ continue;
82
+ if (ortogonal && n > 1)
83
+ continue; // L¹: a diagonal não existe
84
+ fora.push({ x: dx * passo, y: dy * passo, z: dz * passo });
85
+ }
86
+ }
87
+ return fora;
88
+ }
89
+ /** O ponto cabe na extensão declarada? Grade conta células 0..n−1; contínuo conta unidades 0..n. */
90
+ function dentro(topo, s) {
91
+ if (topo.kind === 'hotspots')
92
+ return false;
93
+ const eixo = [s.x, s.y, s.z ?? 0];
94
+ for (let i = 0; i < topo.size.length; i++) {
95
+ const lim = topo.size[i];
96
+ if (eixo[i] < 0)
97
+ return false;
98
+ if (topo.kind === 'grid' ? eixo[i] > lim - 1 : eixo[i] > lim)
99
+ return false;
100
+ }
101
+ return true;
102
+ }
103
+ /**
104
+ * A rota de `de` até o mais próximo ALCANÇÁVEL dos `alvos` — largura primeiro, sobre o que se atravessa.
105
+ *
106
+ * `null` quando não há alvo, quando nenhum é alcançável, quando a topologia não tem espaço (`hotspots`), ou
107
+ * quando o orçamento estourou. Os quatro casos são o mesmo para quem chama: **não sei dizer por onde**.
108
+ *
109
+ * ⚠️ «MAIS PRÓXIMO» AQUI É AO LONGO DO CAMINHO, e é a diferença inteira. Um alvo a três células em linha reta
110
+ * do outro lado de uma parede está mais LONGE do que um a oito células por um corredor aberto — e é o segundo
111
+ * que a criança consegue alcançar.
112
+ */
113
+ export function rotaAte(ctx, de, alvos) {
114
+ const topo = ctx.topology;
115
+ if (topo.kind === 'hotspots' || alvos.length === 0)
116
+ return null;
117
+ const passo = topo.kind === 'continuous' ? topo.unit : 1;
118
+ if (!(passo > 0))
119
+ return null; // uma unidade de zero faria a fila andar sem sair do sítio
120
+ const casas = topo.kind === 'continuous' ? 4 : 0;
121
+ // Chegou? Na grade é a mesma célula; no contínuo é meio passo, porque a grelha não cai em cima do alvo.
122
+ const tolerancia = topo.kind === 'continuous' ? 0.5 : 0;
123
+ const chegou = (s) => alvos.find((a) => distance(topo, s, a) <= tolerancia) ?? null;
124
+ const alvoAqui = chegou(de);
125
+ if (alvoAqui)
126
+ return { proximo: de, ate: alvoAqui, passos: 0 };
127
+ const saltos = vizinhos(topo, passo);
128
+ const teto = ctx.orcamento ?? ORCAMENTO_PADRAO;
129
+ const vistos = new Set([chave(de, casas)]);
130
+ // Cada item leva o PRIMEIRO passo que o originou — é só isso que a pista precisa de saber no fim.
131
+ let fila = [{ at: de, primeiro: de, passos: 0 }];
132
+ while (fila.length) {
133
+ const proxima = [];
134
+ for (const item of fila) {
135
+ for (const d of saltos) {
136
+ const vizinho = { x: item.at.x + d.x, y: item.at.y + d.y, z: (item.at.z ?? 0) + (d.z ?? 0) };
137
+ if (!dentro(topo, vizinho))
138
+ continue;
139
+ const k = chave(vizinho, casas);
140
+ if (vistos.has(k))
141
+ continue;
142
+ vistos.add(k);
143
+ if (vistos.size > teto)
144
+ return null; // «não sei», e é uma resposta
145
+ const primeiro = item.passos === 0 ? vizinho : item.primeiro;
146
+ // O ÚLTIMO PASSO É SEMPRE PERMITIDO: um alvo pode estar declarado numa célula que não se atravessa.
147
+ const alvo = chegou(vizinho);
148
+ if (alvo)
149
+ return { proximo: primeiro, ate: alvo, passos: item.passos + 1 };
150
+ if (!atravessavel(ctx.roleAt(vizinho)))
151
+ continue;
152
+ proxima.push({ at: vizinho, primeiro, passos: item.passos + 1 });
153
+ }
154
+ }
155
+ fila = proxima;
156
+ }
157
+ return null;
158
+ }
@@ -13,7 +13,14 @@
13
13
  * Agora `EventoDoJogo` é um mapa nome → carga, e `emit`/`on` são genéricos sobre ele. Nome inexistente não
14
14
  * compila; carga errada não compila.
15
15
  *
16
- * ⚠️ POR QUE ISTO EXISTE ANTES DE TER ASSINANTE. Hoje o barramento tem ZERO `on()` em produção (só o teste
16
+ * ✅ ERRATA 2026-09-08: O BARRAMENTO TEM ASSINANTES EM PRODUÇÃO. O parágrafo abaixo dizia «ZERO `on()` em
17
+ * produção (só o teste assina)», e isso deixou de ser verdade no dia em que a engine passou a montar a barra
18
+ * de acessibilidade: o `ui/settings-audio` assina `modoCego` para o botão `#opt-modocego` não mentir o
19
+ * estado, e o `boot/create-game` assina o mesmo evento para a barra montada não mentir o dela. Os dois
20
+ * chegaram pela razão que o parágrafo previa — «painéis que se redesenham quando a criança muda um ajuste» —
21
+ * e chegaram com o contrato já tipado, que era o ponto de ele nascer assim.
22
+ *
23
+ * ⚠️ POR QUE ISTO EXISTIU ANTES DE TER ASSINANTE. Hoje o barramento tem ZERO `on()` em produção (só o teste
17
24
  * assina). Um mecanismo sem uso normalmente é dívida — mas este precisa nascer tipado, não ser retipado
18
25
  * depois: o ADR-0031 já exige que painéis e atividades se redesenhem quando a criança muda idioma ou fonte,
19
26
  * e a casca do `demos` vai reagir a ajuste durante a partida. O primeiro assinante chega com o contrato
@@ -93,6 +100,8 @@ export declare const DEFAULTS: Readonly<{
93
100
  readonly hcOutlineFg: 1;
94
101
  readonly hcOutlineBg: 1;
95
102
  readonly letterCase: "upper";
103
+ readonly calmMode: 0;
104
+ readonly viz: "normal";
96
105
  }>;
97
106
  export declare let modoCego: boolean;
98
107
  export declare function setModoCegoValue(on: boolean): void;
@@ -152,6 +152,19 @@ export const DEFAULTS = Object.freeze({
152
152
  hcOutlineBg: 1,
153
153
  // comunicação (hoje só a caixa da letra; o menu de CAA do ADR-0028 amplia isto)
154
154
  letterCase: 'upper',
155
+ // ⚠️ AS DUAS ÚLTIMAS ENTRARAM EM 2026-09-07 (issue #61), e não por simetria: elas faltavam, e a falta
156
+ // tinha consequência. A marca do ADR-0029 lê `DEFAULTS` e mais nada — regra que continua certa —, e por
157
+ // isso um valor sem padrão nomeado aqui é um valor que a marca NÃO PODE marcar. Cinco dos sete ícones da
158
+ // barra rápida caíam nisso.
159
+ //
160
+ // · `calmMode` — o nível TEA (0 normal · 1 calmo · 2 silencioso). Além de não ter padrão, ele não
161
+ // PERSISTIA: ver a nota de `KEYS.tea` em platform/storage.
162
+ // · `viz` — o modo de visão. `core/entity` declara `viz: string` sem dizer qual é o padrão, e o snapshot
163
+ // da barra fazia `p.viz || ''`. Funcionava por acidente: a cadeia vazia não casa `hc-direto` nem
164
+ // `fix-*`, então os dois ícones ficavam apagados. `'normal'` é o modo que `render/viz-modes` declara
165
+ // com `kind:'normal'` — o que não faz nada —, e passa a ser dito em vez de deduzido.
166
+ calmMode: 0,
167
+ viz: 'normal',
155
168
  });
156
169
  // --- modoCego: MODO CEGO (A12e auditiva). Só as ajudas de áudio — bengala, sonar, guarda de beirada,
157
170
  // narração —, sem tela preta; a simulação de cegueira do Modo Empatia é outra coisa e liga esta por cima.