@the-inclusionist/engine 8.0.0-rc.2 → 9.0.0
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.
|
@@ -6,6 +6,7 @@ import { type GameDeclaration } from '../core/contract.js';
|
|
|
6
6
|
import { type SceneStack } from '../core/scenes.js';
|
|
7
7
|
import { createTts, type CarregarVozNeural } from '../platform/tts.js';
|
|
8
8
|
import { type AudioSonar, type SonarPlayer } from '../platform/audio-sonar.js';
|
|
9
|
+
import { type Tema, type Correcao } from '../render/viz-axes.js';
|
|
9
10
|
import type { AlcanceDoFiltro } from '../render/port.js';
|
|
10
11
|
import { type SettingsPanelApi } from '../ui/settings-panel.js';
|
|
11
12
|
import { type MenuNavApi } from '../ui/menu-nav.js';
|
|
@@ -180,6 +181,49 @@ export interface CreateGameOptions {
|
|
|
180
181
|
* teste que não as possa responder não consegue exercitar a tela que depende delas.
|
|
181
182
|
*/
|
|
182
183
|
readonly disponibilidade?: Disponibilidade;
|
|
184
|
+
/**
|
|
185
|
+
* O QUE CADA ITEM DO CARTÃO DE PAUSA FAZ NESTE JOGO — «continuar», «sair», «ajuda», o que o jogo ligar.
|
|
186
|
+
*
|
|
187
|
+
* 🔴 ESTE CAMPO FALTAVA, E A FALTA ALCANÇAVA TODOS OS CONSUMIDORES DE UMA VEZ. O `initPauseIcons`
|
|
188
|
+
* aceita `getPauseActs` desde que existe; esta raiz não o passava e não tinha campo para ele, logo
|
|
189
|
+
* **nenhum jogo montado por `createGame`** conseguia ligar um item. O `refrescarItensDaPausa` esconde o
|
|
190
|
+
* que não acciona — o §5 do ADR-0106, que proíbe botão morto — e o resultado era um cartão com os TRÊS
|
|
191
|
+
* itens que a engine acciona sozinha (`ITENS_DA_ENGINE`) e nada mais, em todo o catálogo.
|
|
192
|
+
*
|
|
193
|
+
* ⚠️ E O CUSTO MAIOR NÃO ERA O CARTÃO, ERA A BARRA. O `entrarNaBarra` chama `acts.resume?.()` para sair
|
|
194
|
+
* do cartão antes de entregar as direcções à barra de acessibilidade; com a tabela vazia esse `resume` era
|
|
195
|
+
* `undefined`, o cartão ficava por cima do jogo, e o item 7 do ADR-0044 — o direccional a conduzir a barra
|
|
196
|
+
* — era **inalcançável a partir de qualquer jogo**.
|
|
197
|
+
*
|
|
198
|
+
* 📌 FUNÇÃO e não valor, pela razão que o próprio `ui/pause-icons` regista: a tabela de um jogo muda
|
|
199
|
+
* durante a partida (um «sair» que só liga depois da primeira fase), e congelá-la no arranque já partiu
|
|
200
|
+
* um caso lá dentro. Ausente = tabela vazia, que é o comportamento de sempre.
|
|
201
|
+
*/
|
|
202
|
+
readonly getPauseActs?: () => Record<string, (() => void) | undefined>;
|
|
203
|
+
/**
|
|
204
|
+
* QUEM ABRIU A PAUSA, quando há mais de um assento — o painel de controle edita o assento DESTE índice.
|
|
205
|
+
*
|
|
206
|
+
* ⚠️ A RAIZ JÁ SE DENUNCIAVA POR NÃO TER ISTO: o bloco 4d empurra uma linha de `problems` quando um jogo
|
|
207
|
+
* declara mais de um jogador, porque sem ator da pausa a criança do SEGUNDO assento não tem como remapear.
|
|
208
|
+
* O que faltava para a linha ser accionável era este campo — até agora ela dizia «conserte» sem haver por
|
|
209
|
+
* onde, e a única saída era declarar `semAtorDePausa`, que é aceitar a perda em vez de a corrigir.
|
|
210
|
+
*/
|
|
211
|
+
readonly setPauseActor?: (i: number, ...resto: unknown[]) => void;
|
|
212
|
+
/**
|
|
213
|
+
* COMO ESTE JOGO REPINTA PARA ALTO CONTRASTE, e como corrige daltonismo — os dois eixos do ADR-0104.
|
|
214
|
+
*
|
|
215
|
+
* 🔴 SEM ELES OS ÍCONES ⚫ E 🚥 NÃO SÃO MONTÁVEIS POR NENHUM JOGO. O `iconesQueAccionam` só os monta
|
|
216
|
+
* para quem entrega quem os escreve, e essa regra está certa — um ícone que não acciona é pior que um
|
|
217
|
+
* ícone a menos. O que estava errado era não haver PORTA: o consumidor externo que mediu isto leu a
|
|
218
|
+
* ausência como «este jogo tem os seus próprios controles», o que é verdade sobre o resultado e falso
|
|
219
|
+
* sobre a causa. Uma lacuna que o consumidor lê como escolha é a pior forma de lacuna.
|
|
220
|
+
*
|
|
221
|
+
* ⚠️ SÃO DOIS CAMPOS E NÃO UM, porque são duas perguntas: um jogo pode saber repintar texturas e não ter
|
|
222
|
+
* como corrigir cor, ou o contrário. O `game-pinball` é o segundo caso — a imagem dele é um framebuffer
|
|
223
|
+
* de 320x180 sem textura para repintar, e o filtro de cor ele aplica há semanas.
|
|
224
|
+
*/
|
|
225
|
+
readonly setTemaDoJogador?: (i: number, tema: Tema) => void;
|
|
226
|
+
readonly setCorrecaoDoJogador?: (i: number, correcao: Correcao) => void;
|
|
183
227
|
}
|
|
184
228
|
export interface Engine {
|
|
185
229
|
readonly declaration: GameDeclaration;
|
|
@@ -265,7 +309,28 @@ export interface Engine {
|
|
|
265
309
|
* tem de poder ser auditado em vez de ficar só numa tela que já fechou.
|
|
266
310
|
*/
|
|
267
311
|
readonly alcance: Alcance;
|
|
312
|
+
/**
|
|
313
|
+
* TORNA ESTE CARTUCHO O CORRENTE (ADR-0142). Uma raiz de composição, vários jogos.
|
|
314
|
+
*
|
|
315
|
+
* ⚠️ **LANÇA** numa declaração malformada, e não a põe em `problems`: o contrato é PRÉ-CONDIÇÃO e não
|
|
316
|
+
* diagnóstico, tal como no arranque. Um cartucho mau não chega a ser montado.
|
|
317
|
+
*
|
|
318
|
+
* 📌 O que ele refaz é só o que não se conserta lendo de novo: os dois registos de mapeamento, que são
|
|
319
|
+
* efeito global, e o alcance com o seu aviso, que escreve DOM. `problems` e `alcance` passam a descrever
|
|
320
|
+
* o cartucho montado porque são derivados, não porque `mount` os copie.
|
|
321
|
+
*/
|
|
322
|
+
mount(declaration: GameDeclaration, ganchos?: GanchosDoCartucho): void;
|
|
323
|
+
/**
|
|
324
|
+
* SOLTA O CORRENTE: mapeamentos a `null`, aviso de alcance retirado, pilha de cenas esvaziada.
|
|
325
|
+
*
|
|
326
|
+
* ⚠️ A pilha esvazia-se com `pop()` e não com um `clear()`, e a diferença é a decisão: 📏 medido nos seis
|
|
327
|
+
* jogos, os quatro `exit()` que existem são LIMPEZA DE DOM, logo dispará-los é o teardown que se quer.
|
|
328
|
+
* Um `clear()` que os saltasse seria o conserto errado.
|
|
329
|
+
*/
|
|
330
|
+
unmount(): void;
|
|
268
331
|
}
|
|
332
|
+
/** A metade do jogo SEM a declaração — o que `mount` recebe ao lado dela. */
|
|
333
|
+
export type GanchosDoCartucho = Omit<MetadeDoJogo, 'declaration'>;
|
|
269
334
|
/**
|
|
270
335
|
* Liga a engine para um jogo declarado.
|
|
271
336
|
*
|
|
@@ -274,4 +339,15 @@ export interface Engine {
|
|
|
274
339
|
* um id ausente é lacuna do HOSPEDEIRO, e o quiz provou que ligar só a parte que serve é legítimo — foi
|
|
275
340
|
* assim que ele recusou o pad e o sonar sem mentir. Por isso um vira exceção e o outro vira `problems`.
|
|
276
341
|
*/
|
|
342
|
+
/**
|
|
343
|
+
* A METADE DO JOGO de `CreateGameOptions` — os quinze campos que o ADR-0139 §1 diz que um cartucho
|
|
344
|
+
* FORNECE, separados dos cinco que descrevem a página e o aparelho.
|
|
345
|
+
*
|
|
346
|
+
* ⚠️ São quinze e não dez: a primeira versão daquele registo contou quinze campos num total de vinte e
|
|
347
|
+
* deixou `declines`, `getPauseActs`, `setPauseActor`, `setTemaDoJogador` e `setCorrecaoDoJogador` de fora.
|
|
348
|
+
* O teste que ele próprio dá — «uma PÁGINA conseguiria responder isto sem saber que jogo corre?» — põe os
|
|
349
|
+
* cinco deste lado, e a errata de 2026-09-11 corrigiu a lista.
|
|
350
|
+
*/
|
|
351
|
+
type MetadeDoJogo = Pick<CreateGameOptions, 'declaration' | 'isNavigable' | 'comIndice' | 'naBarraDe' | 'navBar' | 'players' | 'setPhase' | 'sonarPlayers' | 'isBlindMode' | 'preset' | 'declines' | 'getPauseActs' | 'setPauseActor' | 'setTemaDoJogador' | 'setCorrecaoDoJogador'>;
|
|
277
352
|
export declare function createGame(o: CreateGameOptions): Engine;
|
|
353
|
+
export {};
|
|
@@ -50,7 +50,7 @@ import { initI18n, idiomaPronto } from '../core/i18n.js';
|
|
|
50
50
|
import { entradaDe } from '../input/state.js';
|
|
51
51
|
import { criarAvisoDeQueda } from '../ui/loop-crash.js';
|
|
52
52
|
import { initFocusTrap, focaveisNoDom } from '../ui/focus-trap.js';
|
|
53
|
-
import { mostrarAvisoDeAlcance } from '../ui/reach-notice.js';
|
|
53
|
+
import { mostrarAvisoDeAlcance, REACH_NOTICE_ID } from '../ui/reach-notice.js';
|
|
54
54
|
import { alcance, transportesPadrao } from '../input/transports.js';
|
|
55
55
|
import { presetActions, ACTIONS } from '../core/actions.js';
|
|
56
56
|
import { t } from '../core/i18n.js';
|
|
@@ -76,6 +76,17 @@ import { kb, initKB, registrarMapeamentoDoTeclado } from '../input/keyboard.js';
|
|
|
76
76
|
import { registrarMapeamentoDoPad } from '../input/pad-defaults.js';
|
|
77
77
|
import { baixarPesados } from '../platform/pesados.js';
|
|
78
78
|
import { installCvdFilters } from '../render/cvd-matrices.js';
|
|
79
|
+
/*
|
|
80
|
+
* UMA FRASE SÓ PARA AS DUAS RECUSAS, e o gate do pilar 3 é que a pediu.
|
|
81
|
+
*
|
|
82
|
+
* ⚠️ O arranque e o `mount()` recusam uma declaração malformada pela MESMA razão e com a mesma frase, e
|
|
83
|
+
* escrevê-la duas vezes fez o teto de texto cru deste módulo subir de 15 para 16 — o crivo de i18n reprovou,
|
|
84
|
+
* e reprovou com razão. Duas cópias de uma frase são dois sítios para ela divergir, e é também a segunda
|
|
85
|
+
* tradução a fazer quando o pilar 3 chegar aqui.
|
|
86
|
+
*/
|
|
87
|
+
function recusarDeclaracao(quem, problemas) {
|
|
88
|
+
throw new Error(`${quem}: declaração malformada — ${problemas.join('; ')}`);
|
|
89
|
+
}
|
|
79
90
|
/** Os ids que os painéis emprestados exigem do documento. Achado 6: sem eles o painel abre VAZIO, sem erro. */
|
|
80
91
|
const MARCACAO_EXIGIDA = ['#game-region', '#sr-status', '#sr-alert'];
|
|
81
92
|
/**
|
|
@@ -87,27 +98,38 @@ const MARCACAO_EXIGIDA = ['#game-region', '#sr-status', '#sr-alert'];
|
|
|
87
98
|
* frase própria pode explicar O QUE se perde, e é isso que a torna útil a quem a lê pela primeira vez.
|
|
88
99
|
*/
|
|
89
100
|
const SELETOR_BARRA_A11Y = '#title-icons';
|
|
90
|
-
/**
|
|
91
|
-
* Liga a engine para um jogo declarado.
|
|
92
|
-
*
|
|
93
|
-
* ⚠️ LANÇA se a declaração for malformada, e NÃO lança se faltar marcação. A diferença não é gosto: uma
|
|
94
|
-
* declaração errada é defeito de PROGRAMA, e um jogo que roda meio declarado é pior do que um que não abre;
|
|
95
|
-
* um id ausente é lacuna do HOSPEDEIRO, e o quiz provou que ligar só a parte que serve é legítimo — foi
|
|
96
|
-
* assim que ele recusou o pad e o sonar sem mentir. Por isso um vira exceção e o outro vira `problems`.
|
|
97
|
-
*/
|
|
98
101
|
export function createGame(o) {
|
|
99
|
-
|
|
102
|
+
/*
|
|
103
|
+
* O CARTUCHO CORRENTE, e por enquanto ele É as opções que chegaram.
|
|
104
|
+
*
|
|
105
|
+
* ⚠️ Este passo não muda comportamento nenhum: `cartucho` começa como o próprio `o`, então toda leitura
|
|
106
|
+
* abaixo devolve exatamente o que devolvia. O que ele compra é o LUGAR onde `mount()` vai escrever
|
|
107
|
+
* (ADR-0142) — sem ele, as trinta e seis leituras da metade do jogo estão presas ao argumento, e uma raiz
|
|
108
|
+
* de composição a servir vários cartuchos ficaria com o primeiro deles para sempre.
|
|
109
|
+
*/
|
|
110
|
+
let cartucho = o;
|
|
111
|
+
const problemasDoContrato = conformanceProblems(cartucho.declaration);
|
|
100
112
|
if (problemasDoContrato.length) {
|
|
101
|
-
|
|
113
|
+
recusarDeclaracao('createGame', problemasDoContrato);
|
|
102
114
|
}
|
|
103
115
|
const { doc, win } = o.host;
|
|
104
|
-
|
|
105
|
-
|
|
116
|
+
/*
|
|
117
|
+
* ⚠️ LEITOR E NÃO INSTANTÂNEO — terceira vez que este ficheiro comete e conserta o mesmo padrão, depois do
|
|
118
|
+
* `seguraTeclas` e do `players`. `declines` é da metade do JOGO (ADR-0139, errata de 2026-09-11: é o
|
|
119
|
+
* cartucho que declara o que NÃO tem), então um `const` tirado no arranque devolve, depois de um `mount()`,
|
|
120
|
+
* os declínios do cartucho anterior — e um declínio lido errado esconde uma linha de `problems` ou
|
|
121
|
+
* inventa outra.
|
|
122
|
+
*
|
|
123
|
+
* 📌 O vazio é uma constante e não um literal por chamada: `declines()` é lido em sítios que comparam.
|
|
124
|
+
*/
|
|
125
|
+
const SEM_DECLINIOS = {};
|
|
126
|
+
const declines = () => cartucho.declines ?? SEM_DECLINIOS;
|
|
127
|
+
const problemasDoHospedeiro = [];
|
|
106
128
|
const $ = (sel) => doc.querySelector(sel);
|
|
107
129
|
const $$ = (sel) => [...doc.querySelectorAll(sel)];
|
|
108
130
|
for (const sel of MARCACAO_EXIGIDA) {
|
|
109
131
|
if (!$(sel))
|
|
110
|
-
|
|
132
|
+
problemasDoHospedeiro.push(`marcação ausente: ${sel}`);
|
|
111
133
|
}
|
|
112
134
|
// ⚠️ O MUNDO DECLARADO TEM DE EXISTIR NO DOCUMENTO, e esta é a falha que o ADR-0087 deixaria aberta se
|
|
113
135
|
// parasse na conformidade. `conformanceProblems` confere a FORMA — que há um seletor e que ele não está
|
|
@@ -117,9 +139,40 @@ export function createGame(o) {
|
|
|
117
139
|
// que o registro existe para eliminar: a simulação de empatia aplicada a NADA, e um adulto informado de
|
|
118
140
|
// que sentiu algo que não sentiu. É um problema do HOSPEDEIRO e não do programa, então entra em
|
|
119
141
|
// `problems` como as marcações — o jogo abre, e quem o integrou lê que o mundo dele não está lá.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
142
|
+
/*
|
|
143
|
+
* AS TRÊS LINHAS DE `problems` QUE DEPENDEM DO CARTUCHO, juntas e recalculáveis.
|
|
144
|
+
*
|
|
145
|
+
* 📏 Medido: das oito que esta raiz produz, CINCO são sobre a PÁGINA — marcação ausente, sem host de
|
|
146
|
+
* filtros, sem barra de acessibilidade, uma barra que não aceita conteúdo, sem sítio para a pausa — e
|
|
147
|
+
* essas não mexem quando se troca de jogo, porque não é o jogo que as causa. Só estas três mexem.
|
|
148
|
+
*
|
|
149
|
+
* ⚠️ E é por isso que `problems` não podia continuar a ser UM array construído no arranque: metade dele
|
|
150
|
+
* descreve o hospedeiro e vale para sempre, a outra metade descreve um cartucho e caduca no `mount()`.
|
|
151
|
+
* Recalcular tudo apagaria diagnósticos do hospedeiro que ninguém consertou; não recalcular nada deixaria
|
|
152
|
+
* o diagnóstico a falar do jogo errado.
|
|
153
|
+
*/
|
|
154
|
+
function problemasDoCartucho() {
|
|
155
|
+
const p = [];
|
|
156
|
+
// O mundo declarado tem de existir na página — e quem o declara é o jogo, não o hospedeiro.
|
|
157
|
+
const mundo = cartucho.declaration.world();
|
|
158
|
+
if (mundo.kind === 'element' && !$(mundo.selector)) {
|
|
159
|
+
p.push(`mundo declarado não encontrado: ${mundo.selector}`);
|
|
160
|
+
}
|
|
161
|
+
// ⚠️ MISTA, e fica deste lado por causa da segunda metade: a porta é do hospedeiro
|
|
162
|
+
// (`carregarVozNeural`), mas o declínio é do CARTUCHO — logo a linha pode aparecer ou calar-se ao
|
|
163
|
+
// trocar de jogo, com o mesmo hospedeiro.
|
|
164
|
+
if (!o.carregarVozNeural && !declines().semVozNeural) {
|
|
165
|
+
p.push('sem voz neural: declare `carregarVozNeural` (uma linha — ver ADR-0094) ou `declines.semVozNeural`. '
|
|
166
|
+
+ 'Sem ela a criança que não lê fica com a voz do sistema, que em Chromebook de escola pode não existir '
|
|
167
|
+
+ 'em português');
|
|
168
|
+
}
|
|
169
|
+
const assentos = (cartucho.players ?? []).length;
|
|
170
|
+
if (assentos > 1 && !declines().semAtorDePausa && !cartucho.setPauseActor) {
|
|
171
|
+
p.push(`declarou ${assentos} jogadores e não registra o ator da pausa: o painel de controle edita sempre o `
|
|
172
|
+
+ 'assento 0, então ninguém além do primeiro consegue remapear. Declare `declines.semAtorDePausa` se '
|
|
173
|
+
+ 'for de propósito');
|
|
174
|
+
}
|
|
175
|
+
return p;
|
|
123
176
|
}
|
|
124
177
|
// 1. IDIOMA ANTES DE TUDO. A interface não pode ser construída antes de a língua ser conhecida — foi o que
|
|
125
178
|
// o item 14 consertou movendo `initI18n()` para o topo do boot. O documento entra: ver o achado 15.
|
|
@@ -131,11 +184,7 @@ export function createGame(o) {
|
|
|
131
184
|
getSoundOn: () => soundOn, getVolume: () => volume, getAudioCat: () => audioCat,
|
|
132
185
|
carregarVozNeural: o.carregarVozNeural,
|
|
133
186
|
});
|
|
134
|
-
|
|
135
|
-
problems.push('sem voz neural: declare `carregarVozNeural` (uma linha — ver ADR-0094) ou `declines.semVozNeural`. '
|
|
136
|
-
+ 'Sem ela a criança que não lê fica com a voz do sistema, que em Chromebook de escola pode não existir '
|
|
137
|
-
+ 'em português');
|
|
138
|
-
}
|
|
187
|
+
// 📌 A linha da voz neural mudou-se para `problemasDoCartucho()`: o declínio que a cala é do jogo.
|
|
139
188
|
/**
|
|
140
189
|
* O FILTRO DE VISÃO, aplicado ao MUNDO QUE O JOGO DECLAROU (ADR-0087).
|
|
141
190
|
*
|
|
@@ -149,7 +198,7 @@ export function createGame(o) {
|
|
|
149
198
|
* filtro sobre ela seria a mentira que o ADR-0087 existe para impedir, só que ao contrário.
|
|
150
199
|
*/
|
|
151
200
|
function aplicarFiltroDeVisao(css, alcance) {
|
|
152
|
-
const mundo =
|
|
201
|
+
const mundo = cartucho.declaration.world();
|
|
153
202
|
if (mundo.kind !== 'element')
|
|
154
203
|
return;
|
|
155
204
|
const el = $(mundo.selector);
|
|
@@ -174,13 +223,13 @@ export function createGame(o) {
|
|
|
174
223
|
// 4. Daltonismo: a engine ENTREGA o markup em vez de exigir que o consumidor o adivinhe (achado 7).
|
|
175
224
|
const cvdFilters = installCvdFilters(o.host.cvdHost ?? null);
|
|
176
225
|
if (!cvdFilters)
|
|
177
|
-
|
|
226
|
+
problemasDoHospedeiro.push('sem host de filtros (<svg>): a correção de daltonismo não foi montada');
|
|
178
227
|
// 4c. A BARRA DE ACESSIBILIDADE DA PRIMEIRA TELA. Ver a nota em `EngineHost.a11yBarHost`: cinco dos seis
|
|
179
228
|
// jogos do catálogo não têm nenhuma, e nada o dizia. Isto não a monta — diz que ela falta, que é o
|
|
180
229
|
// passo que tira o silêncio. A frase nomeia a saída, como as outras deste bloco fazem.
|
|
181
230
|
const a11yBar = o.host.a11yBarHost ?? $(SELETOR_BARRA_A11Y);
|
|
182
231
|
if (!a11yBar) {
|
|
183
|
-
|
|
232
|
+
problemasDoHospedeiro.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`);
|
|
184
233
|
}
|
|
185
234
|
/*
|
|
186
235
|
* ⚠️ E AGORA A ENGINE MONTA-A (ADR-0106 §4, etapa 2). Até 2026-09-08 esta raiz só REPORTAVA a ausência, e o
|
|
@@ -213,7 +262,7 @@ export function createGame(o) {
|
|
|
213
262
|
* respostas à mesma pergunta divergem, e foi assim que esta divergiu. `import * as state` dá ligação VIVA,
|
|
214
263
|
* então isto lê o valor de agora e não o do arranque.
|
|
215
264
|
*/
|
|
216
|
-
const lerModoCego =
|
|
265
|
+
const lerModoCego = cartucho.isBlindMode ?? (() => state.modoCego);
|
|
217
266
|
const pauseIcons = initPauseIcons({
|
|
218
267
|
doc,
|
|
219
268
|
/*
|
|
@@ -227,9 +276,12 @@ export function createGame(o) {
|
|
|
227
276
|
* a pé e nada dentro de um veículo declara `true`, e o registo não disse isto porque a pergunta só
|
|
228
277
|
* aparece quando se monta a barra.
|
|
229
278
|
*/
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
279
|
+
// ⚠️ A REFERÊNCIA, e não o resultado. Chamar aqui congelava a resposta no arranque, e o
|
|
280
|
+
// `reflectPauseIcons` — que existe porque a tabela de acções muda (ADR-0106 §5) — refrescava a partir
|
|
281
|
+
// dela. Com vários cartuchos numa raiz de composição (ADR-0142) o ícone descrevia o primeiro deles.
|
|
282
|
+
seguraTeclas: () => cartucho.declaration.seguraTeclas(),
|
|
283
|
+
getPlayers: () => cartucho.players ?? [],
|
|
284
|
+
getNumPlayers: () => (cartucho.players ?? [null]).length,
|
|
233
285
|
srSay, srAlert,
|
|
234
286
|
// ⚠️ NÃO `instanceof HTMLElement`: esse é um GLOBAL DO NAVEGADOR, e lê-lo onde ele não existe LANÇA —
|
|
235
287
|
// não devolve falso. Escrito assim na etapa 2, fazia o `reflectPauseIcons` rebentar em qualquer ambiente
|
|
@@ -252,6 +304,14 @@ export function createGame(o) {
|
|
|
252
304
|
reflectTtsPanelEnabled: false,
|
|
253
305
|
isLibrasOn: vlibrasOpen,
|
|
254
306
|
toggleLibras,
|
|
307
|
+
/*
|
|
308
|
+
* ⚠️ O QUE O JOGO ENTREGA, E QUE ATÉ HOJE NÃO TINHA POR ONDE. Os três campos são opcionais dos dois
|
|
309
|
+
* lados: ausentes, tudo se comporta como antes — tabela de acções vazia e os dois ícones visuais
|
|
310
|
+
* não montados. Ver as notas em `CreateGameOptions` para o que a ausência custava.
|
|
311
|
+
*/
|
|
312
|
+
...(cartucho.getPauseActs ? { getPauseActs: cartucho.getPauseActs } : {}),
|
|
313
|
+
...(cartucho.setTemaDoJogador ? { setTemaDoJogador: cartucho.setTemaDoJogador } : {}),
|
|
314
|
+
...(cartucho.setCorrecaoDoJogador ? { setCorrecaoDoJogador: cartucho.setCorrecaoDoJogador } : {}),
|
|
255
315
|
});
|
|
256
316
|
/*
|
|
257
317
|
* ⚠️ O HOSPEDEIRO TEM DE SABER SER UMA BARRA, e perguntar isso não é zelo: `a11yBarHost` é `Element` no
|
|
@@ -265,7 +325,7 @@ export function createGame(o) {
|
|
|
265
325
|
&& typeof a11yBar.addEventListener === 'function'
|
|
266
326
|
&& 'innerHTML' in a11yBar;
|
|
267
327
|
if (a11yBar && !barraUsavel) {
|
|
268
|
-
|
|
328
|
+
problemasDoHospedeiro.push('o elemento da barra de acessibilidade não aceita conteúdo nem clique: os ícones não foram montados');
|
|
269
329
|
}
|
|
270
330
|
if (a11yBar && barraUsavel) {
|
|
271
331
|
a11yBar.innerHTML = iconsMarkup(pauseIcons.iconesMontados);
|
|
@@ -317,16 +377,11 @@ export function createGame(o) {
|
|
|
317
377
|
// esquema, e não há selector de assento — o `#ctrl-players` é uma FRASE, não abas. Quem decide o assento é
|
|
318
378
|
// o consumidor, passando o ator da pausa: «edita o controle de quem abriu o menu».
|
|
319
379
|
//
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
|
|
325
|
-
if (assentos > 1 && !declines.semAtorDePausa) {
|
|
326
|
-
problems.push(`declarou ${assentos} jogadores e não registra o ator da pausa: o painel de controle edita sempre o `
|
|
327
|
-
+ 'assento 0, então ninguém além do primeiro consegue remapear. Declare `declines.semAtorDePausa` se '
|
|
328
|
-
+ 'for de propósito');
|
|
329
|
-
}
|
|
380
|
+
// ✅ E ISTO DEIXOU DE SER MUDO. O `setPauseActor` desta raiz era `() => {}` LITERAL, sem campo por onde um
|
|
381
|
+
// jogo o entregar: a linha abaixo dizia «conserte» sem haver por onde, e a única saída era declarar
|
|
382
|
+
// `semAtorDePausa`, que é aceitar a perda em vez de a corrigir. Agora ela só acusa quem NÃO respondeu —
|
|
383
|
+
// que é o que uma linha de `problems` deve fazer, pelo §2 do ADR-0106.
|
|
384
|
+
// 📌 A linha do ator da pausa mudou-se para `problemasDoCartucho()`: quem declara assentos é o jogo.
|
|
330
385
|
/*
|
|
331
386
|
* 4e. O CARTÃO DE PAUSA DA PRIMEIRA TELA — e isto fecha um LAÇO QUE ESTAVA ABERTO.
|
|
332
387
|
*
|
|
@@ -343,7 +398,7 @@ export function createGame(o) {
|
|
|
343
398
|
const hospedeiroDaPausa = o.host.pauseHost ?? $('#game-region');
|
|
344
399
|
const pausaUsavel = !!hospedeiroDaPausa && typeof hospedeiroDaPausa.appendChild === 'function';
|
|
345
400
|
if (!pausaUsavel) {
|
|
346
|
-
|
|
401
|
+
problemasDoHospedeiro.push('sem sítio para o menu de pausa: declare `host.pauseHost` ou tenha um #game-region que aceite filhos. '
|
|
347
402
|
+ 'Sem ele a criança não alcança os ajustes durante a partida — e NÃO há como declinar: desde o '
|
|
348
403
|
+ 'ADR-0122 a pausa é da engine em todo jogo, e o que este jogo declara é só ONDE ela cabe');
|
|
349
404
|
}
|
|
@@ -356,13 +411,13 @@ export function createGame(o) {
|
|
|
356
411
|
}
|
|
357
412
|
// 4b. NAVEGAÇÃO SONORA. Só o contrato entra: nada de tile, caixa de colisão ou array de moedas.
|
|
358
413
|
const sonar = createAudioSonar({
|
|
359
|
-
topology: () =>
|
|
360
|
-
targetsOf: (i) =>
|
|
361
|
-
nameAt: (at) =>
|
|
414
|
+
topology: () => cartucho.declaration.topology(),
|
|
415
|
+
targetsOf: (i) => cartucho.declaration.targetsOf(i),
|
|
416
|
+
nameAt: (at) => cartucho.declaration.nameAt(at),
|
|
362
417
|
// Campo 2 + o barramento do mixer: o que o GUIA CONTÍNUO precisa e o bipe não precisava (#84 item 2). O
|
|
363
418
|
// `roleAt` é o que deixa a rota contornar parede; o `catNode`/`audioOut`/`getVolume` são o que põem um
|
|
364
419
|
// grafo PERMANENTE no mesmo cursor de volume que todo o resto do áudio usa.
|
|
365
|
-
roleAt: (at) =>
|
|
420
|
+
roleAt: (at) => cartucho.declaration.roleAt(at),
|
|
366
421
|
tonePan, srSay, narrate: (texto) => tts.narrate(texto),
|
|
367
422
|
catNode, audioOut, getVolume: () => volume,
|
|
368
423
|
// ⚠️ A RESPOSTA, E NÃO A TABELA (#104). O `platform/audio-sonar` recebia o `VIZ_BY_KEY` e atravessava-o
|
|
@@ -375,11 +430,11 @@ export function createGame(o) {
|
|
|
375
430
|
getModoCego: lerModoCego, LOGICAL_W,
|
|
376
431
|
// O jogador DERIVADO do foco: campo 4 respondendo "onde a criança está". Um jogo que não fornece lista
|
|
377
432
|
// ainda tem sonar, e é isso que faz a pilha de acessibilidade não ser acessório.
|
|
378
|
-
getPlayers:
|
|
379
|
-
const f =
|
|
433
|
+
getPlayers: cartucho.sonarPlayers ?? (() => {
|
|
434
|
+
const f = cartucho.declaration.focusOf(0);
|
|
380
435
|
return f ? [{ i: 0, x: f.at.x, y: f.at.y, visual: PADRAO }] : [];
|
|
381
436
|
}),
|
|
382
|
-
getNumPlayers: () => (
|
|
437
|
+
getNumPlayers: () => (cartucho.players ?? [null]).length,
|
|
383
438
|
getAudioCtx: () => audioCtx, getSoundOn: () => soundOn, getAudioCat: () => audioCat,
|
|
384
439
|
});
|
|
385
440
|
// 5. Teclado remapeável — o melhor recorte da base (achado 11): esquema de teclas, sem mundo.
|
|
@@ -389,24 +444,45 @@ export function createGame(o) {
|
|
|
389
444
|
// primeiro arranque com a fábrica da ENGINE e o segundo com a do jogo — a pior espécie de defeito, porque
|
|
390
445
|
// desaparece quando alguém vai ver.
|
|
391
446
|
// 📌 E o registo aceita `null`, que é o que um jogo sem opinião produz: fica a fábrica da engine.
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
447
|
+
/*
|
|
448
|
+
* OS DOIS REGISTOS NUMA FUNÇÃO, porque são EFEITO GLOBAL e não valor: quem os chama por último ganha.
|
|
449
|
+
*
|
|
450
|
+
* ⚠️ É o que os torna diferentes de tudo o mais nesta raiz. Repontar uma leitura para o `cartucho` chega
|
|
451
|
+
* para os campos que são lidos quando alguém pergunta; estes dois já foram escritos noutro sítio no
|
|
452
|
+
* momento do arranque, então trocar de cartucho sem os reescrever deixa o mapa do anterior a valer —
|
|
453
|
+
* calado, e exactamente no lugar onde uma criança que remapeou teclas iria notar primeiro.
|
|
454
|
+
*
|
|
455
|
+
* 📌 `null` é o valor honesto de «este jogo não tem opinião», e é também o que o `desmontar()` escreve.
|
|
456
|
+
*/
|
|
457
|
+
function registrarMapeamentosDoCartucho() {
|
|
458
|
+
registrarMapeamentoDoTeclado(cartucho.declaration.mapeamentoDoTeclado
|
|
459
|
+
? (jogadores, assento) => cartucho.declaration.mapeamentoDoTeclado(jogadores, assento)
|
|
460
|
+
: null);
|
|
461
|
+
// ⚠️ E O DO CONTROLE REGISTA-SE AQUI AINDA QUE ESTA RAIZ NÃO MONTE GAMEPAD NENHUM. Não é descuido: quem
|
|
462
|
+
// chama `initGamepad` é o cartucho, e é exactamente por isso que o registo não pode viver lá — seria mais
|
|
463
|
+
// um campo que um jogo pode esquecer, e esquecê-lo devolve o mapa da ENGINE a quem declarou outro, calado.
|
|
464
|
+
registrarMapeamentoDoPad(cartucho.declaration.mapeamentoDoPad
|
|
465
|
+
? (jogadores, assento) => cartucho.declaration.mapeamentoDoPad(jogadores, assento)
|
|
466
|
+
: null);
|
|
467
|
+
}
|
|
468
|
+
registrarMapeamentosDoCartucho();
|
|
401
469
|
initKB();
|
|
402
470
|
// ⚠️ O ESQUEMA DE ARRANQUE ALCANÇA NADA, e diz isso com `null` em vez de com um objeto vazio (issue #118).
|
|
403
471
|
// Ele vive um instante — `assignControls()` logo abaixo substitui-o pelo esquema real —, mas enquanto vive
|
|
404
472
|
// é um `KeyScheme` como qualquer outro, e a única forma honesta de um esquema que não alcança nada é
|
|
405
473
|
// catorze ausências declaradas. Um `{}` fazia o tipo mentir sobre estar completo.
|
|
406
474
|
const semAlcance = Object.fromEntries(ACTIONS.map((a) => [a, null]));
|
|
407
|
-
|
|
475
|
+
// ⚠️ O FALLBACK É UMA CONSTANTE e não um literal novo a cada chamada: `getPlayers` é lido pelo runtime de
|
|
476
|
+
// teclado a cada leitura de controlo, e devolver um array novo de cada vez faria qualquer comparação de
|
|
477
|
+
// identidade mentir — um defeito que só aparece em quem compara, e tarde.
|
|
478
|
+
const semJogadores = [{ ctrl: semAlcance }];
|
|
479
|
+
// ⚠️ LÊ `cartucho.players`, E NÃO UM INSTANTÂNEO. Os getters já existiam; o que eles fechavam é que era um `const`
|
|
480
|
+
// tirado no arranque. As linhas de `initPauseIcons` e do sonar, neste mesmo ficheiro, já liam a fonte viva —
|
|
481
|
+
// esta era a que faltava. Com vários cartuchos numa raiz de composição (ADR-0142), o teclado ficava com os
|
|
482
|
+
// jogadores do cartucho que arrancou primeiro.
|
|
483
|
+
const players = () => cartucho.players ?? semJogadores;
|
|
408
484
|
const keyboard = initKeyboardRuntime({
|
|
409
|
-
getKB: () => kb, getNumPlayers: () => players.length, getPlayers: () => players,
|
|
485
|
+
getKB: () => kb, getNumPlayers: () => players().length, getPlayers: () => players(),
|
|
410
486
|
});
|
|
411
487
|
keyboard.assignControls();
|
|
412
488
|
// 6. Navegação de menu. Os três declínios entram como AUSÊNCIA DECLARADA, não como getter que devolve null.
|
|
@@ -414,13 +490,13 @@ export function createGame(o) {
|
|
|
414
490
|
$, getActiveElement: () => doc.activeElement,
|
|
415
491
|
topVisibleOverlay: overlays.topVisibleOverlay, closeById: overlays.closeById,
|
|
416
492
|
getPauseMenu: (i) => $(`#vp-pause-${i}`),
|
|
417
|
-
setPhase:
|
|
418
|
-
setPauseActor: () => { },
|
|
493
|
+
setPhase: cartucho.setPhase ?? (() => { }),
|
|
494
|
+
setPauseActor: cartucho.setPauseActor ?? (() => { }),
|
|
419
495
|
srSay,
|
|
420
496
|
// Sem opinião declarada, o índice fica LIGADO: quem precisa dele para se orientar não tem como saber
|
|
421
497
|
// que ele existe se vier desligado (a mesma razão de o modo cego nascer com TTS e sonar).
|
|
422
|
-
comIndice:
|
|
423
|
-
isNavigable:
|
|
498
|
+
comIndice: cartucho.comIndice ?? (() => true),
|
|
499
|
+
isNavigable: cartucho.isNavigable ?? (() => true),
|
|
424
500
|
/*
|
|
425
501
|
* ⚠️ ESTE PADRÃO ERA `() => false` / `() => {}`, E DESDE HOJE ISSO SERIA UM BURACO QUE EU ABRI. O
|
|
426
502
|
* comentário que estava aqui dizia «um hospedeiro que não tenha barra de acessibilidade responde nunca e
|
|
@@ -438,8 +514,8 @@ export function createGame(o) {
|
|
|
438
514
|
* modo (ADR-0044 item 7). No cartucho ela chega por outra rota (o encaminhador do gamepad, `main.ts:1470`)
|
|
439
515
|
* que esta raiz ainda não monta. Logo: o direcional navega a barra; sair por START, por enquanto, não.
|
|
440
516
|
*/
|
|
441
|
-
naBarraDe:
|
|
442
|
-
navBar:
|
|
517
|
+
naBarraDe: cartucho.naBarraDe ?? ((i) => pauseIcons.naBarraDe(i)),
|
|
518
|
+
navBar: cartucho.navBar ?? ((i, k) => pauseIcons.navBar(i, k)),
|
|
443
519
|
isCapturing: () => false,
|
|
444
520
|
closePadWiz: () => { },
|
|
445
521
|
whichPlayer: (code) => keyboard.whichPlayer(code),
|
|
@@ -515,22 +591,50 @@ export function createGame(o) {
|
|
|
515
591
|
return false;
|
|
516
592
|
} },
|
|
517
593
|
};
|
|
518
|
-
const acoesDoJogo = o.preset ? presetActions(o.preset) : [];
|
|
519
594
|
// O segundo eixo entra aqui, e vem do jogo (ADR-0104 §A): quantas posições ele segura ao mesmo tempo.
|
|
520
595
|
// ⚠️ O TERCEIRO EIXO ENTRA AQUI (ADR-0112), e vem do jogo tal como os outros dois. `?? false` e não um
|
|
521
596
|
// padrão inventado: o campo é opcional de propósito — ver a nota nele —, e a ausência significa «este jogo
|
|
522
597
|
// não desenha», que é a resposta certa para a esmagadora maioria dos trezentos.
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
598
|
+
/*
|
|
599
|
+
* O ALCANCE E O SEU AVISO, numa função, porque os dois dependem do cartucho e o segundo CRIA DOM.
|
|
600
|
+
*
|
|
601
|
+
* ⚠️ O aviso é o único sítio desta raiz que escreve um elemento a partir de uma resposta do jogo, e por
|
|
602
|
+
* isso é o único que precisa de ser RETIRADO antes de ser reescrito: `mostrarAvisoDeAlcance` cria um `div`
|
|
603
|
+
* com `id` fixo, então chamá-lo duas vezes deixaria dois — e o segundo cartucho ficaria com o aviso do
|
|
604
|
+
* primeiro por baixo do seu.
|
|
605
|
+
*
|
|
606
|
+
* 📌 `retirarAvisoDeAlcance()` corre SEMPRE antes, e não só quando há o que mostrar: um cartucho que não
|
|
607
|
+
* tem nada a avisar tem de apagar o aviso do anterior, e é esse o caso que se esquece.
|
|
608
|
+
*/
|
|
609
|
+
function retirarAvisoDeAlcance() {
|
|
610
|
+
const aviso = $(`#${REACH_NOTICE_ID}`);
|
|
611
|
+
if (!aviso)
|
|
612
|
+
return;
|
|
613
|
+
// ⚠️ CAPACIDADE E NÃO TIPO, pela mesma razão que este ficheiro já escreve mais acima sobre o
|
|
614
|
+
// `instanceof HTMLElement`: o hospedeiro pode ser um documento falso, e os que estes testes usam têm
|
|
615
|
+
// `parentNode` mas não `remove`. Perguntar pelo método é o que funciona nos dois.
|
|
616
|
+
if (typeof aviso.remove === 'function')
|
|
617
|
+
aviso.remove();
|
|
618
|
+
else
|
|
619
|
+
aviso.parentNode?.removeChild(aviso);
|
|
620
|
+
}
|
|
621
|
+
function derivarAlcance() {
|
|
622
|
+
const acoes = cartucho.preset ? presetActions(cartucho.preset) : [];
|
|
623
|
+
const a = alcance(transportesPadrao(disponibilidade), acoes, cartucho.declaration.holdsAtOnce(), cartucho.declaration.needsPointer?.() ?? false);
|
|
624
|
+
retirarAvisoDeAlcance();
|
|
625
|
+
// ⚠️ SÓ APARECE QUANDO HÁ O QUE DIZER. Um aviso que aparece sempre deixa de ser lido, e um jogo cujas
|
|
626
|
+
// ações cabem no toque não tem nada a avisar — que é o caso comum e tem de continuar silencioso.
|
|
627
|
+
if (acoes.length) {
|
|
628
|
+
mostrarAvisoDeAlcance({
|
|
629
|
+
procurar: (sel) => $(sel),
|
|
630
|
+
criar: (tag) => doc.createElement(tag),
|
|
631
|
+
t,
|
|
632
|
+
srAlert,
|
|
633
|
+
}, a);
|
|
634
|
+
}
|
|
635
|
+
return a;
|
|
533
636
|
}
|
|
637
|
+
let alcanceAtual = derivarAlcance();
|
|
534
638
|
// O ANÚNCIO DE QUE O LAÇO PAROU (ADR-0054). Entregue e não instalado: quem chama `startLoop` é o JOGO, que
|
|
535
639
|
// é o dono do ticker. Um jogo que monte o laço sem passar isto continua a PARAR — parar não é opcional; o
|
|
536
640
|
// que ele perde é dizer que parou.
|
|
@@ -583,5 +687,57 @@ export function createGame(o) {
|
|
|
583
687
|
void baixarPesados({ aoProgredir: o.aoProgredirPesados })
|
|
584
688
|
.catch(() => { });
|
|
585
689
|
}
|
|
586
|
-
|
|
690
|
+
/*
|
|
691
|
+
* ⚠️ `declaration` E `declines` SÃO GETTERS; o resto não é, e a assimetria é a decisão.
|
|
692
|
+
*
|
|
693
|
+
* Os dois pertencem à metade do JOGO (ADR-0139 §1), logo têm de seguir o cartucho que estiver montado —
|
|
694
|
+
* um campo fixo aqui devolveria, depois de um `mount()`, a declaração do cartucho que arrancou primeiro.
|
|
695
|
+
* `pausa`, `tts`, `overlays`, `nav`, `keyboard` e o sonar são da PÁGINA e existem uma vez só, que é a
|
|
696
|
+
* decisão inteira do ADR-0117 §2 — e é por isso que eles ficam como estão.
|
|
697
|
+
*
|
|
698
|
+
* 📌 `problems` e `alcance` ainda são fixos, e ainda descrevem o arranque. É a dívida que o ADR-0142
|
|
699
|
+
* nomeia e que o `mount()` fecha.
|
|
700
|
+
*/
|
|
701
|
+
/*
|
|
702
|
+
* A PILHA DE CENAS É DA RAIZ, e não do retorno, porque o `desmontar()` tem de a alcançar. Nasce uma vez
|
|
703
|
+
* (ADR-0117 §2: a página tem uma) e é esvaziada entre cartuchos, nunca substituída.
|
|
704
|
+
*/
|
|
705
|
+
const cenasDaRaiz = criarPilha();
|
|
706
|
+
function montar(declaration, ganchos = {}) {
|
|
707
|
+
// ⚠️ LANÇA, NÃO DIAGNOSTICA — a mesma regra do arranque, e por isso a mesma frase. Uma declaração
|
|
708
|
+
// malformada é pré-condição: `problems` é para lacunas com que se consegue jogar, e isto não é uma.
|
|
709
|
+
const malformada = conformanceProblems(declaration);
|
|
710
|
+
if (malformada.length) {
|
|
711
|
+
recusarDeclaracao('mount', malformada);
|
|
712
|
+
}
|
|
713
|
+
cartucho = { ...ganchos, declaration };
|
|
714
|
+
registrarMapeamentosDoCartucho();
|
|
715
|
+
alcanceAtual = derivarAlcance();
|
|
716
|
+
}
|
|
717
|
+
function desmontar() {
|
|
718
|
+
registrarMapeamentoDoTeclado(null);
|
|
719
|
+
registrarMapeamentoDoPad(null);
|
|
720
|
+
retirarAvisoDeAlcance();
|
|
721
|
+
// ⚠️ `pop()` E NÃO UM `clear()`: cada `exit()` é a limpeza de DOM daquela cena, e saltá-la deixaria na
|
|
722
|
+
// página o que o cartucho anterior desenhou. O laço tem fim porque `pop()` devolve `null` na pilha vazia.
|
|
723
|
+
while (cenasDaRaiz.pop()) { /* o `exit()` de cada cena É o teardown dela */ }
|
|
724
|
+
}
|
|
725
|
+
return {
|
|
726
|
+
get declaration() { return cartucho.declaration; },
|
|
727
|
+
get declines() { return declines(); },
|
|
728
|
+
mount: montar,
|
|
729
|
+
unmount: desmontar,
|
|
730
|
+
pausa,
|
|
731
|
+
tts,
|
|
732
|
+
overlays,
|
|
733
|
+
nav,
|
|
734
|
+
keyboard,
|
|
735
|
+
sonar,
|
|
736
|
+
aplicarFiltroDeVisao,
|
|
737
|
+
cenas: cenasDaRaiz,
|
|
738
|
+
cvdFilters,
|
|
739
|
+
get problems() { return [...problemasDoHospedeiro, ...problemasDoCartucho()]; },
|
|
740
|
+
aoFalhar,
|
|
741
|
+
get alcance() { return alcanceAtual; },
|
|
742
|
+
};
|
|
587
743
|
}
|
|
@@ -195,8 +195,14 @@ export interface AccionaveisDoJogo {
|
|
|
195
195
|
* controle fica DESABILITADO com o motivo, porque o aparelho EXIGE a alternância e ela não se pode
|
|
196
196
|
* desligar. Aqui não há nada a travar, e um controle que explica por que não faz nada continua a ser um
|
|
197
197
|
* controle que não faz nada.
|
|
198
|
+
*
|
|
199
|
+
* ⚠️ É FUNÇÃO E NÃO VALOR, e a razão é a mesma do ADR-0084: uma resposta lida uma vez envelhece em
|
|
200
|
+
* silêncio. Aqui envelhecia no pior sítio — o `reflectPauseIcons` existe justamente porque «a tabela de
|
|
201
|
+
* acções deste jogo pode ter mudado desde a montagem» (ADR-0106 §5), e refrescava a partir de um booleano
|
|
202
|
+
* congelado no arranque. Com um `createGame` a servir vários cartuchos (ADR-0142), o ícone descrevia o
|
|
203
|
+
* jogo que arrancou primeiro. O contrato nunca esteve errado: `GameDeclaration.seguraTeclas` já é função.
|
|
198
204
|
*/
|
|
199
|
-
readonly seguraTeclas: boolean;
|
|
205
|
+
readonly seguraTeclas: () => boolean;
|
|
200
206
|
}
|
|
201
207
|
/**
|
|
202
208
|
* @deprecated O nome dizia «escritores VISUAIS» e a pergunta deixou de ser só visual quando o `seguraTeclas`
|
|
@@ -429,8 +435,11 @@ export interface PauseIconsCtx {
|
|
|
429
435
|
* 📌 Quem passa pelo `createGame` não escreve isto: a raiz lê a declaração, que já é obrigatória. O campo
|
|
430
436
|
* só é visível para um cartucho que chame `initPauseIcons` por fora — e é exactamente esse que não pode
|
|
431
437
|
* ficar em silêncio.
|
|
438
|
+
*
|
|
439
|
+
* ⚠️ FUNÇÃO, não valor — ver a nota no campo homónimo de `EscritoresVisuais`. Um cartucho que monte isto
|
|
440
|
+
* por fora passa `() => this.declaration.seguraTeclas()` e não o resultado dela.
|
|
432
441
|
*/
|
|
433
|
-
seguraTeclas: boolean;
|
|
442
|
+
seguraTeclas: () => boolean;
|
|
434
443
|
}
|
|
435
444
|
export interface PauseIconsApi {
|
|
436
445
|
/** Builds one `.screen-pause` (hidden), wired for click + hover/focus caption. Caller appends it. */
|
|
@@ -235,7 +235,7 @@ export function iconesQueAccionam(escritores) {
|
|
|
235
235
|
// Um terceiro ramo, e não uma regra nova.
|
|
236
236
|
return PAUSE_ICONS.filter((ic) => (ic.k === 'contrast' ? escritores.tema
|
|
237
237
|
: ic.k === 'cvd' ? escritores.correcao
|
|
238
|
-
: ic.k === 'altmove' ? escritores.seguraTeclas
|
|
238
|
+
: ic.k === 'altmove' ? escritores.seguraTeclas()
|
|
239
239
|
: true));
|
|
240
240
|
}
|
|
241
241
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@the-inclusionist/engine",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Accessibility-first (WCAG 2.2 + GAG) engine for educational 2D pixel-art games, built with PixiJS. Screen reader, sonar, high contrast by role, colour-vision filters, remappable input as intent, i18n and offline PWA — the parts every game inherits (ADR-0035, ADR-0036).",
|
|
6
6
|
"comment:exports": [
|