@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,51 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ import type { PlayerView, ControlledPlayer } from '../core/entity.js';
3
+ import type { AudioSonar, PlayerAudioOut, PlayerCtxOut } from './audio-sonar.js';
4
+ export type { PlayerCtxOut };
5
+ /**
6
+ * O jogador visto pela BENGALA e pelo NADO. `facing` e `wnT` são desta metade; a outra não os usa.
7
+ *
8
+ * A saída dedicada (`_ac`/`_acOut`) entra por INTERSECÇÃO com o tipo do dono, `PlayerAudioOut` de
9
+ * `platform/audio-sonar` — não por uma redeclaração local.
10
+ *
11
+ * ⚠️ A JUSTIFICATIVA QUE ESTAVA AQUI ERA FALSA e vale corrigi-la em vez de apagá-la, senão o próximo leitor
12
+ * a re-deriva: dizia que `core/` "não pode importar tipos de Web Audio". Não há importação a fazer — o
13
+ * `tsconfig.json` tem `"lib": ["ES2022", "DOM", "DOM.Iterable"]`, então `AudioContext` e `GainNode` são
14
+ * globais ambientes em qualquer arquivo desta árvore. O motivo verdadeiro é de CAMADA: `_ac` e `_acOut` não
15
+ * são da entidade da engine, e por isso o `core/entity` deixou de mencioná-los (ADR-0039, opção A1).
16
+ */
17
+ type Player = PlayerView<'x' | 'y' | 'facing' | 'viz' | 'i' | 'audioSink' | 'wnT' | 'guideT' | 'pad'> & PlayerAudioOut & Pick<ControlledPlayer, 'ctrl'>;
18
+ export interface AudioNavCtx {
19
+ tileAt: (x: number, y: number) => number;
20
+ solidAt: (x: number, y: number) => boolean;
21
+ held: (pl: Player, act: string) => boolean;
22
+ tonePan: (freq: number, dur: number, cat: string, pan?: number | null, vol?: number, type?: OscillatorType, pc?: PlayerCtxOut | null) => void;
23
+ noiseHit: (mat: string, pan?: number, pc?: PlayerCtxOut | null) => void;
24
+ BOX: {
25
+ w: number;
26
+ h: number;
27
+ };
28
+ TILE: number;
29
+ getCenario: () => string;
30
+ /**
31
+ * A NAVEGAÇÃO SONORA, pronta (`platform/audio-sonar`). Entra por injeção e não por importação: é o que
32
+ * permite empacotar o sonar sem levar a bengala junto — e é a diferença entre "dois arquivos" e "dois
33
+ * módulos". Onze coisas do ctx antigo saíram com ela.
34
+ */
35
+ sonar: AudioSonar;
36
+ }
37
+ export interface AudioNav {
38
+ playerCtx: (pl: Player) => PlayerCtxOut | null;
39
+ caneProbe: (pl: Player) => string;
40
+ caneTap: (pl: Player) => void;
41
+ waterNav: (pl: Player) => void;
42
+ panFor: (wx: number, pl: Player) => number;
43
+ needsAudioCues: (pl: Player) => boolean;
44
+ sonar: (pl: Player) => void;
45
+ updateGuide: () => void;
46
+ readonly caneCount: number;
47
+ readonly waterNavCount: number;
48
+ readonly sonarCount: number;
49
+ readonly guideCount: number;
50
+ }
51
+ export declare function createAudioNav(ctx: AudioNavCtx): AudioNav;
@@ -0,0 +1,99 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // platform/audio-nav — BENGALA e NADO CEGO: as pistas sonoras que só existem onde há MUNDO (item 19).
3
+ //
4
+ // ========================= ESTE MÓDULO ERA DOIS =========================
5
+ // O achado 9 do segundo consumidor: "o módulo é DOIS módulos com um nome só — `caneProbe`/`caneTap`/
6
+ // `waterNav` são bengala e natação, isto é, plataforma; `sonar`/`panFor`/`needsAudioCues` são navegação
7
+ // sonora, que serve a qualquer jogo". A navegação sonora saiu para `platform/audio-sonar` e recebe o
8
+ // CONTRATO em vez de tiles; aqui ficou a metade que lê o mundo, e que lê sem disfarce.
9
+ //
10
+ // O QUE SOBROU É PLATAFORMA, e é bom que o nome não esconda isso:
11
+ // caneProbe(pl) — material À FRENTE (água/vazio/madeira/chão do tema), lendo tile e chão.
12
+ // caneTap(pl) — batida da bengala no material adiante (ou tom grave oco no vazio).
13
+ // waterNav(pl) — nado cego: contato com paredes/chão/superfície (cordas).
14
+ //
15
+ // A bengala precisa de `facing`, `BOX.w` e `TILE` para saber o que é "à frente"; o nado precisa de `solidAt`
16
+ // em três alturas. Nada disso é traduzível para um jogo sem corpo e sem mundo, e por isso NÃO foi traduzido:
17
+ // o corte separa o que viaja do que não viaja, em vez de fingir que tudo viaja.
18
+ //
19
+ // ========================= A METADE QUE VIAJOU CONTINUA SAINDO DAQUI =========================
20
+ // `playerCtx`, `panFor`, `needsAudioCues`, `sonar` e `updateGuide` seguem no retorno, delegados ao
21
+ // `AudioSonar` injetado. Não é preguiça de mexer no chamador: a bengala PRECISA do `playerCtx` (a batida sai
22
+ // no dispositivo do jogador) e o `main.js` monta um objeto só de navegação. O que mudou é que este módulo
23
+ // deixou de IMPLEMENTAR a metade genérica — ele a repassa, e quem quiser só ela importa o outro arquivo.
24
+ // Extraído do game.js. Ver docs/5-Refactoring/plano-modularizacao-mapa.md (Tier 2, áudio rodada 3).
25
+ export function createAudioNav(ctx) {
26
+ const SURF_MAT = { cidade: 'piso', campo: 'grama', floresta: 'grama', cemiterio: 'terra', espaco: 'pedra', classico: 'pedra' }; // chão por tema
27
+ let _caneCount = 0, _waterNavCount = 0;
28
+ const som = ctx.sonar; // a metade que viaja, injetada
29
+ const playerCtx = (pl) => som.playerCtx(pl);
30
+ function caneProbe(pl) {
31
+ const dir = pl.facing < 0 ? -1 : 1;
32
+ const ax = Math.floor((pl.x + dir * (ctx.BOX.w / 2 + ctx.TILE * 0.6)) / ctx.TILE), footTy = Math.floor((pl.y + 1) / ctx.TILE);
33
+ if (ctx.tileAt(ax, footTy) === 3 || ctx.tileAt(ax, footTy - 1) === 3)
34
+ return 'agua'; // água à frente
35
+ let gty = -1;
36
+ for (let ty = footTy; ty <= footTy + 1; ty++) {
37
+ if (ctx.solidAt(ax, ty)) {
38
+ gty = ty;
39
+ break;
40
+ }
41
+ } // chão (pé ou 1 abaixo = degrau/rampa)
42
+ if (gty < 0)
43
+ return 'vazio'; // sem chão → fosso
44
+ if (ctx.tileAt(ax, gty) === 4)
45
+ return 'madeira'; // escada
46
+ return SURF_MAT[ctx.getCenario()] || 'pedra';
47
+ }
48
+ function caneTap(pl) {
49
+ const mat = caneProbe(pl), dir = pl.facing < 0 ? -1 : 1, pan = dir * 0.5, pc = playerCtx(pl);
50
+ _caneCount++;
51
+ if (mat === 'vazio') {
52
+ ctx.tonePan(150, 0.18, 'guard', pan, 0.16, 'sine', pc);
53
+ return;
54
+ } // vazio = tom grave oco
55
+ ctx.noiseHit(mat, pan, pc); // batida no material adiante (no dispositivo do jogador)
56
+ }
57
+ function waterNav(pl) {
58
+ // dir tipado como number (não 1|-1) DE PROPÓSITO: o original usa `dir!==0` (sempre true aqui, pois facing é ±1) —
59
+ // preservo essa guarda sempre-verdadeira exatamente; alargar o tipo satisfaz o tsc sem mudar o comportamento.
60
+ const dir = pl.facing < 0 ? -1 : 1, tx = Math.floor(pl.x / ctx.TILE), tyF = Math.floor((pl.y - 1) / ctx.TILE), tyH = Math.floor((pl.y - ctx.BOX.h) / ctx.TILE);
61
+ const wallAhead = ctx.solidAt(tx + dir, tyF) || ctx.solidAt(tx + dir, tyH); // parede lateral (azulejo)
62
+ const floorBelow = ctx.solidAt(tx, tyF + 1); // chão (fundo)
63
+ const openAbove = ctx.tileAt(tx, tyH - 1) !== 3 && !ctx.solidAt(tx, tyH - 1); // acima da cabeça é ar → dá p/ subir/sair
64
+ const moving = (dir !== 0) || ctx.held(pl, 'up') || ctx.held(pl, 'down'), pc = playerCtx(pl);
65
+ pl.wnT = (pl.wnT || 0) + 1;
66
+ if (pl.wnT < 18)
67
+ return;
68
+ let played = true;
69
+ if (openAbove && wallAhead)
70
+ ctx.noiseHit('parede', dir * 0.5, pc); // superfície + parede = batida (fim da corda)
71
+ else if (openAbove && moving)
72
+ ctx.tonePan(560, 0.09, 'guide', dir * 0.4, 0.12, 'sine', pc); // corda livre: "dá p/ subir"
73
+ else if (wallAhead && dir !== 0)
74
+ ctx.noiseHit('parede', dir * 0.5, pc); // parede submersa à frente
75
+ else if (floorBelow && ctx.held(pl, 'down'))
76
+ ctx.noiseHit('areia', 0, pc); // fundo (chão)
77
+ else
78
+ played = false;
79
+ if (played) {
80
+ pl.wnT = 0;
81
+ _waterNavCount++;
82
+ }
83
+ else
84
+ pl.wnT = 17; // sem contato = SEM som (pronto p/ tocar ao encostar)
85
+ }
86
+ return {
87
+ playerCtx, caneProbe, caneTap, waterNav,
88
+ // Delegação PURA para a metade que viajou. Sem lógica no meio: um adaptador que decidisse alguma coisa
89
+ // seria uma terceira implementação escondida entre as duas.
90
+ panFor: (wx, pl) => som.panFor(wx, pl),
91
+ needsAudioCues: (pl) => som.needsAudioCues(pl),
92
+ sonar: (pl) => som.sonar(pl),
93
+ updateGuide: () => som.updateGuide(),
94
+ get caneCount() { return _caneCount; },
95
+ get waterNavCount() { return _waterNavCount; },
96
+ get sonarCount() { return som.sonarCount; },
97
+ get guideCount() { return som.guideCount; },
98
+ };
99
+ }
@@ -0,0 +1,71 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ import { type Spot, type Topology, type Speakable } from '../core/contract.js';
3
+ export type SinkAC = AudioContext & {
4
+ setSinkId?: (id: string) => Promise<void>;
5
+ };
6
+ /**
7
+ * O jogador visto pela navegação sonora. É a fatia MÍNIMA, e ela encolheu com o corte: `facing` ficou com a
8
+ * bengala (é ela que bate "à frente") e `wnT` com o nado. Sobrou identidade, posição, visão e o dispositivo.
9
+ */
10
+ export interface SonarPlayer extends PlayerAudioOut {
11
+ readonly i: number;
12
+ readonly x: number;
13
+ readonly y: number;
14
+ readonly viz: string;
15
+ readonly audioSink?: string | null;
16
+ guideT?: number;
17
+ }
18
+ /**
19
+ * A SAÍDA DEDICADA de um jogador, e este módulo é o DONO dela: é aqui que os dois campos NASCEM
20
+ * (`new AC()` + `createGain()`, logo abaixo). Pelo ADR-0039 o dono declara onde o campo nasce, e o
21
+ * `core/entity` não os menciona — eles não são da entidade da engine, são rascunho que o áudio pendura
22
+ * no jogador.
23
+ *
24
+ * `_acOut` admite `null` porque `ui/settings-audio` escreve `null` nos dois ao trocar de dispositivo.
25
+ * Enquanto o `core/entity` dizia `unknown`, essa escrita passava sem que ninguém visse que o dono
26
+ * declarava `GainNode` sem nulo — as duas descrições discordavam e o `unknown` era o que as escondia.
27
+ */
28
+ export interface PlayerAudioOut {
29
+ _ac?: SinkAC | null;
30
+ _acOut?: GainNode | null;
31
+ }
32
+ export interface PlayerCtxOut {
33
+ ac: AudioContext;
34
+ out: GainNode;
35
+ }
36
+ type VizDef = {
37
+ kind?: string;
38
+ } | undefined;
39
+ export interface SonarCtx {
40
+ /** Campo 1: a métrica. Função, porque um jogo com fases troca de topologia entre elas. */
41
+ topology: () => Topology;
42
+ /** Campo 5, metade "alvo": onde estão os alvos ainda válidos deste jogador. Vazio = nada a apontar. */
43
+ targetsOf: (playerIndex: number) => readonly Spot[];
44
+ /** Campo 3: como se chama o que está ali. `null` = sem nome, e o anúncio cai no genérico. */
45
+ nameAt: (at: Spot) => Speakable | null;
46
+ tonePan: (freq: number, dur: number, cat: string, pan?: number | null, vol?: number, type?: OscillatorType, pc?: PlayerCtxOut | null) => void;
47
+ srSay: (text: string) => void;
48
+ narrate: (text: string) => void;
49
+ VIZ_BY_KEY: Record<string, VizDef>;
50
+ getModoCego: () => boolean;
51
+ /** Largura LÓGICA da tela. É do console, não do gênero — por isso ctx, e não campo de contrato. */
52
+ LOGICAL_W: number;
53
+ getPlayers: () => SonarPlayer[];
54
+ getNumPlayers: () => number;
55
+ getAudioCtx: () => AudioContext | null;
56
+ getSoundOn: () => boolean;
57
+ getAudioCat: () => Record<string, {
58
+ on: boolean;
59
+ }> | null;
60
+ }
61
+ export interface AudioSonar {
62
+ playerCtx: (pl: SonarPlayer) => PlayerCtxOut | null;
63
+ panFor: (wx: number, pl: SonarPlayer) => number;
64
+ needsAudioCues: (pl: SonarPlayer) => boolean;
65
+ sonar: (pl: SonarPlayer) => void;
66
+ updateGuide: () => void;
67
+ readonly sonarCount: number;
68
+ readonly guideCount: number;
69
+ }
70
+ export declare function createAudioSonar(ctx: SonarCtx): AudioSonar;
71
+ export {};
@@ -0,0 +1,173 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // platform/audio-sonar — NAVEGAÇÃO SONORA, a metade de `audio-nav` que serve a QUALQUER gênero (item 19).
3
+ //
4
+ // ========================= O ACHADO 9, VIRADO CORTE =========================
5
+ // O segundo consumidor registrou, e não contornou:
6
+ //
7
+ // "O SONAR NÃO PODE SER USADO POR QUEM NÃO É PLATAFORMA… O ctx de `platform/audio-nav` pede 19 coisas, e
8
+ // seis delas são de plataforma pura — `tileAt`, `solidAt`, `BOX`, `TILE`, `getCoins`, `getCenario`. Um
9
+ // quiz teria de inventar tiles falsos, uma caixa de colisão falsa e um cenário falso para pedir 'aponte
10
+ // a alternativa mais próxima'. E o módulo é DOIS módulos com um nome só."
11
+ //
12
+ // Estes são os dois. Aqui fica o que navega por SOM: apontar o alvo mais perto, o beacon em laço, o pan pela
13
+ // posição relativa e a pergunta "esta criança precisa de pista sonora?". Em `audio-nav` ficam a BENGALA e o
14
+ // NADO CEGO, que leem tile, chão e caixa de colisão — plataforma, e sem disfarce.
15
+ //
16
+ // ========================= O QUE SUBSTITUIU AS SEIS COISAS DE PLATAFORMA =========================
17
+ // Nenhuma delas atravessou. O sonar perguntava ao jogo por um array de moedas e filtrava `taken`/`owner`;
18
+ // agora ele recebe o CONTRATO (ADR-0030):
19
+ //
20
+ // · `getCoins()` + o filtro → `targetsOf(i)`, a metade "alvo" do campo 5. O jogo entrega os pontos que
21
+ // ainda contam para aquele jogador, e nunca diz o que eles são.
22
+ // · `TILE` (a régua de "perto") → `topology()` + `distance()`, a métrica declarada no campo 1. Numa
23
+ // plataforma são pixels sobre `unit`; numa grade, passos de rei; num quiz, diferença de índice.
24
+ // · `t('sr.nav.coin')` (o nome do alvo) → `nameAt(spot)`, campo 3. É o que faz o anúncio dizer "moeda",
25
+ // "pergunta" ou "caixa" sem a engine conhecer nenhum dos três.
26
+ //
27
+ // Sobrou UMA constante de mundo: `LOGICAL_W`, usada só pelo pan. Ela é da TELA e não do gênero — todo jogo
28
+ // deste console tem 320 px de largura lógica —, e por isso entra por ctx em vez de virar campo de contrato.
29
+ //
30
+ // ========================= SEM I/O NO IMPORT =========================
31
+ // Nada aqui toca `window` fora de `playerCtx`, que é chamada e não importada. Roda no project `node`.
32
+ import { distance, bearing } from '../core/contract.js';
33
+ import { t } from '../core/i18n.js';
34
+ export function createAudioSonar(ctx) {
35
+ let _sonarCount = 0, _guideCount = 0;
36
+ /** AudioContext do jogador (para o `setSinkId` no dispositivo dele), ou null → contexto global. */
37
+ function playerCtx(pl) {
38
+ if (!pl || !pl.audioSink)
39
+ return null;
40
+ try {
41
+ if (!pl._ac) {
42
+ const AC = window.AudioContext || window.webkitAudioContext;
43
+ if (!AC)
44
+ return null;
45
+ pl._ac = new AC();
46
+ pl._acOut = pl._ac.createGain();
47
+ pl._acOut.connect(pl._ac.destination);
48
+ if (pl._ac.setSinkId)
49
+ pl._ac.setSinkId(pl.audioSink).catch(() => { });
50
+ }
51
+ if (pl._ac.state === 'suspended')
52
+ pl._ac.resume();
53
+ return { ac: pl._ac, out: pl._acOut };
54
+ }
55
+ catch (e) {
56
+ return null;
57
+ }
58
+ }
59
+ function panFor(wx, pl) {
60
+ return Math.max(-1, Math.min(1, (wx - pl.x) / (ctx.LOGICAL_W * 0.55)));
61
+ }
62
+ /** Visão comprometida? Guarda e guia só existem quando a resposta é sim (ou no modo cego). */
63
+ function needsAudioCues(pl) {
64
+ if (ctx.getModoCego())
65
+ return true;
66
+ const m = ctx.VIZ_BY_KEY[pl.viz];
67
+ return !!(m && (m.kind === 'blind' || m.kind === 'lowvision'));
68
+ }
69
+ /**
70
+ * O alvo mais perto deste jogador, na MÉTRICA DECLARADA — e `null` se não houver nenhum.
71
+ *
72
+ * Era um laço sobre `getCoins()` com `if (cn.taken || cn.owner !== pl.i) continue` e `Math.hypot`. As três
73
+ * coisas que ele sabia sobre o jogo (que alvos são moedas, que moedas têm dono, que distância é euclidiana
74
+ * em pixels) viraram uma pergunta ao contrato e uma chamada a `distance`.
75
+ */
76
+ /**
77
+ * O RUMO EM PALAVRAS, na língua de quem joga e no referencial que o JOGO declarou.
78
+ *
79
+ * ⚠️ O QUE ISTO SUBSTITUI ERA UM DEFEITO DE ACESSIBILIDADE, não um detalhe de estilo. O sonar calculava o
80
+ * lado à mão, de `x` cru, com zona morta de ±4:
81
+ *
82
+ * alvo.at.x < pl.x - 4 ? 'left' : alvo.at.x > pl.x + 4 ? 'right' : 'ahead'
83
+ *
84
+ * Três palavras onde o contrato tem oito — e MISTURANDO REFERENCIAIS: *esquerda/direita* é relativo à TELA,
85
+ * *à frente* é relativo ao CORPO, e quem ouve não tem como saber de que origem cada uma fala. Pior: tudo o
86
+ * que estivesse acima ou abaixo da criança virava «à frente», que é justamente a informação que mais falta
87
+ * a quem não vê a tela — apagada por uma zona morta.
88
+ *
89
+ * ⚠️ E O REFERENCIAL É DO JOGO. Num tabuleiro diz-se «a nordeste»; numa plataforma 2D vista de lado, norte
90
+ * não quer dizer nada, e o que se diz é «às 2 horas».
91
+ */
92
+ function emPalavras(r) {
93
+ if (r.kind === 'none')
94
+ return t('sr.nav.here');
95
+ // Uma chave com `{h}` e não doze — mas a forma do singular é sua, porque «às 1 horas» não é português
96
+ // (nem «a las 1» é espanhol). Em inglês as duas coincidem, e coincidir não é motivo para não a ter.
97
+ if (r.kind === 'clock')
98
+ return r.hour === 1 ? t('sr.nav.clockOne') : t('sr.nav.clock', { h: r.hour });
99
+ return t('sr.nav.dir.' + r.heading);
100
+ }
101
+ function alvoMaisProximo(pl) {
102
+ const topo = ctx.topology();
103
+ const aqui = { x: pl.x, y: pl.y };
104
+ let melhor = null, bd = Infinity;
105
+ for (const alvo of ctx.targetsOf(pl.i)) {
106
+ const d = distance(topo, aqui, alvo);
107
+ if (d < bd) {
108
+ bd = d;
109
+ melhor = alvo;
110
+ }
111
+ }
112
+ return melhor ? { at: melhor, d: bd } : null;
113
+ }
114
+ /**
115
+ * "Perto" em PASSOS da métrica declarada, e não em pixels.
116
+ *
117
+ * Os limiares de 4 e 9 tiles do original viram 4 e 9 UNIDADES: numa plataforma com `unit = TILE` a conta é
118
+ * a mesma de antes, letra por letra; numa grade são quatro e nove casas; num quiz, quatro e nove itens de
119
+ * distância na lista. É a mesma frase para a criança em qualquer gênero, que é o ponto do contrato.
120
+ */
121
+ const chaveDeDistancia = (d) => d < 4 ? 'sr.nav.veryClose' : d < 9 ? 'sr.nav.close' : 'sr.nav.far';
122
+ function sonar(pl) {
123
+ _sonarCount++;
124
+ const pc = playerCtx(pl);
125
+ const alvo = alvoMaisProximo(pl);
126
+ // A chave era `sr.nav.noCoinNear` — "Nenhuma moeda por perto." Um sonar que não sabe mais o que é o alvo
127
+ // não pode dizer o nome dele no caso em que NÃO HÁ alvo nenhum: virou "Nada por perto.", que é verdade em
128
+ // qualquer gênero. Foi o gate de fixtures que cobrou, ao acusar a palavra dentro do teste novo.
129
+ if (!alvo) {
130
+ ctx.tonePan(300, 0.2, 'sonar', 0, 0.2, 'sine', pc);
131
+ ctx.srSay(t('sr.nav.noTargetNear'));
132
+ return;
133
+ }
134
+ const pan = panFor(alvo.at.x, pl), near = Math.max(0, 1 - alvo.d / 12);
135
+ ctx.tonePan(380 + 740 * near, 0.16, 'sonar', pan, 0.26, 'sine', pc); // mais perto = mais agudo
136
+ // O NOME vem do jogo (campo 3). Antes era `t('sr.nav.coin')` — a engine dizia "moeda" porque só conhecia
137
+ // moedas. O fallback existe para o jogo que declara alvo sem nome: melhor "alvo" do que uma chave crua.
138
+ const nome = ctx.nameAt(alvo.at);
139
+ const corpo = t('sr.nav.sonarFound', {
140
+ alvo: nome ? nome.text : t('sr.nav.target'),
141
+ lado: emPalavras(bearing(ctx.topology(), { x: pl.x, y: pl.y }, alvo.at)),
142
+ dist: t(chaveDeDistancia(alvo.d)),
143
+ });
144
+ const msg = (ctx.getNumPlayers() > 1 ? t('sr.player.prefix', { n: pl.i + 1 }) : '') + corpo;
145
+ ctx.srSay(msg);
146
+ ctx.narrate(msg);
147
+ }
148
+ /** Beacon automático por quadro: o sonar contínuo de quem não vê a tela. */
149
+ function updateGuide() {
150
+ const cat = ctx.getAudioCat();
151
+ if (!ctx.getAudioCtx() || !ctx.getSoundOn() || !cat || !cat.guide || !cat.guide.on)
152
+ return;
153
+ for (const pl of ctx.getPlayers()) {
154
+ if (!needsAudioCues(pl))
155
+ continue;
156
+ pl.guideT = (pl.guideT || 0) + 1;
157
+ if (pl.guideT < 48)
158
+ continue;
159
+ pl.guideT = 0; // pinga ~0,8s
160
+ const alvo = alvoMaisProximo(pl);
161
+ if (!alvo)
162
+ continue;
163
+ const pan = panFor(alvo.at.x, pl), near = Math.max(0, 1 - alvo.d / 14);
164
+ ctx.tonePan(300 + 380 * near, 0.12, 'guide', pan, 0.11, 'triangle', playerCtx(pl));
165
+ _guideCount++;
166
+ }
167
+ }
168
+ return {
169
+ playerCtx, panFor, needsAudioCues, sonar, updateGuide,
170
+ get sonarCount() { return _sonarCount; },
171
+ get guideCount() { return _guideCount; },
172
+ };
173
+ }
@@ -0,0 +1,32 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ type CatState = {
3
+ on: boolean;
4
+ vol: number;
5
+ };
6
+ type PlayerCtx = {
7
+ ac: AudioContext;
8
+ out: AudioNode;
9
+ };
10
+ type ACWithCache = AudioContext & {
11
+ _noiseBuf?: AudioBuffer;
12
+ };
13
+ export declare let soundOn: boolean;
14
+ export declare function setSoundOn(v: boolean): void;
15
+ export declare let volume: number;
16
+ export declare function setVolume(v: number): void;
17
+ export declare let audioCtx: AudioContext | null;
18
+ export declare function ensureAC(): AudioContext | null;
19
+ export declare let hearingLoss: boolean;
20
+ export declare function audioOut(): GainNode | null;
21
+ export declare function setHearingLossGraph(on: boolean): void;
22
+ export declare function setMasterMuted(muted: boolean): void;
23
+ export declare let audioCat: Record<string, CatState> | null;
24
+ export declare function initAudioMixer(): void;
25
+ export declare function catNode(cat: string): GainNode | null;
26
+ export declare function setCatGain(cat: string): void;
27
+ export declare function tone(freq: number, dur: number, type?: OscillatorType, when?: number, vol?: number): void;
28
+ export declare function tonePan(freq: number, dur: number, cat: string, pan?: number | null, vol?: number, type?: OscillatorType, pc?: PlayerCtx | null): void;
29
+ export declare let _footCount: number;
30
+ export declare function noiseBuffer(ac: ACWithCache): AudioBuffer;
31
+ export declare function noiseHit(mat: string, pan?: number | null, pc?: PlayerCtx | null): void;
32
+ export {};
@@ -0,0 +1,191 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // platform/audio.ts — base do áudio: ciclo do AudioContext (ensureAC) + definições de SFX (dados). Módulo-folha.
3
+ // O grafo (master/mixer), soundOn/volume e as sínteses (tone/noiseHit/tonePan/sfx) ainda ficam no game.js —
4
+ // migram numa rodada dedicada (injeção de dependência). audioCtx é binding vivo; só ensureAC o (re)cria. (Fase 2)
5
+ import { loadAudioCat, saveAudioCat } from './audio-mixer.js'; // mixer por categoria (dados + persistência)
6
+ // Som mestre: liga/desliga + volume (0..1). Sessão-only (sem persistência). Bindings vivos: as sínteses
7
+ // (tone/noiseHit/…) e a narração leem soundOn/volume; o botão/slider mestre do game.js escreve pelos setters.
8
+ export let soundOn = true;
9
+ export function setSoundOn(v) { soundOn = v; }
10
+ export let volume = 0.6;
11
+ export function setVolume(v) { volume = v; }
12
+ export let audioCtx = null;
13
+ export function ensureAC() {
14
+ if (!audioCtx) {
15
+ const AC = window.AudioContext || window.webkitAudioContext;
16
+ if (AC)
17
+ audioCtx = new AC();
18
+ }
19
+ if (audioCtx && audioCtx.state === 'suspended')
20
+ audioCtx.resume();
21
+ return audioCtx;
22
+ }
23
+ // ===== Nó MESTRE: master gain → (filtro de perda auditiva opcional) → destino. =====
24
+ export let hearingLoss = false;
25
+ let _masterGain = null, _hlChain = null;
26
+ export function audioOut() { const ac = ensureAC(); if (!ac)
27
+ return null; if (!_masterGain) {
28
+ _masterGain = ac.createGain();
29
+ wireMaster();
30
+ } return _masterGain; }
31
+ function buildHearingChain(ac) {
32
+ const lp = ac.createBiquadFilter();
33
+ lp.type = 'lowpass';
34
+ lp.frequency.value = 1400;
35
+ lp.Q.value = 0.7; // agudos primeiro
36
+ const sp = ac.createScriptProcessor(512, 1, 1);
37
+ const TH = 0.06, RED = 0.12; // frame < limiar → ×0.12 (contrário de aparelho auditivo)
38
+ sp.onaudioprocess = (e) => { const inp = e.inputBuffer.getChannelData(0), out = e.outputBuffer.getChannelData(0); let s = 0; for (let i = 0; i < inp.length; i++)
39
+ s += inp[i] * inp[i]; const g = Math.sqrt(s / inp.length) < TH ? RED : 1; for (let i = 0; i < inp.length; i++)
40
+ out[i] = inp[i] * g; };
41
+ lp.connect(sp);
42
+ sp.connect(ac.destination);
43
+ return { input: lp };
44
+ }
45
+ function wireMaster() {
46
+ const ac = audioCtx;
47
+ if (!ac || !_masterGain)
48
+ return;
49
+ try {
50
+ _masterGain.disconnect();
51
+ }
52
+ catch (e) { /* noop */ }
53
+ if (hearingLoss) {
54
+ if (!_hlChain)
55
+ _hlChain = buildHearingChain(ac);
56
+ _masterGain.connect(_hlChain.input);
57
+ }
58
+ else
59
+ _masterGain.connect(ac.destination);
60
+ }
61
+ // Empatia de perda auditiva: liga/desliga o filtro no nó mestre. A persistência + srSay ficam no game.js.
62
+ export function setHearingLossGraph(on) { hearingLoss = on; if (audioCtx) {
63
+ audioOut();
64
+ wireMaster();
65
+ } }
66
+ // Mute mestre (pausa/título silenciam tudo; volta ao jogar). Só age se o nó mestre já existe (não o cria à toa).
67
+ export function setMasterMuted(muted) { if (!_masterGain || !audioCtx)
68
+ return; try {
69
+ _masterGain.gain.setTargetAtTime(muted ? 0 : 1, audioCtx.currentTime, 0.04);
70
+ }
71
+ catch (e) { /* noop */ } }
72
+ // ===== Mixer por categoria: cada categoria tem seu gain (liga/desliga + volume), pendurado no nó mestre. =====
73
+ // audioCat é a fonte viva do estado do mixer (mutada pela UI/calmMode no game.js — objeto, nunca reatribuído
74
+ // APÓS init). NULL até initAudioMixer(): o import fica PURO (não lê localStorage). initAudioMixer é chamado no
75
+ // boot do game.js — todos os leitores de audioCat (mixer/ambiente/narração) rodam depois. (Fase 2.25)
76
+ export let audioCat = null;
77
+ // Carrega o estado do mixer (defaults do audio-mixer.js + o que estiver salvo). I/O EXPLÍCITO, idempotente.
78
+ export function initAudioMixer() { if (!audioCat)
79
+ audioCat = loadAudioCat(); }
80
+ const _catNodes = {};
81
+ export function catNode(cat) { const ac = ensureAC(); if (!ac || !audioCat)
82
+ return null; const out = audioOut(); if (!out)
83
+ return null; if (!_catNodes[cat]) {
84
+ const g = ac.createGain();
85
+ g.gain.value = audioCat[cat].on ? audioCat[cat].vol : 0;
86
+ g.connect(out);
87
+ _catNodes[cat] = g;
88
+ } return _catNodes[cat]; }
89
+ export function setCatGain(cat) { if (!audioCat)
90
+ return; const g = _catNodes[cat]; if (g && audioCtx)
91
+ g.gain.setTargetAtTime(audioCat[cat].on ? audioCat[cat].vol : 0, audioCtx.currentTime, 0.02); saveAudioCat(cat, audioCat[cat]); }
92
+ // ===== Sínteses de oscilador (earcons/melodias). Leem soundOn/volume; roteiam pelo mixer→mestre. =====
93
+ // pc = contexto de áudio por-jogador (opcional; o game.js o passa p/ rotear a pista ao dispositivo do jogador).
94
+ export function tone(freq, dur, type, when, vol) {
95
+ if (!soundOn || volume <= 0)
96
+ return;
97
+ try {
98
+ const ac = ensureAC();
99
+ if (!ac)
100
+ return;
101
+ const o = ac.createOscillator(), g = ac.createGain(), t = ac.currentTime + (when || 0);
102
+ o.type = type || 'square';
103
+ o.frequency.setValueAtTime(freq, t);
104
+ g.gain.setValueAtTime(0.0001, t);
105
+ g.gain.exponentialRampToValueAtTime(Math.max(0.02, (vol || 0.22) * volume), t + 0.01);
106
+ g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
107
+ o.connect(g).connect(catNode('earcons') || audioOut() || ac.destination);
108
+ o.start(t);
109
+ o.stop(t + dur + 0.02);
110
+ }
111
+ catch (e) { /* noop */ }
112
+ }
113
+ export function tonePan(freq, dur, cat, pan, vol, type, pc) {
114
+ if (!soundOn || volume <= 0)
115
+ return;
116
+ const ac = pc ? pc.ac : ensureAC();
117
+ if (!ac)
118
+ return;
119
+ try {
120
+ const o = ac.createOscillator(), g = ac.createGain(), t = ac.currentTime;
121
+ o.type = type || 'sine';
122
+ o.frequency.value = freq;
123
+ g.gain.setValueAtTime(0.0001, t);
124
+ g.gain.exponentialRampToValueAtTime(Math.max(0.02, (vol || 0.2) * volume), t + 0.01);
125
+ g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
126
+ let node = g;
127
+ if (pan != null && ac.createStereoPanner) {
128
+ const p = ac.createStereoPanner();
129
+ p.pan.value = Math.max(-1, Math.min(1, pan));
130
+ g.connect(p);
131
+ node = p;
132
+ }
133
+ o.connect(g);
134
+ node.connect(pc ? pc.out : (catNode(cat) || audioOut() || ac.destination));
135
+ o.start(t);
136
+ o.stop(t + dur + 0.02);
137
+ }
138
+ catch (e) { /* noop */ }
139
+ }
140
+ // ===== Synth de RUÍDO (passos por material, bengala). noiseBuffer cacheia por-contexto (suporta AC por jogador). =====
141
+ export let _footCount = 0; // contador de passos/pancadas (estatística de a11y; o __incl do game.js lê via getter)
142
+ export function noiseBuffer(ac) { if (ac._noiseBuf && ac._noiseBuf.length === ((ac.sampleRate * 0.2) | 0))
143
+ return ac._noiseBuf; const n = (ac.sampleRate * 0.2) | 0, b = ac.createBuffer(1, n, ac.sampleRate), d = b.getChannelData(0); for (let i = 0; i < n; i++)
144
+ d[i] = Math.random() * 2 - 1; return ac._noiseBuf = b; }
145
+ const FOOT = { grama: { f: 'highpass', hz: 2000, d: 0.09, v: 0.10 }, piso: { f: 'bandpass', hz: 1200, d: 0.06, v: 0.15 }, pedra: { f: 'highpass', hz: 1600, d: 0.05, v: 0.19 },
146
+ areia: { f: 'lowpass', hz: 650, d: 0.13, v: 0.10 }, madeira: { f: 'bandpass', hz: 480, d: 0.08, v: 0.15 }, ferro: { f: 'bandpass', hz: 2600, d: 0.12, v: 0.16 }, parede: { f: 'highpass', hz: 3200, d: 0.10, v: 0.08 },
147
+ terra: { f: 'lowpass', hz: 520, d: 0.11, v: 0.12 }, agua: { f: 'lowpass', hz: 330, d: 0.15, v: 0.13 } };
148
+ export function noiseHit(mat, pan, pc) {
149
+ if (!soundOn || volume <= 0)
150
+ return;
151
+ const ac = pc ? pc.ac : ensureAC();
152
+ if (!ac)
153
+ return;
154
+ const f = FOOT[mat] || FOOT.piso;
155
+ try {
156
+ const src = ac.createBufferSource();
157
+ src.buffer = noiseBuffer(ac);
158
+ const bq = ac.createBiquadFilter();
159
+ bq.type = f.f;
160
+ bq.frequency.value = f.hz;
161
+ bq.Q.value = 1.2;
162
+ const g = ac.createGain(), t = ac.currentTime;
163
+ g.gain.setValueAtTime(0.0001, t);
164
+ g.gain.exponentialRampToValueAtTime(Math.max(0.02, f.v * volume), t + 0.006);
165
+ g.gain.exponentialRampToValueAtTime(0.0001, t + f.d);
166
+ let node = g;
167
+ if (pan != null && ac.createStereoPanner) {
168
+ const p = ac.createStereoPanner();
169
+ p.pan.value = Math.max(-1, Math.min(1, pan));
170
+ g.connect(p);
171
+ node = p;
172
+ }
173
+ src.connect(bq).connect(g);
174
+ node.connect(pc ? pc.out : (catNode('interact') || audioOut() || ac.destination));
175
+ src.start(t);
176
+ src.stop(t + f.d + 0.03);
177
+ _footCount++;
178
+ }
179
+ catch (e) { /* noop */ }
180
+ }
181
+ /* A TABELA DE EARCONS SAIU daqui em 2026-08-25 (item 19) — está em `game/earcons`.
182
+ *
183
+ * De dez earcons, sete eram deste jogo: `coin`/`key`/`gate`/`power` são objetos deste mundo e
184
+ * `correct`/`wrong`/`place` são eventos da atividade. O gate acusava só a linha do `coin`, porque é a única
185
+ * com palavra que o casador conhece — e a linha acusada era a menos interessante das dez.
186
+ *
187
+ * As legendas eram pt-BR CRU (`'🔊 Coletou'`, `'🔊 Portão abriu'`): nove frases dentro de um módulo de
188
+ * plataforma, que o gate de i18n do `main.js` não alcança. Viraram CHAVES, e quem exibe resolve.
189
+ *
190
+ * O que ficou aqui é a SÍNTESE — oscilador, envelope, ruído, roteamento por categoria —, que serve a
191
+ * qualquer jogo. O que é 880 Hz de triangular por 0,14 s ao pegar uma moeda, não. */
@@ -0,0 +1,19 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ /** O que o chamador precisa fornecer. Nada aqui conhece PIPER, Web Audio ou o navegador. */
3
+ export interface MotorDeFala<Audio, Fonte> {
4
+ /** Texto → áudio. ASSÍNCRONO de propósito: é onde a síntese neural gasta o tempo dela. */
5
+ sintetizar(texto: string): Promise<Audio>;
6
+ /** Começa a tocar e devolve a fonte, para que ela possa ser parada. `null` = não deu para tocar agora. */
7
+ tocar(audio: Audio, aoTerminar: () => void): Fonte | null;
8
+ /** Cala a fonte. Chamado com o que `tocar` devolveu, e nunca com `null`. */
9
+ parar(fonte: Fonte): void;
10
+ }
11
+ export interface FalaInterrompivel {
12
+ /** Fala `texto`, CALANDO na hora o que estiver falando. Não enfileira: o último pedido é o que vale. */
13
+ falar(texto: string): void;
14
+ /** Cala e esquece. Usado ao fechar um menu ou ao desligar a narração. */
15
+ calar(): void;
16
+ /** Está tocando alguma coisa? Só para teste e depuração — a política não depende disto. */
17
+ falando(): boolean;
18
+ }
19
+ export declare function criarFalaInterrompivel<Audio, Fonte>(motor: MotorDeFala<Audio, Fonte>): FalaInterrompivel;