@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.
Files changed (116) hide show
  1. package/README.md +5 -3
  2. package/app/css/style.css +22 -5
  3. package/app/public/vendor/fonts/fondamento-400-ext.woff2 +0 -0
  4. package/app/public/vendor/fonts/fondamento-400.woff2 +0 -0
  5. package/app/public/vendor/fonts/opendyslexic-400.woff2 +0 -0
  6. package/app/public/vendor/fonts/pressstart-400.woff2 +0 -0
  7. package/app/public/vendor/fonts.css +43 -2
  8. package/dist-pkg/boot/create-game.d.ts +76 -2
  9. package/dist-pkg/boot/create-game.js +271 -13
  10. package/dist-pkg/core/constants.d.ts +0 -28
  11. package/dist-pkg/core/constants.js +34 -17
  12. package/dist-pkg/core/contract.d.ts +39 -0
  13. package/dist-pkg/core/contract.js +36 -0
  14. package/dist-pkg/core/entity.d.ts +47 -4
  15. package/dist-pkg/core/layers.d.ts +18 -0
  16. package/dist-pkg/core/layers.js +18 -0
  17. package/dist-pkg/core/rng.js +14 -4
  18. package/dist-pkg/core/route.d.ts +42 -0
  19. package/dist-pkg/core/route.js +158 -0
  20. package/dist-pkg/core/state.d.ts +10 -1
  21. package/dist-pkg/core/state.js +13 -0
  22. package/dist-pkg/educational/adaptive-engine.d.ts +65 -0
  23. package/dist-pkg/educational/adaptive-engine.js +117 -0
  24. package/dist-pkg/educational/segment-bar.d.ts +96 -0
  25. package/dist-pkg/educational/segment-bar.js +89 -0
  26. package/dist-pkg/i18n/en.js +33 -0
  27. package/dist-pkg/i18n/es.js +33 -0
  28. package/dist-pkg/i18n/pt.js +46 -0
  29. package/dist-pkg/input/default-bindings.d.ts +40 -0
  30. package/dist-pkg/input/default-bindings.js +143 -9
  31. package/dist-pkg/input/gamepad.d.ts +18 -11
  32. package/dist-pkg/input/gamepad.js +75 -9
  33. package/dist-pkg/input/keyboard-runtime.d.ts +6 -8
  34. package/dist-pkg/input/keyboard-runtime.js +24 -5
  35. package/dist-pkg/input/keyboard.d.ts +10 -0
  36. package/dist-pkg/input/keyboard.js +41 -11
  37. package/dist-pkg/input/keydown.d.ts +27 -2
  38. package/dist-pkg/input/keydown.js +20 -6
  39. package/dist-pkg/input/latch-scope.d.ts +70 -0
  40. package/dist-pkg/input/latch-scope.js +110 -0
  41. package/dist-pkg/input/latch-store.d.ts +45 -0
  42. package/dist-pkg/input/latch-store.js +74 -0
  43. package/dist-pkg/input/origem-sintetica.d.ts +44 -0
  44. package/dist-pkg/input/origem-sintetica.js +60 -0
  45. package/dist-pkg/input/pointer.d.ts +61 -0
  46. package/dist-pkg/input/pointer.js +71 -0
  47. package/dist-pkg/input/state.d.ts +86 -1
  48. package/dist-pkg/input/state.js +128 -1
  49. package/dist-pkg/input/touch-bindings.d.ts +11 -2
  50. package/dist-pkg/input/touch-bindings.js +9 -3
  51. package/dist-pkg/input/touch.js +9 -1
  52. package/dist-pkg/input/transporte-em-uso.d.ts +101 -0
  53. package/dist-pkg/input/transporte-em-uso.js +130 -0
  54. package/dist-pkg/input/transports.d.ts +88 -2
  55. package/dist-pkg/input/transports.js +60 -5
  56. package/dist-pkg/input/vocabulary-migration.d.ts +18 -7
  57. package/dist-pkg/platform/audio-earcons.d.ts +29 -2
  58. package/dist-pkg/platform/audio-earcons.js +10 -0
  59. package/dist-pkg/platform/audio-nav.d.ts +1 -1
  60. package/dist-pkg/platform/audio-sonar.d.ts +149 -9
  61. package/dist-pkg/platform/audio-sonar.js +232 -21
  62. package/dist-pkg/platform/guide-intensity.d.ts +40 -0
  63. package/dist-pkg/platform/guide-intensity.js +73 -0
  64. package/dist-pkg/platform/storage.d.ts +30 -0
  65. package/dist-pkg/platform/storage.js +35 -0
  66. package/dist-pkg/platform/tts.js +16 -1
  67. package/dist-pkg/platform/voice-plan.d.ts +90 -0
  68. package/dist-pkg/platform/voice-plan.js +137 -0
  69. package/dist-pkg/render/draw.d.ts +1 -1
  70. package/dist-pkg/render/draw.js +15 -5
  71. package/dist-pkg/render/viz-axes.d.ts +17 -0
  72. package/dist-pkg/render/viz-axes.js +19 -0
  73. package/dist-pkg/render/viz-setters.d.ts +34 -2
  74. package/dist-pkg/render/viz-setters.js +189 -33
  75. package/dist-pkg/render/wheelchair-sprites.d.ts +11 -3
  76. package/dist-pkg/render/wheelchair-sprites.js +10 -4
  77. package/dist-pkg/ui/dom.js +20 -2
  78. package/dist-pkg/ui/fonts.d.ts +47 -1
  79. package/dist-pkg/ui/fonts.js +47 -9
  80. package/dist-pkg/ui/latch-refusal.d.ts +32 -0
  81. package/dist-pkg/ui/latch-refusal.js +60 -0
  82. package/dist-pkg/ui/layout.d.ts +32 -0
  83. package/dist-pkg/ui/layout.js +64 -1
  84. package/dist-pkg/ui/motion-scene.d.ts +41 -0
  85. package/dist-pkg/ui/motion-scene.js +76 -0
  86. package/dist-pkg/ui/panel-shell.d.ts +53 -0
  87. package/dist-pkg/ui/panel-shell.js +103 -0
  88. package/dist-pkg/ui/pause-icons.d.ts +134 -20
  89. package/dist-pkg/ui/pause-icons.js +307 -45
  90. package/dist-pkg/ui/reach-notice.js +8 -0
  91. package/dist-pkg/ui/settings-audio.d.ts +14 -1
  92. package/dist-pkg/ui/settings-audio.js +43 -3
  93. package/dist-pkg/ui/settings-controls.d.ts +66 -6
  94. package/dist-pkg/ui/settings-controls.js +179 -17
  95. package/dist-pkg/ui/settings-empathy.d.ts +15 -0
  96. package/dist-pkg/ui/settings-empathy.js +2 -0
  97. package/dist-pkg/ui/settings-motion.d.ts +25 -19
  98. package/dist-pkg/ui/settings-motion.js +32 -19
  99. package/dist-pkg/ui/settings-motor.d.ts +81 -3
  100. package/dist-pkg/ui/settings-motor.js +118 -9
  101. package/dist-pkg/ui/settings-typo.d.ts +32 -2
  102. package/dist-pkg/ui/settings-typo.js +64 -18
  103. package/dist-pkg/ui/settings-visual.d.ts +21 -1
  104. package/dist-pkg/ui/settings-visual.js +48 -4
  105. package/dist-pkg/ui/shell.d.ts +13 -3
  106. package/dist-pkg/ui/shell.js +3 -4
  107. package/dist-pkg/ui/simulation-refusal.d.ts +32 -0
  108. package/dist-pkg/ui/simulation-refusal.js +57 -0
  109. package/dist-pkg/ui/visual-axes-panel.d.ts +48 -0
  110. package/dist-pkg/ui/visual-axes-panel.js +95 -0
  111. package/dist-pkg/ui/webcam.js +6 -1
  112. package/docs/CREDITS.md +18 -0
  113. package/docs/LICENSES.md +12 -0
  114. package/package.json +24 -4
  115. package/app/public/vendor/fonts/greatvibes-400.woff2 +0 -0
  116. package/app/public/vendor/fonts/ufcook-700.woff2 +0 -0
