@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,620 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// ui/pause-icons — the PER-SCREEN pause menu (`.screen-pause`) and the accessibility icon bar (`.pi-btn`).
|
|
3
|
+
// Extracted VERBATIM from game.js (Estágio 4): PAUSE_ICONS, buildScreenPause, calmMode/applyCalm, iconAct,
|
|
4
|
+
// iconLabel, reflectIconBtn, reflectPauseIcons and hasPrivateOutput.
|
|
5
|
+
//
|
|
6
|
+
// WHY THE ICON BAR IS ITS OWN SLICE: the ten `.pi-btn` buttons are the ONLY place in the game where a state
|
|
7
|
+
// owned by another subsystem (blind mode, TTS, Libras, TEA/reduced-motion, toggle-keys, contrast, CVD) is both
|
|
8
|
+
// mutated AND read back as an `aria-label`. That round-trip — "the label must tell the truth about the state" —
|
|
9
|
+
// is the whole reason `iconLabel` exists, and it is what the pure functions below make testable in node.
|
|
10
|
+
//
|
|
11
|
+
// `iconAct` IS A DISPATCHER (its ctx list is long by nature: seven subsystems, one button each), so it is
|
|
12
|
+
// implemented here as a TABLE (`ICON_ACTS`) rather than the original if/else chain. Same order, same
|
|
13
|
+
// guards, same announcements — only the shape changed.
|
|
14
|
+
//
|
|
15
|
+
// WHAT STAYS IN game.js (injected):
|
|
16
|
+
// · `pauseActor` — read by the gamepad (input/gamepad.ts ctx) and the keyboard router, and by
|
|
17
|
+
// openHelp()/openOptions(); this module only WRITES it (`setPauseActor`).
|
|
18
|
+
// · `pauseActs` — the `.pm-btn` action table; every entry calls a panel that still lives in game.js
|
|
19
|
+
// (openTypo/openAudio/openMovement/motion.open/openVisual/empathy.open/printMode/
|
|
20
|
+
// quitGame/openHelp/setPhase/applyLetra/setQuizLevel/joinPlayer/fitsN/vpScreens).
|
|
21
|
+
// Injected LAZILY (`getPauseActs`) because it is a `const` declared ~1200 lines below
|
|
22
|
+
// the init site — an eager reference would hit its temporal dead zone.
|
|
23
|
+
// · `vpPause` — the array of built pause screens; game.js rebuilds it in buildGameHud() and reads it
|
|
24
|
+
// in setPhase/navPause/printMode/pauseSelect/__incl. Injected as a getter.
|
|
25
|
+
// · `rm`/`saveRM` — the reduced-motion flags object, co-owned with ui/settings-motion (same reference).
|
|
26
|
+
// · `PM_BTNS`/`QL_NAME` — owned by ui/activities-menu; injected, never copied.
|
|
27
|
+
import { t } from '../core/i18n.js';
|
|
28
|
+
import { CONTRAST_LEVELS, CONTRAST_LABELS } from './settings-visual.js';
|
|
29
|
+
import { anunciarItem } from './item-announcement.js';
|
|
30
|
+
import { rotuloAcessivel } from '../core/rotulo-acessivel.js';
|
|
31
|
+
import { passoNoAnel } from '../core/anel.js'; // da FOLHA, e não de ui/menu-nav: ver a nota lá
|
|
32
|
+
// LIGAÇÃO VIVA (ESM): o índice pode ser desligado no menu, e o valor aqui acompanha sem assinatura.
|
|
33
|
+
import { menuIndexOn } from '../core/state.js';
|
|
34
|
+
/**
|
|
35
|
+
* A LEGENDA de um ícone da barra de acessibilidade — uma função, e não três cópias da mesma expressão.
|
|
36
|
+
*
|
|
37
|
+
* Ela é escrita em TRÊS momentos que parecem diferentes e são o mesmo: o cursor direcional pousa no ícone
|
|
38
|
+
* (`ui/menu-nav`), o dedo o aciona, e o mouse ou o foco passa por cima. Enquanto eram três linhas soltas, o
|
|
39
|
+
* índice do ADR-0044 teria de ser acrescentado em três lugares — e a chance de um ficar para trás é a mesma
|
|
40
|
+
* que este repositório já pagou dezesseis vezes com o `DomQuery`.
|
|
41
|
+
*
|
|
42
|
+
* O `aria-label` já conta o ESTADO ("Alto contraste, ativado"): é ele que o `reflectIconBtn` reescreve a cada
|
|
43
|
+
* mudança, e é por isso que a legenda o lê de volta em vez de recompor o texto por conta própria.
|
|
44
|
+
*/
|
|
45
|
+
export function legendaDoIcone(barra, el) {
|
|
46
|
+
const icones = [...barra.querySelectorAll('.pi-btn')];
|
|
47
|
+
// A regra "rótulo declarado vence" nasceu AQUI e valia só para os dez ícones. Virou `core/rotulo-acessivel`
|
|
48
|
+
// e agora vale para o menu inicial e para a lista de pausa também — uma resposta para "como se chama este
|
|
49
|
+
// controle", e não três.
|
|
50
|
+
return anunciarItem({ rotulo: rotuloAcessivel(el), posicao: icones.indexOf(el) + 1, total: icones.length }, menuIndexOn);
|
|
51
|
+
}
|
|
52
|
+
/** The accessibility shortcut bar at the top of every pause screen (and of the splash `#title-icons`).
|
|
53
|
+
* Sound-bound icons (blind/TTS) require a private audio output; webcam/voice are still `soon`.
|
|
54
|
+
* VERBATIM from game.js in order and behaviour; the names became i18n keys in the Fase-5 pass. */
|
|
55
|
+
// `n` é a CHAVE i18n do nome do ícone (o emoji `e` não traduz — é o mesmo glifo em toda língua).
|
|
56
|
+
export const PAUSE_ICONS = [
|
|
57
|
+
{ k: 'blind', e: '🦯', n: 'icon.blind' },
|
|
58
|
+
{ k: 'tts', e: '🗨️', n: 'icon.tts' },
|
|
59
|
+
{ k: 'libras', e: '🤟', n: 'icon.libras' },
|
|
60
|
+
{ k: 'tea', e: '🧩', n: 'icon.tea' },
|
|
61
|
+
{ k: 'altmove', e: '☝️', n: 'icon.altmove' }, // ☝️ e não 🦾: o gesto é UM DEDO tocando, que é o que a alternância pede (pedido do Dev)
|
|
62
|
+
{ k: 'contrast', e: '🌗', n: 'icon.contrast' },
|
|
63
|
+
{ k: 'cvd', e: '🚥', n: 'icon.cvd' },
|
|
64
|
+
{ k: 'face', e: '🧑', n: 'icon.face', soon: true },
|
|
65
|
+
{ k: 'eyes', e: '👀', n: 'icon.eyes', soon: true },
|
|
66
|
+
{ k: 'voice', e: '👄', n: 'icon.voice', soon: true },
|
|
67
|
+
];
|
|
68
|
+
const ICON_BY_KEY = new Map(PAUSE_ICONS.map((ic) => [ic.k, ic]));
|
|
69
|
+
export function pauseIcon(k) { return ICON_BY_KEY.get(k); }
|
|
70
|
+
/** TEA cycle: 0 = normal · 1 = calmo (reduces) · 2 = silencioso (switches off). Never touches TTS/blind mode. */
|
|
71
|
+
export const CALM_NAMES = ['calm.off', 'calm.quiet', 'calm.silent'];
|
|
72
|
+
/** The audio categories `applyCalm` governs. TTS/sonar/guarda/guia stay untouched — a calm player still needs them. */
|
|
73
|
+
export const CALM_AUDIO_CATS = ['ambient', 'music', 'earcons', 'other', 'interact'];
|
|
74
|
+
/** Colour-vision-deficiency cycle, in `player.viz` values. */
|
|
75
|
+
export const CVD_SEQ = ['normal', 'fix-protan', 'fix-deuter', 'fix-tritan'];
|
|
76
|
+
/** i18n keys of the CVD announcement names, indexed the same as CVD_SEQ.
|
|
77
|
+
*
|
|
78
|
+
* ⚠️ POSITION 0 IS `cvd.tricro` AND NOT `cvd.off`, AND THE TWO KEYS ARE NOT INTERCHANGEABLE.
|
|
79
|
+
* This list names the four CHOICES of the cycle, so position 0 is a way of seeing — trichromatic
|
|
80
|
+
* vision, the one that needs no correction — and it is said as one. `cvd.off` below is a FALLBACK
|
|
81
|
+
* for `s.viz` values that are not corrections at all, and 13 of the 16 viz modes are exactly that:
|
|
82
|
+
* the three simulations, the three contrast levels, the five low-vision modes and blind mode.
|
|
83
|
+
* Announcing "trichromatic vision" there would have the software assert what the child sees, while
|
|
84
|
+
* she is simulating not seeing it. The choice is named; the fallback is switched off. */
|
|
85
|
+
export const CVD_NAMES = ['cvd.tricro', 'cvd.protan', 'cvd.deuter', 'cvd.tritan'];
|
|
86
|
+
/** `player.viz` → i18n key of the label used by iconLabel (anything else falls back to the 'off' key). */
|
|
87
|
+
export const CVD_LABELS = {
|
|
88
|
+
'fix-protan': 'cvd.protan', 'fix-deuter': 'cvd.deuter', 'fix-tritan': 'cvd.tritan',
|
|
89
|
+
};
|
|
90
|
+
/** A player has private output when nobody else is on the same sink. Single screen ⇒ always private.
|
|
91
|
+
* Pure form of game.js's hasPrivateOutput (see the report: it had no other caller left). */
|
|
92
|
+
export function hasPrivateOutputIn(list, count, i) {
|
|
93
|
+
if (count <= 1)
|
|
94
|
+
return true;
|
|
95
|
+
const p = list[i];
|
|
96
|
+
if (!p || !p.audioSink)
|
|
97
|
+
return false;
|
|
98
|
+
return !list.some((q, j) => j !== i && q && q.audioSink === p.audioSink);
|
|
99
|
+
}
|
|
100
|
+
/** TEA cycle step. */
|
|
101
|
+
export function nextCalmMode(cur) { return (cur + 1) % 3; }
|
|
102
|
+
/** Next high-contrast level. An unlisted `viz` (e.g. a CVD filter) is treated as index 0 ⇒ jumps to 'hc-direto'. */
|
|
103
|
+
export function nextContrast(cur) {
|
|
104
|
+
let idx = CONTRAST_LEVELS.indexOf(cur);
|
|
105
|
+
idx = idx < 0 ? 0 : idx;
|
|
106
|
+
return CONTRAST_LEVELS[(idx + 1) % CONTRAST_LEVELS.length];
|
|
107
|
+
}
|
|
108
|
+
/** Next CVD filter. NOTE the asymmetry with nextContrast: an unlisted `viz` maps to index 1 ('fix-protan'),
|
|
109
|
+
* not 0 — verbatim from game.js (`idx = idx<0 ? 1 : (idx+1)%seq.length`). */
|
|
110
|
+
export function nextCvd(cur) {
|
|
111
|
+
let idx = CVD_SEQ.indexOf(cur);
|
|
112
|
+
idx = idx < 0 ? 1 : (idx + 1) % CVD_SEQ.length;
|
|
113
|
+
return { idx, mode: CVD_SEQ[idx] };
|
|
114
|
+
}
|
|
115
|
+
/** What `applyCalm` does to ONE audio category, given the TEA level. Extracted so the (destructive) volume
|
|
116
|
+
* clamp at level 1 is visible and testable — see the report. */
|
|
117
|
+
export function calmAudioPlan(calmMode, vol) {
|
|
118
|
+
if (calmMode === 0)
|
|
119
|
+
return { on: true, vol };
|
|
120
|
+
if (calmMode === 1)
|
|
121
|
+
return { on: true, vol: Math.min(vol, 0.3) };
|
|
122
|
+
return { on: false, vol };
|
|
123
|
+
}
|
|
124
|
+
/** What `applyCalm` does to the scene/character reduced-motion flags. */
|
|
125
|
+
export function calmMotionPlan(calmMode) {
|
|
126
|
+
return { sceneReduced: calmMode >= 1, charFrozen: calmMode === 2 };
|
|
127
|
+
}
|
|
128
|
+
/** The `aria-label` of one icon — it MUST reflect the current state, on/off or level. This is the whole
|
|
129
|
+
* point of the function: a toggle that looks pressed but does not say so is invisible to a screen reader. */
|
|
130
|
+
export function computeIconLabel(k, s) {
|
|
131
|
+
const ic = ICON_BY_KEY.get(k);
|
|
132
|
+
if (!ic)
|
|
133
|
+
return '';
|
|
134
|
+
// O estado vira SEMPRE um parâmetro (`{v}`), nunca uma concatenação: 'on'/'off' eram palavras inglesas
|
|
135
|
+
// presas numa frase em português, e uma língua que anteponha o estado ao nome precisa do dicionário para
|
|
136
|
+
// reordenar. `nomeDoIcone: estado` é a moldura; o estado é o conteúdo, e ele também é traduzido.
|
|
137
|
+
if (ic.soon)
|
|
138
|
+
return t('icon.soon', { nome: t(ic.n) });
|
|
139
|
+
const rotulo = (v) => t('icon.state', { nome: t(ic.n), v: t(v) });
|
|
140
|
+
if (k === 'blind')
|
|
141
|
+
return rotulo(s.modoCego ? 'state.on' : 'state.off');
|
|
142
|
+
if (k === 'tts')
|
|
143
|
+
return rotulo(s.ttsOn ? 'state.on' : 'state.off');
|
|
144
|
+
if (k === 'libras')
|
|
145
|
+
return rotulo(s.librasOn ? 'state.on' : 'state.off');
|
|
146
|
+
// TEA e daltonismo usam um nome CURTO aqui, diferente do nome do botão: o rótulo já diz o nível, e
|
|
147
|
+
// repetir a lista de níveis do nome ("(calmo / silencioso)", "(protan/deutan/tritan)") a diria duas vezes.
|
|
148
|
+
// Era assim antes da conversão, com o texto curto embutido — preservado, não reinventado.
|
|
149
|
+
if (k === 'tea')
|
|
150
|
+
return t('icon.state', { nome: t('icon.tea.short'), v: t(CALM_NAMES[s.calmMode]) });
|
|
151
|
+
if (k === 'altmove')
|
|
152
|
+
return rotulo(s.toggleMove ? 'state.on' : 'state.off');
|
|
153
|
+
if (k === 'contrast')
|
|
154
|
+
return rotulo(CONTRAST_LABELS[s.viz] || 'contrast.off');
|
|
155
|
+
if (k === 'cvd')
|
|
156
|
+
return t('icon.state', { nome: t('icon.cvd.short'), v: t(CVD_LABELS[s.viz] || 'cvd.off') });
|
|
157
|
+
return t(ic.n);
|
|
158
|
+
}
|
|
159
|
+
/** Pure form of reflectIconBtn's branching. A `soon` icon lands on all-false — it never claims to be on. */
|
|
160
|
+
export function computeIconVisual(k, s) {
|
|
161
|
+
let on = false, dis = false, calm = false, cvd = '';
|
|
162
|
+
if (k === 'blind') {
|
|
163
|
+
on = s.modoCego;
|
|
164
|
+
dis = !s.privateOutput;
|
|
165
|
+
}
|
|
166
|
+
else if (k === 'tts') {
|
|
167
|
+
on = s.ttsOn;
|
|
168
|
+
dis = !s.privateOutput;
|
|
169
|
+
}
|
|
170
|
+
else if (k === 'libras') {
|
|
171
|
+
on = s.librasOn;
|
|
172
|
+
}
|
|
173
|
+
else if (k === 'tea') {
|
|
174
|
+
on = s.calmMode === 2;
|
|
175
|
+
calm = s.calmMode === 1;
|
|
176
|
+
}
|
|
177
|
+
else if (k === 'altmove') {
|
|
178
|
+
on = s.toggleMove;
|
|
179
|
+
}
|
|
180
|
+
else if (k === 'contrast') {
|
|
181
|
+
on = /^hc-direto/.test(s.viz || '');
|
|
182
|
+
}
|
|
183
|
+
else if (k === 'cvd') {
|
|
184
|
+
if (s.viz === 'fix-protan')
|
|
185
|
+
cvd = 'pi-cvd-protan';
|
|
186
|
+
else if (s.viz === 'fix-deuter')
|
|
187
|
+
cvd = 'pi-cvd-deuter';
|
|
188
|
+
else if (s.viz === 'fix-tritan')
|
|
189
|
+
cvd = 'pi-cvd-tritan';
|
|
190
|
+
}
|
|
191
|
+
return { on, dis, calm, cvd, active: on || calm || !!cvd };
|
|
192
|
+
}
|
|
193
|
+
/** The CSS classes reflectIconBtn clears before applying a fresh visual — in the original order. */
|
|
194
|
+
export const ICON_STATE_CLASSES = ['pi-calm', 'pi-cvd-protan', 'pi-cvd-deuter', 'pi-cvd-tritan'];
|
|
195
|
+
// --- markup (pure string builders; the DOM shell below just assigns them) ---
|
|
196
|
+
/** One `.pi-btn`. `soon` icons get `.pi-soon` and the "under construction" suffix baked into the aria-label.
|
|
197
|
+
* The label is the RESTING one: reflectIconBtn overwrites it with the stateful label as soon as the bar is
|
|
198
|
+
* reflected. `ic.n` is an i18n key, so it must be resolved here too — the markup is rendered once at build
|
|
199
|
+
* time and would otherwise ship the raw key to a screen reader. */
|
|
200
|
+
export function iconBtnMarkup(ic) {
|
|
201
|
+
return '<button class="pi-btn' + (ic.soon ? ' pi-soon' : '') + '" type="button" data-pi="' + ic.k +
|
|
202
|
+
'" aria-label="' + (ic.soon ? t('icon.soon', { nome: t(ic.n) }) : t(ic.n)) + '">' + ic.e + '</button>';
|
|
203
|
+
}
|
|
204
|
+
/** The whole icon bar. Used by the pause screen AND by the splash `#title-icons` (which built the same string
|
|
205
|
+
* by hand in game.js — that duplication dies with this export). */
|
|
206
|
+
export function iconsMarkup() { return PAUSE_ICONS.map(iconBtnMarkup).join(''); }
|
|
207
|
+
/** One `.pm-btn`. Dynamic labels (`letra`/`nivel`) are rendered eagerly and carry NO `data-i18n`, so
|
|
208
|
+
* i18n.applyDom() cannot overwrite them. */
|
|
209
|
+
/**
|
|
210
|
+
* ⚠️ O RÓTULO DINÂMICO ENTRA PRONTO (item 19), e a mudança conserta DUAS coisas de uma vez.
|
|
211
|
+
*
|
|
212
|
+
* A linha era `'📚 Nível ' + level + ' · ' + qlName[level]` — e ela tinha dois defeitos que só se enxergam
|
|
213
|
+
* juntos:
|
|
214
|
+
*
|
|
215
|
+
* 1. FRONTEIRA. `level` vinha de `core/state.quizLevel` e `qlName` de uma tabela do jogo. Um menu de pausa
|
|
216
|
+
* da ENGINE montava o rótulo de uma atividade de alfabetização — conteúdo pedagógico, não mecânica.
|
|
217
|
+
* 2. IDIOMA. "Nível" é pt-BR CRU dentro de um módulo de engine. O gate do item 14 vigia o `main.js` e não
|
|
218
|
+
* alcança `ui/`, então esta linha atravessou a i18n inteira sem ser vista. Num build em inglês, o menu
|
|
219
|
+
* de pausa de uma criança dizia "📚 Nível 2 · …".
|
|
220
|
+
*
|
|
221
|
+
* Agora o jogo entrega a frase montada (`dynLabel`), e a engine só a coloca no botão. O jogo é quem sabe o
|
|
222
|
+
* que é um nível, quem sabe o nome dele e quem sabe em que idioma dizê-lo.
|
|
223
|
+
*/
|
|
224
|
+
export function pmBtnMarkup(b, dynLabel, tr) {
|
|
225
|
+
const dyn = b.letra || b.nivel;
|
|
226
|
+
const lbl = dynLabel(b) ?? (dyn ? (b.lbl ?? '') : tr('pause.' + b.act));
|
|
227
|
+
return '<button class="pm-btn' + (b.letra ? ' pm-letra' : '') + (b.nivel ? ' pm-nivel' : '') +
|
|
228
|
+
'" role="menuitem" type="button" data-act="' + b.act + '"' +
|
|
229
|
+
(dyn ? '' : (' data-i18n="pause.' + b.act + '"')) + '>' + lbl + '</button>';
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Os itens navegáveis de um cartão de pausa — os da lista VISÍVEL, e só eles.
|
|
233
|
+
*
|
|
234
|
+
* Uma constante porque TRÊS módulos a consultam (a navegação em `ui/menu-nav`, a seleção inicial em
|
|
235
|
+
* `ui/shell` e a troca de submenu aqui). Enquanto fosse `.pm-btn` escrito três vezes, bastaria um deles
|
|
236
|
+
* esquecer o `:not([hidden])` para o anel atravessar para a lista invisível — e a criança ouviria itens de um
|
|
237
|
+
* menu que não está na tela.
|
|
238
|
+
*/
|
|
239
|
+
export const PM_ITENS_VISIVEIS = '.pause-menu:not([hidden]) .pm-btn';
|
|
240
|
+
/** O innerHTML de UMA `.pause-menu`: a lista, e só ela. */
|
|
241
|
+
export function pauseMenuHtml(bs, sub, dynLabel, tr) {
|
|
242
|
+
return '<div class="pause-menu" role="menu" data-sub="' + sub + '"' + (sub === 'raiz' ? '' : ' hidden') + '>' +
|
|
243
|
+
bs.map((b) => pmBtnMarkup(b, dynLabel, tr)).join('') + '</div>';
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Troca a lista visível de UM cartão de pausa, e põe o cursor no PRIMEIRO item da lista que entrou.
|
|
247
|
+
*
|
|
248
|
+
* Livre (e não um método do `init`) de propósito: `ui/menu-nav` precisa dela para o "não" voltar da lista de
|
|
249
|
+
* opções à raiz, e não tem acesso às tabelas de botões. Como as duas listas já existem no markup, a troca é
|
|
250
|
+
* só DOM — nada a re-renderizar, nada a injetar.
|
|
251
|
+
*/
|
|
252
|
+
export function mostrarSubmenuDaPausa(sp, sub) {
|
|
253
|
+
sp.querySelectorAll('.pause-menu').forEach((m) => { m.hidden = m.dataset.sub !== sub; });
|
|
254
|
+
const primeiro = sp.querySelector(PM_ITENS_VISIVEIS);
|
|
255
|
+
sp.querySelectorAll('.pm-sel,.pi-sel').forEach((b) => b.classList.remove('pm-sel', 'pi-sel'));
|
|
256
|
+
if (primeiro)
|
|
257
|
+
primeiro.classList.add('pm-sel');
|
|
258
|
+
return primeiro;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* A BARRA RÁPIDA DE ACESSIBILIDADE — dez alternadores e a legenda que os explica (ADR-0044, item 7).
|
|
262
|
+
*
|
|
263
|
+
* Saiu do cartão de pausa e passou a viver no HUD. O motivo é de uso, não de arrumação: é DURANTE a partida
|
|
264
|
+
* que uma criança precisa mudar um ajuste que está a atrapalhando, e não depois de pausar. E o motivo
|
|
265
|
+
* secundário é estrutural — sem ela, o cartão deixa de ter duas zonas e vira uma LISTA, o que é o que
|
|
266
|
+
* finalmente autoriza o anel (a XAG 106 permite laço para menu linear e o proíbe para grade).
|
|
267
|
+
*
|
|
268
|
+
* A LEGENDA VIAJA JUNTO. Ela é a dica que substitui, para quem não vê, o `title` que só o mouse revela;
|
|
269
|
+
* deixá-la no cartão tornaria a barra do HUD muda.
|
|
270
|
+
*/
|
|
271
|
+
export function quickBarMarkup() {
|
|
272
|
+
return '<div class="pause-icons" role="group" aria-label="' + t('pause.iconBarAria') + '">' + iconsMarkup() +
|
|
273
|
+
'</div><p class="pause-icons-cap" aria-live="polite"></p>';
|
|
274
|
+
}
|
|
275
|
+
export function acaoNaBarra(k, temStart) {
|
|
276
|
+
if (temStart || k.no)
|
|
277
|
+
return 'sair';
|
|
278
|
+
if (k.yes)
|
|
279
|
+
return 'ativar';
|
|
280
|
+
if (k.up || k.down || k.left || k.right)
|
|
281
|
+
return 'andar';
|
|
282
|
+
return 'nada';
|
|
283
|
+
}
|
|
284
|
+
/** The full innerHTML of a `.screen-pause`. Pure — every input is a parameter. */
|
|
285
|
+
export function screenPauseMarkup(o) {
|
|
286
|
+
// O NOME ACESSÍVEL DO DIÁLOGO passa pelo dicionário. Era texto cru, e MEDIDO num jogo em inglês o efeito
|
|
287
|
+
// era este: o título visível dizia "Paused" e o nome do diálogo, "Menu de pausa do jogador 1". Quem enxerga
|
|
288
|
+
// lia em inglês; quem escuta recebia o menu anunciado em português — a mesma assimetria do item 4 do
|
|
289
|
+
// ADR-0044, um nível acima. E `aria-label`, não `aria-labelledby`: o `<h2>` é rótulo VISUAL, e é por isso
|
|
290
|
+
// que escondê-lo num quadro apertado não tira o nome do diálogo de quem escuta.
|
|
291
|
+
return '<div class="pause-card" role="dialog" aria-modal="true" aria-label="' + t('pause.cardAria', { n: o.player + 1 }) + '">' +
|
|
292
|
+
'<h2><span data-i18n="pause.title">' + o.t('pause.title') + '</span>' + (o.numPlayers > 1 ? ' · Jogador ' + (o.player + 1) : '') + '</h2>' +
|
|
293
|
+
pauseMenuHtml(o.pmButtons, 'raiz', o.dynLabel, o.t) +
|
|
294
|
+
pauseMenuHtml(o.optionsButtons, 'opcoes', o.dynLabel, o.t) +
|
|
295
|
+
'<p class="pause-legend"></p></div>';
|
|
296
|
+
}
|
|
297
|
+
// ---------------------------------------------------------------------------------------------
|
|
298
|
+
// DOM-facing shell
|
|
299
|
+
// ---------------------------------------------------------------------------------------------
|
|
300
|
+
export function initPauseIcons(ctx) {
|
|
301
|
+
// TEA level. It lives HERE (game.js's `let calmMode` had no other reader) and is deliberately NOT persisted
|
|
302
|
+
// — verbatim: game.js never wrote it to storage, even though applyCalm() persists `rm` as a side effect.
|
|
303
|
+
let calmMode = 0;
|
|
304
|
+
const P = () => ctx.getPlayers();
|
|
305
|
+
function hasPrivateOutput(i) { return hasPrivateOutputIn(P(), ctx.getNumPlayers(), i); }
|
|
306
|
+
function iconState(i) {
|
|
307
|
+
const p = P()[i] || {};
|
|
308
|
+
const cat = ctx.getAudioCat();
|
|
309
|
+
return {
|
|
310
|
+
modoCego: ctx.getModoCego(),
|
|
311
|
+
ttsOn: !!(cat && cat.tts && cat.tts.on),
|
|
312
|
+
librasOn: ctx.isLibrasOn(),
|
|
313
|
+
calmMode,
|
|
314
|
+
toggleMove: !!p.toggleMove,
|
|
315
|
+
viz: p.viz || '',
|
|
316
|
+
privateOutput: hasPrivateOutput(i),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
// --- TEA ---------------------------------------------------------------------------------
|
|
320
|
+
function applyCalm() {
|
|
321
|
+
const plan = calmMotionPlan(calmMode);
|
|
322
|
+
for (const k of ctx.rmKeys)
|
|
323
|
+
ctx.rm[k] = plan.sceneReduced;
|
|
324
|
+
ctx.saveRM();
|
|
325
|
+
// "silencioso" freezes the character too. Iterates the WHOLE players array (not just numPlayers) — verbatim.
|
|
326
|
+
for (const p of P())
|
|
327
|
+
for (const c of ctx.rmChar)
|
|
328
|
+
p[c.prop] = plan.charFrozen;
|
|
329
|
+
const cat = ctx.getAudioCat();
|
|
330
|
+
for (const k of CALM_AUDIO_CATS) {
|
|
331
|
+
const c = cat && cat[k];
|
|
332
|
+
if (!c)
|
|
333
|
+
continue;
|
|
334
|
+
const next = calmAudioPlan(calmMode, c.vol);
|
|
335
|
+
c.on = next.on;
|
|
336
|
+
c.vol = next.vol;
|
|
337
|
+
ctx.setCatGain(k);
|
|
338
|
+
}
|
|
339
|
+
// TTS/sonar/guarda/guia stay intact on purpose: TEA is about noise, not about losing navigation.
|
|
340
|
+
}
|
|
341
|
+
// --- icon actions (the dispatcher, as a table) ---------------------------------------------
|
|
342
|
+
const ICON_ACTS = {
|
|
343
|
+
blind: () => {
|
|
344
|
+
ctx.setModoCego(!ctx.getModoCego());
|
|
345
|
+
ctx.srSay(t(ctx.getModoCego() ? 'sr.icon.blindOn' : 'sr.icon.blindOff'));
|
|
346
|
+
},
|
|
347
|
+
tts: () => {
|
|
348
|
+
const cat = ctx.getAudioCat();
|
|
349
|
+
if (!cat || !cat.tts)
|
|
350
|
+
return; // a guarda que `iconLabel` e `applyCalm` já tinham e esta ação não
|
|
351
|
+
cat.tts.on = !cat.tts.on;
|
|
352
|
+
ctx.setCatGain('tts');
|
|
353
|
+
if (ctx.reflectTtsPanelEnabled)
|
|
354
|
+
ctx.reflectTtsPanel();
|
|
355
|
+
ctx.srSay(t(cat.tts.on ? 'sr.audio.ttsOn' : 'sr.audio.ttsOff'));
|
|
356
|
+
},
|
|
357
|
+
libras: () => {
|
|
358
|
+
ctx.toggleLibras();
|
|
359
|
+
ctx.srSay(t(ctx.isLibrasOn() ? 'sr.icon.librasOn' : 'sr.icon.librasOff'));
|
|
360
|
+
},
|
|
361
|
+
tea: () => {
|
|
362
|
+
calmMode = nextCalmMode(calmMode);
|
|
363
|
+
applyCalm();
|
|
364
|
+
ctx.srSay(t('sr.icon.tea', { v: t(CALM_NAMES[calmMode]) }));
|
|
365
|
+
},
|
|
366
|
+
altmove: (i) => {
|
|
367
|
+
// verbatim: `players[i].toggleMove` with no `||{}` guard (unlike contrast/cvd below).
|
|
368
|
+
ctx.setToggleMove(i, !P()[i].toggleMove);
|
|
369
|
+
},
|
|
370
|
+
contrast: (i) => {
|
|
371
|
+
const nx = nextContrast((P()[i] || {}).viz);
|
|
372
|
+
ctx.setPlayerViz(i, nx);
|
|
373
|
+
ctx.srSay(t('sr.visual.contrast', { v: t(CONTRAST_LABELS[nx] || 'contrast.off') }));
|
|
374
|
+
},
|
|
375
|
+
cvd: (i) => {
|
|
376
|
+
const nx = nextCvd((P()[i] || {}).viz);
|
|
377
|
+
ctx.setPlayerViz(i, nx.mode);
|
|
378
|
+
ctx.srSay(t('sr.icon.cvd', { v: t(CVD_NAMES[nx.idx]) }));
|
|
379
|
+
},
|
|
380
|
+
};
|
|
381
|
+
function iconAct(k, i) {
|
|
382
|
+
const ic = ICON_BY_KEY.get(k);
|
|
383
|
+
if (ic && ic.soon) {
|
|
384
|
+
ctx.srAlert(t('sr.icon.underConstruction', { nome: t(ic.n) }));
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if ((k === 'blind' || k === 'tts') && !hasPrivateOutput(i)) {
|
|
388
|
+
ctx.srAlert(t('sr.icon.needsPrivateOutput'));
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const act = ICON_ACTS[k];
|
|
392
|
+
if (act)
|
|
393
|
+
act(i);
|
|
394
|
+
}
|
|
395
|
+
// --- reflection --------------------------------------------------------------------------
|
|
396
|
+
function iconLabel(k, i) { return computeIconLabel(k, iconState(i)); }
|
|
397
|
+
function reflectIconBtn(b, i) {
|
|
398
|
+
const k = b.dataset.pi || '';
|
|
399
|
+
const st = iconState(i);
|
|
400
|
+
const v = computeIconVisual(k, st);
|
|
401
|
+
b.classList.remove(...ICON_STATE_CLASSES);
|
|
402
|
+
if (v.calm)
|
|
403
|
+
b.classList.add('pi-calm');
|
|
404
|
+
if (v.cvd)
|
|
405
|
+
b.classList.add(v.cvd);
|
|
406
|
+
b.classList.toggle('pi-on', v.on);
|
|
407
|
+
b.classList.toggle('pi-dis', v.dis);
|
|
408
|
+
b.setAttribute('aria-pressed', String(v.active));
|
|
409
|
+
// `soon` buttons keep the label the markup gave them (same string) — no state to report.
|
|
410
|
+
if (!(ICON_BY_KEY.get(k) || {}).soon)
|
|
411
|
+
b.setAttribute('aria-label', computeIconLabel(k, st));
|
|
412
|
+
}
|
|
413
|
+
function reflectIconsIn(root, i) {
|
|
414
|
+
if (!root)
|
|
415
|
+
return;
|
|
416
|
+
root.querySelectorAll('.pi-btn').forEach((b) => reflectIconBtn(b, i));
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Reflete os ícones de TODAS as telas. Varre as BARRAS e não mais os cartões de pausa: desde o item 7 do
|
|
420
|
+
* ADR-0044 os ícones vivem no HUD, e um cartão de pausa não contém `.pi-btn` nenhum.
|
|
421
|
+
*/
|
|
422
|
+
function reflectPauseIcons() {
|
|
423
|
+
ctx.getA11yBars().forEach((bar, i) => reflectIconsIn(bar, i));
|
|
424
|
+
}
|
|
425
|
+
// --- the pause screen ----------------------------------------------------------------------
|
|
426
|
+
/**
|
|
427
|
+
* Troca a lista visível E ANUNCIA o primeiro item da lista que entrou.
|
|
428
|
+
*
|
|
429
|
+
* O anúncio não é enfeite: quem não enxerga acabou de mudar de menu e o cursor pulou para outro lugar. Sem
|
|
430
|
+
* a fala, a única pista de que a tela mudou seria o silêncio. O índice "N de M" vem junto (item 3), e é ele
|
|
431
|
+
* que diz de quantos itens é a lista nova.
|
|
432
|
+
*/
|
|
433
|
+
function anunciarLista(sp, sub) {
|
|
434
|
+
const primeiro = mostrarSubmenuDaPausa(sp, sub);
|
|
435
|
+
if (!primeiro)
|
|
436
|
+
return;
|
|
437
|
+
const itens = [...sp.querySelectorAll(PM_ITENS_VISIVEIS)];
|
|
438
|
+
ctx.srSay(anunciarItem({ rotulo: primeiro.textContent || '', posicao: 1, total: itens.length }, menuIndexOn));
|
|
439
|
+
}
|
|
440
|
+
/* ===================== O MODO `accessibility` (ADR-0044, item 7) ===================== */
|
|
441
|
+
/**
|
|
442
|
+
* Quem está com o direcional dirigindo a BARRA em vez do personagem.
|
|
443
|
+
*
|
|
444
|
+
* Vida de RODADA (ADR-0038): mora no closure desta instância, não é persistido, e some com a partida. Um
|
|
445
|
+
* modo de entrada que sobrevivesse ao reinício seria a armadilha voltando pela porta dos fundos — a criança
|
|
446
|
+
* abriria o jogo no dia seguinte e o personagem não andaria.
|
|
447
|
+
*/
|
|
448
|
+
const naBarra = new Set();
|
|
449
|
+
/** O cursor da barra da tela `i`, ou o primeiro ícone quando ainda não há cursor. */
|
|
450
|
+
function iconeSelecionado(bar) {
|
|
451
|
+
return bar.querySelector('.pi-sel') || bar.querySelector('.pi-btn');
|
|
452
|
+
}
|
|
453
|
+
/** Põe o cursor num ícone, escreve a legenda e ANUNCIA — a legenda é o canal de quem não vê o ícone. */
|
|
454
|
+
function selecionarIcone(bar, el) {
|
|
455
|
+
bar.querySelectorAll('.pi-sel').forEach((x) => x.classList.remove('pi-sel'));
|
|
456
|
+
el.classList.add('pi-sel');
|
|
457
|
+
const cap = bar.querySelector('.pause-icons-cap');
|
|
458
|
+
if (cap)
|
|
459
|
+
cap.textContent = legendaDoIcone(bar, el);
|
|
460
|
+
ctx.srSay(legendaDoIcone(bar, el));
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* ENTRA no modo: o direcional passa a dirigir a barra da tela `i`, e o jogo VOLTA a rodar.
|
|
464
|
+
*
|
|
465
|
+
* O anúncio diz como SAIR, e diz na hora de entrar. É a linha que desarma a armadilha que o próprio
|
|
466
|
+
* ADR-0044 anotou como consequência negativa desta decisão: quem não enxerga aperta a direção, o personagem
|
|
467
|
+
* não anda, e sem esta frase não há nada na tela que explique — porque a tela não é o canal dessa criança.
|
|
468
|
+
*/
|
|
469
|
+
function entrarNaBarra(i) {
|
|
470
|
+
const bar = ctx.getA11yBars()[i];
|
|
471
|
+
const primeiro = bar && iconeSelecionado(bar);
|
|
472
|
+
if (!bar || !primeiro)
|
|
473
|
+
return;
|
|
474
|
+
naBarra.add(i);
|
|
475
|
+
const acts = ctx.getPauseActs();
|
|
476
|
+
if (acts.resume)
|
|
477
|
+
acts.resume(); // volta à tela normal: o modo é para usar DURANTE a partida
|
|
478
|
+
ctx.srSay(t('sr.a11y.barEnter'));
|
|
479
|
+
selecionarIcone(bar, primeiro);
|
|
480
|
+
}
|
|
481
|
+
/** SAI do modo e devolve o direcional ao personagem. Anuncia, porque a devolução também é informação. */
|
|
482
|
+
function sairDaBarra(i) {
|
|
483
|
+
if (!naBarra.delete(i))
|
|
484
|
+
return;
|
|
485
|
+
const bar = ctx.getA11yBars()[i];
|
|
486
|
+
if (bar) {
|
|
487
|
+
bar.querySelectorAll('.pi-sel').forEach((x) => x.classList.remove('pi-sel'));
|
|
488
|
+
const cap = bar.querySelector('.pause-icons-cap');
|
|
489
|
+
if (cap)
|
|
490
|
+
cap.textContent = '';
|
|
491
|
+
}
|
|
492
|
+
ctx.srSay(t('sr.a11y.barExit'));
|
|
493
|
+
}
|
|
494
|
+
/** A tela `i` está com o direcional na barra? É o que o roteamento de entrada pergunta a cada quadro. */
|
|
495
|
+
const naBarraDe = (i) => naBarra.has(i);
|
|
496
|
+
/**
|
|
497
|
+
* UM PASSO dentro do modo. `temStart` é a borda do botão que abre a pausa — a segunda saída.
|
|
498
|
+
*
|
|
499
|
+
* A barra é uma fileira, então as QUATRO direções andam nela: para quem navega sem ver, "cima" numa lista
|
|
500
|
+
* de uma linha só não pode ser um beco. E anda em ANEL, como todo menu do jogo desde o item 1.
|
|
501
|
+
*/
|
|
502
|
+
function navBar(i, k, temStart = false) {
|
|
503
|
+
if (!naBarra.has(i))
|
|
504
|
+
return;
|
|
505
|
+
const bar = ctx.getA11yBars()[i];
|
|
506
|
+
if (!bar)
|
|
507
|
+
return;
|
|
508
|
+
const acao = acaoNaBarra(k, temStart);
|
|
509
|
+
if (acao === 'sair') {
|
|
510
|
+
sairDaBarra(i);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
const icones = [...bar.querySelectorAll('.pi-btn')];
|
|
514
|
+
if (!icones.length)
|
|
515
|
+
return;
|
|
516
|
+
const cur = iconeSelecionado(bar);
|
|
517
|
+
const idx = cur ? icones.indexOf(cur) : 0;
|
|
518
|
+
if (acao === 'ativar') {
|
|
519
|
+
ctx.setPauseActor(i);
|
|
520
|
+
if (cur)
|
|
521
|
+
cur.click();
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
if (acao === 'andar') {
|
|
525
|
+
const d = (k.down || k.right) ? 1 : -1;
|
|
526
|
+
selecionarIcone(bar, icones[passoNoAnel(icones.length, idx, d)]);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
function buildScreenPause(i) {
|
|
530
|
+
const sp = document.createElement('div');
|
|
531
|
+
sp.className = 'screen-pause';
|
|
532
|
+
sp.hidden = true;
|
|
533
|
+
sp.dataset.player = String(i);
|
|
534
|
+
sp.innerHTML = screenPauseMarkup({
|
|
535
|
+
player: i, numPlayers: ctx.getNumPlayers(), pmButtons: ctx.pmButtons, optionsButtons: ctx.optionsButtons,
|
|
536
|
+
dynLabel: ctx.dynLabel, t,
|
|
537
|
+
});
|
|
538
|
+
sp.addEventListener('click', (e) => {
|
|
539
|
+
const target = e.target;
|
|
540
|
+
const b = target && target.closest('.pm-btn');
|
|
541
|
+
if (b) {
|
|
542
|
+
ctx.setPauseActor(i);
|
|
543
|
+
const act = b.dataset.act || '';
|
|
544
|
+
// NAVEGAÇÃO DENTRO DO CARTÃO fica aqui, e não na tabela de ações: `options` e `pmback` não fazem nada
|
|
545
|
+
// ao jogo — trocam qual lista está na tela. A tabela vive em `ui/shell`, que não conhece este `sp`.
|
|
546
|
+
if (act === 'options' || act === 'pmback') {
|
|
547
|
+
anunciarLista(sp, act === 'options' ? 'opcoes' : 'raiz');
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
// `acessibilidade` leva o cursor à BARRA RÁPIDA. Enquanto ela mora dentro do cartão, "entrar no modo"
|
|
551
|
+
// é pôr o cursor nela — e a saída continua sendo a saída da pausa, que é a mesma de sempre. Quando o
|
|
552
|
+
// item 7 levar a barra para o HUD, esta linha o segue; o que o item SIGNIFICA não muda.
|
|
553
|
+
if (act === 'acessibilidade') {
|
|
554
|
+
entrarNaBarra(i);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
const acts = ctx.getPauseActs();
|
|
558
|
+
const fn = acts[act];
|
|
559
|
+
if (fn)
|
|
560
|
+
fn();
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
return sp;
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* A BARRA RÁPIDA de uma tela: os dez alternadores, a legenda, e a fiação dos dois.
|
|
568
|
+
*
|
|
569
|
+
* Ela é IRMÃ da `.screen-exp` e não filha, e isso é a decisão do #82 aplicada: a barra é CONTROLE, não
|
|
570
|
+
* experiência. O modo empatia degrada a experiência de propósito — simulação é criar dificuldade onde a
|
|
571
|
+
* facilidade não existe —, e degradar o que existe para DAR acesso seria o contrário do que ele serve.
|
|
572
|
+
*/
|
|
573
|
+
function buildQuickBar(i) {
|
|
574
|
+
const bar = document.createElement('div');
|
|
575
|
+
bar.className = 'screen-a11y';
|
|
576
|
+
bar.dataset.player = String(i);
|
|
577
|
+
bar.innerHTML = quickBarMarkup();
|
|
578
|
+
// FORA DA ORDEM DE TABULAÇÃO durante a partida (ADR-0044, item 7). Dez paradas entre a criança e o jogo
|
|
579
|
+
// seria o preço de deixá-los lá — e o alcance por teclado não se perde: ele passa a ser o modo
|
|
580
|
+
// `accessibility`, que se abre pela pausa. A barra do TÍTULO não é afetada: lá não se está jogando, e o
|
|
581
|
+
// `tabindex` é posto AQUI, no elemento, e não no markup que as duas compartilham.
|
|
582
|
+
bar.querySelectorAll('.pi-btn').forEach((b) => { b.tabIndex = -1; });
|
|
583
|
+
const cap = bar.querySelector('.pause-icons-cap');
|
|
584
|
+
bar.addEventListener('click', (e) => {
|
|
585
|
+
const ib = e.target?.closest('.pi-btn');
|
|
586
|
+
if (!ib)
|
|
587
|
+
return;
|
|
588
|
+
ctx.setPauseActor(i);
|
|
589
|
+
iconAct(ib.dataset.pi || '', i);
|
|
590
|
+
reflectPauseIcons(); // must run BEFORE reading the label back — that is what makes the caption honest
|
|
591
|
+
if (cap)
|
|
592
|
+
cap.textContent = legendaDoIcone(bar, ib);
|
|
593
|
+
});
|
|
594
|
+
// Legenda = o `aria-label` do botão, para que passar o mouse ou focar diga a MESMA verdade que um leitor
|
|
595
|
+
// de tela anunciaria. Uma fonte só para quem vê e para quem escuta.
|
|
596
|
+
//
|
|
597
|
+
// E ELA SOME AO SAIR. Antes ficava: a última explicação apontada permanecia por cima do jogo até alguém
|
|
598
|
+
// apontar outra. Numa barra que agora vive na TELA DE JOGO isso é uma faixa de texto parada em cima da
|
|
599
|
+
// partida — o Dev viu e disse o que é: explicação só enquanto o mouse estiver no botão.
|
|
600
|
+
//
|
|
601
|
+
// A EXCEÇÃO É O CURSOR DO MODO `accessibility`: quando ele está pousado num ícone, a legenda é a única
|
|
602
|
+
// coisa que diz onde ele está, e apagá-la ao mexer o mouse cegaria o modo. Daí a pergunta pelo `.pi-sel`.
|
|
603
|
+
const limpar = () => { if (cap && !bar.querySelector('.pi-sel'))
|
|
604
|
+
cap.textContent = ''; };
|
|
605
|
+
bar.querySelectorAll('.pi-btn').forEach((b) => {
|
|
606
|
+
const show = () => { if (cap)
|
|
607
|
+
cap.textContent = legendaDoIcone(bar, b); };
|
|
608
|
+
b.addEventListener('mouseenter', show);
|
|
609
|
+
b.addEventListener('focus', show);
|
|
610
|
+
b.addEventListener('mouseleave', limpar);
|
|
611
|
+
b.addEventListener('blur', limpar);
|
|
612
|
+
});
|
|
613
|
+
return bar;
|
|
614
|
+
}
|
|
615
|
+
return {
|
|
616
|
+
buildScreenPause, buildQuickBar, entrarNaBarra, sairDaBarra, naBarraDe, navBar,
|
|
617
|
+
iconAct, iconLabel, reflectIconBtn, reflectIconsIn, reflectPauseIcons,
|
|
618
|
+
applyCalm, getCalmMode: () => calmMode, setCalmMode: (n) => { calmMode = n; }, iconState,
|
|
619
|
+
};
|
|
620
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
import type { Alcance } from '../input/transports.js';
|
|
3
|
+
/** `core/i18n.t` — injetado para o núcleo continuar puro e para o teste poder ver as chaves cruas. */
|
|
4
|
+
export type Tradutor = (chave: string, params?: Record<string, string | number>) => string;
|
|
5
|
+
/**
|
|
6
|
+
* AS FRASES DO AVISO, na ordem em que são lidas. Vazio = não há nada a dizer, e é o caso comum.
|
|
7
|
+
*
|
|
8
|
+
* ⚠️ A ÚLTIMA LINHA TEM DUAS FORMAS, e a diferença entre elas é a diferença entre ajudar e mentir:
|
|
9
|
+
*
|
|
10
|
+
* · há transporte que serviria se ligado → «ligue um controle», que é acionável.
|
|
11
|
+
* · não há nenhum → dizer «ligue um controle» seria mandar a criança procurar uma coisa que não resolve.
|
|
12
|
+
* Nesse caso o problema é do JOGO, que pede mais posições do que qualquer transporte deste aparelho
|
|
13
|
+
* oferece, e a frase honesta é outra.
|
|
14
|
+
*/
|
|
15
|
+
export declare function linhasDoAviso(a: Alcance, t: Tradutor): string[];
|
|
16
|
+
export interface ReachNoticeCtx {
|
|
17
|
+
/** `querySelector` do documento deste jogo. */
|
|
18
|
+
procurar: (sel: string) => HTMLElement | null;
|
|
19
|
+
/** `document.createElement`. Injetado como tudo o mais que toca o documento. */
|
|
20
|
+
criar: (tag: string) => HTMLElement;
|
|
21
|
+
t: Tradutor;
|
|
22
|
+
/** Anúncio assertivo. Uma criança cega tem de OUVIR isto — ela não vai ver o cartão. */
|
|
23
|
+
srAlert: (texto: string) => void;
|
|
24
|
+
}
|
|
25
|
+
/** O id do cartão. Estável porque o teste e a folha de estilo o procuram. */
|
|
26
|
+
export declare const REACH_NOTICE_ID = "reach-notice";
|
|
27
|
+
/**
|
|
28
|
+
* Mostra o aviso, se houver o que dizer. Devolve `true` quando mostrou.
|
|
29
|
+
*
|
|
30
|
+
* ⚠️ MONTA POR NÓS E NÃO POR `innerHTML`. As frases passam por `t()` e um dicionário é conteúdo que muda sem
|
|
31
|
+
* passar por revisão de código — é exatamente a fronteira que a issue #106 mapeia. `textContent` fecha a
|
|
32
|
+
* questão sem precisar de escapar nada.
|
|
33
|
+
*/
|
|
34
|
+
export declare function mostrarAvisoDeAlcance(ctx: ReachNoticeCtx, a: Alcance): boolean;
|