@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
@@ -25,12 +25,24 @@
25
25
  // · `rm`/`saveRM` — the reduced-motion flags object, co-owned with ui/settings-motion (same reference).
26
26
  // · `PM_BTNS`/`QL_NAME` — owned by ui/activities-menu; injected, never copied.
27
27
  import { t } from '../core/i18n.js';
28
- import { CONTRAST_LEVELS, CONTRAST_LABELS } from './settings-visual.js';
28
+ import { CONTRAST_LEVELS } from './settings-visual.js';
29
+ import { CURTO_DO_TEMA, CURTO_DA_CORRECAO } from './visual-axes-panel.js';
30
+ import { proximoTema, proximaCorrecao, temAltoContraste, PADRAO, } from '../render/viz-axes.js';
29
31
  import { anunciarItem } from './item-announcement.js';
30
32
  import { rotuloAcessivel } from '../core/rotulo-acessivel.js';
31
33
  import { passoNoAnel } from '../core/anel.js'; // da FOLHA, e não de ui/menu-nav: ver a nota lá
32
34
  // LIGAÇÃO VIVA (ESM): o índice pode ser desligado no menu, e o valor aqui acompanha sem assinatura.
33
- import { menuIndexOn } from '../core/state.js';
35
+ import { menuIndexOn, DEFAULTS, setModoCegoValue } from '../core/state.js';
36
+ // ⚠️ IMPORT DIRETO DE `platform/storage`, e não uma peça a mais no `ctx`, e a escolha é sobre quem pode
37
+ // esquecer: `initPauseIcons` é chamado pela raiz de composição de CADA jogo, e um `store` injetado é um
38
+ // campo que um consumidor pode omitir — e omiti-lo faria o nível TEA voltar a não persistir, em silêncio,
39
+ // exactamente no jogo que se esqueceu. É a mesma forma que `ui/fonts` usa, e `ui/` depender de `platform/`
40
+ // não inverte camada nenhuma.
41
+ import * as store from '../platform/storage.js';
42
+ import { definirAlternanciaDeMarcha } from './settings-motor.js';
43
+ import { recusaDaAlternancia } from './latch-refusal.js';
44
+ import { PM_BTNS, PM_OPTIONS_BTNS } from './activities-menu.js';
45
+ import { CHAVES_DE_CENA, ANIMACOES_DO_PERSONAGEM, lerCenaGuardada, guardarCena } from './motion-scene.js';
34
46
  /**
35
47
  * A LEGENDA de um ícone da barra de acessibilidade — uma função, e não três cópias da mesma expressão.
36
48
  *
@@ -69,6 +81,17 @@ const ICON_BY_KEY = new Map(PAUSE_ICONS.map((ic) => [ic.k, ic]));
69
81
  export function pauseIcon(k) { return ICON_BY_KEY.get(k); }
70
82
  /** TEA cycle: 0 = normal · 1 = calmo (reduces) · 2 = silencioso (switches off). Never touches TTS/blind mode. */
71
83
  export const CALM_NAMES = ['calm.off', 'calm.quiet', 'calm.silent'];
84
+ /**
85
+ * O nível TEA guardado, saneado. Fora de 0..2 devolve o padrão — dado do navegador é dado de fora, e um
86
+ * nível inventado escolheria `CALM_NAMES[3]`, que é `undefined`, e o anúncio ao leitor de tela sairia vazio.
87
+ */
88
+ export function saneiaNivelTea(bruto) {
89
+ return Number.isInteger(bruto) && bruto >= 0 && bruto < CALM_NAMES.length ? bruto : DEFAULTS.calmMode;
90
+ }
91
+ /** Lê o nível TEA do armazenamento. Chamado no `init`, nunca no import. */
92
+ function lerNivelTea() {
93
+ return saneiaNivelTea(store.getNum(store.KEYS.tea, DEFAULTS.calmMode));
94
+ }
72
95
  /** The audio categories `applyCalm` governs. TTS/sonar/guarda/guia stay untouched — a calm player still needs them. */
73
96
  export const CALM_AUDIO_CATS = ['ambient', 'music', 'earcons', 'other', 'interact'];
74
97
  /** Colour-vision-deficiency cycle, in `player.viz` values. */
@@ -151,9 +174,9 @@ export function computeIconLabel(k, s) {
151
174
  if (k === 'altmove')
152
175
  return rotulo(s.toggleMove ? 'state.on' : 'state.off');
153
176
  if (k === 'contrast')
154
- return rotulo(CONTRAST_LABELS[s.viz] || 'contrast.off');
177
+ return rotulo(CURTO_DO_TEMA[s.visual.tema]);
155
178
  if (k === 'cvd')
156
- return t('icon.state', { nome: t('icon.cvd.short'), v: t(CVD_LABELS[s.viz] || 'cvd.off') });
179
+ return t('icon.state', { nome: t('icon.cvd.short'), v: t(CURTO_DA_CORRECAO[s.visual.correcao]) });
157
180
  return t(ic.n);
158
181
  }
159
182
  /** Pure form of reflectIconBtn's branching. A `soon` icon lands on all-false — it never claims to be on. */
@@ -176,17 +199,17 @@ export function computeIconVisual(k, s) {
176
199
  }
177
200
  else if (k === 'altmove') {
178
201
  on = s.toggleMove;
202
+ dis = !!s.alternanciaExigida;
179
203
  }
