@the-inclusionist/engine 9.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 +120 -78
- package/app/css/style.css +761 -377
- package/app/public/vendor/fonts/playwrite-cu.woff2 +0 -0
- package/app/public/vendor/fonts/playwrite-es-deco.woff2 +0 -0
- package/app/public/vendor/fonts/playwrite-es.woff2 +0 -0
- package/app/public/vendor/fonts/playwrite-gb-j.woff2 +0 -0
- package/app/public/vendor/fonts/playwrite-gb-s.woff2 +0 -0
- package/app/public/vendor/fonts/playwrite-pe.woff2 +0 -0
- package/app/public/vendor/fonts/playwrite-pt.woff2 +0 -0
- 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 +36 -84
- package/dist-pkg/boot/create-game.d.ts +499 -219
- package/dist-pkg/boot/create-game.js +3944 -562
- package/dist-pkg/core/a11y-sr.d.ts +25 -3
- package/dist-pkg/core/a11y-sr.js +38 -19
- package/dist-pkg/core/accessible-label.d.ts +17 -0
- package/dist-pkg/core/accessible-label.js +37 -0
- package/dist-pkg/core/accommodation-subjects.d.ts +26 -0
- package/dist-pkg/core/accommodation-subjects.js +32 -0
- package/dist-pkg/core/accommodations.d.ts +125 -0
- package/dist-pkg/core/accommodations.js +195 -0
- package/dist-pkg/core/actions.d.ts +108 -55
- package/dist-pkg/core/actions.js +122 -71
- package/dist-pkg/core/calm-mode.d.ts +31 -0
- package/dist-pkg/core/calm-mode.js +47 -0
- package/dist-pkg/core/camera-cycle.d.ts +15 -0
- package/dist-pkg/core/camera-cycle.js +7 -0
- package/dist-pkg/core/caption-duration.d.ts +15 -0
- package/dist-pkg/core/caption-duration.js +19 -0
- package/dist-pkg/core/cartridge-problems.d.ts +34 -0
- package/dist-pkg/core/cartridge-problems.js +53 -0
- package/dist-pkg/core/constants.d.ts +0 -54
- package/dist-pkg/core/constants.js +9 -76
- package/dist-pkg/core/contract.d.ts +142 -164
- package/dist-pkg/core/contract.js +222 -208
- package/dist-pkg/core/dom-query.d.ts +4 -4
- package/dist-pkg/core/dom-query.js +12 -20
- package/dist-pkg/core/entity.d.ts +62 -77
- package/dist-pkg/core/entity.js +18 -32
- package/dist-pkg/core/escape-html.d.ts +9 -10
- package/dist-pkg/core/escape-html.js +16 -20
- package/dist-pkg/core/flash-threshold.d.ts +17 -0
- package/dist-pkg/core/flash-threshold.js +113 -0
- package/dist-pkg/core/game-speed.d.ts +11 -0
- package/dist-pkg/core/game-speed.js +16 -0
- package/dist-pkg/core/genres.d.ts +20 -0
- package/dist-pkg/core/genres.js +44 -0
- package/dist-pkg/core/i18n.d.ts +71 -60
- package/dist-pkg/core/i18n.js +186 -200
- package/dist-pkg/core/loop.d.ts +17 -6
- package/dist-pkg/core/loop.js +26 -26
- package/dist-pkg/core/pause-icon-catalogue.d.ts +15 -0
- package/dist-pkg/core/pause-icon-catalogue.js +54 -0
- package/dist-pkg/core/ring.d.ts +17 -0
- package/dist-pkg/core/ring.js +29 -0
- package/dist-pkg/core/rng.d.ts +7 -12
- package/dist-pkg/core/rng.js +23 -53
- package/dist-pkg/core/route.d.ts +26 -26
- package/dist-pkg/core/route.js +126 -121
- package/dist-pkg/core/scenes.d.ts +37 -40
- package/dist-pkg/core/scenes.js +41 -43
- package/dist-pkg/core/screens.d.ts +4 -4
- package/dist-pkg/core/screens.js +11 -16
- package/dist-pkg/core/session-clock.d.ts +23 -0
- package/dist-pkg/core/session-clock.js +37 -0
- package/dist-pkg/core/setting-defaults.d.ts +61 -0
- package/dist-pkg/core/setting-defaults.js +66 -0
- package/dist-pkg/core/speech-rate.d.ts +29 -0
- package/dist-pkg/core/speech-rate.js +47 -0
- package/dist-pkg/core/state.d.ts +118 -118
- package/dist-pkg/core/state.js +322 -297
- package/dist-pkg/core/visual-cycles.d.ts +30 -0
- package/dist-pkg/core/visual-cycles.js +52 -0
- package/dist-pkg/core/visual-state.d.ts +13 -0
- package/dist-pkg/core/visual-state.js +6 -0
- package/dist-pkg/educational/activities-registry.d.ts +1 -1
- package/dist-pkg/educational/activities-registry.js +9 -8
- package/dist-pkg/educational/adaptive-engine.d.ts +11 -11
- package/dist-pkg/educational/adaptive-engine.js +17 -17
- package/dist-pkg/educational/segment-bar.d.ts +12 -12
- package/dist-pkg/educational/segment-bar.js +9 -9
- package/dist-pkg/i18n/en.js +324 -223
- package/dist-pkg/i18n/es.js +323 -223
- package/dist-pkg/i18n/pt.js +348 -226
- package/dist-pkg/input/default-bindings.d.ts +37 -38
- package/dist-pkg/input/default-bindings.js +127 -144
- package/dist-pkg/input/devices.d.ts +7 -7
- package/dist-pkg/input/devices.js +17 -16
- package/dist-pkg/input/edges.d.ts +23 -32
- package/dist-pkg/input/edges.js +23 -28
- package/dist-pkg/input/empathy-filter.d.ts +22 -0
- package/dist-pkg/input/empathy-filter.js +34 -0
- package/dist-pkg/input/face-map.d.ts +96 -0
- package/dist-pkg/input/face-map.js +255 -0
- package/dist-pkg/input/face-signals.d.ts +40 -0
- package/dist-pkg/input/face-signals.js +48 -0
- package/dist-pkg/input/gamepad.d.ts +195 -145
- package/dist-pkg/input/gamepad.js +383 -556
- package/dist-pkg/input/gaze-cycle.d.ts +64 -0
- package/dist-pkg/input/gaze-cycle.js +133 -0
- package/dist-pkg/input/gaze-relative.d.ts +68 -0
- package/dist-pkg/input/gaze-relative.js +142 -0
- package/dist-pkg/input/hand-map.d.ts +28 -0
- package/dist-pkg/input/hand-map.js +142 -0
- package/dist-pkg/input/input-cooldown.d.ts +17 -0
- package/dist-pkg/input/input-cooldown.js +39 -0
- 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 +15 -21
- package/dist-pkg/input/keyboard-runtime.js +23 -27
- package/dist-pkg/input/keyboard.d.ts +68 -63
- package/dist-pkg/input/keyboard.js +83 -105
- package/dist-pkg/input/keydown.d.ts +123 -137
- package/dist-pkg/input/keydown.js +284 -289
- package/dist-pkg/input/latch-edge.d.ts +33 -21
- package/dist-pkg/input/latch-edge.js +34 -35
- package/dist-pkg/input/latch-scope.d.ts +51 -50
- package/dist-pkg/input/latch-scope.js +61 -82
- package/dist-pkg/input/latch-store.d.ts +26 -30
- package/dist-pkg/input/latch-store.js +42 -50
- package/dist-pkg/input/latch-sync.d.ts +29 -29
- package/dist-pkg/input/latch-sync.js +25 -25
- package/dist-pkg/input/latch.d.ts +14 -17
- package/dist-pkg/input/latch.js +29 -37
- package/dist-pkg/input/pad-defaults.d.ts +13 -16
- package/dist-pkg/input/pad-defaults.js +21 -28
- package/dist-pkg/input/pad-reading.d.ts +76 -0
- package/dist-pkg/input/pad-reading.js +169 -0
- package/dist-pkg/input/pad-wizard.d.ts +99 -0
- package/dist-pkg/input/pad-wizard.js +256 -0
- package/dist-pkg/input/pointer-space.d.ts +19 -20
- package/dist-pkg/input/pointer-space.js +27 -32
- package/dist-pkg/input/pointer.d.ts +39 -40
- package/dist-pkg/input/pointer.js +42 -45
- package/dist-pkg/input/state.d.ts +101 -89
- package/dist-pkg/input/state.js +54 -134
- package/dist-pkg/input/switch-scan.d.ts +59 -0
- package/dist-pkg/input/switch-scan.js +67 -0
- package/dist-pkg/input/synthetic-source.d.ts +42 -0
- package/dist-pkg/input/synthetic-source.js +57 -0
- package/dist-pkg/input/touch-bindings.d.ts +127 -116
- package/dist-pkg/input/touch-bindings.js +158 -173
- package/dist-pkg/input/touch.d.ts +147 -72
- package/dist-pkg/input/touch.js +362 -126
- package/dist-pkg/input/transport-in-use.d.ts +107 -0
- package/dist-pkg/input/transport-in-use.js +130 -0
- package/dist-pkg/input/transports.d.ts +90 -117
- package/dist-pkg/input/transports.js +104 -106
- package/dist-pkg/input/virtual-controller.d.ts +82 -0
- package/dist-pkg/input/virtual-controller.js +69 -0
- package/dist-pkg/input/vocabulary-migration.d.ts +50 -49
- package/dist-pkg/input/vocabulary-migration.js +80 -79
- package/dist-pkg/input/voice-map.d.ts +59 -0
- package/dist-pkg/input/voice-map.js +123 -0
- package/dist-pkg/platform/audio-ambient.d.ts +13 -2
- package/dist-pkg/platform/audio-ambient.js +14 -14
- package/dist-pkg/platform/audio-earcons.d.ts +20 -19
- package/dist-pkg/platform/audio-earcons.js +51 -42
- package/dist-pkg/platform/audio-jingles.js +9 -10
- package/dist-pkg/platform/audio-mixer.d.ts +14 -2
- package/dist-pkg/platform/audio-mixer.js +31 -39
- package/dist-pkg/platform/audio-sonar.d.ts +49 -152
- package/dist-pkg/platform/audio-sonar.js +124 -302
- package/dist-pkg/platform/audio.d.ts +62 -19
- package/dist-pkg/platform/audio.js +193 -168
- 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/flash-sampler.d.ts +19 -0
- package/dist-pkg/platform/flash-sampler.js +79 -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 +77 -0
- package/dist-pkg/platform/heavy-catalogue.js +256 -0
- package/dist-pkg/platform/heavy-mirror.d.ts +20 -0
- package/dist-pkg/platform/heavy-mirror.js +73 -0
- package/dist-pkg/platform/heavy.d.ts +136 -0
- package/dist-pkg/platform/heavy.js +324 -0
- package/dist-pkg/platform/interruptible-speech.d.ts +23 -16
- package/dist-pkg/platform/interruptible-speech.js +77 -54
- package/dist-pkg/platform/kokoro-port.d.ts +27 -0
- package/dist-pkg/platform/kokoro-port.js +50 -0
- package/dist-pkg/platform/kokoro-runtime.d.ts +20 -0
- package/dist-pkg/platform/kokoro-runtime.js +52 -0
- package/dist-pkg/platform/kokoro.d.ts +70 -0
- package/dist-pkg/platform/kokoro.js +133 -0
- package/dist-pkg/platform/listener-scope.d.ts +12 -0
- package/dist-pkg/platform/listener-scope.js +90 -0
- package/dist-pkg/platform/locale-host.d.ts +14 -0
- package/dist-pkg/platform/locale-host.js +45 -0
- package/dist-pkg/platform/microphone.d.ts +51 -0
- package/dist-pkg/platform/microphone.js +118 -0
- package/dist-pkg/platform/onnx-runtime.d.ts +34 -0
- package/dist-pkg/platform/onnx-runtime.js +42 -0
- package/dist-pkg/platform/reading-in-worker.d.ts +64 -0
- package/dist-pkg/platform/reading-in-worker.js +125 -0
- package/dist-pkg/platform/reading-model.d.ts +94 -0
- package/dist-pkg/platform/reading-model.js +272 -0
- package/dist-pkg/platform/reading-runtime.d.ts +24 -0
- package/dist-pkg/platform/reading-runtime.js +164 -0
- package/dist-pkg/platform/reading-worker.d.ts +41 -0
- package/dist-pkg/platform/reading-worker.js +58 -0
- package/dist-pkg/platform/reading.d.ts +80 -0
- package/dist-pkg/platform/reading.js +136 -0
- package/dist-pkg/platform/speech-recognition.d.ts +60 -0
- package/dist-pkg/platform/speech-recognition.js +104 -0
- package/dist-pkg/platform/speech.d.ts +23 -1
- package/dist-pkg/platform/speech.js +29 -18
- package/dist-pkg/platform/storage-keys.d.ts +96 -0
- package/dist-pkg/platform/storage-keys.js +106 -0
- package/dist-pkg/platform/storage.d.ts +46 -111
- package/dist-pkg/platform/storage.js +104 -169
- package/dist-pkg/platform/tts.d.ts +85 -39
- package/dist-pkg/platform/tts.js +447 -127
- package/dist-pkg/platform/vision-loop.d.ts +23 -0
- package/dist-pkg/platform/vision-loop.js +56 -0
- package/dist-pkg/platform/vision.d.ts +132 -0
- package/dist-pkg/platform/vision.js +117 -0
- package/dist-pkg/platform/voice-listener.d.ts +40 -0
- package/dist-pkg/platform/voice-listener.js +88 -0
- package/dist-pkg/platform/voice-plan.d.ts +15 -98
- package/dist-pkg/platform/voice-plan.js +56 -143
- package/dist-pkg/platform/vosk-runtime.d.ts +96 -0
- package/dist-pkg/platform/vosk-runtime.js +151 -0
- package/dist-pkg/platform/vosk-vocabulary.d.ts +6 -0
- package/dist-pkg/platform/vosk-vocabulary.js +178 -0
- package/dist-pkg/render/canvas.d.ts +13 -7
- package/dist-pkg/render/canvas.js +13 -13
- package/dist-pkg/render/crt.d.ts +48 -12
- package/dist-pkg/render/crt.js +78 -80
- package/dist-pkg/render/cvd-matrices.d.ts +22 -18
- package/dist-pkg/render/cvd-matrices.js +37 -38
- package/dist-pkg/render/hc-role-data.d.ts +7 -7
- package/dist-pkg/render/hc-role-data.js +14 -14
- package/dist-pkg/render/high-contrast.d.ts +67 -56
- package/dist-pkg/render/high-contrast.js +226 -217
- package/dist-pkg/render/low-vision-drawing.d.ts +5 -0
- package/dist-pkg/render/low-vision-drawing.js +46 -0
- package/dist-pkg/render/lq-filter.d.ts +32 -20
- package/dist-pkg/render/lq-filter.js +59 -65
- package/dist-pkg/render/port.d.ts +86 -88
- package/dist-pkg/render/port.js +17 -23
- package/dist-pkg/render/screen-pipeline.d.ts +42 -42
- package/dist-pkg/render/screen-pipeline.js +80 -87
- package/dist-pkg/render/sprite-fx.d.ts +2 -1
- package/dist-pkg/render/sprite-fx.js +16 -14
- package/dist-pkg/render/viewports.d.ts +20 -15
- package/dist-pkg/render/viewports.js +83 -114
- package/dist-pkg/render/viz-axes-labels.d.ts +47 -0
- package/dist-pkg/render/viz-axes-labels.js +88 -0
- package/dist-pkg/render/viz-axes.d.ts +89 -86
- package/dist-pkg/render/viz-axes.js +128 -116
- package/dist-pkg/render/viz-modes.d.ts +28 -29
- package/dist-pkg/render/viz-modes.js +40 -45
- package/dist-pkg/render/viz-refusal.d.ts +32 -0
- package/dist-pkg/render/viz-refusal.js +57 -0
- package/dist-pkg/render/viz-setters.d.ts +104 -66
- package/dist-pkg/render/viz-setters.js +182 -191
- package/dist-pkg/ui/aac-sets.d.ts +45 -0
- package/dist-pkg/ui/aac-sets.js +41 -0
- package/dist-pkg/ui/audio-choices.d.ts +68 -0
- package/dist-pkg/ui/audio-choices.js +98 -0
- package/dist-pkg/ui/audio-rows-that-apply.d.ts +12 -0
- package/dist-pkg/ui/audio-rows-that-apply.js +43 -0
- package/dist-pkg/ui/camera-control.d.ts +53 -0
- package/dist-pkg/ui/camera-control.js +48 -0
- package/dist-pkg/ui/changed-mark.d.ts +9 -8
- package/dist-pkg/ui/changed-mark.js +22 -53
- package/dist-pkg/ui/control-choices.d.ts +37 -0
- package/dist-pkg/ui/control-choices.js +84 -0
- package/dist-pkg/ui/debug-panel.d.ts +59 -43
- package/dist-pkg/ui/debug-panel.js +68 -69
- package/dist-pkg/ui/declared-words.d.ts +21 -0
- package/dist-pkg/ui/declared-words.js +66 -0
- package/dist-pkg/ui/dom.d.ts +10 -14
- package/dist-pkg/ui/dom.js +10 -47
- package/dist-pkg/ui/drawing-problems.d.ts +25 -0
- package/dist-pkg/ui/drawing-problems.js +94 -0
- package/dist-pkg/ui/eye-control.d.ts +36 -0
- package/dist-pkg/ui/eye-control.js +178 -0
- package/dist-pkg/ui/face-control.d.ts +25 -0
- package/dist-pkg/ui/face-control.js +183 -0
- package/dist-pkg/ui/focus-trap.d.ts +39 -49
- package/dist-pkg/ui/focus-trap.js +56 -65
- package/dist-pkg/ui/fonts.d.ts +157 -68
- package/dist-pkg/ui/fonts.js +305 -115
- 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 +47 -0
- package/dist-pkg/ui/game-options.js +158 -0
- package/dist-pkg/ui/gaze-overlay.d.ts +64 -0
- package/dist-pkg/ui/gaze-overlay.js +165 -0
- package/dist-pkg/ui/hand-control.d.ts +25 -0
- package/dist-pkg/ui/hand-control.js +143 -0
- package/dist-pkg/ui/help-panel.d.ts +87 -0
- package/dist-pkg/ui/help-panel.js +242 -0
- package/dist-pkg/ui/hud-bands.d.ts +62 -0
- package/dist-pkg/ui/hud-bands.js +146 -0
- 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 +73 -73
- package/dist-pkg/ui/hud.js +94 -98
- package/dist-pkg/ui/item-announcement.d.ts +13 -12
- package/dist-pkg/ui/item-announcement.js +25 -26
- package/dist-pkg/ui/layout.d.ts +103 -28
- package/dist-pkg/ui/layout.js +117 -99
- 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/locale-flags.d.ts +16 -0
- package/dist-pkg/ui/locale-flags.js +30 -0
- package/dist-pkg/ui/loop-crash.d.ts +17 -17
- package/dist-pkg/ui/loop-crash.js +33 -62
- package/dist-pkg/ui/menu-intent.d.ts +61 -0
- package/dist-pkg/ui/menu-intent.js +88 -0
- package/dist-pkg/ui/menu-items.d.ts +20 -0
- package/dist-pkg/ui/menu-items.js +51 -0
- package/dist-pkg/ui/menu-nav.d.ts +98 -92
- package/dist-pkg/ui/menu-nav.js +398 -286
- package/dist-pkg/ui/mobility-choices.d.ts +26 -0
- package/dist-pkg/ui/mobility-choices.js +41 -0
- package/dist-pkg/ui/motion-choices.d.ts +31 -0
- package/dist-pkg/ui/motion-choices.js +68 -0
- package/dist-pkg/ui/motion-scene.d.ts +24 -19
- package/dist-pkg/ui/motion-scene.js +29 -61
- package/dist-pkg/ui/mount-panel.d.ts +81 -0
- package/dist-pkg/ui/mount-panel.js +61 -0
- package/dist-pkg/ui/panel-shell.d.ts +65 -31
- package/dist-pkg/ui/panel-shell.js +92 -66
- package/dist-pkg/ui/panel-widgets.d.ts +150 -0
- package/dist-pkg/ui/panel-widgets.js +294 -0
- package/dist-pkg/ui/pause-buttons.d.ts +35 -0
- package/dist-pkg/ui/pause-buttons.js +66 -0
- package/dist-pkg/ui/pause-icons.d.ts +261 -347
- package/dist-pkg/ui/pause-icons.js +671 -601
- package/dist-pkg/ui/pause-markup.d.ts +86 -0
- package/dist-pkg/ui/pause-markup.js +125 -0
- package/dist-pkg/ui/reach-notice.d.ts +22 -23
- package/dist-pkg/ui/reach-notice.js +55 -58
- package/dist-pkg/ui/scan-overlay.d.ts +23 -0
- package/dist-pkg/ui/scan-overlay.js +50 -0
- 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 +70 -0
- package/dist-pkg/ui/settings-aac.js +208 -0
- package/dist-pkg/ui/settings-audio.d.ts +97 -114
- package/dist-pkg/ui/settings-audio.js +493 -425
- package/dist-pkg/ui/settings-controls.d.ts +53 -96
- package/dist-pkg/ui/settings-controls.js +143 -215
- package/dist-pkg/ui/settings-empathy.d.ts +39 -30
- package/dist-pkg/ui/settings-empathy.js +53 -60
- package/dist-pkg/ui/settings-mobility.d.ts +192 -0
- package/dist-pkg/ui/settings-mobility.js +332 -0
- package/dist-pkg/ui/settings-motion.d.ts +91 -70
- package/dist-pkg/ui/settings-motion.js +287 -198
- package/dist-pkg/ui/settings-panel.d.ts +58 -45
- package/dist-pkg/ui/settings-panel.js +189 -94
- package/dist-pkg/ui/settings-typo.d.ts +29 -78
- package/dist-pkg/ui/settings-typo.js +158 -129
- package/dist-pkg/ui/settings-visual.d.ts +29 -87
- package/dist-pkg/ui/settings-visual.js +240 -193
- package/dist-pkg/ui/shell.d.ts +112 -135
- package/dist-pkg/ui/shell.js +148 -217
- package/dist-pkg/ui/simulation-list.d.ts +27 -0
- package/dist-pkg/ui/simulation-list.js +76 -0
- package/dist-pkg/ui/simulation-over-the-world.d.ts +23 -0
- package/dist-pkg/ui/simulation-over-the-world.js +131 -0
- package/dist-pkg/ui/simulation-refusal.d.ts +5 -31
- package/dist-pkg/ui/simulation-refusal.js +8 -56
- package/dist-pkg/ui/switchable-control.d.ts +11 -0
- package/dist-pkg/ui/switchable-control.js +19 -0
- package/dist-pkg/ui/title.d.ts +3 -12
- package/dist-pkg/ui/title.js +49 -18
- package/dist-pkg/ui/top-band.d.ts +26 -0
- package/dist-pkg/ui/top-band.js +96 -0
- package/dist-pkg/ui/typo-choices.d.ts +71 -0
- package/dist-pkg/ui/typo-choices.js +85 -0
- package/dist-pkg/ui/visual-axes-panel.d.ts +11 -47
- package/dist-pkg/ui/visual-axes-panel.js +12 -94
- package/dist-pkg/ui/visual-choices.d.ts +77 -0
- package/dist-pkg/ui/visual-choices.js +107 -0
- package/dist-pkg/ui/vlibras.d.ts +77 -10
- package/dist-pkg/ui/vlibras.js +130 -81
- package/dist-pkg/ui/voice-control.d.ts +68 -0
- package/dist-pkg/ui/voice-control.js +276 -0
- package/dist-pkg/ui/voice-settings.d.ts +95 -0
- package/dist-pkg/ui/voice-settings.js +352 -0
- package/dist-pkg/ui/where-the-child-is.d.ts +31 -0
- package/dist-pkg/ui/where-the-child-is.js +70 -0
- package/docs/CREDITS.md +300 -44
- package/docs/LICENSES.md +189 -145
- package/package.json +51 -27
- 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 +388 -0
- 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/Apache-2.0.txt +176 -0
- package/scripts/licences/GPL-3.0.txt +674 -0
- package/scripts/licences/MIT.txt +17 -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 +301 -0
- package/scripts/licences/vosk-browser.NOTICE.txt +268 -0
- package/app/public/vendor/fonts/comicneue-400.woff2 +0 -0
- package/app/public/vendor/fonts/comicneue-700.woff2 +0 -0
- package/app/public/vendor/fonts/fondamento-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/fondamento-400.woff2 +0 -0
- package/app/public/vendor/fonts/inter-400.woff2 +0 -0
- package/app/public/vendor/fonts/inter-700.woff2 +0 -0
- package/app/public/vendor/fonts/lato-400.woff2 +0 -0
- package/app/public/vendor/fonts/lato-700.woff2 +0 -0
- package/app/public/vendor/fonts/literata-400.woff2 +0 -0
- package/app/public/vendor/fonts/literata-700.woff2 +0 -0
- package/app/public/vendor/fonts/newsreader-400.woff2 +0 -0
- package/app/public/vendor/fonts/newsreader-700.woff2 +0 -0
- package/app/public/vendor/fonts/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/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/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/ufmag-400.woff2 +0 -0
- package/dist-pkg/core/anel.d.ts +0 -17
- package/dist-pkg/core/anel.js +0 -31
- package/dist-pkg/core/collision.d.ts +0 -21
- package/dist-pkg/core/collision.js +0 -62
- package/dist-pkg/core/layers.d.ts +0 -63
- package/dist-pkg/core/layers.js +0 -95
- package/dist-pkg/core/letter-grid.d.ts +0 -55
- package/dist-pkg/core/letter-grid.js +0 -116
- package/dist-pkg/core/password.d.ts +0 -34
- package/dist-pkg/core/password.js +0 -205
- package/dist-pkg/core/rotulo-acessivel.d.ts +0 -17
- package/dist-pkg/core/rotulo-acessivel.js +0 -41
- package/dist-pkg/core/run-state.d.ts +0 -115
- package/dist-pkg/core/run-state.js +0 -40
- package/dist-pkg/core/tiles.d.ts +0 -12
- package/dist-pkg/core/tiles.js +0 -61
- package/dist-pkg/core/world.d.ts +0 -4
- package/dist-pkg/core/world.js +0 -58
- package/dist-pkg/input/origem-sintetica.d.ts +0 -44
- package/dist-pkg/input/origem-sintetica.js +0 -60
- package/dist-pkg/input/transporte-em-uso.d.ts +0 -101
- package/dist-pkg/input/transporte-em-uso.js +0 -130
- package/dist-pkg/platform/audio-nav.d.ts +0 -51
- package/dist-pkg/platform/audio-nav.js +0 -99
- package/dist-pkg/platform/guide-intensity.d.ts +0 -40
- package/dist-pkg/platform/guide-intensity.js +0 -73
- package/dist-pkg/platform/pesados-catalogo.d.ts +0 -14
- package/dist-pkg/platform/pesados-catalogo.js +0 -177
- package/dist-pkg/platform/pesados.d.ts +0 -31
- package/dist-pkg/platform/pesados.js +0 -75
- package/dist-pkg/platform/vozes-prontas.d.ts +0 -28
- package/dist-pkg/platform/vozes-prontas.js +0 -61
- package/dist-pkg/render/camera.d.ts +0 -58
- package/dist-pkg/render/camera.js +0 -105
- package/dist-pkg/render/cenario-data.d.ts +0 -127
- package/dist-pkg/render/cenario-data.js +0 -145
- package/dist-pkg/render/city-tex.d.ts +0 -63
- package/dist-pkg/render/city-tex.js +0 -228
- package/dist-pkg/render/city-tiles.d.ts +0 -11
- package/dist-pkg/render/city-tiles.js +0 -140
- package/dist-pkg/render/draw.d.ts +0 -149
- package/dist-pkg/render/draw.js +0 -231
- package/dist-pkg/render/fx.d.ts +0 -63
- package/dist-pkg/render/fx.js +0 -123
- package/dist-pkg/render/minimap.d.ts +0 -11
- package/dist-pkg/render/minimap.js +0 -94
- package/dist-pkg/render/parallax.d.ts +0 -85
- package/dist-pkg/render/parallax.js +0 -158
- package/dist-pkg/render/player-anim.d.ts +0 -71
- package/dist-pkg/render/player-anim.js +0 -152
- package/dist-pkg/render/recycling-tex.d.ts +0 -48
- package/dist-pkg/render/recycling-tex.js +0 -164
- package/dist-pkg/render/scene-city.d.ts +0 -77
- package/dist-pkg/render/scene-city.js +0 -181
- package/dist-pkg/render/scene-parallax.d.ts +0 -72
- package/dist-pkg/render/scene-parallax.js +0 -426
- package/dist-pkg/render/scene-sky.d.ts +0 -126
- package/dist-pkg/render/scene-sky.js +0 -295
- package/dist-pkg/render/set-cenario.d.ts +0 -54
- package/dist-pkg/render/set-cenario.js +0 -103
- package/dist-pkg/render/textures.d.ts +0 -75
- package/dist-pkg/render/textures.js +0 -240
- package/dist-pkg/render/title-scene.d.ts +0 -46
- package/dist-pkg/render/title-scene.js +0 -75
- package/dist-pkg/render/weather.d.ts +0 -96
- package/dist-pkg/render/weather.js +0 -171
- package/dist-pkg/render/wheelchair-sprites.d.ts +0 -32
- package/dist-pkg/render/wheelchair-sprites.js +0 -69
- package/dist-pkg/render/world-tex.d.ts +0 -18
- package/dist-pkg/render/world-tex.js +0 -87
- package/dist-pkg/ui/activities-menu.d.ts +0 -258
- package/dist-pkg/ui/activities-menu.js +0 -648
- package/dist-pkg/ui/caa-sets.d.ts +0 -39
- package/dist-pkg/ui/caa-sets.js +0 -65
- package/dist-pkg/ui/latch-refusal.d.ts +0 -32
- package/dist-pkg/ui/latch-refusal.js +0 -60
- package/dist-pkg/ui/map-hub.d.ts +0 -56
- package/dist-pkg/ui/map-hub.js +0 -138
- package/dist-pkg/ui/settings-caa.d.ts +0 -45
- package/dist-pkg/ui/settings-caa.js +0 -137
- package/dist-pkg/ui/settings-motor.d.ts +0 -175
- package/dist-pkg/ui/settings-motor.js +0 -342
- package/dist-pkg/ui/webcam.d.ts +0 -7
- package/dist-pkg/ui/webcam.js +0 -93
|
@@ -1,117 +1,142 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
-
// ui/pause-icons — the PER-SCREEN pause
|
|
3
|
-
// Extracted VERBATIM from game.js (Estágio 4): PAUSE_ICONS, buildScreenPause, calmMode/applyCalm, iconAct,
|
|
4
|
-
// iconLabel, reflectIconBtn, reflectPauseIcons and hasPrivateOutput.
|
|
2
|
+
// ui/pause-icons — the PER-SCREEN pause card (`.screen-pause`) and the accessibility quick bar (`.pi-btn`).
|
|
5
3
|
//
|
|
6
|
-
// WHY THE
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
4
|
+
// WHY THE QUICK BAR IS ITS OWN MODULE: its buttons are the ONLY place where a state owned by another subsystem (blind
|
|
5
|
+
// mode, TTS, Libras, calm mode, the input mode, contrast, colour correction, camera, voice, speed, language) is both
|
|
6
|
+
// changed AND read back as an `aria-label`. That round-trip — «the label must tell the truth about the state» — is why
|
|
7
|
+
// `computeIconLabel` exists, and it is what the pure functions below make testable in node.
|
|
10
8
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// guards, same announcements — only the shape changed.
|
|
9
|
+
// Each icon's action is a row of a TABLE (`ICON_ACTS`), not a chain of `if`s: the list of subsystems is long by nature,
|
|
10
|
+
// one button each.
|
|
14
11
|
//
|
|
15
|
-
//
|
|
16
|
-
// · `
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
import {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
import {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
import
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
12
|
+
// INJECTED, not owned here:
|
|
13
|
+
// · `setPauseActor` — who opened the card; the panels open scoped to that player. This module only WRITES it.
|
|
14
|
+
// · `getPauseActs` — the `.pm-btn` action table (`ui/shell` or the host's own), asked when an item is pressed, so it
|
|
15
|
+
// can be wired after this module.
|
|
16
|
+
// · the pause screens, asked through a getter: the host rebuilds them when the number of screens changes.
|
|
17
|
+
// · `rm`/`saveRM` — the reduced-motion flags, shared with ui/settings-motion (same reference).
|
|
18
|
+
// · `matchMedia` — the browser's media query, for the reduced-motion default when nothing is stored (ADR-0232).
|
|
19
|
+
// · `PM_BTNS` and the other lists of buttons are `ui/pause-buttons`', imported, never copied.
|
|
20
|
+
import { flagOf, nextLocale, LANGUAGE_NAME } from './locale-flags.js';
|
|
21
|
+
/*
|
|
22
|
+
* 🔴 THE TWO VISUAL CYCLES MOVED HOUSE to `core/visual-cycles` (ADR-0221, issue #203). They lived in TWO modules — the list of
|
|
23
|
+
* levels in the panel, the steps and the names here — and the thing that costs most to maintain is the ASYMMETRY between them,
|
|
24
|
+
* which only reads with both in sight. See the header there.
|
|
25
|
+
*/
|
|
26
|
+
import { SHORT_THEME, SHORT_CORRECTION } from './visual-axes-panel.js';
|
|
27
|
+
/*
|
|
28
|
+
* 🔴 CALM MODE MOVED HOUSE (ADR-0221, issue #203). It is not about ICONS: it is about what a child who cannot bear noise needs
|
|
29
|
+
* the engine to silence, and the icon is only one of the surfaces she asks through. It lives in `core/calm-mode`, a leaf
|
|
30
|
+
* module — zero imports, zero DOM — which is why the destructive clamp of level 1 can be measured without mounting anything.
|
|
31
|
+
*/
|
|
32
|
+
import { CALM_NAMES, CALM_AUDIO_CATS, nextCalmMode, sanitiseTeaLevel, calmAudioPlan, calmMotionPlan } from '../core/calm-mode.js';
|
|
33
|
+
/*
|
|
34
|
+
* 🔴 THE MARKUP MOVED HOUSE to `ui/pause-markup` (ADR-0221, issue #203). Building the strings and wiring the elements the
|
|
35
|
+
* browser makes from them are two jobs that do not need each other; what is left in this file is the second one. See the
|
|
36
|
+
* header there for why the icon catalogue had to leave before the markup could.
|
|
37
|
+
*/
|
|
38
|
+
import { quickBarMarkup, screenPauseMarkup, } from './pause-markup.js';
|
|
39
|
+
/*
|
|
40
|
+
* 🔴 THE ICON CATALOGUE MOVED HOUSE to `core/pause-icon-catalogue` (ADR-0221, issue #203): WHICH icons exist and in what order
|
|
41
|
+
* is DATA, and this file is about what they DO. See the header there for why the data had to leave first.
|
|
42
|
+
*/
|
|
43
|
+
import { PAUSE_ICONS, pauseIcon } from '../core/pause-icon-catalogue.js';
|
|
44
|
+
import { nextTheme, nextCorrection, hasHighContrast, DEFAULT_VISUAL, } from '../render/viz-axes.js';
|
|
45
|
+
import { announceItem } from './item-announcement.js';
|
|
46
|
+
import { keepInView } from './menu-items.js';
|
|
47
|
+
import { accessibleLabel } from '../core/accessible-label.js';
|
|
48
|
+
import { stepInRing } from '../core/ring.js'; // from the LEAF, not from ui/menu-nav: see the note there
|
|
49
|
+
// A LIVE BINDING (ESM): the index can be turned off in the menu, and the value here follows without a subscription.
|
|
50
|
+
// The stateless half (ADR-0232): the defaults, the reduced-motion question and the 📷 cycle are vocabulary, not the store.
|
|
51
|
+
import { DEFAULTS, defaultReducedMotion } from '../core/setting-defaults.js';
|
|
52
|
+
import { nextCameraControl } from '../core/camera-cycle.js';
|
|
53
|
+
/** The word for each position of the 📷 cycle (ADR-0215). */
|
|
54
|
+
const CAMERA_MODE_NAME = { off: 'state.off', hands: 'camera.hands', face: 'camera.face', eyes: 'camera.eyes' };
|
|
55
|
+
/**
|
|
56
|
+
* The word for each position. Private again since 2026-09-21: it was exported for the motor panel's row, and the Dev took that
|
|
57
|
+
* row out («Tire a linha de acessibilidade motora») — the icon is the only surface of this setting now.
|
|
58
|
+
*/
|
|
59
|
+
const INPUT_MODE_NAME = { standard: 'input.standard', sticky: 'input.sticky', scan: 'input.scan' };
|
|
60
|
+
/**
|
|
61
|
+
* Which of the three is showing. The SCAN WINS over the latch on purpose: with one button there is nothing to hold, so a stored
|
|
62
|
+
* latch would otherwise make the icon claim a position the child is not in.
|
|
63
|
+
*/
|
|
64
|
+
export function inputModeOf(s) {
|
|
65
|
+
return s.switchScan ? 'scan' : s.toggleMove ? 'sticky' : 'standard';
|
|
66
|
+
}
|
|
46
67
|
/**
|
|
47
|
-
* A
|
|
68
|
+
* The next position. A GAME THAT HOLDS NO KEY has nothing for the latch to hold, so the icon offers standard and one button:
|
|
69
|
+
* a cycle never stops where nothing would happen (ADR-0155), and a word that promised what this game cannot do would teach a
|
|
70
|
+
* child that her setting is broken (ADR-0106 §5).
|
|
48
71
|
*
|
|
49
|
-
*
|
|
50
|
-
* (
|
|
51
|
-
|
|
52
|
-
|
|
72
|
+
* 📌 THE DEVICE TAKES NOTHING AWAY (ADR-0249): on eyes, face, gestures and speech the latch starts as the game answers
|
|
73
|
+
* `holdsKeys()`, and the child may turn it on or off like on any other device — so all three positions are offered there too.
|
|
74
|
+
*/
|
|
75
|
+
function inputModeOrder(holdsKeys) {
|
|
76
|
+
return holdsKeys ? ['standard', 'sticky', 'scan'] : ['standard', 'scan'];
|
|
77
|
+
}
|
|
78
|
+
export function nextInputMode(m, holdsKeys) {
|
|
79
|
+
const order = inputModeOrder(holdsKeys);
|
|
80
|
+
const i = order.indexOf(m);
|
|
81
|
+
return order[(i < 0 ? 0 : i + 1) % order.length];
|
|
82
|
+
}
|
|
83
|
+
// The rule for applying an input mode is written in the icon's own action, where it happens: the ☝️ is the only surface of
|
|
84
|
+
// this setting.
|
|
85
|
+
import { nextGameSpeed } from '../core/game-speed.js';
|
|
86
|
+
import { KEYS } from '../platform/storage-keys.js';
|
|
87
|
+
import { setMoveLatch } from './settings-mobility.js';
|
|
88
|
+
import { PM_BTNS, PM_OPTIONS_BTNS, PM_GAME_BTNS } from './pause-buttons.js';
|
|
89
|
+
import { aacMenuLocked } from './aac-sets.js';
|
|
90
|
+
import { SCENE_KEYS, CHARACTER_ANIMATIONS, readStoredScene, storeScene } from './motion-scene.js';
|
|
91
|
+
/**
|
|
92
|
+
* Hover and focus on a bar's icons write its NAME in the bar's `.pause-icons-cap` and ask for its EXPLANATION.
|
|
53
93
|
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
94
|
+
* Shared by the per-screen quick bars and the bar the engine mounts in `#title-icons`: two copies of this wiring is
|
|
95
|
+
* how one bar showed the name and the other did not — the Dev found the engine's bar silent on hover and on the
|
|
96
|
+
* cursor. Leaving an icon falls back to the CURSOR of the bar's mode when there is one (`.pi-sel`): it is the only
|
|
97
|
+
* thing saying where that cursor is.
|
|
56
98
|
*/
|
|
57
|
-
export function
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
99
|
+
export function wireBarCaption(bar, explain) {
|
|
100
|
+
const cap = bar.querySelector('.pause-icons-cap');
|
|
101
|
+
const captionFor = (b) => {
|
|
102
|
+
if (cap)
|
|
103
|
+
cap.textContent = accessibleLabel(b); // name and state only: «N de M» is spoken, never written (ADR-0167)
|
|
104
|
+
explain(b.dataset.pi ?? null);
|
|
105
|
+
};
|
|
106
|
+
const restoreCaption = () => {
|
|
107
|
+
const cursor = bar.querySelector('.pi-sel');
|
|
108
|
+
if (cursor) {
|
|
109
|
+
captionFor(cursor);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (cap)
|
|
113
|
+
cap.textContent = '';
|
|
114
|
+
explain(null);
|
|
115
|
+
};
|
|
116
|
+
bar.querySelectorAll('.pi-btn').forEach((b) => {
|
|
117
|
+
b.addEventListener('mouseenter', () => captionFor(b));
|
|
118
|
+
b.addEventListener('focus', () => captionFor(b));
|
|
119
|
+
b.addEventListener('mouseleave', restoreCaption);
|
|
120
|
+
b.addEventListener('blur', restoreCaption);
|
|
121
|
+
});
|
|
63
122
|
}
|
|
64
|
-
/** The accessibility shortcut bar at the top of every pause screen (and of the splash `#title-icons`).
|
|
65
|
-
* Sound-bound icons (blind/TTS) require a private audio output; webcam/voice are still `soon`.
|
|
66
|
-
* VERBATIM from game.js in order and behaviour; the names became i18n keys in the Fase-5 pass. */
|
|
67
|
-
// `n` é a CHAVE i18n do nome do ícone (o emoji `e` não traduz — é o mesmo glifo em toda língua).
|
|
68
|
-
export const PAUSE_ICONS = [
|
|
69
|
-
{ k: 'blind', e: '🦯', n: 'icon.blind' },
|
|
70
|
-
{ k: 'tts', e: '🗨️', n: 'icon.tts' },
|
|
71
|
-
{ k: 'libras', e: '🤟', n: 'icon.libras' },
|
|
72
|
-
{ k: 'tea', e: '🧩', n: 'icon.tea' },
|
|
73
|
-
{ k: 'altmove', e: '☝️', n: 'icon.altmove' }, // ☝️ e não 🦾: o gesto é UM DEDO tocando, que é o que a alternância pede (pedido do Dev)
|
|
74
|
-
{ k: 'contrast', e: '🌗', n: 'icon.contrast' },
|
|
75
|
-
{ k: 'cvd', e: '🚥', n: 'icon.cvd' },
|
|
76
|
-
{ k: 'face', e: '🧑', n: 'icon.face', soon: true },
|
|
77
|
-
{ k: 'eyes', e: '👀', n: 'icon.eyes', soon: true },
|
|
78
|
-
{ k: 'voice', e: '👄', n: 'icon.voice', soon: true },
|
|
79
|
-
];
|
|
80
|
-
const ICON_BY_KEY = new Map(PAUSE_ICONS.map((ic) => [ic.k, ic]));
|
|
81
|
-
export function pauseIcon(k) { return ICON_BY_KEY.get(k); }
|
|
82
|
-
/** TEA cycle: 0 = normal · 1 = calmo (reduces) · 2 = silencioso (switches off). Never touches TTS/blind mode. */
|
|
83
|
-
export const CALM_NAMES = ['calm.off', 'calm.quiet', 'calm.silent'];
|
|
84
123
|
/**
|
|
85
|
-
*
|
|
86
|
-
*
|
|
124
|
+
* What an icon of the quick bar SAYS — one function, not three copies of the same expression. It is spoken at moments
|
|
125
|
+
* that look different and are the same (the cursor lands on the icon, a finger activates it), and the `aria-label` it
|
|
126
|
+
* reads already carries the STATE («Alto contraste, ativado»): `reflectIconBtn` rewrites it on every change, which is why
|
|
127
|
+
* this reads it back instead of rebuilding the text.
|
|
87
128
|
*/
|
|
88
|
-
|
|
89
|
-
|
|
129
|
+
function iconCaption(t, barEl, el, menuIndexOn) {
|
|
130
|
+
const icons = [...barEl.querySelectorAll('.pi-btn')];
|
|
131
|
+
// «A declared label wins» is `core/accessible-label`'s rule, shared with the pause list and every menu: one answer to
|
|
132
|
+
// «what is this control called».
|
|
133
|
+
return announceItem(t, { label: accessibleLabel(el), position: icons.indexOf(el) + 1, total: icons.length }, menuIndexOn);
|
|
90
134
|
}
|
|
91
|
-
/**
|
|
92
|
-
function
|
|
93
|
-
return
|
|
135
|
+
/** Reads the calm level from storage, sanitised. Called in `init`, never on import. */
|
|
136
|
+
function readTeaLevel(store) {
|
|
137
|
+
return sanitiseTeaLevel(store.getNum(KEYS.tea, DEFAULTS.calmMode), DEFAULTS.calmMode);
|
|
94
138
|
}
|
|
95
|
-
/**
|
|
96
|
-
export const CALM_AUDIO_CATS = ['ambient', 'music', 'earcons', 'other', 'interact'];
|
|
97
|
-
/** Colour-vision-deficiency cycle, in `player.viz` values. */
|
|
98
|
-
export const CVD_SEQ = ['normal', 'fix-protan', 'fix-deuter', 'fix-tritan'];
|
|
99
|
-
/** i18n keys of the CVD announcement names, indexed the same as CVD_SEQ.
|
|
100
|
-
*
|
|
101
|
-
* ⚠️ POSITION 0 IS `cvd.tricro` AND NOT `cvd.off`, AND THE TWO KEYS ARE NOT INTERCHANGEABLE.
|
|
102
|
-
* This list names the four CHOICES of the cycle, so position 0 is a way of seeing — trichromatic
|
|
103
|
-
* vision, the one that needs no correction — and it is said as one. `cvd.off` below is a FALLBACK
|
|
104
|
-
* for `s.viz` values that are not corrections at all, and 13 of the 16 viz modes are exactly that:
|
|
105
|
-
* the three simulations, the three contrast levels, the five low-vision modes and blind mode.
|
|
106
|
-
* Announcing "trichromatic vision" there would have the software assert what the child sees, while
|
|
107
|
-
* she is simulating not seeing it. The choice is named; the fallback is switched off. */
|
|
108
|
-
export const CVD_NAMES = ['cvd.tricro', 'cvd.protan', 'cvd.deuter', 'cvd.tritan'];
|
|
109
|
-
/** `player.viz` → i18n key of the label used by iconLabel (anything else falls back to the 'off' key). */
|
|
110
|
-
export const CVD_LABELS = {
|
|
111
|
-
'fix-protan': 'cvd.protan', 'fix-deuter': 'cvd.deuter', 'fix-tritan': 'cvd.tritan',
|
|
112
|
-
};
|
|
113
|
-
/** A player has private output when nobody else is on the same sink. Single screen ⇒ always private.
|
|
114
|
-
* Pure form of game.js's hasPrivateOutput (see the report: it had no other caller left). */
|
|
139
|
+
/** A player has private output when nobody else is on the same sink. Single screen ⇒ always private. */
|
|
115
140
|
export function hasPrivateOutputIn(list, count, i) {
|
|
116
141
|
if (count <= 1)
|
|
117
142
|
return true;
|
|
@@ -120,234 +145,216 @@ export function hasPrivateOutputIn(list, count, i) {
|
|
|
120
145
|
return false;
|
|
121
146
|
return !list.some((q, j) => j !== i && q && q.audioSink === p.audioSink);
|
|
122
147
|
}
|
|
123
|
-
/** TEA cycle step. */
|
|
124
|
-
export function nextCalmMode(cur) { return (cur + 1) % 3; }
|
|
125
|
-
/** Next high-contrast level. An unlisted `viz` (e.g. a CVD filter) is treated as index 0 ⇒ jumps to 'hc-direto'. */
|
|
126
|
-
export function nextContrast(cur) {
|
|
127
|
-
let idx = CONTRAST_LEVELS.indexOf(cur);
|
|
128
|
-
idx = idx < 0 ? 0 : idx;
|
|
129
|
-
return CONTRAST_LEVELS[(idx + 1) % CONTRAST_LEVELS.length];
|
|
130
|
-
}
|
|
131
|
-
/** Next CVD filter. NOTE the asymmetry with nextContrast: an unlisted `viz` maps to index 1 ('fix-protan'),
|
|
132
|
-
* not 0 — verbatim from game.js (`idx = idx<0 ? 1 : (idx+1)%seq.length`). */
|
|
133
|
-
export function nextCvd(cur) {
|
|
134
|
-
let idx = CVD_SEQ.indexOf(cur);
|
|
135
|
-
idx = idx < 0 ? 1 : (idx + 1) % CVD_SEQ.length;
|
|
136
|
-
return { idx, mode: CVD_SEQ[idx] };
|
|
137
|
-
}
|
|
138
|
-
/** What `applyCalm` does to ONE audio category, given the TEA level. Extracted so the (destructive) volume
|
|
139
|
-
* clamp at level 1 is visible and testable — see the report. */
|
|
140
|
-
export function calmAudioPlan(calmMode, vol) {
|
|
141
|
-
if (calmMode === 0)
|
|
142
|
-
return { on: true, vol };
|
|
143
|
-
if (calmMode === 1)
|
|
144
|
-
return { on: true, vol: Math.min(vol, 0.3) };
|
|
145
|
-
return { on: false, vol };
|
|
146
|
-
}
|
|
147
|
-
/** What `applyCalm` does to the scene/character reduced-motion flags. */
|
|
148
|
-
export function calmMotionPlan(calmMode) {
|
|
149
|
-
return { sceneReduced: calmMode >= 1, charFrozen: calmMode === 2 };
|
|
150
|
-
}
|
|
151
148
|
/** The `aria-label` of one icon — it MUST reflect the current state, on/off or level. This is the whole
|
|
152
149
|
* point of the function: a toggle that looks pressed but does not say so is invisible to a screen reader. */
|
|
153
|
-
export function computeIconLabel(k, s) {
|
|
154
|
-
const ic =
|
|
150
|
+
export function computeIconLabel(t, k, s) {
|
|
151
|
+
const ic = pauseIcon(k);
|
|
155
152
|
if (!ic)
|
|
156
153
|
return '';
|
|
157
|
-
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
return t('icon.soon', { nome: t(ic.n) });
|
|
162
|
-
const rotulo = (v) => t('icon.state', { nome: t(ic.n), v: t(v) });
|
|
163
|
-
if (k === 'blind')
|
|
164
|
-
return rotulo(s.modoCego ? 'state.on' : 'state.off');
|
|
165
|
-
if (k === 'tts')
|
|
166
|
-
return rotulo(s.ttsOn ? 'state.on' : 'state.off');
|
|
167
|
-
if (k === 'libras')
|
|
168
|
-
return rotulo(s.librasOn ? 'state.on' : 'state.off');
|
|
169
|
-
// TEA e daltonismo usam um nome CURTO aqui, diferente do nome do botão: o rótulo já diz o nível, e
|
|
170
|
-
// repetir a lista de níveis do nome ("(calmo / silencioso)", "(protan/deutan/tritan)") a diria duas vezes.
|
|
171
|
-
// Era assim antes da conversão, com o texto curto embutido — preservado, não reinventado.
|
|
172
|
-
if (k === 'tea')
|
|
173
|
-
return t('icon.state', { nome: t('icon.tea.short'), v: t(CALM_NAMES[s.calmMode]) });
|
|
174
|
-
if (k === 'altmove')
|
|
175
|
-
return rotulo(s.toggleMove ? 'state.on' : 'state.off');
|
|
176
|
-
if (k === 'contrast')
|
|
177
|
-
return rotulo(CURTO_DO_TEMA[s.visual.tema]);
|
|
178
|
-
if (k === 'cvd')
|
|
179
|
-
return t('icon.state', { nome: t('icon.cvd.short'), v: t(CURTO_DA_CORRECAO[s.visual.correcao]) });
|
|
180
|
-
return t(ic.n);
|
|
154
|
+
const state = STATE_OF_ICON[k];
|
|
155
|
+
// The state is ALWAYS a parameter (`{v}`), never a concatenation: a language that puts the state before the name needs
|
|
156
|
+
// the dictionary to reorder it. «name: state» is the frame; the state is the content, and it is translated too.
|
|
157
|
+
return state ? t('icon.state', { nome: t(SHORT_NAME[k] ?? ic.n), v: state(t, s) }) : t(ic.n);
|
|
181
158
|
}
|
|
182
|
-
|
|
159
|
+
const onOff = (t, on) => t(on ? 'state.on' : 'state.off');
|
|
160
|
+
/** What each icon with a state says after its name, already in the child's language. An icon not here has no state to say. */
|
|
161
|
+
const STATE_OF_ICON = {
|
|
162
|
+
blind: (t, s) => onOff(t, s.blindMode),
|
|
163
|
+
tts: (t, s) => onOff(t, s.ttsOn),
|
|
164
|
+
libras: (t, s) => onOff(t, s.librasOn),
|
|
165
|
+
tea: (t, s) => t(CALM_NAMES[s.calmMode]),
|
|
166
|
+
altmove: (t, s) => t(INPUT_MODE_NAME[inputModeOf(s)]),
|
|
167
|
+
contrast: (t, s) => t(SHORT_THEME[s.visual.tema]),
|
|
168
|
+
cvd: (t, s) => t(SHORT_CORRECTION[s.visual.correcao]),
|
|
169
|
+
camera: (t, s) => t(CAMERA_MODE_NAME[s.camera ?? 'off']),
|
|
170
|
+
voice: (t, s) => onOff(t, s.voice),
|
|
171
|
+
// a language's own name is not translated: «Español» reads the same in every interface
|
|
172
|
+
idioma: (_t, s) => LANGUAGE_NAME[(s.locale ?? 'pt')] ?? LANGUAGE_NAME.pt,
|
|
173
|
+
velocidade: (t, s) => t('icon.velocidade.valor', { pct: Math.round((s.speed ?? 1) * 100) }),
|
|
174
|
+
};
|
|
175
|
+
/**
|
|
176
|
+
* Calm mode and colour correction have a SHORT name key for the label, because the button's name once carried the list of
|
|
177
|
+
* levels («(calmo / silencioso)», «(protan/deutan/tritan)») and the label already says the level. ⚠️ In the three
|
|
178
|
+
* dictionaries the short key now equals the long one word for word: the distinction has no subject left, and merging the
|
|
179
|
+
* keys is a dictionary change.
|
|
180
|
+
*/
|
|
181
|
+
const SHORT_NAME = { tea: 'icon.tea.short', cvd: 'icon.cvd.short' };
|
|
182
|
+
const ICON_VISUAL = Object.freeze({
|
|
183
|
+
blind: (s) => ({ on: s.blindMode, dis: !s.privateOutput }),
|
|
184
|
+
tts: (s) => ({ on: s.ttsOn, dis: !s.privateOutput || !!s.noVoice }),
|
|
185
|
+
libras: (s) => ({ on: s.librasOn }),
|
|
186
|
+
tea: (s) => ({ on: s.calmMode === 2, calm: s.calmMode === 1 }),
|
|
187
|
+
// ⚠️ AND IT IS NEVER GREYED OUT: every device may be in any of its positions (ADR-0249), and what a game cannot hold the
|
|
188
|
+
// CYCLE tells by not offering it — greying the icon would take the one-button scan away with it.
|
|
189
|
+
altmove: (s) => ({ on: inputModeOf(s) !== 'standard' }),
|
|
190
|
+
contrast: (s) => ({ on: hasHighContrast(s.visual) }),
|
|
191
|
+
velocidade: (s) => ({ on: (s.speed ?? 1) < 1 }),
|
|
192
|
+
camera: (s) => ({ on: (s.camera ?? 'off') !== 'off' }),
|
|
193
|
+
voice: (s) => ({ on: !!s.voice }),
|
|
194
|
+
// ⚠️ THE TWO-TONE BACKGROUND IS THIS ICON'S «ON» SIGNAL, and it reads the correction AXIS — so it still says the same when
|
|
195
|
+
// the high-contrast theme is on too, and the child's correction does not vanish from the icon that exists to show it.
|
|
196
|
+
cvd: (s) => ({ cvd: s.visual.correcao !== 'tricro' ? `pi-cvd-${s.visual.correcao}` : '' }),
|
|
197
|
+
});
|
|
198
|
+
/** At rest: an icon the table does not name shows nothing. */
|
|
199
|
+
const ICON_VISUAL_AT_REST = Object.freeze({ on: false, dis: false, calm: false, cvd: '', active: false });
|
|
200
|
+
/** Pure form of reflectIconBtn's branching. */
|
|
183
201
|
export function computeIconVisual(k, s) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
else if (k === 'tts') {
|
|
190
|
-
on = s.ttsOn;
|
|
191
|
-
dis = !s.privateOutput;
|
|
192
|
-
}
|
|
193
|
-
else if (k === 'libras') {
|
|
194
|
-
on = s.librasOn;
|
|
195
|
-
}
|
|
196
|
-
else if (k === 'tea') {
|
|
197
|
-
on = s.calmMode === 2;
|
|
198
|
-
calm = s.calmMode === 1;
|
|
199
|
-
}
|
|
200
|
-
else if (k === 'altmove') {
|
|
201
|
-
on = s.toggleMove;
|
|
202
|
-
dis = !!s.alternanciaExigida;
|
|
203
|
-
}
|
|
204
|
-
else if (k === 'contrast') {
|
|
205
|
-
on = temAltoContraste(s.visual);
|
|
206
|
-
}
|
|
207
|
-
else if (k === 'cvd') {
|
|
208
|
-
// ⚠️ O FUNDO DE DUAS CORES É O SINAL DE LIGADO deste ícone, e agora ele lê o EIXO da correção — que
|
|
209
|
-
// continua a dizer o mesmo quando o tema também está ligado, coisa que a chave única não conseguia: com
|
|
210
|
-
// `hc-direto-7` no campo, a correção da criança desaparecia do ícone que existe para a mostrar.
|
|
211
|
-
if (s.visual.correcao !== 'tricro')
|
|
212
|
-
cvd = 'pi-cvd-' + s.visual.correcao;
|
|
213
|
-
}
|
|
214
|
-
return { on, dis, calm, cvd, active: on || calm || !!cvd };
|
|
202
|
+
const v = { ...ICON_VISUAL_AT_REST, ...ICON_VISUAL[k]?.(s) };
|
|
203
|
+
// 📌 `active` is DERIVED, declared by no rule: it is the `aria-pressed`, and no icon should be able to say it is pressed
|
|
204
|
+
// without showing why.
|
|
205
|
+
return { ...v, active: v.on || v.calm || !!v.cvd };
|
|
215
206
|
}
|
|
216
207
|
/** The CSS classes reflectIconBtn clears before applying a fresh visual — in the original order. */
|
|
217
208
|
export const ICON_STATE_CLASSES = ['pi-calm', 'pi-cvd-protan', 'pi-cvd-deuter', 'pi-cvd-tritan'];
|
|
218
|
-
// --- markup (pure string builders; the DOM shell below just assigns them) ---
|
|
219
|
-
/** One `.pi-btn`. `soon` icons get `.pi-soon` and the "under construction" suffix baked into the aria-label.
|
|
220
|
-
* The label is the RESTING one: reflectIconBtn overwrites it with the stateful label as soon as the bar is
|
|
221
|
-
* reflected. `ic.n` is an i18n key, so it must be resolved here too — the markup is rendered once at build
|
|
222
|
-
* time and would otherwise ship the raw key to a screen reader. */
|
|
223
|
-
export function iconBtnMarkup(ic) {
|
|
224
|
-
return '<button class="pi-btn' + (ic.soon ? ' pi-soon' : '') + '" type="button" data-pi="' + ic.k +
|
|
225
|
-
'" aria-label="' + (ic.soon ? t('icon.soon', { nome: t(ic.n) }) : t(ic.n)) + '">' + ic.e + '</button>';
|
|
226
|
-
}
|
|
227
|
-
export function iconesQueAccionam(escritores) {
|
|
228
|
-
// ⚠️ POR ÍCONE, e não um booleano para os dois — e foi uma MUTAÇÃO SOBREVIVENTE que o mostrou. Com uma
|
|
229
|
-
// única bandeira, `&&` e `||` produziam o mesmo resultado nos casos que eu tinha escrito, porque todos
|
|
230
|
-
// tiravam os DOIS escritores. O `&&` escondia um ícone que FUNCIONA quando só um escritor falta, e o `||`
|
|
231
|
-
// mostrava um que NÃO funciona. Os dois erram, em direcções opostas, e a pergunta certa nunca foi «este
|
|
232
|
-
// jogo tem escritores visuais» — é «este ÍCONE tem quem o accione».
|
|
233
|
-
// 📌 E o `altmove` entra pela MESMA porta, que é o achado: «este jogo segura teclas?» é a mesma pergunta
|
|
234
|
-
// que «este ícone tem quem o accione», feita a um campo do contrato em vez de a um escritor injectado.
|
|
235
|
-
// Um terceiro ramo, e não uma regra nova.
|
|
236
|
-
return PAUSE_ICONS.filter((ic) => (ic.k === 'contrast' ? escritores.tema
|
|
237
|
-
: ic.k === 'cvd' ? escritores.correcao
|
|
238
|
-
: ic.k === 'altmove' ? escritores.seguraTeclas()
|
|
239
|
-
: true));
|
|
240
|
-
}
|
|
241
209
|
/**
|
|
242
|
-
*
|
|
210
|
+
* WHO ACTIVATES EACH ICON — one question per icon, and an icon nothing locks is offered.
|
|
243
211
|
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
212
|
+
* PER ICON, not one flag for the whole bar: with one flag, a single missing writer would either hide an icon that WORKS
|
|
213
|
+
* or show one that does NOT. The question is «does this ICON have something to activate it». A table, so adding an icon
|
|
214
|
+
* is adding a row.
|
|
247
215
|
*/
|
|
248
|
-
|
|
216
|
+
const ICON_IS_ACTIONABLE = Object.freeze({
|
|
217
|
+
contrast: (w) => w.theme,
|
|
218
|
+
cvd: (w) => w.correction,
|
|
219
|
+
// «Does this game hold keys?» is the same question, asked of a contract field rather than an injected writer.
|
|
220
|
+
// AND A SECOND HALF (ADR-0218): «one button only» has a subject wherever the game declares a position to scan, so a game
|
|
221
|
+
// holding no key (the quiz demo) still has a ☝️. A game that declares nothing has none: a scan of nothing is the dead
|
|
222
|
+
// button of ADR-0106 §5 paid for in seconds.
|
|
223
|
+
altmove: (w) => w.holdsKeys() || (w.declaredPositions?.() ?? 0) > 0,
|
|
224
|
+
// The same question asked of the typography cycle (ADR-0149): the icon exists when someone knows how to walk it.
|
|
225
|
+
// `Boolean(...)` and not the raw field: it is optional, and the conversion is written rather than left to a `filter`.
|
|
226
|
+
tipografia: (w) => Boolean(w.typography),
|
|
227
|
+
// the hourglass exists where time runs by itself (ADR-0180): a turn game has nothing to slow
|
|
228
|
+
velocidade: (w) => Boolean(w.clock?.()),
|
|
229
|
+
camera: (w) => Boolean(w.camera),
|
|
230
|
+
// 👄 exists where there is a MICROPHONE to ask for, the same rule as the camera's (ADR-0106 §5)
|
|
231
|
+
voice: (w) => Boolean(w.microphone),
|
|
232
|
+
menu: (w) => Boolean(w.menus),
|
|
233
|
+
});
|
|
234
|
+
export function iconsThatAct(writers) {
|
|
235
|
+
// ABSENCE FROM THE TABLE MEANS YES: an icon no rule locks depends on nobody to work, and hiding it for lack of a row
|
|
236
|
+
// would take away from the child a way that exists.
|
|
237
|
+
return PAUSE_ICONS.filter((ic) => ICON_IS_ACTIONABLE[ic.k]?.(writers) ?? true);
|
|
238
|
+
}
|
|
249
239
|
/**
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
240
|
+
* THE ITEMS THE ENGINE ACTIVATES BY ITSELF, which therefore never depend on a game's `getPauseActs`. Read from the
|
|
241
|
+
* dispatch, not decided here: `options`, `opcoesdojogo` and `pmback` change which list is on the card, and
|
|
242
|
+
* `acessibilidade` takes the cursor to the quick bar — all four are handled in this module before the game's table is
|
|
243
|
+
* consulted. What belongs to the game is the CONTENT of the list `opcoesdojogo` opens.
|
|
244
|
+
*/
|
|
245
|
+
const ENGINE_ITEMS = new Set(['options', 'opcoesdojogo', 'pmback', 'acessibilidade']);
|
|
246
|
+
/**
|
|
247
|
+
* Why a pause item is locked, in the child's words (ADR-0161). A key per item where the reason is particular to it —
|
|
248
|
+
* «Número de jogadores»: the GAME decides how many (ADR-0147) — and one general reason for the rest. Resolved at every
|
|
249
|
+
* refresh, so it follows the language of the moment the card opens.
|
|
257
250
|
*/
|
|
258
|
-
|
|
259
|
-
|
|
251
|
+
const OWN_REASONS = new Set(['ajuda', 'addplayer', 'opcoesdojogo', 'caa']);
|
|
252
|
+
function itemReason(t, act) {
|
|
253
|
+
return t(OWN_REASONS.has(act) ? `pause.motivo.${act}` : 'pause.motivo');
|
|
260
254
|
}
|
|
261
255
|
/**
|
|
262
|
-
* A
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
* ⚠️ Esta é a parte que um filtro item-a-item não apanha. Se todos os painéis de ajuste forem filtrados —
|
|
266
|
-
* um jogo que não monta nenhum —, o item `options` sobrevive (a engine acciona-o) e abre uma lista sem nada.
|
|
267
|
-
* A criança atravessa uma porta e fica presa num submenu vazio, cuja única saída é o `pmback` que também
|
|
268
|
-
* sumiu com ele.
|
|
256
|
+
* A door the ENGINE keeps locked whatever the game hands over. Today one: `caa`, the AAC menu, disabled until a
|
|
257
|
+
* pictogram set is licensed (ADR-0233 erratum). Its reason is only «menu disabled» — no licence explanation reaches the
|
|
258
|
+
* child — and a game's own `caa` action does not unlock it.
|
|
269
259
|
*/
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const viva = itensQueAccionam(raiz, acts);
|
|
273
|
-
return opcoesVivas.length > 0 ? viva : viva.filter((b) => b.act !== 'options');
|
|
260
|
+
function lockedByEngine(act) {
|
|
261
|
+
return act === 'caa' && aacMenuLocked();
|
|
274
262
|
}
|
|
275
|
-
/**
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
|
|
279
|
-
|
|
263
|
+
/**
|
|
264
|
+
* THE MENU ITEMS THIS GAME CAN ACTUALLY ACTIVATE. The others are not hidden: they stay on the card, LOCKED with their
|
|
265
|
+
* reason said (ADR-0161) — an item that does nothing when pressed, and says nothing, reads to a screen-reader user as an
|
|
266
|
+
* item that does not exist.
|
|
267
|
+
*/
|
|
268
|
+
function itemsThatAct(buttons, acts) {
|
|
269
|
+
return buttons.filter((b) => !lockedByEngine(b.act) && (ENGINE_ITEMS.has(b.act) || typeof acts[b.act] === 'function'));
|
|
280
270
|
}
|
|
281
|
-
/** One `.pm-btn`. Dynamic labels (`letra`/`nivel`) are rendered eagerly and carry NO `data-i18n`, so
|
|
282
|
-
* i18n.applyDom() cannot overwrite them. */
|
|
283
271
|
/**
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
* juntos:
|
|
288
|
-
*
|
|
289
|
-
* 1. FRONTEIRA. `level` vinha de `core/state.quizLevel` e `qlName` de uma tabela do jogo. Um menu de pausa
|
|
290
|
-
* da ENGINE montava o rótulo de uma atividade de alfabetização — conteúdo pedagógico, não mecânica.
|
|
291
|
-
* 2. IDIOMA. "Nível" é pt-BR CRU dentro de um módulo de engine. O gate do item 14 vigia o `main.js` e não
|
|
292
|
-
* alcança `ui/`, então esta linha atravessou a i18n inteira sem ser vista. Num build em inglês, o menu
|
|
293
|
-
* de pausa de uma criança dizia "📚 Nível 2 · …".
|
|
294
|
-
*
|
|
295
|
-
* Agora o jogo entrega a frase montada (`dynLabel`), e a engine só a coloca no botão. O jogo é quem sabe o
|
|
296
|
-
* que é um nível, quem sabe o nome dele e quem sabe em que idioma dizê-lo.
|
|
272
|
+
* THE ROOT LIST, with one more rule: `options` is a DOOR, and a door to an empty room acts no more than a dead button.
|
|
273
|
+
* An item-by-item filter misses this: the engine activates `options` itself, so it would pass and open a list with
|
|
274
|
+
* nothing in it.
|
|
297
275
|
*/
|
|
298
|
-
export function
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
276
|
+
export function rootThatActs(rootEl, options, acts,
|
|
277
|
+
// An OPTIONAL fourth argument defaulting to the EMPTY list: a game that declares nothing of its own is exactly that
|
|
278
|
+
// case, so the default is also the right answer.
|
|
279
|
+
fromGame = []) {
|
|
280
|
+
const howManyAct = (bs) => itemsThatAct(bs, acts).filter((b) => b.act !== 'pmback').length;
|
|
281
|
+
let alive = itemsThatAct(rootEl, acts);
|
|
282
|
+
if (howManyAct(options) === 0)
|
|
283
|
+
alive = alive.filter((b) => b.act !== 'options');
|
|
284
|
+
// The same rule for the game's own door (ADR-0146): a game with nothing of its own gets no live «game options».
|
|
285
|
+
// ADR-0182: a door whose room the ENGINE draws from the cartridge's rows is live through its action, with no list behind it
|
|
286
|
+
if (howManyAct(fromGame) === 0 && typeof acts.opcoesdojogo !== 'function')
|
|
287
|
+
alive = alive.filter((b) => b.act !== 'opcoesdojogo');
|
|
288
|
+
return alive;
|
|
304
289
|
}
|
|
305
290
|
/**
|
|
306
|
-
*
|
|
291
|
+
* The navigable items of a pause card — those of the VISIBLE list, and only those.
|
|
307
292
|
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
293
|
+
* One constant because THREE modules ask (navigation in `ui/menu-nav`, the first selection in `ui/shell` and the list
|
|
294
|
+
* switch here): if one of them forgot `:not([hidden])`, the ring would step into the invisible list and the child would
|
|
295
|
+
* hear items of a menu that is not on screen. `:not([hidden])` on the ITEM too: a hidden item must not be reached nor
|
|
296
|
+
* counted in «N de M» (a locked one is not hidden — it is reached, and says why).
|
|
312
297
|
*/
|
|
313
|
-
export const
|
|
314
|
-
/**
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
298
|
+
export const PM_VISIBLE_ITEMS = '.pause-menu:not([hidden]) .pm-btn:not([hidden])';
|
|
299
|
+
/**
|
|
300
|
+
* THE DOORS INSIDE THE CARD, and which list each opens. They do nothing to the game — they change which list is on
|
|
301
|
+
* screen — so they live here and not in the action table, which lives in `ui/shell` and does not know the card.
|
|
302
|
+
* `pmback` always returns to the ROOT, from either list. A table and not a chain of `if`s: a fourth list is one more
|
|
303
|
+
* row, not one more branch.
|
|
304
|
+
*/
|
|
305
|
+
const DOOR_TO_LIST = Object.freeze({ options: 'opcoes', opcoesdojogo: 'jogo', pmback: 'raiz' });
|
|
306
|
+
/**
|
|
307
|
+
* The other direction of the same table: the item of the root that opens each list. It is where «back» puts the cursor
|
|
308
|
+
* (ADR-0130 rule 1), and the name a list is announced by (`ui/where-the-child-is`). The root has no door.
|
|
309
|
+
*/
|
|
310
|
+
export const LIST_DOOR = Object.freeze({ raiz: null, opcoes: 'options', jogo: 'opcoesdojogo' });
|
|
319
311
|
/**
|
|
320
|
-
*
|
|
312
|
+
* Switches the visible list of ONE pause card, and puts the cursor on the FIRST item of the list that came in.
|
|
321
313
|
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
* só DOM — nada a re-renderizar, nada a injetar.
|
|
314
|
+
* Free-standing (not a method of `init`) on purpose: `ui/menu-nav` needs it for «no» to go back to the root, and it has no
|
|
315
|
+
* access to the button tables. Every list already exists in the markup, so the switch is DOM only.
|
|
325
316
|
*/
|
|
326
|
-
export function
|
|
317
|
+
export function showPauseOptions(sp, sub) {
|
|
327
318
|
sp.querySelectorAll('.pause-menu').forEach((m) => { m.hidden = m.dataset.sub !== sub; });
|
|
328
|
-
const
|
|
329
|
-
sp.querySelectorAll('.
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
return primeiro;
|
|
319
|
+
const first = sp.querySelector(PM_VISIBLE_ITEMS);
|
|
320
|
+
sp.querySelectorAll('.pi-sel').forEach((b) => b.classList.remove('pi-sel'));
|
|
321
|
+
markPauseItem(sp, first);
|
|
322
|
+
return first;
|
|
333
323
|
}
|
|
334
324
|
/**
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
|
|
340
|
-
|
|
325
|
+
* PUTS THE CARD'S CURSOR ON `el` — the one place every move of it goes through, whatever moved it: the arrows and the pad
|
|
326
|
+
* (`ui/menu-nav`), a finger, a held press, a door between lists, «back». The card selects by class, not by browser focus,
|
|
327
|
+
* so nothing scrolls it by itself: an item past the visible part of a long list would be marked where the child cannot see
|
|
328
|
+
* it. So the list is scrolled HERE, inside the card, until the item is wholly in view (issue #134, `keepInView`).
|
|
329
|
+
*/
|
|
330
|
+
export function markPauseItem(sp, el) {
|
|
331
|
+
if (!el)
|
|
332
|
+
return;
|
|
333
|
+
sp.querySelectorAll('.pm-sel').forEach((b) => b.classList.remove('pm-sel'));
|
|
334
|
+
el.classList.add('pm-sel');
|
|
335
|
+
keepInView(el);
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* «BACK» FROM A LIST OF THE CARD: the root comes in with the cursor on THE ITEM THAT OPENED the list being left, not on
|
|
339
|
+
* the root's first item (ADR-0130 rule 1). A child who went into «Opções», looked, and came back is where she was; landing
|
|
340
|
+
* on «Voltar» would send her walking the root again to find her place, and a child who cannot see the mark would not even
|
|
341
|
+
* know she had been moved.
|
|
341
342
|
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
343
|
+
* Opening the card is another question and keeps `showPauseOptions(sp, 'raiz')`: a card opens on item 1 (ADR-0158).
|
|
344
|
+
* A door that is gone or hidden leaves the cursor on the first item, which is all there is to return to.
|
|
344
345
|
*/
|
|
345
|
-
export function
|
|
346
|
-
|
|
347
|
-
|
|
346
|
+
export function backToRoot(sp) {
|
|
347
|
+
const left = sp.querySelector('.pause-menu:not([hidden])')?.dataset.sub;
|
|
348
|
+
const first = showPauseOptions(sp, 'raiz');
|
|
349
|
+
const door = left ? LIST_DOOR[left] : null;
|
|
350
|
+
const opener = door ? [...sp.querySelectorAll(PM_VISIBLE_ITEMS)].find((b) => b.dataset.act === door) : undefined;
|
|
351
|
+
if (!opener || opener === first)
|
|
352
|
+
return first;
|
|
353
|
+
markPauseItem(sp, opener);
|
|
354
|
+
return opener;
|
|
348
355
|
}
|
|
349
|
-
export function
|
|
350
|
-
if (
|
|
356
|
+
export function barAction(k, hasStart) {
|
|
357
|
+
if (hasStart || k.no)
|
|
351
358
|
return 'sair';
|
|
352
359
|
if (k.yes)
|
|
353
360
|
return 'ativar';
|
|
@@ -355,130 +362,98 @@ export function acaoNaBarra(k, temStart) {
|
|
|
355
362
|
return 'andar';
|
|
356
363
|
return 'nada';
|
|
357
364
|
}
|
|
358
|
-
/** The full innerHTML of a `.screen-pause`. Pure — every input is a parameter. */
|
|
359
|
-
export function screenPauseMarkup(o) {
|
|
360
|
-
// O NOME ACESSÍVEL DO DIÁLOGO passa pelo dicionário. Era texto cru, e MEDIDO num jogo em inglês o efeito
|
|
361
|
-
// era este: o título visível dizia "Paused" e o nome do diálogo, "Menu de pausa do jogador 1". Quem enxerga
|
|
362
|
-
// lia em inglês; quem escuta recebia o menu anunciado em português — a mesma assimetria do item 4 do
|
|
363
|
-
// ADR-0044, um nível acima. E `aria-label`, não `aria-labelledby`: o `<h2>` é rótulo VISUAL, e é por isso
|
|
364
|
-
// que escondê-lo num quadro apertado não tira o nome do diálogo de quem escuta.
|
|
365
|
-
return '<div class="pause-card" role="dialog" aria-modal="true" aria-label="' + t('pause.cardAria', { n: o.player + 1 }) + '">' +
|
|
366
|
-
'<h2><span data-i18n="pause.title">' + o.t('pause.title') + '</span>' + (o.numPlayers > 1 ? ' · Jogador ' + (o.player + 1) : '') + '</h2>' +
|
|
367
|
-
pauseMenuHtml(o.pmButtons, 'raiz', o.dynLabel, o.t) +
|
|
368
|
-
pauseMenuHtml(o.optionsButtons, 'opcoes', o.dynLabel, o.t) +
|
|
369
|
-
'<p class="pause-legend"></p></div>';
|
|
370
|
-
}
|
|
371
365
|
// ---------------------------------------------------------------------------------------------
|
|
372
366
|
// DOM-facing shell
|
|
373
367
|
// ---------------------------------------------------------------------------------------------
|
|
374
368
|
export function initPauseIcons(ctx) {
|
|
375
|
-
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
// este é um controlo de menu que vive na barra rápida.
|
|
369
|
+
const { t } = ctx.translator;
|
|
370
|
+
// THE CALM LEVEL PERSISTS (issue #61; ADR-0028: every menu setting persists). The child who uses the SILENT mode is the
|
|
371
|
+
// one for whom unexpected noise costs most, and a setting that is forgotten is a daily chore, not a setting.
|
|
379
372
|
//
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
|
|
383
|
-
//
|
|
384
|
-
// ⚠️ LÊ NO INIT, NUNCA NO IMPORT: a regra vale para todo o projeto e aqui tem custo concreto — um teste que
|
|
385
|
-
// importasse este módulo passaria a depender do `localStorage` do ambiente, e um nível herdado de outro
|
|
386
|
-
// caso é uma falha que aparece longe da causa.
|
|
387
|
-
let calmMode = lerNivelTea();
|
|
373
|
+
// READ IN INIT, NEVER ON IMPORT: a test importing this module would otherwise depend on the environment's storage, and a
|
|
374
|
+
// level inherited from another case is a failure that shows up far from its cause.
|
|
375
|
+
let calmMode = readTeaLevel(ctx.store);
|
|
388
376
|
const P = () => ctx.getPlayers();
|
|
389
377
|
/*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
*
|
|
393
|
-
* O `store` vem do import directo deste módulo e não do `ctx`, porque é assim que este ficheiro já persiste
|
|
394
|
-
* o resto: uma chave injectada é um campo que um consumidor pode omitir, e omiti-la faria escrever num nome
|
|
395
|
-
* torto.
|
|
378
|
+
* THE MOVEMENT LATCH HAS AN ENGINE DEFAULT (ADR-0106 §4): a host that injects one still rules; a host that does not is
|
|
379
|
+
* no longer left without it. The `store` is the page's, from the ctx (ADR-0232).
|
|
396
380
|
*/
|
|
397
381
|
const setToggleMove = ctx.setToggleMove
|
|
398
|
-
?? ((i, on) =>
|
|
399
|
-
players: P(), store, srSay: ctx.srSay, getNumPlayers: ctx.getNumPlayers,
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
|
|
382
|
+
?? ((i, on) => setMoveLatch({
|
|
383
|
+
t, players: P(), store: ctx.store, srSay: ctx.srSay, getNumPlayers: ctx.getNumPlayers,
|
|
384
|
+
// Passed through, not resolved here: the icon and the panel must write the SAME thing, and resolving the
|
|
385
|
+
// device in one of them only is how two surfaces of the same engine come to disagree.
|
|
386
|
+
transportInUse: ctx.transportInUse,
|
|
403
387
|
}, i, on));
|
|
404
388
|
/*
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
* `sr.icon.blindOn`/`Off` por si, logo abaixo.
|
|
389
|
+
* BLIND MODE LIKEWISE: the default is exactly what `core/state` decided a setter does — write, persist, notify — and
|
|
390
|
+
* nothing more. Game effects are REACTIONS, subscribed with `on('blindMode', …)`. The announcement is not lost for a host
|
|
391
|
+
* that injects nothing: this icon says `sr.icon.blindOn`/`Off` itself, below.
|
|
409
392
|
*/
|
|
410
|
-
const
|
|
411
|
-
/** O documento onde se constroi. Resolvido a cada uso, e por globalThis — em node o identificador
|
|
412
|
-
* document nem existe, e um ?? sobre ele lançaria ReferenceError em vez de cair no padrão. */
|
|
413
|
-
const docDaMontagem = () => ctx.doc ?? globalThis.document;
|
|
393
|
+
const writeBlindMode = ctx.setBlindMode ?? ((on) => { ctx.settings.setBlindModeValue(on); });
|
|
414
394
|
/*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
* 📏 Medido em 2026-09-08, e é o que separa este caso dos outros seis campos «acidentais»: eles eram estado
|
|
418
|
-
* que um cartucho calhou de guardar, e a engine pôde reclamá-los. Estes precisam de um
|
|
419
|
-
* `render/viz-setters`, cujo contexto pede **34 campos** do grafo de render de UM jogo — `parallaxLayers`,
|
|
420
|
-
* `decoSprites`, `getPowerups`, `rebuildCoins`, `worldSprite`. O `createGame` não monta isso, e um quiz não
|
|
421
|
-
* tem nada disso para montar. A engine não pode dar um padrão aqui; o que ela pode é não FINGIR.
|
|
395
|
+
* CONTRAST AND COLOUR ONLY APPEAR IF SOMEONE CAN WRITE THEM (ADR-0106 §5): the theme and the correction are written by
|
|
396
|
+
* the host (`setPlayerTheme`/`setPlayerCorrection`), and without a writer the engine does not pretend.
|
|
422
397
|
*
|
|
423
|
-
*
|
|
424
|
-
*
|
|
398
|
+
* Decided once, at start, not per bar: a host's set of writers does not change mid-game, and recomputing per screen
|
|
399
|
+
* would let the screens disagree.
|
|
425
400
|
*/
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
|
|
432
|
-
|
|
401
|
+
const gameIcons = iconsThatAct({
|
|
402
|
+
theme: Boolean(ctx.setPlayerTheme),
|
|
403
|
+
correction: Boolean(ctx.setPlayerCorrection),
|
|
404
|
+
// No `Boolean(...)`: the two above ask an optional field «is there a writer?»; this is an ANSWER the game gave, and
|
|
405
|
+
// wrapping it would turn a badly wired ctx's `undefined` into `false` — hiding the control silently.
|
|
406
|
+
holdsKeys: ctx.holdsKeys,
|
|
407
|
+
// the same question the scan asks (ADR-0218): how many positions this game would give it to offer
|
|
408
|
+
declaredPositions: ctx.declaredPositions,
|
|
409
|
+
typography: Boolean(ctx.cycleTypography),
|
|
410
|
+
// mounted when the root can answer the clock question; shown or hidden per cartridge in `reflectIconBtn` (ADR-0142)
|
|
411
|
+
clock: () => Boolean(ctx.clock),
|
|
412
|
+
camera: Boolean(ctx.camera),
|
|
413
|
+
microphone: Boolean(ctx.microphone),
|
|
414
|
+
menus: Boolean(ctx.openMenus),
|
|
433
415
|
});
|
|
434
416
|
/*
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
*
|
|
438
|
-
*
|
|
439
|
-
* 📌 O `getPauseActs` fica FUNÇÃO e não valor, porque a laziness dele é a razão de ele existir assim: no
|
|
440
|
-
* cartucho a tabela é um `const` declarado ~1200 linhas abaixo, e lê-la aqui cairia na zona morta temporal.
|
|
417
|
+
* RESOLVED ONCE, at start, as in `settings-motion`: `rm` is mutated in place and shared by REFERENCE with whoever draws
|
|
418
|
+
* the scene, and resolving it on each use would make a new object per call — the switch would stop reaching the drawing,
|
|
419
|
+
* with no error at all.
|
|
441
420
|
*/
|
|
442
|
-
const rm = ctx.rm ??
|
|
443
|
-
const rmKeys = ctx.rmKeys ??
|
|
444
|
-
const rmChar = ctx.rmChar ??
|
|
445
|
-
const saveRM = ctx.saveRM ?? (() =>
|
|
421
|
+
const rm = ctx.rm ?? readStoredScene(ctx.store, defaultReducedMotion(ctx.matchMedia));
|
|
422
|
+
const rmKeys = ctx.rmKeys ?? SCENE_KEYS;
|
|
423
|
+
const rmChar = ctx.rmChar ?? CHARACTER_ANIMATIONS;
|
|
424
|
+
const saveRM = ctx.saveRM ?? (() => storeScene(ctx.store, rm));
|
|
446
425
|
/*
|
|
447
|
-
*
|
|
448
|
-
*
|
|
449
|
-
*
|
|
450
|
-
* precisamente por a tabela chegar tarde. Um padrão não pode custar a ligação tardia que o campo tem.
|
|
451
|
-
*
|
|
452
|
-
* O `rm` é o contrário e por isso fica congelado: ali o que importa é a IDENTIDADE do objecto, partilhada
|
|
453
|
-
* por referência com quem desenha a cena.
|
|
426
|
+
* THESE THREE ARE READ ON EACH CALL, not resolved once like `rm` above: a host may replace the action table after `init`
|
|
427
|
+
* (it can arrive late), and a default must not cost that late binding. `rm` is the opposite — what matters there is the
|
|
428
|
+
* object's IDENTITY, shared by reference with whoever draws the scene.
|
|
454
429
|
*
|
|
455
|
-
*
|
|
456
|
-
*
|
|
430
|
+
* The type annotation is needed: without it the default `() => ({})` infers `{}`, which does not accept string indexing,
|
|
431
|
+
* and the compiler would refuse `acts[act]` — the whole dispatch.
|
|
457
432
|
*/
|
|
458
433
|
const dynLabel = (b) => (ctx.dynLabel ? ctx.dynLabel(b) : null);
|
|
459
434
|
const getPauseActs = () => (ctx.getPauseActs ? ctx.getPauseActs() : {});
|
|
460
435
|
const setPauseActor = (i) => { if (ctx.setPauseActor)
|
|
461
436
|
ctx.setPauseActor(i); };
|
|
462
437
|
function hasPrivateOutput(i) { return hasPrivateOutputIn(P(), ctx.getNumPlayers(), i); }
|
|
463
|
-
/** A recusa da alternância para este jogador agora, ou `null`. Recalculada: o aparelho em uso muda. */
|
|
464
|
-
function recusaAgora(i) { return ctx.transporteEmUso ? recusaDaAlternancia(ctx.transporteEmUso(i)) : null; }
|
|
465
438
|
function iconState(i) {
|
|
466
439
|
const p = P()[i] || {};
|
|
467
440
|
const cat = ctx.getAudioCat();
|
|
468
441
|
return {
|
|
469
|
-
|
|
442
|
+
blindMode: ctx.getBlindMode(),
|
|
470
443
|
ttsOn: !!(cat && cat.tts && cat.tts.on),
|
|
471
444
|
librasOn: ctx.isLibrasOn(),
|
|
472
445
|
calmMode,
|
|
473
446
|
toggleMove: !!p.toggleMove,
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
//
|
|
477
|
-
//
|
|
478
|
-
|
|
479
|
-
|
|
447
|
+
switchScan: ctx.settings.switchScan,
|
|
448
|
+
voice: ctx.settings.voiceControl,
|
|
449
|
+
// The NAMED default, not an empty value that only happened to match nothing: ADR-0029's changed-mark reads the
|
|
450
|
+
// default and nothing else.
|
|
451
|
+
visual: p.visual ?? DEFAULT_VISUAL,
|
|
452
|
+
speed: ctx.settings.gameSpeed,
|
|
453
|
+
camera: ctx.settings.cameraControl,
|
|
454
|
+
locale: ctx.translator.locale(),
|
|
480
455
|
privateOutput: hasPrivateOutput(i),
|
|
481
|
-
|
|
456
|
+
noVoice: !!ctx.noVoice?.(),
|
|
482
457
|
};
|
|
483
458
|
}
|
|
484
459
|
// --- TEA ---------------------------------------------------------------------------------
|
|
@@ -505,14 +480,15 @@ export function initPauseIcons(ctx) {
|
|
|
505
480
|
}
|
|
506
481
|
// --- icon actions (the dispatcher, as a table) ---------------------------------------------
|
|
507
482
|
const ICON_ACTS = {
|
|
483
|
+
menu: (i) => { ctx.openMenus?.(i); },
|
|
508
484
|
blind: () => {
|
|
509
|
-
|
|
510
|
-
ctx.srSay(t(ctx.
|
|
485
|
+
writeBlindMode(!ctx.getBlindMode());
|
|
486
|
+
ctx.srSay(t(ctx.getBlindMode() ? 'sr.icon.blindOn' : 'sr.icon.blindOff'));
|
|
511
487
|
},
|
|
512
488
|
tts: () => {
|
|
513
489
|
const cat = ctx.getAudioCat();
|
|
514
490
|
if (!cat || !cat.tts)
|
|
515
|
-
return; //
|
|
491
|
+
return; // the same guard `iconLabel` and `applyCalm` have
|
|
516
492
|
cat.tts.on = !cat.tts.on;
|
|
517
493
|
ctx.setCatGain('tts');
|
|
518
494
|
if (ctx.reflectTtsPanelEnabled)
|
|
@@ -525,66 +501,127 @@ export function initPauseIcons(ctx) {
|
|
|
525
501
|
},
|
|
526
502
|
tea: () => {
|
|
527
503
|
calmMode = nextCalmMode(calmMode);
|
|
528
|
-
store.set(
|
|
504
|
+
ctx.store.set(KEYS.tea, calmMode); // ADR-0028: every menu setting persists. See the note at the `let` above.
|
|
529
505
|
applyCalm();
|
|
530
506
|
ctx.srSay(t('sr.icon.tea', { v: t(CALM_NAMES[calmMode]) }));
|
|
531
507
|
},
|
|
508
|
+
/*
|
|
509
|
+
* ☝️ CYCLES THREE READINGS OF A PRESS (ADR-0218): standard · no holding needed · one button only.
|
|
510
|
+
*
|
|
511
|
+
* 📌 ENTERING THE SCAN LEAVES THE LATCH WHERE IT IS, and leaving it turns the latch off. The scan wins in `inputModeOf`, so
|
|
512
|
+
* the position shown is never ambiguous; and because the latch writer announces by itself, it is called ONLY where it
|
|
513
|
+
* changes something — otherwise the child would hear «no holding needed, off» when what ended was the scan.
|
|
514
|
+
*/
|
|
532
515
|
altmove: (i) => {
|
|
533
516
|
// verbatim: `players[i].toggleMove` with no `||{}` guard (unlike contrast/cvd below).
|
|
534
|
-
|
|
517
|
+
const latched = !!P()[i].toggleMove;
|
|
518
|
+
const next = nextInputMode(inputModeOf({ toggleMove: latched, switchScan: ctx.settings.switchScan }), ctx.holdsKeys());
|
|
519
|
+
ctx.settings.setSwitchScanValue(next === 'scan');
|
|
520
|
+
// 📌 ENTERING THE SCAN LEAVES THE LATCH WHERE SHE PUT IT — the scan wins in `inputModeOf`, so the position shown is never
|
|
521
|
+
// ambiguous and coming back out returns her to the choice she had made. Leaving it writes the position she walked to.
|
|
522
|
+
// ⚠️ And the latch writer ANNOUNCES BY ITSELF, so it is called only where it changes something: otherwise the child would
|
|
523
|
+
// hear «não precisa segurar, desligado» when what ended was the scan.
|
|
524
|
+
// 📌 On every device, eyes and voice included (ADR-0249): the write goes under the transport in use, and that stored
|
|
525
|
+
// choice is what wins over the game's default the next time that transport presses.
|
|
526
|
+
if (next !== 'scan' && latched !== (next === 'sticky')) {
|
|
527
|
+
setToggleMove(i, next === 'sticky');
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
ctx.srSay(t('sr.icon.inputMode', { v: t(INPUT_MODE_NAME[next]) }));
|
|
535
531
|
},
|
|
536
|
-
//
|
|
537
|
-
//
|
|
538
|
-
//
|
|
539
|
-
//
|
|
540
|
-
// ⚠️ AS DUAS GUARDAS NÃO SÃO CINTO E SUSPENSÓRIOS. Sem escritor, o ícone nem sequer é montado
|
|
541
|
-
// (`iconesDoJogo`), então este ramo não deveria ser alcançável pela barra — mas `iconAct` é EXPORTADO e
|
|
542
|
-
// qualquer consumidor pode chamá-lo por chave. Sem a guarda, essa chamada rebentaria; com ela, não faz
|
|
543
|
-
// nada e não anuncia — que é o mesmo que dizer a verdade: este jogo não tem por onde.
|
|
532
|
+
// Each of the two icons writes its OWN axis (#104), so changing one leaves the other where it was.
|
|
533
|
+
// ⚠️ THE TWO GUARDS ARE NOT BELT AND BRACES. Without a writer the icon is not even mounted (`gameIcons`), so the bar
|
|
534
|
+
// cannot reach this branch — but `iconAct` is EXPORTED and any host can call it by key. Without the guard that call
|
|
535
|
+
// would throw; with it, nothing happens and nothing is announced — which is the truth: this game has no way to do it.
|
|
544
536
|
contrast: (i) => {
|
|
545
|
-
if (!ctx.
|
|
537
|
+
if (!ctx.setPlayerTheme)
|
|
546
538
|
return;
|
|
547
|
-
const v =
|
|
548
|
-
ctx.
|
|
549
|
-
ctx.srSay(t('sr.visual.contrast', { v: t(
|
|
539
|
+
const v = nextTheme((P()[i] || {}).visual ?? DEFAULT_VISUAL);
|
|
540
|
+
ctx.setPlayerTheme(i, v.tema);
|
|
541
|
+
ctx.srSay(t('sr.visual.contrast', { v: t(SHORT_THEME[v.tema]) }));
|
|
542
|
+
},
|
|
543
|
+
/*
|
|
544
|
+
* THE TYPOGRAPHY CYCLE (ADR-0149 §1): one press changes the letter CASE and the FACE at once. The guard is the one
|
|
545
|
+
* written at `contrast` above, for the same reason.
|
|
546
|
+
*
|
|
547
|
+
* It ANNOUNCES THE FACE, NOT THE POSITION: «position 3 of 5» tells nobody anything; the face's name is what the child
|
|
548
|
+
* recognises — the same reason ADR-0074 forbids `action2` from reaching a person.
|
|
549
|
+
*/
|
|
550
|
+
tipografia: () => {
|
|
551
|
+
if (!ctx.cycleTypography)
|
|
552
|
+
return;
|
|
553
|
+
const face = ctx.cycleTypography();
|
|
554
|
+
if (face)
|
|
555
|
+
ctx.srSay(t('sr.typo.font', { fam: face }));
|
|
556
|
+
},
|
|
557
|
+
/*
|
|
558
|
+
* PLAYING THROUGH THE WEBCAM (ADR-0215) and BY SPEAKING (ADR-0189, issue #184): each writes the child's answer, one stored key.
|
|
559
|
+
*
|
|
560
|
+
* 🔴 A POSITION THAT STARTS A CONTROL IS ANNOUNCED BY THE CONTROL, NOT HERE. At this line nothing has started: the control
|
|
561
|
+
* starts after the write, asynchronously, and answers either way — «Pronto: já pode jogar…» when it is really on, or the
|
|
562
|
+
* reason assertively when it cannot start (and the answer goes back to off). Saying the position here told the child
|
|
563
|
+
* «Comando de voz: ligado» over a button that read «desligado», on every press where the microphone or the runtime failed.
|
|
564
|
+
* Off is true the moment it is written, so off is still said here.
|
|
565
|
+
*/
|
|
566
|
+
camera: () => {
|
|
567
|
+
const v = nextCameraControl(ctx.settings.cameraControl);
|
|
568
|
+
ctx.settings.setCameraControlValue(v);
|
|
569
|
+
if (v === 'off')
|
|
570
|
+
ctx.srSay(t('sr.icon.camera', { v: t('state.off') }));
|
|
571
|
+
},
|
|
572
|
+
voice: () => {
|
|
573
|
+
const v = !ctx.settings.voiceControl;
|
|
574
|
+
ctx.settings.setVoiceControlValue(v);
|
|
575
|
+
if (!v)
|
|
576
|
+
ctx.srSay(t('sr.icon.voice', { v: t('state.off') }));
|
|
577
|
+
},
|
|
578
|
+
// THE LANGUAGE (the Dev, 2026-09-16): the next flag; `setLocale` stores it and every surface redraws on `i18n:change`. Said in the NEW
|
|
579
|
+
// language, once it has loaded.
|
|
580
|
+
idioma: () => {
|
|
581
|
+
const v = nextLocale(ctx.translator.locale());
|
|
582
|
+
void ctx.translator.setLocale(v).then(() => ctx.srSay(t('sr.icon.idioma', { v: LANGUAGE_NAME[v] })));
|
|
583
|
+
},
|
|
584
|
+
// THE GAME SPEED (ADR-0180): one step down, wrapping at 50%; stored, and felt on the next frame of `startLoop`.
|
|
585
|
+
velocidade: () => {
|
|
586
|
+
const v = nextGameSpeed(ctx.settings.gameSpeed);
|
|
587
|
+
ctx.settings.setGameSpeedValue(v);
|
|
588
|
+
ctx.srSay(t('sr.icon.velocidade', { pct: Math.round(v * 100) }));
|
|
550
589
|
},
|
|
551
590
|
cvd: (i) => {
|
|
552
|
-
if (!ctx.
|
|
591
|
+
if (!ctx.setPlayerCorrection)
|
|
553
592
|
return;
|
|
554
|
-
const v =
|
|
555
|
-
ctx.
|
|
556
|
-
ctx.srSay(t('sr.icon.cvd', { v: t(
|
|
593
|
+
const v = nextCorrection((P()[i] || {}).visual ?? DEFAULT_VISUAL);
|
|
594
|
+
ctx.setPlayerCorrection(i, v.correcao);
|
|
595
|
+
ctx.srSay(t('sr.icon.cvd', { v: t(SHORT_CORRECTION[v.correcao]) }));
|
|
557
596
|
},
|
|
558
597
|
};
|
|
559
598
|
function iconAct(k, i) {
|
|
560
|
-
|
|
561
|
-
if (
|
|
562
|
-
ctx.srAlert(t('
|
|
599
|
+
// locked like the panel's row (ADR-0185): the same reason, said, and nothing turned on
|
|
600
|
+
if (k === 'tts' && ctx.noVoice?.()) {
|
|
601
|
+
ctx.srAlert(t('audio.semVoz'));
|
|
563
602
|
return;
|
|
564
603
|
}
|
|
565
604
|
if ((k === 'blind' || k === 'tts') && !hasPrivateOutput(i)) {
|
|
566
605
|
ctx.srAlert(t('sr.icon.needsPrivateOutput'));
|
|
567
606
|
return;
|
|
568
607
|
}
|
|
569
|
-
//
|
|
570
|
-
// ignorá-lo é a outra metade do que o ADR-0076 proíbe, e aqui há um agravante — este ícone e o
|
|
571
|
-
// `#opt-altmove` escrevem o MESMO valor, logo um a aceitar enquanto o outro recusa daria à criança dois
|
|
572
|
-
// botões que discordam sobre o mesmo ajuste.
|
|
573
|
-
if (k === 'altmove') {
|
|
574
|
-
const recusa = recusaAgora(i);
|
|
575
|
-
if (recusa) {
|
|
576
|
-
ctx.srAlert(t(recusa.chave));
|
|
577
|
-
return;
|
|
578
|
-
}
|
|
579
|
-
}
|
|
608
|
+
// No refusal for `altmove` (ADR-0249): no device locks the latch, and what a game cannot hold the CYCLE leaves out.
|
|
580
609
|
const act = ICON_ACTS[k];
|
|
581
610
|
if (act)
|
|
582
611
|
act(i);
|
|
583
612
|
}
|
|
584
613
|
// --- reflection --------------------------------------------------------------------------
|
|
585
|
-
function iconLabel(k, i) { return computeIconLabel(k, iconState(i)); }
|
|
614
|
+
function iconLabel(k, i) { return computeIconLabel(t, k, iconState(i)); }
|
|
586
615
|
function reflectIconBtn(b, i) {
|
|
587
616
|
const k = b.dataset.pi || '';
|
|
617
|
+
// the hourglass follows the CURRENT cartridge's clock (ADR-0180): a turn game mounted later hides it, a clock game shows it
|
|
618
|
+
if (k === 'velocidade')
|
|
619
|
+
b.hidden = !ctx.clock?.();
|
|
620
|
+
if (k === 'idioma') {
|
|
621
|
+
const flag = flagOf(ctx.translator.locale());
|
|
622
|
+
if (b.innerHTML !== flag)
|
|
623
|
+
b.innerHTML = flag;
|
|
624
|
+
}
|
|
588
625
|
const st = iconState(i);
|
|
589
626
|
const v = computeIconVisual(k, st);
|
|
590
627
|
b.classList.remove(...ICON_STATE_CLASSES);
|
|
@@ -595,26 +632,44 @@ export function initPauseIcons(ctx) {
|
|
|
595
632
|
b.classList.toggle('pi-on', v.on);
|
|
596
633
|
b.classList.toggle('pi-dis', v.dis);
|
|
597
634
|
/*
|
|
598
|
-
*
|
|
599
|
-
*
|
|
600
|
-
*
|
|
601
|
-
*
|
|
602
|
-
* ⚠️ E `aria-disabled` e NÃO `disabled`: o segundo tira o botão da ordem de tabulação, e quem navega
|
|
603
|
-
* por teclado deixaria de o alcançar — logo deixaria de poder ouvir POR QUE ele não responde. É a
|
|
604
|
-
* mesma escolha que o `#opt-altmove` faz no painel, pela mesma razão.
|
|
635
|
+
* `pi-dis` is a CSS class (issue #128): a child who sees gets a greyed icon, a child on a screen reader gets nothing — the
|
|
636
|
+
* button announces itself actionable and does not respond. `aria-disabled` mirrors the same fact for whoever listens.
|
|
637
|
+
* `aria-disabled` and NOT `disabled`: the latter takes the button out of the tab order, and a keyboard user could no
|
|
638
|
+
* longer reach it — nor hear WHY it does not respond.
|
|
605
639
|
*/
|
|
606
640
|
if (v.dis)
|
|
607
641
|
b.setAttribute('aria-disabled', 'true');
|
|
608
642
|
else
|
|
609
643
|
b.removeAttribute('aria-disabled');
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
//
|
|
616
|
-
//
|
|
617
|
-
|
|
644
|
+
// the ☰ opens something and holds no state: a pressed/unpressed button would announce a toggle
|
|
645
|
+
if (k === 'menu')
|
|
646
|
+
b.removeAttribute('aria-pressed');
|
|
647
|
+
else
|
|
648
|
+
b.setAttribute('aria-pressed', String(v.active));
|
|
649
|
+
// ⚠️ EVERY icon is relabelled here, including the ones that carry no state — because the markup and this reflection do not
|
|
650
|
+
// run in the same language. `initI18n` loads en/es asynchronously, so the markup is born in pt and only the reflection
|
|
651
|
+
// corrects it. 📌 Measured in a browser back when a guard skipped some of them: five icons in English and three still in
|
|
652
|
+
// Portuguese, on the same bar.
|
|
653
|
+
b.setAttribute('aria-label', computeIconLabel(t, k, st));
|
|
654
|
+
refreshCursorCaption(b);
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* THE NAME UNDER THE ROW FOLLOWS THE LABEL OF THE ICON UNDER THE CURSOR (ADR-0159 rule 10, ADR-0167). The caption is the
|
|
658
|
+
* visible half of what the cursor says, and the reflection is how a state changed ELSEWHERE reaches the icon — the 🦯, 📷
|
|
659
|
+
* and 👄 through the root's `stateOn`, the 🗣 through the mixer's `onCatChange` (ADR-0247). Written only on a move or a press,
|
|
660
|
+
* the caption kept the state the child had left while the label spoke the new one.
|
|
661
|
+
*
|
|
662
|
+
* Only the CURSOR's icon (`.pi-sel`, which lives only on the bars `getA11yBars` hands over): another icon's name under the
|
|
663
|
+
* row would move the cursor's only visible mark. And only when the text CHANGED: the per-screen bar's caption is a live
|
|
664
|
+
* region, and the same words rewritten are said again.
|
|
665
|
+
*/
|
|
666
|
+
function refreshCursorCaption(b) {
|
|
667
|
+
if (!b.classList.contains('pi-sel'))
|
|
668
|
+
return;
|
|
669
|
+
const cap = ctx.getA11yBars().find((bar) => bar.contains(b))?.querySelector('.pause-icons-cap');
|
|
670
|
+
const text = accessibleLabel(b); // name and state only: «N de M» is spoken, never written (ADR-0167)
|
|
671
|
+
if (cap && cap.textContent !== text)
|
|
672
|
+
cap.textContent = text;
|
|
618
673
|
}
|
|
619
674
|
function reflectIconsIn(root, i) {
|
|
620
675
|
if (!root)
|
|
@@ -622,111 +677,137 @@ export function initPauseIcons(ctx) {
|
|
|
622
677
|
root.querySelectorAll('.pi-btn').forEach((b) => reflectIconBtn(b, i));
|
|
623
678
|
}
|
|
624
679
|
/**
|
|
625
|
-
*
|
|
626
|
-
*
|
|
680
|
+
* THE CARDS THIS INSTANCE BUILT. Private: whoever built them keeps them, which asks nothing of any host — a ctx field
|
|
681
|
+
* would be one more thing every game had to remember to pass.
|
|
627
682
|
*/
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
*
|
|
631
|
-
*
|
|
632
|
-
*
|
|
633
|
-
*
|
|
634
|
-
* cada um dos 300 jogos teria de se lembrar de passar.
|
|
683
|
+
const cards = [];
|
|
684
|
+
/*
|
|
685
|
+
* `refreshPauseItems` LOCKS THE ITEMS THIS GAME CANNOT ACTIVATE — recomputed, not decided at start. The action table can
|
|
686
|
+
* arrive after the card is built (`getPauseActs` is asked when needed), so deciding at build time would lose for good
|
|
687
|
+
* every item whose action exists only later. The host calls `reflectPauseIcons()` when the pause opens and when a
|
|
688
|
+
* cartridge is mounted, so the card is right at the moment the child sees it.
|
|
635
689
|
*/
|
|
636
|
-
const cartoes = [];
|
|
637
690
|
/**
|
|
638
|
-
*
|
|
691
|
+
* THE CARD'S NAME — what is SEEN and what is HEARD — repainted in the current language.
|
|
639
692
|
*
|
|
640
|
-
*
|
|
641
|
-
*
|
|
642
|
-
*
|
|
643
|
-
*
|
|
693
|
+
* The markup is born before an asynchronously loaded dictionary arrives, and an `aria-label` set once cannot be corrected
|
|
694
|
+
* later by `applyDom`, which only reaches `[data-i18n]` and `[data-i18n-aria]`. Nor would `data-i18n-aria` do: `applyDom`
|
|
695
|
+
* calls `t(k)` without parameters, and this key carries the seat number — the child would hear «Player {n} pause menu»,
|
|
696
|
+
* braces included. So it is repainted on each `reflectPauseIcons()`, i.e. when the pause opens.
|
|
644
697
|
*
|
|
645
|
-
*
|
|
646
|
-
*
|
|
647
|
-
* item que não acciona —, e agora também vê os que passaram a accionar.
|
|
698
|
+
* The one who lost was the one who LISTENS (ADR-0044 item 4): to a sighted child the card was entirely in the right
|
|
699
|
+
* language, and nothing looked wrong.
|
|
648
700
|
*/
|
|
649
|
-
function
|
|
701
|
+
function renameCard(cardEl, i) {
|
|
702
|
+
const card = cardEl.querySelector('.pause-card');
|
|
703
|
+
if (card)
|
|
704
|
+
card.setAttribute('aria-label', t('pause.cardAria', { n: i + 1 }));
|
|
705
|
+
const seat = cardEl.querySelector('h2 .pause-seat');
|
|
706
|
+
if (seat)
|
|
707
|
+
seat.textContent = ctx.getNumPlayers() > 1 ? t('pause.cardSeat', { n: i + 1 }) : '';
|
|
708
|
+
}
|
|
709
|
+
function refreshPauseItems() {
|
|
650
710
|
const acts = getPauseActs();
|
|
651
|
-
const
|
|
652
|
-
const
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
...
|
|
711
|
+
const rootEl = ctx.pmButtons ?? PM_BTNS;
|
|
712
|
+
const options = ctx.optionsButtons ?? PM_OPTIONS_BTNS;
|
|
713
|
+
const fromGame = ctx.gameButtons ?? PM_GAME_BTNS;
|
|
714
|
+
const actingItems = new Set([
|
|
715
|
+
...rootThatActs(rootEl, options, acts, fromGame).map((b) => b.act),
|
|
716
|
+
...itemsThatAct(options, acts).map((b) => b.act),
|
|
717
|
+
...itemsThatAct(fromGame, acts).map((b) => b.act),
|
|
656
718
|
]);
|
|
657
|
-
//
|
|
658
|
-
//
|
|
659
|
-
for (
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
719
|
+
// An explicit index, not `filter(Boolean)`: the cards are indexed by PLAYER, building only screen 2 leaves a hole at
|
|
720
|
+
// index 0, and closing the hole would renumber the seats in the card's name. The `undefined` guard is written by hand.
|
|
721
|
+
for (let i = 0; i < cards.length; i++) {
|
|
722
|
+
const cardEl = cards[i];
|
|
723
|
+
if (!cardEl)
|
|
724
|
+
continue;
|
|
725
|
+
renameCard(cardEl, i);
|
|
726
|
+
for (const btn of cardEl.querySelectorAll('.pm-btn')) {
|
|
727
|
+
// LOCKED WITH ITS REASON, NOT HIDDEN (ADR-0161, superseding ADR-0106 §5 here): a card that changed shape from game
|
|
728
|
+
// to game hid from the child that an option exists. The cursor still stops on the item and it still counts;
|
|
729
|
+
// reaching or pressing it says why. And not `remove()`: the table can grow later.
|
|
730
|
+
const act = btn.dataset.act ?? '';
|
|
731
|
+
if (actingItems.has(act)) {
|
|
732
|
+
btn.removeAttribute('aria-disabled');
|
|
733
|
+
delete btn.dataset.motivo;
|
|
734
|
+
}
|
|
735
|
+
else {
|
|
736
|
+
btn.setAttribute('aria-disabled', 'true');
|
|
737
|
+
btn.dataset.motivo = itemReason(t, act);
|
|
738
|
+
}
|
|
664
739
|
}
|
|
665
740
|
}
|
|
666
741
|
}
|
|
667
742
|
function reflectPauseIcons() {
|
|
668
743
|
ctx.getA11yBars().forEach((bar, i) => reflectIconsIn(bar, i));
|
|
669
|
-
|
|
744
|
+
refreshPauseItems();
|
|
670
745
|
}
|
|
671
746
|
// --- the pause screen ----------------------------------------------------------------------
|
|
672
747
|
/**
|
|
673
|
-
*
|
|
674
|
-
*
|
|
675
|
-
*
|
|
676
|
-
* a fala, a única pista de que a tela mudou seria o silêncio. O índice "N de M" vem junto (item 3), e é ele
|
|
677
|
-
* que diz de quantos itens é a lista nova.
|
|
748
|
+
* Switches the visible list AND ANNOUNCES the first item of the list that came in. Not decoration: a child who cannot see
|
|
749
|
+
* has just changed menus and the cursor jumped; without speech the only clue would be silence. The «N de M» index comes
|
|
750
|
+
* with it (ADR-0044 item 3) and says how many items the new list has.
|
|
678
751
|
*/
|
|
679
|
-
function
|
|
680
|
-
|
|
681
|
-
|
|
752
|
+
function announceList(sp, sub) {
|
|
753
|
+
// «back» lands on the door that opened the list being left (ADR-0130 rule 1); any other door, on the first item
|
|
754
|
+
const cursor = sub === 'raiz' ? backToRoot(sp) : showPauseOptions(sp, sub);
|
|
755
|
+
if (!cursor)
|
|
682
756
|
return;
|
|
683
|
-
const
|
|
684
|
-
ctx.srSay(
|
|
757
|
+
const items = [...sp.querySelectorAll(PM_VISIBLE_ITEMS)];
|
|
758
|
+
ctx.srSay(announceItem(t, { label: cursor.textContent || '', position: items.indexOf(cursor) + 1, total: items.length }, ctx.settings.menuIndexOn));
|
|
685
759
|
}
|
|
686
|
-
/* =====================
|
|
760
|
+
/* ===================== THE `accessibility` MODE (ADR-0044 item 7) ===================== */
|
|
687
761
|
/**
|
|
688
|
-
*
|
|
689
|
-
*
|
|
690
|
-
*
|
|
691
|
-
* modo de entrada que sobrevivesse ao reinício seria a armadilha voltando pela porta dos fundos — a criança
|
|
692
|
-
* abriria o jogo no dia seguinte e o personagem não andaria.
|
|
762
|
+
* Who has the d-pad steering the BAR instead of the character. ROUND lifetime (ADR-0038): it lives in this instance's
|
|
763
|
+
* closure, is not persisted, and goes with the round — an input mode that survived a restart would be the trap coming
|
|
764
|
+
* back: the child would open the game the next day and the character would not move.
|
|
693
765
|
*/
|
|
694
|
-
const
|
|
695
|
-
/**
|
|
696
|
-
function
|
|
766
|
+
const onBar = new Set();
|
|
767
|
+
/** Screen `i`'s bar cursor, or the first icon when there is no cursor yet. */
|
|
768
|
+
function selectedIcon(bar) {
|
|
697
769
|
return bar.querySelector('.pi-sel') || bar.querySelector('.pi-btn');
|
|
698
770
|
}
|
|
699
|
-
/**
|
|
700
|
-
function
|
|
771
|
+
/** Puts the cursor on an icon, writes the caption and ANNOUNCES — speech is the channel of whoever cannot see the icon. */
|
|
772
|
+
function selectIcon(i, bar, el) {
|
|
701
773
|
bar.querySelectorAll('.pi-sel').forEach((x) => x.classList.remove('pi-sel'));
|
|
702
774
|
el.classList.add('pi-sel');
|
|
703
775
|
const cap = bar.querySelector('.pause-icons-cap');
|
|
704
776
|
if (cap)
|
|
705
|
-
cap.textContent =
|
|
706
|
-
ctx.srSay(
|
|
777
|
+
cap.textContent = accessibleLabel(el);
|
|
778
|
+
ctx.srSay(iconCaption(t, bar, el, ctx.settings.menuIndexOn)); // the spoken one carries the place (ADR-0167)
|
|
779
|
+
ctx.explainIcon?.(i, el.dataset.pi ?? null);
|
|
707
780
|
}
|
|
708
781
|
/**
|
|
709
|
-
*
|
|
782
|
+
* ENTERS the mode: the d-pad now steers screen `i`'s bar.
|
|
783
|
+
*
|
|
784
|
+
* It does NOT resume the game (ADR-0155): START is the QUICK PAUSE, the bar is used with the game FROZEN, and resuming on
|
|
785
|
+
* entry would unfreeze the world the instant the child asked it to stop. Whoever enters decides the phase.
|
|
710
786
|
*
|
|
711
|
-
*
|
|
712
|
-
*
|
|
713
|
-
*
|
|
787
|
+
* The announcement says how to LEAVE, at the moment of entering — the line that disarms the trap ADR-0044 recorded as a
|
|
788
|
+
* negative consequence: a child who cannot see presses a direction, the character does not move, and nothing on screen
|
|
789
|
+
* explains why, because the screen is not that child's channel.
|
|
714
790
|
*/
|
|
715
|
-
function
|
|
791
|
+
function enterBarMode(i) {
|
|
716
792
|
const bar = ctx.getA11yBars()[i];
|
|
717
|
-
const
|
|
718
|
-
if (!bar || !
|
|
793
|
+
const first = bar && selectedIcon(bar);
|
|
794
|
+
if (!bar || !first)
|
|
719
795
|
return;
|
|
720
|
-
|
|
721
|
-
const acts = getPauseActs();
|
|
722
|
-
if (acts.resume)
|
|
723
|
-
acts.resume(); // volta à tela normal: o modo é para usar DURANTE a partida
|
|
796
|
+
onBar.add(i);
|
|
724
797
|
ctx.srSay(t('sr.a11y.barEnter'));
|
|
725
|
-
|
|
798
|
+
selectIcon(i, bar, first);
|
|
726
799
|
}
|
|
727
|
-
/**
|
|
728
|
-
|
|
729
|
-
|
|
800
|
+
/**
|
|
801
|
+
* LEAVES the mode and gives the d-pad back to the character. It announces, because giving it back is information too.
|
|
802
|
+
*
|
|
803
|
+
* `silent` is for leaving the bar for ANOTHER screen, not for the game — SELECT, which swaps the quick pause for the card
|
|
804
|
+
* (ADR-0155): «back to the game» said there would be a lie, with the card opening on top.
|
|
805
|
+
*
|
|
806
|
+
* `onLeaveBar` runs on EVERY exit — Back, START or whoever calls this — because the root must unfreeze the game by any
|
|
807
|
+
* door. An exit only one path knew would leave the child on the other back at the character in a stopped world.
|
|
808
|
+
*/
|
|
809
|
+
function leaveBarMode(i, silent = false) {
|
|
810
|
+
if (!onBar.delete(i))
|
|
730
811
|
return;
|
|
731
812
|
const bar = ctx.getA11yBars()[i];
|
|
732
813
|
if (bar) {
|
|
@@ -735,113 +816,116 @@ export function initPauseIcons(ctx) {
|
|
|
735
816
|
if (cap)
|
|
736
817
|
cap.textContent = '';
|
|
737
818
|
}
|
|
738
|
-
ctx.
|
|
819
|
+
ctx.explainIcon?.(i, null);
|
|
820
|
+
if (!silent)
|
|
821
|
+
ctx.srSay(t('sr.a11y.barExit'));
|
|
822
|
+
ctx.onLeaveBar?.(i, silent);
|
|
739
823
|
}
|
|
740
|
-
/**
|
|
741
|
-
const
|
|
824
|
+
/** Is screen `i`'s d-pad on the bar? What input routing asks every frame. */
|
|
825
|
+
const isOnBar = (i) => onBar.has(i);
|
|
742
826
|
/**
|
|
743
|
-
*
|
|
827
|
+
* ONE STEP inside the mode. `hasStart` is the edge of the button that opens the pause — the second way out.
|
|
744
828
|
*
|
|
745
|
-
*
|
|
746
|
-
*
|
|
829
|
+
* The bar is one row, so all FOUR directions move along it: for someone navigating without sight, «up» in a one-row list
|
|
830
|
+
* must not be a dead end. And it moves in a RING, like every menu of the game (ADR-0044 item 1).
|
|
747
831
|
*/
|
|
748
|
-
function navBar(i, k,
|
|
749
|
-
if (!
|
|
832
|
+
function navBar(i, k, hasStart = false) {
|
|
833
|
+
if (!onBar.has(i))
|
|
750
834
|
return;
|
|
751
835
|
const bar = ctx.getA11yBars()[i];
|
|
752
836
|
if (!bar)
|
|
753
837
|
return;
|
|
754
|
-
const
|
|
755
|
-
if (
|
|
756
|
-
|
|
838
|
+
const action = barAction(k, hasStart);
|
|
839
|
+
if (action === 'sair') {
|
|
840
|
+
leaveBarMode(i);
|
|
757
841
|
return;
|
|
758
842
|
}
|
|
759
|
-
const
|
|
760
|
-
if (!
|
|
843
|
+
const icons = [...bar.querySelectorAll('.pi-btn')];
|
|
844
|
+
if (!icons.length)
|
|
761
845
|
return;
|
|
762
|
-
|
|
763
|
-
const
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
cur.click();
|
|
846
|
+
// never null here: the bar has an icon, and `selectedIcon` falls back to the first one
|
|
847
|
+
const cur = selectedIcon(bar);
|
|
848
|
+
// the click goes through the bar's own listener, which records who pressed it (`setPauseActor`)
|
|
849
|
+
if (action === 'ativar') {
|
|
850
|
+
cur.click();
|
|
768
851
|
return;
|
|
769
852
|
}
|
|
770
|
-
if (
|
|
771
|
-
|
|
772
|
-
selecionarIcone(bar, icones[passoNoAnel(icones.length, idx, d)]);
|
|
773
|
-
}
|
|
853
|
+
if (action === 'andar')
|
|
854
|
+
selectIcon(i, bar, icons[stepInRing(icons.length, icons.indexOf(cur), (k.down || k.right) ? 1 : -1)]);
|
|
774
855
|
}
|
|
775
856
|
function buildScreenPause(i) {
|
|
776
|
-
const sp =
|
|
857
|
+
const sp = ctx.doc.createElement('div');
|
|
777
858
|
sp.className = 'screen-pause';
|
|
778
859
|
sp.hidden = true;
|
|
779
860
|
sp.dataset.player = String(i);
|
|
780
861
|
/*
|
|
781
|
-
*
|
|
782
|
-
*
|
|
783
|
-
* em que a resposta existe»; não é. O `getPauseActs` é um getter precisamente porque a tabela chega
|
|
784
|
-
* TARDE — «`pauseActs` is a `const` declared far below the init site», diz o próprio campo —, e o
|
|
785
|
-
* `createGame` monta durante o próprio `createGame(...)`. Filtrar aqui apagava para sempre todo item cuja
|
|
786
|
-
* acção só passou a existir depois do boot.
|
|
787
|
-
*
|
|
788
|
-
* Quem decide o que se VÊ é o `refrescarItensDaPausa`, a cada `reflectPauseIcons()` — que o `ui/shell`
|
|
789
|
-
* dispara quando a fase vira `pause-menu`, ou seja quando a pausa ABRE. O §5 continua respeitado (a
|
|
790
|
-
* criança nunca vê um item que não acciona) e agora também vê os que passaram a accionar.
|
|
862
|
+
* BUILDS THE WHOLE LIST, and `refreshPauseItems` decides which items act — here and on every `reflectPauseIcons()`.
|
|
863
|
+
* Filtering at build time would lose for good every item whose action exists only after the boot (see above).
|
|
791
864
|
*/
|
|
792
865
|
sp.innerHTML = screenPauseMarkup({
|
|
793
866
|
player: i,
|
|
794
867
|
numPlayers: ctx.getNumPlayers(),
|
|
795
868
|
pmButtons: ctx.pmButtons ?? PM_BTNS,
|
|
796
869
|
optionsButtons: ctx.optionsButtons ?? PM_OPTIONS_BTNS,
|
|
870
|
+
gameButtons: ctx.gameButtons ?? PM_GAME_BTNS,
|
|
797
871
|
dynLabel: dynLabel, t,
|
|
798
872
|
});
|
|
799
|
-
|
|
800
|
-
|
|
873
|
+
cards[i] = sp;
|
|
874
|
+
refreshPauseItems(); // the lock holds from the build, not only from the first opening
|
|
801
875
|
sp.addEventListener('click', (e) => {
|
|
802
|
-
const
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
setPauseActor(i);
|
|
806
|
-
const act = b.dataset.act || '';
|
|
807
|
-
// NAVEGAÇÃO DENTRO DO CARTÃO fica aqui, e não na tabela de ações: `options` e `pmback` não fazem nada
|
|
808
|
-
// ao jogo — trocam qual lista está na tela. A tabela vive em `ui/shell`, que não conhece este `sp`.
|
|
809
|
-
if (act === 'options' || act === 'pmback') {
|
|
810
|
-
anunciarLista(sp, act === 'options' ? 'opcoes' : 'raiz');
|
|
811
|
-
return;
|
|
812
|
-
}
|
|
813
|
-
// `acessibilidade` leva o cursor à BARRA RÁPIDA. Enquanto ela mora dentro do cartão, "entrar no modo"
|
|
814
|
-
// é pôr o cursor nela — e a saída continua sendo a saída da pausa, que é a mesma de sempre. Quando o
|
|
815
|
-
// item 7 levar a barra para o HUD, esta linha o segue; o que o item SIGNIFICA não muda.
|
|
816
|
-
if (act === 'acessibilidade') {
|
|
817
|
-
entrarNaBarra(i);
|
|
818
|
-
return;
|
|
819
|
-
}
|
|
820
|
-
const acts = getPauseActs();
|
|
821
|
-
const fn = acts[act];
|
|
822
|
-
if (fn)
|
|
823
|
-
fn();
|
|
824
|
-
return;
|
|
825
|
-
}
|
|
876
|
+
const b = e.target?.closest('.pm-btn');
|
|
877
|
+
if (b)
|
|
878
|
+
pressPauseItem(i, sp, b);
|
|
826
879
|
});
|
|
827
880
|
return sp;
|
|
828
881
|
}
|
|
882
|
+
/** One item of screen `i`'s pause card pressed — a locked one, a door between its lists, the quick bar, or an action. */
|
|
883
|
+
function pressPauseItem(i, sp, b) {
|
|
884
|
+
// 🔴 THE CURSOR GOES WHERE THE CHILD PRESSED (ADR-0130 rule 1). A finger or a mouse opens a panel without walking the
|
|
885
|
+
// arrows, and the mark stayed on the item the arrows had last reached — so «back» from the panel returned her to an
|
|
886
|
+
// item she never chose. The keyboard's press is already on its item; this is the pointer's half.
|
|
887
|
+
markPauseItem(sp, b);
|
|
888
|
+
// LOCKED (ADR-0161): pressing it SAYS the reason and does nothing — neither door nor action.
|
|
889
|
+
if (b.getAttribute('aria-disabled') === 'true') {
|
|
890
|
+
const reason = b.dataset.motivo ?? '';
|
|
891
|
+
ctx.srSay(reason);
|
|
892
|
+
ctx.explainItem?.(reason);
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
setPauseActor(i);
|
|
896
|
+
const act = b.dataset.act || '';
|
|
897
|
+
// «Opções do jogo» with the cartridge's rows opens the engine's panel (ADR-0182), not the list a host may pass
|
|
898
|
+
const doorWithPanel = act === 'opcoesdojogo' && typeof getPauseActs().opcoesdojogo === 'function';
|
|
899
|
+
if (DOOR_TO_LIST[act] && !doorWithPanel) {
|
|
900
|
+
announceList(sp, DOOR_TO_LIST[act]);
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
// `acessibilidade` takes the cursor to the QUICK BAR. The item is not in the engine's root (ADR-0151); only a list a
|
|
904
|
+
// GAME passes reaches it, and for that list the card closes and the bar is used — explicitly, since `enterBar` does
|
|
905
|
+
// not resume by itself (ADR-0155).
|
|
906
|
+
if (act === 'acessibilidade') {
|
|
907
|
+
getPauseActs().resume?.();
|
|
908
|
+
enterBarMode(i);
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
const fn = getPauseActs()[act];
|
|
912
|
+
if (fn)
|
|
913
|
+
fn();
|
|
914
|
+
}
|
|
829
915
|
/**
|
|
830
|
-
*
|
|
916
|
+
* One screen's QUICK BAR: the toggles, the caption, and their wiring.
|
|
831
917
|
*
|
|
832
|
-
*
|
|
833
|
-
*
|
|
834
|
-
* facilidade não existe —, e degradar o que existe para DAR acesso seria o contrário do que ele serve.
|
|
918
|
+
* A SIBLING of `.screen-exp`, not a child (#82): the bar is CONTROL, not experience. Empathy mode degrades the experience
|
|
919
|
+
* on purpose — a simulation creates difficulty — and degrading what exists to GIVE access would be the opposite.
|
|
835
920
|
*/
|
|
836
921
|
function buildQuickBar(i) {
|
|
837
|
-
const bar =
|
|
922
|
+
const bar = ctx.doc.createElement('div');
|
|
838
923
|
bar.className = 'screen-a11y';
|
|
839
924
|
bar.dataset.player = String(i);
|
|
840
|
-
bar.innerHTML = quickBarMarkup(
|
|
841
|
-
//
|
|
842
|
-
//
|
|
843
|
-
//
|
|
844
|
-
// `tabindex` é posto AQUI, no elemento, e não no markup que as duas compartilham.
|
|
925
|
+
bar.innerHTML = quickBarMarkup(ctx.translator, gameIcons);
|
|
926
|
+
// OUT OF THE TAB ORDER during play (ADR-0044 item 7): a stop per icon between the child and the game would be the price
|
|
927
|
+
// of leaving them in, and keyboard reach is not lost — it becomes the `accessibility` mode, opened from the pause. The
|
|
928
|
+
// bar the root mounts in `#title-icons` is unaffected: the `tabindex` is set HERE, on the element, not in the shared markup.
|
|
845
929
|
bar.querySelectorAll('.pi-btn').forEach((b) => { b.tabIndex = -1; });
|
|
846
930
|
const cap = bar.querySelector('.pause-icons-cap');
|
|
847
931
|
bar.addEventListener('click', (e) => {
|
|
@@ -852,39 +936,25 @@ export function initPauseIcons(ctx) {
|
|
|
852
936
|
iconAct(ib.dataset.pi || '', i);
|
|
853
937
|
reflectPauseIcons(); // must run BEFORE reading the label back — that is what makes the caption honest
|
|
854
938
|
if (cap)
|
|
855
|
-
cap.textContent =
|
|
856
|
-
});
|
|
857
|
-
// Legenda = o `aria-label` do botão, para que passar o mouse ou focar diga a MESMA verdade que um leitor
|
|
858
|
-
// de tela anunciaria. Uma fonte só para quem vê e para quem escuta.
|
|
859
|
-
//
|
|
860
|
-
// E ELA SOME AO SAIR. Antes ficava: a última explicação apontada permanecia por cima do jogo até alguém
|
|
861
|
-
// apontar outra. Numa barra que agora vive na TELA DE JOGO isso é uma faixa de texto parada em cima da
|
|
862
|
-
// partida — o Dev viu e disse o que é: explicação só enquanto o mouse estiver no botão.
|
|
863
|
-
//
|
|
864
|
-
// A EXCEÇÃO É O CURSOR DO MODO `accessibility`: quando ele está pousado num ícone, a legenda é a única
|
|
865
|
-
// coisa que diz onde ele está, e apagá-la ao mexer o mouse cegaria o modo. Daí a pergunta pelo `.pi-sel`.
|
|
866
|
-
const limpar = () => { if (cap && !bar.querySelector('.pi-sel'))
|
|
867
|
-
cap.textContent = ''; };
|
|
868
|
-
bar.querySelectorAll('.pi-btn').forEach((b) => {
|
|
869
|
-
const show = () => { if (cap)
|
|
870
|
-
cap.textContent = legendaDoIcone(bar, b); };
|
|
871
|
-
b.addEventListener('mouseenter', show);
|
|
872
|
-
b.addEventListener('focus', show);
|
|
873
|
-
b.addEventListener('mouseleave', limpar);
|
|
874
|
-
b.addEventListener('blur', limpar);
|
|
939
|
+
cap.textContent = accessibleLabel(ib);
|
|
875
940
|
});
|
|
941
|
+
// Caption = the button's `aria-label`, so hovering or focusing says the SAME truth a screen reader would announce: one
|
|
942
|
+
// source for whoever sees and whoever listens. It GOES AWAY on leaving — a bar on the play screen must not leave a strip
|
|
943
|
+
// of text over the game (the Dev: an explanation only while the pointer is on the button). The exception is the
|
|
944
|
+
// `accessibility` mode's CURSOR: when it sits on an icon the caption is the only thing saying where it is, hence the
|
|
945
|
+
// `.pi-sel` check in `wireBarCaption`.
|
|
946
|
+
wireBarCaption(bar, (k) => ctx.explainIcon?.(i, k));
|
|
876
947
|
return bar;
|
|
877
948
|
}
|
|
878
949
|
return {
|
|
879
|
-
buildScreenPause, buildQuickBar,
|
|
880
|
-
|
|
950
|
+
buildScreenPause, buildQuickBar, enterBar: enterBarMode, leaveBar: leaveBarMode, onBar: isOnBar, navBar,
|
|
951
|
+
mountedIcons: gameIcons,
|
|
881
952
|
iconAct, iconLabel, reflectIconBtn, reflectIconsIn, reflectPauseIcons,
|
|
882
|
-
//
|
|
883
|
-
//
|
|
884
|
-
// pior do defeito: o ajuste parece ter pegado e some depois.
|
|
953
|
+
// `setCalmMode` PERSISTS TOO, and sanitises: it is the other door to the same value, and a level set here that only
|
|
954
|
+
// lasted the session would look applied and then vanish.
|
|
885
955
|
applyCalm,
|
|
886
956
|
getCalmMode: () => calmMode,
|
|
887
|
-
setCalmMode: (n) => { calmMode =
|
|
957
|
+
setCalmMode: (n) => { calmMode = sanitiseTeaLevel(n, DEFAULTS.calmMode); ctx.store.set(KEYS.tea, calmMode); },
|
|
888
958
|
iconState,
|
|
889
959
|
};
|
|
890
960
|
}
|