@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.
- 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
package/README.md
CHANGED
|
@@ -70,9 +70,11 @@ npm test # testes Vitest (node + browser via Playwright); npm run test
|
|
|
70
70
|
`allow_failure: true` e estavam vermelhos havia semanas. O histórico o guarda; o cabeçalho do `ci.yml`
|
|
71
71
|
documenta o porte linha a linha.
|
|
72
72
|
|
|
73
|
-
- **CD** —
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
- **CD** — ⚠️ **NÃO HÁ NENHUM**, hoje. Informado pelo Dev em 2026-09-07: **nenhum projeto do Cloudflare Pages
|
|
74
|
+
está conectado a repositório nenhum**. A ressalva anterior — «a conexão apontava para o GitLab, que agora
|
|
75
|
+
está arquivado» — descrevia um deploy desapontado; o estado atual é mais simples e mais grave de confundir:
|
|
76
|
+
**push não é publicação**, e `dist/` só chega a alguém por um passo manual.
|
|
77
|
+
A tabela abaixo fica como a RECEITA de quando houver conexão, e não como descrição do que existe:
|
|
76
78
|
|
|
77
79
|
| Configuração | Valor |
|
|
78
80
|
|---|---|
|
package/app/css/style.css
CHANGED
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
--z-menu:30000;
|
|
52
52
|
--z-menu-max:39999;
|
|
53
53
|
--z-transition:40000;
|
|
54
|
+
--z-skip-link:41000;
|
|
54
55
|
--z-loop-crash:45000;
|
|
55
56
|
--z-debug:90000;
|
|
56
57
|
}
|
|
@@ -178,7 +179,9 @@ html,body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--font)
|
|
|
178
179
|
(tolerância de 5px lógicos/lado) é cortado pelo .stage-wrap, e os menus rolam DENTRO dos cards. */
|
|
179
180
|
body{display:flex;flex-direction:column;height:100vh;height:100dvh;overflow:hidden}
|
|
180
181
|
.sr-only{position:absolute!important;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
|
|
181
|
-
|
|
182
|
+
/* O `z-index` ADOTOU O SLOT em 2026-09-07. Era `100`, um literal que estava CERTO e cuja tabela estava
|
|
183
|
+
errada: o `Z` punha o skip-link em CAPTIONS(26000), abaixo dos modais. Ver `SKIP_LINK` em core/layers. */
|
|
184
|
+
.skip-link{position:absolute;left:8px;top:-60px;background:var(--accent);color:var(--accent-ink);padding:.6em 1em;border-radius:0 0 10px 10px;font-weight:700;z-index:var(--z-skip-link);transition:top .15s}
|
|
182
185
|
.skip-link:focus{top:0}
|
|
183
186
|
:focus-visible{outline:4px solid var(--focus);outline-offset:2px;border-radius:6px}
|
|
184
187
|
|
|
@@ -300,10 +303,24 @@ body:not(.dbg) .topbar{display:none} /* título da PÁGINA só em ?debug=true (o
|
|
|
300
303
|
navegador, e sem esta linha as duas listas do cartão de pausa apareceriam empilhadas (ADR-0044, item 5). */
|
|
301
304
|
.pause-menu[hidden]{display:none}
|
|
302
305
|
/* 44 px NÃO é o mínimo da WCAG — o 2.5.8 pede 24×24, e o cartão já passava nisso com folga. 44 é a
|
|
303
|
-
recomendação da Apple (HIG)
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
306
|
+
recomendação da Apple (HIG). `min-height` e não `padding` porque a altura de antes (35,6 px) não estava
|
|
307
|
+
declarada em lugar nenhum: ela RESULTAVA da soma de padding, fonte e entrelinha. Um alvo que ninguém
|
|
308
|
+
escreveu é um alvo que ninguém defende.
|
|
309
|
+
|
|
310
|
+
⚠️ ESTA LINHA DIZIA TAMBÉM «e este projeto trata piso como piso», e essa metade morreu no ADR-0095: o
|
|
311
|
+
piso deixou de ser um número único e passou a ser a régua abaixo. Fica o resto, que continua a valer. */
|
|
312
|
+
/* ⚠️ O ALVO DO ITEM DE MENU SEGUE A RÉGUA DO ADR-0095, e não um literal.
|
|
313
|
+
Era `min-height:44px` cravado. O registro fixou que o alvo é função da ALTURA DA TELA — 24 px (WCAG
|
|
314
|
+
2.5.8, AA) abaixo de 540, 34 a partir de 540, 44 (2.5.5, AAA) a partir de 720 — porque um alvo grande
|
|
315
|
+
que exige ROLAGEM para ser alcançado pode custar mais dedo do que um menor que está à vista.
|
|
316
|
+
|
|
317
|
+
⚠️ E ELE USA `--alvo-min` E NÃO `--tap`, que são coisas diferentes: `--tap` é o tamanho PREFERIDO e
|
|
318
|
+
cresce com a escala do canvas (22 × k, nunca menos de 44); `--alvo-min` é o CHÃO por altura. Um botão
|
|
319
|
+
isolado pode ser o preferido; um item de LISTA, que tem de caber inteiro, usa o chão.
|
|
320
|
+
|
|
321
|
+
O `44px` do fallback é o comportamento de antes, para o caso de `layout()` não ter corrido — que era
|
|
322
|
+
exatamente o que acontecia no `quiz.html` até 07/09. */
|
|
323
|
+
.pm-btn{background:#1a2740;color:#eaf2f8;border:1px solid #3a4a6a;border-radius:10px;padding:.5rem .8rem;min-height:var(--alvo-min,44px);display:flex;align-items:center;justify-content:center;font:inherit;font-weight:700;cursor:pointer;white-space:nowrap}
|
|
307
324
|
.pm-btn:hover,.pm-btn:focus,.pm-btn.pm-sel{background:#2a3a5e;border-color:#ffd23f;outline:2px solid #ffd23f;outline-offset:1px}
|
|
308
325
|
/* Legenda sim/não: ícone do botão (conforme layout do controle) + a palavra. Sony/Nintendo invertem 0↔1. */
|
|
309
326
|
.pause-legend{display:flex;gap:1.1rem;justify-content:center;margin:.7rem 0 .1rem;font-size:.82em;color:#c3cfe0}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -80,9 +80,50 @@
|
|
|
80
80
|
@font-face{font-family:'Newsreader';font-style:normal;font-weight:400;font-display:swap;src:url('fonts/newsreader-400.woff2') format('woff2');}
|
|
81
81
|
@font-face{font-family:'Newsreader';font-style:normal;font-weight:700;font-display:swap;src:url('fonts/newsreader-700.woff2') format('woff2');}
|
|
82
82
|
/* Manuscritas (caligráficas inglesas · blackletter alemãs · alfabetização) */
|
|
83
|
-
@font-face{font-family:'Great Vibes';font-style:normal;font-weight:400;font-display:swap;src:url('fonts/greatvibes-400.woff2') format('woff2');}
|
|
84
83
|
@font-face{font-family:'Pinyon Script';font-style:normal;font-weight:400;font-display:swap;src:url('fonts/pinyon-400.woff2') format('woff2');}
|
|
85
|
-
@font-face{font-family:'UnifrakturCook';font-style:normal;font-weight:700;font-display:swap;src:url('fonts/ufcook-700.woff2') format('woff2');}
|
|
86
84
|
@font-face{font-family:'UnifrakturMaguntia';font-style:normal;font-weight:400;font-display:swap;src:url('fonts/ufmag-400.woff2') format('woff2');}
|
|
87
85
|
@font-face{font-family:'Comic Neue';font-style:normal;font-weight:400;font-display:swap;src:url('fonts/comicneue-400.woff2') format('woff2');}
|
|
88
86
|
@font-face{font-family:'Comic Neue';font-style:normal;font-weight:700;font-display:swap;src:url('fonts/comicneue-700.woff2') format('woff2');}
|
|
87
|
+
|
|
88
|
+
/* ===== ARCADE (papel `jogo`: HUD, título, rótulos — NUNCA a face da interface) =====
|
|
89
|
+
⚠️ SEM `unicode-range`, E É DELIBERADO. O Google Fonts serve esta família em CINCO ficheiros ordenados
|
|
90
|
+
por `unicode-range` **com o latino por ÚLTIMO**, e uma vendorização que pegue no primeiro `src:` traz o
|
|
91
|
+
subconjunto cirílico — um woff2 válido, corretamente declarado, corretamente carregado, e sem uma única
|
|
92
|
+
letra latina.
|
|
93
|
+
|
|
94
|
+
Isso aconteceu de verdade, no `SP-the-inclusionist-whackwhack`: o título saiu na fonte errada durante
|
|
95
|
+
QUATRO commits e todas as verificações disponíveis diziam que estava certo —
|
|
96
|
+
`document.fonts.check()` devolvia `true`, o `status` era `loaded`, o `getComputedStyle` mostrava a
|
|
97
|
+
família certa. As três comparam o NOME da família, uma string escrita duas vezes pela mesma mão; nenhuma
|
|
98
|
+
olha para dentro do ficheiro. E o CSS resolve por CODEPOINT, então uma face sem glifo para `U+0057` é
|
|
99
|
+
ignorada para aquele caractere em silêncio.
|
|
100
|
+
|
|
101
|
+
O ficheiro aqui é o subconjunto LATINO, copiado daquele repositório já verificado, e o gate que o mede
|
|
102
|
+
é `tests/fonte-arcade.browser.test.js` — pela LARGURA DE AVANÇO, que vem do glifo e portanto do ficheiro. */
|
|
103
|
+
@font-face{font-family:'Press Start 2P';font-style:normal;font-weight:400;font-display:swap;src:url('fonts/pressstart-400.woff2') format('woff2');}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
/* ===== OpenDyslexic (ACESSIBILIDADE — oferecida, e SEM alegação de eficácia) =====
|
|
107
|
+
SIL OFL 1.1, confirmada na API do repositório oficial (antijingoist/opendyslexic) e não de memória — a
|
|
108
|
+
licença desta face JÁ MUDOU uma vez, e afirmar de cabeça seria afirmar o estado antigo.
|
|
109
|
+
|
|
110
|
+
⚠️ ELA NÃO PROMETE LEITURA MELHOR, e isso é decisão registada. O `docs/game-design/typography.md` diz por
|
|
111
|
+
extenso: «Ofereça a Dyslexie e a OpenDyslexic apenas como escolha do usuário. A pesquisa não mostra ganho
|
|
112
|
+
de leitura com elas.» A descrição no menu fala do desenho — hastes pesadas na base —, nunca do efeito.
|
|
113
|
+
|
|
114
|
+
⚠️ E ELA NÃO É SUBCONJUNTO: 100,9 KB do ficheiro compilado do próprio projeto, contra 23 KB da Fondamento
|
|
115
|
+
latina. É a face mais pesada do roster, e entra assim porque o upstream não publica subconjuntos; quem
|
|
116
|
+
quiser cortá-la depois faz por `pyftsubset`, e o gate de precache é quem reclama se o orçamento apertar. */
|
|
117
|
+
@font-face{font-family:'OpenDyslexic';font-style:normal;font-weight:400;font-display:swap;
|
|
118
|
+
src:url('fonts/opendyslexic-400.woff2') format('woff2');}
|
|
119
|
+
|
|
120
|
+
/* ===== Fondamento (CALIGRÁFICA — fora do menu, para dentro das atividades) =====
|
|
121
|
+
SIL OFL 1.1, do Google Fonts, nos dois subconjuntos que ele serve. Mínimo de 20 px fixado pelo Dev na
|
|
122
|
+
emenda do ADR-0012: abaixo disso a face deixa de ser DIFÍCIL e passa a ser ilegível, e as duas coisas são
|
|
123
|
+
diferentes — a dificuldade é o exercício, a ilegibilidade é a criança a desistir. */
|
|
124
|
+
@font-face{font-family:'Fondamento';font-style:normal;font-weight:400;font-display:swap;
|
|
125
|
+
src:url('fonts/fondamento-400.woff2') format('woff2');
|
|
126
|
+
unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;}
|
|
127
|
+
@font-face{font-family:'Fondamento';font-style:normal;font-weight:400;font-display:swap;
|
|
128
|
+
src:url('fonts/fondamento-400-ext.woff2') format('woff2');
|
|
129
|
+
unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
2
|
import { type Alcance, type Disponibilidade } from '../input/transports.js';
|
|
3
3
|
import { type ActionPreset } from '../core/actions.js';
|
|
4
|
+
import type { KeyScheme } from '../core/entity.js';
|
|
4
5
|
import { type GameDeclaration } from '../core/contract.js';
|
|
5
6
|
import { type SceneStack } from '../core/scenes.js';
|
|
6
7
|
import { createTts, type CarregarVozNeural } from '../platform/tts.js';
|
|
@@ -16,6 +17,41 @@ export interface EngineHost {
|
|
|
16
17
|
readonly win: Window;
|
|
17
18
|
/** Um `<svg>` vazio onde os seis filtros de daltonismo são montados em tempo de execução. */
|
|
18
19
|
readonly cvdHost?: SVGElement | Element | null;
|
|
20
|
+
/**
|
|
21
|
+
* ONDE A BARRA DE ACESSIBILIDADE ENTRA, na PRIMEIRA tela do jogo.
|
|
22
|
+
*
|
|
23
|
+
* ⚠️ PEDIDO DO DEV, 2026-09-07: «o menu de pausa, o design do menu de pausa e os ícones de acessibilidade
|
|
24
|
+
* que aparecem no jogo desde a primeira tela devem ser oferecidos pela ENGINE e não pela programação do
|
|
25
|
+
* jogo. Todo jogo da engine inclusionist deve ter o mesmo menu de pausa e ícones de acessibilidade desde a
|
|
26
|
+
* primeira.»
|
|
27
|
+
*
|
|
28
|
+
* ⚠️ E A MEDIÇÃO DE 2026-09-08 MOSTROU QUE É UM ACHADO, e não uma arrumação: dos seis jogos do catálogo
|
|
29
|
+
* local, CINCO não têm barra de acessibilidade nenhuma — nem menu de pausa. `pixi-15-puzzle`, `game-chess`,
|
|
30
|
+
* `game-soccer`, `2048` e `whackwhack` não chamam `initPauseIcons` nem montam HUD, e o `createGame` nunca
|
|
31
|
+
* os montou por eles. O comentário do `ui/pause-icons` diz porquê sem o notar: «`initPauseIcons` é chamado
|
|
32
|
+
* pela raiz de composição de CADA jogo» — ou seja, cada jogo tinha de se lembrar, e cinco não se lembraram.
|
|
33
|
+
* A criança que depende do modo cego, do TTS ou do alto contraste abre esses cinco jogos e não tem por onde.
|
|
34
|
+
*
|
|
35
|
+
* Ausente, a engine procura `#title-icons` — o id que o jogo de plataforma usa desde sempre — e, não o
|
|
36
|
+
* achando, diz-o em `problems`.
|
|
37
|
+
*
|
|
38
|
+
* ✅ E DESDE 2026-09-08 ELA TAMBÉM **MONTA** (etapa 2 do ADR-0106 §4). Este parágrafo dizia «dizer não é
|
|
39
|
+
* montar, e a montagem é o passo seguinte»; o passo seguinte aconteceu. O que a destravou foram as etapas
|
|
40
|
+
* 1 e 3: nenhuma delas era sobre montar, e sem as duas o `PauseIconsCtx` exigia sete coisas que esta raiz
|
|
41
|
+
* não sabe responder por um jogo que não conhece.
|
|
42
|
+
*
|
|
43
|
+
* ⚠️ Achando o hospedeiro, a barra é escrita e fiada aqui. Não achando, continua a ser `problems` — porque
|
|
44
|
+
* a engine pode oferecer os ícones, mas não pode adivinhar ONDE eles cabem no desenho de um jogo alheio.
|
|
45
|
+
*/
|
|
46
|
+
readonly a11yBarHost?: Element | null;
|
|
47
|
+
/**
|
|
48
|
+
* ONDE O CARTÃO DE PAUSA da primeira tela é pendurado. Ausente, a engine usa `#game-region`.
|
|
49
|
+
*
|
|
50
|
+
* ⚠️ Existe pela mesma razão do `a11yBarHost`: a engine pode OFERECER a pausa, mas não sabe onde ela cabe
|
|
51
|
+
* no desenho de um jogo alheio. Um jogo que não tem pausa nenhuma declara `declines.semMenuDePausa` —
|
|
52
|
+
* declinar é escolha registada, não ter é omissão, e o ADR-0106 §2 é inteiro sobre a diferença.
|
|
53
|
+
*/
|
|
54
|
+
readonly pauseHost?: Element | null;
|
|
19
55
|
}
|
|
20
56
|
/**
|
|
21
57
|
* O que este jogo NÃO tem. Declarado, e não deduzido de um getter que devolve null.
|
|
@@ -30,6 +66,20 @@ export interface Declinios {
|
|
|
30
66
|
readonly semAssistenteDePad?: boolean;
|
|
31
67
|
/** Sem "ator da pausa" — quem apertou o botão que abriu o menu. */
|
|
32
68
|
readonly semAtorDePausa?: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Sem voz neural — este jogo não abre a porta do ADR-0094.
|
|
71
|
+
*
|
|
72
|
+
* ⚠️ EXISTE PORQUE A AUSÊNCIA ESTAVA A SER SILENCIOSA, e a medição de 2026-09-08 diz quanto: dos SEIS jogos
|
|
73
|
+
* do catálogo local, TRÊS declaram `carregarVozNeural` (platformer, 15-puzzle, 2048) e TRÊS não
|
|
74
|
+
* (`game-soccer`, `whackwhack`, `game-chess`). Nos três últimos não há voz neural nenhuma, e nada o dizia.
|
|
75
|
+
*
|
|
76
|
+
* ⚠️ E ISSO CONTRADIZ UMA PROMESSA ESCRITA. O ADR-0065 §3 diz que as vozes «fazem parte da engine, e não do
|
|
77
|
+
* jogo em si» e que um cartucho «não tem de saber que existe»; o ADR-0094 — com razão, e por 135 MB de WASM
|
|
78
|
+
* — passou a exigir UMA LINHA do jogo. As duas coisas podem ser verdade ao mesmo tempo (a engine é dona das
|
|
79
|
+
* VOZES, o jogo nomeia o FORNECEDOR), mas só se quem esquece a linha for avisado. Declinar é escolha; não
|
|
80
|
+
* declarar era omissão.
|
|
81
|
+
*/
|
|
82
|
+
readonly semVozNeural?: boolean;
|
|
33
83
|
}
|
|
34
84
|
export interface CreateGameOptions {
|
|
35
85
|
/** Os SETE CAMPOS (core/contract). É o que a pilha de acessibilidade lê, e a única coisa que ela lê. */
|
|
@@ -51,9 +101,12 @@ export interface CreateGameOptions {
|
|
|
51
101
|
readonly comIndice?: () => boolean;
|
|
52
102
|
readonly naBarraDe?: (i: number) => boolean;
|
|
53
103
|
readonly navBar?: (i: number, k: NavKeys) => void;
|
|
54
|
-
/** Jogadores para o teclado remapeável. `Pick<ControlledPlayer,'ctrl'>` — esquema de teclas e nada mais.
|
|
104
|
+
/** Jogadores para o teclado remapeável. `Pick<ControlledPlayer,'ctrl'>` — esquema de teclas e nada mais.
|
|
105
|
+
* ⚠️ `KeyScheme` e não `Record<string, string[]>` desde a #118: era uma CÓPIA ESTRUTURAL do tipo, e uma
|
|
106
|
+
* cópia que ninguém obriga a concordar diverge — é a lição que o próprio `core/entity` abre a dizer, com
|
|
107
|
+
* o `DomQuery` (dezasseis cópias) e o `KeyScheme` (seis) como as contas já pagas. */
|
|
55
108
|
readonly players?: {
|
|
56
|
-
ctrl:
|
|
109
|
+
ctrl: KeyScheme;
|
|
57
110
|
}[];
|
|
58
111
|
/** Troca de fase, para quem tem fases. Ausente = não faz nada (o jogo sem fases não perde nada). */
|
|
59
112
|
readonly setPhase?: (p: 'title' | 'playing' | 'paused') => void;
|
|
@@ -101,6 +154,27 @@ export interface CreateGameOptions {
|
|
|
101
154
|
}
|
|
102
155
|
export interface Engine {
|
|
103
156
|
readonly declaration: GameDeclaration;
|
|
157
|
+
/**
|
|
158
|
+
* A PAUSA QUE ESTA RAIZ MONTOU — mostrar e esconder, sem o consumidor caçar id nenhum.
|
|
159
|
+
*
|
|
160
|
+
* ⚠️ EXISTE PORQUE MONTAR NÃO É MOSTRAR, e a etapa 2 do ADR-0106 tinha ficado a meio sem que nada o
|
|
161
|
+
* dissesse. O cartão nasce `hidden` (é assim que o `buildScreenPause` o entrega, e tem de ser: a pausa
|
|
162
|
+
* abre-se, não está aberta) e QUEM O REVELA é o `ui/shell`, por fase — que esta raiz **não monta**, de
|
|
163
|
+
* propósito: «não substitui o boot do main.js, que tem catorze anos de ordem própria».
|
|
164
|
+
*
|
|
165
|
+
* ⚠️ Sem estas duas, um jogo montado por `createGame` ficava com um cartão de pausa que NADA mostrava. Não
|
|
166
|
+
* é «o consumidor esqueceu-se»: não havia por onde, a não ser procurar `#vp-pause-0` no documento — que é
|
|
167
|
+
* exactamente o tipo de conhecimento que este ficheiro existe para não exigir.
|
|
168
|
+
*
|
|
169
|
+
* 📌 A engine OFERECE o mecanismo e não toma a fase. Quando abrir a pausa continua a ser do jogo, porque só
|
|
170
|
+
* ele sabe o que é estar a jogar; o que deixa de ser dele é saber COMO.
|
|
171
|
+
*/
|
|
172
|
+
readonly pausa: {
|
|
173
|
+
/** Revela o cartão da tela `i` e refaz os itens — o §5 avaliado no instante em que ela abre. */
|
|
174
|
+
readonly mostrar: (i: number) => void;
|
|
175
|
+
/** Esconde-o outra vez. */
|
|
176
|
+
readonly esconder: (i: number) => void;
|
|
177
|
+
};
|
|
104
178
|
readonly tts: ReturnType<typeof createTts>;
|
|
105
179
|
readonly overlays: SettingsPanelApi;
|
|
106
180
|
readonly nav: MenuNavApi;
|
|
@@ -41,20 +41,27 @@
|
|
|
41
41
|
// exige seis coisas de plataforma. Não substitui o boot do `main.js`, que tem catorze anos de ordem própria.
|
|
42
42
|
// O que ele cobre é o que o quiz provou ser IDÊNTICO em qualquer jogo: idioma, leitor de tela, mixer, voz,
|
|
43
43
|
// pilha de diálogos, filtros de daltonismo, teclado remapeável e navegação de menu.
|
|
44
|
-
import { initI18n } from '../core/i18n.js';
|
|
44
|
+
import { initI18n, idiomaPronto } from '../core/i18n.js';
|
|
45
|
+
import { entradaDe } from '../input/state.js';
|
|
45
46
|
import { criarAvisoDeQueda } from '../ui/loop-crash.js';
|
|
46
47
|
import { initFocusTrap, focaveisNoDom } from '../ui/focus-trap.js';
|
|
47
48
|
import { mostrarAvisoDeAlcance } from '../ui/reach-notice.js';
|
|
48
49
|
import { alcance, transportesPadrao } from '../input/transports.js';
|
|
49
|
-
import { presetActions } from '../core/actions.js';
|
|
50
|
+
import { presetActions, ACTIONS } from '../core/actions.js';
|
|
50
51
|
import { t } from '../core/i18n.js';
|
|
51
52
|
import { srSay, srAlert } from '../core/a11y-sr.js';
|
|
53
|
+
import { initPauseIcons, iconsMarkup } from '../ui/pause-icons.js';
|
|
54
|
+
// O módulo INTEIRO: o on do barramento de eventos, para a barra montada continuar a dizer a verdade.
|
|
55
|
+
import * as state from '../core/state.js';
|
|
56
|
+
import { vlibrasOpen, toggleLibras } from '../ui/vlibras.js';
|
|
52
57
|
import { conformanceProblems } from '../core/contract.js';
|
|
53
58
|
import { criarPilha } from '../core/scenes.js';
|
|
54
59
|
import { createTts } from '../platform/tts.js';
|
|
55
|
-
import { ensureAC, catNode, audioOut, soundOn, volume, audioCat, initAudioMixer, tonePan, audioCtx } from '../platform/audio.js';
|
|
60
|
+
import { ensureAC, catNode, audioOut, soundOn, volume, audioCat, initAudioMixer, tonePan, audioCtx, setCatGain } from '../platform/audio.js';
|
|
56
61
|
import { createAudioSonar } from '../platform/audio-sonar.js';
|
|
57
|
-
|
|
62
|
+
// A raiz é a camada que PODE conhecer os dois eixos: `render/` está abaixo dela, e é dela a tarefa de
|
|
63
|
+
// responder ao `platform/audio-sonar`, que não pode importar daqui sem inverter uma aresta (#104).
|
|
64
|
+
import { ehCego, ehBaixaVisao, PADRAO } from '../render/viz-axes.js';
|
|
58
65
|
import { OVERLAY_SCOPE_SELECTOR } from '../ui/settings-panel.js';
|
|
59
66
|
import { LOGICAL_W } from '../core/constants.js';
|
|
60
67
|
import { initSettingsPanel } from '../ui/settings-panel.js';
|
|
@@ -64,6 +71,15 @@ import { kb, initKB } from '../input/keyboard.js';
|
|
|
64
71
|
import { installCvdFilters } from '../render/cvd-matrices.js';
|
|
65
72
|
/** Os ids que os painéis emprestados exigem do documento. Achado 6: sem eles o painel abre VAZIO, sem erro. */
|
|
66
73
|
const MARCACAO_EXIGIDA = ['#game-region', '#sr-status', '#sr-alert'];
|
|
74
|
+
/**
|
|
75
|
+
* Onde a engine procura a barra de acessibilidade quando o jogo não declara `host.a11yBarHost`.
|
|
76
|
+
*
|
|
77
|
+
* ⚠️ NÃO ENTROU NA `MARCACAO_EXIGIDA` de propósito, e a diferença é de mensagem e não de rigor. Aquela lista
|
|
78
|
+
* produz «marcação ausente: #x», que é o que se diz de um id que o jogo esqueceu. Aqui o que falta não é um
|
|
79
|
+
* id — é a barra inteira, e cinco jogos do catálogo não a têm porque ninguém lhes disse que a deviam ter. A
|
|
80
|
+
* frase própria pode explicar O QUE se perde, e é isso que a torna útil a quem a lê pela primeira vez.
|
|
81
|
+
*/
|
|
82
|
+
const SELETOR_BARRA_A11Y = '#title-icons';
|
|
67
83
|
/**
|
|
68
84
|
* Liga a engine para um jogo declarado.
|
|
69
85
|
*
|
|
@@ -108,6 +124,11 @@ export function createGame(o) {
|
|
|
108
124
|
getSoundOn: () => soundOn, getVolume: () => volume, getAudioCat: () => audioCat,
|
|
109
125
|
carregarVozNeural: o.carregarVozNeural,
|
|
110
126
|
});
|
|
127
|
+
if (!o.carregarVozNeural && !declines.semVozNeural) {
|
|
128
|
+
problems.push('sem voz neural: declare `carregarVozNeural` (uma linha — ver ADR-0094) ou `declines.semVozNeural`. '
|
|
129
|
+
+ 'Sem ela a criança que não lê fica com a voz do sistema, que em Chromebook de escola pode não existir '
|
|
130
|
+
+ 'em português');
|
|
131
|
+
}
|
|
111
132
|
/**
|
|
112
133
|
* O FILTRO DE VISÃO, aplicado ao MUNDO QUE O JOGO DECLAROU (ADR-0087).
|
|
113
134
|
*
|
|
@@ -147,25 +168,208 @@ export function createGame(o) {
|
|
|
147
168
|
const cvdFilters = installCvdFilters(o.host.cvdHost ?? null);
|
|
148
169
|
if (!cvdFilters)
|
|
149
170
|
problems.push('sem host de filtros (<svg>): a correção de daltonismo não foi montada');
|
|
171
|
+
// 4c. A BARRA DE ACESSIBILIDADE DA PRIMEIRA TELA. Ver a nota em `EngineHost.a11yBarHost`: cinco dos seis
|
|
172
|
+
// jogos do catálogo não têm nenhuma, e nada o dizia. Isto não a monta — diz que ela falta, que é o
|
|
173
|
+
// passo que tira o silêncio. A frase nomeia a saída, como as outras deste bloco fazem.
|
|
174
|
+
const a11yBar = o.host.a11yBarHost ?? $(SELETOR_BARRA_A11Y);
|
|
175
|
+
if (!a11yBar) {
|
|
176
|
+
problems.push(`sem barra de acessibilidade na primeira tela: declare \`host.a11yBarHost\` ou ponha um ${SELETOR_BARRA_A11Y} no documento. Sem ela a criança não alcança modo cego, TTS, alto contraste nem Libras antes de começar`);
|
|
177
|
+
}
|
|
178
|
+
/*
|
|
179
|
+
* ⚠️ E AGORA A ENGINE MONTA-A (ADR-0106 §4, etapa 2). Até 2026-09-08 esta raiz só REPORTAVA a ausência, e o
|
|
180
|
+
* registo dizia porquê: «reportar não é oferecer — cinco jogos continuam sem barra até alguém agir na
|
|
181
|
+
* linha». A etapa 1 tirou dos sete campos acidentais a obrigação de virem do jogo, e a 3 deu lista padrão
|
|
182
|
+
* ao menu; com isso o `PauseIconsCtx` deixou de exigir seja o que for que esta raiz não saiba responder.
|
|
183
|
+
*
|
|
184
|
+
* ⚠️ NÃO É `buildQuickBar`, e a diferença tem dono: aquele põe `tabIndex = -1` nos botões porque durante a
|
|
185
|
+
* partida dez paradas de tabulação separam a criança do jogo (ADR-0044 item 7). Na primeira tela não se
|
|
186
|
+
* está a jogar, e tirar os ícones da ordem de tabulação ali seria escondê-los de quem navega por teclado —
|
|
187
|
+
* exactamente a pessoa para quem eles existem.
|
|
188
|
+
*/
|
|
189
|
+
/**
|
|
190
|
+
* O LEITOR DO MODO CEGO — e ele tem de ler ONDE O ESCRITOR PADRÃO ESCREVE.
|
|
191
|
+
*
|
|
192
|
+
* 🔴 ESTAS DUAS METADES GANHARAM PADRÃO EM DIAS DIFERENTES E NÃO SE FALAVAM, o que produziu um defeito que
|
|
193
|
+
* nenhum teste podia ver. O escritor recebeu o padrão da engine na etapa 1b do ADR-0106
|
|
194
|
+
* (`ui/pause-icons` → `ctx.setModoCego ?? setModoCegoValue`), que grava no `core/state`. O leitor ficou com
|
|
195
|
+
* o `() => false` que já cá estava — uma CONSTANTE. Num jogo que não injecta `isBlindMode`:
|
|
196
|
+
*
|
|
197
|
+
* 1. a criança carrega no ícone → `setModoCego(!false)` → o modo LIGA de verdade;
|
|
198
|
+
* 2. o reflexo lê `false` → o ícone diz «desligado» e o anúncio diz o mesmo;
|
|
199
|
+
* 3. ela carrega outra vez → `setModoCegoValue(!false)` = `true` OUTRA VEZ → a guarda de igualdade do
|
|
200
|
+
* `core/state` devolve cedo → nada acontece.
|
|
201
|
+
*
|
|
202
|
+
* ⚠️ O modo cego ligava uma vez e NÃO HAVIA COMO DESLIGAR — um jogo que começa a descrever tudo em voz alta
|
|
203
|
+
* e não se cala, sem erro em lado nenhum. Para quem não depende dele, é o jogo a ficar inutilizável.
|
|
204
|
+
*
|
|
205
|
+
* 📌 UMA CONSTANTE E NÃO A EXPRESSÃO REPETIDA NOS DOIS SÍTIOS, porque a repetição É o defeito: duas
|
|
206
|
+
* respostas à mesma pergunta divergem, e foi assim que esta divergiu. `import * as state` dá ligação VIVA,
|
|
207
|
+
* então isto lê o valor de agora e não o do arranque.
|
|
208
|
+
*/
|
|
209
|
+
const lerModoCego = o.isBlindMode ?? (() => state.modoCego);
|
|
210
|
+
const pauseIcons = initPauseIcons({
|
|
211
|
+
doc,
|
|
212
|
+
getPlayers: () => o.players ?? [],
|
|
213
|
+
getNumPlayers: () => (o.players ?? [null]).length,
|
|
214
|
+
srSay, srAlert,
|
|
215
|
+
// ⚠️ NÃO `instanceof HTMLElement`: esse é um GLOBAL DO NAVEGADOR, e lê-lo onde ele não existe LANÇA —
|
|
216
|
+
// não devolve falso. Escrito assim na etapa 2, fazia o `reflectPauseIcons` rebentar em qualquer ambiente
|
|
217
|
+
// sem DOM. É o mesmo erro de forma do ACHADO 15 no cabeçalho deste ficheiro: alcançar o global por baixo
|
|
218
|
+
// de quem injectou o documento. A pergunta certa é a mesma que o `barraUsavel` faz — sabe ser uma barra?
|
|
219
|
+
getA11yBars: () => (barraUsavel && a11yBar ? [a11yBar] : []),
|
|
220
|
+
getModoCego: lerModoCego,
|
|
221
|
+
getAudioCat: () => audioCat,
|
|
222
|
+
setCatGain,
|
|
223
|
+
/*
|
|
224
|
+
* ⚠️ QUEM RESPONDE PELO APARELHO EM USO É A RAIZ, e é aqui que o autómato do ADR-0109 ganha o primeiro
|
|
225
|
+
* leitor. `input/state.entradaDe(i)` devolve `PADRAO` para quem nunca produziu uma aresta, logo isto
|
|
226
|
+
* nunca é `undefined` e o ícone nunca escreve numa chave torta.
|
|
227
|
+
*
|
|
228
|
+
* 📌 E é a RAIZ que o passa, não o ícone que o importa: `ui/` a ler estado de módulo de `input/` seria
|
|
229
|
+
* uma aresta nova entre camadas para poupar um argumento. A composição é o trabalho deste ficheiro.
|
|
230
|
+
*/
|
|
231
|
+
transporteEmUso: (i) => entradaDe(i).emUso,
|
|
232
|
+
reflectTtsPanel: () => { },
|
|
233
|
+
reflectTtsPanelEnabled: false,
|
|
234
|
+
isLibrasOn: vlibrasOpen,
|
|
235
|
+
toggleLibras,
|
|
236
|
+
});
|
|
237
|
+
/*
|
|
238
|
+
* ⚠️ O HOSPEDEIRO TEM DE SABER SER UMA BARRA, e perguntar isso não é zelo: `a11yBarHost` é `Element` no
|
|
239
|
+
* tipo, e um consumidor pode passar um duplo, um nó de outro documento, ou um elemento de um `<svg>`. Sem
|
|
240
|
+
* esta guarda, um objecto sem `addEventListener` derruba o BOOT INTEIRO — e derrubá-lo por causa da barra
|
|
241
|
+
* de acessibilidade seria tirar o jogo a toda a gente para não o dar a ninguém.
|
|
242
|
+
*
|
|
243
|
+
* Não sabendo, é `problems` como qualquer outra lacuna do hospedeiro: o consumidor lê e conserta.
|
|
244
|
+
*/
|
|
245
|
+
const barraUsavel = !!a11yBar
|
|
246
|
+
&& typeof a11yBar.addEventListener === 'function'
|
|
247
|
+
&& 'innerHTML' in a11yBar;
|
|
248
|
+
if (a11yBar && !barraUsavel) {
|
|
249
|
+
problems.push('o elemento da barra de acessibilidade não aceita conteúdo nem clique: os ícones não foram montados');
|
|
250
|
+
}
|
|
251
|
+
if (a11yBar && barraUsavel) {
|
|
252
|
+
a11yBar.innerHTML = iconsMarkup(pauseIcons.iconesMontados);
|
|
253
|
+
a11yBar.addEventListener('click', (e) => {
|
|
254
|
+
const botao = e.target?.closest('.pi-btn');
|
|
255
|
+
if (!botao)
|
|
256
|
+
return;
|
|
257
|
+
pauseIcons.iconAct(botao.dataset.pi ?? '', 0);
|
|
258
|
+
pauseIcons.reflectIconsIn(a11yBar, 0);
|
|
259
|
+
// O anúncio lê o `aria-label` DEPOIS do reflexo, porque é ele que carrega o estado NOVO — anunciar
|
|
260
|
+
// antes diria o estado que a criança acabou de deixar.
|
|
261
|
+
srSay(botao.getAttribute('aria-label') ?? '');
|
|
262
|
+
});
|
|
263
|
+
pauseIcons.reflectIconsIn(a11yBar, 0);
|
|
264
|
+
/*
|
|
265
|
+
* ⚠️ E OUTRA VEZ QUANDO O IDIOMA DO ARRANQUE CHEGAR — sem isto a barra fica no idioma de RECUO.
|
|
266
|
+
*
|
|
267
|
+
* O `initI18n` aplica pt de forma síncrona (para a página nunca ficar em branco) e, se o idioma
|
|
268
|
+
* preferido for outro, PEDE a troca — que é assíncrona, porque en/es são chunks sob demanda. Esta
|
|
269
|
+
* marcação nasce nesse intervalo. 📏 Medido num navegador em 2026-09-08, com `lang="en"`: a barra
|
|
270
|
+
* servia cinco rótulos em inglês e três ainda em português, na mesma linha de ícones.
|
|
271
|
+
*
|
|
272
|
+
* 📌 O `idiomaPronto()` existe exactamente para isto, e o cabeçalho dele já descreve o defeito noutro
|
|
273
|
+
* lugar: «o `applyDom` conserta o markup ESTÁTICO, mas o que o JavaScript monta tinha capturado o
|
|
274
|
+
* texto de pt e ninguém reconstruía». A barra é a instância nova, criada quando a ENGINE passou a
|
|
275
|
+
* montá-la (ADR-0106 etapa 2).
|
|
276
|
+
*
|
|
277
|
+
* ⚠️ É O `idiomaPronto()` E NÃO O EVENTO `i18n:change`, de propósito: o evento é a troca de idioma EM
|
|
278
|
+
* EXECUÇÃO, e o próprio `core/i18n` declara essa pergunta como sendo do Dev («QUANDO a interface se
|
|
279
|
+
* reconstrói ao trocar de idioma em execução»). Isto responde só a pergunta do ARRANQUE, que aquele
|
|
280
|
+
* mesmo comentário diz não ter duas respostas.
|
|
281
|
+
*/
|
|
282
|
+
void idiomaPronto().then(() => { pauseIcons.reflectIconsIn(a11yBar, 0); });
|
|
283
|
+
/*
|
|
284
|
+
* ⚠️ E ELA TEM DE CONTINUAR A DIZER A VERDADE quando o estado muda NOUTRO SÍTIO. O modo cego liga-se
|
|
285
|
+
* também pelo painel de áudio e pela simulação de empatia; sem esta assinatura, o ícone da barra ficaria
|
|
286
|
+
* a dizer «desligado» com `aria-pressed=false` depois de a criança o ter ligado — o controlo a mentir o
|
|
287
|
+
* estado, que é a família de defeito que o `reflectTTS` e o `#opt-modocego` já custaram a este projeto.
|
|
288
|
+
*
|
|
289
|
+
* 📌 SÓ O MODO CEGO, e a limitação é medida e não preguiça: dos ícones que esta raiz monta, ele é o ÚNICO
|
|
290
|
+
* cujo estado tem evento (`EventoDoJogo` tem `modoCego`; TTS, Libras, TEA e alternância não emitem nada).
|
|
291
|
+
* Os outros continuam a refletir-se ao clique, que é o caminho por onde hoje eles mudam.
|
|
292
|
+
*/
|
|
293
|
+
state.on('modoCego', () => { pauseIcons.reflectIconsIn(a11yBar, 0); });
|
|
294
|
+
}
|
|
295
|
+
// 4d. QUEM ABRIU A PAUSA, quando há mais de um assento — o achado 3 da auditoria do `game-soccer`.
|
|
296
|
+
//
|
|
297
|
+
// ⚠️ O PAINEL DE CONTROLE É PARAMETRIZADO PELO ASSENTO: `render(selPlayer)` desenha as posições DAQUELE
|
|
298
|
+
// esquema, e não há selector de assento — o `#ctrl-players` é uma FRASE, não abas. Quem decide o assento é
|
|
299
|
+
// o consumidor, passando o ator da pausa: «edita o controle de quem abriu o menu».
|
|
300
|
+
//
|
|
301
|
+
// ⚠️ E É AQUI QUE ISTO FICA MUDO. O `setPauseActor` desta raiz é `() => {}` — literal, logo abaixo. Um jogo
|
|
302
|
+
// montado por `createGame` com dois assentos deixa a criança do SEGUNDO sem como remapear, e nada o diz.
|
|
303
|
+
// Não é a mesma coisa que declarar `semAtorDePausa`: essa é uma ausência declarada, e uma ausência
|
|
304
|
+
// declarada é uma escolha. Esta era uma ausência por omissão, que é a forma de defeito do ADR-0106 §2.
|
|
305
|
+
const assentos = (o.players ?? []).length;
|
|
306
|
+
if (assentos > 1 && !declines.semAtorDePausa) {
|
|
307
|
+
problems.push(`declarou ${assentos} jogadores e não registra o ator da pausa: o painel de controle edita sempre o `
|
|
308
|
+
+ 'assento 0, então ninguém além do primeiro consegue remapear. Declare `declines.semAtorDePausa` se '
|
|
309
|
+
+ 'for de propósito');
|
|
310
|
+
}
|
|
311
|
+
/*
|
|
312
|
+
* 4e. O CARTÃO DE PAUSA DA PRIMEIRA TELA — e isto fecha um LAÇO QUE ESTAVA ABERTO.
|
|
313
|
+
*
|
|
314
|
+
* 📏 MEDIDO EM 2026-09-08, nos seis jogos do catálogo local: `#vp-pause-0` é procurado por esta raiz (o
|
|
315
|
+
* `getPauseMenu` do `initMenuNav`, mais abaixo) e **NENHUM jogo o cria**. `git grep vp-pause` devolve zero
|
|
316
|
+
* em `game-platformer`, `game-soccer`, `pixi-15-puzzle`, `2048`, `whackwhack` e `game-chess`. Ou seja: a
|
|
317
|
+
* engine inventou uma convenção, procurou-a, não a achou, e concluiu em silêncio que nenhum jogo tem menu
|
|
318
|
+
* de pausa — que é a MESMA forma de defeito do ADR-0106 §2, desta vez cometida pela engine contra si mesma.
|
|
319
|
+
*
|
|
320
|
+
* Agora ela cria o que procura. Quem declina (`semMenuDePausa`) continua sem nada e sem acusação — declinar
|
|
321
|
+
* é escolha; não ter é omissão.
|
|
322
|
+
*/
|
|
323
|
+
const hospedeiroDaPausa = declines.semMenuDePausa ? null : (o.host.pauseHost ?? $('#game-region'));
|
|
324
|
+
const pausaUsavel = !!hospedeiroDaPausa && typeof hospedeiroDaPausa.appendChild === 'function';
|
|
325
|
+
if (!declines.semMenuDePausa && !pausaUsavel) {
|
|
326
|
+
problems.push('sem sítio para o menu de pausa: declare `host.pauseHost` ou tenha um #game-region que aceite filhos. '
|
|
327
|
+
+ 'Sem ele a criança não alcança os ajustes durante a partida, e `declines.semMenuDePausa` é como se diz '
|
|
328
|
+
+ 'que isso é de propósito');
|
|
329
|
+
}
|
|
330
|
+
if (hospedeiroDaPausa && pausaUsavel) {
|
|
331
|
+
const cartao = pauseIcons.buildScreenPause(0);
|
|
332
|
+
// ⚠️ O ID É O QUE A PRÓPRIA ENGINE PROCURA, logo abaixo, no `getPauseMenu`. Montar sem o pôr deixaria o
|
|
333
|
+
// laço tão aberto como estava — o cartão existiria e a navegação de menu continuaria a não o achar.
|
|
334
|
+
cartao.id = 'vp-pause-0';
|
|
335
|
+
hospedeiroDaPausa.appendChild(cartao);
|
|
336
|
+
}
|
|
150
337
|
// 4b. NAVEGAÇÃO SONORA. Só o contrato entra: nada de tile, caixa de colisão ou array de moedas.
|
|
151
338
|
const sonar = createAudioSonar({
|
|
152
339
|
topology: () => o.declaration.topology(),
|
|
153
340
|
targetsOf: (i) => o.declaration.targetsOf(i),
|
|
154
341
|
nameAt: (at) => o.declaration.nameAt(at),
|
|
342
|
+
// Campo 2 + o barramento do mixer: o que o GUIA CONTÍNUO precisa e o bipe não precisava (#84 item 2). O
|
|
343
|
+
// `roleAt` é o que deixa a rota contornar parede; o `catNode`/`audioOut`/`getVolume` são o que põem um
|
|
344
|
+
// grafo PERMANENTE no mesmo cursor de volume que todo o resto do áudio usa.
|
|
345
|
+
roleAt: (at) => o.declaration.roleAt(at),
|
|
155
346
|
tonePan, srSay, narrate: (texto) => tts.narrate(texto),
|
|
156
|
-
|
|
347
|
+
catNode, audioOut, getVolume: () => volume,
|
|
348
|
+
// ⚠️ A RESPOSTA, E NÃO A TABELA (#104). O `platform/audio-sonar` recebia o `VIZ_BY_KEY` e atravessava-o
|
|
349
|
+
// com `pl.viz`; ele deixou de saber o que é um modo visual, e quem responde é aqui — a raiz é a única
|
|
350
|
+
// camada que conhece os dois eixos E pode importar de `render/`.
|
|
351
|
+
visaoComprometida: (pl) => {
|
|
352
|
+
const v = pl.visual;
|
|
353
|
+
return !!v && (ehCego(v) || ehBaixaVisao(v));
|
|
354
|
+
},
|
|
355
|
+
getModoCego: lerModoCego, LOGICAL_W,
|
|
157
356
|
// O jogador DERIVADO do foco: campo 4 respondendo "onde a criança está". Um jogo que não fornece lista
|
|
158
357
|
// ainda tem sonar, e é isso que faz a pilha de acessibilidade não ser acessório.
|
|
159
358
|
getPlayers: o.sonarPlayers ?? (() => {
|
|
160
359
|
const f = o.declaration.focusOf(0);
|
|
161
|
-
return f ? [{ i: 0, x: f.at.x, y: f.at.y,
|
|
360
|
+
return f ? [{ i: 0, x: f.at.x, y: f.at.y, visual: PADRAO }] : [];
|
|
162
361
|
}),
|
|
163
362
|
getNumPlayers: () => (o.players ?? [null]).length,
|
|
164
363
|
getAudioCtx: () => audioCtx, getSoundOn: () => soundOn, getAudioCat: () => audioCat,
|
|
165
364
|
});
|
|
166
365
|
// 5. Teclado remapeável — o melhor recorte da base (achado 11): esquema de teclas, sem mundo.
|
|
167
366
|
initKB();
|
|
168
|
-
|
|
367
|
+
// ⚠️ O ESQUEMA DE ARRANQUE ALCANÇA NADA, e diz isso com `null` em vez de com um objeto vazio (issue #118).
|
|
368
|
+
// Ele vive um instante — `assignControls()` logo abaixo substitui-o pelo esquema real —, mas enquanto vive
|
|
369
|
+
// é um `KeyScheme` como qualquer outro, e a única forma honesta de um esquema que não alcança nada é
|
|
370
|
+
// catorze ausências declaradas. Um `{}` fazia o tipo mentir sobre estar completo.
|
|
371
|
+
const semAlcance = Object.fromEntries(ACTIONS.map((a) => [a, null]));
|
|
372
|
+
const players = o.players ?? [{ ctrl: semAlcance }];
|
|
169
373
|
const keyboard = initKeyboardRuntime({
|
|
170
374
|
getKB: () => kb, getNumPlayers: () => players.length, getPlayers: () => players,
|
|
171
375
|
});
|
|
@@ -182,10 +386,25 @@ export function createGame(o) {
|
|
|
182
386
|
// que ele existe se vier desligado (a mesma razão de o modo cego nascer com TTS e sonar).
|
|
183
387
|
comIndice: o.comIndice ?? (() => true),
|
|
184
388
|
isNavigable: o.isNavigable ?? (() => true),
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
389
|
+
/*
|
|
390
|
+
* ⚠️ ESTE PADRÃO ERA `() => false` / `() => {}`, E DESDE HOJE ISSO SERIA UM BURACO QUE EU ABRI. O
|
|
391
|
+
* comentário que estava aqui dizia «um hospedeiro que não tenha barra de acessibilidade responde nunca e
|
|
392
|
+
* nunca chama nada» — verdade até a etapa 2 do ADR-0106, quando esta raiz passou a MONTAR a barra.
|
|
393
|
+
*
|
|
394
|
+
* Com a barra montada e estes dois em no-op, ela existiria e **não se conseguiria navegar por teclado nem
|
|
395
|
+
* por controle**: alcançável só por ponteiro. Para uma criança cega, que navega por teclado, uma barra
|
|
396
|
+
* que ela não alcança é o mesmo que barra nenhuma — e é exactamente o «oferece o caminho e depois
|
|
397
|
+
* recusa-o» que o §5 do ADR-0106 proíbe.
|
|
398
|
+
*
|
|
399
|
+
* A engine responde com a SUA instância, que é a mesma que montou a barra. Quem injecta continua a mandar.
|
|
400
|
+
*
|
|
401
|
+
* ⚠️ E FICA UMA METADE POR LIGAR, dita aqui em vez de descoberta: o `navBar` do `ui/menu-nav` recebe
|
|
402
|
+
* `(i, k)` e não o terceiro argumento `temStart`, que é a borda do botão de pausa — a SEGUNDA saída do
|
|
403
|
+
* modo (ADR-0044 item 7). No cartucho ela chega por outra rota (o encaminhador do gamepad, `main.ts:1470`)
|
|
404
|
+
* que esta raiz ainda não monta. Logo: o direcional navega a barra; sair por START, por enquanto, não.
|
|
405
|
+
*/
|
|
406
|
+
naBarraDe: o.naBarraDe ?? ((i) => pauseIcons.naBarraDe(i)),
|
|
407
|
+
navBar: o.navBar ?? ((i, k) => pauseIcons.navBar(i, k)),
|
|
189
408
|
isCapturing: () => false,
|
|
190
409
|
closePadWiz: () => { },
|
|
191
410
|
whichPlayer: (code) => keyboard.whichPlayer(code),
|
|
@@ -243,9 +462,30 @@ export function createGame(o) {
|
|
|
243
462
|
catch {
|
|
244
463
|
return true;
|
|
245
464
|
} },
|
|
465
|
+
/**
|
|
466
|
+
* O RATO (ADR-0112) — e a sonda é `any-pointer` de propósito, não `pointer`.
|
|
467
|
+
*
|
|
468
|
+
* ⚠️ `(pointer:fine)` descreve o ponteiro PRIMÁRIO, então um tablet com rato ligado responde «coarse» e
|
|
469
|
+
* o rato desaparecia — exactamente o aparelho que esta pergunta existe para achar. `any-pointer:fine` diz
|
|
470
|
+
* «ALGUM dos dispositivos apontadores é fino», que é a pergunta certa: para desenhar basta um.
|
|
471
|
+
*
|
|
472
|
+
* ⚠️ ELA ERRA, e a direcção do erro é a que se aceita: um stylus também responde `fine`, e é um ponteiro
|
|
473
|
+
* a sério — logo isso não é erro. O que pode faltar é um rato ligado depois do arranque, e por isso a
|
|
474
|
+
* sonda é uma FUNÇÃO, avaliada a cada pergunta, como as três acima.
|
|
475
|
+
*/
|
|
476
|
+
rato: () => { try {
|
|
477
|
+
return win.matchMedia('(any-pointer:fine)').matches;
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
return false;
|
|
481
|
+
} },
|
|
246
482
|
};
|
|
247
483
|
const acoesDoJogo = o.preset ? presetActions(o.preset) : [];
|
|
248
|
-
|
|
484
|
+
// O segundo eixo entra aqui, e vem do jogo (ADR-0104 §A): quantas posições ele segura ao mesmo tempo.
|
|
485
|
+
// ⚠️ O TERCEIRO EIXO ENTRA AQUI (ADR-0112), e vem do jogo tal como os outros dois. `?? false` e não um
|
|
486
|
+
// padrão inventado: o campo é opcional de propósito — ver a nota nele —, e a ausência significa «este jogo
|
|
487
|
+
// não desenha», que é a resposta certa para a esmagadora maioria dos trezentos.
|
|
488
|
+
const alcanceAqui = alcance(transportesPadrao(disponibilidade), acoesDoJogo, o.declaration.holdsAtOnce(), o.declaration.needsPointer?.() ?? false);
|
|
249
489
|
// ⚠️ SÓ APARECE QUANDO HÁ O QUE DIZER. Um aviso que aparece sempre deixa de ser lido, e um jogo cujas ações
|
|
250
490
|
// cabem no toque não tem nada a avisar — que é o caso comum e tem de continuar silencioso.
|
|
251
491
|
if (acoesDoJogo.length) {
|
|
@@ -264,5 +504,23 @@ export function createGame(o) {
|
|
|
264
504
|
criar: (tag) => doc.createElement(tag),
|
|
265
505
|
narrar: (texto) => tts.narrate(texto),
|
|
266
506
|
});
|
|
267
|
-
|
|
507
|
+
/*
|
|
508
|
+
* ⚠️ MOSTRAR REFAZ OS ITENS ANTES DE REVELAR, e a ordem é a regra: o §5 do ADR-0106 diz que a criança nunca
|
|
509
|
+
* vê um item que não acciona, e a tabela de acções deste jogo pode ter mudado desde a montagem. Revelar
|
|
510
|
+
* primeiro e refazer depois deixaria um piscar em que ela vê o que não pode usar.
|
|
511
|
+
*/
|
|
512
|
+
const pausa = {
|
|
513
|
+
mostrar: (i) => {
|
|
514
|
+
pauseIcons.reflectPauseIcons();
|
|
515
|
+
const cartao = $(`#vp-pause-${i}`);
|
|
516
|
+
if (cartao)
|
|
517
|
+
cartao.hidden = false;
|
|
518
|
+
},
|
|
519
|
+
esconder: (i) => {
|
|
520
|
+
const cartao = $(`#vp-pause-${i}`);
|
|
521
|
+
if (cartao)
|
|
522
|
+
cartao.hidden = true;
|
|
523
|
+
},
|
|
524
|
+
};
|
|
525
|
+
return { declaration: o.declaration, pausa, tts, overlays, nav, keyboard, sonar, aplicarFiltroDeVisao, cenas: criarPilha(), cvdFilters, problems, declines, aoFalhar, alcance: alcanceAqui };
|
|
268
526
|
}
|