180
204
  else if (k === 'contrast') {
181
- on = /^hc-direto/.test(s.viz || '');
205
+ on = temAltoContraste(s.visual);
182
206
  }
183
207
  else if (k === 'cvd') {
184
- if (s.viz === 'fix-protan')
185
- cvd = 'pi-cvd-protan';
186
- else if (s.viz === 'fix-deuter')
187
- cvd = 'pi-cvd-deuter';
188
- else if (s.viz === 'fix-tritan')
189
- cvd = 'pi-cvd-tritan';
208
+ // ⚠️ O FUNDO DE DUAS CORES É O SINAL DE LIGADO deste ícone, e agora ele lê o EIXO da correção — que
209
+ // continua a dizer o mesmo quando o tema também está ligado, coisa que a chave única não conseguia: com
210
+ // `hc-direto-7` no campo, a correção da criança desaparecia do ícone que existe para a mostrar.
211
+ if (s.visual.correcao !== 'tricro')
212
+ cvd = 'pi-cvd-' + s.visual.correcao;
190
213
  }
191
214
  return { on, dis, calm, cvd, active: on || calm || !!cvd };
192
215
  }
@@ -201,9 +224,56 @@ export function iconBtnMarkup(ic) {
201
224
  return '<button class="pi-btn' + (ic.soon ? ' pi-soon' : '') + '" type="button" data-pi="' + ic.k +
202
225
  '" aria-label="' + (ic.soon ? t('icon.soon', { nome: t(ic.n) }) : t(ic.n)) + '">' + ic.e + '</button>';
203
226
  }
227
+ export function iconesQueAccionam(escritores) {
228
+ // ⚠️ POR ÍCONE, e não um booleano para os dois — e foi uma MUTAÇÃO SOBREVIVENTE que o mostrou. Com uma
229
+ // única bandeira, `&&` e `||` produziam o mesmo resultado nos casos que eu tinha escrito, porque todos
230
+ // tiravam os DOIS escritores. O `&&` escondia um ícone que FUNCIONA quando só um escritor falta, e o `||`
231
+ // mostrava um que NÃO funciona. Os dois erram, em direcções opostas, e a pergunta certa nunca foi «este
232
+ // jogo tem escritores visuais» — é «este ÍCONE tem quem o accione».
233
+ return PAUSE_ICONS.filter((ic) => (ic.k === 'contrast' ? escritores.tema
234
+ : ic.k === 'cvd' ? escritores.correcao
235
+ : true));
236
+ }
237
+ /**
238
+ * OS TRÊS ITENS QUE A ENGINE ACCIONA SOZINHA, e que por isso nunca dependem do `getPauseActs` de um jogo.
239
+ *
240
+ * 📏 Lidos do despacho, e não decididos aqui: `options`/`pmback` trocam qual lista está no cartão e
241
+ * `acessibilidade` leva o cursor à barra rápida — os três são tratados neste módulo e voltam antes de a
242
+ * tabela do jogo ser consultada.
243
+ */
244
+ export const ITENS_DA_ENGINE = new Set(['options', 'pmback', 'acessibilidade']);
245
+ /**
246
+ * OS ITENS DO MENU QUE ESTE JOGO CONSEGUE MESMO ACCIONAR (ADR-0106 §5).
247
+ *
248
+ * ⚠️ HOJE UM ITEM SEM ACÇÃO É UM BOTÃO MORTO, E EM SILÊNCIO. O despacho faz `const fn = acts[act]; if (fn)
249
+ * fn();` — quem carrega num item que o jogo não implementou não recebe erro, não recebe anúncio, não recebe
250
+ * nada. Para quem vê, parece que o clique falhou; para quem navega por leitor de tela, o menu leu-lhe um
251
+ * item que não existe. É exactamente o que o §5 chama de pior do que a ausência: «uma barra que oferece um
252
+ * caminho e depois o recusa ensina-lhe que o caminho não é para ela».
253
+ */
254
+ export function itensQueAccionam(botoes, acts) {
255
+ return botoes.filter((b) => ITENS_DA_ENGINE.has(b.act) || typeof acts[b.act] === 'function');
256
+ }
257
+ /**
258
+ * A LISTA RAIZ, com uma regra a mais: `options` é uma PORTA, e uma porta para uma sala vazia também é um
259
+ * botão morto.
260
+ *
261
+ * ⚠️ Esta é a parte que um filtro item-a-item não apanha. Se todos os painéis de ajuste forem filtrados —
262
+ * um jogo que não monta nenhum —, o item `options` sobrevive (a engine acciona-o) e abre uma lista sem nada.
263
+ * A criança atravessa uma porta e fica presa num submenu vazio, cuja única saída é o `pmback` que também
264
+ * sumiu com ele.
265
+ */
266
+ export function raizQueAcciona(raiz, opcoes, acts) {
267
+ const opcoesVivas = itensQueAccionam(opcoes, acts).filter((b) => b.act !== 'pmback');
268
+ const viva = itensQueAccionam(raiz, acts);
269
+ return opcoesVivas.length > 0 ? viva : viva.filter((b) => b.act !== 'options');
270
+ }
204
271
  /** The whole icon bar. Used by the pause screen AND by the splash `#title-icons` (which built the same string
205
- * by hand in game.js — that duplication dies with this export). */
206
- export function iconsMarkup() { return PAUSE_ICONS.map(iconBtnMarkup).join(''); }
272
+ * by hand in game.js — that duplication dies with this export).
273
+ * O parâmetro é ADITIVO e o padrão é a lista inteira: quem já chamava sem argumentos não muda de resultado. */
274
+ export function iconsMarkup(icones = PAUSE_ICONS) {
275
+ return icones.map(iconBtnMarkup).join('');
276
+ }
207
277
  /** One `.pm-btn`. Dynamic labels (`letra`/`nivel`) are rendered eagerly and carry NO `data-i18n`, so
208
278
  * i18n.applyDom() cannot overwrite them. */
