@the-inclusionist/engine 6.36.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 (267) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +107 -0
  3. package/app/css/style.css +622 -0
  4. package/app/public/vendor/fonts/andika-400-ext.woff2 +0 -0
  5. package/app/public/vendor/fonts/andika-400.woff2 +0 -0
  6. package/app/public/vendor/fonts/andika-700-ext.woff2 +0 -0
  7. package/app/public/vendor/fonts/andika-700.woff2 +0 -0
  8. package/app/public/vendor/fonts/atkinson-400-ext.woff2 +0 -0
  9. package/app/public/vendor/fonts/atkinson-400.woff2 +0 -0
  10. package/app/public/vendor/fonts/atkinson-700-ext.woff2 +0 -0
  11. package/app/public/vendor/fonts/atkinson-700.woff2 +0 -0
  12. package/app/public/vendor/fonts/atkinson-mono-400-ext.woff2 +0 -0
  13. package/app/public/vendor/fonts/atkinson-mono-400.woff2 +0 -0
  14. package/app/public/vendor/fonts/comicneue-400.woff2 +0 -0
  15. package/app/public/vendor/fonts/comicneue-700.woff2 +0 -0
  16. package/app/public/vendor/fonts/greatvibes-400.woff2 +0 -0
  17. package/app/public/vendor/fonts/inter-400.woff2 +0 -0
  18. package/app/public/vendor/fonts/inter-700.woff2 +0 -0
  19. package/app/public/vendor/fonts/lato-400.woff2 +0 -0
  20. package/app/public/vendor/fonts/lato-700.woff2 +0 -0
  21. package/app/public/vendor/fonts/lexend-400-ext.woff2 +0 -0
  22. package/app/public/vendor/fonts/lexend-400.woff2 +0 -0
  23. package/app/public/vendor/fonts/lexend-700-ext.woff2 +0 -0
  24. package/app/public/vendor/fonts/lexend-700.woff2 +0 -0
  25. package/app/public/vendor/fonts/literata-400.woff2 +0 -0
  26. package/app/public/vendor/fonts/literata-700.woff2 +0 -0
  27. package/app/public/vendor/fonts/newsreader-400.woff2 +0 -0
  28. package/app/public/vendor/fonts/newsreader-700.woff2 +0 -0
  29. package/app/public/vendor/fonts/opensans-400.woff2 +0 -0
  30. package/app/public/vendor/fonts/opensans-700.woff2 +0 -0
  31. package/app/public/vendor/fonts/pinyon-400.woff2 +0 -0
  32. package/app/public/vendor/fonts/quattro-400.woff2 +0 -0
  33. package/app/public/vendor/fonts/quattro-700.woff2 +0 -0
  34. package/app/public/vendor/fonts/sourcesans-400.woff2 +0 -0
  35. package/app/public/vendor/fonts/sourcesans-700.woff2 +0 -0
  36. package/app/public/vendor/fonts/sourceserif-400.woff2 +0 -0
  37. package/app/public/vendor/fonts/sourceserif-700.woff2 +0 -0
  38. package/app/public/vendor/fonts/ufcook-700.woff2 +0 -0
  39. package/app/public/vendor/fonts/ufmag-400.woff2 +0 -0
  40. package/app/public/vendor/fonts.css +88 -0
  41. package/dist-pkg/boot/create-game.d.ts +159 -0
  42. package/dist-pkg/boot/create-game.js +266 -0
  43. package/dist-pkg/core/a11y-sr.d.ts +4 -0
  44. package/dist-pkg/core/a11y-sr.js +20 -0
  45. package/dist-pkg/core/actions.d.ts +100 -0
  46. package/dist-pkg/core/actions.js +149 -0
  47. package/dist-pkg/core/anel.d.ts +17 -0
  48. package/dist-pkg/core/anel.js +31 -0
  49. package/dist-pkg/core/collision.d.ts +21 -0
  50. package/dist-pkg/core/collision.js +62 -0
  51. package/dist-pkg/core/constants.d.ts +84 -0
  52. package/dist-pkg/core/constants.js +64 -0
  53. package/dist-pkg/core/contract.d.ts +227 -0
  54. package/dist-pkg/core/contract.js +184 -0
  55. package/dist-pkg/core/dom-query.d.ts +9 -0
  56. package/dist-pkg/core/dom-query.js +25 -0
  57. package/dist-pkg/core/entity.d.ts +150 -0
  58. package/dist-pkg/core/entity.js +38 -0
  59. package/dist-pkg/core/escape-html.d.ts +16 -0
  60. package/dist-pkg/core/escape-html.js +32 -0
  61. package/dist-pkg/core/i18n.d.ts +71 -0
  62. package/dist-pkg/core/i18n.js +236 -0
  63. package/dist-pkg/core/layers.d.ts +39 -0
  64. package/dist-pkg/core/layers.js +71 -0
  65. package/dist-pkg/core/letter-grid.d.ts +55 -0
  66. package/dist-pkg/core/letter-grid.js +116 -0
  67. package/dist-pkg/core/loop.d.ts +17 -0
  68. package/dist-pkg/core/loop.js +36 -0
  69. package/dist-pkg/core/password.d.ts +34 -0
  70. package/dist-pkg/core/password.js +205 -0
  71. package/dist-pkg/core/rng.d.ts +20 -0
  72. package/dist-pkg/core/rng.js +61 -0
  73. package/dist-pkg/core/rotulo-acessivel.d.ts +17 -0
  74. package/dist-pkg/core/rotulo-acessivel.js +41 -0
  75. package/dist-pkg/core/run-state.d.ts +115 -0
  76. package/dist-pkg/core/run-state.js +40 -0
  77. package/dist-pkg/core/scenes.d.ts +68 -0
  78. package/dist-pkg/core/scenes.js +64 -0
  79. package/dist-pkg/core/screens.d.ts +16 -0
  80. package/dist-pkg/core/screens.js +28 -0
  81. package/dist-pkg/core/state.d.ts +138 -0
  82. package/dist-pkg/core/state.js +290 -0
  83. package/dist-pkg/core/tiles.d.ts +12 -0
  84. package/dist-pkg/core/tiles.js +61 -0
  85. package/dist-pkg/core/world.d.ts +4 -0
  86. package/dist-pkg/core/world.js +58 -0
  87. package/dist-pkg/educational/activities-registry.d.ts +47 -0
  88. package/dist-pkg/educational/activities-registry.js +95 -0
  89. package/dist-pkg/i18n/en.d.ts +3 -0
  90. package/dist-pkg/i18n/en.js +560 -0
  91. package/dist-pkg/i18n/es.d.ts +3 -0
  92. package/dist-pkg/i18n/es.js +558 -0
  93. package/dist-pkg/i18n/pt.d.ts +3 -0
  94. package/dist-pkg/i18n/pt.js +618 -0
  95. package/dist-pkg/input/default-bindings.d.ts +24 -0
  96. package/dist-pkg/input/default-bindings.js +122 -0
  97. package/dist-pkg/input/devices.d.ts +15 -0
  98. package/dist-pkg/input/devices.js +31 -0
  99. package/dist-pkg/input/edges.d.ts +58 -0
  100. package/dist-pkg/input/edges.js +49 -0
  101. package/dist-pkg/input/gamepad.d.ts +203 -0
  102. package/dist-pkg/input/gamepad.js +531 -0
  103. package/dist-pkg/input/keyboard-runtime.d.ts +61 -0
  104. package/dist-pkg/input/keyboard-runtime.js +83 -0
  105. package/dist-pkg/input/keyboard.d.ts +45 -0
  106. package/dist-pkg/input/keyboard.js +80 -0
  107. package/dist-pkg/input/keydown.d.ts +270 -0
  108. package/dist-pkg/input/keydown.js +381 -0
  109. package/dist-pkg/input/latch.d.ts +30 -0
  110. package/dist-pkg/input/latch.js +63 -0
  111. package/dist-pkg/input/pointer-space.d.ts +40 -0
  112. package/dist-pkg/input/pointer-space.js +51 -0
  113. package/dist-pkg/input/state.d.ts +11 -0
  114. package/dist-pkg/input/state.js +12 -0
  115. package/dist-pkg/input/touch-bindings.d.ts +210 -0
  116. package/dist-pkg/input/touch-bindings.js +365 -0
  117. package/dist-pkg/input/touch.d.ts +173 -0
  118. package/dist-pkg/input/touch.js +314 -0
  119. package/dist-pkg/input/transports.d.ts +90 -0
  120. package/dist-pkg/input/transports.js +96 -0
  121. package/dist-pkg/input/vocabulary-migration.d.ts +58 -0
  122. package/dist-pkg/input/vocabulary-migration.js +125 -0
  123. package/dist-pkg/platform/audio-ambient.d.ts +27 -0
  124. package/dist-pkg/platform/audio-ambient.js +89 -0
  125. package/dist-pkg/platform/audio-earcons.d.ts +24 -0
  126. package/dist-pkg/platform/audio-earcons.js +62 -0
  127. package/dist-pkg/platform/audio-jingles.d.ts +17 -0
  128. package/dist-pkg/platform/audio-jingles.js +54 -0
  129. package/dist-pkg/platform/audio-mixer.d.ts +14 -0
  130. package/dist-pkg/platform/audio-mixer.js +62 -0
  131. package/dist-pkg/platform/audio-nav.d.ts +51 -0
  132. package/dist-pkg/platform/audio-nav.js +99 -0
  133. package/dist-pkg/platform/audio-sonar.d.ts +71 -0
  134. package/dist-pkg/platform/audio-sonar.js +173 -0
  135. package/dist-pkg/platform/audio.d.ts +32 -0
  136. package/dist-pkg/platform/audio.js +191 -0
  137. package/dist-pkg/platform/interruptible-speech.d.ts +19 -0
  138. package/dist-pkg/platform/interruptible-speech.js +79 -0
  139. package/dist-pkg/platform/speech.d.ts +2 -0
  140. package/dist-pkg/platform/speech.js +35 -0
  141. package/dist-pkg/platform/storage.d.ts +87 -0
  142. package/dist-pkg/platform/storage.js +148 -0
  143. package/dist-pkg/platform/tts.d.ts +33 -0
  144. package/dist-pkg/platform/tts.js +142 -0
  145. package/dist-pkg/render/camera.d.ts +58 -0
  146. package/dist-pkg/render/camera.js +105 -0
  147. package/dist-pkg/render/canvas.d.ts +13 -0
  148. package/dist-pkg/render/canvas.js +26 -0
  149. package/dist-pkg/render/cenario-data.d.ts +127 -0
  150. package/dist-pkg/render/cenario-data.js +145 -0
  151. package/dist-pkg/render/city-tex.d.ts +63 -0
  152. package/dist-pkg/render/city-tex.js +228 -0
  153. package/dist-pkg/render/city-tiles.d.ts +11 -0
  154. package/dist-pkg/render/city-tiles.js +140 -0
  155. package/dist-pkg/render/crt.d.ts +19 -0
  156. package/dist-pkg/render/crt.js +92 -0
  157. package/dist-pkg/render/cvd-matrices.d.ts +35 -0
  158. package/dist-pkg/render/cvd-matrices.js +91 -0
  159. package/dist-pkg/render/draw.d.ts +149 -0
  160. package/dist-pkg/render/draw.js +221 -0
  161. package/dist-pkg/render/fx.d.ts +63 -0
  162. package/dist-pkg/render/fx.js +123 -0
  163. package/dist-pkg/render/hc-role-data.d.ts +14 -0
  164. package/dist-pkg/render/hc-role-data.js +24 -0
  165. package/dist-pkg/render/high-contrast.d.ts +109 -0
  166. package/dist-pkg/render/high-contrast.js +254 -0
  167. package/dist-pkg/render/lq-filter.d.ts +37 -0
  168. package/dist-pkg/render/lq-filter.js +94 -0
  169. package/dist-pkg/render/minimap.d.ts +11 -0
  170. package/dist-pkg/render/minimap.js +94 -0
  171. package/dist-pkg/render/parallax.d.ts +85 -0
  172. package/dist-pkg/render/parallax.js +158 -0
  173. package/dist-pkg/render/player-anim.d.ts +71 -0
  174. package/dist-pkg/render/player-anim.js +152 -0
  175. package/dist-pkg/render/port.d.ts +143 -0
  176. package/dist-pkg/render/port.js +29 -0
  177. package/dist-pkg/render/recycling-tex.d.ts +48 -0
  178. package/dist-pkg/render/recycling-tex.js +164 -0
  179. package/dist-pkg/render/scene-city.d.ts +77 -0
  180. package/dist-pkg/render/scene-city.js +181 -0
  181. package/dist-pkg/render/scene-parallax.d.ts +72 -0
  182. package/dist-pkg/render/scene-parallax.js +426 -0
  183. package/dist-pkg/render/scene-sky.d.ts +126 -0
  184. package/dist-pkg/render/scene-sky.js +295 -0
  185. package/dist-pkg/render/screen-pipeline.d.ts +134 -0
  186. package/dist-pkg/render/screen-pipeline.js +177 -0
  187. package/dist-pkg/render/set-cenario.d.ts +54 -0
  188. package/dist-pkg/render/set-cenario.js +103 -0
  189. package/dist-pkg/render/sprite-fx.d.ts +3 -0
  190. package/dist-pkg/render/sprite-fx.js +40 -0
  191. package/dist-pkg/render/textures.d.ts +75 -0
  192. package/dist-pkg/render/textures.js +240 -0
  193. package/dist-pkg/render/title-scene.d.ts +46 -0
  194. package/dist-pkg/render/title-scene.js +75 -0
  195. package/dist-pkg/render/viewports.d.ts +52 -0
  196. package/dist-pkg/render/viewports.js +185 -0
  197. package/dist-pkg/render/viz-axes.d.ts +98 -0
  198. package/dist-pkg/render/viz-axes.js +184 -0
  199. package/dist-pkg/render/viz-modes.d.ts +61 -0
  200. package/dist-pkg/render/viz-modes.js +74 -0
  201. package/dist-pkg/render/viz-setters.d.ts +130 -0
  202. package/dist-pkg/render/viz-setters.js +244 -0
  203. package/dist-pkg/render/weather.d.ts +96 -0
  204. package/dist-pkg/render/weather.js +171 -0
  205. package/dist-pkg/render/wheelchair-sprites.d.ts +24 -0
  206. package/dist-pkg/render/wheelchair-sprites.js +63 -0
  207. package/dist-pkg/render/world-tex.d.ts +18 -0
  208. package/dist-pkg/render/world-tex.js +87 -0
  209. package/dist-pkg/ui/activities-menu.d.ts +258 -0
  210. package/dist-pkg/ui/activities-menu.js +648 -0
  211. package/dist-pkg/ui/caa-sets.d.ts +39 -0
  212. package/dist-pkg/ui/caa-sets.js +65 -0
  213. package/dist-pkg/ui/changed-mark.d.ts +15 -0
  214. package/dist-pkg/ui/changed-mark.js +96 -0
  215. package/dist-pkg/ui/debug-panel.d.ts +67 -0
  216. package/dist-pkg/ui/debug-panel.js +173 -0
  217. package/dist-pkg/ui/dom.d.ts +24 -0
  218. package/dist-pkg/ui/dom.js +47 -0
  219. package/dist-pkg/ui/focus-trap.d.ts +71 -0
  220. package/dist-pkg/ui/focus-trap.js +99 -0
  221. package/dist-pkg/ui/fonts.d.ts +41 -0
  222. package/dist-pkg/ui/fonts.js +59 -0
  223. package/dist-pkg/ui/hud.d.ts +137 -0
  224. package/dist-pkg/ui/hud.js +229 -0
  225. package/dist-pkg/ui/item-announcement.d.ts +19 -0
  226. package/dist-pkg/ui/item-announcement.js +36 -0
  227. package/dist-pkg/ui/layout.d.ts +6 -0
  228. package/dist-pkg/ui/layout.js +54 -0
  229. package/dist-pkg/ui/loop-crash.d.ts +19 -0
  230. package/dist-pkg/ui/loop-crash.js +60 -0
  231. package/dist-pkg/ui/map-hub.d.ts +56 -0
  232. package/dist-pkg/ui/map-hub.js +138 -0
  233. package/dist-pkg/ui/menu-nav.d.ts +141 -0
  234. package/dist-pkg/ui/menu-nav.js +390 -0
  235. package/dist-pkg/ui/pause-icons.d.ts +327 -0
  236. package/dist-pkg/ui/pause-icons.js +620 -0
  237. package/dist-pkg/ui/reach-notice.d.ts +34 -0
  238. package/dist-pkg/ui/reach-notice.js +71 -0
  239. package/dist-pkg/ui/settings-audio.d.ts +139 -0
  240. package/dist-pkg/ui/settings-audio.js +544 -0
  241. package/dist-pkg/ui/settings-caa.d.ts +45 -0
  242. package/dist-pkg/ui/settings-caa.js +137 -0
  243. package/dist-pkg/ui/settings-controls.d.ts +100 -0
  244. package/dist-pkg/ui/settings-controls.js +152 -0
  245. package/dist-pkg/ui/settings-empathy.d.ts +62 -0
  246. package/dist-pkg/ui/settings-empathy.js +137 -0
  247. package/dist-pkg/ui/settings-motion.d.ts +95 -0
  248. package/dist-pkg/ui/settings-motion.js +282 -0
  249. package/dist-pkg/ui/settings-motor.d.ts +77 -0
  250. package/dist-pkg/ui/settings-motor.js +215 -0
  251. package/dist-pkg/ui/settings-panel.d.ts +76 -0
  252. package/dist-pkg/ui/settings-panel.js +187 -0
  253. package/dist-pkg/ui/settings-typo.d.ts +74 -0
  254. package/dist-pkg/ui/settings-typo.js +150 -0
  255. package/dist-pkg/ui/settings-visual.d.ts +111 -0
  256. package/dist-pkg/ui/settings-visual.js +252 -0
  257. package/dist-pkg/ui/shell.d.ts +239 -0
  258. package/dist-pkg/ui/shell.js +430 -0
  259. package/dist-pkg/ui/title.d.ts +36 -0
  260. package/dist-pkg/ui/title.js +60 -0
  261. package/dist-pkg/ui/vlibras.d.ts +11 -0
  262. package/dist-pkg/ui/vlibras.js +87 -0
  263. package/dist-pkg/ui/webcam.d.ts +7 -0
  264. package/dist-pkg/ui/webcam.js +88 -0
  265. package/docs/CREDITS.md +64 -0
  266. package/docs/LICENSES.md +132 -0
  267. package/package.json +115 -0
