@the-inclusionist/engine 10.0.0 → 11.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -1
- package/app/css/style.css +216 -42
- package/app/public/vendor/fonts-licences/NOTICE.txt +166 -0
- package/app/public/vendor/fonts-licences/OFL-1.1.txt +86 -0
- package/app/public/vendor/fonts.css +11 -1175
- package/dist-pkg/boot/create-game.d.ts +84 -33
- package/dist-pkg/boot/create-game.js +708 -307
- package/dist-pkg/core/a11y-sr.d.ts +1 -1
- package/dist-pkg/core/a11y-sr.js +3 -5
- package/dist-pkg/core/accommodations.d.ts +26 -8
- package/dist-pkg/core/accommodations.js +27 -10
- package/dist-pkg/core/actions.d.ts +39 -10
- package/dist-pkg/core/actions.js +28 -6
- package/dist-pkg/core/contract.d.ts +0 -2
- package/dist-pkg/core/contract.js +0 -2
- package/dist-pkg/core/i18n.d.ts +36 -71
- package/dist-pkg/core/i18n.js +131 -198
- package/dist-pkg/core/session-clock.d.ts +23 -0
- package/dist-pkg/core/session-clock.js +37 -0
- package/dist-pkg/i18n/en.js +44 -49
- package/dist-pkg/i18n/es.js +44 -49
- package/dist-pkg/i18n/pt.js +49 -54
- package/dist-pkg/input/gamepad.d.ts +42 -11
- package/dist-pkg/input/gamepad.js +50 -15
- package/dist-pkg/input/key-default.d.ts +30 -0
- package/dist-pkg/input/key-default.js +60 -0
- package/dist-pkg/input/keyboard-runtime.d.ts +0 -4
- package/dist-pkg/input/keyboard.d.ts +0 -1
- package/dist-pkg/input/keydown.d.ts +5 -11
- package/dist-pkg/input/keydown.js +3 -3
- package/dist-pkg/input/latch-edge.d.ts +10 -2
- package/dist-pkg/input/latch-edge.js +4 -3
- package/dist-pkg/input/latch-scope.d.ts +27 -22
- package/dist-pkg/input/latch-scope.js +25 -37
- package/dist-pkg/input/latch-store.d.ts +9 -12
- package/dist-pkg/input/latch-store.js +13 -18
- package/dist-pkg/input/latch-sync.d.ts +5 -4
- package/dist-pkg/input/latch-sync.js +5 -5
- package/dist-pkg/input/pad-reading.js +7 -2
- package/dist-pkg/input/pad-wizard.d.ts +4 -1
- package/dist-pkg/input/pad-wizard.js +60 -9
- package/dist-pkg/input/switch-scan.d.ts +29 -12
- package/dist-pkg/input/switch-scan.js +15 -2
- package/dist-pkg/input/touch-bindings.d.ts +3 -6
- package/dist-pkg/input/touch.d.ts +17 -2
- package/dist-pkg/input/touch.js +55 -17
- package/dist-pkg/input/transport-in-use.d.ts +5 -5
- package/dist-pkg/input/transport-in-use.js +5 -5
- package/dist-pkg/input/transports.d.ts +0 -9
- package/dist-pkg/input/transports.js +1 -1
- package/dist-pkg/input/virtual-controller.d.ts +39 -3
- package/dist-pkg/input/virtual-controller.js +32 -4
- package/dist-pkg/input/voice-map.d.ts +27 -11
- package/dist-pkg/input/voice-map.js +41 -43
- package/dist-pkg/platform/audio-ambient.d.ts +0 -1
- package/dist-pkg/platform/audio-ambient.js +3 -1
- package/dist-pkg/platform/audio-mixer.js +4 -9
- package/dist-pkg/platform/audio-sonar.d.ts +8 -94
- package/dist-pkg/platform/audio-sonar.js +22 -207
- package/dist-pkg/platform/audio.d.ts +10 -1
- package/dist-pkg/platform/audio.js +35 -2
- package/dist-pkg/platform/choose-by-voice.d.ts +70 -0
- package/dist-pkg/platform/choose-by-voice.js +157 -0
- package/dist-pkg/platform/font-library.d.ts +77 -0
- package/dist-pkg/platform/font-library.js +98 -0
- package/dist-pkg/platform/font-library.json +721 -0
- package/dist-pkg/platform/heavy-catalogue.d.ts +46 -0
- package/dist-pkg/platform/heavy-catalogue.js +47 -14
- package/dist-pkg/platform/heavy-mirror.d.ts +3 -9
- package/dist-pkg/platform/heavy-mirror.js +14 -17
- package/dist-pkg/platform/heavy.d.ts +40 -8
- package/dist-pkg/platform/heavy.js +177 -22
- package/dist-pkg/platform/interruptible-speech.d.ts +15 -8
- package/dist-pkg/platform/interruptible-speech.js +37 -13
- package/dist-pkg/platform/listener-scope.js +22 -13
- package/dist-pkg/platform/locale-host.d.ts +7 -3
- package/dist-pkg/platform/locale-host.js +13 -2
- package/dist-pkg/platform/reading-in-worker.d.ts +24 -0
- package/dist-pkg/platform/reading-in-worker.js +38 -0
- package/dist-pkg/platform/reading-model.d.ts +21 -0
- package/dist-pkg/platform/reading-model.js +25 -0
- package/dist-pkg/platform/reading-runtime.js +13 -8
- package/dist-pkg/platform/reading.d.ts +12 -1
- package/dist-pkg/platform/reading.js +9 -1
- package/dist-pkg/platform/speech-recognition.d.ts +10 -1
- package/dist-pkg/platform/speech-recognition.js +29 -9
- package/dist-pkg/platform/speech.d.ts +10 -1
- package/dist-pkg/platform/speech.js +24 -13
- package/dist-pkg/platform/tts.d.ts +37 -4
- package/dist-pkg/platform/tts.js +279 -57
- package/dist-pkg/platform/voice-listener.d.ts +6 -2
- package/dist-pkg/platform/voice-listener.js +1 -1
- package/dist-pkg/platform/voice-plan.d.ts +13 -0
- package/dist-pkg/platform/voice-plan.js +57 -2
- package/dist-pkg/platform/vosk-runtime.d.ts +25 -0
- package/dist-pkg/platform/vosk-runtime.js +59 -1
- package/dist-pkg/platform/vosk-vocabulary.d.ts +6 -0
- package/dist-pkg/platform/vosk-vocabulary.js +178 -0
- package/dist-pkg/render/crt.d.ts +11 -9
- package/dist-pkg/render/crt.js +6 -10
- package/dist-pkg/render/cvd-matrices.d.ts +5 -1
- package/dist-pkg/render/cvd-matrices.js +5 -1
- package/dist-pkg/render/high-contrast.d.ts +1 -2
- package/dist-pkg/render/high-contrast.js +5 -1
- package/dist-pkg/render/viz-axes.d.ts +9 -0
- package/dist-pkg/render/viz-axes.js +15 -2
- package/dist-pkg/ui/aac-sets.d.ts +29 -24
- package/dist-pkg/ui/aac-sets.js +22 -30
- package/dist-pkg/ui/audio-choices.d.ts +10 -5
- package/dist-pkg/ui/audio-choices.js +14 -10
- package/dist-pkg/ui/camera-control.d.ts +47 -0
- package/dist-pkg/ui/camera-control.js +35 -1
- package/dist-pkg/ui/declared-words.d.ts +21 -0
- package/dist-pkg/ui/declared-words.js +66 -0
- package/dist-pkg/ui/drawing-problems.js +4 -3
- package/dist-pkg/ui/eye-control.js +17 -17
- package/dist-pkg/ui/face-control.js +15 -15
- package/dist-pkg/ui/fonts.d.ts +42 -8
- package/dist-pkg/ui/fonts.js +58 -24
- package/dist-pkg/ui/footer-scroll-driver.d.ts +14 -0
- package/dist-pkg/ui/footer-scroll-driver.js +201 -0
- package/dist-pkg/ui/footer-scroll.d.ts +38 -0
- package/dist-pkg/ui/footer-scroll.js +55 -0
- package/dist-pkg/ui/game-options.d.ts +14 -9
- package/dist-pkg/ui/game-options.js +41 -21
- package/dist-pkg/ui/hand-control.js +15 -15
- package/dist-pkg/ui/help-panel.d.ts +19 -8
- package/dist-pkg/ui/help-panel.js +15 -5
- package/dist-pkg/ui/hud-bands.d.ts +29 -12
- package/dist-pkg/ui/hud-bands.js +63 -25
- package/dist-pkg/ui/hud-row.d.ts +34 -0
- package/dist-pkg/ui/hud-row.js +80 -0
- package/dist-pkg/ui/hud.d.ts +0 -3
- package/dist-pkg/ui/layout.js +5 -2
- package/dist-pkg/ui/libras-avatar-clip.d.ts +30 -0
- package/dist-pkg/ui/libras-avatar-clip.js +86 -0
- package/dist-pkg/ui/libras-avatar-load.d.ts +52 -0
- package/dist-pkg/ui/libras-avatar-load.js +134 -0
- package/dist-pkg/ui/libras-avatar-plan.d.ts +102 -0
- package/dist-pkg/ui/libras-avatar-plan.js +211 -0
- package/dist-pkg/ui/libras-avatar-player.d.ts +28 -0
- package/dist-pkg/ui/libras-avatar-player.js +226 -0
- package/dist-pkg/ui/libras-avatar-stage.d.ts +37 -0
- package/dist-pkg/ui/libras-avatar-stage.js +208 -0
- package/dist-pkg/ui/libras-glosses.d.ts +28 -0
- package/dist-pkg/ui/libras-glosses.js +161 -0
- package/dist-pkg/ui/menu-intent.d.ts +29 -0
- package/dist-pkg/ui/menu-intent.js +35 -1
- package/dist-pkg/ui/menu-items.d.ts +10 -0
- package/dist-pkg/ui/menu-items.js +29 -0
- package/dist-pkg/ui/menu-nav.d.ts +40 -4
- package/dist-pkg/ui/menu-nav.js +132 -47
- package/dist-pkg/ui/panel-widgets.d.ts +34 -0
- package/dist-pkg/ui/panel-widgets.js +86 -0
- package/dist-pkg/ui/pause-icons.d.ts +30 -14
- package/dist-pkg/ui/pause-icons.js +106 -45
- package/dist-pkg/ui/scan-overlay.d.ts +3 -3
- package/dist-pkg/ui/scan-overlay.js +11 -3
- package/dist-pkg/ui/screen-text.d.ts +25 -0
- package/dist-pkg/ui/screen-text.js +152 -0
- package/dist-pkg/ui/session-clock.d.ts +32 -0
- package/dist-pkg/ui/session-clock.js +67 -0
- package/dist-pkg/ui/settings-aac.d.ts +3 -1
- package/dist-pkg/ui/settings-aac.js +32 -13
- package/dist-pkg/ui/settings-audio.d.ts +10 -5
- package/dist-pkg/ui/settings-audio.js +101 -35
- package/dist-pkg/ui/settings-controls.d.ts +0 -4
- package/dist-pkg/ui/settings-mobility.d.ts +3 -6
- package/dist-pkg/ui/settings-mobility.js +4 -46
- package/dist-pkg/ui/settings-motion.js +2 -2
- package/dist-pkg/ui/settings-panel.d.ts +10 -0
- package/dist-pkg/ui/settings-panel.js +25 -1
- package/dist-pkg/ui/settings-typo.d.ts +5 -6
- package/dist-pkg/ui/settings-typo.js +20 -11
- package/dist-pkg/ui/settings-visual.js +2 -2
- package/dist-pkg/ui/shell.d.ts +1 -3
- package/dist-pkg/ui/shell.js +8 -4
- package/dist-pkg/ui/title.d.ts +2 -11
- package/dist-pkg/ui/title.js +39 -6
- package/dist-pkg/ui/top-band.d.ts +5 -3
- package/dist-pkg/ui/top-band.js +21 -25
- package/dist-pkg/ui/typo-choices.d.ts +3 -3
- package/dist-pkg/ui/typo-choices.js +9 -8
- package/dist-pkg/ui/vlibras.d.ts +71 -27
- package/dist-pkg/ui/vlibras.js +116 -44
- package/dist-pkg/ui/voice-control.d.ts +23 -1
- package/dist-pkg/ui/voice-control.js +149 -16
- package/dist-pkg/ui/voice-settings.d.ts +2 -3
- package/dist-pkg/ui/voice-settings.js +53 -3
- package/dist-pkg/ui/where-the-child-is.js +2 -2
- package/docs/CREDITS.md +122 -10
- package/docs/LICENSES.md +36 -7
- package/package.json +34 -9
- package/scripts/check-cartridge.mjs +78 -0
- package/scripts/game-build.d.mts +25 -0
- package/scripts/game-build.mjs +144 -0
- package/scripts/heavy-into-the-delivery.mjs +284 -40
- package/scripts/libras-avatar.json +680 -0
- package/scripts/libras-avatar.mjs +246 -0
- package/scripts/libras-glosses/gloss.py +95 -0
- package/scripts/libras-glosses/pyproject.toml +32 -0
- package/scripts/libras-glosses/uv.lock +781 -0
- package/scripts/libras-glosses.mjs +351 -0
- package/scripts/licences/OFL-1.1.txt +86 -0
- package/scripts/licences/UFL-1.0.txt +96 -0
- package/scripts/licences/fonts.mjs +389 -0
- package/scripts/licences/third-party.mjs +35 -21
- package/app/public/vendor/fonts/bitter-var-ext.woff2 +0 -0
- package/app/public/vendor/fonts/bitter-var.woff2 +0 -0
- package/app/public/vendor/fonts/bodonimoda-var-ext.woff2 +0 -0
- package/app/public/vendor/fonts/bodonimoda-var.woff2 +0 -0
- package/app/public/vendor/fonts/cat-abril_fatface-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-abril_fatface-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-adlam_display-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-adlam_display-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-agu_display-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-agu_display-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-alfa_slab_one-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-alfa_slab_one-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-are_you_serious-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-are_you_serious-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-asimovian-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-asimovian-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-atkinson_hyperlegible_next-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-atkinson_hyperlegible_next-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-bakbak_one-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-bakbak_one-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-bangers-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-bangers-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-barriecito-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-barriecito-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-berkshire_swash-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-berkshire_swash-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-bevan-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-bevan-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-bitcount-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-bitcount-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-bowlby_one_sc-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-bowlby_one_sc-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-bungee_shade-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-bungee_shade-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-charm-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-charm-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-charm-700-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-charm-700.woff2 +0 -0
- package/app/public/vendor/fonts/cat-cherry_bomb_one-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-cherry_bomb_one-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-chokokutai-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-chokokutai-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-chonburi-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-chonburi-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-climate_crisis-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-climate_crisis-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-cookie-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-dela_gothic_one-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-dela_gothic_one-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-fascinate_inline-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-fascinate_inline-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-ga_maamli-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-ga_maamli-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-itim-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-itim-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-jacquard_24-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-jacquard_24-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-kablammo-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-kablammo-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-kalam-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-kalam-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-kalam-700-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-kalam-700.woff2 +0 -0
- package/app/public/vendor/fonts/cat-katibeh-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-katibeh-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-lemonada-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-lemonada-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-lilita_one-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-lilita_one-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-lobster-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-lobster-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-luckiest_guy-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-luckiest_guy-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-marhey-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-marhey-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-modak-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-modak-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-momo_signature-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-momo_signature-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-monofett-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-monofett-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-monomakh-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-monomakh-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-monoton-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-monoton-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-nerko_one-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-nerko_one-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-noto_sans_adlam-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-noto_sans_adlam-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-noto_sans_arabic-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-noto_sans_arabic-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-noto_sans_devanagari-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-noto_sans_devanagari-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-noto_sans_hebrew-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-noto_sans_hebrew-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-noto_sans_jp-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-noto_sans_jp-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-noto_sans_khmer-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-noto_sans_khmer-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-noto_sans_kr-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-noto_sans_kr-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-noto_sans_sc-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-noto_sans_sc-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-noto_sans_thai-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-noto_sans_thai-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-ole-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-ole-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-pacifico-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-pacifico-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-patrick_hand-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-patrick_hand-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-petit_formal_script-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-petit_formal_script-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-pixelify_sans-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-pixelify_sans-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-plaster-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-plaster-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_ar_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_at-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_at_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_au_nsw-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_au_nsw_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_au_qld-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_au_qld_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_au_sa-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_au_sa_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_au_tas-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_au_tas_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_au_vic-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_au_vic_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_be_vlg-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_be_vlg_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_be_wal-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_be_wal_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_br_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_ca_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_cl_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_co_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_cu_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_cz-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_cz_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_de_grund-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_de_grund_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_de_la-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_de_la_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_de_sas-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_de_sas_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_de_va-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_de_va_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_dk_loopet-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_dk_loopet_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_dk_uloopet-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_dk_uloopet_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_es_deco_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_es_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_fr_moderne-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_fr_moderne_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_fr_trad-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_fr_trad_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_gb_j_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_gb_s_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_hr-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_hr_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_hr_lijeva-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_hr_lijeva_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_hu-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_hu_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_id-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_id_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_ie-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_ie_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_in-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_in_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_is-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_is_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_it_moderna-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_it_moderna_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_it_trad-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_it_trad_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_mx_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_ng_modern-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_ng_modern_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_nl-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_nl_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_no-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_no_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_nz-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_nz_basic-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_nz_basic_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_nz_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_pe_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_pl-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_pl_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_pt_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_ro-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_ro_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_sk-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_sk_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_tz-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_tz_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_us_modern_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_us_trad_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_vn-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_vn_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_za-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_za_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-potta_one-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-potta_one-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-ranchers-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-ranchers-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-ranga-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-ranga-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-ranga-700-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-ranga-700.woff2 +0 -0
- package/app/public/vendor/fonts/cat-risque-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-risque-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-rowdies-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-rowdies-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-rowdies-700-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-rowdies-700.woff2 +0 -0
- package/app/public/vendor/fonts/cat-ruge_boogie-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-ruge_boogie-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-ruslan_display-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-ruslan_display-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-sacramento-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-sacramento-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-sancreek-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-sancreek-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-sankofa_display-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-sankofa_display-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-sedgwick_ave_display-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-sedgwick_ave_display-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-shantell_sans-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-shantell_sans-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-smokum-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-smokum-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-solitreo-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-solitreo-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-spicy_rice-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-spicy_rice-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-sriracha-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-sriracha-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-srisakdi-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-srisakdi-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-srisakdi-700-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-srisakdi-700.woff2 +0 -0
- package/app/public/vendor/fonts/cat-stix_two_math-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-tillana-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-tillana-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-tillana-700-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-tillana-700.woff2 +0 -0
- package/app/public/vendor/fonts/cat-titan_one-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-titan_one-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-yatra_one-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-yatra_one-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-yomogi-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-yomogi-400.woff2 +0 -0
- package/app/public/vendor/fonts/dmserifdisplay-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/dmserifdisplay-400.woff2 +0 -0
- package/app/public/vendor/fonts/domine-var-ext.woff2 +0 -0
- package/app/public/vendor/fonts/domine-var.woff2 +0 -0
- package/app/public/vendor/fonts/fondamento-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/fondamento-400.woff2 +0 -0
- package/app/public/vendor/fonts/fraunces-var-ext.woff2 +0 -0
- package/app/public/vendor/fonts/fraunces-var.woff2 +0 -0
- package/app/public/vendor/fonts/fredoka-var-ext.woff2 +0 -0
- package/app/public/vendor/fonts/fredoka-var.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/jakarta-var-ext.woff2 +0 -0
- package/app/public/vendor/fonts/jakarta-var.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/literata-400.woff2 +0 -0
- package/app/public/vendor/fonts/literata-700.woff2 +0 -0
- package/app/public/vendor/fonts/lora-var-ext.woff2 +0 -0
- package/app/public/vendor/fonts/lora-var.woff2 +0 -0
- package/app/public/vendor/fonts/merriweather-var-ext.woff2 +0 -0
- package/app/public/vendor/fonts/merriweather-var.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/notosans-var-ext.woff2 +0 -0
- package/app/public/vendor/fonts/notosans-var.woff2 +0 -0
- package/app/public/vendor/fonts/nunito-var-ext.woff2 +0 -0
- package/app/public/vendor/fonts/nunito-var.woff2 +0 -0
- package/app/public/vendor/fonts/opendyslexic-400.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/playfair-var-ext.woff2 +0 -0
- package/app/public/vendor/fonts/playfair-var.woff2 +0 -0
- package/app/public/vendor/fonts/pressstart-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/quicksand-var-ext.woff2 +0 -0
- package/app/public/vendor/fonts/quicksand-var.woff2 +0 -0
- package/app/public/vendor/fonts/robotoflex-var-ext.woff2 +0 -0
- package/app/public/vendor/fonts/robotoflex-var.woff2 +0 -0
- package/app/public/vendor/fonts/sora-var-ext.woff2 +0 -0
- package/app/public/vendor/fonts/sora-var.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/spacegrotesk-var-ext.woff2 +0 -0
- package/app/public/vendor/fonts/spacegrotesk-var.woff2 +0 -0
- package/app/public/vendor/fonts/spectral-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/spectral-400.woff2 +0 -0
- package/app/public/vendor/fonts/spectral-700-ext.woff2 +0 -0
- package/app/public/vendor/fonts/spectral-700.woff2 +0 -0
- package/app/public/vendor/fonts/teachers-var-ext.woff2 +0 -0
- package/app/public/vendor/fonts/teachers-var.woff2 +0 -0
- package/app/public/vendor/fonts/ubuntu-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/ubuntu-400.woff2 +0 -0
- package/app/public/vendor/fonts/ubuntu-700-ext.woff2 +0 -0
- package/app/public/vendor/fonts/ubuntu-700.woff2 +0 -0
- package/app/public/vendor/fonts/ufmag-400.woff2 +0 -0
- package/dist-pkg/platform/guide-intensity.d.ts +0 -39
- package/dist-pkg/platform/guide-intensity.js +0 -71
- package/dist-pkg/ui/latch-refusal.d.ts +0 -33
- package/dist-pkg/ui/latch-refusal.js +0 -60
|
@@ -22,11 +22,10 @@
|
|
|
22
22
|
//
|
|
23
23
|
// And one finding that only appeared when this function tried to boot against an injected document:
|
|
24
24
|
//
|
|
25
|
-
// · FINDING 15 —
|
|
26
|
-
// it makes no difference, which is how it survived; in a pure-logic test it is the difference between booting
|
|
27
|
-
// not, and with two documents (an engine in an iframe, an editor beside the game) it would be the difference
|
|
28
|
-
// between translating the right document and the other one. Hence `
|
|
29
|
-
// exactly as `applyDom` already did.
|
|
25
|
+
// · FINDING 15 — the language's boot called `applyDom(document)`, the GLOBAL, underneath whoever called it. In a
|
|
26
|
+
// browser it makes no difference, which is how it survived; in a pure-logic test it is the difference between booting
|
|
27
|
+
// and not, and with two documents (an engine in an iframe, an editor beside the game) it would be the difference
|
|
28
|
+
// between translating the right document and the other one. Hence `translator.init(root)`: the document comes in.
|
|
30
29
|
//
|
|
31
30
|
// ========================= DECLINING IS NOT LYING =========================
|
|
32
31
|
// The quiz declined the sonar and the pad instead of inventing tiles and a fake collision box, and that distinction
|
|
@@ -44,7 +43,7 @@
|
|
|
44
43
|
// game with a title screen. What it covers is what is the SAME in every game: language, screen reader, mixer, voice,
|
|
45
44
|
// the dialog stack, the colour filters, the remappable keyboard, menu navigation, the pause card and the accessibility
|
|
46
45
|
// bar, the settings panels, the navigation sonar, and every input transport.
|
|
47
|
-
import
|
|
46
|
+
import { createTranslator, availableLocales } from '../core/i18n.js';
|
|
48
47
|
import { localeHostHooks, exposeI18n } from '../platform/locale-host.js';
|
|
49
48
|
import { createInputState } from '../input/state.js';
|
|
50
49
|
import { initTouch, mountTouchControls, touchGaps } from '../input/touch.js';
|
|
@@ -58,20 +57,22 @@ import { accommodationAnswersProblems, subjectWord } from '../core/accommodation
|
|
|
58
57
|
import { genreProblems, genreWarning } from '../core/genres.js';
|
|
59
58
|
import { cartridgeProblems } from '../core/cartridge-problems.js';
|
|
60
59
|
import { contractSubjects } from '../core/accommodation-subjects.js';
|
|
61
|
-
import { presetActions, startClaimProblem, selectClaimProblem, labellerFrom, shortLabellerFrom, ACTIONS } from '../core/actions.js';
|
|
60
|
+
import { presetActions, startClaimProblem, selectClaimProblem, labellerFrom, shortLabellerFrom, wordsOf, ACTIONS } from '../core/actions.js';
|
|
62
61
|
import { createAnnouncer } from '../core/a11y-sr.js';
|
|
63
62
|
import { createEyeControl, videoFeed } from '../ui/eye-control.js';
|
|
64
63
|
import { createFaceControl } from '../ui/face-control.js';
|
|
65
64
|
import { createHandControl } from '../ui/hand-control.js';
|
|
66
65
|
import { checkedCacheHas, sha256With } from '../platform/heavy.js';
|
|
67
|
-
import { createBundleLoader } from '../platform/vosk-runtime.js';
|
|
66
|
+
import { createBundleLoader, keepVoskModels, loadVoskRuntime } from '../platform/vosk-runtime.js';
|
|
67
|
+
import { createVoiceChooser, openWithCommandRecogniser } from '../platform/choose-by-voice.js';
|
|
68
68
|
import { followCameraMode } from '../ui/camera-control.js';
|
|
69
69
|
import { initPauseIcons, wireBarCaption, showPauseOptions } from '../ui/pause-icons.js';
|
|
70
70
|
// 📌 The bar's markup is a pure string builder and lives with the rest of the pause markup (ADR-0221, issue #203); what this
|
|
71
71
|
// root asks `ui/pause-icons` for is the WIRING — the icons this game can actually act on, and the reflection of their state.
|
|
72
72
|
import { iconsMarkup } from '../ui/pause-markup.js';
|
|
73
73
|
import { accessibleLabel } from '../core/accessible-label.js';
|
|
74
|
-
import { helpRows, mountSlides, showSlide, animateFigure, howToPlayProblems } from '../ui/help-panel.js';
|
|
74
|
+
import { helpRows, mountSlides, showSlide, animateFigure, howToPlayProblems, playSlidesOf } from '../ui/help-panel.js';
|
|
75
|
+
import { missingDeclaredKeys } from '../ui/declared-words.js';
|
|
75
76
|
import { initSettingsControls } from '../ui/settings-controls.js';
|
|
76
77
|
import { keyName } from '../ui/control-choices.js';
|
|
77
78
|
import { reserveTopBand } from '../ui/top-band.js';
|
|
@@ -79,7 +80,9 @@ import { reserveTopBand } from '../ui/top-band.js';
|
|
|
79
80
|
import { createSettingsStore } from '../core/state.js';
|
|
80
81
|
import { DEFAULTS, defaultReducedMotion } from '../core/setting-defaults.js';
|
|
81
82
|
import { CAMERA_CONTROLS } from '../core/camera-cycle.js';
|
|
82
|
-
import {
|
|
83
|
+
import { createDeafMode } from '../ui/vlibras.js';
|
|
84
|
+
import { createLibrasAvatarInterpreter } from '../ui/libras-avatar-player.js';
|
|
85
|
+
import { screenText, unreadableWorldProblems, menuSonarPress } from '../ui/screen-text.js';
|
|
83
86
|
import { conformanceProblems } from '../core/contract.js';
|
|
84
87
|
import { createSceneStack } from '../core/scenes.js';
|
|
85
88
|
import { createTts } from '../platform/tts.js';
|
|
@@ -88,11 +91,12 @@ import { createAudio } from '../platform/audio.js';
|
|
|
88
91
|
import { createAudioSonar } from '../platform/audio-sonar.js';
|
|
89
92
|
// The root is the layer that MAY know both axes: `render/` is below it, and it is the root's job to answer
|
|
90
93
|
// `platform/audio-sonar`, which cannot import from here without inverting an edge (#104).
|
|
91
|
-
import { isBlind, isLowVision, DEFAULT_VISUAL, filterKey, simulationUnavailable } from '../render/viz-axes.js';
|
|
94
|
+
import { isBlind, isLowVision, hasHighContrast, onlyColourVision, DEFAULT_VISUAL, filterKey, simulationUnavailable } from '../render/viz-axes.js';
|
|
92
95
|
// 📌 The mode → `url(#...)` table, which `render/cvd-matrices` installs and the `consumer-quiz` consumes.
|
|
93
96
|
import { VIZ_FILTER } from '../render/viz-modes.js';
|
|
94
97
|
import { createPadWizard, createPadMaps } from '../input/pad-wizard.js';
|
|
95
|
-
import { typographyCycle, CYCLE_START, FONT_BY_KEY } from '../ui/fonts.js';
|
|
98
|
+
import { typographyCycle, CYCLE_START, FONT_BY_KEY, ENGINE_FAMILIES } from '../ui/fonts.js';
|
|
99
|
+
import { declaredFamilies, startLibraryFonts } from '../platform/font-library.js';
|
|
96
100
|
// 📏 The drawing reporters (`barIntruders`, `belowFloor`, `minimumTarget` and their types) live in `ui/drawing-problems`
|
|
97
101
|
// (ADR-0221, issue #203), and the root does not know them. An import that can be deleted is coupling that no longer
|
|
98
102
|
// exists, and that is how this debt is paid: by subject.
|
|
@@ -105,18 +109,22 @@ import { LOGICAL_W } from '../core/constants.js';
|
|
|
105
109
|
import { captionDuration, CAPTION_RATES } from '../core/caption-duration.js';
|
|
106
110
|
import { initSettingsPanel } from '../ui/settings-panel.js';
|
|
107
111
|
import { mountPanel } from '../ui/mount-panel.js';
|
|
108
|
-
// 📌 No `
|
|
109
|
-
// the
|
|
112
|
+
// 📌 No `setMoveLatch` here: the sticky keys are written by the bar's ☝️, which already writes both stored keys. What the root
|
|
113
|
+
// does is RESOLVE the latch when any transport presses (ADR-0109 rule 3, ADR-0249) — `createLatchedEdge`, below.
|
|
114
|
+
import { createLatchedEdge } from '../input/latch-edge.js';
|
|
110
115
|
import { stampSource, sourceOfEvent } from '../input/synthetic-source.js';
|
|
111
116
|
import { createVirtualController } from '../input/virtual-controller.js';
|
|
112
|
-
import {
|
|
117
|
+
import { keyGoesToGame, keyPressesOwnControl, keyTypedIntoField } from '../input/key-default.js';
|
|
118
|
+
import { createSwitchScan, playScanList, SWITCH_SCAN_DEFAULTS } from '../input/switch-scan.js';
|
|
119
|
+
import { menuScanFor, menuStepKeys } from '../ui/menu-intent.js';
|
|
113
120
|
import { mountScanOverlay, scanItemText } from '../ui/scan-overlay.js';
|
|
114
121
|
import { createVoiceControl } from '../ui/voice-control.js';
|
|
115
|
-
import { mountSteps, updateSteps, nextStep, controlRow, labelRow } from '../ui/panel-widgets.js';
|
|
122
|
+
import { mountSteps, updateSteps, nextStep, controlRow, labelRow, mountChoice } from '../ui/panel-widgets.js';
|
|
116
123
|
import { PERSONAS_DO_PAD, closestPersona } from '../input/touch.js';
|
|
117
124
|
import { initSettingsTypo } from '../ui/settings-typo.js';
|
|
118
125
|
import { initSettingsMotion } from '../ui/settings-motion.js';
|
|
119
|
-
import { readStoredScene, storeScene } from '../ui/motion-scene.js';
|
|
126
|
+
import { readStoredScene, storeScene, SCENE_KEYS } from '../ui/motion-scene.js';
|
|
127
|
+
import { watchFooterScroll } from '../ui/footer-scroll-driver.js';
|
|
120
128
|
import { initSettingsVisual } from '../ui/settings-visual.js';
|
|
121
129
|
import { initSettingsEmpathy } from '../ui/settings-empathy.js';
|
|
122
130
|
import { HC_ROLE_DEF } from '../render/hc-role-data.js';
|
|
@@ -129,6 +137,8 @@ import { createEmpathyFilter } from '../input/empathy-filter.js';
|
|
|
129
137
|
import { createInputCooldown, COOLDOWN_MS } from '../input/input-cooldown.js';
|
|
130
138
|
import { markChanged } from '../ui/changed-mark.js';
|
|
131
139
|
import { mountHudBands, hudNumbersProblems } from '../ui/hud-bands.js';
|
|
140
|
+
import { mountSessionClock } from '../ui/session-clock.js';
|
|
141
|
+
import { mountHudRow, reserveBottomBand, followThePad } from '../ui/hud-row.js';
|
|
132
142
|
import { gameOptionsProblems, drawGameOptions } from '../ui/game-options.js';
|
|
133
143
|
import { createStorage, keysOutsideScopes } from '../platform/storage.js';
|
|
134
144
|
import { KEYS } from '../platform/storage-keys.js';
|
|
@@ -142,7 +152,7 @@ import { showOnlyRowsThatApply } from '../ui/audio-rows-that-apply.js';
|
|
|
142
152
|
import { initKeyboardRuntime } from '../input/keyboard-runtime.js';
|
|
143
153
|
import { createKeyboardConfig } from '../input/keyboard.js';
|
|
144
154
|
import { createPadTable } from '../input/pad-defaults.js';
|
|
145
|
-
import { downloadHeavy, heavyAtBoot } from '../platform/heavy.js';
|
|
155
|
+
import { downloadHeavy, heavyAtBoot, deliveryPath } from '../platform/heavy.js';
|
|
146
156
|
import { installCvdFilters } from '../render/cvd-matrices.js';
|
|
147
157
|
/**
|
|
148
158
|
* The backend the host lent, else its window's `localStorage` — or `null` where the window has none or reading it throws
|
|
@@ -158,6 +168,13 @@ function hostStorage(host) {
|
|
|
158
168
|
return null;
|
|
159
169
|
}
|
|
160
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* The interpreter the host lent — it always wins — else the one the DELIVERY carries (`delivered`, built only when needed): the
|
|
173
|
+
* free Libras player where the delivery was built with `--libras`, which answers as `NO_INTERPRETER` where it was not.
|
|
174
|
+
*/
|
|
175
|
+
function hostInterpreter(host, delivered) {
|
|
176
|
+
return host.interpreter ?? delivered();
|
|
177
|
+
}
|
|
161
178
|
/*
|
|
162
179
|
* ONE SENTENCE FOR BOTH REFUSALS.
|
|
163
180
|
*
|
|
@@ -168,56 +185,39 @@ function hostStorage(host) {
|
|
|
168
185
|
function refuseDeclaration(who, problemas) {
|
|
169
186
|
throw new Error(`${who}: declaração malformada — ${problemas.join('; ')}`);
|
|
170
187
|
}
|
|
171
|
-
|
|
172
|
-
*
|
|
188
|
+
/**
|
|
189
|
+
* EVERY LINE `createGame` AND `mount()` REFUSE A CARTRIDGE WITH — one list, read by the boot, by `mount()` and by the
|
|
190
|
+
* cartridge checker the engine ships (`inclusionist-check-cartridge`, ADR-0253). EMPTY means the cartridge holds.
|
|
173
191
|
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
* whoever WRITES a preset. A new sentence in this module would raise its cap to pay for text that belongs to the input
|
|
177
|
-
* vocabulary, not to the boot.
|
|
192
|
+
* 🎯 ONE LIST AND NOT A CHECKER'S COPY OF IT: a checker that re-listed these rules would drift from the boot the first time
|
|
193
|
+
* a rule was added here, and it would then accept a cartridge the platform refuses — the drift ADR-0253 exists to end.
|
|
178
194
|
*
|
|
179
|
-
* ⚠️
|
|
180
|
-
*
|
|
195
|
+
* ⚠️ EACH ROW THROWS AT BOOT, by the rubric of ADR-0169: every one is a PROGRAM defect, never a gap of the host, and
|
|
196
|
+
* `problems` is for what still lets the child play.
|
|
197
|
+
* · the declaration's shape (`core/contract`, ADR-0030);
|
|
198
|
+
* · «start» and «select» belong to the pause, and a cartridge does not take them (ADR-0144 §4, ADR-0155) — the sentences
|
|
199
|
+
* live in `core/actions`, where the validity of a preset lives;
|
|
200
|
+
* · the accommodations are ANSWERED, all of them (ADR-0153): the engine would not know which rows to mount;
|
|
201
|
+
* · a genre outside the engine's list, or Casino game (ADR-0156 §2, §4); an absent genre is conformant;
|
|
202
|
+
* · a malformed `hud`, game options or `howToPlay` (ADR-0169): the engine would not know what to place, draw or show.
|
|
181
203
|
*/
|
|
182
|
-
function
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
function refuseIfNoAnswer(who, answers) {
|
|
194
|
-
const problemas = accommodationAnswersProblems(answers);
|
|
195
|
-
if (problemas.length)
|
|
196
|
-
refuseDeclaration(who, problemas);
|
|
197
|
-
}
|
|
198
|
-
/** A genre outside the engine's list, or Casino game, refuses the boot (ADR-0156 §2, §4); an absent genre is conformant. */
|
|
199
|
-
function refuseIfGenreRefused(who, genre) {
|
|
200
|
-
const problemas = genreProblems(genre);
|
|
201
|
-
if (problemas.length)
|
|
202
|
-
refuseDeclaration(who, problemas);
|
|
203
|
-
}
|
|
204
|
-
/** A malformed `hud` is a program defect, refused like the declaration (ADR-0169): the engine would not know what to place. */
|
|
205
|
-
function refuseIfHudMalformed(who, hud) {
|
|
206
|
-
const problemas = hudNumbersProblems(hud);
|
|
207
|
-
if (problemas.length)
|
|
208
|
-
refuseDeclaration(who, problemas);
|
|
204
|
+
export function cartridgeRefusals(declaration, hooks) {
|
|
205
|
+
const h = hooks ?? {};
|
|
206
|
+
return [
|
|
207
|
+
...conformanceProblems(declaration),
|
|
208
|
+
...[startClaimProblem(h.preset), selectClaimProblem(h.preset)].filter((x) => x !== null),
|
|
209
|
+
...accommodationAnswersProblems(h.accommodations),
|
|
210
|
+
...genreProblems(h.genre),
|
|
211
|
+
...hudNumbersProblems(h.hud),
|
|
212
|
+
...gameOptionsProblems(h.gameOptions),
|
|
213
|
+
...howToPlayProblems(h.howToPlay),
|
|
214
|
+
];
|
|
209
215
|
}
|
|
210
|
-
/**
|
|
211
|
-
function
|
|
212
|
-
const
|
|
213
|
-
if (
|
|
214
|
-
refuseDeclaration(who,
|
|
215
|
-
}
|
|
216
|
-
/** A malformed `howToPlay` is a program defect, refused like the declaration (ADR-0169): the help would not know what to show. */
|
|
217
|
-
function refuseIfHowToPlayMalformed(who, slides) {
|
|
218
|
-
const problemas = howToPlayProblems(slides);
|
|
219
|
-
if (problemas.length)
|
|
220
|
-
refuseDeclaration(who, problemas);
|
|
216
|
+
/** Throws with every refusal at once: an author fixes the whole list in one pass instead of meeting it line by line. */
|
|
217
|
+
function refuseCartridge(who, declaration, hooks) {
|
|
218
|
+
const problems = cartridgeRefusals(declaration, hooks);
|
|
219
|
+
if (problems.length)
|
|
220
|
+
refuseDeclaration(who, problems);
|
|
221
221
|
}
|
|
222
222
|
/** The ids the engine announces and draws into. Without them a child who listens hears nothing (finding 6). */
|
|
223
223
|
const REQUIRED_MARKUP = ['#game-region', '#sr-status', '#sr-alert'];
|
|
@@ -229,6 +229,12 @@ const REQUIRED_MARKUP = ['#game-region', '#sr-status', '#sr-alert'];
|
|
|
229
229
|
* bar, and its own sentence can say WHAT is lost, which is what makes it useful to whoever reads it first.
|
|
230
230
|
*/
|
|
231
231
|
const A11Y_BAR_SELECTOR = '#title-icons';
|
|
232
|
+
/**
|
|
233
|
+
* The engine's own controls that stay pressable by key IN PLAY: the accessibility bar's icons, in `#title-icons` and in the
|
|
234
|
+
* HUD. A key that activates one of them is that control's, and play does not hear it (`input/key-default`, ADR-0111 errata of
|
|
235
|
+
* 2026-09-26).
|
|
236
|
+
*/
|
|
237
|
+
const ENGINE_CONTROLS_IN_PLAY = '.pi-btn';
|
|
232
238
|
/**
|
|
233
239
|
* Switches the engine on for a declared game.
|
|
234
240
|
*
|
|
@@ -245,16 +251,7 @@ export function createGame(o) {
|
|
|
245
251
|
* tied to the argument would leave a root serving several cartridges with the first of them forever.
|
|
246
252
|
*/
|
|
247
253
|
let cartridge = o;
|
|
248
|
-
|
|
249
|
-
if (contractProblems.length) {
|
|
250
|
-
refuseDeclaration('createGame', contractProblems);
|
|
251
|
-
}
|
|
252
|
-
refuseIfItClaimsStart('createGame', cartridge.preset);
|
|
253
|
-
refuseIfNoAnswer('createGame', cartridge.accommodations);
|
|
254
|
-
refuseIfGenreRefused('createGame', cartridge.genre);
|
|
255
|
-
refuseIfHudMalformed('createGame', cartridge.hud);
|
|
256
|
-
refuseIfOptionsMalformed('createGame', cartridge.gameOptions);
|
|
257
|
-
refuseIfHowToPlayMalformed('createGame', cartridge.howToPlay);
|
|
254
|
+
refuseCartridge('createGame', cartridge.declaration, cartridge);
|
|
258
255
|
const { doc } = o.host;
|
|
259
256
|
/*
|
|
260
257
|
* 🔴 THE WINDOW THIS ROOT LISTENS ON IS A SCOPED ONE, and it is not plumbing: a root installs about thirty listeners on the
|
|
@@ -275,21 +272,32 @@ export function createGame(o) {
|
|
|
275
272
|
const whenDisposed = (release) => { endOfLife.push(release); };
|
|
276
273
|
const stateOn = (evt, fn) => { const off = state.on(evt, fn); whenDisposed(off); return off; };
|
|
277
274
|
/*
|
|
278
|
-
* THE
|
|
279
|
-
*
|
|
275
|
+
* 🔴 THE PAGE'S ONE STORE IS BUILT HERE, from what the HOST lends (ADR-0232 point 2, issue #207): the backend the host
|
|
276
|
+
* passed, or its window's `localStorage`. Every module below that persists receives THIS store; none reaches the global.
|
|
277
|
+
* Reading `win.localStorage` can itself THROW (file://, some private modes), which is a host with no storage: `null`.
|
|
278
|
+
*/
|
|
279
|
+
const store = createStorage(hostStorage(o.host));
|
|
280
|
+
/*
|
|
281
|
+
* THE ROOT'S TRANSLATOR (ADR-0232 D3): the page's language as this root speaks it, the game's dictionary, and this root's
|
|
282
|
+
* `t`, markup pass and door to a language change. `core/i18n` holds none of it (erratum of 2026-09-25).
|
|
283
|
+
* 🔴 ITS PORT IS THE STORED LANGUAGE **AND** THE BROWSER'S (ADR-0221 step 7g): the page effects — writing `<html lang>`,
|
|
284
|
+
* dispatching on the window, reading `navigator.language`, hearing another root switch — come in through `localeHostHooks`,
|
|
285
|
+
* which is THIS root's host speaking: the document and the scoped window it received, never the globals.
|
|
286
|
+
* `localeOn` is its door — like `stateOn`, whatever subscribes through it is released by `dispose()` (ADR-0220).
|
|
280
287
|
*/
|
|
281
|
-
const translator = createTranslator(
|
|
288
|
+
const translator = createTranslator({
|
|
289
|
+
...store, KEYS, ...localeHostHooks(doc, win, (root) => { translator.applyDom(root); }),
|
|
290
|
+
});
|
|
282
291
|
for (const [code, entries] of Object.entries(o.dictionaries ?? {}))
|
|
283
292
|
translator.registerDict(code, entries);
|
|
284
293
|
// 📌 The root's own words go through ITS translator, which reads this game's dictionary (ADR-0232 D3).
|
|
285
|
-
const { t, bcp47 } = translator;
|
|
294
|
+
const { t, bcp47, word } = translator;
|
|
286
295
|
const localeOn = (react) => { const off = translator.onChange(react); whenDisposed(off); return off; };
|
|
287
|
-
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
* Reading `win.localStorage` can itself THROW (file://, some private modes), which is a host with no storage: `null`.
|
|
296
|
+
/**
|
|
297
|
+
* THE GAME'S POSITIONS IN WORDS, resolved NOW from its keys (ADR-0232 D3, erratum of 2026-09-25). A function and never a
|
|
298
|
+
* value: every surface that shows a position asks at its drawing, so a language change reaches all of them at once.
|
|
291
299
|
*/
|
|
292
|
-
const
|
|
300
|
+
const actionWords = () => (cartridge.preset ? wordsOf(cartridge.preset, word) : {});
|
|
293
301
|
// THE CHILD'S STORED SETTINGS, FIRST (ADR-0178): nothing below reads or writes one before this.
|
|
294
302
|
// ⚠️ The port of ADR-0178 carries the key names beside the store, so `core` names no storage place itself.
|
|
295
303
|
const state = createSettingsStore({ ...store, KEYS });
|
|
@@ -297,15 +305,8 @@ export function createGame(o) {
|
|
|
297
305
|
// Destructured so the call sites below read as they did; a game reads the same object as `Engine.input`.
|
|
298
306
|
const input = createInputState();
|
|
299
307
|
const { inputOf, keys, markKeyFrom, releaseKey, playerEdge, letGoOfTheKeyboard } = input;
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
* writing `<html lang>`, dispatching on the window, reading `navigator.language` — come in through these two functions,
|
|
303
|
-
* which are THIS root's host speaking: the document and window it received, never the globals. `core` is what the
|
|
304
|
-
* engine IS without a browser.
|
|
305
|
-
*/
|
|
306
|
-
loadLocale({ ...store, KEYS, ...localeHostHooks(doc, win, translator.applyDom) });
|
|
307
|
-
// 📌 And the debugging exposure: whoever HAS a window is this root.
|
|
308
|
-
exposeI18n(win, i18nObject);
|
|
308
|
+
// 📌 And the debugging exposure: whoever HAS a window is this root, and what it exposes is its translator.
|
|
309
|
+
exposeI18n(win, translator);
|
|
309
310
|
/*
|
|
310
311
|
* ⚠️ A READER AND NOT A SNAPSHOT. `declines` belongs to the GAME's half (ADR-0139: the cartridge declares what it does
|
|
311
312
|
* NOT have), so a `const` taken at boot would return, after a `mount()`, the previous cartridge's declines — and a
|
|
@@ -319,17 +320,34 @@ export function createGame(o) {
|
|
|
319
320
|
const $ = (sel) => doc.querySelector(sel);
|
|
320
321
|
const $$ = (sel) => [...doc.querySelectorAll(sel)];
|
|
321
322
|
/*
|
|
322
|
-
* THIS ROOT'S ANNOUNCER AND DEAF MODE (ADR-0232 D4): the screen reader's two regions written in the HOST's
|
|
323
|
-
* host's frames, and
|
|
324
|
-
* 📌
|
|
325
|
-
*
|
|
323
|
+
* THIS ROOT'S ANNOUNCER AND DEAF MODE (ADR-0232 D4, ADR-0234): the screen reader's two regions written in the HOST's
|
|
324
|
+
* document on the host's frames, and deaf mode over its store. A window with no frames (a test double) is the announcer's to answer.
|
|
325
|
+
* 📌 Deaf mode hands the interpreter what the SONAR reads — the text on screen (`ui/screen-text`) — never the announcements,
|
|
326
|
+
* and that text is spoken with the mode off. The interpreter is the host's; else the free Libras player the DELIVERY shipped
|
|
327
|
+
* (`ui/libras-avatar-player`, ADR-0234 route B), opened from the page's own origin at the first request — three.js arrives
|
|
328
|
+
* with it, never before; and where the delivery shipped none, that player answers as `NO_INTERPRETER` does — the sonar's text
|
|
329
|
+
* is captioned, and «signing unavailable» goes to `problems` and to the child. The ports it calls later (`tts`, the caption,
|
|
330
|
+
* `measuredProblems`) are read when it calls.
|
|
326
331
|
*/
|
|
327
332
|
const announcer = createAnnouncer({
|
|
328
333
|
doc,
|
|
329
334
|
raf: win.requestAnimationFrame, // as the host has it: the announcer answers a window with none (bound by the listener scope)
|
|
330
335
|
});
|
|
331
336
|
const { say: srSay, alert: srAlert } = announcer;
|
|
332
|
-
const
|
|
337
|
+
const deafMode = createDeafMode({
|
|
338
|
+
store,
|
|
339
|
+
captionsSetting: () => state.captionsOn,
|
|
340
|
+
t: translator.t,
|
|
341
|
+
interpreter: hostInterpreter(o.host, () => createLibrasAvatarInterpreter({
|
|
342
|
+
doc, win, fetch: win.fetch, base: doc.baseURI, title: () => t('sr.deaf.interpreter'),
|
|
343
|
+
})),
|
|
344
|
+
// a player's sonar waits for another player's reading and cuts its own (ADR-0234, errata 2026-09-26)
|
|
345
|
+
speak: (text, seat) => { tts.narrate(text, { seat }); },
|
|
346
|
+
caption: (text) => { writeSoundCaption(text); },
|
|
347
|
+
tell: srSay,
|
|
348
|
+
report: (line) => { measuredProblems.push(line); },
|
|
349
|
+
});
|
|
350
|
+
whenDisposed(deafMode.dispose);
|
|
333
351
|
for (const sel of REQUIRED_MARKUP) {
|
|
334
352
|
if (!$(sel))
|
|
335
353
|
hostProblems.push(`the page lacks ${sel}: the engine announces and draws into it, and without it a child who listens hears nothing — add it to the page`);
|
|
@@ -380,8 +398,7 @@ export function createGame(o) {
|
|
|
380
398
|
}
|
|
381
399
|
// 1. LANGUAGE BEFORE EVERYTHING. The interface cannot be built before the language is known. The document goes in:
|
|
382
400
|
// see finding 15.
|
|
383
|
-
|
|
384
|
-
translator.applyDom(doc); // the host's markup, with THIS game's dictionary too — the module's pass reads only the page's
|
|
401
|
+
translator.init(doc); // the host's markup, with THIS game's dictionary too, and the stored or preferred language asked for
|
|
385
402
|
// 2. MIXER BEFORE VOICE. Finding 3 turned into sequence: `createAudio` loads the mixer, and the voice reads it.
|
|
386
403
|
// 🔴 THE BROWSER'S SOUND AND SPEECH ARE LENT HERE, from the host's window (ADR-0232 D4): the audio context — made at the
|
|
387
404
|
// first sound, never at boot, so it is born inside the child's gesture and the browser lets it run — and the speech
|
|
@@ -396,6 +413,31 @@ export function createGame(o) {
|
|
|
396
413
|
synth: () => speechHost.speechSynthesis ?? null,
|
|
397
414
|
utterance: (text) => new speechHost.SpeechSynthesisUtterance(text),
|
|
398
415
|
};
|
|
416
|
+
/*
|
|
417
|
+
* THE DEVICE'S VOICES ARRIVE LATE, AND TWO THINGS FOLLOW THEM (ADR-0185 §4, erratum 2026-09-26): the hearing panel's speech rows
|
|
418
|
+
* and the 🗣 of the bars. The page has ONE `onvoiceschanged` slot, so this root takes it once and hands each change to every
|
|
419
|
+
* listener. The last root takes the slot, and an ended root empties it only if it is still its own (ADR-0220).
|
|
420
|
+
*/
|
|
421
|
+
const voicesListeners = [];
|
|
422
|
+
const voicesChanged = () => { for (const again of voicesListeners)
|
|
423
|
+
again(); };
|
|
424
|
+
const whenVoicesChange = (again) => {
|
|
425
|
+
voicesListeners.push(again);
|
|
426
|
+
if (voicesListeners.length > 1)
|
|
427
|
+
return;
|
|
428
|
+
try {
|
|
429
|
+
if (speechHost.speechSynthesis)
|
|
430
|
+
speechHost.speechSynthesis.onvoiceschanged = voicesChanged;
|
|
431
|
+
}
|
|
432
|
+
catch (e) { /* noop */ }
|
|
433
|
+
whenDisposed(() => {
|
|
434
|
+
try {
|
|
435
|
+
if (speechHost.speechSynthesis?.onvoiceschanged === voicesChanged)
|
|
436
|
+
speechHost.speechSynthesis.onvoiceschanged = null;
|
|
437
|
+
}
|
|
438
|
+
catch (e) { /* noop */ }
|
|
439
|
+
});
|
|
440
|
+
};
|
|
399
441
|
/*
|
|
400
442
|
* THE ENGINE'S OWN KOKORO LOADER (ADR-0216 §1 and §5) — the Dev, 2026-09-21: «O jogo não deve precisar saber como isso funciona».
|
|
401
443
|
* ⚠️ IMPORTED AT THE FIRST NEURAL UTTERANCE AND NOT BEFORE: `kokoro-runtime` is what names espeak-ng and the ONNX runtime, so a
|
|
@@ -420,6 +462,9 @@ export function createGame(o) {
|
|
|
420
462
|
createAudio: () => doc.createElement('audio'),
|
|
421
463
|
speech,
|
|
422
464
|
now: () => win.performance.now(),
|
|
465
|
+
// a part in a language the device has no voice for (ADR-0243 §3); `measuredProblems` is declared below and read when it calls
|
|
466
|
+
report: (line) => { if (!measuredProblems.includes(line))
|
|
467
|
+
measuredProblems.push(line); },
|
|
423
468
|
});
|
|
424
469
|
// 📌 The neural-voice line lives in `measureCartridgeProblems()`: the decline that silences it is the game's.
|
|
425
470
|
/**
|
|
@@ -488,19 +533,27 @@ export function createGame(o) {
|
|
|
488
533
|
const simulation = simulationSuspended() ? null : worldState.simulacao;
|
|
489
534
|
simulationOverWorld.onlyInPlay(simulation ? (VIZ_FILTER[simulation] ?? '') : '', enhancement);
|
|
490
535
|
simulationOverWorld.drawLayer(simulation);
|
|
491
|
-
applyCrt(); // the decorative CRT yields to
|
|
536
|
+
applyCrt(); // the decorative CRT yields to the visual modes, and comes back when none is on (ADR-0047, ADR-0241)
|
|
492
537
|
}
|
|
538
|
+
/*
|
|
539
|
+
* WHICH VISUAL MODES ARE ON — decided here, once: a colour correction or a simulation (the world filter), the game's
|
|
540
|
+
* high-contrast theme, the contrast enhancement. A colour-vision mode ON ITS OWN only changes hue (ADR-0241).
|
|
541
|
+
*/
|
|
542
|
+
const visualModeOn = () => filterKey(worldState) !== null || hasHighContrast(worldState) || lq.t() > 0;
|
|
543
|
+
const onlyHueChanges = () => lq.t() === 0 && onlyColourVision(worldState);
|
|
493
544
|
/*
|
|
494
545
|
* THE CRT, applied by the engine (study items A5, B1). 📏 Measured: the panel said «Scanlines: on» and the region had no
|
|
495
|
-
* CRT class until a toggle was pressed — `render/crt` was never started under `createGame`.
|
|
496
|
-
*
|
|
546
|
+
* CRT class until a toggle was pressed — `render/crt` was never started under `createGame`. The vignette yields to every
|
|
547
|
+
* visual mode; the scanline to every one but a colour-vision mode on its own (ADR-0241). Its scanlines are re-anchored
|
|
548
|
+
* to real pixels at every scale.
|
|
497
549
|
*/
|
|
498
550
|
const crt = createCrt({
|
|
499
551
|
region: () => $('#game-region'),
|
|
500
552
|
win,
|
|
501
553
|
// `cartucho` and not `players()`: this runs at boot, above the `players` declaration (temporal dead zone)
|
|
502
554
|
numPlayers: () => Math.max(1, (cartridge.players ?? []).length),
|
|
503
|
-
|
|
555
|
+
scanlineYields: () => visualModeOn() && !onlyHueChanges(),
|
|
556
|
+
vignetteYields: visualModeOn,
|
|
504
557
|
store,
|
|
505
558
|
});
|
|
506
559
|
const { apply: applyCrt, scanVars: crtScanVars } = crt;
|
|
@@ -585,6 +638,12 @@ export function createGame(o) {
|
|
|
585
638
|
// The screen footer (see `screenFooter`): declared HERE, before the first `changePhase`, which already clears it.
|
|
586
639
|
let footer = null;
|
|
587
640
|
let barExplanation = null;
|
|
641
|
+
/*
|
|
642
|
+
* WHO HOLDS THE EXPLANATION BAND (ADR-0244): the GAME's text is the band's resting text (`Engine.explain`); an engine item —
|
|
643
|
+
* a quick-bar icon, a pause-card item, a locked item's reason — takes the band while it is pointed and gives it back.
|
|
644
|
+
*/
|
|
645
|
+
let gameExplanation = null;
|
|
646
|
+
let itemExplanation = null;
|
|
588
647
|
// The quick-pause state and the button legend, declared before the first `changePhase` too: its `pauseControls.hide`
|
|
589
648
|
// refreshes the legend (ADR-0164 rule 3), and reading them earlier would be a temporal-dead-zone error at boot.
|
|
590
649
|
const inQuickPause = new Set();
|
|
@@ -618,6 +677,8 @@ export function createGame(o) {
|
|
|
618
677
|
* protect what does not exist.
|
|
619
678
|
*/
|
|
620
679
|
engineActions.quit = () => changePhase('title');
|
|
680
|
+
/** Print mode is armed: the next key is its way out, and not play's (read by the keyboard conductor). */
|
|
681
|
+
let printArmed = false;
|
|
621
682
|
/*
|
|
622
683
|
* PRINT — «ver a tela sem menus», and any button comes back.
|
|
623
684
|
*
|
|
@@ -632,6 +693,11 @@ export function createGame(o) {
|
|
|
632
693
|
* neither to the game nor to the pause, it is the way out. 🎯 And it does not collide with the `start` hook (ADR-0144),
|
|
633
694
|
* which bubbles: `goBack` reveals the card in the capture, and when the bubbling one arrives the card-already-open
|
|
634
695
|
* guard sends it away.
|
|
696
|
+
*
|
|
697
|
+
* 🔴 AND THE KEY THAT ENDS IT IS NOT PLAYED (ADR-0111 erratum of 2026-09-26, one key one action). `goBack` is added after
|
|
698
|
+
* the keyboard conductor, on the same node and phase, so the conductor runs first and saw no menu: 📏 on the served quiz
|
|
699
|
+
* the arrow that ended print mode also moved the quiz's cursor. The conductor reads `printArmed` instead. Not
|
|
700
|
+
* `stopImmediatePropagation`: the cool-down, the motor filter and the pad's hiding listen after, and must still see the key.
|
|
635
701
|
*/
|
|
636
702
|
engineActions.print = () => {
|
|
637
703
|
const findPauseCard = () => $('#vp-pause-0');
|
|
@@ -640,6 +706,7 @@ export function createGame(o) {
|
|
|
640
706
|
return;
|
|
641
707
|
eventTarget.hidden = true;
|
|
642
708
|
const goBack = (e) => {
|
|
709
|
+
printArmed = false;
|
|
643
710
|
if (e && typeof e.preventDefault === 'function') {
|
|
644
711
|
try {
|
|
645
712
|
e.preventDefault();
|
|
@@ -653,23 +720,15 @@ export function createGame(o) {
|
|
|
653
720
|
c.hidden = false;
|
|
654
721
|
};
|
|
655
722
|
win.setTimeout(() => {
|
|
723
|
+
printArmed = true;
|
|
656
724
|
win.addEventListener('keydown', goBack, true);
|
|
657
725
|
win.addEventListener('pointerdown', goBack, true);
|
|
658
726
|
}, 80);
|
|
659
727
|
srSay(t('sr.print.on'));
|
|
660
728
|
};
|
|
661
729
|
/**
|
|
662
|
-
* THE HEARING PANEL, resolved late and read early — the same laziness as `engineActions` above, for the same reason:
|
|
663
|
-
* `
|
|
664
|
-
*
|
|
665
|
-
* 🔴 IT IS A `let` BECAUSE OF A DEFECT KEPT VERBATIM. `ui/pause-icons` documents it: in the monolith the call that
|
|
666
|
-
* refreshed the narration row sat behind `typeof reflectTTS === 'function'`, a symbol that no longer existed, «so it
|
|
667
|
-
* never fires». The guard was ported as `reflectTtsPanelEnabled`, defaulting to `false`, so as not to fix it
|
|
668
|
-
* silently — and with a field to switch it back on.
|
|
669
|
-
*
|
|
670
|
-
* 🎯 THE ENGINE CAN: it mounts the panel, so it has the `reflectTts` to hand over. Without it, the child turns
|
|
671
|
-
* narration on with the bar's 🗣 icon and the panel goes on saying it is off — the family of defect where a control
|
|
672
|
-
* lies about its state.
|
|
730
|
+
* THE HEARING PANEL, resolved late and read early — the same laziness as `engineActions` above, for the same reason: the
|
|
731
|
+
* mixer's listener (`onCatChange`, below) is subscribed before the panels mount further below, and reads this when told.
|
|
673
732
|
*/
|
|
674
733
|
let audio = null;
|
|
675
734
|
/*
|
|
@@ -750,6 +809,7 @@ export function createGame(o) {
|
|
|
750
809
|
*/
|
|
751
810
|
const sceneMotion = readStoredScene(store, defaultReducedMotion(win.matchMedia));
|
|
752
811
|
const saveSceneMotion = () => { storeScene(store, sceneMotion); };
|
|
812
|
+
const setGameTheme = cartridge.setPlayerTheme;
|
|
753
813
|
const pauseIcons = initPauseIcons({
|
|
754
814
|
translator, store,
|
|
755
815
|
settings: state, // the page's settings store itself: its live bindings are the reads the bar asks for (ADR-0232)
|
|
@@ -814,11 +874,13 @@ export function createGame(o) {
|
|
|
814
874
|
* edge between layers to save one argument. Composition is this file's job.
|
|
815
875
|
*/
|
|
816
876
|
transportInUse: (i) => inputOf(i).inUse,
|
|
817
|
-
//
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
877
|
+
// ⚠️ OFF, and not the monolith's dead guard come back: the 🗣's write is told by the mixer, and the root's one listener to
|
|
878
|
+
// it (`onCatChange`, below) repaints the panel — a second path here would be two answers to «who reflects narration»
|
|
879
|
+
// (ADR-0247).
|
|
880
|
+
reflectTtsPanel: () => { },
|
|
881
|
+
reflectTtsPanelEnabled: false,
|
|
882
|
+
isLibrasOn: deafMode.isOn,
|
|
883
|
+
toggleLibras: deafMode.toggle,
|
|
822
884
|
/*
|
|
823
885
|
* ⚠️ WHAT THE GAME HANDS OVER: the three fields are optional on both sides. Absent, the pause card holds only what the
|
|
824
886
|
* engine acts on, and the two visual icons follow their own rules. See the notes in `CreateGameOptions`.
|
|
@@ -865,7 +927,16 @@ export function createGame(o) {
|
|
|
865
927
|
reserveBarBand(); // the name line under the bar grows with the text (issue #160)
|
|
866
928
|
return FONT_BY_KEY[position.font]?.fam ?? null;
|
|
867
929
|
} : undefined,
|
|
868
|
-
|
|
930
|
+
/*
|
|
931
|
+
* 🌗 THE THEME IS THE GAME'S TO DRAW (ADR-0148 erratum) AND THE ROOT'S TO KNOW: the decorative CRT yields to high
|
|
932
|
+
* contrast (ADR-0047), and a writer handed straight through left the root blind to it — the scanlines and the vignette
|
|
933
|
+
* stayed over a high-contrast screen.
|
|
934
|
+
*/
|
|
935
|
+
...(setGameTheme ? { setPlayerTheme: (i, theme) => {
|
|
936
|
+
setGameTheme(i, theme);
|
|
937
|
+
worldState = { ...worldState, tema: theme };
|
|
938
|
+
applyCrt();
|
|
939
|
+
} } : {}),
|
|
869
940
|
/*
|
|
870
941
|
* 🚥 COLOUR-VISION CORRECTION HAS AN ENGINE DEFAULT (ADR-0148 §1), so the icon is never missing for want of a writer.
|
|
871
942
|
*
|
|
@@ -948,7 +1019,7 @@ export function createGame(o) {
|
|
|
948
1019
|
/*
|
|
949
1020
|
* ⚠️ AND AGAIN WHEN THE BOOT LANGUAGE ARRIVES — without it the bar stays in the FALLBACK language.
|
|
950
1021
|
*
|
|
951
|
-
* `
|
|
1022
|
+
* `translator.init` applies pt synchronously (so the page is never blank) and, if the preferred language is another, ASKS
|
|
952
1023
|
* for the switch — which is asynchronous, because en/es are on-demand chunks. This markup is born in that interval.
|
|
953
1024
|
*
|
|
954
1025
|
* 📌 IT IS THE `i18n:change` LISTENER near the pad that repaints it (study item C6, ADR-0031): the boot's preferred
|
|
@@ -961,7 +1032,8 @@ export function createGame(o) {
|
|
|
961
1032
|
* `aria-pressed=false` after the child had turned it on — the control lying about its state.
|
|
962
1033
|
*
|
|
963
1034
|
* 📌 ONLY THE STATES WITH AN EVENT that can change elsewhere: blind mode, the camera control and the voice control
|
|
964
|
-
* (`GameEvent`)
|
|
1035
|
+
* (`GameEvent`), and NARRATION, which is the mixer's category and is told by the mixer (`onCatChange`, below). The other
|
|
1036
|
+
* icons reflect themselves on the click, which is the path by which they change.
|
|
965
1037
|
*/
|
|
966
1038
|
stateOn('blindMode', () => { pauseIcons.reflectIconsIn(a11yBar, 0); });
|
|
967
1039
|
// the 👀 changes elsewhere too: the eye control puts it back to off when the camera or the files are missing (ADR-0213)
|
|
@@ -971,6 +1043,26 @@ export function createGame(o) {
|
|
|
971
1043
|
// is worse than a missing one (ADR-0106 §5), and the click path does not cover it, because this change comes from elsewhere.
|
|
972
1044
|
stateOn('voiceControl', () => { pauseIcons.reflectIconsIn(a11yBar, 0); });
|
|
973
1045
|
}
|
|
1046
|
+
// 🔴 AND THE 🗣 WHEN THE DEVICE'S VOICES CHANGE (ADR-0185 erratum 2026-09-26): they can arrive after load, or leave, and the
|
|
1047
|
+
// icon's greyed-out look is the lock the child sees. Silent: the press says why on its own, and nothing else changed.
|
|
1048
|
+
whenVoicesChange(() => {
|
|
1049
|
+
if (a11yBar && barUsable)
|
|
1050
|
+
pauseIcons.reflectIconsIn(a11yBar, 0);
|
|
1051
|
+
pauseIcons.reflectPauseIcons();
|
|
1052
|
+
});
|
|
1053
|
+
/*
|
|
1054
|
+
* 🔴 THE MIXER TELLS EVERY CHANGE, AND THIS IS THE ONE LISTENER (ADR-0247). The engine hands the mixer itself to the game
|
|
1055
|
+
* (`Engine.audio`), so a category — narration above all — is written by the bar's 🗣, by the panels, and by a game's own mute
|
|
1056
|
+
* button or cut-scene; without this, the bar and the panel said the state the child had left while the voice spoke or fell
|
|
1057
|
+
* silent (ADR-0159 rule 10: the state is said in words, and a stale word is a lie). Every writer is heard here and every
|
|
1058
|
+
* surface is drawn from here: the bar's icons for narration, the only category the bar shows, and the panels for any.
|
|
1059
|
+
* It is the ROOT's, so it ends with `dispose()`; `unmount()` keeps it, as the root and its surfaces live on (ADR-0142).
|
|
1060
|
+
*/
|
|
1061
|
+
whenDisposed(mixer.onCatChange((k) => {
|
|
1062
|
+
if (k === 'tts' && a11yBar && barUsable)
|
|
1063
|
+
pauseIcons.reflectIconsIn(a11yBar, 0);
|
|
1064
|
+
audio?.reflectCategory(k);
|
|
1065
|
+
}));
|
|
974
1066
|
// 4d. WHO OPENED THE PAUSE, when there is more than one seat — finding 3 of the `game-soccer` audit.
|
|
975
1067
|
//
|
|
976
1068
|
// ⚠️ THE KEYBOARD PANEL IS PARAMETERISED BY THE SEAT: `render(selPlayer)` draws the positions of THAT scheme, and the
|
|
@@ -998,6 +1090,11 @@ export function createGame(o) {
|
|
|
998
1090
|
findPauseCard.id = 'vp-pause-0';
|
|
999
1091
|
pauseMountPoint.appendChild(findPauseCard);
|
|
1000
1092
|
}
|
|
1093
|
+
// THE LIBRARY FAMILIES THIS GAME DECLARED (`uses.fonts`, ADR-0255), the engine's own left out; a declaration that is not a list
|
|
1094
|
+
// of names is said in `problems`. Their faces are written and kept at the end of the boot, with the heavy files.
|
|
1095
|
+
const libraryFonts = declaredFamilies(o.uses?.fonts, ENGINE_FAMILIES);
|
|
1096
|
+
if (libraryFonts.problem)
|
|
1097
|
+
hostProblems.push(libraryFonts.problem);
|
|
1001
1098
|
/*
|
|
1002
1099
|
* 4f. THE SETTINGS PANELS (ADR-0106 §1).
|
|
1003
1100
|
*
|
|
@@ -1038,12 +1135,10 @@ export function createGame(o) {
|
|
|
1038
1135
|
t: translator.t, $, srSay, store, root: doc.documentElement,
|
|
1039
1136
|
// The rows' prose goes to the footer at EVERY render, or it appears twice on the first click.
|
|
1040
1137
|
fillExplain: overlays.fillExplain,
|
|
1041
|
-
//
|
|
1042
|
-
//
|
|
1043
|
-
//
|
|
1044
|
-
|
|
1045
|
-
? { fontInstalled: (family) => doc.fonts.check(`16px "${family}"`) }
|
|
1046
|
-
: {}),
|
|
1138
|
+
// 🔴 A LIBRARY FACE IS THERE WHEN THIS GAME DECLARED ITS FAMILY (ADR-0255), and that is the whole detector: the engine
|
|
1139
|
+
// writes the family's `@font-face` for exactly those. `document.fonts.check` is not asked, because it answers `true` for a
|
|
1140
|
+
// family NO rule declares (nothing to load) — it would offer every library row on every page.
|
|
1141
|
+
fontInstalled: (family) => libraryFonts.asked.includes(family),
|
|
1047
1142
|
});
|
|
1048
1143
|
/*
|
|
1049
1144
|
* AAC — COMMUNICATION: THE PANEL IS NOT MOUNTED (ADR-0151).
|
|
@@ -1087,7 +1182,11 @@ export function createGame(o) {
|
|
|
1087
1182
|
return;
|
|
1088
1183
|
while (list.firstChild)
|
|
1089
1184
|
list.removeChild(list.firstChild);
|
|
1090
|
-
|
|
1185
|
+
// the game's words resolved NOW, in the page's language (ADR-0232 D3): a language change re-renders an open panel
|
|
1186
|
+
const slideContents = [
|
|
1187
|
+
...playSlidesOf(cartridge.howToPlay ?? [], word),
|
|
1188
|
+
...helpRows(actionWords(), (a) => keyboard.kbFor(0)[a], (code) => keyName(t, code)),
|
|
1189
|
+
];
|
|
1091
1190
|
const ctxDoSlide = { create: (tag) => doc.createElement(tag), t, title: t('menu.help') };
|
|
1092
1191
|
const slides = mountSlides(ctxDoSlide);
|
|
1093
1192
|
list.appendChild(slides);
|
|
@@ -1143,7 +1242,7 @@ export function createGame(o) {
|
|
|
1143
1242
|
});
|
|
1144
1243
|
gamePanel.shell.reset.hidden = true;
|
|
1145
1244
|
redrawGameOptions = () => {
|
|
1146
|
-
drawGameOptions({ ...panelCtx, say: srSay, t: translator.t }, gamePanel.shell.list, cartridge.gameOptions ?? []);
|
|
1245
|
+
drawGameOptions({ ...panelCtx, say: srSay, t: translator.t, word }, gamePanel.shell.list, cartridge.gameOptions ?? []);
|
|
1147
1246
|
if (!gamePanel.shell.overlay.hidden)
|
|
1148
1247
|
overlays.fillExplain(gamePanel.shell.card);
|
|
1149
1248
|
};
|
|
@@ -1197,9 +1296,9 @@ export function createGame(o) {
|
|
|
1197
1296
|
fillExplain: overlays.fillExplain,
|
|
1198
1297
|
toggleBtn,
|
|
1199
1298
|
// The «Personagem» section exists only if the GAME said it has one (ADR-0153). Read at every render: it changes on `mount()`.
|
|
1200
|
-
hasCharacter: () => subjectWord(cartridge.accommodations, 'reducedCharacterMotion') !== null,
|
|
1201
|
-
// and its title is the game's word for it (ADR-0153 confirmation)
|
|
1202
|
-
characterLabel: () => subjectWord(cartridge.accommodations, 'reducedCharacterMotion')?.label ?? null,
|
|
1299
|
+
hasCharacter: () => subjectWord(cartridge.accommodations, 'reducedCharacterMotion', word) !== null,
|
|
1300
|
+
// and its title is the game's word for it (ADR-0153 confirmation), resolved at every render (ADR-0232 D3)
|
|
1301
|
+
characterLabel: () => subjectWord(cartridge.accommodations, 'reducedCharacterMotion', word)?.label ?? null,
|
|
1203
1302
|
});
|
|
1204
1303
|
engineActions.anim = animPanel.open;
|
|
1205
1304
|
/*
|
|
@@ -1292,8 +1391,8 @@ export function createGame(o) {
|
|
|
1292
1391
|
* Owner colours carries the game's word; the outlines are two positions of one subject, named by the engine.
|
|
1293
1392
|
*/
|
|
1294
1393
|
const ownerSpec = () => {
|
|
1295
|
-
const
|
|
1296
|
-
return { id: 'opt-dono', label:
|
|
1394
|
+
const owner = subjectWord(cartridge.accommodations, 'ownerColors', word);
|
|
1395
|
+
return { id: 'opt-dono', label: owner?.label ?? '', hint: owner?.hint };
|
|
1297
1396
|
};
|
|
1298
1397
|
const { row: ownerRow, control: ownerButton } = controlRow(panelCtx, ownerSpec());
|
|
1299
1398
|
const reflectOwner = () => {
|
|
@@ -1331,7 +1430,7 @@ export function createGame(o) {
|
|
|
1331
1430
|
});
|
|
1332
1431
|
// the hint is written BEFORE the panel's render, which runs `fillExplain`: written after, it stays inside the row
|
|
1333
1432
|
const writeHint = () => {
|
|
1334
|
-
explanation.textContent = subjectWord(cartridge.accommodations, 'contrastOutlines')?.hint ?? t(`visual.contorno.${plane}.dica`);
|
|
1433
|
+
explanation.textContent = subjectWord(cartridge.accommodations, 'contrastOutlines', word)?.hint ?? t(`visual.contorno.${plane}.dica`);
|
|
1335
1434
|
};
|
|
1336
1435
|
writeHint();
|
|
1337
1436
|
const reflect = () => { updateSteps(stepper, spec()); };
|
|
@@ -1340,12 +1439,12 @@ export function createGame(o) {
|
|
|
1340
1439
|
const fgOutline = outline('fg');
|
|
1341
1440
|
const bgOutline = outline('bg');
|
|
1342
1441
|
const offerOwnerAndOutlines = () => {
|
|
1343
|
-
ownerRow.hidden = subjectWord(cartridge.accommodations, 'ownerColors') === null;
|
|
1442
|
+
ownerRow.hidden = subjectWord(cartridge.accommodations, 'ownerColors', word) === null;
|
|
1344
1443
|
if (!ownerRow.hidden) {
|
|
1345
1444
|
labelRow(ownerRow, ownerSpec());
|
|
1346
1445
|
reflectOwner();
|
|
1347
1446
|
}
|
|
1348
|
-
const withoutOutlines = subjectWord(cartridge.accommodations, 'contrastOutlines') === null;
|
|
1447
|
+
const withoutOutlines = subjectWord(cartridge.accommodations, 'contrastOutlines', word) === null;
|
|
1349
1448
|
for (const c of [fgOutline, bgOutline]) {
|
|
1350
1449
|
c.row.hidden = withoutOutlines;
|
|
1351
1450
|
if (!withoutOutlines)
|
|
@@ -1513,7 +1612,7 @@ export function createGame(o) {
|
|
|
1513
1612
|
const audioPanel = mountPanel(panelCtx, {
|
|
1514
1613
|
id: 'audio',
|
|
1515
1614
|
// 📌 THE SHELL'S LIST IS NAVIGATION SOUND since ADR-0151: the taste categories went to «Áudio». The id
|
|
1516
|
-
// `navsound-list` is the one `initSettingsAudio` fills with sonar
|
|
1615
|
+
// `navsound-list` is the one `initSettingsAudio` fills with the sonar and the cane (`guard`/`guide` left, ADR-0258).
|
|
1517
1616
|
listId: 'navsound-list',
|
|
1518
1617
|
labels: () => ({
|
|
1519
1618
|
title: t('menu.audio'),
|
|
@@ -1560,7 +1659,7 @@ export function createGame(o) {
|
|
|
1560
1659
|
// is not wiring. ⚠️ Read at every opening: the topology is a function, and a game changes its demands between phases (ADR-0084).
|
|
1561
1660
|
const hideRowsWithoutSubject = () => showOnlyRowsThatApply({
|
|
1562
1661
|
find: $,
|
|
1563
|
-
caneWord: () => subjectWord(cartridge.accommodations, 'caneSpacing'),
|
|
1662
|
+
caneWord: () => subjectWord(cartridge.accommodations, 'caneSpacing', word),
|
|
1564
1663
|
hasNavigationSound: () => contractSubjects({
|
|
1565
1664
|
declaration: cartridge.declaration,
|
|
1566
1665
|
actions: cartridge.preset ? presetActions(cartridge.preset) : [],
|
|
@@ -1586,6 +1685,12 @@ export function createGame(o) {
|
|
|
1586
1685
|
catch (e) {
|
|
1587
1686
|
return [];
|
|
1588
1687
|
} },
|
|
1688
|
+
/*
|
|
1689
|
+
* 🔴 THE SAMPLE SPEAKS THE PAGE'S LANGUAGE, read at every press: its text comes from the page's dictionary, so a fixed tag
|
|
1690
|
+
* asked the browser for a Portuguese voice to read English words. And a chosen voice of ANOTHER language is not handed
|
|
1691
|
+
* over (ADR-0185): the page's system-voice list offers every voice when none matches, and the one picked before a
|
|
1692
|
+
* language switch outlives it. So the voice goes only if the engine's own list for the language (`tts.voices()`) has it.
|
|
1693
|
+
*/
|
|
1589
1694
|
speakSample: (sample, chosen) => {
|
|
1590
1695
|
try {
|
|
1591
1696
|
const ss = win.speechSynthesis;
|
|
@@ -1593,8 +1698,8 @@ export function createGame(o) {
|
|
|
1593
1698
|
return;
|
|
1594
1699
|
ss.cancel();
|
|
1595
1700
|
const u = speech.utterance(sample);
|
|
1596
|
-
u.lang =
|
|
1597
|
-
if (chosen)
|
|
1701
|
+
u.lang = bcp47();
|
|
1702
|
+
if (chosen && tts.voices().some((v) => v.voice === 'webspeech:' + chosen.name))
|
|
1598
1703
|
u.voice = chosen;
|
|
1599
1704
|
u.rate = 1;
|
|
1600
1705
|
u.volume = 1;
|
|
@@ -1602,21 +1707,7 @@ export function createGame(o) {
|
|
|
1602
1707
|
}
|
|
1603
1708
|
catch (e) { /* the device refused to speak; the panel already says what it can do */ }
|
|
1604
1709
|
},
|
|
1605
|
-
whenVoicesChange
|
|
1606
|
-
try {
|
|
1607
|
-
if (win.speechSynthesis)
|
|
1608
|
-
win.speechSynthesis.onvoiceschanged = again;
|
|
1609
|
-
}
|
|
1610
|
-
catch (e) { /* noop */ }
|
|
1611
|
-
// one slot per page and the last root takes it: an ended root empties it only if it is still its own (ADR-0220)
|
|
1612
|
-
whenDisposed(() => {
|
|
1613
|
-
try {
|
|
1614
|
-
if (win.speechSynthesis?.onvoiceschanged === again)
|
|
1615
|
-
win.speechSynthesis.onvoiceschanged = null;
|
|
1616
|
-
}
|
|
1617
|
-
catch (e) { /* noop */ }
|
|
1618
|
-
});
|
|
1619
|
-
},
|
|
1710
|
+
whenVoicesChange,
|
|
1620
1711
|
},
|
|
1621
1712
|
audioOutputs: {
|
|
1622
1713
|
canList: () => !!(win.navigator?.mediaDevices && win.navigator.mediaDevices.enumerateDevices),
|
|
@@ -1649,6 +1740,7 @@ export function createGame(o) {
|
|
|
1649
1740
|
getVolume: () => mixer.volume,
|
|
1650
1741
|
setVolume,
|
|
1651
1742
|
getAudioCat: () => mixer.audioCat,
|
|
1743
|
+
// the bar follows what the panel writes through the mixer's own event, `onCatChange` above (ADR-0247)
|
|
1652
1744
|
setCatGain,
|
|
1653
1745
|
tts,
|
|
1654
1746
|
getBlindMode: readBlindMode,
|
|
@@ -1662,18 +1754,17 @@ export function createGame(o) {
|
|
|
1662
1754
|
engineActions.audio = audioPanel.open;
|
|
1663
1755
|
engineActions.som = soundPanel.open;
|
|
1664
1756
|
}
|
|
1665
|
-
// 4b.
|
|
1757
|
+
// 4b. THE SONAR. Only the contract goes in: no tile, collision box or coin array. Its WORDS are the text on screen at the
|
|
1758
|
+
// press, read from the page by `ui/screen-text` (ADR-0234: «sonar do que está na tela»); its tone points at the target.
|
|
1759
|
+
const screen = { doc, world: () => cartridge.declaration.world() };
|
|
1666
1760
|
const sonar = createAudioSonar({
|
|
1761
|
+
screenText: () => screenText(screen),
|
|
1667
1762
|
t: translator.t,
|
|
1668
1763
|
topology: () => cartridge.declaration.topology(),
|
|
1669
1764
|
targetsOf: (i) => cartridge.declaration.targetsOf(i),
|
|
1670
1765
|
nameAt: (at) => cartridge.declaration.nameAt(at),
|
|
1671
|
-
//
|
|
1672
|
-
|
|
1673
|
-
// all the rest of the audio.
|
|
1674
|
-
roleAt: (at) => cartridge.declaration.roleAt(at),
|
|
1675
|
-
tonePan, srSay, narrate: (text) => tts.narrate(text),
|
|
1676
|
-
catNode, audioOut, getVolume: () => mixer.volume,
|
|
1766
|
+
// what the sonar reads is spoken — or, in deaf mode, captioned and signed by the interpreter (ADR-0234)
|
|
1767
|
+
tonePan, srSay, narrate: deafMode.sonar,
|
|
1677
1768
|
// ⚠️ THE ANSWER, NOT THE TABLE (#104): `platform/audio-sonar` does not know what a visual mode is, so it is answered
|
|
1678
1769
|
// here — the root is the only layer that knows both axes AND may import from `render/`.
|
|
1679
1770
|
visionImpaired: (pl) => {
|
|
@@ -1681,14 +1772,7 @@ export function createGame(o) {
|
|
|
1681
1772
|
return !!v && (isBlind(v) || isLowVision(v));
|
|
1682
1773
|
},
|
|
1683
1774
|
getBlindMode: readBlindMode, LOGICAL_W,
|
|
1684
|
-
// The player DERIVED from the focus: field 4 answering where the child is. A game that supplies no list still has
|
|
1685
|
-
// the sonar, and that is what makes the accessibility stack not an accessory.
|
|
1686
|
-
getPlayers: cartridge.sonarPlayers ?? (() => {
|
|
1687
|
-
const f = cartridge.declaration.focusOf(0);
|
|
1688
|
-
return f ? [{ i: 0, x: f.at.x, y: f.at.y, visual: DEFAULT_VISUAL }] : [];
|
|
1689
|
-
}),
|
|
1690
1775
|
getNumPlayers: () => (cartridge.players ?? [null]).length,
|
|
1691
|
-
getAudioCtx: () => mixer.audioCtx, getSoundOn: () => mixer.soundOn, getAudioCat: () => mixer.audioCat,
|
|
1692
1776
|
newContext: newAudioContext,
|
|
1693
1777
|
});
|
|
1694
1778
|
// 5. Remappable keyboard — the best cut of the base (finding 11): a key scheme, no world.
|
|
@@ -1768,6 +1852,8 @@ export function createGame(o) {
|
|
|
1768
1852
|
// registered with the overlays, which cancels a running wizard without storing its map.
|
|
1769
1853
|
closePadWiz: () => { overlays.closeById('padwiz'); },
|
|
1770
1854
|
whichPlayer: (code) => keyboard.whichPlayer(code),
|
|
1855
|
+
// this root's one screen: another seat's keys steer none of its menus (below, at the rule)
|
|
1856
|
+
leadsTheScreen,
|
|
1771
1857
|
actionOf: (code, i) => keyboard.actionOf(code, i),
|
|
1772
1858
|
// Finding 12, SOLVED IN THE ENGINE: the port is generic over `WindowEventMap` (see `EventTargetLike` in
|
|
1773
1859
|
// input/touch-bindings), so the real `window` goes straight in, with no adapter per consumer.
|
|
@@ -1794,6 +1880,7 @@ export function createGame(o) {
|
|
|
1794
1880
|
* first is not what can be built first.
|
|
1795
1881
|
*/
|
|
1796
1882
|
const blockKey = (e) => { e.preventDefault(); e.stopImmediatePropagation(); };
|
|
1883
|
+
/** Takes the item the scan shows, for a press from `source`; answers whether one-button scanning took it (ADR-0218 §4). */
|
|
1797
1884
|
let scanPress = null;
|
|
1798
1885
|
win.addEventListener('keydown', (e) => {
|
|
1799
1886
|
if (!state.switchScan || !scanPress || keyboard.whichPlayer(e.code) < 0)
|
|
@@ -1807,6 +1894,36 @@ export function createGame(o) {
|
|
|
1807
1894
|
if (state.switchScan && scanPress && keyboard.whichPlayer(e.code) >= 0)
|
|
1808
1895
|
blockKey(e);
|
|
1809
1896
|
}, true);
|
|
1897
|
+
/*
|
|
1898
|
+
* 🔴 AND EVERY TOUCH ON THE GAME REGION (ADR-0218 §4) — and on the on-screen pad, wherever the host put it. Registered here
|
|
1899
|
+
* for the key's reason: above the menus' own pointer listeners (a hold places the cursor, ADR-0159), the pad's and the game's,
|
|
1900
|
+
* so one touch is one take and nothing else. The click the browser fires after it is swallowed too; a click the ENGINE sends
|
|
1901
|
+
* — a menu's «confirm» pressing its item — is not, which is why `scanPress` clears the mark before every take.
|
|
1902
|
+
* 📌 THE QUICK BAR STAYS ITSELF: ☝️ on it is the way OUT of one-button mode, and 📷 and 👄 turn the camera and voice transports
|
|
1903
|
+
* on and off — the child's own switches, which a scan must not swallow (ADR-0218 erratum of 2026-09-26).
|
|
1904
|
+
*/
|
|
1905
|
+
let swallowClick = false;
|
|
1906
|
+
const scanTakesTouch = (target) => {
|
|
1907
|
+
const el = target;
|
|
1908
|
+
if (!state.switchScan || !scanPress || !el || typeof el.closest !== 'function')
|
|
1909
|
+
return false;
|
|
1910
|
+
const onTheBar = !!el.closest(`${ENGINE_CONTROLS_IN_PLAY}, .pause-icons`) || !!a11yBar?.contains(el);
|
|
1911
|
+
return !onTheBar && !!el.closest('#game-region, #touch-controls');
|
|
1912
|
+
};
|
|
1913
|
+
win.addEventListener('pointerdown', (e) => {
|
|
1914
|
+
swallowClick = false;
|
|
1915
|
+
if (!scanTakesTouch(e.target))
|
|
1916
|
+
return;
|
|
1917
|
+
blockKey(e);
|
|
1918
|
+
scanPress?.('toque');
|
|
1919
|
+
swallowClick = true;
|
|
1920
|
+
}, true);
|
|
1921
|
+
win.addEventListener('pointerup', (e) => { if (scanTakesTouch(e.target))
|
|
1922
|
+
blockKey(e); }, true);
|
|
1923
|
+
win.addEventListener('click', (e) => { if (swallowClick) {
|
|
1924
|
+
swallowClick = false;
|
|
1925
|
+
blockKey(e);
|
|
1926
|
+
} }, true);
|
|
1810
1927
|
// ⚠️ AND NOW THE MENU NAVIGATION SWITCHES ON — after the block above, and the ORDER IS THE BEHAVIOUR: listeners on one
|
|
1811
1928
|
// node run in registration order, so the scan sees the key first and can stop it. The other way round, one press would
|
|
1812
1929
|
// move the menu AND take an item (ADR-0218). 🔴 No type sees a lost call: browser cases hold that the menus answer the
|
|
@@ -1963,8 +2080,8 @@ export function createGame(o) {
|
|
|
1963
2080
|
* 📌 The name line is counted whether or not a name is showing — reserving only while pointing would move the game
|
|
1964
2081
|
* under the child's finger. Measured again at every scale and every typography step: both change the text's size.
|
|
1965
2082
|
* ⚠️ Zero without a bar: nothing to reserve.
|
|
1966
|
-
* 📌 With a HUD
|
|
1967
|
-
*
|
|
2083
|
+
* 📌 With a HUD the room also holds the mission, centred just under the bar (ADR-0239 erratum), when it reaches lower than
|
|
2084
|
+
* the bar's room. The rest of the HUD is the row at the bottom, measured with it.
|
|
1968
2085
|
*/
|
|
1969
2086
|
function reserveBarBand() {
|
|
1970
2087
|
reserveTopBand({
|
|
@@ -1973,6 +2090,18 @@ export function createGame(o) {
|
|
|
1973
2090
|
hud: hudMounted,
|
|
1974
2091
|
...(typeof win.getComputedStyle === 'function' ? { computedStyle: (el) => win.getComputedStyle(el) } : {}),
|
|
1975
2092
|
});
|
|
2093
|
+
reserveRowBand();
|
|
2094
|
+
}
|
|
2095
|
+
/*
|
|
2096
|
+
* THE HUD ROW AT THE BOTTOM (ADR-0239; issue #94): learning bars · the session clock · the power over the score · the game's
|
|
2097
|
+
* map, left to right. The ROOT's, for its whole life — the clock lives in it and the session is the root's — and the bands
|
|
2098
|
+
* of each cartridge go into its cells. Measured (`--hud-row-h`, `--hud-row-bottom`) at every measure of the top and at every
|
|
2099
|
+
* tick of the clock: the on-screen pad shows and hides with the child's hands, and the row must stand above it.
|
|
2100
|
+
*/
|
|
2101
|
+
const hudRow = mountHudRow(doc, $('#game-region'));
|
|
2102
|
+
whenDisposed(() => { hudRow?.row.parentNode?.removeChild(hudRow.row); });
|
|
2103
|
+
function reserveRowBand() {
|
|
2104
|
+
reserveBottomBand({ region: $('#game-region'), row: hudRow?.row ?? null, pad: $('#touch-controls') });
|
|
1976
2105
|
}
|
|
1977
2106
|
/*
|
|
1978
2107
|
* THE HUD the engine mounts from the cartridge's `hud` (ADR-0168; issue #162). Read on every animation frame while mounted —
|
|
@@ -1981,13 +2110,27 @@ export function createGame(o) {
|
|
|
1981
2110
|
*/
|
|
1982
2111
|
let hudMounted = null;
|
|
1983
2112
|
let hudFrame = false;
|
|
2113
|
+
/*
|
|
2114
|
+
* THE SESSION CLOCK (ADR-0236, ADR-0239, ADR-0050 §3; issue #94): a label over the time left in digits and a Time Timer pie,
|
|
2115
|
+
* in the centre of the HUD row, for every cartridge — it measures the SESSION, which is this root's life, so a `mount()` does
|
|
2116
|
+
* not restart it and only `dispose()` takes it away. It always shows, measures one hour and turns red at the end: nothing on
|
|
2117
|
+
* the child's side sets it, and nothing about it is stored (ADR-0240).
|
|
2118
|
+
*/
|
|
2119
|
+
const sessionClock = mountSessionClock({
|
|
2120
|
+
doc, slot: hudRow?.clock ?? null, t: translator.t,
|
|
2121
|
+
// as the host has them: a host with no clock mounts none, answered in the module (ADR-0232)
|
|
2122
|
+
performance: win.performance, setInterval: win.setInterval, clearInterval: win.clearInterval,
|
|
2123
|
+
announce: srSay, drawn: reserveRowBand,
|
|
2124
|
+
});
|
|
2125
|
+
whenDisposed(() => { sessionClock?.remove(); });
|
|
2126
|
+
localeOn(() => { sessionClock?.refresh(); });
|
|
1984
2127
|
function mountHud() {
|
|
1985
2128
|
hudMounted?.remove();
|
|
1986
2129
|
hudMounted = null;
|
|
1987
2130
|
const regionEl = $('#game-region');
|
|
1988
2131
|
const numbers = cartridge.hud ?? [];
|
|
1989
2132
|
if (numbers.length && regionEl && typeof regionEl.appendChild === 'function')
|
|
1990
|
-
hudMounted = mountHudBands(translator
|
|
2133
|
+
hudMounted = mountHudBands(translator, doc, regionEl, numbers, 0, hudRow ?? undefined);
|
|
1991
2134
|
reserveBarBand();
|
|
1992
2135
|
if (hudMounted && !hudFrame && typeof win.requestAnimationFrame === 'function') {
|
|
1993
2136
|
hudFrame = true;
|
|
@@ -2215,8 +2358,15 @@ export function createGame(o) {
|
|
|
2215
2358
|
const classes = records.map((r) => r.target.classList);
|
|
2216
2359
|
if (classes.some((c) => c?.contains('screen-pause')))
|
|
2217
2360
|
updateCaption();
|
|
2218
|
-
|
|
2361
|
+
// the modal boundary follows the front card by ANY door, including a close that returns no focus (ADR-0130 rule 1)
|
|
2362
|
+
if (classes.some((c) => c?.contains('screen-pause') || c?.contains('overlay')))
|
|
2363
|
+
overlays.syncLayers?.();
|
|
2364
|
+
if (classes.some((c) => c?.contains('screen-pause') || c?.contains('overlay') || c?.contains('pause-menu'))) {
|
|
2219
2365
|
announceContext();
|
|
2366
|
+
// 🔴 THE NAMES A CHILD CAN SAY CHANGED (ADR-0194 §1): this is the one signal that a card, one of its lists or a panel
|
|
2367
|
+
// showed or hid — without it no menu's names reach the recogniser, and no name can be said.
|
|
2368
|
+
voiceControl?.refreshGrammar();
|
|
2369
|
+
}
|
|
2220
2370
|
// ADR-0166: the pad leaves when the card or a panel opens, and comes back when the last of them closes
|
|
2221
2371
|
if (classes.some((c) => c?.contains('screen-pause') || c?.contains('overlay')))
|
|
2222
2372
|
reflectPadInMenus();
|
|
@@ -2225,6 +2375,16 @@ export function createGame(o) {
|
|
|
2225
2375
|
recomposeWorldFilter();
|
|
2226
2376
|
}).observe(regionEl, { attributes: true, attributeFilter: ['hidden'], subtree: true });
|
|
2227
2377
|
}
|
|
2378
|
+
// ADR-0245: a footer explanation longer than its lines scrolls inside them, at the child's caption rate; reduced motion is the
|
|
2379
|
+
// system's or the child's (any scene switch reduced: pages carry the same words, so a false «reduced» costs nothing)
|
|
2380
|
+
if (regionEl && Observer && typeof win.getComputedStyle === 'function' && typeof doc.createRange === 'function') {
|
|
2381
|
+
watchFooterScroll(regionEl, {
|
|
2382
|
+
doc, getComputedStyle: (el) => win.getComputedStyle(el), MutationObserver: Observer,
|
|
2383
|
+
setTimeout: (fn, ms) => win.setTimeout(fn, ms), clearTimeout: (id) => win.clearTimeout(id),
|
|
2384
|
+
ppm: () => state.captionPpm,
|
|
2385
|
+
reduced: () => defaultReducedMotion(win.matchMedia) || SCENE_KEYS.some((k) => sceneMotion[k]),
|
|
2386
|
+
});
|
|
2387
|
+
}
|
|
2228
2388
|
}
|
|
2229
2389
|
/*
|
|
2230
2390
|
* THE SCREEN FOOTER: one column, at the bottom of the region, with the EXPLANATION of the pointed icon above the quick
|
|
@@ -2265,11 +2425,12 @@ export function createGame(o) {
|
|
|
2265
2425
|
* «the sound caption above, the explanation below it»); `aria-hidden`, because whoever listens heard the sound itself.
|
|
2266
2426
|
* 📌 Its time on screen is a child's reading time for its words, never under the 2600 ms the games had measured in play
|
|
2267
2427
|
* (`core/caption-duration`, plan phase 5c); a new caption restarts it.
|
|
2428
|
+
* 📌 Written while captions are on OR deaf mode is: in deaf mode every sound gets its caption (ADR-0234).
|
|
2268
2429
|
*/
|
|
2269
2430
|
let soundCaption = null;
|
|
2270
2431
|
let clearSoundCaption = null;
|
|
2271
2432
|
function writeSoundCaption(text) {
|
|
2272
|
-
if (!
|
|
2433
|
+
if (!deafMode.captionsOn() || !text)
|
|
2273
2434
|
return;
|
|
2274
2435
|
if (!soundCaption) {
|
|
2275
2436
|
const home = screenFooter($('#game-region'));
|
|
@@ -2287,8 +2448,21 @@ export function createGame(o) {
|
|
|
2287
2448
|
clearTimeout(clearSoundCaption);
|
|
2288
2449
|
clearSoundCaption = setTimeout(() => { captionHome.hidden = true; captionHome.textContent = ''; }, captionDuration(text, state.captionPpm));
|
|
2289
2450
|
}
|
|
2290
|
-
/**
|
|
2451
|
+
/**
|
|
2452
|
+
* The footer says ONE explanation at a time: the pointed icon's, or the reason a locked item is locked (ADR-0161). `null` is
|
|
2453
|
+
* the item leaving — and the band goes back to the game's text, not to empty (ADR-0244 §2).
|
|
2454
|
+
*/
|
|
2291
2455
|
function writeInFooter(text) {
|
|
2456
|
+
itemExplanation = text || null;
|
|
2457
|
+
showExplanation();
|
|
2458
|
+
}
|
|
2459
|
+
/** The game's own explanation, its resting text (ADR-0244 §1); `null` clears it. Under a pointed engine item until it leaves. */
|
|
2460
|
+
function explainForGame(text) {
|
|
2461
|
+
gameExplanation = text || null;
|
|
2462
|
+
showExplanation();
|
|
2463
|
+
}
|
|
2464
|
+
function showExplanation() {
|
|
2465
|
+
const text = itemExplanation ?? gameExplanation;
|
|
2292
2466
|
if (!barExplanation && text) {
|
|
2293
2467
|
const home = screenFooter($('#game-region'));
|
|
2294
2468
|
if (home) {
|
|
@@ -2330,30 +2504,61 @@ export function createGame(o) {
|
|
|
2330
2504
|
if (!toOtherScreen)
|
|
2331
2505
|
changePhase('playing');
|
|
2332
2506
|
}
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2507
|
+
/**
|
|
2508
|
+
* THE PAUSE IS PER SCREEN, AND A SHARED SCREEN'S IS ITS FIRST PLAYER'S (ADR-0144, erratum of 2026-09-26). This root draws ONE
|
|
2509
|
+
* screen — one region, one pause card (`#vp-pause-0`), one quick bar (`getA11yBars`) — and every seat it seats plays on it, so
|
|
2510
|
+
* its pause and its menus answer seat 0: START and SELECT from any other seat, by any transport, do nothing. A player with a
|
|
2511
|
+
* screen of her own has a root of her own, and its pause. START asks this below and the pad's START on an open menu asks it
|
|
2512
|
+
* too (`resume`); SELECT needs no question, because `openSeatMenus` opens the seat's own card and this root mounts seat 0's only.
|
|
2513
|
+
* And INSIDE an open menu the same: `ui/menu-nav` and `input/gamepad` ask it before handing a seat the dialog on top or the
|
|
2514
|
+
* card, so another seat's direction, confirm and back — by its keys, its pad, or a transport pressing the controller, whose
|
|
2515
|
+
* press becomes that seat's key — move, press and close nothing, and reach no game under the menu either.
|
|
2516
|
+
*/
|
|
2517
|
+
function leadsTheScreen(seat) {
|
|
2518
|
+
return seat === 0;
|
|
2519
|
+
}
|
|
2520
|
+
/**
|
|
2521
|
+
* START for a seat, by whatever door: the key below, or a transport pressing the virtual controller in play (`systemPress`,
|
|
2522
|
+
* ADR-0144 §1 «from any transport»). Answers whether it acted — the key is consumed only then.
|
|
2523
|
+
*/
|
|
2524
|
+
function startForSeat(seat) {
|
|
2525
|
+
if (!leadsTheScreen(seat))
|
|
2526
|
+
return false; // another seat of this screen: its START does nothing (ADR-0144 erratum)
|
|
2337
2527
|
// GUARD 1 — A PANEL IS OPEN. With an overlay visible, `menuNavKey` receives the key, finds no intent in it and leaves
|
|
2338
2528
|
// without consuming it. Without this guard the quick pause would open UNDER the panel the child is in. Escape closes
|
|
2339
2529
|
// a panel, not START.
|
|
2340
2530
|
if (overlays.topVisibleOverlay())
|
|
2341
|
-
return;
|
|
2531
|
+
return false;
|
|
2342
2532
|
// START AGAIN LEAVES — the second way out of the mode that ADR-0044 item 7 already gave START.
|
|
2343
2533
|
if (inQuickPause.has(seat)) {
|
|
2344
2534
|
leaveQuickPause(seat, 'jogo');
|
|
2345
|
-
|
|
2346
|
-
return;
|
|
2535
|
+
return true;
|
|
2347
2536
|
}
|
|
2348
2537
|
// GUARD 2 — THE CARD IS OPEN. With it open and a «start» key other than `Enter`, `menuNavKey` finds no intent and
|
|
2349
2538
|
// lets it through. Closing the card belongs to «Voltar ao jogo» and to Escape.
|
|
2350
2539
|
const findPauseCard = $(`#vp-pause-${seat}`);
|
|
2351
2540
|
if (findPauseCard && findPauseCard.hidden === false)
|
|
2352
|
-
return;
|
|
2541
|
+
return false;
|
|
2353
2542
|
enterQuickPause(seat);
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2543
|
+
return true;
|
|
2544
|
+
}
|
|
2545
|
+
function toggleQuickPauseByStart(e) {
|
|
2546
|
+
const seat = seatOfPosition(e.code, 'start');
|
|
2547
|
+
if (seat === null)
|
|
2548
|
+
return;
|
|
2549
|
+
// GUARD 0 — THE KEY WAS TYPED INTO A FIELD. A text field owns Enter and every character, so `Enter` or `H` typed there is
|
|
2550
|
+
// the field's and not START, on the way in and out alike (ADR-0111 erratum of 2026-09-26). Left undecided, so it types.
|
|
2551
|
+
if (keyTypedIntoField(e.target))
|
|
2552
|
+
return;
|
|
2553
|
+
// GUARD 3 — THE KEY PRESSES THE ENGINE'S FOCUSED CONTROL. `Enter` is «start», and a focused button is activated by Enter as
|
|
2554
|
+
// well as Space: on ☰ it is ☰'s press, not the pause (ADR-0111 erratum of 2026-09-26, one key one action). Left undecided
|
|
2555
|
+
// here, so the browser clicks it. Only on the way IN: on the quick pause's own bar the leave keeps its meaning.
|
|
2556
|
+
if (!inQuickPause.has(seat) && keyPressesOwnControl(e.code, e.target, ENGINE_CONTROLS_IN_PLAY))
|
|
2557
|
+
return;
|
|
2558
|
+
// 📌 CONSUMED ONLY when it was in fact OURS. `Enter` is «start» by default (`input/default-bindings`), and without this
|
|
2559
|
+
// the same press would pause AND activate whatever had focus.
|
|
2560
|
+
if (startForSeat(seat))
|
|
2561
|
+
e.preventDefault();
|
|
2357
2562
|
}
|
|
2358
2563
|
win.addEventListener('keydown', toggleQuickPauseByStart);
|
|
2359
2564
|
/*
|
|
@@ -2384,7 +2589,8 @@ export function createGame(o) {
|
|
|
2384
2589
|
}
|
|
2385
2590
|
function openMenusBySelect(e) {
|
|
2386
2591
|
const seat = seatOfPosition(e.code, 'select');
|
|
2387
|
-
|
|
2592
|
+
// `F` typed into a field is a letter, not SELECT (ADR-0111 erratum of 2026-09-26): the field owns every character.
|
|
2593
|
+
if (seat === null || keyTypedIntoField(e.target))
|
|
2388
2594
|
return;
|
|
2389
2595
|
if (openSeatMenus(seat))
|
|
2390
2596
|
e.preventDefault();
|
|
@@ -2399,8 +2605,8 @@ export function createGame(o) {
|
|
|
2399
2605
|
const seat = seatOfPosition(e.code, 'action4');
|
|
2400
2606
|
if (seat === null || !inQuickPause.has(seat))
|
|
2401
2607
|
return;
|
|
2402
|
-
if (overlays.topVisibleOverlay())
|
|
2403
|
-
return;
|
|
2608
|
+
if (overlays.topVisibleOverlay() || keyTypedIntoField(e.target))
|
|
2609
|
+
return; // a letter typed into a field
|
|
2404
2610
|
leaveQuickPause(seat, 'cartao');
|
|
2405
2611
|
pauseControls.show(seat);
|
|
2406
2612
|
e.preventDefault();
|
|
@@ -2418,6 +2624,8 @@ export function createGame(o) {
|
|
|
2418
2624
|
*/
|
|
2419
2625
|
const touchHostEl = o.host.touchHost ?? $('#game-region');
|
|
2420
2626
|
const touchUsable = !!touchHostEl && typeof touchHostEl.appendChild === 'function';
|
|
2627
|
+
// the HUD row and the footer stand above the pad from the frame it shows in, and come down in the frame it leaves (ADR-0239 §6)
|
|
2628
|
+
whenDisposed(followThePad(touchHostEl, win.MutationObserver, reserveRowBand));
|
|
2421
2629
|
const seat0CardOpen = () => {
|
|
2422
2630
|
const c = $('#vp-pause-0');
|
|
2423
2631
|
return !!c && c.hidden === false;
|
|
@@ -2437,9 +2645,7 @@ export function createGame(o) {
|
|
|
2437
2645
|
eventTarget.dispatchEvent(stampSource(new KeyboardEvent('keydown', { code, key: code, bubbles: true, cancelable: true }), origin));
|
|
2438
2646
|
};
|
|
2439
2647
|
const labelledActions = () => {
|
|
2440
|
-
const preset =
|
|
2441
|
-
if (!preset)
|
|
2442
|
-
return [];
|
|
2648
|
+
const preset = actionWords();
|
|
2443
2649
|
const labelOf = labellerFrom(preset);
|
|
2444
2650
|
return presetActions(preset).flatMap((a) => {
|
|
2445
2651
|
const label = labelOf(a);
|
|
@@ -2457,6 +2663,8 @@ export function createGame(o) {
|
|
|
2457
2663
|
// buttons DIRECTLY (ADR-0166, which undid ADR-0157's «the pad stays over the menus»). The only two callers that show
|
|
2458
2664
|
// the pad — the touch listener below and `reflectPadInMenus` — already ask `isMenuOpen()` first.
|
|
2459
2665
|
padAllowed: () => players().length <= 1,
|
|
2666
|
+
// each slot drawn by its size, five named positions or fewer a cycle row (ADR-0130 rule 3)
|
|
2667
|
+
drawChoice: (spec, pick) => mountChoice({ find: (sel) => $(sel), create: (tag) => doc.createElement(tag) }, spec, pick),
|
|
2460
2668
|
});
|
|
2461
2669
|
/** A menu the child touches directly is open: the pause card or a settings panel (ADR-0166 rule 2). */
|
|
2462
2670
|
function isMenuOpen() {
|
|
@@ -2490,7 +2698,7 @@ export function createGame(o) {
|
|
|
2490
2698
|
return;
|
|
2491
2699
|
}
|
|
2492
2700
|
const padMap = touchPad.getTouchMap();
|
|
2493
|
-
const short =
|
|
2701
|
+
const short = shortLabellerFrom(actionWords());
|
|
2494
2702
|
const pad = mountTouchControls({ find: (sel) => $(sel), create: (tag) => doc.createElement(tag), t: translator.t }, {
|
|
2495
2703
|
map: padMap,
|
|
2496
2704
|
gameActions: cartridgeActions(),
|
|
@@ -2539,6 +2747,8 @@ export function createGame(o) {
|
|
|
2539
2747
|
*/
|
|
2540
2748
|
press: (action, source) => virtualController.press(action, source),
|
|
2541
2749
|
release: (action, source) => virtualController.release(action, source),
|
|
2750
|
+
// the RAW edge: the press above already recorded it and resolved touch's latch at the controller's `pressedBy`, so this only
|
|
2751
|
+
// repeats an edge the automaton holds (ADR-0109 rule 3)
|
|
2542
2752
|
playerEdge,
|
|
2543
2753
|
heldKeys: keys,
|
|
2544
2754
|
attractOnInput: () => false,
|
|
@@ -2628,6 +2838,8 @@ export function createGame(o) {
|
|
|
2628
2838
|
* ⚠️ IT EXISTS ONLY WHERE THERE IS A TOUCH HOST: without one there is no pad size to choose, and the submenu's door
|
|
2629
2839
|
* falls away by itself (`engineActions.motora` is not defined).
|
|
2630
2840
|
*/
|
|
2841
|
+
/** The motor panel's mapping wizard, once mounted: declared out here because the pad's poll below stands aside while it maps. */
|
|
2842
|
+
let padWizard = null;
|
|
2631
2843
|
if (pauseMountPoint && pauseUsable && touchUsable) {
|
|
2632
2844
|
const mobilityCtx = {
|
|
2633
2845
|
find: (sel) => $(sel),
|
|
@@ -2701,11 +2913,10 @@ export function createGame(o) {
|
|
|
2701
2913
|
const modeLabel = (m) => t(m === 1 ? 'motora.teclado.1' : m === 2 ? 'motora.teclado.2' : 'motora.teclado.34');
|
|
2702
2914
|
const SIDES = ['leftShoulder', 'leftTrigger', 'rightShoulder', 'rightTrigger'];
|
|
2703
2915
|
const actionsToMap = () => {
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
const actionWord = word(presetAction);
|
|
2916
|
+
const words = actionWords();
|
|
2917
|
+
const nameOf = labellerFrom(words);
|
|
2918
|
+
return presetActions(words).flatMap((presetAction) => {
|
|
2919
|
+
const actionWord = nameOf(presetAction);
|
|
2709
2920
|
return actionWord ? [{ action: presetAction, label: actionWord }] : [];
|
|
2710
2921
|
});
|
|
2711
2922
|
};
|
|
@@ -2831,18 +3042,30 @@ export function createGame(o) {
|
|
|
2831
3042
|
};
|
|
2832
3043
|
reflectKeyboardRows();
|
|
2833
3044
|
/*
|
|
2834
|
-
* MAPEAR CONTROLE (ADR-0151 §2; issue #182): the engine's own wizard (`input/pad-wizard`), asking
|
|
2835
|
-
* game names, in its words, and
|
|
2836
|
-
*
|
|
2837
|
-
*
|
|
3045
|
+
* MAPEAR CONTROLE (ADR-0151 §2; issue #182): the engine's own wizard (`input/pad-wizard`), asking the positions this
|
|
3046
|
+
* game names, in its words, then START and SELECT in the engine's (ADR-0144 erratum of 2026-09-26 — this labeller is the
|
|
3047
|
+
* preset's, which may not name them), and storing the map `initGamepad` reads — one cache for the page. It reads the pads
|
|
3048
|
+
* only while it is open. «Voltar» cancels; SELECT, the last step, saves and closes. The shell's «restore» is hidden: a
|
|
3049
|
+
* pad's map is replaced by mapping again.
|
|
3050
|
+
* 🔴 ONE WIZARD OWNS THE PAD: the gamepad transport builds its own wizard too (the one that opens by itself on a DirectInput
|
|
3051
|
+
* pad with no map), and each asks only about itself. So the root, which holds both, keeps them apart: while this one maps,
|
|
3052
|
+
* the pad's poll does not run (below), and while that one maps, this row does not open a second.
|
|
3053
|
+
*/
|
|
3054
|
+
/**
|
|
3055
|
+
* One closer for «Voltar» and Escape: a running wizard is cancelled (and its close hides the panel); an idle one just hides.
|
|
3056
|
+
* 🔴 WHICHEVER WIZARD IS MAPPING: the transport's own shows in this same overlay, and hiding it alone left that wizard
|
|
3057
|
+
* mapping unseen — the game paused, the pad answering questions nobody showed, and its map stored at the end.
|
|
2838
3058
|
*/
|
|
2839
|
-
let padWizard = null;
|
|
2840
|
-
/** One closer for «Voltar» and Escape: a running wizard is cancelled (and its close hides the panel); an idle one just hides. */
|
|
2841
3059
|
const closeControl = () => {
|
|
2842
3060
|
if (padWizard?.state()) {
|
|
2843
3061
|
padWizard.close(false);
|
|
2844
3062
|
return;
|
|
2845
3063
|
}
|
|
3064
|
+
// its close hides the overlay, resumes the game it paused, and gives the focus back through `wizardClosed` (ADR-0248)
|
|
3065
|
+
if (gamepad.getPadWiz()) {
|
|
3066
|
+
gamepad.closePadWiz(false);
|
|
3067
|
+
return;
|
|
3068
|
+
}
|
|
2846
3069
|
controlPanel.shell.overlay.hidden = true;
|
|
2847
3070
|
overlays.restoreFocus?.('padwiz');
|
|
2848
3071
|
};
|
|
@@ -2893,6 +3116,8 @@ export function createGame(o) {
|
|
|
2893
3116
|
controlButton.setAttribute('type', 'button');
|
|
2894
3117
|
controlButton.id = 'opt-controle';
|
|
2895
3118
|
controlButton.addEventListener('click', () => {
|
|
3119
|
+
if (gamepad.getPadWiz())
|
|
3120
|
+
return; // the transport's own wizard is mapping a pad: it keeps it
|
|
2896
3121
|
controlPanel.open();
|
|
2897
3122
|
padWizard?.open();
|
|
2898
3123
|
});
|
|
@@ -2902,7 +3127,8 @@ export function createGame(o) {
|
|
|
2902
3127
|
controlStrong.textContent = t('motora.controle');
|
|
2903
3128
|
controlButton.textContent = t('motora.abrir');
|
|
2904
3129
|
controlButton.setAttribute('aria-label', t('motora.controle'));
|
|
2905
|
-
|
|
3130
|
+
// ⚠️ SHOWN FOR EVERY GAME: one that names no position gets the wizard's fourteen default positions (ADR-0144, erratum of
|
|
3131
|
+
// 2026-09-26), so there is always something to map.
|
|
2906
3132
|
};
|
|
2907
3133
|
reflectControlRow();
|
|
2908
3134
|
/*
|
|
@@ -2926,7 +3152,7 @@ export function createGame(o) {
|
|
|
2926
3152
|
const hideSlotsWithoutAction = () => {
|
|
2927
3153
|
const named = cartridgeActions();
|
|
2928
3154
|
const padMap = touchPad.getTouchMap();
|
|
2929
|
-
for (const sel of Array.from(touchPanel.shell.list.querySelectorAll('
|
|
3155
|
+
for (const sel of Array.from(touchPanel.shell.list.querySelectorAll('[data-slot]'))) {
|
|
2930
3156
|
const rowNode = sel.closest('.ctrl-row');
|
|
2931
3157
|
if (rowNode)
|
|
2932
3158
|
rowNode.hidden = !named.has(padMap[sel.dataset.slot ?? ''] ?? '');
|
|
@@ -2958,10 +3184,8 @@ export function createGame(o) {
|
|
|
2958
3184
|
* 🔴 NO STICKY-KEYS ROW IN THIS PANEL (the Dev: «Tire a linha de acessibilidade motora»): the bar's ☝️ is the one
|
|
2959
3185
|
* surface for that cycle (ADR-0218), and two surfaces of one setting was what the row had become.
|
|
2960
3186
|
*
|
|
2961
|
-
*
|
|
2962
|
-
*
|
|
2963
|
-
* «padrão» does not appear, and there is nothing to explain. The «Esperar entre toques» row (ADR-0217) stays: it is
|
|
2964
|
-
* another setting.
|
|
3187
|
+
* 📌 No device locks it any more (ADR-0249): on eyes, face, gestures and speech the latch starts as the game's `holdsKeys()`
|
|
3188
|
+
* and the ☝️ offers all three positions there too. The «Esperar entre toques» row (ADR-0217) stays: it is another setting.
|
|
2965
3189
|
*/
|
|
2966
3190
|
/*
|
|
2967
3191
|
* «ESPERAR ENTRE TOQUES» (ADR-0217; GAG Advanced/Motor, issue #182). The row beside the sticky keys, and the other half of
|
|
@@ -3031,7 +3255,10 @@ export function createGame(o) {
|
|
|
3031
3255
|
return; // at the end of the line nothing moved, and nothing is announced
|
|
3032
3256
|
state.setCameraControlValue(mode);
|
|
3033
3257
|
reflectCamera();
|
|
3034
|
-
|
|
3258
|
+
// 🔴 A MODE THAT STARTS IS ANNOUNCED BY ITS CONTROL, which answers once it has really started or says why it could not —
|
|
3259
|
+
// the rule and its reason are at the 📷 in `ui/pause-icons`. Off is true at once, and is said here.
|
|
3260
|
+
if (mode === 'off')
|
|
3261
|
+
srSay(`${t('motora.camera')}: ${t('state.off')}`);
|
|
3035
3262
|
});
|
|
3036
3263
|
// the 📷 and this row are one setting: whoever changes it, both show it
|
|
3037
3264
|
stateOn('cameraControl', () => { reflectCamera(); });
|
|
@@ -3058,10 +3285,12 @@ export function createGame(o) {
|
|
|
3058
3285
|
};
|
|
3059
3286
|
voiceButton.addEventListener('click', () => {
|
|
3060
3287
|
state.setVoiceControlValue(!state.voiceControl);
|
|
3061
|
-
// ⚠️ THE ANNOUNCEMENT READS THE STATE AFTER THE WRITE, and not the value it meant to write: what cannot start puts the
|
|
3062
|
-
// answer back to off inside the same click, and announcing the intention would tell the child the opposite of what is true.
|
|
3063
3288
|
reflectVoice();
|
|
3064
|
-
|
|
3289
|
+
// 🔴 ON IS ANNOUNCED BY `ui/voice-control`, which starts AFTER this click, asynchronously, and answers either way: ready,
|
|
3290
|
+
// or the reason it could not start. Reading the state here still read «on», because the failure had not happened yet —
|
|
3291
|
+
// the rule and its reason are at the 👄 in `ui/pause-icons`. Off is true at once, and is said here.
|
|
3292
|
+
if (!state.voiceControl)
|
|
3293
|
+
srSay(`${t('motora.voz')}: ${t('state.off')}`);
|
|
3065
3294
|
});
|
|
3066
3295
|
stateOn('voiceControl', () => { reflectVoice(); });
|
|
3067
3296
|
reflectVoice();
|
|
@@ -3162,17 +3391,38 @@ export function createGame(o) {
|
|
|
3162
3391
|
* 📌 The right channel is the one the function already has: `onHeavyProgress`, handed to whoever calls. A consumer who
|
|
3163
3392
|
* wants to show «N MB left» or «the voice did not come down» has a way; the engine invents no surface.
|
|
3164
3393
|
*/
|
|
3394
|
+
/*
|
|
3395
|
+
* THE LIBRARY FONTS THIS GAME DECLARED (ADR-0255): their `@font-face` rules go into the document as soon as the catalogue
|
|
3396
|
+
* loads, pointing at the delivery's `heavy/`, and their files are kept FIRST in the checked cache — a family the library does
|
|
3397
|
+
* not hold, or the delivery did not carry, becomes a line of `problems`. No catalogue is loaded for a game that declared none.
|
|
3398
|
+
*/
|
|
3399
|
+
const fontsStarted = libraryFonts.asked.length ? startLibraryFonts({
|
|
3400
|
+
asked: libraryFonts.asked,
|
|
3401
|
+
declare: (css) => { const sheet = doc.createElement('style'); sheet.id = 'incl-library-fonts'; sheet.textContent = css; (doc.head ?? doc.documentElement).appendChild(sheet); },
|
|
3402
|
+
href: (url) => new URL(deliveryPath(url), doc.baseURI).href,
|
|
3403
|
+
report: (line) => { if (!measuredProblems.includes(line))
|
|
3404
|
+
measuredProblems.push(line); },
|
|
3405
|
+
}) : null;
|
|
3165
3406
|
if (o.downloadHeavy !== false) {
|
|
3166
|
-
// ⚠️ THE READING
|
|
3167
|
-
//
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
//
|
|
3172
|
-
|
|
3173
|
-
|
|
3407
|
+
// ⚠️ THE READING MODELS OF EVERY LANGUAGE THE PAGE CAN SWITCH TO, when the game listens (ADR-0225 erratum, the Dev:
|
|
3408
|
+
// «Negativo, baixar os três. Toda criança vai experimentar as três línguas imediatamente.»). `bcp47()` is the language the
|
|
3409
|
+
// interface booted in (ADR-0031), named first so everything of hers comes down before the 850 MiB of the three does.
|
|
3410
|
+
const keepHeavy = (fonts) => downloadHeavy({
|
|
3411
|
+
also: fonts?.files ?? [],
|
|
3412
|
+
// 📌 AND THE COMMAND MODELS ARE ASKED FOR WITHOUT ASKING THE GAME (issue #184): a child who says «menu» instead of pressing
|
|
3413
|
+
// it is reaching the controller, and no cartridge declares — or denies — a way in (ADR-0111). ONE PER LANGUAGE THE PAGE
|
|
3414
|
+
// CAN SWITCH TO, the child's first (ADR-0225 erratum): a language changed mid-game, offline the next day, must find its
|
|
3415
|
+
// model kept. A language the delivery's `--commands` left out fails quietly here, and the transport says so when she speaks.
|
|
3416
|
+
// 📌 The Libras player only while deaf mode is on (ADR-0234): the delivery's list of its avatar, clips, glosses and three.js,
|
|
3417
|
+
// which a child who never asks for signing does not pay — and which keep it signing on the days without a network.
|
|
3418
|
+
only: heavyAtBoot({
|
|
3419
|
+
kokoro: !!o.uses?.neuralVoice, reading: o.uses?.reading ? [bcp47(), ...availableLocales()] : null,
|
|
3420
|
+
commands: [bcp47(), ...availableLocales()], libras: deafMode.isOn(),
|
|
3421
|
+
}),
|
|
3422
|
+
onProgress: (r) => { fonts?.onReport(r); o.onHeavyProgress?.(r); },
|
|
3174
3423
|
cacheStorage: heavyCaches, fetch: win.fetch, digest: sha256With(win.crypto?.subtle), base: doc.baseURI,
|
|
3175
|
-
})
|
|
3424
|
+
});
|
|
3425
|
+
void (fontsStarted ? fontsStarted.then(keepHeavy) : keepHeavy(null))
|
|
3176
3426
|
.catch(() => { });
|
|
3177
3427
|
}
|
|
3178
3428
|
/*
|
|
@@ -3192,22 +3442,16 @@ export function createGame(o) {
|
|
|
3192
3442
|
// absent value becomes `{}` — the cartridge that did not answer — and is refused below with ADR-0153's sentence, never
|
|
3193
3443
|
// with a TypeError on `hooks.preset`.
|
|
3194
3444
|
function mountAll(declaration, hooks = {}) {
|
|
3195
|
-
// ⚠️ IT THROWS, IT DOES NOT DIAGNOSE — the boot's rule, and so the boot's sentence. A
|
|
3196
|
-
// precondition: `problems` is for gaps one can still play with, and this is not one.
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
}
|
|
3201
|
-
// ⚠️ AND `mount()` REFUSES BY THE SAME RULES, before writing to `cartridge`. `CartridgeHooks` is
|
|
3202
|
-
// `Omit<GameHalf, 'declaration'>`, so it carries `preset` — a second cartridge could take the «start» the first one
|
|
3203
|
-
// respected, and the root would be left with the pause unreachable mid-session.
|
|
3204
|
-
refuseIfItClaimsStart('mount', hooks.preset);
|
|
3205
|
-
refuseIfNoAnswer('mount', hooks.accommodations);
|
|
3206
|
-
refuseIfGenreRefused('mount', hooks.genre);
|
|
3207
|
-
refuseIfHudMalformed('mount', hooks.hud);
|
|
3208
|
-
refuseIfOptionsMalformed('mount', hooks.gameOptions);
|
|
3209
|
-
refuseIfHowToPlayMalformed('mount', hooks.howToPlay);
|
|
3445
|
+
// ⚠️ IT THROWS, IT DOES NOT DIAGNOSE — the boot's rule, and so the boot's list and sentence (`cartridgeRefusals`). A
|
|
3446
|
+
// malformed cartridge is a precondition: `problems` is for gaps one can still play with, and this is not one. And it
|
|
3447
|
+
// refuses BEFORE writing to `cartridge`: `CartridgeHooks` carries `preset`, so a second cartridge could take the
|
|
3448
|
+
// «start» the first one respected, and the root would be left with the pause unreachable mid-session.
|
|
3449
|
+
refuseCartridge('mount', declaration, hooks);
|
|
3210
3450
|
cartridge = { ...hooks, declaration };
|
|
3451
|
+
explainForGame(null); // the explanation was the replaced cartridge's (ADR-0244)
|
|
3452
|
+
// its words, before anything draws them: ADDED to the root's dictionary, so the keys a shell registered at boot stay
|
|
3453
|
+
for (const [code, entries] of Object.entries(hooks.dictionaries ?? {}))
|
|
3454
|
+
translator.registerDict(code, entries);
|
|
3211
3455
|
mountHud(); // the numbers are the cartridge's: the new one's replace the old one's, and the room is measured again
|
|
3212
3456
|
followCartridgeMappings(declaration);
|
|
3213
3457
|
redrawGameOptions(); // the rows are the new cartridge's, drawn or cleared before its door is weighed
|
|
@@ -3286,11 +3530,42 @@ unstyled, so a child who plays by keyboard cannot see where focus is — link th
|
|
|
3286
3530
|
* vocabulary (ADR-0216): a method only this file calls has no business in a contract seven repositories read.
|
|
3287
3531
|
*/
|
|
3288
3532
|
let closeReadingThread = () => { };
|
|
3533
|
+
// the command recogniser's bundle, put in the page once and shared by the 👄 and a choice by voice
|
|
3534
|
+
const voskBundle = createBundleLoader((url) => import(/* @vite-ignore */ url));
|
|
3535
|
+
// one command model per language, loaded once for the 👄 and a choice alike, ended with the root (ADR-0220, ADR-0256)
|
|
3536
|
+
const voskModels = keepVoskModels(loadVoskRuntime);
|
|
3537
|
+
whenDisposed(() => { voskModels.closeAll(); });
|
|
3538
|
+
/*
|
|
3539
|
+
* A CHOICE AMONG THE OPTIONS THE GAME SHOWS (ADR-0256): heard by the command recogniser with a grammar of exactly those options,
|
|
3540
|
+
* and the 👄 paused meanwhile — one ear per word. Not tied to `uses.reading`: the command models are asked of every delivery. A
|
|
3541
|
+
* device with no microphone or no model is told by the chooser itself, in `problems` and to the game.
|
|
3542
|
+
*/
|
|
3543
|
+
const chooser = createVoiceChooser({
|
|
3544
|
+
language: () => bcp47(),
|
|
3545
|
+
open: openWithCommandRecogniser({
|
|
3546
|
+
base: doc.baseURI, hasFile: hasHeavyFile, loadBundle: voskBundle, fetch: (url) => win.fetch(url),
|
|
3547
|
+
getUserMedia, createContext: () => new HostAudioContext(), load: voskModels.load,
|
|
3548
|
+
}),
|
|
3549
|
+
// the 👄 already listening in the options' language lends its ear: no second model, no second microphone
|
|
3550
|
+
borrow: (language, grammar, onHeard) => voiceControl?.borrow(language, grammar, onHeard) ?? null,
|
|
3551
|
+
after: (fn, ms) => win.setTimeout(fn, ms),
|
|
3552
|
+
cancel: (h) => win.clearTimeout(h),
|
|
3553
|
+
report: (line) => { if (!measuredProblems.includes(line))
|
|
3554
|
+
measuredProblems.push(line); },
|
|
3555
|
+
// `apply(false)` stops without writing the child's stored answer (ADR-0220), so resuming is applying that answer again
|
|
3556
|
+
pause: () => { void voiceControl?.apply(false); },
|
|
3557
|
+
resume: () => { void voiceControl?.apply(state.voiceControl); },
|
|
3558
|
+
});
|
|
3289
3559
|
const reading = (() => {
|
|
3290
3560
|
const browserApis = win;
|
|
3291
3561
|
let microphone = null;
|
|
3292
|
-
/**
|
|
3293
|
-
|
|
3562
|
+
/**
|
|
3563
|
+
* The reading thread, ONE PER LANGUAGE and kept between readings — opening it compiles the model again, up to 378 MiB — and let
|
|
3564
|
+
* go with the game (`platform/reading-in-worker.keepOneThreadPerLanguage`, built at the first reading with the module).
|
|
3565
|
+
*/
|
|
3566
|
+
let readingThreads = null;
|
|
3567
|
+
let readingRuntimes = null;
|
|
3568
|
+
closeReadingThread = () => { readingThreads?.close(); readingRuntimes?.close(); };
|
|
3294
3569
|
const listener = createReading({
|
|
3295
3570
|
language: () => bcp47(),
|
|
3296
3571
|
api: (browserApis.SpeechRecognition ?? browserApis.webkitSpeechRecognition ?? null),
|
|
@@ -3299,6 +3574,7 @@ unstyled, so a child who plays by keyboard cannot see where focus is — link th
|
|
|
3299
3574
|
stopEvery: (h) => win.clearInterval(h),
|
|
3300
3575
|
report: (rowNode) => { if (!measuredProblems.includes(rowNode))
|
|
3301
3576
|
measuredProblems.push(rowNode); },
|
|
3577
|
+
chooser,
|
|
3302
3578
|
/**
|
|
3303
3579
|
* THE ENGINE'S OWN RECOGNISER, and it arrives late on purpose (ADR-0216 §5): `platform/reading-runtime` is what names the
|
|
3304
3580
|
* model files, so a game that never listens — and a child of a game that does, until the first `listen()` — loads none of
|
|
@@ -3322,31 +3598,45 @@ unstyled, so a child who plays by keyboard cannot see where focus is — link th
|
|
|
3322
3598
|
*/
|
|
3323
3599
|
const Worker = browserApis.Worker;
|
|
3324
3600
|
if (typeof Worker === 'function') {
|
|
3325
|
-
const { createReadingInWorker } = await import('../platform/reading-in-worker.js');
|
|
3326
|
-
|
|
3327
|
-
|
|
3601
|
+
const { createReadingInWorker, keepOneThreadPerLanguage } = await import('../platform/reading-in-worker.js');
|
|
3602
|
+
// 🔴 THE SAME LANGUAGE REUSES ITS THREAD: this port is asked at EVERY `listen()`, and it used to close and reopen the
|
|
3603
|
+
// thread each time — recompiling up to 378 MiB per reading. Another language closes the old one and opens hers.
|
|
3604
|
+
readingThreads ??= keepOneThreadPerLanguage();
|
|
3605
|
+
return readingThreads.forLanguage(language, (failed) => createReadingInWorker({
|
|
3328
3606
|
base: doc.baseURI,
|
|
3329
3607
|
language,
|
|
3330
3608
|
spawn: () => new Worker(new URL('../platform/reading-worker.js', import.meta.url), { type: 'module' }),
|
|
3331
3609
|
// 📌 THE SENTENCE IS WRITTEN HERE and the module hands over only the REASON: a thread that fails to open when
|
|
3332
3610
|
// nobody is waiting for the answer had nowhere to be said (ADR-0169), and whoever knows what the child loses
|
|
3333
|
-
// is the diagnostic channel, not a thread's protocol.
|
|
3611
|
+
// is the diagnostic channel, not a thread's protocol. It names BOTH fixes, since `--reading <language>` alone
|
|
3612
|
+
// would now narrow the delivery (ADR-0225 erratum) — and the child is told too, not only the adult.
|
|
3334
3613
|
report: (reason) => {
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
+ 'language
|
|
3614
|
+
failed(); // forgotten: the next reading opens it again, the model may have come down since
|
|
3615
|
+
const line = `reading: the transcription thread could not open for ${language} — ${reason}; a child who reads `
|
|
3616
|
+
+ 'aloud in this language gets no answer — build the delivery with `npx inclusionist-heavy --reading` alone (it '
|
|
3617
|
+
+ `carries every language) or with \`--reading ${language.split('-')[0].toLowerCase()}\` in its list, and open `
|
|
3618
|
+
+ 'the game once online so the install fetches it';
|
|
3338
3619
|
if (!measuredProblems.includes(line))
|
|
3339
3620
|
measuredProblems.push(line);
|
|
3621
|
+
srAlert(t('sr.reading.failed'));
|
|
3340
3622
|
},
|
|
3341
|
-
});
|
|
3342
|
-
closeReadingThread = () => { readingThread?.close(); readingThread = null; };
|
|
3343
|
-
return readingThread;
|
|
3623
|
+
}));
|
|
3344
3624
|
}
|
|
3345
|
-
|
|
3625
|
+
const sameThreadLine = 'reading: this browser has no `Worker`, so the transcription runs on the same thread that '
|
|
3346
3626
|
+ 'draws the game and feeds the microphone — measured, that cuts the recording in gaps of seconds and the child '
|
|
3347
|
-
+ 'loses the words she said meanwhile; serve the game where workers are available'
|
|
3348
|
-
|
|
3349
|
-
|
|
3627
|
+
+ 'loses the words she said meanwhile; serve the game where workers are available';
|
|
3628
|
+
if (!measuredProblems.includes(sameThreadLine))
|
|
3629
|
+
measuredProblems.push(sameThreadLine);
|
|
3630
|
+
// 🔴 ONE RUNTIME PER LANGUAGE HERE TOO (issue #185): loading it at every `listen()` fetched and compiled up to 378 MiB per
|
|
3631
|
+
// reading, on the very thread that draws the game. A switch loads hers; a load that failed is forgotten and tried again.
|
|
3632
|
+
const { keepOneThreadPerLanguage } = await import('../platform/reading-in-worker.js');
|
|
3633
|
+
readingRuntimes ??= keepOneThreadPerLanguage();
|
|
3634
|
+
return readingRuntimes.forLanguage(language, (failed) => {
|
|
3635
|
+
const runtime = import('../platform/reading-runtime.js')
|
|
3636
|
+
.then(({ loadReadingRuntime }) => loadReadingRuntime({ base: doc.baseURI, language, fetch: (url) => win.fetch(url) }));
|
|
3637
|
+
runtime.catch(() => failed()); // the reading that asked still hears the reason: it awaits this same promise
|
|
3638
|
+
return { runtime, close: () => { } };
|
|
3639
|
+
}).runtime;
|
|
3350
3640
|
}
|
|
3351
3641
|
: undefined,
|
|
3352
3642
|
/**
|
|
@@ -3368,6 +3658,7 @@ unstyled, so a child who plays by keyboard cannot see where focus is — link th
|
|
|
3368
3658
|
return {
|
|
3369
3659
|
ready: () => (o.uses?.reading ? listener.ready() : Promise.resolve({ can: false, why: 'no-model' })),
|
|
3370
3660
|
stop: () => listener.stop(),
|
|
3661
|
+
choose: (options, settings) => listener.choose(options, settings),
|
|
3371
3662
|
listen: (options) => {
|
|
3372
3663
|
if (!o.uses?.reading) {
|
|
3373
3664
|
if (!measuredProblems.includes(notDeclared))
|
|
@@ -3392,52 +3683,90 @@ unstyled, so a child who plays by keyboard cannot see where focus is — link th
|
|
|
3392
3683
|
/*
|
|
3393
3684
|
* THE SCAN ITSELF (ADR-0218): the list is the positions this cartridge declared AND NAMED, because the chip says the game's
|
|
3394
3685
|
* own words and a position nobody named would cost the child a pass of silence (ADR-0074). It is rebuilt every time the scan
|
|
3395
|
-
* starts, so a `mount()` of another cartridge scans ITS positions and not the ones that booted first (ADR-0142).
|
|
3686
|
+
* starts, so a `mount()` of another cartridge scans ITS positions and not the ones that booted first (ADR-0142). After them come
|
|
3687
|
+
* the engine's own doors, «menu» (SELECT) and «pausar» (START), each only where it has something behind it (ADR-0218 §3).
|
|
3688
|
+
*
|
|
3689
|
+
* 🔴 AND INSIDE A MENU THE LIST IS THAT MENU'S STEPS (ADR-0218 erratum of 2026-09-26). The switch's key is stopped before any
|
|
3690
|
+
* listener, so no menu ever saw it: 📏 a child who opened the quick pause by scanning was left in it, every press doing nothing.
|
|
3691
|
+
* With the quick bar, the card or a panel in front, the scan offers «cancel · next · confirm · back · previous» and a press
|
|
3692
|
+
* moves the menu through `ui/menu-nav`, the path the keys and the pad take. On a panel's control with a value — a slider, a
|
|
3693
|
+
* list, a ⯇ ⯈ row — the pass also offers «increase» and «decrease», the right and left keys' step (`menuScanFor`).
|
|
3396
3694
|
*/
|
|
3397
3695
|
const gameRegion = $('#game-region');
|
|
3398
3696
|
const scanChip = gameRegion ? mountScanOverlay(doc, gameRegion) : null;
|
|
3399
|
-
let
|
|
3697
|
+
let scanning = null;
|
|
3400
3698
|
let scanFrame = 0;
|
|
3401
|
-
|
|
3699
|
+
// «Is a menu in front?» is the question the virtual controller asks (`menuWithDpad`): the quick pause, the card or a panel.
|
|
3700
|
+
const freshScan = () => {
|
|
3701
|
+
if (menuWithDpad()) {
|
|
3702
|
+
const cursor = nav.underCursor(0);
|
|
3703
|
+
return { inMenu: true, cursor, pass: createSwitchScan(menuScanFor(cursor)) };
|
|
3704
|
+
}
|
|
3705
|
+
const words = actionWords();
|
|
3706
|
+
const names = labellerFrom(words);
|
|
3707
|
+
// the ENGINE'S DOORS after the game's words (ADR-0218 §3): the card where it is mounted, the quick pause where it has its bar
|
|
3708
|
+
const doors = { menus: pauseUsable, quickPause: barUsable };
|
|
3709
|
+
return { inMenu: false, pass: createSwitchScan(playScanList(presetActions(words).filter((a) => !!names(a)), doors)) };
|
|
3710
|
+
};
|
|
3711
|
+
const scanWord = (item) => scanItemText(item, labellerFrom(actionWords()), t);
|
|
3402
3712
|
// A word of a different length is a different amount of room to keep free, so the band is measured again — and only then.
|
|
3403
3713
|
const scanShow = (item) => { if (scanChip?.showing(scanWord(item)))
|
|
3404
3714
|
reserveBarBand(); };
|
|
3405
3715
|
const scanTick = () => {
|
|
3406
|
-
if (!
|
|
3716
|
+
if (!scanning)
|
|
3407
3717
|
return;
|
|
3408
|
-
|
|
3718
|
+
// 📌 A MENU OPENED OR CLOSED, OR ITS CURSOR REACHED A CONTROL OF ANOTHER KIND: the pass becomes the other list, from
|
|
3719
|
+
// «cancel», which is also what a press does — so the first thing offered in a menu just opened is the item that takes nothing.
|
|
3720
|
+
if (scanning.inMenu !== menuWithDpad() || (scanning.inMenu && scanning.cursor !== nav.underCursor(0)))
|
|
3721
|
+
scanning = freshScan();
|
|
3722
|
+
scanShow(scanning.pass(win.performance.now()).showing.item);
|
|
3409
3723
|
scanFrame = win.requestAnimationFrame(scanTick);
|
|
3410
3724
|
};
|
|
3411
3725
|
const stopScan = () => {
|
|
3412
3726
|
if (scanFrame)
|
|
3413
3727
|
win.cancelAnimationFrame(scanFrame);
|
|
3414
3728
|
scanFrame = 0;
|
|
3415
|
-
|
|
3729
|
+
scanning = null;
|
|
3416
3730
|
scanChip?.hide();
|
|
3417
3731
|
reserveBarBand(); // the room the chip was keeping goes back to the game
|
|
3418
3732
|
};
|
|
3419
3733
|
const startScan = () => {
|
|
3420
|
-
if (
|
|
3734
|
+
if (scanning)
|
|
3421
3735
|
return;
|
|
3422
|
-
|
|
3423
|
-
const offered = (cartridge.preset ? presetActions(cartridge.preset) : []).filter((a) => !!names?.(a));
|
|
3424
|
-
scanner = createSwitchScan(offered);
|
|
3736
|
+
scanning = freshScan();
|
|
3425
3737
|
scanTick();
|
|
3426
3738
|
};
|
|
3739
|
+
/** The scan is pressing the controller itself: that press is the position it took, and must not be taken again. */
|
|
3740
|
+
let scanIsPressing = false;
|
|
3427
3741
|
scanPress = (source) => {
|
|
3428
|
-
if (!
|
|
3429
|
-
return;
|
|
3742
|
+
if (!scanning || scanIsPressing)
|
|
3743
|
+
return false;
|
|
3744
|
+
swallowClick = false; // a click this take sends — a menu's «confirm» — is the take's, not a touch's (see the pointer above)
|
|
3430
3745
|
// 📌 THE CHIP IS NOT REDRAWN HERE, and a surviving mutation is why: the frame loop above draws every frame, so a second
|
|
3431
3746
|
// drawing path only saved the sixteen milliseconds until the next one — a line that could disagree with the loop and could
|
|
3432
3747
|
// never be seen doing it.
|
|
3433
|
-
const
|
|
3434
|
-
|
|
3748
|
+
const now = win.performance.now();
|
|
3749
|
+
if (scanning.inMenu) {
|
|
3750
|
+
// THE MENU MOVES BY ITS OWN PATH: the step is the intent a key with that meaning carries, handed to `ui/menu-nav` as seat 0.
|
|
3751
|
+
const step = scanning.pass(now, { press: true }).commanded;
|
|
3752
|
+
if (step)
|
|
3753
|
+
nav.navIntent(0, menuStepKeys(step));
|
|
3754
|
+
return true;
|
|
3755
|
+
}
|
|
3756
|
+
const action = scanning.pass(now, { press: true }).commanded;
|
|
3435
3757
|
if (!action)
|
|
3436
|
-
return;
|
|
3437
|
-
// 📌 THROUGH THE VIRTUAL CONTROLLER, like every other transport (ADR-0111): in
|
|
3438
|
-
//
|
|
3439
|
-
|
|
3758
|
+
return true; // «cancel»: the press was taken, and means nothing
|
|
3759
|
+
// 📌 THROUGH THE VIRTUAL CONTROLLER, like every other transport (ADR-0111): in play it holds the child's key and reaches the
|
|
3760
|
+
// cartridge. The scan decides WHICH position; it does not decide what a position does.
|
|
3761
|
+
scanIsPressing = true;
|
|
3762
|
+
try {
|
|
3763
|
+
virtualController.press(action, source, 0);
|
|
3764
|
+
}
|
|
3765
|
+
finally {
|
|
3766
|
+
scanIsPressing = false;
|
|
3767
|
+
}
|
|
3440
3768
|
win.setTimeout(() => virtualController.release(action, source, 0), SWITCH_SCAN_DEFAULTS.pulseMs);
|
|
3769
|
+
return true;
|
|
3441
3770
|
};
|
|
3442
3771
|
stateOn('switchScan', (on) => { if (on)
|
|
3443
3772
|
startScan();
|
|
@@ -3446,13 +3775,43 @@ unstyled, so a child who plays by keyboard cannot see where focus is — link th
|
|
|
3446
3775
|
whenDisposed(stopScan); // the scan's frames are this root's, and an ended root keeps none running (ADR-0220)
|
|
3447
3776
|
if (state.switchScan)
|
|
3448
3777
|
startScan();
|
|
3778
|
+
/*
|
|
3779
|
+
* 🔴 THE DEVICE IN USE AND ITS LATCH, AT ONE DOOR (ADR-0109 rule 3, ADR-0113, ADR-0249). Every transport that plays presses
|
|
3780
|
+
* this controller — the keyboard conductor below (`teclado`), the on-screen pad (`toque`), the gamepad, the eyes, the face,
|
|
3781
|
+
* the hands, the voice — so its `pressedBy` is where the root hears «the child is using this device now». Each press is that
|
|
3782
|
+
* device's edge: the seat's transport automaton moves to it, and the seat's latch is resolved again for it — the child's
|
|
3783
|
+
* stored choice for that transport, else its default: the MOUNTED game's `holdsKeys()` read now on the four one-command
|
|
3784
|
+
* transports (a platform game keeps walking on «direita», a quiz's «abaixo» presses once), the factory's on the keyboard, the
|
|
3785
|
+
* pad and touch. So a key after a spoken word brings back the keyboard's latch, and the ☝️ writes under the keyboard.
|
|
3786
|
+
* ⚠️ EVERY TRANSPORT, not one hook per transport: a transport this door did not hear would leave the automaton on the device
|
|
3787
|
+
* the child put down, and play would go on under that device's latch — in silence. The seats are the ones the bar's ☝️
|
|
3788
|
+
* writes (`PausePlayer`, the same cast).
|
|
3789
|
+
*/
|
|
3790
|
+
const transportEdge = createLatchedEdge(() => players(), {
|
|
3791
|
+
input, store, holdsKeys: () => cartridge.declaration.holdsKeys(),
|
|
3792
|
+
});
|
|
3449
3793
|
const virtualController = createVirtualController({
|
|
3794
|
+
// told first, for every press that names its transport: the edge and the latch of the device that pressed
|
|
3795
|
+
pressedBy: (source, seat) => { transportEdge(seat, source); },
|
|
3450
3796
|
scheme: (i) => keyboard.kbFor(i), menuOpen: menuWithDpad,
|
|
3451
|
-
// ⚠️ `markKeyFrom` AND NOT RAW `markKey`: a key that arrives WITHOUT a source —
|
|
3452
|
-
//
|
|
3453
|
-
// `input/state`.
|
|
3797
|
+
// ⚠️ `markKeyFrom` AND NOT RAW `markKey`: a key that arrives WITHOUT a source — a synthetic key nobody stamped — must ERASE
|
|
3798
|
+
// whoever held it last instead of inheriting them (ADR-0109). The choice between the two doors lives in `input/state`.
|
|
3454
3799
|
holdKey: markKeyFrom,
|
|
3455
3800
|
releaseKey: releaseKey, menuKey: keyToMenu, deliver: deliverCommand,
|
|
3801
|
+
// With a menu open the game hears no press, so the engine answers the sonar there: R1 reads the menu in front, spoken or,
|
|
3802
|
+
// in deaf mode, captioned and signed — the same two doors the play sonar's words take (ADR-0234). Any seat's R1 reads it,
|
|
3803
|
+
// though only the first steers the menu (ADR-0144), and a reading takes turns with the other seats' (ADR-0234 errata).
|
|
3804
|
+
menuAnswers: (action, seat) => menuSonarPress(action, screen, (text) => { srSay(text); deafMode.sonar(text, seat); }),
|
|
3805
|
+
// START and SELECT from a transport with no key in the world — the eyes, the face, the hands, the voice, the scan — do what
|
|
3806
|
+
// the keys do, for the seat that pressed (ADR-0144 §1, ADR-0155): in play and in the quick pause alike, so START there
|
|
3807
|
+
// always LEAVES, whatever key the child bound first to `start` (ADR-0144 erratum of 2026-09-26).
|
|
3808
|
+
systemPress: (action, seat) => { if (action === 'start')
|
|
3809
|
+
startForSeat(seat);
|
|
3810
|
+
else
|
|
3811
|
+
openSeatMenus(seat); },
|
|
3812
|
+
// 🔴 WITH ONE BUTTON ONLY ON, EVERY PRESS HERE IS THE SWITCH (ADR-0218 §4): the eyes, the face, the hands, the voice, the
|
|
3813
|
+
// on-screen pad and the gamepad press this controller, so asking here is asking once for all of them.
|
|
3814
|
+
takeShown: (source) => !!scanPress?.(source),
|
|
3456
3815
|
});
|
|
3457
3816
|
/*
|
|
3458
3817
|
* 🔴 THE KEYBOARD CONDUCTOR, AND ONLY THAT (ADR-0223). It resolves the action and PRESSES the virtual controller, like the
|
|
@@ -3466,6 +3825,11 @@ unstyled, so a child who plays by keyboard cannot see where focus is — link th
|
|
|
3466
3825
|
*
|
|
3467
3826
|
* 📌 And it does not ask whether a menu is open. That question has ONE answer, the controller's; what makes it true
|
|
3468
3827
|
* for the keyboard is `keyToMenu` above, which does not redispatch a key that is already in the world.
|
|
3828
|
+
*
|
|
3829
|
+
* 🔴 ONE KEY PRESS IS ONE ACTION (ADR-0111 errata of 2026-09-26), from both sides. A key the game HEARD is spent: its browser
|
|
3830
|
+
* default is cancelled, or Space on a focused button was the game's `action2` AND the button's click. A key that went to
|
|
3831
|
+
* something else — a field being typed into, the engine's own control it activates — is not also played: `input/key-default`
|
|
3832
|
+
* says which, and the controller holds it back from play (with a menu open it is the menu's either way).
|
|
3469
3833
|
*/
|
|
3470
3834
|
for (const kind of ['keydown', 'keyup']) {
|
|
3471
3835
|
win.addEventListener(kind, (e) => {
|
|
@@ -3480,12 +3844,23 @@ unstyled, so a child who plays by keyboard cannot see where focus is — link th
|
|
|
3480
3844
|
if (seat < 0)
|
|
3481
3845
|
return;
|
|
3482
3846
|
const action = keyboard.actionOf(e.code, seat);
|
|
3483
|
-
|
|
3847
|
+
// ⚠️ START AND SELECT ARE NOT PRESSED HERE: their key is answered by the engine's own listeners above, which run for a
|
|
3848
|
+
// cartridge with no `onCommand` too — pressed here as well, the controller would open the pause a second time (ADR-0144).
|
|
3849
|
+
if (!action || action === 'start' || action === 'select')
|
|
3484
3850
|
return;
|
|
3485
|
-
if (kind === '
|
|
3486
|
-
virtualController.press(action, source, seat);
|
|
3487
|
-
else
|
|
3851
|
+
if (kind === 'keyup') {
|
|
3488
3852
|
virtualController.release(action, source, seat);
|
|
3853
|
+
return;
|
|
3854
|
+
}
|
|
3855
|
+
// 🔴 A KEY THE MENUS CONSUMED IS NOT ALSO PLAYED (ADR-0111 erratum of 2026-09-26). `nav.attach()` put `menuNavKey` on this
|
|
3856
|
+
// window's capture first, so it has already decided — and a key that CLOSED the last menu (resume, «no» at the card's
|
|
3857
|
+
// root, a panel's «Voltar») leaves the controller no menu to see: 📏 Space on resume resumed AND answered the quiz.
|
|
3858
|
+
// 📌 And the key that ends print mode is print's: its listener runs after this one, so it is asked by its state (see there).
|
|
3859
|
+
if (nav.consumed(e) || printArmed)
|
|
3860
|
+
return;
|
|
3861
|
+
const toPlay = keyGoesToGame(e.code, e.target, ENGINE_CONTROLS_IN_PLAY);
|
|
3862
|
+
if (virtualController.press(action, source, seat, toPlay))
|
|
3863
|
+
e.preventDefault();
|
|
3489
3864
|
}, true);
|
|
3490
3865
|
}
|
|
3491
3866
|
/*
|
|
@@ -3521,17 +3896,20 @@ unstyled, so a child who plays by keyboard cannot see where focus is — link th
|
|
|
3521
3896
|
getGamepads: () => win.navigator?.getGamepads?.() ?? [],
|
|
3522
3897
|
// THE GAME'S WORD for a position: the engine knows the position exists, only the cartridge knows what it is called —
|
|
3523
3898
|
// and it already declared that in the `preset` to exist.
|
|
3524
|
-
actionLabel: (action) =>
|
|
3899
|
+
actionLabel: (action) => labellerFrom(actionWords())(action),
|
|
3525
3900
|
srSay, srAlert,
|
|
3526
3901
|
frontOverlay: overlays.frontOverlay,
|
|
3527
3902
|
// ⚠️ «PAUSE MENU» HERE IS EVERY MENU WITH A DIRECTIONAL, not only the card: the transport's `steerPause` already
|
|
3528
3903
|
// handles the shared dialog before the card, which is the panel open on top. The same question the controller asks.
|
|
3529
3904
|
pauseMenu: menuWithDpad,
|
|
3530
3905
|
worldRunning: gameHooks.worldRunning,
|
|
3531
|
-
// The
|
|
3532
|
-
//
|
|
3906
|
+
// The pad's START in play is PRESSED for its seat and answered by `systemPress` (ADR-0144 §1); this `pause` is the mapping
|
|
3907
|
+
// wizard's, which stops the game while a pad is mapped. The way out on the card and on seat 0's quick pause reuses the
|
|
3908
|
+
// decision already written for the finger, which knows leaving the quick pause from closing the card — for the screen's
|
|
3909
|
+
// first seat only: another seat's START there does nothing (ADR-0144, erratum of 2026-09-26).
|
|
3533
3910
|
pause: () => { enterQuickPause(0); },
|
|
3534
|
-
resume:
|
|
3911
|
+
resume: (seat) => { if (leadsTheScreen(seat))
|
|
3912
|
+
togglePauseByTouch(); },
|
|
3535
3913
|
isAttractActive: gameHooks.attractActive,
|
|
3536
3914
|
stopAttract: gameHooks.stopAttract,
|
|
3537
3915
|
// A PHYSICAL button makes the on-screen pad vanish — the same per-modality switch as the keyboard.
|
|
@@ -3548,12 +3926,16 @@ unstyled, so a child who plays by keyboard cannot see where focus is — link th
|
|
|
3548
3926
|
navBar,
|
|
3549
3927
|
sharedDialogOpen: nav.sharedDialogOpen,
|
|
3550
3928
|
navDialog: nav.navDialog,
|
|
3929
|
+
leadsTheScreen, // another seat's pad steers none of this screen's menus, as its keys do not
|
|
3551
3930
|
getPauseMenu: (i) => $(`#vp-pause-${i}`),
|
|
3552
3931
|
navPause: nav.navPause,
|
|
3553
3932
|
setPauseActor,
|
|
3933
|
+
// the RAW edge: every pad press already reached the controller's `pressedBy`, which recorded it and resolved the pad's latch
|
|
3554
3934
|
playerEdge,
|
|
3555
3935
|
press: (action, source, player) => virtualController.press(action, source, player),
|
|
3556
3936
|
release: (action, source, player) => virtualController.release(action, source, player),
|
|
3937
|
+
// the pad also steers menus and the pause without pressing a position, so it asks the scan's question itself (ADR-0218 §4)
|
|
3938
|
+
takeShown: () => !!scanPress?.('gamepad'),
|
|
3557
3939
|
modalInput: gameHooks.modalInput,
|
|
3558
3940
|
hasModal: gameHooks.hasModal,
|
|
3559
3941
|
joinPlayer: gameHooks.joinPlayer,
|
|
@@ -3561,6 +3943,9 @@ unstyled, so a child who plays by keyboard cannot see where focus is — link th
|
|
|
3561
3943
|
clearWaitingBadge: gameHooks.clearWaitingBadge,
|
|
3562
3944
|
wizardStep: gameHooks.wizardStep,
|
|
3563
3945
|
wizardTick: gameHooks.wizardTick,
|
|
3946
|
+
// the root's answer, not a game's: its own wizard closed however it ended, and the focus goes back to whoever held it
|
|
3947
|
+
// when that wizard opened — the same return the panel's wizard makes (ADR-0248)
|
|
3948
|
+
wizardClosed: () => { overlays.restoreFocus?.('padwiz'); },
|
|
3564
3949
|
});
|
|
3565
3950
|
/*
|
|
3566
3951
|
* AND THE ENGINE POLLS, because whoever mounts polls. ⚠️ The game loop is the CARTRIDGE's (`core/loop.startLoop` is
|
|
@@ -3569,7 +3954,10 @@ unstyled, so a child who plays by keyboard cannot see where focus is — link th
|
|
|
3569
3954
|
*/
|
|
3570
3955
|
let padFrameHandle = 0;
|
|
3571
3956
|
const anyPadConnected = () => (win.navigator?.getGamepads?.() ?? []).some(Boolean);
|
|
3572
|
-
|
|
3957
|
+
// 📌 While the motor panel's wizard maps, the pads talk only to it (the transport asks only about its own wizard): read
|
|
3958
|
+
// under it, an unmapped DirectInput pad opened a second wizard on the same pad, and a standard pad steered the menus.
|
|
3959
|
+
const pollPad = () => { if (!padWizard?.state())
|
|
3960
|
+
gamepad.pollPads(); padFrameHandle = win.requestAnimationFrame(pollPad); };
|
|
3573
3961
|
const stopPollingPad = () => { if (padFrameHandle)
|
|
3574
3962
|
win.cancelAnimationFrame(padFrameHandle); padFrameHandle = 0; };
|
|
3575
3963
|
/*
|
|
@@ -3616,37 +4004,37 @@ unstyled, so a child who plays by keyboard cannot see where focus is — link th
|
|
|
3616
4004
|
* PLAYING BY SPEAKING (ADR-0189, ADR-0193, ADR-0194; issue #184): the stored 👄 drives `ui/voice-control` — the recogniser
|
|
3617
4005
|
* from the delivery, the microphone that stays open, and presses stamped `fala` on the virtual controller.
|
|
3618
4006
|
*
|
|
3619
|
-
* 📌 THE GRAMMAR FOLLOWS THE OPEN MENU: the names the child can see are the names she can say
|
|
3620
|
-
*
|
|
4007
|
+
* 📌 THE GRAMMAR FOLLOWS THE OPEN MENU: the names the child can see are the names she can say, and a name heard activates
|
|
4008
|
+
* its item (ADR-0194 §2). Both are asked of the MENU NAVIGATION for seat 0 — the seat the voice presses — so the names are
|
|
4009
|
+
* those of the menu a confirm would reach (the panel on top, else the pause card), and saying one puts that navigation's
|
|
4010
|
+
* cursor on it before the voice confirms through the virtual controller. The grammar is re-read when a card, a list of it or
|
|
4011
|
+
* a panel shows or hides — the region's observer above.
|
|
3621
4012
|
*/
|
|
3622
4013
|
if (canCaptureMedia) {
|
|
3623
|
-
const menuWords = () => {
|
|
3624
|
-
const card = overlays.topVisibleOverlay();
|
|
3625
|
-
if (!card)
|
|
3626
|
-
return [];
|
|
3627
|
-
return [...card.querySelectorAll('button, [data-passos]')]
|
|
3628
|
-
.filter((el) => !el.hidden && el.getAttribute('aria-disabled') !== 'true')
|
|
3629
|
-
.map((el) => accessibleLabel(el))
|
|
3630
|
-
.filter((s) => s.length > 1);
|
|
3631
|
-
};
|
|
3632
4014
|
voiceControl = createVoiceControl({
|
|
3633
|
-
t: translator.t, base: doc.baseURI, language: () => bcp47(), controller: virtualController,
|
|
4015
|
+
t: translator.t, base: doc.baseURI, language: () => bcp47(), controller: virtualController,
|
|
4016
|
+
menuWords: () => nav.itemNames(0), pointAt: (name) => nav.pointAt(name, 0),
|
|
4017
|
+
// with one button only on, a word heard is the switch and nothing else: a name is not pointed at (ADR-0218 §4)
|
|
4018
|
+
oneButtonOnly: () => state.switchScan,
|
|
3634
4019
|
say: srSay, alert: srAlert,
|
|
3635
4020
|
report: (line) => { if (!measuredProblems.includes(line))
|
|
3636
4021
|
measuredProblems.push(line); },
|
|
3637
4022
|
turnOff: () => { state.setVoiceControlValue(false); },
|
|
3638
4023
|
after: (fn, ms) => { win.setTimeout(fn, ms); },
|
|
3639
4024
|
// ⚠️ THE ADDRESS IS ABSOLUTE AND THE BUNDLER MUST NOT FOLLOW IT: the recogniser arrives with the delivery at runtime.
|
|
3640
|
-
hasFile: hasHeavyFile, loadBundle:
|
|
4025
|
+
hasFile: hasHeavyFile, loadBundle: voskBundle, loadRuntime: voskModels.load,
|
|
4026
|
+
// the model's vocabulary is read from its archive, so a menu name the model cannot hear is a line of `problems` (ADR-0194 §4)
|
|
4027
|
+
fetch: (url) => win.fetch(url),
|
|
3641
4028
|
getUserMedia, createContext: () => new HostAudioContext(),
|
|
3642
4029
|
});
|
|
3643
4030
|
stateOn('voiceControl', (on) => { void voiceControl?.apply(on); });
|
|
3644
|
-
//
|
|
3645
|
-
//
|
|
3646
|
-
|
|
3647
|
-
void
|
|
3648
|
-
|
|
3649
|
-
|
|
4031
|
+
// 📌 A STORED 👄 STARTS ONCE THE STORED LANGUAGE HAS APPLIED: started before, it opened the boot language's model and then the
|
|
4032
|
+
// child's — two workers and ~40 MiB unpacked for nothing on every English or Spanish boot (measured on 2026-09-27)
|
|
4033
|
+
let voiceEnded = false;
|
|
4034
|
+
void translator.ready().then(() => { if (!voiceEnded)
|
|
4035
|
+
void voiceControl?.apply(state.voiceControl); }, () => { });
|
|
4036
|
+
// and so does the microphone; `apply(false)` stops without writing the stored answer, which belongs to the child (ADR-0220)
|
|
4037
|
+
whenDisposed(() => { voiceEnded = true; void voiceControl?.apply(false); });
|
|
3650
4038
|
}
|
|
3651
4039
|
/*
|
|
3652
4040
|
/*
|
|
@@ -3667,8 +4055,13 @@ unstyled, so a child who plays by keyboard cannot see where focus is — link th
|
|
|
3667
4055
|
closeReadingThread();
|
|
3668
4056
|
followCartridgeMappings(null);
|
|
3669
4057
|
removeReachNotice();
|
|
4058
|
+
// the released cartridge's explanation goes with it: the next one has not explained anything yet (ADR-0244)
|
|
4059
|
+
explainForGame(null);
|
|
3670
4060
|
hudMounted?.remove();
|
|
3671
4061
|
hudMounted = null;
|
|
4062
|
+
// the map was the released cartridge's; the next one mounts its own (ADR-0239)
|
|
4063
|
+
while (hudRow?.map.firstChild)
|
|
4064
|
+
hudRow.map.removeChild(hudRow.map.firstChild);
|
|
3672
4065
|
reserveBarBand();
|
|
3673
4066
|
// ⚠️ `pop()` AND NOT A `clear()`: each `exit()` is that scene's DOM cleanup, and skipping it would leave on the page
|
|
3674
4067
|
// what the previous cartridge drew. The loop ends because `pop()` returns `null` on an empty stack.
|
|
@@ -3693,15 +4086,19 @@ unstyled, so a child who plays by keyboard cannot see where focus is — link th
|
|
|
3693
4086
|
tts,
|
|
3694
4087
|
reading,
|
|
3695
4088
|
captionSound: writeSoundCaption,
|
|
4089
|
+
explain: explainForGame,
|
|
3696
4090
|
gameSpeed: () => state.gameSpeed,
|
|
3697
4091
|
menuIndexOn: () => state.menuIndexOn,
|
|
3698
4092
|
t: translator.t,
|
|
3699
4093
|
localeReady: translator.ready,
|
|
4094
|
+
locale: translator.locale,
|
|
4095
|
+
setLocale: translator.setLocale,
|
|
3700
4096
|
say: srSay,
|
|
3701
4097
|
alert: srAlert,
|
|
3702
4098
|
mirrorAnnouncements: announcer.mirrorTo,
|
|
3703
|
-
|
|
4099
|
+
deafMode: { isOn: deafMode.isOn, toggle: deafMode.toggle, captionsOn: deafMode.captionsOn },
|
|
3704
4100
|
settings: state,
|
|
4101
|
+
mapSlot: hudRow?.map ?? null,
|
|
3705
4102
|
input,
|
|
3706
4103
|
keyboardConfig,
|
|
3707
4104
|
audio: mixer,
|
|
@@ -3717,7 +4114,11 @@ unstyled, so a child who plays by keyboard cannot see where focus is — link th
|
|
|
3717
4114
|
scenes: rootScenes,
|
|
3718
4115
|
onLocaleChange: (fn) => { localeListeners.push(fn); },
|
|
3719
4116
|
cvdFilters,
|
|
3720
|
-
get problems() {
|
|
4117
|
+
get problems() {
|
|
4118
|
+
return [...hostProblems, ...stylesheetMissing(), ...measureCartridgeProblems(), ...translator.dictionaryGaps(),
|
|
4119
|
+
...missingDeclaredKeys(cartridge, translator.declares), ...measuredProblems, ...storageOutsideScope(),
|
|
4120
|
+
...unreadableWorldProblems(screen)];
|
|
4121
|
+
},
|
|
3721
4122
|
onFailure: announceFailure,
|
|
3722
4123
|
get reach() { return currentReach; },
|
|
3723
4124
|
};
|