@the-inclusionist/engine 7.0.0 → 8.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/README.md +5 -3
  2. package/app/css/style.css +22 -5
  3. package/app/public/vendor/fonts/fondamento-400-ext.woff2 +0 -0
  4. package/app/public/vendor/fonts/fondamento-400.woff2 +0 -0
  5. package/app/public/vendor/fonts/opendyslexic-400.woff2 +0 -0
  6. package/app/public/vendor/fonts/pressstart-400.woff2 +0 -0
  7. package/app/public/vendor/fonts.css +43 -2
  8. package/dist-pkg/boot/create-game.d.ts +76 -2
  9. package/dist-pkg/boot/create-game.js +271 -13
  10. package/dist-pkg/core/constants.d.ts +0 -28
  11. package/dist-pkg/core/constants.js +34 -17
  12. package/dist-pkg/core/contract.d.ts +39 -0
  13. package/dist-pkg/core/contract.js +36 -0
  14. package/dist-pkg/core/entity.d.ts +47 -4
  15. package/dist-pkg/core/layers.d.ts +18 -0
  16. package/dist-pkg/core/layers.js +18 -0
  17. package/dist-pkg/core/rng.js +14 -4
  18. package/dist-pkg/core/route.d.ts +42 -0
  19. package/dist-pkg/core/route.js +158 -0
  20. package/dist-pkg/core/state.d.ts +10 -1
  21. package/dist-pkg/core/state.js +13 -0
  22. package/dist-pkg/educational/adaptive-engine.d.ts +65 -0
  23. package/dist-pkg/educational/adaptive-engine.js +117 -0
  24. package/dist-pkg/educational/segment-bar.d.ts +96 -0
  25. package/dist-pkg/educational/segment-bar.js +89 -0
  26. package/dist-pkg/i18n/en.js +33 -0
  27. package/dist-pkg/i18n/es.js +33 -0
  28. package/dist-pkg/i18n/pt.js +46 -0
  29. package/dist-pkg/input/default-bindings.d.ts +40 -0
  30. package/dist-pkg/input/default-bindings.js +143 -9
  31. package/dist-pkg/input/gamepad.d.ts +18 -11
  32. package/dist-pkg/input/gamepad.js +75 -9
  33. package/dist-pkg/input/keyboard-runtime.d.ts +6 -8
  34. package/dist-pkg/input/keyboard-runtime.js +24 -5
  35. package/dist-pkg/input/keyboard.d.ts +10 -0
  36. package/dist-pkg/input/keyboard.js +41 -11
  37. package/dist-pkg/input/keydown.d.ts +27 -2
  38. package/dist-pkg/input/keydown.js +20 -6
  39. package/dist-pkg/input/latch-scope.d.ts +70 -0
  40. package/dist-pkg/input/latch-scope.js +110 -0
  41. package/dist-pkg/input/latch-store.d.ts +45 -0
  42. package/dist-pkg/input/latch-store.js +74 -0
  43. package/dist-pkg/input/origem-sintetica.d.ts +44 -0
  44. package/dist-pkg/input/origem-sintetica.js +60 -0
  45. package/dist-pkg/input/pointer.d.ts +61 -0
  46. package/dist-pkg/input/pointer.js +71 -0
  47. package/dist-pkg/input/state.d.ts +86 -1
  48. package/dist-pkg/input/state.js +128 -1
  49. package/dist-pkg/input/touch-bindings.d.ts +11 -2
  50. package/dist-pkg/input/touch-bindings.js +9 -3
  51. package/dist-pkg/input/touch.js +9 -1
  52. package/dist-pkg/input/transporte-em-uso.d.ts +101 -0
  53. package/dist-pkg/input/transporte-em-uso.js +130 -0
  54. package/dist-pkg/input/transports.d.ts +88 -2
  55. package/dist-pkg/input/transports.js +60 -5
  56. package/dist-pkg/input/vocabulary-migration.d.ts +18 -7
  57. package/dist-pkg/platform/audio-earcons.d.ts +29 -2
  58. package/dist-pkg/platform/audio-earcons.js +10 -0
  59. package/dist-pkg/platform/audio-nav.d.ts +1 -1
  60. package/dist-pkg/platform/audio-sonar.d.ts +149 -9
  61. package/dist-pkg/platform/audio-sonar.js +232 -21
  62. package/dist-pkg/platform/guide-intensity.d.ts +40 -0
  63. package/dist-pkg/platform/guide-intensity.js +73 -0
  64. package/dist-pkg/platform/storage.d.ts +30 -0
  65. package/dist-pkg/platform/storage.js +35 -0
  66. package/dist-pkg/platform/tts.js +16 -1
  67. package/dist-pkg/platform/voice-plan.d.ts +90 -0
  68. package/dist-pkg/platform/voice-plan.js +137 -0
  69. package/dist-pkg/render/draw.d.ts +1 -1
  70. package/dist-pkg/render/draw.js +15 -5
  71. package/dist-pkg/render/viz-axes.d.ts +17 -0
  72. package/dist-pkg/render/viz-axes.js +19 -0
  73. package/dist-pkg/render/viz-setters.d.ts +34 -2
  74. package/dist-pkg/render/viz-setters.js +189 -33
  75. package/dist-pkg/render/wheelchair-sprites.d.ts +11 -3
  76. package/dist-pkg/render/wheelchair-sprites.js +10 -4
  77. package/dist-pkg/ui/dom.js +20 -2
  78. package/dist-pkg/ui/fonts.d.ts +47 -1
  79. package/dist-pkg/ui/fonts.js +47 -9
  80. package/dist-pkg/ui/latch-refusal.d.ts +32 -0
  81. package/dist-pkg/ui/latch-refusal.js +60 -0
  82. package/dist-pkg/ui/layout.d.ts +32 -0
  83. package/dist-pkg/ui/layout.js +64 -1
  84. package/dist-pkg/ui/motion-scene.d.ts +41 -0
  85. package/dist-pkg/ui/motion-scene.js +76 -0
  86. package/dist-pkg/ui/panel-shell.d.ts +53 -0
  87. package/dist-pkg/ui/panel-shell.js +103 -0
  88. package/dist-pkg/ui/pause-icons.d.ts +134 -20
  89. package/dist-pkg/ui/pause-icons.js +307 -45
  90. package/dist-pkg/ui/reach-notice.js +8 -0
  91. package/dist-pkg/ui/settings-audio.d.ts +14 -1
  92. package/dist-pkg/ui/settings-audio.js +43 -3
  93. package/dist-pkg/ui/settings-controls.d.ts +66 -6
  94. package/dist-pkg/ui/settings-controls.js +179 -17
  95. package/dist-pkg/ui/settings-empathy.d.ts +15 -0
  96. package/dist-pkg/ui/settings-empathy.js +2 -0
  97. package/dist-pkg/ui/settings-motion.d.ts +25 -19
  98. package/dist-pkg/ui/settings-motion.js +32 -19
  99. package/dist-pkg/ui/settings-motor.d.ts +81 -3
  100. package/dist-pkg/ui/settings-motor.js +118 -9
  101. package/dist-pkg/ui/settings-typo.d.ts +32 -2
  102. package/dist-pkg/ui/settings-typo.js +64 -18
  103. package/dist-pkg/ui/settings-visual.d.ts +21 -1
  104. package/dist-pkg/ui/settings-visual.js +48 -4
  105. package/dist-pkg/ui/shell.d.ts +13 -3
  106. package/dist-pkg/ui/shell.js +3 -4
  107. package/dist-pkg/ui/simulation-refusal.d.ts +32 -0
  108. package/dist-pkg/ui/simulation-refusal.js +57 -0
  109. package/dist-pkg/ui/visual-axes-panel.d.ts +48 -0
  110. package/dist-pkg/ui/visual-axes-panel.js +95 -0
  111. package/dist-pkg/ui/webcam.js +6 -1
  112. package/docs/CREDITS.md +18 -0
  113. package/docs/LICENSES.md +12 -0
  114. package/package.json +24 -4
  115. package/app/public/vendor/fonts/greatvibes-400.woff2 +0 -0
  116. package/app/public/vendor/fonts/ufcook-700.woff2 +0 -0
