@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,36 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // core/loop.ts — driver do loop de jogo. Registra a função de frame num "ticker" (o app.ticker do PixiJS, ou
3
+ // qualquer objeto com .add(fn) e .deltaTime) e passa o dt CLAMPADO (evita saltos gigantes após a aba ficar em
4
+ // segundo plano). Módulo-folha. Mantém a cadência ATUAL (não muda o timing) — o fixed-timestep determinístico
5
+ // fica para depois, pois mudaria a física.
6
+ //
7
+ // ========================= O BOUNDARY DE ERRO (D16), E POR QUE ELE FALHA ALTO =========================
8
+ // Sem ele, um quadro que lança lança OUTRA VEZ no quadro seguinte, para sempre: tela congelada, console cheio, e
9
+ // nada na tela dizendo o que houve. A spec do `demos` pede isto por escala — "across 383 games, one bad game has
10
+ // to be distinguishable from a broken engine". A razão daqui é mais urgente: **criança cega não vê tela
11
+ // congelada.** Sem anúncio, o modo cego não distingue "travou" de "está pensando".
12
+ //
13
+ // ⚠️ E A TENTAÇÃO É O CONTRÁRIO DO CONSERTO. `try { frame() } catch { /* segue */ }` é pior que o defeito: vira
14
+ // jogo silenciosamente errado, rodando para sempre computando lixo. A regra aqui é a mesma que o ADR-0047 aplicou
15
+ // ao CRT — pega UMA vez, PARA, e ANUNCIA.
16
+ export function startLoop(ticker, frame, maxDt = 2, opcoes = {}) {
17
+ let parado = false;
18
+ const passo = () => {
19
+ if (parado)
20
+ return; // ticker sem `remove` não desregistra — a trava é o que faz o laço parar mesmo assim
21
+ try {
22
+ frame(Math.min(ticker.deltaTime, maxDt));
23
+ }
24
+ catch (erro) {
25
+ parado = true;
26
+ ticker.remove?.(passo); // some do ticker quando dá: callback que roda 60×/s para nada custa em hardware fraco
27
+ // O anúncio não pode ressuscitar o problema. Se o próprio aviso quebrar — sem leitor de tela, sem DOM —,
28
+ // uma exceção aqui voltaria a ser invisível dentro do ticker, que é exatamente o defeito que isto fecha.
29
+ try {
30
+ opcoes.aoFalhar?.(erro);
31
+ }
32
+ catch { /* noop: o aviso falhou; o laço já parou, que é o essencial */ }
33
+ }
34
+ };
35
+ ticker.add(passo);
36
+ }
@@ -0,0 +1,34 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ /** Um campo do esquema: um nome e quantos BITS ele ocupa (⇒ valores de 0 a 2^bits − 1). */
3
+ export interface CampoSenha {
4
+ readonly nome: string;
5
+ readonly bits: number;
6
+ }
7
+ export interface CodecSenha {
8
+ /** Quantos caracteres a senha tem, sem separadores. Constante: toda senha deste esquema tem este tamanho. */
9
+ readonly comprimento: number;
10
+ /** Os campos, na ordem em que foram declarados (cópia — quem lê não altera o esquema). */
11
+ readonly campos: readonly CampoSenha[];
12
+ /** Empacota os valores. Lança se algum campo faltar ou não couber — ver a nota em `codificar`. */
13
+ codificar(valores: Readonly<Record<string, number>>): string;
14
+ /** Desempacota. `null` = senha inválida (comprimento, símbolo, soma ou enchimento). */
15
+ decodificar(senha: string): Record<string, number> | null;
16
+ }
17
+ /** Crockford base32: sem I, L, O e U. Índice = valor de 5 bits. */
18
+ export declare const ALFABETO = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
19
+ /**
20
+ * Cria um codec para o esquema `campos`.
21
+ *
22
+ * ⚠️ LANÇA em esquema malformado, e isso é decisão: um esquema errado é erro de PROGRAMA, e um erro de
23
+ * programa que só aparece como senha estranha três meses depois custa mais caro que um boot que não sobe.
24
+ * É a mesma escolha de `boot/create-game` para a declaração malformada.
25
+ */
26
+ export declare function criarCodec(campos: readonly CampoSenha[]): CodecSenha;
27
+ /**
28
+ * Insere um separador a cada `grupo` caracteres — `'A1B2C3D4'` vira `'A1B2-C3D4'`.
29
+ *
30
+ * É acessibilidade, não enfeite: uma sequência sem grupos é lida caractere a caractere pelo leitor de tela e
31
+ * copiada perdendo o lugar por quem lê da tela para o caderno. `decodificar` ignora o separador, então a
32
+ * senha agrupada e a senha corrida são a MESMA senha — quem digitar sem o traço não é punido por isso.
33
+ */
34
+ export declare function formatar(senha: string, grupo?: number, sep?: string): string;
@@ -0,0 +1,205 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // core/password — O CODEC DE CÓDIGOS CURTOS copiados à mão. Módulo-FOLHA: zero dependências, zero I/O, zero DOM.
3
+ //
4
+ // ========================= PARA QUE ESTE CODEC EXISTE — E PARA QUE ELE NÃO EXISTE MAIS =========================
5
+ // Ele nasceu para outra coisa, e a honestidade do arquivo depende de dizer isso.
6
+ //
7
+ // A primeira versão era uma SENHA DE PROGRESSÃO: a criança anotava quatro letras no caderno e recuperava o
8
+ // nível de alfabetização numa máquina de escola restaurada de madrugada. O ADR-0034 registrou o desenho
9
+ // inteiro; o ADR-0037 o enterrou, e a razão veio do Dev, não de um defeito: **não existe salvar jogo, e não
10
+ // deve existir**. Os jogos têm cinco fases e terminam em quinze minutos. Uma senha para recuperar progresso
11
+ // de uma partida que acaba antes do recreio é máquina sem carga.
12
+ //
13
+ // O QUE SOBROU, e é o motivo de o arquivo continuar aqui: a única senha que o desenho tem é a que o PROFESSOR
14
+ // cria para a turma entrar na mesma sala. Enquanto não houver servidor, essa sala é o computador do professor
15
+ // na rede local. E um código de sala é o mesmo problema de engenharia que uma senha de progressão — poucos
16
+ // bits, copiados do quadro por uma criança de sete anos, onde um erro de cópia ACEITO é pior que um recusado.
17
+ // Por isso o codec permaneceu inteiro e o registro do lado do jogo (`game/progress`) foi apagado: o que morreu
18
+ // foi o significado dos campos, não a aritmética.
19
+ //
20
+ // ⚠️ NENHUM CHAMADOR HOJE. Este módulo está sem consumidor desde que `game/progress` saiu, e isso está dito
21
+ // aqui em vez de descoberto por alguém daqui a três meses. Ele fica porque a tela de sala vai precisar dele e
22
+ // porque apagá-lo custaria reescrever a verificação exaustiva que já está testada — não porque esteja em uso.
23
+ //
24
+ // ========================= O QUE ESTE MÓDULO SABE, E O QUE ELE NUNCA SABERÁ =========================
25
+ // A regra do ADR-0033 vale aqui inteira: "a entidade da engine pode declarar o que a ENGINE possui; não pode
26
+ // declarar o que o JOGO possui". Uma senha com os campos `nivel`, `pontos` e `fases` seria exatamente a
27
+ // violação que o ADR proíbe — seriam os campos DESTE jogo, congelados na engine, e o segundo consumidor
28
+ // (`quiz.html`) teria de fingir ter um "nivel" para caber.
29
+ //
30
+ // Então este módulo não conhece campo nenhum: ele recebe um ESQUEMA — uma lista de `{nome, bits}` que o jogo
31
+ // declara — e sabe empacotar inteiros pequenos em símbolos legíveis, com sobra de detecção de erro. O que a
32
+ // engine possui é a CODIFICAÇÃO; o que o jogo possui é o significado de cada campo.
33
+ //
34
+ // ========================= AS TRÊS DECISÕES QUE FAZEM O CÓDIGO SERVIR A UMA CRIANÇA =========================
35
+ //
36
+ // 1. O ALFABETO É DE 32 SÍMBOLOS, SEM I, L, O E U (o de Crockford). Os três primeiros saem por ambiguidade
37
+ // visual — I/1, L/1, O/0 são o erro clássico de quem copia de um caderno a lápis; o U sai porque sem ele
38
+ // o alfabeto não forma palavrão por acidente, e uma senha que sorteia uma ofensa na tela de uma sala de
39
+ // aula é um problema real, não hipotético. O que sobra são 32 símbolos = 5 bits exatos por caractere.
40
+ // A leniência de Crockford entra na LEITURA: quem digitar I, L ou O recebe 1, 1 e 0. Escrever nunca os
41
+ // produz; ler perdoa quem os escreveu. E o ADR-0027 diz que a entrada não é teclado, é GRADE DE LETRAS —
42
+ // 32 símbolos cabem numa grade 8x4 legível a 320x180, que é o que a grade precisa desenhar.
43
+ //
44
+ // 2. A VERIFICAÇÃO É EXATA PARA OS DOIS ERROS QUE A CRIANÇA COMETE, e não "provável". Um dígito de soma
45
+ // simples de 5 bits deixaria 1 código errado em 32 passar — e um código errado que PASSA é pior que um
46
+ // rejeitado: ele leva a criança para OUTRA SALA, com a atividade de outra turma, e ninguém na sala entende
47
+ // por quê. Aqui a soma é PONDERADA PELA POSIÇÃO, módulo 1021, em 2 símbolos:
48
+ // · trocar UM símbolo por outro, em qualquer posição → SEMPRE detectado (a diferença que ele causa na
49
+ // soma é (i+1)·d, e |(i+1)·d| < 1021, logo nunca cai em zero por acaso do módulo);
50
+ // · TROCAR DOIS símbolos DO CORPO de lugar → SEMPRE detectado ((i−j)·(vj−vi), idem).
51
+ // Não é probabilidade: é aritmética, e é por isso que `criarCodec` limita o corpo a 32 símbolos — acima
52
+ // disso o produto passaria de 1021 e a garantia cairia em silêncio. O que fica de fora da garantia é a
53
+ // troca de um símbolo do corpo com um dos dois da soma; para essa, e para uma senha forjada ao acaso, a
54
+ // chance de colar é ~1/1021.
55
+ // ⚠️ O QUE DÁ A GARANTIA É O TAMANHO DO MÓDULO, NÃO A PRIMALIDADE — e eu conferi em vez de supor: trocar
56
+ // 1021 por 1024 (potência de dois) NÃO faz nenhum dos 26 casos deste módulo falhar. É informação sobre o
57
+ // alcance do teste, e fica registrada aqui em vez de virar uma crença. 1021 continua sendo a escolha por
58
+ // ser o maior primo abaixo do teto de dois símbolos: para as duas classes provadas os dois módulos
59
+ // empatam, mas um módulo com fator 2^k é cego a erros MÚLTIPLOS cuja diferença total seja múltipla dele,
60
+ // e um primo não tem esse buraco de graça.
61
+ //
62
+ // 3. `decodificar` DEVOLVE `null`, e não uma exceção nem um objeto "meio válido". Senha errada é o caso
63
+ // NORMAL desta função — é o que acontece quando a criança erra uma letra —, e caso normal não se sinaliza
64
+ // com exceção. Quem chama sabe o comprimento esperado (`codec.comprimento`) e pode distinguir "faltam
65
+ // letras" de "senha errada" sem que este módulo invente vocabulário de interface para isso.
66
+ /** Crockford base32: sem I, L, O e U. Índice = valor de 5 bits. */
67
+ export const ALFABETO = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
68
+ /** O módulo da soma ponderada. Ver a decisão 2 no topo: é o TAMANHO dele que dá a garantia, e o limite de 32. */
69
+ const MODULO = 1021;
70
+ /** Símbolos do corpo, no máximo. Acima disso a garantia da soma cairia sem nenhum sintoma visível. */
71
+ const MAX_SIMBOLOS = 32;
72
+ const VALOR_DE = new Map();
73
+ for (let i = 0; i < ALFABETO.length; i++)
74
+ VALOR_DE.set(ALFABETO[i], i);
75
+ // A leniência de Crockford, só na LEITURA. Escrever jamais produz estes três.
76
+ VALOR_DE.set('I', 1);
77
+ VALOR_DE.set('L', 1);
78
+ VALOR_DE.set('O', 0);
79
+ /**
80
+ * A soma ponderada pela posição, módulo `MODULO`, como DOIS símbolos.
81
+ *
82
+ * O peso `i+1` (e não `i`) é o que faz o primeiro símbolo contar: com peso 0, trocar o primeiro caractere
83
+ * por qualquer outro passaria despercebido — e o primeiro caractere é justamente o que mais se digita errado.
84
+ */
85
+ function soma(simbolos) {
86
+ let c = 0;
87
+ for (let i = 0; i < simbolos.length; i++)
88
+ c = (c + (i + 1) * simbolos[i]) % MODULO;
89
+ return [(c >> 5) & 31, c & 31];
90
+ }
91
+ /**
92
+ * Cria um codec para o esquema `campos`.
93
+ *
94
+ * ⚠️ LANÇA em esquema malformado, e isso é decisão: um esquema errado é erro de PROGRAMA, e um erro de
95
+ * programa que só aparece como senha estranha três meses depois custa mais caro que um boot que não sobe.
96
+ * É a mesma escolha de `boot/create-game` para a declaração malformada.
97
+ */
98
+ // ⚠️ AS MENSAGENS DE ERRO ABAIXO ESTÃO EM INGLÊS, e os comentários deste arquivo não. Não é descuido: o gate
99
+ // `tests/engine-i18n.node.test.js` reprovou este módulo quando elas nasceram em português, e a gaveta que ele
100
+ // oferece — "mensagens de programador", onde já moram `core/contract`, `boot/create-game` e `render/sprites` —
101
+ // só me aceitaria subindo o teto global de 76 para 82 no mesmo commit que cria a dívida. Afrouxar o gate para
102
+ // caber nele é o defeito que o gate existe para impedir. Comentário não vai para tela nenhuma e segue em
103
+ // pt-BR, como no resto do repositório; STRING vai, mesmo que só até o console de quem programa.
104
+ export function criarCodec(campos) {
105
+ if (campos.length === 0) {
106
+ throw new Error('core/password: empty schema — a password with zero fields carries only its own checksum. Declare at least one field, or skip the codec.');
107
+ }
108
+ const vistos = new Set();
109
+ let bitsTotal = 0;
110
+ for (const c of campos) {
111
+ if (!c.nome)
112
+ throw new Error('core/password: field without a name — the name is the key it gets in the object `decodificar` returns.');
113
+ if (vistos.has(c.nome))
114
+ throw new Error(`core/password: field "${c.nome}" declared twice — the second one would shadow the first when reading.`);
115
+ vistos.add(c.nome);
116
+ if (!Number.isInteger(c.bits) || c.bits < 1 || c.bits > 30) {
117
+ throw new Error(`core/password: field "${c.nome}" asks for ${c.bits} bits — use an integer from 1 to 30 (a 0-bit field stores nothing; above 30 the bitwise arithmetic in JS stops being exact).`);
118
+ }
119
+ bitsTotal += c.bits;
120
+ }
121
+ const simbolosCorpo = Math.ceil(bitsTotal / 5);
122
+ if (simbolosCorpo > MAX_SIMBOLOS) {
123
+ throw new Error(`core/password: ${bitsTotal} bits would take ${simbolosCorpo} symbols, and above ${MAX_SIMBOLOS} the weighted checksum stops detecting EVERY transposition (see decision 2 at the top). Store less, or split it into two passwords.`);
124
+ }
125
+ const listaCampos = campos.map((c) => Object.freeze({ nome: c.nome, bits: c.bits }));
126
+ return {
127
+ comprimento: simbolosCorpo + 2,
128
+ campos: Object.freeze(listaCampos),
129
+ codificar(valores) {
130
+ // Bit a bit, do mais significativo ao menos, na ordem declarada. Escrito assim — e não com `<<` sobre
131
+ // um acumulador — porque um esquema de 40 bits estouraria os 32 bits dos operadores do JS, e o sintoma
132
+ // seria uma senha que decodifica errado só nos campos do fim.
133
+ const bits = [];
134
+ for (const c of listaCampos) {
135
+ const v = valores[c.nome];
136
+ const teto = 2 ** c.bits - 1;
137
+ if (typeof v !== 'number' || !Number.isInteger(v) || v < 0 || v > teto) {
138
+ throw new Error(`core/password: field "${c.nome}" got ${String(v)} — expected an integer from 0 to ${teto}. Fix the value BEFORE encoding (clamping here silently would write a password that restores different progress).`);
139
+ }
140
+ for (let b = c.bits - 1; b >= 0; b--)
141
+ bits.push((v >> b) & 1);
142
+ }
143
+ while (bits.length % 5 !== 0)
144
+ bits.push(0); // enchimento: zeros à direita, conferidos na leitura
145
+ const simbolos = [];
146
+ for (let i = 0; i < bits.length; i += 5) {
147
+ simbolos.push((bits[i] << 4) | (bits[i + 1] << 3) | (bits[i + 2] << 2) | (bits[i + 3] << 1) | bits[i + 4]);
148
+ }
149
+ const [s1, s2] = soma(simbolos);
150
+ return [...simbolos, s1, s2].map((v) => ALFABETO[v]).join('');
151
+ },
152
+ decodificar(senha) {
153
+ if (typeof senha !== 'string')
154
+ return null;
155
+ const cru = [];
156
+ for (const ch of senha.toUpperCase()) {
157
+ if (ch === ' ' || ch === '-' || ch === '·')
158
+ continue; // separadores de leitura (ver `formatar`)
159
+ const v = VALOR_DE.get(ch);
160
+ if (v === undefined)
161
+ return null; // símbolo fora do alfabeto: não vale adivinhar o que ela quis dizer
162
+ cru.push(v);
163
+ }
164
+ if (cru.length !== simbolosCorpo + 2)
165
+ return null;
166
+ const simbolos = cru.slice(0, simbolosCorpo);
167
+ const [s1, s2] = soma(simbolos);
168
+ if (s1 !== cru[simbolosCorpo] || s2 !== cru[simbolosCorpo + 1])
169
+ return null;
170
+ const bits = [];
171
+ for (const v of simbolos)
172
+ for (let b = 4; b >= 0; b--)
173
+ bits.push((v >> b) & 1);
174
+ // O enchimento tem de ser zero: `codificar` só produz zeros ali. Uma senha com lixo no fim não saiu
175
+ // daqui, e aceitá-la seria aceitar uma senha que este módulo é incapaz de gerar.
176
+ for (let i = bitsTotal; i < bits.length; i++)
177
+ if (bits[i] !== 0)
178
+ return null;
179
+ const fora = {};
180
+ let p = 0;
181
+ for (const c of listaCampos) {
182
+ let v = 0;
183
+ for (let b = 0; b < c.bits; b++)
184
+ v = v * 2 + bits[p++];
185
+ fora[c.nome] = v;
186
+ }
187
+ return fora;
188
+ },
189
+ };
190
+ }
191
+ /**
192
+ * Insere um separador a cada `grupo` caracteres — `'A1B2C3D4'` vira `'A1B2-C3D4'`.
193
+ *
194
+ * É acessibilidade, não enfeite: uma sequência sem grupos é lida caractere a caractere pelo leitor de tela e
195
+ * copiada perdendo o lugar por quem lê da tela para o caderno. `decodificar` ignora o separador, então a
196
+ * senha agrupada e a senha corrida são a MESMA senha — quem digitar sem o traço não é punido por isso.
197
+ */
198
+ export function formatar(senha, grupo = 4, sep = '-') {
199
+ if (grupo < 1)
200
+ return senha;
201
+ const partes = [];
202
+ for (let i = 0; i < senha.length; i += grupo)
203
+ partes.push(senha.slice(i, i + grupo));
204
+ return partes.join(sep);
205
+ }
@@ -0,0 +1,20 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ /** Uma corrente de números pseudoaleatórios independente de qualquer outra. */
3
+ export interface Rng {
4
+ /** [0, 1). */
5
+ readonly rnd: () => number;
6
+ /** Inteiro em [lo, hi], ambos inclusive. */
7
+ readonly randInt: (lo: number, hi: number) => number;
8
+ /** Cópia baralhada (Fisher-Yates); não toca no original. */
9
+ readonly shuffle: <T>(arr: readonly T[]) => T[];
10
+ /** Reposiciona ESTA corrente. Não alcança nenhuma outra. */
11
+ readonly reseed: (s: number) => void;
12
+ }
13
+ /** A semente do jogo próprio da engine. Um consumidor externo escolhe a sua. */
14
+ export declare const SEMENTE_PADRAO = 20260601;
15
+ export declare const createRng: (semente?: number) => Rng;
16
+ export declare const reseed: (s: number) => void;
17
+ export declare const rnd: () => number;
18
+ export declare const randInt: (lo: number, hi: number) => number;
19
+ export declare const shuffle: <T>(arr: readonly T[]) => T[];
20
+ export declare const rngDecoracao: Rng;
@@ -0,0 +1,61 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // core/rng.ts — RNG semeado (LCG determinístico) p/ reprodutibilidade (coin placement / testes estáveis).
3
+ // Módulo-folha PURO, ZERO deps.
4
+ //
5
+ // ⚠️ DUAS COISAS MUDARAM AQUI NA MESMA MIGRAÇÃO, E ISSO É DE PROPÓSITO (issue #107).
6
+ //
7
+ // 1. `createRng(semente)` existe. A regra D13 do ADR-0038 — «um módulo que guarda estado exporta uma
8
+ // fábrica `createX()`» — nomeava este módulo desde sempre e a conversão nunca aconteceu. O preço foi
9
+ // medido pelo `game-15puzzle`, o segundo consumidor externo: com uma única corrente compartilhada por
10
+ // doze módulos (`game/*`, `render/fx`, `render/weather`, `render/draw`), «embaralhamento determinístico
11
+ // a partir da semente S» só é reproduzível numa página onde mais nada desenha — e a engine existe
12
+ // justamente para que o jogo NÃO esteja sozinho na página. Cada consumidor faz a sua corrente.
13
+ //
14
+ // 2. A aritmética do LCG estava a perder precisão. `_seed * 1103515245` com `_seed` perto de 2³¹ chega a
15
+ // ~2,37×10¹⁸, acima de 2⁵³: os bits BAIXOS — exatamente os que o `& 0x7fffffff` guarda — eram
16
+ // arredondados fora antes de a máscara correr. `Math.imul` faz a multiplicação em 32 bits, que é o que
17
+ // um LCG de 32 bits sempre quis dizer.
18
+ //
19
+ // ⚠️ POR QUE AS DUAS JUNTAS. Cada uma sozinha muda a sequência semeada. Fazer uma hoje e a outra depois
20
+ // mudaria as sequências DUAS vezes, e da segunda vez ninguém se lembraria do porquê — o defeito ficaria a
21
+ // parecer regressão. Uma migração, uma mudança de sequência, uma explicação.
22
+ //
23
+ // ⚠️ O QUE NÃO MUDA: continua determinístico. A aritmética antiga também era reprodutível (ponto flutuante
24
+ // é determinístico), e foi por isso que o defeito nunca apareceu: não era aleatoriedade errada, era um
25
+ // gerador PIOR do que o que foi escrito. Fixtures que dependiam dos valores concretos mudam de valor.
26
+ /** A semente do jogo próprio da engine. Um consumidor externo escolhe a sua. */
27
+ export const SEMENTE_PADRAO = 20260601;
28
+ export const createRng = (semente = SEMENTE_PADRAO) => {
29
+ let _seed = semente >>> 0;
30
+ // `Math.imul` e não `*`: ver o cabeçalho. O `+ 12345` cabe em segurança porque `imul` já devolveu
31
+ // um inteiro de 32 bits com sinal, e a máscara `& 0x7fffffff` desfaz o sinal.
32
+ const rnd = () => (_seed = (Math.imul(_seed, 1103515245) + 12345) & 0x7fffffff) / 0x7fffffff;
33
+ const randInt = (lo, hi) => lo + Math.floor(rnd() * (hi - lo + 1));
34
+ const shuffle = (arr) => {
35
+ const a = [...arr];
36
+ for (let i = a.length - 1; i > 0; i--) {
37
+ const j = (rnd() * (i + 1)) | 0;
38
+ [a[i], a[j]] = [a[j], a[i]];
39
+ }
40
+ return a;
41
+ };
42
+ const reseed = (s) => { _seed = s >>> 0; };
43
+ return { rnd, randInt, shuffle, reseed };
44
+ };
45
+ // ⚠️ A CORRENTE DO JOGO PRÓPRIO DA ENGINE, e SÓ dela. Existe porque `game/` ainda vive aqui dentro
46
+ // (issue #111: o cartucho que ainda não saiu, ADR-0036/ADR-0083). Quando `game/` sair, isto sai com ele.
47
+ // UM CONSUMIDOR EXTERNO NÃO DEVE IMPORTAR ESTES QUATRO — são estado partilhado, que é o defeito que a
48
+ // fábrica acima conserta. Faça `createRng(suaSemente)`.
49
+ const _padrao = createRng(SEMENTE_PADRAO);
50
+ export const reseed = _padrao.reseed;
51
+ export const rnd = _padrao.rnd;
52
+ export const randInt = _padrao.randInt;
53
+ export const shuffle = _padrao.shuffle;
54
+ // ⚠️ A CORRENTE DA DECORAÇÃO, separada da de cima porque a mistura era o defeito CONCRETO.
55
+ // `render/fx` tira um número por partícula, `render/weather` por gota, `render/draw` dois por tremor de
56
+ // câmara — dezenas por quadro. Saindo da mesma corrente das moedas, «semeie com S e o mapa sai igual»
57
+ // passava a depender de quantas partículas a tela desenhou antes, o que ninguém controla nem repara.
58
+ // Enfeite NÃO pode mover o sorteio do jogo. São dois assuntos, e agora são duas correntes.
59
+ // A semente é outra de propósito: se fosse a mesma, as duas correntes andariam em paralelo e o enfeite
60
+ // ficaria correlacionado com o mapa — determinístico, mas visivelmente repetitivo.
61
+ export const rngDecoracao = createRng(SEMENTE_PADRAO ^ 0x5eed);
@@ -0,0 +1,17 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ /** A fatia mínima de `Element` que este módulo lê. Estrutural para o teste de node não precisar de DOM real. */
3
+ export interface ElementoComRotulo {
4
+ getAttribute(nome: string): string | null;
5
+ textContent: string | null;
6
+ }
7
+ /**
8
+ * O nome do controle: `aria-label` quando existe, o texto visível quando não.
9
+ *
10
+ * A ORDEM É A DECISÃO, e ela não é arbitrária: `aria-label` é o que a plataforma de acessibilidade JÁ vai
11
+ * anunciar. Narrar outra coisa não acrescenta informação — cria uma segunda versão do mesmo item, e quem
12
+ * escuta as duas não tem como saber qual é a verdadeira.
13
+ *
14
+ * O texto visível entra quando não há rótulo declarado, que é o caso da maioria dos botões: ali as duas
15
+ * fontes já coincidem por construção.
16
+ */
17
+ export declare function rotuloAcessivel(el: ElementoComRotulo | null | undefined): string;
@@ -0,0 +1,41 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // core/rotulo-acessivel — COMO SE CHAMA UM CONTROLE, e uma resposta só para quem vê e para quem escuta.
3
+ //
4
+ // ========================= O DEFEITO QUE ISTO FECHA =========================
5
+ // MEDIDO no jogo construído, pousando o cursor no botão de número de jogadores do menu inicial:
6
+ //
7
+ // o jogo narrou: "◀ Number of players: 1 ▶, 1 of 4"
8
+ // o leitor de tela diz: "Number of players: 1. Click on the left for fewer, on the right for more."
9
+ //
10
+ // Duas frases diferentes para o MESMO item, no mesmo instante. Uma criança que usa leitor de tela E a
11
+ // narração do jogo ouve o item duas vezes, de dois jeitos — e a versão do jogo lê os glifos `◀` e `▶`, que é
12
+ // exatamente o ruído que o item 4 do ADR-0044 tirou da legenda da pausa.
13
+ //
14
+ // A REGRA JÁ EXISTIA, escrita uma vez: `legendaDoIcone` (ui/pause-icons) lê o `aria-label` do ícone justamente
15
+ // para que "passar o mouse ou focar diga a MESMA verdade que um leitor de tela anunciaria". Ela valia para os
16
+ // dez ícones e não para o resto dos menus. Aqui ela vira uma função, e as três chamadas passam a ser a mesma.
17
+ //
18
+ // ========================= POR QUE UM MÓDULO-FOLHA =========================
19
+ // Três consumidores em camadas diferentes (`ui/activities-menu`, `ui/menu-nav`, `ui/pause-icons`), e
20
+ // `ui/menu-nav` já importa `ui/pause-icons` — pôr a regra num deles fecharia ciclo ou obrigaria alguém a
21
+ // importar de quem não devia. Mesma lição de `core/anel`, e ela é recente: ciclo em ESM não estoura na hora,
22
+ // estoura no boot em TDZ, uma vez, em produção.
23
+ //
24
+ // Zero dependências, zero I/O, nenhum `document` global: recebe o elemento e devolve texto.
25
+ /** Espaço em branco de markup vira UM espaço; pontas somem. */
26
+ const enxuto = (s) => (s || '').replace(/\s+/g, ' ').trim();
27
+ /**
28
+ * O nome do controle: `aria-label` quando existe, o texto visível quando não.
29
+ *
30
+ * A ORDEM É A DECISÃO, e ela não é arbitrária: `aria-label` é o que a plataforma de acessibilidade JÁ vai
31
+ * anunciar. Narrar outra coisa não acrescenta informação — cria uma segunda versão do mesmo item, e quem
32
+ * escuta as duas não tem como saber qual é a verdadeira.
33
+ *
34
+ * O texto visível entra quando não há rótulo declarado, que é o caso da maioria dos botões: ali as duas
35
+ * fontes já coincidem por construção.
36
+ */
37
+ export function rotuloAcessivel(el) {
38
+ if (!el)
39
+ return '';
40
+ return enxuto(el.getAttribute('aria-label')) || enxuto(el.textContent);
41
+ }
@@ -0,0 +1,115 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ import type { GateTile } from './state.js';
3
+ import type { Player } from './entity.js';
4
+ /**
5
+ * OS EXTRAS DO NÍVEL — o que o mapa monta a cada rodada e ninguém persiste.
6
+ *
7
+ * `P` é o tipo do power-up, e ele vem de quem cria a instância. Ver a nota do genérico no cabeçalho.
8
+ */
9
+ export interface ExtrasDoNivel<P> {
10
+ /** Os power-ups espalhados pelo nível. `readonly`: quem lê não reordena. */
11
+ powerups: readonly P[];
12
+ /** Os tiles do portão, como chaves `"tx,ty"` — `core/collision` só pergunta `.has()`. */
13
+ gateTiles: ReadonlySet<string>;
14
+ /** A lista de tiles do portão, ou `null` quando o nível não tem portão. */
15
+ gate: readonly GateTile[] | null;
16
+ /** Portão aberto? FECHADO significa que os tiles acima são sólidos. */
17
+ gateOpen: boolean;
18
+ }
19
+ /**
20
+ * O estado de RODADA. Hoje são os extras do nível mais o `wcSolid`; os outros campos chegam nos próximos
21
+ * passos da Fase B.
22
+ *
23
+ * O `wcSolid` fica FORA do `ExtrasDoNivel` porque ele não nasce com os outros quatro: a geometria de
24
+ * cadeirante é recalculada por conta própria quando o modo liga ou desliga, e tem setter só dela. Juntá-los
25
+ * num objeto só faria a assinatura mentir sobre quando cada um muda.
26
+ */
27
+ export interface RunState<P> extends ExtrasDoNivel<P> {
28
+ /** Sólidos que só existem no modo cadeirante — rampas e plataformas, como chaves `"x,y"`. */
29
+ wcSolid: ReadonlySet<string>;
30
+ /**
31
+ * OS JOGADORES (1..4). Sem setter, e isso é declaração, não esquecimento: o array NUNCA é reatribuído —
32
+ * ele é mutado no lugar (`push`, `splice`, `length`, `players[i]`), e quem faz isso é `game/session`, que
33
+ * é o dono da entrada e da saída de jogador. Um setter aqui daria a impressão de que trocar a lista
34
+ * inteira é uma operação prevista, e ela não é: as referências que os módulos guardam sobreviveriam à
35
+ * troca apontando para a lista velha.
36
+ *
37
+ * `Player[]` é a visão da ENGINE. Cada jogo acrescenta campos (o `GamePlayer` daqui tem `quiz`), e os
38
+ * consumidores que precisam deles estreitam por conta própria — a dívida de conversões que isso gera é
39
+ * anterior a este arquivo e não muda com a mudança de endereço.
40
+ */
41
+ players: Player[];
42
+ /**
43
+ * QUANTOS jogadores/telas (1..4). Não persistido — a partida seguinte começa com um de novo, que é o
44
+ * comportamento que uma sala de aula quer.
45
+ */
46
+ numPlayers: number;
47
+ /** A rodada acabou (vitória). Trava a entrada e deixa o cenário simulando por trás do aviso. */
48
+ ended: boolean;
49
+ /** Semente da decoração do cenário. Sorteada a cada fase DE PROPÓSITO — é o que faz duas partidas da
50
+ * mesma fase não terem a mesma grama. */
51
+ decorSeed: number;
52
+ /** Fração das superfícies com grama: 1 = todas, 0.6 = 60% (a base das estações que virão). */
53
+ grassDensity: number;
54
+ /** QUAL jogador os painéis de acessibilidade visual estão editando. Não é preferência — é qual aba está
55
+ * aberta —, e por isso não se persiste: guardá-la faria a criança reabrir o jogo já editando o jogador 2. */
56
+ selVizPlayer: number;
57
+ /**
58
+ * QUEM abriu o menu de pausa, e portanto o ESCOPO de tudo o que se faz dentro dele.
59
+ *
60
+ * Importa mais do que parece: em telas separadas, este índice é o que faz o menu do jogador 2 editar as
61
+ * configurações DELE. Errar aqui não dá erro — dá a criança certa mexendo nos ajustes da criança errada,
62
+ * em silêncio.
63
+ */
64
+ pauseActor: number;
65
+ /**
66
+ * Grava os quatro extras JUNTOS, que é como eles nascem.
67
+ *
68
+ * Um setter só, e não quatro, porque eles são um resultado só: `game/level-geometry.computeExtras()`
69
+ * devolve os quatro de uma vez, e gravá-los separadamente abriria uma janela em que o portão de um nível
70
+ * convive com os tiles de outro.
71
+ */
72
+ setLevelExtras(x: ExtrasDoNivel<P>): void;
73
+ /** Abre ou fecha o portão. É o único dos cinco que muda DURANTE a rodada. */
74
+ setGateOpen(v: boolean): void;
75
+ /** Troca os sólidos de cadeirante (recalculados quando a geometria do nível muda). */
76
+ setWcSolid(s: ReadonlySet<string>): void;
77
+ setEnded(v: boolean): void;
78
+ setDecorSeed(s: number): void;
79
+ /**
80
+ * ⚠️ O CLAMP MORA AQUI, e é a razão de este setter existir.
81
+ *
82
+ * `grassDensity` é uma FRAÇÃO, e antes de `core/state` a proteção vivia no `window.__incl` — ou seja, só
83
+ * quem entrasse por ali era protegido. Qualquer outro caminho podia escrever 5 ou -1 e o cenário nascia
84
+ * errado sem nada reclamar. Um valor com faixa válida que depende de quem escreve é um valor sem faixa.
85
+ */
86
+ setGrassDensity(v: number): void;
87
+ setSelVizPlayer(i: number): void;
88
+ setPauseActor(i: number): void;
89
+ /** Troca o número de jogadores e AVISA (ver `OpcoesDaRodada.aoTrocarJogadores`). */
90
+ setNumPlayers(n: number): void;
91
+ }
92
+ /**
93
+ * O que a rodada precisa saber do mundo lá fora. Hoje é uma coisa só, e ela existe por um motivo concreto.
94
+ *
95
+ * `core/state.setNumPlayersValue` emitia `numPlayers` no barramento. Mover o campo para cá e simplesmente
96
+ * PARAR de emitir removeria em silêncio uma capacidade que a Fase C acabou de formalizar — e removê-la sem
97
+ * ninguém notar (o barramento ainda não tem assinante) é exatamente o tipo de perda que este repositório
98
+ * escreve ADR para não repetir.
99
+ *
100
+ * Então o aviso entra por INJEÇÃO, como o cabeçalho deste arquivo prometeu, e é OPCIONAL: uma rodada sem
101
+ * barramento funciona igual. Avisos são para painéis, e um painel ausente não é erro.
102
+ */
103
+ export interface OpcoesDaRodada {
104
+ /** Chamado depois de `setNumPlayers`. Na raiz de composição é `(n) => emit('numPlayers', n)`. */
105
+ aoTrocarJogadores?: (n: number) => void;
106
+ }
107
+ /**
108
+ * Cria uma rodada. Sem I/O, sem estado de módulo, sem `emit` — quem quiser avisar alguém avisa por fora.
109
+ *
110
+ * ⚠️ OS EVENTOS FICARAM DE FORA DE PROPÓSITO. `core/state` emitia `gateOpen` e `wcSolid`, e o barramento
111
+ * tinha ZERO assinantes (medido: 25 nomes de evento, 26 `emit()`, nenhum `on()`). Emitir para ninguém é
112
+ * cerimônia, e mantê-la aqui obrigaria a fábrica a depender do barramento — justo o que a Fase C vai
113
+ * refazer. Quando existir o primeiro assinante, ele entra por injeção, com o barramento já tipado.
114
+ */
115
+ export declare function createRunState<P>(opcoes?: OpcoesDaRodada): RunState<P>;
@@ -0,0 +1,40 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ /**
3
+ * Cria uma rodada. Sem I/O, sem estado de módulo, sem `emit` — quem quiser avisar alguém avisa por fora.
4
+ *
5
+ * ⚠️ OS EVENTOS FICARAM DE FORA DE PROPÓSITO. `core/state` emitia `gateOpen` e `wcSolid`, e o barramento
6
+ * tinha ZERO assinantes (medido: 25 nomes de evento, 26 `emit()`, nenhum `on()`). Emitir para ninguém é
7
+ * cerimônia, e mantê-la aqui obrigaria a fábrica a depender do barramento — justo o que a Fase C vai
8
+ * refazer. Quando existir o primeiro assinante, ele entra por injeção, com o barramento já tipado.
9
+ */
10
+ export function createRunState(opcoes = {}) {
11
+ const r = {
12
+ powerups: [],
13
+ gateTiles: new Set(),
14
+ gate: null,
15
+ gateOpen: true,
16
+ wcSolid: new Set(),
17
+ players: [],
18
+ numPlayers: 1,
19
+ setLevelExtras(x) {
20
+ r.powerups = x.powerups;
21
+ r.gateTiles = x.gateTiles;
22
+ r.gate = x.gate;
23
+ r.gateOpen = x.gateOpen;
24
+ },
25
+ setGateOpen(v) { r.gateOpen = v; },
26
+ setWcSolid(s) { r.wcSolid = s; },
27
+ ended: false,
28
+ decorSeed: 0,
29
+ grassDensity: 1,
30
+ selVizPlayer: 0,
31
+ pauseActor: 0,
32
+ setEnded(v) { r.ended = !!v; },
33
+ setDecorSeed(s) { r.decorSeed = s >>> 0; },
34
+ setGrassDensity(v) { r.grassDensity = Math.max(0, Math.min(1, +v || 0)); },
35
+ setSelVizPlayer(i) { r.selVizPlayer = i; },
36
+ setPauseActor(i) { r.pauseActor = i; },
37
+ setNumPlayers(n) { r.numPlayers = n; opcoes.aoTrocarJogadores?.(n); },
38
+ };
39
+ return r;
40
+ }
@@ -0,0 +1,68 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ /**
3
+ * Uma cena. Todos os ganchos são OPCIONAIS de propósito: uma tela de título que só desenha não deveria
4
+ * precisar declarar um `update` vazio, e um diálogo que só ouve não deveria precisar declarar um `draw`.
5
+ */
6
+ export interface Scene {
7
+ /** Nome legível — para depuração e para os testes afirmarem a pilha por nome, não por identidade. */
8
+ readonly nome: string;
9
+ /** Chamado ao ENTRAR (push, ou ao reaparecer no topo por um pop). */
10
+ enter?(): void;
11
+ /** Chamado ao SAIR (pop, ou ao ser coberto por um push). */
12
+ exit?(): void;
13
+ /** Tempo. Só o topo recebe. */
14
+ update?(dt: number): void;
15
+ /** Desenho. Todas recebem, de baixo para cima. */
16
+ draw?(): void;
17
+ /** Entrada. Só o topo recebe. Devolva `true` se consumiu — `false`/nada deixa a tecla seguir. */
18
+ input?(intent: string): boolean | void;
19
+ }
20
+ export interface SceneStack {
21
+ /** Empilha `s` no topo. A anterior recebe `exit()` mas CONTINUA na pilha (e continua sendo desenhada). */
22
+ push(s: Scene): void;
23
+ /** Desempilha o topo e devolve-o (ou `null` se vazia). Quem reaparecer recebe `enter()`. */
24
+ pop(): Scene | null;
25
+ /** Troca o topo — `pop` seguido de `push`, numa chamada, porque é o que "ir para outra tela" quer dizer. */
26
+ replace(s: Scene): void;
27
+ /** A cena do topo, ou `null`. */
28
+ top(): Scene | null;
29
+ /** Os nomes, da base para o topo. Cópia: quem lê não muta a pilha por acidente. */
30
+ nomes(): string[];
31
+ update(dt: number): void;
32
+ draw(): void;
33
+ /** Devolve `true` se a cena do topo consumiu a intenção. */
34
+ input(intent: string): boolean;
35
+ }
36
+ /**
37
+ * Cria uma pilha vazia.
38
+ *
39
+ * ⚠️ `enter`/`exit` são chamados em TRY/CATCH? NÃO — e é decisão, não esquecimento. Um erro dentro de
40
+ * `enter()` significa que a cena não montou; engolir isso deixaria a pilha num estado que ninguém declarou, e
41
+ * o sintoma apareceria três quadros depois, longe da causa. As chamadas de `update`/`draw`/`input` também
42
+ * propagam: a engine não é o lugar de decidir que o erro de um jogo não importa.
43
+ */
44
+ export declare function criarPilha(): SceneStack;
45
+ /**
46
+ * O QUE A CASCA PRECISA SABER DA CENA — três booleanos, e nenhum nome de fase.
47
+ *
48
+ * Era `Phase = 'title' | 'playing' | 'paused'`, importado de `core/state`. O ADR-0030 registra ALARGAR essa
49
+ * união como NÃO-OPÇÃO, e a razão é curta: um segundo jogo continuaria amarrado ao NOSSO vocabulário — um
50
+ * jogo com mapa de fases ou tela de resultados teria de pedir uma constante nova à engine para existir.
51
+ *
52
+ * E o repositório já aprendeu isto uma vez, por evidência e não por gosto: o `consumer-quiz` precisou se
53
+ * declarar "pausado" para navegar os próprios menus, porque o `menu-nav` importava a fase. A correção de lá
54
+ * foi trocar `getPhase()` por `isNavigable()`, um BOOLEANO — e está registrada em `core/constants` como o
55
+ * erro a não repetir. Isto aqui é a mesma correção, aplicada à casca inteira.
56
+ *
57
+ * Quem NOMEIA as cenas é a raiz de composição, que é este jogo. Ela monta a pilha (`core/scenes`) e traduz o
58
+ * topo nestes três fatos. Uma quarta cena que a casca não conheça responde `false` nos três, e a projeção
59
+ * abaixo continua fazendo sentido: sem splash, sem menu de pausa, som mudo, foco no título.
60
+ */
61
+ export interface FatosDaCena {
62
+ /** O topo é a tela de título (o splash cobre o mundo). */
63
+ telaDeTitulo: boolean;
64
+ /** O topo é o JOGO — o mundo recebe tempo, o som toca, o controle de toque pode aparecer. */
65
+ mundoRodando: boolean;
66
+ /** O topo é o menu de pausa. O jogo continua na pilha por baixo, e continua desenhado. */
67
+ menuDePausa: boolean;
68
+ }