@the-inclusionist/engine 6.36.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.
- package/LICENSE +661 -0
- package/README.md +107 -0
- package/app/css/style.css +622 -0
- package/app/public/vendor/fonts/andika-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/andika-400.woff2 +0 -0
- package/app/public/vendor/fonts/andika-700-ext.woff2 +0 -0
- package/app/public/vendor/fonts/andika-700.woff2 +0 -0
- package/app/public/vendor/fonts/atkinson-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/atkinson-400.woff2 +0 -0
- package/app/public/vendor/fonts/atkinson-700-ext.woff2 +0 -0
- package/app/public/vendor/fonts/atkinson-700.woff2 +0 -0
- package/app/public/vendor/fonts/atkinson-mono-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/atkinson-mono-400.woff2 +0 -0
- package/app/public/vendor/fonts/comicneue-400.woff2 +0 -0
- package/app/public/vendor/fonts/comicneue-700.woff2 +0 -0
- package/app/public/vendor/fonts/greatvibes-400.woff2 +0 -0
- package/app/public/vendor/fonts/inter-400.woff2 +0 -0
- package/app/public/vendor/fonts/inter-700.woff2 +0 -0
- package/app/public/vendor/fonts/lato-400.woff2 +0 -0
- package/app/public/vendor/fonts/lato-700.woff2 +0 -0
- package/app/public/vendor/fonts/lexend-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/lexend-400.woff2 +0 -0
- package/app/public/vendor/fonts/lexend-700-ext.woff2 +0 -0
- package/app/public/vendor/fonts/lexend-700.woff2 +0 -0
- package/app/public/vendor/fonts/literata-400.woff2 +0 -0
- package/app/public/vendor/fonts/literata-700.woff2 +0 -0
- package/app/public/vendor/fonts/newsreader-400.woff2 +0 -0
- package/app/public/vendor/fonts/newsreader-700.woff2 +0 -0
- package/app/public/vendor/fonts/opensans-400.woff2 +0 -0
- package/app/public/vendor/fonts/opensans-700.woff2 +0 -0
- package/app/public/vendor/fonts/pinyon-400.woff2 +0 -0
- package/app/public/vendor/fonts/quattro-400.woff2 +0 -0
- package/app/public/vendor/fonts/quattro-700.woff2 +0 -0
- package/app/public/vendor/fonts/sourcesans-400.woff2 +0 -0
- package/app/public/vendor/fonts/sourcesans-700.woff2 +0 -0
- package/app/public/vendor/fonts/sourceserif-400.woff2 +0 -0
- package/app/public/vendor/fonts/sourceserif-700.woff2 +0 -0
- package/app/public/vendor/fonts/ufcook-700.woff2 +0 -0
- package/app/public/vendor/fonts/ufmag-400.woff2 +0 -0
- package/app/public/vendor/fonts.css +88 -0
- package/dist-pkg/boot/create-game.d.ts +159 -0
- package/dist-pkg/boot/create-game.js +266 -0
- package/dist-pkg/core/a11y-sr.d.ts +4 -0
- package/dist-pkg/core/a11y-sr.js +20 -0
- package/dist-pkg/core/actions.d.ts +100 -0
- package/dist-pkg/core/actions.js +149 -0
- package/dist-pkg/core/anel.d.ts +17 -0
- package/dist-pkg/core/anel.js +31 -0
- package/dist-pkg/core/collision.d.ts +21 -0
- package/dist-pkg/core/collision.js +62 -0
- package/dist-pkg/core/constants.d.ts +84 -0
- package/dist-pkg/core/constants.js +64 -0
- package/dist-pkg/core/contract.d.ts +227 -0
- package/dist-pkg/core/contract.js +184 -0
- package/dist-pkg/core/dom-query.d.ts +9 -0
- package/dist-pkg/core/dom-query.js +25 -0
- package/dist-pkg/core/entity.d.ts +150 -0
- package/dist-pkg/core/entity.js +38 -0
- package/dist-pkg/core/escape-html.d.ts +16 -0
- package/dist-pkg/core/escape-html.js +32 -0
- package/dist-pkg/core/i18n.d.ts +71 -0
- package/dist-pkg/core/i18n.js +236 -0
- package/dist-pkg/core/layers.d.ts +39 -0
- package/dist-pkg/core/layers.js +71 -0
- package/dist-pkg/core/letter-grid.d.ts +55 -0
- package/dist-pkg/core/letter-grid.js +116 -0
- package/dist-pkg/core/loop.d.ts +17 -0
- package/dist-pkg/core/loop.js +36 -0
- package/dist-pkg/core/password.d.ts +34 -0
- package/dist-pkg/core/password.js +205 -0
- package/dist-pkg/core/rng.d.ts +20 -0
- package/dist-pkg/core/rng.js +61 -0
- package/dist-pkg/core/rotulo-acessivel.d.ts +17 -0
- package/dist-pkg/core/rotulo-acessivel.js +41 -0
- package/dist-pkg/core/run-state.d.ts +115 -0
- package/dist-pkg/core/run-state.js +40 -0
- package/dist-pkg/core/scenes.d.ts +68 -0
- package/dist-pkg/core/scenes.js +64 -0
- package/dist-pkg/core/screens.d.ts +16 -0
- package/dist-pkg/core/screens.js +28 -0
- package/dist-pkg/core/state.d.ts +138 -0
- package/dist-pkg/core/state.js +290 -0
- package/dist-pkg/core/tiles.d.ts +12 -0
- package/dist-pkg/core/tiles.js +61 -0
- package/dist-pkg/core/world.d.ts +4 -0
- package/dist-pkg/core/world.js +58 -0
- package/dist-pkg/educational/activities-registry.d.ts +47 -0
- package/dist-pkg/educational/activities-registry.js +95 -0
- package/dist-pkg/i18n/en.d.ts +3 -0
- package/dist-pkg/i18n/en.js +560 -0
- package/dist-pkg/i18n/es.d.ts +3 -0
- package/dist-pkg/i18n/es.js +558 -0
- package/dist-pkg/i18n/pt.d.ts +3 -0
- package/dist-pkg/i18n/pt.js +618 -0
- package/dist-pkg/input/default-bindings.d.ts +24 -0
- package/dist-pkg/input/default-bindings.js +122 -0
- package/dist-pkg/input/devices.d.ts +15 -0
- package/dist-pkg/input/devices.js +31 -0
- package/dist-pkg/input/edges.d.ts +58 -0
- package/dist-pkg/input/edges.js +49 -0
- package/dist-pkg/input/gamepad.d.ts +203 -0
- package/dist-pkg/input/gamepad.js +531 -0
- package/dist-pkg/input/keyboard-runtime.d.ts +61 -0
- package/dist-pkg/input/keyboard-runtime.js +83 -0
- package/dist-pkg/input/keyboard.d.ts +45 -0
- package/dist-pkg/input/keyboard.js +80 -0
- package/dist-pkg/input/keydown.d.ts +270 -0
- package/dist-pkg/input/keydown.js +381 -0
- package/dist-pkg/input/latch.d.ts +30 -0
- package/dist-pkg/input/latch.js +63 -0
- package/dist-pkg/input/pointer-space.d.ts +40 -0
- package/dist-pkg/input/pointer-space.js +51 -0
- package/dist-pkg/input/state.d.ts +11 -0
- package/dist-pkg/input/state.js +12 -0
- package/dist-pkg/input/touch-bindings.d.ts +210 -0
- package/dist-pkg/input/touch-bindings.js +365 -0
- package/dist-pkg/input/touch.d.ts +173 -0
- package/dist-pkg/input/touch.js +314 -0
- package/dist-pkg/input/transports.d.ts +90 -0
- package/dist-pkg/input/transports.js +96 -0
- package/dist-pkg/input/vocabulary-migration.d.ts +58 -0
- package/dist-pkg/input/vocabulary-migration.js +125 -0
- package/dist-pkg/platform/audio-ambient.d.ts +27 -0
- package/dist-pkg/platform/audio-ambient.js +89 -0
- package/dist-pkg/platform/audio-earcons.d.ts +24 -0
- package/dist-pkg/platform/audio-earcons.js +62 -0
- package/dist-pkg/platform/audio-jingles.d.ts +17 -0
- package/dist-pkg/platform/audio-jingles.js +54 -0
- package/dist-pkg/platform/audio-mixer.d.ts +14 -0
- package/dist-pkg/platform/audio-mixer.js +62 -0
- package/dist-pkg/platform/audio-nav.d.ts +51 -0
- package/dist-pkg/platform/audio-nav.js +99 -0
- package/dist-pkg/platform/audio-sonar.d.ts +71 -0
- package/dist-pkg/platform/audio-sonar.js +173 -0
- package/dist-pkg/platform/audio.d.ts +32 -0
- package/dist-pkg/platform/audio.js +191 -0
- package/dist-pkg/platform/interruptible-speech.d.ts +19 -0
- package/dist-pkg/platform/interruptible-speech.js +79 -0
- package/dist-pkg/platform/speech.d.ts +2 -0
- package/dist-pkg/platform/speech.js +35 -0
- package/dist-pkg/platform/storage.d.ts +87 -0
- package/dist-pkg/platform/storage.js +148 -0
- package/dist-pkg/platform/tts.d.ts +33 -0
- package/dist-pkg/platform/tts.js +142 -0
- package/dist-pkg/render/camera.d.ts +58 -0
- package/dist-pkg/render/camera.js +105 -0
- package/dist-pkg/render/canvas.d.ts +13 -0
- package/dist-pkg/render/canvas.js +26 -0
- package/dist-pkg/render/cenario-data.d.ts +127 -0
- package/dist-pkg/render/cenario-data.js +145 -0
- package/dist-pkg/render/city-tex.d.ts +63 -0
- package/dist-pkg/render/city-tex.js +228 -0
- package/dist-pkg/render/city-tiles.d.ts +11 -0
- package/dist-pkg/render/city-tiles.js +140 -0
- package/dist-pkg/render/crt.d.ts +19 -0
- package/dist-pkg/render/crt.js +92 -0
- package/dist-pkg/render/cvd-matrices.d.ts +35 -0
- package/dist-pkg/render/cvd-matrices.js +91 -0
- package/dist-pkg/render/draw.d.ts +149 -0
- package/dist-pkg/render/draw.js +221 -0
- package/dist-pkg/render/fx.d.ts +63 -0
- package/dist-pkg/render/fx.js +123 -0
- package/dist-pkg/render/hc-role-data.d.ts +14 -0
- package/dist-pkg/render/hc-role-data.js +24 -0
- package/dist-pkg/render/high-contrast.d.ts +109 -0
- package/dist-pkg/render/high-contrast.js +254 -0
- package/dist-pkg/render/lq-filter.d.ts +37 -0
- package/dist-pkg/render/lq-filter.js +94 -0
- package/dist-pkg/render/minimap.d.ts +11 -0
- package/dist-pkg/render/minimap.js +94 -0
- package/dist-pkg/render/parallax.d.ts +85 -0
- package/dist-pkg/render/parallax.js +158 -0
- package/dist-pkg/render/player-anim.d.ts +71 -0
- package/dist-pkg/render/player-anim.js +152 -0
- package/dist-pkg/render/port.d.ts +143 -0
- package/dist-pkg/render/port.js +29 -0
- package/dist-pkg/render/recycling-tex.d.ts +48 -0
- package/dist-pkg/render/recycling-tex.js +164 -0
- package/dist-pkg/render/scene-city.d.ts +77 -0
- package/dist-pkg/render/scene-city.js +181 -0
- package/dist-pkg/render/scene-parallax.d.ts +72 -0
- package/dist-pkg/render/scene-parallax.js +426 -0
- package/dist-pkg/render/scene-sky.d.ts +126 -0
- package/dist-pkg/render/scene-sky.js +295 -0
- package/dist-pkg/render/screen-pipeline.d.ts +134 -0
- package/dist-pkg/render/screen-pipeline.js +177 -0
- package/dist-pkg/render/set-cenario.d.ts +54 -0
- package/dist-pkg/render/set-cenario.js +103 -0
- package/dist-pkg/render/sprite-fx.d.ts +3 -0
- package/dist-pkg/render/sprite-fx.js +40 -0
- package/dist-pkg/render/textures.d.ts +75 -0
- package/dist-pkg/render/textures.js +240 -0
- package/dist-pkg/render/title-scene.d.ts +46 -0
- package/dist-pkg/render/title-scene.js +75 -0
- package/dist-pkg/render/viewports.d.ts +52 -0
- package/dist-pkg/render/viewports.js +185 -0
- package/dist-pkg/render/viz-axes.d.ts +98 -0
- package/dist-pkg/render/viz-axes.js +184 -0
- package/dist-pkg/render/viz-modes.d.ts +61 -0
- package/dist-pkg/render/viz-modes.js +74 -0
- package/dist-pkg/render/viz-setters.d.ts +130 -0
- package/dist-pkg/render/viz-setters.js +244 -0
- package/dist-pkg/render/weather.d.ts +96 -0
- package/dist-pkg/render/weather.js +171 -0
- package/dist-pkg/render/wheelchair-sprites.d.ts +24 -0
- package/dist-pkg/render/wheelchair-sprites.js +63 -0
- package/dist-pkg/render/world-tex.d.ts +18 -0
- package/dist-pkg/render/world-tex.js +87 -0
- package/dist-pkg/ui/activities-menu.d.ts +258 -0
- package/dist-pkg/ui/activities-menu.js +648 -0
- package/dist-pkg/ui/caa-sets.d.ts +39 -0
- package/dist-pkg/ui/caa-sets.js +65 -0
- package/dist-pkg/ui/changed-mark.d.ts +15 -0
- package/dist-pkg/ui/changed-mark.js +96 -0
- package/dist-pkg/ui/debug-panel.d.ts +67 -0
- package/dist-pkg/ui/debug-panel.js +173 -0
- package/dist-pkg/ui/dom.d.ts +24 -0
- package/dist-pkg/ui/dom.js +47 -0
- package/dist-pkg/ui/focus-trap.d.ts +71 -0
- package/dist-pkg/ui/focus-trap.js +99 -0
- package/dist-pkg/ui/fonts.d.ts +41 -0
- package/dist-pkg/ui/fonts.js +59 -0
- package/dist-pkg/ui/hud.d.ts +137 -0
- package/dist-pkg/ui/hud.js +229 -0
- package/dist-pkg/ui/item-announcement.d.ts +19 -0
- package/dist-pkg/ui/item-announcement.js +36 -0
- package/dist-pkg/ui/layout.d.ts +6 -0
- package/dist-pkg/ui/layout.js +54 -0
- package/dist-pkg/ui/loop-crash.d.ts +19 -0
- package/dist-pkg/ui/loop-crash.js +60 -0
- package/dist-pkg/ui/map-hub.d.ts +56 -0
- package/dist-pkg/ui/map-hub.js +138 -0
- package/dist-pkg/ui/menu-nav.d.ts +141 -0
- package/dist-pkg/ui/menu-nav.js +390 -0
- package/dist-pkg/ui/pause-icons.d.ts +327 -0
- package/dist-pkg/ui/pause-icons.js +620 -0
- package/dist-pkg/ui/reach-notice.d.ts +34 -0
- package/dist-pkg/ui/reach-notice.js +71 -0
- package/dist-pkg/ui/settings-audio.d.ts +139 -0
- package/dist-pkg/ui/settings-audio.js +544 -0
- package/dist-pkg/ui/settings-caa.d.ts +45 -0
- package/dist-pkg/ui/settings-caa.js +137 -0
- package/dist-pkg/ui/settings-controls.d.ts +100 -0
- package/dist-pkg/ui/settings-controls.js +152 -0
- package/dist-pkg/ui/settings-empathy.d.ts +62 -0
- package/dist-pkg/ui/settings-empathy.js +137 -0
- package/dist-pkg/ui/settings-motion.d.ts +95 -0
- package/dist-pkg/ui/settings-motion.js +282 -0
- package/dist-pkg/ui/settings-motor.d.ts +77 -0
- package/dist-pkg/ui/settings-motor.js +215 -0
- package/dist-pkg/ui/settings-panel.d.ts +76 -0
- package/dist-pkg/ui/settings-panel.js +187 -0
- package/dist-pkg/ui/settings-typo.d.ts +74 -0
- package/dist-pkg/ui/settings-typo.js +150 -0
- package/dist-pkg/ui/settings-visual.d.ts +111 -0
- package/dist-pkg/ui/settings-visual.js +252 -0
- package/dist-pkg/ui/shell.d.ts +239 -0
- package/dist-pkg/ui/shell.js +430 -0
- package/dist-pkg/ui/title.d.ts +36 -0
- package/dist-pkg/ui/title.js +60 -0
- package/dist-pkg/ui/vlibras.d.ts +11 -0
- package/dist-pkg/ui/vlibras.js +87 -0
- package/dist-pkg/ui/webcam.d.ts +7 -0
- package/dist-pkg/ui/webcam.js +88 -0
- package/docs/CREDITS.md +64 -0
- package/docs/LICENSES.md +132 -0
- package/package.json +115 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// platform/interruptible-speech — FALAR O ÚLTIMO PEDIDO, E CALAR O ANTERIOR NA HORA (ADR-0044, item 2).
|
|
3
|
+
//
|
|
4
|
+
// ========================= O DEFEITO QUE ISTO SUBSTITUI =========================
|
|
5
|
+
// O motor neural falava assim:
|
|
6
|
+
//
|
|
7
|
+
// speak: (text) => { if (busy) next = text; else speakNow(text); } // fila de 1
|
|
8
|
+
//
|
|
9
|
+
// Uma FILA, e de tamanho um. Varrendo cinco itens de menu, a criança ouvia o PRIMEIRO inteiro e depois o
|
|
10
|
+
// ÚLTIMO — os três do meio sumiam em silêncio, porque cada novo pedido sobrescrevia o `next`. É lento E
|
|
11
|
+
// perde informação, e as duas coisas doem no mesmo lugar: a pessoa que não enxerga navega POR ESCUTA, e a
|
|
12
|
+
// escuta estava vários itens atrás do foco.
|
|
13
|
+
//
|
|
14
|
+
// A XAG 106 diz o que fazer, e diz por quê: se a narração do item em foco ainda está sendo lida e o foco
|
|
15
|
+
// muda, "a narração do elemento original deve PARAR IMEDIATAMENTE, e a do novo elemento começar".
|
|
16
|
+
//
|
|
17
|
+
// ========================= AS DUAS COISAS QUE ISTO GARANTE =========================
|
|
18
|
+
//
|
|
19
|
+
// 1. CALA ANTES DE SINTETIZAR, não depois. Síntese neural leva tempo; parar só quando o áudio novo fica
|
|
20
|
+
// pronto deixaria a voz velha falando durante a espera — o item errado, com convicção. Silêncio é a
|
|
21
|
+
// resposta certa: a criança sabe que se moveu, e o silêncio confirma.
|
|
22
|
+
//
|
|
23
|
+
// 2. GERAÇÃO, e é o que impede a corrida. `sintetizar` é assíncrono, e dois pedidos podem terminar fora
|
|
24
|
+
// de ordem: o segundo fica pronto primeiro, começa a tocar, e o primeiro chega depois e o atropela —
|
|
25
|
+
// a criança ouviria o item que ela JÁ passou, por cima do atual. Cada pedido carrega o seu número; o
|
|
26
|
+
// que voltar com número velho é descartado, em cada um dos dois `await`.
|
|
27
|
+
//
|
|
28
|
+
// ========================= POR QUE É UM MÓDULO, E NÃO UMA LINHA NO `tts` =========================
|
|
29
|
+
// Porque assim se pode PROVAR. O bloco neural vive dentro de um `import()` dinâmico que só resolve com o
|
|
30
|
+
// runtime de 25,6 MB presente; testá-lo lá dentro exigiria o motor de verdade. Aqui a política é pura —
|
|
31
|
+
// recebe "como sintetizar", "como tocar" e "como parar" — e o teste de node exercita a corrida e a
|
|
32
|
+
// interrupção com falsos, em milissegundos.
|
|
33
|
+
export function criarFalaInterrompivel(motor) {
|
|
34
|
+
let tocando = null;
|
|
35
|
+
let geracao = 0;
|
|
36
|
+
/** Cala o que estiver tocando. O `try` existe porque parar uma fonte já terminada lança em alguns motores. */
|
|
37
|
+
function pararTudo() {
|
|
38
|
+
if (tocando !== null) {
|
|
39
|
+
try {
|
|
40
|
+
motor.parar(tocando);
|
|
41
|
+
}
|
|
42
|
+
catch (e) { /* fonte já encerrada — parar de novo não é erro */ }
|
|
43
|
+
tocando = null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function falar(texto) {
|
|
47
|
+
const minha = ++geracao; // reivindica a vez ANTES de qualquer espera
|
|
48
|
+
pararTudo(); // garantia 1: silêncio imediato, não ao fim da síntese
|
|
49
|
+
if (!texto)
|
|
50
|
+
return;
|
|
51
|
+
try {
|
|
52
|
+
const audio = await motor.sintetizar(texto);
|
|
53
|
+
if (minha !== geracao)
|
|
54
|
+
return; // garantia 2: chegou tarde — outro pedido já assumiu
|
|
55
|
+
pararTudo(); // de novo: alguém pode ter começado a tocar durante a espera
|
|
56
|
+
const fonte = motor.tocar(audio, () => { if (tocando === fonte)
|
|
57
|
+
tocando = null; });
|
|
58
|
+
if (minha !== geracao) {
|
|
59
|
+
if (fonte !== null) {
|
|
60
|
+
try {
|
|
61
|
+
motor.parar(fonte);
|
|
62
|
+
}
|
|
63
|
+
catch (e) { /* noop */ }
|
|
64
|
+
}
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
tocando = fonte;
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
// Síntese falhou para ESTE texto. Não é motivo para derrubar a narração inteira: o próximo pedido
|
|
71
|
+
// tenta de novo, e o silêncio de um item é melhor que um motor morto.
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
falar: (texto) => { void falar(texto); },
|
|
76
|
+
calar: () => { geracao++; pararTudo(); }, // o `++` invalida o que estiver sintetizando agora
|
|
77
|
+
falando: () => tocando !== null,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// platform/speech.ts — voz do LETRAMENTO (gameSay): fala pt-BR SEMPRE-ativa (independe do toggle 'Narração
|
|
3
|
+
// (TTS)' do mixer), pela voz nativa do navegador. Escolhe uma voz pt-BR e evita pt-PT. Módulo-folha (só áudio).
|
|
4
|
+
// O TTS do MENU (narrate/ttsSpeak + motores neurais Piper/Kokoro/…) é outro caminho, gated pelo mixer — fica no game.js.
|
|
5
|
+
import { soundOn, volume } from './audio.js';
|
|
6
|
+
function ptbrVoice() {
|
|
7
|
+
try {
|
|
8
|
+
const vs = (window.speechSynthesis && window.speechSynthesis.getVoices()) || [];
|
|
9
|
+
if (!vs.length)
|
|
10
|
+
return null;
|
|
11
|
+
return vs.find((v) => /pt[-_]?br/i.test(v.lang)) || vs.find((v) => /pt/i.test(v.lang) && /bras|brazil/i.test(v.name)) || vs.find((v) => /pt/i.test(v.lang) && !/pt[-_]?pt/i.test(v.lang)) || null;
|
|
12
|
+
}
|
|
13
|
+
catch (e) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
// FORÇA pt-BR (não usa a voz do mixer, que pode ser pt-PT). Volume ×1.4 para a fala soar acima dos efeitos.
|
|
18
|
+
export function gameSay(text) {
|
|
19
|
+
if (!text || !soundOn)
|
|
20
|
+
return;
|
|
21
|
+
try {
|
|
22
|
+
const ss = window.speechSynthesis;
|
|
23
|
+
if (!ss)
|
|
24
|
+
return;
|
|
25
|
+
ss.cancel();
|
|
26
|
+
const u = new SpeechSynthesisUtterance(text);
|
|
27
|
+
u.lang = 'pt-BR';
|
|
28
|
+
const v = ptbrVoice();
|
|
29
|
+
if (v)
|
|
30
|
+
u.voice = v;
|
|
31
|
+
u.volume = Math.min(1, volume * 1.4);
|
|
32
|
+
ss.speak(u);
|
|
33
|
+
}
|
|
34
|
+
catch (e) { /* noop */ }
|
|
35
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
export declare function get(key: string, fallback: string): string;
|
|
3
|
+
export declare function get(key: string, fallback?: null): string | null;
|
|
4
|
+
export declare function set(key: string, value: string | number | boolean): boolean;
|
|
5
|
+
export declare function remove(key: string): void;
|
|
6
|
+
export declare function getBool(key: string, fallback?: boolean): boolean;
|
|
7
|
+
export declare function setBool(key: string, on: boolean): void;
|
|
8
|
+
export declare function getNum(key: string, fallback?: number): number;
|
|
9
|
+
export declare function getJSON<T = unknown>(key: string, fallback?: T | null): T | null;
|
|
10
|
+
export declare function setJSON(key: string, obj: unknown): void;
|
|
11
|
+
/**
|
|
12
|
+
* Nome completo de uma chave do escopo DO JOGO.
|
|
13
|
+
*
|
|
14
|
+
* ⚠️ O ID ENTRA COMO ARGUMENTO, e ele já foi uma constante aqui (`JOGO_ID = 'inclusionist'`). Pelo teste do
|
|
15
|
+
* ADR-0080 — *um segundo jogo quereria um valor diferente aqui?* — a resposta é sim e é imediata: dois jogos
|
|
16
|
+
* no mesmo perfil de navegador colidiam em `activity`, `quizlevel`, `cenario`, `tabsel`, `fracnot` e em toda
|
|
17
|
+
* gravação `attract_*`. Um sobrescrevia o progresso do outro sem erro nenhum.
|
|
18
|
+
*
|
|
19
|
+
* ⚠️ E O ID NÃO VEM DA DECLARAÇÃO, que era o desenho óbvio. Ele não pode: `game/state` lê o armazenamento no
|
|
20
|
+
* IMPORT, e o import corre antes de qualquer `createGame()`. Um id vindo da declaração chegaria depois de as
|
|
21
|
+
* três chaves já terem sido resolvidas — contra vazio, e em silêncio. Quem sabe o próprio id é o JOGO, que o
|
|
22
|
+
* passa como constante sua; o ADR-0080 proíbe a ENGINE de o saber, não o jogo.
|
|
23
|
+
*/
|
|
24
|
+
export declare function kJogo(jogo: string, nome: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* Lê a chave NOVA; se ela ainda não existe, herda o valor da LEGADA. Só de leitura: quem grava, grava na nova.
|
|
27
|
+
* É o que permite renomear chave sem um passo de migração e sem perder o ajuste de ninguém.
|
|
28
|
+
*/
|
|
29
|
+
export declare function getComLegado(nova: string, legada: string, fallback: string): string;
|
|
30
|
+
export declare function getComLegado(nova: string, legada: string, fallback?: null): string | null;
|
|
31
|
+
/** O par de `getComLegado` para valor em JSON — a herança tem de valer para os dois formatos, senão metade
|
|
32
|
+
* das chaves migra e a outra metade some, que é o pior dos dois mundos. */
|
|
33
|
+
export declare function getJSONComLegado<T = unknown>(nova: string, legada: string, fallback?: T | null): T | null;
|
|
34
|
+
export declare const KEYS: {
|
|
35
|
+
onebtn: string;
|
|
36
|
+
wheelchair: string;
|
|
37
|
+
modocego: string;
|
|
38
|
+
caneDiv: string;
|
|
39
|
+
hearingloss: string;
|
|
40
|
+
activity: (jogo: string) => string;
|
|
41
|
+
activityLegado: string;
|
|
42
|
+
quizlevel: (jogo: string) => string;
|
|
43
|
+
quizlevelLegado: string;
|
|
44
|
+
cenario: (jogo: string) => string;
|
|
45
|
+
cenarioLegado: string;
|
|
46
|
+
tabsel: (jogo: string) => string;
|
|
47
|
+
tabselLegado: string;
|
|
48
|
+
fracnot: (jogo: string) => string;
|
|
49
|
+
fracnotLegado: string;
|
|
50
|
+
viz: string;
|
|
51
|
+
lq: string;
|
|
52
|
+
cbsafe: string;
|
|
53
|
+
ownercolors: string;
|
|
54
|
+
outfg: string;
|
|
55
|
+
outbg: string;
|
|
56
|
+
hcrole: string;
|
|
57
|
+
juice: string;
|
|
58
|
+
crt: string;
|
|
59
|
+
crtLegacy: string;
|
|
60
|
+
ttsEngine: string;
|
|
61
|
+
ttsVoice: string;
|
|
62
|
+
lang: string;
|
|
63
|
+
letterCase: string;
|
|
64
|
+
captions: string;
|
|
65
|
+
menuIndex: string;
|
|
66
|
+
fontKey: string;
|
|
67
|
+
padDesign: string;
|
|
68
|
+
padDir: string;
|
|
69
|
+
touchmap: string;
|
|
70
|
+
padBtnMm: string;
|
|
71
|
+
padGapMm: string;
|
|
72
|
+
padStickMm: string;
|
|
73
|
+
padTravelMm: string;
|
|
74
|
+
padDpadMm: string;
|
|
75
|
+
reducedMotion: string;
|
|
76
|
+
toggleMoveLegacy: string;
|
|
77
|
+
vizP: (i: number) => string;
|
|
78
|
+
sinkP: (i: number) => string;
|
|
79
|
+
easyP: (i: number) => string;
|
|
80
|
+
toggleMoveP: (i: number) => string;
|
|
81
|
+
toggleRunP: (i: number) => string;
|
|
82
|
+
rmWalkP: (i: number) => string;
|
|
83
|
+
rmBreathP: (i: number) => string;
|
|
84
|
+
rmFlavorP: (i: number) => string;
|
|
85
|
+
attract: (jogo: string, cen: string) => string;
|
|
86
|
+
attractLegado: (cen: string) => string;
|
|
87
|
+
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// platform/storage.ts — Camada única de persistência (localStorage) — módulo-folha. À prova de exceção:
|
|
3
|
+
// localStorage LANÇA em file:// e no modo privado de alguns navegadores, e isso derrubava o boot inteiro (por
|
|
4
|
+
// isso todo acesso é try/catch). Centralizar aqui: um lugar para trocar a estratégia (namespacing, IndexedDB…)
|
|
5
|
+
// sem caçar ~60 pontos. Migração gradual — nem todo game.js usa isto ainda.
|
|
6
|
+
export function get(key, fallback = null) {
|
|
7
|
+
try {
|
|
8
|
+
const v = localStorage.getItem(key);
|
|
9
|
+
return v == null ? fallback : v;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return fallback;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function set(key, value) {
|
|
16
|
+
try {
|
|
17
|
+
localStorage.setItem(key, String(value));
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function remove(key) { try {
|
|
25
|
+
localStorage.removeItem(key);
|
|
26
|
+
}
|
|
27
|
+
catch { /* noop */ } }
|
|
28
|
+
export function getBool(key, fallback = false) { const v = get(key, null); return v == null ? fallback : v === '1'; }
|
|
29
|
+
export function setBool(key, on) { set(key, on ? '1' : '0'); }
|
|
30
|
+
export function getNum(key, fallback = 0) {
|
|
31
|
+
const v = get(key, null);
|
|
32
|
+
const n = v == null ? NaN : parseFloat(v);
|
|
33
|
+
return isFinite(n) ? n : fallback;
|
|
34
|
+
}
|
|
35
|
+
export function getJSON(key, fallback = null) {
|
|
36
|
+
try {
|
|
37
|
+
const s = get(key, null);
|
|
38
|
+
return s == null ? fallback : JSON.parse(s);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return fallback;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function setJSON(key, obj) { try {
|
|
45
|
+
set(key, JSON.stringify(obj));
|
|
46
|
+
}
|
|
47
|
+
catch { /* noop */ } }
|
|
48
|
+
/* ===================== os DOIS escopos (save com namespace, ADR-0027 passo 7) ===================== */
|
|
49
|
+
//
|
|
50
|
+
// O namespacing óbvio — um prefixo por jogo em TUDO — seria um defeito de acessibilidade grave, e vale dizer
|
|
51
|
+
// por quê antes de dizer o que foi feito.
|
|
52
|
+
//
|
|
53
|
+
// Uma criança cega configura o modo cego, a bengala, a voz, a velocidade da narração. Uma criança daltônica
|
|
54
|
+
// escolhe a correção. Uma criança disléxica escolhe a fonte. Se cada jogo do catálogo tivesse o próprio
|
|
55
|
+
// espaço de nomes, ela teria de REFAZER tudo isso em cada jogo do catálogo — e quem mais depende dos ajustes
|
|
56
|
+
// é justamente quem tem menos margem para refazê-los. (O número que estava aqui era 35, do plano que morreu
|
|
57
|
+
// em 2026-08-28; o catálogo do MVP são 300+ jogos, o que só torna o argumento mais forte.)
|
|
58
|
+
//
|
|
59
|
+
// Então são DOIS escopos, e a linha entre eles não é técnica, é de quem a coisa pertence:
|
|
60
|
+
//
|
|
61
|
+
// · COMPARTILHADO (`incl_*`, como sempre foi) — o que pertence à CRIANÇA: acessibilidade, tipografia,
|
|
62
|
+
// idioma, voz, controles, toque. Segue com ela de jogo em jogo, de propósito. O segundo consumidor (o
|
|
63
|
+
// quiz) já lê a fonte escolhida no jogo de plataforma, e isso está CERTO.
|
|
64
|
+
// · DO JOGO (`incl.<jogo>.*`) — o que pertence a ESTA partida: atividade, nível, cenário, gravação da
|
|
65
|
+
// demonstração. Dois jogos com um "nível 3" não são o mesmo nível 3.
|
|
66
|
+
//
|
|
67
|
+
// A LEITURA HERDA DA CHAVE ANTIGA e a escrita vai só para a nova (`getComLegado`). Sem passo de migração no
|
|
68
|
+
// boot, porque `core/state` lê no IMPORT — uma migração agendada chegaria tarde. E a chave velha fica onde
|
|
69
|
+
// está: é dado da criança, não meu para apagar, e a sua permanência é o que torna um retorno possível.
|
|
70
|
+
/**
|
|
71
|
+
* Nome completo de uma chave do escopo DO JOGO.
|
|
72
|
+
*
|
|
73
|
+
* ⚠️ O ID ENTRA COMO ARGUMENTO, e ele já foi uma constante aqui (`JOGO_ID = 'inclusionist'`). Pelo teste do
|
|
74
|
+
* ADR-0080 — *um segundo jogo quereria um valor diferente aqui?* — a resposta é sim e é imediata: dois jogos
|
|
75
|
+
* no mesmo perfil de navegador colidiam em `activity`, `quizlevel`, `cenario`, `tabsel`, `fracnot` e em toda
|
|
76
|
+
* gravação `attract_*`. Um sobrescrevia o progresso do outro sem erro nenhum.
|
|
77
|
+
*
|
|
78
|
+
* ⚠️ E O ID NÃO VEM DA DECLARAÇÃO, que era o desenho óbvio. Ele não pode: `game/state` lê o armazenamento no
|
|
79
|
+
* IMPORT, e o import corre antes de qualquer `createGame()`. Um id vindo da declaração chegaria depois de as
|
|
80
|
+
* três chaves já terem sido resolvidas — contra vazio, e em silêncio. Quem sabe o próprio id é o JOGO, que o
|
|
81
|
+
* passa como constante sua; o ADR-0080 proíbe a ENGINE de o saber, não o jogo.
|
|
82
|
+
*/
|
|
83
|
+
export function kJogo(jogo, nome) { return 'incl.' + jogo + '.' + nome; }
|
|
84
|
+
export function getComLegado(nova, legada, fallback = null) {
|
|
85
|
+
const v = get(nova, null);
|
|
86
|
+
if (v !== null)
|
|
87
|
+
return v;
|
|
88
|
+
const antigo = get(legada, null);
|
|
89
|
+
return antigo !== null ? antigo : fallback;
|
|
90
|
+
}
|
|
91
|
+
/** O par de `getComLegado` para valor em JSON — a herança tem de valer para os dois formatos, senão metade
|
|
92
|
+
* das chaves migra e a outra metade some, que é o pior dos dois mundos. */
|
|
93
|
+
export function getJSONComLegado(nova, legada, fallback = null) {
|
|
94
|
+
const v = getJSON(nova, null);
|
|
95
|
+
if (v !== null)
|
|
96
|
+
return v;
|
|
97
|
+
const antigo = getJSON(legada, null);
|
|
98
|
+
return antigo !== null ? antigo : fallback;
|
|
99
|
+
}
|
|
100
|
+
// Registro das chaves conhecidas (documentação em UM lugar; a fonte de verdade ainda é o uso). Vai sendo
|
|
101
|
+
// completado à medida que os lotes migram. Chaves com {i}/{cen}/{id} são parametrizadas por jogador/cenário/controle.
|
|
102
|
+
export const KEYS = {
|
|
103
|
+
// empatia motora/auditiva
|
|
104
|
+
onebtn: 'incl_onebtn', wheelchair: 'incl_wheelchair', modocego: 'incl_modocego', caneDiv: 'incl_cane_div',
|
|
105
|
+
hearingloss: 'incl_hearingloss',
|
|
106
|
+
// atividade / quiz / cenário — ESCOPO DO JOGO (ver os dois escopos acima). `*Legado` é o nome antigo, de
|
|
107
|
+
// onde a leitura herda uma vez; a escrita vai só para o novo.
|
|
108
|
+
//
|
|
109
|
+
// ⚠️ SÃO FUNÇÕES DO ID DO JOGO, e as da criança são strings. A diferença de FORMA é o que impede o engano:
|
|
110
|
+
// não há como prefixar por engano uma preferência da criança, porque ela não tem onde receber o id — e não
|
|
111
|
+
// há como esquecer de escopar uma chave da partida, porque sem o argumento não compila. A regra que antes
|
|
112
|
+
// vivia só num comentário passou a viver no tipo.
|
|
113
|
+
activity: (jogo) => kJogo(jogo, 'activity'), activityLegado: 'incl_activity',
|
|
114
|
+
quizlevel: (jogo) => kJogo(jogo, 'quizlevel'), quizlevelLegado: 'incl_quizlevel',
|
|
115
|
+
cenario: (jogo) => kJogo(jogo, 'cenario'), cenarioLegado: 'incl_cenario',
|
|
116
|
+
tabsel: (jogo) => kJogo(jogo, 'tabsel'), tabselLegado: 'incl_tabsel',
|
|
117
|
+
fracnot: (jogo) => kJogo(jogo, 'fracnot'), fracnotLegado: 'incl_fracnot',
|
|
118
|
+
// visual / contraste / cor
|
|
119
|
+
viz: 'incl_viz', lq: 'incl_lq', cbsafe: 'incl_cbsafe', ownercolors: 'incl_ownercolors',
|
|
120
|
+
outfg: 'incl_outfg', outbg: 'incl_outbg', hcrole: 'incl_hcrole', juice: 'incl_juice', crt: 'incl_crt2',
|
|
121
|
+
crtLegacy: 'incl_crt', // formato antigo (booleano); crt.ts migra p/ incl_crt2 na 1ª leitura (fresh)
|
|
122
|
+
// áudio / voz / i18n
|
|
123
|
+
ttsEngine: 'incl_tts_engine', ttsVoice: 'incl_tts_voice', lang: 'incl_lang', // audiocat_{k}
|
|
124
|
+
// comunicação / legendas (ADR-0028: todo menu persiste)
|
|
125
|
+
letterCase: 'incl_lettercase', captions: 'incl_captions',
|
|
126
|
+
menuIndex: 'incl_menuindex', // "6 de 10" no fim do anuncio de item (ADR-0044, item 3)
|
|
127
|
+
// tipografia / controles / toque
|
|
128
|
+
fontKey: 'incl_font_k', padDesign: 'incl_paddesign', padDir: 'incl_paddir', touchmap: 'incl_touchmap',
|
|
129
|
+
padBtnMm: 'incl_padbtnmm', padGapMm: 'incl_padgapmm', padStickMm: 'incl_padstickmm',
|
|
130
|
+
padTravelMm: 'incl_padtravelmm', padDpadMm: 'incl_paddpadmm',
|
|
131
|
+
// movimento reduzido (objeto inteiro num JSON so) + a chave antiga de alternar-movimento, que
|
|
132
|
+
// loadPlayerA11y ainda le uma vez para migrar quem vinha da versao anterior
|
|
133
|
+
reducedMotion: 'inclusionist.reducedmotion.v1', toggleMoveLegacy: 'inclusionist.togglemove',
|
|
134
|
+
// POR JOGADOR — parametrizadas pelo indice da tela. Eram sufixos '_p'+i montados a mao em varios
|
|
135
|
+
// pontos do game.js; virar funcao aqui e o que impede que um deles escreva num nome torto.
|
|
136
|
+
vizP: (i) => 'incl_viz_p' + i,
|
|
137
|
+
sinkP: (i) => 'incl_sink_p' + i,
|
|
138
|
+
easyP: (i) => 'incl_easy_p' + i,
|
|
139
|
+
toggleMoveP: (i) => 'incl_togglemove_p' + i,
|
|
140
|
+
toggleRunP: (i) => 'incl_togglerun_p' + i, // alternância do botão de CORRER (irmã da de movimento)
|
|
141
|
+
rmWalkP: (i) => 'incl_rmWalk_p' + i,
|
|
142
|
+
rmBreathP: (i) => 'incl_rmBreath_p' + i,
|
|
143
|
+
rmFlavorP: (i) => 'incl_rmFlavor_p' + i,
|
|
144
|
+
// demo/attract: uma gravação por cenário (fn em vez de string — chave parametrizada). ESCOPO DO JOGO: a
|
|
145
|
+
// gravação é de uma fase DESTE jogo e não faz sentido nenhum em outro.
|
|
146
|
+
attract: (jogo, cen) => kJogo(jogo, 'attract_' + cen),
|
|
147
|
+
attractLegado: (cen) => 'incl_attract_' + cen,
|
|
148
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
interface TtsEngine {
|
|
3
|
+
id: string;
|
|
4
|
+
speak: (text: string) => void;
|
|
5
|
+
}
|
|
6
|
+
export interface TtsCtx {
|
|
7
|
+
srSay: (t: string) => void;
|
|
8
|
+
srAlert: (t: string) => void;
|
|
9
|
+
ensureAC: () => AudioContext | null;
|
|
10
|
+
catNode: (cat: string) => AudioNode | null;
|
|
11
|
+
audioOut: () => AudioNode | null;
|
|
12
|
+
getSoundOn: () => boolean;
|
|
13
|
+
getVolume: () => number;
|
|
14
|
+
getAudioCat: () => Record<string, {
|
|
15
|
+
on: boolean;
|
|
16
|
+
}> | null;
|
|
17
|
+
}
|
|
18
|
+
export interface Tts {
|
|
19
|
+
narrate: (text: string) => void;
|
|
20
|
+
ttsSpeak: (text: string) => boolean;
|
|
21
|
+
loadTTS: () => void;
|
|
22
|
+
speakWebSpeech: (text: string) => boolean;
|
|
23
|
+
getEngineSel: () => string;
|
|
24
|
+
setEngineSel: (v: string) => void;
|
|
25
|
+
getEngine: () => TtsEngine | null;
|
|
26
|
+
getVoiceObj: () => SpeechSynthesisVoice | null;
|
|
27
|
+
setVoiceObj: (v: SpeechSynthesisVoice | null) => void;
|
|
28
|
+
readonly loading: boolean;
|
|
29
|
+
readonly failed: boolean;
|
|
30
|
+
readonly narrateCount: number;
|
|
31
|
+
}
|
|
32
|
+
export declare function createTts(ctx: TtsCtx): Tts;
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// platform/tts — narração por voz. Motor NEURAL Piper pt-BR carregado LAZY (import de CDN + cache OPFS → a 2ª sessão fala
|
|
3
|
+
// offline), com FALLBACK imediato p/ a voz nativa do navegador (Web Speech) enquanto o neural não chega. narrate() é o
|
|
4
|
+
// ponto de entrada, gated pelo toggle 'Narração (TTS)' do mixer (audioCat.tts.on) — independe das legendas. As funções de
|
|
5
|
+
// PAINEL (populateTTSEngines/Voices/reflectTTS) ficam no game.js (→ ui/settings-audio, #38→#54) e usam get/setEngineSel +
|
|
6
|
+
// get/setVoiceObj daqui. Injeção por closure. Ver docs/plano-tts-fase-f5.md + docs/5-Refactoring/plano-modularizacao-mapa.md.
|
|
7
|
+
import * as store from './storage.js';
|
|
8
|
+
import { t, bcp47 } from '../core/i18n.js';
|
|
9
|
+
import { criarFalaInterrompivel } from './interruptible-speech.js';
|
|
10
|
+
// ⚠️ MIGRAÇÃO PENDENTE (ADR-0022): esta implementação com @mintplex-labs/piper-tts-web será SUBSTITUÍDA por
|
|
11
|
+
// sherpa-onnx-wasm (loader universal, modelos VITS/Piper + Kokoro-multi-lang carregados de qq URL R2 em runtime via
|
|
12
|
+
// FS.writeFile; lazy-fetch; eSpeak/Web Speech de fallback). @mintplex-labs foi descontinuado e só carrega 2 vozes pt-BR.
|
|
13
|
+
// Enquanto a migração não vem: a lib vem do npm (code-split pelo Vite no 1º uso), modelo do HF em cache OPFS. (#69→#38)
|
|
14
|
+
/**
|
|
15
|
+
* Voz NEURAL por idioma, indexada pela etiqueta BCP-47 de core/i18n. Só o pt-BR tem uma hoje, e essa ausência
|
|
16
|
+
* é agora CONSULTÁVEL em vez de presumida: `loadTTS` pergunta à tabela se existe voz para o idioma corrente,
|
|
17
|
+
* em vez de carregar a portuguesa aconteça o que acontecer.
|
|
18
|
+
*
|
|
19
|
+
* Era `TTS_SOURCES['pt-BR']` escrito à mão no ponto de uso, o que fazia o jogo em inglês baixar 25,6 MB de
|
|
20
|
+
* voz portuguesa para ler texto em inglês com fonética errada — pior que não ter voz neural nenhuma, porque
|
|
21
|
+
* gasta a banda da escola para entregar algo ininteligível.
|
|
22
|
+
*/
|
|
23
|
+
const TTS_SOURCES = {
|
|
24
|
+
'pt-BR': { engine: 'piper', voice: 'pt_BR-faber-medium' },
|
|
25
|
+
};
|
|
26
|
+
export function createTts(ctx) {
|
|
27
|
+
let ttsEngine = null, ttsLoading = false, ttsFailed = false, _ttsPct = 0, _narrateCount = 0;
|
|
28
|
+
let _ttsVoiceObj = null; // voz do Web Speech selecionada
|
|
29
|
+
let ttsEngineSel = store.get(store.KEYS.ttsEngine, null) || 'webspeech'; // webspeech | piper | kokoro | kitten | espeak
|
|
30
|
+
function speakWebSpeech(text) {
|
|
31
|
+
try {
|
|
32
|
+
const ss = window.speechSynthesis;
|
|
33
|
+
if (!ss)
|
|
34
|
+
return false;
|
|
35
|
+
ss.cancel();
|
|
36
|
+
// `u.lang` era 'pt-BR' fixo. Com o jogo em inglês ou espanhol isso pedia ao navegador uma voz
|
|
37
|
+
// PORTUGUESA para um texto que não é português — e o resultado não é sotaque, é ininteligível: a
|
|
38
|
+
// fonética errada aplicada às letras erradas. Segue o idioma do jogo.
|
|
39
|
+
const u = new SpeechSynthesisUtterance(text);
|
|
40
|
+
u.lang = bcp47();
|
|
41
|
+
if (_ttsVoiceObj)
|
|
42
|
+
u.voice = _ttsVoiceObj;
|
|
43
|
+
u.rate = 1;
|
|
44
|
+
u.volume = Math.min(1, ctx.getVolume() * 1.4);
|
|
45
|
+
ss.speak(u);
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function loadTTS() {
|
|
53
|
+
if (ttsEngine || ttsLoading || ttsFailed)
|
|
54
|
+
return;
|
|
55
|
+
if (ttsEngineSel !== 'piper') { // Kokoro/Kitten/eSpeak NG ainda não entraram
|
|
56
|
+
if (ttsEngineSel !== 'webspeech')
|
|
57
|
+
ctx.srAlert(t('sr.tts.engineNoLanguage'));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
// O Piper só tem voz para os idiomas em TTS_SOURCES. Sem voz para o idioma corrente, NÃO se baixa a de
|
|
61
|
+
// outro idioma: avisa e segue na voz do navegador, que fala a língua certa.
|
|
62
|
+
const fonte = TTS_SOURCES[bcp47()];
|
|
63
|
+
if (!fonte) {
|
|
64
|
+
ctx.srAlert(t('sr.tts.noNeuralForLanguage'));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
ttsLoading = true;
|
|
68
|
+
const t0 = performance.now();
|
|
69
|
+
ctx.srSay(t('sr.tts.downloading'));
|
|
70
|
+
import('@mintplex-labs/piper-tts-web').then(async (mod) => {
|
|
71
|
+
const session = await mod.TtsSession.create({ voiceId: fonte.voice,
|
|
72
|
+
progress: (p) => { if (!p || !p.total)
|
|
73
|
+
return; const pct = Math.round(p.loaded * 100 / p.total); if (pct >= _ttsPct + 25 && pct < 100) {
|
|
74
|
+
_ttsPct = pct;
|
|
75
|
+
ctx.srSay(t('sr.tts.progress', { pct }));
|
|
76
|
+
} },
|
|
77
|
+
logger: () => { } });
|
|
78
|
+
// A FILA DE UM SAIU (ADR-0044, item 2). Ela era `if (busy) next = text; else speakNow(text)`, e o
|
|
79
|
+
// efeito, varrendo cinco itens de menu, era ouvir o PRIMEIRO inteiro e depois o ÚLTIMO — os três do
|
|
80
|
+
// meio sumiam, porque cada pedido sobrescrevia o `next`. Lento e lacunar, e quem não enxerga navega
|
|
81
|
+
// POR ESCUTA: a escuta ficava vários itens atrás do foco.
|
|
82
|
+
//
|
|
83
|
+
// A política agora mora em `platform/interruptible-speech`, pura e testada com falsos. Aqui só se
|
|
84
|
+
// diz COMO sintetizar, tocar e parar — o quando é lá, com a guarda de geração que impede uma síntese
|
|
85
|
+
// lenta de atropelar a mais nova.
|
|
86
|
+
const fala = criarFalaInterrompivel({
|
|
87
|
+
sintetizar: async (texto) => {
|
|
88
|
+
const wav = await session.predict(texto);
|
|
89
|
+
const ac = ctx.ensureAC();
|
|
90
|
+
if (!ac)
|
|
91
|
+
throw new Error('AudioContext unavailable'); // o `catch` de lá trata: silêncio deste item, motor vivo
|
|
92
|
+
return ac.decodeAudioData(await wav.arrayBuffer());
|
|
93
|
+
},
|
|
94
|
+
tocar: (buf, aoTerminar) => {
|
|
95
|
+
const ac = ctx.ensureAC();
|
|
96
|
+
if (!ac)
|
|
97
|
+
return null;
|
|
98
|
+
const src = ac.createBufferSource();
|
|
99
|
+
src.buffer = buf;
|
|
100
|
+
src.connect(ctx.catNode('tts') || ctx.audioOut() || ac.destination);
|
|
101
|
+
src.onended = aoTerminar;
|
|
102
|
+
src.start();
|
|
103
|
+
return src;
|
|
104
|
+
},
|
|
105
|
+
parar: (src) => { src.stop(); },
|
|
106
|
+
});
|
|
107
|
+
ttsEngine = { id: 'piper', speak: (text) => { fala.falar(text); } };
|
|
108
|
+
ttsLoading = false;
|
|
109
|
+
try {
|
|
110
|
+
window.speechSynthesis && window.speechSynthesis.cancel();
|
|
111
|
+
}
|
|
112
|
+
catch (e) { /* noop */ }
|
|
113
|
+
narrate(t('sr.tts.ready', { s: ((performance.now() - t0) / 1000).toFixed(0) })); // já sai NA voz nova
|
|
114
|
+
}).catch(() => { ttsLoading = false; ttsFailed = true; ctx.srAlert(t('sr.tts.loadFailed')); });
|
|
115
|
+
}
|
|
116
|
+
function ttsSpeak(text) {
|
|
117
|
+
if (ttsEngineSel !== 'webspeech') {
|
|
118
|
+
if (ttsEngine && ttsEngine.id === ttsEngineSel && ttsEngine.speak) {
|
|
119
|
+
try {
|
|
120
|
+
ttsEngine.speak(text);
|
|
121
|
+
}
|
|
122
|
+
catch (e) { /* noop */ }
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
loadTTS(); // motor neural (baixando/indisponível) → cai no fallback
|
|
126
|
+
}
|
|
127
|
+
return speakWebSpeech(text); // fallback imediato: Web Speech, no idioma do jogo
|
|
128
|
+
}
|
|
129
|
+
function narrate(text) {
|
|
130
|
+
const cat = ctx.getAudioCat();
|
|
131
|
+
if (!ctx.getSoundOn() || !cat || !cat.tts || !cat.tts.on || !text)
|
|
132
|
+
return;
|
|
133
|
+
_narrateCount++;
|
|
134
|
+
ttsSpeak(text);
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
narrate, ttsSpeak, loadTTS, speakWebSpeech,
|
|
138
|
+
getEngineSel: () => ttsEngineSel, setEngineSel: (v) => { ttsEngineSel = v; },
|
|
139
|
+
getEngine: () => ttsEngine, getVoiceObj: () => _ttsVoiceObj, setVoiceObj: (v) => { _ttsVoiceObj = v; },
|
|
140
|
+
get loading() { return ttsLoading; }, get failed() { return ttsFailed; }, get narrateCount() { return _narrateCount; },
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
/** Um tamanho em pixels de mundo ou de tela. Só isto: a câmera não precisa saber de mais nada. */
|
|
3
|
+
export interface Tamanho {
|
|
4
|
+
w: number;
|
|
5
|
+
h: number;
|
|
6
|
+
}
|
|
7
|
+
/** Onde a câmera está, ANTES de arredondar. O arredondamento é do desenho, não da conta — ver `prender`. */
|
|
8
|
+
export interface Camera {
|
|
9
|
+
camX: number;
|
|
10
|
+
camY: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Prende a câmera dentro do mundo.
|
|
14
|
+
*
|
|
15
|
+
* ⚠️ MUNDO MENOR QUE A TELA: `mundo.w - tela.w` fica NEGATIVO, o `min` devolve esse negativo e o `max(0, …)`
|
|
16
|
+
* o zera — a câmera encosta no canto superior esquerdo e sobra vazio à direita e abaixo. É o comportamento
|
|
17
|
+
* que já existia e está preservado de propósito; o mapa de hoje (896×992 contra 320×180) nunca chega lá, mas
|
|
18
|
+
* uma fase pequena chegaria, e é melhor que o caso esteja escrito do que descoberto.
|
|
19
|
+
*/
|
|
20
|
+
export declare function prender(cam: Camera, mundo: Tamanho, tela: Tamanho): Camera;
|
|
21
|
+
/** Põe `(alvoX, alvoY)` no CENTRO da tela e prende no mundo. O alvo é um ponto — quem tem corpo o converte. */
|
|
22
|
+
export declare function enquadrar(alvoX: number, alvoY: number, mundo: Tamanho, tela: Tamanho): Camera;
|
|
23
|
+
/**
|
|
24
|
+
* Aplica o tremor (JUICE) e PRENDE DE NOVO.
|
|
25
|
+
*
|
|
26
|
+
* A segunda prisão não é zelo: sem ela o tremor empurra a câmera para fora do mundo na beirada da fase e a
|
|
27
|
+
* criança vê o vazio atrás do cenário — justamente no momento em que alguma coisa explodiu e ela está olhando.
|
|
28
|
+
*
|
|
29
|
+
* @param amp amplitude em px (0 = sem tremor; `render/fx.shakeAmp`, que decai linearmente)
|
|
30
|
+
* @param rx deslocamento horizontal já sorteado, em [-1, 1]
|
|
31
|
+
* @param ry idem, vertical
|
|
32
|
+
*/
|
|
33
|
+
export declare function tremer(cam: Camera, mundo: Tamanho, tela: Tamanho, amp: number, rx: number, ry: number): Camera;
|
|
34
|
+
/** A zona-morta, em pixels de TELA. `{w:0,h:0}` = sem zona: a câmera cola no alvo, que é o de hoje. */
|
|
35
|
+
export interface ZonaMorta {
|
|
36
|
+
readonly w: number;
|
|
37
|
+
readonly h: number;
|
|
38
|
+
}
|
|
39
|
+
export interface CameraObj {
|
|
40
|
+
/** Onde a câmera está, sem tremor, pré-arredondamento. */
|
|
41
|
+
readonly base: Camera;
|
|
42
|
+
/** Segue o alvo respeitando a zona-morta e prende no mundo. Devolve a base nova. */
|
|
43
|
+
seguir(alvoX: number, alvoY: number): Camera;
|
|
44
|
+
/** Centra no alvo AGORA, ignorando a zona-morta: nascimento, renascimento, troca de fase. */
|
|
45
|
+
pular(alvoX: number, alvoY: number): Camera;
|
|
46
|
+
/** Base + tremor, preso no mundo. NÃO altera a base — ver o cabeçalho. */
|
|
47
|
+
quadro(amp: number, rx: number, ry: number): Camera;
|
|
48
|
+
/** Mundo e/ou tela mudaram (fase nova, viewport dividido). Reprende a base no que passou a valer. */
|
|
49
|
+
redimensionar(mundo?: Tamanho, tela?: Tamanho): Camera;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Cria uma câmera com alvo, zona-morta, prisão no mundo e tremor.
|
|
53
|
+
*
|
|
54
|
+
* ⚠️ ZONA ZERO É O COMPORTAMENTO DE HOJE, e não por coincidência: com `w = h = 0` a correção de `seguir` vira
|
|
55
|
+
* `alvo - tela/2`, que é `enquadrar` letra por letra. É o que permite trocar o `placeCam` por esta câmera sem
|
|
56
|
+
* mudar um pixel do que a criança vê, e escolher um valor de zona depois, como decisão separada.
|
|
57
|
+
*/
|
|
58
|
+
export declare function criarCamera(mundo: Tamanho, tela: Tamanho, zona?: ZonaMorta): CameraObj;
|