@the-inclusionist/engine 7.0.0 → 8.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/app/css/style.css +22 -5
- package/app/public/vendor/fonts/fondamento-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/fondamento-400.woff2 +0 -0
- package/app/public/vendor/fonts/opendyslexic-400.woff2 +0 -0
- package/app/public/vendor/fonts/pressstart-400.woff2 +0 -0
- package/app/public/vendor/fonts.css +43 -2
- package/dist-pkg/boot/create-game.d.ts +76 -2
- package/dist-pkg/boot/create-game.js +271 -13
- package/dist-pkg/core/constants.d.ts +0 -28
- package/dist-pkg/core/constants.js +34 -17
- package/dist-pkg/core/contract.d.ts +39 -0
- package/dist-pkg/core/contract.js +36 -0
- package/dist-pkg/core/entity.d.ts +47 -4
- package/dist-pkg/core/layers.d.ts +18 -0
- package/dist-pkg/core/layers.js +18 -0
- package/dist-pkg/core/rng.js +14 -4
- package/dist-pkg/core/route.d.ts +42 -0
- package/dist-pkg/core/route.js +158 -0
- package/dist-pkg/core/state.d.ts +10 -1
- package/dist-pkg/core/state.js +13 -0
- package/dist-pkg/educational/adaptive-engine.d.ts +65 -0
- package/dist-pkg/educational/adaptive-engine.js +117 -0
- package/dist-pkg/educational/segment-bar.d.ts +96 -0
- package/dist-pkg/educational/segment-bar.js +89 -0
- package/dist-pkg/i18n/en.js +33 -0
- package/dist-pkg/i18n/es.js +33 -0
- package/dist-pkg/i18n/pt.js +46 -0
- package/dist-pkg/input/default-bindings.d.ts +40 -0
- package/dist-pkg/input/default-bindings.js +143 -9
- package/dist-pkg/input/gamepad.d.ts +18 -11
- package/dist-pkg/input/gamepad.js +75 -9
- package/dist-pkg/input/keyboard-runtime.d.ts +6 -8
- package/dist-pkg/input/keyboard-runtime.js +24 -5
- package/dist-pkg/input/keyboard.d.ts +10 -0
- package/dist-pkg/input/keyboard.js +41 -11
- package/dist-pkg/input/keydown.d.ts +27 -2
- package/dist-pkg/input/keydown.js +20 -6
- package/dist-pkg/input/latch-scope.d.ts +70 -0
- package/dist-pkg/input/latch-scope.js +110 -0
- package/dist-pkg/input/latch-store.d.ts +45 -0
- package/dist-pkg/input/latch-store.js +74 -0
- package/dist-pkg/input/origem-sintetica.d.ts +44 -0
- package/dist-pkg/input/origem-sintetica.js +60 -0
- package/dist-pkg/input/pointer.d.ts +61 -0
- package/dist-pkg/input/pointer.js +71 -0
- package/dist-pkg/input/state.d.ts +86 -1
- package/dist-pkg/input/state.js +128 -1
- package/dist-pkg/input/touch-bindings.d.ts +11 -2
- package/dist-pkg/input/touch-bindings.js +9 -3
- package/dist-pkg/input/touch.js +9 -1
- package/dist-pkg/input/transporte-em-uso.d.ts +101 -0
- package/dist-pkg/input/transporte-em-uso.js +130 -0
- package/dist-pkg/input/transports.d.ts +88 -2
- package/dist-pkg/input/transports.js +60 -5
- package/dist-pkg/input/vocabulary-migration.d.ts +18 -7
- package/dist-pkg/platform/audio-earcons.d.ts +29 -2
- package/dist-pkg/platform/audio-earcons.js +10 -0
- package/dist-pkg/platform/audio-nav.d.ts +1 -1
- package/dist-pkg/platform/audio-sonar.d.ts +149 -9
- package/dist-pkg/platform/audio-sonar.js +232 -21
- package/dist-pkg/platform/guide-intensity.d.ts +40 -0
- package/dist-pkg/platform/guide-intensity.js +73 -0
- package/dist-pkg/platform/storage.d.ts +30 -0
- package/dist-pkg/platform/storage.js +35 -0
- package/dist-pkg/platform/tts.js +16 -1
- package/dist-pkg/platform/voice-plan.d.ts +90 -0
- package/dist-pkg/platform/voice-plan.js +137 -0
- package/dist-pkg/render/draw.d.ts +1 -1
- package/dist-pkg/render/draw.js +15 -5
- package/dist-pkg/render/viz-axes.d.ts +17 -0
- package/dist-pkg/render/viz-axes.js +19 -0
- package/dist-pkg/render/viz-setters.d.ts +34 -2
- package/dist-pkg/render/viz-setters.js +189 -33
- package/dist-pkg/render/wheelchair-sprites.d.ts +11 -3
- package/dist-pkg/render/wheelchair-sprites.js +10 -4
- package/dist-pkg/ui/dom.js +20 -2
- package/dist-pkg/ui/fonts.d.ts +47 -1
- package/dist-pkg/ui/fonts.js +47 -9
- package/dist-pkg/ui/latch-refusal.d.ts +32 -0
- package/dist-pkg/ui/latch-refusal.js +60 -0
- package/dist-pkg/ui/layout.d.ts +32 -0
- package/dist-pkg/ui/layout.js +64 -1
- package/dist-pkg/ui/motion-scene.d.ts +41 -0
- package/dist-pkg/ui/motion-scene.js +76 -0
- package/dist-pkg/ui/panel-shell.d.ts +53 -0
- package/dist-pkg/ui/panel-shell.js +103 -0
- package/dist-pkg/ui/pause-icons.d.ts +134 -20
- package/dist-pkg/ui/pause-icons.js +307 -45
- package/dist-pkg/ui/reach-notice.js +8 -0
- package/dist-pkg/ui/settings-audio.d.ts +14 -1
- package/dist-pkg/ui/settings-audio.js +43 -3
- package/dist-pkg/ui/settings-controls.d.ts +66 -6
- package/dist-pkg/ui/settings-controls.js +179 -17
- package/dist-pkg/ui/settings-empathy.d.ts +15 -0
- package/dist-pkg/ui/settings-empathy.js +2 -0
- package/dist-pkg/ui/settings-motion.d.ts +25 -19
- package/dist-pkg/ui/settings-motion.js +32 -19
- package/dist-pkg/ui/settings-motor.d.ts +81 -3
- package/dist-pkg/ui/settings-motor.js +118 -9
- package/dist-pkg/ui/settings-typo.d.ts +32 -2
- package/dist-pkg/ui/settings-typo.js +64 -18
- package/dist-pkg/ui/settings-visual.d.ts +21 -1
- package/dist-pkg/ui/settings-visual.js +48 -4
- package/dist-pkg/ui/shell.d.ts +13 -3
- package/dist-pkg/ui/shell.js +3 -4
- package/dist-pkg/ui/simulation-refusal.d.ts +32 -0
- package/dist-pkg/ui/simulation-refusal.js +57 -0
- package/dist-pkg/ui/visual-axes-panel.d.ts +48 -0
- package/dist-pkg/ui/visual-axes-panel.js +95 -0
- package/dist-pkg/ui/webcam.js +6 -1
- package/docs/CREDITS.md +18 -0
- package/docs/LICENSES.md +12 -0
- package/package.json +24 -4
- package/app/public/vendor/fonts/greatvibes-400.woff2 +0 -0
- package/app/public/vendor/fonts/ufcook-700.woff2 +0 -0
|
@@ -121,6 +121,10 @@ export function sinkSelectValue(p) {
|
|
|
121
121
|
// DOM-facing (thin) — requires `document`/injected ctx
|
|
122
122
|
// ---------------------------------------------------------------------------------------------
|
|
123
123
|
export function initSettingsAudio(ctx) {
|
|
124
|
+
// ⚠️ PADRÃO DA ENGINE (ADR-0106 §4): quem injecta manda; quem não injecta deixa de ficar sem modo cego.
|
|
125
|
+
// O `setModoCegoValue` faz as três coisas que o `core/state` diz que um setter faz — grava, persiste, avisa
|
|
126
|
+
// — e nada mais: os efeitos (refazer os extras do nível) são reação, e quem reage assina o evento.
|
|
127
|
+
const setModoCego = ctx.setModoCego ?? state.setModoCegoValue;
|
|
124
128
|
let audioDevices = [];
|
|
125
129
|
function reflectMaster() {
|
|
126
130
|
const b = ctx.$('#audio-master');
|
|
@@ -179,6 +183,8 @@ export function initSettingsAudio(ctx) {
|
|
|
179
183
|
return;
|
|
180
184
|
el.innerHTML = catsListHTML(keys, ctx.audioCats, state);
|
|
181
185
|
wireCatControls(el);
|
|
186
|
+
// A prosa volta para o rodapé depois de as linhas serem reconstruídas (CLAUDE.md §4, #109).
|
|
187
|
+
ctx.fillExplain?.(ctx.$('#audio .overlay__card'));
|
|
182
188
|
}
|
|
183
189
|
function renderNavSound() {
|
|
184
190
|
renderCategoryList('#navsound-list', NAV_CATS);
|
|
@@ -417,9 +423,28 @@ export function initSettingsAudio(ctx) {
|
|
|
417
423
|
NAV_CATS.forEach((k) => { state[k].vol = v; state[k].on = true; ctx.setCatGain(k); });
|
|
418
424
|
renderNavSound();
|
|
419
425
|
});
|
|
426
|
+
/*
|
|
427
|
+
* ⚠️ ESTE BOTÃO ERA O ÚNICO DESTE PAINEL QUE NÃO ANUNCIAVA. Medido em 2026-09-08: os cinco irmãos daqui
|
|
428
|
+
* anunciam (som, TTS, divisor da bengala, índice de menu, saída de áudio) e o modo cego não — ele parecia
|
|
429
|
+
* anunciar porque UM cartucho o fazia a partir do próprio `setModoCego`, e o painel herdava o efeito.
|
|
430
|
+
*
|
|
431
|
+
* ⚠️ E ISSO PASSOU A EXPOR SILÊNCIO no mesmo dia: desde que o campo ganhou padrão da engine
|
|
432
|
+
* (`setModoCegoValue`, que grava/persiste/avisa e NÃO fala), um jogo que não injecta o seu próprio setter
|
|
433
|
+
* ficava com este botão mudo. Um alternador que muda estado sem o dizer é invisível para quem usa leitor de
|
|
434
|
+
* tela — a mesma família de defeito que o `reflectTTS` e o `reflectModoCego` já custaram aqui.
|
|
435
|
+
*
|
|
436
|
+
* O anúncio pertence a QUEM É ACCIONADO, não ao setter: `core/state` diz que o setter faz três coisas e só
|
|
437
|
+
* três. ⚠️ Consequência de lockstep, escrita para não se descobrir depois: quando o `game-platformer` subir
|
|
438
|
+
* de versão, tem de TIRAR o `srSay` do `setModoCego` dele, senão a criança ouve o estado duas vezes.
|
|
439
|
+
*/
|
|
420
440
|
const mcBtn = ctx.$('#opt-modocego');
|
|
421
|
-
if (mcBtn)
|
|
422
|
-
mcBtn.addEventListener('click', () => {
|
|
441
|
+
if (mcBtn) {
|
|
442
|
+
mcBtn.addEventListener('click', () => {
|
|
443
|
+
setModoCego(!ctx.getModoCego());
|
|
444
|
+
reflectModoCego();
|
|
445
|
+
ctx.srSay(t(ctx.getModoCego() ? 'sr.blind.on' : 'sr.blind.off'));
|
|
446
|
+
});
|
|
447
|
+
}
|
|
423
448
|
const caneDivSel = ctx.$('#cane-div');
|
|
424
449
|
if (caneDivSel) {
|
|
425
450
|
caneDivSel.value = String(ctx.getCaneBlockDiv());
|
|
@@ -516,7 +541,7 @@ export function initSettingsAudio(ctx) {
|
|
|
516
541
|
const resetBtn = ctx.$('#audio-reset');
|
|
517
542
|
if (resetBtn)
|
|
518
543
|
resetBtn.addEventListener('click', () => {
|
|
519
|
-
|
|
544
|
+
setModoCego(DEFAULTS.modoCego);
|
|
520
545
|
ctx.setCaneBlockDiv(DEFAULTS.caneBlockDiv);
|
|
521
546
|
const state = ctx.getAudioCat();
|
|
522
547
|
if (state)
|
|
@@ -554,5 +579,20 @@ export function initSettingsAudio(ctx) {
|
|
|
554
579
|
if (audioDetectBtn)
|
|
555
580
|
audioDetectBtn.addEventListener('click', () => { void detectAudioDevices(); });
|
|
556
581
|
reflectMaster(); // estado inicial do botão/slider mestre, antes de qualquer abertura do painel
|
|
582
|
+
/*
|
|
583
|
+
* ⚠️ O PAINEL ASSINA O EVENTO, e isto não é uma ideia nova: é a decisão que o `core/state` já tinha
|
|
584
|
+
* escrito ao lado do `setModoCegoValue` — «o setter faz três coisas e só três: grava, persiste, avisa. Os
|
|
585
|
+
* efeitos … são reação, e quem reage assina o evento».
|
|
586
|
+
*
|
|
587
|
+
* Sem esta assinatura, um jogo que NÃO injecta o seu próprio `setModoCego` liga o modo cego pelo ícone da
|
|
588
|
+
* barra e o botão `#opt-modocego` deste painel continua a dizer «Desligado», com `aria-pressed=false` — o
|
|
589
|
+
* controlo a mentir o estado para o leitor de tela. É o gémeo exacto do defeito do `reflectTTS` que já está
|
|
590
|
+
* registado no `ui/pause-icons`, e não vale a pena descobri-lo uma terceira vez.
|
|
591
|
+
*
|
|
592
|
+
* ⚠️ É seguro para quem JÁ reflecte a partir do seu próprio setter: reflectir é idempotente — relê o estado
|
|
593
|
+
* e reescreve o botão. Um anúncio duplicado seria outra história, e por isso a assinatura NÃO anuncia: o
|
|
594
|
+
* ícone da barra já diz `sr.icon.blindOn`/`Off` por si.
|
|
595
|
+
*/
|
|
596
|
+
state.on('modoCego', () => { reflectModoCego(); });
|
|
557
597
|
return { renderAudio, reflectModoCego, reflectTts };
|
|
558
598
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
2
|
import type { DomQuery } from '../core/dom-query.js';
|
|
3
3
|
import type { KeyScheme } from '../core/entity.js';
|
|
4
|
+
import { type Action } from '../core/actions.js';
|
|
4
5
|
import type { KBDefaults } from '../input/keyboard.js';
|
|
5
6
|
import type { KeydownEventLike } from '../input/keydown.js';
|
|
6
7
|
/** Minimal DOM-selector shape (matches ui/dom.ts's `$`). */
|
|
@@ -29,7 +30,7 @@ export interface SettingsControlsCtx {
|
|
|
29
30
|
* remapear um botão que não faz nada.
|
|
30
31
|
*/
|
|
31
32
|
acoesDoJogo: () => readonly {
|
|
32
|
-
readonly acao:
|
|
33
|
+
readonly acao: Action;
|
|
33
34
|
readonly rotulo: string;
|
|
34
35
|
}[];
|
|
35
36
|
/** Screen-reader "polite" announcement (core/a11y-sr's srSay), injected. */
|
|
@@ -45,12 +46,37 @@ export interface SettingsControlsCtx {
|
|
|
45
46
|
/** Shared helper (game.js): the scheme for a given player index, given `kb`/numPlayers. Not owned by this panel —
|
|
46
47
|
* other systems (gamepad binding, HUD) call the same game.js function. */
|
|
47
48
|
kbFor: (playerIndex: number) => KeyScheme;
|
|
49
|
+
/**
|
|
50
|
+
* O esquema DE FÁBRICA deste assento (ADR-0029) — o que ele teria se ninguém tivesse remapeado nada.
|
|
51
|
+
*
|
|
52
|
+
* ⚠️ INJECTADO PELA MESMA RAZÃO QUE O `kbFor` LOGO ACIMA: o mapeamento «quantos jogadores → que balde»
|
|
53
|
+
* (`solo`/`p2`/`p3`/`p4`) é do consumidor, e uma segunda cópia dessa regra dentro da engine divergiria da
|
|
54
|
+
* primeira no dia em que um dos dois mudasse.
|
|
55
|
+
*
|
|
56
|
+
* 🎯 E NÃO SE OBTÉM CHAMANDO `store.resetKB()`, embora ele devolva exactamente a configuração de fábrica:
|
|
57
|
+
* o `input/keyboard.resetKB` faz `store.remove(CKEY)` ANTES de devolver a cópia. Usá-lo como leitor
|
|
58
|
+
* apagaria o remapeamento da criança a cada render, e o estrago só apareceria no arranque seguinte.
|
|
59
|
+
*/
|
|
60
|
+
kbPadraoFor: (playerIndex: number) => KeyScheme;
|
|
48
61
|
/** Shared: current player count (core/state.ts's numPlayers, read live via game.js). */
|
|
49
62
|
getNumPlayers: () => number;
|
|
50
63
|
/** Shared: propagates `kb` -> the live control aliases (game.js's applyControls). Called after remap/reset. */
|
|
51
64
|
applyControls: () => void;
|
|
52
65
|
/** Shared: propagates `kb` -> each player's `p.ctrl` (game.js's assignControls). Called after remap/reset. */
|
|
53
66
|
assignControls: () => void;
|
|
67
|
+
/**
|
|
68
|
+
* Move a prosa das linhas para o rodapé (`ui/settings-panel` → `fillExplain`). Chamado a CADA render.
|
|
69
|
+
*
|
|
70
|
+
* ⚠️ NÃO É OPCIONAL POR ELEGÂNCIA: `fillExplain` roda uma vez quando o overlay é frontalizado e move o
|
|
71
|
+
* `.opt-hint` de dentro de cada linha para o rodapé. Este painel RECONSTRÓI as linhas, e as linhas novas
|
|
72
|
+
* voltam com a prosa lá dentro — então a explicação aparece duas vezes, no rodapé e sob o rótulo, a
|
|
73
|
+
* partir do primeiro clique. O `CLAUDE.md` §4 regista exatamente isto, e a issue #109 já o consertou
|
|
74
|
+
* uma vez noutros painéis.
|
|
75
|
+
*
|
|
76
|
+
* Opcional na assinatura porque um consumidor pode montar o painel sem a casca (um teste, o segundo
|
|
77
|
+
* consumidor): sem casca não há rodapé para duplicar.
|
|
78
|
+
*/
|
|
79
|
+
fillExplain?: (card: HTMLElement | null) => void;
|
|
54
80
|
}
|
|
55
81
|
export interface SettingsControlsApi {
|
|
56
82
|
/** Re-renders #ctrl-list for the given player index and (re)wires its "Alterar" buttons. Idempotent. */
|
|
@@ -70,12 +96,27 @@ export interface SettingsControlsApi {
|
|
|
70
96
|
handleCaptureKeydown: (e: KeydownEventLike) => boolean;
|
|
71
97
|
}
|
|
72
98
|
/**
|
|
73
|
-
*
|
|
74
|
-
*
|
|
99
|
+
* As oito posições do jogo de plataforma, ligadas às chaves de i18n das palavras DELE.
|
|
100
|
+
*
|
|
101
|
+
* ⚠️ ELA TEM CONSUMIDOR, e eu já disse aqui que não tinha. A afirmação anterior — «sem um único consumidor,
|
|
102
|
+
* nem aqui nem no `game-platformer`» — vinha de uma varredura com um padrão que **excluía o `main.ts`** do
|
|
103
|
+
* cartucho, por ele estar directamente em `app/js/` e o glob exigir um subdirectório. Medido de novo com
|
|
104
|
+
* `git grep`: `game-platformer/app/js/main.ts` importa-a e usa-a na TELA DE AJUDA do menu de pausa (a linha
|
|
105
|
+
* que lista posição ↔ tecla). O `openHelp()` que o cabeçalho original citava não morreu — mudou de
|
|
106
|
+
* repositório com o cartucho (#111) e continua a ler daqui.
|
|
75
107
|
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
108
|
+
* ⚠️ E ELA CONTINUA A SER A CAUSA DA #125, o que é diferente de estar morta. O defeito era o `aria-label` da
|
|
109
|
+
* tela de remapeamento ser montado a partir dela: oito posições contra as catorze do vocabulário, e as
|
|
110
|
+
* palavras de UM jogo dentro do motor. Esse uso saiu. O que resta é um consumidor para quem a tabela está
|
|
111
|
+
* certa — porque ele É o jogo de plataforma.
|
|
112
|
+
*
|
|
113
|
+
* ⚠️ REMOVÊ-LA NÃO É LIMPEZA, É MIGRAÇÃO. O cartucho já tem `acoesDoJogo()` (`main.ts:122`), derivado do
|
|
114
|
+
* preset dele; a tela de ajuda passar a usá-lo é edição de lá, e só depois disso é que isto pode sair daqui.
|
|
115
|
+
* Enquanto não sair, quem escrever código NOVO na engine pede a palavra ao jogo por `ctx.acoesDoJogo()` — a
|
|
116
|
+
* engine sabe que a posição existe, só o jogo sabe como ela se chama (ADR-0086).
|
|
117
|
+
*
|
|
118
|
+
* Guarda CHAVES e não texto porque uma `const` de módulo é avaliada uma vez no import, e o `dict` do
|
|
119
|
+
* `core/i18n` é um `let` que o `setLocale` reatribui — texto capturado aqui congelaria o idioma no boot.
|
|
79
120
|
*/
|
|
80
121
|
export declare const ACT_LABEL: Record<string, string>;
|
|
81
122
|
/**
|
|
@@ -97,4 +138,23 @@ export declare function keyName(code: string): string;
|
|
|
97
138
|
* index) so a scheme with many bound keys doesn't cost a full re-scan per lookup.
|
|
98
139
|
*/
|
|
99
140
|
export declare function keyUsedByOther(code: string, mapRef: KeyScheme, schemes: readonly KeyScheme[]): number;
|
|
141
|
+
/**
|
|
142
|
+
* Qual OUTRA ação DO MESMO esquema já tem `code` — ou `null` se nenhuma.
|
|
143
|
+
*
|
|
144
|
+
* ⚠️ O IRMÃO QUE FALTAVA AO `keyUsedByOther`, E A FALTA ERA INVISÍVEL NUM JOGO DE UM JOGADOR (#126). Aquele
|
|
145
|
+
* exclui o esquema em edição **por referência**; com um jogador só, `schemesFor()` devolve exatamente esse
|
|
146
|
+
* esquema, então a guarda varre uma lista vazia e **nunca pode disparar**. A criança que põe `W` numa ação
|
|
147
|
+
* nova continua com `W` na antiga, e passa o jogo inteiro com as duas a disparar juntas.
|
|
148
|
+
*
|
|
149
|
+
* ⚠️ E O DEFEITO É O PIOR FEITIO POSSÍVEL, escrito no cabeçalho do `input/default-bindings` desde sempre:
|
|
150
|
+
* «as duas ações disparam juntas, e a criança vê uma ação dupla intermitente que ninguém consegue reproduzir
|
|
151
|
+
* de propósito». Numa tela que ela abriu **porque** não conseguia usar os controles padrão.
|
|
152
|
+
*
|
|
153
|
+
* ⚠️ A guarda entre JOGADORES não estava partida — estava inalcançável. Medido na auditoria: com dois
|
|
154
|
+
* assentos ela funciona e recusa certo. O que faltava era a verificação dentro do mesmo esquema.
|
|
155
|
+
*
|
|
156
|
+
* Devolve a AÇÃO e não um booleano, porque o anúncio tem de dizer qual — «essa tecla já está em uso» manda a
|
|
157
|
+
* criança procurar o que a função já sabe.
|
|
158
|
+
*/
|
|
159
|
+
export declare function acaoQueJaTem(code: string, mapRef: KeyScheme, exceto: Action): Action | null;
|
|
100
160
|
export declare function initSettingsControls(ctx: SettingsControlsCtx): SettingsControlsApi;
|
|
@@ -6,19 +6,41 @@
|
|
|
6
6
|
// shared per-player helpers (`kbFor`/`getNumPlayers`/`applyControls`/`assignControls`) that game.js also uses
|
|
7
7
|
// elsewhere (gamepad binding, HUD, other settings panels) and therefore stay there, injected. Overlay open/close
|
|
8
8
|
// plumbing (#options hidden toggle, focus management, Escape-closes-dialog) and the pad-button-design select are
|
|
9
|
-
// shared/unrelated infra and stay in game.js. `openHelp()` (pause-menu help screen) reuses
|
|
10
|
-
//
|
|
9
|
+
// shared/unrelated infra and stay in game.js. `openHelp()` (pause-menu help screen) reuses `keyName`, which is
|
|
10
|
+
// exported here instead of duplicated.
|
|
11
|
+
//
|
|
12
|
+
// ⚠️ ELE TAMBÉM LIA O `ACT_LABEL`, E DEIXOU DE LER EM 2026-09-07. A tela de ajuda do cartucho passou a montar
|
|
13
|
+
// as linhas do preset dele (`acoesDoJogo`), que é quem sabe quantas posições este jogo usa e como elas se
|
|
14
|
+
// chamam. Com isso o `ACT_LABEL` ficou sem UM leitor sequer — conferido com `git grep` nos dois repositórios,
|
|
15
|
+
// e o que resta dele são comentários e a própria declaração. Ver `docs/6-DevOps-SRE/Breaking-Changes.md`.
|
|
11
16
|
import { t } from '../core/i18n.js';
|
|
17
|
+
import { ACTIONS, isAction } from '../core/actions.js';
|
|
18
|
+
import { markChanged } from './changed-mark.js';
|
|
12
19
|
// ---------------------------------------------------------------------------------------------
|
|
13
20
|
// Pure logic (no `document`, testable in node)
|
|
14
21
|
// ---------------------------------------------------------------------------------------------
|
|
15
22
|
/**
|
|
16
|
-
*
|
|
17
|
-
* screen), which is why it lives here instead of being duplicated in two places.
|
|
23
|
+
* As oito posições do jogo de plataforma, ligadas às chaves de i18n das palavras DELE.
|
|
18
24
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
25
|
+
* ⚠️ ELA TEM CONSUMIDOR, e eu já disse aqui que não tinha. A afirmação anterior — «sem um único consumidor,
|
|
26
|
+
* nem aqui nem no `game-platformer`» — vinha de uma varredura com um padrão que **excluía o `main.ts`** do
|
|
27
|
+
* cartucho, por ele estar directamente em `app/js/` e o glob exigir um subdirectório. Medido de novo com
|
|
28
|
+
* `git grep`: `game-platformer/app/js/main.ts` importa-a e usa-a na TELA DE AJUDA do menu de pausa (a linha
|
|
29
|
+
* que lista posição ↔ tecla). O `openHelp()` que o cabeçalho original citava não morreu — mudou de
|
|
30
|
+
* repositório com o cartucho (#111) e continua a ler daqui.
|
|
31
|
+
*
|
|
32
|
+
* ⚠️ E ELA CONTINUA A SER A CAUSA DA #125, o que é diferente de estar morta. O defeito era o `aria-label` da
|
|
33
|
+
* tela de remapeamento ser montado a partir dela: oito posições contra as catorze do vocabulário, e as
|
|
34
|
+
* palavras de UM jogo dentro do motor. Esse uso saiu. O que resta é um consumidor para quem a tabela está
|
|
35
|
+
* certa — porque ele É o jogo de plataforma.
|
|
36
|
+
*
|
|
37
|
+
* ⚠️ REMOVÊ-LA NÃO É LIMPEZA, É MIGRAÇÃO. O cartucho já tem `acoesDoJogo()` (`main.ts:122`), derivado do
|
|
38
|
+
* preset dele; a tela de ajuda passar a usá-lo é edição de lá, e só depois disso é que isto pode sair daqui.
|
|
39
|
+
* Enquanto não sair, quem escrever código NOVO na engine pede a palavra ao jogo por `ctx.acoesDoJogo()` — a
|
|
40
|
+
* engine sabe que a posição existe, só o jogo sabe como ela se chama (ADR-0086).
|
|
41
|
+
*
|
|
42
|
+
* Guarda CHAVES e não texto porque uma `const` de módulo é avaliada uma vez no import, e o `dict` do
|
|
43
|
+
* `core/i18n` é um `let` que o `setLocale` reatribui — texto capturado aqui congelaria o idioma no boot.
|
|
22
44
|
*/
|
|
23
45
|
export const ACT_LABEL = {
|
|
24
46
|
left: 'act.left', right: 'act.right', up: 'act.up', down: 'act.down',
|
|
@@ -54,13 +76,40 @@ export function keyUsedByOther(code, mapRef, schemes) {
|
|
|
54
76
|
schemes.forEach((m, i) => {
|
|
55
77
|
if (m === mapRef)
|
|
56
78
|
return;
|
|
57
|
-
for (const a
|
|
79
|
+
for (const a of ACTIONS)
|
|
58
80
|
for (const c of m[a] || [])
|
|
59
81
|
if (!owners.has(c))
|
|
60
82
|
owners.set(c, i);
|
|
61
83
|
});
|
|
62
84
|
return owners.get(code) ?? -1;
|
|
63
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Qual OUTRA ação DO MESMO esquema já tem `code` — ou `null` se nenhuma.
|
|
88
|
+
*
|
|
89
|
+
* ⚠️ O IRMÃO QUE FALTAVA AO `keyUsedByOther`, E A FALTA ERA INVISÍVEL NUM JOGO DE UM JOGADOR (#126). Aquele
|
|
90
|
+
* exclui o esquema em edição **por referência**; com um jogador só, `schemesFor()` devolve exatamente esse
|
|
91
|
+
* esquema, então a guarda varre uma lista vazia e **nunca pode disparar**. A criança que põe `W` numa ação
|
|
92
|
+
* nova continua com `W` na antiga, e passa o jogo inteiro com as duas a disparar juntas.
|
|
93
|
+
*
|
|
94
|
+
* ⚠️ E O DEFEITO É O PIOR FEITIO POSSÍVEL, escrito no cabeçalho do `input/default-bindings` desde sempre:
|
|
95
|
+
* «as duas ações disparam juntas, e a criança vê uma ação dupla intermitente que ninguém consegue reproduzir
|
|
96
|
+
* de propósito». Numa tela que ela abriu **porque** não conseguia usar os controles padrão.
|
|
97
|
+
*
|
|
98
|
+
* ⚠️ A guarda entre JOGADORES não estava partida — estava inalcançável. Medido na auditoria: com dois
|
|
99
|
+
* assentos ela funciona e recusa certo. O que faltava era a verificação dentro do mesmo esquema.
|
|
100
|
+
*
|
|
101
|
+
* Devolve a AÇÃO e não um booleano, porque o anúncio tem de dizer qual — «essa tecla já está em uso» manda a
|
|
102
|
+
* criança procurar o que a função já sabe.
|
|
103
|
+
*/
|
|
104
|
+
export function acaoQueJaTem(code, mapRef, exceto) {
|
|
105
|
+
for (const a of ACTIONS) {
|
|
106
|
+
if (a === exceto)
|
|
107
|
+
continue;
|
|
108
|
+
if ((mapRef[a] || []).includes(code))
|
|
109
|
+
return a;
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
64
113
|
export function initSettingsControls(ctx) {
|
|
65
114
|
let kb = ctx.kb;
|
|
66
115
|
let capture = null;
|
|
@@ -69,6 +118,27 @@ export function initSettingsControls(ctx) {
|
|
|
69
118
|
const n = ctx.getNumPlayers();
|
|
70
119
|
return Array.from({ length: n }, (_, i) => ctx.kbFor(i));
|
|
71
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* COMO ESTE JOGO CHAMA esta posição, ou `null` se ele não a nomeia. Um sítio só, porque três pontos
|
|
123
|
+
* precisavam dela e cada um a ia buscar por sua conta — e um deles ia buscá-la à tabela errada (#125).
|
|
124
|
+
*
|
|
125
|
+
* 🔴 O RECUO ERA O ID ABSTRATO (`?? a`), DEFENDIDO AQUI COM «`action3` é feio, mas é verdade». Era um
|
|
126
|
+
* defeito, e o ADR-0074 chama-lhe isso em tantas palavras: «o nome que a CRIANÇA lê e ouve — na tela de
|
|
127
|
+
* remapeamento, na bolha de toque, no anúncio — é sempre a palavra do jogo, nunca `action1`. Um nome
|
|
128
|
+
* abstracto que chega a uma pessoa é um defeito.»
|
|
129
|
+
*
|
|
130
|
+
* ⚠️ E ESTAVA A UM TOQUE DE DISTÂNCIA, com o esquema PADRÃO desta engine: ele liga OITO posições e um quiz
|
|
131
|
+
* nomeia três. A criança escolhia «Confirmar», carregava numa tecla que o padrão tinha em `action2`, e o
|
|
132
|
+
* leitor de tela dizia «Essa tecla já é de action2» — precisamente a ela, que é quem não tem outro canal.
|
|
133
|
+
*
|
|
134
|
+
* 📌 A TERCEIRA SAÍDA JÁ ESTAVA DECIDIDA UM MÓDULO ABAIXO, e este ficheiro tinha decidido outra:
|
|
135
|
+
* `core/actions.labellerFrom` devolve `null` «e quem chama decide — uma ausência vira menos um passo, nunca
|
|
136
|
+
* um passo mudo». Duas respostas à mesma pergunta no mesmo repositório é o defeito que o `DomQuery` já
|
|
137
|
+
* custou dezasseis vezes; agora são uma.
|
|
138
|
+
*/
|
|
139
|
+
function palavraDaAcao(a) {
|
|
140
|
+
return ctx.acoesDoJogo().find((x) => x.acao === a)?.rotulo ?? null;
|
|
141
|
+
}
|
|
72
142
|
function render(selPlayer) {
|
|
73
143
|
const el = ctx.$('#ctrl-list');
|
|
74
144
|
if (!el)
|
|
@@ -77,10 +147,27 @@ export function initSettingsControls(ctx) {
|
|
|
77
147
|
const player = selPlayer >= n ? 0 : selPlayer;
|
|
78
148
|
lastPlayer = player;
|
|
79
149
|
// E3: sem abas de outros jogadores — você edita só o seu controle; só o hint muda com o modo.
|
|
150
|
+
//
|
|
151
|
+
// ⚠️ ESTA FRASE ESTAVA EM PORTUGUÊS CRU DENTRO DO MOTOR (#125), inteira, com o `<strong>` e o plural à
|
|
152
|
+
// mão. Vai pelo `t()` agora — e o realce sobrevive porque o molde é PARTIDO no marcador `{modo}` antes
|
|
153
|
+
// da substituição, em vez de o dicionário carregar markup (que o gate `i18n-sem-markup` proíbe, e com
|
|
154
|
+
// razão: string de dicionário que vira markup é a porta por onde uma tradução passa a ser código).
|
|
80
155
|
const tabs = ctx.$('#ctrl-players');
|
|
81
156
|
if (tabs) {
|
|
82
157
|
tabs.hidden = false;
|
|
83
|
-
|
|
158
|
+
// O esqueleto por `innerHTML` — ele não tem dado nenhum de fora; o TEXTO entra por `textContent`, que
|
|
159
|
+
// é o mesmo idioma que o `.ctrl-nome` abaixo já usa.
|
|
160
|
+
tabs.innerHTML = '<span class="opt-hint" style="width:100%;margin:0">'
|
|
161
|
+
+ '<span data-modo="pre"></span><strong data-modo="v"></strong><span data-modo="pos"></span></span>';
|
|
162
|
+
const [antes, depois] = t('ctrl.editingYours').split('{modo}');
|
|
163
|
+
const posto = (sel, txt) => {
|
|
164
|
+
const el2 = tabs.querySelector(sel);
|
|
165
|
+
if (el2)
|
|
166
|
+
el2.textContent = txt;
|
|
167
|
+
};
|
|
168
|
+
posto('[data-modo="pre"]', antes ?? '');
|
|
169
|
+
posto('[data-modo="v"]', n === 1 ? t('ctrl.mode.one') : t('ctrl.mode.many', { n }));
|
|
170
|
+
posto('[data-modo="pos"]', depois ?? '');
|
|
84
171
|
}
|
|
85
172
|
const map = ctx.kbFor(player);
|
|
86
173
|
// ⚠️ O `rotulo` SAIU DO MARKUP (issue #106) e entra logo abaixo por `textContent`. Ele é a PALAVRA do
|
|
@@ -88,24 +175,83 @@ export function initSettingsControls(ctx) {
|
|
|
88
175
|
// `data-act="${a}"` fica, e a diferença é a razão: `a` é o nome ABSTRATO da ação, que a engine enumera em
|
|
89
176
|
// `core/actions`. É separação que o ADR-0086 fez, e é ela que torna um dos dois seguro e o outro não.
|
|
90
177
|
el.innerHTML = ctx.acoesDoJogo().map(({ acao: a }) => `<div class="ctrl-row"><span><b class="ctrl-nome"></b>: ${(map[a] || []).map(keyName).map((k) => `<kbd>${k}</kbd>`).join(' ')}</span>` +
|
|
91
|
-
`<button class="mode-btn" data-act="${a}" type="button"
|
|
92
|
-
// As palavras do jogo, por
|
|
178
|
+
`<button class="mode-btn" data-act="${a}" type="button">${t('ctrl.change')}</button></div>`).join('');
|
|
179
|
+
// As palavras do jogo, por API do DOM — que escapa por construção. A ordem casa porque é a mesma lista.
|
|
180
|
+
//
|
|
181
|
+
// ⚠️ O `aria-label` DESCEU PARA CÁ, E É A CORREÇÃO DA #125. Ele era montado no template acima a partir do
|
|
182
|
+
// `ACT_LABEL`, que ficou a ser a tabela do JOGO DE PLATAFORMA quando a #106 mudou o rótulo visível para o
|
|
183
|
+
// `acoesDoJogo()`. Medido no `game-soccer`: **«Alterar tecla de undefined do Jogador 1» em seis de doze
|
|
184
|
+
// botões**, enquanto uma criança que vê lia «Conter» na mesma linha. E um `aria-label` SOBREPÕE-SE ao
|
|
185
|
+
// texto visível, então quem depende do leitor de tela ouvia a palavra errada nos outros seis — que é pior
|
|
186
|
+
// do que não ter `aria-label` nenhum, e invisível de dentro da engine, porque a plataforma é o único
|
|
187
|
+
// consumidor para o qual a tabela está certa.
|
|
188
|
+
//
|
|
189
|
+
// ⚠️ E DESCEU POR `setAttribute` E NÃO PARA O TEMPLATE, de propósito: o `rotulo` é TEXTO DO JOGO. Metê-lo
|
|
190
|
+
// num `aria-label="…"` dentro de um template literal seria interpolar texto de fora em markup — o mesmo
|
|
191
|
+
// motivo pelo qual o `.ctrl-nome` já entrava por `textContent`.
|
|
93
192
|
{
|
|
94
|
-
const
|
|
193
|
+
const linhas = el.querySelectorAll('.ctrl-row');
|
|
95
194
|
const palavras = ctx.acoesDoJogo();
|
|
96
|
-
for (let i = 0; i <
|
|
97
|
-
|
|
195
|
+
for (let i = 0; i < linhas.length && i < palavras.length; i++) {
|
|
196
|
+
const nome = linhas[i].querySelector('.ctrl-nome');
|
|
197
|
+
if (nome)
|
|
198
|
+
nome.textContent = palavras[i].rotulo;
|
|
199
|
+
const botao = linhas[i].querySelector('button[data-act]');
|
|
200
|
+
if (botao)
|
|
201
|
+
botao.setAttribute('aria-label', t('ctrl.changeKeyAria', { acao: palavras[i].rotulo, n: player + 1 }));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* A MARCA DE «SAIU DO PADRÃO» (ADR-0029), e este era o ÚLTIMO menu sem ela.
|
|
206
|
+
*
|
|
207
|
+
* ⚠️ E É O MENU ONDE ELA MAIS FALTAVA, porque é o único cuja razão de existir é mexer: uma criança que
|
|
208
|
+
* remapeou as teclas percorria a lista, ouvia os nomes das acções, e nada lhe dizia onde ela própria tinha
|
|
209
|
+
* alterado. Os três canais do ADR-0029 — cor, forma (anéis) e NOME — passam a valer aqui.
|
|
210
|
+
*
|
|
211
|
+
* ⚠️ O PADRÃO VEM DO CONSUMIDOR (`ctx.kbPadraoFor`) e não de uma tabela lida aqui, pela mesma razão que o
|
|
212
|
+
* `kbFor` é injectado: o mapeamento «quantos jogadores → que balde» (`p2`/`p3`/`p4`) é dele, e uma segunda
|
|
213
|
+
* cópia dessa regra divergiria da primeira.
|
|
214
|
+
*
|
|
215
|
+
* 🎯 E NÃO SE USA O `resetKB` PARA LER O PADRÃO, embora ele devolva exactamente a configuração de fábrica:
|
|
216
|
+
* ele é DESTRUTIVO — `input/keyboard.resetKB` faz `store.remove(CKEY)` antes de devolver a cópia. Chamá-lo
|
|
217
|
+
* a cada render apagaria o remapeamento da criança, e o estrago só apareceria no arranque seguinte.
|
|
218
|
+
*
|
|
219
|
+
* 📌 COMPARA-SE A LISTA DE CÓDIGOS, não a identidade do objecto: reatribuir a MESMA tecla não é uma
|
|
220
|
+
* mudança, e uma criança que experimenta e volta atrás não pode ficar com a marca acesa para sempre.
|
|
221
|
+
*/
|
|
222
|
+
{
|
|
223
|
+
const padrao = ctx.kbPadraoFor(player);
|
|
224
|
+
const atual = ctx.kbFor(player);
|
|
225
|
+
const mesmas = (a, b) => (a ?? []).length === (b ?? []).length && (a ?? []).every((k, i) => k === (b ?? [])[i]);
|
|
226
|
+
for (const linha of el.querySelectorAll('.ctrl-row')) {
|
|
227
|
+
const act = linha.querySelector('button[data-act]')?.dataset.act;
|
|
228
|
+
if (!act || !isAction(act))
|
|
229
|
+
continue;
|
|
230
|
+
markChanged(linha, !mesmas(atual[act], padrao[act]));
|
|
231
|
+
}
|
|
98
232
|
}
|
|
99
233
|
el.querySelectorAll('button[data-act]').forEach((b) => {
|
|
100
234
|
b.addEventListener('click', () => {
|
|
235
|
+
// ⚠️ `isAction` E NÃO SÓ `if (!act)`: o valor vem de um atributo do DOM, e desde a #118 o esquema só
|
|
236
|
+
// aceita as quatorze posições. Uma captura iniciada sobre uma posição inventada gravaria uma tecla
|
|
237
|
+
// numa chave que transporte nenhum lê — a criança carregaria a tecla nova e nada aconteceria.
|
|
101
238
|
const act = b.dataset.act;
|
|
102
|
-
if (!act)
|
|
239
|
+
if (!act || !isAction(act))
|
|
240
|
+
return;
|
|
241
|
+
// ⚠️ SEM PALAVRA, NÃO SE PERGUNTA — a regra do `labellerFrom`, aplicada onde ela é visível: «se o jogo
|
|
242
|
+
// não a usa, não há o que mapear; uma ausência vira menos um passo, nunca um passo mudo». As linhas
|
|
243
|
+
// vêm todas de `acoesDoJogo()`, logo isto não acontece hoje — e é essa garantia que fica escrita em
|
|
244
|
+
// vez de assumida, porque quem a partir amanhã acorda um anúncio sem sujeito.
|
|
245
|
+
const palavra = palavraDaAcao(act);
|
|
246
|
+
if (!palavra)
|
|
103
247
|
return;
|
|
104
248
|
capture = { action: act, mapRef: map, player };
|
|
105
|
-
b.textContent = '
|
|
106
|
-
ctx.srAlert(t('sr.ctrl.pressNewKey', { acao:
|
|
249
|
+
b.textContent = t('ctrl.pressing'); // estava cravado em português dentro do motor (#125)
|
|
250
|
+
ctx.srAlert(t('sr.ctrl.pressNewKey', { acao: palavra, n: player + 1 }));
|
|
107
251
|
});
|
|
108
252
|
});
|
|
253
|
+
// A prosa volta para o rodapé depois de as linhas serem reconstruídas (CLAUDE.md §4, #109).
|
|
254
|
+
ctx.fillExplain?.(ctx.$('#options .overlay__card'));
|
|
109
255
|
}
|
|
110
256
|
function isCapturing() {
|
|
111
257
|
return capture !== null;
|
|
@@ -128,6 +274,22 @@ export function initSettingsControls(ctx) {
|
|
|
128
274
|
e.preventDefault();
|
|
129
275
|
return true; // não associa: segue capturando
|
|
130
276
|
}
|
|
277
|
+
// A MESMA guarda, dentro do próprio esquema (#126). Recusa em vez de MOVER, e a escolha tem motivo:
|
|
278
|
+
// mover deixaria a ação antiga com lista vazia — que o `bindingProblems` classifica como problema, e que
|
|
279
|
+
// a criança descobriria no meio do jogo, sem anúncio, com uma ação que deixou de existir. Recusar custa
|
|
280
|
+
// dois passos (soltar a antiga, prender a nova) e não perde nada pelo caminho.
|
|
281
|
+
const aqui = acaoQueJaTem(e.code, capture.mapRef, capture.action);
|
|
282
|
+
if (aqui) {
|
|
283
|
+
// ⚠️ `aqui` VEM DO ESQUEMA, e o esquema liga posições que o jogo pode não nomear — é por aqui que o id
|
|
284
|
+
// abstracto chegava a uma criança. Sem palavra, a frase diz a verdade que INTERESSA («a tecla está
|
|
285
|
+
// ocupada aqui») em vez do nome interno: calar seria o defeito gémeo, e dizer `action2` era o defeito.
|
|
286
|
+
const palavra = palavraDaAcao(aqui);
|
|
287
|
+
ctx.srAlert(palavra
|
|
288
|
+
? t('sr.ctrl.keyTakenHere', { acao: palavra })
|
|
289
|
+
: t('sr.ctrl.keyTakenHereUnnamed'));
|
|
290
|
+
e.preventDefault();
|
|
291
|
+
return true; // não associa: segue capturando
|
|
292
|
+
}
|
|
131
293
|
capture.mapRef[capture.action] = [e.code];
|
|
132
294
|
ctx.store.saveKB(kb);
|
|
133
295
|
ctx.applyControls();
|
|
@@ -31,6 +31,21 @@ export interface EmpathySettingsCtx {
|
|
|
31
31
|
reflectVizButtons(): void;
|
|
32
32
|
/** Brings an overlay to front + fills its footer explanations; shared by every Sensibilidade panel. */
|
|
33
33
|
frontOverlay(el: HTMLElement | null): void;
|
|
34
|
+
/**
|
|
35
|
+
* Move a prosa das linhas para o rodapé (`ui/settings-panel` → `fillExplain`). Chamado a CADA render.
|
|
36
|
+
*
|
|
37
|
+
* ⚠️ E AQUI A RECONSTRUÇÃO NÃO SE VÊ NO FICHEIRO, que foi o que atrasou este conserto. Este painel não tem
|
|
38
|
+
* um `innerHTML` sequer: quem reconstrói a lista é o `renderVizGroup` injetado, cuja implementação
|
|
39
|
+
* (`render/viz-setters`) faz `el.innerHTML = vizGroupHtml(…)` e devolve `.ctrl-row`s NOVAS, com o
|
|
40
|
+
* `.opt-hint` outra vez lá dentro e sem o `data-explain-done` que torna o `fillExplain` idempotente.
|
|
41
|
+
*
|
|
42
|
+
* Isto acontece a cada clique numa simulação e a cada uso do botão de perda auditiva — que é o pior caso
|
|
43
|
+
* possível, porque a criança que acabou de ligar "Simular cegueira total" está com a tela preta e depende
|
|
44
|
+
* do rodapé `aria-live` para saber onde está.
|
|
45
|
+
*
|
|
46
|
+
* Opcional (`?.`) como nos irmãos: um consumidor que não injete continua a desenhar o painel.
|
|
47
|
+
*/
|
|
48
|
+
fillExplain?: (card: HTMLElement | null) => void;
|
|
34
49
|
/** Devolve o foco a quem abriu o diálogo (ui/settings-panel `restoreFocus`). Injetado, e não um `#opt-*`
|
|
35
50
|
* fixo: o id que este módulo focava não existe no documento, então fechar deixava o foco no `<body>`. */
|
|
36
51
|
restoreFocus?: (id: string) => boolean;
|
|
@@ -40,6 +40,8 @@ export function initSettingsEmpathy(ctx) {
|
|
|
40
40
|
}
|
|
41
41
|
ctx.reflectMotorEmpathy();
|
|
42
42
|
refreshMarks();
|
|
43
|
+
// Depois do `renderVizGroup`, e não antes: é ele quem repõe o `.opt-hint` dentro das linhas.
|
|
44
|
+
ctx.fillExplain?.(ctx.$('#empathy .overlay__card'));
|
|
43
45
|
}
|
|
44
46
|
/**
|
|
45
47
|
* A marca de "saiu do padrão" (ADR-0029). Neste menu ela quer dizer "esta simulação está LIGADA", e por
|
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
-
import type {
|
|
3
|
-
export type MotionSceneKey =
|
|
4
|
-
export type MotionCharProp =
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
/** As três chaves de Movimento Reduzido por personagem, derivadas de core/entity. Eram um
|
|
10
|
-
* `Partial<Record<MotionCharProp, boolean>>`, o que dizia a forma certa sem dizer que os campos são DO
|
|
11
|
-
* JOGADOR — um `Record` aceita qualquer objeto com essas chaves, inclusive um que não seja jogador nenhum. */
|
|
12
|
-
export type MotionPlayer = PlayerView<'rmWalk' | 'rmBreath' | 'rmFlavor'>;
|
|
13
|
-
export type MotionSceneFlags = Record<MotionSceneKey, boolean>;
|
|
2
|
+
import type { MotionSceneKey as ChaveDeCenaLeaf, MotionCharProp as PropDoPersonagemLeaf, MotionCharDef as DefDoPersonagemLeaf, MotionPlayer as JogadorDeMovimentoLeaf, MotionSceneFlags as BandeirasDeCenaLeaf } from './motion-scene.js';
|
|
3
|
+
export type MotionSceneKey = ChaveDeCenaLeaf;
|
|
4
|
+
export type MotionCharProp = PropDoPersonagemLeaf;
|
|
5
|
+
export type MotionCharDef = DefDoPersonagemLeaf;
|
|
6
|
+
export type MotionPlayer = JogadorDeMovimentoLeaf;
|
|
7
|
+
export type MotionSceneFlags = BandeirasDeCenaLeaf;
|
|
14
8
|
export interface SettingsMotionCtx {
|
|
15
9
|
/** Quantos jogadores/telas. Estado de RODADA (ADR-0038): vem da instância que a raiz possui.
|
|
16
10
|
* Era `numPlayers`, um `let` de `core/state` importado como binding vivo — e um `let` de módulo
|
|
@@ -34,15 +28,27 @@ export interface SettingsMotionCtx {
|
|
|
34
28
|
restoreFocus?: (id: string) => boolean;
|
|
35
29
|
/** Reflete on/off num botão (classe is-on + aria-pressed) — helper genérico usado por vários botões-mestre. */
|
|
36
30
|
toggleBtn: (el: HTMLElement, on: boolean) => void;
|
|
37
|
-
/** Movimento reduzido de CENA (parallax/decor/items/particles) — objeto VIVO, mutado in-place.
|
|
38
|
-
|
|
39
|
-
rm
|
|
40
|
-
|
|
41
|
-
saveRM: () => void;
|
|
31
|
+
/** Movimento reduzido de CENA (parallax/decor/items/particles) — objeto VIVO, mutado in-place. */
|
|
32
|
+
rm?: MotionSceneFlags;
|
|
33
|
+
/** Persiste `rm` (localStorage 'inclusionist.reducedmotion.v1'). */
|
|
34
|
+
saveRM?: () => void;
|
|
42
35
|
/** As 4 chaves de cena — a MESMA array que applyCalm() itera. */
|
|
43
|
-
rmKeys
|
|
36
|
+
rmKeys?: readonly MotionSceneKey[];
|
|
44
37
|
/** Os 3 alvos de movimento reduzido do PERSONAGEM — a MESMA array que applyCalm() itera. */
|
|
45
|
-
rmChar
|
|
38
|
+
rmChar?: readonly MotionCharDef[];
|
|
39
|
+
/**
|
|
40
|
+
* Move a prosa das linhas para o rodapé (`ui/settings-panel` → `fillExplain`). Chamado a CADA render.
|
|
41
|
+
*
|
|
42
|
+
* ⚠️ NÃO É OPCIONAL POR ELEGÂNCIA: `fillExplain` roda uma vez quando o overlay é frontalizado e move o
|
|
43
|
+
* `.opt-hint` de dentro de cada linha para o rodapé. Este painel RECONSTRÓI as linhas, e as linhas novas
|
|
44
|
+
* voltam com a prosa lá dentro — então a explicação aparece duas vezes, no rodapé e sob o rótulo, a
|
|
45
|
+
* partir do primeiro clique. O `CLAUDE.md` §4 regista exatamente isto, e a issue #109 já o consertou
|
|
46
|
+
* uma vez noutros painéis.
|
|
47
|
+
*
|
|
48
|
+
* Opcional na assinatura porque um consumidor pode montar o painel sem a casca (um teste, o segundo
|
|
49
|
+
* consumidor): sem casca não há rodapé para duplicar.
|
|
50
|
+
*/
|
|
51
|
+
fillExplain?: (card: HTMLElement | null) => void;
|
|
46
52
|
}
|
|
47
53
|
/**
|
|
48
54
|
* Alvo → CHAVE i18n do rótulo. CHAVES, e não texto, pelo motivo de sempre: uma tabela de `const` com texto
|