@@ -12,6 +12,8 @@ import { lqName as lqLabel } from '../render/lq-filter.js';
12
12
  import { HC_ROLE_KEYS, HC_ROLE_DEF } from '../render/hc-role-data.js';
13
13
  import { VIZ_CORRECTIONS, VIZ_MODES } from '../render/viz-modes.js';
14
14
  import { DEFAULTS } from '../core/state.js';
15
+ // O padrao dos DOIS EIXOS (ADR-0076/#104). A marca pergunta ao modelo novo, nao ao espelho p.viz.
16
+ import { PADRAO as PADRAO_VISUAL } from '../render/viz-axes.js';
15
17
  import { markChanged, markMenuChanged } from './changed-mark.js';
16
18
  /** Contrast levels, in cycle order — mirrors game.js's HC_SEQ (also used there by the physical contrast-cycle button). */
17
19
  export const CONTRAST_LEVELS = ['normal', 'hc-direto', 'hc-direto-45', 'hc-direto-7'];
@@ -77,6 +79,21 @@ export function rgbToHex(rgb) {
77
79
  export function onOffLabel(on) {
78
80
  return toggleLabel(on);
79
81
  }
82
+ /**
83
+ * Lê `player[i].visual` defensivamente, sem importar o tipo do jogador — o mesmo molde do `playerViz` abaixo,
84
+ * e pela mesma razão: `getPlayers()` devolve `readonly unknown[]` porque o tipo real é do JOGO (ADR-0033), e
85
+ * cada consumidor estreita para a SUA fatia.
86
+ *
87
+ * ⚠️ Sem jogador ou sem o campo, devolve o PADRÃO — que é a resposta certa para «esta criança mexeu em
88
+ * alguma coisa?»: quem não existe não mexeu. Inventar `hc7` aqui marcaria um menu que ninguém tocou.
89
+ */
90
+ function playerVisual(list, i) {
91
+ const v = list[i]?.visual;
92
+ return {
93
+ tema: typeof v?.tema === 'string' ? v.tema : PADRAO_VISUAL.tema,
94
+ correcao: typeof v?.correcao === 'string' ? v.correcao : PADRAO_VISUAL.correcao,
95
+ };
96
+ }
80
97
  /** Reads player[i].viz defensively (no player at that index -> 'normal'), without a Player type import. */
81
98
  function playerViz(list, i) {
82
99
  const p = list[i];
@@ -144,7 +161,11 @@ export function initSettingsVisual(ctx) {
144
161
  ctx.setSelectedPlayer(selected);
145
162
  const contrastValue = resolveVisualMode(playerViz(ctx.getPlayers(), selected));
146
163
  const settings = ctx.getVisualSettings();
147
- ctx.renderVizGroup('#visual-modes', '#visual-players', VISUAL_MODE_LIST);
164
+ // ⚠️ `renderEixosVisuais` E NÃO `renderVizGroup` desde a #104: este painel passou a ter DOIS controles,
165
+ // e o `renderVizGroup` continua a servir o painel de EMPATIA, cuja lista de simulações é mesmo exclusiva.
166
+ // Trocar o corpo daquela função em vez de acrescentar esta teria posto os dois eixos na lista de
167
+ // simulações — foi o que quase aconteceu, e o que a separação impede.
168
+ ctx.renderEixosVisuais('#visual-modes', '#visual-players');
148
169
  el.innerHTML = renderVisualPanelHtml(contrastValue, settings);
149
170
  const lq = ctx.$('#opt-lq');
150
171
  const lqv = ctx.$('#opt-lq-val');
@@ -182,6 +203,8 @@ export function initSettingsVisual(ctx) {
182
203
  rr.addEventListener('click', () => { ctx.resetRoleColors(); render(); });
183
204
  reflectOutlines();
184
205
  refreshMarks();
206
+ // A prosa volta para o rodapé depois de as linhas serem reconstruídas (CLAUDE.md §4, #109).
207
+ ctx.fillExplain?.(ctx.$('#visual .overlay__card'));
185
208
  }
186
209
  /**
187
210
  * A marca de "saiu do padrão" (ADR-0029). Cada linha contra o SEU padrão, e o botão do menu por cima.
@@ -193,7 +216,27 @@ export function initSettingsVisual(ctx) {
193
216
  */
194
217
  function refreshMarks() {
195
218
  const s = ctx.getVisualSettings();
196
- const contraste = resolveVisualMode(playerViz(ctx.getPlayers(), ctx.getSelectedPlayer())) !== 'normal';
219
+ /**
220
+ * 🔴 A MARCA É POR EIXO DESDE 2026-09-08, e a granularidade que ela recupera foi perdida por mim na #104
221
+ * etapa 4d. Enquanto o `#visual-modes` era UM rádio de sete opções, marcar o contentor era marcar a
222
+ * escolha. A etapa 4d pôs DOIS eixos lá dentro, e uma marca no contentor deixou de dizer QUAL saiu do
223
+ * padrão — que é o terceiro canal do ADR-0029 a perder informação: «a criança cega percorre o menu e OUVE,
224
+ * em ordem, o que saiu do padrão». Ouvir «mudado» sem saber de quê manda-a procurar em oito linhas.
225
+ *
226
+ * ⚠️ E VEM DO MODELO NOVO. Isto era `resolveVisualMode(playerViz(...))`, que lê o `p.viz` que a etapa 1a
227
+ * marcou `@deprecated` — um leitor que ficou para trás na migração.
228
+ *
229
+ * 📌 E A REGRA DA SIMULAÇÃO DEIXA DE SER DERIVADA E PASSA A SER ESTRUTURAL, que é o ganho de fundo. Ela
230
+ * está escrita logo acima: uma simulação de empatia não marca ESTE menu, porque a marca dela pertence ao
231
+ * menu de empatia e é lá que ela leva a criança. Antes isso dependia de o `resolveVisualMode` responder
232
+ * `normal`; agora a simulação vive noutro campo do `VisualState`, e perguntar pelo tema e pela correcção
233
+ * nunca a alcança.
234
+ */
235
+ const visual = playerVisual(ctx.getPlayers(), ctx.getSelectedPlayer());
236
+ const tema = visual.tema !== PADRAO_VISUAL.tema;
237
+ const correcao = visual.correcao !== PADRAO_VISUAL.correcao;
238
+ const linhaDoEixo = (eixo) => ctx.$(`#visual-modes button[data-eixo="${eixo}"][aria-checked="true"]`)
239
+ ?.closest('.ctrl-row') ?? null;
197
240
  const lqOff = s.lq !== DEFAULTS.lq;
198
241
  const owner = s.ownerColors !== DEFAULTS.ownerColors;
199
242
  const cb = s.cbSafe !== DEFAULTS.cbSafe;
@@ -201,14 +244,15 @@ export function initSettingsVisual(ctx) {
201
244
  const bg = s.outlineBg !== DEFAULTS.hcOutlineBg;
202
245
  const papeis = ROLE_KEYS.some((k) => !sameRgb(s.roleColors[k], HC_ROLE_DEF[k]));
203
246
  const linha = (sel) => ctx.$(sel)?.closest('.ctrl-row') ?? null;
204
- markChanged(ctx.$('#visual-modes'), contraste);
247
+ markChanged(linhaDoEixo('tema'), tema);
248
+ markChanged(linhaDoEixo('correcao'), correcao);
205
249
  markChanged(linha('#opt-lq'), lqOff);
206
250
  markChanged(linha('#opt-ownercolors'), owner);
207
251
  markChanged(linha('#opt-cbsafe'), cb);
208
252
  markChanged(linha('#opt-outline-fg'), fg);
209
253
  markChanged(linha('#opt-outline-bg'), bg);
210
254
  markChanged(linha('#opt-role-reset'), papeis);
211
- markMenuChanged(ctx.$('[data-act="visual"]'), [contraste, lqOff, owner, cb, fg, bg, papeis]);
255
+ markMenuChanged(ctx.$('[data-act="visual"]'), [tema, correcao, lqOff, owner, cb, fg, bg, papeis]);
212
256
  }
213
257
  // ---- restaurar os padrões DESTE menu (ADR-0028) ----
214
258
  //
@@ -32,9 +32,19 @@ export type { FatosDaCena } from '../core/scenes.js';
32
32
  export interface PhaseView {
33
33
  /** `#title-overlay`.hidden — o splash só aparece no título. */
34
34
  titleOverlayHidden: boolean;
35
- /** `#pause-overlay`.hidden — SEMPRE true. A pausa global foi aposentada na Etapa 2 (agora é uma por tela);
36
- * o elemento continua no index.html e o original o escondia incondicionalmente. Verbatim. */
37
- pauseOverlayHidden: true;
35
+ /**
36
+ * ⚠️ `pauseOverlayHidden` SAIU EM 2026-09-08, e a ausência é a notícia.
37
+ *
38
+ * Ele era `true` em toda fase, e existia porque a Etapa 2 aposentou a pausa GLOBAL sem apagar o elemento: a
39
+ * casca continuava a procurá-lo e a escondê-lo a cada troca de fase, com dois gates a afirmar que ele ficava
40
+ * escondido. Código a segurar um cadáver.
41
+ *
42
+ * E o cadáver custava mais do que as linhas: aquelas quarenta linhas de `#pause-overlay` no `index.html` do
43
+ * cartucho eram o menu de pausa com aspecto mais OFICIAL do repositório — o que o próximo autor de cartucho
44
+ * copia junto com o ficheiro —, e já tinham derivado do que a engine gera (dois itens «Comunicação», e um
45
+ * `#opt-letra` que hoje sai do `dynLabel`). Um campo que diz «está escondido» aceita que ele exista; não o
46
+ * ter diz que ele não existe.
47
+ */
38
48
  /** `.screen-pause`.hidden de CADA tela — os menus por tela só aparecem na pausa. */
39
49
  screenPauseHidden: boolean;
40
50
  /** `setMasterMuted(...)` — GAG: fora de 'playing' TODO o som cala (loops de ambiente/chuva inclusive). */
@@ -99,7 +99,6 @@ import { PM_ITENS_VISIVEIS } from './pause-icons.js';
99
99
  export function phaseView(f) {
100
100
  return {
101
101
  titleOverlayHidden: !f.telaDeTitulo,
102
- pauseOverlayHidden: true,
103
102
  screenPauseHidden: !f.menuDePausa,
104
103
  masterMuted: !f.mundoRodando,
105
104
  hideTouchControls: !f.mundoRodando,
@@ -308,11 +307,11 @@ export function initShell(ctx) {
308
307
  /* ===================== a troca de fase ===================== */
309
308
  /** A metade IMPURA: pega a projeção pronta e a carimba no documento. */
310
309
  function applyPhaseView(v) {
311
- const t = ctx.$('#title-overlay'), pa = ctx.$('#pause-overlay');
310
+ // A pausa GLOBAL não é procurada: ela foi aposentada na Etapa 2, e a casca deixou de a segurar em
311
+ // 2026-09-08. Ver a nota no `PhaseView`, onde o campo estava.
312
+ const t = ctx.$('#title-overlay');
312
313
  if (t)
313
314
  t.hidden = v.titleOverlayHidden;
314
- if (pa)
315
- pa.hidden = v.pauseOverlayHidden; // pausa GLOBAL aposentada (Etapa 2): agora é uma por tela
316
315
  ctx.getPauseScreens().forEach((sp) => { sp.hidden = v.screenPauseHidden; });
317
316
  }
318
317
  /** O estado do `#touch-controls` ANTES de qualquer coisa desta troca de fase mexer nele. */
@@ -0,0 +1,32 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ import { type MotivoIndisponivel, type VisualState } from '../render/viz-axes.js';
3
+ /** Uma linha pronta a traduzir: a chave e o que ela precisa. `null` = não há nada a dizer. */
4
+ export interface Recusa {
5
+ readonly chave: string;
6
+ /** O que a criança tem de desfazer para a demonstração ser honesta. Entra na frase por `{eixo}`. */
7
+ readonly eixo: MotivoIndisponivel;
8
+ }
9
+ /**
10
+ * A chave i18n do motivo, por eixo fora do padrão.
11
+ *
12
+ * ⚠️ TRÊS CHAVES E NÃO UMA COM PARÂMETRO, e a diferença é de tradução e não de estilo: em português «o tema»
13
+ * e «a correção de cor» levam artigos diferentes, e «os dois» não é o plural de nenhum dos dois. Uma frase
14
+ * com `{eixo}` obrigaria cada idioma a montar concordância a partir de um substantivo solto — que é
15
+ * exactamente o defeito que o `sr.nav.clockOne` já registou para «às 1 horas».
16
+ */
17
+ export declare const CHAVE_DO_MOTIVO: Readonly<Record<MotivoIndisponivel, string>>;
18
+ /**
19
+ * Esta simulação pode ser oferecida? E se não, o que dizer.
20
+ *
21
+ * `null` = pode; a interface não mostra nada, porque um aviso que aparece sempre deixa de ser lido.
22
+ */
23
+ export declare function recusaDaSimulacao(v: VisualState): Recusa | null;
24
+ /**
25
+ * A simulação deve aparecer DESABILITADA em vez de sumir?
26
+ *
27
+ * ⚠️ SEMPRE, E É A METADE «never silently removed» DO ADR-0076. Existe como função com nome próprio, e não
28
+ * como um `!recusa` no ponto de uso, porque ela responde a uma pergunta diferente: `recusaDaSimulacao` diz
29
+ * *por que* não dá; esta diz *que a linha continua na tela*. Quem desenha precisa das duas, e juntá-las numa
30
+ * só faria a resposta «não há motivo» parecer «não desenhe a linha».
31
+ */
32
+ export declare function mostraMesmoIndisponivel(): boolean;
@@ -0,0 +1,57 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // ui/simulation-refusal — POR QUE A SIMULAÇÃO NÃO ESTÁ DISPONÍVEL, dito à criança (ADR-0076, issue #104).
3
+ //
4
+ // ========================= O QUE A RECUSA TEM DE FAZER, E O QUE ELA NÃO PODE FAZER =========================
5
+ // O ADR-0076 exige que uma simulação indisponível apareça **VISÍVEL e explicada** — «never silently removed,
6
+ // never accepted then ignored». As duas metades dessa frase descrevem dois defeitos diferentes:
7
+ //
8
+ // · REMOVER EM SILÊNCIO ensina que a coisa não existe. Um adulto que ontem mostrou a simulação a uma turma
9
+ // e hoje não a acha conclui que ela foi tirada, e não que ele próprio ligou o alto contraste.
10
+ // · ACEITAR E IGNORAR é pior: a demonstração PARECE estar a correr. Sobre uma tela já corrigida, ela não
11
+ // mostra nem a deficiência nem a correção; sobre um tema de alto contraste, mostra o que o TEMA faz e não
12
+ // o que a deuteranopia faz. ⚠️ Não é uma demonstração mais fraca — **ela ensina uma coisa falsa**.
13
+ //
14
+ // ⚠️ E O MOTIVO É UM FACTO SOBRE A DEMONSTRAÇÃO, NUNCA UMA REPREENSÃO A QUEM ESCOLHEU. Quem ligou o alto
15
+ // contraste ligou-o porque precisa; quem ligou a correção de cor vê melhor com ela. A frase diz o que a
16
+ // demonstração precisa para ser honesta, e diz como voltar — nunca «desligue isso».
17
+ //
18
+ // ========================= POR QUE ISTO É UM MÓDULO PURO =========================
19
+ // Mesma forma do `ui/reach-notice`: devolve DADO (chave i18n + parâmetros), não texto pronto. A frase é da
20
+ // interface e tem de passar por `t()`; devolver português daqui repetiria o defeito que o `PADWIZ_STEPS` já
21
+ // deixou de cometer. E, sendo puro, o desenho da recusa é conferível no project `node`, sem documento.
22
+ //
23
+ // Módulo-folha: importa só os tipos e o predicado do modelo de dois eixos.
24
+ import { simulacaoIndisponivel } from '../render/viz-axes.js';
25
+ /**
26
+ * A chave i18n do motivo, por eixo fora do padrão.
27
+ *
28
+ * ⚠️ TRÊS CHAVES E NÃO UMA COM PARÂMETRO, e a diferença é de tradução e não de estilo: em português «o tema»
29
+ * e «a correção de cor» levam artigos diferentes, e «os dois» não é o plural de nenhum dos dois. Uma frase
30
+ * com `{eixo}` obrigaria cada idioma a montar concordância a partir de um substantivo solto — que é
31
+ * exactamente o defeito que o `sr.nav.clockOne` já registou para «às 1 horas».
32
+ */
33
+ export const CHAVE_DO_MOTIVO = Object.freeze({
34
+ tema: 'sim.indisponivel.tema',
35
+ correcao: 'sim.indisponivel.correcao',
36
+ ambos: 'sim.indisponivel.ambos',
37
+ });
38
+ /**
39
+ * Esta simulação pode ser oferecida? E se não, o que dizer.
40
+ *
41
+ * `null` = pode; a interface não mostra nada, porque um aviso que aparece sempre deixa de ser lido.
42
+ */
43
+ export function recusaDaSimulacao(v) {
44
+ const motivo = simulacaoIndisponivel(v);
45
+ return motivo === null ? null : { chave: CHAVE_DO_MOTIVO[motivo], eixo: motivo };
46
+ }
47
+ /**
48
+ * A simulação deve aparecer DESABILITADA em vez de sumir?
49
+ *
50
+ * ⚠️ SEMPRE, E É A METADE «never silently removed» DO ADR-0076. Existe como função com nome próprio, e não
51
+ * como um `!recusa` no ponto de uso, porque ela responde a uma pergunta diferente: `recusaDaSimulacao` diz
52
+ * *por que* não dá; esta diz *que a linha continua na tela*. Quem desenha precisa das duas, e juntá-las numa
53
+ * só faria a resposta «não há motivo» parecer «não desenhe a linha».
54
+ */
55
+ export function mostraMesmoIndisponivel() {
56
+ return true;
57
+ }
@@ -0,0 +1,48 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ import { type Tema, type Correcao, type VisualState } from '../render/viz-axes.js';
3
+ /** O rótulo de cada tema. Chave i18n — quem exibe resolve, como todo o resto do menu. */
4
+ export declare const ROTULO_DO_TEMA: Readonly<Record<Tema, string>>;
5
+ /** O rótulo de cada correção de cor. */
6
+ export declare const ROTULO_DA_CORRECAO: Readonly<Record<Correcao, string>>;
7
+ /**
8
+ * Os rótulos CURTOS, para os ícones da barra rápida — «7:1», «deuteranopia».
9
+ *
10
+ * ⚠️ SÃO AS CHAVES QUE JÁ EXISTIAM (`contrast.*`, `cvd.*`), re-chaveadas por eixo. A barra rápida sempre
11
+ * falou curto porque anuncia UM ícone de cada vez, e o painel sempre falou por extenso porque a criança está
12
+ * a ler uma lista — a diferença é de contexto e sobrevive à divisão. Reaproveitar em vez de traduzir de novo
13
+ * é o que mantém a mesma palavra nos dois sítios.
14
+ */
15
+ export declare const CURTO_DO_TEMA: Readonly<Record<Tema, string>>;
16
+ export declare const CURTO_DA_CORRECAO: Readonly<Record<Correcao, string>>;
17
+ /** Os dois eixos, como o painel os identifica no DOM. */
18
+ export type EixoVisual = 'tema' | 'correcao';
19
+ /** Um tradutor, igual ao que o resto da interface recebe. */
20
+ export type Tradutor = (chave: string, params?: Record<string, string | number>) => string;
21
+ /**
22
+ * As linhas de UM eixo, no formato de rádio que o painel já usa.
23
+ *
24
+ * ⚠️ RÁDIO E NÃO SETE BOTÕES, e o motivo sobrevive à divisão: DENTRO de um eixo os valores continuam
25
+ * exclusivos — um tema de cada vez, uma correção de cada vez. O que deixou de ser exclusivo é a relação
26
+ * ENTRE os eixos, e é por isso que eles viram dois rádios em vez de um.
27
+ *
28
+ * ⚠️ E MANTÉM-SE A FORMA DE LINHA VISÍVEL, não um `<select>`. O `settings-visual` regista o erro que o Dev
29
+ * apanhou na primeira tentativa: dentro de uma caixa fechada, um controle cuja razão de existir é ser ACHADO
30
+ * por quem enxerga mal fica «quase o mesmo que não ter movido».
31
+ */
32
+ export declare function linhasDoEixo(eixo: EixoVisual, valores: readonly string[], rotulos: Readonly<Record<string, string>>, atual: string, t: Tradutor): string;
33
+ /**
34
+ * Os DOIS eixos, um a seguir ao outro, com um título cada.
35
+ *
36
+ * O título existe porque dois rádios seguidos sem nome são um rádio de oito para quem lê depressa — e essa
37
+ * leitura é exactamente o mal-entendido que a divisão existe para desfazer.
38
+ */
39
+ export declare function eixosHtml(v: VisualState, t: Tradutor): string;
40
+ /** O que um clique num botão do painel quer dizer. `null` quando o botão não é de eixo nenhum. */
41
+ export interface EscolhaDeEixo {
42
+ readonly eixo: EixoVisual;
43
+ readonly valor: string;
44
+ }
45
+ export declare function escolhaDoBotao(dataset: {
46
+ eixo?: string;
47
+ valor?: string;
48
+ }): EscolhaDeEixo | null;
@@ -0,0 +1,95 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // ui/visual-axes-panel — O MENU VISUAL COM DOIS CONTROLES (ADR-0076, issue #104), na metade pura.
3
+ //
4
+ // ========================= O COMENTÁRIO QUE AUTORIZA ESTA MUDANÇA =========================
5
+ // O `ui/settings-visual` justifica o controle ÚNICO com estas palavras, e elas continuam lá, verdadeiras
6
+ // sobre o dia em que foram escritas:
7
+ //
8
+ // «Os sete vivem num CONTROLE SÓ, e isso não é economia de espaço: `p.viz` guarda UM valor. Dois
9
+ // controles separados se sobrescreveriam em silêncio — a criança escolheria a correção, depois o
10
+ // contraste, e perderia a correção sem nada dizer que perdeu.»
11
+ //
12
+ // ⚠️ A RAZÃO QUE ELE DÁ DEIXOU DE EXISTIR. `p.viz` já não guarda um valor: guarda dois eixos mais a
13
+ // simulação, e os escritores por eixo (`setTemaDoJogador`/`setCorrecaoDoJogador`) mexem num sem tocar no
14
+ // outro. O controle único era a forma honesta de contar uma exclusividade REAL; mantê-lo agora seria contar
15
+ // uma exclusividade que já não existe — e continuar a negar os dois a quem precisa dos dois.
16
+ //
17
+ // ========================= OS NOMES DOS PADRÕES SÃO DECISÃO, NÃO ROTINA =========================
18
+ // ⚠️ «Tema padrão» e «Visão tricromática», e o ADR-0076 fecha a *definition of done* com a regra que os
19
+ // escolheu: **nenhum rótulo de padrão diagnostica quem lê**. `modo sem deficiência visual` foi oferecido e
20
+ // recusado — ele diz à criança o que ela NÃO é, no menu que ela abriu para conseguir jogar. Um padrão
21
+ // chama-se pelo que ele É.
22
+ //
23
+ // E `Modo padrão` (o `viz.normal` de hoje) deixa de servir aqui: era o neutro PARTILHADO, de quando os dois
24
+ // eixos eram um só. Com dois controles, um «padrão» sem dizer padrão de QUÊ é ambíguo em ambos.
25
+ //
26
+ // Módulo puro: devolve HTML e chaves i18n, não toca documento nenhum. Quem monta é o `render/viz-setters`.
27
+ import { TEMAS, CORRECOES } from '../render/viz-axes.js';
28
+ /** O rótulo de cada tema. Chave i18n — quem exibe resolve, como todo o resto do menu. */
29
+ export const ROTULO_DO_TEMA = Object.freeze({
30
+ padrao: 'eixo.tema.padrao',
31
+ hc3: 'viz.hc-direto',
32
+ hc45: 'viz.hc-direto-45',
33
+ hc7: 'viz.hc-direto-7',
34
+ });
35
+ /** O rótulo de cada correção de cor. */
36
+ export const ROTULO_DA_CORRECAO = Object.freeze({
37
+ tricro: 'eixo.correcao.tricro',
38
+ protan: 'viz.fix-protan',
39
+ deuter: 'viz.fix-deuter',
40
+ tritan: 'viz.fix-tritan',
41
+ });
42
+ /**
43
+ * Os rótulos CURTOS, para os ícones da barra rápida — «7:1», «deuteranopia».
44
+ *
45
+ * ⚠️ SÃO AS CHAVES QUE JÁ EXISTIAM (`contrast.*`, `cvd.*`), re-chaveadas por eixo. A barra rápida sempre
46
+ * falou curto porque anuncia UM ícone de cada vez, e o painel sempre falou por extenso porque a criança está
47
+ * a ler uma lista — a diferença é de contexto e sobrevive à divisão. Reaproveitar em vez de traduzir de novo
48
+ * é o que mantém a mesma palavra nos dois sítios.
49
+ */
50
+ export const CURTO_DO_TEMA = Object.freeze({
51
+ padrao: 'contrast.off', hc3: 'contrast.3', hc45: 'contrast.45', hc7: 'contrast.7',
52
+ });
53
+ export const CURTO_DA_CORRECAO = Object.freeze({
54
+ tricro: 'cvd.off', protan: 'cvd.protan', deuter: 'cvd.deuter', tritan: 'cvd.tritan',
55
+ });
56
+ /**
57
+ * As linhas de UM eixo, no formato de rádio que o painel já usa.
58
+ *
59
+ * ⚠️ RÁDIO E NÃO SETE BOTÕES, e o motivo sobrevive à divisão: DENTRO de um eixo os valores continuam
60
+ * exclusivos — um tema de cada vez, uma correção de cada vez. O que deixou de ser exclusivo é a relação
61
+ * ENTRE os eixos, e é por isso que eles viram dois rádios em vez de um.
62
+ *
63
+ * ⚠️ E MANTÉM-SE A FORMA DE LINHA VISÍVEL, não um `<select>`. O `settings-visual` regista o erro que o Dev
64
+ * apanhou na primeira tentativa: dentro de uma caixa fechada, um controle cuja razão de existir é ser ACHADO
65
+ * por quem enxerga mal fica «quase o mesmo que não ter movido».
66
+ */
67
+ export function linhasDoEixo(eixo, valores, rotulos, atual, t) {
68
+ return valores.map((valor) => {
69
+ const sel = valor === atual;
70
+ return `<div class="ctrl-row"><span><strong>${t(rotulos[valor])}</strong></span>`
71
+ + `<button class="mode-btn${sel ? ' is-on' : ''}" role="radio" aria-checked="${sel}"`
72
+ + ` data-eixo="${eixo}" data-valor="${valor}" type="button">${sel ? t('viz.escolhido') : t('viz.escolher')}</button></div>`;
73
+ }).join('');
74
+ }
75
+ /**
76
+ * Os DOIS eixos, um a seguir ao outro, com um título cada.
77
+ *
78
+ * O título existe porque dois rádios seguidos sem nome são um rádio de oito para quem lê depressa — e essa
79
+ * leitura é exactamente o mal-entendido que a divisão existe para desfazer.
80
+ */
81
+ export function eixosHtml(v, t) {
82
+ return `<h3 class="opt-sub">${t('eixo.tema.titulo')}</h3>`
83
+ + linhasDoEixo('tema', TEMAS, ROTULO_DO_TEMA, v.tema, t)
84
+ + `<h3 class="opt-sub">${t('eixo.correcao.titulo')}</h3>`
85
+ + linhasDoEixo('correcao', CORRECOES, ROTULO_DA_CORRECAO, v.correcao, t);
86
+ }
87
+ export function escolhaDoBotao(dataset) {
88
+ const { eixo, valor } = dataset;
89
+ if (eixo !== 'tema' && eixo !== 'correcao')
90
+ return null;
91
+ if (!valor)
92
+ return null;
93
+ const validos = eixo === 'tema' ? TEMAS : CORRECOES;
94
+ return validos.includes(valor) ? { eixo, valor } : null;
95
+ }
@@ -7,6 +7,7 @@ import { t } from '../core/i18n.js';
7
7
  import { $ } from './dom.js';
8
8
  import { srAlert } from '../core/a11y-sr.js';
9
9
  import { emFracao } from '../input/pointer-space.js'; // #105: um lugar so converte um ponto de tela
10
+ import { carimbarOrigem } from '../input/origem-sintetica.js'; // ADR-0109: a tecla sintetica declara quem a produziu
10
11
  const wg = () => window.webgazer;
11
12
  export let eyeMode = false; // ligado pelo botão; onGaze só age com ele ligado
12
13
  export function setEyeMode(on) { eyeMode = on; }
@@ -16,7 +17,11 @@ function eyeSet(k, on, code) {
16
17
  if (_eyeKeys[k] === on)
17
18
  return;
18
19
  _eyeKeys[k] = on;
19
- const ev = new KeyboardEvent(on ? 'keydown' : 'keyup', { code, bubbles: true });
20
+ // ⚠️ CARIMBADO `'olhos'` ANTES DE DESPACHAR (ADR-0109). Sem esta linha o evento chega ao `input/keydown`
21
+ // indistinguível de uma tecla premida e é carimbado `teclado` — e o efeito não é cosmético: a regra 3 diz
22
+ // que apertar uma tecla devolve o teclado SEM alternância, logo o olhar da criança desligaria, sozinho e em
23
+ // silêncio, a alternância de que ela depende para jogar. É o defeito que o §C desta issue existe para fechar.
24
+ const ev = carimbarOrigem(new KeyboardEvent(on ? 'keydown' : 'keyup', { code, bubbles: true }), 'olhos');
20
25
  window.dispatchEvent(ev);
21
26
  document.dispatchEvent(ev);
22
27
  }
package/docs/CREDITS.md CHANGED
@@ -62,3 +62,21 @@ A narração por voz roda **inteiramente no navegador/offline** graças ao traba
62
62
 
63
63
  > Os pesos das vozes são **baixados uma vez** (de um host público) e rodam **100% localmente** depois — nenhum áudio de
64
64
  > criança sai do dispositivo. Ver `docs/2-Architecture/adr/ADR-0065-three-neural-voices-owned-by-the-engine-and-cached-on-first-use.yaml`.
65
+
66
+ ## Arte — Liberated Pixel Cup — CC BY-SA 3.0
67
+
68
+ A atribuição do [Liberated Pixel Cup](https://github.com/OpenGameArt/LiberatedPixelCup) é **por recurso**, e por
69
+ isso não cabe nesta página em prosa: o repositório do LCP guarda ficheiros de autoria separados para originais e
70
+ derivados, e a cadeia lê-se recurso a recurso.
71
+
72
+ **Ela mora em [`../art/lcp/ATTRIBUTION.csv`](../art/lcp/ATTRIBUTION.csv)** — caminho, autor, fonte, licença e de que
73
+ recursos cada um derivou. É a mesma forma que o `LICENSES.md` §4 já usa para as 100 gerações do PixelLab: um
74
+ ficheiro tabular ao lado do apontador em prosa, porque um livro-razão que cresce com o catálogo não se mantém à mão.
75
+
76
+ ⚠️ **Atribuição aqui é condição de uso, não linha de crédito.** Os dois braços da licença exigem-na, e um recurso
77
+ sem autor conhecido **não entra** — «não consegui descobrir» não é licença. O gate
78
+ `tests/lcp-quarantine.node.test.js` reprova a entrada vazia, a entrada órfã e o recurso sem entrada, e a árvore
79
+ está em **quarentena** por uma razão que o [`LICENSES.md`](LICENSES.md) §3 e o **ADR-0107** explicam: misturar o LCP
80
+ com a arte própria licenciaria em share-alike a obra de alguém que não está neste repositório para discordar.
81
+
82
+ **Hoje o livro está vazio** — o LCP foi decidido em 2026-09-08 e ainda não entrou.
package/docs/LICENSES.md CHANGED
@@ -72,6 +72,18 @@ Nem tudo aqui é nosso, e o que não é **não muda de licença por estar neste
72
72
  - **Tipografias** — roster e restrições no **ADR-0012**. ⚠️ Ronde e as alternativas OPTIFrench-Script e
73
73
  Merveille são **gratuitas só para uso pessoal e NÃO podem ser empacotadas**: oferece-se download, e a
74
74
  opção fica desabilitada quando nenhuma está presente.
75
+ - **Arte de terceiros — o Liberated Pixel Cup** (`github.com/OpenGameArt/LiberatedPixelCup`), sob
76
+ **CC BY-SA 3.0** (**ADR-0107**). O conjunto é duplamente licenciado — CC BY-SA 3.0 **ou** GPL-3.0 — e o
77
+ projeto toma o braço **CC BY-SA**, para que arte continue governada como arte e não seja puxada para a
78
+ licença do CÓDIGO; é a mesma separação de regimes que este ficheiro tem em três linhas.
79
+ ⚠️ **E ele vive em QUARENTENA, o que é mecanismo e não arrumação.** Nenhum recurso pode conter ao mesmo
80
+ tempo material derivado do LCP e a **arte própria** da autora: o SA alcança o que nós alterarmos — como já
81
+ está escrito acima para os pictogramas — e misturar os dois licenciaria a arte DELA em share-alike, que é
82
+ precisamente a decisão que o **pilar 10** deixa com ela e não connosco. O CC BY-SA 3.0 também é
83
+ **incompatível com a família GPL**, então a mesma quarentena é o que mantém o regime do código limpo.
84
+ ⚠️ **Atribuição é condição de uso, por recurso**: o repositório do LCP guarda ficheiros de autoria
85
+ separados para originais e derivados. Recurso sem autor conhecido **não entra** — «não consegui descobrir»
86
+ não é licença.
75
87
 
76
88
  ---
77
89
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@the-inclusionist/engine",
3
- "version": "7.0.0",
3
+ "version": "8.0.0-rc.1",
4
4
  "type": "module",
5
5
  "description": "Accessibility-first (WCAG 2.2 + GAG) engine for educational 2D pixel-art games, built with PixiJS. Screen reader, sonar, high contrast by role, colour-vision filters, remappable input as intent, i18n and offline PWA — the parts every game inherits (ADR-0035, ADR-0036).",
6
6
  "comment:exports": [
@@ -17,10 +17,25 @@
17
17
  "form and it is the one every module in this repository already writes internally, so a consumer's",
18
18
  "import line looks exactly like ours. Extensionless is NOT offered: supporting both needs two patterns",
19
19
  "per layer, and two spellings of one import is how two halves of a codebase drift.",
20
- "`./style.css` and `./assets/*` exist because the borrowed panels REQUIRE them and nothing said so — a",
20
+ "⚠️ `./assets/*` IS GONE (2026-09-07, issue #119). It matched the whole of `app/public/` while `files`",
21
+ "ships only `app/public/vendor`, so the pattern promised what it did not deliver and a consumer found",
22
+ "out through a 404 at runtime. Measured before removing: `app/public/` holds three entries — `vendor/`,",
23
+ "`_headers`, `icon.svg` — and only `vendor` ships, so everything the wide door could resolve in a",
24
+ "published tarball is already covered by `./assets/vendor/*`, and the rest already 404'd. Nobody's import",
25
+ "line changes: `engine/assets/vendor/fonts.css` still matches.",
26
+ "`./style.css` and `./assets/vendor/*` exist because the borrowed panels REQUIRE them and nothing said so — a",
21
27
  "consumer that installed this package got dialogs with no styling and a typography menu offering 18",
22
28
  "fonts that 404. Copy `assets/vendor/` into your own `public/` at build time: `fonts.css` addresses the",
23
- "faces by RELATIVE url, so the folder travels whole or not at all."
29
+ "faces by RELATIVE url, so the folder travels whole or not at all.",
30
+ "⚠️ AND COPYING IS NOT ENOUGH — THE CONSUMER MUST ALSO *LINK* THE SHEET, which nothing said either:",
31
+ " <link rel=\"stylesheet\" href=\"/vendor/fonts.css\">",
32
+ " <link rel=\"preload\" href=\"/vendor/fonts/atkinson-400.woff2\" as=\"font\" type=\"font/woff2\" crossorigin>",
33
+ "Measured on 2026-09-07: NOBODY in this repository referenced `vendor/fonts.css`. The `app/index.html`",
34
+ "that used to link it left with the cartridge (issue #111), and the typography panel went on offering",
35
+ "seventeen families of which none loaded — the child picks one, the menu marks it active, and the browser",
36
+ "draws the system font. No error anywhere. `app/quiz.html` links it now, and",
37
+ "`tests/fontes-carregam.node.test.js` holds both halves: no offerable font without a `@font-face`, and",
38
+ "the host actually loading the sheet."
24
39
  ],
25
40
  "exports": {
26
41
  ".": {
@@ -51,8 +66,12 @@
51
66
  "types": "./dist-pkg/educational/*.d.ts",
52
67
  "default": "./dist-pkg/educational/*.js"
53
68
  },
69
+ "./i18n/*.js": {
70
+ "types": "./dist-pkg/i18n/*.d.ts",
71
+ "default": "./dist-pkg/i18n/*.js"
72
+ },
54
73
  "./style.css": "./app/css/style.css",
55
- "./assets/*": "./app/public/*",
74
+ "./assets/vendor/*": "./app/public/vendor/*",
56
75
  "./package.json": "./package.json"
57
76
  },
58
77
  "types": "./dist-pkg/boot/create-game.d.ts",
@@ -133,6 +152,7 @@
133
152
  "test:node": "vitest run --project node",
134
153
  "test:browser": "vitest run --project browser",
135
154
  "check:precache": "node scripts/check-precache.mjs",
155
+ "check:annual-report": "node scripts/check-annual-report.mjs",
136
156
  "test:a11y": "node scripts/axe-check.mjs",
137
157
  "release": "release-it"
138
158
  },