@the-inclusionist/engine 9.0.0 → 10.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 +89 -78
- package/app/css/style.css +584 -374
- package/app/public/vendor/fonts/bitter-var-ext.woff2 +0 -0
- package/app/public/vendor/fonts/bitter-var.woff2 +0 -0
- package/app/public/vendor/fonts/bodonimoda-var-ext.woff2 +0 -0
- package/app/public/vendor/fonts/bodonimoda-var.woff2 +0 -0
- package/app/public/vendor/fonts/cat-abril_fatface-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-abril_fatface-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-adlam_display-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-adlam_display-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-agu_display-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-agu_display-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-alfa_slab_one-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-alfa_slab_one-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-are_you_serious-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-are_you_serious-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-asimovian-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-asimovian-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-atkinson_hyperlegible_next-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-atkinson_hyperlegible_next-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-bakbak_one-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-bakbak_one-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-bangers-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-bangers-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-barriecito-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-barriecito-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-berkshire_swash-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-berkshire_swash-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-bevan-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-bevan-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-bitcount-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-bitcount-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-bowlby_one_sc-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-bowlby_one_sc-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-bungee_shade-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-bungee_shade-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-charm-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-charm-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-charm-700-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-charm-700.woff2 +0 -0
- package/app/public/vendor/fonts/cat-cherry_bomb_one-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-cherry_bomb_one-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-chokokutai-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-chokokutai-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-chonburi-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-chonburi-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-climate_crisis-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-climate_crisis-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-cookie-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-dela_gothic_one-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-dela_gothic_one-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-fascinate_inline-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-fascinate_inline-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-ga_maamli-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-ga_maamli-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-itim-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-itim-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-jacquard_24-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-jacquard_24-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-kablammo-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-kablammo-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-kalam-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-kalam-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-kalam-700-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-kalam-700.woff2 +0 -0
- package/app/public/vendor/fonts/cat-katibeh-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-katibeh-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-lemonada-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-lemonada-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-lilita_one-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-lilita_one-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-lobster-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-lobster-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-luckiest_guy-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-luckiest_guy-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-marhey-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-marhey-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-modak-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-modak-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-momo_signature-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-momo_signature-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-monofett-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-monofett-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-monomakh-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-monomakh-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-monoton-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-monoton-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-nerko_one-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-nerko_one-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-noto_sans_adlam-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-noto_sans_adlam-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-noto_sans_arabic-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-noto_sans_arabic-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-noto_sans_devanagari-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-noto_sans_devanagari-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-noto_sans_hebrew-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-noto_sans_hebrew-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-noto_sans_jp-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-noto_sans_jp-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-noto_sans_khmer-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-noto_sans_khmer-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-noto_sans_kr-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-noto_sans_kr-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-noto_sans_sc-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-noto_sans_sc-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-noto_sans_thai-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-noto_sans_thai-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-ole-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-ole-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-pacifico-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-pacifico-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-patrick_hand-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-patrick_hand-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-petit_formal_script-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-petit_formal_script-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-pixelify_sans-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-pixelify_sans-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-plaster-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-plaster-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_ar_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_at-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_at_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_au_nsw-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_au_nsw_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_au_qld-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_au_qld_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_au_sa-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_au_sa_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_au_tas-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_au_tas_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_au_vic-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_au_vic_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_be_vlg-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_be_vlg_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_be_wal-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_be_wal_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_br_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_ca_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_cl_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_co_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_cu_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_cz-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_cz_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_de_grund-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_de_grund_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_de_la-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_de_la_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_de_sas-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_de_sas_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_de_va-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_de_va_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_dk_loopet-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_dk_loopet_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_dk_uloopet-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_dk_uloopet_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_es_deco_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_es_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_fr_moderne-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_fr_moderne_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_fr_trad-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_fr_trad_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_gb_j_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_gb_s_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_hr-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_hr_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_hr_lijeva-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_hr_lijeva_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_hu-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_hu_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_id-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_id_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_ie-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_ie_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_in-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_in_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_is-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_is_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_it_moderna-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_it_moderna_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_it_trad-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_it_trad_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_mx_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_ng_modern-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_ng_modern_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_nl-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_nl_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_no-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_no_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_nz-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_nz_basic-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_nz_basic_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_nz_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_pe_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_pl-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_pl_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_pt_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_ro-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_ro_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_sk-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_sk_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_tz-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_tz_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_us_modern_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_us_trad_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_vn-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_vn_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_za-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-playwrite_za_guides-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-potta_one-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-potta_one-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-ranchers-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-ranchers-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-ranga-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-ranga-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-ranga-700-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-ranga-700.woff2 +0 -0
- package/app/public/vendor/fonts/cat-risque-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-risque-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-rowdies-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-rowdies-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-rowdies-700-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-rowdies-700.woff2 +0 -0
- package/app/public/vendor/fonts/cat-ruge_boogie-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-ruge_boogie-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-ruslan_display-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-ruslan_display-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-sacramento-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-sacramento-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-sancreek-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-sancreek-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-sankofa_display-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-sankofa_display-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-sedgwick_ave_display-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-sedgwick_ave_display-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-shantell_sans-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-shantell_sans-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-smokum-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-smokum-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-solitreo-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-solitreo-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-spicy_rice-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-spicy_rice-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-sriracha-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-sriracha-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-srisakdi-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-srisakdi-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-srisakdi-700-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-srisakdi-700.woff2 +0 -0
- package/app/public/vendor/fonts/cat-stix_two_math-400-0.woff2 +0 -0
- package/app/public/vendor/fonts/cat-tillana-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-tillana-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-tillana-700-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-tillana-700.woff2 +0 -0
- package/app/public/vendor/fonts/cat-titan_one-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-titan_one-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-yatra_one-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-yatra_one-400.woff2 +0 -0
- package/app/public/vendor/fonts/cat-yomogi-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/cat-yomogi-400.woff2 +0 -0
- package/app/public/vendor/fonts/dmserifdisplay-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/dmserifdisplay-400.woff2 +0 -0
- package/app/public/vendor/fonts/domine-var-ext.woff2 +0 -0
- package/app/public/vendor/fonts/domine-var.woff2 +0 -0
- package/app/public/vendor/fonts/fraunces-var-ext.woff2 +0 -0
- package/app/public/vendor/fonts/fraunces-var.woff2 +0 -0
- package/app/public/vendor/fonts/fredoka-var-ext.woff2 +0 -0
- package/app/public/vendor/fonts/fredoka-var.woff2 +0 -0
- package/app/public/vendor/fonts/jakarta-var-ext.woff2 +0 -0
- package/app/public/vendor/fonts/jakarta-var.woff2 +0 -0
- package/app/public/vendor/fonts/lora-var-ext.woff2 +0 -0
- package/app/public/vendor/fonts/lora-var.woff2 +0 -0
- package/app/public/vendor/fonts/merriweather-var-ext.woff2 +0 -0
- package/app/public/vendor/fonts/merriweather-var.woff2 +0 -0
- package/app/public/vendor/fonts/notosans-var-ext.woff2 +0 -0
- package/app/public/vendor/fonts/notosans-var.woff2 +0 -0
- package/app/public/vendor/fonts/nunito-var-ext.woff2 +0 -0
- package/app/public/vendor/fonts/nunito-var.woff2 +0 -0
- package/app/public/vendor/fonts/playfair-var-ext.woff2 +0 -0
- package/app/public/vendor/fonts/playfair-var.woff2 +0 -0
- 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/quicksand-var-ext.woff2 +0 -0
- package/app/public/vendor/fonts/quicksand-var.woff2 +0 -0
- package/app/public/vendor/fonts/robotoflex-var-ext.woff2 +0 -0
- package/app/public/vendor/fonts/robotoflex-var.woff2 +0 -0
- package/app/public/vendor/fonts/sora-var-ext.woff2 +0 -0
- package/app/public/vendor/fonts/sora-var.woff2 +0 -0
- package/app/public/vendor/fonts/spacegrotesk-var-ext.woff2 +0 -0
- package/app/public/vendor/fonts/spacegrotesk-var.woff2 +0 -0
- package/app/public/vendor/fonts/spectral-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/spectral-400.woff2 +0 -0
- package/app/public/vendor/fonts/spectral-700-ext.woff2 +0 -0
- package/app/public/vendor/fonts/spectral-700.woff2 +0 -0
- package/app/public/vendor/fonts/teachers-var-ext.woff2 +0 -0
- package/app/public/vendor/fonts/teachers-var.woff2 +0 -0
- package/app/public/vendor/fonts/ubuntu-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/ubuntu-400.woff2 +0 -0
- package/app/public/vendor/fonts/ubuntu-700-ext.woff2 +0 -0
- package/app/public/vendor/fonts/ubuntu-700.woff2 +0 -0
- package/app/public/vendor/fonts.css +1167 -51
- package/dist-pkg/boot/create-game.d.ts +444 -215
- package/dist-pkg/boot/create-game.js +3530 -549
- package/dist-pkg/core/a11y-sr.d.ts +25 -3
- package/dist-pkg/core/a11y-sr.js +40 -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 +107 -0
- package/dist-pkg/core/accommodations.js +178 -0
- package/dist-pkg/core/actions.d.ts +75 -51
- package/dist-pkg/core/actions.js +98 -69
- 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 +143 -163
- package/dist-pkg/core/contract.js +223 -207
- 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 +87 -41
- package/dist-pkg/core/i18n.js +206 -153
- 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/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 +321 -215
- package/dist-pkg/i18n/es.js +320 -215
- package/dist-pkg/i18n/pt.js +342 -215
- 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 +164 -145
- package/dist-pkg/input/gamepad.js +351 -559
- 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/keyboard-runtime.d.ts +18 -20
- package/dist-pkg/input/keyboard-runtime.js +23 -27
- package/dist-pkg/input/keyboard.d.ts +68 -62
- package/dist-pkg/input/keyboard.js +83 -105
- package/dist-pkg/input/keydown.d.ts +124 -132
- package/dist-pkg/input/keydown.js +283 -288
- package/dist-pkg/input/latch-edge.d.ts +25 -21
- package/dist-pkg/input/latch-edge.js +33 -35
- package/dist-pkg/input/latch-scope.d.ts +43 -47
- package/dist-pkg/input/latch-scope.js +66 -75
- package/dist-pkg/input/latch-store.d.ts +28 -29
- package/dist-pkg/input/latch-store.js +44 -47
- package/dist-pkg/input/latch-sync.d.ts +28 -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 +164 -0
- package/dist-pkg/input/pad-wizard.d.ts +96 -0
- package/dist-pkg/input/pad-wizard.js +205 -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 +42 -0
- package/dist-pkg/input/switch-scan.js +54 -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 -113
- package/dist-pkg/input/touch-bindings.js +158 -173
- package/dist-pkg/input/touch.d.ts +130 -70
- package/dist-pkg/input/touch.js +321 -123
- 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 +96 -114
- package/dist-pkg/input/transports.js +104 -106
- package/dist-pkg/input/virtual-controller.d.ts +46 -0
- package/dist-pkg/input/virtual-controller.js +41 -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 +43 -0
- package/dist-pkg/input/voice-map.js +125 -0
- package/dist-pkg/platform/audio-ambient.d.ts +13 -1
- package/dist-pkg/platform/audio-ambient.js +11 -13
- 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 +36 -39
- package/dist-pkg/platform/audio-sonar.d.ts +96 -113
- package/dist-pkg/platform/audio-sonar.js +245 -238
- package/dist-pkg/platform/audio.d.ts +53 -19
- package/dist-pkg/platform/audio.js +161 -169
- package/dist-pkg/platform/flash-sampler.d.ts +19 -0
- package/dist-pkg/platform/flash-sampler.js +79 -0
- package/dist-pkg/platform/guide-intensity.d.ts +26 -27
- package/dist-pkg/platform/guide-intensity.js +50 -52
- package/dist-pkg/platform/heavy-catalogue.d.ts +31 -0
- package/dist-pkg/platform/heavy-catalogue.js +223 -0
- package/dist-pkg/platform/heavy-mirror.d.ts +26 -0
- package/dist-pkg/platform/heavy-mirror.js +76 -0
- package/dist-pkg/platform/heavy.d.ts +104 -0
- package/dist-pkg/platform/heavy.js +169 -0
- package/dist-pkg/platform/interruptible-speech.d.ts +16 -16
- package/dist-pkg/platform/interruptible-speech.js +50 -51
- 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 +81 -0
- package/dist-pkg/platform/locale-host.d.ts +10 -0
- package/dist-pkg/platform/locale-host.js +34 -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 +40 -0
- package/dist-pkg/platform/reading-in-worker.js +87 -0
- package/dist-pkg/platform/reading-model.d.ts +73 -0
- package/dist-pkg/platform/reading-model.js +247 -0
- package/dist-pkg/platform/reading-runtime.d.ts +24 -0
- package/dist-pkg/platform/reading-runtime.js +159 -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 +69 -0
- package/dist-pkg/platform/reading.js +128 -0
- package/dist-pkg/platform/speech-recognition.d.ts +51 -0
- package/dist-pkg/platform/speech-recognition.js +84 -0
- package/dist-pkg/platform/speech.d.ts +14 -1
- package/dist-pkg/platform/speech.js +13 -13
- 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 +51 -38
- package/dist-pkg/platform/tts.js +217 -119
- 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 +36 -0
- package/dist-pkg/platform/voice-listener.js +88 -0
- package/dist-pkg/platform/voice-plan.d.ts +6 -102
- package/dist-pkg/platform/voice-plan.js +9 -151
- package/dist-pkg/platform/vosk-runtime.d.ts +71 -0
- package/dist-pkg/platform/vosk-runtime.js +93 -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 +46 -12
- package/dist-pkg/render/crt.js +82 -80
- package/dist-pkg/render/cvd-matrices.d.ts +18 -18
- package/dist-pkg/render/cvd-matrices.js +33 -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 +66 -54
- package/dist-pkg/render/high-contrast.js +221 -216
- 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 +80 -86
- package/dist-pkg/render/viz-axes.js +115 -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 +40 -0
- package/dist-pkg/ui/aac-sets.js +49 -0
- package/dist-pkg/ui/audio-choices.d.ts +63 -0
- package/dist-pkg/ui/audio-choices.js +94 -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 +6 -0
- package/dist-pkg/ui/camera-control.js +14 -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/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 +93 -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 +118 -63
- package/dist-pkg/ui/fonts.js +267 -111
- package/dist-pkg/ui/game-options.d.ts +42 -0
- package/dist-pkg/ui/game-options.js +138 -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 +76 -0
- package/dist-pkg/ui/help-panel.js +232 -0
- package/dist-pkg/ui/hud-bands.d.ts +45 -0
- package/dist-pkg/ui/hud-bands.js +108 -0
- package/dist-pkg/ui/hud.d.ts +73 -70
- 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/latch-refusal.d.ts +22 -21
- package/dist-pkg/ui/latch-refusal.js +37 -37
- package/dist-pkg/ui/layout.d.ts +103 -28
- package/dist-pkg/ui/layout.js +114 -99
- 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 +32 -0
- package/dist-pkg/ui/menu-intent.js +54 -0
- package/dist-pkg/ui/menu-items.d.ts +10 -0
- package/dist-pkg/ui/menu-items.js +22 -0
- package/dist-pkg/ui/menu-nav.d.ts +60 -90
- package/dist-pkg/ui/menu-nav.js +301 -274
- 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 +116 -0
- package/dist-pkg/ui/panel-widgets.js +208 -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 +249 -351
- package/dist-pkg/ui/pause-icons.js +614 -605
- 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 +42 -0
- package/dist-pkg/ui/settings-aac.d.ts +68 -0
- package/dist-pkg/ui/settings-aac.js +189 -0
- package/dist-pkg/ui/settings-audio.d.ts +91 -113
- package/dist-pkg/ui/settings-audio.js +407 -405
- package/dist-pkg/ui/settings-controls.d.ts +53 -92
- 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 +195 -0
- package/dist-pkg/ui/settings-mobility.js +374 -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 +48 -45
- package/dist-pkg/ui/settings-panel.js +164 -93
- package/dist-pkg/ui/settings-typo.d.ts +28 -76
- package/dist-pkg/ui/settings-typo.js +145 -125
- 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 +113 -134
- package/dist-pkg/ui/shell.js +143 -216
- 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 +4 -4
- package/dist-pkg/ui/title.js +11 -13
- package/dist-pkg/ui/top-band.d.ts +24 -0
- package/dist-pkg/ui/top-band.js +100 -0
- package/dist-pkg/ui/typo-choices.d.ts +71 -0
- package/dist-pkg/ui/typo-choices.js +84 -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 +33 -10
- package/dist-pkg/ui/vlibras.js +58 -81
- package/dist-pkg/ui/voice-control.d.ts +46 -0
- package/dist-pkg/ui/voice-control.js +143 -0
- package/dist-pkg/ui/voice-settings.d.ts +96 -0
- package/dist-pkg/ui/voice-settings.js +302 -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 +198 -54
- package/docs/LICENSES.md +160 -145
- package/package.json +24 -25
- package/scripts/heavy-into-the-delivery.mjs +144 -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/third-party.mjs +287 -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/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/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/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,743 +1,3724 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
-
// boot/create-game —
|
|
2
|
+
// boot/create-game — THE BOUNDARY TEST OF ADR-0027 (step 4), written instead of argued.
|
|
3
3
|
//
|
|
4
|
-
// =========================
|
|
5
|
-
//
|
|
4
|
+
// ========================= THE QUESTION THIS FILE EXISTS TO ANSWER =========================
|
|
5
|
+
// ADR-0027 set a verdict and did not leave it vague: if `createGame()` could not be written without a parameter
|
|
6
|
+
// called `coinTarget`, the boundary that record proposed was wrong, and its steps 5 to 7 could not begin.
|
|
6
7
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
8
|
+
// It can. There is no `coinTarget` here, and not out of discipline: ADR-0030 settled where the target comes from —
|
|
9
|
+
// field 5 of the contract (`objectiveOf`). The HUD that asked for a coin count asks the game how many of how many, and
|
|
10
|
+
// the coin stops being an engine word. `tests/boot-create-game.node.test.js` holds this.
|
|
9
11
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
12
|
+
// ========================= WHAT THIS IS: THE ORDER =========================
|
|
13
|
+
// Not a framework. It is the sequence that switches the engine on, which every consumer would otherwise rewrite by
|
|
14
|
+
// hand — and the second consumer measured the price of rewriting it (`consumer-quiz`, findings 3, 6 and 12):
|
|
13
15
|
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
16
|
+
// · FINDING 3 — the mixer MUST be loaded before `createTts`, or `audioCat` is null and `narrate` gives up
|
|
17
|
+
// SILENTLY. An order dependency no type declared, found by the silence. Here it cannot be got wrong: `createAudio`
|
|
18
|
+
// loads the mixer as it is built, and the voice is built after it.
|
|
19
|
+
// · FINDING 6 — the panels need fixed ids in the document and, when they are missing, OPEN EMPTY, with no error.
|
|
20
|
+
// Here the engine builds its panels, and what the page lacks becomes `problems`, a list the consumer can read.
|
|
21
|
+
// · FINDING 12 — every consumer wrote the same one-line adapter for `window`. Written once.
|
|
17
22
|
//
|
|
18
|
-
//
|
|
19
|
-
// desiste CALADO. Uma dependência de ordem que nenhum tipo declara e que se descobre pelo silêncio. Aqui
|
|
20
|
-
// ela é impossível de errar: quem chama não escolhe a ordem.
|
|
21
|
-
// · ACHADO 6 — os painéis exigem ids fixos no documento (`#typo`, `#typo-list`…) e, quando faltam, ABREM
|
|
22
|
-
// VAZIOS. Sem erro. Aqui a falta vira `problems`, que é uma lista que o consumidor pode ler e mostrar.
|
|
23
|
-
// · ACHADO 12 — todo consumidor escreve o mesmo adaptador de uma linha para o `window`. Escrito uma vez.
|
|
23
|
+
// And one finding that only appeared when this function tried to boot against an injected document:
|
|
24
24
|
//
|
|
25
|
-
//
|
|
25
|
+
// · FINDING 15 — `initI18n()` called `applyDom(document)`, the GLOBAL, underneath whoever called it. In a browser
|
|
26
|
+
// it makes no difference, which is how it survived; in a pure-logic test it is the difference between booting and
|
|
27
|
+
// not, and with two documents (an engine in an iframe, an editor beside the game) it would be the difference
|
|
28
|
+
// between translating the right document and the other one. Hence `initI18n(root)`, with the global as default,
|
|
29
|
+
// exactly as `applyDom` already did.
|
|
26
30
|
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
31
|
+
// ========================= DECLINING IS NOT LYING =========================
|
|
32
|
+
// The quiz declined the sonar and the pad instead of inventing tiles and a fake collision box, and that distinction
|
|
33
|
+
// separates a finding from a false green. Here it becomes a TYPE: a game without a piece says so in a field instead
|
|
34
|
+
// of returning `null` from a getter and hoping. What is declined is kept in the returned object, so a consumer can be
|
|
35
|
+
// audited by what it refused.
|
|
36
|
+
// ⚠️ THIS PARAGRAPH NAMES NONE OF THE FIELDS, which looks odd and is deliberate: `tests/no-decline-is-dead` counts
|
|
37
|
+
// MENTIONS, comments included, on purpose — erring towards «alive» is the right direction for that error, because a
|
|
38
|
+
// false accusation switches a gate off. So using a decline as an EXAMPLE in prose makes it look read, and a field
|
|
39
|
+
// that is genuinely dead stops being accused.
|
|
32
40
|
//
|
|
33
|
-
// =========================
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
import {
|
|
50
|
-
import {
|
|
51
|
-
import {
|
|
52
|
-
import {
|
|
53
|
-
import {
|
|
54
|
-
import {
|
|
55
|
-
import {
|
|
56
|
-
import {
|
|
57
|
-
import {
|
|
58
|
-
import {
|
|
59
|
-
|
|
60
|
-
import
|
|
61
|
-
import {
|
|
41
|
+
// ========================= WHAT THIS DOES NOT DO =========================
|
|
42
|
+
// It does not start a renderer, physics or tiles, nor the game loop: none of that belongs to every game, and the loop
|
|
43
|
+
// is the cartridge's (`core/loop.startLoop`, called by the game). It does not mount `ui/shell`, the phase machine of a
|
|
44
|
+
// game with a title screen. What it covers is what is the SAME in every game: language, screen reader, mixer, voice,
|
|
45
|
+
// the dialog stack, the colour filters, the remappable keyboard, menu navigation, the pause card and the accessibility
|
|
46
|
+
// bar, the settings panels, the navigation sonar, and every input transport.
|
|
47
|
+
import i18nObject, { initI18n, loadLocale, createTranslator } from '../core/i18n.js';
|
|
48
|
+
import { localeHostHooks, exposeI18n } from '../platform/locale-host.js';
|
|
49
|
+
import { createInputState } from '../input/state.js';
|
|
50
|
+
import { initTouch, mountTouchControls, touchGaps } from '../input/touch.js';
|
|
51
|
+
import { initTouchBindings } from '../input/touch-bindings.js';
|
|
52
|
+
import { createCrashNotice } from '../ui/loop-crash.js';
|
|
53
|
+
import { sampleFlashes } from '../platform/flash-sampler.js';
|
|
54
|
+
import { initFocusTrap, focusablesInDom } from '../ui/focus-trap.js';
|
|
55
|
+
import { showReachNotice, REACH_NOTICE_ID } from '../ui/reach-notice.js';
|
|
56
|
+
import { reach, defaultTransports } from '../input/transports.js';
|
|
57
|
+
import { accommodationAnswersProblems, subjectWord } from '../core/accommodations.js';
|
|
58
|
+
import { genreProblems, genreWarning } from '../core/genres.js';
|
|
59
|
+
import { cartridgeProblems } from '../core/cartridge-problems.js';
|
|
60
|
+
import { contractSubjects } from '../core/accommodation-subjects.js';
|
|
61
|
+
import { presetActions, startClaimProblem, selectClaimProblem, labellerFrom, shortLabellerFrom, ACTIONS } from '../core/actions.js';
|
|
62
|
+
import { createAnnouncer } from '../core/a11y-sr.js';
|
|
63
|
+
import { createEyeControl, videoFeed } from '../ui/eye-control.js';
|
|
64
|
+
import { createFaceControl } from '../ui/face-control.js';
|
|
65
|
+
import { createHandControl } from '../ui/hand-control.js';
|
|
66
|
+
import { checkedCacheHas, sha256With } from '../platform/heavy.js';
|
|
67
|
+
import { createBundleLoader } from '../platform/vosk-runtime.js';
|
|
68
|
+
import { followCameraMode } from '../ui/camera-control.js';
|
|
69
|
+
import { initPauseIcons, wireBarCaption, showPauseOptions } from '../ui/pause-icons.js';
|
|
70
|
+
// 📌 The bar's markup is a pure string builder and lives with the rest of the pause markup (ADR-0221, issue #203); what this
|
|
71
|
+
// root asks `ui/pause-icons` for is the WIRING — the icons this game can actually act on, and the reflection of their state.
|
|
72
|
+
import { iconsMarkup } from '../ui/pause-markup.js';
|
|
73
|
+
import { accessibleLabel } from '../core/accessible-label.js';
|
|
74
|
+
import { helpRows, mountSlides, showSlide, animateFigure, howToPlayProblems } from '../ui/help-panel.js';
|
|
75
|
+
import { initSettingsControls } from '../ui/settings-controls.js';
|
|
76
|
+
import { keyName } from '../ui/control-choices.js';
|
|
77
|
+
import { reserveTopBand } from '../ui/top-band.js';
|
|
78
|
+
// The settings store's FACTORY: this root builds the one store it, its modules and its game read (ADR-0232 D4).
|
|
79
|
+
import { createSettingsStore } from '../core/state.js';
|
|
80
|
+
import { DEFAULTS, defaultReducedMotion } from '../core/setting-defaults.js';
|
|
81
|
+
import { CAMERA_CONTROLS } from '../core/camera-cycle.js';
|
|
82
|
+
import { createLibras } from '../ui/vlibras.js';
|
|
62
83
|
import { conformanceProblems } from '../core/contract.js';
|
|
63
|
-
import {
|
|
84
|
+
import { createSceneStack } from '../core/scenes.js';
|
|
64
85
|
import { createTts } from '../platform/tts.js';
|
|
65
|
-
import {
|
|
86
|
+
import { createReading } from '../platform/reading.js';
|
|
87
|
+
import { createAudio } from '../platform/audio.js';
|
|
66
88
|
import { createAudioSonar } from '../platform/audio-sonar.js';
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
import {
|
|
89
|
+
// The root is the layer that MAY know both axes: `render/` is below it, and it is the root's job to answer
|
|
90
|
+
// `platform/audio-sonar`, which cannot import from here without inverting an edge (#104).
|
|
91
|
+
import { isBlind, isLowVision, DEFAULT_VISUAL, filterKey, simulationUnavailable } from '../render/viz-axes.js';
|
|
92
|
+
// 📌 The mode → `url(#...)` table, which `render/cvd-matrices` installs and the `consumer-quiz` consumes.
|
|
93
|
+
import { VIZ_FILTER } from '../render/viz-modes.js';
|
|
94
|
+
import { createPadWizard, createPadMaps } from '../input/pad-wizard.js';
|
|
95
|
+
import { typographyCycle, CYCLE_START, FONT_BY_KEY } from '../ui/fonts.js';
|
|
96
|
+
// 📏 The drawing reporters (`barIntruders`, `belowFloor`, `minimumTarget` and their types) live in `ui/drawing-problems`
|
|
97
|
+
// (ADR-0221, issue #203), and the root does not know them. An import that can be deleted is coupling that no longer
|
|
98
|
+
// exists, and that is how this debt is paid: by subject.
|
|
99
|
+
import { stageScale, applyScale } from '../ui/layout.js';
|
|
100
|
+
import { screenBaseSize } from '../core/screens.js';
|
|
70
101
|
import { OVERLAY_SCOPE_SELECTOR } from '../ui/settings-panel.js';
|
|
102
|
+
import { drawnBelowTheFloor, barIntruderProblems } from '../ui/drawing-problems.js';
|
|
103
|
+
import { createListenerScope } from '../platform/listener-scope.js';
|
|
71
104
|
import { LOGICAL_W } from '../core/constants.js';
|
|
105
|
+
import { captionDuration, CAPTION_RATES } from '../core/caption-duration.js';
|
|
72
106
|
import { initSettingsPanel } from '../ui/settings-panel.js';
|
|
107
|
+
import { mountPanel } from '../ui/mount-panel.js';
|
|
108
|
+
// 📌 No `latchRefusal` or `setMoveLatch` here: the sticky keys are written by the bar's ☝️, which already resolves both
|
|
109
|
+
// the device's refusal and the two stored keys.
|
|
110
|
+
import { stampSource, sourceOfEvent } from '../input/synthetic-source.js';
|
|
111
|
+
import { createVirtualController } from '../input/virtual-controller.js';
|
|
112
|
+
import { createSwitchScan, SWITCH_SCAN_DEFAULTS } from '../input/switch-scan.js';
|
|
113
|
+
import { mountScanOverlay, scanItemText } from '../ui/scan-overlay.js';
|
|
114
|
+
import { createVoiceControl } from '../ui/voice-control.js';
|
|
115
|
+
import { mountSteps, updateSteps, nextStep, controlRow, labelRow } from '../ui/panel-widgets.js';
|
|
116
|
+
import { PERSONAS_DO_PAD, closestPersona } from '../input/touch.js';
|
|
117
|
+
import { initSettingsTypo } from '../ui/settings-typo.js';
|
|
118
|
+
import { initSettingsMotion } from '../ui/settings-motion.js';
|
|
119
|
+
import { readStoredScene, storeScene } from '../ui/motion-scene.js';
|
|
120
|
+
import { initSettingsVisual } from '../ui/settings-visual.js';
|
|
121
|
+
import { initSettingsEmpathy } from '../ui/settings-empathy.js';
|
|
122
|
+
import { HC_ROLE_DEF } from '../render/hc-role-data.js';
|
|
123
|
+
import { createLqFilter } from '../render/lq-filter.js';
|
|
124
|
+
import { createCrt } from '../render/crt.js';
|
|
125
|
+
import { initSettingsAudio, mountAudioInside, mountSoundInside } from '../ui/settings-audio.js';
|
|
126
|
+
import { AUDIO_CATS } from '../platform/audio-mixer.js';
|
|
127
|
+
import { toggleBtn, toggleLabel } from '../ui/dom.js';
|
|
128
|
+
import { createEmpathyFilter } from '../input/empathy-filter.js';
|
|
129
|
+
import { createInputCooldown, COOLDOWN_MS } from '../input/input-cooldown.js';
|
|
130
|
+
import { markChanged } from '../ui/changed-mark.js';
|
|
131
|
+
import { mountHudBands, hudNumbersProblems } from '../ui/hud-bands.js';
|
|
132
|
+
import { gameOptionsProblems, drawGameOptions } from '../ui/game-options.js';
|
|
133
|
+
import { createStorage, keysOutsideScopes } from '../platform/storage.js';
|
|
134
|
+
import { KEYS } from '../platform/storage-keys.js';
|
|
73
135
|
import { initMenuNav } from '../ui/menu-nav.js';
|
|
136
|
+
// 🔴 THE GAMEPAD IS MOUNTED HERE (ADR-0224), like every other transport — no longer by the cartridge.
|
|
137
|
+
import { initGamepad, padGameAnswers, seatEveryPlayer } from '../input/gamepad.js';
|
|
138
|
+
import { whereTheChildIs } from '../ui/where-the-child-is.js';
|
|
139
|
+
import { createSimulationOverTheWorld } from '../ui/simulation-over-the-world.js';
|
|
140
|
+
import { createSimulationList } from '../ui/simulation-list.js';
|
|
141
|
+
import { showOnlyRowsThatApply } from '../ui/audio-rows-that-apply.js';
|
|
74
142
|
import { initKeyboardRuntime } from '../input/keyboard-runtime.js';
|
|
75
|
-
import {
|
|
76
|
-
import {
|
|
77
|
-
import {
|
|
143
|
+
import { createKeyboardConfig } from '../input/keyboard.js';
|
|
144
|
+
import { createPadTable } from '../input/pad-defaults.js';
|
|
145
|
+
import { downloadHeavy, heavyAtBoot } from '../platform/heavy.js';
|
|
78
146
|
import { installCvdFilters } from '../render/cvd-matrices.js';
|
|
147
|
+
/**
|
|
148
|
+
* The backend the host lent, else its window's `localStorage` — or `null` where the window has none or reading it throws
|
|
149
|
+
* (file://, private modes): a host with no storage, whose reads give their fallbacks.
|
|
150
|
+
*/
|
|
151
|
+
function hostStorage(host) {
|
|
152
|
+
if (host.storage)
|
|
153
|
+
return host.storage;
|
|
154
|
+
try {
|
|
155
|
+
return host.win.localStorage ?? null;
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/*
|
|
162
|
+
* ONE SENTENCE FOR BOTH REFUSALS.
|
|
163
|
+
*
|
|
164
|
+
* ⚠️ The boot and `mount()` refuse a malformed declaration for the SAME reason, so with the same sentence: two copies
|
|
165
|
+
* of a sentence are two places for it to drift, and a second translation to do when pillar 3 reaches it (the i18n
|
|
166
|
+
* gate caps this module's raw text).
|
|
167
|
+
*/
|
|
168
|
+
function refuseDeclaration(who, problemas) {
|
|
169
|
+
throw new Error(`${who}: declaração malformada — ${problemas.join('; ')}`);
|
|
170
|
+
}
|
|
79
171
|
/*
|
|
80
|
-
*
|
|
172
|
+
* «start» BELONGS TO THE PAUSE, AND A CARTRIDGE DOES NOT TAKE IT (ADR-0144 §4).
|
|
81
173
|
*
|
|
82
|
-
* ⚠️
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
174
|
+
* ⚠️ THE SENTENCE LIVES IN `core/actions`, not here, for two reasons that point the same way: that is where the
|
|
175
|
+
* validity of an `ActionPreset` lives, and that is where the raw-prose ledger already answers for messages read by
|
|
176
|
+
* whoever WRITES a preset. A new sentence in this module would raise its cap to pay for text that belongs to the input
|
|
177
|
+
* vocabulary, not to the boot.
|
|
178
|
+
*
|
|
179
|
+
* ⚠️ IT THROWS, by the rubric of the two refusals above: this is a PROGRAM defect — the game declared a word for a
|
|
180
|
+
* position that is not its own — and not a gap of the host. `problems` is for what still lets the child play.
|
|
86
181
|
*/
|
|
87
|
-
function
|
|
88
|
-
|
|
182
|
+
function refuseIfItClaimsStart(who, preset) {
|
|
183
|
+
// 📌 AND SELECT TOO, since ADR-0155: the two system positions are the two doors of the pause.
|
|
184
|
+
const problemas = [startClaimProblem(preset), selectClaimProblem(preset)].filter((x) => x !== null);
|
|
185
|
+
if (problemas.length)
|
|
186
|
+
refuseDeclaration(who, problemas);
|
|
89
187
|
}
|
|
90
|
-
/** Os ids que os painéis emprestados exigem do documento. Achado 6: sem eles o painel abre VAZIO, sem erro. */
|
|
91
|
-
const MARCACAO_EXIGIDA = ['#game-region', '#sr-status', '#sr-alert'];
|
|
92
188
|
/**
|
|
93
|
-
*
|
|
189
|
+
* Did the cartridge ANSWER its accommodations? (ADR-0153.) Same rubric as the contract: a missing or incomplete answer is
|
|
190
|
+
* a precondition, not a gap — `problems` is for what still lets the child play, and here the engine would not know which
|
|
191
|
+
* rows to mount.
|
|
192
|
+
*/
|
|
193
|
+
function refuseIfNoAnswer(who, answers) {
|
|
194
|
+
const problemas = accommodationAnswersProblems(answers);
|
|
195
|
+
if (problemas.length)
|
|
196
|
+
refuseDeclaration(who, problemas);
|
|
197
|
+
}
|
|
198
|
+
/** A genre outside the engine's list, or Casino game, refuses the boot (ADR-0156 §2, §4); an absent genre is conformant. */
|
|
199
|
+
function refuseIfGenreRefused(who, genre) {
|
|
200
|
+
const problemas = genreProblems(genre);
|
|
201
|
+
if (problemas.length)
|
|
202
|
+
refuseDeclaration(who, problemas);
|
|
203
|
+
}
|
|
204
|
+
/** A malformed `hud` is a program defect, refused like the declaration (ADR-0169): the engine would not know what to place. */
|
|
205
|
+
function refuseIfHudMalformed(who, hud) {
|
|
206
|
+
const problemas = hudNumbersProblems(hud);
|
|
207
|
+
if (problemas.length)
|
|
208
|
+
refuseDeclaration(who, problemas);
|
|
209
|
+
}
|
|
210
|
+
/** Malformed game options are a program defect, refused like the declaration (ADR-0169): the engine would not know what to draw. */
|
|
211
|
+
function refuseIfOptionsMalformed(who, options) {
|
|
212
|
+
const problemas = gameOptionsProblems(options);
|
|
213
|
+
if (problemas.length)
|
|
214
|
+
refuseDeclaration(who, problemas);
|
|
215
|
+
}
|
|
216
|
+
/** A malformed `howToPlay` is a program defect, refused like the declaration (ADR-0169): the help would not know what to show. */
|
|
217
|
+
function refuseIfHowToPlayMalformed(who, slides) {
|
|
218
|
+
const problemas = howToPlayProblems(slides);
|
|
219
|
+
if (problemas.length)
|
|
220
|
+
refuseDeclaration(who, problemas);
|
|
221
|
+
}
|
|
222
|
+
/** The ids the engine announces and draws into. Without them a child who listens hears nothing (finding 6). */
|
|
223
|
+
const REQUIRED_MARKUP = ['#game-region', '#sr-status', '#sr-alert'];
|
|
224
|
+
/**
|
|
225
|
+
* Where the engine looks for the accessibility bar when the game does not declare `host.a11yBarHost`.
|
|
226
|
+
*
|
|
227
|
+
* ⚠️ NOT IN `REQUIRED_MARKUP` on purpose, and the difference is one of message, not of rigour. That list produces «the
|
|
228
|
+
* page lacks #x», which is what one says of an id the game forgot. What is missing here is not an id — it is the whole
|
|
229
|
+
* bar, and its own sentence can say WHAT is lost, which is what makes it useful to whoever reads it first.
|
|
230
|
+
*/
|
|
231
|
+
const A11Y_BAR_SELECTOR = '#title-icons';
|
|
232
|
+
/**
|
|
233
|
+
* Switches the engine on for a declared game.
|
|
94
234
|
*
|
|
95
|
-
* ⚠️
|
|
96
|
-
*
|
|
97
|
-
* id
|
|
98
|
-
*
|
|
235
|
+
* ⚠️ THROWS if the declaration is malformed, and does NOT throw if markup is missing. The difference is not taste: a
|
|
236
|
+
* wrong declaration is a PROGRAM defect, and a game that runs half-declared is worse than one that does not open; a
|
|
237
|
+
* missing id is a gap of the HOST, and the quiz proved that switching on only the part that serves is legitimate —
|
|
238
|
+
* that is how it declined the pad and the sonar without lying. So one becomes an exception and the other `problems`.
|
|
99
239
|
*/
|
|
100
|
-
const SELETOR_BARRA_A11Y = '#title-icons';
|
|
101
240
|
export function createGame(o) {
|
|
102
241
|
/*
|
|
103
|
-
*
|
|
242
|
+
* THE CURRENT CARTRIDGE, which starts as the options that arrived.
|
|
104
243
|
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* (ADR-0142) — sem ele, as trinta e seis leituras da metade do jogo estão presas ao argumento, e uma raiz
|
|
108
|
-
* de composição a servir vários cartuchos ficaria com o primeiro deles para sempre.
|
|
244
|
+
* 📌 Every read of the game's half goes through `cartridge`, which is the PLACE `mount()` writes (ADR-0142): a read
|
|
245
|
+
* tied to the argument would leave a root serving several cartridges with the first of them forever.
|
|
109
246
|
*/
|
|
110
|
-
let
|
|
111
|
-
const
|
|
112
|
-
if (
|
|
113
|
-
|
|
247
|
+
let cartridge = o;
|
|
248
|
+
const contractProblems = conformanceProblems(cartridge.declaration);
|
|
249
|
+
if (contractProblems.length) {
|
|
250
|
+
refuseDeclaration('createGame', contractProblems);
|
|
114
251
|
}
|
|
115
|
-
|
|
252
|
+
refuseIfItClaimsStart('createGame', cartridge.preset);
|
|
253
|
+
refuseIfNoAnswer('createGame', cartridge.accommodations);
|
|
254
|
+
refuseIfGenreRefused('createGame', cartridge.genre);
|
|
255
|
+
refuseIfHudMalformed('createGame', cartridge.hud);
|
|
256
|
+
refuseIfOptionsMalformed('createGame', cartridge.gameOptions);
|
|
257
|
+
refuseIfHowToPlayMalformed('createGame', cartridge.howToPlay);
|
|
258
|
+
const { doc } = o.host;
|
|
259
|
+
/*
|
|
260
|
+
* 🔴 THE WINDOW THIS ROOT LISTENS ON IS A SCOPED ONE, and it is not plumbing: a root installs about thirty listeners on the
|
|
261
|
+
* window and, until this line existed, NOTHING COULD TAKE THEM OFF. A root whose host was removed from the document kept
|
|
262
|
+
* listening, and because every query it makes is document-wide (`getPauseMenu` below is `doc.querySelector('#vp-pause-0')`) it
|
|
263
|
+
* drove the NEXT root's pause card: measured in the browser, one ArrowDown moved the cursor one item with one root, two with a
|
|
264
|
+
* second, three with a third. `dispose()` is the end of life this had never had. See `platform/listener-scope`.
|
|
265
|
+
*/
|
|
266
|
+
const listeners = createListenerScope(o.host.win);
|
|
267
|
+
const win = listeners.win;
|
|
268
|
+
/*
|
|
269
|
+
* 🔴 AND THE REST OF WHAT THIS ROOT HOLDS ENDS WITH IT (ADR-0220): the window's scope never saw the STATE BUS, which keeps
|
|
270
|
+
* its subscribers for the whole page. So every `state.on` of this root goes through `stateOn`, and whatever a subscription
|
|
271
|
+
* started (the scan's frames, the recogniser, the camera) hands its stop to `whenDisposed`. `dispose()` runs them all;
|
|
272
|
+
* `unmount()` runs none, because it releases the cartridge and the root goes on hearing (ADR-0142).
|
|
273
|
+
*/
|
|
274
|
+
const endOfLife = [];
|
|
275
|
+
const whenDisposed = (release) => { endOfLife.push(release); };
|
|
276
|
+
const stateOn = (evt, fn) => { const off = state.on(evt, fn); whenDisposed(off); return off; };
|
|
277
|
+
/*
|
|
278
|
+
* THE ROOT'S TRANSLATOR (ADR-0232 D3): the page's language, and this root's `t`, markup pass and door to a language change.
|
|
279
|
+
* `localeOn` is that door — like `stateOn`, whatever subscribes through it is released by `dispose()` (ADR-0220).
|
|
280
|
+
*/
|
|
281
|
+
const translator = createTranslator();
|
|
282
|
+
for (const [code, entries] of Object.entries(o.dictionaries ?? {}))
|
|
283
|
+
translator.registerDict(code, entries);
|
|
284
|
+
// 📌 The root's own words go through ITS translator, which reads this game's dictionary (ADR-0232 D3).
|
|
285
|
+
const { t, bcp47 } = translator;
|
|
286
|
+
const localeOn = (react) => { const off = translator.onChange(react); whenDisposed(off); return off; };
|
|
287
|
+
/*
|
|
288
|
+
* 🔴 THE PAGE'S ONE STORE IS BUILT HERE, from what the HOST lends (ADR-0232 point 2, issue #207): the backend the host
|
|
289
|
+
* passed, or its window's `localStorage`. Every module below that persists receives THIS store; none reaches the global.
|
|
290
|
+
* Reading `win.localStorage` can itself THROW (file://, some private modes), which is a host with no storage: `null`.
|
|
291
|
+
*/
|
|
292
|
+
const store = createStorage(hostStorage(o.host));
|
|
293
|
+
// THE CHILD'S STORED SETTINGS, FIRST (ADR-0178): nothing below reads or writes one before this.
|
|
294
|
+
// ⚠️ The port of ADR-0178 carries the key names beside the store, so `core` names no storage place itself.
|
|
295
|
+
const state = createSettingsStore({ ...store, KEYS });
|
|
296
|
+
// THIS ROOT'S INPUT STATE (ADR-0232 D4): held keys and their sources, the transport in use per player, the pads' frames.
|
|
297
|
+
// Destructured so the call sites below read as they did; a game reads the same object as `Engine.input`.
|
|
298
|
+
const input = createInputState();
|
|
299
|
+
const { inputOf, keys, markKeyFrom, releaseKey, playerEdge, letGoOfTheKeyboard } = input;
|
|
300
|
+
/*
|
|
301
|
+
* 🔴 THE STORED LANGUAGE **AND** THE BROWSER'S (ADR-0221 step 7g). `core/i18n` keeps the decisions; the page effects —
|
|
302
|
+
* writing `<html lang>`, dispatching on the window, reading `navigator.language` — come in through these two functions,
|
|
303
|
+
* which are THIS root's host speaking: the document and window it received, never the globals. `core` is what the
|
|
304
|
+
* engine IS without a browser.
|
|
305
|
+
*/
|
|
306
|
+
loadLocale({ ...store, KEYS, ...localeHostHooks(doc, win, translator.applyDom) });
|
|
307
|
+
// 📌 And the debugging exposure: whoever HAS a window is this root.
|
|
308
|
+
exposeI18n(win, i18nObject);
|
|
116
309
|
/*
|
|
117
|
-
* ⚠️
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
* os declínios do cartucho anterior — e um declínio lido errado esconde uma linha de `problems` ou
|
|
121
|
-
* inventa outra.
|
|
310
|
+
* ⚠️ A READER AND NOT A SNAPSHOT. `declines` belongs to the GAME's half (ADR-0139: the cartridge declares what it does
|
|
311
|
+
* NOT have), so a `const` taken at boot would return, after a `mount()`, the previous cartridge's declines — and a
|
|
312
|
+
* decline read wrong hides a line of `problems` or invents one.
|
|
122
313
|
*
|
|
123
|
-
* 📌
|
|
314
|
+
* 📌 The empty value is a constant and not a literal per call: `declines()` is read in places that compare.
|
|
124
315
|
*/
|
|
125
|
-
const
|
|
126
|
-
const declines = () =>
|
|
127
|
-
const
|
|
316
|
+
const NO_DECLINES = {};
|
|
317
|
+
const declines = () => cartridge.declines ?? NO_DECLINES;
|
|
318
|
+
const hostProblems = [];
|
|
128
319
|
const $ = (sel) => doc.querySelector(sel);
|
|
129
320
|
const $$ = (sel) => [...doc.querySelectorAll(sel)];
|
|
130
|
-
|
|
321
|
+
/*
|
|
322
|
+
* THIS ROOT'S ANNOUNCER AND DEAF MODE (ADR-0232 D4): the screen reader's two regions written in the HOST's document on the
|
|
323
|
+
* host's frames, and the Libras interpreter over its store. A window with no frames (a test double) is the announcer's to answer.
|
|
324
|
+
* 📌 DD1: the root does NOT mirror its announcements into Libras — a game connects that sink (`Engine.mirrorAnnouncements`),
|
|
325
|
+
* as before; mirroring everything would be `announcer.mirrorTo(libras.say)` here.
|
|
326
|
+
*/
|
|
327
|
+
const announcer = createAnnouncer({
|
|
328
|
+
doc,
|
|
329
|
+
raf: win.requestAnimationFrame, // as the host has it: the announcer answers a window with none (bound by the listener scope)
|
|
330
|
+
});
|
|
331
|
+
const { say: srSay, alert: srAlert } = announcer;
|
|
332
|
+
const libras = createLibras({ doc, win, store, now: () => Date.now() });
|
|
333
|
+
for (const sel of REQUIRED_MARKUP) {
|
|
131
334
|
if (!$(sel))
|
|
132
|
-
|
|
335
|
+
hostProblems.push(`the page lacks ${sel}: the engine announces and draws into it, and without it a child who listens hears nothing — add it to the page`);
|
|
133
336
|
}
|
|
134
|
-
// ⚠️
|
|
135
|
-
//
|
|
136
|
-
//
|
|
337
|
+
// ⚠️ THE DECLARED WORLD MUST EXIST IN THE DOCUMENT, the gap ADR-0087 would leave open if it stopped at conformance.
|
|
338
|
+
// `conformanceProblems` checks the SHAPE — that there is a selector and it is not empty — and cannot check whether it
|
|
339
|
+
// MATCHES anything, because `core/contract` is pure and sees no DOM.
|
|
137
340
|
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
/*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
341
|
+
// A selector with a typo (`#gaem-region`) passes conformance and produces exactly the defect that record exists to
|
|
342
|
+
// remove: the empathy simulation applied to NOTHING, and an adult told they felt something they did not. It is a
|
|
343
|
+
// HOST problem and not a program one, so it goes into `problems` like the markup — the game opens, and whoever
|
|
344
|
+
// integrated it reads that their world is not there.
|
|
345
|
+
/*
|
|
346
|
+
* `problems` HAS TWO HALVES. The HOST's lines (`hostProblems`) are measured once at boot and hold for as long as the
|
|
347
|
+
* page does: they are about the page, not the game, and do not change when a cartridge is swapped. The CARTRIDGE's
|
|
348
|
+
* lines are measured again at every read, so they follow `mount()`. Recomputing everything would drop host
|
|
349
|
+
* diagnoses nobody fixed; recomputing nothing would leave the diagnosis talking about the wrong game.
|
|
350
|
+
*/
|
|
351
|
+
/**
|
|
352
|
+
* The VIRTUAL CONTROLLER's gaps for this cartridge. Starts empty and is replaced once the pad is mounted, below:
|
|
353
|
+
* `measureCartridgeProblems` runs only when `problems` is read, after boot, but reading the pad from here before it
|
|
354
|
+
* exists would fall into the temporal dead zone.
|
|
355
|
+
*/
|
|
356
|
+
let padGapProblems = () => [];
|
|
357
|
+
/*
|
|
358
|
+
* 🔴 THE RULES LIVE IN `core/cartridge-problems` (ADR-0221 step 7c). What stays here is MEASURING the page and answering
|
|
359
|
+
* that module; what is a decision — the rules, the order of the lines, when to keep quiet — lives where a sentence can
|
|
360
|
+
* be read whole. A composition root is big on purpose and carries WIRING; branches are logic, and ADR-0221's erratum
|
|
361
|
+
* measures a root's debt in branches.
|
|
362
|
+
*/
|
|
363
|
+
function measureCartridgeProblems() {
|
|
364
|
+
const declaredWorld = cartridge.declaration.world();
|
|
365
|
+
const worldCssSelector = declaredWorld.kind === 'element' ? declaredWorld.selector : null;
|
|
366
|
+
return cartridgeProblems({
|
|
367
|
+
padGaps: padGapProblems(),
|
|
368
|
+
resizedRegion: regionResizedByCartridge(),
|
|
369
|
+
drawnBelowFloor: drawnBelowTheFloor(drawingContext),
|
|
370
|
+
genreWarning: genreWarning(cartridge.genre),
|
|
371
|
+
}, {
|
|
372
|
+
worldSelector: worldCssSelector,
|
|
373
|
+
worldIsInPage: !!worldCssSelector && !!$(worldCssSelector),
|
|
374
|
+
wantsNeuralVoice: !!o.uses?.neuralVoice,
|
|
375
|
+
declinesNeuralVoice: !!declines().noNeuralVoice,
|
|
376
|
+
seats: (cartridge.players ?? []).length,
|
|
377
|
+
setsPauseActor: !!cartridge.setPauseActor,
|
|
378
|
+
declinesPauseActor: !!declines().noPauseActor,
|
|
379
|
+
});
|
|
176
380
|
}
|
|
177
|
-
// 1.
|
|
178
|
-
//
|
|
381
|
+
// 1. LANGUAGE BEFORE EVERYTHING. The interface cannot be built before the language is known. The document goes in:
|
|
382
|
+
// see finding 15.
|
|
179
383
|
initI18n(doc);
|
|
180
|
-
|
|
181
|
-
|
|
384
|
+
translator.applyDom(doc); // the host's markup, with THIS game's dictionary too — the module's pass reads only the page's
|
|
385
|
+
// 2. MIXER BEFORE VOICE. Finding 3 turned into sequence: `createAudio` loads the mixer, and the voice reads it.
|
|
386
|
+
// 🔴 THE BROWSER'S SOUND AND SPEECH ARE LENT HERE, from the host's window (ADR-0232 D4): the audio context — made at the
|
|
387
|
+
// first sound, never at boot, so it is born inside the child's gesture and the browser lets it run — and the speech
|
|
388
|
+
// synthesis the voice speaks through. The sonar's per-player contexts come from the same maker.
|
|
389
|
+
const audioHost = win;
|
|
390
|
+
const newAudioContext = () => {
|
|
391
|
+
const AC = audioHost.AudioContext ?? audioHost.webkitAudioContext;
|
|
392
|
+
return AC ? new AC() : null;
|
|
393
|
+
};
|
|
394
|
+
const speechHost = win;
|
|
395
|
+
const speech = {
|
|
396
|
+
synth: () => speechHost.speechSynthesis ?? null,
|
|
397
|
+
utterance: (text) => new speechHost.SpeechSynthesisUtterance(text),
|
|
398
|
+
};
|
|
399
|
+
/*
|
|
400
|
+
* THE ENGINE'S OWN KOKORO LOADER (ADR-0216 §1 and §5) — the Dev, 2026-09-21: «O jogo não deve precisar saber como isso funciona».
|
|
401
|
+
* ⚠️ IMPORTED AT THE FIRST NEURAL UTTERANCE AND NOT BEFORE: `kokoro-runtime` is what names espeak-ng and the ONNX runtime, so a
|
|
402
|
+
* game that never speaks neurally never loads a byte of them — which is the whole reason this is an `import()` and not an
|
|
403
|
+
* import. The page's address, `fetch` and WebAssembly are the host's.
|
|
404
|
+
*/
|
|
405
|
+
const wasmHost = win.WebAssembly;
|
|
406
|
+
const loadKokoro = () => import('../platform/kokoro-runtime.js').then((m) => m.loadKokoroRuntime({
|
|
407
|
+
base: doc.baseURI,
|
|
408
|
+
fetch: (url) => win.fetch(url),
|
|
409
|
+
compileWasm: (bytes) => wasmHost.compile(bytes),
|
|
410
|
+
instantiateWasm: (module, imports) => wasmHost.instantiate(module, imports),
|
|
411
|
+
}));
|
|
412
|
+
const mixer = createAudio({ newContext: newAudioContext, store });
|
|
413
|
+
const { ensureAC, catNode, audioOut, setSoundOn, setVolume, tonePan, setCatGain, setHearingLossGraph } = mixer;
|
|
182
414
|
const tts = createTts({
|
|
183
|
-
srSay, srAlert, ensureAC, catNode, audioOut,
|
|
184
|
-
getSoundOn: () => soundOn, getVolume: () => volume, getAudioCat: () => audioCat,
|
|
185
|
-
|
|
415
|
+
store, translator, srSay, srAlert, ensureAC, catNode, audioOut,
|
|
416
|
+
getSoundOn: () => mixer.soundOn, getVolume: () => mixer.volume, getAudioCat: () => mixer.audioCat,
|
|
417
|
+
neuralVoice: !!o.uses?.neuralVoice, // ADR-0216 §3: the game says it wants one; the engine loads it
|
|
418
|
+
loadKokoro,
|
|
419
|
+
getSpeechPpm: () => state.speechPpm, // ADR-0183 §1: the child's speech rate
|
|
420
|
+
createAudio: () => doc.createElement('audio'),
|
|
421
|
+
speech,
|
|
422
|
+
now: () => win.performance.now(),
|
|
186
423
|
});
|
|
187
|
-
// 📌
|
|
424
|
+
// 📌 The neural-voice line lives in `measureCartridgeProblems()`: the decline that silences it is the game's.
|
|
188
425
|
/**
|
|
189
|
-
*
|
|
426
|
+
* THE VISION FILTER, applied to THE WORLD THE GAME DECLARED (ADR-0087).
|
|
190
427
|
*
|
|
191
|
-
* ⚠️
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
428
|
+
* ⚠️ THE RULE OF MENUS IS A GENERALISATION, not a second rule. A CSS filter is inherited, so a menu inside the world
|
|
429
|
+
* would be filtered with it — a blindness simulation would black out the pause menu and lock the child inside it
|
|
430
|
+
* (#82). A world with nothing inside it (the 15-puzzle) is untouched by the same line. One code serves both shapes
|
|
431
|
+
* because it asks the DOM instead of assuming the shape: it clears the filter on the overlays that ARE inside the
|
|
432
|
+
* world.
|
|
196
433
|
*
|
|
197
|
-
* ⚠️ `{kind:'none'}`
|
|
198
|
-
*
|
|
434
|
+
* ⚠️ `{kind:'none'}` APPLIES NOTHING. An activity with no space has no world to simulate, and painting a filter over
|
|
435
|
+
* it would be the lie ADR-0087 exists to prevent, only backwards.
|
|
199
436
|
*/
|
|
200
|
-
function
|
|
201
|
-
const
|
|
202
|
-
if (
|
|
437
|
+
function setVisionFilter(css, reach) {
|
|
438
|
+
const declaredWorld = cartridge.declaration.world();
|
|
439
|
+
if (declaredWorld.kind !== 'element')
|
|
203
440
|
return;
|
|
204
|
-
const el = $(
|
|
441
|
+
const el = $(declaredWorld.selector);
|
|
205
442
|
if (!el)
|
|
206
|
-
return; //
|
|
443
|
+
return; // already reported in `problems`; no surface is invented
|
|
207
444
|
el.style.filter = css;
|
|
208
|
-
if (
|
|
209
|
-
//
|
|
210
|
-
//
|
|
445
|
+
if (reach === 'mundo') {
|
|
446
|
+
// Menus live ABOVE the simulation and are the way out of it: if they inherited the filter by being inside the
|
|
447
|
+
// world, it is undone on them.
|
|
211
448
|
for (const ov of $$(OVERLAY_SCOPE_SELECTOR)) {
|
|
212
449
|
if (el.contains(ov))
|
|
213
450
|
ov.style.filter = '';
|
|
214
451
|
}
|
|
215
452
|
}
|
|
216
453
|
}
|
|
217
|
-
|
|
218
|
-
|
|
454
|
+
/*
|
|
455
|
+
* THE WORLD'S FILTER IS A COMPOSITION: the 🚥's colour correction and the L→Q contrast enhancement (ADR-0151). Written
|
|
456
|
+
* apart, the second writer erased the first — turning the enhancement on turned off the correction a colour-blind
|
|
457
|
+
* child had set. Each writer keeps its part and asks for the composition.
|
|
458
|
+
*/
|
|
459
|
+
// The visual state the WORLD shows: the correction (🚥) and the simulation (empathy mode) are two fields of it, and
|
|
460
|
+
// `filterKey` already knows the simulation runs only with the correction at its default (ADR-0076).
|
|
461
|
+
let worldState = DEFAULT_VISUAL;
|
|
462
|
+
/*
|
|
463
|
+
* A DISABILITY SIMULATION RUNS IN THE GAME, NEVER IN A MENU (issue #182). The Dev: «Simulação de deficiência não pode
|
|
464
|
+
* funcionar no menu! Só no jogo! Senão fica impossível desabilitar em certos casos.» Two rules make it so:
|
|
465
|
+
* · SUSPENDED while a menu is open — the pause card, a panel, the quick pause — and back when the child returns to play;
|
|
466
|
+
* · NEVER ON AN ANCESTOR OF A MENU: a CSS filter reaches every descendant, and clearing it on the child does not undo it.
|
|
467
|
+
* 📏 In the quiz the world is the whole region, menus inside it: a simulated blindness blacked out the empathy panel
|
|
468
|
+
* where it is turned off. So the filter goes on the world's parts that hold no menu, down to the world itself when
|
|
469
|
+
* it holds none (a canvas world).
|
|
470
|
+
*/
|
|
471
|
+
/** Assigned once the menus exist (below): until then no menu can be open. */
|
|
472
|
+
let simulationSuspended = () => false;
|
|
473
|
+
/*
|
|
474
|
+
* 🔴 WHERE THE SIMULATION LANDS lives in `ui/simulation-over-the-world` (ADR-0221 step 7c). Going down into a box that
|
|
475
|
+
* holds a door, sending the layer to a canvas's parent because a canvas has no children, keeping what HELPS under what
|
|
476
|
+
* SIMULATES: those are rules with reasons, not wiring.
|
|
477
|
+
*/
|
|
478
|
+
const simulationOverWorld = createSimulationOverTheWorld({
|
|
479
|
+
world: () => cartridge.declaration.world(),
|
|
480
|
+
find: (sel) => $(sel),
|
|
481
|
+
createCanvas: () => doc.createElement('canvas'),
|
|
482
|
+
});
|
|
483
|
+
function recomposeWorldFilter() {
|
|
484
|
+
// what HELPS (the colour correction, the contrast enhancement) stays on the world as before; the SIMULATION is laid apart
|
|
485
|
+
const enhancementKey = filterKey({ ...worldState, simulacao: null });
|
|
486
|
+
const enhancement = [enhancementKey ? (VIZ_FILTER[enhancementKey] ?? '') : '', lq.filter()].filter(Boolean).join(' ');
|
|
487
|
+
setVisionFilter(enhancement, 'mundo');
|
|
488
|
+
const simulation = simulationSuspended() ? null : worldState.simulacao;
|
|
489
|
+
simulationOverWorld.onlyInPlay(simulation ? (VIZ_FILTER[simulation] ?? '') : '', enhancement);
|
|
490
|
+
simulationOverWorld.drawLayer(simulation);
|
|
491
|
+
applyCrt(); // the decorative CRT yields to every visual mode, and comes back when none is on (ADR-0047)
|
|
492
|
+
}
|
|
493
|
+
/*
|
|
494
|
+
* THE CRT, applied by the engine (study items A5, B1). 📏 Measured: the panel said «Scanlines: on» and the region had no
|
|
495
|
+
* CRT class until a toggle was pressed — `render/crt` was never started under `createGame`. It yields to a colour
|
|
496
|
+
* correction, a simulation and the contrast enhancement; its scanlines are re-anchored to real pixels at every scale.
|
|
497
|
+
*/
|
|
498
|
+
const crt = createCrt({
|
|
499
|
+
region: () => $('#game-region'),
|
|
500
|
+
win,
|
|
501
|
+
// `cartucho` and not `players()`: this runs at boot, above the `players` declaration (temporal dead zone)
|
|
502
|
+
numPlayers: () => Math.max(1, (cartridge.players ?? []).length),
|
|
503
|
+
a11yVisualOn: () => filterKey(worldState) !== null || lq.t() > 0,
|
|
504
|
+
store,
|
|
505
|
+
});
|
|
506
|
+
const { apply: applyCrt, scanVars: crtScanVars } = crt;
|
|
507
|
+
const lq = createLqFilter({ doc, onChange: recomposeWorldFilter, store });
|
|
508
|
+
if (lq.t() > 0)
|
|
509
|
+
recomposeWorldFilter(); // the stored enhancement holds from boot
|
|
510
|
+
else
|
|
511
|
+
applyCrt(); // and the stored CRT too (the recompose above applies it when it runs)
|
|
512
|
+
// 3. The dialog stack. The ctx is the same in every game — it is boilerplate, and repeated boilerplate is where
|
|
513
|
+
// consumers diverge without meaning to.
|
|
219
514
|
const overlays = initSettingsPanel({
|
|
220
|
-
$, $$, doc,
|
|
515
|
+
t: translator.t, $, $$, doc,
|
|
221
516
|
computedZ: (el) => +win.getComputedStyle(el).zIndex || 0,
|
|
222
517
|
});
|
|
223
|
-
// 4.
|
|
518
|
+
// 4. Colour vision: the engine HANDS OVER the markup instead of making the consumer guess it (finding 7).
|
|
224
519
|
const cvdFilters = installCvdFilters(o.host.cvdHost ?? null);
|
|
225
520
|
if (!cvdFilters)
|
|
226
|
-
|
|
227
|
-
// 4c.
|
|
228
|
-
//
|
|
229
|
-
|
|
230
|
-
const a11yBar = o.host.a11yBarHost ?? $(SELETOR_BARRA_A11Y);
|
|
521
|
+
hostProblems.push('there is no filter host (<svg>): colour-vision correction was not mounted, so a colour-blind child cannot turn it on — set `host.cvdHost`');
|
|
522
|
+
// 4c. THE FIRST SCREEN'S ACCESSIBILITY BAR. See the note on `EngineHost.a11yBarHost`. This block only reports that it
|
|
523
|
+
// is missing, with a sentence that names the way out; the bar is mounted further below, once the icons exist.
|
|
524
|
+
const a11yBar = o.host.a11yBarHost ?? $(A11Y_BAR_SELECTOR);
|
|
231
525
|
if (!a11yBar) {
|
|
232
|
-
|
|
526
|
+
hostProblems.push(`there is no accessibility bar on the first screen: a child cannot reach blind mode, narration or Libras before starting — set \`host.a11yBarHost\` or put a ${A11Y_BAR_SELECTOR} in the page`);
|
|
527
|
+
}
|
|
528
|
+
/*
|
|
529
|
+
* ⚠️ THE ENGINE MOUNTS IT (ADR-0106 §4, step 2): step 1 took from the game the duty of answering the seven incidental
|
|
530
|
+
* fields, and step 3 gave the menu a default list, so `PauseIconsCtx` demands nothing this root cannot answer.
|
|
531
|
+
*
|
|
532
|
+
* ⚠️ IT IS NOT `buildQuickBar`, and the difference has a reason: that one sets `tabIndex = -1` on the buttons because
|
|
533
|
+
* during play ten tab stops separate the child from the game (ADR-0044 item 7). On the first screen nobody is playing,
|
|
534
|
+
* and taking the icons out of the tab order there would hide them from whoever navigates by keyboard — exactly the
|
|
535
|
+
* person they exist for.
|
|
536
|
+
*/
|
|
537
|
+
/**
|
|
538
|
+
* THE BLIND-MODE READER — and it must read WHERE THE DEFAULT WRITER WRITES.
|
|
539
|
+
*
|
|
540
|
+
* 🔴 The writer's default is the engine's (`ui/pause-icons`: `ctx.setBlindMode ?? setBlindModeValue`), which stores
|
|
541
|
+
* in `core/state`. A reader that answered a CONSTANT `false` made blind mode impossible to turn off in a game that does
|
|
542
|
+
* not inject `isBlindMode`: the first press turned it on; the reflection read `false` and the icon said off; the
|
|
543
|
+
* second press wrote `true` AGAIN, `core/state`'s equality guard returned early, and nothing happened. A game that
|
|
544
|
+
* starts describing everything aloud and never stops, with no error anywhere.
|
|
545
|
+
*
|
|
546
|
+
* 📌 ONE CONSTANT AND NOT THE EXPRESSION REPEATED IN TWO PLACES, because repetition IS the defect: two answers to the
|
|
547
|
+
* same question drift. The store's `blindMode` is a LIVE getter, so this reads the value of now and not of boot.
|
|
548
|
+
*/
|
|
549
|
+
const readBlindMode = cartridge.isBlindMode ?? (() => state.blindMode);
|
|
550
|
+
/**
|
|
551
|
+
* WHAT THE ENGINE CAN ACT ON BY ITSELF IN THE PAUSE MENU — filled below, read when the pause opens.
|
|
552
|
+
*
|
|
553
|
+
* 🔴 Without it, a game that calls only `createGame` got a pause card of one item: with an empty table
|
|
554
|
+
* `itemsThatAct` keeps only the `ENGINE_ITEMS`, and `rootThatActs` also drops `options` — «uma porta para uma sala
|
|
555
|
+
* vazia». A pause menu with one item is not a pause menu.
|
|
556
|
+
*
|
|
557
|
+
* ⚠️ MUTABLE AND READ LATE, on purpose, which is what the field's laziness is for: `initPauseIcons` runs here and the
|
|
558
|
+
* panels mount further below. `refreshPauseItems` evaluates it when the pause OPENS, not at mounting; reading the
|
|
559
|
+
* table now would freeze an empty object.
|
|
560
|
+
*
|
|
561
|
+
* 📌 AND THE CARTRIDGE OVERRIDES, not the other way round: a game that brings its own entry wins over the engine's.
|
|
562
|
+
* What ADR-0122 makes non-declinable is that the pause EXISTS, not that the engine owns every item in it.
|
|
563
|
+
*/
|
|
564
|
+
const engineActions = {};
|
|
565
|
+
/** Opens the game options panel, once mounted (ADR-0182). Offered as the door's action only while the cartridge declares rows. */
|
|
566
|
+
let openGameOptions = null;
|
|
567
|
+
let redrawGameOptions = () => { };
|
|
568
|
+
/*
|
|
569
|
+
* ===================== THE WAY OUT, BORN WITH THE WAY IN (ADR-0144, erratum) =====================
|
|
570
|
+
*
|
|
571
|
+
* 🔴 `resume` is not in `ENGINE_ITEMS`, so without this entry `itemsThatAct` cut «continuar» from the card of EVERY game
|
|
572
|
+
* that calls only `createGame`; and Escape at the card's root calls `setPhase('playing')` (`ui/menu-nav`), which with
|
|
573
|
+
* no hook was a no-op. Both go through `changePhase` now.
|
|
574
|
+
*
|
|
575
|
+
* ⚠️ OPENING A DOOR WITH NO WAY OUT IS WORSE THAN NOT OPENING IT. It is ADR-0106 §5 in so many words, and the child
|
|
576
|
+
* stuck on the card would be precisely the one who navigates without seeing, who has no mouse to fall back on.
|
|
577
|
+
*
|
|
578
|
+
* 📌 It also unlocks a third thing: `enterBarMode` in `ui/pause-icons` calls `resume` before handing the directional
|
|
579
|
+
* to the bar, so ADR-0044's item 7 is reachable from any game.
|
|
580
|
+
*
|
|
581
|
+
* 📌 THE CARTRIDGE STILL OVERRIDES (the order of the spread in `getPauseActs` does not change): a game with its own
|
|
582
|
+
* «continuar» — because resuming there means unfreezing physics, resuming audio and more — wins over this one. What the
|
|
583
|
+
* engine guarantees is that there is ALWAYS one.
|
|
584
|
+
*/
|
|
585
|
+
// The screen footer (see `screenFooter`): declared HERE, before the first `changePhase`, which already clears it.
|
|
586
|
+
let footer = null;
|
|
587
|
+
let barExplanation = null;
|
|
588
|
+
// The quick-pause state and the button legend, declared before the first `changePhase` too: its `pauseControls.hide`
|
|
589
|
+
// refreshes the legend (ADR-0164 rule 3), and reading them earlier would be a temporal-dead-zone error at boot.
|
|
590
|
+
const inQuickPause = new Set();
|
|
591
|
+
// the menus and the quick pause exist from here on: a simulation is suspended while one is open (issue #182)
|
|
592
|
+
simulationSuspended = () => isMenuOpen() || inQuickPause.size > 0;
|
|
593
|
+
let pauseCaption = null;
|
|
594
|
+
function changePhase(p) {
|
|
595
|
+
// ⚠️ THE ENGINE CLOSES ITS CARD; THE GAME STILL DECIDES THE WORLD. It is the exact symmetry of ADR-0144 §2 from the
|
|
596
|
+
// other side: there the engine reveals and ASKS for the pause, here it hides and ASKS for the resume.
|
|
597
|
+
if (p !== 'paused') {
|
|
598
|
+
pauseControls.hide(0);
|
|
599
|
+
writeInFooter(null);
|
|
600
|
+
} // an item's reason does not stay over the game
|
|
601
|
+
cartridge.setPhase?.(p);
|
|
233
602
|
}
|
|
603
|
+
engineActions.resume = () => changePhase('playing');
|
|
234
604
|
/*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
605
|
+
* QUIT — «voltar à tela de press start» (the Dev's decision, 2026-09-12; erratum of ADR-0144 §5).
|
|
606
|
+
*
|
|
607
|
+
* 🔴 ADR-0144 §5 had left this OPEN on purpose, because the wrong answer loses a child's game: reload? `history.back()`?
|
|
608
|
+
* an activities menu that may not exist? The answer is none of the three — it is a PHASE the project already has a
|
|
609
|
+
* name and a contract for.
|
|
610
|
+
*
|
|
611
|
+
* ⚠️ AND THE EDGE IS STATED: `ui/shell`, which draws that screen, is deliberately not mounted by this root. The engine
|
|
612
|
+
* hides its card and ASKS for the phase; a game without the hook stays where it is. It is the same asymmetry as
|
|
613
|
+
* `setPhase('paused')` in ADR-0144 §2, and that is why the gate asserts the CALL.
|
|
239
614
|
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
615
|
+
* 📌 NO CONFIRMATION, and that is a choice, not an omission. The ring puts `quit` one step from `resume` (ADR-0044
|
|
616
|
+
* item 1), which makes it easy to reach by mistake — but ADR-0037 decides: this project saves nothing, so what is lost
|
|
617
|
+
* is the current round and not progress. A confirmation dialog would cost one more stop in the scan of EVERY exit to
|
|
618
|
+
* protect what does not exist.
|
|
244
619
|
*/
|
|
620
|
+
engineActions.quit = () => changePhase('title');
|
|
621
|
+
/*
|
|
622
|
+
* PRINT — «ver a tela sem menus», and any button comes back.
|
|
623
|
+
*
|
|
624
|
+
* 📌 `ui/shell.printMode` does this, and `ui/shell` is not mounted by this root. But it needs no phase machine: it needs
|
|
625
|
+
* the cards, the window and the announcement — three things the engine has. Rewritten here with the SAME behaviour,
|
|
626
|
+
* including the delay.
|
|
627
|
+
*
|
|
628
|
+
* ⚠️ THE 80 ms ARE NOT SUPERSTITION: without them, the very event that TRIGGERED print is what undoes it — the child
|
|
629
|
+
* presses once and sees the clean screen blink.
|
|
630
|
+
*
|
|
631
|
+
* 📌 IN CAPTURE, not bubbling, because the point is to intercept BEFORE anyone else: in print mode the key belongs
|
|
632
|
+
* neither to the game nor to the pause, it is the way out. 🎯 And it does not collide with the `start` hook (ADR-0144),
|
|
633
|
+
* which bubbles: `goBack` reveals the card in the capture, and when the bubbling one arrives the card-already-open
|
|
634
|
+
* guard sends it away.
|
|
635
|
+
*/
|
|
636
|
+
engineActions.print = () => {
|
|
637
|
+
const findPauseCard = () => $('#vp-pause-0');
|
|
638
|
+
const eventTarget = findPauseCard();
|
|
639
|
+
if (!eventTarget)
|
|
640
|
+
return;
|
|
641
|
+
eventTarget.hidden = true;
|
|
642
|
+
const goBack = (e) => {
|
|
643
|
+
if (e && typeof e.preventDefault === 'function') {
|
|
644
|
+
try {
|
|
645
|
+
e.preventDefault();
|
|
646
|
+
}
|
|
647
|
+
catch { /* noop */ }
|
|
648
|
+
}
|
|
649
|
+
win.removeEventListener('keydown', goBack, true);
|
|
650
|
+
win.removeEventListener('pointerdown', goBack, true);
|
|
651
|
+
const c = findPauseCard();
|
|
652
|
+
if (c)
|
|
653
|
+
c.hidden = false;
|
|
654
|
+
};
|
|
655
|
+
win.setTimeout(() => {
|
|
656
|
+
win.addEventListener('keydown', goBack, true);
|
|
657
|
+
win.addEventListener('pointerdown', goBack, true);
|
|
658
|
+
}, 80);
|
|
659
|
+
srSay(t('sr.print.on'));
|
|
660
|
+
};
|
|
245
661
|
/**
|
|
246
|
-
*
|
|
662
|
+
* THE HEARING PANEL, resolved late and read early — the same laziness as `engineActions` above, for the same reason:
|
|
663
|
+
* `initPauseIcons` runs here and the panels mount further below.
|
|
247
664
|
*
|
|
248
|
-
* 🔴
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
665
|
+
* 🔴 IT IS A `let` BECAUSE OF A DEFECT KEPT VERBATIM. `ui/pause-icons` documents it: in the monolith the call that
|
|
666
|
+
* refreshed the narration row sat behind `typeof reflectTTS === 'function'`, a symbol that no longer existed, «so it
|
|
667
|
+
* never fires». The guard was ported as `reflectTtsPanelEnabled`, defaulting to `false`, so as not to fix it
|
|
668
|
+
* silently — and with a field to switch it back on.
|
|
669
|
+
*
|
|
670
|
+
* 🎯 THE ENGINE CAN: it mounts the panel, so it has the `reflectTts` to hand over. Without it, the child turns
|
|
671
|
+
* narration on with the bar's 🗣 icon and the panel goes on saying it is off — the family of defect where a control
|
|
672
|
+
* lies about its state.
|
|
673
|
+
*/
|
|
674
|
+
let audio = null;
|
|
675
|
+
/*
|
|
676
|
+
* ⚠️ HOISTED ABOVE `initPauseIcons`, and the reason is ORDER, not tidiness: the bar decides WHICH ICONS it mounts at
|
|
677
|
+
* boot (`iconsThatAct`, resolved once), and the 11th — the typography cycle — exists only if the typography writer
|
|
678
|
+
* exists. That writer is born inside `if (pauseMountPoint && pauseUsable)`, further below; read there, the bar would
|
|
679
|
+
* already have decided.
|
|
252
680
|
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
|
|
256
|
-
|
|
681
|
+
* 📌 Hoisting instead of repeating the question: `o.host.pauseHost ?? $('#game-region')` written in two places would be
|
|
682
|
+
* the same answer with two sources.
|
|
683
|
+
*/
|
|
684
|
+
const pauseMountPoint = o.host.pauseHost ?? $('#game-region');
|
|
685
|
+
const pauseUsable = !!pauseMountPoint && typeof pauseMountPoint.appendChild === 'function';
|
|
686
|
+
/*
|
|
687
|
+
* ⚠️ `typo` IS HOISTED TOO, for the same reason: the bar's typography cycle, decided earlier, must reach it. It is still
|
|
688
|
+
* assigned below; what changed is the SCOPE, not the moment.
|
|
689
|
+
*/
|
|
690
|
+
let typo = null;
|
|
691
|
+
/** The typography cycle's current position. See the note on `cycleTypography`, below. */
|
|
692
|
+
let typographyStep = CYCLE_START;
|
|
693
|
+
/*
|
|
694
|
+
* 🔴 THE PLAYERS ARE HOISTED HERE (issue #147), for an order-of-boot reason: `initPauseIcons` consumes `getPlayers()`
|
|
695
|
+
* EAGERLY (when building the audio sub-ctx), so a `players` declared lower down fell into the temporal dead zone and
|
|
696
|
+
* brought the boot down. And with the bar reading `cartridge.players ?? []` instead, a game that declares no players
|
|
697
|
+
* had ZERO seats for the bar and ONE for the keyboard, and the bar's cycles (🚥 correction, ☝️, ASD) stuck on the
|
|
698
|
+
* first position: the state had nowhere to be kept and each press re-read the default.
|
|
699
|
+
*/
|
|
700
|
+
// ⚠️ THE BOOT SCHEME REACHES NOTHING, and says so with `null` instead of with an empty object (issue #118). It lives an
|
|
701
|
+
// instant — `assignControls()` just below replaces it with the real scheme — but while it lives it is a `KeyScheme` like
|
|
702
|
+
// any other, and the only honest form of a scheme that reaches nothing is fourteen declared absences. A `{}` made the
|
|
703
|
+
// type lie about being complete.
|
|
704
|
+
const withoutReach = Object.fromEntries(ACTIONS.map((a) => [a, null]));
|
|
705
|
+
// ⚠️ THE FALLBACK IS A CONSTANT and not a new literal per call: `getPlayers` is read by the keyboard runtime at every
|
|
706
|
+
// control read, and returning a new array each time would make any identity comparison lie — a defect that shows
|
|
707
|
+
// only in whoever compares, and late.
|
|
708
|
+
const withoutPlayers = [{ ctrl: withoutReach }];
|
|
709
|
+
// ⚠️ IT READS `cartridge.players`, NOT A SNAPSHOT: with several cartridges on one composition root (ADR-0142), a `const`
|
|
710
|
+
// taken at boot would leave the keyboard with the players of the cartridge that booted first.
|
|
711
|
+
const players = () => cartridge.players ?? withoutPlayers;
|
|
712
|
+
/*
|
|
713
|
+
* THE COLOUR-BLIND-SAFE PALETTE IN THE MENUS AND THE HUD (ADR-0151) — Okabe-Ito, through `:root[data-paleta]`.
|
|
257
714
|
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
715
|
+
* 📌 `core/state.cbSafe` stores, persists and notifies; this is its writer on the page. The colours, and the measure
|
|
716
|
+
* that chose them, are in the stylesheet (`style.css`, beside `data-cursiva`).
|
|
260
717
|
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
718
|
+
* 🎯 THE RULE IS THE DEV'S, in their words: «ativada automaticamente quando se liga correção para protano,
|
|
719
|
+
* deutero e tritanopia e desativada automaticamente quando muda para visão padrão (tricromática). Aqui se
|
|
720
|
+
* permite ativá-la sem usar o filtro.» So the state is ONE (`cbSafe`), and the correction only pushes it.
|
|
721
|
+
*/
|
|
722
|
+
const applySafePalette = (on) => {
|
|
723
|
+
if (on)
|
|
724
|
+
doc.documentElement.dataset.paleta = 'okabe-ito';
|
|
725
|
+
else
|
|
726
|
+
delete doc.documentElement.dataset.paleta;
|
|
727
|
+
};
|
|
728
|
+
applySafePalette(state.cbSafe);
|
|
729
|
+
stateOn('cbSafe', (v) => applySafePalette(Boolean(v)));
|
|
730
|
+
/**
|
|
731
|
+
* Wraps ANY correction writer — the cartridge's or the engine's: the palette follows the correction whoever applies it.
|
|
732
|
+
* ⚠️ Wrapping only the engine's would leave a game that corrects in its own render (`game-pinball`) without the palette
|
|
733
|
+
* the child asked for by pressing the same icon.
|
|
734
|
+
*/
|
|
735
|
+
function withSafePalette(write) {
|
|
736
|
+
return (i, correction) => {
|
|
737
|
+
write(i, correction);
|
|
738
|
+
state.setCbSafeValue(correction !== 'tricro');
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
// 📌 ONE DOOR FOR BOTH, and the name says so since 2026-09-21: getUserMedia is what a page has to ask the camera AND the
|
|
742
|
+
// microphone for. It used to be called «temCamera» and the 👄 read it anyway — a name that describes half of what it answers is
|
|
743
|
+
// how a device with a headset and no webcam would have lost the voice for a reason nobody could see in the code.
|
|
744
|
+
const canCaptureMedia = typeof win.navigator?.mediaDevices?.getUserMedia === 'function';
|
|
745
|
+
/*
|
|
746
|
+
* ONE SET OF SCENE REDUCED-MOTION FLAGS, built here and handed to BOTH writers — the quick bar's calm icon and the motion
|
|
747
|
+
* panel (ADR-0232: state is built by the root). Left to themselves each read its own copy from storage, so the panel
|
|
748
|
+
* showed the scene animated after the calm mode reduced it, and its next switch stored that stale copy over the calm mode.
|
|
749
|
+
* The object is mutated in place; its identity is what the two share.
|
|
264
750
|
*/
|
|
265
|
-
const
|
|
751
|
+
const sceneMotion = readStoredScene(store, defaultReducedMotion(win.matchMedia));
|
|
752
|
+
const saveSceneMotion = () => { storeScene(store, sceneMotion); };
|
|
266
753
|
const pauseIcons = initPauseIcons({
|
|
267
|
-
|
|
754
|
+
translator, store,
|
|
755
|
+
settings: state, // the page's settings store itself: its live bindings are the reads the bar asks for (ADR-0232)
|
|
756
|
+
doc, matchMedia: win.matchMedia, // the reduced-motion default when nothing is stored (ADR-0232)
|
|
757
|
+
rm: sceneMotion, saveRM: saveSceneMotion,
|
|
268
758
|
/*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
* ⚠️
|
|
272
|
-
* ADR-0084
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
* 📌
|
|
276
|
-
*
|
|
277
|
-
* aparece quando se monta a barra.
|
|
759
|
+
* THE GAME'S ANSWER, read from the declaration (ADR-0115). Without it the `altmove` icon is not mounted.
|
|
760
|
+
*
|
|
761
|
+
* ⚠️ WHICH ICONS THE BAR HAS IS DECIDED ONCE, AT BOOT, and the reason is not economy — it is the child. The field is
|
|
762
|
+
* a FUNCTION because ADR-0084 says a game changes its demands between phases, but the bar's COMPOSITION must not
|
|
763
|
+
* change under the hand of whoever is using it: an icon that comes and goes between phases is worse than one that
|
|
764
|
+
* was never there, and for someone navigating by keyboard it shifts the tab order midway.
|
|
765
|
+
* 📌 So the right reading of the contract is: this game holds keys in SOME phase — a game that holds keys on foot
|
|
766
|
+
* and nothing inside a vehicle declares `true`.
|
|
278
767
|
*/
|
|
279
|
-
// ⚠️
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
768
|
+
// ⚠️ THE REFERENCE, not the result: the ☝️'s cycle reads it again at each press, and with several cartridges on one
|
|
769
|
+
// composition root (ADR-0142) it must describe the mounted one.
|
|
770
|
+
holdsKeys: () => cartridge.declaration.holdsKeys(),
|
|
771
|
+
// how many positions this cartridge declared — what «one button only» would have to offer (ADR-0218, issue #201)
|
|
772
|
+
declaredPositions: () => (cartridge.preset ? presetActions(cartridge.preset).length : 0),
|
|
773
|
+
// the hourglass is offered where time runs by itself (ADR-0180), read per cartridge
|
|
774
|
+
clock: () => cartridge.declaration.tick === 'clock',
|
|
775
|
+
// the 📷 is offered where there is a camera to ask for (ADR-0215); the three camera controls below follow its position
|
|
776
|
+
camera: canCaptureMedia,
|
|
777
|
+
// and the 👄 where there is a MICROPHONE (issue #184) — the same door as the camera's, so the same answer
|
|
778
|
+
microphone: canCaptureMedia,
|
|
779
|
+
// the ☰, the bar's first icon (interface log 2026-09-16): the SELECT door, where there is a card to open. Hoisted, read at the press.
|
|
780
|
+
...(pauseUsable ? { openMenus: (i) => { openSeatMenus(i); } } : {}),
|
|
781
|
+
// no voice speaks the current language: the narration icon locks like the panel's rows (ADR-0185)
|
|
782
|
+
noVoice: () => tts.voices().length === 0,
|
|
783
|
+
/*
|
|
784
|
+
* ✅ THE SAME LIST AS THE KEYBOARD (issue #147).
|
|
785
|
+
*
|
|
786
|
+
* 📏 With an empty list, `iconAct('cvd', i)` re-read `(P()[i] || {}).visual` as `undefined` at every press: the cycle
|
|
787
|
+
* STUCK on the first position while the icon announced different corrections — and the same for the ☝️ and ASD.
|
|
788
|
+
* Since ADR-0151 the correction also switches the safe palette, which is PERSISTED, so it stuck for good.
|
|
789
|
+
*
|
|
790
|
+
* ⚠️ `initPauseIcons` consumes this EAGERLY, which is why `players` is hoisted above this call. A game that declares
|
|
791
|
+
* no players has ONE child playing, not none.
|
|
792
|
+
*/
|
|
793
|
+
getPlayers: () => players(),
|
|
794
|
+
getNumPlayers: () => players().length,
|
|
285
795
|
srSay, srAlert,
|
|
286
|
-
//
|
|
287
|
-
|
|
288
|
-
//
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
796
|
+
// Leaving the bar is leaving the quick pause, by any door (ADR-0155). Hoisted function, read when called.
|
|
797
|
+
onLeaveBar: (i, silent) => endQuickPause(i, silent),
|
|
798
|
+
// What the pointed icon DOES goes to the footer (hoisted function, read when called).
|
|
799
|
+
explainIcon: (_i, k) => explainIconInFooter(k),
|
|
800
|
+
explainItem: (text) => writeInFooter(text),
|
|
801
|
+
// ⚠️ NOT `instanceof HTMLElement`: that is a BROWSER GLOBAL, and reading it where it does not exist THROWS — it does
|
|
802
|
+
// not return false. It is the same shape of error as FINDING 15 in this file's header: reaching the global underneath
|
|
803
|
+
// whoever injected the document. The right question is the one `barUsable` asks — can it be a bar?
|
|
804
|
+
getA11yBars: () => (barUsable && a11yBar ? [a11yBar] : []),
|
|
805
|
+
getBlindMode: readBlindMode,
|
|
806
|
+
getAudioCat: () => mixer.audioCat,
|
|
293
807
|
setCatGain,
|
|
294
808
|
/*
|
|
295
|
-
* ⚠️
|
|
296
|
-
*
|
|
297
|
-
*
|
|
809
|
+
* ⚠️ THE ROOT ANSWERS FOR THE DEVICE IN USE, and here ADR-0109's automaton gets its reader. `input/state.inputOf(i)`
|
|
810
|
+
* returns `DEFAULT_INPUT_STATE` for whoever never produced an edge, so this is never `undefined` and the icon never
|
|
811
|
+
* writes to a crooked key.
|
|
298
812
|
*
|
|
299
|
-
* 📌
|
|
300
|
-
*
|
|
813
|
+
* 📌 And the ROOT passes it rather than the icon importing it: `ui/` reading module state from `input/` would be a new
|
|
814
|
+
* edge between layers to save one argument. Composition is this file's job.
|
|
301
815
|
*/
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
816
|
+
transportInUse: (i) => inputOf(i).inUse,
|
|
817
|
+
// ✅ The monolith's dead guard works again — see the note on `audio`, above.
|
|
818
|
+
reflectTtsPanel: () => { audio?.reflectTts(); },
|
|
819
|
+
reflectTtsPanelEnabled: true,
|
|
820
|
+
isLibrasOn: libras.isOpen,
|
|
821
|
+
toggleLibras: () => libras.toggle(translator.t),
|
|
307
822
|
/*
|
|
308
|
-
* ⚠️
|
|
309
|
-
*
|
|
310
|
-
* não montados. Ver as notas em `CreateGameOptions` para o que a ausência custava.
|
|
823
|
+
* ⚠️ WHAT THE GAME HANDS OVER: the three fields are optional on both sides. Absent, the pause card holds only what the
|
|
824
|
+
* engine acts on, and the two visual icons follow their own rules. See the notes in `CreateGameOptions`.
|
|
311
825
|
*/
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
826
|
+
// ⚠️ ALWAYS PASSED: a cartridge's absence means only what the engine acts on, which is what ADR-0106 §1 asks for.
|
|
827
|
+
getPauseActs: () => ({
|
|
828
|
+
...engineActions,
|
|
829
|
+
...(openGameOptions && cartridge.gameOptions?.length ? { opcoesdojogo: openGameOptions } : {}),
|
|
830
|
+
...(cartridge.getPauseActs ? cartridge.getPauseActs() : {}),
|
|
831
|
+
}),
|
|
832
|
+
/*
|
|
833
|
+
* THE TYPOGRAPHY CYCLE OF THE 11th ICON (ADR-0149 §1, ADR-0150 §2).
|
|
834
|
+
*
|
|
835
|
+
* 🎯 ONE PRESS CHANGES THE CASE **AND** THE FACE, and that is the decision: `letterCase` (ADR-0028) and the face are
|
|
836
|
+
* two settings, and Andika in capitals is ONE pedagogical choice of whoever teaches reading. A child should not
|
|
837
|
+
* have to know the model to make it.
|
|
838
|
+
*
|
|
839
|
+
* 📌 THE POSITION LIVES HERE, in a `let` of the root, and not in `core/state`: it is derived — the case and the face
|
|
840
|
+
* are each persisted on their own —, and storing an index beside what it derives from is a third place for the
|
|
841
|
+
* three to drift. On reopening, the cycle restarts at the default position with the face that was left.
|
|
842
|
+
*
|
|
843
|
+
* ⚠️ THE COUNTRY'S HAND COMES FROM THE BCP-47 TAG of the current language, and the fallback is the COLONISER's
|
|
844
|
+
* (ADR-0150): Spanish → Spain, Portuguese → Portugal, English → England. A language outside the repertoire returns an
|
|
845
|
+
* empty list and the cycle has four positions — better one fewer than the hand of a country that is not that child's.
|
|
846
|
+
*/
|
|
847
|
+
cycleTypography: pauseUsable ? () => {
|
|
848
|
+
// ⚠️ `typo` is read HERE and not in the condition: it is born further below, and the condition runs now. Whether
|
|
849
|
+
// the icon exists is decided by `pauseUsable`, the SAME question that decides whether the writer is born.
|
|
850
|
+
if (!typo)
|
|
851
|
+
return null;
|
|
852
|
+
const cycle = typographyCycle(bcp47());
|
|
853
|
+
typographyStep = (typographyStep + 1) % cycle.length;
|
|
854
|
+
const position = cycle[typographyStep];
|
|
855
|
+
state.setLetterCaseValue(position.letterCase);
|
|
856
|
+
typo.setFont(position.font, false);
|
|
857
|
+
/*
|
|
858
|
+
* ⚠️ THE SCALE IS ALWAYS WRITTEN, not only when it is above 1. Writing only on the way up would leave the country's
|
|
859
|
+
* hand in force after the child went back to Atkinson — the whole text 25% larger with nothing explaining it, and
|
|
860
|
+
* her pressing the button again to try to undo it.
|
|
861
|
+
* 📌 On the document root and not on `#game-region`: the base `font-size` belongs to `html,body`, and that is what
|
|
862
|
+
* this ratio multiplies.
|
|
863
|
+
*/
|
|
864
|
+
doc.documentElement.style.setProperty('--fonte-escala', String(position.scale));
|
|
865
|
+
reserveBarBand(); // the name line under the bar grows with the text (issue #160)
|
|
866
|
+
return FONT_BY_KEY[position.font]?.fam ?? null;
|
|
867
|
+
} : undefined,
|
|
868
|
+
...(cartridge.setPlayerTheme ? { setPlayerTheme: cartridge.setPlayerTheme } : {}),
|
|
869
|
+
/*
|
|
870
|
+
* 🚥 COLOUR-VISION CORRECTION HAS AN ENGINE DEFAULT (ADR-0148 §1), so the icon is never missing for want of a writer.
|
|
871
|
+
*
|
|
872
|
+
* 🎯 A FILTER NEEDS NOT KNOW THE GAME — the argument that makes this legitimate. It goes over whatever the game drew,
|
|
873
|
+
* the same way the `consumer-quiz` does by hand. What the engine CANNOT do is repaint textures, which is why the 🌗
|
|
874
|
+
* stays the game's (see the ADR-0148 erratum).
|
|
875
|
+
*
|
|
876
|
+
* ⚠️ AND ONLY IF THE FILTERS EXIST. Without `host.cvdHost`, `installCvdFilters` returns zero, `url(#cvd-fix-protan)`
|
|
877
|
+
* points at nothing and the icon would announce a correction that does not happen — a control that lies about its
|
|
878
|
+
* state, worse than one icon fewer (ADR-0106 §5). The `problems` line for that gap already exists.
|
|
879
|
+
*
|
|
880
|
+
* 📌 THE CARTRIDGE STILL WINS: a game that corrects colour in its own render (`game-pinball`, in a framebuffer) hands in
|
|
881
|
+
* its own and the engine steps aside.
|
|
882
|
+
*/
|
|
883
|
+
...(cartridge.setPlayerCorrection
|
|
884
|
+
? { setPlayerCorrection: withSafePalette(cartridge.setPlayerCorrection) }
|
|
885
|
+
: cvdFilters
|
|
886
|
+
/*
|
|
887
|
+
* ⚠️ `filterKey` AND NOT `VIZ_FILTER[correction]`: the two vocabularies are DIFFERENT. The axis says `protan`;
|
|
888
|
+
* `VIZ_FILTER` knows `fix-protan`. Translated by hand, the lookup gave `undefined`, the filter came out empty and
|
|
889
|
+
* the icon ANNOUNCED a correction that did not happen — exactly the control lying about its state.
|
|
890
|
+
* 📌 And `filterKey` does more than glue a prefix: it puts the SIMULATION ahead of the correction when there is
|
|
891
|
+
* one, a rule this module should not reinvent.
|
|
892
|
+
*/
|
|
893
|
+
? { setPlayerCorrection: withSafePalette((i, correction) => {
|
|
894
|
+
/*
|
|
895
|
+
* 🔴 STORE BEFORE APPLYING: applying alone left the cycle STUCK on the first position. The next step is computed
|
|
896
|
+
* by `nextCorrection(p.visual)` in `ui/pause-icons`; without writing it back, every press re-read the default and
|
|
897
|
+
* returned `protan`.
|
|
898
|
+
* ⚠️ No error at all: the icon announced the right correction, the filter changed the first time, and the child
|
|
899
|
+
* pressed twice more to see the same screen. Caught by a surviving MUTATION — always apply, never clear — that stayed
|
|
900
|
+
* green because the case pressed only once.
|
|
901
|
+
*/
|
|
902
|
+
// ⚠️ `players()`, the SAME list `ui/pause-icons` reads (`getPlayers`, above) to compute the next step: written
|
|
903
|
+
// anywhere else, the state would land where nobody reads it again.
|
|
904
|
+
const player = players()[i];
|
|
905
|
+
// A correction switched ON stops the demonstration (ADR-0076): a simulation over an adaptation teaches something false.
|
|
906
|
+
const before = player?.visual ?? worldState;
|
|
907
|
+
const nextVisual = { ...before, correcao: correction, simulacao: correction === 'tricro' ? before.simulacao : null };
|
|
908
|
+
if (player)
|
|
909
|
+
player.visual = nextVisual;
|
|
910
|
+
worldState = nextVisual;
|
|
911
|
+
recomposeWorldFilter();
|
|
912
|
+
}) }
|
|
913
|
+
: {}),
|
|
315
914
|
});
|
|
316
915
|
/*
|
|
317
|
-
* ⚠️
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
*
|
|
916
|
+
* ⚠️ THE HOST MUST BE ABLE TO BE A BAR, and asking is not fussiness: `a11yBarHost` is `Element` in the type, and a
|
|
917
|
+
* consumer may pass a double, a node from another document, or an element of an `<svg>`. Without this guard, an object
|
|
918
|
+
* without `addEventListener` brings the WHOLE BOOT down — and bringing it down for the accessibility bar would take
|
|
919
|
+
* the game from everyone so as to give it to no one.
|
|
321
920
|
*
|
|
322
|
-
*
|
|
921
|
+
* If it cannot, it is `problems` like any other gap of the host: the consumer reads and fixes.
|
|
323
922
|
*/
|
|
324
|
-
const
|
|
923
|
+
const barUsable = !!a11yBar
|
|
325
924
|
&& typeof a11yBar.addEventListener === 'function'
|
|
326
925
|
&& 'innerHTML' in a11yBar;
|
|
327
|
-
if (a11yBar && !
|
|
328
|
-
|
|
926
|
+
if (a11yBar && !barUsable) {
|
|
927
|
+
hostProblems.push('the accessibility bar element takes neither content nor clicks: its icons were not mounted, so a child cannot reach them — give `host.a11yBarHost` a real element');
|
|
329
928
|
}
|
|
330
|
-
if (a11yBar &&
|
|
331
|
-
|
|
929
|
+
if (a11yBar && barUsable) {
|
|
930
|
+
// 🔴 WITH THE NAME CAPTION under the row: the bar the engine mounted had none, and the Dev saw it mute when navigating
|
|
931
|
+
// and hovering. `aria-hidden` because the name is already SPOKEN (`srSay` on the cursor, the `aria-label` on focus).
|
|
932
|
+
a11yBar.innerHTML = iconsMarkup(translator, pauseIcons.mountedIcons) + '<p class="pause-icons-cap" aria-hidden="true"></p>';
|
|
933
|
+
wireBarCaption(a11yBar, explainIconInFooter);
|
|
332
934
|
a11yBar.addEventListener('click', (e) => {
|
|
333
|
-
const
|
|
334
|
-
if (!
|
|
935
|
+
const rowButton = e.target?.closest('.pi-btn');
|
|
936
|
+
if (!rowButton)
|
|
335
937
|
return;
|
|
336
|
-
pauseIcons.iconAct(
|
|
938
|
+
pauseIcons.iconAct(rowButton.dataset.pi ?? '', 0);
|
|
337
939
|
pauseIcons.reflectIconsIn(a11yBar, 0);
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
940
|
+
const caption = a11yBar.querySelector('.pause-icons-cap');
|
|
941
|
+
if (caption)
|
|
942
|
+
caption.textContent = accessibleLabel(rowButton); // the NEW state, after the reflection; «N de M» is spoken, never written (ADR-0167)
|
|
943
|
+
// The announcement reads the `aria-label` AFTER the reflection, because that carries the NEW state — announcing
|
|
944
|
+
// before would say the state the child just left.
|
|
945
|
+
srSay(rowButton.getAttribute('aria-label') ?? '');
|
|
341
946
|
});
|
|
342
947
|
pauseIcons.reflectIconsIn(a11yBar, 0);
|
|
343
948
|
/*
|
|
344
|
-
* ⚠️
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
* lugar: «o `applyDom` conserta o markup ESTÁTICO, mas o que o JavaScript monta tinha capturado o
|
|
353
|
-
* texto de pt e ninguém reconstruía». A barra é a instância nova, criada quando a ENGINE passou a
|
|
354
|
-
* montá-la (ADR-0106 etapa 2).
|
|
355
|
-
*
|
|
356
|
-
* ⚠️ É O `idiomaPronto()` E NÃO O EVENTO `i18n:change`, de propósito: o evento é a troca de idioma EM
|
|
357
|
-
* EXECUÇÃO, e o próprio `core/i18n` declara essa pergunta como sendo do Dev («QUANDO a interface se
|
|
358
|
-
* reconstrói ao trocar de idioma em execução»). Isto responde só a pergunta do ARRANQUE, que aquele
|
|
359
|
-
* mesmo comentário diz não ter duas respostas.
|
|
949
|
+
* ⚠️ AND AGAIN WHEN THE BOOT LANGUAGE ARRIVES — without it the bar stays in the FALLBACK language.
|
|
950
|
+
*
|
|
951
|
+
* `initI18n` applies pt synchronously (so the page is never blank) and, if the preferred language is another, ASKS
|
|
952
|
+
* for the switch — which is asynchronous, because en/es are on-demand chunks. This markup is born in that interval.
|
|
953
|
+
*
|
|
954
|
+
* 📌 IT IS THE `i18n:change` LISTENER near the pad that repaints it (study item C6, ADR-0031): the boot's preferred
|
|
955
|
+
* language arrives through `setLocale`, which dispatches that same event, so one path serves the boot and a change
|
|
956
|
+
* made mid-game.
|
|
360
957
|
*/
|
|
361
|
-
void idiomaPronto().then(() => { pauseIcons.reflectIconsIn(a11yBar, 0); });
|
|
362
958
|
/*
|
|
363
|
-
* ⚠️
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
* cujo estado tem evento (`EventoDoJogo` tem `modoCego`; TTS, Libras, TEA e alternância não emitem nada).
|
|
370
|
-
* Os outros continuam a refletir-se ao clique, que é o caminho por onde hoje eles mudam.
|
|
959
|
+
* ⚠️ AND IT MUST GO ON TELLING THE TRUTH when a state changes ELSEWHERE. Blind mode is also switched by the hearing
|
|
960
|
+
* panel and by the empathy simulation; without this subscription, the bar's icon would go on saying off with
|
|
961
|
+
* `aria-pressed=false` after the child had turned it on — the control lying about its state.
|
|
962
|
+
*
|
|
963
|
+
* 📌 ONLY THE STATES WITH AN EVENT that can change elsewhere: blind mode, the camera control and the voice control
|
|
964
|
+
* (`GameEvent`). The other icons reflect themselves on the click, which is the path by which they change.
|
|
371
965
|
*/
|
|
372
|
-
|
|
966
|
+
stateOn('blindMode', () => { pauseIcons.reflectIconsIn(a11yBar, 0); });
|
|
967
|
+
// the 👀 changes elsewhere too: the eye control puts it back to off when the camera or the files are missing (ADR-0213)
|
|
968
|
+
stateOn('cameraControl', () => { pauseIcons.reflectIconsIn(a11yBar, 0); }); // a mode that cannot start puts the 📷 back to off
|
|
969
|
+
// 🔴 AND THE 👄 FOR THE SAME REASON, measured in a browser on 2026-09-21: with the microphone refused, the child heard «it did
|
|
970
|
+
// not open», the stored answer went back to off — and the button went on saying «ligado». A control that lies about its state
|
|
971
|
+
// is worse than a missing one (ADR-0106 §5), and the click path does not cover it, because this change comes from elsewhere.
|
|
972
|
+
stateOn('voiceControl', () => { pauseIcons.reflectIconsIn(a11yBar, 0); });
|
|
373
973
|
}
|
|
374
|
-
// 4d.
|
|
974
|
+
// 4d. WHO OPENED THE PAUSE, when there is more than one seat — finding 3 of the `game-soccer` audit.
|
|
375
975
|
//
|
|
376
|
-
// ⚠️
|
|
377
|
-
//
|
|
378
|
-
// o consumidor, passando o ator da pausa: «edita o controle de quem abriu o menu».
|
|
976
|
+
// ⚠️ THE KEYBOARD PANEL IS PARAMETERISED BY THE SEAT: `render(selPlayer)` draws the positions of THAT scheme, and the
|
|
977
|
+
// consumer decides the seat by passing the pause actor: the panel edits the controls of whoever opened the menu.
|
|
379
978
|
//
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
979
|
+
// 📌 The pause-actor line of `problems` lives in `measureCartridgeProblems()`: whoever declares seats is the game, and
|
|
980
|
+
// the line accuses only a game that did not answer (ADR-0106 §2).
|
|
981
|
+
/*
|
|
982
|
+
* 4e. THE FIRST SCREEN'S PAUSE CARD. `#vp-pause-0` is what this root looks for (`getPauseMenu` of `initMenuNav`, below),
|
|
983
|
+
* and games did not create it: the engine had invented a convention, looked for it, not found it, and concluded in
|
|
984
|
+
* silence that no game had a pause menu — the same shape of defect as ADR-0106 §2, committed by the engine against
|
|
985
|
+
* itself. So the engine creates what it looks for, for EVERY game (ADR-0120, ADR-0122): the pause and the
|
|
986
|
+
* accessibility icons are in every game, so they are the engine's.
|
|
987
|
+
*/
|
|
988
|
+
// (`pauseMountPoint` and `pauseUsable` are hoisted above `initPauseIcons` — see the note there. They depend only on
|
|
989
|
+
// `o.host` and `$`, and the bar needs the answer BEFORE deciding which icons it mounts.)
|
|
990
|
+
if (!pauseUsable) {
|
|
991
|
+
hostProblems.push('the pause menu has nowhere to mount: a child cannot reach the settings during play — set `host.pauseHost` or give '
|
|
992
|
+
+ '#game-region room for children (the pause is the engine\'s in every game, ADR-0122; the game only says where it fits)');
|
|
993
|
+
}
|
|
994
|
+
if (pauseMountPoint && pauseUsable) {
|
|
995
|
+
const findPauseCard = pauseIcons.buildScreenPause(0);
|
|
996
|
+
// ⚠️ THE ID IS WHAT THE ENGINE ITSELF LOOKS FOR, just below, in `getPauseMenu`. Mounting without it would leave the
|
|
997
|
+
// loop as open as it was — the card would exist and menu navigation would still not find it.
|
|
998
|
+
findPauseCard.id = 'vp-pause-0';
|
|
999
|
+
pauseMountPoint.appendChild(findPauseCard);
|
|
1000
|
+
}
|
|
1001
|
+
/*
|
|
1002
|
+
* 4f. THE SETTINGS PANELS (ADR-0106 §1).
|
|
1003
|
+
*
|
|
1004
|
+
* 🔴 `ui/panel-shell.mountShell` BUILDS a panel's shell, and each `ui/settings-*` fills the INSIDE of ids somebody
|
|
1005
|
+
* must create. Left to the page, the failure took the worst form available — the quiz recorded it as finding 6: the
|
|
1006
|
+
* panel opens EMPTY, with no error. So the engine mounts each shell (`ui/mount-panel`) and then its writer.
|
|
1007
|
+
*
|
|
1008
|
+
* ⚠️ AND THE PANELS LIVE WHERE THE CARD LIVES, which is not tidiness: `ui/settings-panel.topVisibleOverlay` scans
|
|
1009
|
+
* `'#game-region .overlay'` (`OVERLAY_SCOPE_SELECTOR`), and that is how `ui/menu-nav` reaches the top dialog to move
|
|
1010
|
+
* with the arrows. A panel hung outside that scope OPENS, closes with Escape — and **the arrows do not move inside
|
|
1011
|
+
* it**, with no error at all. Hence the `problems` line below instead of silence.
|
|
1012
|
+
*/
|
|
1013
|
+
if (pauseMountPoint && pauseUsable) {
|
|
1014
|
+
const regionEl = $('#game-region');
|
|
1015
|
+
const insideScope = !!regionEl && typeof regionEl.contains === 'function'
|
|
1016
|
+
&& regionEl.contains(pauseMountPoint);
|
|
1017
|
+
if (!insideScope) {
|
|
1018
|
+
hostProblems.push('the pause host is outside #game-region: settings panels open, but arrows do not move inside them, so a child '
|
|
1019
|
+
+ 'who plays by keyboard cannot reach the settings — put `host.pauseHost` inside #game-region');
|
|
1020
|
+
}
|
|
1021
|
+
const panelCtx = {
|
|
1022
|
+
find: (sel) => $(sel),
|
|
1023
|
+
create: (tag) => doc.createElement(tag),
|
|
1024
|
+
host: pauseMountPoint,
|
|
1025
|
+
overlays,
|
|
1026
|
+
localeOn, // a language change redraws an open panel, and `dispose()` releases it (ADR-0232 D4)
|
|
1027
|
+
};
|
|
1028
|
+
/*
|
|
1029
|
+
* TYPOGRAPHY — NO PANEL since ADR-0151: «quem escolhe a tipografia é o jogo, o jogador escolhe suas fontes via o
|
|
1030
|
+
* menu de acessibilidade rápida». Its door left the inclusion settings and its shell is not mounted — a dialog in the
|
|
1031
|
+
* document that nobody reaches is the defect ADR-0144 measured.
|
|
1032
|
+
*
|
|
1033
|
+
* 📌 BUT THE WRITER STAYS, which is why the `init` is still here: the 11th button's cycle writes the face through this
|
|
1034
|
+
* API (`typo.setFont`), and the `init` applies at boot the font the child left stored. `initSettingsTypo` guards each
|
|
1035
|
+
* access to the document, so it runs without the shell.
|
|
1036
|
+
*/
|
|
1037
|
+
typo = initSettingsTypo({
|
|
1038
|
+
t: translator.t, $, srSay, store, root: doc.documentElement,
|
|
1039
|
+
// The rows' prose goes to the footer at EVERY render, or it appears twice on the first click.
|
|
1040
|
+
fillExplain: overlays.fillExplain,
|
|
1041
|
+
// ⚠️ `doc.fonts` IS A BROWSER GLOBAL — FINDING 15 of this file. Here it comes from the host's `doc` and is still
|
|
1042
|
+
// asked whether it exists: a fake document has no `fonts`, and the absence has a declared answer (the row is
|
|
1043
|
+
// disabled WITH the message that tells the adult which fonts solve it).
|
|
1044
|
+
...(typeof doc.fonts?.check === 'function'
|
|
1045
|
+
? { fontInstalled: (family) => doc.fonts.check(`16px "${family}"`) }
|
|
1046
|
+
: {}),
|
|
1047
|
+
});
|
|
1048
|
+
/*
|
|
1049
|
+
* AAC — COMMUNICATION: THE PANEL IS NOT MOUNTED (ADR-0151).
|
|
1050
|
+
*
|
|
1051
|
+
* 🔴 The «Comunicação» door left the inclusion settings: the letter case moves with the bar's 11th button, which is the
|
|
1052
|
+
* COMMUNICATION cycle (with ARASAAC and PCS disabled). Mounting a panel with no door would leave in the document a
|
|
1053
|
+
* dialog nobody reaches — the defect ADR-0144 measured. The `ui/settings-aac` module stays in the engine for whoever
|
|
1054
|
+
* wants to mount it.
|
|
1055
|
+
*/
|
|
1056
|
+
/*
|
|
1057
|
+
* HELP — which button does what, IN THIS game, on THIS child's keyboard (ADR-0147 §4).
|
|
1058
|
+
*
|
|
1059
|
+
* 🔴 The `ajuda` item has been on the pause list since ADR-0044 and the engine could not act on it, so `itemsThatAct`
|
|
1060
|
+
* hid it in every game. The engine draws it now, so no game has to write its own.
|
|
1061
|
+
*
|
|
1062
|
+
* ⚠️ IT IS NOT MOUNTED WITHOUT `howToPlay` OR `preset`, and the absence is the right answer: without the game's words,
|
|
1063
|
+
* the table could only show `action2` — an identifier in front of a child, the defect ADR-0074 forbids in so many
|
|
1064
|
+
* words. Better no help than one that cannot be read.
|
|
1065
|
+
*
|
|
1066
|
+
* 📌 THE KEY COMES FROM `kbFor(0)`, not the factory map: whoever remapped sees HER key. It is the same reason ADR-0144
|
|
1067
|
+
* listens to the action and not the key.
|
|
1068
|
+
*/
|
|
1069
|
+
// The cartridge's «how to play» slides come first (ADR-0195; issue #188); the help stands with them, with the buttons, or both.
|
|
1070
|
+
if (cartridge.preset || cartridge.howToPlay?.length) {
|
|
1071
|
+
let stopFigure = () => { };
|
|
1072
|
+
const helpPanel = mountPanel(panelCtx, {
|
|
1073
|
+
id: 'help',
|
|
1074
|
+
labels: () => ({
|
|
1075
|
+
title: t('menu.help'),
|
|
1076
|
+
listLabel: t('help.grupo.rotulo'),
|
|
1077
|
+
resetLabel: t('menu.restoreDefaults'),
|
|
1078
|
+
closeLabel: t('pause.pmback'),
|
|
1079
|
+
}),
|
|
1080
|
+
// ⚠️ `render` AND NOT A ONE-OFF MOUNTING: the preset may change with another cartridge's `mount()` (ADR-0142) and
|
|
1081
|
+
// the child may have remapped between two openings. A table built at boot would show yesterday's key — the exact
|
|
1082
|
+
// shape of the control lying about its state.
|
|
1083
|
+
// The slide show opens on its first slide (interface log, 2026-09-13; `ui/help-panel`).
|
|
1084
|
+
render: () => {
|
|
1085
|
+
const list = $('#help-list');
|
|
1086
|
+
if (!list)
|
|
1087
|
+
return;
|
|
1088
|
+
while (list.firstChild)
|
|
1089
|
+
list.removeChild(list.firstChild);
|
|
1090
|
+
const slideContents = [...(cartridge.howToPlay ?? []), ...helpRows(cartridge.preset, (a) => keyboard.kbFor(0)[a], (code) => keyName(t, code))];
|
|
1091
|
+
const ctxDoSlide = { create: (tag) => doc.createElement(tag), t, title: t('menu.help') };
|
|
1092
|
+
const slides = mountSlides(ctxDoSlide);
|
|
1093
|
+
list.appendChild(slides);
|
|
1094
|
+
const slideTimer = {
|
|
1095
|
+
requestFrame: (cb) => win.requestAnimationFrame(cb),
|
|
1096
|
+
cancelFrame: (id) => win.cancelAnimationFrame(id),
|
|
1097
|
+
reduced: defaultReducedMotion(win.matchMedia),
|
|
1098
|
+
};
|
|
1099
|
+
const showSlideAt = (i) => {
|
|
1100
|
+
stopFigure();
|
|
1101
|
+
const shown = showSlide(slides, slideContents, i, ctxDoSlide);
|
|
1102
|
+
const slide = slideContents[shown.index];
|
|
1103
|
+
if (slide && 'text' in slide)
|
|
1104
|
+
stopFigure = animateFigure(slides, slide, slideTimer);
|
|
1105
|
+
return shown;
|
|
1106
|
+
};
|
|
1107
|
+
let currentSlide = showSlideAt(0).index;
|
|
1108
|
+
slides.addEventListener('passo', (ev) => {
|
|
1109
|
+
const fresh = nextStep(currentSlide, slideContents.length, ev.detail);
|
|
1110
|
+
if (fresh === currentSlide)
|
|
1111
|
+
return;
|
|
1112
|
+
const shown = showSlideAt(fresh);
|
|
1113
|
+
currentSlide = shown.index;
|
|
1114
|
+
srSay(shown.spoken);
|
|
1115
|
+
});
|
|
1116
|
+
},
|
|
1117
|
+
});
|
|
1118
|
+
// A slide show restores nothing: the reset row the panel shell builds does not show here.
|
|
1119
|
+
const helpActions = helpPanel.shell.reset.parentElement;
|
|
1120
|
+
if (helpActions)
|
|
1121
|
+
helpActions.hidden = true;
|
|
1122
|
+
engineActions.ajuda = helpPanel.open;
|
|
1123
|
+
}
|
|
1124
|
+
else {
|
|
1125
|
+
hostProblems.push('the help screen was not mounted: it shows how to play and each position, its key and the game\'s word, and without '
|
|
1126
|
+
+ '`howToPlay` or `preset` it could only show a child `action2` — declare `howToPlay` (ADR-0195) or `preset` (ADR-0085)');
|
|
1127
|
+
}
|
|
1128
|
+
/*
|
|
1129
|
+
* OPÇÕES DO JOGO — the cartridge's rows, drawn by the engine (ADR-0182; issue #178).
|
|
1130
|
+
* 📌 Drawn at every opening and at every `mount()`: the rows are the CURRENT cartridge's, and each value is read from it.
|
|
1131
|
+
* The shell's «restore defaults» is hidden: a cartridge declares no defaults, and a button that does nothing is the
|
|
1132
|
+
* dead control ADR-0106 §5 forbids.
|
|
1133
|
+
*/
|
|
1134
|
+
const gamePanel = mountPanel(panelCtx, {
|
|
1135
|
+
id: 'game-options',
|
|
1136
|
+
labels: () => ({
|
|
1137
|
+
title: t('pause.opcoesdojogo'),
|
|
1138
|
+
listLabel: t('pause.opcoesdojogo'),
|
|
1139
|
+
resetLabel: t('menu.restoreDefaults'),
|
|
1140
|
+
closeLabel: t('pause.pmback'),
|
|
1141
|
+
}),
|
|
1142
|
+
render: () => redrawGameOptions(),
|
|
1143
|
+
});
|
|
1144
|
+
gamePanel.shell.reset.hidden = true;
|
|
1145
|
+
redrawGameOptions = () => {
|
|
1146
|
+
drawGameOptions({ ...panelCtx, say: srSay, t: translator.t }, gamePanel.shell.list, cartridge.gameOptions ?? []);
|
|
1147
|
+
if (!gamePanel.shell.overlay.hidden)
|
|
1148
|
+
overlays.fillExplain(gamePanel.shell.card);
|
|
1149
|
+
};
|
|
1150
|
+
openGameOptions = gamePanel.open;
|
|
1151
|
+
/*
|
|
1152
|
+
* ANIMATION — motion sensitivity.
|
|
1153
|
+
*
|
|
1154
|
+
* 📌 `rm`, `saveRM`, `rmKeys` and `rmChar` are OPTIONAL (ADR-0106 step 1), and the absence is the news: none of them
|
|
1155
|
+
* held a choice of the game — `rmKeys` was the `MotionSceneKey` union written by hand and `rm`/`saveRM` read an engine
|
|
1156
|
+
* storage key with an engine default. `ui/motion-scene` answers for the four, so this panel needs nothing only the
|
|
1157
|
+
* cartridge knows. The root still passes `rm`/`saveRM`: not the game's, but the ONE object the quick bar also writes.
|
|
1158
|
+
*
|
|
1159
|
+
* ⚠️ AND ITS LIST IS `#motion-list`, NOT `#animation-list` — inherited from the monolith, where the panel was called
|
|
1160
|
+
* «motion» and the overlay «animation». See `PanelShellSpec.listId`: renaming would touch the contract with markup of
|
|
1161
|
+
* consumers this repository cannot measure.
|
|
1162
|
+
*/
|
|
1163
|
+
let motion = null;
|
|
1164
|
+
const animPanel = mountPanel(panelCtx, {
|
|
1165
|
+
id: 'animation',
|
|
1166
|
+
listId: 'motion-list',
|
|
1167
|
+
labels: () => ({
|
|
1168
|
+
title: t('menu.animation'),
|
|
1169
|
+
listLabel: t('animation.grupo.rotulo'),
|
|
1170
|
+
resetLabel: t('menu.restoreDefaults'),
|
|
1171
|
+
closeLabel: t('pause.pmback'),
|
|
1172
|
+
}),
|
|
1173
|
+
render: () => motion?.render(),
|
|
1174
|
+
});
|
|
1175
|
+
/*
|
|
1176
|
+
* THE MASTER BUTTON — stop all animations at once.
|
|
1177
|
+
*
|
|
1178
|
+
* ⚠️ CREATED HERE AND BEFORE THE `init`: `initSettingsMotion` wires its click ONCE, at boot. And it is not
|
|
1179
|
+
* decoration — it is the way out for someone who felt sick with the screen moving and needs to stop EVERYTHING in one
|
|
1180
|
+
* gesture, instead of going through seven rows one by one.
|
|
1181
|
+
* 📌 The label comes from the panel itself (`motionMasterLabel`), which swaps it by state; setting it here would give
|
|
1182
|
+
* two hands writing the same text, and the one left behind would lie about the state.
|
|
1183
|
+
*/
|
|
1184
|
+
const animMaster = doc.createElement('button');
|
|
1185
|
+
animMaster.id = 'motion-master';
|
|
1186
|
+
animMaster.className = 'mode-btn switch';
|
|
1187
|
+
animMaster.setAttribute('type', 'button');
|
|
1188
|
+
animPanel.shell.card.insertBefore(animMaster, animPanel.shell.list);
|
|
1189
|
+
motion = initSettingsMotion({
|
|
1190
|
+
t: translator.t, $, srSay, store, matchMedia: win.matchMedia, crt,
|
|
1191
|
+
// the SAME flags the quick bar's calm icon writes (see `sceneMotion`, above)
|
|
1192
|
+
rm: sceneMotion, saveRM: saveSceneMotion,
|
|
1193
|
+
getNumPlayers: () => (cartridge.players ?? [null]).length,
|
|
1194
|
+
getPlayers: () => cartridge.players ?? [],
|
|
1195
|
+
frontOverlay: overlays.frontOverlay,
|
|
1196
|
+
restoreFocus: overlays.restoreFocus,
|
|
1197
|
+
fillExplain: overlays.fillExplain,
|
|
1198
|
+
toggleBtn,
|
|
1199
|
+
// The «Personagem» section exists only if the GAME said it has one (ADR-0153). Read at every render: it changes on `mount()`.
|
|
1200
|
+
hasCharacter: () => subjectWord(cartridge.accommodations, 'reducedCharacterMotion') !== null,
|
|
1201
|
+
// and its title is the game's word for it (ADR-0153 confirmation)
|
|
1202
|
+
characterLabel: () => subjectWord(cartridge.accommodations, 'reducedCharacterMotion')?.label ?? null,
|
|
1203
|
+
});
|
|
1204
|
+
engineActions.anim = animPanel.open;
|
|
1205
|
+
/*
|
|
1206
|
+
* VISUAL ACCESSIBILITY (ADR-0151) — and the item is no longer locked (ADR-0161).
|
|
1207
|
+
*
|
|
1208
|
+
* 📌 THE MODULE IS `ui/settings-visual`, with the two rows the engine CAN act on: the contrast enhancement (the L→Q
|
|
1209
|
+
* filter, composed on the world with the colour correction) and the safe palette (`core/state.cbSafe`, which paints
|
|
1210
|
+
* menus and HUD). ⚠️ Role colours («lava, escada, água, portão») stay OUT: they belong to a game with those roles, and
|
|
1211
|
+
* the engine does not describe a game it does not know. Owner colours and outlines are offered below, where the game
|
|
1212
|
+
* answers them (ADR-0188). High contrast and colour correction left this panel for the quick bar (ADR-0151).
|
|
1213
|
+
* ⚠️ The writers of the rows not offered are inert ON PURPOSE: `reset` calls them only when the value read differs
|
|
1214
|
+
* from the default, and the value returned here IS the default.
|
|
1215
|
+
*/
|
|
1216
|
+
const visualPanel = mountPanel(panelCtx, {
|
|
1217
|
+
id: 'visual',
|
|
1218
|
+
labels: () => ({
|
|
1219
|
+
title: t('menu.visual'),
|
|
1220
|
+
listLabel: t('visual.grupo.rotulo'),
|
|
1221
|
+
resetLabel: t('menu.restoreDefaults'),
|
|
1222
|
+
closeLabel: t('pause.pmback'),
|
|
1223
|
+
}),
|
|
1224
|
+
render: () => {
|
|
1225
|
+
rateHint.textContent = t('visual.legenda.ritmo.dica'); // before `visual.render()` runs `fillExplain`
|
|
1226
|
+
fgOutline.escreverDica();
|
|
1227
|
+
bgOutline.escreverDica();
|
|
1228
|
+
visual.render();
|
|
1229
|
+
offerOwnerAndOutlines();
|
|
1230
|
+
labelRow(captionsRow, captionsSpec()); // in the language of the opening
|
|
1231
|
+
reflectCaptions();
|
|
1232
|
+
reflectCaptionRate();
|
|
1233
|
+
},
|
|
1234
|
+
});
|
|
1235
|
+
/*
|
|
1236
|
+
* CAPTIONS (ADR-0151 §2; issue #182): the Dev listed them in the visual panel. `state.captionsOn` was stored and read by
|
|
1237
|
+
* the sound captions (`captionSound`) with no row to change it. Placed after the panel's list, which `visual.render()`
|
|
1238
|
+
* rewrites by markup; built once, so its listener is not lost.
|
|
1239
|
+
*/
|
|
1240
|
+
const captionsSpec = () => ({ id: 'opt-captions', label: t('visual.captions'), hint: t('visual.captions.dica') });
|
|
1241
|
+
const { row: captionsRow, control: captionsButton } = controlRow(panelCtx, captionsSpec());
|
|
1242
|
+
visualPanel.shell.card.insertBefore(captionsRow, visualPanel.shell.list.nextSibling);
|
|
1243
|
+
const reflectCaptions = () => {
|
|
1244
|
+
toggleBtn(captionsButton, state.captionsOn);
|
|
1245
|
+
captionsButton.textContent = toggleLabel(t, state.captionsOn);
|
|
1246
|
+
markChanged(t, captionsRow, state.captionsOn !== DEFAULTS.captionsOn);
|
|
1247
|
+
};
|
|
1248
|
+
/* THE CAPTION RATE (ADR-0183 §4; issue #179): 125, 145 or 175 words a minute, by steps, right after the captions switch. */
|
|
1249
|
+
const rateSpec = () => ({
|
|
1250
|
+
label: t('visual.legenda.ritmo'),
|
|
1251
|
+
values: CAPTION_RATES.map((n) => t('visual.legenda.ppm', { n })),
|
|
1252
|
+
current: Math.max(0, CAPTION_RATES.indexOf(state.captionPpm)),
|
|
1253
|
+
});
|
|
1254
|
+
const speechRateRow = doc.createElement('div');
|
|
1255
|
+
speechRateRow.className = 'ctrl-row ctrl-row--passos';
|
|
1256
|
+
const rateEnvelope = doc.createElement('span');
|
|
1257
|
+
const rateHint = doc.createElement('span');
|
|
1258
|
+
rateHint.className = 'opt-hint';
|
|
1259
|
+
// ⚠️ WRITTEN NOW, and again before the panel's render: `visual.render()` runs `fillExplain` on the whole card, and a row it
|
|
1260
|
+
// meets with an empty hint is marked done and keeps its hint INSIDE — the Dev saw the explanation in the row.
|
|
1261
|
+
rateHint.textContent = t('visual.legenda.ritmo.dica');
|
|
1262
|
+
rateEnvelope.appendChild(rateHint);
|
|
1263
|
+
speechRateRow.appendChild(rateEnvelope);
|
|
1264
|
+
const rateSteps = mountSteps(panelCtx, rateSpec());
|
|
1265
|
+
rateSteps.id = 'opt-legenda-ppm';
|
|
1266
|
+
speechRateRow.appendChild(rateSteps);
|
|
1267
|
+
visualPanel.shell.card.insertBefore(speechRateRow, captionsRow.nextSibling);
|
|
1268
|
+
rateSteps.addEventListener('passo', (ev) => {
|
|
1269
|
+
const currentSlide = rateSpec().current;
|
|
1270
|
+
const fresh = nextStep(currentSlide, CAPTION_RATES.length, ev.detail);
|
|
1271
|
+
if (fresh === currentSlide)
|
|
1272
|
+
return;
|
|
1273
|
+
state.setCaptionPpmValue(CAPTION_RATES[fresh]);
|
|
1274
|
+
updateSteps(rateSteps, rateSpec());
|
|
1275
|
+
markChanged(t, speechRateRow, state.captionPpm !== DEFAULTS.captionPpm);
|
|
1276
|
+
srSay(`${t('visual.legenda.ritmo')}: ${t('visual.legenda.ppm', { n: state.captionPpm })}`);
|
|
1277
|
+
});
|
|
1278
|
+
const reflectCaptionRate = () => {
|
|
1279
|
+
updateSteps(rateSteps, rateSpec());
|
|
1280
|
+
rateHint.textContent = t('visual.legenda.ritmo.dica');
|
|
1281
|
+
markChanged(t, speechRateRow, state.captionPpm !== DEFAULTS.captionPpm);
|
|
1282
|
+
};
|
|
1283
|
+
captionsButton.addEventListener('click', () => {
|
|
1284
|
+
state.setCaptionsOnValue(!state.captionsOn);
|
|
1285
|
+
reflectCaptions();
|
|
1286
|
+
srSay(t(state.captionsOn ? 'sr.visual.captionsOn' : 'sr.visual.captionsOff'));
|
|
1287
|
+
});
|
|
1288
|
+
/*
|
|
1289
|
+
* OWNER COLOURS AND CONTRAST OUTLINES (ADR-0188; issue #183): rows only where the cartridge answers the subject. Built once
|
|
1290
|
+
* beside the panel's list (`visual.render()` rewrites that by markup), with their own ids — `ui/settings-visual` wires
|
|
1291
|
+
* `#opt-ownercolors` at every render — and shown or hidden at each opening: `mount()` may have swapped the answer.
|
|
1292
|
+
* Owner colours carries the game's word; the outlines are two positions of one subject, named by the engine.
|
|
1293
|
+
*/
|
|
1294
|
+
const ownerSpec = () => {
|
|
1295
|
+
const word = subjectWord(cartridge.accommodations, 'ownerColors');
|
|
1296
|
+
return { id: 'opt-dono', label: word?.label ?? '', hint: word?.hint };
|
|
1297
|
+
};
|
|
1298
|
+
const { row: ownerRow, control: ownerButton } = controlRow(panelCtx, ownerSpec());
|
|
1299
|
+
const reflectOwner = () => {
|
|
1300
|
+
toggleBtn(ownerButton, state.ownerColors);
|
|
1301
|
+
ownerButton.textContent = toggleLabel(t, state.ownerColors);
|
|
1302
|
+
markChanged(t, ownerRow, state.ownerColors !== DEFAULTS.ownerColors);
|
|
1303
|
+
};
|
|
1304
|
+
ownerButton.addEventListener('click', () => {
|
|
1305
|
+
state.setOwnerColorsValue(!state.ownerColors);
|
|
1306
|
+
reflectOwner();
|
|
1307
|
+
srSay(`${ownerSpec().label}: ${toggleLabel(t, state.ownerColors)}`);
|
|
1308
|
+
});
|
|
1309
|
+
const OUTLINE_LEVELS = ['visual.contorno.0', 'visual.contorno.1', 'visual.contorno.2'];
|
|
1310
|
+
const outline = (plane) => {
|
|
1311
|
+
const readOutline = () => (plane === 'fg' ? state.hcOutlineFg : state.hcOutlineBg);
|
|
1312
|
+
const write = plane === 'fg' ? state.setOutlineFgValue : state.setOutlineBgValue;
|
|
1313
|
+
const spec = () => ({ label: t(`visual.contorno.${plane}`), values: OUTLINE_LEVELS.map((k) => t(k)), current: readOutline() });
|
|
1314
|
+
const rowC = doc.createElement('div');
|
|
1315
|
+
rowC.className = 'ctrl-row ctrl-row--passos';
|
|
1316
|
+
const envelope = doc.createElement('span');
|
|
1317
|
+
const explanation = doc.createElement('span');
|
|
1318
|
+
explanation.className = 'opt-hint';
|
|
1319
|
+
envelope.appendChild(explanation);
|
|
1320
|
+
rowC.appendChild(envelope);
|
|
1321
|
+
const stepper = mountSteps(panelCtx, spec());
|
|
1322
|
+
stepper.id = `opt-contorno-${plane}`;
|
|
1323
|
+
rowC.appendChild(stepper);
|
|
1324
|
+
stepper.addEventListener('passo', (ev) => {
|
|
1325
|
+
const fresh = nextStep(readOutline(), OUTLINE_LEVELS.length, ev.detail);
|
|
1326
|
+
if (fresh === readOutline())
|
|
1327
|
+
return;
|
|
1328
|
+
write(fresh);
|
|
1329
|
+
updateSteps(stepper, spec());
|
|
1330
|
+
srSay(`${t(`visual.contorno.${plane}`)}: ${t(OUTLINE_LEVELS[fresh])}`);
|
|
1331
|
+
});
|
|
1332
|
+
// the hint is written BEFORE the panel's render, which runs `fillExplain`: written after, it stays inside the row
|
|
1333
|
+
const writeHint = () => {
|
|
1334
|
+
explanation.textContent = subjectWord(cartridge.accommodations, 'contrastOutlines')?.hint ?? t(`visual.contorno.${plane}.dica`);
|
|
1335
|
+
};
|
|
1336
|
+
writeHint();
|
|
1337
|
+
const reflect = () => { updateSteps(stepper, spec()); };
|
|
1338
|
+
return { row: rowC, refletir: reflect, escreverDica: writeHint };
|
|
1339
|
+
};
|
|
1340
|
+
const fgOutline = outline('fg');
|
|
1341
|
+
const bgOutline = outline('bg');
|
|
1342
|
+
const offerOwnerAndOutlines = () => {
|
|
1343
|
+
ownerRow.hidden = subjectWord(cartridge.accommodations, 'ownerColors') === null;
|
|
1344
|
+
if (!ownerRow.hidden) {
|
|
1345
|
+
labelRow(ownerRow, ownerSpec());
|
|
1346
|
+
reflectOwner();
|
|
1347
|
+
}
|
|
1348
|
+
const withoutOutlines = subjectWord(cartridge.accommodations, 'contrastOutlines') === null;
|
|
1349
|
+
for (const c of [fgOutline, bgOutline]) {
|
|
1350
|
+
c.row.hidden = withoutOutlines;
|
|
1351
|
+
if (!withoutOutlines)
|
|
1352
|
+
c.refletir();
|
|
1353
|
+
}
|
|
1354
|
+
};
|
|
1355
|
+
// after the list, owner colours first, then the two outlines (the captions row, built above, follows them)
|
|
1356
|
+
for (const l of [bgOutline.row, fgOutline.row, ownerRow])
|
|
1357
|
+
visualPanel.shell.card.insertBefore(l, visualPanel.shell.list.nextSibling);
|
|
1358
|
+
offerOwnerAndOutlines();
|
|
1359
|
+
const noEffect = () => { };
|
|
1360
|
+
const visual = initSettingsVisual({
|
|
1361
|
+
t: translator.t, $, srSay,
|
|
1362
|
+
getNumPlayers: () => players().length,
|
|
1363
|
+
getPlayers: () => cartridge.players ?? [],
|
|
1364
|
+
getVisualSettings: () => ({
|
|
1365
|
+
lq: lq.t(), cbSafe: state.cbSafe,
|
|
1366
|
+
ownerColors: state.ownerColors, outlineFg: state.hcOutlineFg, outlineBg: state.hcOutlineBg,
|
|
1367
|
+
roleColors: { ...HC_ROLE_DEF },
|
|
1368
|
+
}),
|
|
1369
|
+
getSelectedPlayer: () => 0,
|
|
1370
|
+
setSelectedPlayer: noEffect,
|
|
1371
|
+
setPlayerViz: noEffect,
|
|
1372
|
+
renderVisualAxes: noEffect,
|
|
1373
|
+
setLq: lq.set,
|
|
1374
|
+
setCbSafe: state.setCbSafeValue,
|
|
1375
|
+
// the panel's «restore» puts these back too (ADR-0188): their rows now exist where the game answered them
|
|
1376
|
+
setOwnerColors: state.setOwnerColorsValue, setOutlineFg: state.setOutlineFgValue, setOutlineBg: state.setOutlineBgValue,
|
|
1377
|
+
setRoleColor: noEffect, resetRoleColors: noEffect,
|
|
1378
|
+
fillExplain: overlays.fillExplain,
|
|
1379
|
+
offer: { owner: false, roles: false },
|
|
1380
|
+
});
|
|
1381
|
+
engineActions.visual = visualPanel.open;
|
|
1382
|
+
/*
|
|
1383
|
+
* EMPATHY MODE (ADR-0151) — and the last locked item of the submenu unlocks (ADR-0161).
|
|
1384
|
+
*
|
|
1385
|
+
* 📌 THE MODULE IS `ui/settings-empathy`, with what the engine CAN do:
|
|
1386
|
+
* · the SIMULATIONS that are a filter on the world — the three colour-blindness ones (the matrices of
|
|
1387
|
+
* `installCvdFilters`), blur, haze and blindness; and the three DRAWN ones — tunnel vision, a central scotoma,
|
|
1388
|
+
* scattered scotomas — whose filter is only a blur: `ui/simulation-over-the-world` lays their drawing over the
|
|
1389
|
+
* world (issue #182);
|
|
1390
|
+
* · HEARING LOSS (`platform/audio.setHearingLossGraph`, already the engine's);
|
|
1391
|
+
* · the two MOTOR simulations (ADR-0181): «um botão por vez» and «sem força para segurar», applied to game keys by
|
|
1392
|
+
* the filter at the end of the boot, before any cartridge hears them.
|
|
1393
|
+
* ⚠️ No wheelchair (cut by ADR-0151).
|
|
1394
|
+
* ⚠️ And the simulation respects ADR-0076: with a colour correction on it does not run, and SAYS why.
|
|
1395
|
+
*/
|
|
1396
|
+
const WORLD_SIMULATIONS = ['normal', 'sim-protan', 'sim-deuter', 'sim-tritan', 'lv-blur', 'lv-haze', 'lv-tunnel', 'lv-macular', 'lv-diabetic', 'blind'];
|
|
1397
|
+
const empathyPanel = mountPanel(panelCtx, {
|
|
1398
|
+
id: 'empathy',
|
|
1399
|
+
labels: () => ({
|
|
1400
|
+
title: t('menu.empathy'),
|
|
1401
|
+
listLabel: t('empathy.grupo.rotulo'),
|
|
1402
|
+
resetLabel: t('menu.restoreDefaults'),
|
|
1403
|
+
closeLabel: t('pause.pmback'),
|
|
1404
|
+
}),
|
|
1405
|
+
// the hearing-loss row was born in the fallback language: relabelled at every opening, like the hearing panel's inside
|
|
1406
|
+
render: () => {
|
|
1407
|
+
labelRow(hearingRow, hearingSpec());
|
|
1408
|
+
labelRow(oneAtOnceRow, oneAtOnceSpec());
|
|
1409
|
+
labelRow(noStrengthRow, noStrengthSpec());
|
|
1410
|
+
empathy.render();
|
|
1411
|
+
},
|
|
1412
|
+
});
|
|
1413
|
+
// The hearing-loss row is born BEFORE the `init`, which wires its click once (the same order rule as the motion master).
|
|
1414
|
+
const hearingSpec = () => ({ id: 'opt-hearing', label: t('empathy.hearing'), hint: t('empathy.hearing.dica') });
|
|
1415
|
+
const hearingRow = controlRow(panelCtx, hearingSpec()).row;
|
|
1416
|
+
empathyPanel.shell.card.insertBefore(hearingRow, empathyPanel.shell.list);
|
|
1417
|
+
// THE TWO MOTOR SIMULATIONS (ADR-0181), before the `init`, which wires `#opt-onebtn` once (the same order rule).
|
|
1418
|
+
const oneAtOnceSpec = () => ({ id: 'opt-onebtn', label: t('empathy.onebtn'), hint: t('empathy.onebtn.dica') });
|
|
1419
|
+
const noStrengthSpec = () => ({ id: 'opt-semforca', label: t('empathy.semforca'), hint: t('empathy.semforca.dica') });
|
|
1420
|
+
const oneAtOnceRow = controlRow(panelCtx, oneAtOnceSpec()).row;
|
|
1421
|
+
const noStrengthRow = controlRow(panelCtx, noStrengthSpec()).row;
|
|
1422
|
+
empathyPanel.shell.card.insertBefore(oneAtOnceRow, empathyPanel.shell.list);
|
|
1423
|
+
empathyPanel.shell.card.insertBefore(noStrengthRow, empathyPanel.shell.list);
|
|
1424
|
+
const reflectMobilitySimulations = () => {
|
|
1425
|
+
for (const [id, on] of [['#opt-onebtn', state.oneButton], ['#opt-semforca', state.noGripStrength]]) {
|
|
1426
|
+
const b = $(id);
|
|
1427
|
+
if (!b)
|
|
1428
|
+
continue;
|
|
1429
|
+
toggleBtn(b, on);
|
|
1430
|
+
b.textContent = toggleLabel(t, on);
|
|
1431
|
+
}
|
|
1432
|
+
markChanged(t, noStrengthRow, state.noGripStrength !== DEFAULTS.noGripStrength);
|
|
1433
|
+
};
|
|
1434
|
+
const simulate = (i, key) => {
|
|
1435
|
+
const simulation = (key === 'normal' ? null : key);
|
|
1436
|
+
const player = players()[i];
|
|
1437
|
+
const base = player?.visual ?? worldState;
|
|
1438
|
+
const reason = simulation ? simulationUnavailable(base) : null;
|
|
1439
|
+
if (reason) {
|
|
1440
|
+
srSay(t(`sim.indisponivel.${reason}`));
|
|
1441
|
+
return false;
|
|
1442
|
+
} // a VISIBLE, explained refusal (ADR-0076)
|
|
1443
|
+
const nextVisual = { ...base, simulacao: simulation };
|
|
1444
|
+
if (player) {
|
|
1445
|
+
player.visual = nextVisual;
|
|
1446
|
+
player.viz = key;
|
|
1447
|
+
}
|
|
1448
|
+
worldState = nextVisual;
|
|
1449
|
+
recomposeWorldFilter();
|
|
1450
|
+
return true;
|
|
1451
|
+
};
|
|
1452
|
+
// ONE LIST, NOT SEVEN BUTTONS (ADR-0159 rule 7) — `ui/simulation-list` says why, and it is not wiring.
|
|
1453
|
+
const simulationPicker = createSimulationList({
|
|
1454
|
+
t: translator.t,
|
|
1455
|
+
find: $,
|
|
1456
|
+
make: (tag) => doc.createElement(tag),
|
|
1457
|
+
keys: WORLD_SIMULATIONS,
|
|
1458
|
+
running: () => worldState.simulacao ?? null,
|
|
1459
|
+
// a refused simulation (ADR-0076) is announced by `simular` and the re-render puts the list back on what runs
|
|
1460
|
+
picked: (key) => { simulate(0, key); empathy.render(); },
|
|
1461
|
+
});
|
|
1462
|
+
const empathy = initSettingsEmpathy({
|
|
1463
|
+
t: translator.t, $, srSay, store,
|
|
1464
|
+
hearing: mixer,
|
|
1465
|
+
renderVizGroup: (listSelector) => { simulationPicker.render(listSelector); },
|
|
1466
|
+
reflectMobilityEmpathy: reflectMobilitySimulations,
|
|
1467
|
+
reflectVizButtons: noEffect,
|
|
1468
|
+
frontOverlay: overlays.frontOverlay,
|
|
1469
|
+
fillExplain: overlays.fillExplain,
|
|
1470
|
+
restoreFocus: overlays.restoreFocus,
|
|
1471
|
+
setHearingLoss: (on) => {
|
|
1472
|
+
setHearingLossGraph(on);
|
|
1473
|
+
store.set(KEYS.hearingloss, on);
|
|
1474
|
+
srSay(t(on ? 'sr.empathy.hearingOn' : 'sr.empathy.hearingOff'));
|
|
1475
|
+
},
|
|
1476
|
+
setOneButton: (on) => {
|
|
1477
|
+
state.setOneButtonValue(on);
|
|
1478
|
+
srSay(t(on ? 'sr.empathy.onebtnOn' : 'sr.empathy.onebtnOff'));
|
|
1479
|
+
reflectMobilitySimulations();
|
|
1480
|
+
},
|
|
1481
|
+
setWheelchair: noEffect,
|
|
1482
|
+
getOneButton: () => state.oneButton,
|
|
1483
|
+
getWheelchair: () => DEFAULTS.wheelchair,
|
|
1484
|
+
getPlayers: () => [{ viz: worldState.simulacao ?? 'normal' }],
|
|
1485
|
+
setPlayerViz: (i, rowMode) => { simulate(i, rowMode); },
|
|
1486
|
+
});
|
|
1487
|
+
// «sem força para segurar» is wired here: the empathy module predates it. Refused over toggle keys (ADR-0076): a latch
|
|
1488
|
+
// would hold what the simulation lets go, and the demonstration would show the accommodation instead of the difficulty.
|
|
1489
|
+
$('#opt-semforca')?.addEventListener('click', () => {
|
|
1490
|
+
const wire = !state.noGripStrength;
|
|
1491
|
+
if (wire && players().some((p) => p.toggleMove)) {
|
|
1492
|
+
srAlert(t('sim.indisponivel.alternancia'));
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
state.setNoGripStrengthValue(wire);
|
|
1496
|
+
srSay(t(wire ? 'sr.empathy.semforcaOn' : 'sr.empathy.semforcaOff'));
|
|
1497
|
+
empathy.render();
|
|
1498
|
+
});
|
|
1499
|
+
// and the panel's «restore defaults» turns it off too, after the module's own reset
|
|
1500
|
+
$('#empathy-reset')?.addEventListener('click', () => { state.setNoGripStrengthValue(false); empathy.render(); });
|
|
1501
|
+
engineActions.empatia = empathyPanel.open;
|
|
1502
|
+
/*
|
|
1503
|
+
* HEARING ACCESSIBILITY — the biggest of the panels, and the one with the most to lose by not existing.
|
|
1504
|
+
*
|
|
1505
|
+
* 📏 The panel reached nodes it never created; `mountAudioInside`, beside it, builds them. ⚠️ And the inside goes in
|
|
1506
|
+
* BEFORE the `init`, by the order rule the panels above already paid for: `initSettingsAudio` wires its clicks once,
|
|
1507
|
+
* at boot.
|
|
1508
|
+
*
|
|
1509
|
+
* 📌 None of the `ctx` fields is the game's: the mixer, the volume, blind mode, the cane and the voice are all the
|
|
1510
|
+
* engine's, and the cartridge's players serve as they are. It was the most expensive panel to mount and the least
|
|
1511
|
+
* dependent on who mounts it.
|
|
1512
|
+
*/
|
|
1513
|
+
const audioPanel = mountPanel(panelCtx, {
|
|
1514
|
+
id: 'audio',
|
|
1515
|
+
// 📌 THE SHELL'S LIST IS NAVIGATION SOUND since ADR-0151: the taste categories went to «Áudio». The id
|
|
1516
|
+
// `navsound-list` is the one `initSettingsAudio` fills with sonar, cane and guide.
|
|
1517
|
+
listId: 'navsound-list',
|
|
1518
|
+
labels: () => ({
|
|
1519
|
+
title: t('menu.audio'),
|
|
1520
|
+
listLabel: t('audio.navsound.grupo'),
|
|
1521
|
+
resetLabel: t('menu.restoreDefaults'),
|
|
1522
|
+
closeLabel: t('pause.pmback'),
|
|
1523
|
+
}),
|
|
1524
|
+
/*
|
|
1525
|
+
* ⚠️ THE INSIDE GOES IN AT RENDER, not only at mounting. The frame is retranslated (`MountPanelSpec.labels`); the
|
|
1526
|
+
* inside, run once, would keep the text of the boot interval, where the language is still the fallback — the
|
|
1527
|
+
* title in English and the rows in Portuguese on the same screen, as measured in a real browser with `lang="en"`.
|
|
1528
|
+
*
|
|
1529
|
+
* 📌 `mountAudioInside` RELABELS what exists instead of rebuilding it — rebuilding would leave controls in the
|
|
1530
|
+
* document with no listener. And no unit test catches this: they all run in one language.
|
|
1531
|
+
*/
|
|
1532
|
+
render: () => {
|
|
1533
|
+
mountAudioInside(translator.t, panelCtx, audioPanel.shell.card, audioPanel.shell.list);
|
|
1534
|
+
hideRowsWithoutSubject();
|
|
1535
|
+
audio?.renderAudio();
|
|
1536
|
+
},
|
|
1537
|
+
});
|
|
1538
|
+
mountAudioInside(translator.t, panelCtx, audioPanel.shell.card, audioPanel.shell.list);
|
|
1539
|
+
/*
|
|
1540
|
+
* AUDIO — general sound and the four taste categories (ADR-0151 §2 item 4), apart from hearing accessibility.
|
|
1541
|
+
* ⚠️ MOUNTED BEFORE `initSettingsAudio`, by the siblings' order rule: this panel's master switch, volume and «restore»
|
|
1542
|
+
* are wired ONCE, at boot.
|
|
1543
|
+
*/
|
|
1544
|
+
const soundPanel = mountPanel(panelCtx, {
|
|
1545
|
+
id: 'som',
|
|
1546
|
+
listId: 'audio-list',
|
|
1547
|
+
labels: () => ({
|
|
1548
|
+
title: t('menu.som'),
|
|
1549
|
+
listLabel: t('audio.grupo.rotulo'),
|
|
1550
|
+
resetLabel: t('menu.restoreDefaults'),
|
|
1551
|
+
closeLabel: t('pause.pmback'),
|
|
1552
|
+
}),
|
|
1553
|
+
render: () => {
|
|
1554
|
+
mountSoundInside(translator.t, panelCtx, soundPanel.shell.card, soundPanel.shell.list);
|
|
1555
|
+
audio?.renderAudio();
|
|
1556
|
+
},
|
|
1557
|
+
});
|
|
1558
|
+
mountSoundInside(translator.t, panelCtx, soundPanel.shell.card, soundPanel.shell.list);
|
|
1559
|
+
// The two hearing-panel rows that exist only where there is a subject — `ui/audio-rows-that-apply` says why, and it
|
|
1560
|
+
// is not wiring. ⚠️ Read at every opening: the topology is a function, and a game changes its demands between phases (ADR-0084).
|
|
1561
|
+
const hideRowsWithoutSubject = () => showOnlyRowsThatApply({
|
|
1562
|
+
find: $,
|
|
1563
|
+
caneWord: () => subjectWord(cartridge.accommodations, 'caneSpacing'),
|
|
1564
|
+
hasNavigationSound: () => contractSubjects({
|
|
1565
|
+
declaration: cartridge.declaration,
|
|
1566
|
+
actions: cartridge.preset ? presetActions(cartridge.preset) : [],
|
|
1567
|
+
players: players().length,
|
|
1568
|
+
}).has('navigationSound'),
|
|
1569
|
+
});
|
|
1570
|
+
hideRowsWithoutSubject();
|
|
1571
|
+
audio = initSettingsAudio({
|
|
1572
|
+
translator, $, srSay, store,
|
|
1573
|
+
// the page's settings store, and the ROOT'S door to its bus: the panel's subscription ends with `dispose()` (ADR-0220)
|
|
1574
|
+
settings: state, on: stateOn,
|
|
1575
|
+
/*
|
|
1576
|
+
* 🔴 THE AUDIO PANEL'S BROWSER COMES FROM HERE (ADR-0227). What ADR-0221 step 7d asks is not that nobody touches the
|
|
1577
|
+
* browser — it is that whoever touches it is whoever RECEIVED it. This root receives `doc` and `win` from the host
|
|
1578
|
+
* (`EngineHost`), so the reach moves place and not owner: out of a panel that does not know which page it is in,
|
|
1579
|
+
* into the only module of the tree that legitimately knows.
|
|
1580
|
+
*/
|
|
1581
|
+
newElement: (tag) => doc.createElement(tag),
|
|
1582
|
+
speech: {
|
|
1583
|
+
voices: () => { try {
|
|
1584
|
+
return win.speechSynthesis?.getVoices() ?? [];
|
|
1585
|
+
}
|
|
1586
|
+
catch (e) {
|
|
1587
|
+
return [];
|
|
1588
|
+
} },
|
|
1589
|
+
speakSample: (sample, chosen) => {
|
|
1590
|
+
try {
|
|
1591
|
+
const ss = win.speechSynthesis;
|
|
1592
|
+
if (!ss)
|
|
1593
|
+
return;
|
|
1594
|
+
ss.cancel();
|
|
1595
|
+
const u = speech.utterance(sample);
|
|
1596
|
+
u.lang = 'pt-BR';
|
|
1597
|
+
if (chosen)
|
|
1598
|
+
u.voice = chosen;
|
|
1599
|
+
u.rate = 1;
|
|
1600
|
+
u.volume = 1;
|
|
1601
|
+
ss.speak(u);
|
|
1602
|
+
}
|
|
1603
|
+
catch (e) { /* the device refused to speak; the panel already says what it can do */ }
|
|
1604
|
+
},
|
|
1605
|
+
whenVoicesChange: (again) => {
|
|
1606
|
+
try {
|
|
1607
|
+
if (win.speechSynthesis)
|
|
1608
|
+
win.speechSynthesis.onvoiceschanged = again;
|
|
1609
|
+
}
|
|
1610
|
+
catch (e) { /* noop */ }
|
|
1611
|
+
// one slot per page and the last root takes it: an ended root empties it only if it is still its own (ADR-0220)
|
|
1612
|
+
whenDisposed(() => {
|
|
1613
|
+
try {
|
|
1614
|
+
if (win.speechSynthesis?.onvoiceschanged === again)
|
|
1615
|
+
win.speechSynthesis.onvoiceschanged = null;
|
|
1616
|
+
}
|
|
1617
|
+
catch (e) { /* noop */ }
|
|
1618
|
+
});
|
|
1619
|
+
},
|
|
1620
|
+
},
|
|
1621
|
+
audioOutputs: {
|
|
1622
|
+
canList: () => !!(win.navigator?.mediaDevices && win.navigator.mediaDevices.enumerateDevices),
|
|
1623
|
+
canRoute: () => {
|
|
1624
|
+
const w = win;
|
|
1625
|
+
return typeof (w.AudioContext ?? w.webkitAudioContext) !== 'undefined';
|
|
1626
|
+
},
|
|
1627
|
+
list: async () => {
|
|
1628
|
+
const md = win.navigator?.mediaDevices;
|
|
1629
|
+
if (!md?.enumerateDevices)
|
|
1630
|
+
return [];
|
|
1631
|
+
return (await md.enumerateDevices()).filter((d) => d.kind === 'audiooutput');
|
|
1632
|
+
},
|
|
1633
|
+
detect: async () => {
|
|
1634
|
+
const md = win.navigator?.mediaDevices;
|
|
1635
|
+
if (!md?.enumerateDevices)
|
|
1636
|
+
return [];
|
|
1637
|
+
// 📌 The permission is what gives the outputs NAMES: without it the browser lists anonymous devices, and a choice
|
|
1638
|
+
// between «Saída 1» and «Saída 2» is not a choice. The track is let go at once.
|
|
1639
|
+
await md.getUserMedia?.({ audio: true }).then((s) => s.getTracks().forEach((t) => t.stop())).catch(() => { });
|
|
1640
|
+
return (await md.enumerateDevices()).filter((d) => d.kind === 'audiooutput');
|
|
1641
|
+
},
|
|
1642
|
+
},
|
|
1643
|
+
audioCats: AUDIO_CATS,
|
|
1644
|
+
toggleBtn,
|
|
1645
|
+
getNumPlayers: () => players().length,
|
|
1646
|
+
getPlayers: () => cartridge.players ?? [],
|
|
1647
|
+
getSoundOn: () => mixer.soundOn,
|
|
1648
|
+
setSoundOn,
|
|
1649
|
+
getVolume: () => mixer.volume,
|
|
1650
|
+
setVolume,
|
|
1651
|
+
getAudioCat: () => mixer.audioCat,
|
|
1652
|
+
setCatGain,
|
|
1653
|
+
tts,
|
|
1654
|
+
getBlindMode: readBlindMode,
|
|
1655
|
+
// 📌 The `core/state` pattern: store, persist, notify. The game effects are a REACTION, and whoever reacts
|
|
1656
|
+
// subscribes to `on('blindMode', …)` — the same decision `ui/pause-icons` took for the icon.
|
|
1657
|
+
setBlindMode: state.setBlindModeValue,
|
|
1658
|
+
getCaneBlockDiv: () => state.caneBlockDiv,
|
|
1659
|
+
setCaneBlockDiv: state.setCaneBlockDivValue,
|
|
1660
|
+
fillExplain: overlays.fillExplain,
|
|
1661
|
+
});
|
|
1662
|
+
engineActions.audio = audioPanel.open;
|
|
1663
|
+
engineActions.som = soundPanel.open;
|
|
1664
|
+
}
|
|
1665
|
+
// 4b. NAVIGATION SOUND. Only the contract goes in: no tile, collision box or coin array.
|
|
413
1666
|
const sonar = createAudioSonar({
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
//
|
|
419
|
-
//
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
//
|
|
425
|
-
//
|
|
426
|
-
|
|
1667
|
+
t: translator.t,
|
|
1668
|
+
topology: () => cartridge.declaration.topology(),
|
|
1669
|
+
targetsOf: (i) => cartridge.declaration.targetsOf(i),
|
|
1670
|
+
nameAt: (at) => cartridge.declaration.nameAt(at),
|
|
1671
|
+
// Field 2 + the mixer bus: what the CONTINUOUS GUIDE needs and the beep did not (#84 item 2). `roleAt` is what lets
|
|
1672
|
+
// the route go round a wall; `catNode`/`audioOut`/`getVolume` put a PERMANENT graph on the same volume slider as
|
|
1673
|
+
// all the rest of the audio.
|
|
1674
|
+
roleAt: (at) => cartridge.declaration.roleAt(at),
|
|
1675
|
+
tonePan, srSay, narrate: (text) => tts.narrate(text),
|
|
1676
|
+
catNode, audioOut, getVolume: () => mixer.volume,
|
|
1677
|
+
// ⚠️ THE ANSWER, NOT THE TABLE (#104): `platform/audio-sonar` does not know what a visual mode is, so it is answered
|
|
1678
|
+
// here — the root is the only layer that knows both axes AND may import from `render/`.
|
|
1679
|
+
visionImpaired: (pl) => {
|
|
427
1680
|
const v = pl.visual;
|
|
428
|
-
return !!v && (
|
|
1681
|
+
return !!v && (isBlind(v) || isLowVision(v));
|
|
429
1682
|
},
|
|
430
|
-
|
|
431
|
-
//
|
|
432
|
-
//
|
|
433
|
-
getPlayers:
|
|
434
|
-
const f =
|
|
435
|
-
return f ? [{ i: 0, x: f.at.x, y: f.at.y, visual:
|
|
1683
|
+
getBlindMode: readBlindMode, LOGICAL_W,
|
|
1684
|
+
// The player DERIVED from the focus: field 4 answering where the child is. A game that supplies no list still has
|
|
1685
|
+
// the sonar, and that is what makes the accessibility stack not an accessory.
|
|
1686
|
+
getPlayers: cartridge.sonarPlayers ?? (() => {
|
|
1687
|
+
const f = cartridge.declaration.focusOf(0);
|
|
1688
|
+
return f ? [{ i: 0, x: f.at.x, y: f.at.y, visual: DEFAULT_VISUAL }] : [];
|
|
436
1689
|
}),
|
|
437
|
-
getNumPlayers: () => (
|
|
438
|
-
getAudioCtx: () => audioCtx, getSoundOn: () => soundOn, getAudioCat: () => audioCat,
|
|
1690
|
+
getNumPlayers: () => (cartridge.players ?? [null]).length,
|
|
1691
|
+
getAudioCtx: () => mixer.audioCtx, getSoundOn: () => mixer.soundOn, getAudioCat: () => mixer.audioCat,
|
|
1692
|
+
newContext: newAudioContext,
|
|
439
1693
|
});
|
|
440
|
-
// 5.
|
|
1694
|
+
// 5. Remappable keyboard — the best cut of the base (finding 11): a key scheme, no world.
|
|
441
1695
|
//
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
}
|
|
468
|
-
registrarMapeamentosDoCartucho();
|
|
469
|
-
initKB();
|
|
470
|
-
// ⚠️ O ESQUEMA DE ARRANQUE ALCANÇA NADA, e diz isso com `null` em vez de com um objeto vazio (issue #118).
|
|
471
|
-
// Ele vive um instante — `assignControls()` logo abaixo substitui-o pelo esquema real —, mas enquanto vive
|
|
472
|
-
// é um `KeyScheme` como qualquer outro, e a única forma honesta de um esquema que não alcança nada é
|
|
473
|
-
// catorze ausências declaradas. Um `{}` fazia o tipo mentir sobre estar completo.
|
|
474
|
-
const semAlcance = Object.fromEntries(ACTIONS.map((a) => [a, null]));
|
|
475
|
-
// ⚠️ O FALLBACK É UMA CONSTANTE e não um literal novo a cada chamada: `getPlayers` é lido pelo runtime de
|
|
476
|
-
// teclado a cada leitura de controlo, e devolver um array novo de cada vez faria qualquer comparação de
|
|
477
|
-
// identidade mentir — um defeito que só aparece em quem compara, e tarde.
|
|
478
|
-
const semJogadores = [{ ctrl: semAlcance }];
|
|
479
|
-
// ⚠️ LÊ `cartucho.players`, E NÃO UM INSTANTÂNEO. Os getters já existiam; o que eles fechavam é que era um `const`
|
|
480
|
-
// tirado no arranque. As linhas de `initPauseIcons` e do sonar, neste mesmo ficheiro, já liam a fonte viva —
|
|
481
|
-
// esta era a que faltava. Com vários cartuchos numa raiz de composição (ADR-0142), o teclado ficava com os
|
|
482
|
-
// jogadores do cartucho que arrancou primeiro.
|
|
483
|
-
const players = () => cartucho.players ?? semJogadores;
|
|
1696
|
+
/*
|
|
1697
|
+
* THE MOUNTED CARTRIDGE'S TWO DEFAULT MAPPINGS (ADR-0115), held by THIS root (ADR-0232 D4): they were module registrations,
|
|
1698
|
+
* and a second root overwrote the first root's. `null` is the honest value of «this game has no opinion», and it is also
|
|
1699
|
+
* what `unmount()` sets. The keyboard config reads the mapping through a closure, so a `mount()` answers with the new
|
|
1700
|
+
* cartridge's; the pad table is REBUILT, because its memo belongs to one mapping — a kept memo would serve the previous
|
|
1701
|
+
* game's table.
|
|
1702
|
+
*/
|
|
1703
|
+
let keyboardMappingNow = null;
|
|
1704
|
+
let padTableNow = createPadTable(null);
|
|
1705
|
+
function followCartridgeMappings(declaration) {
|
|
1706
|
+
const keyboardMapping = declaration?.keyboardMapping;
|
|
1707
|
+
keyboardMappingNow = keyboardMapping ? (players, seat) => keyboardMapping(players, seat) : null;
|
|
1708
|
+
const padMapping = declaration?.padMapping;
|
|
1709
|
+
padTableNow = createPadTable(padMapping ? (players, seat) => padMapping(players, seat) : null);
|
|
1710
|
+
}
|
|
1711
|
+
followCartridgeMappings(cartridge.declaration);
|
|
1712
|
+
// ⚠️ THE GAME'S DEFAULT IS KNOWN BEFORE `load()`, and the order is the rule: whoever reads the disk must already know which
|
|
1713
|
+
// factory the child's data overlays (ADR-0115).
|
|
1714
|
+
const keyboardConfig = createKeyboardConfig({
|
|
1715
|
+
store, mapping: (players, seat) => keyboardMappingNow?.(players, seat) ?? null,
|
|
1716
|
+
});
|
|
1717
|
+
keyboardConfig.load();
|
|
1718
|
+
// THE CHILD'S PAD MAPS, one cache for this root, handed to BOTH readers: the motor panel's wizard and the gamepad.
|
|
1719
|
+
const padMaps = createPadMaps(store);
|
|
1720
|
+
// (`withoutReach`, `withoutPlayers` and `players` are hoisted above `initPauseIcons` — issue #147.)
|
|
484
1721
|
const keyboard = initKeyboardRuntime({
|
|
485
|
-
getKB:
|
|
1722
|
+
getKB: keyboardConfig.kb, getNumPlayers: () => players().length, getPlayers: () => players(),
|
|
486
1723
|
});
|
|
1724
|
+
/** The «Mapear teclado» panel, once mounted. Declared here because the menu navigation, just below, asks it whether it
|
|
1725
|
+
* is capturing a key — and it is born only with the motor panel, further on. */
|
|
1726
|
+
let keyboardControls = null;
|
|
487
1727
|
keyboard.assignControls();
|
|
488
|
-
|
|
1728
|
+
/*
|
|
1729
|
+
* THE THREE ANSWERS THE CARTRIDGE MAY REPLACE, resolved ONCE. They feed the menu navigation and, since ADR-0224, the
|
|
1730
|
+
* gamepad too — and writing the same `??` in two places is writing the same decision twice.
|
|
1731
|
+
*/
|
|
1732
|
+
const isOnBar = cartridge.onBar ?? ((i) => pauseIcons.onBar(i));
|
|
1733
|
+
const navBar = cartridge.navBar ?? ((i, k, withStart) => pauseIcons.navBar(i, k, withStart));
|
|
1734
|
+
const setPauseActor = cartridge.setPauseActor ?? (() => { });
|
|
1735
|
+
// 6. Menu navigation.
|
|
489
1736
|
const nav = initMenuNav({
|
|
490
|
-
$, getActiveElement: () => doc.activeElement,
|
|
1737
|
+
$, t: translator.t, getActiveElement: () => doc.activeElement,
|
|
491
1738
|
topVisibleOverlay: overlays.topVisibleOverlay, closeById: overlays.closeById,
|
|
492
1739
|
getPauseMenu: (i) => $(`#vp-pause-${i}`),
|
|
493
|
-
|
|
494
|
-
|
|
1740
|
+
// ⚠️ NOT `cartridge.setPhase ?? (() => {})`: «no» at the card's root calls `setPhase('playing')` (`ui/menu-nav`) and
|
|
1741
|
+
// nothing more — it hides nothing —, so in a game without the hook Escape did not close the pause ADR-0144 opens. It
|
|
1742
|
+
// goes through the same `changePhase` as the «continuar» item: one way out, whatever door the child leaves by.
|
|
1743
|
+
setPhase: changePhase,
|
|
1744
|
+
setPauseActor,
|
|
495
1745
|
srSay,
|
|
496
|
-
//
|
|
497
|
-
//
|
|
498
|
-
|
|
499
|
-
|
|
1746
|
+
// With no declared opinion, the index is ON: whoever needs it to find their way cannot know it exists if it comes
|
|
1747
|
+
// switched off (the same reason blind mode is born with narration and sonar).
|
|
1748
|
+
withIndex: cartridge.withIndex ?? (() => true),
|
|
1749
|
+
explainItem: (text) => writeInFooter(text),
|
|
1750
|
+
isNavigable: cartridge.isNavigable ?? (() => true),
|
|
500
1751
|
/*
|
|
501
|
-
* ⚠️
|
|
502
|
-
*
|
|
503
|
-
*
|
|
1752
|
+
* ⚠️ THE DEFAULT IS THE ENGINE'S BAR, and not `() => false` / `() => {}`: this root MOUNTS the bar (ADR-0106 step 2).
|
|
1753
|
+
* With these two as no-ops the bar would exist and **could not be navigated by keyboard or gamepad** — reachable only
|
|
1754
|
+
* by pointer. For a blind child, who navigates by keyboard, a bar she cannot reach is the same as no bar — exactly
|
|
1755
|
+
* the offer-the-path-then-refuse-it that ADR-0106 §5 forbids.
|
|
504
1756
|
*
|
|
505
|
-
*
|
|
506
|
-
* por controle**: alcançável só por ponteiro. Para uma criança cega, que navega por teclado, uma barra
|
|
507
|
-
* que ela não alcança é o mesmo que barra nenhuma — e é exactamente o «oferece o caminho e depois
|
|
508
|
-
* recusa-o» que o §5 do ADR-0106 proíbe.
|
|
1757
|
+
* The engine answers with ITS instance, the same one that mounted the bar. Whoever injects still rules.
|
|
509
1758
|
*
|
|
510
|
-
*
|
|
511
|
-
*
|
|
512
|
-
* ⚠️ E FICA UMA METADE POR LIGAR, dita aqui em vez de descoberta: o `navBar` do `ui/menu-nav` recebe
|
|
513
|
-
* `(i, k)` e não o terceiro argumento `temStart`, que é a borda do botão de pausa — a SEGUNDA saída do
|
|
514
|
-
* modo (ADR-0044 item 7). No cartucho ela chega por outra rota (o encaminhador do gamepad, `main.ts:1470`)
|
|
515
|
-
* que esta raiz ainda não monta. Logo: o direcional navega a barra; sair por START, por enquanto, não.
|
|
1759
|
+
* 📌 `ui/menu-nav` calls `navBar` with `(i, k)` and never the third argument, the START edge — the SECOND way out of
|
|
1760
|
+
* the mode (ADR-0044 item 7). That one arrives through the gamepad transport, which this root mounts below.
|
|
516
1761
|
*/
|
|
517
|
-
|
|
518
|
-
navBar
|
|
519
|
-
|
|
520
|
-
|
|
1762
|
+
onBar: isOnBar,
|
|
1763
|
+
navBar,
|
|
1764
|
+
// ⚠️ NOT `() => false`: the remapping panel captures a key (ADR-0151), and without this the arrow the child wants to
|
|
1765
|
+
// record would move the menu instead of landing on the key.
|
|
1766
|
+
isCapturing: () => keyboardControls?.isCapturing() ?? false,
|
|
1767
|
+
// Escape on the controller-mapping panel (`#padwiz`) closes it the way «Voltar» does: through the panel's own closer,
|
|
1768
|
+
// registered with the overlays, which cancels a running wizard without storing its map.
|
|
1769
|
+
closePadWiz: () => { overlays.closeById('padwiz'); },
|
|
521
1770
|
whichPlayer: (code) => keyboard.whichPlayer(code),
|
|
522
1771
|
actionOf: (code, i) => keyboard.actionOf(code, i),
|
|
523
|
-
//
|
|
524
|
-
//
|
|
525
|
-
// `WindowEventMap` (ver `EventTargetLike` em input/touch-bindings), então o `window` entra direto.
|
|
1772
|
+
// Finding 12, SOLVED IN THE ENGINE: the port is generic over `WindowEventMap` (see `EventTargetLike` in
|
|
1773
|
+
// input/touch-bindings), so the real `window` goes straight in, with no adapter per consumer.
|
|
526
1774
|
win,
|
|
527
1775
|
});
|
|
528
|
-
// ⚠️
|
|
529
|
-
//
|
|
530
|
-
//
|
|
531
|
-
// respondem só ao RATO, o que é o pilar 2 a falhar por inteiro — e o `consumer-quiz` teve de a chamar à mão
|
|
532
|
-
// depois do `createGame`, que é o sintoma da fronteira estar no lugar errado.
|
|
533
|
-
//
|
|
534
|
-
// Um fio que a engine MONTA e não liga é pior do que um que ela não monta: a ausência seria visível — o
|
|
535
|
-
// objeto tem uma `nav`, e ela parece pronta.
|
|
1776
|
+
// ⚠️ AND IT IS SWITCHED ON. A wire the engine MOUNTS and does not switch on is worse than one it does not mount: the
|
|
1777
|
+
// absence would be visible — the object has a `nav`, and it looks ready — while the accessibility dialogs and the pause
|
|
1778
|
+
// menu answered only to the MOUSE, pillar 2 failing whole.
|
|
536
1779
|
//
|
|
537
|
-
//
|
|
538
|
-
//
|
|
1780
|
+
// Attaching here is safe before the game finishes booting: with no dialog open and no pause menu, `menuNavKey`
|
|
1781
|
+
// consumes no key and lets it go on to its owner.
|
|
1782
|
+
/*
|
|
1783
|
+
* 🔴 ONE BUTTON ONLY TAKES THE KEY BEFORE EVERYONE ELSE (ADR-0218, issue #201), and that is why this listener is registered
|
|
1784
|
+
* HERE, above `nav.attach()`, instead of beside the cool-down and the simulations further down.
|
|
1785
|
+
*
|
|
1786
|
+
* With the scan on, the child has ONE input in the world, and every press of it means the same thing: take what is showing.
|
|
1787
|
+
* It must not also move a menu, reach the game or feed the motor filters — so the press is stopped dead (`barrar`) and the
|
|
1788
|
+
* scan decides what happens. Listeners on one node run in the order they were registered, and `stopImmediatePropagation`
|
|
1789
|
+
* only reaches the ones after it: registered below the menu navigation, this would have let the menu move AND the scan take,
|
|
1790
|
+
* which is one press doing two things.
|
|
1791
|
+
*
|
|
1792
|
+
* ⚠️ `scanPress` is filled in much later, where the virtual controller exists. Until then, and whenever the scan is off,
|
|
1793
|
+
* this listener answers nothing — the same hoisting the pause card's host uses, and for the same reason: what has to run
|
|
1794
|
+
* first is not what can be built first.
|
|
1795
|
+
*/
|
|
1796
|
+
const blockKey = (e) => { e.preventDefault(); e.stopImmediatePropagation(); };
|
|
1797
|
+
let scanPress = null;
|
|
1798
|
+
win.addEventListener('keydown', (e) => {
|
|
1799
|
+
if (!state.switchScan || !scanPress || keyboard.whichPlayer(e.code) < 0)
|
|
1800
|
+
return;
|
|
1801
|
+
// A HELD KEY IS ONE PRESS, not one a frame: a child who cannot let go would otherwise take an item every repeat.
|
|
1802
|
+
if (!e.repeat)
|
|
1803
|
+
scanPress(sourceOfEvent(e) ?? 'teclado');
|
|
1804
|
+
blockKey(e);
|
|
1805
|
+
}, true);
|
|
1806
|
+
win.addEventListener('keyup', (e) => {
|
|
1807
|
+
if (state.switchScan && scanPress && keyboard.whichPlayer(e.code) >= 0)
|
|
1808
|
+
blockKey(e);
|
|
1809
|
+
}, true);
|
|
1810
|
+
// ⚠️ AND NOW THE MENU NAVIGATION SWITCHES ON — after the block above, and the ORDER IS THE BEHAVIOUR: listeners on one
|
|
1811
|
+
// node run in registration order, so the scan sees the key first and can stop it. The other way round, one press would
|
|
1812
|
+
// move the menu AND take an item (ADR-0218). 🔴 No type sees a lost call: browser cases hold that the menus answer the
|
|
1813
|
+
// keyboard.
|
|
539
1814
|
nav.attach();
|
|
540
|
-
// ⚠️
|
|
541
|
-
//
|
|
542
|
-
//
|
|
543
|
-
// é pior do que promessa nenhuma: quem usa leitor de tela sai para um jogo cujo estado não percebe.
|
|
1815
|
+
// ⚠️ AND THE FOCUS TRAP. With an overlay open, Tab went into the board underneath, while every `.overlay__card` in the
|
|
1816
|
+
// document says `aria-modal="true"`. A promise the keyboard contradicts is worse than no promise: a screen-reader user
|
|
1817
|
+
// steps out into a game whose state they do not understand.
|
|
544
1818
|
initFocusTrap({
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
1819
|
+
topOverlay: overlays.topVisibleOverlay,
|
|
1820
|
+
currentFocus: () => doc.activeElement,
|
|
1821
|
+
focusablesIn: focusablesInDom,
|
|
548
1822
|
win,
|
|
549
1823
|
}).attach();
|
|
550
1824
|
/**
|
|
551
|
-
*
|
|
1825
|
+
* THE REACH: among the transports AVAILABLE to this child, does any carry this game's actions?
|
|
552
1826
|
*
|
|
553
|
-
* ⚠️
|
|
554
|
-
*
|
|
555
|
-
*
|
|
1827
|
+
* ⚠️ The detection: `pointer:coarse && hover:none` is touch, and the opposite is keyboard. It is wrong on a tablet WITH
|
|
1828
|
+
* a keyboard — and the error is tolerable only because the screen INFORMS instead of refusing. See the header of
|
|
1829
|
+
* `ui/reach-notice`.
|
|
556
1830
|
*/
|
|
557
|
-
const
|
|
1831
|
+
const deviceAvailability = o.availability ?? {
|
|
558
1832
|
gamepad: () => { try {
|
|
559
1833
|
return [...(win.navigator?.getGamepads?.() ?? [])].some(Boolean);
|
|
560
1834
|
}
|
|
561
1835
|
catch {
|
|
562
1836
|
return false;
|
|
563
1837
|
} },
|
|
564
|
-
|
|
1838
|
+
touch: () => { try {
|
|
565
1839
|
return win.matchMedia('(pointer:coarse)').matches && win.matchMedia('(hover:none)').matches;
|
|
566
1840
|
}
|
|
567
1841
|
catch {
|
|
568
1842
|
return false;
|
|
569
1843
|
} },
|
|
570
|
-
|
|
1844
|
+
keyboard: () => { try {
|
|
571
1845
|
return !(win.matchMedia('(pointer:coarse)').matches && win.matchMedia('(hover:none)').matches);
|
|
572
1846
|
}
|
|
573
1847
|
catch {
|
|
574
1848
|
return true;
|
|
575
1849
|
} },
|
|
576
1850
|
/**
|
|
577
|
-
*
|
|
1851
|
+
* THE MOUSE (ADR-0112) — and the probe is `any-pointer` on purpose, not `pointer`.
|
|
578
1852
|
*
|
|
579
|
-
* ⚠️ `(pointer:fine)`
|
|
580
|
-
*
|
|
581
|
-
*
|
|
1853
|
+
* ⚠️ `(pointer:fine)` describes the PRIMARY pointer, so a tablet with a mouse plugged in answers coarse and the mouse
|
|
1854
|
+
* disappeared — exactly the device this question exists to find. `any-pointer:fine` says ANY of the pointing devices
|
|
1855
|
+
* is fine, which is the right question: to draw, one is enough.
|
|
582
1856
|
*
|
|
583
|
-
* ⚠️
|
|
584
|
-
*
|
|
585
|
-
* sonda é uma FUNÇÃO, avaliada a cada pergunta, como as três acima.
|
|
1857
|
+
* ⚠️ A stylus also answers `fine`, and it is a real pointer — so that is not an error. What can be missing is a mouse
|
|
1858
|
+
* plugged in after boot, which is why the probe is a FUNCTION, evaluated at every question, like the three above.
|
|
586
1859
|
*/
|
|
587
|
-
|
|
1860
|
+
mouse: () => { try {
|
|
588
1861
|
return win.matchMedia('(any-pointer:fine)').matches;
|
|
589
1862
|
}
|
|
590
1863
|
catch {
|
|
591
1864
|
return false;
|
|
592
1865
|
} },
|
|
593
1866
|
};
|
|
594
|
-
//
|
|
595
|
-
//
|
|
596
|
-
//
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
*
|
|
600
|
-
*
|
|
601
|
-
*
|
|
602
|
-
*
|
|
603
|
-
*
|
|
604
|
-
*
|
|
605
|
-
*
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
// `parentNode` mas não `remove`. Perguntar pelo método é o que funciona nos dois.
|
|
616
|
-
if (typeof aviso.remove === 'function')
|
|
617
|
-
aviso.remove();
|
|
1867
|
+
// The second axis (ADR-0104 §A) and the third (ADR-0112) come from the game, like the first: how many positions it
|
|
1868
|
+
// holds at once, and whether it needs a pointer. `?? false` and not an invented default: the field is optional on
|
|
1869
|
+
// purpose — see its note —, and its absence means the game does not draw.
|
|
1870
|
+
/*
|
|
1871
|
+
* THE REACH AND ITS NOTICE, in one function, because both depend on the cartridge and the second CREATES DOM.
|
|
1872
|
+
*
|
|
1873
|
+
* ⚠️ The notice is the only place in this root that writes an element from a game's answer, and so the only one that
|
|
1874
|
+
* must be REMOVED before being rewritten: `showReachNotice` creates a `div` with a fixed `id`, so calling it twice
|
|
1875
|
+
* would leave two — and the second cartridge would have the first one's notice under its own.
|
|
1876
|
+
*
|
|
1877
|
+
* 📌 `removeReachNotice()` ALWAYS runs first, not only when there is something to show: a cartridge with nothing to
|
|
1878
|
+
* warn about must erase the previous one's notice, and that is the case that gets forgotten.
|
|
1879
|
+
*/
|
|
1880
|
+
function removeReachNotice() {
|
|
1881
|
+
const notice = $(`#${REACH_NOTICE_ID}`);
|
|
1882
|
+
if (!notice)
|
|
1883
|
+
return;
|
|
1884
|
+
// ⚠️ CAPABILITY AND NOT TYPE, for the reason this file gives about `instanceof HTMLElement`: the host may be a fake
|
|
1885
|
+
// document, and the ones these tests use have `parentNode` but no `remove`. Asking for the method works in both.
|
|
1886
|
+
if (typeof notice.remove === 'function')
|
|
1887
|
+
notice.remove();
|
|
618
1888
|
else
|
|
619
|
-
|
|
620
|
-
}
|
|
621
|
-
function
|
|
622
|
-
const
|
|
623
|
-
const a =
|
|
624
|
-
|
|
625
|
-
// ⚠️
|
|
626
|
-
//
|
|
627
|
-
if (
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
1889
|
+
notice.parentNode?.removeChild(notice);
|
|
1890
|
+
}
|
|
1891
|
+
function deriveReach() {
|
|
1892
|
+
const declaredActions = cartridge.preset ? presetActions(cartridge.preset) : [];
|
|
1893
|
+
const a = reach(defaultTransports(deviceAvailability), declaredActions, cartridge.declaration.holdsAtOnce(), cartridge.declaration.needsPointer?.() ?? false);
|
|
1894
|
+
removeReachNotice();
|
|
1895
|
+
// ⚠️ IT APPEARS ONLY WHEN THERE IS SOMETHING TO SAY. A notice that always appears stops being read, and a game whose
|
|
1896
|
+
// actions fit the touch screen has nothing to warn about — the common case, which must stay silent.
|
|
1897
|
+
if (declaredActions.length) {
|
|
1898
|
+
showReachNotice({
|
|
1899
|
+
find: (sel) => $(sel),
|
|
1900
|
+
create: (tag) => doc.createElement(tag),
|
|
631
1901
|
t,
|
|
632
1902
|
srAlert,
|
|
633
1903
|
}, a);
|
|
634
1904
|
}
|
|
635
1905
|
return a;
|
|
636
1906
|
}
|
|
637
|
-
let
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
1907
|
+
let currentReach = deriveReach();
|
|
1908
|
+
/*
|
|
1909
|
+
* THE RESOLUTION IS THE ENGINE'S (ADR-0163), and the cartridge has no other. The Dev: «A Engine deve forçar isso e
|
|
1910
|
+
* guiar esta construção, de modo que o cartucho não tenha alternativa». The region gets the largest integer multiple
|
|
1911
|
+
* of 320×180 in REAL pixels that fits the stage, never less than 640×360, with the tolerance of ≤5 logical px cut
|
|
1912
|
+
* per side — and again at every window resize (ADR-0001).
|
|
1913
|
+
* ⚠️ The STAGE is the `#stage-wrap`/`.stage-wrap` shell when it exists; without it, the region's parent — the room it has.
|
|
1914
|
+
* ⚠️ BY CAPABILITY, like the rest: a double without `style.setProperty` is not resized, and the boot does not fall for it.
|
|
1915
|
+
*/
|
|
1916
|
+
let appliedScale = null;
|
|
1917
|
+
/**
|
|
1918
|
+
* What departs is SAID (ADR-0163 rule 4): the region's measured size against the one the engine gave it, read when
|
|
1919
|
+
* `problems` is read — a cartridge that resizes the region after boot is seen then, and the line goes when it stops.
|
|
1920
|
+
*/
|
|
1921
|
+
function regionResizedByCartridge() {
|
|
1922
|
+
const regionEl = $('#game-region');
|
|
1923
|
+
if (!appliedScale || !regionEl || typeof regionEl.getBoundingClientRect !== 'function')
|
|
1924
|
+
return null;
|
|
1925
|
+
const r = regionEl.getBoundingClientRect();
|
|
1926
|
+
if (!r.width || !r.height)
|
|
1927
|
+
return null; // not laid out: nothing measured, nothing to accuse
|
|
1928
|
+
const { width: scaledWidth, height: scaledHeight } = appliedScale;
|
|
1929
|
+
if (Math.abs(r.width - scaledWidth) < 1 && Math.abs(r.height - scaledHeight) < 1)
|
|
1930
|
+
return null;
|
|
1931
|
+
return `the cartridge sized #game-region to ${Math.round(r.width)}×${Math.round(r.height)} over the engine's `
|
|
1932
|
+
+ `${Math.round(scaledWidth)}×${Math.round(scaledHeight)}: text and targets stop following the screen, so a child with low vision `
|
|
1933
|
+
+ 'gets them small — the resolution is the engine\'s (ADR-0163): lay the game out inside the region and read `--ui-fs` and `--alvo-min`';
|
|
1934
|
+
}
|
|
1935
|
+
/*
|
|
1936
|
+
* THE CONTEXT OF THE TWO DRAWING REPORTERS (`ui/drawing-problems`), and the three readers are FUNCTIONS on purpose: the
|
|
1937
|
+
* region, the bar and the scale are things this root SWAPS — a `mount()` swaps the cartridge, the bar is born after the
|
|
1938
|
+
* root, and the scale changes at every `resize`. Passing them by value would freeze the first answer, and an element
|
|
1939
|
+
* out of the page still answers `getBoundingClientRect()` without complaint.
|
|
1940
|
+
*/
|
|
1941
|
+
const drawingContext = {
|
|
1942
|
+
region: () => $('#game-region'),
|
|
1943
|
+
bar: () => a11yBar,
|
|
1944
|
+
scale: () => appliedScale,
|
|
1945
|
+
computedStyle: typeof win.getComputedStyle === 'function' ? (el) => win.getComputedStyle(el) : undefined,
|
|
1946
|
+
};
|
|
1947
|
+
function applyResolution() {
|
|
1948
|
+
const regionEl = $('#game-region');
|
|
1949
|
+
const stage = $('#stage-wrap') ?? $('.stage-wrap') ?? (regionEl?.parentElement ?? null);
|
|
1950
|
+
if (!regionEl || !stage || typeof regionEl.style?.setProperty !== 'function')
|
|
1951
|
+
return;
|
|
1952
|
+
const { w, h } = screenBaseSize(Math.max(1, players().length));
|
|
1953
|
+
appliedScale = stageScale(stage.clientWidth || w, stage.clientHeight || h, win.devicePixelRatio || 1, w, h);
|
|
1954
|
+
applyScale(regionEl, appliedScale);
|
|
1955
|
+
crtScanVars(); // the scanline period is one art pixel in REAL pixels, so it follows the scale (study item A5)
|
|
1956
|
+
reserveBarBand();
|
|
1957
|
+
}
|
|
1958
|
+
/**
|
|
1959
|
+
* THE ROOM THE GAME LEAVES FREE UNDER THE TOP EDGE (ADR-0148 §3, erratum of 2026-09-13; issue #160), written as
|
|
1960
|
+
* `--barra-a11y-h` on the region: the bar's own offset, the bar, the line of the pointed icon's NAME under it, and a
|
|
1961
|
+
* light gap of a quarter of that line's font (4 px at 640×360). 📏 Measured at 640×360: the variable said 44 px (the bar alone) and the name, at
|
|
1962
|
+
* 57–87 px, covered the quiz statement. The Dev: «é necessário que exista um leve espaçamento abaixo da barra».
|
|
1963
|
+
* 📌 The name line is counted whether or not a name is showing — reserving only while pointing would move the game
|
|
1964
|
+
* under the child's finger. Measured again at every scale and every typography step: both change the text's size.
|
|
1965
|
+
* ⚠️ Zero without a bar: nothing to reserve.
|
|
1966
|
+
* 📌 With a HUD (ADR-0175) the room also holds the two top columns — points and mission on the left, power on the right —
|
|
1967
|
+
* when either reaches lower than the bar's room; each is narrowed so it never reaches the bar.
|
|
1968
|
+
*/
|
|
1969
|
+
function reserveBarBand() {
|
|
1970
|
+
reserveTopBand({
|
|
1971
|
+
region: $('#game-region'),
|
|
1972
|
+
bar: a11yBar,
|
|
1973
|
+
hud: hudMounted,
|
|
1974
|
+
...(typeof win.getComputedStyle === 'function' ? { computedStyle: (el) => win.getComputedStyle(el) } : {}),
|
|
1975
|
+
});
|
|
1976
|
+
}
|
|
1977
|
+
/*
|
|
1978
|
+
* THE HUD the engine mounts from the cartridge's `hud` (ADR-0168; issue #162). Read on every animation frame while mounted —
|
|
1979
|
+
* the numbers are functions, so a game never has to say «refresh»; only a changed text is written, and a changed text
|
|
1980
|
+
* measures the room again, because a longer number can wrap.
|
|
1981
|
+
*/
|
|
1982
|
+
let hudMounted = null;
|
|
1983
|
+
let hudFrame = false;
|
|
1984
|
+
function mountHud() {
|
|
1985
|
+
hudMounted?.remove();
|
|
1986
|
+
hudMounted = null;
|
|
1987
|
+
const regionEl = $('#game-region');
|
|
1988
|
+
const numbers = cartridge.hud ?? [];
|
|
1989
|
+
if (numbers.length && regionEl && typeof regionEl.appendChild === 'function')
|
|
1990
|
+
hudMounted = mountHudBands(translator.t, doc, regionEl, numbers);
|
|
1991
|
+
reserveBarBand();
|
|
1992
|
+
if (hudMounted && !hudFrame && typeof win.requestAnimationFrame === 'function') {
|
|
1993
|
+
hudFrame = true;
|
|
1994
|
+
const position = () => {
|
|
1995
|
+
if (!hudMounted) {
|
|
1996
|
+
hudFrame = false;
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
if (hudMounted.refresh())
|
|
2000
|
+
reserveBarBand();
|
|
2001
|
+
win.requestAnimationFrame(position);
|
|
2002
|
+
};
|
|
2003
|
+
win.requestAnimationFrame(position);
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
mountHud();
|
|
2007
|
+
applyResolution();
|
|
2008
|
+
if (typeof win.addEventListener === 'function')
|
|
2009
|
+
win.addEventListener('resize', applyResolution);
|
|
2010
|
+
/*
|
|
2011
|
+
* THE SKIP LINK, when the page has none (study item C5; WCAG 2.4.1). The stylesheet rule, the dictionary sentence and its
|
|
2012
|
+
* layer (ADR-0102) already existed, and the element depended on each page remembering it: the quiz writes its own, and a
|
|
2013
|
+
* cartridge page that did not copy it left a keyboard no way over what precedes the game. First in the body, so it is the
|
|
2014
|
+
* first thing a keyboard reaches; a page's own link is kept. By capability: a host double without a body mounts nothing.
|
|
2015
|
+
*/
|
|
2016
|
+
if (doc.body && typeof doc.body.insertBefore === 'function' && !$('.skip-link')) {
|
|
2017
|
+
const skipLink = doc.createElement('a');
|
|
2018
|
+
skipLink.className = 'skip-link';
|
|
2019
|
+
skipLink.setAttribute('href', '#game-region');
|
|
2020
|
+
skipLink.setAttribute('data-i18n', 'skip.toGame');
|
|
2021
|
+
skipLink.textContent = t('skip.toGame');
|
|
2022
|
+
doc.body.insertBefore(skipLink, doc.body.firstChild); // `data-i18n`: every `setLocale` rewrites it, the boot's included
|
|
2023
|
+
}
|
|
2024
|
+
/*
|
|
2025
|
+
* THE LETTER CASE REACHES THE PAGE (ADR-0028; ADR-0149 §1). The stylesheet capitalises under `:root[data-letras="upper"]`,
|
|
2026
|
+
* and nothing wrote that attribute: the communication button's first position kept «capitals» in the state and showed
|
|
2027
|
+
* natural case (reported by the Dev). Written at every change, and at boot only when the child CHOSE a case — the state's
|
|
2028
|
+
* default is `upper` (ADR-0028) while the cycle's default is position (c), natural case (ADR-0149), and writing the default
|
|
2029
|
+
* would put every new child's game in capitals.
|
|
2030
|
+
*/
|
|
2031
|
+
const writeBox = (c) => {
|
|
2032
|
+
if (doc.documentElement?.dataset)
|
|
2033
|
+
doc.documentElement.dataset.letras = c;
|
|
2034
|
+
};
|
|
2035
|
+
if (store.get(KEYS.letterCase, null) !== null)
|
|
2036
|
+
writeBox(state.letterCase);
|
|
2037
|
+
stateOn('letterCase', writeBox);
|
|
2038
|
+
{
|
|
2039
|
+
/*
|
|
2040
|
+
* THE BAR IS HUD, AND THE GAME DOES NOT WRITE OVER IT (ADR-0148 §3). `#title-icons` is `position:absolute` INSIDE
|
|
2041
|
+
* `#game-region`, and a game's heading can take the same pixels: the child looking for blind mode finds the question's
|
|
2042
|
+
* title over the buttons, with no error, no type, no console — and whoever depends on the row most is whoever cannot
|
|
2043
|
+
* see it is covered.
|
|
2044
|
+
*
|
|
2045
|
+
* 📌 THE ENGINE SAYS, and does not fix — because it cannot. Pushing the game's content would mean changing the width
|
|
2046
|
+
* and height `ui/layout` locks to an integer multiple of real pixels, which is ADR-0001's scale. The engine has the
|
|
2047
|
+
* rectangle; the game draws, and now knows where not to draw. The reserved room itself (`--barra-a11y-h`, next to
|
|
2048
|
+
* `--tap` and `--alvo-min`) is written by `reserveBarBand`, inside `applyResolution`.
|
|
2049
|
+
*
|
|
2050
|
+
* ⚠️ BY CAPABILITY AND NOT BY TYPE: a fake document has no `getBoundingClientRect`, and reading it blindly would bring
|
|
2051
|
+
* the boot down where there is no DOM. Without the measure, nobody is accused: silence beats an invented accusation.
|
|
2052
|
+
*/
|
|
2053
|
+
const intruders = barIntruderProblems(drawingContext);
|
|
2054
|
+
if (intruders.length) {
|
|
2055
|
+
hostProblems.push(`the game draws over the accessibility bar (${intruders.slice(0, 4).join(', ')}): a child who needs its buttons `
|
|
2056
|
+
+ 'to start cannot reach them — read `--barra-a11y-h` on #game-region and leave that room free (ADR-0148)');
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
const announceFailure = createCrashNotice({
|
|
2060
|
+
t: translator.t,
|
|
2061
|
+
find: (sel) => $(sel),
|
|
2062
|
+
create: (tag) => doc.createElement(tag),
|
|
2063
|
+
narrate: (text) => tts.narrate(text),
|
|
645
2064
|
});
|
|
646
2065
|
/*
|
|
647
|
-
* ⚠️
|
|
648
|
-
*
|
|
649
|
-
*
|
|
2066
|
+
* ⚠️ SHOWING REBUILDS THE ITEMS BEFORE REVEALING, and the order is the rule: ADR-0106 §5 says the child never sees an
|
|
2067
|
+
* item that does not act, and this game's action table may have changed since mounting. Revealing first and
|
|
2068
|
+
* rebuilding after would leave a blink in which she sees what she cannot use.
|
|
650
2069
|
*/
|
|
651
|
-
const
|
|
652
|
-
|
|
2070
|
+
const pauseControls = {
|
|
2071
|
+
show: (i) => {
|
|
653
2072
|
pauseIcons.reflectPauseIcons();
|
|
654
|
-
const
|
|
655
|
-
if (
|
|
656
|
-
|
|
2073
|
+
const findPauseCard = $(`#vp-pause-${i}`);
|
|
2074
|
+
if (!findPauseCard)
|
|
2075
|
+
return;
|
|
2076
|
+
findPauseCard.hidden = false;
|
|
2077
|
+
updateCaption();
|
|
2078
|
+
// 🔴 THE CURSOR LANDS ON ITEM 1, at the root (ADR-0158: «a saída é onde o cursor cai ao abrir»). Without it, opened
|
|
2079
|
+
// by SELECT no item was marked, and the first arrow jumped to item 2.
|
|
2080
|
+
showPauseOptions(findPauseCard, 'raiz');
|
|
657
2081
|
},
|
|
658
|
-
|
|
659
|
-
const
|
|
660
|
-
if (
|
|
661
|
-
|
|
2082
|
+
hide: (i) => {
|
|
2083
|
+
const findPauseCard = $(`#vp-pause-${i}`);
|
|
2084
|
+
if (findPauseCard)
|
|
2085
|
+
findPauseCard.hidden = true;
|
|
2086
|
+
updateCaption();
|
|
662
2087
|
},
|
|
663
2088
|
};
|
|
664
2089
|
/*
|
|
665
|
-
*
|
|
2090
|
+
* ===================== THE KEYS THAT OPEN THE PAUSE (ADR-0144, ADR-0155) =====================
|
|
2091
|
+
*
|
|
2092
|
+
* ⚠️ BUBBLING, NOT CAPTURE. `menuNavKey` runs in CAPTURE with `stopPropagation()`, and the header of `ui/menu-nav` keeps
|
|
2093
|
+
* TWO preserved defects about it — today's correct behaviour depends on that `stopPropagation()` and not on the
|
|
2094
|
+
* registered chain. Putting a second meaning in the same phase would lean on that accidental safety net. In bubbling,
|
|
2095
|
+
* `menuNavKey` always has the first refusal, and the game's OWN listener — which lives in `#game-region`, under the
|
|
2096
|
+
* window — runs before these. Whoever owns the key stays its owner.
|
|
666
2097
|
*
|
|
667
|
-
*
|
|
668
|
-
*
|
|
669
|
-
*
|
|
2098
|
+
* 📏 AND `menuNavKey` DOES NOT EAT these keys: `menuKeyIntent` (`ui/menu-intent`) has no branch for `start`, `select`
|
|
2099
|
+
* or `action4` (only `action2`, `action3` and the four directions), so it finds no intent and lets them go. That is
|
|
2100
|
+
* why the guards below exist — in real situations the key arrives here and is not ours.
|
|
2101
|
+
*
|
|
2102
|
+
* ⚠️ THE ENGINE OPENS THE CARD; THE GAME STOPS THE WORLD (ADR-0144 §2). Freezing physics and silencing the ambience are
|
|
2103
|
+
* the game's, so the second half is a request, `setPhase('paused')`, and not an order.
|
|
2104
|
+
*/
|
|
2105
|
+
/** The seat that owns a key, if it is ITS `presetAction` position; otherwise `null`. */
|
|
2106
|
+
function seatOfPosition(code, presetAction) {
|
|
2107
|
+
// ⚠️ THE KEY'S OWNER DECIDES THE SEAT, as in `menuNavKey`: whoever pressed opens THEIR pause. A key that is nobody's
|
|
2108
|
+
// (`-1`) cannot be any seat's «start» — asking seat 0 about it would hand Player 1's pause to whoever pressed a loose
|
|
2109
|
+
// key.
|
|
2110
|
+
const keyOwner = keyboard.whichPlayer(code);
|
|
2111
|
+
if (keyOwner < 0)
|
|
2112
|
+
return null;
|
|
2113
|
+
return keyboard.actionOf(code, keyOwner) === presetAction ? keyOwner : null;
|
|
2114
|
+
}
|
|
2115
|
+
/*
|
|
2116
|
+
* ===================== THE QUICK PAUSE — START (ADR-0155) =====================
|
|
670
2117
|
*
|
|
671
|
-
*
|
|
672
|
-
*
|
|
2118
|
+
* The game FREEZES (`setPhase('paused')` is asked, as ADR-0144 §2 asked for the card), the directional goes to the BAR,
|
|
2119
|
+
* and the word PAUSED appears in the centre. No card is drawn: it is the print view.
|
|
673
2120
|
*
|
|
674
|
-
*
|
|
675
|
-
*
|
|
676
|
-
*
|
|
677
|
-
* depois do arranque. Um relatório que chega depois do leitor não é um relatório — é a forma exacta do
|
|
678
|
-
* `srSay` a escrever onde não havia `#sr-status`.
|
|
679
|
-
* 2. **AFOGAVA O QUE SE PODE RESOLVER.** Sem rede — uma escola sem rede, que é o alvo e não a excepção —
|
|
680
|
-
* são OITO falhas a empurrar para uma lista que o ADR-0106 §2 construiu para dizer o que FALTA NO
|
|
681
|
-
* HOSPEDEIRO. A criança perde a barra de acessibilidade e a linha que o diz fica em nono lugar.
|
|
2121
|
+
* 📌 ONE WAY OUT, by any door. «Voltar» on the bar, START again and SELECT all end in `pauseIcons.leaveBar`, and the
|
|
2122
|
+
* `onLeaveBar` hook is what unfreezes. Two ways out written apart would be two chances for one of them to leave the
|
|
2123
|
+
* world stopped with the child back on the character.
|
|
682
2124
|
*
|
|
683
|
-
*
|
|
684
|
-
*
|
|
2125
|
+
* ⚠️ AND THE STATE IS IN-QUICK-PAUSE, not on-the-bar: a host without `#title-icons` has no bar to enter, and the quick
|
|
2126
|
+
* pause still freezes and says PAUSED — with START leaving it. Asking the bar would trap that child in a stopped game.
|
|
685
2127
|
*/
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
2128
|
+
let pausedWord = null;
|
|
2129
|
+
/*
|
|
2130
|
+
* THE FOOTER LEGEND of the frozen screen (ADR-0155 erratum): «Ação 2: confirmar · Ação 3: voltar · Ação 4: menu ·
|
|
2131
|
+
* START: voltar ao jogo». It is the menus' second door — `action4` — and it also tells the child SELECT is not the
|
|
2132
|
+
* only one: the stopped screen teaches how to leave it and where to.
|
|
2133
|
+
* (`pauseCaption` is declared before `changePhase`; see there.)
|
|
2134
|
+
*/
|
|
2135
|
+
function showPaused() {
|
|
2136
|
+
const regionEl = $('#game-region');
|
|
2137
|
+
if (!pausedWord && regionEl && typeof regionEl.appendChild === 'function') {
|
|
2138
|
+
pausedWord = doc.createElement('div');
|
|
2139
|
+
pausedWord.className = 'pausa-rapida';
|
|
2140
|
+
// The screen reader already hears the entry into the bar, which says the game stopped and how to go back; the word
|
|
2141
|
+
// is for the eyes, and said twice it trampled the announcement that teaches the way out.
|
|
2142
|
+
pausedWord.setAttribute('aria-hidden', 'true');
|
|
2143
|
+
regionEl.appendChild(pausedWord);
|
|
2144
|
+
}
|
|
2145
|
+
if (!pausedWord)
|
|
2146
|
+
return;
|
|
2147
|
+
// resolved WHEN SHOWN: the language may have changed since boot
|
|
2148
|
+
pausedWord.textContent = t('pause.quick');
|
|
2149
|
+
pausedWord.hidden = false;
|
|
2150
|
+
updateCaption();
|
|
2151
|
+
}
|
|
2152
|
+
/**
|
|
2153
|
+
* THE BUTTON LEGEND FOLLOWS THE SCREEN (ADR-0164 rule 3): on the quick pause it says the quick pause's buttons, on the
|
|
2154
|
+
* pause card (SELECT) the menu's — «2: confirmar · 3: voltar» —, and nothing in play. Asked again whenever one of them
|
|
2155
|
+
* opens or closes; a card hidden by a path that calls nothing here (the print mode) is seen by the observer below.
|
|
2156
|
+
*/
|
|
2157
|
+
function updateCaption() {
|
|
2158
|
+
const cardOpen = !!$('.screen-pause:not([hidden])');
|
|
2159
|
+
const text = inQuickPause.size ? t('pause.quick.legenda') : cardOpen ? t('pause.card.legenda') : null;
|
|
2160
|
+
if (!text) {
|
|
2161
|
+
if (pauseCaption)
|
|
2162
|
+
pauseCaption.hidden = true;
|
|
2163
|
+
return;
|
|
2164
|
+
}
|
|
2165
|
+
if (!pauseCaption) {
|
|
2166
|
+
const home = screenFooter($('#game-region'));
|
|
2167
|
+
if (!home)
|
|
2168
|
+
return;
|
|
2169
|
+
pauseCaption = doc.createElement('div');
|
|
2170
|
+
pauseCaption.className = 'pausa-legenda';
|
|
2171
|
+
pauseCaption.setAttribute('aria-hidden', 'true');
|
|
2172
|
+
home.appendChild(pauseCaption);
|
|
2173
|
+
}
|
|
2174
|
+
writeCaption(pauseCaption, text); // resolved when shown: the language may have changed since boot
|
|
2175
|
+
pauseCaption.hidden = false;
|
|
2176
|
+
}
|
|
2177
|
+
/*
|
|
2178
|
+
* EVERY CHANGE OF CONTEXT IS ANNOUNCED (ADR-0159 rule 3): «Opening a menu or panel speaks its title; closing it speaks
|
|
2179
|
+
* where the child is back to; entering play is announced.» 📏 Measured in the dist: SELECT opened the card in silence,
|
|
2180
|
+
* and a panel opened, closed and went back to the root without a word.
|
|
2181
|
+
* 📌 ONE PLACE, by comparison: the observer below asks where the child is now — a panel, a list of the card, or the
|
|
2182
|
+
* game — and speaks only when that changed. An arrow changes no `hidden`, so it still says only the item.
|
|
2183
|
+
*/
|
|
2184
|
+
let screenAnnounced = 'jogo';
|
|
2185
|
+
const rootWithIndex = () => (cartridge.withIndex ?? (() => true))();
|
|
2186
|
+
/*
|
|
2187
|
+
* 🔴 THE QUESTION LIVES IN `ui/where-the-child-is` (ADR-0221 step 7c); what stays here is answering where it reads the
|
|
2188
|
+
* document from. Whether this is a panel, a list of the card or the game, and what it is called, is a rule with a
|
|
2189
|
+
* reason — not wiring — and ADR-0221's erratum measures a root's debt in branches.
|
|
2190
|
+
*/
|
|
2191
|
+
const whereIsTheChild = () => whereTheChildIs({
|
|
2192
|
+
t: translator.t,
|
|
2193
|
+
topVisibleOverlay: overlays.topVisibleOverlay,
|
|
2194
|
+
pauseCard: () => $('.screen-pause:not([hidden])'),
|
|
2195
|
+
focused: () => doc.activeElement,
|
|
2196
|
+
withIndex: rootWithIndex,
|
|
2197
|
+
});
|
|
2198
|
+
function announceContext() {
|
|
2199
|
+
const nowMs = whereIsTheChild();
|
|
2200
|
+
if (nowMs.key === screenAnnounced)
|
|
2201
|
+
return;
|
|
2202
|
+
const cameFromMenu = screenAnnounced !== 'jogo';
|
|
2203
|
+
screenAnnounced = nowMs.key;
|
|
2204
|
+
if (nowMs.sentence)
|
|
2205
|
+
srSay(nowMs.sentence);
|
|
2206
|
+
// back in play from a menu — the quick pause says its own exit
|
|
2207
|
+
else if (cameFromMenu && !inQuickPause.size)
|
|
2208
|
+
srSay(t('sr.a11y.barExit'));
|
|
2209
|
+
}
|
|
2210
|
+
{
|
|
2211
|
+
const regionEl = $('#game-region');
|
|
2212
|
+
const Observer = win.MutationObserver;
|
|
2213
|
+
if (regionEl && Observer && typeof regionEl.appendChild === 'function') {
|
|
2214
|
+
new Observer((records) => {
|
|
2215
|
+
const classes = records.map((r) => r.target.classList);
|
|
2216
|
+
if (classes.some((c) => c?.contains('screen-pause')))
|
|
2217
|
+
updateCaption();
|
|
2218
|
+
if (classes.some((c) => c?.contains('screen-pause') || c?.contains('overlay') || c?.contains('pause-menu')))
|
|
2219
|
+
announceContext();
|
|
2220
|
+
// ADR-0166: the pad leaves when the card or a panel opens, and comes back when the last of them closes
|
|
2221
|
+
if (classes.some((c) => c?.contains('screen-pause') || c?.contains('overlay')))
|
|
2222
|
+
reflectPadInMenus();
|
|
2223
|
+
// a simulation stops while a menu is open and comes back with the game (issue #182)
|
|
2224
|
+
if (classes.some((c) => c?.contains('screen-pause') || c?.contains('overlay')))
|
|
2225
|
+
recomposeWorldFilter();
|
|
2226
|
+
}).observe(regionEl, { attributes: true, attributeFilter: ['hidden'], subtree: true });
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
/*
|
|
2230
|
+
* THE SCREEN FOOTER: one column, at the bottom of the region, with the EXPLANATION of the pointed icon above the quick
|
|
2231
|
+
* pause's LEGEND. The Dev's request — the name under the row, what it does in the footer (`CLAUDE.md` §4, the three
|
|
2232
|
+
* zones).
|
|
2233
|
+
* ⚠️ ONE COLUMN and not two bands positioned apart: the quick pause lands the cursor on the first icon on entering, so
|
|
2234
|
+
* both appear together from the first instant, and two absolute boxes covered each other when one wrapped.
|
|
2235
|
+
*/
|
|
2236
|
+
function screenFooter(regionEl) {
|
|
2237
|
+
if (footer || !regionEl || typeof regionEl.appendChild !== 'function')
|
|
2238
|
+
return footer;
|
|
2239
|
+
footer = doc.createElement('div');
|
|
2240
|
+
footer.className = 'rodape-da-tela';
|
|
2241
|
+
regionEl.appendChild(footer);
|
|
2242
|
+
return footer;
|
|
2243
|
+
}
|
|
2244
|
+
/**
|
|
2245
|
+
* The button legend as one dark chip per «name: function» (ADR-0164 rule 3) — the dictionary writes the items joined
|
|
2246
|
+
* by « · », and each becomes its own element so the background sits behind the words and not across the screen.
|
|
2247
|
+
*/
|
|
2248
|
+
function writeCaption(home, text) {
|
|
2249
|
+
home.textContent = '';
|
|
2250
|
+
text.split('·').map((s) => s.trim()).filter(Boolean).forEach((item, i) => {
|
|
2251
|
+
// a space between chips, by capability: a host document without createTextNode still gets the chips
|
|
2252
|
+
if (i > 0 && typeof doc.createTextNode === 'function')
|
|
2253
|
+
home.appendChild(doc.createTextNode(' '));
|
|
2254
|
+
const name = doc.createElement('span');
|
|
2255
|
+
name.className = 'lg-nome';
|
|
2256
|
+
name.textContent = item;
|
|
2257
|
+
home.appendChild(name);
|
|
2258
|
+
});
|
|
2259
|
+
}
|
|
2260
|
+
function explainIconInFooter(k) {
|
|
2261
|
+
writeInFooter(k ? t(`icon.${k}.dica`) : null);
|
|
2262
|
+
}
|
|
2263
|
+
/*
|
|
2264
|
+
* THE SOUND CAPTION (study item D3). In the footer column, above the button legend and the explanation (ADR-0164 rule 4:
|
|
2265
|
+
* «the sound caption above, the explanation below it»); `aria-hidden`, because whoever listens heard the sound itself.
|
|
2266
|
+
* 📌 Its time on screen is a child's reading time for its words, never under the 2600 ms the games had measured in play
|
|
2267
|
+
* (`core/caption-duration`, plan phase 5c); a new caption restarts it.
|
|
2268
|
+
*/
|
|
2269
|
+
let soundCaption = null;
|
|
2270
|
+
let clearSoundCaption = null;
|
|
2271
|
+
function writeSoundCaption(text) {
|
|
2272
|
+
if (!state.captionsOn || !text)
|
|
2273
|
+
return;
|
|
2274
|
+
if (!soundCaption) {
|
|
2275
|
+
const home = screenFooter($('#game-region'));
|
|
2276
|
+
if (!home)
|
|
2277
|
+
return;
|
|
2278
|
+
soundCaption = doc.createElement('div');
|
|
2279
|
+
soundCaption.className = 'legenda-de-som';
|
|
2280
|
+
soundCaption.setAttribute('aria-hidden', 'true');
|
|
2281
|
+
home.appendChild(soundCaption);
|
|
2282
|
+
}
|
|
2283
|
+
const captionHome = soundCaption;
|
|
2284
|
+
captionHome.textContent = text;
|
|
2285
|
+
captionHome.hidden = false;
|
|
2286
|
+
if (clearSoundCaption !== null)
|
|
2287
|
+
clearTimeout(clearSoundCaption);
|
|
2288
|
+
clearSoundCaption = setTimeout(() => { captionHome.hidden = true; captionHome.textContent = ''; }, captionDuration(text, state.captionPpm));
|
|
2289
|
+
}
|
|
2290
|
+
/** The footer says ONE explanation at a time: the pointed icon's, or the reason a locked item is locked (ADR-0161). */
|
|
2291
|
+
function writeInFooter(text) {
|
|
2292
|
+
if (!barExplanation && text) {
|
|
2293
|
+
const home = screenFooter($('#game-region'));
|
|
2294
|
+
if (home) {
|
|
2295
|
+
barExplanation = doc.createElement('div');
|
|
2296
|
+
barExplanation.className = 'barra-explicacao';
|
|
2297
|
+
barExplanation.setAttribute('aria-live', 'polite');
|
|
2298
|
+
home.insertBefore(barExplanation, home.firstChild);
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
if (!barExplanation)
|
|
2302
|
+
return;
|
|
2303
|
+
barExplanation.textContent = text ?? '';
|
|
2304
|
+
barExplanation.hidden = !text;
|
|
2305
|
+
}
|
|
2306
|
+
function enterQuickPause(seat) {
|
|
2307
|
+
inQuickPause.add(seat);
|
|
2308
|
+
recomposeWorldFilter(); // the quick pause is a menu: the simulation stops (issue #182)
|
|
2309
|
+
pauseIcons.enterBar(seat);
|
|
2310
|
+
if (!pauseIcons.onBar(seat))
|
|
2311
|
+
srSay(t('sr.a11y.quickPause'));
|
|
2312
|
+
showPaused();
|
|
2313
|
+
changePhase('paused');
|
|
2314
|
+
}
|
|
2315
|
+
/** Leaves by any door. `to` says where: the game (unfreezes) or the card (stays stopped). */
|
|
2316
|
+
function leaveQuickPause(seat, to) {
|
|
2317
|
+
if (pauseIcons.onBar(seat)) {
|
|
2318
|
+
pauseIcons.leaveBar(seat, to === 'cartao');
|
|
2319
|
+
return;
|
|
2320
|
+
} // the hook finishes the job
|
|
2321
|
+
if (to === 'jogo')
|
|
2322
|
+
srSay(t('sr.a11y.barExit'));
|
|
2323
|
+
endQuickPause(seat, to === 'cartao');
|
|
2324
|
+
}
|
|
2325
|
+
function endQuickPause(seat, toOtherScreen) {
|
|
2326
|
+
if (!inQuickPause.delete(seat))
|
|
2327
|
+
return; // the simulation returns through the observer: leaving writes the card's `hidden`
|
|
2328
|
+
if (pausedWord && inQuickPause.size === 0)
|
|
2329
|
+
pausedWord.hidden = true;
|
|
2330
|
+
if (!toOtherScreen)
|
|
2331
|
+
changePhase('playing');
|
|
2332
|
+
}
|
|
2333
|
+
function toggleQuickPauseByStart(e) {
|
|
2334
|
+
const seat = seatOfPosition(e.code, 'start');
|
|
2335
|
+
if (seat === null)
|
|
2336
|
+
return;
|
|
2337
|
+
// GUARD 1 — A PANEL IS OPEN. With an overlay visible, `menuNavKey` receives the key, finds no intent in it and leaves
|
|
2338
|
+
// without consuming it. Without this guard the quick pause would open UNDER the panel the child is in. Escape closes
|
|
2339
|
+
// a panel, not START.
|
|
2340
|
+
if (overlays.topVisibleOverlay())
|
|
2341
|
+
return;
|
|
2342
|
+
// START AGAIN LEAVES — the second way out of the mode that ADR-0044 item 7 already gave START.
|
|
2343
|
+
if (inQuickPause.has(seat)) {
|
|
2344
|
+
leaveQuickPause(seat, 'jogo');
|
|
2345
|
+
e.preventDefault();
|
|
2346
|
+
return;
|
|
2347
|
+
}
|
|
2348
|
+
// GUARD 2 — THE CARD IS OPEN. With it open and a «start» key other than `Enter`, `menuNavKey` finds no intent and
|
|
2349
|
+
// lets it through. Closing the card belongs to «Voltar ao jogo» and to Escape.
|
|
2350
|
+
const findPauseCard = $(`#vp-pause-${seat}`);
|
|
2351
|
+
if (findPauseCard && findPauseCard.hidden === false)
|
|
2352
|
+
return;
|
|
2353
|
+
enterQuickPause(seat);
|
|
2354
|
+
// 📌 AND ONLY HERE, once the key was in fact OURS. `Enter` is «start» by default (`input/default-bindings`), and
|
|
2355
|
+
// without this the same press would pause AND activate whatever had focus.
|
|
2356
|
+
e.preventDefault();
|
|
2357
|
+
}
|
|
2358
|
+
win.addEventListener('keydown', toggleQuickPauseByStart);
|
|
2359
|
+
/*
|
|
2360
|
+
* ===================== SELECT OPENS THE MENUS (ADR-0155) =====================
|
|
2361
|
+
*
|
|
2362
|
+
* The card of ADR-0151. `select` is mapped (`KeyF`, `input/default-bindings`), and ADR-0086 kept it for «what belongs
|
|
2363
|
+
* to the session» — the pause menus are that.
|
|
2364
|
+
*
|
|
2365
|
+
* ⚠️ FROM THE QUICK PAUSE TO THE CARD the game does NOT unfreeze: the bar is left in silence (saying «back to the game»
|
|
2366
|
+
* with the card opening would be a lie) and the phase is already `paused` — asking for it again would be a second
|
|
2367
|
+
* `paused` in a stopped game.
|
|
2368
|
+
*/
|
|
2369
|
+
/** Opens the seat's card, whichever door it comes from — the SELECT key, the ☰ or the touch pill. Returns whether it opened. */
|
|
2370
|
+
function openSeatMenus(seat) {
|
|
2371
|
+
if (overlays.topVisibleOverlay())
|
|
2372
|
+
return false;
|
|
2373
|
+
const findPauseCard = $(`#vp-pause-${seat}`);
|
|
2374
|
+
if (!findPauseCard || findPauseCard.hidden === false)
|
|
2375
|
+
return false;
|
|
2376
|
+
const alreadyStopped = inQuickPause.has(seat);
|
|
2377
|
+
if (alreadyStopped)
|
|
2378
|
+
leaveQuickPause(seat, 'cartao');
|
|
2379
|
+
// ⚠️ SHOWING COMES FIRST, and the order is the defence: a game without `setPhase` must get the card all the same.
|
|
2380
|
+
pauseControls.show(seat);
|
|
2381
|
+
if (!alreadyStopped)
|
|
2382
|
+
changePhase('paused');
|
|
2383
|
+
return true;
|
|
2384
|
+
}
|
|
2385
|
+
function openMenusBySelect(e) {
|
|
2386
|
+
const seat = seatOfPosition(e.code, 'select');
|
|
2387
|
+
if (seat === null)
|
|
2388
|
+
return;
|
|
2389
|
+
if (openSeatMenus(seat))
|
|
2390
|
+
e.preventDefault();
|
|
2391
|
+
}
|
|
2392
|
+
win.addEventListener('keydown', openMenusBySelect);
|
|
2393
|
+
/*
|
|
2394
|
+
* THE MENUS' SECOND DOOR: `action4`, only INSIDE the quick pause (ADR-0155 erratum). Outside it `action4` is the game's,
|
|
2395
|
+
* and the engine does not touch it — that is the pair that keeps the door from stealing a verb mid-match.
|
|
2396
|
+
* 📏 `menuNavKey` has no intent for `action4` and, on the bar, lets it rise without consuming it: it arrives here.
|
|
2397
|
+
*/
|
|
2398
|
+
function openMenusByAction4(e) {
|
|
2399
|
+
const seat = seatOfPosition(e.code, 'action4');
|
|
2400
|
+
if (seat === null || !inQuickPause.has(seat))
|
|
2401
|
+
return;
|
|
2402
|
+
if (overlays.topVisibleOverlay())
|
|
2403
|
+
return;
|
|
2404
|
+
leaveQuickPause(seat, 'cartao');
|
|
2405
|
+
pauseControls.show(seat);
|
|
2406
|
+
e.preventDefault();
|
|
2407
|
+
}
|
|
2408
|
+
win.addEventListener('keydown', openMenusByAction4);
|
|
2409
|
+
/*
|
|
2410
|
+
* ===================== THE VIRTUAL CONTROLLER (ADR-0143, plan phase 4) =====================
|
|
2411
|
+
*
|
|
2412
|
+
* 🔴 `mountTouchControls`, `initTouch` and `initTouchBindings` are wired HERE: in a school where the device is a tablet
|
|
2413
|
+
* with no keyboard, a game booted by this root has to be playable, and nothing else would call them.
|
|
2414
|
+
*
|
|
2415
|
+
* 📌 THE PAD IS DRAWN ONLY WHEN THE CARTRIDGE ASKS FOR IT (`onScreenPad`, ADR-0166): a game played by touching its own
|
|
2416
|
+
* elements needs none. What it lacks is said in `problems` (`touchGaps`); with no `preset` it carries only START and
|
|
2417
|
+
* SELECT, the doors to the pause, which is not declinable (ADR-0122).
|
|
2418
|
+
*/
|
|
2419
|
+
const touchHostEl = o.host.touchHost ?? $('#game-region');
|
|
2420
|
+
const touchUsable = !!touchHostEl && typeof touchHostEl.appendChild === 'function';
|
|
2421
|
+
const seat0CardOpen = () => {
|
|
2422
|
+
const c = $('#vp-pause-0');
|
|
2423
|
+
return !!c && c.hidden === false;
|
|
2424
|
+
};
|
|
2425
|
+
/** A menu the directional moves is open: an overlay, the seat-0 card, or the quick pause (ADR-0157). */
|
|
2426
|
+
const menuWithDpad = () => !!overlays.topVisibleOverlay() || seat0CardOpen() || inQuickPause.has(0);
|
|
2427
|
+
/** A position's key handed to the menus, which read keys — stamped with who produced it (ADR-0109). */
|
|
2428
|
+
const keyToMenu = (code, origin) => {
|
|
2429
|
+
// 🔴 THE KEYBOARD'S KEY IS ALREADY IN THE WORLD (ADR-0223). This function translates POSITION → key, and it exists for
|
|
2430
|
+
// the transports that produce no keys: the finger, the eyes, the face, the hands, the voice, the scan. The keyboard
|
|
2431
|
+
// does — the event that got here IS the key — so dispatching it again would move the menu TWICE.
|
|
2432
|
+
// 📌 Because this is written here, the keyboard conductor need not ask whether a menu is open: that question has ONE
|
|
2433
|
+
// answer, the controller's, and this line is what makes it true for the keyboard too.
|
|
2434
|
+
if (!origin || origin === 'teclado')
|
|
2435
|
+
return;
|
|
2436
|
+
const eventTarget = $('#game-region') ?? doc.body;
|
|
2437
|
+
eventTarget.dispatchEvent(stampSource(new KeyboardEvent('keydown', { code, key: code, bubbles: true, cancelable: true }), origin));
|
|
2438
|
+
};
|
|
2439
|
+
const labelledActions = () => {
|
|
2440
|
+
const preset = cartridge.preset;
|
|
2441
|
+
if (!preset)
|
|
2442
|
+
return [];
|
|
2443
|
+
const labelOf = labellerFrom(preset);
|
|
2444
|
+
return presetActions(preset).flatMap((a) => {
|
|
2445
|
+
const label = labelOf(a);
|
|
2446
|
+
return label ? [{ action: a, label }] : [];
|
|
2447
|
+
});
|
|
2448
|
+
};
|
|
2449
|
+
const touchPad = initTouch({
|
|
2450
|
+
$, srSay, store, win, t: translator.t,
|
|
2451
|
+
gameActions: labelledActions,
|
|
2452
|
+
root: doc.documentElement,
|
|
2453
|
+
isMobile: deviceAvailability.touch,
|
|
2454
|
+
viewport: () => ({ w: win.innerWidth, h: win.innerHeight }),
|
|
2455
|
+
frontOverlay: overlays.frontOverlay,
|
|
2456
|
+
// Touch always belongs to Player 1 (`touch-bindings`), and with the card or a panel open the child touches the menu's
|
|
2457
|
+
// buttons DIRECTLY (ADR-0166, which undid ADR-0157's «the pad stays over the menus»). The only two callers that show
|
|
2458
|
+
// the pad — the touch listener below and `reflectPadInMenus` — already ask `isMenuOpen()` first.
|
|
2459
|
+
padAllowed: () => players().length <= 1,
|
|
2460
|
+
});
|
|
2461
|
+
/** A menu the child touches directly is open: the pause card or a settings panel (ADR-0166 rule 2). */
|
|
2462
|
+
function isMenuOpen() {
|
|
2463
|
+
return !!overlays.topVisibleOverlay() || !!$('.screen-pause:not([hidden])');
|
|
2464
|
+
}
|
|
2465
|
+
/** The pad was in view (or asked for by a touch) when a menu took the screen — it comes back when the menu goes. */
|
|
2466
|
+
let padBeforeMenu = false;
|
|
2467
|
+
function reflectPadInMenus() {
|
|
2468
|
+
const pad = $('#touch-controls');
|
|
2469
|
+
if (!pad)
|
|
2470
|
+
return;
|
|
2471
|
+
if (isMenuOpen()) {
|
|
2472
|
+
if (!pad.hidden) {
|
|
2473
|
+
padBeforeMenu = true;
|
|
2474
|
+
touchPad.hideTouchControls('menu');
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
else if (padBeforeMenu) {
|
|
2478
|
+
padBeforeMenu = false;
|
|
2479
|
+
touchPad.showTouchControls();
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
const cartridgeActions = () => new Set(cartridge.preset ? presetActions(cartridge.preset) : []);
|
|
2483
|
+
function drawPad() {
|
|
2484
|
+
if (!touchUsable || !touchHostEl)
|
|
2485
|
+
return;
|
|
2486
|
+
// ADR-0166: a cartridge that does not ask for the pad gets none — and one swapped in by `mount()` takes the last one away
|
|
2487
|
+
if (!cartridge.onScreenPad) {
|
|
2488
|
+
const old = $('#touch-controls');
|
|
2489
|
+
old?.parentNode?.removeChild(old);
|
|
2490
|
+
return;
|
|
2491
|
+
}
|
|
2492
|
+
const padMap = touchPad.getTouchMap();
|
|
2493
|
+
const short = cartridge.preset ? shortLabellerFrom(cartridge.preset) : () => null;
|
|
2494
|
+
const pad = mountTouchControls({ find: (sel) => $(sel), create: (tag) => doc.createElement(tag), t: translator.t }, {
|
|
2495
|
+
map: padMap,
|
|
2496
|
+
gameActions: cartridgeActions(),
|
|
2497
|
+
// The FUNCTION of each slot (ADR-0165): the game's SHORT word, said after the button's name in its accessible
|
|
2498
|
+
// name; the face shows the name. `start`/`select` are system positions the engine names itself.
|
|
2499
|
+
slotLabel: (slot) => (slot === 'start' ? t('touch.start') : slot === 'select' ? t('touch.select')
|
|
2500
|
+
// only what the game names is drawn (ADR-0162), so its word always exists
|
|
2501
|
+
: (short(padMap[slot]) ?? '')),
|
|
2502
|
+
dpad: store.get(KEYS.padDir, 'stick') === 'cross' ? 'cruz' : 'analogico',
|
|
2503
|
+
});
|
|
2504
|
+
if (!pad.parentNode)
|
|
2505
|
+
touchHostEl.appendChild(pad);
|
|
2506
|
+
}
|
|
2507
|
+
padGapProblems = () => (!cartridge.onScreenPad ? [] : touchUsable
|
|
2508
|
+
? touchGaps({ map: touchPad.getTouchMap(), gameActions: cartridgeActions() })
|
|
2509
|
+
: ['the virtual pad has nowhere to mount: set `host.touchHost`, or give #game-region room for children. '
|
|
2510
|
+
+ 'Without it, a child on a keyboardless tablet cannot play, nor reach the pause']);
|
|
2511
|
+
/**
|
|
2512
|
+
* The screen's START: seat 0's QUICK PAUSE, like the key (ADR-0155) — and a second touch leaves it.
|
|
2513
|
+
*
|
|
2514
|
+
* ⚠️ THE PAD STAYS IN VIEW on the quick pause: the START pill IS the way out for someone who has only a finger, and
|
|
2515
|
+
* hiding it would leave the child in a stopped game with no door. With the card open, START closes it.
|
|
2516
|
+
*/
|
|
2517
|
+
function togglePauseByTouch() {
|
|
2518
|
+
if (overlays.topVisibleOverlay())
|
|
2519
|
+
return;
|
|
2520
|
+
if (inQuickPause.has(0)) {
|
|
2521
|
+
leaveQuickPause(0, 'jogo');
|
|
2522
|
+
return;
|
|
2523
|
+
}
|
|
2524
|
+
if (seat0CardOpen()) {
|
|
2525
|
+
changePhase('playing');
|
|
2526
|
+
return;
|
|
2527
|
+
}
|
|
2528
|
+
enterQuickPause(0);
|
|
2529
|
+
}
|
|
2530
|
+
const touchBindings = initTouchBindings({
|
|
2531
|
+
$, win,
|
|
2532
|
+
getSearch: () => win.location?.search ?? '',
|
|
2533
|
+
getControls: () => keyboard.controlsState().controls,
|
|
2534
|
+
getPlayers: () => players(),
|
|
2535
|
+
/*
|
|
2536
|
+
* 🔴 TOUCH PRESSES THE VIRTUAL CONTROLLER (ADR-0223), like every other transport: one decision, not a second copy of
|
|
2537
|
+
* it inside the pad — a copy that, when it existed, left a cartridge listening to `onCommand` deaf to the finger.
|
|
2538
|
+
* 📌 Arrows and not direct references: `virtualController` is born further below (the temporal dead zone).
|
|
2539
|
+
*/
|
|
2540
|
+
press: (action, source) => virtualController.press(action, source),
|
|
2541
|
+
release: (action, source) => virtualController.release(action, source),
|
|
2542
|
+
playerEdge,
|
|
2543
|
+
heldKeys: keys,
|
|
2544
|
+
attractOnInput: () => false,
|
|
2545
|
+
// a touch inside a menu does not bring the pad over it; it only remembers that the child is on touch (ADR-0166)
|
|
2546
|
+
showTouchControls: () => { if (isMenuOpen()) {
|
|
2547
|
+
padBeforeMenu = true;
|
|
2548
|
+
return;
|
|
2549
|
+
} touchPad.showTouchControls(); },
|
|
2550
|
+
hideTips: () => { },
|
|
2551
|
+
togglePause: togglePauseByTouch,
|
|
2552
|
+
/*
|
|
2553
|
+
* THE SELECT PILL (ADR-0155): the menus by touch. Without it, someone with only a finger could not reach «Sair», the
|
|
2554
|
+
* number of players or the settings — SELECT was a key. ⚠️ THE PAD LEAVES when the card opens (ADR-0166,
|
|
2555
|
+
* `reflectPadInMenus`): over the card it would cover the buttons that are now her way out («Voltar ao jogo»).
|
|
2556
|
+
*/
|
|
2557
|
+
openMenus: () => { openSeatMenus(0); },
|
|
2558
|
+
getTouchMap: () => touchPad.getTouchMap(),
|
|
2559
|
+
// ✅ This root HAS the map, so the screen's START reads the action it carries.
|
|
2560
|
+
getStartAction: () => touchPad.getTouchMap().start,
|
|
2561
|
+
getStickTravelPx: () => touchPad.getStickTravelPx(),
|
|
2562
|
+
getStickDeadPx: () => touchPad.getStickDeadPx(),
|
|
2563
|
+
});
|
|
2564
|
+
drawPad();
|
|
2565
|
+
touchBindings.attach();
|
|
2566
|
+
/*
|
|
2567
|
+
* A LANGUAGE CHANGED MID-GAME REACHES WHAT THE ENGINE DREW (study item C6; ADR-0031). 📏 Measured in the quiz: after
|
|
2568
|
+
* `setLocale('en')` the icon bar's names, the card's name, the button legend and the PAUSED word stayed in the old
|
|
2569
|
+
* language — `applyDom` reaches only `[data-i18n]`, and these are written by code. An open panel redraws itself
|
|
2570
|
+
* (`ui/mount-panel`), keeping focus where it was; nothing here moves focus.
|
|
2571
|
+
* 🔴 THE BOOT IS THE SAME EVENT: the pad and the bar are drawn in the fallback language, and the preferred one arrives
|
|
2572
|
+
* through `setLocale` (measured in the `dist`: «Cima/Baixo» on an English page). This listener replaced the
|
|
2573
|
+
* `localeReady()` repaints that did it for the boot alone.
|
|
2574
|
+
*/
|
|
2575
|
+
/**
|
|
2576
|
+
* The 👄, when it exists. Declared HERE because the language change — just below — must reach it, and it is born further
|
|
2577
|
+
* down: the temporal dead zone again.
|
|
2578
|
+
*/
|
|
2579
|
+
let voiceControl = null;
|
|
2580
|
+
/*
|
|
2581
|
+
* 🔴 AND THE CARTRIDGE NEEDS TO KNOW TOO (ADR-0225): changing the flag took `<html lang>`, the footer, the bar and the
|
|
2582
|
+
* panels to the new language and left the ACTIVITY — the quiz's statement — in the old one. The frame followed; the
|
|
2583
|
+
* activity did not.
|
|
2584
|
+
*
|
|
2585
|
+
* 📌 The door is the engine's and not the event's, by the rule the Dev wrote for reading and speech (ADR-0216): «o jogo
|
|
2586
|
+
* não deve precisar saber como isso funciona». A cartridge that had to subscribe to `i18n:change` on the WINDOW would
|
|
2587
|
+
* have to know the event's name, the object it is dispatched on and the order in which the engine handles it — and
|
|
2588
|
+
* would reach a global to do it, which ADR-0221 step 7d refuses to a new module.
|
|
2589
|
+
*/
|
|
2590
|
+
const localeListeners = [];
|
|
2591
|
+
{
|
|
2592
|
+
// through the root's door, released by `dispose()`; the window's `i18n:change` stays the host's page-level signal
|
|
2593
|
+
localeOn(() => {
|
|
2594
|
+
pauseIcons.reflectPauseIcons();
|
|
2595
|
+
updateCaption();
|
|
2596
|
+
if (pausedWord && !pausedWord.hidden)
|
|
2597
|
+
pausedWord.textContent = t('pause.quick');
|
|
2598
|
+
drawPad();
|
|
2599
|
+
touchBindings.rewire();
|
|
2600
|
+
/*
|
|
2601
|
+
* 🔴 AND WHAT THE ENGINE HEARS CHANGES TOO (ADR-0225). What it DRAWS follows since study item C6; what it SAYS follows
|
|
2602
|
+
* by itself (`tts` reads `bcp47()` at every utterance) and what it READS too (at every `listen()`). Command
|
|
2603
|
+
* recognition chose its language ONCE — and listening in the old language is worse than stopping, because the
|
|
2604
|
+
* grammar follows the menu and the new words would feed the old model.
|
|
2605
|
+
*/
|
|
2606
|
+
void voiceControl?.languageChanged();
|
|
2607
|
+
// ⚠️ THE CARTRIDGE LAST, on purpose: when it redraws, the bar, the caption and the pad are already in the new language,
|
|
2608
|
+
// so it never measures a half-translated screen. And a cartridge that throws does not take the engine's frame with it.
|
|
2609
|
+
for (const listener of localeListeners) {
|
|
2610
|
+
try {
|
|
2611
|
+
listener();
|
|
2612
|
+
}
|
|
2613
|
+
catch (e) { /* the cartridge failed; the engine goes on */ }
|
|
2614
|
+
}
|
|
2615
|
+
});
|
|
2616
|
+
}
|
|
2617
|
+
/*
|
|
2618
|
+
* ===================== MOTOR ACCESSIBILITY — the engine's panel (ADR-0151 §2 item 5) =====================
|
|
2619
|
+
*
|
|
2620
|
+
* 🔴 THE OLD PANEL DOES NOT SERVE, and not by taste: `ui/settings-mobility` mounts Easy Mode and the two toggles, and
|
|
2621
|
+
* ADR-0151 took the three out of this panel («dificuldade é opção do jogo»; the toggles live on the ☝️). So this is a
|
|
2622
|
+
* NEW panel (`#motora`), and the old one still serves whoever mounts it with their own markup.
|
|
2623
|
+
*
|
|
2624
|
+
* 📌 ITS ROWS are the Dev's list: the CONTROLLER SIZE in four steps, one per persona; mapping the keyboard (for 1, 2 and
|
|
2625
|
+
* 3–4 players), the gamepad and the touch pad; waiting between presses; the camera; the microphone. Each is offered
|
|
2626
|
+
* only where it has a subject.
|
|
2627
|
+
*
|
|
2628
|
+
* ⚠️ IT EXISTS ONLY WHERE THERE IS A TOUCH HOST: without one there is no pad size to choose, and the submenu's door
|
|
2629
|
+
* falls away by itself (`engineActions.motora` is not defined).
|
|
2630
|
+
*/
|
|
2631
|
+
if (pauseMountPoint && pauseUsable && touchUsable) {
|
|
2632
|
+
const mobilityCtx = {
|
|
2633
|
+
find: (sel) => $(sel),
|
|
2634
|
+
create: (tag) => doc.createElement(tag),
|
|
2635
|
+
host: pauseMountPoint,
|
|
2636
|
+
overlays,
|
|
2637
|
+
localeOn, // a language change redraws an open panel, and `dispose()` releases it (ADR-0232 D4)
|
|
2638
|
+
};
|
|
2639
|
+
let padSteps = null;
|
|
2640
|
+
let padHint = null;
|
|
2641
|
+
let padSizeRow = null;
|
|
2642
|
+
/** Reflects the keyboard-mapping rows (defined further below, with the `#ctrl` panel). */
|
|
2643
|
+
let reflectKeyboard = () => { };
|
|
2644
|
+
let currentPersona = closestPersona(store.getNum(KEYS.padBtnMm, 12.5));
|
|
2645
|
+
const specDoPad = () => ({
|
|
2646
|
+
label: t('motora.pad'),
|
|
2647
|
+
values: PERSONAS_DO_PAD.map((p) => t(p.label)),
|
|
2648
|
+
current: currentPersona,
|
|
2649
|
+
});
|
|
2650
|
+
const mobilityPanel = mountPanel(mobilityCtx, {
|
|
2651
|
+
id: 'motora',
|
|
2652
|
+
labels: () => ({
|
|
2653
|
+
title: t('menu.motora'),
|
|
2654
|
+
listLabel: t('menu.motora'),
|
|
2655
|
+
resetLabel: t('menu.restoreDefaults'),
|
|
2656
|
+
closeLabel: t('pause.pmback'),
|
|
2657
|
+
}),
|
|
2658
|
+
// Read again at every opening: the size may have changed elsewhere, and the labels follow the language of now.
|
|
2659
|
+
render: () => {
|
|
2660
|
+
currentPersona = closestPersona(store.getNum(KEYS.padBtnMm, 12.5));
|
|
2661
|
+
// ADR-0166 + ADR-0106 §5: the pad's size is offered only to a cartridge that has a pad — hidden, not locked, because
|
|
2662
|
+
// there is nothing to unlock. Read at each opening: `mount()` may have swapped the cartridge.
|
|
2663
|
+
if (padSizeRow)
|
|
2664
|
+
padSizeRow.hidden = !cartridge.onScreenPad;
|
|
2665
|
+
if (padSteps)
|
|
2666
|
+
updateSteps(padSteps, specDoPad());
|
|
2667
|
+
// ⚠️ THE HINT IN THE LANGUAGE OF NOW, before the footer collects it: written at boot, it came out in the fallback
|
|
2668
|
+
// language.
|
|
2669
|
+
if (padHint)
|
|
2670
|
+
padHint.textContent = t('motora.pad.dica');
|
|
2671
|
+
reflectKeyboard();
|
|
2672
|
+
},
|
|
2673
|
+
});
|
|
2674
|
+
const rowNode = doc.createElement('div');
|
|
2675
|
+
rowNode.className = 'ctrl-row ctrl-row--passos';
|
|
2676
|
+
padSizeRow = rowNode; // offered or not is decided at each opening (`render` above)
|
|
2677
|
+
const explanation = doc.createElement('span');
|
|
2678
|
+
explanation.className = 'opt-hint';
|
|
2679
|
+
padHint = explanation;
|
|
2680
|
+
const envelope = doc.createElement('span');
|
|
2681
|
+
envelope.appendChild(explanation);
|
|
2682
|
+
rowNode.appendChild(envelope);
|
|
2683
|
+
padSteps = mountSteps(mobilityCtx, specDoPad());
|
|
2684
|
+
padSteps.id = 'opt-pad-persona';
|
|
2685
|
+
rowNode.appendChild(padSteps);
|
|
2686
|
+
mobilityPanel.shell.list.appendChild(rowNode);
|
|
2687
|
+
padSteps.addEventListener('passo', (ev) => {
|
|
2688
|
+
const fresh = nextStep(currentPersona, PERSONAS_DO_PAD.length, ev.detail);
|
|
2689
|
+
if (fresh === currentPersona)
|
|
2690
|
+
return; // at the end, a step that did not happen is not announced
|
|
2691
|
+
currentPersona = fresh;
|
|
2692
|
+
touchPad.setPadMm(PERSONAS_DO_PAD[fresh].mm);
|
|
2693
|
+
updateSteps(padSteps, specDoPad());
|
|
2694
|
+
srSay(`${t('motora.pad')}: ${t(PERSONAS_DO_PAD[fresh].label)}`);
|
|
2695
|
+
});
|
|
2696
|
+
engineActions.motora = mobilityPanel.open;
|
|
2697
|
+
let keyboardMode = 1;
|
|
2698
|
+
let seatInMap = 0;
|
|
2699
|
+
const modeScheme = (conf, i) => (keyboardMode === 1 ? conf.solo
|
|
2700
|
+
: keyboardMode === 2 ? (conf.p2[i] ?? conf.p2[0]) : (conf.p4[i] ?? conf.p4[0]));
|
|
2701
|
+
const modeLabel = (m) => t(m === 1 ? 'motora.teclado.1' : m === 2 ? 'motora.teclado.2' : 'motora.teclado.34');
|
|
2702
|
+
const SIDES = ['leftShoulder', 'leftTrigger', 'rightShoulder', 'rightTrigger'];
|
|
2703
|
+
const actionsToMap = () => {
|
|
2704
|
+
if (!cartridge.preset)
|
|
2705
|
+
return [];
|
|
2706
|
+
const word = labellerFrom(cartridge.preset);
|
|
2707
|
+
return presetActions(cartridge.preset).flatMap((presetAction) => {
|
|
2708
|
+
const actionWord = word(presetAction);
|
|
2709
|
+
return actionWord ? [{ action: presetAction, label: actionWord }] : [];
|
|
2710
|
+
});
|
|
2711
|
+
};
|
|
2712
|
+
let seatSteps = null;
|
|
2713
|
+
const seatSpec = () => ({
|
|
2714
|
+
label: t('ctrl.assento'),
|
|
2715
|
+
values: Array.from({ length: keyboardMode }, (_, i) => t('ctrl.jogador', { n: i + 1 })),
|
|
2716
|
+
current: seatInMap,
|
|
2717
|
+
});
|
|
2718
|
+
const keyboardPanel = mountPanel(mobilityCtx, {
|
|
2719
|
+
id: 'ctrl',
|
|
2720
|
+
labels: () => ({
|
|
2721
|
+
title: modeLabel(keyboardMode),
|
|
2722
|
+
listLabel: modeLabel(keyboardMode),
|
|
2723
|
+
resetLabel: t('menu.restoreDefaults'),
|
|
2724
|
+
closeLabel: t('pause.pmback'),
|
|
2725
|
+
}),
|
|
2726
|
+
render: () => {
|
|
2727
|
+
if (seatSteps) {
|
|
2728
|
+
seatInMap = Math.min(seatInMap, keyboardMode - 1);
|
|
2729
|
+
updateSteps(seatSteps, seatSpec());
|
|
2730
|
+
seatSteps.closest('.ctrl-row').hidden = keyboardMode === 1;
|
|
2731
|
+
}
|
|
2732
|
+
keyboardControls?.render(seatInMap);
|
|
2733
|
+
},
|
|
2734
|
+
});
|
|
2735
|
+
{
|
|
2736
|
+
// THE SEAT, by steps — only in the modes of more than one: «◀ Teclado de: Jogador 2 ▶».
|
|
2737
|
+
const seatRow = doc.createElement('div');
|
|
2738
|
+
seatRow.className = 'ctrl-row ctrl-row--passos';
|
|
2739
|
+
seatSteps = mountSteps(mobilityCtx, seatSpec());
|
|
2740
|
+
seatSteps.id = 'ctrl-assento';
|
|
2741
|
+
seatRow.appendChild(seatSteps);
|
|
2742
|
+
keyboardPanel.shell.card.insertBefore(seatRow, keyboardPanel.shell.list);
|
|
2743
|
+
seatSteps.addEventListener('passo', (ev) => {
|
|
2744
|
+
const nextValue = nextStep(seatInMap, keyboardMode, ev.detail);
|
|
2745
|
+
if (nextValue === seatInMap)
|
|
2746
|
+
return;
|
|
2747
|
+
seatInMap = nextValue;
|
|
2748
|
+
updateSteps(seatSteps, seatSpec());
|
|
2749
|
+
keyboardControls?.render(seatInMap);
|
|
2750
|
+
srSay(`${t('ctrl.assento')}: ${t('ctrl.jogador', { n: nextValue + 1 })}`);
|
|
2751
|
+
});
|
|
2752
|
+
}
|
|
2753
|
+
/** The four-player mode carries the first three seats of the three-player mode — see the header above. */
|
|
2754
|
+
const syncThree = (conf) => {
|
|
2755
|
+
conf.p3.forEach((left, i) => {
|
|
2756
|
+
const de = conf.p4[i];
|
|
2757
|
+
if (de)
|
|
2758
|
+
for (const a of ACTIONS)
|
|
2759
|
+
left[a] = de[a] ? [...de[a]] : de[a];
|
|
2760
|
+
});
|
|
2761
|
+
};
|
|
2762
|
+
keyboardControls = initSettingsControls({
|
|
2763
|
+
t: translator.t, $, srSay, srAlert,
|
|
2764
|
+
gameActions: actionsToMap,
|
|
2765
|
+
store: {
|
|
2766
|
+
saveKB: (conf) => { if (keyboardMode === 4)
|
|
2767
|
+
syncThree(conf); keyboardConfig.save(conf); },
|
|
2768
|
+
// ⚠️ «RESTORE» FOR THIS MODE, not for the whole keyboard: whoever resets the two-player keyboard does not erase the one-player one.
|
|
2769
|
+
resetKB: () => {
|
|
2770
|
+
const factory = keyboardConfig.factoryWithGame();
|
|
2771
|
+
const kb = keyboardConfig.kb();
|
|
2772
|
+
if (keyboardMode === 1)
|
|
2773
|
+
kb.solo = factory.solo;
|
|
2774
|
+
else if (keyboardMode === 2)
|
|
2775
|
+
kb.p2 = factory.p2;
|
|
2776
|
+
else {
|
|
2777
|
+
kb.p4 = factory.p4;
|
|
2778
|
+
kb.p3 = factory.p3;
|
|
2779
|
+
}
|
|
2780
|
+
keyboardConfig.save(kb);
|
|
2781
|
+
return kb;
|
|
2782
|
+
},
|
|
2783
|
+
},
|
|
2784
|
+
kb: keyboardConfig.kb(),
|
|
2785
|
+
setKB: keyboardConfig.set,
|
|
2786
|
+
kbFor: (i) => modeScheme(keyboardConfig.kb(), i),
|
|
2787
|
+
defaultSchemeFor: (i) => modeScheme(keyboardConfig.factoryWithGame(), i),
|
|
2788
|
+
getNumPlayers: () => keyboardMode,
|
|
2789
|
+
applyControls: () => { keyboard.refreshControls(); },
|
|
2790
|
+
assignControls: () => { keyboard.assignControls(); },
|
|
2791
|
+
fillExplain: overlays.fillExplain,
|
|
2792
|
+
});
|
|
2793
|
+
// THE CAPTURE GETS THE KEY BEFORE EVERYTHING ELSE: while capturing, the menu navigation already steps aside, and this
|
|
2794
|
+
// keeps the recorded key from rising on to START, SELECT or the game.
|
|
2795
|
+
win.addEventListener('keydown', (e) => {
|
|
2796
|
+
if (keyboardControls?.isCapturing() && keyboardControls.handleCaptureKeydown(e))
|
|
2797
|
+
e.stopPropagation();
|
|
2798
|
+
}, true);
|
|
2799
|
+
// THE THREE ROWS in the motor panel, each a DOOR to `#ctrl` in its mode.
|
|
2800
|
+
const keyboardRows = [];
|
|
2801
|
+
for (const rowMode of [1, 2, 4]) {
|
|
2802
|
+
const rowT = doc.createElement('div');
|
|
2803
|
+
rowT.className = 'ctrl-row';
|
|
2804
|
+
const envelope = doc.createElement('span');
|
|
2805
|
+
const strongLabel = doc.createElement('strong');
|
|
2806
|
+
envelope.appendChild(strongLabel);
|
|
2807
|
+
rowT.appendChild(envelope);
|
|
2808
|
+
const rowButton = doc.createElement('button');
|
|
2809
|
+
rowButton.className = 'mode-btn';
|
|
2810
|
+
rowButton.setAttribute('type', 'button');
|
|
2811
|
+
rowButton.id = `opt-teclado-${rowMode}`;
|
|
2812
|
+
rowButton.addEventListener('click', () => {
|
|
2813
|
+
keyboardMode = rowMode;
|
|
2814
|
+
seatInMap = 0;
|
|
2815
|
+
keyboardPanel.open();
|
|
2816
|
+
});
|
|
2817
|
+
rowT.appendChild(rowButton);
|
|
2818
|
+
mobilityPanel.shell.list.appendChild(rowT);
|
|
2819
|
+
keyboardRows.push({ mode: rowMode, row: rowT, strong: strongLabel, button: rowButton });
|
|
2820
|
+
}
|
|
2821
|
+
/** Labels in the language of now, and who appears: with no named positions there is nothing to map; «3–4» without sides. */
|
|
2822
|
+
const reflectKeyboardRows = () => {
|
|
2823
|
+
const declaredActions = cartridge.preset ? presetActions(cartridge.preset) : [];
|
|
2824
|
+
const hasSides = declaredActions.some((a) => SIDES.includes(a));
|
|
2825
|
+
for (const { mode: rowMode, row: l, strong: strongLabel, button: rowButton } of keyboardRows) {
|
|
2826
|
+
strongLabel.textContent = modeLabel(rowMode);
|
|
2827
|
+
rowButton.textContent = t('motora.abrir');
|
|
2828
|
+
rowButton.setAttribute('aria-label', modeLabel(rowMode));
|
|
2829
|
+
l.hidden = actionsToMap().length === 0 || (rowMode === 4 && hasSides);
|
|
2830
|
+
}
|
|
2831
|
+
};
|
|
2832
|
+
reflectKeyboardRows();
|
|
2833
|
+
/*
|
|
2834
|
+
* MAPEAR CONTROLE (ADR-0151 §2; issue #182): the engine's own wizard (`input/pad-wizard`), asking only the positions this
|
|
2835
|
+
* game names, in its words, and storing the map `initGamepad` reads — one cache for the page. It reads the pads only
|
|
2836
|
+
* while it is open. «Voltar» cancels; the last named position saves and closes. The shell's «restore» is hidden: a pad's
|
|
2837
|
+
* map is replaced by mapping again.
|
|
2838
|
+
*/
|
|
2839
|
+
let padWizard = null;
|
|
2840
|
+
/** One closer for «Voltar» and Escape: a running wizard is cancelled (and its close hides the panel); an idle one just hides. */
|
|
2841
|
+
const closeControl = () => {
|
|
2842
|
+
if (padWizard?.state()) {
|
|
2843
|
+
padWizard.close(false);
|
|
2844
|
+
return;
|
|
2845
|
+
}
|
|
2846
|
+
controlPanel.shell.overlay.hidden = true;
|
|
2847
|
+
overlays.restoreFocus?.('padwiz');
|
|
2848
|
+
};
|
|
2849
|
+
const controlPanel = mountPanel(mobilityCtx, {
|
|
2850
|
+
id: 'padwiz',
|
|
2851
|
+
labels: () => ({
|
|
2852
|
+
title: t('motora.controle'),
|
|
2853
|
+
listLabel: t('motora.controle'),
|
|
2854
|
+
resetLabel: t('menu.restoreDefaults'),
|
|
2855
|
+
closeLabel: t('pause.pmback'),
|
|
2856
|
+
}),
|
|
2857
|
+
render: () => { },
|
|
2858
|
+
closeOwn: () => closeControl(),
|
|
2859
|
+
});
|
|
2860
|
+
controlPanel.shell.reset.hidden = true;
|
|
2861
|
+
controlPanel.shell.close.addEventListener('click', closeControl); // `closeOwn` means this panel wires its own button
|
|
2862
|
+
const controlSentence = doc.createElement('p');
|
|
2863
|
+
controlSentence.id = 'padwiz-prompt';
|
|
2864
|
+
controlSentence.setAttribute('aria-live', 'assertive');
|
|
2865
|
+
const controlProgress = doc.createElement('p');
|
|
2866
|
+
controlProgress.id = 'padwiz-progress';
|
|
2867
|
+
controlProgress.className = 'opt-hint';
|
|
2868
|
+
controlPanel.shell.card.insertBefore(controlSentence, controlPanel.shell.list);
|
|
2869
|
+
controlPanel.shell.card.insertBefore(controlProgress, controlPanel.shell.list);
|
|
2870
|
+
padWizard = createPadWizard({
|
|
2871
|
+
maps: padMaps, t: translator.t,
|
|
2872
|
+
getGamepads: () => {
|
|
2873
|
+
const nav = win.navigator;
|
|
2874
|
+
return typeof nav?.getGamepads === 'function' ? nav.getGamepads() : null;
|
|
2875
|
+
},
|
|
2876
|
+
actionLabel: (presetAction) => actionsToMap().find((x) => x.action === presetAction)?.label ?? null,
|
|
2877
|
+
say: (phrase) => { controlSentence.textContent = phrase; srSay(phrase); },
|
|
2878
|
+
progress: (text) => { controlProgress.textContent = text; },
|
|
2879
|
+
srAlert,
|
|
2880
|
+
onClose: () => {
|
|
2881
|
+
controlPanel.shell.overlay.hidden = true;
|
|
2882
|
+
overlays.restoreFocus?.('padwiz');
|
|
2883
|
+
},
|
|
2884
|
+
});
|
|
2885
|
+
const controlMappingRow = doc.createElement('div');
|
|
2886
|
+
controlMappingRow.className = 'ctrl-row';
|
|
2887
|
+
const controlMappingLabel = doc.createElement('span');
|
|
2888
|
+
const controlStrong = doc.createElement('strong');
|
|
2889
|
+
controlMappingLabel.appendChild(controlStrong);
|
|
2890
|
+
controlMappingRow.appendChild(controlMappingLabel);
|
|
2891
|
+
const controlButton = doc.createElement('button');
|
|
2892
|
+
controlButton.className = 'mode-btn';
|
|
2893
|
+
controlButton.setAttribute('type', 'button');
|
|
2894
|
+
controlButton.id = 'opt-controle';
|
|
2895
|
+
controlButton.addEventListener('click', () => {
|
|
2896
|
+
controlPanel.open();
|
|
2897
|
+
padWizard?.open();
|
|
2898
|
+
});
|
|
2899
|
+
controlMappingRow.appendChild(controlButton);
|
|
2900
|
+
mobilityPanel.shell.list.appendChild(controlMappingRow);
|
|
2901
|
+
const reflectControlRow = () => {
|
|
2902
|
+
controlStrong.textContent = t('motora.controle');
|
|
2903
|
+
controlButton.textContent = t('motora.abrir');
|
|
2904
|
+
controlButton.setAttribute('aria-label', t('motora.controle'));
|
|
2905
|
+
controlMappingRow.hidden = actionsToMap().length === 0; // nothing named, nothing to map
|
|
2906
|
+
};
|
|
2907
|
+
reflectControlRow();
|
|
2908
|
+
/*
|
|
2909
|
+
* MAPEAR TOQUE (ADR-0151 §2; issue #182): which function each on-screen pad button carries, with `input/touch`'s own
|
|
2910
|
+
* editor (`renderTouchMap`, slot → one of the functions the game names). Offered only to a cartridge with a pad (ADR-0166),
|
|
2911
|
+
* and only for the buttons the pad DRAWS — a slot whose function the game does not name is not drawn (ADR-0162), so its
|
|
2912
|
+
* row would change nothing on screen. The pad is redrawn with every choice.
|
|
2913
|
+
*/
|
|
2914
|
+
const touchPanel = mountPanel(mobilityCtx, {
|
|
2915
|
+
id: 'touchcfg',
|
|
2916
|
+
listId: 'touchmap-list',
|
|
2917
|
+
labels: () => ({
|
|
2918
|
+
title: t('motora.toque'),
|
|
2919
|
+
listLabel: t('motora.toque'),
|
|
2920
|
+
resetLabel: t('menu.restoreDefaults'),
|
|
2921
|
+
closeLabel: t('pause.pmback'),
|
|
2922
|
+
}),
|
|
2923
|
+
render: () => { touchPad.renderTouchMap(); hideSlotsWithoutAction(); },
|
|
2924
|
+
});
|
|
2925
|
+
touchPanel.shell.reset.hidden = true;
|
|
2926
|
+
const hideSlotsWithoutAction = () => {
|
|
2927
|
+
const named = cartridgeActions();
|
|
2928
|
+
const padMap = touchPad.getTouchMap();
|
|
2929
|
+
for (const sel of Array.from(touchPanel.shell.list.querySelectorAll('select[data-slot]'))) {
|
|
2930
|
+
const rowNode = sel.closest('.ctrl-row');
|
|
2931
|
+
if (rowNode)
|
|
2932
|
+
rowNode.hidden = !named.has(padMap[sel.dataset.slot ?? ''] ?? '');
|
|
2933
|
+
}
|
|
2934
|
+
};
|
|
2935
|
+
// after the select's own listener (it writes the map), the pad is drawn again with the new function
|
|
2936
|
+
touchPanel.shell.list.addEventListener('change', () => { drawPad(); hideSlotsWithoutAction(); });
|
|
2937
|
+
const touchMappingRow = doc.createElement('div');
|
|
2938
|
+
touchMappingRow.className = 'ctrl-row';
|
|
2939
|
+
const touchEnvelope = doc.createElement('span');
|
|
2940
|
+
const touchStrong = doc.createElement('strong');
|
|
2941
|
+
touchEnvelope.appendChild(touchStrong);
|
|
2942
|
+
touchMappingRow.appendChild(touchEnvelope);
|
|
2943
|
+
const touchButton = doc.createElement('button');
|
|
2944
|
+
touchButton.className = 'mode-btn';
|
|
2945
|
+
touchButton.setAttribute('type', 'button');
|
|
2946
|
+
touchButton.id = 'opt-toque';
|
|
2947
|
+
touchButton.addEventListener('click', () => touchPanel.open());
|
|
2948
|
+
touchMappingRow.appendChild(touchButton);
|
|
2949
|
+
mobilityPanel.shell.list.appendChild(touchMappingRow);
|
|
2950
|
+
const reflectTouchRow = () => {
|
|
2951
|
+
touchStrong.textContent = t('motora.toque');
|
|
2952
|
+
touchButton.textContent = t('motora.abrir');
|
|
2953
|
+
touchButton.setAttribute('aria-label', t('motora.toque'));
|
|
2954
|
+
touchMappingRow.hidden = !cartridge.onScreenPad || actionsToMap().length === 0; // no pad, or nothing named
|
|
2955
|
+
};
|
|
2956
|
+
reflectTouchRow();
|
|
2957
|
+
/*
|
|
2958
|
+
* 🔴 NO STICKY-KEYS ROW IN THIS PANEL (the Dev: «Tire a linha de acessibilidade motora»): the bar's ☝️ is the one
|
|
2959
|
+
* surface for that cycle (ADR-0218), and two surfaces of one setting was what the row had become.
|
|
2960
|
+
*
|
|
2961
|
+
* ⚠️ The row was also the only place that said WHY the latch is locked on a device that sends one command at a time
|
|
2962
|
+
* (ADR-0113 clause 3). It stays honest because the cycle does not OFFER what is locked: where the latch is required,
|
|
2963
|
+
* «padrão» does not appear, and there is nothing to explain. The «Esperar entre toques» row (ADR-0217) stays: it is
|
|
2964
|
+
* another setting.
|
|
2965
|
+
*/
|
|
2966
|
+
/*
|
|
2967
|
+
* «ESPERAR ENTRE TOQUES» (ADR-0217; GAG Advanced/Motor, issue #182). The row beside the sticky keys, and the other half of
|
|
2968
|
+
* the same problem: that one is for a hand that cannot HOLD, this one for a hand that cannot press ONCE.
|
|
2969
|
+
*
|
|
2970
|
+
* ⚠️ OFF BY DEFAULT and offered as a choice, because for a child with no tremor it is half a second lost between every two
|
|
2971
|
+
* presses — in a game of reaction, the game. Hidden where the game holds no key, like its neighbour: what it refuses is a
|
|
2972
|
+
* second press, and a game nobody presses twice has none to refuse.
|
|
2973
|
+
*/
|
|
2974
|
+
const cooldownRowSpec = () => ({ id: 'opt-cooldown', label: t('motor.espera'), hint: t('motor.espera.dica') });
|
|
2975
|
+
const { row: cooldownRow, control: cooldownButton } = controlRow(mobilityCtx, cooldownRowSpec());
|
|
2976
|
+
mobilityPanel.shell.list.appendChild(cooldownRow);
|
|
2977
|
+
const reflectCooldown = () => {
|
|
2978
|
+
labelRow(cooldownRow, cooldownRowSpec());
|
|
2979
|
+
const on = state.inputCooldown > 0;
|
|
2980
|
+
toggleBtn(cooldownButton, on);
|
|
2981
|
+
cooldownButton.textContent = toggleLabel(t, on);
|
|
2982
|
+
markChanged(t, cooldownRow, on !== (DEFAULTS.inputCooldown > 0));
|
|
2983
|
+
cooldownRow.hidden = !cartridge.declaration.holdsKeys();
|
|
2984
|
+
};
|
|
2985
|
+
cooldownButton.addEventListener('click', () => {
|
|
2986
|
+
state.setInputCooldownValue(state.inputCooldown > 0 ? 0 : COOLDOWN_MS);
|
|
2987
|
+
reflectCooldown();
|
|
2988
|
+
srSay(`${t('motor.espera')}: ${t(state.inputCooldown > 0 ? 'state.on' : 'state.off')}`);
|
|
2989
|
+
});
|
|
2990
|
+
reflectCooldown();
|
|
2991
|
+
/*
|
|
2992
|
+
* PLAYING WITH THE CAMERA, IN THE PANEL (issue #182; ADR-0215): «webcam (gestos/rosto/olhos)» was one of the motor rows the
|
|
2993
|
+
* Dev listed as missing, and until the 📷 existed there was nothing to put in it. Now there is, and this row is the same
|
|
2994
|
+
* setting the bar's 📷 cycles — one stored value (`incl_camera_control`), two surfaces, the panel's being the one that says
|
|
2995
|
+
* what each position does.
|
|
2996
|
+
*
|
|
2997
|
+
* ⚠️ HIDDEN WHERE THERE IS NO CAMERA TO ASK FOR, the same rule the icon uses: a row that offers a device the browser does
|
|
2998
|
+
* not have is the dead button of ADR-0106 §5, and a child who picks it waits for a permission dialog that never comes.
|
|
2999
|
+
*
|
|
3000
|
+
* 📌 AND THE MICROPHONE ROW IS RIGHT BELOW, since 2026-09-21: it used to be missing on purpose, because with no voice-command
|
|
3001
|
+
* transport a «microfone» row would switch nothing. The transport landed (issue #184), so the row has a subject.
|
|
3002
|
+
*/
|
|
3003
|
+
const CAMERA_MODE_WORD = {
|
|
3004
|
+
off: 'state.off', hands: 'camera.hands', face: 'camera.face', eyes: 'camera.eyes',
|
|
3005
|
+
};
|
|
3006
|
+
const cameraRowSpec = () => ({
|
|
3007
|
+
label: t('motora.camera'),
|
|
3008
|
+
values: CAMERA_CONTROLS.map((m) => t(CAMERA_MODE_WORD[m])),
|
|
3009
|
+
current: Math.max(0, CAMERA_CONTROLS.indexOf(state.cameraControl)),
|
|
3010
|
+
});
|
|
3011
|
+
const cameraRow = doc.createElement('div');
|
|
3012
|
+
cameraRow.className = 'ctrl-row ctrl-row--passos';
|
|
3013
|
+
const cameraHint = doc.createElement('span');
|
|
3014
|
+
cameraHint.className = 'opt-hint';
|
|
3015
|
+
const cameraWrap = doc.createElement('span');
|
|
3016
|
+
cameraWrap.appendChild(cameraHint);
|
|
3017
|
+
cameraRow.appendChild(cameraWrap);
|
|
3018
|
+
const cameraSteps = mountSteps(mobilityCtx, cameraRowSpec());
|
|
3019
|
+
cameraSteps.id = 'opt-camera';
|
|
3020
|
+
cameraRow.appendChild(cameraSteps);
|
|
3021
|
+
mobilityPanel.shell.list.appendChild(cameraRow);
|
|
3022
|
+
const reflectCamera = () => {
|
|
3023
|
+
updateSteps(cameraSteps, cameraRowSpec());
|
|
3024
|
+
cameraHint.textContent = t('motora.camera.dica');
|
|
3025
|
+
cameraRow.hidden = !canCaptureMedia;
|
|
3026
|
+
};
|
|
3027
|
+
cameraSteps.addEventListener('passo', (ev) => {
|
|
3028
|
+
const next = nextStep(Math.max(0, CAMERA_CONTROLS.indexOf(state.cameraControl)), CAMERA_CONTROLS.length, ev.detail);
|
|
3029
|
+
const mode = CAMERA_CONTROLS[next];
|
|
3030
|
+
if (mode === state.cameraControl)
|
|
3031
|
+
return; // at the end of the line nothing moved, and nothing is announced
|
|
3032
|
+
state.setCameraControlValue(mode);
|
|
3033
|
+
reflectCamera();
|
|
3034
|
+
srSay(`${t('motora.camera')}: ${t(CAMERA_MODE_WORD[mode])}`);
|
|
3035
|
+
});
|
|
3036
|
+
// the 📷 and this row are one setting: whoever changes it, both show it
|
|
3037
|
+
stateOn('cameraControl', () => { reflectCamera(); });
|
|
3038
|
+
reflectCamera();
|
|
3039
|
+
/*
|
|
3040
|
+
* PLAYING BY SPEAKING, IN THE PANEL (issue #182's «microfone» row; ADR-0189): the same stored answer the bar's 👄 writes
|
|
3041
|
+
* (`incl_voice_control`), on the surface that has room to say what it does. Two surfaces of one setting, and neither may
|
|
3042
|
+
* name it differently — which is the correction ADR-0218 had just made to the ☝️.
|
|
3043
|
+
*
|
|
3044
|
+
* ⚠️ HIDDEN WHERE THERE IS NO MICROPHONE TO ASK FOR, the rule the 📷 above follows and the one ADR-0106 §5 states: a row
|
|
3045
|
+
* that offers a device this browser cannot even ask for is a dead control, and a child who picks it waits for a permission
|
|
3046
|
+
* dialog that never comes. What happens when the microphone EXISTS and is refused is another matter and is already
|
|
3047
|
+
* answered: `ui/voice-control` says why and puts the answer back to off, and this row follows it like the icon does.
|
|
3048
|
+
*/
|
|
3049
|
+
const voiceRowSpec = () => ({ id: 'opt-voice', label: t('motora.voz'), hint: t('motora.voz.dica') });
|
|
3050
|
+
const { row: voiceRow, control: voiceButton } = controlRow(mobilityCtx, voiceRowSpec());
|
|
3051
|
+
mobilityPanel.shell.list.appendChild(voiceRow);
|
|
3052
|
+
const reflectVoice = () => {
|
|
3053
|
+
labelRow(voiceRow, voiceRowSpec());
|
|
3054
|
+
toggleBtn(voiceButton, state.voiceControl);
|
|
3055
|
+
voiceButton.textContent = toggleLabel(t, state.voiceControl);
|
|
3056
|
+
markChanged(t, voiceRow, state.voiceControl !== DEFAULTS.voiceControl);
|
|
3057
|
+
voiceRow.hidden = !canCaptureMedia;
|
|
3058
|
+
};
|
|
3059
|
+
voiceButton.addEventListener('click', () => {
|
|
3060
|
+
state.setVoiceControlValue(!state.voiceControl);
|
|
3061
|
+
// ⚠️ THE ANNOUNCEMENT READS THE STATE AFTER THE WRITE, and not the value it meant to write: what cannot start puts the
|
|
3062
|
+
// answer back to off inside the same click, and announcing the intention would tell the child the opposite of what is true.
|
|
3063
|
+
reflectVoice();
|
|
3064
|
+
srSay(`${t('motora.voz')}: ${t(state.voiceControl ? 'state.on' : 'state.off')}`);
|
|
3065
|
+
});
|
|
3066
|
+
stateOn('voiceControl', () => { reflectVoice(); });
|
|
3067
|
+
reflectVoice();
|
|
3068
|
+
reflectKeyboard = () => {
|
|
3069
|
+
reflectKeyboardRows();
|
|
3070
|
+
reflectControlRow();
|
|
3071
|
+
reflectTouchRow();
|
|
3072
|
+
reflectCooldown();
|
|
3073
|
+
reflectCamera();
|
|
3074
|
+
reflectVoice();
|
|
3075
|
+
};
|
|
689
3076
|
}
|
|
690
3077
|
/*
|
|
691
|
-
*
|
|
3078
|
+
* THE MOTOR EMPATHY SIMULATIONS REACH THE GAME HERE (ADR-0181): in the window's capture, after the menu navigation registered
|
|
3079
|
+
* its own, and before any cartridge hears a game key. A refused key and its release stop here; a tapped key passes and is
|
|
3080
|
+
* released at once by a synthetic keyup, which this filter lets through.
|
|
3081
|
+
*/
|
|
3082
|
+
const empathyFilter = createEmpathyFilter();
|
|
3083
|
+
/*
|
|
3084
|
+
* AND THE COOL-DOWN, WHICH IS THE OPPOSITE OF THEM (ADR-0217): the simulations above make play harder so an adult can feel
|
|
3085
|
+
* what a motor disability costs; this refuses the SECOND press of a hand that shakes, which is a child losing a turn she did
|
|
3086
|
+
* not play. It sits in the same pass because the question is the same one — does this key reach the game — and it comes
|
|
3087
|
+
* FIRST: a press the cool-down refuses never happened, so it must not teach the simulations that a key is held.
|
|
3088
|
+
*/
|
|
3089
|
+
const cooldown = createInputCooldown();
|
|
3090
|
+
stateOn('inputCooldown', () => { cooldown.reset(); }); // turning it off must not leave a press refused by an old wait
|
|
3091
|
+
const releasedByFilter = new WeakSet();
|
|
3092
|
+
const block = (e) => { e.preventDefault(); e.stopImmediatePropagation(); };
|
|
3093
|
+
win.addEventListener('keydown', (e) => {
|
|
3094
|
+
if (keyboard.whichPlayer(e.code) < 0)
|
|
3095
|
+
return;
|
|
3096
|
+
if (cooldown.keydown(e.code, win.performance.now(), state.inputCooldown, e.repeat || keys.has(e.code)) === 'refuse') {
|
|
3097
|
+
block(e);
|
|
3098
|
+
return;
|
|
3099
|
+
}
|
|
3100
|
+
const decision = empathyFilter.keydown(e.code, e.repeat, { noChords: state.oneButton, noGripStrength: state.noGripStrength });
|
|
3101
|
+
if (decision === 'barrar') {
|
|
3102
|
+
block(e);
|
|
3103
|
+
return;
|
|
3104
|
+
}
|
|
3105
|
+
if (decision === 'tocar') {
|
|
3106
|
+
const eventTarget = e.target ?? win;
|
|
3107
|
+
setTimeout(() => {
|
|
3108
|
+
// a release the keyboard's own press produced
|
|
3109
|
+
const released = stampSource(new KeyboardEvent('keyup', { code: e.code, key: e.key, bubbles: true, cancelable: true }), 'teclado');
|
|
3110
|
+
releasedByFilter.add(released);
|
|
3111
|
+
eventTarget.dispatchEvent(released);
|
|
3112
|
+
}, 0);
|
|
3113
|
+
}
|
|
3114
|
+
}, true);
|
|
3115
|
+
win.addEventListener('keyup', (e) => {
|
|
3116
|
+
if (releasedByFilter.has(e) || keyboard.whichPlayer(e.code) < 0)
|
|
3117
|
+
return;
|
|
3118
|
+
if (empathyFilter.keyup(e.code) === 'barrar')
|
|
3119
|
+
block(e);
|
|
3120
|
+
}, true);
|
|
3121
|
+
// Playing on the keyboard HIDES the pad — the same per-modality switch `input/keydown` makes. Only some player's keys:
|
|
3122
|
+
// a browser shortcut is not the child changing device.
|
|
3123
|
+
// 🔴 IN CAPTURE (`true`): `ui/menu-nav` consumes a menu's key in the window's capture with `stopPropagation()`, and a
|
|
3124
|
+
// bubbling listener never heard it — in a menu, the child moved to the keyboard and the pad stayed over the card (seen
|
|
3125
|
+
// by the Dev). `stopPropagation` does not silence another listener on the SAME node, so registration order does not matter.
|
|
3126
|
+
win.addEventListener('keydown', (e) => {
|
|
3127
|
+
if (sourceOfEvent(e) === 'toque')
|
|
3128
|
+
return; // the key the pad itself handed to a menu
|
|
3129
|
+
if (keyboard.whichPlayer(e.code) >= 0) {
|
|
3130
|
+
touchPad.hideTouchControls();
|
|
3131
|
+
padBeforeMenu = false;
|
|
3132
|
+
} // on the keyboard now
|
|
3133
|
+
}, true);
|
|
3134
|
+
/*
|
|
3135
|
+
* 🔴 THE BROWSER THE HEAVY FILES AND THE RECOGNISERS USE IS LENT HERE, ONCE (ADR-0232 D4, issue #207): the checked cache, the
|
|
3136
|
+
* hash and `fetch` are read from the HOST's window, and the modules below receive them instead of reaching the globals.
|
|
3137
|
+
* 📌 `caches` and `crypto.subtle` are ABSENT outside a secure context, and absent is an answer each module already gives: the
|
|
3138
|
+
* download reports every file, a loader names the files it cannot find, and nothing is kept unverified.
|
|
3139
|
+
*/
|
|
3140
|
+
const heavyCaches = win.caches;
|
|
3141
|
+
const hasHeavyFile = checkedCacheHas(heavyCaches);
|
|
3142
|
+
// 📌 The microphone and the audio context the recognisers open, from the same window: `undefined` is a device without one.
|
|
3143
|
+
const mediaDevices = win.navigator?.mediaDevices;
|
|
3144
|
+
const getUserMedia = mediaDevices?.getUserMedia?.bind(mediaDevices);
|
|
3145
|
+
const HostAudioContext = win.AudioContext;
|
|
3146
|
+
/*
|
|
3147
|
+
* THE HEAVY FILES START COMING DOWN HERE, and the line is deliberately the LAST thing of the boot.
|
|
692
3148
|
*
|
|
693
|
-
*
|
|
694
|
-
*
|
|
695
|
-
*
|
|
696
|
-
* decisão inteira do ADR-0117 §2 — e é por isso que eles ficam como estão.
|
|
3149
|
+
* ⚠️ NO `await`. The start does not wait for the heavy files — if it did, a 3G school's first screen would stay blank for minutes
|
|
3150
|
+
* and the child would conclude the game does not open. The empty `catch` is the same rule written twice: a network failure here
|
|
3151
|
+
* cannot bring down a game that may not even use the voice.
|
|
697
3152
|
*
|
|
698
|
-
*
|
|
699
|
-
*
|
|
3153
|
+
* 🔴 AND THE REPORT DOES NOT GO TO `problems`, for two reasons, and the first is the one that matters:
|
|
3154
|
+
*
|
|
3155
|
+
* 1. **IT ARRIVES AFTER THE READER HAS GONE.** `problems` is returned synchronously; the download runs in the background,
|
|
3156
|
+
* so EVERY line of it would land in an array the consumer has already read. Whoever does `if (motor.problems.length) …`
|
|
3157
|
+
* would see nothing, and whoever read it later would see a list that grew after boot.
|
|
3158
|
+
* 2. **IT WOULD DROWN WHAT CAN BE FIXED.** Without a network — a school without one is the target, not the exception —
|
|
3159
|
+
* every file is a failure pushed into a list ADR-0106 §2 built to say what the HOST LACKS. The child loses the
|
|
3160
|
+
* accessibility bar and the line that says so sits under all of them.
|
|
3161
|
+
*
|
|
3162
|
+
* 📌 The right channel is the one the function already has: `onHeavyProgress`, handed to whoever calls. A consumer who
|
|
3163
|
+
* wants to show «N MB left» or «the voice did not come down» has a way; the engine invents no surface.
|
|
700
3164
|
*/
|
|
3165
|
+
if (o.downloadHeavy !== false) {
|
|
3166
|
+
// ⚠️ THE READING MODEL IS ASKED FOR BY LANGUAGE and not by a yes: the three together are 850 MiB, and the child is reading in
|
|
3167
|
+
// one of them. `bcp47()` is already the language the interface booted in (ADR-0031), so nothing new has to be decided here.
|
|
3168
|
+
void downloadHeavy({
|
|
3169
|
+
// 📌 AND THE COMMAND MODEL IS ASKED FOR WITHOUT ASKING THE GAME (issue #184): a child who says «menu» instead of pressing
|
|
3170
|
+
// it is reaching the controller, and no cartridge declares — or denies — a way in (ADR-0111). A delivery built without
|
|
3171
|
+
// `--commands` simply has none, this background fetch fails quietly, and the transport says so when she turns it on.
|
|
3172
|
+
only: heavyAtBoot({ kokoro: !!o.uses?.neuralVoice, reading: o.uses?.reading ? bcp47() : null, commands: bcp47() }),
|
|
3173
|
+
onProgress: o.onHeavyProgress,
|
|
3174
|
+
cacheStorage: heavyCaches, fetch: win.fetch, digest: sha256With(win.crypto?.subtle), base: doc.baseURI,
|
|
3175
|
+
})
|
|
3176
|
+
.catch(() => { });
|
|
3177
|
+
}
|
|
701
3178
|
/*
|
|
702
|
-
*
|
|
703
|
-
*
|
|
3179
|
+
* ⚠️ `declaration`, `declines`, `problems` AND `reach` ARE GETTERS; the rest are not, and the asymmetry is the decision.
|
|
3180
|
+
*
|
|
3181
|
+
* The first two belong to the GAME's half (ADR-0139 §1), and the last two are derived from it, so all four must follow
|
|
3182
|
+
* the mounted cartridge — a plain field here would return, after a `mount()`, what the cartridge that booted first had.
|
|
3183
|
+
* `pause`, `tts`, `overlays`, `nav`, `keyboard` and the sonar belong to the PAGE and exist once, which is the whole
|
|
3184
|
+
* decision of ADR-0117 §2.
|
|
3185
|
+
*/
|
|
3186
|
+
/*
|
|
3187
|
+
* THE SCENE STACK BELONGS TO THE ROOT, and not to the return value, because `unmount()` must reach it. Born once
|
|
3188
|
+
* (ADR-0117 §2: the page has one) and emptied between cartridges, never replaced.
|
|
704
3189
|
*/
|
|
705
|
-
const
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
3190
|
+
const rootScenes = createSceneStack();
|
|
3191
|
+
// The hooks carry the REQUIRED answer to the accommodations (ADR-0153). The public type lets them be omitted, so an
|
|
3192
|
+
// absent value becomes `{}` — the cartridge that did not answer — and is refused below with ADR-0153's sentence, never
|
|
3193
|
+
// with a TypeError on `hooks.preset`.
|
|
3194
|
+
function mountAll(declaration, hooks = {}) {
|
|
3195
|
+
// ⚠️ IT THROWS, IT DOES NOT DIAGNOSE — the boot's rule, and so the boot's sentence. A malformed declaration is a
|
|
3196
|
+
// precondition: `problems` is for gaps one can still play with, and this is not one.
|
|
3197
|
+
const malformed = conformanceProblems(declaration);
|
|
3198
|
+
if (malformed.length) {
|
|
3199
|
+
refuseDeclaration('mount', malformed);
|
|
712
3200
|
}
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
3201
|
+
// ⚠️ AND `mount()` REFUSES BY THE SAME RULES, before writing to `cartridge`. `CartridgeHooks` is
|
|
3202
|
+
// `Omit<GameHalf, 'declaration'>`, so it carries `preset` — a second cartridge could take the «start» the first one
|
|
3203
|
+
// respected, and the root would be left with the pause unreachable mid-session.
|
|
3204
|
+
refuseIfItClaimsStart('mount', hooks.preset);
|
|
3205
|
+
refuseIfNoAnswer('mount', hooks.accommodations);
|
|
3206
|
+
refuseIfGenreRefused('mount', hooks.genre);
|
|
3207
|
+
refuseIfHudMalformed('mount', hooks.hud);
|
|
3208
|
+
refuseIfOptionsMalformed('mount', hooks.gameOptions);
|
|
3209
|
+
refuseIfHowToPlayMalformed('mount', hooks.howToPlay);
|
|
3210
|
+
cartridge = { ...hooks, declaration };
|
|
3211
|
+
mountHud(); // the numbers are the cartridge's: the new one's replace the old one's, and the room is measured again
|
|
3212
|
+
followCartridgeMappings(declaration);
|
|
3213
|
+
redrawGameOptions(); // the rows are the new cartridge's, drawn or cleared before its door is weighed
|
|
3214
|
+
pauseIcons.reflectPauseIcons(); // the bar follows the new cartridge: the hourglass exists only where time runs by itself
|
|
3215
|
+
currentReach = deriveReach();
|
|
3216
|
+
// The pad has the SHAPE of the preset, so it changes with the cartridge; the window listeners stay (`rewire`, not `attach`).
|
|
3217
|
+
drawPad();
|
|
3218
|
+
touchBindings.rewire();
|
|
3219
|
+
}
|
|
3220
|
+
/**
|
|
3221
|
+
* THE PAGE LINKS THE ENGINE STYLESHEET, or it is told (study item B4). `createGame` injects no CSS; without
|
|
3222
|
+
* `style.css` the panels, the footer band, the target floor and the focus rings are all missing, and nothing said so.
|
|
3223
|
+
* Read when `problems` is read, by the sentinel only that stylesheet declares — a stylesheet loading late is not
|
|
3224
|
+
* accused; a host without `getComputedStyle` measures nothing and accuses nothing.
|
|
3225
|
+
*/
|
|
3226
|
+
function stylesheetMissing() {
|
|
3227
|
+
if (typeof win.getComputedStyle !== 'function' || !doc.documentElement)
|
|
3228
|
+
return [];
|
|
3229
|
+
const rootStyles = win.getComputedStyle(doc.documentElement);
|
|
3230
|
+
if (!rootStyles || typeof rootStyles.getPropertyValue !== 'function')
|
|
3231
|
+
return [];
|
|
3232
|
+
return rootStyles.getPropertyValue('--incl-engine-stylesheet').trim() ? [] : [`the page does not link the engine stylesheet \
|
|
3233
|
+
(package export \`the-inclusionist-engine/style.css\`): panels, the footer band, the target floor and the focus rings are \
|
|
3234
|
+
unstyled, so a child who plays by keyboard cannot see where focus is — link that stylesheet`];
|
|
3235
|
+
}
|
|
3236
|
+
/*
|
|
3237
|
+
* THE FLASH SAMPLER (study item B2, cut 2). Each animation frame the world's canvas is drawn into 160×120, the relative
|
|
3238
|
+
* luminance is computed per pixel (WCAG's sRGB formula) and averaged into the 16×12 grid of `core/flash-threshold` — per
|
|
3239
|
+
* pixel and then averaged, so a small bright area weighs by its area; a direct 16×12 downscale samples a few pixels and a
|
|
3240
|
+
* flash between them goes unseen. Registered from the frame after the call, gone when the time is up.
|
|
3241
|
+
*/
|
|
3242
|
+
/*
|
|
3243
|
+
* STORAGE OUTSIDE THE ENGINE'S SCOPES (study item E2). The keys of both storages are photographed at boot; `problems`
|
|
3244
|
+
* names the ones that appeared since and sit outside every scope. Only what appeared: on a shared origin (localhost)
|
|
3245
|
+
* the keys already there are other pages'. By capability: a host without storage, or one that throws, measures nothing.
|
|
3246
|
+
*/
|
|
3247
|
+
function storageKeys() {
|
|
3248
|
+
const keysFound = new Set();
|
|
3249
|
+
for (const name of ['localStorage', 'sessionStorage']) {
|
|
3250
|
+
try {
|
|
3251
|
+
const area = win[name];
|
|
3252
|
+
if (!area || typeof area.key !== 'function')
|
|
3253
|
+
continue;
|
|
3254
|
+
for (let i = 0; i < area.length; i++) {
|
|
3255
|
+
const k = area.key(i);
|
|
3256
|
+
if (k !== null)
|
|
3257
|
+
keysFound.add(k);
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
3260
|
+
catch { /* private mode or a host double: nothing to read */ }
|
|
3261
|
+
}
|
|
3262
|
+
return keysFound;
|
|
3263
|
+
}
|
|
3264
|
+
const keysAtBoot = storageKeys();
|
|
3265
|
+
function storageOutsideScope() {
|
|
3266
|
+
const keysSinceBoot = [...storageKeys()].filter((k) => !keysAtBoot.has(k));
|
|
3267
|
+
const outsideScopes = keysOutsideScopes(keysSinceBoot);
|
|
3268
|
+
if (!outsideScopes.length)
|
|
3269
|
+
return [];
|
|
3270
|
+
return [`the cartridge stored keys outside the engine's scopes (${outsideScopes.slice(0, 5).join(', ')}): a child's settings `
|
|
3271
|
+
+ 'kept there do not follow them to the next game, and a game\'s own collide with other games\' — what belongs to '
|
|
3272
|
+
+ 'the child goes under incl_* through the engine\'s settings, what belongs to the game under incl.<game>.* (storage-keys.gameKey)'];
|
|
3273
|
+
}
|
|
3274
|
+
const measuredProblems = [];
|
|
3275
|
+
/*
|
|
3276
|
+
* THE CHILD READS ALOUD, AND THE GAME RECEIVES TEXT (ADR-0216, issue #200). The engine owns the microphone, the route and
|
|
3277
|
+
* the promise of privacy; the cartridge calls `listen()`. ⚠️ Recognition on the device or nothing: `platform/reading` only
|
|
3278
|
+
* uses the browser's recogniser where it says it recognises locally, and refuses otherwise instead of quietly sending a
|
|
3279
|
+
* child's voice to a server.
|
|
3280
|
+
*/
|
|
3281
|
+
/*
|
|
3282
|
+
* 📌 THE READING THREAD GOES WITH THE CARTRIDGE (issue #185), and the closer lives OUT HERE rather than on the `Reading`
|
|
3283
|
+
* object: a root that mounts another game keeps the same reading object, and a worker holding a compiled model of up to
|
|
3284
|
+
* 378 MiB for a cartridge that never listens is a school machine's memory spent on nothing. It is opened again at the next
|
|
3285
|
+
* `listen()`, which is also the moment the child is willing to wait. ⚠️ Out here because `Reading` is the CARTRIDGE's
|
|
3286
|
+
* vocabulary (ADR-0216): a method only this file calls has no business in a contract seven repositories read.
|
|
3287
|
+
*/
|
|
3288
|
+
let closeReadingThread = () => { };
|
|
3289
|
+
const reading = (() => {
|
|
3290
|
+
const browserApis = win;
|
|
3291
|
+
let microphone = null;
|
|
3292
|
+
/** The reading thread, kept between readings (opening it compiles the model again) and let go with the game. */
|
|
3293
|
+
let readingThread = null;
|
|
3294
|
+
const listener = createReading({
|
|
3295
|
+
language: () => bcp47(),
|
|
3296
|
+
api: (browserApis.SpeechRecognition ?? browserApis.webkitSpeechRecognition ?? null),
|
|
3297
|
+
now: () => win.performance.now(),
|
|
3298
|
+
every: (fn, ms) => win.setInterval(fn, ms),
|
|
3299
|
+
stopEvery: (h) => win.clearInterval(h),
|
|
3300
|
+
report: (rowNode) => { if (!measuredProblems.includes(rowNode))
|
|
3301
|
+
measuredProblems.push(rowNode); },
|
|
3302
|
+
/**
|
|
3303
|
+
* THE ENGINE'S OWN RECOGNISER, and it arrives late on purpose (ADR-0216 §5): `platform/reading-runtime` is what names the
|
|
3304
|
+
* model files, so a game that never listens — and a child of a game that does, until the first `listen()` — loads none of
|
|
3305
|
+
* it. Only reached where the device's own recogniser cannot serve the language (ADR-0200 erratum).
|
|
3306
|
+
*/
|
|
3307
|
+
/*
|
|
3308
|
+
* 🔴 AND IT RUNS IN A THREAD OF ITS OWN (issue #185). 📏 Measured in the lab: transcribing on the main thread cut the
|
|
3309
|
+
* recording in gaps of 4 s — the child goes on reading and the words she says while the page is busy are not in the
|
|
3310
|
+
* sound at all. In a worker the biggest gap was 264 ms.
|
|
3311
|
+
* ⚠️ WHERE THERE IS NO `Worker` the reading still works, on this thread, and the LINE SAYS SO: an engine that quietly
|
|
3312
|
+
* fell back would put the defect back exactly where nobody looks for it.
|
|
3313
|
+
*/
|
|
3314
|
+
model: o.uses?.reading
|
|
3315
|
+
? async (language) => {
|
|
3316
|
+
/*
|
|
3317
|
+
* ⚠️ A LOCAL NAMED `Worker`, HOLDING THE HOST'S, AND THE LITERAL BELOW IN EXACTLY THIS FORM (ADR-0232 D4). A bundler
|
|
3318
|
+
* emits the worker's file and rewrites its address only for `new Worker(new URL('…', import.meta.url), { … })`
|
|
3319
|
+
* written out — which is what makes the thread travel with whoever installs the package; a plain string would
|
|
3320
|
+
* resolve against the PAGE and 404 in every game whose folders differ. The bundler reads the NAME; the VALUE is the
|
|
3321
|
+
* window this root was lent, so the thread is opened by the host and no global is reached.
|
|
3322
|
+
*/
|
|
3323
|
+
const Worker = browserApis.Worker;
|
|
3324
|
+
if (typeof Worker === 'function') {
|
|
3325
|
+
const { createReadingInWorker } = await import('../platform/reading-in-worker.js');
|
|
3326
|
+
readingThread?.close();
|
|
3327
|
+
readingThread = createReadingInWorker({
|
|
3328
|
+
base: doc.baseURI,
|
|
3329
|
+
language,
|
|
3330
|
+
spawn: () => new Worker(new URL('../platform/reading-worker.js', import.meta.url), { type: 'module' }),
|
|
3331
|
+
// 📌 THE SENTENCE IS WRITTEN HERE and the module hands over only the REASON: a thread that fails to open when
|
|
3332
|
+
// nobody is waiting for the answer had nowhere to be said (ADR-0169), and whoever knows what the child loses
|
|
3333
|
+
// is the diagnostic channel, not a thread's protocol.
|
|
3334
|
+
report: (reason) => {
|
|
3335
|
+
const line = `reading: the transcription thread could not open — ${reason}; a child who reads aloud `
|
|
3336
|
+
+ 'gets no answer, and nothing else in the page will say so — check that the reading model for this '
|
|
3337
|
+
+ 'language reached `heavy/` (npx inclusionist-heavy --reading <language>)';
|
|
3338
|
+
if (!measuredProblems.includes(line))
|
|
3339
|
+
measuredProblems.push(line);
|
|
3340
|
+
},
|
|
3341
|
+
});
|
|
3342
|
+
closeReadingThread = () => { readingThread?.close(); readingThread = null; };
|
|
3343
|
+
return readingThread;
|
|
3344
|
+
}
|
|
3345
|
+
measuredProblems.push('reading: this browser has no `Worker`, so the transcription runs on the same thread that '
|
|
3346
|
+
+ 'draws the game and feeds the microphone — measured, that cuts the recording in gaps of seconds and the child '
|
|
3347
|
+
+ 'loses the words she said meanwhile; serve the game where workers are available');
|
|
3348
|
+
const { loadReadingRuntime } = await import('../platform/reading-runtime.js');
|
|
3349
|
+
return loadReadingRuntime({ base: doc.baseURI, language, fetch: (url) => win.fetch(url) });
|
|
3350
|
+
}
|
|
3351
|
+
: undefined,
|
|
3352
|
+
/**
|
|
3353
|
+
* AND THE MICROPHONE, for the model route only: the browser's own recogniser opens one itself. It is built at the first
|
|
3354
|
+
* reading and let go at the end of each — a track left running is a browser still saying «this page is listening».
|
|
3355
|
+
*/
|
|
3356
|
+
record: o.uses?.reading
|
|
3357
|
+
? async (options) => {
|
|
3358
|
+
const { createMicrophone } = await import('../platform/microphone.js');
|
|
3359
|
+
microphone ??= createMicrophone({
|
|
3360
|
+
getUserMedia, createContext: (rate) => new HostAudioContext({ sampleRate: rate }), now: () => win.performance.now(),
|
|
3361
|
+
});
|
|
3362
|
+
return microphone.record(options);
|
|
3363
|
+
}
|
|
3364
|
+
: undefined,
|
|
3365
|
+
});
|
|
3366
|
+
const notDeclared = 'reading: this game called `reading.listen()` without declaring `uses: { reading: true }` — the child '
|
|
3367
|
+
+ 'speaks and nothing answers, because a delivery built from this declaration carries no reading model; declare it';
|
|
3368
|
+
return {
|
|
3369
|
+
ready: () => (o.uses?.reading ? listener.ready() : Promise.resolve({ can: false, why: 'no-model' })),
|
|
3370
|
+
stop: () => listener.stop(),
|
|
3371
|
+
listen: (options) => {
|
|
3372
|
+
if (!o.uses?.reading) {
|
|
3373
|
+
if (!measuredProblems.includes(notDeclared))
|
|
3374
|
+
measuredProblems.push(notDeclared);
|
|
3375
|
+
return Promise.reject(new Error('reading was not declared by this game: `uses: { reading: true }`'));
|
|
3376
|
+
}
|
|
3377
|
+
return listener.listen(options);
|
|
3378
|
+
},
|
|
3379
|
+
};
|
|
3380
|
+
})();
|
|
3381
|
+
/*
|
|
3382
|
+
* PLAYING WITH THE EYES (ADR-0213; issues #194, #196): the stored 👀 position drives `ui/eye-control` — the camera, the reading, the
|
|
3383
|
+
* keys stamped `olhos` on `#game-region` for seat 0, and the regions drawn over the game. What cannot start is said, lands here in
|
|
3384
|
+
* `problems`, and puts the 👀 back to off. A stored position opens the camera at start, which asks the child's permission.
|
|
3385
|
+
*/
|
|
3386
|
+
/*
|
|
3387
|
+
* THE VIRTUAL CONTROLLER CARRIES COMMANDS TO THE GAME (ADR-0111 erratum; issue #197). The keyboard reaches it by the child's scheme,
|
|
3388
|
+
* in the window's capture after the menu navigation and the motor simulations (a key they refused stopped there); a transport that
|
|
3389
|
+
* reads positions presses the controller directly.
|
|
3390
|
+
*/
|
|
3391
|
+
const deliverCommand = (cmd) => { cartridge.onCommand?.(cmd); };
|
|
3392
|
+
/*
|
|
3393
|
+
* THE SCAN ITSELF (ADR-0218): the list is the positions this cartridge declared AND NAMED, because the chip says the game's
|
|
3394
|
+
* own words and a position nobody named would cost the child a pass of silence (ADR-0074). It is rebuilt every time the scan
|
|
3395
|
+
* starts, so a `mount()` of another cartridge scans ITS positions and not the ones that booted first (ADR-0142).
|
|
3396
|
+
*/
|
|
3397
|
+
const gameRegion = $('#game-region');
|
|
3398
|
+
const scanChip = gameRegion ? mountScanOverlay(doc, gameRegion) : null;
|
|
3399
|
+
let scanner = null;
|
|
3400
|
+
let scanFrame = 0;
|
|
3401
|
+
const scanWord = (item) => scanItemText(item, (a) => (cartridge.preset ? labellerFrom(cartridge.preset)(a) : null), t('scan.nothing'));
|
|
3402
|
+
// A word of a different length is a different amount of room to keep free, so the band is measured again — and only then.
|
|
3403
|
+
const scanShow = (item) => { if (scanChip?.showing(scanWord(item)))
|
|
3404
|
+
reserveBarBand(); };
|
|
3405
|
+
const scanTick = () => {
|
|
3406
|
+
if (!scanner)
|
|
3407
|
+
return;
|
|
3408
|
+
scanShow(scanner(win.performance.now()).showing.item);
|
|
3409
|
+
scanFrame = win.requestAnimationFrame(scanTick);
|
|
3410
|
+
};
|
|
3411
|
+
const stopScan = () => {
|
|
3412
|
+
if (scanFrame)
|
|
3413
|
+
win.cancelAnimationFrame(scanFrame);
|
|
3414
|
+
scanFrame = 0;
|
|
3415
|
+
scanner = null;
|
|
3416
|
+
scanChip?.hide();
|
|
3417
|
+
reserveBarBand(); // the room the chip was keeping goes back to the game
|
|
3418
|
+
};
|
|
3419
|
+
const startScan = () => {
|
|
3420
|
+
if (scanner)
|
|
3421
|
+
return;
|
|
3422
|
+
const names = cartridge.preset ? labellerFrom(cartridge.preset) : null;
|
|
3423
|
+
const offered = (cartridge.preset ? presetActions(cartridge.preset) : []).filter((a) => !!names?.(a));
|
|
3424
|
+
scanner = createSwitchScan(offered);
|
|
3425
|
+
scanTick();
|
|
3426
|
+
};
|
|
3427
|
+
scanPress = (source) => {
|
|
3428
|
+
if (!scanner)
|
|
3429
|
+
return;
|
|
3430
|
+
// 📌 THE CHIP IS NOT REDRAWN HERE, and a surviving mutation is why: the frame loop above draws every frame, so a second
|
|
3431
|
+
// drawing path only saved the sixteen milliseconds until the next one — a line that could disagree with the loop and could
|
|
3432
|
+
// never be seen doing it.
|
|
3433
|
+
const out = scanner(win.performance.now(), { press: true });
|
|
3434
|
+
const action = out.commanded;
|
|
3435
|
+
if (!action)
|
|
3436
|
+
return;
|
|
3437
|
+
// 📌 THROUGH THE VIRTUAL CONTROLLER, like every other transport (ADR-0111): in a menu it becomes that menu's key, in play it
|
|
3438
|
+
// holds the child's key and reaches the cartridge. The scan decides WHICH position; it does not decide what a position does.
|
|
3439
|
+
virtualController.press(action, source, 0);
|
|
3440
|
+
win.setTimeout(() => virtualController.release(action, source, 0), SWITCH_SCAN_DEFAULTS.pulseMs);
|
|
3441
|
+
};
|
|
3442
|
+
stateOn('switchScan', (on) => { if (on)
|
|
3443
|
+
startScan();
|
|
3444
|
+
else
|
|
3445
|
+
stopScan(); });
|
|
3446
|
+
whenDisposed(stopScan); // the scan's frames are this root's, and an ended root keeps none running (ADR-0220)
|
|
3447
|
+
if (state.switchScan)
|
|
3448
|
+
startScan();
|
|
3449
|
+
const virtualController = createVirtualController({
|
|
3450
|
+
scheme: (i) => keyboard.kbFor(i), menuOpen: menuWithDpad,
|
|
3451
|
+
// ⚠️ `markKeyFrom` AND NOT RAW `markKey`: a key that arrives WITHOUT a source — which is every real keyboard event —
|
|
3452
|
+
// must ERASE whoever held it last instead of inheriting them (ADR-0109). The choice between the two doors lives in
|
|
3453
|
+
// `input/state`.
|
|
3454
|
+
holdKey: markKeyFrom,
|
|
3455
|
+
releaseKey: releaseKey, menuKey: keyToMenu, deliver: deliverCommand,
|
|
3456
|
+
});
|
|
3457
|
+
/*
|
|
3458
|
+
* 🔴 THE KEYBOARD CONDUCTOR, AND ONLY THAT (ADR-0223). It resolves the action and PRESSES the virtual controller, like the
|
|
3459
|
+
* other transports — so there is ONE `deliver`, called from one place.
|
|
3460
|
+
*
|
|
3461
|
+
* 📏 What that guarantees:
|
|
3462
|
+
* · with a menu open, a release is delivered only for a press the game HEARD — the controller's `held` memory;
|
|
3463
|
+
* · a press swallowed by a menu followed by a release does not deliver a release without a press;
|
|
3464
|
+
* · the question is asked the INVERSE way and does not age: what is not the keyboard is not this conductor's. A
|
|
3465
|
+
* list of transports to exclude would age with the list, as it did when voice and scan arrived.
|
|
3466
|
+
*
|
|
3467
|
+
* 📌 And it does not ask whether a menu is open. That question has ONE answer, the controller's; what makes it true
|
|
3468
|
+
* for the keyboard is `keyToMenu` above, which does not redispatch a key that is already in the world.
|
|
3469
|
+
*/
|
|
3470
|
+
for (const kind of ['keydown', 'keyup']) {
|
|
3471
|
+
win.addEventListener(kind, (e) => {
|
|
3472
|
+
if (e.repeat || !cartridge.onCommand)
|
|
3473
|
+
return;
|
|
3474
|
+
const source = sourceOfEvent(e);
|
|
3475
|
+
// ⚠️ NO STAMP IS THE REAL KEYBOARD: an event the child produced carries no expando. The only stamp that belongs to
|
|
3476
|
+
// this conductor is `teclado`, and it exists for the release the motor filter synthesises.
|
|
3477
|
+
if (source && source !== 'teclado')
|
|
3478
|
+
return;
|
|
3479
|
+
const seat = keyboard.whichPlayer(e.code);
|
|
3480
|
+
if (seat < 0)
|
|
3481
|
+
return;
|
|
3482
|
+
const action = keyboard.actionOf(e.code, seat);
|
|
3483
|
+
if (!action)
|
|
3484
|
+
return;
|
|
3485
|
+
if (kind === 'keydown')
|
|
3486
|
+
virtualController.press(action, source, seat);
|
|
3487
|
+
else
|
|
3488
|
+
virtualController.release(action, source, seat);
|
|
3489
|
+
}, true);
|
|
3490
|
+
}
|
|
3491
|
+
/*
|
|
3492
|
+
* 🔴 LOSING FOCUS IS THE KEYUP THAT NEVER ARRIVES. With a key down, a click on the browser's own bar, an on-screen
|
|
3493
|
+
* keyboard or a switch-access program taking focus, or a tab change leaves the page without its keyup: the key stays
|
|
3494
|
+
* held, the character keeps walking, and the cartridge believes the button is still down. The window's `blur` is the
|
|
3495
|
+
* only signal left, so it dispatches the keyup each held key is owed — through every listener a real one would reach
|
|
3496
|
+
* (the simulations, the cool-down, this conductor), which is what keeps them all agreeing that the key is up.
|
|
3497
|
+
*
|
|
3498
|
+
* Which keys those are is `input/state`'s answer (`letGoOfTheKeyboard`); stamped `teclado` because this engine never
|
|
3499
|
+
* dispatches an unsigned synthetic key (ADR-0109), and the keyboard's own stamp is what the motor filter's release uses.
|
|
3500
|
+
*/
|
|
3501
|
+
win.addEventListener('blur', () => letGoOfTheKeyboard((code) => {
|
|
3502
|
+
win.dispatchEvent(stampSource(new KeyboardEvent('keyup', { code, bubbles: true, cancelable: true }), 'teclado'));
|
|
3503
|
+
}));
|
|
3504
|
+
/*
|
|
3505
|
+
* 🔴 THE GAMEPAD, MOUNTED BY THE ENGINE (ADR-0224), like every other transport: the virtual controller is a local of
|
|
3506
|
+
* this function, and the one door (ADR-0223) has to reach it. A physical gamepad works because the child plugged one
|
|
3507
|
+
* in, not because a game remembered to ask.
|
|
3508
|
+
*
|
|
3509
|
+
* 📌 Almost every port of `GamepadCtx` is answered here with what this root already has. The rest are the cartridge's
|
|
3510
|
+
* world and arrive in one field (`GamepadGameHooks`), with **each absence having a written meaning** — never guessed.
|
|
3511
|
+
* A cartridge that declares nothing has a working gamepad.
|
|
3512
|
+
*/
|
|
3513
|
+
// 📌 The absences are resolved in `input/gamepad`, in a table: what an absence MEANS is a decision, and a composition
|
|
3514
|
+
// root carries wiring (ADR-0221, erratum). Only `worldRunning`'s is from here, because only whoever mounts knows which
|
|
3515
|
+
// menus it has open.
|
|
3516
|
+
const gameHooks = padGameAnswers(cartridge.gamepad, () => !menuWithDpad());
|
|
3517
|
+
const gamepad = initGamepad({
|
|
3518
|
+
$, padMaps, input, t: translator.t,
|
|
3519
|
+
padTable: (players, seat) => padTableNow(players, seat), // the MOUNTED cartridge's table, rebuilt by `mount()`
|
|
3520
|
+
oneButton: () => state.oneButton, // the motor empathy, read each frame from the settings store (ADR-0232)
|
|
3521
|
+
getGamepads: () => win.navigator?.getGamepads?.() ?? [],
|
|
3522
|
+
// THE GAME'S WORD for a position: the engine knows the position exists, only the cartridge knows what it is called —
|
|
3523
|
+
// and it already declared that in the `preset` to exist.
|
|
3524
|
+
actionLabel: (action) => (cartridge.preset ? labellerFrom(cartridge.preset)(action) : null),
|
|
3525
|
+
srSay, srAlert,
|
|
3526
|
+
frontOverlay: overlays.frontOverlay,
|
|
3527
|
+
// ⚠️ «PAUSE MENU» HERE IS EVERY MENU WITH A DIRECTIONAL, not only the card: the transport's `steerPause` already
|
|
3528
|
+
// handles the shared dialog before the card, which is the panel open on top. The same question the controller asks.
|
|
3529
|
+
pauseMenu: menuWithDpad,
|
|
3530
|
+
worldRunning: gameHooks.worldRunning,
|
|
3531
|
+
// The gamepad's START is the QUICK PAUSE (ADR-0155), like the screen's; and the way out reuses the decision already
|
|
3532
|
+
// written for the finger, which knows leaving the quick pause from closing the card.
|
|
3533
|
+
pause: () => { enterQuickPause(0); },
|
|
3534
|
+
resume: togglePauseByTouch,
|
|
3535
|
+
isAttractActive: gameHooks.attractActive,
|
|
3536
|
+
stopAttract: gameHooks.stopAttract,
|
|
3537
|
+
// A PHYSICAL button makes the on-screen pad vanish — the same per-modality switch as the keyboard.
|
|
3538
|
+
isTouchMode: () => { const p = $('#touch-controls'); return !!p && !p.hidden; },
|
|
3539
|
+
hideTouchControls: () => { touchPad.hideTouchControls(); padBeforeMenu = false; },
|
|
3540
|
+
// ⚠️ SEEDED AT EVERY READ and not once: the cartridge repopulates the list at every restart, and a seat seeded only at
|
|
3541
|
+
// boot would leave the new players without a `pad` — invisible to the transport, with no error anywhere.
|
|
3542
|
+
getPlayers: () => seatEveryPlayer(players()),
|
|
3543
|
+
getNumPlayers: () => players().length,
|
|
3544
|
+
navTitle: gameHooks.navTitle,
|
|
3545
|
+
onBar: isOnBar,
|
|
3546
|
+
// ✅ THE BAR'S SECOND WAY OUT: `ui/menu-nav` calls `navBar` with `(i, k)` and never the third argument, the START edge —
|
|
3547
|
+
// the SECOND way out of the mode (ADR-0044 item 7). With the gamepad mounted by the root, it arrives here.
|
|
3548
|
+
navBar,
|
|
3549
|
+
sharedDialogOpen: nav.sharedDialogOpen,
|
|
3550
|
+
navDialog: nav.navDialog,
|
|
3551
|
+
getPauseMenu: (i) => $(`#vp-pause-${i}`),
|
|
3552
|
+
navPause: nav.navPause,
|
|
3553
|
+
setPauseActor,
|
|
3554
|
+
playerEdge,
|
|
3555
|
+
press: (action, source, player) => virtualController.press(action, source, player),
|
|
3556
|
+
release: (action, source, player) => virtualController.release(action, source, player),
|
|
3557
|
+
modalInput: gameHooks.modalInput,
|
|
3558
|
+
hasModal: gameHooks.hasModal,
|
|
3559
|
+
joinPlayer: gameHooks.joinPlayer,
|
|
3560
|
+
respawnPlayer: gameHooks.respawnPlayer,
|
|
3561
|
+
clearWaitingBadge: gameHooks.clearWaitingBadge,
|
|
3562
|
+
wizardStep: gameHooks.wizardStep,
|
|
3563
|
+
wizardTick: gameHooks.wizardTick,
|
|
3564
|
+
});
|
|
3565
|
+
/*
|
|
3566
|
+
* AND THE ENGINE POLLS, because whoever mounts polls. ⚠️ The game loop is the CARTRIDGE's (`core/loop.startLoop` is
|
|
3567
|
+
* called by it), so the root has nowhere to hang a frame — it opens its own, as it already does for the scan and the
|
|
3568
|
+
* camera. A gamepad read every frame is the price written in ADR-0224's negative consequence.
|
|
3569
|
+
*/
|
|
3570
|
+
let padFrameHandle = 0;
|
|
3571
|
+
const anyPadConnected = () => (win.navigator?.getGamepads?.() ?? []).some(Boolean);
|
|
3572
|
+
const pollPad = () => { gamepad.pollPads(); padFrameHandle = win.requestAnimationFrame(pollPad); };
|
|
3573
|
+
const stopPollingPad = () => { if (padFrameHandle)
|
|
3574
|
+
win.cancelAnimationFrame(padFrameHandle); padFrameHandle = 0; };
|
|
3575
|
+
/*
|
|
3576
|
+
* ⚠️ THE LOOP EXISTS ONLY WHILE A GAMEPAD IS CONNECTED, and that is pillar 1 deciding: a `requestAnimationFrame` that
|
|
3577
|
+
* never sleeps costs battery on a school Chromebook, and the vast majority of machines will never see a gamepad.
|
|
3578
|
+
* 📌 `gamepadconnected` is the event the specification requires to arrive before the pad appears in the list, and the
|
|
3579
|
+
* query at boot covers the root born with one already connected (another cartridge's `mount()`, for example).
|
|
3580
|
+
* 📌 A host WITHOUT frames — the node project is one — is a capability of the environment and stays quiet (ADR-0169):
|
|
3581
|
+
* with no frames there is no game running for the gamepad to drive.
|
|
3582
|
+
*/
|
|
3583
|
+
const startPollingPad = () => { if (padFrameHandle || typeof win.requestAnimationFrame !== 'function')
|
|
3584
|
+
return; padFrameHandle = win.requestAnimationFrame(pollPad); };
|
|
3585
|
+
win.addEventListener('gamepadconnected', startPollingPad);
|
|
3586
|
+
win.addEventListener('gamepaddisconnected', () => { if (!anyPadConnected())
|
|
3587
|
+
stopPollingPad(); });
|
|
3588
|
+
if (anyPadConnected())
|
|
3589
|
+
startPollingPad();
|
|
3590
|
+
const gazeRegion = $('#game-region');
|
|
3591
|
+
if (canCaptureMedia && gazeRegion) {
|
|
3592
|
+
// PLAYING THROUGH THE WEBCAM (ADR-0215): one stored position, off · hands · face · eyes; `ui/camera-control` starts only the control at
|
|
3593
|
+
// that position. Each control opens the camera itself and lets it go when the position moves on.
|
|
3594
|
+
const visionLoop = {
|
|
3595
|
+
requestFrame: (cb) => win.requestAnimationFrame(cb), cancelFrame: (h) => win.cancelAnimationFrame(h),
|
|
3596
|
+
now: () => win.performance.now(), every: (cb, ms) => win.setInterval(cb, ms), stopEvery: (h) => win.clearInterval(h),
|
|
3597
|
+
};
|
|
3598
|
+
const cameraDeps = {
|
|
3599
|
+
t: translator.t, doc, region: gazeRegion, base: doc.baseURI, hasFile: hasHeavyFile, loop: visionLoop, controller: virtualController, say: srSay, alert: srAlert,
|
|
3600
|
+
report: (rowNode) => { if (!measuredProblems.includes(rowNode))
|
|
3601
|
+
measuredProblems.push(rowNode); },
|
|
3602
|
+
turnOff: () => state.setCameraControlValue('off'),
|
|
3603
|
+
};
|
|
3604
|
+
// the eyes: the relative reading and the four-zone cycle (ADR-0213), presses from `olhos`, the eye lines and the regions' outlines
|
|
3605
|
+
const eyes = createEyeControl(cameraDeps);
|
|
3606
|
+
// the face: the Dev's face map (ADR-0210), presses from `rosto`, the eyes, brows and lips lines
|
|
3607
|
+
const face = createFaceControl({ ...cameraDeps, openFeed: videoFeed(doc, win.navigator.mediaDevices) });
|
|
3608
|
+
// the hands: the Gesture Recognizer and the Dev's hands map (ADR-0210), presses from `gestos`, the hands' lines (issue #191)
|
|
3609
|
+
const hands = createHandControl({ ...cameraDeps, openFeed: videoFeed(doc, win.navigator.mediaDevices) });
|
|
3610
|
+
const cameraControls = { eyes, face, hands };
|
|
3611
|
+
stateOn('cameraControl', (mode) => { followCameraMode(mode, cameraControls); });
|
|
3612
|
+
followCameraMode(state.cameraControl, cameraControls);
|
|
3613
|
+
whenDisposed(() => { followCameraMode('off', cameraControls); }); // the camera closes with the root that opened it (ADR-0220)
|
|
3614
|
+
}
|
|
3615
|
+
/*
|
|
3616
|
+
* PLAYING BY SPEAKING (ADR-0189, ADR-0193, ADR-0194; issue #184): the stored 👄 drives `ui/voice-control` — the recogniser
|
|
3617
|
+
* from the delivery, the microphone that stays open, and presses stamped `fala` on the virtual controller.
|
|
3618
|
+
*
|
|
3619
|
+
* 📌 THE GRAMMAR FOLLOWS THE OPEN MENU: the names the child can see are the names she can say. They are read from the overlay
|
|
3620
|
+
* on top, which is the same one the focus trap and the menu navigation already treat as «the menu that is open».
|
|
3621
|
+
*/
|
|
3622
|
+
if (canCaptureMedia) {
|
|
3623
|
+
const menuWords = () => {
|
|
3624
|
+
const card = overlays.topVisibleOverlay();
|
|
3625
|
+
if (!card)
|
|
3626
|
+
return [];
|
|
3627
|
+
return [...card.querySelectorAll('button, [data-passos]')]
|
|
3628
|
+
.filter((el) => !el.hidden && el.getAttribute('aria-disabled') !== 'true')
|
|
3629
|
+
.map((el) => accessibleLabel(el))
|
|
3630
|
+
.filter((s) => s.length > 1);
|
|
3631
|
+
};
|
|
3632
|
+
voiceControl = createVoiceControl({
|
|
3633
|
+
t: translator.t, base: doc.baseURI, language: () => bcp47(), controller: virtualController, menuWords,
|
|
3634
|
+
say: srSay, alert: srAlert,
|
|
3635
|
+
report: (line) => { if (!measuredProblems.includes(line))
|
|
3636
|
+
measuredProblems.push(line); },
|
|
3637
|
+
turnOff: () => { state.setVoiceControlValue(false); },
|
|
3638
|
+
after: (fn, ms) => { win.setTimeout(fn, ms); },
|
|
3639
|
+
// ⚠️ THE ADDRESS IS ABSOLUTE AND THE BUNDLER MUST NOT FOLLOW IT: the recogniser arrives with the delivery at runtime.
|
|
3640
|
+
hasFile: hasHeavyFile, loadBundle: createBundleLoader((url) => import(/* @vite-ignore */ url)),
|
|
3641
|
+
getUserMedia, createContext: () => new HostAudioContext(),
|
|
3642
|
+
});
|
|
3643
|
+
stateOn('voiceControl', (on) => { void voiceControl?.apply(on); });
|
|
3644
|
+
// the words change with the menu that is open, and a menu opens on a key or a touch — so they are re-read on every draw of
|
|
3645
|
+
// the bar, which is what already happens whenever a card or a panel appears (ADR-0106 §5)
|
|
3646
|
+
stateOn('menuIndexOn', () => { voiceControl?.refreshGrammar(); });
|
|
3647
|
+
void voiceControl.apply(state.voiceControl);
|
|
3648
|
+
// and so does the microphone; pply(false) stops without writing the stored answer, which belongs to the child (ADR-0220)
|
|
3649
|
+
whenDisposed(() => { void voiceControl?.apply(false); });
|
|
3650
|
+
}
|
|
3651
|
+
/*
|
|
3652
|
+
/*
|
|
3653
|
+
* THE FLASH SAMPLER LIVES IN `platform/flash-sampler` (ADR-0221, issue #203); what stays here is what only the root
|
|
3654
|
+
* knows: which is THIS cartridge's world canvas, and where a failure goes.
|
|
3655
|
+
*/
|
|
3656
|
+
const sampleWorldFlashes = (ms) => sampleFlashes({
|
|
3657
|
+
canvas: () => {
|
|
3658
|
+
const declaredWorld = cartridge.declaration.world();
|
|
3659
|
+
const eventTarget = declaredWorld.kind === 'element' ? $(declaredWorld.selector) : null;
|
|
3660
|
+
return eventTarget?.tagName === 'CANVAS' ? eventTarget : eventTarget?.querySelector('canvas') ?? null;
|
|
3661
|
+
},
|
|
3662
|
+
scratch: () => doc.createElement('canvas'),
|
|
3663
|
+
frame: typeof win.requestAnimationFrame === 'function' ? (cb) => { win.requestAnimationFrame(cb); } : undefined,
|
|
3664
|
+
report: (rowNode) => { measuredProblems.push(rowNode); },
|
|
3665
|
+
}, ms);
|
|
3666
|
+
function unmountAll() {
|
|
3667
|
+
closeReadingThread();
|
|
3668
|
+
followCartridgeMappings(null);
|
|
3669
|
+
removeReachNotice();
|
|
3670
|
+
hudMounted?.remove();
|
|
3671
|
+
hudMounted = null;
|
|
3672
|
+
reserveBarBand();
|
|
3673
|
+
// ⚠️ `pop()` AND NOT A `clear()`: each `exit()` is that scene's DOM cleanup, and skipping it would leave on the page
|
|
3674
|
+
// what the previous cartridge drew. The loop ends because `pop()` returns `null` on an empty stack.
|
|
3675
|
+
while (rootScenes.pop()) { /* each scene's `exit()` IS its teardown */ }
|
|
3676
|
+
}
|
|
3677
|
+
function dispose() {
|
|
3678
|
+
unmountAll();
|
|
3679
|
+
// 📌 The gamepad's polling loop belongs to the ROOT since ADR-0224, so it dies with it: a `requestAnimationFrame` that
|
|
3680
|
+
// survives `dispose()` reads the Gamepad API forever, in a root that has no players any more (ADR-0220).
|
|
3681
|
+
stopPollingPad();
|
|
3682
|
+
for (const release of endOfLife.splice(0))
|
|
3683
|
+
release(); // drained as it goes: a second `dispose()` finds nothing to release
|
|
3684
|
+
listeners.releaseAll();
|
|
724
3685
|
}
|
|
725
3686
|
return {
|
|
726
|
-
get declaration() { return
|
|
3687
|
+
get declaration() { return cartridge.declaration; },
|
|
727
3688
|
get declines() { return declines(); },
|
|
728
|
-
mount:
|
|
729
|
-
unmount:
|
|
730
|
-
|
|
3689
|
+
mount: mountAll,
|
|
3690
|
+
unmount: unmountAll,
|
|
3691
|
+
dispose,
|
|
3692
|
+
pause: pauseControls,
|
|
731
3693
|
tts,
|
|
3694
|
+
reading,
|
|
3695
|
+
captionSound: writeSoundCaption,
|
|
3696
|
+
gameSpeed: () => state.gameSpeed,
|
|
3697
|
+
menuIndexOn: () => state.menuIndexOn,
|
|
3698
|
+
t: translator.t,
|
|
3699
|
+
localeReady: translator.ready,
|
|
3700
|
+
say: srSay,
|
|
3701
|
+
alert: srAlert,
|
|
3702
|
+
mirrorAnnouncements: announcer.mirrorTo,
|
|
3703
|
+
libras: { isOpen: libras.isOpen, say: libras.say, tick: libras.tick, toggle: () => { libras.toggle(translator.t); } },
|
|
3704
|
+
settings: state,
|
|
3705
|
+
input,
|
|
3706
|
+
keyboardConfig,
|
|
3707
|
+
audio: mixer,
|
|
3708
|
+
crt,
|
|
3709
|
+
lq,
|
|
3710
|
+
measureFlashes: sampleWorldFlashes,
|
|
732
3711
|
overlays,
|
|
733
3712
|
nav,
|
|
734
3713
|
keyboard,
|
|
3714
|
+
controller: virtualController,
|
|
735
3715
|
sonar,
|
|
736
|
-
|
|
737
|
-
|
|
3716
|
+
applyVisionFilter: setVisionFilter,
|
|
3717
|
+
scenes: rootScenes,
|
|
3718
|
+
onLocaleChange: (fn) => { localeListeners.push(fn); },
|
|
738
3719
|
cvdFilters,
|
|
739
|
-
get problems() { return [...
|
|
740
|
-
|
|
741
|
-
get
|
|
3720
|
+
get problems() { return [...hostProblems, ...stylesheetMissing(), ...measureCartridgeProblems(), ...translator.dictionaryGaps(), ...measuredProblems, ...storageOutsideScope()]; },
|
|
3721
|
+
onFailure: announceFailure,
|
|
3722
|
+
get reach() { return currentReach; },
|
|
742
3723
|
};
|
|
743
3724
|
}
|