@the-inclusionist/engine 7.0.1 → 8.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/app/css/style.css +22 -5
- package/app/public/vendor/fonts/fondamento-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/fondamento-400.woff2 +0 -0
- package/app/public/vendor/fonts/opendyslexic-400.woff2 +0 -0
- package/app/public/vendor/fonts/pressstart-400.woff2 +0 -0
- package/app/public/vendor/fonts.css +43 -2
- package/dist-pkg/boot/create-game.d.ts +76 -2
- package/dist-pkg/boot/create-game.js +271 -13
- package/dist-pkg/core/constants.d.ts +0 -28
- package/dist-pkg/core/constants.js +34 -17
- package/dist-pkg/core/contract.d.ts +39 -0
- package/dist-pkg/core/contract.js +36 -0
- package/dist-pkg/core/entity.d.ts +47 -4
- package/dist-pkg/core/layers.d.ts +18 -0
- package/dist-pkg/core/layers.js +18 -0
- package/dist-pkg/core/rng.js +14 -4
- package/dist-pkg/core/route.d.ts +42 -0
- package/dist-pkg/core/route.js +158 -0
- package/dist-pkg/core/state.d.ts +10 -1
- package/dist-pkg/core/state.js +13 -0
- package/dist-pkg/educational/adaptive-engine.d.ts +65 -0
- package/dist-pkg/educational/adaptive-engine.js +117 -0
- package/dist-pkg/educational/segment-bar.d.ts +96 -0
- package/dist-pkg/educational/segment-bar.js +89 -0
- package/dist-pkg/i18n/en.js +33 -0
- package/dist-pkg/i18n/es.js +33 -0
- package/dist-pkg/i18n/pt.js +46 -0
- package/dist-pkg/input/default-bindings.d.ts +40 -0
- package/dist-pkg/input/default-bindings.js +143 -9
- package/dist-pkg/input/gamepad.d.ts +18 -11
- package/dist-pkg/input/gamepad.js +75 -9
- package/dist-pkg/input/keyboard-runtime.d.ts +6 -8
- package/dist-pkg/input/keyboard-runtime.js +24 -5
- package/dist-pkg/input/keyboard.d.ts +10 -0
- package/dist-pkg/input/keyboard.js +41 -11
- package/dist-pkg/input/keydown.d.ts +27 -2
- package/dist-pkg/input/keydown.js +20 -6
- package/dist-pkg/input/latch-scope.d.ts +70 -0
- package/dist-pkg/input/latch-scope.js +110 -0
- package/dist-pkg/input/latch-store.d.ts +45 -0
- package/dist-pkg/input/latch-store.js +74 -0
- package/dist-pkg/input/origem-sintetica.d.ts +44 -0
- package/dist-pkg/input/origem-sintetica.js +60 -0
- package/dist-pkg/input/pointer.d.ts +61 -0
- package/dist-pkg/input/pointer.js +71 -0
- package/dist-pkg/input/state.d.ts +86 -1
- package/dist-pkg/input/state.js +128 -1
- package/dist-pkg/input/touch-bindings.d.ts +11 -2
- package/dist-pkg/input/touch-bindings.js +9 -3
- package/dist-pkg/input/touch.js +9 -1
- package/dist-pkg/input/transporte-em-uso.d.ts +101 -0
- package/dist-pkg/input/transporte-em-uso.js +130 -0
- package/dist-pkg/input/transports.d.ts +88 -2
- package/dist-pkg/input/transports.js +60 -5
- package/dist-pkg/input/vocabulary-migration.d.ts +18 -7
- package/dist-pkg/platform/audio-earcons.d.ts +29 -2
- package/dist-pkg/platform/audio-earcons.js +10 -0
- package/dist-pkg/platform/audio-nav.d.ts +1 -1
- package/dist-pkg/platform/audio-sonar.d.ts +149 -9
- package/dist-pkg/platform/audio-sonar.js +232 -21
- package/dist-pkg/platform/guide-intensity.d.ts +40 -0
- package/dist-pkg/platform/guide-intensity.js +73 -0
- package/dist-pkg/platform/storage.d.ts +30 -0
- package/dist-pkg/platform/storage.js +35 -0
- package/dist-pkg/platform/tts.js +16 -1
- package/dist-pkg/platform/voice-plan.d.ts +90 -0
- package/dist-pkg/platform/voice-plan.js +137 -0
- package/dist-pkg/render/draw.d.ts +1 -1
- package/dist-pkg/render/draw.js +15 -5
- package/dist-pkg/render/viz-axes.d.ts +17 -0
- package/dist-pkg/render/viz-axes.js +19 -0
- package/dist-pkg/render/viz-setters.d.ts +34 -2
- package/dist-pkg/render/viz-setters.js +189 -33
- package/dist-pkg/render/wheelchair-sprites.d.ts +11 -3
- package/dist-pkg/render/wheelchair-sprites.js +10 -4
- package/dist-pkg/ui/dom.js +20 -2
- package/dist-pkg/ui/fonts.d.ts +47 -1
- package/dist-pkg/ui/fonts.js +47 -9
- package/dist-pkg/ui/latch-refusal.d.ts +32 -0
- package/dist-pkg/ui/latch-refusal.js +60 -0
- package/dist-pkg/ui/layout.d.ts +32 -0
- package/dist-pkg/ui/layout.js +64 -1
- package/dist-pkg/ui/motion-scene.d.ts +41 -0
- package/dist-pkg/ui/motion-scene.js +76 -0
- package/dist-pkg/ui/panel-shell.d.ts +53 -0
- package/dist-pkg/ui/panel-shell.js +103 -0
- package/dist-pkg/ui/pause-icons.d.ts +134 -20
- package/dist-pkg/ui/pause-icons.js +307 -45
- package/dist-pkg/ui/reach-notice.js +8 -0
- package/dist-pkg/ui/settings-audio.d.ts +14 -1
- package/dist-pkg/ui/settings-audio.js +43 -3
- package/dist-pkg/ui/settings-controls.d.ts +66 -6
- package/dist-pkg/ui/settings-controls.js +179 -17
- package/dist-pkg/ui/settings-empathy.d.ts +15 -0
- package/dist-pkg/ui/settings-empathy.js +2 -0
- package/dist-pkg/ui/settings-motion.d.ts +25 -19
- package/dist-pkg/ui/settings-motion.js +32 -19
- package/dist-pkg/ui/settings-motor.d.ts +81 -3
- package/dist-pkg/ui/settings-motor.js +118 -9
- package/dist-pkg/ui/settings-typo.d.ts +32 -2
- package/dist-pkg/ui/settings-typo.js +64 -18
- package/dist-pkg/ui/settings-visual.d.ts +21 -1
- package/dist-pkg/ui/settings-visual.js +48 -4
- package/dist-pkg/ui/shell.d.ts +13 -3
- package/dist-pkg/ui/shell.js +3 -4
- package/dist-pkg/ui/simulation-refusal.d.ts +32 -0
- package/dist-pkg/ui/simulation-refusal.js +57 -0
- package/dist-pkg/ui/visual-axes-panel.d.ts +48 -0
- package/dist-pkg/ui/visual-axes-panel.js +95 -0
- package/dist-pkg/ui/webcam.js +6 -1
- package/docs/CREDITS.md +18 -0
- package/docs/LICENSES.md +12 -0
- package/package.json +24 -4
- package/app/public/vendor/fonts/greatvibes-400.woff2 +0 -0
- package/app/public/vendor/fonts/ufcook-700.woff2 +0 -0
|
@@ -1,23 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
2
|
export declare const LOGICAL_W = 320, LOGICAL_H = 180, TILE = 16;
|
|
3
|
-
export declare const COIN_TARGET = 10;
|
|
4
|
-
export declare const TUNE: {
|
|
5
|
-
jumpVel: number;
|
|
6
|
-
waterJump: number;
|
|
7
|
-
waterJumpRun: number;
|
|
8
|
-
waterStrokeFrames: number;
|
|
9
|
-
trampBase: number;
|
|
10
|
-
trampMax: number;
|
|
11
|
-
gravity: number;
|
|
12
|
-
hWalk: number;
|
|
13
|
-
hRun: number;
|
|
14
|
-
climbSpeed: number;
|
|
15
|
-
maxFall: number;
|
|
16
|
-
waterMaxFall: number;
|
|
17
|
-
hTurbo: number;
|
|
18
|
-
ultraJumpVel: number;
|
|
19
|
-
};
|
|
20
|
-
export declare const JUMP_BASE: number;
|
|
21
3
|
export declare const ANIM: {
|
|
22
4
|
walkHold: number;
|
|
23
5
|
runHold: number;
|
|
@@ -53,18 +35,8 @@ export type TileType = {
|
|
|
53
35
|
export declare const TILE_TYPES: Record<number, TileType>;
|
|
54
36
|
/** Machuca ao encostar (lava). */
|
|
55
37
|
export declare const ehPerigo: (t: number) => boolean;
|
|
56
|
-
/** Água: nada-se dentro, e o pulo funciona lá. */
|
|
57
|
-
export declare const ehAgua: (t: number) => boolean;
|
|
58
|
-
/** Escada: sobe e desce. */
|
|
59
|
-
export declare const ehEscada: (t: number) => boolean;
|
|
60
38
|
/** Trampolim: arremessa para cima. */
|
|
61
39
|
export declare const ehTrampolim: (t: number) => boolean;
|
|
62
|
-
/** Portão: barra até a chave abrir. */
|
|
63
|
-
export declare const ehPortao: (t: number) => boolean;
|
|
64
|
-
/** A chave que abre o portão. */
|
|
65
|
-
export declare const ehChave: (t: number) => boolean;
|
|
66
|
-
/** Ar de região secreta: o que a escuridão cobre até alguém entrar. */
|
|
67
|
-
export declare const ehSecreto: (t: number) => boolean;
|
|
68
40
|
export declare const TILE_COLOR: {
|
|
69
41
|
0: string;
|
|
70
42
|
1: string;
|
|
@@ -3,13 +3,22 @@
|
|
|
3
3
|
// TUNE é objeto mutável (o painel de debug ajusta propriedades), mas NUNCA reatribuído → o import const
|
|
4
4
|
// funciona (mutar propriedade é ok; reatribuir é que quebraria). Ver docs/plano-modularizacao.md.
|
|
5
5
|
export const LOGICAL_W = 320, LOGICAL_H = 180, TILE = 16;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
// ⚠️ `COIN_TARGET` e `TUNE` MUDARAM DE CASA EM 2026-09-07 — foram para `game/tuning.ts` do `game-platformer`
|
|
7
|
+
// (issue #63, etapa B, decisão do Dev). Tinham ZERO importadores dentro desta árvore e TODOS do outro lado
|
|
8
|
+
// da fronteira do pacote: não eram código morto, eram a superfície pública que o cartucho consumia.
|
|
9
|
+
//
|
|
10
|
+
// Uma engine que exporta a GRAVIDADE e a META DE MOEDAS está a decidir que todo jogo é uma plataforma de
|
|
11
|
+
// coletar coisas. `TUNE` é gravidade, velocidade de nado e curso do trampolim — a física DAQUELE jogo; um
|
|
12
|
+
// quiz não tem gravidade. `COIN_TARGET` é o objetivo, que é conteúdo.
|
|
13
|
+
//
|
|
14
|
+
// A etapa C (o segundo consumidor) é o que autorizou o corte: catorze achados, e nenhum reclama estes dois
|
|
15
|
+
// para a engine. O `ui/debug-panel` continua a afinar o `TUNE` ao vivo — ele sempre o recebeu por INJEÇÃO
|
|
16
|
+
// (`TUNE: Tune` no ctx), nunca por importação, e é por isso que a mudança não lhe custou nada.
|
|
17
|
+
// ⚠️ `JUMP_BASE` SAIU EM 2026-09-07 (issue #63, etapa B, decisão do Dev). Não tinha um único consumidor:
|
|
18
|
+
// nem na engine, nem no `game-platformer`, nem nos testes — fora UM, que verificava que
|
|
19
|
+
// `JUMP_BASE === jumpVel * sqrt(8/5)`, ou seja **reafirmava a própria definição**. Um teste que não pode
|
|
20
|
+
// falhar por motivo que importe, a manter viva uma constante que ninguém usa. A própria issue #63 já o
|
|
21
|
+
// nomeava no seu «achado solto».
|
|
13
22
|
// E15: cadência de animação (ticks por quadro) — regulável ao vivo no painel ?debug=true. Como TUNE, é objeto
|
|
14
23
|
// mutável (o debug ajusta propriedades) mas NUNCA reatribuído → import const funciona. andar 6; correr 8 (~8fps,
|
|
15
24
|
// pedido do José); idle 20; swim 24; cling 10; escada 8; flavor ~6s.
|
|
@@ -46,18 +55,26 @@ export const TILE_TYPES = {
|
|
|
46
55
|
const prop = (t, k) => !!TILE_TYPES[t]?.[k];
|
|
47
56
|
/** Machuca ao encostar (lava). */
|
|
48
57
|
export const ehPerigo = (t) => prop(t, 'hazard');
|
|
49
|
-
/** Água: nada-se dentro, e o pulo funciona lá. */
|
|
50
|
-
export const ehAgua = (t) => prop(t, 'water');
|
|
51
|
-
/** Escada: sobe e desce. */
|
|
52
|
-
export const ehEscada = (t) => prop(t, 'ladder');
|
|
53
58
|
/** Trampolim: arremessa para cima. */
|
|
54
59
|
export const ehTrampolim = (t) => prop(t, 'tramp');
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
// ⚠️ `ehAgua`, `ehEscada`, `ehPortao` e `ehSecreto` MUDARAM DE CASA EM 2026-09-07 — foram para
|
|
61
|
+
// `game/tile-flags.ts` do `game-platformer` (issue #63, etapa B). Tinham zero importadores aqui dentro.
|
|
62
|
+
//
|
|
63
|
+
// E as duas que ficaram — `ehPerigo` e `ehTrampolim` — ficaram por um motivo MEDIDO, não por simetria: a
|
|
64
|
+
// engine lê-as, e não por geometria. O `isSolidType` de `core/collision` torna perigo e trampolim SÓLIDOS
|
|
65
|
+
// no modo cadeira de rodas, e perigo sólido no modo cego. É uma regra de ACESSIBILIDADE — a criança em
|
|
66
|
+
// cadeira de rodas não cai no fosso — e essa é da engine.
|
|
67
|
+
//
|
|
68
|
+
// A engine precisa de saber que um tile é PERIGO; não precisa de saber que ele é ESCADA.
|
|
69
|
+
//
|
|
70
|
+
// ⚠️ O QUE FICA POR DECIDIR, e está registado na #63: a `TILE_TYPES` acima continua a carregar as bandeiras
|
|
71
|
+
// `water`, `ladder`, `gate`, `key` e `secreto`, que **nenhum módulo desta árvore lê**. Enquanto elas aqui
|
|
72
|
+
// estiverem, o jogo faz perguntas sobre uma tabela que a engine publica. O fim honesto é a tabela mudar de
|
|
73
|
+
// casa e o jogo declarar os papéis pelo `core/contract` (`roleOf`) — que é o mecanismo que já existe para
|
|
74
|
+
// isso. Não é execução: é a fronteira seguinte, e ela é decisão do Dev.
|
|
75
|
+
// ⚠️ `ehChave` SAIU EM 2026-09-07, pelo mesmo motivo e no mesmo passo que a `JUMP_BASE`: zero consumidores
|
|
76
|
+
// em lado nenhum — engine, testes e cartucho. Os outros predicados de tile ficam por enquanto porque o
|
|
77
|
+
// `game-platformer` os importa; estes dois não eram fronteira mal cortada, eram peso morto.
|
|
61
78
|
export const TILE_COLOR = {
|
|
62
79
|
0: '#0a0a14', 1: '#241f38', 2: '#6b6480', 3: '#2f6fae', 4: '#8a5a2b', 5: '#34e29b', 6: '#3a3a46',
|
|
63
80
|
7: '#7fdcff', 8: '#ffd23f', 9: '#ff5b3a', 10: '#9a8a6f', 11: '#ffe06a', 12: '#3a86ff', 13: '#8a5cff', 14: '#ff6fae',
|
|
@@ -174,6 +174,45 @@ export interface GameDeclaration {
|
|
|
174
174
|
* o esquecimento passar como se fosse escolha.
|
|
175
175
|
*/
|
|
176
176
|
world(): WorldScope;
|
|
177
|
+
/**
|
|
178
|
+
* QUANTAS POSIÇÕES ESTE JOGO PRECISA DE SEGURAR AO MESMO TEMPO. Correr + andar + pular são TRÊS; um quiz é
|
|
179
|
+
* UM. (ADR-0104 §A.)
|
|
180
|
+
*
|
|
181
|
+
* ⚠️ É UM EIXO DIFERENTE DE «QUANTAS AÇÕES», e é por não serem o mesmo que existia um ponto cego onde o
|
|
182
|
+
* aviso nunca disparava. Medido: a plataforma declara NOVE ações e o controle de tela tem NOVE lugares,
|
|
183
|
+
* então o `alcance().ok` era verdadeiro e o cartão da #112 nunca aparecia — mas correr, andar e pular ao
|
|
184
|
+
* mesmo tempo são três dedos, e num telemóvel de dois a criança simplesmente não consegue, sem nada em
|
|
185
|
+
* lado nenhum a dizer porquê. Alcançar uma ação e segurá-la junto com outra são perguntas distintas.
|
|
186
|
+
*
|
|
187
|
+
* ⚠️ OBRIGATÓRIO, e a obrigatoriedade É a decisão, na frase do Dev: «os 300 jogos precisam declarar sim!
|
|
188
|
+
* Não declarar é ter a acessibilidade programada no controle pro sorte». Um campo opcional é respondido
|
|
189
|
+
* por SILÊNCIO, e aqui o silêncio decide pela criança — decide-o quem não pensou no assunto. Trezentos
|
|
190
|
+
* jogos a responder deliberadamente é o custo; acessibilidade por sorte é a alternativa.
|
|
191
|
+
*
|
|
192
|
+
* FUNÇÃO e não valor, pela mesma razão que a `topology`: um jogo com fases troca de exigência entre elas —
|
|
193
|
+
* uma fase a pé pede três, a mesma fase num veículo pode pedir uma. Um valor memorizado ficaria defasado
|
|
194
|
+
* em silêncio, que é o defeito que o ADR-0084 nomeou.
|
|
195
|
+
*/
|
|
196
|
+
holdsAtOnce(): number;
|
|
197
|
+
/**
|
|
198
|
+
* ESTE JOGO PRECISA DE UM PONTEIRO — posição contínua? (ADR-0112.)
|
|
199
|
+
*
|
|
200
|
+
* Um jogo de desenho precisa; um quiz não. Declarar faz um aparelho sem ponteiro RECUSAR-SE antes de a
|
|
201
|
+
* criança começar, em vez de ela descobrir a meio do primeiro traço.
|
|
202
|
+
*
|
|
203
|
+
* ⚠️ OPCIONAL, E A DIFERENÇA PARA O `holdsAtOnce` LOGO ACIMA É DELIBERADA — copiar a obrigatoriedade dele
|
|
204
|
+
* seria aplicar uma regra cuja premissa não se sustenta aqui. O `holdsAtOnce` é obrigatório porque não tem
|
|
205
|
+
* padrão seguro E porque falha INVISIVELMENTE a quem escreve o jogo: ele tem teclado completo, o jogo corre,
|
|
206
|
+
* e quem descobre o defeito é a criança no telemóvel de dois dedos. Este tem padrão seguro (`false`) e falha
|
|
207
|
+
* VISIVELMENTE — um jogo de desenho que se esqueça de o declarar é inoperável no próprio aparelho de quem o
|
|
208
|
+
* escreve, porque ele também precisaria do ponteiro para o experimentar.
|
|
209
|
+
*
|
|
210
|
+
* Obrigar trezentos jogos a escrever `needsPointer: () => false` cobraria o preço do `holdsAtOnce` sem o
|
|
211
|
+
* motivo dele.
|
|
212
|
+
*
|
|
213
|
+
* FUNÇÃO e não valor, pela mesma razão das outras: uma actividade pode desenhar numa fase e não noutra.
|
|
214
|
+
*/
|
|
215
|
+
needsPointer?(): boolean;
|
|
177
216
|
readonly tick: TickOwner;
|
|
178
217
|
/** O papel do que está em `at`. É o campo 2, e é o que substitui `roleOf`. */
|
|
179
218
|
roleAt(at: Spot): Role;
|
|
@@ -108,6 +108,42 @@ export function conformanceProblems(d) {
|
|
|
108
108
|
else if (w.kind !== 'none')
|
|
109
109
|
p.push('world: unknown kind');
|
|
110
110
|
}
|
|
111
|
+
// ⚠️ A MENSAGEM NOMEIA A SAÍDA, como as outras quatro fazem. Um jogo que não declara isto não recebe um
|
|
112
|
+
// padrão — recebe uma frase que diz o que perguntar a si próprio, porque a resposta é do jogo e de mais
|
|
113
|
+
// ninguém. (ADR-0104 §A.)
|
|
114
|
+
if (typeof d.holdsAtOnce !== 'function') {
|
|
115
|
+
// ⚠️ A MENSAGEM NÃO NOMEIA GÊNERO, e o gate de fronteira cobrou-o: a primeira escrita dizia «run+walk+jump
|
|
116
|
+
// is 3, a quiz is 1» e o `engine-boundary` reprovou a palavra «quiz» em linha de CÓDIGO da engine. Ele
|
|
117
|
+
// tinha razão, e a frase ficou melhor: descreve a FORMA da pergunta, que serve aos 300 jogos, em vez de
|
|
118
|
+
// dois exemplos que servem a dois.
|
|
119
|
+
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)');
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
const n = d.holdsAtOnce();
|
|
123
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
124
|
+
// Zero não é «não usa controle»: um jogo que não segura posição nenhuma não é jogável, e devolver zero
|
|
125
|
+
// faria a aritmética do alcance passar por vacuidade — o mesmo defeito que o `reachable` recusa.
|
|
126
|
+
p.push('holdsAtOnce: must be an integer >= 1 - a game that holds nothing cannot be played');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// ⚠️ OPCIONAL, MAS NÃO IMPUNE. Ausente é a resposta `false` e não é problema — ver a nota no campo. O que
|
|
130
|
+
// se recusa é declará-lo MAL: um `needsPointer: true` (valor em vez de função) seria sempre verdadeiro por
|
|
131
|
+
// ser um objecto, e um que devolvesse `'sim'` também. Nos dois casos o jogo julgaria ter declarado, o
|
|
132
|
+
// alcance leria uma coisa diferente do que ele quis dizer, e ninguém saberia — que é o defeito silencioso
|
|
133
|
+
// que esta função inteira existe para não deixar acontecer.
|
|
134
|
+
if (d.needsPointer !== undefined) {
|
|
135
|
+
if (typeof d.needsPointer !== 'function') {
|
|
136
|
+
// ⚠️ MENSAGENS SEM A PALAVRA `as`, e o motivo merece uma linha porque volta a morder: o detector de prosa
|
|
137
|
+
// pt-BR do `engine-i18n` casa palavras funcionais isoladas, e `as` é artigo plural em português. Uma
|
|
138
|
+
// mensagem INGLESA que diga «declare it as …» é contada como texto cru e faz o tecto do módulo subir.
|
|
139
|
+
// O cabeçalho daquele gate já admite a aproximação («senão 'mode' casa 'de'»); reescrever a frase custa
|
|
140
|
+
// nada e afrouxar o detector custaria a razão de ele existir.
|
|
141
|
+
p.push('needsPointer: must be a function - write `needsPointer: () => true`, because a game may draw in one phase and not in another');
|
|
142
|
+
}
|
|
143
|
+
else if (typeof d.needsPointer() !== 'boolean') {
|
|
144
|
+
p.push('needsPointer: must return a boolean - a non-boolean would be truthy and refuse devices this game can actually use');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
111
147
|
if (d.tick !== 'player' && d.tick !== 'clock')
|
|
112
148
|
p.push('tick: must be "player" or "clock"');
|
|
113
149
|
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.
|