@@ -0,0 +1,150 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // ui/settings-typo — Typography panel (Estágio 4): extracted from game.js's renderTypo()/setGameFont(). Pure
3
+ // logic (catalog view-model, key→CSS-target mapping, persisted-value validation) is separated from the thin
4
+ // DOM-touching render()/setFont(). DI via initSettingsTypo(ctx): `$` (DOM selector), `srSay`, `store`
5
+ // (platform/storage shape) and `root` (the element the chosen font is applied to — document.documentElement in
6
+ // production). Overlay open/close plumbing (frontOverlay, focus management, the #typo hidden toggle, Escape
7
+ // handling) is the SHARED helper used by every settings panel and stays in game.js. The font catalog itself
8
+ // (FONT_GROUPS/FONT_BY_KEY) stays in ./fonts.js (Phase 2 extraction) — imported here, never duplicated.
9
+ import { toggleLabel } from './dom.js';
10
+ import { t } from '../core/i18n.js';
11
+ import { FONT_GROUPS, FONT_BY_KEY, DEFAULT_FONT_KEY } from './fonts.js';
12
+ import { markChanged, markMenuChanged, CHANGED_CLASS } from './changed-mark.js';
13
+ // ---------------------------------------------------------------------------------------------
14
+ // Pure logic (no `document`, testable in node)
15
+ // ---------------------------------------------------------------------------------------------
16
+ /** A key is selectable when it exists in the catalog and is not marked `.off` (licence pending, etc.). */
17
+ export function isSelectableFont(k) {
18
+ const it = FONT_BY_KEY[k];
19
+ return !!it && !it.off;
20
+ }
21
+ // A semantica da chave (validacao + migracao da chave antiga) mora em ui/fonts.ts, que e o dono do
22
+ // catalogo; re-exportada aqui para quem ja consome este modulo. Uma implementacao, nao duas.
23
+ export { resolveFontKey, persistFontKey } from './fonts.js';
24
+ import { resolveFontKey, persistFontKey } from './fonts.js';
25
+ /**
26
+ * Maps a selectable font key to the CSS it drives. The three canonical EdSP fonts (atkinson/andika/lexend) use
27
+ * dedicated data-fonte values (Lexend's keeps the BDA letter/word spacing tied to data-fonte="dislexia"); every
28
+ * other catalog font goes through --font-custom with a generic fallback by family (serif/cursive/none).
29
+ * Verbatim port of the branching inside the old setGameFont().
30
+ */
31
+ export function fontCssTarget(k, it) {
32
+ if (k === 'atkinson')
33
+ return { fonte: 'padrao', customFamily: null };
34
+ if (k === 'andika')
35
+ return { fonte: 'alfabetizacao', customFamily: null };
36
+ if (k === 'lexend')
37
+ return { fonte: 'dislexia', customFamily: null };
38
+ const suffix = it.fb === 'serif' ? ',Georgia,serif' : it.fb === 'cursive' ? ',cursive' : '';
39
+ return { fonte: 'custom', customFamily: `'${it.fam}'${suffix}` };
40
+ }
41
+ /** Pure view-model for the typography list: which row is selected/disabled and its note, per catalog group. */
42
+ export function typoGroups(fontKey) {
43
+ return FONT_GROUPS.map((g) => ({
44
+ g: t(g.g), // `g` guarda CHAVE i18n desde o item 14 (ver ui/fonts)
45
+ rows: g.items.map((it) => {
46
+ const disabled = !!it.off;
47
+ // `d` e `off` também guardam CHAVE. O travessão que junta os dois é pontuação, não frase — as duas
48
+ // metades são independentes e cada uma traduz por si.
49
+ const desc = it.d ? t(it.d) : '', motivo = it.off ? t(it.off) : '';
50
+ const note = desc ? desc + (disabled ? ' — ' + motivo : '') : disabled ? motivo : '';
51
+ return { key: it.k, fam: it.fam, selected: fontKey === it.k, disabled, note };
52
+ }),
53
+ }));
54
+ }
55
+ function rowHTML(row) {
56
+ const noteHTML = row.note
57
+ ? `<br><span class="opt-hint" style="margin:0;font-family:var(--font)">${row.note}</span>`
58
+ : '';
59
+ const ariaLabel = row.fam + (row.note ? ' — ' + row.note : '');
60
+ const stateLabel = toggleLabel(row.selected);
61
+ return (`<div class="ctrl-row"><span style="font-family:'${row.fam}'"><strong>${row.fam}</strong>${noteHTML}</span>` +
62
+ `<button class="mode-btn switch${row.selected ? ' is-on' : ''}" data-font="${row.key}" type="button"` +
63
+ `${row.disabled ? ' disabled' : ''} aria-pressed="${row.selected}" aria-label="${ariaLabel}">${stateLabel}</button></div>`);
64
+ }
65
+ /** Full innerHTML for #typo-list, given the currently selected key. Pure string building — no DOM. */
66
+ export function typoListHTML(fontKey) {
67
+ return typoGroups(fontKey)
68
+ .map((group) => `<h3 class="panel-sub">${group.g}</h3>` + group.rows.map(rowHTML).join(''))
69
+ .join('');
70
+ }
71
+ // ---------------------------------------------------------------------------------------------
72
+ // DOM-facing (thin) — requires `document`/injected ctx
73
+ // ---------------------------------------------------------------------------------------------
74
+ export function initSettingsTypo(ctx) {
75
+ let fontKey = resolveFontKey(ctx.store);
76
+ function setFont(k, announce = false) {
77
+ const it = FONT_BY_KEY[k];
78
+ if (!it || it.off)
79
+ return;
80
+ fontKey = k;
81
+ persistFontKey(ctx.store, k);
82
+ const target = fontCssTarget(k, it);
83
+ ctx.root.dataset.fonte = target.fonte;
84
+ if (target.customFamily)
85
+ ctx.root.style.setProperty('--font-custom', target.customFamily);
86
+ else
87
+ ctx.root.style.removeProperty('--font-custom');
88
+ const pv = ctx.$('#typo-preview');
89
+ if (pv)
90
+ pv.style.fontFamily = `'${it.fam}'`;
91
+ if (announce)
92
+ ctx.srSay(t('sr.typo.font', { fam: it.fam }));
93
+ }
94
+ function render() {
95
+ const el = ctx.$('#typo-list');
96
+ if (!el)
97
+ return;
98
+ el.innerHTML = typoListHTML(fontKey);
99
+ el.querySelectorAll('button[data-font]').forEach((b) => {
100
+ b.addEventListener('click', () => {
101
+ const k = b.dataset.font;
102
+ if (!k)
103
+ return;
104
+ setFont(k, true);
105
+ render();
106
+ });
107
+ });
108
+ const cur = FONT_BY_KEY[fontKey];
109
+ const pv = ctx.$('#typo-preview');
110
+ if (pv && cur)
111
+ pv.style.fontFamily = `'${cur.fam}'`;
112
+ refreshMarks();
113
+ // A ÚLTIMA COISA DO RENDER, e tem de ser: as linhas acabaram de ser recriadas com a prosa dentro delas.
114
+ ctx.fillExplain?.(ctx.$('#typo .overlay__card'));
115
+ }
116
+ /**
117
+ * A marca de "saiu do padrão" (ADR-0029). Mora DENTRO do render porque é derivada, nunca guardada: ela é
118
+ * recalculada de valor-atual-contra-padrão a cada desenho, então não tem como envelhecer no armazenamento.
119
+ * Envelhecer na TELA ela tem — se algum dia alguém mudar a fonte sem redesenhar —, e é por isso que a
120
+ * atualização anda junto com quem já redesenha, e não numa função própria que se possa esquecer de chamar.
121
+ *
122
+ * Aqui a linha marcada é a da fonte ESCOLHIDA, e só quando ela não é a padrão: as outras quinze não saíram
123
+ * do padrão, foram apenas oferecidas.
124
+ */
125
+ function refreshMarks() {
126
+ const changed = fontKey !== DEFAULT_FONT_KEY;
127
+ const list = ctx.$('#typo-list');
128
+ list?.querySelectorAll('.' + CHANGED_CLASS).forEach((el) => markChanged(el, false));
129
+ const sel = list?.querySelector(`button[data-font="${fontKey}"]`);
130
+ markChanged(sel?.closest('.ctrl-row') ?? null, changed);
131
+ markMenuChanged(ctx.$('[data-act="tipo"]'), [changed]);
132
+ }
133
+ // ---- restaurar os padrões DESTE menu (ADR-0028) ----
134
+ //
135
+ // O menu mais simples dos oito: a tipografia guarda uma escolha só, então o reset é uma linha. Ainda assim
136
+ // vale dizer para onde ele volta — a Atkinson Hyperlegible não é o padrão por ser bonita, é o padrão por ter
137
+ // sido desenhada para quem tem baixa visão. Uma criança que experimentou seis fontes e não consegue mais ler
138
+ // a tela precisa de um caminho de volta que termine na MAIS legível, não numa qualquer.
139
+ //
140
+ // O anúncio nomeia a fonte porque a mudança é visível para quem enxerga e invisível para quem não enxerga.
141
+ const resetBtn = ctx.$('#typo-reset');
142
+ if (resetBtn)
143
+ resetBtn.addEventListener('click', () => {
144
+ setFont(DEFAULT_FONT_KEY, false);
145
+ render();
146
+ ctx.srSay(t('sr.typo.reset', { fam: FONT_BY_KEY[DEFAULT_FONT_KEY].fam }));
147
+ });
148
+ setFont(fontKey, false); // aplica a fonte persistida ao boot (== antigo `setGameFont(fontKey,false)`)
149
+ return { render, setFont, getFontKey: () => fontKey };
150
+ }
@@ -0,0 +1,111 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ import type { HcRoleKey } from '../render/hc-role-data.js';
3
+ import { type VizMode } from '../render/viz-modes.js';
4
+ export type { HcRoleKey as RoleKey } from '../render/hc-role-data.js';
5
+ type RoleKey = HcRoleKey;
6
+ export type RGB = readonly [number, number, number];
7
+ /** Contrast levels, in cycle order — mirrors game.js's HC_SEQ (also used there by the physical contrast-cycle button). */
8
+ export declare const CONTRAST_LEVELS: readonly string[];
9
+ /**
10
+ * TUDO que este menu escreve em `p.viz`: os 4 níveis de contraste MAIS as 3 correções de daltonismo, que
11
+ * mudaram de casa por decisão do Dev (#60). Elas moravam no Modo empatia, onde a criança daltônica precisava
12
+ * entrar no menu "sentir como é ter uma deficiência" para achar a correção da deficiência que ela tem.
13
+ *
14
+ * Os sete vivem num CONTROLE SÓ, e isso não é economia de espaço: `p.viz` guarda UM valor. Dois controles
15
+ * separados se sobrescreveriam em silêncio — a criança escolheria a correção, depois o contraste, e perderia
16
+ * a correção sem nada dizer que perdeu. Uma lista só conta a verdade sobre a exclusividade.
17
+ */
18
+ export declare const VISUAL_MODES: readonly string[];
19
+ /** Short announcement labels — mirrors game.js's HC_LABEL. */
20
+ export declare const CONTRAST_LABELS: Readonly<Record<string, string>>;
21
+ export declare const ROLE_KEYS: readonly RoleKey[];
22
+ /** Readable labels for the color-blocking roles — mirrors game.js's ROLE_LBL. */
23
+ export declare const ROLE_LABELS: Readonly<Record<RoleKey, string>>;
24
+ /** Live snapshot of the state this panel does not own — read fresh on every render(). */
25
+ export interface VisualSettings {
26
+ lq: number;
27
+ ownerColors: boolean;
28
+ cbSafe: boolean;
29
+ outlineFg: number;
30
+ outlineBg: number;
31
+ roleColors: Record<RoleKey, RGB>;
32
+ }
33
+ export interface SettingsVisualCtx {
34
+ /** Quantos jogadores/telas. Estado de RODADA (ADR-0038): vem da instância que a raiz possui.
35
+ * Era `numPlayers`, um `let` de `core/state` importado como binding vivo — e um `let` de módulo
36
+ * é compartilhado por qualquer segundo jogo que a mesma página carregue (D13 do `demos`). */
37
+ getNumPlayers: () => number;
38
+ /** Os jogadores. Estado de RODADA, pelo mesmo motivo. `readonly unknown[]` porque cada consumidor
39
+ * estreita para a SUA fatia — o tipo real é do jogo, não da engine (ADR-0033). */
40
+ getPlayers: () => readonly unknown[];
41
+ $: <T extends Element = Element>(sel: string) => T | null;
42
+ srSay: (text: string) => void;
43
+ /** Fresh read of lq/ownerColors/cbSafe/outlineFg/outlineBg/roleColors (game.js live vars). */
44
+ getVisualSettings: () => VisualSettings;
45
+ /** `selVizPlayer` — shared with the Empathy panel; owned by game.js. */
46
+ getSelectedPlayer: () => number;
47
+ setSelectedPlayer: (i: number) => void;
48
+ /** Same setPlayerViz used by the Empathy panel and the physical contrast-cycle shortcut. */
49
+ setPlayerViz: (i: number, mode: string) => void;
50
+ /** Desenha UMA lista de rádio de modos visuais (+ abas por jogador). O MESMO helper que o painel de
51
+ * empatia usa — de propósito: as três correções mudaram de menu, e mudar junto a aparência delas faria a
52
+ * criança ter de reaprender um controle que ela já conhecia. */
53
+ renderVizGroup: (listSel: string, tabsSel: string, modes: readonly VizMode[]) => void;
54
+ setLq: (t: number) => void;
55
+ setOwnerColors: (on: boolean) => void;
56
+ setCbSafe: (on: boolean) => void;
57
+ setOutlineFg: (level: number) => void;
58
+ setOutlineBg: (level: number) => void;
59
+ setRoleColor: (key: RoleKey, hex: string) => void;
60
+ resetRoleColors: () => void;
61
+ }
62
+ /**
63
+ * `viz` quando ele é um modo DESTE menu (contraste ou correção), senão 'normal'.
64
+ *
65
+ * Chamava-se `resolveContrastValue` enquanto o menu só tinha contraste. O nome antigo passaria a mentir ao
66
+ * devolver `fix-deuter`, e um nome que mente sobre o que devolve é pior que um nome comprido.
67
+ */
68
+ export declare function resolveVisualMode(viz: string): string;
69
+ /** i18n KEY of a contrast level's label; unknown modes fall back to the 'off' key. Resolve with `t()`. */
70
+ export declare function contrastLabel(mode: string): string;
71
+ export declare function clamp01(t: number): number;
72
+ /**
73
+ * i18n KEY of the L->Q slider label ('lq.off'/'lq.linear'/'lq.mixed'/'lq.quadratic'). The label belongs to the
74
+ * L->Q feature, so it lives with the filter that owns it (render/lq-filter) and is re-exported here under the
75
+ * name this overlay has always used. Two copies of one rule is one copy too many: only the owner may change
76
+ * what the levels mean. Callers resolve with `t()` — see the note on lqName about the `t` shadowing.
77
+ */
78
+ export { lqName as lqLabel } from '../render/lq-filter.js';
79
+ /** t (0..1) -> slider percent (0..100, rounded) — mirrors `Math.round(lqT*100)`. */
80
+ export declare function lqPercent(t: number): number;
81
+ /** slider percent (any number) -> clamped t (0..1) — mirrors `+lq.value/100` fed into setLq's clamp. */
82
+ export declare function lqFromPercent(pct: number): number;
83
+ /** Defensive clamp when the player count shrank — mirrors `if(selVizPlayer>=numPlayers)selVizPlayer=0`. */
84
+ export declare function clampSelectedPlayer(selected: number, playerCount: number): number;
85
+ /** [r,g,b] (0..255) -> '#rrggbb' — mirrors game.js's rgbHex. */
86
+ export declare function rgbToHex(rgb: RGB): string;
87
+ /** Shared on/off button label used by this panel's toggle buttons. */
88
+ export declare function onOffLabel(on: boolean): string;
89
+ /** Builds the #visual-list innerHTML — pure string templating, no DOM access. Mirrors renderVisual()'s markup. */
90
+ /**
91
+ * Os 7 modos como o painel os OFERECE: uma lista de rádio, com nome e descrição em cada linha.
92
+ *
93
+ * A primeira versão desta mudança usou um `<select>`, e foi um erro que o Dev pegou na hora: no menu de
94
+ * empatia as três correções eram LINHAS VISÍVEIS, com descrição; dentro de um `<select>` viraram uma linha
95
+ * fechada dentro de uma caixa fechada. Para um controle cuja razão de existir é ser ACHADO por quem enxerga
96
+ * mal, esconder atrás de um clique é quase o mesmo que não ter movido. Voltam a ser linhas, e pelo mesmo
97
+ * renderizador que o painel de empatia usa — o que a criança já sabia procurar continua com a mesma cara.
98
+ *
99
+ * Rádio, e não sete botões: `p.viz` guarda UM valor, e a exclusividade fica dita pela forma do controle em
100
+ * vez de ser descoberta ao perder a correção que se acabou de escolher.
101
+ */
102
+ export declare const VISUAL_MODE_LIST: readonly VizMode[];
103
+ export declare function renderVisualPanelHtml(_contrastValue: string, s: VisualSettings): string;
104
+ /** Duas cores de papel são a mesma? Comparação por componente — `[0,0,0] === [0,0,0]` é `false` em JS, e
105
+ * esse `false` diria "alterado" para uma cor que ninguém tocou, mandando a criança desfazer o que não fez. */
106
+ export declare function sameRgb(a: RGB | undefined, b: RGB | undefined): boolean;
107
+ export interface SettingsVisual {
108
+ /** Rebuilds #visual-list and (re)wires its controls — call whenever the panel should reflect fresh state. */
109
+ render: () => void;
110
+ }
111
+ export declare function initSettingsVisual(ctx: SettingsVisualCtx): SettingsVisual;
@@ -0,0 +1,252 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // ui/settings-visual — the "Acessibilidade visual" overlay: high-contrast level, L→Q contrast enhancement,
3
+ // owner-colored items, CB-safe (Okabe-Ito) palette, color-blocking role colors, and the two outline selects
4
+ // (foreground/background). `selVizPlayer` (chosen player), `setPlayerViz`/`setLq`/`setOwnerColors`/`setCbSafe`/
5
+ // `setOutlineFg`/`setOutlineBg`/`setRoleColor`/`resetRoleColors` are INJECTED — they mutate PIXI texture caches
6
+ // and rebake the world (`_rebakeDirect`/`rebuildExtras`), which stay in game.js. `numPlayers`/`players` are read
7
+ // live from core/state.js (same source game.js itself uses). Extracted verbatim from renderVisual() in game.js
8
+ // (behavior-preserving) — see docs/5-Refactoring/plano-modularizacao-mapa.md.
9
+ import { toggleLabel } from './dom.js';
10
+ import { t } from '../core/i18n.js';
11
+ import { lqName as lqLabel } from '../render/lq-filter.js';
12
+ import { HC_ROLE_KEYS, HC_ROLE_DEF } from '../render/hc-role-data.js';
13
+ import { VIZ_CORRECTIONS, VIZ_MODES } from '../render/viz-modes.js';
14
+ import { DEFAULTS } from '../core/state.js';
15
+ import { markChanged, markMenuChanged } from './changed-mark.js';
16
+ /** Contrast levels, in cycle order — mirrors game.js's HC_SEQ (also used there by the physical contrast-cycle button). */
17
+ export const CONTRAST_LEVELS = ['normal', 'hc-direto', 'hc-direto-45', 'hc-direto-7'];
18
+ /**
19
+ * TUDO que este menu escreve em `p.viz`: os 4 níveis de contraste MAIS as 3 correções de daltonismo, que
20
+ * mudaram de casa por decisão do Dev (#60). Elas moravam no Modo empatia, onde a criança daltônica precisava
21
+ * entrar no menu "sentir como é ter uma deficiência" para achar a correção da deficiência que ela tem.
22
+ *
23
+ * Os sete vivem num CONTROLE SÓ, e isso não é economia de espaço: `p.viz` guarda UM valor. Dois controles
24
+ * separados se sobrescreveriam em silêncio — a criança escolheria a correção, depois o contraste, e perderia
25
+ * a correção sem nada dizer que perdeu. Uma lista só conta a verdade sobre a exclusividade.
26
+ */
27
+ export const VISUAL_MODES = [...CONTRAST_LEVELS, ...VIZ_CORRECTIONS.map((m) => m.key)];
28
+ const VISUAL_MODE_SET = new Set(VISUAL_MODES);
29
+ /** Short announcement labels — mirrors game.js's HC_LABEL. */
30
+ // Chaves i18n, não texto. Os dois extremos parecem números universais, mas '4,5:1' usa a vírgula decimal do
31
+ // pt-BR e vira '4.5:1' em inglês — e 'off' era uma palavra inglesa dentro de uma frase em português.
32
+ export const CONTRAST_LABELS = { normal: 'contrast.off', 'hc-direto': 'contrast.3', 'hc-direto-45': 'contrast.45', 'hc-direto-7': 'contrast.7' };
33
+ export const ROLE_KEYS = HC_ROLE_KEYS;
34
+ /** Readable labels for the color-blocking roles — mirrors game.js's ROLE_LBL. */
35
+ export const ROLE_LABELS = { hazard: 'perigo (lava)', climb: 'escalável (escada/trampolim)', water: 'água', gate: 'portão' };
36
+ // ---------- Pure logic (Right-BICEP/ZOMBIES-tested in node) ----------
37
+ /**
38
+ * `viz` quando ele é um modo DESTE menu (contraste ou correção), senão 'normal'.
39
+ *
40
+ * Chamava-se `resolveContrastValue` enquanto o menu só tinha contraste. O nome antigo passaria a mentir ao
41
+ * devolver `fix-deuter`, e um nome que mente sobre o que devolve é pior que um nome comprido.
42
+ */
43
+ export function resolveVisualMode(viz) {
44
+ return VISUAL_MODE_SET.has(viz) ? viz : 'normal';
45
+ }
46
+ /** i18n KEY of a contrast level's label; unknown modes fall back to the 'off' key. Resolve with `t()`. */
47
+ export function contrastLabel(mode) {
48
+ return CONTRAST_LABELS[mode] ?? CONTRAST_LABELS.normal;
49
+ }
50
+ export function clamp01(t) {
51
+ return Math.max(0, Math.min(1, t));
52
+ }
53
+ /**
54
+ * i18n KEY of the L->Q slider label ('lq.off'/'lq.linear'/'lq.mixed'/'lq.quadratic'). The label belongs to the
55
+ * L->Q feature, so it lives with the filter that owns it (render/lq-filter) and is re-exported here under the
56
+ * name this overlay has always used. Two copies of one rule is one copy too many: only the owner may change
57
+ * what the levels mean. Callers resolve with `t()` — see the note on lqName about the `t` shadowing.
58
+ */
59
+ export { lqName as lqLabel } from '../render/lq-filter.js';
60
+ /** t (0..1) -> slider percent (0..100, rounded) — mirrors `Math.round(lqT*100)`. */
61
+ export function lqPercent(t) {
62
+ return Math.round(clamp01(t) * 100);
63
+ }
64
+ /** slider percent (any number) -> clamped t (0..1) — mirrors `+lq.value/100` fed into setLq's clamp. */
65
+ export function lqFromPercent(pct) {
66
+ return clamp01(pct / 100);
67
+ }
68
+ /** Defensive clamp when the player count shrank — mirrors `if(selVizPlayer>=numPlayers)selVizPlayer=0`. */
69
+ export function clampSelectedPlayer(selected, playerCount) {
70
+ return selected >= playerCount ? 0 : selected;
71
+ }
72
+ /** [r,g,b] (0..255) -> '#rrggbb' — mirrors game.js's rgbHex. */
73
+ export function rgbToHex(rgb) {
74
+ return '#' + rgb.map((n) => n.toString(16).padStart(2, '0')).join('');
75
+ }
76
+ /** Shared on/off button label used by this panel's toggle buttons. */
77
+ export function onOffLabel(on) {
78
+ return toggleLabel(on);
79
+ }
80
+ /** Reads player[i].viz defensively (no player at that index -> 'normal'), without a Player type import. */
81
+ function playerViz(list, i) {
82
+ const p = list[i];
83
+ return typeof p?.viz === 'string' ? p.viz : 'normal';
84
+ }
85
+ /** Builds the #visual-list innerHTML — pure string templating, no DOM access. Mirrors renderVisual()'s markup. */
86
+ /**
87
+ * Os 7 modos como o painel os OFERECE: uma lista de rádio, com nome e descrição em cada linha.
88
+ *
89
+ * A primeira versão desta mudança usou um `<select>`, e foi um erro que o Dev pegou na hora: no menu de
90
+ * empatia as três correções eram LINHAS VISÍVEIS, com descrição; dentro de um `<select>` viraram uma linha
91
+ * fechada dentro de uma caixa fechada. Para um controle cuja razão de existir é ser ACHADO por quem enxerga
92
+ * mal, esconder atrás de um clique é quase o mesmo que não ter movido. Voltam a ser linhas, e pelo mesmo
93
+ * renderizador que o painel de empatia usa — o que a criança já sabia procurar continua com a mesma cara.
94
+ *
95
+ * Rádio, e não sete botões: `p.viz` guarda UM valor, e a exclusividade fica dita pela forma do controle em
96
+ * vez de ser descoberta ao perder a correção que se acabou de escolher.
97
+ */
98
+ export const VISUAL_MODE_LIST = VIZ_MODES.filter((m) => m.kind === 'normal' || m.kind === 'hcnew').concat(VIZ_CORRECTIONS);
99
+ export function renderVisualPanelHtml(_contrastValue, s) {
100
+ const roleInputs = ROLE_KEYS.map((k) => `<input type="color" id="opt-role-${k}" value="${rgbToHex(s.roleColors[k])}" aria-label="Cor de ${ROLE_LABELS[k]}" style="inline-size:2.2em;block-size:1.8em;padding:0;border:1px solid #666;border-radius:4px;background:none">`).join('');
101
+ return ('<div class="ctrl-row"><span><strong>Realce de contraste (Linear → Quadrático)</strong> — curva de tom na tela inteira: o começo da faixa estica o contraste (linear), o fim realça sombras e altas-luzes (curva S quadrática). Zero desliga. Vale para todos os jogadores.</span>' +
102
+ '<span style="display:flex;align-items:center;gap:.4rem"><input type="range" id="opt-lq" min="0" max="100" step="5" style="width:9em" aria-label="Realce de contraste: zero desligado, começo linear, fim quadrático"><strong id="opt-lq-val" aria-hidden="true"></strong></span></div>' +
103
+ '<div class="ctrl-row"><span><strong>Itens na cor do dono</strong> — no multiplayer, cada jogador vê os próprios itens na cor dele. Desligado: itens na cor original para todos.</span>' +
104
+ `<button id="opt-ownercolors" class="mode-btn${s.ownerColors ? ' is-on' : ''}" type="button" aria-pressed="${s.ownerColors}">${onOffLabel(s.ownerColors)}</button></div>` +
105
+ '<div class="ctrl-row"><span><strong>Paleta segura para daltonismo</strong> — troca as cores de jogadores, itens e efeitos pela paleta Okabe-Ito (distinguível em protan/deutan/tritan). O cenário mantém as cores naturais.</span>' +
106
+ `<button id="opt-cbsafe" class="mode-btn${s.cbSafe ? ' is-on' : ''}" type="button" aria-pressed="${s.cbSafe}">${onOffLabel(s.cbSafe)}</button></div>` +
107
+ '<div class="ctrl-row"><span><strong>Cores do color-blocking</strong> — nos modos de alto contraste, escolha a cor de cada papel: perigo, escalável, água e portão. ↺ restaura o padrão.</span>' +
108
+ '<span style="display:flex;gap:.35rem;align-items:center">' +
109
+ roleInputs +
110
+ '<button id="opt-role-reset" class="mode-btn" type="button" aria-label="Restaurar cores padrão">↺</button></span></div>');
111
+ }
112
+ /** Duas cores de papel são a mesma? Comparação por componente — `[0,0,0] === [0,0,0]` é `false` em JS, e
113
+ * esse `false` diria "alterado" para uma cor que ninguém tocou, mandando a criança desfazer o que não fez. */
114
+ export function sameRgb(a, b) {
115
+ if (!a || !b)
116
+ return false;
117
+ return a[0] === b[0] && a[1] === b[1] && a[2] === b[2];
118
+ }
119
+ export function initSettingsVisual(ctx) {
120
+ function reflectOutlines() {
121
+ const s = ctx.getVisualSettings();
122
+ const f = ctx.$('#opt-outline-fg');
123
+ if (f)
124
+ f.value = String(s.outlineFg);
125
+ const b = ctx.$('#opt-outline-bg');
126
+ if (b)
127
+ b.value = String(s.outlineBg);
128
+ }
129
+ // Outline selects live in static HTML outside #visual-list (untouched by innerHTML rebuilds) -> wire once.
130
+ const outFg = ctx.$('#opt-outline-fg');
131
+ if (outFg)
132
+ outFg.addEventListener('change', () => ctx.setOutlineFg(+outFg.value));
133
+ const outBg = ctx.$('#opt-outline-bg');
134
+ if (outBg)
135
+ outBg.addEventListener('change', () => ctx.setOutlineBg(+outBg.value));
136
+ reflectOutlines();
137
+ function render() {
138
+ const el = ctx.$('#visual-list');
139
+ if (!el)
140
+ return;
141
+ const rawSelected = ctx.getSelectedPlayer();
142
+ const selected = clampSelectedPlayer(rawSelected, ctx.getNumPlayers());
143
+ if (selected !== rawSelected)
144
+ ctx.setSelectedPlayer(selected);
145
+ const contrastValue = resolveVisualMode(playerViz(ctx.getPlayers(), selected));
146
+ const settings = ctx.getVisualSettings();
147
+ ctx.renderVizGroup('#visual-modes', '#visual-players', VISUAL_MODE_LIST);
148
+ el.innerHTML = renderVisualPanelHtml(contrastValue, settings);
149
+ const lq = ctx.$('#opt-lq');
150
+ const lqv = ctx.$('#opt-lq-val');
151
+ if (lq) {
152
+ // O parâmetro chamava-se `t` e passou a `amount`: `lqLabel` agora devolve uma chave que precisa de
153
+ // `t()` para virar texto, e o nome antigo sombreava justamente a função que faltava chamar aqui.
154
+ const reflect = (amount) => {
155
+ if (lqv)
156
+ lqv.textContent = t(lqLabel(amount));
157
+ };
158
+ lq.value = String(lqPercent(settings.lq));
159
+ reflect(settings.lq);
160
+ lq.addEventListener('input', () => {
161
+ const amount = lqFromPercent(Number(lq.value));
162
+ ctx.setLq(amount);
163
+ reflect(amount);
164
+ });
165
+ lq.addEventListener('change', () => {
166
+ ctx.srSay(t('sr.visual.lq', { v: t(lqLabel(lqFromPercent(Number(lq.value)))) }));
167
+ });
168
+ }
169
+ const oc = ctx.$('#opt-ownercolors');
170
+ if (oc)
171
+ oc.addEventListener('click', () => { ctx.setOwnerColors(!settings.ownerColors); render(); });
172
+ const cb = ctx.$('#opt-cbsafe');
173
+ if (cb)
174
+ cb.addEventListener('click', () => { ctx.setCbSafe(!settings.cbSafe); render(); });
175
+ for (const k of ROLE_KEYS) {
176
+ const inp = ctx.$('#opt-role-' + k);
177
+ if (inp)
178
+ inp.addEventListener('change', () => ctx.setRoleColor(k, inp.value));
179
+ }
180
+ const rr = ctx.$('#opt-role-reset');
181
+ if (rr)
182
+ rr.addEventListener('click', () => { ctx.resetRoleColors(); render(); });
183
+ reflectOutlines();
184
+ refreshMarks();
185
+ }
186
+ /**
187
+ * A marca de "saiu do padrão" (ADR-0029). Cada linha contra o SEU padrão, e o botão do menu por cima.
188
+ *
189
+ * O contraste é comparado pelo valor RESOLVIDO, não pelo `viz` cru: quem está com uma simulação ou uma
190
+ * O modo visual é comparado pelo valor RESOLVIDO: quem está com uma SIMULAÇÃO ligada (empatia) tem
191
+ * `resolveVisualMode` respondendo 'normal', que é a verdade sobre ESTE menu — aquele modo não saiu do
192
+ * padrão daqui, e é no menu de empatia que a marca precisa aparecer para levar a criança ao lugar certo.
193
+ */
194
+ function refreshMarks() {
195
+ const s = ctx.getVisualSettings();
196
+ const contraste = resolveVisualMode(playerViz(ctx.getPlayers(), ctx.getSelectedPlayer())) !== 'normal';
197
+ const lqOff = s.lq !== DEFAULTS.lq;
198
+ const owner = s.ownerColors !== DEFAULTS.ownerColors;
199
+ const cb = s.cbSafe !== DEFAULTS.cbSafe;
200
+ const fg = s.outlineFg !== DEFAULTS.hcOutlineFg;
201
+ const bg = s.outlineBg !== DEFAULTS.hcOutlineBg;
202
+ const papeis = ROLE_KEYS.some((k) => !sameRgb(s.roleColors[k], HC_ROLE_DEF[k]));
203
+ const linha = (sel) => ctx.$(sel)?.closest('.ctrl-row') ?? null;
204
+ markChanged(ctx.$('#visual-modes'), contraste);
205
+ markChanged(linha('#opt-lq'), lqOff);
206
+ markChanged(linha('#opt-ownercolors'), owner);
207
+ markChanged(linha('#opt-cbsafe'), cb);
208
+ markChanged(linha('#opt-outline-fg'), fg);
209
+ markChanged(linha('#opt-outline-bg'), bg);
210
+ markChanged(linha('#opt-role-reset'), papeis);
211
+ markMenuChanged(ctx.$('[data-act="visual"]'), [contraste, lqOff, owner, cb, fg, bg, papeis]);
212
+ }
213
+ // ---- restaurar os padrões DESTE menu (ADR-0028) ----
214
+ //
215
+ // `p.viz` é UM campo compartilhado com o menu de empatia, então o reset só pode zerá-lo quando o que
216
+ // estiver lá for um modo DESTE menu. Se a criança está com uma simulação de baixa visão ou de cegueira
217
+ // ligada, este botão não tem nada a dizer sobre isso.
218
+ //
219
+ // As correções de daltonismo AGORA entram no que ele zera, e isso mudou com a #60: elas passaram a morar
220
+ // aqui. Desfazê-las é legítimo porque a criança as reencontra no MESMO seletor que acabou de usar — a
221
+ // regra é "um reset só pode desfazer o que ele também consegue refazer", e aqui ela é satisfeita. Por isso
222
+ // o anúncio nomeia o modo visual entre o que voltou.
223
+ //
224
+ // As LEGENDAS (#opt-captions) estão nesta tela mas ficam de fora: quem as liga e persiste é o main.js, e a
225
+ // pergunta de a qual menu elas pertencem está aberta (#58 — são uma acomodação de surdez morando no menu
226
+ // visual). Puxá-las para cá agora responderia essa pergunta por acidente, num commit sobre outra coisa.
227
+ const resetBtn = ctx.$('#visual-reset');
228
+ if (resetBtn)
229
+ resetBtn.addEventListener('click', () => {
230
+ ctx.getPlayers().forEach((_p, i) => {
231
+ const viz = playerViz(ctx.getPlayers(), i);
232
+ if (VISUAL_MODE_SET.has(viz) && viz !== 'normal')
233
+ ctx.setPlayerViz(i, 'normal');
234
+ });
235
+ const s = ctx.getVisualSettings();
236
+ if (s.lq !== DEFAULTS.lq)
237
+ ctx.setLq(DEFAULTS.lq);
238
+ if (s.ownerColors !== DEFAULTS.ownerColors)
239
+ ctx.setOwnerColors(DEFAULTS.ownerColors);
240
+ if (s.cbSafe !== DEFAULTS.cbSafe)
241
+ ctx.setCbSafe(DEFAULTS.cbSafe);
242
+ if (s.outlineFg !== DEFAULTS.hcOutlineFg)
243
+ ctx.setOutlineFg(DEFAULTS.hcOutlineFg);
244
+ if (s.outlineBg !== DEFAULTS.hcOutlineBg)
245
+ ctx.setOutlineBg(DEFAULTS.hcOutlineBg);
246
+ if (ROLE_KEYS.some((k) => !sameRgb(s.roleColors[k], HC_ROLE_DEF[k])))
247
+ ctx.resetRoleColors();
248
+ render();
249
+ ctx.srSay(t('sr.visual.reset'));
250
+ });
251
+ return { render };
252
+ }