@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,381 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // input/keydown.ts — O ROTEADOR DE TECLADO: o que UMA tecla SIGNIFICA, aqui e agora.
3
+ //
4
+ // Este é o ouvinte de `keydown` de BOLHA do game.js — o único ponto do jogo por onde passa toda tecla que não
5
+ // foi interceptada antes. Ele não é "o controle do personagem": andar/pular são consequência da ÚLTIMA das
6
+ // nove perguntas que ele faz. As oito primeiras decidem se a tecla é do jogo ou de outra coisa — demo,
7
+ // remapeamento, diálogo, tela de vitória, menu inicial, número de telas, pausa, quiz — e é essa SEQUÊNCIA que
8
+ // é a especificação: quem vem antes vence, e mudar a ordem de duas guardas muda o jogo sem quebrar nada que
9
+ // um build, um `tsc` ou um olho consigam ver.
10
+ //
11
+ // ======================= A ENTREGA: DECIDIR ≠ EXECUTAR =======================
12
+ // O bloco original é uma CADEIA DE DECISÃO PURA DISFARÇADA DE EFEITO. Dado (código da tecla, fase, quem tem
13
+ // quiz aberto, qual diálogo está visível, número de telas, esquemas de tecla), existe UMA resposta sobre o que
14
+ // a tecla significa — e o `preventDefault`, o `click()`, o `togglePause()` vêm só DEPOIS dessa resposta. No
15
+ // monólito as duas metades estavam trançadas: cada `if` misturava a pergunta ("tem diálogo aberto?") com a
16
+ // resposta ("então esconde e sai"), e por isso a ordem de precedência — a parte que realmente importa — só
17
+ // podia ser verificada jogando.
18
+ //
19
+ // Aqui elas são dois objetos separados:
20
+ // · `decideKeydown(evento, estado) -> KeydownDecision` — PURA. Sem DOM, sem PIXI, sem áudio, sem `window`.
21
+ // Roda no project `node`. Cada ramo da ordem de precedência é um caso de teste, e a precedência ENTRE os
22
+ // ramos (quiz aberto E diálogo visível: quem ganha?) é uma asserção, não um relato.
23
+ // · `initKeydown(ctx).onKeydown(e)` — a metade IMPURA: pega a decisão pronta e a carimba no mundo.
24
+ // Mesmo movimento que `ui/shell.ts` fez com `phaseView`/`applyPhaseView` e `ui/title.ts` com
25
+ // `computeTitleMenuView`; é o precedente da casa, e aqui ele vale mais do que lá, porque o que está em jogo é
26
+ // o teclado — a única modalidade que TODA pessoa que joga sem mouse, sem visão ou sem controle depende.
27
+ //
28
+ // ======================= O QUE FICOU DE FORA DA PARTE PURA, E POR QUÊ =======================
29
+ // Duas guardas do topo NÃO cabem em `decideKeydown`, e forçá-las lá seria mentira:
30
+ // · `attractCtl.onInput()` (game/attract.ts) — ela DECIDE E AGE na mesma chamada: zera o contador de
31
+ // ociosidade, encerra a demo se estiver rodando, e devolve se encerrou. Não há como perguntar sem mexer.
32
+ // · `ctrlPanel.handleCaptureKeydown(e)` (ui/settings-controls.ts) — idem: se há remapeamento em curso, ela
33
+ // GRAVA a tecla no esquema e devolve `true`.
34
+ // As duas continuam sendo as duas primeiras linhas de `onKeydown`, na mesma ordem, e o teste que prova que
35
+ // elas vêm antes de tudo é do wrapper (com sondas de mentira), não da função pura.
36
+ // A leitura de VISIBILIDADE dos diálogos também não entra pura: ela é `querySelector` + `.hidden`. O que entra
37
+ // na parte pura é o RESULTADO dela (três booleanos e um id), montado por `snapshot()`.
38
+ //
39
+ // ======================= ⚠️ PAUSADO, ESTE OUVINTE NÃO É ALCANÇADO PARA Escape =======================
40
+ // `ui/menu-nav.ts` registra o PRÓPRIO `keydown` em fase de CAPTURA e dá `stopPropagation()`. Com o jogo
41
+ // PAUSADO (`phase==='paused'`), toda tecla com intenção de menu — Escape inclusive — morre lá em cima e nunca
42
+ // desce até aqui. Consequência MEDIDA no navegador, e anotada tanto no game.js quanto no cabeçalho de
43
+ // menu-nav (DEFEITO 2): a cadeia `overlays.escapeTarget()` e o `togglePause()` de Escape são, na prática,
44
+ // código morto enquanto pausado; quem fecha é sempre o topo da pilha de z-index.
45
+ // ISTO É COMPORTAMENTO ATUAL E FOI PRESERVADO — não é bug para consertar aqui, é rede de segurança de que o
46
+ // conserto futuro vai precisar (tirar o `stopPropagation()` sem mais nada faz o `if(Escape||Enter)
47
+ // togglePause()` daqui DESPAUSAR o jogo com a Ajuda aberta). Os testes o ancoram como está: `decideKeydown`
48
+ // devolve a decisão que ELE tomaria, e o teste do wrapper documenta que, pausado, ela não é consultada porque
49
+ // o evento não chega. Ver `tests/keydown.node.test.js`, bloco "a captura do menu-nav chega antes".
50
+ //
51
+ // ======================= O QUE VEIO JUNTO, E O QUE NÃO VEIO =======================
52
+ // · `keyup` VEIO. Ele é a outra metade exata do `keys.add(e.code)` da última linha do keydown: sem os dois no
53
+ // mesmo lugar, quem lê "quando uma tecla entra em `keys`" tem de procurar a saída em outro arquivo. `attach()`
54
+ // instala os dois, na mesma ordem de antes.
55
+ // · `blur` NÃO veio. Parece irmão, mas não é: `keys` também recebe códigos INJETADOS pelos botões de toque
56
+ // (o `press`/`release` do game.js, que faz `keys.add(codeFor(act))`) e pelo controle por webcam
57
+ // (`ui/webcam.ts` despacha `KeyboardEvent` sintético). `blur -> keys.clear()` é uma rede de ciclo de vida da
58
+ // JANELA sobre um conjunto que não é só do teclado; trazê-la para cá daria a este módulo a
59
+ // responsabilidade de limpar estado que ele não escreve. Fica no game.js, uma linha, ao lado do `resize`.
60
+ // · A cadeia de Escape, o registro de overlays e o `closeById` são de `ui/settings-panel.ts`. Este módulo
61
+ // CONSOME (`escapeTarget`, `closeOverlayById`) e não reimplementa — mesma disciplina de menu-nav.
62
+ // · `whichPlayer`/`actionOf`/`controlsState` são de `input/keyboard-runtime.ts`, que é quem sabe de esquema de
63
+ // teclas. Aqui eles entram injetados e são só consultados. `controlsState()` é MEMORIZADO lá: este módulo o
64
+ // chama uma vez por tecla, como o original.
65
+ // · A navegação do menu inicial (`navTitle`) é de `ui/activities-menu.ts`; a do menu de pausa é de
66
+ // `ui/menu-nav.ts`. Este módulo decide QUE a tecla é de navegação e para QUEM ela vai — nunca desenha menu.
67
+ //
68
+ // ======================= INJEÇÃO E ORDEM DE BOOT =======================
69
+ // `initKeydown` tem de ser chamado NO LUGAR EXATO onde o `addEventListener('keydown', …)` estava (logo depois
70
+ // de `assignControls()`), por dois motivos independentes:
71
+ // 1. ORDEM DE REGISTRO. Os ouvintes de bolha do mesmo alvo disparam na ordem em que foram registrados. Este é
72
+ // hoje o primeiro `keydown` de bolha da janela, e o único de captura (menu-nav) já vence por fase.
73
+ // Registrar mais tarde não muda nada HOJE, mas passa a depender de nenhum outro ouvinte de janela nascer
74
+ // no meio — dependência invisível que não vale a pena criar.
75
+ // 2. TDZ. Quase tudo o que este módulo aciona (`attractCtl`, `ctrlPanel`, `gamepadApi`, `hud`, `navTitle`,
76
+ // `activateScreens`, `togglePause`, `hideTouchControls`, as quatro funções de quiz) é `const`/`function`
77
+ // declarado CENTENAS de linhas ABAIXO desse ponto. O ouvinte original só funcionava porque o corpo dele é
78
+ // preguiçoso: nada é lido até a primeira tecla. Por isso TODO o ctx é `() => …`: valor nenhum é resolvido
79
+ // na montagem. Passar `attractCtl` (ou `hud`, ou `navTitle`) por VALOR derruba o boot em TDZ.
80
+ // A regra da casa vale igual: o que o game.js REATRIBUI entra por GETTER (`oneButton`, `numPlayers`, `players`,
81
+ // o estado de controles). `keys` é `const` de `input/state.ts`, mutado in place — entra por VALOR (é o mesmo
82
+ // objeto para sempre). `phase` é binding vivo de `core/state.ts`, importado direto, como em menu-nav.
83
+ //
84
+ // ÚNICA DIFERENÇA DE COMPORTAMENTO EM RELAÇÃO AO MONÓLITO, declarada: `snapshot()` lê os três `hidden` e o
85
+ // `escapeTarget()` de uma vez, enquanto o original os lia em curto-circuito (só chegava ao `#padwiz` se não
86
+ // houvesse overlay nem `#touchcfg`). São no máximo quatro `querySelector` a mais por tecla, todos de leitura
87
+ // pura (`escapeTarget` só varre o registro e testa `.hidden` — conferido em ui/settings-panel.ts). Nada
88
+ // observável muda; anotado porque medir depois é mais caro do que escrever agora.
89
+ //
90
+ // SEM I/O NO IMPORT: o corpo do módulo só declara tabelas congeladas e funções puras. Todo efeito passa por
91
+ // `initKeydown` (e mesmo ele não faz nada até `attach()`).
92
+ //
93
+ // Ver docs/5-Refactoring/plano-modularizacao-mapa.md (D2-a).
94
+ import { t } from '../core/i18n.js';
95
+ import { EDGE_BY_ACTION, edgeAllowed } from './edges.js';
96
+ /* --- tabelas (congeladas: são dados, e um `Set` nomeado deixa a tabela auditável de fora) --- */
97
+ /** Fácil (SOLO): Ctrl = Especial, Shift = Trocar poder. Fora de Win/Alt/AltGr, de propósito. */
98
+ export const EASY_SHORTCUTS = new Set(['ControlLeft', 'ControlRight', 'ShiftLeft', 'ShiftRight']);
99
+ /** Alt + fileira de números (NÃO o teclado numérico) = 1..4 telas. Alt fica livre: o solo não o usa. */
100
+ export const SCREEN_DIGITS = /^Digit[1234]$/;
101
+ /** Pausa: Escape ou o Enter CENTRAL. `NumpadEnter` NÃO pausa (E14, verbatim). */
102
+ export const PAUSE_KEYS = new Set(['Escape', 'Enter']);
103
+ // A tabela e a regra do Fácil vivem em input/edges.ts, compartilhadas com gamepad e touch-bindings. Reexporta
104
+ // pelo nome antigo porque quem importa daqui (teste e leitor) espera achá-la aqui.
105
+ export { EDGE_BY_ACTION, edgeAllowed } from './edges.js';
106
+ /* --- predicados puros (exportados: o teste importa a peça, não repete a expressão) --- */
107
+ /** A tecla vale como PULO para alguém? O alias do J1 OU a ação `jump` de qualquer jogador (é o que faz
108
+ * "qualquer um pode apertar Jogar de novo" e "qualquer um confirma no título" serem verdade). */
109
+ export function isJumpKey(code, s) {
110
+ return s.controls.action2.includes(code) || s.players.some((_p, i) => s.actionOf(code, i) === 'action2');
111
+ }
112
+ /** A tecla é de JOGO? Verbatim do `isGameKeyCode` do game.js — o alias achatado OU o esquema de algum
113
+ * jogador. NÃO inclui os atalhos do Fácil (o original também não: por isso `easyKey` é somado à parte). */
114
+ export function isGameKeyCode(code, s) {
115
+ return s.controls.gameKeys.includes(code)
116
+ || s.players.some((p) => !!p.ctrl && Object.values(p.ctrl).some((arr) => arr.includes(code)));
117
+ }
118
+ /** Os atalhos de acessibilidade do Fácil só existem SOLO (`numPlayers<=1`) e só para o Jogador 1. */
119
+ export function isEasyShortcut(code, s) {
120
+ return !!s.players[0]?.easy && s.numPlayers <= 1 && EASY_SHORTCUTS.has(code);
121
+ }
122
+ /**
123
+ * A intenção no menu inicial. Repare na ASSIMETRIA, preservada verbatim: `up`/`down` aceitam ArrowUp/ArrowDown
124
+ * ALÉM do alias configurado, `left`/`right` não aceitam ArrowLeft/ArrowRight. No esquema de fábrica isso não
125
+ * aparece (o solo já tem as setas nos quatro lados); aparece para quem REMAPEIA — remapeou esquerda para KeyA
126
+ * e a seta deixa de andar no menu, enquanto cima continua funcionando pelas duas. Está anotado como caso de
127
+ * teste em tests/keydown.node.test.js, e é candidato a conserto, não a "limpeza" silenciosa.
128
+ */
129
+ export function titleNavOf(code, s, action2) {
130
+ return {
131
+ yes: action2 || code === 'Enter',
132
+ no: code === 'Escape' || s.players.some((_p, i) => s.actionOf(code, i) === 'action3'),
133
+ up: s.controls.up.includes(code) || code === 'ArrowUp',
134
+ down: s.controls.down.includes(code) || code === 'ArrowDown',
135
+ left: s.controls.left.includes(code),
136
+ right: s.controls.right.includes(code),
137
+ };
138
+ }
139
+ /** Alguma intenção foi expressa? Definição única em input/edges; aqui só o nome que este módulo sempre teve. */
140
+ import { hasNavIntent as hasTitleIntent } from './edges.js';
141
+ export { hasNavIntent as hasTitleIntent } from './edges.js';
142
+ /**
143
+ * Quem é o dono do modal que esta tecla comanda? Devolve a POSIÇÃO no array, ou -1.
144
+ * Regra verbatim: tecla de um jogador vai para o modal DAQUELE jogador (e só se ele tiver um aberto); tecla
145
+ * genérica cai no Jogador 1. Tecla de um jogador SEM modal aberto não cai no P1 — ela desce para o jogo
146
+ * normal, que é o que deixa a partida dele continuar enquanto o outro resolve o desafio.
147
+ */
148
+ export function modalOwnerIndex(code, s) {
149
+ const owner = s.whichPlayer(code);
150
+ if (owner >= 0)
151
+ return s.modalOpen[owner] ? owner : -1;
152
+ return s.modalOpen[0] ? 0 : -1;
153
+ }
154
+ /**
155
+ * Que INTENÇÃO esta tecla exprime dentro do modal do dono. `owner` é a posição no array; `generic` diz se a
156
+ * tecla chegou sem dono (aí as seis leituras usam os aliases do J1 em vez da ação remapeada).
157
+ *
158
+ * As seis leituras são as mesmas de antes, letra por letra. O que saiu foi a linha seguinte: a que trocava
159
+ * "cima" por `delta:-3` e "esquerda" por `delta:-1`, e o desvio de Braille. Isso é grade, e grade é do jogo.
160
+ */
161
+ export function modalIntentOf(code, s, owner, generic) {
162
+ const pl = s.players[owner];
163
+ if (!pl)
164
+ return null;
165
+ const act = generic ? null : s.actionOf(code, pl.i); // `qpl.i`, não a posição — verbatim
166
+ const K = s.controls;
167
+ const L = act ? act === 'left' : K.left.includes(code);
168
+ const R = act ? act === 'right' : K.right.includes(code);
169
+ const U = act ? act === 'up' : K.up.includes(code);
170
+ const D = act ? act === 'down' : K.down.includes(code);
171
+ const J = act ? act === 'action2' : K.action2.includes(code);
172
+ // ESPECIAL = apagar a última sílaba/letra. A leitura genérica sai de `pl.ctrl`, NÃO de `K` — assimetria do
173
+ // original preservada (não existe alias `KESPECIAL` em ControlsState; o original alcançava `qpl.ctrl`).
174
+ const E = act ? act === 'action3' : ((pl.ctrl?.action3 || []).includes(code));
175
+ // A ORDEM é a do original e importa: com um esquema em que a mesma tecla é `left` e `jump`, ganha `left`.
176
+ if (L)
177
+ return 'left';
178
+ if (R)
179
+ return 'right';
180
+ if (U)
181
+ return 'up';
182
+ if (D)
183
+ return 'down';
184
+ if (J)
185
+ return 'confirm';
186
+ if (E)
187
+ return 'erase';
188
+ return null;
189
+ }
190
+ /**
191
+ * As bordas que esta tecla levanta. Só é chamada quando a tecla AINDA NÃO está segurada — é isso que a torna
192
+ * uma BORDA e não um estado, e é o que impede o auto-repeat do sistema de virar dez pulos.
193
+ * `p.ctrl[act] || []`: o original alcançava `p.ctrl.jump` direto e estouraria num esquema sem a ação. Trocar
194
+ * um TypeError por um no-op não remove rede de conserto nenhuma (o precedente é a guarda de `navPause` em
195
+ * ui/menu-nav.ts) — é a única linha desta função que não é cópia literal.
196
+ */
197
+ export function edgesFor(code, s) {
198
+ const out = [];
199
+ s.players.forEach((p, idx) => {
200
+ if (!p.ctrl)
201
+ return;
202
+ for (const [act, edge] of EDGE_BY_ACTION) {
203
+ if (!edgeAllowed(act, p.easy))
204
+ continue; // Fácil: sem correr (input/edges.ts, valendo nos três caminhos)
205
+ if ((p.ctrl?.[act] || []).includes(code))
206
+ out.push({ playerIndex: idx, edge });
207
+ }
208
+ });
209
+ // Fácil (solo): Ctrl vira Especial e Shift vira Trocar poder, SEMPRE no Jogador 1 (`player` do original).
210
+ if (isEasyShortcut(code, s))
211
+ out.push({ playerIndex: 0, edge: code.startsWith('Control') ? 'specialEdge' : 'swapEdge' });
212
+ return out;
213
+ }
214
+ /**
215
+ * A CADEIA. Nove perguntas, nesta ordem — e a ordem É a especificação.
216
+ * O que NÃO está aqui, e vem antes no wrapper: a demo (attract) e a captura de remapeamento. Ver o cabeçalho.
217
+ */
218
+ export function decideKeydown(ev, s) {
219
+ const code = ev.code;
220
+ // 1..3 · diálogo aberto BLOQUEIA o jogo — mas só se o elemento estiver DE FATO visível (uma flag presa não
221
+ // trava mais o teclado). Os três saem sem `preventDefault`, verbatim: o diálogo é DOM e quer o comportamento
222
+ // nativo do navegador (Tab, digitação num range) por baixo.
223
+ if (s.escapeTargetId)
224
+ return { kind: 'overlay', closeId: code === 'Escape' ? s.escapeTargetId : null, preventDefault: false };
225
+ if (s.touchCfgVisible)
226
+ return { kind: 'touchcfg', close: code === 'Escape', preventDefault: false };
227
+ if (s.padWizVisible)
228
+ return { kind: 'padwiz', close: code === 'Escape', preventDefault: false };
229
+ // 4..5 · fim de fase / título. O pulo de QUALQUER jogador aciona o botão principal, sem depender do foco do
230
+ // mouse (clicar na tela tirava o foco do botão e o teclado parava de funcionar — report do José).
231
+ const action2 = isJumpKey(code, s);
232
+ const pauseKey = PAUSE_KEYS.has(code);
233
+ if (s.winVisible) {
234
+ const again = action2 || pauseKey;
235
+ return { kind: 'win', again, preventDefault: again };
236
+ }
237
+ if (s.telaDeTitulo) {
238
+ // multi-tela: só o Jogador 1 escolhe o jogo. A tecla de um dos outros é consumida com um aviso falado.
239
+ if (s.numPlayers > 1 && s.whichPlayer(code) > 0)
240
+ return { kind: 'title', wait: true, nav: null, preventDefault: true };
241
+ const nav = titleNavOf(code, s, action2);
242
+ const has = hasTitleIntent(nav);
243
+ return { kind: 'title', wait: false, nav: has ? nav : null, preventDefault: has };
244
+ }
245
+ // 6..7 · número de telas e pausa. As DUAS exigem quiz FECHADO: com um desafio aberto na tela, Alt+3 não pode
246
+ // reconfigurar o jogo por baixo dele, e Enter é a confirmação do quiz, não a pausa.
247
+ const anyModal = s.modalOpen.some(Boolean);
248
+ const inGame = s.emJogo;
249
+ if (ev.altKey && !ev.ctrlKey && SCREEN_DIGITS.test(code) && inGame && !anyModal) {
250
+ return { kind: 'screens', count: +code.slice(5), preventDefault: true };
251
+ }
252
+ if (!anyModal && pauseKey && inGame)
253
+ return { kind: 'pause', preventDefault: true };
254
+ // 8 · modal. A tecla age no modal do DONO dela; genérica cai no P1. `preventDefault` NÃO depende de a
255
+ // tecla ter significado lá dentro: basta ser tecla de jogo — o desafio engole a tecla de qualquer forma.
256
+ if (anyModal) {
257
+ const owner = modalOwnerIndex(code, s);
258
+ if (owner >= 0) {
259
+ return { kind: 'modal', playerIndex: owner, intent: modalIntentOf(code, s, owner, s.whichPlayer(code) < 0),
260
+ preventDefault: s.controls.gameKeys.includes(code) };
261
+ }
262
+ // tecla de um jogador SEM modal cai no jogo normal (a partida dele continua) — segue adiante
263
+ }
264
+ // 9 · jogo normal.
265
+ const gameKey = isEasyShortcut(code, s) || isGameKeyCode(code, s);
266
+ const wake = s.players.reduce((acc, p, idx) => { if (p.waiting && s.actionOf(code, p.i))
267
+ acc.push(idx); return acc; }, []);
268
+ const edges = s.heldKeys.has(code) ? [] : edgesFor(code, s);
269
+ // Empatia motora (um botão por vez): a tecla nova entra e as OUTRAS de jogo saem. `keys.add` vem depois da
270
+ // limpeza no wrapper, então a recém-chegada sobrevive mesmo se já estivesse na lista.
271
+ const releaseKeys = s.oneButton && gameKey ? [...s.heldKeys].filter((k) => isGameKeyCode(k, s)) : [];
272
+ return { kind: 'play', gameKey, wake, edges, releaseKeys, preventDefault: gameKey };
273
+ }
274
+ export function initKeydown(ctx) {
275
+ /** Diálogo aberto = elemento que existe E não está `hidden`. Verbatim do `dlgVis` do monólito, que nasceu
276
+ * justamente para uma flag presa em `true` não travar o teclado do jogo inteiro. */
277
+ const visible = (id) => { const el = ctx.$('#' + id); return !!el && !el.hidden; };
278
+ function snapshot() {
279
+ return {
280
+ telaDeTitulo: ctx.isTelaDeTitulo(), emJogo: ctx.isEmJogo(), numPlayers: ctx.getNumPlayers(), players: ctx.getPlayers(), controls: ctx.getControls(),
281
+ // A RESPOSTA, ao lado dos jogadores — e montada aqui, onde eles já estão na mão (ADR-0033).
282
+ modalOpen: ctx.getPlayers().map((_, i) => ctx.hasModal(i)),
283
+ heldKeys: ctx.heldKeys, oneButton: ctx.isOneButton(),
284
+ escapeTargetId: ctx.escapeTarget(),
285
+ touchCfgVisible: visible('touchcfg'), padWizVisible: visible('padwiz'), winVisible: visible('win-overlay'),
286
+ actionOf: ctx.actionOf, whichPlayer: ctx.whichPlayer,
287
+ };
288
+ }
289
+ /** A metade IMPURA: pega a decisão pronta e a carimba no mundo. */
290
+ function apply(d, code) {
291
+ switch (d.kind) {
292
+ case 'overlay':
293
+ if (d.closeId)
294
+ ctx.closeOverlayById(d.closeId);
295
+ return;
296
+ case 'touchcfg': {
297
+ if (!d.close)
298
+ return;
299
+ const t = ctx.$('#touchcfg');
300
+ if (t)
301
+ t.hidden = true;
302
+ return;
303
+ }
304
+ case 'padwiz':
305
+ if (d.close)
306
+ ctx.closePadWiz(false);
307
+ return; // wizard de gamepad: Esc CANCELA (não salva)
308
+ case 'win': {
309
+ if (!d.again)
310
+ return;
311
+ const b = ctx.$('#btn-again');
312
+ if (b)
313
+ b.click();
314
+ return;
315
+ }
316
+ case 'title':
317
+ ctx.hideTouchControls(); // teclado no splash oculta os controles virtuais — ANTES do aviso, verbatim
318
+ if (d.wait) {
319
+ ctx.srSay(t('sr.title.waitP1'));
320
+ return;
321
+ }
322
+ if (d.nav)
323
+ ctx.navTitle(d.nav);
324
+ return;
325
+ case 'screens':
326
+ ctx.activateScreens(d.count);
327
+ return;
328
+ case 'pause':
329
+ ctx.togglePause();
330
+ return;
331
+ case 'modal': {
332
+ // A BUSCA DO JOGADOR SAIU DAQUI: este módulo a fazia só para repassar o objeto, e o objeto que ele
333
+ // sabia descrever não tinha `quiz` — que é justamente o que o outro lado precisa ler. Passa o índice.
334
+ if (!d.intent)
335
+ return;
336
+ ctx.modalInput(d.playerIndex, d.intent);
337
+ return;
338
+ }
339
+ case 'play': {
340
+ const players = ctx.getPlayers();
341
+ if (d.gameKey)
342
+ ctx.hideTouchControls('teclado'); // E13: jogar no teclado oculta os botões de toque
343
+ for (const idx of d.wake) { // a tecla DAQUELE jogador ativa a tela em espera
344
+ const p = players[idx];
345
+ if (!p)
346
+ continue;
347
+ p.waiting = false;
348
+ ctx.clearWaitingBadge(p.i);
349
+ ctx.srSay(t('sr.player.entered', { n: p.i + 1 }));
350
+ }
351
+ for (const { playerIndex, edge } of d.edges) {
352
+ const p = players[playerIndex];
353
+ if (p)
354
+ p[edge] = true;
355
+ }
356
+ for (const k of d.releaseKeys)
357
+ ctx.heldKeys.delete(k);
358
+ ctx.heldKeys.add(code);
359
+ return;
360
+ }
361
+ }
362
+ }
363
+ function onKeydown(e) {
364
+ if (ctx.attractOnInput()) {
365
+ e.preventDefault();
366
+ return;
367
+ } // qualquer tecla encerra a demo
368
+ if (ctx.handleCaptureKeydown(e))
369
+ return; // remap: a próxima tecla vira o controle
370
+ const d = decideKeydown(e, snapshot());
371
+ if (d.preventDefault)
372
+ e.preventDefault();
373
+ apply(d, e.code);
374
+ }
375
+ function onKeyup(e) { ctx.heldKeys.delete(e.code); }
376
+ function attach() {
377
+ ctx.win.addEventListener('keydown', onKeydown);
378
+ ctx.win.addEventListener('keyup', onKeyup);
379
+ }
380
+ return { snapshot, onKeydown, onKeyup, attach };
381
+ }
@@ -0,0 +1,30 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ /** Sentido travado: -1 esquerda, 0 parado, 1 direita. */
3
+ export type LatchDir = -1 | 0 | 1;
4
+ /** Fração da velocidade de caminhada quando o jogador SEGURA o botão no sentido travado. */
5
+ export declare const LATCH_HELD: number;
6
+ /** Fração quando a direção está travada mas nenhum botão está pressionado — o "andar sozinho". */
7
+ export declare const LATCH_IDLE: number;
8
+ /**
9
+ * O próximo sentido travado, dado o atual e as bordas de toque deste quadro.
10
+ *
11
+ * Um toque no sentido em que já se anda PARA (é o mesmo botão que liga e desliga — não há botão de parar, e
12
+ * inventar um custaria a quem tem um dedo só o dobro de alcances). Um toque no sentido oposto inverte
13
+ * direto, sem passar pelo zero: quem toca "esquerda" andando para a direita quer ir para a esquerda, não
14
+ * quer parar e tocar de novo.
15
+ *
16
+ * As duas bordas no MESMO quadro são possíveis (dois dedos, ou um switch duplo mal calibrado) e a ordem
17
+ * importa: esquerda é avaliada primeiro e direita depois, então direita vence. É o comportamento que a
18
+ * física sempre teve — duas linhas `if` em sequência —, preservado aqui de propósito e agora com um teste que
19
+ * o fixa, em vez de ser um efeito não intencional da ordem em que alguém escreveu dois `if`.
20
+ */
21
+ export declare function nextLatchedDir(current: LatchDir, leftEdge: boolean, rightEdge: boolean): LatchDir;
22
+ /**
23
+ * O multiplicador da velocidade de caminhada, COM sinal: o sentido travado vezes a fração que corresponde a
24
+ * estar ou não segurando o botão daquele sentido.
25
+ *
26
+ * Devolve o produto (e não só a fração) porque parado é 0 e nenhuma fração precisa opinar sobre isso —
27
+ * exatamente como a física fazia ao multiplicar por `dir`. Quem chama multiplica por `TUNE.hWalk`; a
28
+ * constante de ajuste do jogo não entra neste módulo.
29
+ */
30
+ export declare function latchedDrive(dir: LatchDir, holdingLeft: boolean, holdingRight: boolean): number;
@@ -0,0 +1,63 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // input/latch — SEGURAR VIRA ALTERNAR. Passo 2 do ADR-0027, metade da "entrada como dado".
3
+ //
4
+ // O QUE É. Quem não consegue MANTER um botão pressionado — espasticidade, tremor, fadiga, um único dedo, um
5
+ // switch de sopro — não perde o jogo: um TOQUE trava a direção, e o personagem segue andando sozinho. Tocar de
6
+ // novo no mesmo sentido para; tocar no sentido oposto inverte. Segurar, para quem consegue, acelera.
7
+ //
8
+ // POR QUE SAIU DA FÍSICA. A política morava dentro de `game/physics.ts`, no meio do cálculo do movimento
9
+ // horizontal. Três consequências, todas ruins:
10
+ // 1. Uma decisão de ENTRADA estava escrita como se fosse uma regra do jogo, num arquivo que ninguém abre
11
+ // para mexer em acessibilidade motora.
12
+ // 2. Só o movimento horizontal tinha caminho de alternância. Pulo, corrida e troca de poder não têm — e não
13
+ // por decisão, por acidente de onde o código foi parar. Este módulo é onde essa lacuna se fecha quando
14
+ // for fechada; NÃO a fecho agora, porque cada ação precisa da sua própria resposta de desenho (o que
15
+ // significa "pulo travado"?) e inventar isso sem o Dev seria pior que a lacuna.
16
+ // 3. A engine não podia oferecer a alternância a OUTRO jogo sem levar a física do plataforma junto.
17
+ //
18
+ // DE QUE LADO DA LINHA ISTO CAI. O ADR-0027 separa PRESENTAÇÃO (como o estado vira pixel e som), OPERAÇÃO
19
+ // (como a intenção vira entrada) e REGRA (o que é alcançável). O critério objetivo: uma adaptação cruza a
20
+ // linha quando altera o CONJUNTO DE ESTADOS ALCANÇÁVEIS ou a PROBABILIDADE de alcançá-los. A alternância não
21
+ // altera nem um nem outro — o jogador chega exatamente aos mesmos lugares, gastando menos motricidade. É
22
+ // OPERAÇÃO, e por isso a engine a oferece e o jogo não pode recusar. (O modo Fácil, no mesmo botão hoje, é
23
+ // REGRA: hitbox de +4px, proteção de beirada e gravidade 2/3 mudam o que é alcançável. Não estão aqui.)
24
+ //
25
+ // PURO DE PROPÓSITO: sem `document`, sem PIXI, sem `TUNE`. Recebe números e devolve números, roda no project
26
+ // `node` do Vitest, e não sabe que existe um plataforma do outro lado.
27
+ /** Fração da velocidade de caminhada quando o jogador SEGURA o botão no sentido travado. */
28
+ export const LATCH_HELD = 2 / 3;
29
+ /** Fração quando a direção está travada mas nenhum botão está pressionado — o "andar sozinho". */
30
+ export const LATCH_IDLE = 1 / 3;
31
+ /**
32
+ * O próximo sentido travado, dado o atual e as bordas de toque deste quadro.
33
+ *
34
+ * Um toque no sentido em que já se anda PARA (é o mesmo botão que liga e desliga — não há botão de parar, e
35
+ * inventar um custaria a quem tem um dedo só o dobro de alcances). Um toque no sentido oposto inverte
36
+ * direto, sem passar pelo zero: quem toca "esquerda" andando para a direita quer ir para a esquerda, não
37
+ * quer parar e tocar de novo.
38
+ *
39
+ * As duas bordas no MESMO quadro são possíveis (dois dedos, ou um switch duplo mal calibrado) e a ordem
40
+ * importa: esquerda é avaliada primeiro e direita depois, então direita vence. É o comportamento que a
41
+ * física sempre teve — duas linhas `if` em sequência —, preservado aqui de propósito e agora com um teste que
42
+ * o fixa, em vez de ser um efeito não intencional da ordem em que alguém escreveu dois `if`.
43
+ */
44
+ export function nextLatchedDir(current, leftEdge, rightEdge) {
45
+ let dir = current;
46
+ if (leftEdge)
47
+ dir = dir === -1 ? 0 : -1;
48
+ if (rightEdge)
49
+ dir = dir === 1 ? 0 : 1;
50
+ return dir;
51
+ }
52
+ /**
53
+ * O multiplicador da velocidade de caminhada, COM sinal: o sentido travado vezes a fração que corresponde a
54
+ * estar ou não segurando o botão daquele sentido.
55
+ *
56
+ * Devolve o produto (e não só a fração) porque parado é 0 e nenhuma fração precisa opinar sobre isso —
57
+ * exatamente como a física fazia ao multiplicar por `dir`. Quem chama multiplica por `TUNE.hWalk`; a
58
+ * constante de ajuste do jogo não entra neste módulo.
59
+ */
60
+ export function latchedDrive(dir, holdingLeft, holdingRight) {
61
+ const holding = (dir === -1 && holdingLeft) || (dir === 1 && holdingRight);
62
+ return dir * (holding ? LATCH_HELD : LATCH_IDLE);
63
+ }
@@ -0,0 +1,40 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ /** Retângulo do elemento (o que `getBoundingClientRect()` entrega, reduzido ao que a conta usa). */
3
+ export interface RectLike {
4
+ left: number;
5
+ top: number;
6
+ width: number;
7
+ height: number;
8
+ }
9
+ /** Um ponto em px, relativo ao CENTRO do elemento. É a forma que um direcional lê. */
10
+ export interface DoCentro {
11
+ dx: number;
12
+ dy: number;
13
+ }
14
+ /** Um ponto em FRAÇÃO do elemento: `0,0` é o canto superior esquerdo e `1,1` o inferior direito. */
15
+ export interface EmFracao {
16
+ fx: number;
17
+ fy: number;
18
+ }
19
+ /**
20
+ * O ponto relativo ao CENTRO, em px do elemento.
21
+ *
22
+ * As três funções de direcional de `touch-bindings` abriam com estas mesmas duas linhas. Repetição de duas
23
+ * linhas não costuma pagar uma extração — esta paga por outra razão: enquanto a conta do centro estava
24
+ * escrita dentro de cada uma, não havia como um consumidor NOVO (um ponteiro, um olhar) obter o ponto sem
25
+ * passar por uma função que já o transformou em direções.
26
+ */
27
+ export declare function doCentro(px: number, py: number, rect: RectLike): DoCentro;
28
+ /**
29
+ * O ponto como FRAÇÃO do elemento.
30
+ *
31
+ * ⚠️ NÃO SATURA EM 0..1 de propósito. Um ponteiro capturado sai do elemento e continua a valer — é isso que
32
+ * `setPointerCapture` existe para permitir, e saturar aqui apagaria a diferença entre «na borda» e «muito
33
+ * para lá da borda», que é justamente o que um arrasto precisa de saber. Quem quiser saturar, satura; quem
34
+ * saturasse aqui não teria como voltar atrás.
35
+ *
36
+ * ⚠️ E LARGURA ZERO DEVOLVE ZERO em vez de `Infinity`. Um elemento ainda não medido (display:none, primeiro
37
+ * quadro) dá `width: 0`, e um `NaN`/`Infinity` a partir daqui viajaria para dentro da física antes de alguém
38
+ * o ver. O `onGaze` já se protegia disso com um `if (!r.width) return`; a proteção passa a ser da conta.
39
+ */
40
+ export declare function emFracao(px: number, py: number, rect: RectLike): EmFracao;
@@ -0,0 +1,51 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // input/pointer-space.ts — O ÚNICO LUGAR QUE CONVERTE UM PONTO DE TELA. Módulo-folha: só aritmética.
3
+ //
4
+ // ========================= O QUE A MEDIÇÃO DA ISSUE #105 ENCONTROU =========================
5
+ // A issue manda LER antes de escrever — *"estabeleça quanto de um ponteiro já existe dentro de
6
+ // `input/touch.ts`; se a resposta for «quase tudo», isto vira uma extração e não um subsistema novo"*. A
7
+ // resposta medida é mais interessante do que «quase tudo» ou «quase nada»:
8
+ //
9
+ // · A CAPTURA já existe INTEIRA, em `touch-bindings.wirePointerPad`: `pointerdown`/`move`/`up`/`cancel`,
10
+ // `setPointerCapture` com recuo gracioso, `lostpointercapture` como rede, `contextmenu` suprimido, e um
11
+ // ponteiro de cada vez. Nada disso precisa de ser escrito outra vez.
12
+ // · O PONTO, ao contrário, é RECEBIDO E DEITADO FORA — e em quatro lugares, com duas convenções:
13
+ // `crossDirsAt`, `stickDirsAt` e `stickKnobOffset` reduzem-no a `dx,dy` do CENTRO e devolvem direções;
14
+ // `ui/webcam.onGaze` reduz o olhar a uma FRAÇÃO do elemento e a três limiares, emitindo teclas.
15
+ //
16
+ // ⚠️ E É O MESMO DEFEITO NOS DOIS SÍTIOS: uma posição contínua chega, é comprimida em «esquerda/direita/cima»
17
+ // antes de qualquer outro consumidor a ver, e não sobra nada para quem precisasse dela. O ponteiro não é
18
+ // fundação nova — é a fundação que o OLHAR já precisava e nunca teve.
19
+ //
20
+ // ⚠️ E O QUE NÃO EXISTE EM LADO NENHUM: hover (posição sem aperto), botões (`PointerLike` não tem `button`),
21
+ // roda (zero ocorrências) e — o que a `definition of done` da #105 pede em primeiro lugar — UM lugar que
22
+ // converta tela→jogo. Este ficheiro é esse lugar; o resto continua aberto na issue.
23
+ /**
24
+ * O ponto relativo ao CENTRO, em px do elemento.
25
+ *
26
+ * As três funções de direcional de `touch-bindings` abriam com estas mesmas duas linhas. Repetição de duas
27
+ * linhas não costuma pagar uma extração — esta paga por outra razão: enquanto a conta do centro estava
28
+ * escrita dentro de cada uma, não havia como um consumidor NOVO (um ponteiro, um olhar) obter o ponto sem
29
+ * passar por uma função que já o transformou em direções.
30
+ */
31
+ export function doCentro(px, py, rect) {
32
+ return { dx: px - (rect.left + rect.width / 2), dy: py - (rect.top + rect.height / 2) };
33
+ }
34
+ /**
35
+ * O ponto como FRAÇÃO do elemento.
36
+ *
37
+ * ⚠️ NÃO SATURA EM 0..1 de propósito. Um ponteiro capturado sai do elemento e continua a valer — é isso que
38
+ * `setPointerCapture` existe para permitir, e saturar aqui apagaria a diferença entre «na borda» e «muito
39
+ * para lá da borda», que é justamente o que um arrasto precisa de saber. Quem quiser saturar, satura; quem
40
+ * saturasse aqui não teria como voltar atrás.
41
+ *
42
+ * ⚠️ E LARGURA ZERO DEVOLVE ZERO em vez de `Infinity`. Um elemento ainda não medido (display:none, primeiro
43
+ * quadro) dá `width: 0`, e um `NaN`/`Infinity` a partir daqui viajaria para dentro da física antes de alguém
44
+ * o ver. O `onGaze` já se protegia disso com um `if (!r.width) return`; a proteção passa a ser da conta.
45
+ */
46
+ export function emFracao(px, py, rect) {
47
+ return {
48
+ fx: rect.width ? (px - rect.left) / rect.width : 0,
49
+ fy: rect.height ? (py - rect.top) / rect.height : 0,
50
+ };
51
+ }
@@ -0,0 +1,11 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ import type { ControlledPlayer } from '../core/entity.js';
3
+ export declare const keys: Set<string>;
4
+ type PadState = Record<string, boolean>;
5
+ export declare const padCur: Record<number, PadState>;
6
+ export declare const padPrevAct: Record<number, PadState>;
7
+ export declare const padPrevStart: Record<number, boolean>;
8
+ export declare const PAD_DEAD = 0.5;
9
+ type HeldPlayer = Pick<ControlledPlayer, 'ctrl' | 'pad'>;
10
+ export declare const held: (pl: HeldPlayer, act: string) => boolean;
11
+ export {};
@@ -0,0 +1,12 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // input/state.ts — ESTADO de input em runtime + query genérica, compartilhado pelos handlers (que ficam no
3
+ // game.js e mutam estes objetos IN-PLACE): teclas seguradas (keys), estado de gamepad por controle
4
+ // (padCur/padPrevAct/padPrevStart) e a zona morta do analógico (PAD_DEAD). held(pl,act) = o jogador está
5
+ // segurando a ação, por teclado (pl.ctrl) OU pelo gamepad associado (pl.pad). Módulo-folha, ZERO deps. (Fase 2.22)
6
+ export const keys = new Set();
7
+ export const padCur = {};
8
+ export const padPrevAct = {};
9
+ export const padPrevStart = {};
10
+ export const PAD_DEAD = 0.5; // zona morta = primeira METADE do curso do analógico (ergonomia — José 2026-07-02)
11
+ // Jogador está segurando a ação? teclado (algum code do esquema pl.ctrl) OU o gamepad associado (pl.pad).
12
+ export const held = (pl, act) => pl.ctrl[act].some((k) => keys.has(k)) || (pl.pad >= 0 && !!padCur[pl.pad]?.[act]);