209
279
  /**
@@ -268,8 +338,8 @@ export function mostrarSubmenuDaPausa(sp, sub) {
268
338
  * A LEGENDA VIAJA JUNTO. Ela é a dica que substitui, para quem não vê, o `title` que só o mouse revela;
269
339
  * deixá-la no cartão tornaria a barra do HUD muda.
270
340
  */
271
- export function quickBarMarkup() {
272
- return '<div class="pause-icons" role="group" aria-label="' + t('pause.iconBarAria') + '">' + iconsMarkup() +
341
+ export function quickBarMarkup(icones = PAUSE_ICONS) {
342
+ return '<div class="pause-icons" role="group" aria-label="' + t('pause.iconBarAria') + '">' + iconsMarkup(icones) +
273
343
  '</div><p class="pause-icons-cap" aria-live="polite"></p>';
274
344
  }
275
345
  export function acaoNaBarra(k, temStart) {
@@ -298,11 +368,92 @@ export function screenPauseMarkup(o) {
298
368
  // DOM-facing shell
299
369
  // ---------------------------------------------------------------------------------------------
300
370
  export function initPauseIcons(ctx) {
301
- // TEA level. It lives HERE (game.js's `let calmMode` had no other reader) and is deliberately NOT persisted
302
- // — verbatim: game.js never wrote it to storage, even though applyCalm() persists `rm` as a side effect.
303
- let calmMode = 0;
371
+ // ⚠️ O NÍVEL TEA PASSOU A PERSISTIR EM 2026-09-07 (issue #61). Este comentário dizia «deliberately NOT
372
+ // persisted — verbatim: game.js never wrote it to storage», e o **verbatim** é o que o desqualificava como
373
+ // decisão: foi PRESERVADO na extração do monólito, não escolhido. O ADR-0028 diz que todo menu persiste, e
374
+ // este é um controlo de menu que vive na barra rápida.
375
+ //
376
+ // O custo de não persistir era da criança que mais precisa dele: quem usa o modo SILENCIOSO voltava a
377
+ // pô-lo a cada sessão — e é para quem o barulho inesperado custa mais. Um ajuste que se esquece não é um
378
+ // ajuste, é uma tarefa diária.
379
+ //
380
+ // ⚠️ LÊ NO INIT, NUNCA NO IMPORT: a regra vale para todo o projeto e aqui tem custo concreto — um teste que
381
+ // importasse este módulo passaria a depender do `localStorage` do ambiente, e um nível herdado de outro
382
+ // caso é uma falha que aparece longe da causa.
383
+ let calmMode = lerNivelTea();
304
384
  const P = () => ctx.getPlayers();
385
+ /*
386
+ * ⚠️ A ALTERNÂNCIA DE MARCHA PASSOU A TER PADRÃO DA ENGINE (ADR-0106 §4, etapa 1b). Quem injecta continua a
387
+ * mandar; quem não injecta deixa de ficar sem ela — que era o caso dos cinco jogos sem barra.
388
+ *
389
+ * O `store` vem do import directo deste módulo e não do `ctx`, porque é assim que este ficheiro já persiste
390
+ * o resto: uma chave injectada é um campo que um consumidor pode omitir, e omiti-la faria escrever num nome
391
+ * torto.
392
+ */
393
+ const setToggleMove = ctx.setToggleMove
394
+ ?? ((i, on) => definirAlternanciaDeMarcha({
395
+ players: P(), store, srSay: ctx.srSay, getNumPlayers: ctx.getNumPlayers,
396
+ // ⚠️ ATRAVESSA, e não se resolve aqui: o ícone e o painel têm de escrever a MESMA coisa. Resolver
397
+ // o aparelho num deles e não no outro é como duas superfícies da mesma engine passam a discordar.
398
+ transporteEmUso: ctx.transporteEmUso,
399
+ }, i, on));
400
+ /*
401
+ * ⚠️ O MODO CEGO IDEM, e aqui o padrão é literalmente o que o `core/state` já decidiu que um setter faz:
402
+ * «grava, persiste, avisa» — e nada mais. Os efeitos de jogo (refazer os extras do nível) são REACÇÃO, e
403
+ * quem reage assina `on('modoCego', …)`. O anúncio não se perde para quem não injecta: este ícone já diz
404
+ * `sr.icon.blindOn`/`Off` por si, logo abaixo.
405
+ */
406
+ const setModoCego = ctx.setModoCego ?? setModoCegoValue;
407
+ /** O documento onde se constroi. Resolvido a cada uso, e por globalThis — em node o identificador
408
+ * document nem existe, e um ?? sobre ele lançaria ReferenceError em vez de cair no padrão. */
409
+ const docDaMontagem = () => ctx.doc ?? globalThis.document;
410
+ /*
411
+ * ⚠️ O CONTRASTE E A COR SÓ APARECEM SE HOUVER QUEM OS ESCREVA (ADR-0106 §5).
412
+ *
413
+ * 📏 Medido em 2026-09-08, e é o que separa este caso dos outros seis campos «acidentais»: eles eram estado
414
+ * que um cartucho calhou de guardar, e a engine pôde reclamá-los. Estes precisam de um
415
+ * `render/viz-setters`, cujo contexto pede **34 campos** do grafo de render de UM jogo — `parallaxLayers`,
416
+ * `decoSprites`, `getPowerups`, `rebuildCoins`, `worldSprite`. O `createGame` não monta isso, e um quiz não
417
+ * tem nada disso para montar. A engine não pode dar um padrão aqui; o que ela pode é não FINGIR.
418
+ *
419
+ * Decidido uma vez, no arranque, e não a cada montagem de barra: o conjunto de escritores de um consumidor
420
+ * não muda a meio de uma partida, e recalcular por tela faria as telas discordarem entre si.
421
+ */
422
+ const iconesDoJogo = iconesQueAccionam({
423
+ tema: Boolean(ctx.setTemaDoJogador),
424
+ correcao: Boolean(ctx.setCorrecaoDoJogador),
425
+ });
426
+ /*
427
+ * ⚠️ RESOLVIDOS UMA VEZ, no arranque, pela mesma razão do `settings-motion`: o `rm` é mutado in-place e
428
+ * partilhado por REFERÊNCIA com quem desenha a cena, e resolvê-lo a cada uso criaria um objecto novo por
429
+ * chamada — o interruptor deixaria de alcançar o desenho, sem erro nenhum.
430
+ *
431
+ * 📌 O `getPauseActs` fica FUNÇÃO e não valor, porque a laziness dele é a razão de ele existir assim: no
432
+ * cartucho a tabela é um `const` declarado ~1200 linhas abaixo, e lê-la aqui cairia na zona morta temporal.
433
+ */
434
+ const rm = ctx.rm ?? lerCenaGuardada();
435
+ const rmKeys = ctx.rmKeys ?? CHAVES_DE_CENA;
436
+ const rmChar = ctx.rmChar ?? ANIMACOES_DO_PERSONAGEM;
437
+ const saveRM = ctx.saveRM ?? (() => guardarCena(rm));
438
+ /*
439
+ * ⚠️ ESTES TRÊS SÃO LIDOS A CADA CHAMADA, e não resolvidos uma vez como o `rm` acima. A diferença é
440
+ * deliberada e um teste apanhou-me a errá-la: congelar `ctx.getPauseActs` no arranque partiu um caso que
441
+ * TROCA a tabela depois do `init` — e trocar depois é legítimo, porque a laziness deste campo existe
442
+ * precisamente por a tabela chegar tarde. Um padrão não pode custar a ligação tardia que o campo tem.
443
+ *
444
+ * O `rm` é o contrário e por isso fica congelado: ali o que importa é a IDENTIDADE do objecto, partilhada
445
+ * por referência com quem desenha a cena.
446
+ *
447
+ * A anotação de tipo é necessária: sem ela o padrão `() => ({})` infere `{}`, que não aceita indexação por
448
+ * string, e o compilador passaria a recusar `acts[act]` — o despacho inteiro.
449
+ */
450
+ const dynLabel = (b) => (ctx.dynLabel ? ctx.dynLabel(b) : null);
451
+ const getPauseActs = () => (ctx.getPauseActs ? ctx.getPauseActs() : {});
452
+ const setPauseActor = (i) => { if (ctx.setPauseActor)
453
+ ctx.setPauseActor(i); };
305
454
  function hasPrivateOutput(i) { return hasPrivateOutputIn(P(), ctx.getNumPlayers(), i); }
455
+ /** A recusa da alternância para este jogador agora, ou `null`. Recalculada: o aparelho em uso muda. */
456
+ function recusaAgora(i) { return ctx.transporteEmUso ? recusaDaAlternancia(ctx.transporteEmUso(i)) : null; }
306
457
  function iconState(i) {
307
458
  const p = P()[i] || {};
308
459
  const cat = ctx.getAudioCat();
@@ -312,19 +463,25 @@ export function initPauseIcons(ctx) {
312
463
  librasOn: ctx.isLibrasOn(),
313
464
  calmMode,
314
465
  toggleMove: !!p.toggleMove,
315
- viz: p.viz || '',
466
+ // ⚠️ `DEFAULTS.viz` E NÃO `''` (issue #61). A cadeia vazia funcionava por ACIDENTE: não casa
467
+ // `hc-direto` nem `fix-*`, então os dois ícones ficavam apagados pelo motivo certo por engano. O padrão
468
+ // passou a ter nome em `core/state`, e `render/viz-modes` já declarava esse modo com `kind:'normal'` —
469
+ // o que não faz nada. Dizer o padrão em vez de o deduzir é o que torna a marca do ADR-0029 possível
470
+ // aqui, porque ela lê `DEFAULTS` e mais nada.
471
+ visual: p.visual ?? PADRAO,
316
472
  privateOutput: hasPrivateOutput(i),
473
+ alternanciaExigida: recusaAgora(i) !== null,
317
474
  };
318
475
  }
319
476
  // --- TEA ---------------------------------------------------------------------------------
320
477
  function applyCalm() {
321
478
  const plan = calmMotionPlan(calmMode);
322
- for (const k of ctx.rmKeys)
323
- ctx.rm[k] = plan.sceneReduced;
324
- ctx.saveRM();
479
+ for (const k of rmKeys)
480
+ rm[k] = plan.sceneReduced;
481
+ saveRM();
325
482
  // "silencioso" freezes the character too. Iterates the WHOLE players array (not just numPlayers) — verbatim.
326
483
  for (const p of P())
327
- for (const c of ctx.rmChar)
484
+ for (const c of rmChar)
328
485
  p[c.prop] = plan.charFrozen;
329
486
  const cat = ctx.getAudioCat();
330
487
  for (const k of CALM_AUDIO_CATS) {
@@ -341,7 +498,7 @@ export function initPauseIcons(ctx) {
341
498
  // --- icon actions (the dispatcher, as a table) ---------------------------------------------
342
499
  const ICON_ACTS = {
343
500
  blind: () => {
344
- ctx.setModoCego(!ctx.getModoCego());
501
+ setModoCego(!ctx.getModoCego());
345
502
  ctx.srSay(t(ctx.getModoCego() ? 'sr.icon.blindOn' : 'sr.icon.blindOff'));
346
503
  },
347
504
  tts: () => {
@@ -360,22 +517,35 @@ export function initPauseIcons(ctx) {
360
517
  },
361
518
  tea: () => {
362
519
  calmMode = nextCalmMode(calmMode);
520
+ store.set(store.KEYS.tea, calmMode); // ADR-0028: todo menu persiste. Ver a nota no `let` acima.
363
521
  applyCalm();
364
522
  ctx.srSay(t('sr.icon.tea', { v: t(CALM_NAMES[calmMode]) }));
365
523
  },
366
524
  altmove: (i) => {
367
525
  // verbatim: `players[i].toggleMove` with no `||{}` guard (unlike contrast/cvd below).
368
- ctx.setToggleMove(i, !P()[i].toggleMove);
526
+ setToggleMove(i, !P()[i].toggleMove);
369
527
  },
528
+ // ⚠️ OS DOIS ÍCONES DEIXARAM DE SE APAGAR UM AO OUTRO (#104). Eles SEMPRE ciclaram dentro do seu eixo —
529
+ // `nextContrast` e `nextCvd` existem separados desde sempre —, mas escreviam os dois no mesmo campo, e
530
+ // por isso mexer num zerava o outro. O snapshot dizia isso como se fosse desenho: «they overwrite each
531
+ // other; that is by design». Agora cada um escreve no seu eixo e o outro fica onde estava.
532
+ // ⚠️ AS DUAS GUARDAS NÃO SÃO CINTO E SUSPENSÓRIOS. Sem escritor, o ícone nem sequer é montado
533
+ // (`iconesDoJogo`), então este ramo não deveria ser alcançável pela barra — mas `iconAct` é EXPORTADO e
534
+ // qualquer consumidor pode chamá-lo por chave. Sem a guarda, essa chamada rebentaria; com ela, não faz
535
+ // nada e não anuncia — que é o mesmo que dizer a verdade: este jogo não tem por onde.
370
536
  contrast: (i) => {
371
- const nx = nextContrast((P()[i] || {}).viz);
372
- ctx.setPlayerViz(i, nx);
373
- ctx.srSay(t('sr.visual.contrast', { v: t(CONTRAST_LABELS[nx] || 'contrast.off') }));
537
+ if (!ctx.setTemaDoJogador)
538
+ return;
539
+ const v = proximoTema((P()[i] || {}).visual ?? PADRAO);
540
+ ctx.setTemaDoJogador(i, v.tema);
541
+ ctx.srSay(t('sr.visual.contrast', { v: t(CURTO_DO_TEMA[v.tema]) }));
374
542
  },
375
543
  cvd: (i) => {
376
- const nx = nextCvd((P()[i] || {}).viz);
377
- ctx.setPlayerViz(i, nx.mode);
378
- ctx.srSay(t('sr.icon.cvd', { v: t(CVD_NAMES[nx.idx]) }));
544
+ if (!ctx.setCorrecaoDoJogador)
545
+ return;
546
+ const v = proximaCorrecao((P()[i] || {}).visual ?? PADRAO);
547
+ ctx.setCorrecaoDoJogador(i, v.correcao);
548
+ ctx.srSay(t('sr.icon.cvd', { v: t(CURTO_DA_CORRECAO[v.correcao]) }));
379
549
  },
380
550
  };
381
551
  function iconAct(k, i) {
@@ -388,6 +558,17 @@ export function initPauseIcons(ctx) {
388
558
  ctx.srAlert(t('sr.icon.needsPrivateOutput'));
389
559
  return;
390
560
  }
561
+ // ⚠️ A MESMA RECUSA DO PAINEL, na mesma forma que as duas acima: DIZER e voltar. Aceitar o clique e
562
+ // ignorá-lo é a outra metade do que o ADR-0076 proíbe, e aqui há um agravante — este ícone e o
563
+ // `#opt-altmove` escrevem o MESMO valor, logo um a aceitar enquanto o outro recusa daria à criança dois
564
+ // botões que discordam sobre o mesmo ajuste.
565
+ if (k === 'altmove') {
566
+ const recusa = recusaAgora(i);
567
+ if (recusa) {
568
+ ctx.srAlert(t(recusa.chave));
569
+ return;
570
+ }
571
+ }
391
572
  const act = ICON_ACTS[k];
392
573
  if (act)
393
574
  act(i);
@@ -405,10 +586,27 @@ export function initPauseIcons(ctx) {
405
586
  b.classList.add(v.cvd);
406
587
  b.classList.toggle('pi-on', v.on);
407
588
  b.classList.toggle('pi-dis', v.dis);
589
+ /*
590
+ * 🔴 A ISSUE #128, E ELA É DE UMA LINHA: `pi-dis` é CLASSE CSS. A criança que enxerga vê o ícone
591
+ * apagado; a que navega por leitor de tela não recebe nada — o botão anuncia-se accionável e não
592
+ * responde. `aria-disabled` espelha o mesmo facto para quem ouve.
593
+ *
594
+ * ⚠️ E `aria-disabled` e NÃO `disabled`: o segundo tira o botão da ordem de tabulação, e quem navega
595
+ * por teclado deixaria de o alcançar — logo deixaria de poder ouvir POR QUE ele não responde. É a
596
+ * mesma escolha que o `#opt-altmove` faz no painel, pela mesma razão.
597
+ */
598
+ if (v.dis)
599
+ b.setAttribute('aria-disabled', 'true');
600
+ else
601
+ b.removeAttribute('aria-disabled');
408
602
  b.setAttribute('aria-pressed', String(v.active));
409
- // `soon` buttons keep the label the markup gave them (same string) — no state to report.
410
- if (!(ICON_BY_KEY.get(k) || {}).soon)
411
- b.setAttribute('aria-label', computeIconLabel(k, st));
603
+ // ⚠️ TODO ÍCONE RECEBE RÓTULO, `soon` INCLUÍDO — e o guarda que aqui estava dizia por que não: «`soon`
604
+ // buttons keep the label the markup gave them (same string)». A segunda metade continua certa (não há
605
+ // estado a reportar), mas «mesma string» era verdade só enquanto a marcação e o reflexo corressem no
606
+ // MESMO IDIOMA — e desde que a engine passou a montar a barra (ADR-0106 etapa 2) deixam de correr.
607
+ // O `initI18n` carrega en/es de forma assíncrona; a marcação nasce em pt e só o reflexo a corrige.
608
+ // 📌 Medido num navegador: cinco ícones em inglês e três ainda em «(em construção)», na mesma barra.
609
+ b.setAttribute('aria-label', computeIconLabel(k, st));
412
610
  }
413
611
  function reflectIconsIn(root, i) {
414
612
  if (!root)
@@ -419,8 +617,48 @@ export function initPauseIcons(ctx) {
419
617
  * Reflete os ícones de TODAS as telas. Varre as BARRAS e não mais os cartões de pausa: desde o item 7 do
420
618
  * ADR-0044 os ícones vivem no HUD, e um cartão de pausa não contém `.pi-btn` nenhum.
421
619
  */
620
+ /**
621
+ * OS CARTÕES QUE ESTA INSTÂNCIA CONSTRUIU. Privado, e é a resposta a um problema que eu próprio criei.
622
+ *
623
+ * ⚠️ O `PauseIconsCtx` tinha um `getPauseScreens` e eu removi-o hoje, com razão: tinha ZERO leitores. Agora
624
+ * este módulo precisa de alcançar os cartões — e a saída certa NÃO é repor o campo. Quem os construiu foi
625
+ * ele; guardar o que construiu não pede nada a consumidor nenhum, e um campo de ctx é mais uma coisa que
626
+ * cada um dos 300 jogos teria de se lembrar de passar.
627
+ */
628
+ const cartoes = [];
629
+ /**
630
+ * ESCONDE OS ITENS QUE ESTE JOGO NÃO CONSEGUE ACCIONAR — recalculado, e não decidido no arranque.
631
+ *
632
+ * ⚠️ ESTA FUNÇÃO EXISTE POR UM DEFEITO DE TEMPO. O cartão era FILTRADO no `buildScreenPause`, e o
633
+ * `getPauseActs` é um getter precisamente porque a tabela CHEGA TARDE — «`pauseActs` is a `const` declared
634
+ * far below the init site», diz o próprio campo. Um consumidor que siga esse padrão documentado montava um
635
+ * cartão sem os itens cuja acção só existiu depois do boot, e nunca mais os recuperava.
636
+ *
637
+ * 📌 A avaliação passou para o ÚLTIMO instante possível: o `ui/shell` chama `reflectPauseIcons()` quando a
638
+ * fase vira `pause-menu`, ou seja quando a pausa ABRE. O §5 continua respeitado — a criança nunca vê um
639
+ * item que não acciona —, e agora também vê os que passaram a accionar.
640
+ */
641
+ function refrescarItensDaPausa() {
642
+ const acts = getPauseActs();
643
+ const raiz = ctx.pmButtons ?? PM_BTNS;
644
+ const opcoes = ctx.optionsButtons ?? PM_OPTIONS_BTNS;
645
+ const vivos = new Set([
646
+ ...raizQueAcciona(raiz, opcoes, acts).map((b) => b.act),
647
+ ...itensQueAccionam(opcoes, acts).map((b) => b.act),
648
+ ]);
649
+ // ⚠️ `filter(Boolean)`: os cartões são indexados por JOGADOR, e montar só a tela 2 deixa um buraco no
650
+ // índice 0. Um `for…of` sobre array esparso entrega `undefined`, e foi o que rebentou à primeira.
651
+ for (const cartao of cartoes.filter(Boolean)) {
652
+ for (const btn of cartao.querySelectorAll('.pm-btn')) {
653
+ // ⚠️ `hidden` e não `remove()`: reaparecer tem de ser possível, porque a tabela pode crescer outra vez
654
+ // (um jogo que só liga «sair» depois da primeira fase). Remover seria decidir uma vez de novo.
655
+ btn.hidden = !vivos.has(btn.dataset.act ?? '');
656
+ }
657
+ }
658
+ }
422
659
  function reflectPauseIcons() {
423
660
  ctx.getA11yBars().forEach((bar, i) => reflectIconsIn(bar, i));
661
+ refrescarItensDaPausa();
424
662
  }
425
663
  // --- the pause screen ----------------------------------------------------------------------
426
664
  /**
@@ -472,7 +710,7 @@ export function initPauseIcons(ctx) {
472
710
  if (!bar || !primeiro)
473
711
  return;
474
712
  naBarra.add(i);
475
- const acts = ctx.getPauseActs();
713
+ const acts = getPauseActs();
476
714
  if (acts.resume)
477
715
  acts.resume(); // volta à tela normal: o modo é para usar DURANTE a partida
478
716
  ctx.srSay(t('sr.a11y.barEnter'));
@@ -516,7 +754,7 @@ export function initPauseIcons(ctx) {
516
754
  const cur = iconeSelecionado(bar);
517
755
  const idx = cur ? icones.indexOf(cur) : 0;
518
756
  if (acao === 'ativar') {
519
- ctx.setPauseActor(i);
757
+ setPauseActor(i);
520
758
  if (cur)
521
759
  cur.click();
522
760
  return;
@@ -527,19 +765,36 @@ export function initPauseIcons(ctx) {
527
765
  }
528
766
  }
529
767
  function buildScreenPause(i) {
530
- const sp = document.createElement('div');
768
+ const sp = docDaMontagem().createElement('div');
531
769
  sp.className = 'screen-pause';
532
770
  sp.hidden = true;
533
771
  sp.dataset.player = String(i);
772
+ /*
773
+ * ⚠️ MONTA A LISTA INTEIRA E ESCONDE DEPOIS — e a versão anterior desta linha FILTRAVA aqui, o que estava
774
+ * errado por uma razão de TEMPO. O comentário que estava neste sítio dizia «montar é o primeiro instante
775
+ * em que a resposta existe»; não é. O `getPauseActs` é um getter precisamente porque a tabela chega
776
+ * TARDE — «`pauseActs` is a `const` declared far below the init site», diz o próprio campo —, e o
777
+ * `createGame` monta durante o próprio `createGame(...)`. Filtrar aqui apagava para sempre todo item cuja
778
+ * acção só passou a existir depois do boot.
779
+ *
780
+ * Quem decide o que se VÊ é o `refrescarItensDaPausa`, a cada `reflectPauseIcons()` — que o `ui/shell`
781
+ * dispara quando a fase vira `pause-menu`, ou seja quando a pausa ABRE. O §5 continua respeitado (a
782
+ * criança nunca vê um item que não acciona) e agora também vê os que passaram a accionar.
783
+ */
534
784
  sp.innerHTML = screenPauseMarkup({
535
- player: i, numPlayers: ctx.getNumPlayers(), pmButtons: ctx.pmButtons, optionsButtons: ctx.optionsButtons,
536
- dynLabel: ctx.dynLabel, t,
785
+ player: i,
786
+ numPlayers: ctx.getNumPlayers(),
787
+ pmButtons: ctx.pmButtons ?? PM_BTNS,
788
+ optionsButtons: ctx.optionsButtons ?? PM_OPTIONS_BTNS,
789
+ dynLabel: dynLabel, t,
537
790
  });
791
+ cartoes[i] = sp;
792
+ refrescarItensDaPausa(); // o §5 vale já na montagem, e não só na primeira abertura
538
793
  sp.addEventListener('click', (e) => {
539
794
  const target = e.target;
540
795
  const b = target && target.closest('.pm-btn');
541
796
  if (b) {
542
- ctx.setPauseActor(i);
797
+ setPauseActor(i);
543
798
  const act = b.dataset.act || '';
544
799
  // NAVEGAÇÃO DENTRO DO CARTÃO fica aqui, e não na tabela de ações: `options` e `pmback` não fazem nada
545
800
  // ao jogo — trocam qual lista está na tela. A tabela vive em `ui/shell`, que não conhece este `sp`.
@@ -554,7 +809,7 @@ export function initPauseIcons(ctx) {
554
809
  entrarNaBarra(i);
555
810
  return;
556
811
  }
557
- const acts = ctx.getPauseActs();
812
+ const acts = getPauseActs();
558
813
  const fn = acts[act];
559
814
  if (fn)
560
815
  fn();
@@ -571,10 +826,10 @@ export function initPauseIcons(ctx) {
571
826
  * facilidade não existe —, e degradar o que existe para DAR acesso seria o contrário do que ele serve.
572
827
  */
573
828
  function buildQuickBar(i) {
574
- const bar = document.createElement('div');
829
+ const bar = docDaMontagem().createElement('div');
575
830
  bar.className = 'screen-a11y';
576
831
  bar.dataset.player = String(i);
577
- bar.innerHTML = quickBarMarkup();
832
+ bar.innerHTML = quickBarMarkup(iconesDoJogo);
578
833
  // FORA DA ORDEM DE TABULAÇÃO durante a partida (ADR-0044, item 7). Dez paradas entre a criança e o jogo
579
834
  // seria o preço de deixá-los lá — e o alcance por teclado não se perde: ele passa a ser o modo
580
835
  // `accessibility`, que se abre pela pausa. A barra do TÍTULO não é afetada: lá não se está jogando, e o
@@ -585,7 +840,7 @@ export function initPauseIcons(ctx) {
585
840
  const ib = e.target?.closest('.pi-btn');
586
841
  if (!ib)
587
842
  return;
588
- ctx.setPauseActor(i);
843
+ setPauseActor(i);
589
844
  iconAct(ib.dataset.pi || '', i);
590
845
  reflectPauseIcons(); // must run BEFORE reading the label back — that is what makes the caption honest
591
846
  if (cap)
@@ -614,7 +869,14 @@ export function initPauseIcons(ctx) {
614
869
  }
615
870
  return {
616
871
  buildScreenPause, buildQuickBar, entrarNaBarra, sairDaBarra, naBarraDe, navBar,
872
+ iconesMontados: iconesDoJogo,
617
873
  iconAct, iconLabel, reflectIconBtn, reflectIconsIn, reflectPauseIcons,
618
- applyCalm, getCalmMode: () => calmMode, setCalmMode: (n) => { calmMode = n; }, iconState,
874
+ // ⚠️ O `setCalmMode` PERSISTE TAMBÉM, e sanea. Ele é a outra porta para o mesmo valor — se só o ciclo do
875
+ // ícone gravasse, um nível posto por aqui sobreviveria à sessão e não ao fecho da aba, que é a metade
876
+ // pior do defeito: o ajuste parece ter pegado e some depois.
877
+ applyCalm,
878
+ getCalmMode: () => calmMode,
879
+ setCalmMode: (n) => { calmMode = saneiaNivelTea(n); store.set(store.KEYS.tea, calmMode); },
880
+ iconState,
619
881
  };
620
882
  }
@@ -17,6 +17,14 @@ export function linhasDoAviso(a, t) {
17
17
  for (const c of a.curtos) {
18
18
  linhas.push(t('reach.curto', { transporte: nome(c.id), lugares: c.slots }));
19
19
  }
20
+ // ⚠️ A TERCEIRA FRASE, e ela existe porque um transporte pode CHEGAR a todas as ações e ainda assim não
21
+ // deixar a criança jogar (ADR-0104). Medido: a plataforma pede nove ações, o controle de tela tem nove
22
+ // lugares, e o cartão nunca aparecia — mas correr, andar e pular ao mesmo tempo são três dedos, e um
23
+ // telemóvel que reconhece dois não os dá. A criança tentava, não acontecia nada, e concluía que o jogo
24
+ // estava partido. Uma frase antes de começar é a resposta honesta; meia tela jogável não é.
25
+ for (const s of a.naoSeguram) {
26
+ linhas.push(t('reach.naoSegura', { transporte: nome(s.id), segura: s.holds, pedidas: a.seguraPedidas }));
27
+ }
20
28
  linhas.push(a.serviriamSeLigados.length
21
29
  ? t('reach.ligue', { saida: a.serviriamSeLigados.map(nome).join(t('reach.ou')) })
22
30
  : t('reach.semSaida'));
@@ -74,10 +74,23 @@ export interface SettingsAudioCtx {
74
74
  /** Core collision state (NOT owned by this panel — core/collision.ts reads it via isModoCego). The toggle's
75
75
  * widget lives inside #audio; the state and its gameplay side effects (setupExtras) stay in game.js. */
76
76
  getModoCego: () => boolean;
77
- setModoCego: (on: boolean) => void;
77
+ setModoCego?: (on: boolean) => void;
78
78
  /** Core collision state (cane hit spacing). Same reasoning as modo cego. */
79
79
  getCaneBlockDiv: () => number;
80
80
  setCaneBlockDiv: (div: number) => void;
81
+ /**
82
+ * Move a prosa das linhas para o rodapé (`ui/settings-panel` → `fillExplain`). Chamado a CADA render.
83
+ *
84
+ * ⚠️ NÃO É OPCIONAL POR ELEGÂNCIA: `fillExplain` roda uma vez quando o overlay é frontalizado e move o
85
+ * `.opt-hint` de dentro de cada linha para o rodapé. Este painel RECONSTRÓI as linhas, e as linhas novas
86
+ * voltam com a prosa lá dentro — então a explicação aparece duas vezes, no rodapé e sob o rótulo, a
87
+ * partir do primeiro clique. O `CLAUDE.md` §4 regista exatamente isto, e a issue #109 já o consertou
88
+ * uma vez noutros painéis.
89
+ *
90
+ * Opcional na assinatura porque um consumidor pode montar o painel sem a casca (um teste, o segundo
91
+ * consumidor): sem casca não há rodapé para duplicar.
92
+ */
93
+ fillExplain?: (card: HTMLElement | null) => void;
81
94
  }
82
95
  export interface SettingsAudioApi {
83
96
  /** Re-renders the whole #audio overlay content (master, categories, nav-sound, TTS panel, sinks, modo-cego and