@the-inclusionist/engine 7.0.1 → 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.
- package/README.md +5 -3
- package/app/css/style.css +22 -5
- package/app/public/vendor/fonts/fondamento-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/fondamento-400.woff2 +0 -0
- package/app/public/vendor/fonts/opendyslexic-400.woff2 +0 -0
- package/app/public/vendor/fonts/pressstart-400.woff2 +0 -0
- package/app/public/vendor/fonts.css +43 -2
- package/dist-pkg/boot/create-game.d.ts +76 -2
- package/dist-pkg/boot/create-game.js +271 -13
- package/dist-pkg/core/constants.d.ts +0 -28
- package/dist-pkg/core/constants.js +34 -17
- package/dist-pkg/core/contract.d.ts +39 -0
- package/dist-pkg/core/contract.js +36 -0
- package/dist-pkg/core/entity.d.ts +47 -4
- package/dist-pkg/core/layers.d.ts +18 -0
- package/dist-pkg/core/layers.js +18 -0
- package/dist-pkg/core/rng.js +14 -4
- package/dist-pkg/core/route.d.ts +42 -0
- package/dist-pkg/core/route.js +158 -0
- package/dist-pkg/core/state.d.ts +10 -1
- package/dist-pkg/core/state.js +13 -0
- package/dist-pkg/educational/adaptive-engine.d.ts +65 -0
- package/dist-pkg/educational/adaptive-engine.js +117 -0
- package/dist-pkg/educational/segment-bar.d.ts +96 -0
- package/dist-pkg/educational/segment-bar.js +89 -0
- package/dist-pkg/i18n/en.js +33 -0
- package/dist-pkg/i18n/es.js +33 -0
- package/dist-pkg/i18n/pt.js +46 -0
- package/dist-pkg/input/default-bindings.d.ts +40 -0
- package/dist-pkg/input/default-bindings.js +143 -9
- package/dist-pkg/input/gamepad.d.ts +18 -11
- package/dist-pkg/input/gamepad.js +75 -9
- package/dist-pkg/input/keyboard-runtime.d.ts +6 -8
- package/dist-pkg/input/keyboard-runtime.js +24 -5
- package/dist-pkg/input/keyboard.d.ts +10 -0
- package/dist-pkg/input/keyboard.js +41 -11
- package/dist-pkg/input/keydown.d.ts +27 -2
- package/dist-pkg/input/keydown.js +20 -6
- package/dist-pkg/input/latch-scope.d.ts +70 -0
- package/dist-pkg/input/latch-scope.js +110 -0
- package/dist-pkg/input/latch-store.d.ts +45 -0
- package/dist-pkg/input/latch-store.js +74 -0
- package/dist-pkg/input/origem-sintetica.d.ts +44 -0
- package/dist-pkg/input/origem-sintetica.js +60 -0
- package/dist-pkg/input/pointer.d.ts +61 -0
- package/dist-pkg/input/pointer.js +71 -0
- package/dist-pkg/input/state.d.ts +86 -1
- package/dist-pkg/input/state.js +128 -1
- package/dist-pkg/input/touch-bindings.d.ts +11 -2
- package/dist-pkg/input/touch-bindings.js +9 -3
- package/dist-pkg/input/touch.js +9 -1
- package/dist-pkg/input/transporte-em-uso.d.ts +101 -0
- package/dist-pkg/input/transporte-em-uso.js +130 -0
- package/dist-pkg/input/transports.d.ts +88 -2
- package/dist-pkg/input/transports.js +60 -5
- package/dist-pkg/input/vocabulary-migration.d.ts +18 -7
- package/dist-pkg/platform/audio-earcons.d.ts +29 -2
- package/dist-pkg/platform/audio-earcons.js +10 -0
- package/dist-pkg/platform/audio-nav.d.ts +1 -1
- package/dist-pkg/platform/audio-sonar.d.ts +149 -9
- package/dist-pkg/platform/audio-sonar.js +232 -21
- package/dist-pkg/platform/guide-intensity.d.ts +40 -0
- package/dist-pkg/platform/guide-intensity.js +73 -0
- package/dist-pkg/platform/storage.d.ts +30 -0
- package/dist-pkg/platform/storage.js +35 -0
- package/dist-pkg/platform/tts.js +16 -1
- package/dist-pkg/platform/voice-plan.d.ts +90 -0
- package/dist-pkg/platform/voice-plan.js +137 -0
- package/dist-pkg/render/draw.d.ts +1 -1
- package/dist-pkg/render/draw.js +15 -5
- package/dist-pkg/render/viz-axes.d.ts +17 -0
- package/dist-pkg/render/viz-axes.js +19 -0
- package/dist-pkg/render/viz-setters.d.ts +34 -2
- package/dist-pkg/render/viz-setters.js +189 -33
- package/dist-pkg/render/wheelchair-sprites.d.ts +11 -3
- package/dist-pkg/render/wheelchair-sprites.js +10 -4
- package/dist-pkg/ui/dom.js +20 -2
- package/dist-pkg/ui/fonts.d.ts +47 -1
- package/dist-pkg/ui/fonts.js +47 -9
- package/dist-pkg/ui/latch-refusal.d.ts +32 -0
- package/dist-pkg/ui/latch-refusal.js +60 -0
- package/dist-pkg/ui/layout.d.ts +32 -0
- package/dist-pkg/ui/layout.js +64 -1
- package/dist-pkg/ui/motion-scene.d.ts +41 -0
- package/dist-pkg/ui/motion-scene.js +76 -0
- package/dist-pkg/ui/panel-shell.d.ts +53 -0
- package/dist-pkg/ui/panel-shell.js +103 -0
- package/dist-pkg/ui/pause-icons.d.ts +134 -20
- package/dist-pkg/ui/pause-icons.js +307 -45
- package/dist-pkg/ui/reach-notice.js +8 -0
- package/dist-pkg/ui/settings-audio.d.ts +14 -1
- package/dist-pkg/ui/settings-audio.js +43 -3
- package/dist-pkg/ui/settings-controls.d.ts +66 -6
- package/dist-pkg/ui/settings-controls.js +179 -17
- package/dist-pkg/ui/settings-empathy.d.ts +15 -0
- package/dist-pkg/ui/settings-empathy.js +2 -0
- package/dist-pkg/ui/settings-motion.d.ts +25 -19
- package/dist-pkg/ui/settings-motion.js +32 -19
- package/dist-pkg/ui/settings-motor.d.ts +81 -3
- package/dist-pkg/ui/settings-motor.js +118 -9
- package/dist-pkg/ui/settings-typo.d.ts +32 -2
- package/dist-pkg/ui/settings-typo.js +64 -18
- package/dist-pkg/ui/settings-visual.d.ts +21 -1
- package/dist-pkg/ui/settings-visual.js +48 -4
- package/dist-pkg/ui/shell.d.ts +13 -3
- package/dist-pkg/ui/shell.js +3 -4
- package/dist-pkg/ui/simulation-refusal.d.ts +32 -0
- package/dist-pkg/ui/simulation-refusal.js +57 -0
- package/dist-pkg/ui/visual-axes-panel.d.ts +48 -0
- package/dist-pkg/ui/visual-axes-panel.js +95 -0
- package/dist-pkg/ui/webcam.js +6 -1
- package/docs/CREDITS.md +18 -0
- package/docs/LICENSES.md +12 -0
- package/package.json +24 -4
- package/app/public/vendor/fonts/greatvibes-400.woff2 +0 -0
- package/app/public/vendor/fonts/ufcook-700.woff2 +0 -0
|
@@ -1,8 +1,28 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
-
import { type Spot, type Topology, type Speakable } from '../core/contract.js';
|
|
2
|
+
import { type Spot, type Topology, type Speakable, type Role } from '../core/contract.js';
|
|
3
3
|
export type SinkAC = AudioContext & {
|
|
4
4
|
setSinkId?: (id: string) => Promise<void>;
|
|
5
5
|
};
|
|
6
|
+
/**
|
|
7
|
+
* Quantos PASSOS da métrica declarada saturam o estéreo. Além disto, "à direita" é só à direita.
|
|
8
|
+
*
|
|
9
|
+
* ⚠️ ONZE NÃO É NÚMERO NOVO — é o que a plataforma sempre teve, relido na régua certa. O denominador antigo
|
|
10
|
+
* era `LOGICAL_W * 0.55 = 320 × 0,55 = 176` pixels, e a plataforma declara `unit: TILE` = 16: são **exatamente
|
|
11
|
+
* 11 tiles**. Reescrever em passos preserva o que essa criança já ouve, letra por letra, e passa a dizer o
|
|
12
|
+
* mesmo em qualquer gênero — 11 casas num tabuleiro, 11 metros num campo.
|
|
13
|
+
*
|
|
14
|
+
* E encaixa na régua que o resto do módulo já usa: `chaveDeDistancia` corta "muito perto" em 4 passos e
|
|
15
|
+
* "perto" em 9. O estéreo satura logo depois de a coisa passar a ser "longe", que é onde a direção deixa de
|
|
16
|
+
* precisar de mais precisão.
|
|
17
|
+
*/
|
|
18
|
+
export declare const PAN_PACES = 11;
|
|
19
|
+
/**
|
|
20
|
+
* Quanto vale UM passo, em unidades do mundo. Zero = este espaço não tem lado.
|
|
21
|
+
*
|
|
22
|
+
* Contínuo: o `unit` declarado. Grade: uma célula, por definição. Lista: nada — `hotspots` é uma ordem, não
|
|
23
|
+
* uma geometria, e inventar-lhe uma largura de estéreo seria apontar para um lado que não existe.
|
|
24
|
+
*/
|
|
25
|
+
export declare function passoDoMundo(topo: Topology): number;
|
|
6
26
|
/**
|
|
7
27
|
* O jogador visto pela navegação sonora. É a fatia MÍNIMA, e ela encolheu com o corte: `facing` ficou com a
|
|
8
28
|
* bengala (é ela que bate "à frente") e `wnT` com o nado. Sobrou identidade, posição, visão e o dispositivo.
|
|
@@ -11,10 +31,70 @@ export interface SonarPlayer extends PlayerAudioOut {
|
|
|
11
31
|
readonly i: number;
|
|
12
32
|
readonly x: number;
|
|
13
33
|
readonly y: number;
|
|
14
|
-
readonly viz: string;
|
|
15
34
|
readonly audioSink?: string | null;
|
|
16
|
-
guideT?: number;
|
|
17
35
|
}
|
|
36
|
+
/** O grafo contínuo do guia: oscilador → passa-baixo → ganho → panorâmica → categoria `guide`. */
|
|
37
|
+
export interface GuiaVivo {
|
|
38
|
+
/** O contexto que o construiu — é dele que sai o `currentTime` de cada `setTargetAtTime`. */
|
|
39
|
+
readonly ac: AudioContext;
|
|
40
|
+
readonly osc: OscillatorNode;
|
|
41
|
+
readonly filtro: BiquadFilterNode;
|
|
42
|
+
readonly ganho: GainNode;
|
|
43
|
+
/** `null` em motor sem `createStereoPanner` — o guia fica mono em vez de não existir. */
|
|
44
|
+
readonly panner: StereoPannerNode | null;
|
|
45
|
+
/** Quadros desde a última vez que a ROTA foi recalculada. A rota é cara; o som não pode esperar por ela. */
|
|
46
|
+
desdeARota: number;
|
|
47
|
+
/** O último `passos` medido. É daqui que a intensidade sai a cada quadro. */
|
|
48
|
+
passos: number;
|
|
49
|
+
/** A última panorâmica medida, pelo mesmo motivo: ela vem do alvo, que só se procura com a rota. */
|
|
50
|
+
pan: number;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* ⚠️ O TIMBRE TEM DE TER HARMÓNICOS, e isto é requisito técnico, não gosto.
|
|
54
|
+
*
|
|
55
|
+
* O eixo do #84 item 2 é o BRILHO, e brilho é um passa-baixo a abrir e a fechar. **Um passa-baixo sobre uma
|
|
56
|
+
* onda `sine` não faz absolutamente nada**: a senoide não tem nada acima da fundamental para o filtro cortar,
|
|
57
|
+
* e o guia ficaria com um eixo morto e só o volume a trabalhar. A dente-de-serra é a mais rica das quatro
|
|
58
|
+
* ondas do Web Audio — tem TODOS os harmónicos —, e é por isso que ela é a escolha.
|
|
59
|
+
*
|
|
60
|
+
* E ela resolve, de graça, a outra restrição do plano: o sonar e a bengala usam `sine`. Um timbre diferente
|
|
61
|
+
* era exigência de não colidirem no mesmo canal; aqui o timbre diferente É o mecanismo.
|
|
62
|
+
*/
|
|
63
|
+
export declare const GUIA_TIPO: OscillatorType;
|
|
64
|
+
/**
|
|
65
|
+
* A fundamental do guia, fixa.
|
|
66
|
+
*
|
|
67
|
+
* ⚠️ FIXA DE PROPÓSITO: a ALTURA já é a linguagem do sonar (`380 + 740 * near`, mais perto = mais agudo). Se o
|
|
68
|
+
* guia também subisse de tom, os dois estariam a dizer a mesma coisa pelo mesmo meio, e quem ouve os dois ao
|
|
69
|
+
* mesmo tempo não teria como separá-los. O guia diz distância por brilho; o sonar, por altura.
|
|
70
|
+
*/
|
|
71
|
+
export declare const GUIA_HZ = 220;
|
|
72
|
+
/**
|
|
73
|
+
* Quantos quadros entre dois cálculos de rota.
|
|
74
|
+
*
|
|
75
|
+
* ⚠️ A ROTA É UMA BUSCA EM LARGURA, e o `__incl.update(dt)` conta QUADROS: correr uma BFS a cada quadro num
|
|
76
|
+
* mapa de plataforma gasta o orçamento do quadro inteiro no aparelho-alvo (Positivo/Chromebook, pilar 1). Doze
|
|
77
|
+
* quadros são ~0,2 s a 60 fps — mais depressa do que a criança anda um passo, e o som não espera por eles: a
|
|
78
|
+
* intensidade é reescrita TODO quadro, com o `passos` que a última rota deixou.
|
|
79
|
+
*/
|
|
80
|
+
export declare const QUADROS_ENTRE_ROTAS = 12;
|
|
81
|
+
/**
|
|
82
|
+
* O tecto de pontos da rota do guia. Bem abaixo dos 4096 do `core/route`, e o próprio módulo diz porquê:
|
|
83
|
+
* «uma pista por quadro tolera muito menos do que um cálculo ao carregar a fase». Estourar devolve `null`, que
|
|
84
|
+
* é «não sei» — e o guia cai na reta, que ainda soa.
|
|
85
|
+
*/
|
|
86
|
+
export declare const ORCAMENTO_DA_ROTA = 1024;
|
|
87
|
+
/**
|
|
88
|
+
* O ganho de base do guia, antes de a intensidade e o volume mestre o multiplicarem.
|
|
89
|
+
*
|
|
90
|
+
* ⚠️ MAIS BAIXO DO QUE O BIPE QUE ELE SUBSTITUI (0,11), e não por engano: um som que **nunca para** é
|
|
91
|
+
* percebido como mais alto do que um transiente do mesmo pico, e cansa por permanência em vez de por
|
|
92
|
+
* intensidade — exactamente o que o modo TEA existe para não fazer.
|
|
93
|
+
*/
|
|
94
|
+
export declare const GUIA_VOL = 0.06;
|
|
95
|
+
/** Constante de tempo do `setTargetAtTime`. Curta o bastante para acompanhar o passo, longa o bastante para
|
|
96
|
+
* que a mudança seja um deslize e não um degrau — um degrau a cada rota seria um bipe outra vez. */
|
|
97
|
+
export declare const TAU_DO_GUIA = 0.08;
|
|
18
98
|
/**
|
|
19
99
|
* A SAÍDA DEDICADA de um jogador, e este módulo é o DONO dela: é aqui que os dois campos NASCEM
|
|
20
100
|
* (`new AC()` + `createGain()`, logo abaixo). Pelo ADR-0039 o dono declara onde o campo nasce, e o
|
|
@@ -28,14 +108,25 @@ export interface SonarPlayer extends PlayerAudioOut {
|
|
|
28
108
|
export interface PlayerAudioOut {
|
|
29
109
|
_ac?: SinkAC | null;
|
|
30
110
|
_acOut?: GainNode | null;
|
|
111
|
+
/**
|
|
112
|
+
* O GRAFO VIVO DO GUIA deste jogador, e mora AQUI, ao lado dos outros dois, pelo mesmo motivo que eles
|
|
113
|
+
* (ADR-0039: o dono declara onde o campo nasce; ADR-0033: a entidade da engine declara o que a ENGINE
|
|
114
|
+
* possui, e um oscilador não é dela). `null`/ausente = calado.
|
|
115
|
+
*
|
|
116
|
+
* ⚠️ ELE PRECISA DE SOBREVIVER AOS QUADROS, e é aí que ele difere de tudo o resto neste ficheiro. O sonar e
|
|
117
|
+
* o bipe antigo criavam um oscilador, tocavam-no e deitavam-no fora; uma presença CONTÍNUA (#84 item 2) é um
|
|
118
|
+
* oscilador que FICA, com o filtro e o ganho a mover-se por baixo dele. É por isso que ele tem de estar
|
|
119
|
+
* pendurado no jogador: não há outro lugar onde algo por jogador dure de um quadro para o outro.
|
|
120
|
+
*
|
|
121
|
+
* ⚠️ E É POR ISSO QUE `desligarGuia` EXISTE. Um campo que dura é um campo que vaza: sem alguém a pará-lo,
|
|
122
|
+
* desligar a categoria `guide` no mixer deixaria o som a tocar.
|
|
123
|
+
*/
|
|
124
|
+
_guia?: GuiaVivo | null;
|
|
31
125
|
}
|
|
32
126
|
export interface PlayerCtxOut {
|
|
33
127
|
ac: AudioContext;
|
|
34
128
|
out: GainNode;
|
|
35
129
|
}
|
|
36
|
-
type VizDef = {
|
|
37
|
-
kind?: string;
|
|
38
|
-
} | undefined;
|
|
39
130
|
export interface SonarCtx {
|
|
40
131
|
/** Campo 1: a métrica. Função, porque um jogo com fases troca de topologia entre elas. */
|
|
41
132
|
topology: () => Topology;
|
|
@@ -43,13 +134,57 @@ export interface SonarCtx {
|
|
|
43
134
|
targetsOf: (playerIndex: number) => readonly Spot[];
|
|
44
135
|
/** Campo 3: como se chama o que está ali. `null` = sem nome, e o anúncio cai no genérico. */
|
|
45
136
|
nameAt: (at: Spot) => Speakable | null;
|
|
137
|
+
/**
|
|
138
|
+
* Campo 2: o que há neste ponto. É o que o `core/route` atravessa (ou não) para achar o caminho.
|
|
139
|
+
*
|
|
140
|
+
* ⚠️ OPCIONAL AQUI, e obrigatório na `GameDeclaration` — a diferença não é descuido. Tornar um campo do
|
|
141
|
+
* `SonarCtx` obrigatório quebra todo o consumidor que já monta este ctx à mão, e o `create-game` (que é
|
|
142
|
+
* quem o monta de verdade) sempre o tem, porque o `validateDeclaration` o exige. Ausente aqui, o guia não
|
|
143
|
+
* fica calado: ele cai na distância em reta, que é o que ele já fazia antes desta mudança.
|
|
144
|
+
*/
|
|
145
|
+
roleAt?: (at: Spot) => Role;
|
|
46
146
|
tonePan: (freq: number, dur: number, cat: string, pan?: number | null, vol?: number, type?: OscillatorType, pc?: PlayerCtxOut | null) => void;
|
|
47
147
|
srSay: (text: string) => void;
|
|
48
148
|
narrate: (text: string) => void;
|
|
49
|
-
|
|
149
|
+
/**
|
|
150
|
+
* O barramento de uma categoria do mixer. A MESMA forma que `audio-ambient`, `audio-earcons`, `audio-jingles`
|
|
151
|
+
* e `tts` já recebem — o guia deixou de poder usar o `tonePan` porque `tonePan` toca e esquece, e uma
|
|
152
|
+
* presença contínua é um grafo que FICA.
|
|
153
|
+
*
|
|
154
|
+
* Opcional pelo mesmo motivo que o `roleAt`: quem não o injectar cai no `audioOut` e, na falta dele, no
|
|
155
|
+
* `destination`. O que se perde é o cursor da categoria `guide`, não o som.
|
|
156
|
+
*/
|
|
157
|
+
catNode?: (cat: string) => AudioNode | null;
|
|
158
|
+
/** O nó mestre, recuo do `catNode`. */
|
|
159
|
+
audioOut?: () => AudioNode | null;
|
|
160
|
+
/**
|
|
161
|
+
* O volume mestre (0..1), que cada síntese multiplica por si — o `_masterGain` do `platform/audio` é o mudo
|
|
162
|
+
* da pausa, não o cursor. Ausente = 1: o guia soa, e ignora o cursor. Por isso o `create-game` injecta-o.
|
|
163
|
+
*/
|
|
164
|
+
getVolume?: () => number;
|
|
165
|
+
/**
|
|
166
|
+
* Esta criança tem a visão comprometida — cegueira simulada ou baixa visão?
|
|
167
|
+
*
|
|
168
|
+
* ⚠️ SUBSTITUIU O `VIZ_BY_KEY` EM 2026-09-08 (#104), e a troca encolheu este módulo em vez de o migrar.
|
|
169
|
+
* Ele recebia a TABELA de modos de render e atravessava-a com `pl.viz`; agora recebe a RESPOSTA. Quem a dá
|
|
170
|
+
* é a raiz de composição, que conhece os dois eixos e pode importar de `render/` — o que este ficheiro,
|
|
171
|
+
* estando em `platform/`, não pode sem inverter uma aresta de camada.
|
|
172
|
+
*
|
|
173
|
+
* O `pl` inteiro e não o índice: quem responde já tem o jogador em mão, e passar o índice obrigaria a raiz
|
|
174
|
+
* a procurá-lo outra vez numa lista que ela acabou de percorrer.
|
|
175
|
+
*/
|
|
176
|
+
visaoComprometida: (pl: SonarPlayer) => boolean;
|
|
50
177
|
getModoCego: () => boolean;
|
|
51
178
|
/** Largura LÓGICA da tela. É do console, não do gênero — por isso ctx, e não campo de contrato. */
|
|
52
|
-
|
|
179
|
+
/**
|
|
180
|
+
* @deprecated ⚠️ SEM LEITOR DESDE 2026-09-07 (#121). Era o denominador do pan, e era o defeito: media a
|
|
181
|
+
* largura do estéreo em pixels de ecrã e dividia por ela uma distância de MUNDO. Agora a largura vem da
|
|
182
|
+
* topologia (`PAN_PACES * passoDoMundo`).
|
|
183
|
+
*
|
|
184
|
+
* Ficou OPCIONAL em vez de removido — tornar um campo obrigatório em opcional é compatível para trás, e
|
|
185
|
+
* quem já o injecta continua a compilar. Removê-lo de vez é candidato ao próximo major.
|
|
186
|
+
*/
|
|
187
|
+
LOGICAL_W?: number;
|
|
53
188
|
getPlayers: () => SonarPlayer[];
|
|
54
189
|
getNumPlayers: () => number;
|
|
55
190
|
getAudioCtx: () => AudioContext | null;
|
|
@@ -65,7 +200,12 @@ export interface AudioSonar {
|
|
|
65
200
|
sonar: (pl: SonarPlayer) => void;
|
|
66
201
|
updateGuide: () => void;
|
|
67
202
|
readonly sonarCount: number;
|
|
203
|
+
/**
|
|
204
|
+
* ⚠️ MUDOU DE SIGNIFICADO com o #84 item 2, e o número passa a ser muito maior. Contava BIPES (um a cada 48
|
|
205
|
+
* quadros); conta agora QUADROS EM QUE O GUIA SOA, porque não há mais nada discreto para contar — é essa a
|
|
206
|
+
* mudança. Quem o lê para dizer «o guia está a funcionar» continua certo; quem o lesse para dizer «tocou
|
|
207
|
+
* três vezes» estaria a perguntar por uma coisa que deixou de existir.
|
|
208
|
+
*/
|
|
68
209
|
readonly guideCount: number;
|
|
69
210
|
}
|
|
70
211
|
export declare function createAudioSonar(ctx: SonarCtx): AudioSonar;
|
|
71
|
-
export {};
|
|
@@ -24,13 +24,96 @@
|
|
|
24
24
|
// · `t('sr.nav.coin')` (o nome do alvo) → `nameAt(spot)`, campo 3. É o que faz o anúncio dizer "moeda",
|
|
25
25
|
// "pergunta" ou "caixa" sem a engine conhecer nenhum dos três.
|
|
26
26
|
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
27
|
+
// ⚠️ E SOBRAVA UMA, QUE ERA A ÚLTIMA E A PIOR: o pan media a largura do estéreo em `LOGICAL_W * 0.55` — a
|
|
28
|
+
// largura da TELA. Escrevi aqui que ela era «da tela e não do gênero, e por isso entra por ctx». A frase
|
|
29
|
+
// estava certa sobre a origem e errada sobre a consequência: o `wx` que ela divide vem da TOPOLOGIA, e
|
|
30
|
+
// dividir uma medida de mundo pela largura de um ecrã só funciona quando as duas usam a mesma régua.
|
|
31
|
+
//
|
|
32
|
+
// Medido ao construir o `game-soccer` (#121): num campo de 90 METROS, um colega dez metros à direita dá
|
|
33
|
+
// `10 / 176 = 0,057` — mono, na prática. O sonar ficaria **certo e inaudível**, que é a mesma classe de
|
|
34
|
+
// defeito que o quiz registou como «certo e inútil». Para a plataforma era verdade por acaso (o mundo dela é
|
|
35
|
+
// medido em pixels) e para `grid`/`hotspots` era vácuo, e é por isso que nunca se viu.
|
|
36
|
+
//
|
|
37
|
+
// A largura do estéreo passa a vir da métrica declarada: `PAN_PACES * passo`, onde o passo é o `unit` do
|
|
38
|
+
// contínuo, uma célula na grade, e nada na lista — que não tem espaço.
|
|
29
39
|
//
|
|
30
40
|
// ========================= SEM I/O NO IMPORT =========================
|
|
31
41
|
// Nada aqui toca `window` fora de `playerCtx`, que é chamada e não importada. Roda no project `node`.
|
|
32
42
|
import { distance, bearing } from '../core/contract.js';
|
|
33
43
|
import { t } from '../core/i18n.js';
|
|
44
|
+
// O GUIA (#84 item 2) é feito destes dois, e de mais nada: a ROTA diz quantos passos faltam contornando
|
|
45
|
+
// parede, e a INTENSIDADE traduz esse número em brilho e volume. Nenhum dos dois toca no Web Audio; a fiação
|
|
46
|
+
// — a única parte que toca — é o `updateGuide` lá em baixo, e é por isso que o desenho é conferível em `node`.
|
|
47
|
+
import { rotaAte } from '../core/route.js';
|
|
48
|
+
import { intensidadeDoGuia, CORTE_LONGE, PASSOS_ATE_O_FUNDO } from './guide-intensity.js';
|
|
49
|
+
/**
|
|
50
|
+
* Quantos PASSOS da métrica declarada saturam o estéreo. Além disto, "à direita" é só à direita.
|
|
51
|
+
*
|
|
52
|
+
* ⚠️ ONZE NÃO É NÚMERO NOVO — é o que a plataforma sempre teve, relido na régua certa. O denominador antigo
|
|
53
|
+
* era `LOGICAL_W * 0.55 = 320 × 0,55 = 176` pixels, e a plataforma declara `unit: TILE` = 16: são **exatamente
|
|
54
|
+
* 11 tiles**. Reescrever em passos preserva o que essa criança já ouve, letra por letra, e passa a dizer o
|
|
55
|
+
* mesmo em qualquer gênero — 11 casas num tabuleiro, 11 metros num campo.
|
|
56
|
+
*
|
|
57
|
+
* E encaixa na régua que o resto do módulo já usa: `chaveDeDistancia` corta "muito perto" em 4 passos e
|
|
58
|
+
* "perto" em 9. O estéreo satura logo depois de a coisa passar a ser "longe", que é onde a direção deixa de
|
|
59
|
+
* precisar de mais precisão.
|
|
60
|
+
*/
|
|
61
|
+
export const PAN_PACES = 11;
|
|
62
|
+
/**
|
|
63
|
+
* Quanto vale UM passo, em unidades do mundo. Zero = este espaço não tem lado.
|
|
64
|
+
*
|
|
65
|
+
* Contínuo: o `unit` declarado. Grade: uma célula, por definição. Lista: nada — `hotspots` é uma ordem, não
|
|
66
|
+
* uma geometria, e inventar-lhe uma largura de estéreo seria apontar para um lado que não existe.
|
|
67
|
+
*/
|
|
68
|
+
export function passoDoMundo(topo) {
|
|
69
|
+
return topo.kind === 'continuous' ? topo.unit : topo.kind === 'grid' ? 1 : 0;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* ⚠️ O TIMBRE TEM DE TER HARMÓNICOS, e isto é requisito técnico, não gosto.
|
|
73
|
+
*
|
|
74
|
+
* O eixo do #84 item 2 é o BRILHO, e brilho é um passa-baixo a abrir e a fechar. **Um passa-baixo sobre uma
|
|
75
|
+
* onda `sine` não faz absolutamente nada**: a senoide não tem nada acima da fundamental para o filtro cortar,
|
|
76
|
+
* e o guia ficaria com um eixo morto e só o volume a trabalhar. A dente-de-serra é a mais rica das quatro
|
|
77
|
+
* ondas do Web Audio — tem TODOS os harmónicos —, e é por isso que ela é a escolha.
|
|
78
|
+
*
|
|
79
|
+
* E ela resolve, de graça, a outra restrição do plano: o sonar e a bengala usam `sine`. Um timbre diferente
|
|
80
|
+
* era exigência de não colidirem no mesmo canal; aqui o timbre diferente É o mecanismo.
|
|
81
|
+
*/
|
|
82
|
+
export const GUIA_TIPO = 'sawtooth';
|
|
83
|
+
/**
|
|
84
|
+
* A fundamental do guia, fixa.
|
|
85
|
+
*
|
|
86
|
+
* ⚠️ FIXA DE PROPÓSITO: a ALTURA já é a linguagem do sonar (`380 + 740 * near`, mais perto = mais agudo). Se o
|
|
87
|
+
* guia também subisse de tom, os dois estariam a dizer a mesma coisa pelo mesmo meio, e quem ouve os dois ao
|
|
88
|
+
* mesmo tempo não teria como separá-los. O guia diz distância por brilho; o sonar, por altura.
|
|
89
|
+
*/
|
|
90
|
+
export const GUIA_HZ = 220;
|
|
91
|
+
/**
|
|
92
|
+
* Quantos quadros entre dois cálculos de rota.
|
|
93
|
+
*
|
|
94
|
+
* ⚠️ A ROTA É UMA BUSCA EM LARGURA, e o `__incl.update(dt)` conta QUADROS: correr uma BFS a cada quadro num
|
|
95
|
+
* mapa de plataforma gasta o orçamento do quadro inteiro no aparelho-alvo (Positivo/Chromebook, pilar 1). Doze
|
|
96
|
+
* quadros são ~0,2 s a 60 fps — mais depressa do que a criança anda um passo, e o som não espera por eles: a
|
|
97
|
+
* intensidade é reescrita TODO quadro, com o `passos` que a última rota deixou.
|
|
98
|
+
*/
|
|
99
|
+
export const QUADROS_ENTRE_ROTAS = 12;
|
|
100
|
+
/**
|
|
101
|
+
* O tecto de pontos da rota do guia. Bem abaixo dos 4096 do `core/route`, e o próprio módulo diz porquê:
|
|
102
|
+
* «uma pista por quadro tolera muito menos do que um cálculo ao carregar a fase». Estourar devolve `null`, que
|
|
103
|
+
* é «não sei» — e o guia cai na reta, que ainda soa.
|
|
104
|
+
*/
|
|
105
|
+
export const ORCAMENTO_DA_ROTA = 1024;
|
|
106
|
+
/**
|
|
107
|
+
* O ganho de base do guia, antes de a intensidade e o volume mestre o multiplicarem.
|
|
108
|
+
*
|
|
109
|
+
* ⚠️ MAIS BAIXO DO QUE O BIPE QUE ELE SUBSTITUI (0,11), e não por engano: um som que **nunca para** é
|
|
110
|
+
* percebido como mais alto do que um transiente do mesmo pico, e cansa por permanência em vez de por
|
|
111
|
+
* intensidade — exactamente o que o modo TEA existe para não fazer.
|
|
112
|
+
*/
|
|
113
|
+
export const GUIA_VOL = 0.06;
|
|
114
|
+
/** Constante de tempo do `setTargetAtTime`. Curta o bastante para acompanhar o passo, longa o bastante para
|
|
115
|
+
* que a mudança seja um deslize e não um degrau — um degrau a cada rota seria um bipe outra vez. */
|
|
116
|
+
export const TAU_DO_GUIA = 0.08;
|
|
34
117
|
export function createAudioSonar(ctx) {
|
|
35
118
|
let _sonarCount = 0, _guideCount = 0;
|
|
36
119
|
/** AudioContext do jogador (para o `setSinkId` no dispositivo dele), ou null → contexto global. */
|
|
@@ -57,14 +140,32 @@ export function createAudioSonar(ctx) {
|
|
|
57
140
|
}
|
|
58
141
|
}
|
|
59
142
|
function panFor(wx, pl) {
|
|
60
|
-
|
|
143
|
+
const passo = passoDoMundo(ctx.topology());
|
|
144
|
+
// `hotspots` não tem espaço, logo não tem lado. O `bearing` já responde `none` pelo mesmo motivo, e
|
|
145
|
+
// centrar é a única resposta honesta — um pan calculado sobre índices de lista aponta para nada.
|
|
146
|
+
if (!(passo > 0))
|
|
147
|
+
return 0;
|
|
148
|
+
return Math.max(-1, Math.min(1, (wx - pl.x) / (PAN_PACES * passo)));
|
|
61
149
|
}
|
|
62
|
-
/**
|
|
150
|
+
/**
|
|
151
|
+
* Visão comprometida? Guarda e guia só existem quando a resposta é sim (ou no modo cego).
|
|
152
|
+
*
|
|
153
|
+
* ⚠️ A METADE VISUAL PASSOU A SER INJECTADA (#104), e o módulo ficou MENOR em vez de migrado. Ele
|
|
154
|
+
* consultava `ctx.VIZ_BY_KEY[pl.viz]` — uma tabela de modos de RENDER, atravessada por uma chave de
|
|
155
|
+
* render, dentro de `platform/`. A #104 obrigava a escolher: ou este ficheiro passava a importar
|
|
156
|
+
* `render/viz-axes` (uma aresta ao contrário: medido, `render/` importa de `platform/` em cinco pontos e o
|
|
157
|
+
* inverso em nenhum), ou deixava de saber o que é um modo visual.
|
|
158
|
+
*
|
|
159
|
+
* A segunda é a certa, e é o movimento que o `touch.ts` já nomeia como «o mesmo do achado 10
|
|
160
|
+
* (`isNavigable`): injetar o BOOLEANO, não o estado». O que este módulo precisa de saber é «esta criança
|
|
161
|
+
* precisa de pista sonora», e isso não é uma pergunta sobre tabelas de filtro — é uma pergunta que a raiz
|
|
162
|
+
* de composição responde, porque é ela que conhece os dois eixos.
|
|
163
|
+
*
|
|
164
|
+
* O que FICA aqui é a regra que é mesmo deste módulo: **o modo cego liga as pistas para toda a gente**,
|
|
165
|
+
* independentemente do que a visão diga.
|
|
166
|
+
*/
|
|
63
167
|
function needsAudioCues(pl) {
|
|
64
|
-
|
|
65
|
-
return true;
|
|
66
|
-
const m = ctx.VIZ_BY_KEY[pl.viz];
|
|
67
|
-
return !!(m && (m.kind === 'blind' || m.kind === 'lowvision'));
|
|
168
|
+
return ctx.getModoCego() || ctx.visaoComprometida(pl);
|
|
68
169
|
}
|
|
69
170
|
/**
|
|
70
171
|
* O alvo mais perto deste jogador, na MÉTRICA DECLARADA — e `null` se não houver nenhum.
|
|
@@ -145,23 +246,133 @@ export function createAudioSonar(ctx) {
|
|
|
145
246
|
ctx.srSay(msg);
|
|
146
247
|
ctx.narrate(msg);
|
|
147
248
|
}
|
|
148
|
-
/**
|
|
249
|
+
/**
|
|
250
|
+
* QUANTOS PASSOS FALTAM, e a resposta preferida é a que contorna parede.
|
|
251
|
+
*
|
|
252
|
+
* ⚠️ AS DUAS RESPOSTAS JÁ ESTÃO NA MESMA UNIDADE, e é a única razão pela qual esta função é uma linha em vez
|
|
253
|
+
* de uma conversão: `rotaAte().passos` conta passos por definição, e `distance()` também devolve passos em
|
|
254
|
+
* TODA topologia — ela própria divide pela `unit` no ramo contínuo. Dividir aqui outra vez pelo passo do
|
|
255
|
+
* mundo era o erro à espera de ser cometido, e num jogo com `unit = 16` ele poria o guia no brilho máximo
|
|
256
|
+
* para sempre. É o mesmo defeito que a #121 tirou do `panFor`: misturar régua de mundo com régua de ecrã.
|
|
257
|
+
*
|
|
258
|
+
* A rota perde-se de duas maneiras — jogo que não injectou `roleAt`, e orçamento estourado — e as duas caem
|
|
259
|
+
* no mesmo recuo: a RETA. ⚠️ Ela mente atrás de parede (diz «perto» de um alvo que exige dar a volta), e é
|
|
260
|
+
* por isso que é recuo e não escolha. Mas é o que o guia já dizia antes desta mudança, e continuar a dizê-lo
|
|
261
|
+
* é estritamente melhor do que calar — calar afirmaria que não há alvo.
|
|
262
|
+
*/
|
|
263
|
+
function passosAteOAlvo(pl, alvo) {
|
|
264
|
+
const roleAt = ctx.roleAt;
|
|
265
|
+
if (roleAt) {
|
|
266
|
+
const rota = rotaAte({ topology: ctx.topology(), roleAt, orcamento: ORCAMENTO_DA_ROTA }, { x: pl.x, y: pl.y }, [alvo.at]);
|
|
267
|
+
if (rota)
|
|
268
|
+
return rota.passos;
|
|
269
|
+
}
|
|
270
|
+
return alvo.d;
|
|
271
|
+
}
|
|
272
|
+
/** Acende o grafo contínuo deste jogador. `null` = não deu (sem contexto, ou motor sem Web Audio). */
|
|
273
|
+
function ligarGuia(pl) {
|
|
274
|
+
const pc = playerCtx(pl);
|
|
275
|
+
const ac = pc ? pc.ac : ctx.getAudioCtx();
|
|
276
|
+
if (!ac)
|
|
277
|
+
return null;
|
|
278
|
+
try {
|
|
279
|
+
const osc = ac.createOscillator(), filtro = ac.createBiquadFilter(), ganho = ac.createGain();
|
|
280
|
+
osc.type = GUIA_TIPO;
|
|
281
|
+
osc.frequency.value = GUIA_HZ;
|
|
282
|
+
filtro.type = 'lowpass';
|
|
283
|
+
filtro.frequency.value = CORTE_LONGE; // nasce no fundo da escala e sobe; nascer aberto seria um susto
|
|
284
|
+
ganho.gain.value = 0; // e nasce calado, para não estalar ao ligar
|
|
285
|
+
let saida = ganho;
|
|
286
|
+
let panner = null;
|
|
287
|
+
if (ac.createStereoPanner) {
|
|
288
|
+
panner = ac.createStereoPanner();
|
|
289
|
+
ganho.connect(panner);
|
|
290
|
+
saida = panner;
|
|
291
|
+
}
|
|
292
|
+
osc.connect(filtro).connect(ganho);
|
|
293
|
+
saida.connect(pc ? pc.out : (ctx.catNode?.('guide') || ctx.audioOut?.() || ac.destination));
|
|
294
|
+
osc.start();
|
|
295
|
+
// `desdeARota` nasce no tecto para que a PRIMEIRA volta já meça a rota, em vez de soar doze quadros
|
|
296
|
+
// com um `passos` inventado.
|
|
297
|
+
return { ac, osc, filtro, ganho, panner, desdeARota: QUADROS_ENTRE_ROTAS, passos: PASSOS_ATE_O_FUNDO, pan: 0 };
|
|
298
|
+
}
|
|
299
|
+
catch (e) {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Apaga o grafo — e ele TEM de ser apagado, porque um oscilador que fica é a diferença entre esta forma e a
|
|
305
|
+
* anterior. O bipe morria sozinho ao fim de 0,12 s; este toca até alguém o parar. Sem esta função, desligar
|
|
306
|
+
* a categoria `guide` no mixer deixaria o som a tocar, e trocar de modo visual deixaria um segundo grafo a
|
|
307
|
+
* somar-se ao primeiro.
|
|
308
|
+
*
|
|
309
|
+
* Desce em rampa (não corta) porque um corte seco num oscilador vivo é um clique — um transiente, que é
|
|
310
|
+
* precisamente o que este item existe para tirar do ouvido da criança.
|
|
311
|
+
*/
|
|
312
|
+
function desligarGuia(pl) {
|
|
313
|
+
const g = pl._guia;
|
|
314
|
+
if (!g)
|
|
315
|
+
return;
|
|
316
|
+
pl._guia = null;
|
|
317
|
+
try {
|
|
318
|
+
const agora = g.ac.currentTime;
|
|
319
|
+
g.ganho.gain.setTargetAtTime(0, agora, 0.05);
|
|
320
|
+
g.osc.stop(agora + 0.3);
|
|
321
|
+
}
|
|
322
|
+
catch (e) { /* noop */ }
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* A PRESENÇA CONTÍNUA, um quadro de cada vez (#84 item 2).
|
|
326
|
+
*
|
|
327
|
+
* ⚠️ O QUE SAIU DAQUI FOI UM BIPE: um `triangle` de 0,12 s a cada 48 quadros, para sempre, independente de a
|
|
328
|
+
* criança se mexer ou de algo ter mudado. O veredicto do Dev: «um ping é a pior escolha possível, tenebroso
|
|
329
|
+
* para quem tem TEA». O que entra não dispara nada — o som já está lá, e muda de brilho.
|
|
330
|
+
*
|
|
331
|
+
* ⚠️ E CALAR CONTINUA A SER UMA AFIRMAÇÃO, com um significado só: NÃO HÁ ALVO. É por isso que «sem alvo»
|
|
332
|
+
* apaga o grafo e «longe» não: o piso do `guide-intensity` (`VOL_LONGE`) existe exactamente para que a
|
|
333
|
+
* criança não confunda «está longe» com «não há nada para achar».
|
|
334
|
+
*/
|
|
149
335
|
function updateGuide() {
|
|
150
336
|
const cat = ctx.getAudioCat();
|
|
151
|
-
|
|
152
|
-
|
|
337
|
+
const ligado = !!ctx.getAudioCtx() && ctx.getSoundOn() && !!cat && !!cat.guide && cat.guide.on;
|
|
338
|
+
const vol = ctx.getVolume ? ctx.getVolume() : 1;
|
|
153
339
|
for (const pl of ctx.getPlayers()) {
|
|
154
|
-
if (!needsAudioCues(pl))
|
|
155
|
-
|
|
156
|
-
pl.guideT = (pl.guideT || 0) + 1;
|
|
157
|
-
if (pl.guideT < 48)
|
|
340
|
+
if (!ligado || !needsAudioCues(pl)) {
|
|
341
|
+
desligarGuia(pl);
|
|
158
342
|
continue;
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (!
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
343
|
+
}
|
|
344
|
+
let g = pl._guia;
|
|
345
|
+
if (!g) {
|
|
346
|
+
// ⚠️ A PERGUNTA «HÁ ALVO?» VEM ANTES DE ACENDER, e o gate cobrou-a: com o grafo a nascer primeiro, um
|
|
347
|
+
// jogador sem alvo criava um oscilador, media a rota, não achava nada e apagava-o — SESSENTA VEZES
|
|
348
|
+
// POR SEGUNDO. O bipe não tinha este problema porque não tinha nada que durasse; foi a permanência
|
|
349
|
+
// que o trouxe. `alvoMaisProximo` é um laço sobre `targetsOf`, não a BFS: perguntar por quadro custa
|
|
350
|
+
// zero quando a lista está vazia, que é exactamente o caso em questão.
|
|
351
|
+
if (!alvoMaisProximo(pl))
|
|
352
|
+
continue;
|
|
353
|
+
g = pl._guia = ligarGuia(pl);
|
|
354
|
+
if (!g)
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (++g.desdeARota >= QUADROS_ENTRE_ROTAS) {
|
|
358
|
+
g.desdeARota = 0;
|
|
359
|
+
const alvo = alvoMaisProximo(pl);
|
|
360
|
+
if (!alvo) {
|
|
361
|
+
desligarGuia(pl);
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
g.passos = passosAteOAlvo(pl, alvo);
|
|
365
|
+
g.pan = panFor(alvo.at.x, pl);
|
|
366
|
+
}
|
|
367
|
+
// TODO quadro, e não só quando a rota é nova: é isto que faz a mudança ser um deslize.
|
|
368
|
+
const i = intensidadeDoGuia(g.passos);
|
|
369
|
+
try {
|
|
370
|
+
const agora = g.ac.currentTime;
|
|
371
|
+
g.filtro.frequency.setTargetAtTime(i.corte, agora, TAU_DO_GUIA);
|
|
372
|
+
g.ganho.gain.setTargetAtTime(GUIA_VOL * i.volume * vol, agora, TAU_DO_GUIA);
|
|
373
|
+
g.panner?.pan.setTargetAtTime(g.pan, agora, TAU_DO_GUIA);
|
|
374
|
+
}
|
|
375
|
+
catch (e) { /* noop */ }
|
|
165
376
|
_guideCount++;
|
|
166
377
|
}
|
|
167
378
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
/** O que o guia soa, para uma dada distância. */
|
|
3
|
+
export interface Intensidade {
|
|
4
|
+
/** Corte do passa-baixo, em hertz. Grave e abafado longe; aberto e brilhante perto. */
|
|
5
|
+
readonly corte: number;
|
|
6
|
+
/** Fator sobre o volume da categoria `guide`, entre `VOL_LONGE` e 1. NUNCA zero. */
|
|
7
|
+
readonly volume: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A partir de quantos passos o guia deixa de escurecer mais.
|
|
11
|
+
*
|
|
12
|
+
* Doze, e o número não é novo: o `chaveDeDistancia` do sonar corta «muito perto» em 4 passos e «perto» em 9,
|
|
13
|
+
* e o `PAN_PACES` satura o estéreo em 11. O guia satura logo depois — a informação fina serve para quem já
|
|
14
|
+
* está a chegar, e mais longe do que isso «longe» basta.
|
|
15
|
+
*/
|
|
16
|
+
export declare const PASSOS_ATE_O_FUNDO = 12;
|
|
17
|
+
/** O corte no fundo da escala: abafado, presente, sem ser um som de alarme. */
|
|
18
|
+
export declare const CORTE_LONGE = 320;
|
|
19
|
+
/** O corte no alvo: aberto. Acima disto o timbre passa a sibilar, e sibilar chama atenção como um bipe. */
|
|
20
|
+
export declare const CORTE_PERTO = 3200;
|
|
21
|
+
/**
|
|
22
|
+
* O fator de volume mais baixo.
|
|
23
|
+
*
|
|
24
|
+
* ⚠️ NUNCA ZERO, E É A ASSERÇÃO MAIS IMPORTANTE DESTE FICHEIRO. Se o guia emudecesse ao longe, «longe» ficaria
|
|
25
|
+
* indistinguível de «não há alvo» — e a criança que depende dele concluiria que não há nada para achar,
|
|
26
|
+
* exatamente quando há e está distante. Silêncio é uma afirmação, e aqui seria uma afirmação falsa.
|
|
27
|
+
*/
|
|
28
|
+
export declare const VOL_LONGE = 0.55;
|
|
29
|
+
/**
|
|
30
|
+
* A intensidade para `passos` de distância ao longo da rota.
|
|
31
|
+
*
|
|
32
|
+
* ⚠️ A INTERPOLAÇÃO DO CORTE É EXPONENCIAL, e não linear, pelo mesmo motivo do earcon da #124: o ouvido
|
|
33
|
+
* percebe altura e brilho em RAZÃO, não em diferença. Uma rampa linear de 320 a 3200 abriria quase tudo no
|
|
34
|
+
* primeiro terço do caminho e depois pareceria parada — a criança sentiria que chegou quando ainda faltava
|
|
35
|
+
* metade.
|
|
36
|
+
*
|
|
37
|
+
* O volume interpola LINEARMENTE, e a assimetria é deliberada: ele é o eixo secundário, e uma curva também
|
|
38
|
+
* exponencial ali faria os dois acelerarem no mesmo ponto, que é o oposto de ter dois eixos.
|
|
39
|
+
*/
|
|
40
|
+
export declare function intensidadeDoGuia(passos: number): Intensidade;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// platform/guide-intensity — QUÃO PERTO SOA, sem bipe (#84 item 2).
|
|
3
|
+
//
|
|
4
|
+
// ========================= O QUE ISTO SUBSTITUI =========================
|
|
5
|
+
// O guia tocava um `triangle` de 0,12 s a cada 0,8 s, PARA SEMPRE, sem depender de movimento nem de nada ter
|
|
6
|
+
// mudado. O veredicto do Dev: «um ping é a pior escolha possível, tenebroso para quem tem TEA». Não era a
|
|
7
|
+
// frequência que estava errada — era o bipe. Reduzi-lo a «só andando» deixaria a mesma coisa a doer menos
|
|
8
|
+
// vezes.
|
|
9
|
+
//
|
|
10
|
+
// O que entra é uma presença CONTÍNUA que fica mais intensa conforme a criança se aproxima, ao longo da rota
|
|
11
|
+
// mapeada (`core/route`). Nada dispara; a coisa apenas fica mais presente.
|
|
12
|
+
//
|
|
13
|
+
// ========================= O EIXO É O BRILHO, E A DECISÃO É DO DEV =========================
|
|
14
|
+
// Quatro eixos foram postos na mesa — volume, brilho (corte de filtro), camadas e andamento — e a escolha foi
|
|
15
|
+
// **brilho como principal, com uma parcela pequena de volume como secundário**. As razões, para quem reabrir:
|
|
16
|
+
//
|
|
17
|
+
// · VOLUME SOZINHO colide com o cursor do mixer: a criança que baixou a categoria `guide` perderia o sinal
|
|
18
|
+
// inteiro, e variação de volume é a mais cansativa das quatro.
|
|
19
|
+
// · ANDAMENTO lê-se como PRESSA, que é o oposto do que o modo TEA existe para proteger.
|
|
20
|
+
// · CAMADAS precisa de material composto, e esta engine sintetiza.
|
|
21
|
+
// · BRILHO é contínuo, sai de um `BiquadFilter` que o Web Audio já tem, e não disputa o cursor.
|
|
22
|
+
//
|
|
23
|
+
// ⚠️ E A PARCELA DE VOLUME NÃO É ENFEITE: para uma criança com perda auditiva o brilho pode cair exatamente na
|
|
24
|
+
// banda que ela não alcança. Dois eixos redundantes significam que nenhum deles sozinho decide.
|
|
25
|
+
//
|
|
26
|
+
// ========================= O QUE ESTE MÓDULO NÃO FAZ =========================
|
|
27
|
+
// Não toca nada. Devolve dois números a partir de UM: quantos passos faltam ao longo da rota. Quem monta o
|
|
28
|
+
// grafo é o `platform/audio-sonar`, e quem calcula a rota é o `core/route` — que já sabe contornar parede, e é
|
|
29
|
+
// isso que torna a intensidade honesta: ela cresce com a distância que a criança REALMENTE vai andar.
|
|
30
|
+
//
|
|
31
|
+
// Módulo-folha: não importa nada.
|
|
32
|
+
/**
|
|
33
|
+
* A partir de quantos passos o guia deixa de escurecer mais.
|
|
34
|
+
*
|
|
35
|
+
* Doze, e o número não é novo: o `chaveDeDistancia` do sonar corta «muito perto» em 4 passos e «perto» em 9,
|
|
36
|
+
* e o `PAN_PACES` satura o estéreo em 11. O guia satura logo depois — a informação fina serve para quem já
|
|
37
|
+
* está a chegar, e mais longe do que isso «longe» basta.
|
|
38
|
+
*/
|
|
39
|
+
export const PASSOS_ATE_O_FUNDO = 12;
|
|
40
|
+
/** O corte no fundo da escala: abafado, presente, sem ser um som de alarme. */
|
|
41
|
+
export const CORTE_LONGE = 320;
|
|
42
|
+
/** O corte no alvo: aberto. Acima disto o timbre passa a sibilar, e sibilar chama atenção como um bipe. */
|
|
43
|
+
export const CORTE_PERTO = 3200;
|
|
44
|
+
/**
|
|
45
|
+
* O fator de volume mais baixo.
|
|
46
|
+
*
|
|
47
|
+
* ⚠️ NUNCA ZERO, E É A ASSERÇÃO MAIS IMPORTANTE DESTE FICHEIRO. Se o guia emudecesse ao longe, «longe» ficaria
|
|
48
|
+
* indistinguível de «não há alvo» — e a criança que depende dele concluiria que não há nada para achar,
|
|
49
|
+
* exatamente quando há e está distante. Silêncio é uma afirmação, e aqui seria uma afirmação falsa.
|
|
50
|
+
*/
|
|
51
|
+
export const VOL_LONGE = 0.55;
|
|
52
|
+
/**
|
|
53
|
+
* A intensidade para `passos` de distância ao longo da rota.
|
|
54
|
+
*
|
|
55
|
+
* ⚠️ A INTERPOLAÇÃO DO CORTE É EXPONENCIAL, e não linear, pelo mesmo motivo do earcon da #124: o ouvido
|
|
56
|
+
* percebe altura e brilho em RAZÃO, não em diferença. Uma rampa linear de 320 a 3200 abriria quase tudo no
|
|
57
|
+
* primeiro terço do caminho e depois pareceria parada — a criança sentiria que chegou quando ainda faltava
|
|
58
|
+
* metade.
|
|
59
|
+
*
|
|
60
|
+
* O volume interpola LINEARMENTE, e a assimetria é deliberada: ele é o eixo secundário, e uma curva também
|
|
61
|
+
* exponencial ali faria os dois acelerarem no mesmo ponto, que é o oposto de ter dois eixos.
|
|
62
|
+
*/
|
|
63
|
+
export function intensidadeDoGuia(passos) {
|
|
64
|
+
if (!Number.isFinite(passos) || passos < 0)
|
|
65
|
+
return { corte: CORTE_LONGE, volume: VOL_LONGE };
|
|
66
|
+
// 0 = em cima do alvo; 1 = no fundo da escala ou além.
|
|
67
|
+
const longe = Math.min(1, passos / PASSOS_ATE_O_FUNDO);
|
|
68
|
+
const perto = 1 - longe;
|
|
69
|
+
return {
|
|
70
|
+
corte: CORTE_LONGE * Math.pow(CORTE_PERTO / CORTE_LONGE, perto),
|
|
71
|
+
volume: VOL_LONGE + (1 - VOL_LONGE) * perto,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -62,6 +62,7 @@ export declare const KEYS: {
|
|
|
62
62
|
lang: string;
|
|
63
63
|
letterCase: string;
|
|
64
64
|
captions: string;
|
|
65
|
+
tea: string;
|
|
65
66
|
menuIndex: string;
|
|
66
67
|
fontKey: string;
|
|
67
68
|
padDesign: string;
|
|
@@ -74,9 +75,38 @@ export declare const KEYS: {
|
|
|
74
75
|
padDpadMm: string;
|
|
75
76
|
reducedMotion: string;
|
|
76
77
|
toggleMoveLegacy: string;
|
|
78
|
+
/**
|
|
79
|
+
* @deprecated ⚠️ A CHAVE LEGADA do modo visual — UM valor, do tempo em que só cabia um (issue #104).
|
|
80
|
+
*
|
|
81
|
+
* Continua a ser LIDA, e é isso que impede a criança de perder o que já escolheu; continua a ser ESCRITA
|
|
82
|
+
* enquanto os controles ainda escreverem um valor de cada vez, porque um leitor antigo (o cartucho na
|
|
83
|
+
* versão publicada) faz `if (v && VIZ_BY_KEY[v])` e rejeitaria um JSON — escrever a forma nova AQUI
|
|
84
|
+
* apagaria o ajuste dela em silêncio, que é exactamente o defeito que a migração existe para não cometer.
|
|
85
|
+
*/
|
|
77
86
|
vizP: (i: number) => string;
|
|
87
|
+
/**
|
|
88
|
+
* O ESTADO VISUAL de dois eixos, em JSON (ADR-0076, issue #104).
|
|
89
|
+
*
|
|
90
|
+
* ⚠️ CHAVE NOVA AO LADO DA VELHA, e não a mesma chave com conteúdo novo. É o mesmo desenho que o campo
|
|
91
|
+
* `visual` usa ao lado do `viz`: as duas formas coexistem enquanto houver leitores das duas, cada um lê a
|
|
92
|
+
* que entende, e a velha só morre quando não sobrar quem a leia. `migrarVisual` aceita as duas, então o
|
|
93
|
+
* recuo — chave nova ausente, chave velha presente — devolve exactamente o que a criança escolheu.
|
|
94
|
+
*/
|
|
95
|
+
visualP: (i: number) => string;
|
|
78
96
|
sinkP: (i: number) => string;
|
|
79
97
|
easyP: (i: number) => string;
|
|
98
|
+
/**
|
|
99
|
+
* ⚠️ AS DUAS DE BAIXO SÃO AS CHAVES LEGADAS desde 2026-09-08 (ADR-0104 §C, issue #114). Continuam a ser
|
|
100
|
+
* LIDAS — é o ajuste da criança, e a herança dele é o que a impede de o perder — e não voltam a ser
|
|
101
|
+
* escritas. O que se escreve é a chave COM TRANSPORTE, porque a alternância é do APARELHO e não da pessoa:
|
|
102
|
+
* ligá-la no controle de tela, onde ninguém segura um botão virtual com conforto, ligava-a também no
|
|
103
|
+
* teclado, onde segurar uma tecla é exactamente o que a criança sabe fazer.
|
|
104
|
+
*
|
|
105
|
+
* ⚠️ E A CHAVE NOVA NÃO MORA AQUI, de propósito. Ela é `chaveDaAlternancia`, em `input/latch-scope` — este
|
|
106
|
+
* ficheiro é módulo-FOLHA e `platform/` não importa de `input/`, que é a camada acima. Montá-la aqui
|
|
107
|
+
* exigiria ou uma aresta ao contrário ou uma segunda cópia do nome, e a segunda cópia é exactamente o que
|
|
108
|
+
* o comentário do bloco acima existe para impedir. O dono do nome é quem conhece a regra do transporte.
|
|
109
|
+
*/
|
|
80
110
|
toggleMoveP: (i: number) => string;
|
|
81
111
|
toggleRunP: (i: number) => string;
|
|
82
112
|
rmWalkP: (i: number) => string;
|