@@ -1,23 +1,5 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-or-later
2
2
  export declare const LOGICAL_W = 320, LOGICAL_H = 180, TILE = 16;
3
- export declare const COIN_TARGET = 10;
4
- export declare const TUNE: {
5
- jumpVel: number;
6
- waterJump: number;
7
- waterJumpRun: number;
8
- waterStrokeFrames: number;
9
- trampBase: number;
10
- trampMax: number;
11
- gravity: number;
12
- hWalk: number;
13
- hRun: number;
14
- climbSpeed: number;
15
- maxFall: number;
16
- waterMaxFall: number;
17
- hTurbo: number;
18
- ultraJumpVel: number;
19
- };
20
- export declare const JUMP_BASE: number;
21
3
  export declare const ANIM: {
22
4
  walkHold: number;
23
5
  runHold: number;
@@ -53,18 +35,8 @@ export type TileType = {
53
35
  export declare const TILE_TYPES: Record<number, TileType>;
54
36
  /** Machuca ao encostar (lava). */
55
37
  export declare const ehPerigo: (t: number) => boolean;
56
- /** Água: nada-se dentro, e o pulo funciona lá. */
57
- export declare const ehAgua: (t: number) => boolean;
58
- /** Escada: sobe e desce. */
59
- export declare const ehEscada: (t: number) => boolean;
60
38
  /** Trampolim: arremessa para cima. */
61
39
  export declare const ehTrampolim: (t: number) => boolean;
62
- /** Portão: barra até a chave abrir. */
63
- export declare const ehPortao: (t: number) => boolean;
64
- /** A chave que abre o portão. */
65
- export declare const ehChave: (t: number) => boolean;
66
- /** Ar de região secreta: o que a escuridão cobre até alguém entrar. */
67
- export declare const ehSecreto: (t: number) => boolean;
68
40
  export declare const TILE_COLOR: {
69
41
  0: string;
70
42
  1: string;
@@ -3,13 +3,22 @@
3
3
  // TUNE é objeto mutável (o painel de debug ajusta propriedades), mas NUNCA reatribuído → o import const
4
4
  // funciona (mutar propriedade é ok; reatribuir é que quebraria). Ver docs/plano-modularizacao.md.
5
5
  export const LOGICAL_W = 320, LOGICAL_H = 180, TILE = 16;
6
- export const COIN_TARGET = 10;
7
- export const TUNE = {
8
- jumpVel: 3.5, waterJump: 3.5, waterJumpRun: 4, waterStrokeFrames: 30,
9
- trampBase: 5, trampMax: 8, gravity: 0.15, hWalk: 2, hRun: 3, climbSpeed: 1.5,
10
- maxFall: 7, waterMaxFall: 3, hTurbo: 4.5, ultraJumpVel: 10, // E12: power-ups (valores do José)
11
- };
12
- export const JUMP_BASE = TUNE.jumpVel * Math.sqrt(8 / 5); // ~4.43 (altura confortável)
6
+ // ⚠️ `COIN_TARGET` e `TUNE` MUDARAM DE CASA EM 2026-09-07 — foram para `game/tuning.ts` do `game-platformer`
7
+ // (issue #63, etapa B, decisão do Dev). Tinham ZERO importadores dentro desta árvore e TODOS do outro lado
8
+ // da fronteira do pacote: não eram código morto, eram a superfície pública que o cartucho consumia.
9
+ //
10
+ // Uma engine que exporta a GRAVIDADE e a META DE MOEDAS está a decidir que todo jogo é uma plataforma de
11
+ // coletar coisas. `TUNE` é gravidade, velocidade de nado e curso do trampolim — a física DAQUELE jogo; um
12
+ // quiz não tem gravidade. `COIN_TARGET` é o objetivo, que é conteúdo.
13
+ //
14
+ // A etapa C (o segundo consumidor) é o que autorizou o corte: catorze achados, e nenhum reclama estes dois
15
+ // para a engine. O `ui/debug-panel` continua a afinar o `TUNE` ao vivo — ele sempre o recebeu por INJEÇÃO
16
+ // (`TUNE: Tune` no ctx), nunca por importação, e é por isso que a mudança não lhe custou nada.
17
+ // ⚠️ `JUMP_BASE` SAIU EM 2026-09-07 (issue #63, etapa B, decisão do Dev). Não tinha um único consumidor:
18
+ // nem na engine, nem no `game-platformer`, nem nos testes — fora UM, que verificava que
19
+ // `JUMP_BASE === jumpVel * sqrt(8/5)`, ou seja **reafirmava a própria definição**. Um teste que não pode
20
+ // falhar por motivo que importe, a manter viva uma constante que ninguém usa. A própria issue #63 já o
21
+ // nomeava no seu «achado solto».
13
22
  // E15: cadência de animação (ticks por quadro) — regulável ao vivo no painel ?debug=true. Como TUNE, é objeto
14
23
  // mutável (o debug ajusta propriedades) mas NUNCA reatribuído → import const funciona. andar 6; correr 8 (~8fps,
15
24
  // pedido do José); idle 20; swim 24; cling 10; escada 8; flavor ~6s.
@@ -46,18 +55,26 @@ export const TILE_TYPES = {
46
55
  const prop = (t, k) => !!TILE_TYPES[t]?.[k];
47
56
  /** Machuca ao encostar (lava). */
48
57
  export const ehPerigo = (t) => prop(t, 'hazard');
49
- /** Água: nada-se dentro, e o pulo funciona lá. */
50
- export const ehAgua = (t) => prop(t, 'water');
51
- /** Escada: sobe e desce. */
52
- export const ehEscada = (t) => prop(t, 'ladder');
53
58
  /** Trampolim: arremessa para cima. */
54
59
  export const ehTrampolim = (t) => prop(t, 'tramp');
55
- /** Portão: barra até a chave abrir. */
56
- export const ehPortao = (t) => prop(t, 'gate');
57
- /** A chave que abre o portão. */
58
- export const ehChave = (t) => prop(t, 'key');
59
- /** Ar de região secreta: o que a escuridão cobre até alguém entrar. */
60
- export const ehSecreto = (t) => prop(t, 'secreto');
60
+ // ⚠️ `ehAgua`, `ehEscada`, `ehPortao` e `ehSecreto` MUDARAM DE CASA EM 2026-09-07 — foram para
61
+ // `game/tile-flags.ts` do `game-platformer` (issue #63, etapa B). Tinham zero importadores aqui dentro.
62
+ //
63
+ // E as duas que ficaram — `ehPerigo` e `ehTrampolim` — ficaram por um motivo MEDIDO, não por simetria: a
64
+ // engine lê-as, e não por geometria. O `isSolidType` de `core/collision` torna perigo e trampolim SÓLIDOS
65
+ // no modo cadeira de rodas, e perigo sólido no modo cego. É uma regra de ACESSIBILIDADE — a criança em
66
+ // cadeira de rodas não cai no fosso — e essa é da engine.
67
+ //
68
+ // A engine precisa de saber que um tile é PERIGO; não precisa de saber que ele é ESCADA.
69
+ //
70
+ // ⚠️ O QUE FICA POR DECIDIR, e está registado na #63: a `TILE_TYPES` acima continua a carregar as bandeiras
71
+ // `water`, `ladder`, `gate`, `key` e `secreto`, que **nenhum módulo desta árvore lê**. Enquanto elas aqui
72
+ // estiverem, o jogo faz perguntas sobre uma tabela que a engine publica. O fim honesto é a tabela mudar de
73
+ // casa e o jogo declarar os papéis pelo `core/contract` (`roleOf`) — que é o mecanismo que já existe para
74
+ // isso. Não é execução: é a fronteira seguinte, e ela é decisão do Dev.
75
+ // ⚠️ `ehChave` SAIU EM 2026-09-07, pelo mesmo motivo e no mesmo passo que a `JUMP_BASE`: zero consumidores
76
+ // em lado nenhum — engine, testes e cartucho. Os outros predicados de tile ficam por enquanto porque o
77
+ // `game-platformer` os importa; estes dois não eram fronteira mal cortada, eram peso morto.
61
78
  export const TILE_COLOR = {
62
79
  0: '#0a0a14', 1: '#241f38', 2: '#6b6480', 3: '#2f6fae', 4: '#8a5a2b', 5: '#34e29b', 6: '#3a3a46',
63
80
  7: '#7fdcff', 8: '#ffd23f', 9: '#ff5b3a', 10: '#9a8a6f', 11: '#ffe06a', 12: '#3a86ff', 13: '#8a5cff', 14: '#ff6fae',
@@ -174,6 +174,45 @@ export interface GameDeclaration {
174
174
  * o esquecimento passar como se fosse escolha.
175
175
  */
176
176
  world(): WorldScope;
177
+ /**
178
+ * QUANTAS POSIÇÕES ESTE JOGO PRECISA DE SEGURAR AO MESMO TEMPO. Correr + andar + pular são TRÊS; um quiz é
179
+ * UM. (ADR-0104 §A.)
180
+ *
181
+ * ⚠️ É UM EIXO DIFERENTE DE «QUANTAS AÇÕES», e é por não serem o mesmo que existia um ponto cego onde o
182
+ * aviso nunca disparava. Medido: a plataforma declara NOVE ações e o controle de tela tem NOVE lugares,
183
+ * então o `alcance().ok` era verdadeiro e o cartão da #112 nunca aparecia — mas correr, andar e pular ao
184
+ * mesmo tempo são três dedos, e num telemóvel de dois a criança simplesmente não consegue, sem nada em
185
+ * lado nenhum a dizer porquê. Alcançar uma ação e segurá-la junto com outra são perguntas distintas.
186
+ *
187
+ * ⚠️ OBRIGATÓRIO, e a obrigatoriedade É a decisão, na frase do Dev: «os 300 jogos precisam declarar sim!
188
+ * Não declarar é ter a acessibilidade programada no controle pro sorte». Um campo opcional é respondido
189
+ * por SILÊNCIO, e aqui o silêncio decide pela criança — decide-o quem não pensou no assunto. Trezentos
190
+ * jogos a responder deliberadamente é o custo; acessibilidade por sorte é a alternativa.
191
+ *
192
+ * FUNÇÃO e não valor, pela mesma razão que a `topology`: um jogo com fases troca de exigência entre elas —
193
+ * uma fase a pé pede três, a mesma fase num veículo pode pedir uma. Um valor memorizado ficaria defasado
194
+ * em silêncio, que é o defeito que o ADR-0084 nomeou.
195
+ */
196
+ holdsAtOnce(): number;
197
+ /**
198
+ * ESTE JOGO PRECISA DE UM PONTEIRO — posição contínua? (ADR-0112.)
199
+ *
200
+ * Um jogo de desenho precisa; um quiz não. Declarar faz um aparelho sem ponteiro RECUSAR-SE antes de a
201
+ * criança começar, em vez de ela descobrir a meio do primeiro traço.
202
+ *
203
+ * ⚠️ OPCIONAL, E A DIFERENÇA PARA O `holdsAtOnce` LOGO ACIMA É DELIBERADA — copiar a obrigatoriedade dele
204
+ * seria aplicar uma regra cuja premissa não se sustenta aqui. O `holdsAtOnce` é obrigatório porque não tem
205
+ * padrão seguro E porque falha INVISIVELMENTE a quem escreve o jogo: ele tem teclado completo, o jogo corre,
206
+ * e quem descobre o defeito é a criança no telemóvel de dois dedos. Este tem padrão seguro (`false`) e falha
207
+ * VISIVELMENTE — um jogo de desenho que se esqueça de o declarar é inoperável no próprio aparelho de quem o
208
+ * escreve, porque ele também precisaria do ponteiro para o experimentar.
209
+ *
210
+ * Obrigar trezentos jogos a escrever `needsPointer: () => false` cobraria o preço do `holdsAtOnce` sem o
211
+ * motivo dele.
212
+ *
213
+ * FUNÇÃO e não valor, pela mesma razão das outras: uma actividade pode desenhar numa fase e não noutra.
214
+ */
215
+ needsPointer?(): boolean;
177
216
  readonly tick: TickOwner;
178
217
  /** O papel do que está em `at`. É o campo 2, e é o que substitui `roleOf`. */
179
218
  roleAt(at: Spot): Role;
@@ -108,6 +108,42 @@ export function conformanceProblems(d) {
108
108
  else if (w.kind !== 'none')
109
109
  p.push('world: unknown kind');
110
110
  }
111
+ // ⚠️ A MENSAGEM NOMEIA A SAÍDA, como as outras quatro fazem. Um jogo que não declara isto não recebe um
112
+ // padrão — recebe uma frase que diz o que perguntar a si próprio, porque a resposta é do jogo e de mais
113
+ // ninguém. (ADR-0104 §A.)
114
+ if (typeof d.holdsAtOnce !== 'function') {
115
+ // ⚠️ A MENSAGEM NÃO NOMEIA GÊNERO, e o gate de fronteira cobrou-o: a primeira escrita dizia «run+walk+jump
116
+ // is 3, a quiz is 1» e o `engine-boundary` reprovou a palavra «quiz» em linha de CÓDIGO da engine. Ele
117
+ // tinha razão, e a frase ficou melhor: descreve a FORMA da pergunta, que serve aos 300 jogos, em vez de
118
+ // dois exemplos que servem a dois.
119
+ 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)');
120
+ }
121
+ else {
122
+ const n = d.holdsAtOnce();
123
+ if (!Number.isInteger(n) || n < 1) {
124
+ // Zero não é «não usa controle»: um jogo que não segura posição nenhuma não é jogável, e devolver zero
125
+ // faria a aritmética do alcance passar por vacuidade — o mesmo defeito que o `reachable` recusa.
126
+ p.push('holdsAtOnce: must be an integer >= 1 - a game that holds nothing cannot be played');
127
+ }
128
+ }
129
+ // ⚠️ OPCIONAL, MAS NÃO IMPUNE. Ausente é a resposta `false` e não é problema — ver a nota no campo. O que
130
+ // se recusa é declará-lo MAL: um `needsPointer: true` (valor em vez de função) seria sempre verdadeiro por
131
+ // ser um objecto, e um que devolvesse `'sim'` também. Nos dois casos o jogo julgaria ter declarado, o
132
+ // alcance leria uma coisa diferente do que ele quis dizer, e ninguém saberia — que é o defeito silencioso
133
+ // que esta função inteira existe para não deixar acontecer.
134
+ if (d.needsPointer !== undefined) {
135
+ if (typeof d.needsPointer !== 'function') {
136
+ // ⚠️ MENSAGENS SEM A PALAVRA `as`, e o motivo merece uma linha porque volta a morder: o detector de prosa
137
+ // pt-BR do `engine-i18n` casa palavras funcionais isoladas, e `as` é artigo plural em português. Uma
138
+ // mensagem INGLESA que diga «declare it as …» é contada como texto cru e faz o tecto do módulo subir.
139
+ // O cabeçalho daquele gate já admite a aproximação («senão 'mode' casa 'de'»); reescrever a frase custa
140
+ // nada e afrouxar o detector custaria a razão de ele existir.
141
+ p.push('needsPointer: must be a function - write `needsPointer: () => true`, because a game may draw in one phase and not in another');
142
+ }
143
+ else if (typeof d.needsPointer() !== 'boolean') {
144
+ p.push('needsPointer: must return a boolean - a non-boolean would be truthy and refuse devices this game can actually use');
145
+ }
146
+ }
111
147
  if (d.tick !== 'player' && d.tick !== 'clock')
112
148
  p.push('tick: must be "player" or "clock"');
113
149
  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.