@the-inclusionist/engine 7.0.1 → 8.0.0-rc.2
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/playwrite-ar.woff2 +0 -0
- package/app/public/vendor/fonts/playwrite-br.woff2 +0 -0
- package/app/public/vendor/fonts/playwrite-ca.woff2 +0 -0
- package/app/public/vendor/fonts/playwrite-cl.woff2 +0 -0
- package/app/public/vendor/fonts/playwrite-co.woff2 +0 -0
- package/app/public/vendor/fonts/playwrite-mx.woff2 +0 -0
- package/app/public/vendor/fonts/playwrite-us-modern.woff2 +0 -0
- package/app/public/vendor/fonts/playwrite-us-trad.woff2 +0 -0
- package/app/public/vendor/fonts/pressstart-400.woff2 +0 -0
- package/app/public/vendor/fonts.css +64 -2
- package/dist-pkg/boot/create-game.d.ts +107 -4
- package/dist-pkg/boot/create-game.js +337 -18
- package/dist-pkg/core/constants.d.ts +0 -28
- package/dist-pkg/core/constants.js +34 -17
- package/dist-pkg/core/contract.d.ts +99 -0
- package/dist-pkg/core/contract.js +78 -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 +44 -0
- package/dist-pkg/i18n/es.js +44 -0
- package/dist-pkg/i18n/pt.js +60 -0
- package/dist-pkg/input/default-bindings.d.ts +40 -0
- package/dist-pkg/input/default-bindings.js +151 -9
- package/dist-pkg/input/gamepad.d.ts +33 -12
- package/dist-pkg/input/gamepad.js +103 -16
- 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 +42 -0
- package/dist-pkg/input/keyboard.js +87 -17
- package/dist-pkg/input/keydown.d.ts +40 -2
- package/dist-pkg/input/keydown.js +29 -6
- package/dist-pkg/input/latch-edge.d.ts +28 -0
- package/dist-pkg/input/latch-edge.js +45 -0
- 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/latch-sync.d.ts +47 -0
- package/dist-pkg/input/latch-sync.js +43 -0
- package/dist-pkg/input/origem-sintetica.d.ts +44 -0
- package/dist-pkg/input/origem-sintetica.js +60 -0
- package/dist-pkg/input/pad-defaults.d.ts +22 -0
- package/dist-pkg/input/pad-defaults.js +34 -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 +21 -2
- package/dist-pkg/input/touch-bindings.js +15 -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/pesados-catalogo.d.ts +14 -0
- package/dist-pkg/platform/pesados-catalogo.js +177 -0
- package/dist-pkg/platform/pesados.d.ts +31 -0
- package/dist-pkg/platform/pesados.js +75 -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 +109 -0
- package/dist-pkg/platform/voice-plan.js +156 -0
- package/dist-pkg/platform/vozes-prontas.d.ts +28 -0
- package/dist-pkg/platform/vozes-prontas.js +61 -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 +71 -1
- package/dist-pkg/ui/fonts.js +111 -8
- 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 +168 -20
- package/dist-pkg/ui/pause-icons.js +315 -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 +101 -3
- package/dist-pkg/ui/settings-motor.js +136 -9
- package/dist-pkg/ui/settings-typo.d.ts +49 -5
- package/dist-pkg/ui/settings-typo.js +74 -23
- 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 +22 -1
- package/docs/LICENSES.md +170 -132
- package/package.json +25 -4
- package/app/public/vendor/fonts/greatvibes-400.woff2 +0 -0
- package/app/public/vendor/fonts/ufcook-700.woff2 +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
import type { Action } from './actions.js';
|
|
2
3
|
/** Gênero gramatical do nome. `n` = neutro/indefinido (o pt-BR usa o masculino como default nesse caso). */
|
|
3
4
|
export type Gender = 'm' | 'f' | 'n';
|
|
4
5
|
/** Um nome que pode ser FALADO (leitor de tela) e SINALIZADO (Libras, que traduz o mesmo texto). */
|
|
@@ -174,6 +175,104 @@ export interface GameDeclaration {
|
|
|
174
175
|
* o esquecimento passar como se fosse escolha.
|
|
175
176
|
*/
|
|
176
177
|
world(): WorldScope;
|
|
178
|
+
/**
|
|
179
|
+
* QUANTAS POSIÇÕES ESTE JOGO PRECISA DE SEGURAR AO MESMO TEMPO. Correr + andar + pular são TRÊS; um quiz é
|
|
180
|
+
* UM. (ADR-0104 §A.)
|
|
181
|
+
*
|
|
182
|
+
* ⚠️ É UM EIXO DIFERENTE DE «QUANTAS AÇÕES», e é por não serem o mesmo que existia um ponto cego onde o
|
|
183
|
+
* aviso nunca disparava. Medido: a plataforma declara NOVE ações e o controle de tela tem NOVE lugares,
|
|
184
|
+
* então o `alcance().ok` era verdadeiro e o cartão da #112 nunca aparecia — mas correr, andar e pular ao
|
|
185
|
+
* mesmo tempo são três dedos, e num telemóvel de dois a criança simplesmente não consegue, sem nada em
|
|
186
|
+
* lado nenhum a dizer porquê. Alcançar uma ação e segurá-la junto com outra são perguntas distintas.
|
|
187
|
+
*
|
|
188
|
+
* ⚠️ OBRIGATÓRIO, e a obrigatoriedade É a decisão, na frase do Dev: «os 300 jogos precisam declarar sim!
|
|
189
|
+
* Não declarar é ter a acessibilidade programada no controle pro sorte». Um campo opcional é respondido
|
|
190
|
+
* por SILÊNCIO, e aqui o silêncio decide pela criança — decide-o quem não pensou no assunto. Trezentos
|
|
191
|
+
* jogos a responder deliberadamente é o custo; acessibilidade por sorte é a alternativa.
|
|
192
|
+
*
|
|
193
|
+
* FUNÇÃO e não valor, pela mesma razão que a `topology`: um jogo com fases troca de exigência entre elas —
|
|
194
|
+
* uma fase a pé pede três, a mesma fase num veículo pode pedir uma. Um valor memorizado ficaria defasado
|
|
195
|
+
* em silêncio, que é o defeito que o ADR-0084 nomeou.
|
|
196
|
+
*/
|
|
197
|
+
holdsAtOnce(): number;
|
|
198
|
+
/**
|
|
199
|
+
* ESTE JOGO SEGURA ALGUMA TECLA? — e a resposta não é derivável de mais nada. (ADR-0115.)
|
|
200
|
+
*
|
|
201
|
+
* 🔴 A ALTERNÂNCIA EXISTE PARA UMA CRIANÇA CONCRETA: quem não consegue MANTER uma tecla premida carrega uma
|
|
202
|
+
* vez para andar e outra para parar. Num jogo onde nada se segura — um quiz, um tabuleiro, um puzzle de
|
|
203
|
+
* peças — não há nada a travar, e o controle passa a ser uma opção que **não faz nada**. A criança abre o
|
|
204
|
+
* menu de acessibilidade, liga o ajuste de que depende, e não acontece nada: ela aprende que o ajuste está
|
|
205
|
+
* partido. É o botão morto que o ADR-0106 §5 proíbe.
|
|
206
|
+
*
|
|
207
|
+
* ⚠️ E O `holdsAtOnce` ACIMA NÃO RESPONDE ISTO, o que foi o achado que obrigou a este campo: ele conta
|
|
208
|
+
* POSIÇÕES SIMULTÂNEAS e recusa zero, porque zero faria a aritmética do alcance passar por vacuidade. O
|
|
209
|
+
* `consumer-quiz` declara **1 sem segurar coisa nenhuma**. «Um de cada vez» e «um SEGURADO» são o mesmo
|
|
210
|
+
* número, e toda decisão a jusante vinha a ler um número que responde a outra pergunta.
|
|
211
|
+
*
|
|
212
|
+
* ⚠️ OBRIGATÓRIO, e a obrigatoriedade É a decisão — a mesma do `holdsAtOnce`, pela mesma frase do Dev:
|
|
213
|
+
* «não declarar é ter a acessibilidade programada no controle pro sorte». Um campo opcional faria um jogo
|
|
214
|
+
* que ESQUECE a linha perder a alternância em silêncio, e quem paga é a criança com dificuldade motora.
|
|
215
|
+
*
|
|
216
|
+
* 📌 FUNÇÃO e não valor, pelo ADR-0084: um jogo muda de exigência entre fases. A pé segura-se uma direcção;
|
|
217
|
+
* o mesmo jogo dentro de um veículo pode não segurar nada.
|
|
218
|
+
*/
|
|
219
|
+
seguraTeclas(): boolean;
|
|
220
|
+
/**
|
|
221
|
+
* ESTE JOGO PRECISA DE UM PONTEIRO — posição contínua? (ADR-0112.)
|
|
222
|
+
*
|
|
223
|
+
* Um jogo de desenho precisa; um quiz não. Declarar faz um aparelho sem ponteiro RECUSAR-SE antes de a
|
|
224
|
+
* criança começar, em vez de ela descobrir a meio do primeiro traço.
|
|
225
|
+
*
|
|
226
|
+
* ⚠️ OPCIONAL, E A DIFERENÇA PARA O `holdsAtOnce` LOGO ACIMA É DELIBERADA — copiar a obrigatoriedade dele
|
|
227
|
+
* seria aplicar uma regra cuja premissa não se sustenta aqui. O `holdsAtOnce` é obrigatório porque não tem
|
|
228
|
+
* padrão seguro E porque falha INVISIVELMENTE a quem escreve o jogo: ele tem teclado completo, o jogo corre,
|
|
229
|
+
* e quem descobre o defeito é a criança no telemóvel de dois dedos. Este tem padrão seguro (`false`) e falha
|
|
230
|
+
* VISIVELMENTE — um jogo de desenho que se esqueça de o declarar é inoperável no próprio aparelho de quem o
|
|
231
|
+
* escreve, porque ele também precisaria do ponteiro para o experimentar.
|
|
232
|
+
*
|
|
233
|
+
* Obrigar trezentos jogos a escrever `needsPointer: () => false` cobraria o preço do `holdsAtOnce` sem o
|
|
234
|
+
* motivo dele.
|
|
235
|
+
*
|
|
236
|
+
* FUNÇÃO e não valor, pela mesma razão das outras: uma actividade pode desenhar numa fase e não noutra.
|
|
237
|
+
*/
|
|
238
|
+
needsPointer?(): boolean;
|
|
239
|
+
/**
|
|
240
|
+
* O MAPEAMENTO DE TECLADO QUE ESTE JOGO QUER — por número de jogadores e por assento (ADR-0115).
|
|
241
|
+
*
|
|
242
|
+
* A precedência é a que o registo pede, e ela cabe entre duas linhas que já existiam no `input/keyboard`:
|
|
243
|
+
* **fábrica da engine → padrão do JOGO → remapeamento da CRIANÇA.** Devolver `null` (ou não declarar) deixa
|
|
244
|
+
* a fábrica da engine intacta, que é o comportamento de sempre.
|
|
245
|
+
*
|
|
246
|
+
* PARCIAL de propósito: um jogo que só queira trocar o `action1` troca o `action1`. A fusão já existe — é o
|
|
247
|
+
* `Object.assign` que sobrepõe o dado guardado —, então esta é mais uma camada no mesmo sítio e não uma
|
|
248
|
+
* segunda forma de fundir.
|
|
249
|
+
*
|
|
250
|
+
* ⚠️ OPCIONAL, e aqui, ao contrário do `seguraTeclas`, o silêncio tem um lado seguro: sem declaração o jogo
|
|
251
|
+
* fica com a fábrica da engine, que é jogável e é o que ele já tem hoje. Não há lado errado na ausência.
|
|
252
|
+
*
|
|
253
|
+
* ⚠️ E LEVA O ASSENTO porque o teclado de dois jogadores não é o de um: as setas mudam de dono, e um padrão
|
|
254
|
+
* que não soubesse o assento daria as mesmas teclas a duas crianças. `jogadores` é 1, 2, 3 ou 4; `assento` é
|
|
255
|
+
* o índice dentro desse arranjo.
|
|
256
|
+
*
|
|
257
|
+
* 📌 O irmão do CONTROLE é o campo logo abaixo, e chegou um commit depois: o obstáculo era que o assento
|
|
258
|
+
* ainda não se conhecia no ponto em que a tabela de botões é lida, e a saída foi subi-lo no laço.
|
|
259
|
+
*/
|
|
260
|
+
mapeamentoDoTeclado?(jogadores: number, assento: number): Partial<Record<Action, readonly string[] | null>> | null;
|
|
261
|
+
/**
|
|
262
|
+
* O MAPEAMENTO DE BOTÕES QUE ESTE JOGO QUER NO CONTROLE — mesma pergunta, outro aparelho (ADR-0115).
|
|
263
|
+
*
|
|
264
|
+
* Índices de botão da Gamepad API «standard», parciais: `{ action1: 3 }` troca só essa. `null` num botão diz
|
|
265
|
+
* «esta posição não existe neste jogo», que é diferente de a deixar na fábrica.
|
|
266
|
+
*
|
|
267
|
+
* ⚠️ A PRECEDÊNCIA TEM UMA DIFERENÇA DE SÍTIO QUE VALE SABER: no teclado, o que a criança remapeou é uma
|
|
268
|
+
* camada POR CIMA desta; no controle, o mapa que ela gravou no assistente é um RAMO inteiro — se ele existe,
|
|
269
|
+
* este padrão não é consultado. Nos dois casos ela ganha, que é o que importa.
|
|
270
|
+
*
|
|
271
|
+
* 📌 E leva o assento pela mesma razão do teclado, ainda que por um caminho diferente: dois controles são
|
|
272
|
+
* dois aparelhos, mas o JOGO pode querer arranjos distintos por assento (o guarda-redes e o atacante não
|
|
273
|
+
* fazem o mesmo).
|
|
274
|
+
*/
|
|
275
|
+
mapeamentoDoPad?(jogadores: number, assento: number): Partial<Record<Action, number | null>> | null;
|
|
177
276
|
readonly tick: TickOwner;
|
|
178
277
|
/** O papel do que está em `at`. É o campo 2, e é o que substitui `roleOf`. */
|
|
179
278
|
roleAt(at: Spot): Role;
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
2
|
// core/contract — OS SETE CAMPOS. A interface que o ADR-0030 escolheu como o eixo da engine.
|
|
3
3
|
//
|
|
4
|
+
// ⚠️ ESTE FICHEIRO NÃO TINHA IMPORT NENHUM, e o único que ganhou é `import type` — apagado na compilação, logo
|
|
5
|
+
// o módulo publicado continua sem dependência de execução. O que entra é o VOCABULÁRIO das catorze posições
|
|
6
|
+
// (ADR-0074), e ele tinha de entrar: um campo que fala de mapeamento e escrevesse as suas próprias chaves
|
|
7
|
+
// seria a segunda cópia da união que o `core/actions` existe para ser a primeira.
|
|
8
|
+
//
|
|
4
9
|
// ========================= O QUE ISTO É, E O QUE NÃO É =========================
|
|
5
10
|
// Não é um framework nem uma classe-base. É a ÚNICA coisa que a pilha de acessibilidade sabe sobre um jogo.
|
|
6
11
|
// O ADR-0027 mediu que as três funções que MAIS pareciam genéricas eram as três mais amarradas à plataforma —
|
|
@@ -108,6 +113,79 @@ export function conformanceProblems(d) {
|
|
|
108
113
|
else if (w.kind !== 'none')
|
|
109
114
|
p.push('world: unknown kind');
|
|
110
115
|
}
|
|
116
|
+
// ⚠️ A MENSAGEM NOMEIA A SAÍDA, como as outras quatro fazem. Um jogo que não declara isto não recebe um
|
|
117
|
+
// padrão — recebe uma frase que diz o que perguntar a si próprio, porque a resposta é do jogo e de mais
|
|
118
|
+
// ninguém. (ADR-0104 §A.)
|
|
119
|
+
if (typeof d.holdsAtOnce !== 'function') {
|
|
120
|
+
// ⚠️ A MENSAGEM NÃO NOMEIA GÊNERO, e o gate de fronteira cobrou-o: a primeira escrita dizia «run+walk+jump
|
|
121
|
+
// is 3, a quiz is 1» e o `engine-boundary` reprovou a palavra «quiz» em linha de CÓDIGO da engine. Ele
|
|
122
|
+
// tinha razão, e a frase ficou melhor: descreve a FORMA da pergunta, que serve aos 300 jogos, em vez de
|
|
123
|
+
// dois exemplos que servem a dois.
|
|
124
|
+
p.push('holdsAtOnce: missing - declare how many positions are held AT ONCE (three if three fingers must press together, one if commands arrive one at a time)');
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
const n = d.holdsAtOnce();
|
|
128
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
129
|
+
// Zero não é «não usa controle»: um jogo que não segura posição nenhuma não é jogável, e devolver zero
|
|
130
|
+
// faria a aritmética do alcance passar por vacuidade — o mesmo defeito que o `reachable` recusa.
|
|
131
|
+
p.push('holdsAtOnce: must be an integer >= 1 - a game that holds nothing cannot be played');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// ⚠️ E ESTA É A OUTRA PERGUNTA, que o número acima parecia responder e não responde (ADR-0115). A mensagem
|
|
135
|
+
// diz o que a ausência CUSTA, e não só o que falta: sem ela, um jogo que nada segura oferece um controle de
|
|
136
|
+
// acessibilidade que não faz nada, e um que segura tudo pode não o oferecer a quem depende dele.
|
|
137
|
+
if (typeof d.seguraTeclas !== 'function') {
|
|
138
|
+
p.push('seguraTeclas: missing - declare whether any key is HELD in this game (latching is offered only where something can be held, and a game that holds nothing must not show a control that does nothing)');
|
|
139
|
+
}
|
|
140
|
+
else if (typeof d.seguraTeclas() !== 'boolean') {
|
|
141
|
+
// Um valor não-booleano seria truthy e ofereceria a alternância a toda a gente — o mesmo defeito
|
|
142
|
+
// silencioso que o `needsPointer` recusa logo abaixo, e pela mesma razão.
|
|
143
|
+
p.push('seguraTeclas: must return a boolean - a non-boolean is truthy and would offer latching in a game where nothing is held');
|
|
144
|
+
}
|
|
145
|
+
// ⚠️ OPCIONAL, MAS NÃO IMPUNE. Ausente é a resposta `false` e não é problema — ver a nota no campo. O que
|
|
146
|
+
// se recusa é declará-lo MAL: um `needsPointer: true` (valor em vez de função) seria sempre verdadeiro por
|
|
147
|
+
// ser um objecto, e um que devolvesse `'sim'` também. Nos dois casos o jogo julgaria ter declarado, o
|
|
148
|
+
// alcance leria uma coisa diferente do que ele quis dizer, e ninguém saberia — que é o defeito silencioso
|
|
149
|
+
// que esta função inteira existe para não deixar acontecer.
|
|
150
|
+
if (d.needsPointer !== undefined) {
|
|
151
|
+
if (typeof d.needsPointer !== 'function') {
|
|
152
|
+
// ⚠️ MENSAGENS SEM A PALAVRA `as`, e o motivo merece uma linha porque volta a morder: o detector de prosa
|
|
153
|
+
// pt-BR do `engine-i18n` casa palavras funcionais isoladas, e `as` é artigo plural em português. Uma
|
|
154
|
+
// mensagem INGLESA que diga «declare it as …» é contada como texto cru e faz o tecto do módulo subir.
|
|
155
|
+
// O cabeçalho daquele gate já admite a aproximação («senão 'mode' casa 'de'»); reescrever a frase custa
|
|
156
|
+
// nada e afrouxar o detector custaria a razão de ele existir.
|
|
157
|
+
p.push('needsPointer: must be a function - write `needsPointer: () => true`, because a game may draw in one phase and not in another');
|
|
158
|
+
}
|
|
159
|
+
else if (typeof d.needsPointer() !== 'boolean') {
|
|
160
|
+
p.push('needsPointer: must return a boolean - a non-boolean would be truthy and refuse devices this game can actually use');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// ⚠️ E O MESMO PARA O MAPEAMENTO, com uma razão própria: aqui um valor em vez de uma função não seria um
|
|
164
|
+
// erro barulhento, seria um mapeamento SILENCIOSAMENTE ignorado — a fábrica da engine ficava, e a criança
|
|
165
|
+
// jogava com um teclado que o autor do jogo julga ter mudado. E devolver algo que não é objecto nem `null`
|
|
166
|
+
// atravessaria o `Object.assign` sem escrever nada, que é a mesma ausência com outra roupa.
|
|
167
|
+
if (d.mapeamentoDoTeclado !== undefined) {
|
|
168
|
+
if (typeof d.mapeamentoDoTeclado !== 'function') {
|
|
169
|
+
p.push('mapeamentoDoTeclado: must be a function - write `mapeamentoDoTeclado: (jogadores, assento) => ({ action1: ["KeyQ"] })`, because the keyboard of two players is not the keyboard of one');
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
const m = d.mapeamentoDoTeclado(1, 0);
|
|
173
|
+
if (m !== null && (typeof m !== 'object' || Array.isArray(m))) {
|
|
174
|
+
p.push('mapeamentoDoTeclado: must return an object or null - anything else is merged into nothing, and this game keeps the engine factory while its author believes otherwise');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (d.mapeamentoDoPad !== undefined) {
|
|
179
|
+
if (typeof d.mapeamentoDoPad !== 'function') {
|
|
180
|
+
p.push('mapeamentoDoPad: must be a function - write `mapeamentoDoPad: (jogadores, assento) => ({ action1: 3 })`, because two seats may want different arrangements');
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
const m = d.mapeamentoDoPad(1, 0);
|
|
184
|
+
if (m !== null && (typeof m !== 'object' || Array.isArray(m))) {
|
|
185
|
+
p.push('mapeamentoDoPad: must return an object or null - anything else is merged into nothing, and this game keeps the engine factory while its author believes otherwise');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
111
189
|
if (d.tick !== 'player' && d.tick !== 'clock')
|
|
112
190
|
p.push('tick: must be "player" or "clock"');
|
|
113
191
|
for (const f of ['roleAt', 'nameAt', 'focusOf', 'objectiveOf', 'targetsOf']) {
|
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import type { Action } from './actions.js';
|
|
3
|
+
import type { VisualState } from '../render/viz-axes.js';
|
|
4
|
+
/**
|
|
5
|
+
* Ação → lista de códigos físicos (`KeyA`, `ArrowLeft`…). Fonte única: estava triplicado em input/.
|
|
6
|
+
*
|
|
7
|
+
* ⚠️ FECHADO EM `Action` DESDE 2026-09-07 (issue #118, decisão do Dev), e era `Record<string, string[]>`.
|
|
8
|
+
* Enquanto foi aberto, **faltar uma posição não dava erro de compilação** — e foi assim que o esquema de
|
|
9
|
+
* dupla ficou com oito das quatorze, e que a `Space` do jogador 1 desapareceu sem que nada apitasse. O
|
|
10
|
+
* defeito não era de digitação: era de o tipo aceitar um esquema incompleto como se fosse completo.
|
|
11
|
+
*
|
|
12
|
+
* ⚠️ E `null` NÃO É BURACO — é AUSÊNCIA DECLARADA, e é a metade que dá sentido a fechar o tipo. Um teclado
|
|
13
|
+
* partido por quatro pode não ter lugar físico para ombros e gatilhos; dizer `null` afirma isso, e é o que o
|
|
14
|
+
* aviso de alcance (`ui/reach-notice`, issue #112) lê para dizer à criança, ANTES de ela começar, quais das
|
|
15
|
+
* ações do jogo o controlo dela não alcança. Inventar teclas para preencher seria mentir-lhe em silêncio.
|
|
16
|
+
*
|
|
17
|
+
* Quem consome tem de tratar o `null`: `input/keyboard-runtime`, `input/touch-bindings` e
|
|
18
|
+
* `ui/settings-controls` fazem-no, e é isso que impede um `null` de virar um `undefined.includes`.
|
|
19
|
+
*/
|
|
20
|
+
export type KeyScheme = Record<Action, readonly string[] | null>;
|
|
4
21
|
/** Lados da ventosa-aranha: direita, esquerda, teto, chão. */
|
|
5
22
|
export type ClingSide = 'R' | 'L' | 'U' | 'D';
|
|
6
23
|
/**
|
|
@@ -76,6 +93,26 @@ export interface Player {
|
|
|
76
93
|
ctrl: KeyScheme | null;
|
|
77
94
|
/** Índice do gamepad, ou -1 quando o jogador não tem controle físico. */
|
|
78
95
|
pad: number;
|
|
96
|
+
/**
|
|
97
|
+
* O ESTADO VISUAL desta criança, em DOIS EIXOS mais a simulação (ADR-0076, issue #104).
|
|
98
|
+
*
|
|
99
|
+
* ⚠️ `import type`, e a seta aponta para cima. `core/` não deveria importar de `render/`, e o tipo do
|
|
100
|
+
* ESTADO discutivelmente devia morar aqui em vez de lá. Fica assim de propósito e declarado: é só tipo,
|
|
101
|
+
* apagado em execução, sem aresta em runtime — e mover o módulo a meio da migração seria churn no ficheiro
|
|
102
|
+
* mais partilhado do repositório. Se `viz-axes` se dividir um dia, o `VisualState` vem para `core/` e as
|
|
103
|
+
* funções de renderização ficam lá.
|
|
104
|
+
*/
|
|
105
|
+
visual: VisualState;
|
|
106
|
+
/**
|
|
107
|
+
* @deprecated ⚠️ O ESPELHO LEGADO, e ele morre nesta migração. Enquanto os dois existirem, `setPlayerViz`
|
|
108
|
+
* escreve os DOIS e um gate exige que nunca discordem — é o que deixa cada leitor migrar sozinho, com a
|
|
109
|
+
* árvore verde entre cada passo, em vez de uma passagem única onde não há onde parar.
|
|
110
|
+
*
|
|
111
|
+
* ⚠️ E ELE NÃO CONSEGUE EXPRIMIR O QUE A #104 EXISTE PARA PERMITIR: uma criança com `hc7` E `fix-deuter` ao
|
|
112
|
+
* mesmo tempo não tem chave única que a descreva. Por isso o espelho só sobrevive enquanto os controles
|
|
113
|
+
* ainda escrevem um valor de cada vez; assim que eles passam a escrever por eixo, ele deixa de poder
|
|
114
|
+
* acompanhar e sai.
|
|
115
|
+
*/
|
|
79
116
|
viz: string;
|
|
80
117
|
easy: boolean;
|
|
81
118
|
toggleMove: boolean;
|
|
@@ -124,9 +161,15 @@ export interface Player {
|
|
|
124
161
|
alfWins?: number;
|
|
125
162
|
/** ui/settings-audio, ui/pause-icons: saída de áudio própria; `null`/ausente = compartilhada. */
|
|
126
163
|
audioSink?: string | null;
|
|
127
|
-
/**
|
|
164
|
+
/**
|
|
165
|
+
* platform/audio-nav: temporizador do sonar de parede.
|
|
166
|
+
*
|
|
167
|
+
* ⚠️ `guideT` SAIU em 2026-09-07 (#84 item 2), e a ausência é a notícia: ele contava os 48 quadros entre
|
|
168
|
+
* dois bipes do guia, e não há mais bipe nenhum para temporizar. O que o substituiu — o grafo de áudio vivo
|
|
169
|
+
* — não é da entidade da engine e por isso não nasce aqui: mora em `PlayerAudioOut`, ao lado do `_ac`, pela
|
|
170
|
+
* mesma regra que o parágrafo acima explica.
|
|
171
|
+
*/
|
|
128
172
|
wnT?: number;
|
|
129
|
-
guideT?: number;
|
|
130
173
|
}
|
|
131
174
|
/**
|
|
132
175
|
* Um jogador DEPOIS de `assignControls` — o `ctrl` deixou de ser `null`.
|
|
@@ -32,6 +32,24 @@ export declare const Z: {
|
|
|
32
32
|
readonly MENU: 30000;
|
|
33
33
|
readonly MENU_MAX: 39999;
|
|
34
34
|
readonly TRANSITION: 40000;
|
|
35
|
+
/**
|
|
36
|
+
* ⚠️ A SAÍDA DE EMERGÊNCIA, E ELA FICA ACIMA DA TRANSIÇÃO (decisão do Dev, 2026-09-07).
|
|
37
|
+
*
|
|
38
|
+
* Este comentário dizia que o *skip-link* mora em `CAPTIONS` (26000) — e isso deixava-o ABAIXO de
|
|
39
|
+
* `MENU` (30000). Um «pular para o conteúdo» que um modal cobre não é alcançável, o que é a WCAG 2.4.1 ao
|
|
40
|
+
* contrário: o mecanismo que existe para atravessar blocos repetidos passa a ser mais um bloco.
|
|
41
|
+
* O gate do `z-order-css` já registava o defeito com todas as letras — «o `Z` ERRA, não o CSS» —, e ficou
|
|
42
|
+
* assim durante semanas porque nada o obrigava a mudar.
|
|
43
|
+
*
|
|
44
|
+
* ⚠️ E É ACIMA DA `TRANSITION`, não entre o menu e ela. O Dev pediu 41000 e o motivo é o que sobra depois
|
|
45
|
+
* de tudo o resto falhar: a saída não pode ficar atrás de nada que a pessoa não controla, e um fade de
|
|
46
|
+
* troca de fase é precisamente isso. O custo — o link aparecer por cima de uma animação — é nulo na
|
|
47
|
+
* prática, porque ele só é visível AO FOCO (`top:-60px` até `:focus`), e porque durante a transição já não
|
|
48
|
+
* se tabula: o foco sai da tela antiga e só reaparece na seguinte (ver a regra de foco no ADR).
|
|
49
|
+
*
|
|
50
|
+
* Só o aviso de laço morto (ADR-0054) e o painel de desenvolvimento ficam acima dela.
|
|
51
|
+
*/
|
|
52
|
+
readonly SKIP_LINK: 41000;
|
|
35
53
|
/**
|
|
36
54
|
* O LAÇO MORREU (ADR-0054). Acima do `TRANSITION` de propósito: quando o quadro lança, o que estiver na
|
|
37
55
|
* tela — menu aberto, fade a meio, mensagem de fase — está congelado e deixou de importar. O aviso tem de
|
package/dist-pkg/core/layers.js
CHANGED
|
@@ -49,6 +49,24 @@ export const Z = {
|
|
|
49
49
|
MENU: 30000, // menus — RANGE reservado 30000–39999 (níveis aninhados: +1000 por nível)
|
|
50
50
|
MENU_MAX: 39999,
|
|
51
51
|
TRANSITION: 40000, // fade/wipe de troca de fase (cobre tudo)
|
|
52
|
+
/**
|
|
53
|
+
* ⚠️ A SAÍDA DE EMERGÊNCIA, E ELA FICA ACIMA DA TRANSIÇÃO (decisão do Dev, 2026-09-07).
|
|
54
|
+
*
|
|
55
|
+
* Este comentário dizia que o *skip-link* mora em `CAPTIONS` (26000) — e isso deixava-o ABAIXO de
|
|
56
|
+
* `MENU` (30000). Um «pular para o conteúdo» que um modal cobre não é alcançável, o que é a WCAG 2.4.1 ao
|
|
57
|
+
* contrário: o mecanismo que existe para atravessar blocos repetidos passa a ser mais um bloco.
|
|
58
|
+
* O gate do `z-order-css` já registava o defeito com todas as letras — «o `Z` ERRA, não o CSS» —, e ficou
|
|
59
|
+
* assim durante semanas porque nada o obrigava a mudar.
|
|
60
|
+
*
|
|
61
|
+
* ⚠️ E É ACIMA DA `TRANSITION`, não entre o menu e ela. O Dev pediu 41000 e o motivo é o que sobra depois
|
|
62
|
+
* de tudo o resto falhar: a saída não pode ficar atrás de nada que a pessoa não controla, e um fade de
|
|
63
|
+
* troca de fase é precisamente isso. O custo — o link aparecer por cima de uma animação — é nulo na
|
|
64
|
+
* prática, porque ele só é visível AO FOCO (`top:-60px` até `:focus`), e porque durante a transição já não
|
|
65
|
+
* se tabula: o foco sai da tela antiga e só reaparece na seguinte (ver a regra de foco no ADR).
|
|
66
|
+
*
|
|
67
|
+
* Só o aviso de laço morto (ADR-0054) e o painel de desenvolvimento ficam acima dela.
|
|
68
|
+
*/
|
|
69
|
+
SKIP_LINK: 41000,
|
|
52
70
|
/**
|
|
53
71
|
* O LAÇO MORREU (ADR-0054). Acima do `TRANSITION` de propósito: quando o quadro lança, o que estiver na
|
|
54
72
|
* tela — menu aberto, fade a meio, mensagem de fase — está congelado e deixou de importar. O aviso tem de
|
package/dist-pkg/core/rng.js
CHANGED
|
@@ -42,10 +42,20 @@ export const createRng = (semente = SEMENTE_PADRAO) => {
|
|
|
42
42
|
const reseed = (s) => { _seed = s >>> 0; };
|
|
43
43
|
return { rnd, randInt, shuffle, reseed };
|
|
44
44
|
};
|
|
45
|
-
// ⚠️
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
45
|
+
// ⚠️ ESTE BLOCO TINHA DATA DE MORTE ESCRITA, E ELA NÃO CHEGOU — A PREMISSA É QUE MORREU.
|
|
46
|
+
// Dizia: «Existe porque `game/` ainda vive aqui dentro (issue #111). Quando `game/` sair, isto sai com ele.»
|
|
47
|
+
// O `game/` saiu em 2026-09-07 — e saiu para OUTRO REPOSITÓRIO, onde continua a consumir estes quatro pelo
|
|
48
|
+
// pacote. Medido no dia: `@the-inclusionist/game-platformer` importa `rnd`, `randInt` e `shuffle` em OITO
|
|
49
|
+
// módulos (`quiz`, `physics`, `life`, `traffic`, `fractions`, `coins`, `coin-spawning`,
|
|
50
|
+
// `literacy-distractors`) mais o `main.ts`.
|
|
51
|
+
//
|
|
52
|
+
// Ou seja, o que se previu como «sai junto» virou o oposto: saindo, o cartucho tornou-os SUPERFÍCIE
|
|
53
|
+
// PÚBLICA de um pacote publicado. Removê-los agora é quebra de contrato maior, não limpeza.
|
|
54
|
+
//
|
|
55
|
+
// ⚠️ E A ADVERTÊNCIA CONTINUA VERDADEIRA, o que é o incómodo: são estado partilhado de módulo, que é
|
|
56
|
+
// exatamente o defeito que a fábrica acima conserta. Um segundo jogo na mesma página divide esta corrente
|
|
57
|
+
// com o primeiro. O caminho de saída é o consumidor passar a `createRng(suaSemente)` — e isso é migração
|
|
58
|
+
// dele, com aviso e um major, não uma remoção unilateral daqui.
|
|
49
59
|
const _padrao = createRng(SEMENTE_PADRAO);
|
|
50
60
|
export const reseed = _padrao.reseed;
|
|
51
61
|
export const rnd = _padrao.rnd;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
import type { Role, Spot, Topology } from './contract.js';
|
|
3
|
+
/**
|
|
4
|
+
* Os papéis que uma rota atravessa. Leitura do `core/contract`, com o `hazard` de fora por decisão declarada
|
|
5
|
+
* no cabeçalho — e `goal` de fora porque ele entra pela regra do último passo, não pela do meio.
|
|
6
|
+
*/
|
|
7
|
+
export declare const PAPEIS_ATRAVESSAVEIS: ReadonlySet<Role>;
|
|
8
|
+
/** Este papel deixa passar? */
|
|
9
|
+
export declare function atravessavel(papel: Role): boolean;
|
|
10
|
+
export interface RouteCtx {
|
|
11
|
+
/** Campo 1 do contrato. A métrica decide QUANTOS vizinhos um ponto tem. */
|
|
12
|
+
readonly topology: Topology;
|
|
13
|
+
/** Campo 2 do contrato: o que há neste ponto. Obrigatório na `GameDeclaration`, logo sempre disponível. */
|
|
14
|
+
readonly roleAt: (at: Spot) => Role;
|
|
15
|
+
/**
|
|
16
|
+
* Teto de pontos visitados. Estourou → `null`, que é «não sei».
|
|
17
|
+
*
|
|
18
|
+
* 4096 é ~64×64 numa grade e cobre com folga os tabuleiros que existem hoje; num mapa de plataforma grande
|
|
19
|
+
* ele corta antes de a busca custar um quadro. O número é PARÂMETRO porque o custo aceitável é de quem
|
|
20
|
+
* chama: uma pista por quadro tolera muito menos do que um cálculo ao carregar a fase.
|
|
21
|
+
*/
|
|
22
|
+
readonly orcamento?: number;
|
|
23
|
+
}
|
|
24
|
+
export interface Rota {
|
|
25
|
+
/** O PRÓXIMO ponto a pisar — a um passo de onde se está. É isto que uma pista aponta. */
|
|
26
|
+
readonly proximo: Spot;
|
|
27
|
+
/** Qual dos alvos a rota alcançou. Pode não ser o mais próximo em linha reta, e é esse o ponto. */
|
|
28
|
+
readonly ate: Spot;
|
|
29
|
+
/** Quantos passos ao longo do caminho. ⚠️ NÃO é `distance()`, que mede a reta que atravessa parede. */
|
|
30
|
+
readonly passos: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A rota de `de` até o mais próximo ALCANÇÁVEL dos `alvos` — largura primeiro, sobre o que se atravessa.
|
|
34
|
+
*
|
|
35
|
+
* `null` quando não há alvo, quando nenhum é alcançável, quando a topologia não tem espaço (`hotspots`), ou
|
|
36
|
+
* quando o orçamento estourou. Os quatro casos são o mesmo para quem chama: **não sei dizer por onde**.
|
|
37
|
+
*
|
|
38
|
+
* ⚠️ «MAIS PRÓXIMO» AQUI É AO LONGO DO CAMINHO, e é a diferença inteira. Um alvo a três células em linha reta
|
|
39
|
+
* do outro lado de uma parede está mais LONGE do que um a oito células por um corredor aberto — e é o segundo
|
|
40
|
+
* que a criança consegue alcançar.
|
|
41
|
+
*/
|
|
42
|
+
export declare function rotaAte(ctx: RouteCtx, de: Spot, alvos: readonly Spot[]): Rota | null;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// core/route — POR ONDE SE VAI ATÉ LÁ, e não só onde é lá (#84, item 1).
|
|
3
|
+
//
|
|
4
|
+
// ========================= O DEFEITO QUE ISTO EXISTE PARA CONSERTAR =========================
|
|
5
|
+
// A issue #84 escreve-o em duas linhas, e elas são o módulo inteiro:
|
|
6
|
+
//
|
|
7
|
+
// «Sem isso, qualquer pista aponta em LINHA RETA para o alvo, e a linha reta atravessa parede: hoje
|
|
8
|
+
// `alvoMaisProximo()` mede distância pela métrica declarada e `panFor()` só compara `x`, então o guia
|
|
9
|
+
// manda a criança andar para dentro de um bloco sólido e ela não tem como saber por quê.»
|
|
10
|
+
//
|
|
11
|
+
// ⚠️ E ELA NÃO TEM MESMO COMO SABER. Uma criança que vê ignora uma seta que aponta para uma parede sem sequer
|
|
12
|
+
// reparar que a ignorou. Quem depende da pista faz o que ela diz — e depois faz outra vez, porque a pista
|
|
13
|
+
// continua a dizer o mesmo. A pista errada é pior do que pista nenhuma: pista nenhuma deixa-a explorar.
|
|
14
|
+
//
|
|
15
|
+
// ========================= O QUE É «ATRAVESSÁVEL», E POR QUE NÃO É INVENÇÃO MINHA =========================
|
|
16
|
+
// A #84 pede «as direções por onde há AR OU ÁGUA». Essas duas palavras já estão no contrato, e cada `Role`
|
|
17
|
+
// diz de si mesmo se se atravessa — o conjunto abaixo é uma LEITURA do `core/contract`, não uma decisão nova:
|
|
18
|
+
//
|
|
19
|
+
// free «atravessável e sem significado próprio» → o ar
|
|
20
|
+
// water «atravessa-se nadando» → a água
|
|
21
|
+
// climb «muda-se de altura interagindo com isto» → a escada: é POR ELA que se sobe
|
|
22
|
+
// key «satisfaz um gate» → um objeto pousado, não uma barreira
|
|
23
|
+
// structure «cenário: chão, parede, o que sustenta» → NÃO
|
|
24
|
+
// gate «barra até uma condição» → NÃO: barrar é o que ele faz
|
|
25
|
+
// hazard «machuca ao encostar» → NÃO, e é a única onde eu escolhi
|
|
26
|
+
// goal «o que a rodada pede» → caso à parte, ver `alvo` abaixo
|
|
27
|
+
//
|
|
28
|
+
// ⚠️ O `hazard` É A ESCOLHA, e declaro-a: a métrica dele não diz se se atravessa, diz o que custa. Rota que
|
|
29
|
+
// passa por espinho é rota que manda a criança levar dano — e no modo cego e no modo cadeirante o próprio
|
|
30
|
+
// `core/collision.isSolidType` já o trata como sólido, por decisão de acessibilidade. Encaminhar por cima
|
|
31
|
+
// dele contradiria a camada que existe para a proteger. Um jogo em que o espinho seja passagem obrigatória
|
|
32
|
+
// terá de dizê-lo por outro meio; hoje nenhum diz.
|
|
33
|
+
//
|
|
34
|
+
// ========================= O ALVO É SEMPRE PISÁVEL, E O CAMINHO NÃO =========================
|
|
35
|
+
// Um `goal` pode estar declarado numa célula que não se atravessa (uma bandeira dentro de um portão). Recusar
|
|
36
|
+
// entrar nela faria a rota nunca chegar a lado nenhum. A regra é assimétrica de propósito: **o último passo é
|
|
37
|
+
// sempre permitido; os do meio obedecem ao conjunto.** É a diferença entre «entrar no portão» e «atravessar o
|
|
38
|
+
// portão para continuar do outro lado».
|
|
39
|
+
//
|
|
40
|
+
// ========================= O ORÇAMENTO, QUE O PRÓPRIO CONTRATO PEDIU =========================
|
|
41
|
+
// ⚠️ `core/contract` avisa, na sua secção 5: «num espaço CONTÍNUO não há como enumerar os pontos, e num mapa
|
|
42
|
+
// grande enumerar seria caro por quadro». Este módulo não pode ignorar esse aviso só porque é conveniente.
|
|
43
|
+
// Por isso tem TETO de células visitadas e devolve `null` ao estourá-lo. `null` quer dizer **«não sei»**, e
|
|
44
|
+
// não «não há caminho» — quem chamar tem de tratar os dois iguais, que é o que uma pista honesta faz: cala-se.
|
|
45
|
+
//
|
|
46
|
+
// E não importa nada além do contrato: é lógica pura, aferida no project `node`.
|
|
47
|
+
import { distance } from './contract.js';
|
|
48
|
+
/**
|
|
49
|
+
* Os papéis que uma rota atravessa. Leitura do `core/contract`, com o `hazard` de fora por decisão declarada
|
|
50
|
+
* no cabeçalho — e `goal` de fora porque ele entra pela regra do último passo, não pela do meio.
|
|
51
|
+
*/
|
|
52
|
+
export const PAPEIS_ATRAVESSAVEIS = new Set(['free', 'water', 'climb', 'key']);
|
|
53
|
+
/** Este papel deixa passar? */
|
|
54
|
+
export function atravessavel(papel) {
|
|
55
|
+
return PAPEIS_ATRAVESSAVEIS.has(papel);
|
|
56
|
+
}
|
|
57
|
+
const ORCAMENTO_PADRAO = 4096;
|
|
58
|
+
/** Chave de um ponto na fila. Arredondada, porque no contínuo os pontos nascem de somas de `passo`. */
|
|
59
|
+
const chave = (s, casas) => s.x.toFixed(casas) + '|' + s.y.toFixed(casas) + '|' + (s.z ?? 0).toFixed(casas);
|
|
60
|
+
/**
|
|
61
|
+
* Os deslocamentos de UM passo, na métrica declarada.
|
|
62
|
+
*
|
|
63
|
+
* ⚠️ `free` (L², espaço contínuo sem passo discreto) NÃO TEM VIZINHOS — e fingir que tem é a aproximação que
|
|
64
|
+
* este módulo faz e declara: ele anda numa GRELHA de lado `unit`, nas oito direções. A alternativa seria não
|
|
65
|
+
* responder nada num jogo de plataforma, que é justamente o gênero da issue. Quem ler uma rota `free` está a
|
|
66
|
+
* ler uma amostragem, não uma trajetória — e é por isso que `passos` é uma contagem de células e não uma
|
|
67
|
+
* medida física.
|
|
68
|
+
*/
|
|
69
|
+
function vizinhos(topo, passo) {
|
|
70
|
+
if (topo.kind === 'hotspots')
|
|
71
|
+
return [];
|
|
72
|
+
const dims = topo.size.length;
|
|
73
|
+
const ortogonal = topo.move === 'orthogonal';
|
|
74
|
+
const fora = [];
|
|
75
|
+
const eixos = [-1, 0, 1];
|
|
76
|
+
for (const dx of eixos)
|
|
77
|
+
for (const dy of eixos) {
|
|
78
|
+
for (const dz of dims > 2 ? eixos : [0]) {
|
|
79
|
+
const n = Math.abs(dx) + Math.abs(dy) + Math.abs(dz);
|
|
80
|
+
if (n === 0)
|
|
81
|
+
continue;
|
|
82
|
+
if (ortogonal && n > 1)
|
|
83
|
+
continue; // L¹: a diagonal não existe
|
|
84
|
+
fora.push({ x: dx * passo, y: dy * passo, z: dz * passo });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return fora;
|
|
88
|
+
}
|
|
89
|
+
/** O ponto cabe na extensão declarada? Grade conta células 0..n−1; contínuo conta unidades 0..n. */
|
|
90
|
+
function dentro(topo, s) {
|
|
91
|
+
if (topo.kind === 'hotspots')
|
|
92
|
+
return false;
|
|
93
|
+
const eixo = [s.x, s.y, s.z ?? 0];
|
|
94
|
+
for (let i = 0; i < topo.size.length; i++) {
|
|
95
|
+
const lim = topo.size[i];
|
|
96
|
+
if (eixo[i] < 0)
|
|
97
|
+
return false;
|
|
98
|
+
if (topo.kind === 'grid' ? eixo[i] > lim - 1 : eixo[i] > lim)
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* A rota de `de` até o mais próximo ALCANÇÁVEL dos `alvos` — largura primeiro, sobre o que se atravessa.
|
|
105
|
+
*
|
|
106
|
+
* `null` quando não há alvo, quando nenhum é alcançável, quando a topologia não tem espaço (`hotspots`), ou
|
|
107
|
+
* quando o orçamento estourou. Os quatro casos são o mesmo para quem chama: **não sei dizer por onde**.
|
|
108
|
+
*
|
|
109
|
+
* ⚠️ «MAIS PRÓXIMO» AQUI É AO LONGO DO CAMINHO, e é a diferença inteira. Um alvo a três células em linha reta
|
|
110
|
+
* do outro lado de uma parede está mais LONGE do que um a oito células por um corredor aberto — e é o segundo
|
|
111
|
+
* que a criança consegue alcançar.
|
|
112
|
+
*/
|
|
113
|
+
export function rotaAte(ctx, de, alvos) {
|
|
114
|
+
const topo = ctx.topology;
|
|
115
|
+
if (topo.kind === 'hotspots' || alvos.length === 0)
|
|
116
|
+
return null;
|
|
117
|
+
const passo = topo.kind === 'continuous' ? topo.unit : 1;
|
|
118
|
+
if (!(passo > 0))
|
|
119
|
+
return null; // uma unidade de zero faria a fila andar sem sair do sítio
|
|
120
|
+
const casas = topo.kind === 'continuous' ? 4 : 0;
|
|
121
|
+
// Chegou? Na grade é a mesma célula; no contínuo é meio passo, porque a grelha não cai em cima do alvo.
|
|
122
|
+
const tolerancia = topo.kind === 'continuous' ? 0.5 : 0;
|
|
123
|
+
const chegou = (s) => alvos.find((a) => distance(topo, s, a) <= tolerancia) ?? null;
|
|
124
|
+
const alvoAqui = chegou(de);
|
|
125
|
+
if (alvoAqui)
|
|
126
|
+
return { proximo: de, ate: alvoAqui, passos: 0 };
|
|
127
|
+
const saltos = vizinhos(topo, passo);
|
|
128
|
+
const teto = ctx.orcamento ?? ORCAMENTO_PADRAO;
|
|
129
|
+
const vistos = new Set([chave(de, casas)]);
|
|
130
|
+
// Cada item leva o PRIMEIRO passo que o originou — é só isso que a pista precisa de saber no fim.
|
|
131
|
+
let fila = [{ at: de, primeiro: de, passos: 0 }];
|
|
132
|
+
while (fila.length) {
|
|
133
|
+
const proxima = [];
|
|
134
|
+
for (const item of fila) {
|
|
135
|
+
for (const d of saltos) {
|
|
136
|
+
const vizinho = { x: item.at.x + d.x, y: item.at.y + d.y, z: (item.at.z ?? 0) + (d.z ?? 0) };
|
|
137
|
+
if (!dentro(topo, vizinho))
|
|
138
|
+
continue;
|
|
139
|
+
const k = chave(vizinho, casas);
|
|
140
|
+
if (vistos.has(k))
|
|
141
|
+
continue;
|
|
142
|
+
vistos.add(k);
|
|
143
|
+
if (vistos.size > teto)
|
|
144
|
+
return null; // «não sei», e é uma resposta
|
|
145
|
+
const primeiro = item.passos === 0 ? vizinho : item.primeiro;
|
|
146
|
+
// O ÚLTIMO PASSO É SEMPRE PERMITIDO: um alvo pode estar declarado numa célula que não se atravessa.
|
|
147
|
+
const alvo = chegou(vizinho);
|
|
148
|
+
if (alvo)
|
|
149
|
+
return { proximo: primeiro, ate: alvo, passos: item.passos + 1 };
|
|
150
|
+
if (!atravessavel(ctx.roleAt(vizinho)))
|
|
151
|
+
continue;
|
|
152
|
+
proxima.push({ at: vizinho, primeiro, passos: item.passos + 1 });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
fila = proxima;
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
package/dist-pkg/core/state.d.ts
CHANGED
|
@@ -13,7 +13,14 @@
|
|
|
13
13
|
* Agora `EventoDoJogo` é um mapa nome → carga, e `emit`/`on` são genéricos sobre ele. Nome inexistente não
|
|
14
14
|
* compila; carga errada não compila.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
16
|
+
* ✅ ERRATA 2026-09-08: O BARRAMENTO TEM ASSINANTES EM PRODUÇÃO. O parágrafo abaixo dizia «ZERO `on()` em
|
|
17
|
+
* produção (só o teste assina)», e isso deixou de ser verdade no dia em que a engine passou a montar a barra
|
|
18
|
+
* de acessibilidade: o `ui/settings-audio` assina `modoCego` para o botão `#opt-modocego` não mentir o
|
|
19
|
+
* estado, e o `boot/create-game` assina o mesmo evento para a barra montada não mentir o dela. Os dois
|
|
20
|
+
* chegaram pela razão que o parágrafo previa — «painéis que se redesenham quando a criança muda um ajuste» —
|
|
21
|
+
* e chegaram com o contrato já tipado, que era o ponto de ele nascer assim.
|
|
22
|
+
*
|
|
23
|
+
* ⚠️ POR QUE ISTO EXISTIU ANTES DE TER ASSINANTE. Hoje o barramento tem ZERO `on()` em produção (só o teste
|
|
17
24
|
* assina). Um mecanismo sem uso normalmente é dívida — mas este precisa nascer tipado, não ser retipado
|
|
18
25
|
* depois: o ADR-0031 já exige que painéis e atividades se redesenhem quando a criança muda idioma ou fonte,
|
|
19
26
|
* e a casca do `demos` vai reagir a ajuste durante a partida. O primeiro assinante chega com o contrato
|
|
@@ -93,6 +100,8 @@ export declare const DEFAULTS: Readonly<{
|
|
|
93
100
|
readonly hcOutlineFg: 1;
|
|
94
101
|
readonly hcOutlineBg: 1;
|
|
95
102
|
readonly letterCase: "upper";
|
|
103
|
+
readonly calmMode: 0;
|
|
104
|
+
readonly viz: "normal";
|
|
96
105
|
}>;
|
|
97
106
|
export declare let modoCego: boolean;
|
|
98
107
|
export declare function setModoCegoValue(on: boolean): void;
|
package/dist-pkg/core/state.js
CHANGED
|
@@ -152,6 +152,19 @@ export const DEFAULTS = Object.freeze({
|
|
|
152
152
|
hcOutlineBg: 1,
|
|
153
153
|
// comunicação (hoje só a caixa da letra; o menu de CAA do ADR-0028 amplia isto)
|
|
154
154
|
letterCase: 'upper',
|
|
155
|
+
// ⚠️ AS DUAS ÚLTIMAS ENTRARAM EM 2026-09-07 (issue #61), e não por simetria: elas faltavam, e a falta
|
|
156
|
+
// tinha consequência. A marca do ADR-0029 lê `DEFAULTS` e mais nada — regra que continua certa —, e por
|
|
157
|
+
// isso um valor sem padrão nomeado aqui é um valor que a marca NÃO PODE marcar. Cinco dos sete ícones da
|
|
158
|
+
// barra rápida caíam nisso.
|
|
159
|
+
//
|
|
160
|
+
// · `calmMode` — o nível TEA (0 normal · 1 calmo · 2 silencioso). Além de não ter padrão, ele não
|
|
161
|
+
// PERSISTIA: ver a nota de `KEYS.tea` em platform/storage.
|
|
162
|
+
// · `viz` — o modo de visão. `core/entity` declara `viz: string` sem dizer qual é o padrão, e o snapshot
|
|
163
|
+
// da barra fazia `p.viz || ''`. Funcionava por acidente: a cadeia vazia não casa `hc-direto` nem
|
|
164
|
+
// `fix-*`, então os dois ícones ficavam apagados. `'normal'` é o modo que `render/viz-modes` declara
|
|
165
|
+
// com `kind:'normal'` — o que não faz nada —, e passa a ser dito em vez de deduzido.
|
|
166
|
+
calmMode: 0,
|
|
167
|
+
viz: 'normal',
|
|
155
168
|
});
|
|
156
169
|
// --- modoCego: MODO CEGO (A12e auditiva). Só as ajudas de áudio — bengala, sonar, guarda de beirada,
|
|
157
170
|
// narração —, sem tela preta; a simulação de cegueira do Modo Empatia é outra coisa e liga esta por cima.
|