@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
@@ -0,0 +1,156 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // platform/voice-plan.ts — O PLANO DAS VOZES NEURAIS (ADR-0110), na metade PURA.
3
+ //
4
+ // ========================= O QUE ESTE MÓDULO É =========================
5
+ // O catálogo das quatro vozes que a engine GARANTE, a ordem por que se buscam, e — a parte que mais importa —
6
+ // a resposta honesta a «o que é que esta criança está a ouvir AGORA». Sem rede, sem cache, sem tempo. Quem
7
+ // busca de facto é a outra metade; quem decide O QUE buscar e o que DIZER é esta.
8
+ //
9
+ // ⚠️ POR QUE O CATÁLOGO VIVE AQUI E NÃO NO `TTS_SOURCES`. A tabela do `platform/tts` é indexada por idioma e
10
+ // tem UMA voz por idioma; este catálogo tem QUATRO vozes para TRÊS idiomas, porque o en-US tem duas. Ligá-las
11
+ // hoje mudaria comportamento: o `loadTTS` passaria a pedir `en_US-ryan-medium` à PORTA DO CARTUCHO
12
+ // (ADR-0094), que pode não ter essa voz. A tabela liga-se a isto quando o buscador existir, e nem um dia
13
+ // antes — uma lista ligada a um fornecedor que não a conhece é pior do que uma lista à espera.
14
+ //
15
+ // ⚠️ E QUAL DAS DUAS VOZES EN-US A CRIANÇA OUVE POR PADRÃO NÃO SE DECIDE AQUI. O ADR-0110 deixa-o
16
+ // explicitamente em aberto, e a ORDEM DESTE ARRAY NÃO É UMA PREFERÊNCIA — é a ordem em que o Dev as nomeou.
17
+ // Ler ordem como escolha seria decidir por omissão, que é o defeito que este projeto persegue.
18
+ /**
19
+ * AS QUATRO QUE A ENGINE GARANTE (ADR-0110), decididas pelo Dev em 2026-09-08.
20
+ *
21
+ * ⚠️ TODAS `vits-piper` E NENHUMA `ncnn`, que o ADR-0065 §5 já proibia: `ncnn` é outro MOTOR DE INFERÊNCIA —
22
+ * modelos `.param`/`.bin` e não `.onnx` — e este projeto corre sherpa-onnx. Dois dos links que chegaram em
23
+ * 2026-09-08 apontavam para os espelhos `ncnn`, e é por isso que a proibição tem gate e não só um comentário.
24
+ * ⚠️ E NENHUMA `int8`, pela razão MEDIDA do mesmo §5: no backend WASM o int8 corre ~3× MAIS LENTO que o fp32
25
+ * (RTF 5,6 contra 2,5, do log do próprio Dev) e produz saída incorrecta ou muda. Quantizar não encurta a
26
+ * espera aqui — alonga-a.
27
+ */
28
+ export const VOZES_NEURAIS = Object.freeze([
29
+ Object.freeze({ locale: 'pt-BR', engine: 'piper', voice: 'pt_BR-faber-medium' }),
30
+ Object.freeze({ locale: 'en-US', engine: 'piper', voice: 'en_US-ryan-medium' }),
31
+ Object.freeze({ locale: 'en-US', engine: 'piper', voice: 'en_US-amy-medium' }),
32
+ Object.freeze({ locale: 'es-MX', engine: 'piper', voice: 'es_MX-claude-high' }),
33
+ ]);
34
+ /**
35
+ * DE ONDE VÊM OS MODELOS (ADR-0114), num sítio só.
36
+ *
37
+ * 📏 MEDIDO EM 2026-09-08, e não escolhido: o `piper.ttstool.com` — que o Dev nomeou ao perguntar — serve o
38
+ * próprio runtime da própria origem e busca os modelos aqui. As quatro vozes deste catálogo respondem 200
39
+ * neste endereço, com `Access-Control-Allow-Origin: *`, logo um PWA pode buscá-las de outra origem.
40
+ *
41
+ * ⚠️ UM SÍTIO SÓ É A METADE QUE O REGISTO PEDE. Um endereço repetido no ponto de uso é como o CDN do
42
+ * WebGazer chegou ao `ui/webcam` — escrito à mão, sem política, e sem ninguém a poder mudá-lo de uma vez.
43
+ *
44
+ * ========================= 🔴 MUDOU DE ESPELHO EM 2026-09-09, E A REGRA É «BUSCAR DE ONDE O LEITOR LÊ» ====
45
+ * Era `rhasspy/piper-voices`, medido a 08/09 como o espelho que o `piper.ttstool.com` usa. Depois de o
46
+ * `platform/pesados` passar a descer os modelos no primeiro carregamento, mediu-se o outro lado — o
47
+ * `@mintplex-labs/piper-tts-web@1.0.4` INSTALADO, lido do bundle e não do README — e ele busca em
48
+ * `diffusionstudio/piper-voices`, com uma guarda que recusa qualquer URL fora de `huggingface.co`.
49
+ *
50
+ * 🎯 ENDEREÇOS DIFERENTES SIGNIFICAM CACHE DIFERENTE: a Cache Storage é indexada pela URL do pedido, logo os
51
+ * 241 MB que desciam no primeiro dia NÃO ERAM LIDOS POR NINGUÉM, e a biblioteca descarregava tudo outra vez
52
+ * no dia em que a voz fosse pedida. Até 482 MB num link de escola para uma voz.
53
+ *
54
+ * 📏 E OS DOIS ESPELHOS SERVEM OS MESMOS BYTES — 63 201 294 para o `pt_BR-faber-medium`, caminhos idênticos,
55
+ * `Access-Control-Allow-Origin: *` nos dois. Medido nos dois no mesmo minuto, o que torna esta troca uma
56
+ * correcção de facto e não uma preferência entre fornecedores.
57
+ *
58
+ * ⚠️ A CLÁUSULA DO ADR-0114 NÃO SE MEXE: o host continua nomeado num ponto só. O que mudou foi QUAL, e a
59
+ * regra que decide isso passa a estar escrita para não voltar a divergir — **busca-se de onde o LEITOR lê**.
60
+ * No dia em que a engine for dona do runtime (ADR-0124 cláusula 2), o leitor passa a ser ela e é ela que
61
+ * escolhe; até lá, o único leitor é a porta do cartucho.
62
+ */
63
+ export const HOST_DOS_MODELOS = 'https://huggingface.co/diffusionstudio/piper-voices/resolve/main/';
64
+ /**
65
+ * O CAMINHO DO MODELO, DERIVADO DO IDENTIFICADOR — e não uma segunda tabela.
66
+ *
67
+ * `pt_BR-faber-medium` diz tudo o que o caminho precisa: `pt/pt_BR/faber/medium/pt_BR-faber-medium.onnx`.
68
+ * 🎯 DERIVAR EM VEZ DE TABELAR é a decisão inteira desta função: uma tabela de caminhos ao lado da tabela
69
+ * de vozes seria o mesmo facto escrito duas vezes, e este repositório já pagou isso três vezes — o
70
+ * `DomQuery`, os rótulos de movimento reduzido, as chaves de armazenamento. Duas listas divergem, e
71
+ * divergem uma entrada de cada vez.
72
+ *
73
+ * ⚠️ Devolve `null` para um identificador que não tenha a forma esperada, em vez de montar um caminho
74
+ * torto: uma URL inventada dá 404 na escola, e um `null` dá para reportar antes de sair de casa.
75
+ */
76
+ export function caminhoDoModelo(v) {
77
+ const partes = v.voice.split('-');
78
+ if (partes.length !== 3)
79
+ return null;
80
+ const [locale, nome, qualidade] = partes;
81
+ const idioma = locale.split('_')[0];
82
+ if (!idioma || !nome || !qualidade || idioma === locale)
83
+ return null;
84
+ return `${idioma}/${locale}/${nome}/${qualidade}/${v.voice}.onnx`;
85
+ }
86
+ /** O endereço completo do modelo. `null` quando o identificador não se deixa ler. */
87
+ export function urlDoModelo(v) {
88
+ const caminho = caminhoDoModelo(v);
89
+ return caminho === null ? null : HOST_DOS_MODELOS + caminho;
90
+ }
91
+ /**
92
+ * A CONFIGURAÇÃO da voz, que o motor lê junto com o modelo.
93
+ *
94
+ * 📌 `.onnx.json` e não um segundo caminho: é o mesmo ficheiro com outro sufixo, medido a responder 200 no
95
+ * mesmo sítio. Escrevê-lo como derivação mantém a regra de que o identificador é a única fonte.
96
+ */
97
+ export function urlDaConfig(v) {
98
+ const url = urlDoModelo(v);
99
+ return url === null ? null : url + '.json';
100
+ }
101
+ export const estadoDe = (estados, v) => estados[v.voice] ?? 'ausente';
102
+ /**
103
+ * A ORDEM POR QUE SE BUSCAM: primeiro as do idioma corrente, depois as outras, cada grupo na ordem do
104
+ * catálogo. Só entram as que ainda não estão prontas nem em curso.
105
+ *
106
+ * ⚠️ O IDIOMA CORRENTE PRIMEIRO É A REGRA INTEIRA, e ela é sobre uma criança e não sobre eficiência: as
107
+ * outras são para «conforme interesse do jogador», mas a dela é a que ela precisa AGORA. Buscar em ordem de
108
+ * catálogo faria uma criança brasileira esperar por duas vozes inglesas na rede de uma escola.
109
+ *
110
+ * 📌 `falhou` VOLTA À FILA. Uma escola perde a rede a meio da manhã e recupera-a; uma voz marcada como
111
+ * falhada para sempre seria uma criança sem voz até alguém recarregar a página. Quem chama decide QUANDO
112
+ * tentar de novo — este módulo só diz que ainda há o que buscar.
113
+ */
114
+ export function ordemDeBusca(estados, localeCorrente, catalogo = VOZES_NEURAIS) {
115
+ const porBuscar = catalogo.filter((v) => {
116
+ const e = estadoDe(estados, v);
117
+ return e === 'ausente' || e === 'falhou';
118
+ });
119
+ return [
120
+ ...porBuscar.filter((v) => v.locale === localeCorrente),
121
+ ...porBuscar.filter((v) => v.locale !== localeCorrente),
122
+ ];
123
+ }
124
+ /**
125
+ * QUEM ESTÁ A FALAR, E POR QUE — e esta função é o gate 3 do ADR-0110 em forma de código.
126
+ *
127
+ * ⚠️ O DEFEITO QUE ELA IMPEDE TEM NOME NESTE REPOSITÓRIO: «um recuo que se apresenta como a coisa real é a
128
+ * família do `reflectTTS`». Uma engine que dissesse «voz neural» enquanto o modelo ainda desce faria um adulto
129
+ * concluir que a qualidade que ouve É a qualidade final — e desistir de esperar por uma coisa que já vinha a
130
+ * caminho. Dizer `recuo` e dizer PORQUÊ custa uma linha e é a diferença entre informar e enganar.
131
+ *
132
+ * 📌 A PRIMEIRA VOZ PRONTA DO IDIOMA, e nada mais: onde há duas prontas, esta função NÃO escolhe entre elas —
133
+ * devolve a primeira do catálogo porque tem de devolver alguma, e qual delas a criança prefere é a pergunta
134
+ * que o ADR-0110 deixou em aberto. Ver o cabeçalho.
135
+ */
136
+ export function vozEmUso(estados, localeCorrente, catalogo = VOZES_NEURAIS) {
137
+ const doIdioma = catalogo.filter((v) => v.locale === localeCorrente);
138
+ if (doIdioma.length === 0)
139
+ return { tipo: 'recuo', porque: 'sem-voz-para-o-idioma' };
140
+ const pronta = doIdioma.find((v) => estadoDe(estados, v) === 'pronta');
141
+ if (pronta)
142
+ return { tipo: 'neural', voice: pronta.voice };
143
+ // A PRECEDÊNCIA DOS TRÊS RECUOS, e cada degrau tem a sua razão:
144
+ //
145
+ // ⚠️ «A BUSCAR» GANHA DE «FALHOU» quando as duas coexistem: enquanto alguma ainda vem a caminho, dizer
146
+ // «falhou» seria anunciar uma derrota que ainda não aconteceu, e um adulto desligaria a espera cedo.
147
+ // ⚠️ E «AUSENTE» NÃO SE DISFARÇA DE «A BUSCAR», que era como esta função estava escrita à primeira. Nada
148
+ // começou é diferente de está a caminho: o primeiro é uma engine que ainda não pediu — possivelmente
149
+ // porque ninguém a mandou —, e chamar-lhe «a buscar» esconderia um buscador que nunca arrancou atrás de
150
+ // uma frase tranquilizadora. É o mesmo defeito de forma que o `neuralDisponivel` já custou à #91.
151
+ if (doIdioma.some((v) => estadoDe(estados, v) === 'a-buscar'))
152
+ return { tipo: 'recuo', porque: 'a-buscar' };
153
+ if (doIdioma.some((v) => estadoDe(estados, v) === 'falhou'))
154
+ return { tipo: 'recuo', porque: 'falhou' };
155
+ return { tipo: 'recuo', porque: 'ausente' };
156
+ }
@@ -0,0 +1,28 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ import { type EstadosDasVozes, type VozNeural } from './voice-plan.js';
3
+ import type { RelatorioPesado } from './pesados.js';
4
+ /**
5
+ * O ESTADO DE CADA VOZ A PARTIR DO QUE O BUSCADOR RELATOU.
6
+ *
7
+ * ⚠️ `emCurso` NÃO É COSMÉTICA, E É O QUE IMPEDE ESTA PONTE DE MENTIR NOS DOIS SENTIDOS. O relatório só
8
+ * contém o que JÁ resolveu, logo uma voz por relatar é ambígua: pode estar a caminho ou nunca ter sido
9
+ * pedida. O `voice-plan` recusa-se a confundir as duas por escrito — «ausente não se disfarça de a buscar»,
10
+ * porque isso esconderia um buscador que nunca arrancou atrás de uma frase tranquilizadora — e a única coisa
11
+ * que desfaz a ambiguidade é quem chamou saber se a descarga ainda corre.
12
+ *
13
+ * 📌 E o par contrário importa igualmente: com a descarga TERMINADA, uma voz por relatar é `ausente` e nunca
14
+ * `a-buscar`, senão a interface prometeria para sempre uma coisa que já não vem.
15
+ */
16
+ export declare function estadosDasVozes(relatorio: readonly RelatorioPesado[], opcoes?: {
17
+ readonly emCurso?: boolean;
18
+ }, catalogo?: readonly VozNeural[]): EstadosDasVozes;
19
+ /**
20
+ * AS IDS DAS COISAS PESADAS QUE SÃO VOZ — para quem queira descê-las primeiro, sem adivinhar o formato da id.
21
+ *
22
+ * ⚠️ Existe porque a alternativa é o consumidor escrever `p.id.startsWith('voz:')` no ponto de uso, e aí o
23
+ * formato da id passa a ser contrato público sem nunca ter sido decidido como tal.
24
+ */
25
+ export declare const idsDasVozes: (catalogo?: readonly VozNeural[]) => string[];
26
+ /** Reexportado para quem faz a ponte não ter de importar de dois sítios para responder a uma pergunta. */
27
+ export { vozEmUso, estadoDe } from './voice-plan.js';
28
+ export type { VozEmUso, EstadosDasVozes } from './voice-plan.js';
@@ -0,0 +1,61 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // platform/vozes-prontas.ts — A PONTE ENTRE O QUE DESCEU E O QUE A CRIANÇA OUVE (ADR-0110, gates 3 e 4).
3
+ //
4
+ // ========================= POR QUE ESTE MÓDULO EXISTE =========================
5
+ // 📏 MEDIDO EM 2026-09-09: `vozEmUso` — a função que o ADR-0110 chama «o gate 3 em forma de código» — tinha
6
+ // ZERO leitores em produção, e NADA no repositório produzia um `EstadosDasVozes`. O buscador que nasceu hoje
7
+ // relata `RelatorioPesado[]`. São DUAS linguagens para o mesmo facto, e nenhuma falava com a outra.
8
+ //
9
+ // ⚠️ ISSO NÃO É UM ENCAIXE QUE FALTAVA, É A FORMA DE DEFEITO QUE ESTE REPOSITÓRIO PERSEGUE: um modelo aferido
10
+ // e correcto, sem ninguém a lê-lo, ao lado de um mecanismo que produz a mesma verdade noutro vocabulário. O
11
+ // `render/viz-axes` esteve exactamente assim — «é ÓRFÃO» — e o que isso custou foi a composição dos dois eixos
12
+ // visuais a existir no papel e não na tela.
13
+ //
14
+ // ========================= 🎯 A REGRA QUE ESTE MÓDULO CARREGA, E É UMA SÓ =========================
15
+ // **UMA VOZ SÓ ESTÁ PRONTA QUANDO OS DOIS FICHEIROS DESCERAM.** O piper recusa-se a falar sem o `.onnx.json`,
16
+ // então uma voz com o modelo e sem a configuração é uma voz que NÃO FALA — e marcá-la `pronta` faria a engine
17
+ // anunciar «voz neural» e ficar muda. É pior do que a voz em falta, porque a primeira parece resolvida, e é
18
+ // literalmente a família do `reflectTTS` que o ADR-0110 nomeia ao pedir o gate 3.
19
+ import { VOZES_NEURAIS } from './voice-plan.js';
20
+ /** As duas ids que o catálogo dá a uma voz. Derivadas, não tabeladas — a tabela vive no catálogo. */
21
+ const idsDe = (v) => [`voz:${v.voice}`, `voz:${v.voice}:cfg`];
22
+ /**
23
+ * O ESTADO DE CADA VOZ A PARTIR DO QUE O BUSCADOR RELATOU.
24
+ *
25
+ * ⚠️ `emCurso` NÃO É COSMÉTICA, E É O QUE IMPEDE ESTA PONTE DE MENTIR NOS DOIS SENTIDOS. O relatório só
26
+ * contém o que JÁ resolveu, logo uma voz por relatar é ambígua: pode estar a caminho ou nunca ter sido
27
+ * pedida. O `voice-plan` recusa-se a confundir as duas por escrito — «ausente não se disfarça de a buscar»,
28
+ * porque isso esconderia um buscador que nunca arrancou atrás de uma frase tranquilizadora — e a única coisa
29
+ * que desfaz a ambiguidade é quem chamou saber se a descarga ainda corre.
30
+ *
31
+ * 📌 E o par contrário importa igualmente: com a descarga TERMINADA, uma voz por relatar é `ausente` e nunca
32
+ * `a-buscar`, senão a interface prometeria para sempre uma coisa que já não vem.
33
+ */
34
+ export function estadosDasVozes(relatorio, opcoes = {}, catalogo = VOZES_NEURAIS) {
35
+ const porId = new Map(relatorio.map((r) => [r.id, r]));
36
+ const saida = {};
37
+ for (const v of catalogo) {
38
+ const partes = idsDe(v).map((id) => porId.get(id));
39
+ if (partes.some((p) => p && (p.estado === 'falhou' || p.estado === 'sem-fonte'))) {
40
+ // 📌 UMA METADE FALHADA CHEGA PARA A VOZ INTEIRA FALHAR, e é o mesmo argumento da regra do cabeçalho
41
+ // visto do outro lado: sem os dois ficheiros não há fala nenhuma, logo não há meia voz para oferecer.
42
+ saida[v.voice] = 'falhou';
43
+ continue;
44
+ }
45
+ if (partes.every((p) => p && (p.estado === 'baixado' || p.estado === 'ja-tinha'))) {
46
+ saida[v.voice] = 'pronta';
47
+ continue;
48
+ }
49
+ saida[v.voice] = opcoes.emCurso ? 'a-buscar' : 'ausente';
50
+ }
51
+ return Object.freeze(saida);
52
+ }
53
+ /**
54
+ * AS IDS DAS COISAS PESADAS QUE SÃO VOZ — para quem queira descê-las primeiro, sem adivinhar o formato da id.
55
+ *
56
+ * ⚠️ Existe porque a alternativa é o consumidor escrever `p.id.startsWith('voz:')` no ponto de uso, e aí o
57
+ * formato da id passa a ser contrato público sem nunca ter sido decidido como tal.
58
+ */
59
+ export const idsDasVozes = (catalogo = VOZES_NEURAIS) => catalogo.flatMap((v) => [...idsDe(v)]);
60
+ /** Reexportado para quem faz a ponte não ter de importar de dois sítios para responder a uma pergunta. */
61
+ export { vozEmUso, estadoDe } from './voice-plan.js';
@@ -65,7 +65,7 @@ interface PowerupWithSprite {
65
65
  * `sq`/`sqT` são o squash&stretch (amplitude e relógio, escritos por render/fx.setSquash); `easy` desenha a
66
66
  * hitbox de coleta tolerante translúcida; `viz` é o modo de visão, que recolore por viewport.
67
67
  */
68
- export type DrawPlayer = AnimPlayer & PlayerView<'i' | 'x' | 'y' | 'facing' | 'hurtTimer' | 'sq' | 'sqT' | 'easy' | 'viz' | 'sprite'>;
68
+ export type DrawPlayer = AnimPlayer & PlayerView<'i' | 'x' | 'y' | 'facing' | 'hurtTimer' | 'sq' | 'sqT' | 'easy' | 'visual' | 'sprite'>;
69
69
  /** Flags de Movimento Reduzido que o DESENHO consulta (o objeto `rm` do game.js tem mais chaves). */
70
70
  export interface ReducedMotion {
71
71
  items?: boolean;
@@ -67,7 +67,7 @@ const rnd = rngDecoracao.rnd;
67
67
  import { JUICE, easeOut3, shakeAmp, drawFx } from './fx.js';
68
68
  import { criarCamera } from './camera.js';
69
69
  import { drawCane, drawRunCane, drawChair } from './wheelchair-sprites.js';
70
- import { VIZ_BY_KEY } from './viz-modes.js';
70
+ import { chaveDeTextura, ehBaixaVisao } from './viz-axes.js';
71
71
  // (`game/powerups` e `game/coin-spawning` SAÍRAM daqui no item 19 — ver o bloco "ITENS DECLARADOS" abaixo.)
72
72
  import { drawWeather } from './weather.js';
73
73
  import { choosePlayerFrame } from './player-anim.js';
@@ -121,8 +121,11 @@ export function initDraw(ctx) {
121
121
  dt, dir, wheelchair: ctx.isWheelchair(), held: ctx.held, rnd, tex: ctx.playerTextures(),
122
122
  });
123
123
  // solo/default; no MP o drawFrame troca a textura por viewport (applySharedTextures)
124
+ // ⚠️ `chaveDeTextura` E NÃO A ASSINATURA DE `playerVizTex`. Medido na etapa 0 da #104: aquela função só
125
+ // age quando existe `DIRECT_CFG[mode]`, e devolve a textura como veio para todo o resto. Então o que
126
+ // muda é o CHAMADOR — a metade do estado que interessa à textura — e a porta fica onde estava.
124
127
  if (pl.sprite)
125
- pl.sprite.texture = ctx.playerVizTex(tx, pl.viz);
128
+ pl.sprite.texture = ctx.playerVizTex(tx, chaveDeTextura(pl.visual));
126
129
  return tx;
127
130
  }
