@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
@@ -123,6 +123,12 @@ export const KEYS = {
123
123
  ttsEngine: 'incl_tts_engine', ttsVoice: 'incl_tts_voice', lang: 'incl_lang', // audiocat_{k}
124
124
  // comunicação / legendas (ADR-0028: todo menu persiste)
125
125
  letterCase: 'incl_lettercase', captions: 'incl_captions',
126
+ // ⚠️ O NÍVEL TEA (calmo / silencioso) PASSOU A PERSISTIR EM 2026-09-07, e antes não persistia: era um
127
+ // `let calmMode = 0` em `ui/pause-icons`, com o comentário «deliberately NOT persisted — verbatim: game.js
128
+ // never wrote it to storage». O «verbatim» é a chave — foi PRESERVADO na extração do monólito, não
129
+ // decidido. O custo era da criança que mais precisa dele: quem usa o modo silencioso voltava a pô-lo a
130
+ // cada sessão, e é para quem o barulho inesperado custa mais. O ADR-0028 diz que todo menu persiste.
131
+ tea: 'incl_tea',
126
132
  menuIndex: 'incl_menuindex', // "6 de 10" no fim do anuncio de item (ADR-0044, item 3)
127
133
  // tipografia / controles / toque
128
134
  fontKey: 'incl_font_k', padDesign: 'incl_paddesign', padDir: 'incl_paddir', touchmap: 'incl_touchmap',
@@ -133,9 +139,38 @@ export const KEYS = {
133
139
  reducedMotion: 'inclusionist.reducedmotion.v1', toggleMoveLegacy: 'inclusionist.togglemove',
134
140
  // POR JOGADOR — parametrizadas pelo indice da tela. Eram sufixos '_p'+i montados a mao em varios
135
141
  // pontos do game.js; virar funcao aqui e o que impede que um deles escreva num nome torto.
142
+ /**
143
+ * @deprecated ⚠️ A CHAVE LEGADA do modo visual — UM valor, do tempo em que só cabia um (issue #104).
144
+ *
145
+ * Continua a ser LIDA, e é isso que impede a criança de perder o que já escolheu; continua a ser ESCRITA
146
+ * enquanto os controles ainda escreverem um valor de cada vez, porque um leitor antigo (o cartucho na
147
+ * versão publicada) faz `if (v && VIZ_BY_KEY[v])` e rejeitaria um JSON — escrever a forma nova AQUI
148
+ * apagaria o ajuste dela em silêncio, que é exactamente o defeito que a migração existe para não cometer.
149
+ */
136
150
  vizP: (i) => 'incl_viz_p' + i,
151
+ /**
152
+ * O ESTADO VISUAL de dois eixos, em JSON (ADR-0076, issue #104).
153
+ *
154
+ * ⚠️ CHAVE NOVA AO LADO DA VELHA, e não a mesma chave com conteúdo novo. É o mesmo desenho que o campo
155
+ * `visual` usa ao lado do `viz`: as duas formas coexistem enquanto houver leitores das duas, cada um lê a
156
+ * que entende, e a velha só morre quando não sobrar quem a leia. `migrarVisual` aceita as duas, então o
157
+ * recuo — chave nova ausente, chave velha presente — devolve exactamente o que a criança escolheu.
158
+ */
159
+ visualP: (i) => 'incl_visual_p' + i,
137
160
  sinkP: (i) => 'incl_sink_p' + i,
138
161
  easyP: (i) => 'incl_easy_p' + i,
162
+ /**
163
+ * ⚠️ AS DUAS DE BAIXO SÃO AS CHAVES LEGADAS desde 2026-09-08 (ADR-0104 §C, issue #114). Continuam a ser
164
+ * LIDAS — é o ajuste da criança, e a herança dele é o que a impede de o perder — e não voltam a ser
165
+ * escritas. O que se escreve é a chave COM TRANSPORTE, porque a alternância é do APARELHO e não da pessoa:
166
+ * ligá-la no controle de tela, onde ninguém segura um botão virtual com conforto, ligava-a também no
167
+ * teclado, onde segurar uma tecla é exactamente o que a criança sabe fazer.
168
+ *
169
+ * ⚠️ E A CHAVE NOVA NÃO MORA AQUI, de propósito. Ela é `chaveDaAlternancia`, em `input/latch-scope` — este
170
+ * ficheiro é módulo-FOLHA e `platform/` não importa de `input/`, que é a camada acima. Montá-la aqui
171
+ * exigiria ou uma aresta ao contrário ou uma segunda cópia do nome, e a segunda cópia é exactamente o que
172
+ * o comentário do bloco acima existe para impedir. O dono do nome é quem conhece a regra do transporte.
173
+ */
139
174
  toggleMoveP: (i) => 'incl_togglemove_p' + i,
140
175
  toggleRunP: (i) => 'incl_togglerun_p' + i, // alternância do botão de CORRER (irmã da de movimento)
141
176
  rmWalkP: (i) => 'incl_rmWalk_p' + i,
@@ -10,7 +10,22 @@ import { t, bcp47 } from '../core/i18n.js';
10
10
  import { criarFalaInterrompivel } from './interruptible-speech.js';
11
11
  // ⚠️ MIGRAÇÃO PENDENTE (ADR-0022): a implementação neural de hoje é o @mintplex-labs/piper-tts-web e será SUBSTITUÍDA
12
12
  // por sherpa-onnx-wasm (loader universal, modelos VITS/Piper + Kokoro-multi-lang carregados de qq URL R2 em runtime via
13
- // FS.writeFile; lazy-fetch; eSpeak/Web Speech de fallback). @mintplex-labs foi descontinuado e só carrega 2 vozes pt-BR.
13
+ // FS.writeFile; lazy-fetch; eSpeak/Web Speech de fallback).
14
+ // 🔴 O MOTIVO ESCRITO AQUI ERA «@mintplex-labs foi descontinuado e só carrega 2 vozes pt-BR», E AS DUAS METADES
15
+ // FORAM MEDIDAS FALSAS EM 2026-09-09 — não pela leitura de um registo, que é como elas se propagaram por seis
16
+ // sítios, mas pelo registo npm e pelo pacote instalado:
17
+ // · DESCONTINUADO: nenhuma versão tem campo `deprecated`, e a 1.0.5 foi publicada em 2026-08-11 — depois de
18
+ // o ADR-0022 (2026-07-06) a dar por morta. A frase é de Julho e está atribuída ao Dev; o registo não a nega
19
+ // no passado, nega-a HOJE.
20
+ // · DUAS VOZES pt-BR: o `VoiceId` da 1.0.4 instalada traz 118 vozes, e entre elas as QUATRO exactas do
21
+ // ADR-0110 — `pt_BR-faber-medium`, `en_US-ryan-medium`, `en_US-amy-medium`, `es_MX-claude-high`.
22
+ // ⚠️ O QUE CONTINUA VERDADEIRO, e é mais afiado do que o motivo velho: o bundle prende `HF_BASE` a
23
+ // `huggingface.co/diffusionstudio/piper-voices` COM UMA GUARDA (`if (!url.match("https://huggingface.co")) return`),
24
+ // logo nenhum espelho de escola é alcançável por ele; e ele busca o PRÓPRIO runtime de cdnjs/jsDelivr por
25
+ // omissão, que é o que o ADR-0114 retira. A montante, o Piper mudou para `OHF-Voice/piper1-gpl` e a Open Home
26
+ // Foundation procura mantenedores — o risco existe, mas está noutro sítio.
27
+ // 📌 A ESCOLHA CONTINUA A SER A #129, e este comentário não a toma: descreve o que foi medido para que o
28
+ // próximo leitor não herde o motivo velho como se fosse medição.
14
29
  // ⚠️ E DESDE O ADR-0094 ESTE MÓDULO NÃO NOMEIA FORNECEDOR NENHUM — quem o nomeia é o JOGO, por `ctx.carregarVozNeural`.
15
30
  // O nome estava aqui num `import()` e o pacote em `devDependencies`, o que publicou uma engine que não compilava
16
31
  // (ADR-0093); pô-lo em `dependencies` consertava o build e obrigava todo consumidor a 135,4 MB de `onnxruntime-web`,
@@ -0,0 +1,90 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ /** Uma voz do catálogo. `voice` é o identificador no fornecedor; `engine` é o motor que a lê. */
3
+ export interface VozNeural {
4
+ readonly locale: string;
5
+ readonly engine: string;
6
+ readonly voice: string;
7
+ }
8
+ /**
9
+ * AS QUATRO QUE A ENGINE GARANTE (ADR-0110), decididas pelo Dev em 2026-09-08.
10
+ *
11
+ * ⚠️ TODAS `vits-piper` E NENHUMA `ncnn`, que o ADR-0065 §5 já proibia: `ncnn` é outro MOTOR DE INFERÊNCIA —
12
+ * modelos `.param`/`.bin` e não `.onnx` — e este projeto corre sherpa-onnx. Dois dos links que chegaram em
13
+ * 2026-09-08 apontavam para os espelhos `ncnn`, e é por isso que a proibição tem gate e não só um comentário.
14
+ * ⚠️ E NENHUMA `int8`, pela razão MEDIDA do mesmo §5: no backend WASM o int8 corre ~3× MAIS LENTO que o fp32
15
+ * (RTF 5,6 contra 2,5, do log do próprio Dev) e produz saída incorrecta ou muda. Quantizar não encurta a
16
+ * espera aqui — alonga-a.
17
+ */
18
+ export declare const VOZES_NEURAIS: readonly VozNeural[];
19
+ /**
20
+ * DE ONDE VÊM OS MODELOS (ADR-0114), num sítio só.
21
+ *
22
+ * 📏 MEDIDO EM 2026-09-08, e não escolhido: o `piper.ttstool.com` — que o Dev nomeou ao perguntar — serve o
23
+ * próprio runtime da própria origem e busca os modelos aqui. As quatro vozes deste catálogo respondem 200
24
+ * neste endereço, com `Access-Control-Allow-Origin: *`, logo um PWA pode buscá-las de outra origem.
25
+ *
26
+ * ⚠️ UM SÍTIO SÓ É A METADE QUE O REGISTO PEDE. Um endereço repetido no ponto de uso é como o CDN do
27
+ * WebGazer chegou ao `ui/webcam` — escrito à mão, sem política, e sem ninguém a poder mudá-lo de uma vez.
28
+ */
29
+ export declare const HOST_DOS_MODELOS = "https://huggingface.co/rhasspy/piper-voices/resolve/main/";
30
+ /**
31
+ * O CAMINHO DO MODELO, DERIVADO DO IDENTIFICADOR — e não uma segunda tabela.
32
+ *
33
+ * `pt_BR-faber-medium` diz tudo o que o caminho precisa: `pt/pt_BR/faber/medium/pt_BR-faber-medium.onnx`.
34
+ * 🎯 DERIVAR EM VEZ DE TABELAR é a decisão inteira desta função: uma tabela de caminhos ao lado da tabela
35
+ * de vozes seria o mesmo facto escrito duas vezes, e este repositório já pagou isso três vezes — o
36
+ * `DomQuery`, os rótulos de movimento reduzido, as chaves de armazenamento. Duas listas divergem, e
37
+ * divergem uma entrada de cada vez.
38
+ *
39
+ * ⚠️ Devolve `null` para um identificador que não tenha a forma esperada, em vez de montar um caminho
40
+ * torto: uma URL inventada dá 404 na escola, e um `null` dá para reportar antes de sair de casa.
41
+ */
42
+ export declare function caminhoDoModelo(v: VozNeural): string | null;
43
+ /** O endereço completo do modelo. `null` quando o identificador não se deixa ler. */
44
+ export declare function urlDoModelo(v: VozNeural): string | null;
45
+ /**
46
+ * A CONFIGURAÇÃO da voz, que o motor lê junto com o modelo.
47
+ *
48
+ * 📌 `.onnx.json` e não um segundo caminho: é o mesmo ficheiro com outro sufixo, medido a responder 200 no
49
+ * mesmo sítio. Escrevê-lo como derivação mantém a regra de que o identificador é a única fonte.
50
+ */
51
+ export declare function urlDaConfig(v: VozNeural): string | null;
52
+ /** Em que pé está cada voz. `falhou` é um estado e não uma excepção — ver `vozEmUso`. */
53
+ export type EstadoDaVoz = 'ausente' | 'a-buscar' | 'pronta' | 'falhou';
54
+ /** O que se sabe de cada voz, por identificador. Uma voz ausente do mapa é `ausente`. */
55
+ export type EstadosDasVozes = Readonly<Record<string, EstadoDaVoz | undefined>>;
56
+ export declare const estadoDe: (estados: EstadosDasVozes, v: VozNeural) => EstadoDaVoz;
57
+ /**
58
+ * A ORDEM POR QUE SE BUSCAM: primeiro as do idioma corrente, depois as outras, cada grupo na ordem do
59
+ * catálogo. Só entram as que ainda não estão prontas nem em curso.
60
+ *
61
+ * ⚠️ O IDIOMA CORRENTE PRIMEIRO É A REGRA INTEIRA, e ela é sobre uma criança e não sobre eficiência: as
62
+ * outras são para «conforme interesse do jogador», mas a dela é a que ela precisa AGORA. Buscar em ordem de
63
+ * catálogo faria uma criança brasileira esperar por duas vozes inglesas na rede de uma escola.
64
+ *
65
+ * 📌 `falhou` VOLTA À FILA. Uma escola perde a rede a meio da manhã e recupera-a; uma voz marcada como
66
+ * falhada para sempre seria uma criança sem voz até alguém recarregar a página. Quem chama decide QUANDO
67
+ * tentar de novo — este módulo só diz que ainda há o que buscar.
68
+ */
69
+ export declare function ordemDeBusca(estados: EstadosDasVozes, localeCorrente: string, catalogo?: readonly VozNeural[]): VozNeural[];
70
+ /** O que a criança ouve agora. `recuo` é a voz do sistema — eSpeak ou Web Speech. */
71
+ export type VozEmUso = {
72
+ readonly tipo: 'neural';
73
+ readonly voice: string;
74
+ } | {
75
+ readonly tipo: 'recuo';
76
+ readonly porque: 'a-buscar' | 'sem-voz-para-o-idioma' | 'falhou' | 'ausente';
77
+ };
78
+ /**
79
+ * QUEM ESTÁ A FALAR, E POR QUE — e esta função é o gate 3 do ADR-0110 em forma de código.
80
+ *
81
+ * ⚠️ O DEFEITO QUE ELA IMPEDE TEM NOME NESTE REPOSITÓRIO: «um recuo que se apresenta como a coisa real é a
82
+ * família do `reflectTTS`». Uma engine que dissesse «voz neural» enquanto o modelo ainda desce faria um adulto
83
+ * concluir que a qualidade que ouve É a qualidade final — e desistir de esperar por uma coisa que já vinha a
84
+ * caminho. Dizer `recuo` e dizer PORQUÊ custa uma linha e é a diferença entre informar e enganar.
85
+ *
86
+ * 📌 A PRIMEIRA VOZ PRONTA DO IDIOMA, e nada mais: onde há duas prontas, esta função NÃO escolhe entre elas —
87
+ * devolve a primeira do catálogo porque tem de devolver alguma, e qual delas a criança prefere é a pergunta
88
+ * que o ADR-0110 deixou em aberto. Ver o cabeçalho.
89
+ */
90
+ export declare function vozEmUso(estados: EstadosDasVozes, localeCorrente: string, catalogo?: readonly VozNeural[]): VozEmUso;
@@ -0,0 +1,137 @@
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
+ export const HOST_DOS_MODELOS = 'https://huggingface.co/rhasspy/piper-voices/resolve/main/';
45
+ /**
46
+ * O CAMINHO DO MODELO, DERIVADO DO IDENTIFICADOR — e não uma segunda tabela.
47
+ *
48
+ * `pt_BR-faber-medium` diz tudo o que o caminho precisa: `pt/pt_BR/faber/medium/pt_BR-faber-medium.onnx`.
49
+ * 🎯 DERIVAR EM VEZ DE TABELAR é a decisão inteira desta função: uma tabela de caminhos ao lado da tabela
50
+ * de vozes seria o mesmo facto escrito duas vezes, e este repositório já pagou isso três vezes — o
51
+ * `DomQuery`, os rótulos de movimento reduzido, as chaves de armazenamento. Duas listas divergem, e
52
+ * divergem uma entrada de cada vez.
53
+ *
54
+ * ⚠️ Devolve `null` para um identificador que não tenha a forma esperada, em vez de montar um caminho
55
+ * torto: uma URL inventada dá 404 na escola, e um `null` dá para reportar antes de sair de casa.
56
+ */
57
+ export function caminhoDoModelo(v) {
58
+ const partes = v.voice.split('-');
59
+ if (partes.length !== 3)
60
+ return null;
61
+ const [locale, nome, qualidade] = partes;
62
+ const idioma = locale.split('_')[0];
63
+ if (!idioma || !nome || !qualidade || idioma === locale)
64
+ return null;
65
+ return `${idioma}/${locale}/${nome}/${qualidade}/${v.voice}.onnx`;
66
+ }
67
+ /** O endereço completo do modelo. `null` quando o identificador não se deixa ler. */
68
+ export function urlDoModelo(v) {
69
+ const caminho = caminhoDoModelo(v);
70
+ return caminho === null ? null : HOST_DOS_MODELOS + caminho;
71
+ }
72
+ /**
73
+ * A CONFIGURAÇÃO da voz, que o motor lê junto com o modelo.
74
+ *
75
+ * 📌 `.onnx.json` e não um segundo caminho: é o mesmo ficheiro com outro sufixo, medido a responder 200 no
76
+ * mesmo sítio. Escrevê-lo como derivação mantém a regra de que o identificador é a única fonte.
77
+ */
78
+ export function urlDaConfig(v) {
79
+ const url = urlDoModelo(v);
80
+ return url === null ? null : url + '.json';
81
+ }
82
+ export const estadoDe = (estados, v) => estados[v.voice] ?? 'ausente';
83
+ /**
84
+ * A ORDEM POR QUE SE BUSCAM: primeiro as do idioma corrente, depois as outras, cada grupo na ordem do
85
+ * catálogo. Só entram as que ainda não estão prontas nem em curso.
86
+ *
87
+ * ⚠️ O IDIOMA CORRENTE PRIMEIRO É A REGRA INTEIRA, e ela é sobre uma criança e não sobre eficiência: as
88
+ * outras são para «conforme interesse do jogador», mas a dela é a que ela precisa AGORA. Buscar em ordem de
89
+ * catálogo faria uma criança brasileira esperar por duas vozes inglesas na rede de uma escola.
90
+ *
91
+ * 📌 `falhou` VOLTA À FILA. Uma escola perde a rede a meio da manhã e recupera-a; uma voz marcada como
92
+ * falhada para sempre seria uma criança sem voz até alguém recarregar a página. Quem chama decide QUANDO
93
+ * tentar de novo — este módulo só diz que ainda há o que buscar.
94
+ */
95
+ export function ordemDeBusca(estados, localeCorrente, catalogo = VOZES_NEURAIS) {
96
+ const porBuscar = catalogo.filter((v) => {
97
+ const e = estadoDe(estados, v);
98
+ return e === 'ausente' || e === 'falhou';
99
+ });
100
+ return [
101
+ ...porBuscar.filter((v) => v.locale === localeCorrente),
102
+ ...porBuscar.filter((v) => v.locale !== localeCorrente),
103
+ ];
104
+ }
105
+ /**
106
+ * QUEM ESTÁ A FALAR, E POR QUE — e esta função é o gate 3 do ADR-0110 em forma de código.
107
+ *
108
+ * ⚠️ O DEFEITO QUE ELA IMPEDE TEM NOME NESTE REPOSITÓRIO: «um recuo que se apresenta como a coisa real é a
109
+ * família do `reflectTTS`». Uma engine que dissesse «voz neural» enquanto o modelo ainda desce faria um adulto
110
+ * concluir que a qualidade que ouve É a qualidade final — e desistir de esperar por uma coisa que já vinha a
111
+ * caminho. Dizer `recuo` e dizer PORQUÊ custa uma linha e é a diferença entre informar e enganar.
112
+ *
113
+ * 📌 A PRIMEIRA VOZ PRONTA DO IDIOMA, e nada mais: onde há duas prontas, esta função NÃO escolhe entre elas —
114
+ * devolve a primeira do catálogo porque tem de devolver alguma, e qual delas a criança prefere é a pergunta
115
+ * que o ADR-0110 deixou em aberto. Ver o cabeçalho.
116
+ */
117
+ export function vozEmUso(estados, localeCorrente, catalogo = VOZES_NEURAIS) {
118
+ const doIdioma = catalogo.filter((v) => v.locale === localeCorrente);
119
+ if (doIdioma.length === 0)
120
+ return { tipo: 'recuo', porque: 'sem-voz-para-o-idioma' };
121
+ const pronta = doIdioma.find((v) => estadoDe(estados, v) === 'pronta');
122
+ if (pronta)
123
+ return { tipo: 'neural', voice: pronta.voice };
124
+ // A PRECEDÊNCIA DOS TRÊS RECUOS, e cada degrau tem a sua razão:
125
+ //
126
+ // ⚠️ «A BUSCAR» GANHA DE «FALHOU» quando as duas coexistem: enquanto alguma ainda vem a caminho, dizer
127
+ // «falhou» seria anunciar uma derrota que ainda não aconteceu, e um adulto desligaria a espera cedo.
128
+ // ⚠️ E «AUSENTE» NÃO SE DISFARÇA DE «A BUSCAR», que era como esta função estava escrita à primeira. Nada
129
+ // começou é diferente de está a caminho: o primeiro é uma engine que ainda não pediu — possivelmente
130
+ // porque ninguém a mandou —, e chamar-lhe «a buscar» esconderia um buscador que nunca arrancou atrás de
131
+ // uma frase tranquilizadora. É o mesmo defeito de forma que o `neuralDisponivel` já custou à #91.
132
+ if (doIdioma.some((v) => estadoDe(estados, v) === 'a-buscar'))
133
+ return { tipo: 'recuo', porque: 'a-buscar' };
134
+ if (doIdioma.some((v) => estadoDe(estados, v) === 'falhou'))
135
+ return { tipo: 'recuo', porque: 'falhou' };
136
+ return { tipo: 'recuo', porque: 'ausente' };
137
+ }
@@ -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 {};