@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,64 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // core/scenes — A PILHA DE CENAS (item 22, opção C3 do ADR-0030). Módulo-FOLHA: zero dependências, zero I/O.
3
+ //
4
+ // ========================= O QUE ELA SUBSTITUI, E POR QUE NÃO É UM ENUM MAIOR =========================
5
+ // Hoje o jogo tem `phase: 'title' | 'playing' | 'paused'` em `core/state`, lido por doze módulos. O ADR-0030
6
+ // registra C1 — alargar essa união — como NÃO-OPÇÃO, e a razão é curta: um segundo jogo continuaria amarrado
7
+ // ao NOSSO enum. Um jogo com tela de mapa, um com tela de resultados, um com senha entre sessões (item 23)
8
+ // precisariam pedir uma constante nova na engine para existir.
9
+ //
10
+ // Uma PILHA não tem esse problema porque não tem vocabulário: ela sabe que há cenas empilhadas e nada sobre o
11
+ // que cada uma significa. `title/playing/paused` vira `[titulo]`, `[jogo]`, `[jogo, pausa]` — e um mapa de
12
+ // fases vira `[mapa]` sem que este arquivo mude uma linha.
13
+ //
14
+ // ========================= AS TRÊS REGRAS, E O QUE CADA UMA PRESERVA =========================
15
+ // Elas não são convenção de biblioteca: cada uma reproduz um comportamento que o jogo já tem.
16
+ //
17
+ // · `update` SÓ NO TOPO. É o congelamento da pausa. Hoje ele é `if (phase !== 'playing') return` espalhado;
18
+ // aqui é estrutural — uma cena que não está no topo não recebe tempo, ponto.
19
+ // · `draw` DE BAIXO PARA CIMA. É o menu de pausa desenhado SOBRE o mundo, com o mundo ainda visível. Se
20
+ // desenhasse só o topo, pausar apagaria o jogo da tela.
21
+ // · `input` SÓ NO TOPO, e ele DIZ se consumiu. É a mesma forma que o ADR-0033 deu à entrada modal: quem
22
+ // está por cima decide, e o que ele não quer volta a descer. Sem o retorno, a pilha teria de adivinhar.
23
+ //
24
+ // ========================= O QUE ELA NÃO FAZ =========================
25
+ // Não desenha, não escuta evento, não conhece PIXI nem DOM. Recebe `dt` e devolve nada; recebe uma intenção e
26
+ // devolve um booleano. É o que a torna testável sem navegador e importável dos dois lados da fronteira.
27
+ /**
28
+ * Cria uma pilha vazia.
29
+ *
30
+ * ⚠️ `enter`/`exit` são chamados em TRY/CATCH? NÃO — e é decisão, não esquecimento. Um erro dentro de
31
+ * `enter()` significa que a cena não montou; engolir isso deixaria a pilha num estado que ninguém declarou, e
32
+ * o sintoma apareceria três quadros depois, longe da causa. As chamadas de `update`/`draw`/`input` também
33
+ * propagam: a engine não é o lugar de decidir que o erro de um jogo não importa.
34
+ */
35
+ export function criarPilha() {
36
+ const pilha = [];
37
+ return {
38
+ push(s) {
39
+ pilha[pilha.length - 1]?.exit?.();
40
+ pilha.push(s);
41
+ s.enter?.();
42
+ },
43
+ pop() {
44
+ const fora = pilha.pop() ?? null;
45
+ fora?.exit?.();
46
+ pilha[pilha.length - 1]?.enter?.();
47
+ return fora;
48
+ },
49
+ replace(s) {
50
+ // NÃO é `this.pop()` seguido de `this.push(s)`: isso daria `enter()` à cena de baixo por um instante,
51
+ // e ela reapareceria no topo entre as duas chamadas. Trocar é UMA transição, não duas.
52
+ const fora = pilha.pop() ?? null;
53
+ fora?.exit?.();
54
+ pilha.push(s);
55
+ s.enter?.();
56
+ },
57
+ top() { return pilha[pilha.length - 1] ?? null; },
58
+ nomes() { return pilha.map((s) => s.nome); },
59
+ update(dt) { pilha[pilha.length - 1]?.update?.(dt); },
60
+ draw() { for (const s of pilha)
61
+ s.draw?.(); },
62
+ input(intent) { return pilha[pilha.length - 1]?.input?.(intent) === true; },
63
+ };
64
+ }
@@ -0,0 +1,16 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ /** Colunas e linhas da grade para `n` telas. */
3
+ export interface ScreenGrid {
4
+ cols: number;
5
+ rows: number;
6
+ }
7
+ /**
8
+ * A grade para `n` jogadores: 1 -> 1x1 · 2 -> 2x1 · 3 e 4 -> 2x2.
9
+ * `n` menor que 1 devolve a grade de uma tela, e não zero colunas.
10
+ */
11
+ export declare function screenGrid(n: number): ScreenGrid;
12
+ /** Dimensões em pixels de arte da grade de `n` telas (320x180 por tela — o pixel canônico do ADR-0010). */
13
+ export declare function screenBaseSize(n: number): {
14
+ w: number;
15
+ h: number;
16
+ };
@@ -0,0 +1,28 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // core/screens — a grade de telas do multiplayer, num só lugar.
3
+ //
4
+ // O jogo divide a janela em uma tela por jogador (pilar: multiplayer em telas SEPARADAS, sem split-screen —
5
+ // ADR-0010). Quantas colunas e quantas linhas isso dá era uma conta de uma linha copiada em CINCO lugares:
6
+ // ui/layout.ts (escala inteira), render/crt.ts (só as linhas, para o passo do scanline), ui/hud.ts (posição
7
+ // de cada `.player-screen`), e no game.js em configureRender() e fitsN().
8
+ //
9
+ // A quinta cópia estava DIVERGENTE: `cols = n<=2 ? n : 2`, sem a guarda de `n<=1`. Para n>=1 as duas contas
10
+ // concordam, então a divergência nunca chegou a aparecer — com n=0 uma devolve 1 e a outra devolve 0, e
11
+ // dividir por 0 colunas daria NaN em toda a geometria. Vale a pena guardar essa história: a cópia errada
12
+ // sobreviveu justamente porque o caso onde ela erra é inalcançável hoje, e não porque alguém a conferiu.
13
+ //
14
+ // Módulo FOLHA de propósito: zero dependências, nem de estado. `n` é parâmetro, nunca `numPlayers` global —
15
+ // é isso que deixa a conta testável e que permite o render, o layout e o HUD importarem daqui sem que um
16
+ // passe a depender do outro.
17
+ /**
18
+ * A grade para `n` jogadores: 1 -> 1x1 · 2 -> 2x1 · 3 e 4 -> 2x2.
19
+ * `n` menor que 1 devolve a grade de uma tela, e não zero colunas.
20
+ */
21
+ export function screenGrid(n) {
22
+ return { cols: n <= 1 ? 1 : (n <= 2 ? n : 2), rows: n <= 2 ? 1 : 2 };
23
+ }
24
+ /** Dimensões em pixels de arte da grade de `n` telas (320x180 por tela — o pixel canônico do ADR-0010). */
25
+ export function screenBaseSize(n) {
26
+ const { cols, rows } = screenGrid(n);
27
+ return { w: 320 * cols, h: 180 * rows };
28
+ }
@@ -0,0 +1,138 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ /**
3
+ * ========================= O BARRAMENTO, TIPADO (Fase C do plano) =========================
4
+ *
5
+ * Era `emit(evt: string, val: unknown)`. Duas coisas erradas numa assinatura só:
6
+ *
7
+ * · O NOME ERA `string`. Um `emit('viz', …)` em vez de `'vizMode'` não é erro em lugar nenhum — é
8
+ * SILÊNCIO. O assinante certo nunca é chamado, nada fica vermelho, e a única pista é um painel que
9
+ * parou de se atualizar. Hoje são dezessete pontos de emissão e cada nome aparece UMA vez; a chance de
10
+ * digitar errado é exatamente a chance de escrever a próxima linha.
11
+ * · A CARGA ERA `unknown`. Quem assinasse tinha de converter, e a conversão é onde a mentira entra.
12
+ *
13
+ * Agora `EventoDoJogo` é um mapa nome → carga, e `emit`/`on` são genéricos sobre ele. Nome inexistente não
14
+ * compila; carga errada não compila.
15
+ *
16
+ * ⚠️ POR QUE ISTO EXISTE ANTES DE TER ASSINANTE. Hoje o barramento tem ZERO `on()` em produção (só o teste
17
+ * assina). Um mecanismo sem uso normalmente é dívida — mas este precisa nascer tipado, não ser retipado
18
+ * depois: o ADR-0031 já exige que painéis e atividades se redesenhem quando a criança muda idioma ou fonte,
19
+ * e a casca do `demos` vai reagir a ajuste durante a partida. O primeiro assinante chega com o contrato
20
+ * pronto, em vez de chegar e ser seguido por uma migração.
21
+ */
22
+ export interface EventoDoJogo {
23
+ numPlayers: number;
24
+ vizMode: string;
25
+ modoCego: boolean;
26
+ letterCase: LetterCase;
27
+ captionsOn: boolean;
28
+ menuIndexOn: boolean;
29
+ cbSafe: boolean;
30
+ ownerColors: boolean;
31
+ hcOutlineFg: OutlineLevel;
32
+ hcOutlineBg: OutlineLevel;
33
+ caneBlockDiv: number;
34
+ wheelchair: boolean;
35
+ oneButton: boolean;
36
+ }
37
+ type Ouvinte<K extends keyof EventoDoJogo> = (val: EventoDoJogo[K]) => void;
38
+ /** Assina `evt`. Devolve a função que cancela — guardar o retorno é mais barato que lembrar do `off`. */
39
+ export declare function on<K extends keyof EventoDoJogo>(evt: K, fn: Ouvinte<K>): () => void;
40
+ export declare function off<K extends keyof EventoDoJogo>(evt: K, fn: Ouvinte<K>): void;
41
+ /**
42
+ * Avisa os assinantes de `evt`. EXPORTADO desde 2026-08-25 (item 19) porque `game/state` emite pelos mesmos
43
+ * canais: um segundo mapa de assinantes seria um segundo barramento, e quem assinasse `coins` no lugar errado
44
+ * simplesmente não seria avisado — sem erro, sem teste vermelho.
45
+ *
46
+ * O `try` em volta de cada assinante NÃO é preguiça: um ouvinte que estoura não pode impedir os outros de
47
+ * receber. Um painel quebrado derruba o painel; não derruba o jogo.
48
+ */
49
+ export declare function emit<K extends keyof EventoDoJogo>(evt: K, val: EventoDoJogo[K]): void;
50
+ export declare let vizMode: string;
51
+ export declare function initVizMode(mode: string): void;
52
+ export declare function setVizModeValue(mode: string): void;
53
+ /**
54
+ * OS PADRÕES, com nome. Um valor por linha, e cada um usado em DOIS lugares: a leitura do boot (quando não há
55
+ * nada gravado) e o "restaurar padrões" do menu que o contém (ADR-0028).
56
+ *
57
+ * Existe porque a alternativa é escrever o mesmo padrão duas vezes — uma no `store.getBool(chave, X)` e outra
58
+ * no reset — e este repositório já mostrou quatro vezes o que acontece com duas cópias que ninguém obriga a
59
+ * concordar. Aqui a divergência seria pior que as anteriores: um reset que restaura um valor DIFERENTE do que
60
+ * o jogo usa quando nunca foi configurado deixa a criança num terceiro estado, que não é nem o dela nem o de
61
+ * fábrica, e que ela não tem como nomear para pedir ajuda.
62
+ *
63
+ * `as const` + `Object.freeze` de propósito: um padrão que alguém consiga escrever em tempo de execução deixa
64
+ * de ser padrão.
65
+ */
66
+ /**
67
+ * O padrão do MOVIMENTO REDUZIDO não é uma constante — é o que o sistema operacional pede.
68
+ *
69
+ * Mora aqui, ao lado do DEFAULTS, porque a regra do ADR-0029 é que existe UMA fonte sobre o que é padrão, e
70
+ * um padrão calculado não deixa de ser padrão por não caber num objeto congelado. Quem lê isto: a carga do
71
+ * boot (main.js) e o "restaurar padrões" do menu de sensibilidade visual.
72
+ *
73
+ * Por que importa que o reset leia isto em vez de `false`: numa máquina cujo dono pediu menos movimento,
74
+ * devolver `false` RELIGARIA a animação — o reset passaria a fazer, sozinho, exatamente o que a WCAG 2.3.3
75
+ * existe para impedir, e faria isso na tela de quem já tinha dito que não aguenta.
76
+ *
77
+ * `matchMedia` é guardado: este módulo roda no projeto `node` dos testes, onde `window` não existe.
78
+ */
79
+ export declare function defaultReducedMotion(): boolean;
80
+ export declare const DEFAULTS: Readonly<{
81
+ readonly modoCego: false;
82
+ readonly caneBlockDiv: 1;
83
+ readonly captionsOn: true;
84
+ readonly menuIndexOn: true;
85
+ readonly wheelchair: false;
86
+ readonly oneButton: false;
87
+ readonly easy: false;
88
+ readonly toggleMove: false;
89
+ readonly toggleRun: false;
90
+ readonly cbSafe: false;
91
+ readonly ownerColors: true;
92
+ readonly lq: 0;
93
+ readonly hcOutlineFg: 1;
94
+ readonly hcOutlineBg: 1;
95
+ readonly letterCase: "upper";
96
+ }>;
97
+ export declare let modoCego: boolean;
98
+ export declare function setModoCegoValue(on: boolean): void;
99
+ /**
100
+ * Um tile do portão. O portão é uma LISTA deles, não um objeto com posição — escrevi `{x, y}` na primeira
101
+ * versão e o navegador me desmentiu: `gate` é `[{tx:29,ty:36}, {tx:30,ty:36}, …]`.
102
+ *
103
+ * O `tsc` não podia pegar. Quem chama `setLevelExtras` é o `main.js`, que é JavaScript, então o tipo declarado
104
+ * aqui não tinha do outro lado nada que o contradissesse. É um argumento concreto para o `createGame()` do
105
+ * passo 4 nascer em TypeScript: enquanto o composition root for JS, todo contrato que só ele exercita é uma
106
+ * afirmação sem verificador.
107
+ *
108
+ * Mínimo ESTRUTURAL, como o `PlayerQuiz`: o `MapGateTile` de verdade mora em `game/level-geometry` e é
109
+ * atribuível a este. `core/` não importa de `game/`.
110
+ */
111
+ export interface GateTile {
112
+ readonly tx: number;
113
+ readonly ty: number;
114
+ }
115
+ export type LetterCase = 'mixed' | 'upper';
116
+ export declare let letterCase: LetterCase;
117
+ export declare function setLetterCaseValue(c: LetterCase): void;
118
+ export declare let captionsOn: boolean;
119
+ export declare function setCaptionsOnValue(on: boolean): void;
120
+ export declare let menuIndexOn: boolean;
121
+ export declare function setMenuIndexOnValue(on: boolean): void;
122
+ export declare let cbSafe: boolean;
123
+ export declare function setCbSafeValue(on: boolean): void;
124
+ export declare let ownerColors: boolean;
125
+ export declare function setOwnerColorsValue(on: boolean): void;
126
+ /** Espessura de contorno: 0 nenhum · 1 fino · 2 grosso. Fora da faixa satura, não rejeita. */
127
+ export type OutlineLevel = 0 | 1 | 2;
128
+ export declare let hcOutlineFg: OutlineLevel;
129
+ export declare function setOutlineFgValue(v: number): void;
130
+ export declare let hcOutlineBg: OutlineLevel;
131
+ export declare function setOutlineBgValue(v: number): void;
132
+ export declare let caneBlockDiv: number;
133
+ export declare function setCaneBlockDivValue(div: number): void;
134
+ export declare let wheelchair: boolean;
135
+ export declare function setWheelchairValue(on: boolean): void;
136
+ export declare let oneButton: boolean;
137
+ export declare function setOneButtonValue(on: boolean): void;
138
+ export {};
@@ -0,0 +1,290 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // core/state.ts — estado/cena do jogo (FONTE ÚNICA). Módulo-folha. As 8 mega-variáveis migram do game.js
3
+ // UMA A UMA, lidas como binding vivo (import) e escritas por setter.
4
+ // Bus mínimo (Map<evento, Set<fn>>) para os poucos leitores "de longe" que virão com os outros subsistemas.
5
+ import * as store from '../platform/storage.js'; // persistência (as mega-vars com chave leem/gravam aqui)
6
+ const _subs = new Map();
7
+ /** Assina `evt`. Devolve a função que cancela — guardar o retorno é mais barato que lembrar do `off`. */
8
+ export function on(evt, fn) {
9
+ if (!_subs.has(evt))
10
+ _subs.set(evt, new Set());
11
+ _subs.get(evt).add(fn);
12
+ return () => off(evt, fn);
13
+ }
14
+ export function off(evt, fn) {
15
+ const s = _subs.get(evt);
16
+ if (s)
17
+ s.delete(fn);
18
+ }
19
+ /**
20
+ * Avisa os assinantes de `evt`. EXPORTADO desde 2026-08-25 (item 19) porque `game/state` emite pelos mesmos
21
+ * canais: um segundo mapa de assinantes seria um segundo barramento, e quem assinasse `coins` no lugar errado
22
+ * simplesmente não seria avisado — sem erro, sem teste vermelho.
23
+ *
24
+ * O `try` em volta de cada assinante NÃO é preguiça: um ouvinte que estoura não pode impedir os outros de
25
+ * receber. Um painel quebrado derruba o painel; não derruba o jogo.
26
+ */
27
+ export function emit(evt, val) {
28
+ const s = _subs.get(evt);
29
+ if (s)
30
+ for (const fn of s) {
31
+ try {
32
+ fn(val);
33
+ }
34
+ catch (e) { /* noop */ }
35
+ }
36
+ }
37
+ // ========================= `phase` SAIU DAQUI (ADR-0030 C3, passo 3 da Fase B) =========================
38
+ // Ele não virou campo de fábrica como os outros doze de RODADA: virou uma PILHA. `core/scenes` já existia,
39
+ // testado e sem consumidor; agora a raiz de composição o usa, e `title`/`playing`/`paused` são
40
+ // `[titulo]`, `[jogo]` e `[jogo, pausa]`.
41
+ //
42
+ // A diferença que motivou a troca: `phase === 'paused'` APAGAVA a informação de que há um jogo por baixo. A
43
+ // pilha a mantém, e é dela que sai "o mundo continua desenhado, mas não recebe tempo".
44
+ //
45
+ // E o ADR-0030 registra ALARGAR a união (`'mapa' | 'resultado' | …`) como NÃO-OPÇÃO: um segundo jogo
46
+ // continuaria amarrado ao NOSSO vocabulário, e teria de pedir uma constante nova à engine para existir. Por
47
+ // isso os três nomes não moram em módulo nenhum da engine — moram na raiz, que é este jogo. Quem é engine
48
+ // recebe BOOLEANOS: `mundoRodando()`, `menuDePausa()`, `telaDeTitulo`. É a mesma correção que o
49
+ // `consumer-quiz` obrigou a fazer no `menu-nav` (`getPhase()` → `isNavigable()`), registrada em
50
+ // `core/constants` como o erro a não repetir.
51
+ /* (`quizLevel` SAIU daqui em 2026-08-25, item 19 — está em `game/state`. É o nível da atividade de
52
+ * alfabetização: conteúdo pedagógico, e nem sequer mecânica de engine. A regra é a do ADR-0033, aplicada ao
53
+ * estado: o estado COMPARTILHADO guarda o que a engine possui — acessibilidade, idioma, dispositivo.) */
54
+ // ========================= O RESTO DA RODADA SAIU DAQUI (ADR-0038, Fase B) =========================
55
+ // `ended`, `decorSeed`, `grassDensity`, `selVizPlayer` e `pauseActor` foram para `core/run-state`, na
56
+ // segunda fatia do passo 2. Nenhum deles persiste — que é o critério do corte —, e todos eram importados
57
+ // APENAS pelo composition root, o que manteve a mudança em dois arquivos, como na primeira fatia.
58
+ //
59
+ // O CLAMP do `grassDensity` foi junto, e ele é a razão de aquele setter existir: a fração vinha protegida só
60
+ // para quem entrasse pelo `window.__incl`, e qualquer outro caminho podia gravar 5 ou -1. Deixá-lo para trás
61
+ // transformaria a fábrica num `let` com nome novo.
62
+ //
63
+ // E a fatia grande saiu em 2026-08-26: `players` e `numPlayers`, com quatorze importadores CADA. Com o
64
+ // `phase` indo para a pilha de cenas no mesmo dia, a RODADA saiu INTEIRA daqui — este módulo é só de PÁGINA
65
+ // agora, e `tests/lifetime-gate.node.test.ts` afirma isso a cada rodada de testes.
66
+ // ========================= `cenario` E `activity` SAÍRAM DAQUI (ADR-0038, Fase B) =========================
67
+ // Os dois eram estado de JOGO morando na engine — a mesma exceção que o `coins` e o `quizLevel` já haviam
68
+ // deixado no item 19. O corte por LIFETIME do ADR-0038 os classifica como GAME: ambos são persistidos em
69
+ // chave `kJogo()`, que é o critério mecânico, e ambos viajam com o cartucho quando o jogo mudar de
70
+ // repositório (ADR-0036).
71
+ //
72
+ // Moram agora em `game/state`, com a MESMA forma — binding vivo + setter que persiste e emite. O que mudou
73
+ // foi só o endereço, e é isso que torna a mudança conferível.
74
+ //
75
+ // Um consumidor precisou de mais que um import novo: `ui/activities-menu` é ENGINE, e o gate de fronteira
76
+ // proíbe engine importar de `game/` (a lista dele esvaziou em 2026-08-25). Ele passou a receber
77
+ // `getActivityId`/`setActivityId` por injeção, que é o que a raiz de composição existe para fazer.
78
+ // --- vizMode: modo visual/cor ativo (persistido em incl_viz). A validação (VIZ_CYCLE) e o default por
79
+ // prefers-contrast ficam no game.js. initVizMode NÃO persiste (o default de mídia deve seguir o SO a cada
80
+ // boot; persistir travaria o rastreio de prefers-contrast). Mudanças do usuário usam setVizModeValue. ---
81
+ export let vizMode = 'normal';
82
+ export function initVizMode(mode) { vizMode = mode; }
83
+ export function setVizModeValue(mode) { vizMode = mode; store.set('incl_viz', mode); emit('vizMode', mode); }
84
+ /* (`coins` SAIU daqui em 2026-08-25, item 19 — está em `game/state`. Ele era `unknown[]` porque `core/` não
85
+ * podia conhecer o tipo, e esse `unknown` era o SINTOMA: um estado que não consegue declarar o próprio tipo
86
+ * está na camada errada. Do outro lado da fronteira ele é `unknown[]` ainda, mas por escolha de quem pode
87
+ * decidir — e o `game/coins` que o consome sabe exatamente o que há dentro.) */
88
+ // ========================= `players` E `numPlayers` SAÍRAM DAQUI (ADR-0038, Fase B) =========================
89
+ // Os dois são RODADA pelo critério mecânico do ADR-0038 — nenhum é persistido —, e moram em
90
+ // `core/run-state`, na instância que a raiz de composição possui.
91
+ //
92
+ // O `players` era o caso mais caro de todos os treze. Um `export let` é um binding vivo COMPARTILHADO: dois
93
+ // jogos na mesma página (que é o que a casca do `demos` faz, ADR-0036) veriam a MESMA lista, e o segundo
94
+ // começaria com os jogadores do primeiro ainda dentro. Não haveria erro em lugar nenhum — só uma criança a
95
+ // mais na tela.
96
+ //
97
+ // A referência continua sendo mutada NO LUGAR (`push`/`length`/`players[i]`), e é por isso que a rodada não
98
+ // tem setter para ela: trocar a lista inteira deixaria para trás as referências que os módulos já guardaram.
99
+ // Quem entra e quem sai é `game/session`, e só ele recebe a lista mutável.
100
+ //
101
+ // (O tipo continua sendo `Player[]` — a visão da ENGINE. Cada jogo acrescenta campos e estreita por conta
102
+ // própria, que é o que o ADR-0033 desenhou.)
103
+ /**
104
+ * OS PADRÕES, com nome. Um valor por linha, e cada um usado em DOIS lugares: a leitura do boot (quando não há
105
+ * nada gravado) e o "restaurar padrões" do menu que o contém (ADR-0028).
106
+ *
107
+ * Existe porque a alternativa é escrever o mesmo padrão duas vezes — uma no `store.getBool(chave, X)` e outra
108
+ * no reset — e este repositório já mostrou quatro vezes o que acontece com duas cópias que ninguém obriga a
109
+ * concordar. Aqui a divergência seria pior que as anteriores: um reset que restaura um valor DIFERENTE do que
110
+ * o jogo usa quando nunca foi configurado deixa a criança num terceiro estado, que não é nem o dela nem o de
111
+ * fábrica, e que ela não tem como nomear para pedir ajuda.
112
+ *
113
+ * `as const` + `Object.freeze` de propósito: um padrão que alguém consiga escrever em tempo de execução deixa
114
+ * de ser padrão.
115
+ */
116
+ /**
117
+ * O padrão do MOVIMENTO REDUZIDO não é uma constante — é o que o sistema operacional pede.
118
+ *
119
+ * Mora aqui, ao lado do DEFAULTS, porque a regra do ADR-0029 é que existe UMA fonte sobre o que é padrão, e
120
+ * um padrão calculado não deixa de ser padrão por não caber num objeto congelado. Quem lê isto: a carga do
121
+ * boot (main.js) e o "restaurar padrões" do menu de sensibilidade visual.
122
+ *
123
+ * Por que importa que o reset leia isto em vez de `false`: numa máquina cujo dono pediu menos movimento,
124
+ * devolver `false` RELIGARIA a animação — o reset passaria a fazer, sozinho, exatamente o que a WCAG 2.3.3
125
+ * existe para impedir, e faria isso na tela de quem já tinha dito que não aguenta.
126
+ *
127
+ * `matchMedia` é guardado: este módulo roda no projeto `node` dos testes, onde `window` não existe.
128
+ */
129
+ export function defaultReducedMotion() {
130
+ return !!(typeof window !== 'undefined' && window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches);
131
+ }
132
+ export const DEFAULTS = Object.freeze({
133
+ // auditiva
134
+ modoCego: false,
135
+ caneBlockDiv: 1,
136
+ captionsOn: true,
137
+ menuIndexOn: true, // o indice nasce LIGADO: quem nao sabe que ele existe e quem mais precisa dele
138
+ // motora
139
+ wheelchair: false,
140
+ oneButton: false,
141
+ easy: false, // por jogador (Modo Fácil)
142
+ toggleMove: false, // por jogador (movimento por alternância)
143
+ // A alternância do botão de CORRER nasce desligada de FÁBRICA — e liga sozinha no controle de tela, que é
144
+ // contexto e não escolha. A distinção importa para a marca do ADR-0029: ver `refreshMarks` em
145
+ // ui/settings-motor, que marca a ESCOLHA guardada e não o estado.
146
+ toggleRun: false, // por jogador (alternância do botão de correr)
147
+ // visual
148
+ cbSafe: false,
149
+ ownerColors: true,
150
+ lq: 0, // realce de contraste L→Q desligado
151
+ hcOutlineFg: 1,
152
+ hcOutlineBg: 1,
153
+ // comunicação (hoje só a caixa da letra; o menu de CAA do ADR-0028 amplia isto)
154
+ letterCase: 'upper',
155
+ });
156
+ // --- modoCego: MODO CEGO (A12e auditiva). Só as ajudas de áudio — bengala, sonar, guarda de beirada,
157
+ // narração —, sem tela preta; a simulação de cegueira do Modo Empatia é outra coisa e liga esta por cima.
158
+ //
159
+ // Migrado do main.js (ADR-0027 passo 4 / #50). Era a variável com MAIS encanamento de injeção do projeto:
160
+ // dezesseis pontos em seis módulos passavam `getModoCego`/`setModoCego` por ctx, e a colisão a lia por
161
+ // closure. Estado que seis módulos consultam não é do composition root; e enquanto for, `createGame()` não
162
+ // pode existir sem capturá-la, que é justamente o teste de fronteira que o ADR-0027 quer rodar.
163
+ //
164
+ // O SETTER FAZ TRÊS COISAS E SÓ TRÊS: grava, persiste, avisa. Os efeitos que o main.js pendurava no
165
+ // antigo `setModoCego` — refazer os extras do nível, refletir o painel, anunciar ao leitor de tela — NÃO
166
+ // entram aqui: são reação, e quem reage assina o evento. Um setter que sabe redesenhar a tela é um setter
167
+ // que nenhum teste consegue chamar. ---
168
+ export let modoCego = store.getBool('incl_modocego', DEFAULTS.modoCego);
169
+ export function setModoCegoValue(on) {
170
+ if (modoCego === on)
171
+ return; // a guarda VEM DO ORIGINAL: sem ela o anúncio repetiria a cada clique redundante
172
+ modoCego = on;
173
+ store.setBool('incl_modocego', on);
174
+ emit('modoCego', on);
175
+ }
176
+ export let letterCase = store.get(store.KEYS.letterCase, DEFAULTS.letterCase) === 'upper' ? 'upper' : 'mixed';
177
+ export function setLetterCaseValue(c) {
178
+ if (letterCase === c)
179
+ return;
180
+ letterCase = c;
181
+ store.set(store.KEYS.letterCase, c);
182
+ emit('letterCase', c);
183
+ }
184
+ export let captionsOn = store.getBool(store.KEYS.captions, DEFAULTS.captionsOn);
185
+ export function setCaptionsOnValue(on) {
186
+ const v = !!on;
187
+ if (captionsOn === v)
188
+ return;
189
+ captionsOn = v;
190
+ store.setBool(store.KEYS.captions, v);
191
+ emit('captionsOn', v);
192
+ }
193
+ // --- menuIndexOn: o "6 de 10" no fim do anuncio de cada item de menu (ADR-0044, item 3).
194
+ //
195
+ // NASCE LIGADO, e a razao e a mesma do modo cego nascer com TTS e sonar: quem precisa do indice para se
196
+ // orientar nao tem como saber que ele existe se ele vier desligado. Quem NAO precisa descobre o ajuste
197
+ // lendo o menu, que e justamente a coisa que essa pessoa consegue fazer.
198
+ //
199
+ // PERSISTE em `incl_menuindex` (escopo da CRIANCA, ADR-0027): a preferencia segue com ela de jogo em jogo.
200
+ export let menuIndexOn = store.getBool(store.KEYS.menuIndex, DEFAULTS.menuIndexOn);
201
+ export function setMenuIndexOnValue(on) {
202
+ const v = !!on;
203
+ if (menuIndexOn === v)
204
+ return;
205
+ menuIndexOn = v;
206
+ store.setBool(store.KEYS.menuIndex, v);
207
+ emit('menuIndexOn', v);
208
+ }
209
+ // --- cbSafe: PALETA SEGURA PARA DALTONISMO (Okabe-Ito). Não é um filtro sobre a imagem — é a escolha das
210
+ // cores de origem, aplicada IN-PLACE em PCOLOR para que todo mundo que já referencia a array veja a troca. ---
211
+ export let cbSafe = store.getBool(store.KEYS.cbsafe, DEFAULTS.cbSafe);
212
+ export function setCbSafeValue(on) {
213
+ const v = !!on;
214
+ if (cbSafe === v)
215
+ return;
216
+ cbSafe = v;
217
+ store.setBool(store.KEYS.cbsafe, v);
218
+ emit('cbSafe', v);
219
+ }
220
+ // --- ownerColors: no multijogador, cada item aparece na cor de QUEM pode pegá-lo. Desligado, todos veem a cor
221
+ // original — o que é preferível para quem não distingue as cores dos donos. ---
222
+ export let ownerColors = store.getBool(store.KEYS.ownercolors, DEFAULTS.ownerColors);
223
+ export function setOwnerColorsValue(on) {
224
+ const v = !!on;
225
+ if (ownerColors === v)
226
+ return;
227
+ ownerColors = v;
228
+ store.setBool(store.KEYS.ownercolors, v);
229
+ emit('ownerColors', v);
230
+ }
231
+ const nivelContorno = (v) => Math.max(0, Math.min(2, v | 0));
232
+ // --- hcOutlineFg / hcOutlineBg: CONTORNOS do alto contraste, e são dois porque servem a critérios diferentes.
233
+ // `fg` contorna o primeiro plano — personagem e itens — e atende a WCAG 2.4.7 (foco visível). `bg` contorna
234
+ // o perímetro externo de plataforma, água e lava, delimitando navegável × não-navegável, e atende a
235
+ // WCAG 1.4.11 (contraste de componente ≥ 3:1). Confundi-los apagaria uma das duas garantias.
236
+ //
237
+ // A saturação em 0..2 vem do original e é dupla: no boot (contra um localStorage corrompido) e na escrita
238
+ // (contra um chamador). No main.js isso obrigava a declarar com um valor provisório e reatribuir na linha
239
+ // seguinte, porque a leitura saturada não cabia no mesmo `let`; aqui a função a resolve de uma vez. ---
240
+ export let hcOutlineFg = nivelContorno(store.getNum(store.KEYS.outfg, DEFAULTS.hcOutlineFg));
241
+ export function setOutlineFgValue(v) {
242
+ const n = nivelContorno(v);
243
+ if (hcOutlineFg === n)
244
+ return;
245
+ hcOutlineFg = n;
246
+ store.set(store.KEYS.outfg, n);
247
+ emit('hcOutlineFg', n);
248
+ }
249
+ export let hcOutlineBg = nivelContorno(store.getNum(store.KEYS.outbg, DEFAULTS.hcOutlineBg));
250
+ export function setOutlineBgValue(v) {
251
+ const n = nivelContorno(v);
252
+ if (hcOutlineBg === n)
253
+ return;
254
+ hcOutlineBg = n;
255
+ store.set(store.KEYS.outbg, n);
256
+ emit('hcOutlineBg', n);
257
+ }
258
+ // --- caneBlockDiv: espaçamento da batida da BENGALA, em blocos pisados. 1 = uma batida por bloco;
259
+ // 2 = uma batida a cada meio bloco. Não é preferência de som: é a resolução com que uma criança cega
260
+ // mede a distância que andou, e por isso a colisão a lê a cada passo. ---
261
+ export let caneBlockDiv = store.getNum('incl_cane_div', DEFAULTS.caneBlockDiv) || DEFAULTS.caneBlockDiv;
262
+ export function setCaneBlockDivValue(div) {
263
+ const d = (+div) || 1; // o `|| 1` vem do original: um valor corrompido no localStorage viraria NaN e a
264
+ if (caneBlockDiv === d)
265
+ return; // bengala pararia de bater, que é o modo de falha mais silencioso possível
266
+ caneBlockDiv = d;
267
+ store.set('incl_cane_div', d);
268
+ emit('caneBlockDiv', d);
269
+ }
270
+ // --- wheelchair: MODO CADEIRANTE. Muda a geometria do nível inteiro — degraus e escada viram rampas e
271
+ // elevadores, moedas descem para o chão, lava vira chão, e só voo e super-corrida sobrevivem como poderes.
272
+ // Por isso a colisão a lê: `isSolidType` responde diferente com ela ligada. ---
273
+ export let wheelchair = store.getBool('incl_wheelchair', DEFAULTS.wheelchair);
274
+ export function setWheelchairValue(on) {
275
+ if (wheelchair === on)
276
+ return;
277
+ wheelchair = on;
278
+ store.setBool('incl_wheelchair', on);
279
+ emit('wheelchair', on);
280
+ }
281
+ // --- oneButton: UM BOTÃO POR VEZ. Ignora combinações simultâneas, para quem não consegue pressionar duas
282
+ // teclas ao mesmo tempo. ---
283
+ export let oneButton = store.getBool('incl_onebtn', DEFAULTS.oneButton);
284
+ export function setOneButtonValue(on) {
285
+ if (oneButton === on)
286
+ return;
287
+ oneButton = on;
288
+ store.setBool('incl_onebtn', on);
289
+ emit('oneButton', on);
290
+ }
@@ -0,0 +1,12 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ export declare const TYPE_GLYPH: Record<number, string>;
3
+ export declare const TILE_NAME: Record<number, string>;
4
+ export declare const GLYPH_TYPE: Record<string, number>;
5
+ export declare function parseLevel(text: string): number[][];
6
+ export declare function gridToGlyphs(grid: number[][]): string;
7
+ export declare function selfTest(): {
8
+ ok: boolean;
9
+ unique: boolean;
10
+ invertible: boolean;
11
+ missing: number[];
12
+ };
@@ -0,0 +1,61 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // core/tiles.ts — Legend de tiles + parser do mapa em texto-glifo (1 glifo significativo por tile). Módulo-folha,
3
+ // ZERO deps. O jogo NÃO lê Tiled/Aseprite em runtime — só este texto-glifo, legível/diffável como ascii art.
4
+ // tipo (0–14) → glifo. Terreno = símbolos; power-ups = LETRAS maiúsculas mnemônicas.
5
+ export const TYPE_GLYPH = {
6
+ 1: '.', // ar (iluminado)
7
+ 0: ':', // ar escuro / região secreta
8
+ 2: '#', // pedra / chão sólido
9
+ 6: '=', // parede dura (sem quicar)
10
+ 3: '~', // água
11
+ 4: 'H', // escada
12
+ 5: '^', // trampolim
13
+ 9: 'x', // lava / perigo
14
+ 10: '|', // portão
15
+ 11: '*', // chave
16
+ 7: 'S', // power-up super-pulo
17
+ 8: 'F', // power-up voo
18
+ 12: 'T', // power-up super-corrida (turbo)
19
+ 13: 'U', // power-up ultra-pulo
20
+ 14: 'C', // power-up ventosa (cling)
21
+ };
22
+ // tipo → nome legível (UI e editor de mapa). Uma verdade só.
23
+ export const TILE_NAME = {
24
+ 0: 'ar escuro/secreto', 1: 'ar', 2: 'pedra', 3: 'água', 4: 'escada', 5: 'trampolim', 6: 'parede',
25
+ 7: 'super-pulo', 8: 'voo', 9: 'lava/perigo', 10: 'portão', 11: 'chave', 12: 'super-corrida',
26
+ 13: 'ultra-pulo', 14: 'ventosa',
27
+ };
28
+ // glifo → tipo (inverso). Construído do TYPE_GLYPH para não divergir.
29
+ export const GLYPH_TYPE = Object.fromEntries(Object.entries(TYPE_GLYPH).map(([t, g]) => [g, +t]));
30
+ const AIR = 1; // glifo desconhecido / vazio à direita → ar iluminado
31
+ // Texto-glifo → grid numérico. Preserva o comprimento de cada linha (mapa é irregular; o buildWorld preenche
32
+ // o vazio à direita com ar). Linhas iniciadas por "#!" são meta (nome/autor) e são ignoradas — "#" sozinho é
33
+ // parede, então o marcador de meta é "#!" no INÍCIO da linha.
34
+ export function parseLevel(text) {
35
+ const rows = [];
36
+ for (const line of text.split(/\r?\n/)) {
37
+ if (line.startsWith('#!'))
38
+ continue; // meta
39
+ if (line.length === 0)
40
+ continue; // linha vazia (ex.: \n final) — nossos mapas não têm linha vazia
41
+ const row = [];
42
+ for (const ch of line)
43
+ row.push(ch in GLYPH_TYPE ? GLYPH_TYPE[ch] : AIR);
44
+ rows.push(row);
45
+ }
46
+ return rows;
47
+ }
48
+ // Grid numérico → texto-glifo (para gerar o .map.txt e para o teste de ida-e-volta). Sem newline final.
49
+ export function gridToGlyphs(grid) {
50
+ return grid.map((row) => row.map((t) => TYPE_GLYPH[t] ?? '.').join('')).join('\n');
51
+ }
52
+ // Sanidade: o legend é uma bijeção sobre 0..14? (glifos únicos e todos os tipos mapeados). Prova que a
53
+ // conversão é sem perda para QUALQUER grid. Retorna {ok, unique, invertible, missing}.
54
+ export function selfTest() {
55
+ const types = Array.from({ length: 15 }, (_, i) => i);
56
+ const glyphs = types.map((t) => TYPE_GLYPH[t]);
57
+ const missing = types.filter((t) => !(t in TYPE_GLYPH));
58
+ const unique = new Set(glyphs).size === glyphs.length;
59
+ const invertible = types.every((t) => GLYPH_TYPE[TYPE_GLYPH[t]] === t);
60
+ return { ok: missing.length === 0 && unique && invertible, unique, invertible, missing };
61
+ }
@@ -0,0 +1,4 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ declare function buildWorld(rawGrid: number[][]): number[][];
3
+ export declare function buildWorldFromText(text: string): number[][];
4
+ export { buildWorld };