@the-inclusionist/engine 6.36.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/LICENSE +661 -0
- package/README.md +107 -0
- package/app/css/style.css +622 -0
- package/app/public/vendor/fonts/andika-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/andika-400.woff2 +0 -0
- package/app/public/vendor/fonts/andika-700-ext.woff2 +0 -0
- package/app/public/vendor/fonts/andika-700.woff2 +0 -0
- package/app/public/vendor/fonts/atkinson-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/atkinson-400.woff2 +0 -0
- package/app/public/vendor/fonts/atkinson-700-ext.woff2 +0 -0
- package/app/public/vendor/fonts/atkinson-700.woff2 +0 -0
- package/app/public/vendor/fonts/atkinson-mono-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/atkinson-mono-400.woff2 +0 -0
- package/app/public/vendor/fonts/comicneue-400.woff2 +0 -0
- package/app/public/vendor/fonts/comicneue-700.woff2 +0 -0
- package/app/public/vendor/fonts/greatvibes-400.woff2 +0 -0
- package/app/public/vendor/fonts/inter-400.woff2 +0 -0
- package/app/public/vendor/fonts/inter-700.woff2 +0 -0
- package/app/public/vendor/fonts/lato-400.woff2 +0 -0
- package/app/public/vendor/fonts/lato-700.woff2 +0 -0
- package/app/public/vendor/fonts/lexend-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/lexend-400.woff2 +0 -0
- package/app/public/vendor/fonts/lexend-700-ext.woff2 +0 -0
- package/app/public/vendor/fonts/lexend-700.woff2 +0 -0
- package/app/public/vendor/fonts/literata-400.woff2 +0 -0
- package/app/public/vendor/fonts/literata-700.woff2 +0 -0
- package/app/public/vendor/fonts/newsreader-400.woff2 +0 -0
- package/app/public/vendor/fonts/newsreader-700.woff2 +0 -0
- package/app/public/vendor/fonts/opensans-400.woff2 +0 -0
- package/app/public/vendor/fonts/opensans-700.woff2 +0 -0
- package/app/public/vendor/fonts/pinyon-400.woff2 +0 -0
- package/app/public/vendor/fonts/quattro-400.woff2 +0 -0
- package/app/public/vendor/fonts/quattro-700.woff2 +0 -0
- package/app/public/vendor/fonts/sourcesans-400.woff2 +0 -0
- package/app/public/vendor/fonts/sourcesans-700.woff2 +0 -0
- package/app/public/vendor/fonts/sourceserif-400.woff2 +0 -0
- package/app/public/vendor/fonts/sourceserif-700.woff2 +0 -0
- package/app/public/vendor/fonts/ufcook-700.woff2 +0 -0
- package/app/public/vendor/fonts/ufmag-400.woff2 +0 -0
- package/app/public/vendor/fonts.css +88 -0
- package/dist-pkg/boot/create-game.d.ts +159 -0
- package/dist-pkg/boot/create-game.js +266 -0
- package/dist-pkg/core/a11y-sr.d.ts +4 -0
- package/dist-pkg/core/a11y-sr.js +20 -0
- package/dist-pkg/core/actions.d.ts +100 -0
- package/dist-pkg/core/actions.js +149 -0
- package/dist-pkg/core/anel.d.ts +17 -0
- package/dist-pkg/core/anel.js +31 -0
- package/dist-pkg/core/collision.d.ts +21 -0
- package/dist-pkg/core/collision.js +62 -0
- package/dist-pkg/core/constants.d.ts +84 -0
- package/dist-pkg/core/constants.js +64 -0
- package/dist-pkg/core/contract.d.ts +227 -0
- package/dist-pkg/core/contract.js +184 -0
- package/dist-pkg/core/dom-query.d.ts +9 -0
- package/dist-pkg/core/dom-query.js +25 -0
- package/dist-pkg/core/entity.d.ts +150 -0
- package/dist-pkg/core/entity.js +38 -0
- package/dist-pkg/core/escape-html.d.ts +16 -0
- package/dist-pkg/core/escape-html.js +32 -0
- package/dist-pkg/core/i18n.d.ts +71 -0
- package/dist-pkg/core/i18n.js +236 -0
- package/dist-pkg/core/layers.d.ts +39 -0
- package/dist-pkg/core/layers.js +71 -0
- package/dist-pkg/core/letter-grid.d.ts +55 -0
- package/dist-pkg/core/letter-grid.js +116 -0
- package/dist-pkg/core/loop.d.ts +17 -0
- package/dist-pkg/core/loop.js +36 -0
- package/dist-pkg/core/password.d.ts +34 -0
- package/dist-pkg/core/password.js +205 -0
- package/dist-pkg/core/rng.d.ts +20 -0
- package/dist-pkg/core/rng.js +61 -0
- package/dist-pkg/core/rotulo-acessivel.d.ts +17 -0
- package/dist-pkg/core/rotulo-acessivel.js +41 -0
- package/dist-pkg/core/run-state.d.ts +115 -0
- package/dist-pkg/core/run-state.js +40 -0
- package/dist-pkg/core/scenes.d.ts +68 -0
- package/dist-pkg/core/scenes.js +64 -0
- package/dist-pkg/core/screens.d.ts +16 -0
- package/dist-pkg/core/screens.js +28 -0
- package/dist-pkg/core/state.d.ts +138 -0
- package/dist-pkg/core/state.js +290 -0
- package/dist-pkg/core/tiles.d.ts +12 -0
- package/dist-pkg/core/tiles.js +61 -0
- package/dist-pkg/core/world.d.ts +4 -0
- package/dist-pkg/core/world.js +58 -0
- package/dist-pkg/educational/activities-registry.d.ts +47 -0
- package/dist-pkg/educational/activities-registry.js +95 -0
- package/dist-pkg/i18n/en.d.ts +3 -0
- package/dist-pkg/i18n/en.js +560 -0
- package/dist-pkg/i18n/es.d.ts +3 -0
- package/dist-pkg/i18n/es.js +558 -0
- package/dist-pkg/i18n/pt.d.ts +3 -0
- package/dist-pkg/i18n/pt.js +618 -0
- package/dist-pkg/input/default-bindings.d.ts +24 -0
- package/dist-pkg/input/default-bindings.js +122 -0
- package/dist-pkg/input/devices.d.ts +15 -0
- package/dist-pkg/input/devices.js +31 -0
- package/dist-pkg/input/edges.d.ts +58 -0
- package/dist-pkg/input/edges.js +49 -0
- package/dist-pkg/input/gamepad.d.ts +203 -0
- package/dist-pkg/input/gamepad.js +531 -0
- package/dist-pkg/input/keyboard-runtime.d.ts +61 -0
- package/dist-pkg/input/keyboard-runtime.js +83 -0
- package/dist-pkg/input/keyboard.d.ts +45 -0
- package/dist-pkg/input/keyboard.js +80 -0
- package/dist-pkg/input/keydown.d.ts +270 -0
- package/dist-pkg/input/keydown.js +381 -0
- package/dist-pkg/input/latch.d.ts +30 -0
- package/dist-pkg/input/latch.js +63 -0
- package/dist-pkg/input/pointer-space.d.ts +40 -0
- package/dist-pkg/input/pointer-space.js +51 -0
- package/dist-pkg/input/state.d.ts +11 -0
- package/dist-pkg/input/state.js +12 -0
- package/dist-pkg/input/touch-bindings.d.ts +210 -0
- package/dist-pkg/input/touch-bindings.js +365 -0
- package/dist-pkg/input/touch.d.ts +173 -0
- package/dist-pkg/input/touch.js +314 -0
- package/dist-pkg/input/transports.d.ts +90 -0
- package/dist-pkg/input/transports.js +96 -0
- package/dist-pkg/input/vocabulary-migration.d.ts +58 -0
- package/dist-pkg/input/vocabulary-migration.js +125 -0
- package/dist-pkg/platform/audio-ambient.d.ts +27 -0
- package/dist-pkg/platform/audio-ambient.js +89 -0
- package/dist-pkg/platform/audio-earcons.d.ts +24 -0
- package/dist-pkg/platform/audio-earcons.js +62 -0
- package/dist-pkg/platform/audio-jingles.d.ts +17 -0
- package/dist-pkg/platform/audio-jingles.js +54 -0
- package/dist-pkg/platform/audio-mixer.d.ts +14 -0
- package/dist-pkg/platform/audio-mixer.js +62 -0
- package/dist-pkg/platform/audio-nav.d.ts +51 -0
- package/dist-pkg/platform/audio-nav.js +99 -0
- package/dist-pkg/platform/audio-sonar.d.ts +71 -0
- package/dist-pkg/platform/audio-sonar.js +173 -0
- package/dist-pkg/platform/audio.d.ts +32 -0
- package/dist-pkg/platform/audio.js +191 -0
- package/dist-pkg/platform/interruptible-speech.d.ts +19 -0
- package/dist-pkg/platform/interruptible-speech.js +79 -0
- package/dist-pkg/platform/speech.d.ts +2 -0
- package/dist-pkg/platform/speech.js +35 -0
- package/dist-pkg/platform/storage.d.ts +87 -0
- package/dist-pkg/platform/storage.js +148 -0
- package/dist-pkg/platform/tts.d.ts +33 -0
- package/dist-pkg/platform/tts.js +142 -0
- package/dist-pkg/render/camera.d.ts +58 -0
- package/dist-pkg/render/camera.js +105 -0
- package/dist-pkg/render/canvas.d.ts +13 -0
- package/dist-pkg/render/canvas.js +26 -0
- package/dist-pkg/render/cenario-data.d.ts +127 -0
- package/dist-pkg/render/cenario-data.js +145 -0
- package/dist-pkg/render/city-tex.d.ts +63 -0
- package/dist-pkg/render/city-tex.js +228 -0
- package/dist-pkg/render/city-tiles.d.ts +11 -0
- package/dist-pkg/render/city-tiles.js +140 -0
- package/dist-pkg/render/crt.d.ts +19 -0
- package/dist-pkg/render/crt.js +92 -0
- package/dist-pkg/render/cvd-matrices.d.ts +35 -0
- package/dist-pkg/render/cvd-matrices.js +91 -0
- package/dist-pkg/render/draw.d.ts +149 -0
- package/dist-pkg/render/draw.js +221 -0
- package/dist-pkg/render/fx.d.ts +63 -0
- package/dist-pkg/render/fx.js +123 -0
- package/dist-pkg/render/hc-role-data.d.ts +14 -0
- package/dist-pkg/render/hc-role-data.js +24 -0
- package/dist-pkg/render/high-contrast.d.ts +109 -0
- package/dist-pkg/render/high-contrast.js +254 -0
- package/dist-pkg/render/lq-filter.d.ts +37 -0
- package/dist-pkg/render/lq-filter.js +94 -0
- package/dist-pkg/render/minimap.d.ts +11 -0
- package/dist-pkg/render/minimap.js +94 -0
- package/dist-pkg/render/parallax.d.ts +85 -0
- package/dist-pkg/render/parallax.js +158 -0
- package/dist-pkg/render/player-anim.d.ts +71 -0
- package/dist-pkg/render/player-anim.js +152 -0
- package/dist-pkg/render/port.d.ts +143 -0
- package/dist-pkg/render/port.js +29 -0
- package/dist-pkg/render/recycling-tex.d.ts +48 -0
- package/dist-pkg/render/recycling-tex.js +164 -0
- package/dist-pkg/render/scene-city.d.ts +77 -0
- package/dist-pkg/render/scene-city.js +181 -0
- package/dist-pkg/render/scene-parallax.d.ts +72 -0
- package/dist-pkg/render/scene-parallax.js +426 -0
- package/dist-pkg/render/scene-sky.d.ts +126 -0
- package/dist-pkg/render/scene-sky.js +295 -0
- package/dist-pkg/render/screen-pipeline.d.ts +134 -0
- package/dist-pkg/render/screen-pipeline.js +177 -0
- package/dist-pkg/render/set-cenario.d.ts +54 -0
- package/dist-pkg/render/set-cenario.js +103 -0
- package/dist-pkg/render/sprite-fx.d.ts +3 -0
- package/dist-pkg/render/sprite-fx.js +40 -0
- package/dist-pkg/render/textures.d.ts +75 -0
- package/dist-pkg/render/textures.js +240 -0
- package/dist-pkg/render/title-scene.d.ts +46 -0
- package/dist-pkg/render/title-scene.js +75 -0
- package/dist-pkg/render/viewports.d.ts +52 -0
- package/dist-pkg/render/viewports.js +185 -0
- package/dist-pkg/render/viz-axes.d.ts +98 -0
- package/dist-pkg/render/viz-axes.js +184 -0
- package/dist-pkg/render/viz-modes.d.ts +61 -0
- package/dist-pkg/render/viz-modes.js +74 -0
- package/dist-pkg/render/viz-setters.d.ts +130 -0
- package/dist-pkg/render/viz-setters.js +244 -0
- package/dist-pkg/render/weather.d.ts +96 -0
- package/dist-pkg/render/weather.js +171 -0
- package/dist-pkg/render/wheelchair-sprites.d.ts +24 -0
- package/dist-pkg/render/wheelchair-sprites.js +63 -0
- package/dist-pkg/render/world-tex.d.ts +18 -0
- package/dist-pkg/render/world-tex.js +87 -0
- package/dist-pkg/ui/activities-menu.d.ts +258 -0
- package/dist-pkg/ui/activities-menu.js +648 -0
- package/dist-pkg/ui/caa-sets.d.ts +39 -0
- package/dist-pkg/ui/caa-sets.js +65 -0
- package/dist-pkg/ui/changed-mark.d.ts +15 -0
- package/dist-pkg/ui/changed-mark.js +96 -0
- package/dist-pkg/ui/debug-panel.d.ts +67 -0
- package/dist-pkg/ui/debug-panel.js +173 -0
- package/dist-pkg/ui/dom.d.ts +24 -0
- package/dist-pkg/ui/dom.js +47 -0
- package/dist-pkg/ui/focus-trap.d.ts +71 -0
- package/dist-pkg/ui/focus-trap.js +99 -0
- package/dist-pkg/ui/fonts.d.ts +41 -0
- package/dist-pkg/ui/fonts.js +59 -0
- package/dist-pkg/ui/hud.d.ts +137 -0
- package/dist-pkg/ui/hud.js +229 -0
- package/dist-pkg/ui/item-announcement.d.ts +19 -0
- package/dist-pkg/ui/item-announcement.js +36 -0
- package/dist-pkg/ui/layout.d.ts +6 -0
- package/dist-pkg/ui/layout.js +54 -0
- package/dist-pkg/ui/loop-crash.d.ts +19 -0
- package/dist-pkg/ui/loop-crash.js +60 -0
- package/dist-pkg/ui/map-hub.d.ts +56 -0
- package/dist-pkg/ui/map-hub.js +138 -0
- package/dist-pkg/ui/menu-nav.d.ts +141 -0
- package/dist-pkg/ui/menu-nav.js +390 -0
- package/dist-pkg/ui/pause-icons.d.ts +327 -0
- package/dist-pkg/ui/pause-icons.js +620 -0
- package/dist-pkg/ui/reach-notice.d.ts +34 -0
- package/dist-pkg/ui/reach-notice.js +71 -0
- package/dist-pkg/ui/settings-audio.d.ts +139 -0
- package/dist-pkg/ui/settings-audio.js +544 -0
- package/dist-pkg/ui/settings-caa.d.ts +45 -0
- package/dist-pkg/ui/settings-caa.js +137 -0
- package/dist-pkg/ui/settings-controls.d.ts +100 -0
- package/dist-pkg/ui/settings-controls.js +152 -0
- package/dist-pkg/ui/settings-empathy.d.ts +62 -0
- package/dist-pkg/ui/settings-empathy.js +137 -0
- package/dist-pkg/ui/settings-motion.d.ts +95 -0
- package/dist-pkg/ui/settings-motion.js +282 -0
- package/dist-pkg/ui/settings-motor.d.ts +77 -0
- package/dist-pkg/ui/settings-motor.js +215 -0
- package/dist-pkg/ui/settings-panel.d.ts +76 -0
- package/dist-pkg/ui/settings-panel.js +187 -0
- package/dist-pkg/ui/settings-typo.d.ts +74 -0
- package/dist-pkg/ui/settings-typo.js +150 -0
- package/dist-pkg/ui/settings-visual.d.ts +111 -0
- package/dist-pkg/ui/settings-visual.js +252 -0
- package/dist-pkg/ui/shell.d.ts +239 -0
- package/dist-pkg/ui/shell.js +430 -0
- package/dist-pkg/ui/title.d.ts +36 -0
- package/dist-pkg/ui/title.js +60 -0
- package/dist-pkg/ui/vlibras.d.ts +11 -0
- package/dist-pkg/ui/vlibras.js +87 -0
- package/dist-pkg/ui/webcam.d.ts +7 -0
- package/dist-pkg/ui/webcam.js +88 -0
- package/docs/CREDITS.md +64 -0
- package/docs/LICENSES.md +132 -0
- package/package.json +115 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// core/escape-html.ts — ESCAPAR TEXTO QUE VAI PARAR EM MARCAÇÃO. Módulo-folha: uma função, zero dependências.
|
|
3
|
+
//
|
|
4
|
+
// ⚠️ NASCEU DENTRO DE `game/quiz.ts` E TEVE DE SAIR NO DIA SEGUINTE, o que vale registrar porque a razão não
|
|
5
|
+
// é arrumação: `game/` é o CARTUCHO, e a issue #111 leva-o para um repositório próprio. O ajudante sairia com
|
|
6
|
+
// ele, e o `consumer-quiz` — que é prova da ENGINE — nem sequer pode importar de `game/`, porque o
|
|
7
|
+
// `engine-boundary` o proíbe. Um utilitário de segurança que vive do lado errado da fronteira é um utilitário
|
|
8
|
+
// que o próximo consumidor reescreve, e duas versões de um escape divergem em silêncio.
|
|
9
|
+
//
|
|
10
|
+
// ⚠️ E O ESCAPE É O SEGUNDO RECURSO, NÃO O PRIMEIRO. Onde o valor cabe num nó — `textContent`, `setAttribute`
|
|
11
|
+
// —, é assim que ele entra, porque aí não há o que esquecer. Isto existe para o caso em que o texto está
|
|
12
|
+
// dentro de um construtor denso de marcação, e nesse caso vem sempre acompanhado de um gate com conteúdo
|
|
13
|
+
// hostil: um escape sem rasto é pior do que construir nós.
|
|
14
|
+
/**
|
|
15
|
+
* Os CINCO caracteres, e cobre os dois contextos: elemento (`<` `>`) e ATRIBUTO (`"` `'`).
|
|
16
|
+
*
|
|
17
|
+
* O atributo é o que costuma faltar, e é o pior: dentro de um elemento uma aspa é inofensiva; dentro de
|
|
18
|
+
* `aria-label="…"` ela FECHA o atributo e o que vem a seguir vira atributo — um `onmouseover` sem precisar de
|
|
19
|
+
* uma única tag.
|
|
20
|
+
*
|
|
21
|
+
* ⚠️ O `&` É O PRIMEIRO, E A ORDEM É O DEFEITO CLÁSSICO DESTE AJUDANTE. Escapando-o por último, o `&` do
|
|
22
|
+
* `<` que acabou de ser produzido é escapado outra vez e a tela mostra `<` literal. É silencioso porque
|
|
23
|
+
* quem testa só com `<b>` nunca o vê: a saída ainda «parece» escapada.
|
|
24
|
+
*
|
|
25
|
+
* E ele escapa, não apaga. Apagar mudaria a palavra que a criança digitou, e uma atividade cujo texto muda
|
|
26
|
+
* sozinho é um defeito diferente e igualmente sério.
|
|
27
|
+
*/
|
|
28
|
+
export function escaparHtml(s) {
|
|
29
|
+
return String(s)
|
|
30
|
+
.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>')
|
|
31
|
+
.replaceAll('"', '"').replaceAll("'", ''');
|
|
32
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
type LocaleDict = Record<string, string>;
|
|
3
|
+
/**
|
|
4
|
+
* Registra as chaves DESTE jogo para um idioma. Chamável antes de o idioma existir — quem registra `en` antes
|
|
5
|
+
* de qualquer `setLocale('en')` é atendido quando a troca acontecer.
|
|
6
|
+
*
|
|
7
|
+
* ⚠️ NÃO REAPLICA O DOM, de propósito. `applyDom` precisa de uma RAIZ, e alcançar o `document` global por
|
|
8
|
+
* baixo de quem chama é o achado 15, que já custou uma correção. Um consumidor que registre depois de o
|
|
9
|
+
* markup estático ter sido traduzido chama `applyDom(raiz)` ele mesmo — e o caso normal é registrar no boot,
|
|
10
|
+
* antes de existir texto na tela.
|
|
11
|
+
*/
|
|
12
|
+
export declare function registerDict(code: string, entries: LocaleDict): string[];
|
|
13
|
+
export declare function t(key: string, params?: Record<string, string | number>): string;
|
|
14
|
+
export declare function getLocale(): string;
|
|
15
|
+
/**
|
|
16
|
+
* A etiqueta BCP-47 do idioma corrente — o que se escreve em `<html lang>` e o que se entrega a APIs do
|
|
17
|
+
* navegador que falam (Web Speech) ou comparam idioma.
|
|
18
|
+
*
|
|
19
|
+
* Só o português precisa de região: 'pt' sozinho deixaria o navegador escolher entre pt-PT e pt-BR, e a
|
|
20
|
+
* diferença de prosódia é audível para uma criança brasileira. Inglês e espanhol ficam sem região de
|
|
21
|
+
* propósito — o navegador escolhe a variante local, que é o certo, e fixar 'en-US' imporia sotaque americano
|
|
22
|
+
* a quem estivesse na Índia ou na Nigéria.
|
|
23
|
+
*
|
|
24
|
+
* `setLocale` calculava isto em linha, ao escrever `<html lang>`. Agora os dois leem daqui: uma regra, dois
|
|
25
|
+
* consumidores — que é exatamente a forma que este projeto já viu divergir quatro vezes.
|
|
26
|
+
*/
|
|
27
|
+
export declare function bcp47(code?: string): string;
|
|
28
|
+
export declare function availableLocales(): string[];
|
|
29
|
+
export declare function applyDom(root?: ParentNode): void;
|
|
30
|
+
export declare function setLocale(code: string): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Boot: aplica pt (síncrono, para a página nunca ficar em branco) e, se o idioma preferido for outro, PEDE a
|
|
33
|
+
* troca — que é assíncrona, porque os outros locales são chunks sob demanda.
|
|
34
|
+
*
|
|
35
|
+
* Continua devolvendo o locale de forma síncrona e NÃO bloqueia por si: quem precisar esperar chama
|
|
36
|
+
* `idiomaPronto()`. Foi essa separação que faltava — ver o comentário lá embaixo.
|
|
37
|
+
*
|
|
38
|
+
* `root` ENTRA em vez de ser lido do global, e foi o `boot/createGame()` que cobrou (item 13): a raiz de
|
|
39
|
+
* composição recebe o documento do hospedeiro por injeção e não tinha como repassá-lo — esta linha alcançava
|
|
40
|
+
* o `document` global por baixo dela. Num navegador dá no mesmo; no project `node` é a diferença entre
|
|
41
|
+
* bootar contra um DOM de mentira e não bootar. O padrão continua sendo o global, então nenhum chamador
|
|
42
|
+
* muda: é a mesma regra do `applyDom` logo acima.
|
|
43
|
+
*/
|
|
44
|
+
export declare function initI18n(root?: ParentNode): string;
|
|
45
|
+
/**
|
|
46
|
+
* Resolve quando o idioma escolhido no boot terminou de carregar (na hora, se for pt).
|
|
47
|
+
*
|
|
48
|
+
* ========================= POR QUE ISTO PRECISOU EXISTIR =========================
|
|
49
|
+
* O `initI18n()` do main.js era a ÚLTIMA linha do boot, depois de o HUD, os menus de título e as telas de
|
|
50
|
+
* pausa já estarem montados. Para pt isso não custava nada — já estava tudo em português. Para en/es, custava
|
|
51
|
+
* metade da interface: o `applyDom` conserta o markup ESTÁTICO (`data-i18n`), mas o que o JavaScript monta
|
|
52
|
+
* (os botões de cenário, os de atividade) tinha capturado o texto de pt e ninguém reconstruía.
|
|
53
|
+
*
|
|
54
|
+
* O sintoma era desconcertante: `t('cen.cidade')` devolvia "City" e o botão na tela dizia "Cidade".
|
|
55
|
+
*
|
|
56
|
+
* Isto NÃO responde à pergunta maior — QUANDO a interface se reconstrói ao trocar de idioma EM EXECUÇÃO —,
|
|
57
|
+
* que segue com o Dev. Responde à menor, que não tem duas respostas: a interface não se constrói antes de o
|
|
58
|
+
* idioma ser conhecido.
|
|
59
|
+
*/
|
|
60
|
+
export declare function idiomaPronto(): Promise<void>;
|
|
61
|
+
declare const i18n: {
|
|
62
|
+
t: typeof t;
|
|
63
|
+
getLocale: typeof getLocale;
|
|
64
|
+
availableLocales: typeof availableLocales;
|
|
65
|
+
applyDom: typeof applyDom;
|
|
66
|
+
setLocale: typeof setLocale;
|
|
67
|
+
initI18n: typeof initI18n;
|
|
68
|
+
idiomaPronto: typeof idiomaPronto;
|
|
69
|
+
registerDict: typeof registerDict;
|
|
70
|
+
};
|
|
71
|
+
export default i18n;
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// i18n — internacionalização (ver docs/plano-i18n.md).
|
|
3
|
+
// O idioma padrão (pt) é import ESTÁTICO → dicionário pronto antes do game.js rodar (boot síncrono, sem
|
|
4
|
+
// refatorar o init para async). Os demais entram sob demanda ao trocar de idioma, via import.meta.glob (o
|
|
5
|
+
// Vite gera um chunk por locale e o SW cacheia). import.meta.glob (em vez de import(`…${code}.ts`) cru) é o
|
|
6
|
+
// jeito nativo do Vite: casa arquivos .ts no build de forma explícita, sem depender do glob "adivinhado".
|
|
7
|
+
import pt from '../i18n/pt.js';
|
|
8
|
+
import * as store from '../platform/storage.js';
|
|
9
|
+
const AVAILABLE = ['pt', 'en', 'es'];
|
|
10
|
+
const base = pt; // dicionário-base (fallback), tipado
|
|
11
|
+
const DICTS = { pt: base }; // dicionários já carregados (pt embutido)
|
|
12
|
+
const STORE_KEY = store.KEYS.lang;
|
|
13
|
+
/* ===================== O DICIONÁRIO DE QUEM CONSOME A ENGINE =====================
|
|
14
|
+
*
|
|
15
|
+
* O achado 2 do `consumer-quiz` dizia que os dicionários são do jogo de plataforma e que um segundo jogo
|
|
16
|
+
* herda 253 chaves para usar um punhado — "peso morto no pacote". Estava certo para um consumidor que mora
|
|
17
|
+
* DENTRO deste repositório, porque as chaves dele cabem em `../i18n/pt.ts`.
|
|
18
|
+
*
|
|
19
|
+
* ⚠️ DE FORA, O MESMO ACHADO DEIXA DE SER PESO E VIRA PAREDE. Os locales entram pelo `import.meta.glob` logo
|
|
20
|
+
* abaixo, e esse glob é resolvido NO BUILD DESTA ENGINE, contra ESTA pasta. Um jogo que instala
|
|
21
|
+
* `@the-inclusionist/engine` não tem como pôr arquivo lá dentro, e `DICTS` é privado. Sem esta camada ele
|
|
22
|
+
* fica sem NENHUM caminho para as próprias chaves — e o pilar 3 não abre exceção para consumidor.
|
|
23
|
+
*
|
|
24
|
+
* CAMADA SEPARADA, e não `DICTS[code] = {...DICTS[code], ...extra}`: `DICTS.pt` É o objeto importado de
|
|
25
|
+
* `../i18n/pt.ts`. Mesclar ali mutaria o dicionário da própria engine, e dois jogos na mesma página herdariam
|
|
26
|
+
* as strings um do outro. `tests/i18n-consumer-dict.node.test.js` prende exatamente isso.
|
|
27
|
+
*/
|
|
28
|
+
const EXTRA = {};
|
|
29
|
+
/**
|
|
30
|
+
* Registra as chaves DESTE jogo para um idioma. Chamável antes de o idioma existir — quem registra `en` antes
|
|
31
|
+
* de qualquer `setLocale('en')` é atendido quando a troca acontecer.
|
|
32
|
+
*
|
|
33
|
+
* ⚠️ NÃO REAPLICA O DOM, de propósito. `applyDom` precisa de uma RAIZ, e alcançar o `document` global por
|
|
34
|
+
* baixo de quem chama é o achado 15, que já custou uma correção. Um consumidor que registre depois de o
|
|
35
|
+
* markup estático ter sido traduzido chama `applyDom(raiz)` ele mesmo — e o caso normal é registrar no boot,
|
|
36
|
+
* antes de existir texto na tela.
|
|
37
|
+
*/
|
|
38
|
+
export function registerDict(code, entries) {
|
|
39
|
+
const recusadas = [];
|
|
40
|
+
const aceites = {};
|
|
41
|
+
for (const chave in entries) {
|
|
42
|
+
if (temMarcacao(entries[chave]))
|
|
43
|
+
recusadas.push(chave);
|
|
44
|
+
else
|
|
45
|
+
aceites[chave] = entries[chave];
|
|
46
|
+
}
|
|
47
|
+
if (recusadas.length) {
|
|
48
|
+
// Alto, e não em silêncio: quem escreveu a string tem de saber que ela não entrou. Descartar calado
|
|
49
|
+
// faria a chave crua aparecer na tela sem nada explicando, e isso lê-se como defeito da engine.
|
|
50
|
+
try {
|
|
51
|
+
console.error('[inclusionist] i18n: chaves recusadas por conterem marcação — ' + recusadas.join(', '));
|
|
52
|
+
}
|
|
53
|
+
catch { /* noop */ }
|
|
54
|
+
}
|
|
55
|
+
EXTRA[code] = { ...EXTRA[code], ...aceites };
|
|
56
|
+
return recusadas;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A string traz marcação?
|
|
60
|
+
*
|
|
61
|
+
* ⚠️ POR QUE ISTO EXISTE AQUI, E NÃO NOS ~15 SINKS QUE CONSOMEM i18n. O gate
|
|
62
|
+
* `tests/i18n-sem-markup.node.test.js` varre os dicionários DESTA árvore e prova que nenhuma entrada tem
|
|
63
|
+
* tag. Ele não alcança — e não tem como alcançar — o `EXTRA`: são strings que um JOGO regista em tempo de
|
|
64
|
+
* execução, de outro repositório (ADR-0083), e um teste desta árvore não as vê.
|
|
65
|
+
*
|
|
66
|
+
* ⚠️ E ELAS GANHAM DO DICIONÁRIO DA ENGINE. `resolver` consulta `EXTRA` primeiro, então um jogo pode sobrepor
|
|
67
|
+
* QUALQUER chave — inclusive as que a engine cola em markup. Sem este cheque, «i18n» tinha deixado de
|
|
68
|
+
* significar «texto que alguém desta árvore reviu», e nada registava a mudança.
|
|
69
|
+
*
|
|
70
|
+
* A verificação vai na FRONTEIRA e não nos sinks porque a fronteira é UMA: toda string de um jogo passa por
|
|
71
|
+
* aqui. Quinze sinks seriam quinze lugares para esquecer, e o esquecimento não deixa rasto.
|
|
72
|
+
*
|
|
73
|
+
* O crivo é deliberadamente grosseiro — `<` seguido de letra ou de barra, e `&` de entidade. Ele recusa
|
|
74
|
+
* `a < b` escrito com espaço? Não: `< ` não casa. Recusa «5<10»? Não, o dígito não casa. O que ele recusa é
|
|
75
|
+
* o que se parece com uma tag, e uma frase de interface que precise disso precisa de outra frase.
|
|
76
|
+
*/
|
|
77
|
+
function temMarcacao(valor) {
|
|
78
|
+
return typeof valor === 'string' && (/<[a-zA-Z/!?]/.test(valor) || /&[a-zA-Z#][a-zA-Z0-9]*;/.test(valor));
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* A cadeia de resolução, em cinco degraus. Ela ESPELHA a que já existia (`locale → pt → a própria chave`),
|
|
82
|
+
* com o consumidor colado a cada degrau em vez de empilhado por cima:
|
|
83
|
+
*
|
|
84
|
+
* 1. consumidor no idioma corrente · 2. engine no idioma corrente ·
|
|
85
|
+
* 3. consumidor em pt · 4. engine em pt · 5. a própria chave
|
|
86
|
+
*
|
|
87
|
+
* O degrau 3 é o que faz um jogo que só escreveu pt seguir LEGÍVEL quando a criança troca para inglês: ela lê
|
|
88
|
+
* português, exatamente como já lê hoje quando falta chave na engine. Degradar é melhor que calar, e é a
|
|
89
|
+
* mesma escolha que o `.catch` mudo do `initI18n` já faz para o chunk que não carrega.
|
|
90
|
+
*
|
|
91
|
+
* O degrau 2 vir ANTES do 3 é a única ordem defensável: idioma certo da engine vale mais que idioma errado do
|
|
92
|
+
* consumidor. A inversão daria "Potência de 2" numa interface em espanhol que tinha a tradução na mão.
|
|
93
|
+
*/
|
|
94
|
+
function resolver(key) {
|
|
95
|
+
const doJogo = EXTRA[locale];
|
|
96
|
+
if (doJogo && key in doJogo)
|
|
97
|
+
return doJogo[key];
|
|
98
|
+
if (key in dict)
|
|
99
|
+
return dict[key];
|
|
100
|
+
const doJogoEmPt = EXTRA.pt;
|
|
101
|
+
if (doJogoEmPt && key in doJogoEmPt)
|
|
102
|
+
return doJogoEmPt[key];
|
|
103
|
+
return key in base ? base[key] : key;
|
|
104
|
+
}
|
|
105
|
+
/* ===================== OS CARREGADORES, E POR QUE DEIXARAM DE SER UM GLOB =====================
|
|
106
|
+
*
|
|
107
|
+
* Isto era `import.meta.glob<{default: LocaleDict}>('../i18n/*.ts')`, e a razão escrita no topo do arquivo
|
|
108
|
+
* continua verdadeira para este repositório: o Vite gera um chunk por locale e o service worker o cacheia.
|
|
109
|
+
*
|
|
110
|
+
* ⚠️ O QUE MUDOU FOI O DESTINO DO ARQUIVO, NÃO O ARGUMENTO. Medido em 2026-09-05 no primeiro build de
|
|
111
|
+
* pacote (`tsc -p tsconfig.pkg.json`): a linha SOBREVIVE ao emit, intacta, em `dist-pkg/core/i18n.js` —
|
|
112
|
+
* e ali ela é falsa em dois níveis ao mesmo tempo.
|
|
113
|
+
* · O `tsc` não é o Vite: ele copia `import.meta.glob(...)` como chamada comum. Num consumidor que não
|
|
114
|
+
* transforme o módulo, `import.meta.glob` é `undefined` e a chamada estoura no carregamento.
|
|
115
|
+
* · E mesmo transformado, o padrão diz `*.ts` — ao lado do arquivo EMITIDO só existem `.js`. O glob casaria
|
|
116
|
+
* zero arquivos, `ensure()` cairia no `return base`, e todo idioma que não fosse pt viraria português
|
|
117
|
+
* SEM ERRO NENHUM. É o modo de falhar que este projeto já pagou uma vez, quando uma regex morreu em
|
|
118
|
+
* silêncio com a checagem verde.
|
|
119
|
+
*
|
|
120
|
+
* ENUMERADO, ENTÃO — e o preço é MENOR do que eu escrevi antes de medir. O glob varria três arquivos que a
|
|
121
|
+
* constante `AVAILABLE` logo acima JÁ ENUMERA, então não havia descoberta nenhuma a preservar.
|
|
122
|
+
*
|
|
123
|
+
* ⚠️ E O CODE-SPLITTING NÃO SE PERDE, o que eu tinha suposto que se perderia. O que o Vite divide é o
|
|
124
|
+
* `import()` DINÂMICO, e ele continua aqui — a primeira versão deste comentário dizia que o custo eram
|
|
125
|
+
* "~63 KB de parse a mais", e o build de 2026-09-05 mostrou o contrário na saída: `dist/assets/en-*.js`
|
|
126
|
+
* (24,8 KB) e `dist/assets/es-*.js` (26,7 KB) seguem como chunks próprios, exatamente como com o glob. Quem
|
|
127
|
+
* fica em pt nunca os avalia. O custo real é UM: três linhas a manter à mão no dia em que entrar um quarto
|
|
128
|
+
* idioma — e `AVAILABLE` já era essa lista, então é o mesmo dia e o mesmo arquivo. */
|
|
129
|
+
const loaders = {
|
|
130
|
+
en: () => import('../i18n/en.js'),
|
|
131
|
+
es: () => import('../i18n/es.js'),
|
|
132
|
+
};
|
|
133
|
+
let locale = 'pt';
|
|
134
|
+
let dict = base;
|
|
135
|
+
// Traduz uma chave; a cadeia de fallback está em `resolver()` logo acima. Interpola {param}.
|
|
136
|
+
export function t(key, params) {
|
|
137
|
+
let s = resolver(key);
|
|
138
|
+
if (params)
|
|
139
|
+
for (const k in params)
|
|
140
|
+
s = s.replaceAll('{' + k + '}', String(params[k]));
|
|
141
|
+
return s;
|
|
142
|
+
}
|
|
143
|
+
export function getLocale() { return locale; }
|
|
144
|
+
/**
|
|
145
|
+
* A etiqueta BCP-47 do idioma corrente — o que se escreve em `<html lang>` e o que se entrega a APIs do
|
|
146
|
+
* navegador que falam (Web Speech) ou comparam idioma.
|
|
147
|
+
*
|
|
148
|
+
* Só o português precisa de região: 'pt' sozinho deixaria o navegador escolher entre pt-PT e pt-BR, e a
|
|
149
|
+
* diferença de prosódia é audível para uma criança brasileira. Inglês e espanhol ficam sem região de
|
|
150
|
+
* propósito — o navegador escolhe a variante local, que é o certo, e fixar 'en-US' imporia sotaque americano
|
|
151
|
+
* a quem estivesse na Índia ou na Nigéria.
|
|
152
|
+
*
|
|
153
|
+
* `setLocale` calculava isto em linha, ao escrever `<html lang>`. Agora os dois leem daqui: uma regra, dois
|
|
154
|
+
* consumidores — que é exatamente a forma que este projeto já viu divergir quatro vezes.
|
|
155
|
+
*/
|
|
156
|
+
export function bcp47(code = locale) { return code === 'pt' ? 'pt-BR' : code; }
|
|
157
|
+
export function availableLocales() { return AVAILABLE.slice(); }
|
|
158
|
+
// Aplica as traduções declarativas do HTML: [data-i18n] → textContent; [data-i18n-aria] → aria-label.
|
|
159
|
+
export function applyDom(root = document) {
|
|
160
|
+
root.querySelectorAll('[data-i18n]').forEach((el) => { const k = el.getAttribute('data-i18n'); if (k)
|
|
161
|
+
el.textContent = t(k); });
|
|
162
|
+
root.querySelectorAll('[data-i18n-aria]').forEach((el) => { const k = el.getAttribute('data-i18n-aria'); if (k)
|
|
163
|
+
el.setAttribute('aria-label', t(k)); });
|
|
164
|
+
}
|
|
165
|
+
async function ensure(code) {
|
|
166
|
+
if (DICTS[code])
|
|
167
|
+
return DICTS[code];
|
|
168
|
+
const load = loaders[code];
|
|
169
|
+
if (!load)
|
|
170
|
+
return base; // idioma sem arquivo → cai no pt
|
|
171
|
+
const mod = await load();
|
|
172
|
+
DICTS[code] = mod.default;
|
|
173
|
+
return DICTS[code];
|
|
174
|
+
}
|
|
175
|
+
// Troca o idioma (carrega sob demanda), persiste, atualiza <html lang>, reaplica o DOM e avisa a UI.
|
|
176
|
+
export async function setLocale(code) {
|
|
177
|
+
if (!AVAILABLE.includes(code))
|
|
178
|
+
code = 'pt';
|
|
179
|
+
dict = await ensure(code);
|
|
180
|
+
locale = code;
|
|
181
|
+
store.set(STORE_KEY, code);
|
|
182
|
+
document.documentElement.lang = bcp47(code);
|
|
183
|
+
applyDom(document);
|
|
184
|
+
window.dispatchEvent(new CustomEvent('i18n:change', { detail: { locale } }));
|
|
185
|
+
}
|
|
186
|
+
function pickDefault() {
|
|
187
|
+
const saved = store.get(STORE_KEY, null);
|
|
188
|
+
if (saved && AVAILABLE.includes(saved))
|
|
189
|
+
return saved;
|
|
190
|
+
const nav = ((navigator.language || 'pt').slice(0, 2)).toLowerCase();
|
|
191
|
+
return AVAILABLE.includes(nav) ? nav : 'pt';
|
|
192
|
+
}
|
|
193
|
+
/** Promessa do carregamento pedido no boot. Resolve na hora quando o idioma é pt (dicionário estático). */
|
|
194
|
+
let pendente = Promise.resolve();
|
|
195
|
+
/**
|
|
196
|
+
* Boot: aplica pt (síncrono, para a página nunca ficar em branco) e, se o idioma preferido for outro, PEDE a
|
|
197
|
+
* troca — que é assíncrona, porque os outros locales são chunks sob demanda.
|
|
198
|
+
*
|
|
199
|
+
* Continua devolvendo o locale de forma síncrona e NÃO bloqueia por si: quem precisar esperar chama
|
|
200
|
+
* `idiomaPronto()`. Foi essa separação que faltava — ver o comentário lá embaixo.
|
|
201
|
+
*
|
|
202
|
+
* `root` ENTRA em vez de ser lido do global, e foi o `boot/createGame()` que cobrou (item 13): a raiz de
|
|
203
|
+
* composição recebe o documento do hospedeiro por injeção e não tinha como repassá-lo — esta linha alcançava
|
|
204
|
+
* o `document` global por baixo dela. Num navegador dá no mesmo; no project `node` é a diferença entre
|
|
205
|
+
* bootar contra um DOM de mentira e não bootar. O padrão continua sendo o global, então nenhum chamador
|
|
206
|
+
* muda: é a mesma regra do `applyDom` logo acima.
|
|
207
|
+
*/
|
|
208
|
+
export function initI18n(root = document) {
|
|
209
|
+
applyDom(root);
|
|
210
|
+
const def = pickDefault();
|
|
211
|
+
// `.catch` mudo de propósito: um chunk de locale que não carrega degrada para pt, e degradar é MUITO melhor
|
|
212
|
+
// que travar o boot. Sem ele, um `await idiomaPronto()` lá fora derrubaria o jogo inteiro por causa do idioma.
|
|
213
|
+
if (def !== 'pt')
|
|
214
|
+
pendente = setLocale(def).catch(() => { });
|
|
215
|
+
return locale;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Resolve quando o idioma escolhido no boot terminou de carregar (na hora, se for pt).
|
|
219
|
+
*
|
|
220
|
+
* ========================= POR QUE ISTO PRECISOU EXISTIR =========================
|
|
221
|
+
* O `initI18n()` do main.js era a ÚLTIMA linha do boot, depois de o HUD, os menus de título e as telas de
|
|
222
|
+
* pausa já estarem montados. Para pt isso não custava nada — já estava tudo em português. Para en/es, custava
|
|
223
|
+
* metade da interface: o `applyDom` conserta o markup ESTÁTICO (`data-i18n`), mas o que o JavaScript monta
|
|
224
|
+
* (os botões de cenário, os de atividade) tinha capturado o texto de pt e ninguém reconstruía.
|
|
225
|
+
*
|
|
226
|
+
* O sintoma era desconcertante: `t('cen.cidade')` devolvia "City" e o botão na tela dizia "Cidade".
|
|
227
|
+
*
|
|
228
|
+
* Isto NÃO responde à pergunta maior — QUANDO a interface se reconstrói ao trocar de idioma EM EXECUÇÃO —,
|
|
229
|
+
* que segue com o Dev. Responde à menor, que não tem duas respostas: a interface não se constrói antes de o
|
|
230
|
+
* idioma ser conhecido.
|
|
231
|
+
*/
|
|
232
|
+
export function idiomaPronto() { return pendente; }
|
|
233
|
+
const i18n = { t, getLocale, availableLocales, applyDom, setLocale, initI18n, idiomaPronto, registerDict };
|
|
234
|
+
export default i18n;
|
|
235
|
+
if (typeof window !== 'undefined')
|
|
236
|
+
window.__i18n = i18n; // exposto p/ teste/preview
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
export declare const Z: {
|
|
3
|
+
readonly SKY: 1000;
|
|
4
|
+
readonly SKY_ANIM: 2000;
|
|
5
|
+
readonly PARALLAX_4: 3000;
|
|
6
|
+
readonly PARALLAX_3: 4000;
|
|
7
|
+
readonly PARALLAX_2: 5000;
|
|
8
|
+
readonly PARALLAX_1: 6000;
|
|
9
|
+
readonly BG_DECOR: 7000;
|
|
10
|
+
readonly TILES: 8000;
|
|
11
|
+
readonly SCENERY_INTERACT: 9000;
|
|
12
|
+
readonly VFX_BACK: 10000;
|
|
13
|
+
readonly FLORA_BACK: 11000;
|
|
14
|
+
readonly FAUNA_BACK: 12000;
|
|
15
|
+
readonly NPC: 13000;
|
|
16
|
+
readonly ITEMS: 14000;
|
|
17
|
+
readonly PLAYER: 15000;
|
|
18
|
+
readonly VFX_FRONT: 16000;
|
|
19
|
+
readonly VEHICLES: 17000;
|
|
20
|
+
readonly FLORA_FRONT: 18000;
|
|
21
|
+
readonly FAUNA_FRONT: 19000;
|
|
22
|
+
readonly FOREGROUND: 20000;
|
|
23
|
+
readonly WEATHER: 21000;
|
|
24
|
+
readonly DARK_WORLD: 22000;
|
|
25
|
+
readonly WORLD_A11Y: 23000;
|
|
26
|
+
readonly HUD: 24000;
|
|
27
|
+
readonly TOUCH_CONTROLS: 25000;
|
|
28
|
+
readonly CAPTIONS: 26000;
|
|
29
|
+
readonly DIALOGUE: 27000;
|
|
30
|
+
readonly GAME_MSG: 28000;
|
|
31
|
+
readonly MODAL_SCRIM: 29000;
|
|
32
|
+
readonly MENU: 30000;
|
|
33
|
+
readonly MENU_MAX: 39999;
|
|
34
|
+
readonly TRANSITION: 40000;
|
|
35
|
+
readonly DEBUG: 90000;
|
|
36
|
+
};
|
|
37
|
+
export type LayerName = keyof typeof Z;
|
|
38
|
+
export declare const POST_FX_ORDER: readonly ["CRT_VIGNETTE", "EMPATHY_SIM", "A11Y_CORRECTION", "FLASH_LIMIT"];
|
|
39
|
+
export type PostFx = typeof POST_FX_ORDER[number];
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// core/layers — ORDEM-Z CANÔNICA (fonte única de verdade). Cada camada tem um Z NOMEADO; ninguém mais faz
|
|
3
|
+
// addChildAt(getChildIndex(...)) nem "re-adiciona ao topo" — a inserção passa a ser pelo Z (fim do acoplamento).
|
|
4
|
+
// Duas escalas: MUNDO (por viewport — PIXI zIndex dentro da câmera, com sortableChildren) e OVERLAY (global, tela —
|
|
5
|
+
// vira CSS z-index no DOM, pois menu/HUD/legenda são DOM). **Passo 1000** = folga generosa: 999 slots livres entre
|
|
6
|
+
// camadas nomeadas (e sub-slots dentro de cada uma) p/ inserir futuras SEM renumerar. Elementos concretos entram por
|
|
7
|
+
// `Z.BANDA + offset` pequeno (ex.: cane/cadeira = PLAYER+10/+20) — ver docs/5-Refactoring/plano-religacao-z-camadas.md.
|
|
8
|
+
//
|
|
9
|
+
// NATUREZA (fauna/flora) STRADDLE o jogador: há um par BACK (atrás) e FRONT (à frente) p/ cada, deixando bichos/plantas
|
|
10
|
+
// aparecerem dos dois lados do player (imersão — pedido do José). PIXI sortableChildren é tudo-ou-nada por container →
|
|
11
|
+
// o mundo inteiro migra numa rodada só (todos os filhos ganham zIndex de uma vez, reproduzindo a ordem atual = no-op).
|
|
12
|
+
//
|
|
13
|
+
// PÓS-PROCESSO ≠ camada Z: CRT/vinheta e os filtros de a11y (alto-contraste, correção de daltonismo, simulação de
|
|
14
|
+
// deficiência) são uma CADEIA de passes sobre o frame JÁ COMPOSTO (POST_FX_ORDER), não itens da display-list — e cobrem
|
|
15
|
+
// TUDO, inclusive o menu. A11Y_CORRECTION é SEMPRE o último passe; modos de a11y SUPRIMEM o CRT/efeitos decorativos; o
|
|
16
|
+
// overlay é palette-aware (troca p/ CB-safe). Ver ADR-0020 + ADR-0011.
|
|
17
|
+
export const Z = {
|
|
18
|
+
// ===== MUNDO (por viewport; PIXI zIndex dentro da câmera) =====
|
|
19
|
+
SKY: 1000, // gradiente de céu (base)
|
|
20
|
+
SKY_ANIM: 2000, // animações do céu atrás do parallax (estrelas)
|
|
21
|
+
PARALLAX_4: 3000, // paralaxe mais distante. Nuvens de céu podem entrar em PARALLAX_*+offset (profundidades)
|
|
22
|
+
PARALLAX_3: 4000,
|
|
23
|
+
PARALLAX_2: 5000,
|
|
24
|
+
PARALLAX_1: 6000, // fundo próximo do jogo
|
|
25
|
+
BG_DECOR: 7000, // deco ATRÁS dos tiles: árvores, água/corais/algas, ruínas, deco de cidade, nuvens de tela
|
|
26
|
+
TILES: 8000, // tileset / plataforma (o nível)
|
|
27
|
+
SCENERY_INTERACT: 9000, // cenário interativo: porta, alavanca, interruptor, manivela, corda, portão, rampa, elevador
|
|
28
|
+
VFX_BACK: 10000, // efeitos ATRÁS dos atores: poeira de pé, god-rays, tracinhos de lava, sombras
|
|
29
|
+
FLORA_BACK: 11000, // plantas ATRÁS do player (grama, flores, arbustos de fundo)
|
|
30
|
+
FAUNA_BACK: 12000, // criaturas ATRÁS do player (bichos da cidade, animais de fundo)
|
|
31
|
+
NPC: 13000, // personagens interativos (não-jogador)
|
|
32
|
+
ITEMS: 14000, // coletáveis (moedas, poderes, chave) — ATRÁS do player (barril DK / Yoshi / chave SMW)
|
|
33
|
+
PLAYER: 15000, // jogador(es). Aparatos presos (bengala/cadeira) = PLAYER+offset
|
|
34
|
+
VFX_FRONT: 16000, // efeitos NA FRENTE do player: partículas/juice, explosão, faísca, fogos, confete
|
|
35
|
+
VEHICLES: 17000, // veículos na frente (carros/trânsito da cidade)
|
|
36
|
+
FLORA_FRONT: 18000, // plantas NA FRENTE do player (folhagem que tampa o ator — imersão)
|
|
37
|
+
FAUNA_FRONT: 19000, // criaturas NA FRENTE do player (borboletas/vagalumes/minhocas v3 passando à frente)
|
|
38
|
+
FOREGROUND: 20000, // primeiro-plano oclusivo genérico (props que tampam o ator)
|
|
39
|
+
WEATHER: 21000, // clima na frente de tudo do mundo (névoa, chuva, clarão)
|
|
40
|
+
DARK_WORLD: 22000, // escurecimento do mundo (modo cego / empatia baixa-visão)
|
|
41
|
+
WORLD_A11Y: 23000, // pistas visuais de a11y no mundo (sonar, guarda de beirada, hitbox fácil, realce)
|
|
42
|
+
// ===== OVERLAY (global, tela; = CSS z-index no DOM / topo da stage PIXI) =====
|
|
43
|
+
HUD: 24000, // placar/moedas/poder/objetivo/minimapa (persistente durante o jogo)
|
|
44
|
+
TOUCH_CONTROLS: 25000, // gamepad virtual / botões de toque (mobile)
|
|
45
|
+
CAPTIONS: 26000, // legendas de som (a11y surdez) + skip-link de navegação
|
|
46
|
+
DIALOGUE: 27000, // fala/diálogo/atividade (quiz)
|
|
47
|
+
GAME_MSG: 28000, // vitória / game over / pause / faixa de fase
|
|
48
|
+
MODAL_SCRIM: 29000, // escurecimento do fundo quando abre um modal
|
|
49
|
+
MENU: 30000, // menus — RANGE reservado 30000–39999 (níveis aninhados: +1000 por nível)
|
|
50
|
+
MENU_MAX: 39999,
|
|
51
|
+
TRANSITION: 40000, // fade/wipe de troca de fase (cobre tudo)
|
|
52
|
+
DEBUG: 90000, // painel ?debug / FPS / hitboxes (dev; topo absoluto)
|
|
53
|
+
};
|
|
54
|
+
// Cadeia de PÓS-PROCESSO — passes sobre o frame COMPOSTO, do interno p/ o externo. NÃO são camadas Z.
|
|
55
|
+
// Modos de a11y suprimem CRT_VIGNETTE e flashes decorativos (precedência a11y > estética). Ver ADR-0020.
|
|
56
|
+
//
|
|
57
|
+
// DUAS COISAS DIFERENTES NO FIM DA FILA, e confundi-las é o que este comentário existe para impedir:
|
|
58
|
+
//
|
|
59
|
+
// · A11Y_CORRECTION é o último passe de CORREÇÃO. Ele decide como a imagem SE PARECE, e por isso precisa ver o
|
|
60
|
+
// composto final, menus inclusive — é essa a razão de ele vir depois do CRT, que senão re-tinge e desfaz a
|
|
61
|
+
// correção de daltonismo.
|
|
62
|
+
// · FLASH_LIMIT é o último passe, ponto. Ele decide se a imagem pode FAZER MAL. A WCAG 2.3.1 limita a variação de
|
|
63
|
+
// luminância no tempo, e o único quadro cuja luminância importa é o que chega ao olho. Um limitador colocado
|
|
64
|
+
// ANTES da correção limita uma imagem que já não existe, e a correção fica livre para reabrir a oscilação que
|
|
65
|
+
// ele acabou de fechar — uma matriz de daltonismo redistribui luminância por definição. Segurança é o passe mais
|
|
66
|
+
// externo porque é a última coisa verdadeira sobre o quadro.
|
|
67
|
+
//
|
|
68
|
+
// O ADR-0020 dizia "A11Y_CORRECTION sempre por último" sem essa distinção; foi emendado em 2026-08-24. FLASH_LIMIT
|
|
69
|
+
// ainda NÃO está implementado — está declarado aqui, e aferido por teste, para que quem o implementar encontre o
|
|
70
|
+
// lugar certo já ocupado em vez de deduzir a ordem errada a partir da redação antiga.
|
|
71
|
+
export const POST_FX_ORDER = ['CRT_VIGNETTE', 'EMPATHY_SIM', 'A11Y_CORRECTION', 'FLASH_LIMIT'];
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
/** As quatro direções que um direcional produz. Nomes em português porque são vocabulário do jogo, não da web. */
|
|
3
|
+
export type Direcao = 'esquerda' | 'direita' | 'cima' | 'baixo';
|
|
4
|
+
/** Onde o cursor está, em coordenadas de grade — o que a tela desenha e o leitor de tela anuncia. */
|
|
5
|
+
export interface Posicao {
|
|
6
|
+
readonly linha: number;
|
|
7
|
+
readonly coluna: number;
|
|
8
|
+
}
|
|
9
|
+
export interface GradeDeLetras {
|
|
10
|
+
/** A grade, como dado: quem desenha lê daqui em vez de recontar. */
|
|
11
|
+
readonly simbolos: readonly string[];
|
|
12
|
+
readonly colunas: number;
|
|
13
|
+
readonly linhas: number;
|
|
14
|
+
/** Quantas casas o valor aceita. `0` = sem limite (jogos de palavra). */
|
|
15
|
+
readonly capacidade: number;
|
|
16
|
+
/** Índice linear do cursor, `0..simbolos.length-1`. */
|
|
17
|
+
indice(): number;
|
|
18
|
+
/** A posição do cursor em linha/coluna, base ZERO — quem anuncia soma 1 se quiser falar "linha 1". */
|
|
19
|
+
posicao(): Posicao;
|
|
20
|
+
/** O símbolo sob o cursor. */
|
|
21
|
+
sob(): string;
|
|
22
|
+
/** Move o cursor uma casa, enrolando em toroide. */
|
|
23
|
+
mover(dir: Direcao): void;
|
|
24
|
+
/** Leva o cursor ao símbolo dado. Devolve `false` — e não move — quando ele não está na grade. */
|
|
25
|
+
irPara(simbolo: string): boolean;
|
|
26
|
+
/** Acrescenta o símbolo sob o cursor ao valor. Não faz nada quando o valor já está cheio. */
|
|
27
|
+
digitar(): void;
|
|
28
|
+
/** Apaga a última casa. Não faz nada quando o valor está vazio. */
|
|
29
|
+
apagar(): void;
|
|
30
|
+
/** Esvazia o valor. O cursor NÃO se mexe: quem apagou continua olhando para onde estava. */
|
|
31
|
+
limpar(): void;
|
|
32
|
+
/** O que foi digitado até agora. */
|
|
33
|
+
valor(): string;
|
|
34
|
+
/** Quantas casas ainda faltam. `null` quando não há capacidade declarada. */
|
|
35
|
+
faltam(): number | null;
|
|
36
|
+
/** `true` quando o valor atingiu a capacidade. Sempre `false` sem capacidade declarada. */
|
|
37
|
+
completo(): boolean;
|
|
38
|
+
}
|
|
39
|
+
export interface OpcoesDaGrade {
|
|
40
|
+
/** Os símbolos, em ordem de leitura (esquerda→direita, cima→baixo). */
|
|
41
|
+
simbolos: string;
|
|
42
|
+
/** Largura da grade. As linhas saem da divisão, e a última pode ficar incompleta. */
|
|
43
|
+
colunas: number;
|
|
44
|
+
/** Quantas casas o valor aceita; omitir ou `0` deixa sem limite. */
|
|
45
|
+
capacidade?: number;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* A grade, como instância.
|
|
49
|
+
*
|
|
50
|
+
* `colunas` precisa ser inteiro ≥ 1 e `simbolos` não pode ser vazio: uma grade sem casas não tem cursor, e
|
|
51
|
+
* um cursor sem casa é a origem de todo `undefined` que aparece três telas adiante. As mensagens são
|
|
52
|
+
* SELETORES e não prosa — o gate de i18n de engine proíbe frase em qualquer idioma aqui, e o nome do campo
|
|
53
|
+
* que falhou é mais útil que uma frase de qualquer forma.
|
|
54
|
+
*/
|
|
55
|
+
export declare function criarGrade(opcoes: OpcoesDaGrade): GradeDeLetras;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// core/letter-grid — ENTRADA DE TEXTO SEM TECLADO, e é essa a coisa que este arquivo é.
|
|
3
|
+
//
|
|
4
|
+
// ========================= POR QUE ISTO EXISTE =========================
|
|
5
|
+
// O ADR-0027 diz o que falta ao motor em uma linha: *"the engine needs a LETTER GRID SELECTOR, not a
|
|
6
|
+
// keyboard"*. Um console não tem teclado, e o alvo desta engine é uma máquina de escola operada por
|
|
7
|
+
// direcional — quando não por UM botão só. Digitar, aqui, é mover um cursor por uma grade e confirmar.
|
|
8
|
+
//
|
|
9
|
+
// O consumidor é o CÓDIGO DA SALA DA PROFESSORA. Não é a senha de progressão da criança: o ADR-0037 a
|
|
10
|
+
// enterrou junto com a ideia de salvar jogo — não existe save, e o Inclusionist não guarda nada sobre uma
|
|
11
|
+
// criança. O que o ADR-0037 diz que sobrevive é o `core/password`, e por outro motivo: *"the teacher's room
|
|
12
|
+
// code is the same engineering problem… it stays because the room screen will need it"*. O modo de falha
|
|
13
|
+
// mudou de forma junto: um código errado que seja ACEITO não carrega o progresso de outra criança — larga
|
|
14
|
+
// esta criança na sala de outra turma, fazendo a atividade de outra professora.
|
|
15
|
+
//
|
|
16
|
+
// O segundo consumidor já está previsto e é o que impede este módulo de nascer como componente de senha: os
|
|
17
|
+
// jogos de palavras do catálogo do `inclusionist-demos` precisam da mesma coisa.
|
|
18
|
+
//
|
|
19
|
+
// ========================= O CORTE (ADR-0041) =========================
|
|
20
|
+
// MECÂNICA aqui, TELA no `ui/`. É a mesma divisão que o `core/password` acabou de usar, e ela existe para
|
|
21
|
+
// preservar a propriedade mais cara que o `core/` tem: ser testável sem navegador. O `core/scenes` foi
|
|
22
|
+
// escrito de propósito sem PIXI e sem DOM por esse motivo, e um módulo que DESENHE dentro do `core/` abre
|
|
23
|
+
// uma porta que a próxima tela atravessa sem discussão.
|
|
24
|
+
//
|
|
25
|
+
// O que NÃO está aqui, e é honesto dizer: a repetição de tecla. Ela é temporização de entrada e mora com o
|
|
26
|
+
// `input/`, junto das bordas de tecla — o ADR-0041 a listou como conteúdo desta metade e ela não é.
|
|
27
|
+
//
|
|
28
|
+
// ========================= A ESCOLHA DE ENROLAMENTO, E O MOTIVO =========================
|
|
29
|
+
// Nas bordas o cursor enrola em TOROIDE: andar para a direita mantém a LINHA, andar para baixo mantém a
|
|
30
|
+
// COLUNA. A alternativa — a da direita na última coluna cair na linha seguinte, como texto — é comum em
|
|
31
|
+
// teclados de console e está errada aqui, e o motivo é o leitor de tela: a leitura anuncia "linha 2,
|
|
32
|
+
// coluna 1", e um movimento horizontal que mude a linha faz o anúncio contradizer a direção que a criança
|
|
33
|
+
// apertou. Movimento previsível vale mais que economia de apertos.
|
|
34
|
+
//
|
|
35
|
+
// SEM I/O NO IMPORT, e sem estado de módulo: `criarGrade()` devolve a instância e quem compõe a possui. É a
|
|
36
|
+
// D13 do `inclusionist-demos`, e é o que permite duas grades na mesma página sem uma pisar na outra.
|
|
37
|
+
/**
|
|
38
|
+
* A grade, como instância.
|
|
39
|
+
*
|
|
40
|
+
* `colunas` precisa ser inteiro ≥ 1 e `simbolos` não pode ser vazio: uma grade sem casas não tem cursor, e
|
|
41
|
+
* um cursor sem casa é a origem de todo `undefined` que aparece três telas adiante. As mensagens são
|
|
42
|
+
* SELETORES e não prosa — o gate de i18n de engine proíbe frase em qualquer idioma aqui, e o nome do campo
|
|
43
|
+
* que falhou é mais útil que uma frase de qualquer forma.
|
|
44
|
+
*/
|
|
45
|
+
export function criarGrade(opcoes) {
|
|
46
|
+
const simbolos = [...opcoes.simbolos];
|
|
47
|
+
const colunas = opcoes.colunas;
|
|
48
|
+
if (simbolos.length === 0)
|
|
49
|
+
throw new Error('simbolos');
|
|
50
|
+
if (!Number.isInteger(colunas) || colunas < 1)
|
|
51
|
+
throw new Error('colunas');
|
|
52
|
+
const linhas = Math.ceil(simbolos.length / colunas);
|
|
53
|
+
const capacidade = opcoes.capacidade ?? 0;
|
|
54
|
+
if (!Number.isInteger(capacidade) || capacidade < 0)
|
|
55
|
+
throw new Error('capacidade');
|
|
56
|
+
let cursor = 0;
|
|
57
|
+
let digitado = '';
|
|
58
|
+
/** Enrolamento de um eixo, escrito uma vez: `((v % n) + n) % n` sobrevive a negativo, que `v % n` não. */
|
|
59
|
+
const enrolar = (v, n) => ((v % n) + n) % n;
|
|
60
|
+
/**
|
|
61
|
+
* A ÚLTIMA LINHA PODE SER INCOMPLETA — 32 símbolos em 8 colunas dão 4 linhas cheias, mas 30 dariam uma
|
|
62
|
+
* linha com 6. Descer numa coluna que não existe na linha de destino cairia num buraco, e este é o ponto
|
|
63
|
+
* que decide o que fazer: o cursor pula a linha incompleta naquela coluna e segue descendo. Assim toda
|
|
64
|
+
* tecla move, sempre, para uma casa que existe — e o leitor de tela nunca anuncia vazio.
|
|
65
|
+
*/
|
|
66
|
+
function indiceDe(linha, coluna) {
|
|
67
|
+
for (let i = 0; i < linhas; i++) {
|
|
68
|
+
const l = enrolar(linha + i, linhas);
|
|
69
|
+
const idx = l * colunas + coluna;
|
|
70
|
+
if (idx < simbolos.length)
|
|
71
|
+
return idx;
|
|
72
|
+
}
|
|
73
|
+
return cursor; // inalcançável com colunas ≥ 1: a linha 0 sempre tem a coluna 0
|
|
74
|
+
}
|
|
75
|
+
function posicao() {
|
|
76
|
+
return { linha: Math.floor(cursor / colunas), coluna: cursor % colunas };
|
|
77
|
+
}
|
|
78
|
+
function mover(dir) {
|
|
79
|
+
const { linha, coluna } = posicao();
|
|
80
|
+
if (dir === 'esquerda' || dir === 'direita') {
|
|
81
|
+
// Enrola DENTRO da linha, e a linha incompleta enrola no tamanho dela, não no da grade.
|
|
82
|
+
const inicio = linha * colunas;
|
|
83
|
+
const largura = Math.min(colunas, simbolos.length - inicio);
|
|
84
|
+
cursor = inicio + enrolar(coluna + (dir === 'direita' ? 1 : -1), largura);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
cursor = indiceDe(linha + (dir === 'baixo' ? 1 : -1), coluna);
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
simbolos,
|
|
91
|
+
colunas,
|
|
92
|
+
linhas,
|
|
93
|
+
capacidade,
|
|
94
|
+
indice: () => cursor,
|
|
95
|
+
posicao,
|
|
96
|
+
sob: () => simbolos[cursor],
|
|
97
|
+
mover,
|
|
98
|
+
irPara(simbolo) {
|
|
99
|
+
const i = simbolos.indexOf(simbolo);
|
|
100
|
+
if (i < 0)
|
|
101
|
+
return false;
|
|
102
|
+
cursor = i;
|
|
103
|
+
return true;
|
|
104
|
+
},
|
|
105
|
+
digitar() {
|
|
106
|
+
if (capacidade > 0 && digitado.length >= capacidade)
|
|
107
|
+
return;
|
|
108
|
+
digitado += simbolos[cursor];
|
|
109
|
+
},
|
|
110
|
+
apagar() { digitado = digitado.slice(0, -1); },
|
|
111
|
+
limpar() { digitado = ''; },
|
|
112
|
+
valor: () => digitado,
|
|
113
|
+
faltam: () => (capacidade > 0 ? capacidade - digitado.length : null),
|
|
114
|
+
completo: () => capacidade > 0 && digitado.length >= capacidade,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
type Ticker = {
|
|
3
|
+
add: (fn: () => void) => void;
|
|
4
|
+
deltaTime: number;
|
|
5
|
+
remove?: (fn: () => void) => void;
|
|
6
|
+
};
|
|
7
|
+
export interface OpcoesDoLaco {
|
|
8
|
+
/**
|
|
9
|
+
* Chamado UMA vez, com o erro, quando o quadro lança. É o canal de quem não enxerga a tela parar.
|
|
10
|
+
*
|
|
11
|
+
* A raiz de composição liga isto ao `srAlert` e a uma mensagem visível. Opcional de propósito: um consumidor
|
|
12
|
+
* que monte o laço sem casca (um teste, o quiz) continua parando — o anúncio é opcional, **parar não é**.
|
|
13
|
+
*/
|
|
14
|
+
aoFalhar?: (erro: unknown) => void;
|
|
15
|
+
}
|
|
16
|
+
export declare function startLoop(ticker: Ticker, frame: (dt: number) => void, maxDt?: number, opcoes?: OpcoesDoLaco): void;
|
|
17
|
+
export {};
|