@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,96 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // input/transports — O REGISTRO DE TRANSPORTES (ADR-0079). Módulo-folha: só tipos e aritmética.
3
+ //
4
+ // ========================= O QUE UM TRANSPORTE É =========================
5
+ // Uma forma de alcançar as ações, e nada sobre significado. Ele declara o que OFERECE FISICAMENTE — quantos
6
+ // lugares atribuíveis tem — e o jogador atribui a eles as ações do jogo. `TOUCH_DEFAULT` já era uma instância
7
+ // disto antes de o modelo existir: uma atribuição padrão que a criança pode mudar.
8
+ //
9
+ // ========================= A GARANTIA MUDOU DE FORMA =========================
10
+ // ⚠️ NÃO é «todo transporte alcança toda ação». É: **para os transportes disponíveis a esta criança, PELO
11
+ // MENOS UM carrega o conjunto de ações deste jogo.** A diferença é o que permite um transporte estreito
12
+ // existir — um acionador de dois toques, um piscar — sem que ele reprove o produto inteiro.
13
+ //
14
+ // ========================= E UM TRANSPORTE NUNCA REMOVE OUTRO =========================
15
+ // Escolher o rato não desliga o teclado, o controle nem o toque. A engine OFERECE; quem escolhe é a criança
16
+ // (ou quem a acompanha). Um método que exige coordenação fina não é exclusão enquanto os outros existirem —
17
+ // a exclusão seria uma lista curta, não um item exigente numa lista longa.
18
+ //
19
+ // ========================= O QUE ESTE MÓDULO EXISTE PARA IMPEDIR =========================
20
+ // ⚠️ Um jogo que precisa de doze ações e um transporte de nove lugares é uma combinação REAL — o controle de
21
+ // tela tem nove slots desde sempre e o conjunto de ações passou a catorze em 2026-09-06. A resposta honesta é
22
+ // UMA FRASE ANTES DE COMEÇAR, e não meia tela jogável: a criança que descobre no meio que não consegue
23
+ // alcançar uma ação conclui que o jogo está partido, e ela não tem como saber que não está.
24
+ import { ACTIONS } from '../core/actions.js';
25
+ /**
26
+ * QUANTOS LUGARES CADA TRANSPORTE REAL TEM, e de onde cada número vem. Estavam medidos e viviam só no teste;
27
+ * um número que só existe num teste não chega a criança nenhuma.
28
+ *
29
+ * · `gamepad: 17` — a Gamepad API «standard» declara dezassete botões. Não é escolha nossa.
30
+ * · `toque: 9` — `TOUCH_DEFAULT` (input/devices) nomeia nove slots, e são nove desde sempre.
31
+ * · `teclado: ACTIONS.length` — ⚠️ e este é o que precisou de decisão.
32
+ *
33
+ * ⚠️ O NÚMERO DO TECLADO NÃO É O NÚMERO DE TECLAS, e também não é o número de ligações do esquema atual. Não
34
+ * é o de teclas porque cem teclas não são cem lugares que uma criança encontra e lembra; não é o do esquema
35
+ * porque o esquema cresce, e o limite passaria a ser a configuração e não o aparelho.
36
+ *
37
+ * É `ACTIONS.length` porque o que limita um teclado, na prática deste produto, é O VOCABULÁRIO QUE A ENGINE
38
+ * SABE NOMEAR: um transporte que cobre todas as posições que existem não tem como ser o curto. Derivado e não
39
+ * escrito à mão de propósito — quando o vocabulário crescer (foi de nove para catorze em 2026-09-06), este
40
+ * número cresce com ele e nunca passa a mentir.
41
+ */
42
+ export const LUGARES = Object.freeze({ gamepad: 17, toque: 9, teclado: ACTIONS.length });
43
+ /**
44
+ * Os três transportes que esta engine sabe oferecer hoje.
45
+ *
46
+ * ⚠️ A DETECÇÃO DO TECLADO É IMPRECISA E ISSO É ACEITÁVEL — mas só porque a tela que consome isto INFORMA em
47
+ * vez de RECUSAR. Não há API que diga «há um teclado físico ligado»; o que o projeto já usa para a mesma
48
+ * pergunta é `pointer:coarse && hover:none` (o `isCoarsePointer` do `game/session`), e um tablet COM teclado
49
+ * responde «toque» a isso. Se a tela barrasse, esse tablet levaria uma recusa falsa num jogo que ele joga.
50
+ * Como ela apenas diz o que falta e deixa continuar, o erro custa uma frase a mais e nunca uma porta fechada.
51
+ */
52
+ export function transportesPadrao(d) {
53
+ return [
54
+ { id: 'gamepad', slots: LUGARES.gamepad, available: d.gamepad },
55
+ { id: 'teclado', slots: LUGARES.teclado, available: d.teclado },
56
+ { id: 'toque', slots: LUGARES.toque, available: d.toque },
57
+ ];
58
+ }
59
+ /** Este transporte carrega este conjunto de ações? Aritmética, como o ADR-0079 §3 a descreve. */
60
+ export function carries(t, acoes) {
61
+ return t.slots >= acoes.length;
62
+ }
63
+ /**
64
+ * Os transportes DISPONÍVEIS que carregam este conjunto.
65
+ *
66
+ * ⚠️ Disponibilidade e capacidade são conferidas nesta ordem de propósito: um transporte que caberia mas não
67
+ * está ligado não é resposta para uma criança que está à frente do aparelho agora.
68
+ */
69
+ export function carriedBy(lista, acoes) {
70
+ return lista.filter((t) => t.available() && carries(t, acoes));
71
+ }
72
+ /**
73
+ * A garantia do ADR-0079 §3, como pergunta de sim ou não.
74
+ *
75
+ * ⚠️ CONJUNTO VAZIO DE AÇÕES DEVOLVE `false`, e não `true` por vacuidade. Um jogo que não declara ação
76
+ * nenhuma não é um jogo que qualquer transporte serve — é um jogo que ninguém consegue jogar, e
77
+ * `actionSetProblems` já o reprova. Devolver `true` aqui esconderia esse defeito atrás desta função.
78
+ */
79
+ export function reachable(lista, acoes) {
80
+ return acoes.length > 0 && carriedBy(lista, acoes).length > 0;
81
+ }
82
+ /**
83
+ * Mede o alcance ANTES de a criança começar.
84
+ *
85
+ * ⚠️ Devolve DADO e não texto. A frase é da interface e tem de passar por `t()`; devolver português daqui
86
+ * repetiria o defeito que o `PADWIZ_STEPS` acabou de deixar de cometer.
87
+ */
88
+ export function alcance(lista, acoes) {
89
+ const disponiveis = lista.filter((t) => t.available());
90
+ return {
91
+ ok: reachable(lista, acoes),
92
+ pedidas: acoes.length,
93
+ serviriamSeLigados: lista.filter((t) => !t.available() && carries(t, acoes)).map((t) => t.id),
94
+ curtos: disponiveis.filter((t) => !carries(t, acoes)).map((t) => ({ id: t.id, slots: t.slots })),
95
+ };
96
+ }
@@ -0,0 +1,58 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ import type { KeyScheme } from '../core/entity.js';
3
+ /**
4
+ * Nome de plataforma → posição abstrata. **ADR-0086 §2**, e não o ADR-0074.
5
+ *
6
+ * ⚠️ A DIFERENÇA ENTRE OS DOIS REGISTROS É A TECLA DA CRIANÇA. O ADR-0074 dizia `jump → action1` e
7
+ * `run → action4`; o ADR-0086 corrigiu para `run → action1` e `jump → action2`, medindo que a correção é a
8
+ * leitura CONSERVADORA — ela deixa cada verbo na tecla e no botão que já ocupava. Traduzir por engano pela
9
+ * tabela do 0074 não daria erro nenhum: moveria o pulo de `J` para `U` em silêncio.
10
+ */
11
+ export declare const VOCABULARIO_ANTIGO: Readonly<Record<string, string>>;
12
+ /**
13
+ * ⚠️ O SEGUNDO DADO SALVO, e ele quase passou. O mapa de toque (`incl_touchmap`) guarda SLOT → AÇÃO, ou seja
14
+ * o nome da ação está no VALOR e não na chave: `{ b0: 'jump', b1: 'especial' }`. O tradutor de esquemas de
15
+ * teclado, que traduz CHAVES, passaria por cima dele sem tocar em nada.
16
+ *
17
+ * ⚠️ E O DANO SERIA PIOR DO QUE NO TECLADO. `normalizeTouchMap` funde o guardado SOBRE o padrão, então um
18
+ * `b0: 'jump'` gravado sobrescreveria o `b0: 'action2'` correto — e o botão da tela deixaria de fazer
19
+ * qualquer coisa. Num tablet de escola pública o toque não é o caminho alternativo: é o único.
20
+ *
21
+ * Foi um teste de NAVEGADOR que o encontrou (`tests/touch.browser.test.js`), depois de a suíte `node` já
22
+ * estar verde — o que é o argumento para os dois projetos existirem.
23
+ */
24
+ export declare function migrarMapaDeToque(mapa: Record<string, string> | null | undefined): Record<string, string> | null;
25
+ /**
26
+ * ⚠️ O TERCEIRO DADO SALVO, encontrado por varredura e não por acidente. Depois de o segundo aparecer num
27
+ * teste de navegador, a pergunta certa deixou de ser «este está migrado?» e passou a ser «QUANTOS formatos
28
+ * persistidos existem?». São três, e este é o do assistente de controle: `incl_padmap_<id>` guarda
29
+ * AÇÃO → BINDING FÍSICO, `{ jump: { b: 0 }, run: { b: 2 } }`, um por modelo de controle.
30
+ *
31
+ * ⚠️ E ELE É O MAIS CARO DE PERDER DOS TRÊS. Um mapa desses existe porque a criança (ou quem a acompanha)
32
+ * passou por um assistente de nove passos apertando botão a botão, provavelmente porque o controle dela não
33
+ * é «standard» — controles genéricos e adaptados raramente são. Perdê-lo manda essa pessoa de volta ao
34
+ * assistente inteiro.
35
+ *
36
+ * `_skip` e qualquer chave desconhecida atravessam, pela mesma razão das outras duas migrações.
37
+ */
38
+ export declare function migrarMapaDeControle<T>(mapa: Record<string, T> | null | undefined): Record<string, T> | null;
39
+ /** O objeto salvo, tal como `input/keyboard` o persiste. `p34` é o formato mais antigo de todos. */
40
+ export interface SavedKB {
41
+ solo?: KeyScheme;
42
+ p2?: KeyScheme[];
43
+ p3?: KeyScheme[];
44
+ p4?: KeyScheme[];
45
+ p34?: (KeyScheme | null)[];
46
+ }
47
+ /**
48
+ * Traduz UM esquema salvo do vocabulário antigo para o abstrato.
49
+ *
50
+ * ⚠️ CHAVE DESCONHECIDA ATRAVESSA INTACTA, e é decisão e não descuido: `up`, `down`, `left` e `right` nunca
51
+ * mudaram de nome, e um esquema pode carregar uma chave que este código não conhece — dado de uma versão
52
+ * futura, ou lixo. Apagá-la seria destruir dado que não entendemos, e é o que faria as quatro direções
53
+ * sumirem. É também o que torna esta função IDEMPOTENTE: aplicada sobre um esquema já migrado, nenhuma chave
54
+ * casa e o resultado é igual à entrada, o que importa porque `loadKB` pode correr mais de uma vez na sessão.
55
+ */
56
+ export declare function migrarEsquema(esquema: KeyScheme | null | undefined): KeyScheme | null;
57
+ /** Traduz o objeto salvo inteiro — o esquema solo e as listas por contagem de jogadores. */
58
+ export declare function migrarSalvo(s: SavedKB | null | undefined): SavedKB | null;
@@ -0,0 +1,125 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // input/vocabulary-migration — O ÚNICO SÍTIO DA ENGINE QUE PODE DIZER `jump`, E POR QUANTO TEMPO.
3
+ //
4
+ // ========================= POR QUE ESTE MÓDULO EXISTE SEPARADO =========================
5
+ // O gate `action-vocabulary-boundary` reprova a engine por nomear os verbos do jogo, e estava certo ao
6
+ // reprovar esta tabela quando ela nasceu dentro de `input/keyboard.ts`. Só que a tabela PRECISA de os nomear:
7
+ // traduzir o vocabulário antigo é literalmente a função dela.
8
+ //
9
+ // ⚠️ E A SAÍDA NÃO FOI LEVANTAR O TETO. Levantar um teto que "só encolhe" é o afrouxamento que o gate existe
10
+ // para impedir, e uma exceção sem endereço vira precedente para a próxima. A saída foi QUARENTENAR: o
11
+ // acoplamento inteiro mora aqui, num ficheiro cujo nome diz que ele é histórico, com um teto próprio e uma
12
+ // data de morte.
13
+ //
14
+ // ⚠️ QUANDO ISTO SE APAGA: quando não restar dado salvo no formato antigo. Não há como saber isso do lado do
15
+ // código — o dado está no navegador de cada criança —, então o critério é de tempo e é do Dev. Enquanto
16
+ // houver, apagar este ficheiro apaga o remapeamento de quem o fez.
17
+ //
18
+ // ========================= O QUE SE PERDE SE ISTO ESTIVER ERRADO =========================
19
+ // Quem remapeou teclas normalmente remapeou por NECESSIDADE — alcance de mão, dedo que não estica, teclado
20
+ // sem numpad. Um esquema salvo que deixe de casar não dá erro: as teclas simplesmente param de responder, e
21
+ // a criança conclui que o jogo quebrou. É perda de uma adaptação, não de uma preferência.
22
+ /**
23
+ * Nome de plataforma → posição abstrata. **ADR-0086 §2**, e não o ADR-0074.
24
+ *
25
+ * ⚠️ A DIFERENÇA ENTRE OS DOIS REGISTROS É A TECLA DA CRIANÇA. O ADR-0074 dizia `jump → action1` e
26
+ * `run → action4`; o ADR-0086 corrigiu para `run → action1` e `jump → action2`, medindo que a correção é a
27
+ * leitura CONSERVADORA — ela deixa cada verbo na tecla e no botão que já ocupava. Traduzir por engano pela
28
+ * tabela do 0074 não daria erro nenhum: moveria o pulo de `J` para `U` em silêncio.
29
+ */
30
+ export const VOCABULARIO_ANTIGO = Object.freeze({
31
+ run: 'action1',
32
+ jump: 'action2',
33
+ especial: 'action3',
34
+ swap: 'action4',
35
+ // ⚠️ E UMA QUINTA ENTRADA QUE NÃO É UM VERBO DE PLATAFORMA. A camada de toque chamava `pause` a posição
36
+ // que todo o resto chama `start` — duas palavras para a mesma coisa, e a do toque era a única que não
37
+ // existia no conjunto abstrato. Um mapa de toque gravado antes disto tem `start: 'pause'` no slot do
38
+ // START, e sem esta linha esse botão deixaria de pausar: `decide()` passa a procurar `'start'` e
39
+ // receberia `'pause'`, que já não é nada — sem erro, sem aviso, e o único botão de pausa de um tablet.
40
+ pause: 'start',
41
+ });
42
+ /**
43
+ * ⚠️ O SEGUNDO DADO SALVO, e ele quase passou. O mapa de toque (`incl_touchmap`) guarda SLOT → AÇÃO, ou seja
44
+ * o nome da ação está no VALOR e não na chave: `{ b0: 'jump', b1: 'especial' }`. O tradutor de esquemas de
45
+ * teclado, que traduz CHAVES, passaria por cima dele sem tocar em nada.
46
+ *
47
+ * ⚠️ E O DANO SERIA PIOR DO QUE NO TECLADO. `normalizeTouchMap` funde o guardado SOBRE o padrão, então um
48
+ * `b0: 'jump'` gravado sobrescreveria o `b0: 'action2'` correto — e o botão da tela deixaria de fazer
49
+ * qualquer coisa. Num tablet de escola pública o toque não é o caminho alternativo: é o único.
50
+ *
51
+ * Foi um teste de NAVEGADOR que o encontrou (`tests/touch.browser.test.js`), depois de a suíte `node` já
52
+ * estar verde — o que é o argumento para os dois projetos existirem.
53
+ */
54
+ export function migrarMapaDeToque(mapa) {
55
+ if (!mapa)
56
+ return null;
57
+ const saida = {};
58
+ for (const [slot, acao] of Object.entries(mapa)) {
59
+ saida[slot] = VOCABULARIO_ANTIGO[acao] ?? acao;
60
+ }
61
+ return saida;
62
+ }
63
+ /**
64
+ * ⚠️ O TERCEIRO DADO SALVO, encontrado por varredura e não por acidente. Depois de o segundo aparecer num
65
+ * teste de navegador, a pergunta certa deixou de ser «este está migrado?» e passou a ser «QUANTOS formatos
66
+ * persistidos existem?». São três, e este é o do assistente de controle: `incl_padmap_<id>` guarda
67
+ * AÇÃO → BINDING FÍSICO, `{ jump: { b: 0 }, run: { b: 2 } }`, um por modelo de controle.
68
+ *
69
+ * ⚠️ E ELE É O MAIS CARO DE PERDER DOS TRÊS. Um mapa desses existe porque a criança (ou quem a acompanha)
70
+ * passou por um assistente de nove passos apertando botão a botão, provavelmente porque o controle dela não
71
+ * é «standard» — controles genéricos e adaptados raramente são. Perdê-lo manda essa pessoa de volta ao
72
+ * assistente inteiro.
73
+ *
74
+ * `_skip` e qualquer chave desconhecida atravessam, pela mesma razão das outras duas migrações.
75
+ */
76
+ export function migrarMapaDeControle(mapa) {
77
+ if (!mapa)
78
+ return null;
79
+ const saida = {};
80
+ for (const [chave, valor] of Object.entries(mapa)) {
81
+ saida[VOCABULARIO_ANTIGO[chave] ?? chave] = valor;
82
+ }
83
+ return saida;
84
+ }
85
+ /**
86
+ * Traduz UM esquema salvo do vocabulário antigo para o abstrato.
87
+ *
88
+ * ⚠️ CHAVE DESCONHECIDA ATRAVESSA INTACTA, e é decisão e não descuido: `up`, `down`, `left` e `right` nunca
89
+ * mudaram de nome, e um esquema pode carregar uma chave que este código não conhece — dado de uma versão
90
+ * futura, ou lixo. Apagá-la seria destruir dado que não entendemos, e é o que faria as quatro direções
91
+ * sumirem. É também o que torna esta função IDEMPOTENTE: aplicada sobre um esquema já migrado, nenhuma chave
92
+ * casa e o resultado é igual à entrada, o que importa porque `loadKB` pode correr mais de uma vez na sessão.
93
+ */
94
+ export function migrarEsquema(esquema) {
95
+ if (!esquema)
96
+ return null;
97
+ const saida = {};
98
+ for (const [chave, teclas] of Object.entries(esquema)) {
99
+ const nova = VOCABULARIO_ANTIGO[chave] ?? chave;
100
+ // ⚠️ Esquema MEIO migrado (as duas chaves presentes): a UNIÃO, nunca a sobreposição. Perder uma tecla é
101
+ // o dano que este módulo existe para impedir; ter a mesma tecla duas vezes não é dano nenhum.
102
+ saida[nova] = saida[nova] ? [...new Set([...saida[nova], ...teclas])] : [...teclas];
103
+ }
104
+ return saida;
105
+ }
106
+ /** Traduz o objeto salvo inteiro — o esquema solo e as listas por contagem de jogadores. */
107
+ export function migrarSalvo(s) {
108
+ if (!s)
109
+ return null;
110
+ const lista = (arr) => (Array.isArray(arr) ? arr.map((m) => migrarEsquema(m)) : undefined);
111
+ const out = {};
112
+ const solo = migrarEsquema(s.solo);
113
+ if (solo)
114
+ out.solo = solo;
115
+ for (const g of ['p2', 'p3', 'p4']) {
116
+ const v = lista(s[g]);
117
+ if (v)
118
+ out[g] = v;
119
+ }
120
+ // `p34` migra de VOCABULÁRIO aqui e de FORMA em `loadKB`, que já o fazia antes deste módulo existir.
121
+ // Sem esta linha, o dado mais velho de todos seria o único a perder-se.
122
+ if (Array.isArray(s.p34))
123
+ out.p34 = s.p34.map((m) => migrarEsquema(m));
124
+ return out;
125
+ }
@@ -0,0 +1,27 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ interface Vec2 {
3
+ x: number;
4
+ y: number;
5
+ }
6
+ export interface AudioAmbientCtx {
7
+ ensureAC: () => AudioContext | null;
8
+ getAudioCtx: () => AudioContext | null;
9
+ catNode: (cat: string) => AudioNode | null;
10
+ audioOut: () => AudioNode | null;
11
+ noiseBuffer: (ac: AudioContext) => AudioBuffer;
12
+ getSoundOn: () => boolean;
13
+ getVolume: () => number;
14
+ getAudioCat: () => Record<string, {
15
+ on: boolean;
16
+ }> | null;
17
+ getPlayers: () => Vec2[];
18
+ tileAt: (x: number, y: number) => number;
19
+ TILE: number;
20
+ getRainLevel: () => number;
21
+ }
22
+ export interface AudioAmbient {
23
+ updateAmbient: () => void;
24
+ thunder: (inten: number) => void;
25
+ }
26
+ export declare function createAudioAmbient(ctx: AudioAmbientCtx): AudioAmbient;
27
+ export {};
@@ -0,0 +1,89 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // platform/audio-ambient — trilha de AMBIENTE sintetizada (loops de ruído filtrado) + trovão. Última rodada do Tier 2 de
3
+ // áudio (r4). O clima VISUAL (updateWeather/drawWeather/chuva) fica no game.js e migra p/ render depois — aqui só o SOM.
4
+ // Ponte com o clima: `_rainLevel` (0..1) é calculado por updateWeather (game.js) e LIDO aqui por getter → o volume da
5
+ // chuva "segue o visual". Injeção por closure (padrão Tier 1).
6
+ // updateAmbient() — por frame: constrói a trilha 1x (lazy) e ajusta o ganho de ÁGUA (proximidade de tiles de água) e
7
+ // de CHUVA (segue _rainLevel). Gated por audioCat.ambient.on.
8
+ // thunder(inten) — rumor grave sintetizado (ruído passa-baixas), intensidade variável; chamado por updateWeather.
9
+ // Extraído do game.js. Ver docs/5-Refactoring/plano-modularizacao-mapa.md (Tier 2, áudio rodada 4).
10
+ export function createAudioAmbient(ctx) {
11
+ let _ambient = null;
12
+ function buildAmbient(ac) {
13
+ const cat = ctx.catNode('ambient');
14
+ if (!cat)
15
+ return null;
16
+ const n = (ac.sampleRate * 2) | 0, buf = ac.createBuffer(1, n, ac.sampleRate), d = buf.getChannelData(0);
17
+ let last = 0;
18
+ for (let i = 0; i < n; i++) {
19
+ const w = Math.random() * 2 - 1;
20
+ last = (last + 0.02 * w) / 1.02;
21
+ d[i] = last * 3.2;
22
+ } // ruído rosa em loop
23
+ const mk = (type, freq, q, vol) => {
24
+ const s = ac.createBufferSource();
25
+ s.buffer = buf;
26
+ s.loop = true;
27
+ const f = ac.createBiquadFilter();
28
+ f.type = type;
29
+ f.frequency.value = freq;
30
+ if (q)
31
+ f.Q.value = q;
32
+ const g = ac.createGain();
33
+ g.gain.value = vol;
34
+ s.connect(f).connect(g).connect(cat);
35
+ try {
36
+ s.start();
37
+ }
38
+ catch (e) { /* noop */ }
39
+ return g;
40
+ };
41
+ // trânsito/rumor · folhas/vento · água (proximidade) · chuva (ciclo)
42
+ return { hum: mk('lowpass', 480, 0, 0.055), wind: mk('highpass', 3200, 0, 0.028), water: mk('bandpass', 820, 1.4, 0), rain: mk('highpass', 1700, 0, 0) };
43
+ }
44
+ function updateAmbient() {
45
+ const ac = ctx.getAudioCtx(), cat = ctx.getAudioCat();
46
+ if (!ac || !ctx.getSoundOn() || !cat || !cat.ambient || !cat.ambient.on)
47
+ return;
48
+ if (!_ambient) {
49
+ _ambient = buildAmbient(ac);
50
+ if (!_ambient)
51
+ return;
52
+ }
53
+ const pl = ctx.getPlayers()[0], px = Math.floor(pl.x / ctx.TILE), py = Math.floor(pl.y / ctx.TILE);
54
+ let nearWater = 0;
55
+ for (let dx = -3; dx <= 3; dx++)
56
+ for (let dy = -3; dy <= 3; dy++) {
57
+ if (ctx.tileAt(px + dx, py + dy) === 3)
58
+ nearWater = Math.max(nearWater, 1 - Math.hypot(dx, dy) / 4.2);
59
+ }
60
+ _ambient.water.gain.setTargetAtTime(0.15 * nearWater, ac.currentTime, 0.3);
61
+ _ambient.rain.gain.setTargetAtTime(0.09 * ctx.getRainLevel(), ac.currentTime, 0.5); // chuva segue _rainLevel; 0 = silêncio total
62
+ }
63
+ function thunder(inten) {
64
+ if (!ctx.getSoundOn() || ctx.getVolume() <= 0)
65
+ return;
66
+ const ac = ctx.ensureAC();
67
+ if (!ac)
68
+ return;
69
+ try {
70
+ const vol = ctx.getVolume();
71
+ const src = ac.createBufferSource();
72
+ src.buffer = ctx.noiseBuffer(ac);
73
+ src.loop = true;
74
+ const bq = ac.createBiquadFilter();
75
+ bq.type = 'lowpass';
76
+ bq.frequency.value = 140 + Math.random() * 220;
77
+ bq.Q.value = 0.7;
78
+ const g = ac.createGain(), t = ac.currentTime, dur = 0.7 + inten * 1.4;
79
+ g.gain.setValueAtTime(0.0001, t);
80
+ g.gain.exponentialRampToValueAtTime(Math.min(0.55, 0.2 * inten) * vol, t + 0.04);
81
+ g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
82
+ src.connect(bq).connect(g).connect(ctx.catNode('ambient') || ctx.audioOut() || ac.destination);
83
+ src.start(t);
84
+ src.stop(t + dur + 0.1);
85
+ }
86
+ catch (e) { /* Web Audio indisponível */ }
87
+ }
88
+ return { updateAmbient, thunder };
89
+ }
@@ -0,0 +1,24 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ interface SfxDef {
3
+ t: OscillatorType;
4
+ f: number;
5
+ d: number;
6
+ cap?: string;
7
+ }
8
+ export interface AudioEarconsCtx {
9
+ SFX: Record<string, SfxDef | undefined>;
10
+ ensureAC: () => AudioContext | null;
11
+ catNode: (cat: string) => AudioNode | null;
12
+ audioOut: () => AudioNode | null;
13
+ noiseHit: (mat: string) => void;
14
+ getSoundOn: () => boolean;
15
+ getVolume: () => number;
16
+ getCaptionsOn: () => boolean;
17
+ showCaption: (txt: string) => void;
18
+ }
19
+ export interface AudioEarcons {
20
+ sfx: (name: string) => void;
21
+ doorSound: (mat: string) => void;
22
+ }
23
+ export declare function createAudioEarcons(ctx: AudioEarconsCtx): AudioEarcons;
24
+ export {};
@@ -0,0 +1,62 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // platform/audio-earcons — earcons (ícones sonoros) do jogo + a ponte com as LEGENDAS visuais (a11y surdez).
3
+ // Tier 2 do áudio, rodada 2. Depende das primitivas de platform/audio (SFX/ensureAC/catNode/audioOut/noiseHit) e,
4
+ // por injeção, do estado de legenda que VIVE no game.js (captionsOn é alternado pela UI; showCaption toca o #caption
5
+ // e é reusado por win()). Injeção por closure (padrão Tier 1).
6
+ // sfx(name) — toca o earcon da tabela SFX (oscilador) E, se as legendas estão ON e o earcon tem `.cap`, mostra a
7
+ // legenda ANTES de checar o som → um jogador surdo "vê" o som mesmo com o áudio desligado.
8
+ // doorSound(mat) — porta: rangido (madeira, sawtooth) ou clangor (ferro, square) + baque de ruído (noiseHit).
9
+ // Extraído do game.js. Ver docs/5-Refactoring/plano-modularizacao-mapa.md (Tier 2, áudio rodada 2).
10
+ import { t } from '../core/i18n.js'; // item 19: `cap` guarda CHAVE, e quem exibe resolve
11
+ export function createAudioEarcons(ctx) {
12
+ function sfx(name) {
13
+ const c = ctx.SFX[name];
14
+ if (!c)
15
+ return;
16
+ // LEGENDA primeiro (visual + aria-live via role=status) — e RESOLVIDA no ponto de uso: `cap` guarda a
17
+ // CHAVE desde o item 19, porque a tabela vive no jogo e uma tabela de `const` com texto congelaria no
18
+ // idioma do boot. Quem exibe resolve; é a mesma regra de `render/viz-modes`.
19
+ if (ctx.getCaptionsOn() && c.cap)
20
+ ctx.showCaption(t(c.cap));
21
+ if (!ctx.getSoundOn() || ctx.getVolume() <= 0)
22
+ return; // ...só então o som — surdez: a legenda já saiu
23
+ try {
24
+ const ac = ctx.ensureAC();
25
+ if (!ac)
26
+ return;
27
+ const o = ac.createOscillator(), g = ac.createGain();
28
+ o.type = c.t;
29
+ o.frequency.value = c.f;
30
+ g.gain.value = 0.0001;
31
+ o.connect(g).connect(ctx.catNode('earcons') || ctx.audioOut() || ac.destination);
32
+ const t = ac.currentTime, vol = ctx.getVolume();
33
+ g.gain.exponentialRampToValueAtTime(Math.max(0.02, 0.25 * vol), t + 0.01);
34
+ g.gain.exponentialRampToValueAtTime(0.0001, t + c.d);
35
+ o.start(t);
36
+ o.stop(t + c.d + 0.02);
37
+ }
38
+ catch (e) { /* Web Audio indisponível */ }
39
+ }
40
+ function doorSound(mat) {
41
+ if (!ctx.getSoundOn() || ctx.getVolume() <= 0)
42
+ return;
43
+ const ac = ctx.ensureAC();
44
+ if (!ac)
45
+ return;
46
+ try { // porta: rangido (madeira) ou clangor (ferro) + baque
47
+ const o = ac.createOscillator(), g = ac.createGain(), t = ac.currentTime, vol = ctx.getVolume();
48
+ o.type = mat === 'ferro' ? 'square' : 'sawtooth';
49
+ o.frequency.setValueAtTime(mat === 'ferro' ? 520 : 200, t);
50
+ o.frequency.exponentialRampToValueAtTime(mat === 'ferro' ? 300 : 110, t + 0.3);
51
+ g.gain.setValueAtTime(0.0001, t);
52
+ g.gain.exponentialRampToValueAtTime(0.14 * vol, t + 0.03);
53
+ g.gain.exponentialRampToValueAtTime(0.0001, t + 0.35);
54
+ o.connect(g).connect(ctx.catNode('interact') || ctx.audioOut() || ac.destination);
55
+ o.start(t);
56
+ o.stop(t + 0.4);
57
+ ctx.noiseHit(mat === 'ferro' ? 'ferro' : 'madeira');
58
+ }
59
+ catch (e) { /* Web Audio indisponível */ }
60
+ }
61
+ return { sfx, doorSound };
62
+ }
@@ -0,0 +1,17 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ type ToneFn = (freq: number, dur: number, type?: OscillatorType, when?: number, vol?: number) => void;
3
+ export interface AudioJinglesCtx {
4
+ tone: ToneFn;
5
+ ensureAC: () => AudioContext | null;
6
+ catNode: (cat: string) => AudioNode | null;
7
+ audioOut: () => AudioNode | null;
8
+ getSoundOn: () => boolean;
9
+ getVolume: () => number;
10
+ }
11
+ export interface AudioJingles {
12
+ playVictory: () => void;
13
+ playPuzzleSolved: () => void;
14
+ firework: (when?: number) => void;
15
+ }
16
+ export declare function createAudioJingles(ctx: AudioJinglesCtx): AudioJingles;
17
+ export {};
@@ -0,0 +1,54 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // platform/audio-jingles — recompensas sonoras (jingles) SEM estado de jogo: vitória, enigma resolvido e fogos.
3
+ // Primeiro passo do Tier 2 (áudio): o cluster mais puro — só depende das primitivas de platform/audio (tone/ensureAC/
4
+ // catNode/audioOut) + soundOn/volume vivos. Sem tiles, players, coins nem DOM. Injeção por closure (padrão Tier 1).
5
+ // playVictory — jingle 8-bit ascendente (C5 E5 G5 C6 B5 E6) + 4 fogos de artifício.
6
+ // playPuzzleSolved — frase DOCE senoidal ascendente (E5 G5 B5 E6 + C6 de fundo), Zelda OoT; NUNCA agressiva (José).
7
+ // firework — assobio subindo (sine 300→1200 Hz) + estouro/crepitar (5 osciladores square); usado por playVictory.
8
+ // Extraído do game.js (linhas 625-633). Ver docs/5-Refactoring/plano-modularizacao-mapa.md (Tier 2, áudio rodada 1).
9
+ export function createAudioJingles(ctx) {
10
+ // Fogos: só osciladores crus (não usam `tone`), então guardam soundOn/volume aqui e escalam o ganho pelo volume.
11
+ function firework(when) {
12
+ if (!ctx.getSoundOn() || ctx.getVolume() <= 0)
13
+ return;
14
+ try {
15
+ const ac = ctx.ensureAC();
16
+ if (!ac)
17
+ return;
18
+ const vol = ctx.getVolume(), t = ac.currentTime + (when || 0);
19
+ const o = ac.createOscillator(), g = ac.createGain();
20
+ o.type = 'sine';
21
+ o.frequency.setValueAtTime(300, t);
22
+ o.frequency.exponentialRampToValueAtTime(1200, t + 0.35); // assobio subindo
23
+ const out = ctx.catNode('earcons') || ctx.audioOut() || ac.destination;
24
+ g.gain.setValueAtTime(0.0001, t);
25
+ g.gain.exponentialRampToValueAtTime(0.12 * vol, t + 0.05);
26
+ g.gain.exponentialRampToValueAtTime(0.0001, t + 0.36);
27
+ o.connect(g).connect(out);
28
+ o.start(t);
29
+ o.stop(t + 0.4);
30
+ [0, 0.04, 0.09, 0.15, 0.22].forEach((dt, i) => {
31
+ const po = ac.createOscillator(), pg = ac.createGain(), tt = t + 0.36 + dt;
32
+ po.type = 'square';
33
+ po.frequency.setValueAtTime(180 + ((i * 131) % 520), tt);
34
+ pg.gain.setValueAtTime(0.18 * vol, tt);
35
+ pg.gain.exponentialRampToValueAtTime(0.0001, tt + 0.09);
36
+ po.connect(pg).connect(out);
37
+ po.start(tt);
38
+ po.stop(tt + 0.11);
39
+ });
40
+ }
41
+ catch (e) { /* Web Audio indisponível */ }
42
+ }
43
+ function playVictory() {
44
+ [[523, 0], [659, 0.12], [784, 0.24], [1047, 0.36], [988, 0.52], [1319, 0.64]]
45
+ .forEach(([f, w]) => ctx.tone(f, 0.16, 'square', w, 0.22));
46
+ [0.2, 0.8, 1.35, 1.9].forEach((w) => firework(w));
47
+ }
48
+ function playPuzzleSolved() {
49
+ [[659, 0], [784, 0.13], [988, 0.26], [1319, 0.42]]
50
+ .forEach(([f, w]) => ctx.tone(f, 0.32, 'sine', w, 0.12));
51
+ ctx.tone(1047, 0.6, 'sine', 0.42, 0.07);
52
+ }
53
+ return { playVictory, playPuzzleSolved, firework };
54
+ }
@@ -0,0 +1,14 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ type AudioCat = {
3
+ k: string;
4
+ lbl: string;
5
+ };
6
+ type AudioCatState = {
7
+ on: boolean;
8
+ vol: number;
9
+ };
10
+ export declare const AUDIO_CATS: AudioCat[];
11
+ export declare function defaultAudioCat(k: string): AudioCatState;
12
+ export declare function loadAudioCat(): Record<string, AudioCatState>;
13
+ export declare function saveAudioCat(k: string, obj: AudioCatState): void;
14
+ export {};
@@ -0,0 +1,62 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // platform/audio-mixer.ts — categorias do mixer de áudio (dados) + carga/persistência. Módulo-folha (storage).
3
+ // O grafo de áudio (catNode/setCatGain/_catNodes/audioCtx) e o objeto audioCat VIVO ficam no game.js — aqui só
4
+ // a lista das categorias, o estado inicial (com o default TTS-off) e o save por categoria. (Fase 2, áudio)
5
+ import * as store from './storage.js';
6
+ export const AUDIO_CATS = [
7
+ { k: 'music', lbl: 'Música' }, { k: 'ambient', lbl: 'Sons ambiente (água, rua, trânsito, folhas, chuva)' },
8
+ { k: 'interact', lbl: 'Efeitos de interação (passos, portas, escada)' }, { k: 'earcons', lbl: 'Earcons (pulo, moeda, dano…)' },
9
+ { k: 'other', lbl: 'Outros efeitos' }, { k: 'tts', lbl: 'Narração (TTS)' }, { k: 'sonar', lbl: 'Sonar' },
10
+ { k: 'guard', lbl: 'Guarda de beirada' }, { k: 'guide', lbl: 'Pista / guia auditivo' },
11
+ ];
12
+ // Estado inicial por categoria. O que estiver SALVO sobrepõe o default — e isso é deliberado: um valor salvo
13
+ // significa que alguém MEXEU naquele controle, e a escolha da criança não é minha para desfazer. Quem já
14
+ // tinha ligado o guia continua com ele ligado.
15
+ /**
16
+ * O estado de FÁBRICA de uma categoria. Existe com nome porque dois lugares precisam dele: a leitura do boot
17
+ * (quando nada foi salvo) e o "restaurar padrões" do menu auditivo (ADR-0028). Escrever `k !== 'tts'` e `0.8`
18
+ * nos dois seria a mesma cópia sem dono que este repositório já viu divergir — e aqui a divergência colocaria
19
+ * a criança num terceiro estado, nem o dela nem o de fábrica.
20
+ */
21
+ /**
22
+ * As categorias que nascem DESLIGADAS, e o motivo de cada uma. Lista com razão escrita, e não um `k !== 'x'`
23
+ * pendurado numa expressão — a próxima que entrar precisa dizer por quê.
24
+ *
25
+ * · `tts` — voz robótica irrita e sobrecarrega pessoas com TEA. Quem precisa liga no menu. (O TTS do
26
+ * letramento é o `gameSay()`, independente disto e sempre ativo.)
27
+ *
28
+ * · `guide` — o BEACON do guia auditivo, DESLIGADO desde 2026-08-26 por decisão do Dev, e é uma medida
29
+ * PROVISÓRIA que não deve virar permanente sem alguém a rever.
30
+ *
31
+ * O que ele faz hoje: um `triangle` de 0,12 s apontado para o alvo mais próximo, a cada 0,8 s, PARA
32
+ * SEMPRE — sem depender de movimento, de tecla ou de nada ter mudado. No modo cego `needsAudioCues` é
33
+ * sempre verdadeiro, então a criança que mais precisa de pistas é a que ouve o bipe a partida inteira.
34
+ *
35
+ * O veredito do Dev: "um ping é a pior escolha possível, tenebroso para quem tem TEA". Não é a FREQUÊNCIA
36
+ * que está errada — é o bipe. Reduzi-lo a "só andando" deixaria a mesma coisa doendo menos vezes.
37
+ *
38
+ * O substituto que ele descreveu é maior que uma troca de som e por isso não entra junto: uma música que
39
+ * fica mais intensa conforme se aproxima, e ANTES dela é preciso MAPEAR A ROTA — preencher o mapa com as
40
+ * direções por onde há ar ou água, para que a pista siga um caminho navegável em vez de apontar em linha
41
+ * reta para dentro de uma parede. Está registrado como issue; até ela existir, o silêncio é melhor que o
42
+ * bipe, e a criança que quiser o bipe continua podendo ligá-lo no menu auditivo.
43
+ */
44
+ const NASCEM_DESLIGADAS = new Set(['tts', 'guide']);
45
+ export function defaultAudioCat(k) {
46
+ return { on: !NASCEM_DESLIGADAS.has(k), vol: 0.8 };
47
+ }
48
+ export function loadAudioCat() {
49
+ const cat = {};
50
+ AUDIO_CATS.forEach((c) => {
51
+ const d = defaultAudioCat(c.k);
52
+ let on = d.on, vol = d.vol;
53
+ const o = store.getJSON('incl_audiocat_' + c.k, null);
54
+ if (o) {
55
+ on = !!o.on;
56
+ vol = +o.vol;
57
+ }
58
+ cat[c.k] = { on, vol };
59
+ });
60
+ return cat;
61
+ }
62
+ export function saveAudioCat(k, obj) { store.setJSON('incl_audiocat_' + k, obj); }