128
131
  /* ===================== o quadro ===================== */
@@ -187,12 +190,19 @@ export function initDraw(ctx) {
187
190
  }
188
191
  else {
189
192
  // Otimização: se TODOS estão no mesmo modo (caso comum), troca as texturas UMA vez; senão, por viewport.
190
- const v0 = PLS[0].viz, allSame = PLS.every((p) => p.viz === v0);
191
- const anyOverlay = PLS.some((p) => { const m = VIZ_BY_KEY[p.viz]; return !!m && m.kind === 'lowvision'; });
193
+ // ⚠️ A COMPARAÇÃO PASSOU A SER PELA CHAVE DE TEXTURA, e isso é output-preservador e mais barato ao
194
+ // mesmo tempo. `applySharedTextures` só usa o modo para escolher TEXTURA (`worldTexFor`,
195
+ // `parallaxTexFor`, `treeTexFor`, `spriteTexFor`, `pupTexFor`), e todas devolvem a base para o que não
196
+ // está em `DIRECT_CFG` — conferido em `high-contrast.worldTexFor`. Antes, dois jogadores em `normal` e
197
+ // `fix-deuter` contavam como modos DIFERENTES e disparavam uma re-aplicação de texturas que produzia
198
+ // exactamente as mesmas texturas. Agora contam como iguais, porque para a textura eles são.
199
+ const v0 = chaveDeTextura(PLS[0].visual);
200
+ const allSame = PLS.every((p) => chaveDeTextura(p.visual) === v0);
201
+ const anyOverlay = PLS.some((p) => ehBaixaVisao(p.visual));
192
202
  if (allSame)
193
203
  ctx.applySharedTextures(v0);
194
204
  for (let i = 0, n = ctx.getNumPlayers(); i < n; i++) {
195
- const viz = PLS[i].viz;
205
+ const viz = chaveDeTextura(PLS[i].visual);
196
206
  if (!allSame)
197
207
  ctx.applySharedTextures(viz); // só troca por viewport quando os modos diferem
198
208
  const itens2 = ctx.getItemSprites();
@@ -80,6 +80,23 @@ export declare function proximaCorrecao(v: VisualState): VisualState;
80
80
  * uma cegueira simulada apaga a tela inteira, e nesse instante o tema não muda nada do que ela percebe.
81
81
  */
82
82
  export declare function chaveDeTextura(v: VisualState): string;
83
+ /**
84
+ * A CHAVE ÚNICA que melhor descreve este estado no vocabulário ANTIGO — para quem só sabe ler uma.
85
+ *
86
+ * ⚠️ NÃO É A `chaveDeTextura`, e a diferença custou um gate vermelho para aparecer. A de textura devolve
87
+ * `normal` para uma correção de cor, porque correção não muda textura nenhuma — e usá-la como espelho faria
88
+ * uma criança em `fix-deuter` passar a gravar `'normal'` na chave legada. **Um leitor antigo perderia a
89
+ * correção dela**, que é exactamente o estrago que a migração inteira existe para não cometer.
90
+ *
91
+ * A ordem é simulação → tema → correção → padrão, e ela preserva TODO ajuste que já existia: nenhum estado
92
+ * antigo tinha dois eixos, então nenhum deles perde nada aqui.
93
+ *
94
+ * ⚠️ O ÚNICO CASO COM PERDA É O NOVO — `hc7 + fix-deuter` só cabe como uma das duas metades, e a escolhida é
95
+ * o tema. Não há regressão possível nisso: esse estado NÃO EXISTIA antes, e um leitor que só entende uma
96
+ * chave nunca soube exprimi-lo. Quem quiser as duas metades lê a chave nova, que existe precisamente para
97
+ * isso.
98
+ */
99
+ export declare function chaveLegada(v: VisualState): string;
83
100
  /**
84
101
  * Traduz o valor salvo. Aceita o antigo (string) e o novo (objeto), e devolve sempre um estado válido.
85
102
  *
@@ -122,6 +122,25 @@ export function proximaCorrecao(v) {
122
122
  export function chaveDeTextura(v) {
123
123
  return v.simulacao ?? temaDireto(v) ?? 'normal';
124
124
  }
125
+ /**
126
+ * A CHAVE ÚNICA que melhor descreve este estado no vocabulário ANTIGO — para quem só sabe ler uma.
127
+ *
128
+ * ⚠️ NÃO É A `chaveDeTextura`, e a diferença custou um gate vermelho para aparecer. A de textura devolve
129
+ * `normal` para uma correção de cor, porque correção não muda textura nenhuma — e usá-la como espelho faria
130
+ * uma criança em `fix-deuter` passar a gravar `'normal'` na chave legada. **Um leitor antigo perderia a
131
+ * correção dela**, que é exactamente o estrago que a migração inteira existe para não cometer.
132
+ *
133
+ * A ordem é simulação → tema → correção → padrão, e ela preserva TODO ajuste que já existia: nenhum estado
134
+ * antigo tinha dois eixos, então nenhum deles perde nada aqui.
135
+ *
136
+ * ⚠️ O ÚNICO CASO COM PERDA É O NOVO — `hc7 + fix-deuter` só cabe como uma das duas metades, e a escolhida é
137
+ * o tema. Não há regressão possível nisso: esse estado NÃO EXISTIA antes, e um leitor que só entende uma
138
+ * chave nunca soube exprimi-lo. Quem quiser as duas metades lê a chave nova, que existe precisamente para
139
+ * isso.
140
+ */
141
+ export function chaveLegada(v) {
142
+ return v.simulacao ?? temaDireto(v) ?? filtroChave(v) ?? 'normal';
143
+ }
125
144
  /* ===================== A MIGRAÇÃO ===================== */
126
145
  //
127
146
  // ⚠️ ELA NÃO É OPCIONAL E VEM ANTES DA PRIMEIRA LEITURA DA FORMA NOVA. O ajuste salvo guarda o valor único
@@ -1,5 +1,6 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-or-later
2
2
  import { type VizMode } from './viz-modes.js';
3
+ import { type Tema, type Correcao, type VisualState } from './viz-axes.js';
3
4
  import type { DomQuery } from '../core/dom-query.js';
4
5
  import type { ComFiltro, ComTextura, Visivel, DesenhoComCirculo, AplicarFiltroCss, AlcanceDoFiltro, AplicarAltoContrasteNoDom } from './port.js';
5
6
  /**
@@ -10,6 +11,24 @@ import type { ComFiltro, ComTextura, Visivel, DesenhoComCirculo, AplicarFiltroCs
10
11
  export declare function alcanceDoModo(mode: string): AlcanceDoFiltro;
11
12
  /** Modo por chave, com o fallback do original: chave desconhecida (ou nula) CAI em `normal`. */
12
13
  export declare function resolveViz(key: string | null | undefined): VizMode;
14
+ /**
15
+ * O ESTADO VISUAL GUARDADO deste jogador, de qualquer das duas formas (issue #104).
16
+ *
17
+ * ⚠️ ESTA FUNÇÃO É A CAIXA «um ajuste salvo antes da divisão restaura o mesmo estado visível» da definition
18
+ * of done, e a ordem das duas leituras é a decisão inteira:
19
+ *
20
+ * 1. a chave NOVA (`visualP`), que é a única que sabe dizer dois eixos;
21
+ * 2. na falta dela, a chave VELHA (`vizP`), que guarda a string única — e é aqui que mora o ajuste de toda
22
+ * criança que já jogou este jogo antes de hoje;
23
+ * 3. na falta das duas, o padrão.
24
+ *
25
+ * ⚠️ O RECUO NÃO É ZELO: sem ele, a primeira sessão depois da actualização apagaria o modo visual que ela
26
+ * escolheu — e quem escolheu `fix-deuter` ou `hc-direto-7` escolheu-o porque enxerga assim. É a diferença
27
+ * entre migrar e recomeçar.
28
+ *
29
+ * `migrarVisual` aceita as duas formas e é idempotente, então isto pode correr quantas vezes for preciso.
30
+ */
31
+ export declare function lerVisualGuardado(i: number): VisualState;
13
32
  /** É um dos três níveis de Renderização Direta (alto contraste)? Mesmo teste do original: `!!DIRECT_CFG[mode]`. */
14
33
  export declare function isDirectMode(mode: string): boolean;
15
34
  /** Filtro CSS da canvas no SOLO: simulação/correção de visão + realce L→Q, compostos e sem vazios. */
@@ -44,6 +63,7 @@ interface ClassListHost {
44
63
  }
45
64
  interface Pl {
46
65
  viz: string;
66
+ visual?: VisualState;
47
67
  sprite?: Textured | null;
48
68
  _tx?: unknown;
49
69
  }
@@ -100,7 +120,7 @@ export interface VizSettersCtx {
100
120
  setFrontDim: (on: boolean) => void;
101
121
  rebuildExtras: () => void;
102
122
  rebuildCoins: () => void;
103
- setModoCego: (on: boolean) => void;
123
+ setModoCego?: (on: boolean) => void;
104
124
  hideTouchControls: (reason?: string) => void;
105
125
  reflectVizButtons: () => void;
106
126
  renderVisualPanel: () => void;
@@ -115,8 +135,18 @@ export interface VizSettersApi {
115
135
  applyVpFilters(): void;
116
136
  /** Troca o modo de UM jogador: persiste, invalida o render estático e reaplica pelo caminho certo. */
117
137
  setPlayerViz(i: number, mode: string): void;
138
+ /**
139
+ * O ESTADO INTEIRO de um jogador, e os DOIS escritores por eixo (#104).
140
+ *
141
+ * ⚠️ Os dois de eixo existem separados porque é isso que um painel de dois controles precisa: mexer no
142
+ * TEMA sem tocar na correção, e vice-versa. Enquanto havia um campo só, «mexer num» significava
143
+ * inevitavelmente «apagar o outro» — e era o defeito, não a API.
144
+ */
145
+ setVisualDoJogador(i: number, v: VisualState): void;
146
+ setTemaDoJogador(i: number, tema: Tema): void;
147
+ setCorrecaoDoJogador(i: number, correcao: Correcao): void;
118
148
  /** Caminho SOLO: filtro CSS na canvas + texturas globais + overlay DOM + bolinha. */
119
- applyVizGlobal(mode: string): void;
149
+ applyVizGlobal(v: VisualState): void;
120
150
  /** Reaplica tudo depois de uma mudança estrutural (cenário, nº de telas). */
121
151
  reapplyVizAll(): void;
122
152
  /** Bolinha global (#viz-indicator) para um `kind`. */
@@ -125,6 +155,8 @@ export interface VizSettersApi {
125
155
  rebakeDirect(): void;
126
156
  /** Grupo de rádios de modos visuais nos painéis (visual/empatia). */
127
157
  renderVizGroup(listSel: string, tabsSel: string, modes: readonly VizMode[]): void;
158
+ /** Os DOIS eixos do painel visual (#104). Irmão do de cima — ver a nota na implementação. */
159
+ renderEixosVisuais(listSel: string, tabsSel: string): void;
128
160
  }
129
161
  export declare function initVizSetters(ctx: VizSettersCtx): VizSettersApi;
130
162
  export {};