@agentdeck/bridge 0.1.0 → 0.2.0

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 (380) hide show
  1. package/LICENSE +21 -0
  2. package/dist/adapters/claude-code.d.ts +11 -27
  3. package/dist/adapters/claude-code.d.ts.map +1 -1
  4. package/dist/adapters/claude-code.js +29 -123
  5. package/dist/adapters/claude-code.js.map +1 -1
  6. package/dist/adapters/codex-cli.d.ts +21 -0
  7. package/dist/adapters/codex-cli.d.ts.map +1 -0
  8. package/dist/adapters/codex-cli.js +53 -0
  9. package/dist/adapters/codex-cli.js.map +1 -0
  10. package/dist/adapters/index.d.ts +4 -0
  11. package/dist/adapters/index.d.ts.map +1 -1
  12. package/dist/adapters/index.js +13 -0
  13. package/dist/adapters/index.js.map +1 -1
  14. package/dist/adapters/monitor.d.ts +34 -0
  15. package/dist/adapters/monitor.d.ts.map +1 -0
  16. package/dist/adapters/monitor.js +86 -0
  17. package/dist/adapters/monitor.js.map +1 -0
  18. package/dist/adapters/openclaw.d.ts +36 -2
  19. package/dist/adapters/openclaw.d.ts.map +1 -1
  20. package/dist/adapters/openclaw.js +329 -59
  21. package/dist/adapters/openclaw.js.map +1 -1
  22. package/dist/adapters/opencode-adapter.d.ts +48 -0
  23. package/dist/adapters/opencode-adapter.d.ts.map +1 -0
  24. package/dist/adapters/opencode-adapter.js +345 -0
  25. package/dist/adapters/opencode-adapter.js.map +1 -0
  26. package/dist/adapters/pty-adapter.d.ts +56 -0
  27. package/dist/adapters/pty-adapter.d.ts.map +1 -0
  28. package/dist/adapters/pty-adapter.js +143 -0
  29. package/dist/adapters/pty-adapter.js.map +1 -0
  30. package/dist/adb-reverse.d.ts +25 -0
  31. package/dist/adb-reverse.d.ts.map +1 -0
  32. package/dist/adb-reverse.js +129 -0
  33. package/dist/adb-reverse.js.map +1 -0
  34. package/dist/antigravity-local.d.ts +7 -0
  35. package/dist/antigravity-local.d.ts.map +1 -0
  36. package/dist/antigravity-local.js +153 -0
  37. package/dist/antigravity-local.js.map +1 -0
  38. package/dist/apme/adapters/claude-hook.d.ts +15 -0
  39. package/dist/apme/adapters/claude-hook.d.ts.map +1 -0
  40. package/dist/apme/adapters/claude-hook.js +85 -0
  41. package/dist/apme/adapters/claude-hook.js.map +1 -0
  42. package/dist/apme/adapters/claude-pty.d.ts +28 -0
  43. package/dist/apme/adapters/claude-pty.d.ts.map +1 -0
  44. package/dist/apme/adapters/claude-pty.js +88 -0
  45. package/dist/apme/adapters/claude-pty.js.map +1 -0
  46. package/dist/apme/adapters/timeline.d.ts +16 -0
  47. package/dist/apme/adapters/timeline.d.ts.map +1 -0
  48. package/dist/apme/adapters/timeline.js +66 -0
  49. package/dist/apme/adapters/timeline.js.map +1 -0
  50. package/dist/apme/classifier.d.ts +53 -0
  51. package/dist/apme/classifier.d.ts.map +1 -0
  52. package/dist/apme/classifier.js +266 -0
  53. package/dist/apme/classifier.js.map +1 -0
  54. package/dist/apme/claude-transcript-reader.d.ts +38 -0
  55. package/dist/apme/claude-transcript-reader.d.ts.map +1 -0
  56. package/dist/apme/claude-transcript-reader.js +127 -0
  57. package/dist/apme/claude-transcript-reader.js.map +1 -0
  58. package/dist/apme/collector.d.ts +108 -0
  59. package/dist/apme/collector.d.ts.map +1 -0
  60. package/dist/apme/collector.js +659 -0
  61. package/dist/apme/collector.js.map +1 -0
  62. package/dist/apme/dashboard-html.d.ts +9 -0
  63. package/dist/apme/dashboard-html.d.ts.map +1 -0
  64. package/dist/apme/dashboard-html.js +416 -0
  65. package/dist/apme/dashboard-html.js.map +1 -0
  66. package/dist/apme/http.d.ts +19 -0
  67. package/dist/apme/http.d.ts.map +1 -0
  68. package/dist/apme/http.js +210 -0
  69. package/dist/apme/http.js.map +1 -0
  70. package/dist/apme/hw-sampler.d.ts +30 -0
  71. package/dist/apme/hw-sampler.d.ts.map +1 -0
  72. package/dist/apme/hw-sampler.js +106 -0
  73. package/dist/apme/hw-sampler.js.map +1 -0
  74. package/dist/apme/index.d.ts +43 -0
  75. package/dist/apme/index.d.ts.map +1 -0
  76. package/dist/apme/index.js +75 -0
  77. package/dist/apme/index.js.map +1 -0
  78. package/dist/apme/outcome.d.ts +48 -0
  79. package/dist/apme/outcome.d.ts.map +1 -0
  80. package/dist/apme/outcome.js +232 -0
  81. package/dist/apme/outcome.js.map +1 -0
  82. package/dist/apme/recommend.d.ts +32 -0
  83. package/dist/apme/recommend.d.ts.map +1 -0
  84. package/dist/apme/recommend.js +41 -0
  85. package/dist/apme/recommend.js.map +1 -0
  86. package/dist/apme/runner.d.ts +94 -0
  87. package/dist/apme/runner.d.ts.map +1 -0
  88. package/dist/apme/runner.js +812 -0
  89. package/dist/apme/runner.js.map +1 -0
  90. package/dist/apme/settings.d.ts +65 -0
  91. package/dist/apme/settings.d.ts.map +1 -0
  92. package/dist/apme/settings.js +97 -0
  93. package/dist/apme/settings.js.map +1 -0
  94. package/dist/apme/store.d.ts +99 -0
  95. package/dist/apme/store.d.ts.map +1 -0
  96. package/dist/apme/store.js +911 -0
  97. package/dist/apme/store.js.map +1 -0
  98. package/dist/apme/tuner.d.ts +64 -0
  99. package/dist/apme/tuner.d.ts.map +1 -0
  100. package/dist/apme/tuner.js +297 -0
  101. package/dist/apme/tuner.js.map +1 -0
  102. package/dist/apme/types.d.ts +11 -0
  103. package/dist/apme/types.d.ts.map +1 -0
  104. package/dist/apme/types.js +10 -0
  105. package/dist/apme/types.js.map +1 -0
  106. package/dist/auth.d.ts +1 -1
  107. package/dist/auth.d.ts.map +1 -1
  108. package/dist/auth.js +15 -5
  109. package/dist/auth.js.map +1 -1
  110. package/dist/bridge-core.d.ts +162 -0
  111. package/dist/bridge-core.d.ts.map +1 -0
  112. package/dist/bridge-core.js +627 -0
  113. package/dist/bridge-core.js.map +1 -0
  114. package/dist/check-deps.d.ts +3 -1
  115. package/dist/check-deps.d.ts.map +1 -1
  116. package/dist/check-deps.js +34 -13
  117. package/dist/check-deps.js.map +1 -1
  118. package/dist/cli.d.ts +3 -0
  119. package/dist/cli.d.ts.map +1 -0
  120. package/dist/cli.js +1235 -0
  121. package/dist/cli.js.map +1 -0
  122. package/dist/codex-auth.d.ts +10 -0
  123. package/dist/codex-auth.d.ts.map +1 -0
  124. package/dist/codex-auth.js +63 -0
  125. package/dist/codex-auth.js.map +1 -0
  126. package/dist/codex-output-parser.d.ts +21 -0
  127. package/dist/codex-output-parser.d.ts.map +1 -0
  128. package/dist/codex-output-parser.js +253 -0
  129. package/dist/codex-output-parser.js.map +1 -0
  130. package/dist/d200h/hid-protocol.d.ts +62 -0
  131. package/dist/d200h/hid-protocol.d.ts.map +1 -0
  132. package/dist/d200h/hid-protocol.js +124 -0
  133. package/dist/d200h/hid-protocol.js.map +1 -0
  134. package/dist/d200h/image-renderer.d.ts +36 -0
  135. package/dist/d200h/image-renderer.d.ts.map +1 -0
  136. package/dist/d200h/image-renderer.js +487 -0
  137. package/dist/d200h/image-renderer.js.map +1 -0
  138. package/dist/daemon-server.d.ts +19 -0
  139. package/dist/daemon-server.d.ts.map +1 -0
  140. package/dist/daemon-server.js +1168 -0
  141. package/dist/daemon-server.js.map +1 -0
  142. package/dist/daemon-ws-client.d.ts +82 -0
  143. package/dist/daemon-ws-client.d.ts.map +1 -0
  144. package/dist/daemon-ws-client.js +179 -0
  145. package/dist/daemon-ws-client.js.map +1 -0
  146. package/dist/daemon.d.ts +10 -0
  147. package/dist/daemon.d.ts.map +1 -0
  148. package/dist/daemon.js +56 -0
  149. package/dist/daemon.js.map +1 -0
  150. package/dist/display-monitor.d.ts +25 -0
  151. package/dist/display-monitor.d.ts.map +1 -0
  152. package/dist/display-monitor.js +143 -0
  153. package/dist/display-monitor.js.map +1 -0
  154. package/dist/esp32-serial.d.ts +112 -0
  155. package/dist/esp32-serial.d.ts.map +1 -0
  156. package/dist/esp32-serial.js +502 -0
  157. package/dist/esp32-serial.js.map +1 -0
  158. package/dist/gateway-probe.d.ts +12 -0
  159. package/dist/gateway-probe.d.ts.map +1 -0
  160. package/dist/gateway-probe.js +39 -0
  161. package/dist/gateway-probe.js.map +1 -0
  162. package/dist/hook-server.d.ts +29 -0
  163. package/dist/hook-server.d.ts.map +1 -1
  164. package/dist/hook-server.js +366 -8
  165. package/dist/hook-server.js.map +1 -1
  166. package/dist/index.d.ts +20 -2
  167. package/dist/index.d.ts.map +1 -1
  168. package/dist/index.js +1305 -562
  169. package/dist/index.js.map +1 -1
  170. package/dist/log-stream.d.ts +26 -0
  171. package/dist/log-stream.d.ts.map +1 -0
  172. package/dist/log-stream.js +111 -0
  173. package/dist/log-stream.js.map +1 -0
  174. package/dist/logger.d.ts +9 -2
  175. package/dist/logger.d.ts.map +1 -1
  176. package/dist/logger.js +36 -5
  177. package/dist/logger.js.map +1 -1
  178. package/dist/mdns.d.ts +15 -1
  179. package/dist/mdns.d.ts.map +1 -1
  180. package/dist/mdns.js +143 -28
  181. package/dist/mdns.js.map +1 -1
  182. package/dist/mlx-probe.d.ts +14 -0
  183. package/dist/mlx-probe.d.ts.map +1 -0
  184. package/dist/mlx-probe.js +44 -0
  185. package/dist/mlx-probe.js.map +1 -0
  186. package/dist/model-catalog.d.ts +5 -3
  187. package/dist/model-catalog.d.ts.map +1 -1
  188. package/dist/model-catalog.js +45 -29
  189. package/dist/model-catalog.js.map +1 -1
  190. package/dist/modules/adb-module.d.ts +14 -0
  191. package/dist/modules/adb-module.d.ts.map +1 -0
  192. package/dist/modules/adb-module.js +36 -0
  193. package/dist/modules/adb-module.js.map +1 -0
  194. package/dist/modules/d200h-module.d.ts +43 -0
  195. package/dist/modules/d200h-module.d.ts.map +1 -0
  196. package/dist/modules/d200h-module.js +360 -0
  197. package/dist/modules/d200h-module.js.map +1 -0
  198. package/dist/modules/d200h-renderer.d.ts +7 -0
  199. package/dist/modules/d200h-renderer.d.ts.map +1 -0
  200. package/dist/modules/d200h-renderer.js +321 -0
  201. package/dist/modules/d200h-renderer.js.map +1 -0
  202. package/dist/modules/index.d.ts +23 -0
  203. package/dist/modules/index.d.ts.map +1 -0
  204. package/dist/modules/index.js +65 -0
  205. package/dist/modules/index.js.map +1 -0
  206. package/dist/modules/mdns-module.d.ts +12 -0
  207. package/dist/modules/mdns-module.d.ts.map +1 -0
  208. package/dist/modules/mdns-module.js +21 -0
  209. package/dist/modules/mdns-module.js.map +1 -0
  210. package/dist/modules/pixoo-module.d.ts +8 -0
  211. package/dist/modules/pixoo-module.d.ts.map +1 -0
  212. package/dist/modules/pixoo-module.js +24 -0
  213. package/dist/modules/pixoo-module.js.map +1 -0
  214. package/dist/modules/serial-module.d.ts +18 -0
  215. package/dist/modules/serial-module.d.ts.map +1 -0
  216. package/dist/modules/serial-module.js +47 -0
  217. package/dist/modules/serial-module.js.map +1 -0
  218. package/dist/modules/types.d.ts +39 -0
  219. package/dist/modules/types.d.ts.map +1 -0
  220. package/dist/modules/types.js +2 -0
  221. package/dist/modules/types.js.map +1 -0
  222. package/dist/ollama-probe.d.ts +13 -0
  223. package/dist/ollama-probe.d.ts.map +1 -0
  224. package/dist/ollama-probe.js +46 -0
  225. package/dist/ollama-probe.js.map +1 -0
  226. package/dist/opencode-client.d.ts +126 -0
  227. package/dist/opencode-client.d.ts.map +1 -0
  228. package/dist/opencode-client.js +181 -0
  229. package/dist/opencode-client.js.map +1 -0
  230. package/dist/output-parser.d.ts +3 -0
  231. package/dist/output-parser.d.ts.map +1 -1
  232. package/dist/output-parser.js +29 -3
  233. package/dist/output-parser.js.map +1 -1
  234. package/dist/passive-observer.d.ts +38 -0
  235. package/dist/passive-observer.d.ts.map +1 -0
  236. package/dist/passive-observer.js +632 -0
  237. package/dist/passive-observer.js.map +1 -0
  238. package/dist/pixoo/pixoo-bridge.d.ts +50 -0
  239. package/dist/pixoo/pixoo-bridge.d.ts.map +1 -0
  240. package/dist/pixoo/pixoo-bridge.js +280 -0
  241. package/dist/pixoo/pixoo-bridge.js.map +1 -0
  242. package/dist/pixoo/pixoo-camera.d.ts +80 -0
  243. package/dist/pixoo/pixoo-camera.d.ts.map +1 -0
  244. package/dist/pixoo/pixoo-camera.js +271 -0
  245. package/dist/pixoo/pixoo-camera.js.map +1 -0
  246. package/dist/pixoo/pixoo-client.d.ts +67 -0
  247. package/dist/pixoo/pixoo-client.d.ts.map +1 -0
  248. package/dist/pixoo/pixoo-client.js +335 -0
  249. package/dist/pixoo/pixoo-client.js.map +1 -0
  250. package/dist/pixoo/pixoo-font.d.ts +24 -0
  251. package/dist/pixoo/pixoo-font.d.ts.map +1 -0
  252. package/dist/pixoo/pixoo-font.js +121 -0
  253. package/dist/pixoo/pixoo-font.js.map +1 -0
  254. package/dist/pixoo/pixoo-renderer.d.ts +32 -0
  255. package/dist/pixoo/pixoo-renderer.d.ts.map +1 -0
  256. package/dist/pixoo/pixoo-renderer.js +696 -0
  257. package/dist/pixoo/pixoo-renderer.js.map +1 -0
  258. package/dist/pixoo/pixoo-settings.d.ts +10 -0
  259. package/dist/pixoo/pixoo-settings.d.ts.map +1 -0
  260. package/dist/pixoo/pixoo-settings.js +47 -0
  261. package/dist/pixoo/pixoo-settings.js.map +1 -0
  262. package/dist/pixoo/pixoo-sprites.d.ts +187 -0
  263. package/dist/pixoo/pixoo-sprites.d.ts.map +1 -0
  264. package/dist/pixoo/pixoo-sprites.js +926 -0
  265. package/dist/pixoo/pixoo-sprites.js.map +1 -0
  266. package/dist/pty-manager.d.ts +1 -1
  267. package/dist/pty-manager.d.ts.map +1 -1
  268. package/dist/pty-manager.js +36 -15
  269. package/dist/pty-manager.js.map +1 -1
  270. package/dist/session-aggregator.d.ts +41 -0
  271. package/dist/session-aggregator.d.ts.map +1 -0
  272. package/dist/session-aggregator.js +76 -0
  273. package/dist/session-aggregator.js.map +1 -0
  274. package/dist/session-focus-relay.d.ts +35 -0
  275. package/dist/session-focus-relay.d.ts.map +1 -0
  276. package/dist/session-focus-relay.js +127 -0
  277. package/dist/session-focus-relay.js.map +1 -0
  278. package/dist/session-registry.d.ts +88 -1
  279. package/dist/session-registry.d.ts.map +1 -1
  280. package/dist/session-registry.js +222 -15
  281. package/dist/session-registry.js.map +1 -1
  282. package/dist/session-timeline-relay.d.ts +27 -0
  283. package/dist/session-timeline-relay.d.ts.map +1 -0
  284. package/dist/session-timeline-relay.js +124 -0
  285. package/dist/session-timeline-relay.js.map +1 -0
  286. package/dist/state-machine.d.ts +1 -0
  287. package/dist/state-machine.d.ts.map +1 -1
  288. package/dist/state-machine.js +14 -0
  289. package/dist/state-machine.js.map +1 -1
  290. package/dist/terminal-status.d.ts +28 -0
  291. package/dist/terminal-status.d.ts.map +1 -0
  292. package/dist/terminal-status.js +152 -0
  293. package/dist/terminal-status.js.map +1 -0
  294. package/dist/timeline-store.d.ts +22 -0
  295. package/dist/timeline-store.d.ts.map +1 -0
  296. package/dist/timeline-store.js +79 -0
  297. package/dist/timeline-store.js.map +1 -0
  298. package/dist/timeline-summarizer.d.ts +14 -0
  299. package/dist/timeline-summarizer.d.ts.map +1 -0
  300. package/dist/timeline-summarizer.js +154 -0
  301. package/dist/timeline-summarizer.js.map +1 -0
  302. package/dist/tts.d.ts +25 -0
  303. package/dist/tts.d.ts.map +1 -0
  304. package/dist/tts.js +99 -0
  305. package/dist/tts.js.map +1 -0
  306. package/dist/tui/ansi.d.ts +92 -0
  307. package/dist/tui/ansi.d.ts.map +1 -0
  308. package/dist/tui/ansi.js +181 -0
  309. package/dist/tui/ansi.js.map +1 -0
  310. package/dist/tui/dashboard.d.ts +35 -0
  311. package/dist/tui/dashboard.d.ts.map +1 -0
  312. package/dist/tui/dashboard.js +436 -0
  313. package/dist/tui/dashboard.js.map +1 -0
  314. package/dist/tui/gauge.d.ts +15 -0
  315. package/dist/tui/gauge.d.ts.map +1 -0
  316. package/dist/tui/gauge.js +82 -0
  317. package/dist/tui/gauge.js.map +1 -0
  318. package/dist/tui/renderer.d.ts +15 -0
  319. package/dist/tui/renderer.d.ts.map +1 -0
  320. package/dist/tui/renderer.js +758 -0
  321. package/dist/tui/renderer.js.map +1 -0
  322. package/dist/tui/screen.d.ts +27 -0
  323. package/dist/tui/screen.d.ts.map +1 -0
  324. package/dist/tui/screen.js +109 -0
  325. package/dist/tui/screen.js.map +1 -0
  326. package/dist/tui/terrarium.d.ts +96 -0
  327. package/dist/tui/terrarium.d.ts.map +1 -0
  328. package/dist/tui/terrarium.js +912 -0
  329. package/dist/tui/terrarium.js.map +1 -0
  330. package/dist/types.d.ts +4 -2
  331. package/dist/types.d.ts.map +1 -1
  332. package/dist/types.js +3 -2
  333. package/dist/types.js.map +1 -1
  334. package/dist/usage-api.d.ts +9 -2
  335. package/dist/usage-api.d.ts.map +1 -1
  336. package/dist/usage-api.js +224 -42
  337. package/dist/usage-api.js.map +1 -1
  338. package/dist/usage-event.d.ts +12 -0
  339. package/dist/usage-event.d.ts.map +1 -0
  340. package/dist/usage-event.js +78 -0
  341. package/dist/usage-event.js.map +1 -0
  342. package/dist/usage-tracker.d.ts +4 -0
  343. package/dist/usage-tracker.d.ts.map +1 -1
  344. package/dist/usage-tracker.js +17 -0
  345. package/dist/usage-tracker.js.map +1 -1
  346. package/dist/utility-proxy.d.ts +30 -0
  347. package/dist/utility-proxy.d.ts.map +1 -0
  348. package/dist/utility-proxy.js +217 -0
  349. package/dist/utility-proxy.js.map +1 -0
  350. package/dist/utils/project-name.d.ts +22 -0
  351. package/dist/utils/project-name.d.ts.map +1 -0
  352. package/dist/utils/project-name.js +72 -0
  353. package/dist/utils/project-name.js.map +1 -0
  354. package/dist/version-check.d.ts +26 -0
  355. package/dist/version-check.d.ts.map +1 -0
  356. package/dist/version-check.js +225 -0
  357. package/dist/version-check.js.map +1 -0
  358. package/dist/voice-assistant.d.ts +63 -0
  359. package/dist/voice-assistant.d.ts.map +1 -0
  360. package/dist/voice-assistant.js +406 -0
  361. package/dist/voice-assistant.js.map +1 -0
  362. package/dist/voice.d.ts +2 -0
  363. package/dist/voice.d.ts.map +1 -1
  364. package/dist/voice.js +26 -1
  365. package/dist/voice.js.map +1 -1
  366. package/dist/wake-word.d.ts +35 -0
  367. package/dist/wake-word.d.ts.map +1 -0
  368. package/dist/wake-word.js +225 -0
  369. package/dist/wake-word.js.map +1 -0
  370. package/dist/whisper-server-manager.d.ts +1 -1
  371. package/dist/whisper-server-manager.js +1 -1
  372. package/dist/wifi-config.d.ts +24 -0
  373. package/dist/wifi-config.d.ts.map +1 -0
  374. package/dist/wifi-config.js +84 -0
  375. package/dist/wifi-config.js.map +1 -0
  376. package/dist/ws-server.d.ts +12 -0
  377. package/dist/ws-server.d.ts.map +1 -1
  378. package/dist/ws-server.js +81 -3
  379. package/dist/ws-server.js.map +1 -1
  380. package/package.json +22 -13
@@ -0,0 +1,926 @@
1
+ /**
2
+ * Pixoo64 Creature Sprites + Environment — pixel art for 64×64 LED matrix.
3
+ *
4
+ * Grids match Android TerrainCreature designs:
5
+ * Octopus: 14×7 grid (Android OctopusCreature), PIXEL_ASPECT 2.0
6
+ * Crayfish: 12×8 grid (Android CrayfishCreature simplified)
7
+ * Tetra: 4-pixel fish (body 2 + stripe 1 + fin 1)
8
+ *
9
+ * Pixel art readability stack:
10
+ * 1. Colored outline (body-color darkened 50% — silhouette definition)
11
+ * 2. Silhouette-first grids (round head, separated tentacles)
12
+ * 3. Bright eye highlights (warm white — visible on LED)
13
+ * 4. LOD sprites (chunky grids for zoom < 1.3)
14
+ * 5. Glow halo (subtle radial glow behind creatures, moderate for LOD — ×1.5 intensity)
15
+ * 6. Camera timing (wide zone zoom 1.2, shorter dwell)
16
+ * 7. LOD outline (alpha 0.6 — visible silhouette without shape smearing)
17
+ * 8. Composite breathing wave (sin + harmonic — organic rhythm)
18
+ * 9. Minimum animation amplitude (1px floor — no invisible motion)
19
+ *
20
+ * Camera-scaled rendering:
21
+ * cellPx = creature world width / grid columns × camera zoom
22
+ * zoom 1.2 → octopus ~8.4px wide, crayfish ~14.4px wide
23
+ * zoom 2.0 → octopus ~14px wide, crayfish ~24px wide
24
+ */
25
+ import { worldToScreen, isVisible } from './pixoo-camera.js';
26
+ // ===== Cell Types (shared with ESP32/Android) =====
27
+ const EMPTY = 0;
28
+ const BODY = 1;
29
+ const EYE = 2;
30
+ const LEFT_ARM = 3;
31
+ const RIGHT_ARM = 4;
32
+ const LEFT_LEG = 5;
33
+ const RIGHT_LEG = 6;
34
+ const ANTENNA = 7;
35
+ // ===== Octopus 13×13 — Pixoo64 LED pixel art (square pixels, no PIXEL_ASPECT) =====
36
+ //
37
+ // Designed for LED matrix: solid filled pixels, eyes as negative space (black).
38
+ // No outline needed — LED pixel glow provides natural edge definition.
39
+ // At zoom 3.2 with cellSize = zoom/3 ≈ 1.07, sprite ≈ 14×14 screen pixels.
40
+ //
41
+ export const OCTOPUS_GRID = [
42
+ [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], // flat top (9) — angular
43
+ [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], // (11)
44
+ [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], // (11)
45
+ [0, 1, 1, 1, 2, 1, 1, 1, 2, 1, 1, 1, 0], // eyes top — col 4,8 (11)
46
+ [0, 1, 1, 1, 2, 1, 1, 1, 2, 1, 1, 1, 0], // eyes bottom — 2px vertical (11)
47
+ [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4], // arms start + body (13)
48
+ [3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 4], // arms thick (13) — 2 rows, 2-3px wide
49
+ [3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 4], // arms continue (13)
50
+ [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], // narrowing (9)
51
+ [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], // (9)
52
+ [0, 0, 5, 0, 5, 0, 1, 0, 6, 0, 6, 0, 0], // tentacles + center
53
+ [0, 0, 5, 0, 5, 0, 0, 0, 6, 0, 6, 0, 0], // tentacles
54
+ [0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0], // outer tentacles only
55
+ ];
56
+ const OCTO_COLS = 13;
57
+ const OCTO_ROWS = 13;
58
+ /** World width of octopus in normalized coords. */
59
+ export const OCTO_WORLD_W = 7 / 64;
60
+ // ===== Octopus LOD 7×7 — chunky grid for zoom < 1.3 =====
61
+ const OCTOPUS_LOD = [
62
+ [0, 1, 1, 1, 1, 1, 0], // flat top (5) — angular
63
+ [1, 1, 1, 1, 1, 1, 1], // full width (7)
64
+ [1, 1, 2, 1, 2, 1, 1], // eyes (7)
65
+ [1, 1, 1, 1, 1, 1, 1], // body (7)
66
+ [0, 1, 1, 1, 1, 1, 0], // waist (5)
67
+ [0, 0, 5, 1, 6, 0, 0], // tentacles + center
68
+ [0, 0, 5, 0, 6, 0, 0], // tentacle tips
69
+ ];
70
+ const OCTO_LOD_COLS = 7;
71
+ const OCTO_LOD_ROWS = 7;
72
+ // ===== Crayfish 12×8 — front-facing, matches OpenClaw SVG =====
73
+ //
74
+ // OpenClaw SVG: rounded body, compact side claws, antennae from top,
75
+ // dark eyes with teal (#00e5cc) highlights, red gradient body.
76
+ // Body shape: oval (narrow top → wide middle → narrow bottom).
77
+ //
78
+ export const CRAYFISH_GRID = [
79
+ [0, 0, 7, 0, 0, 0, 0, 0, 0, 7, 0, 0], // antennae tips (wide spread)
80
+ [0, 0, 0, 7, 0, 0, 0, 0, 7, 0, 0, 0], // antennae shafts
81
+ [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0], // head dome top (6w — rounder)
82
+ [3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 4, 4], // head + claw tips (8w body + 2+2 claws)
83
+ [0, 3, 1, 1, 2, 1, 1, 2, 1, 1, 4, 0], // eyes + claw arms (8w body)
84
+ [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], // thorax widest (8w)
85
+ [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0], // abdomen (6w)
86
+ [0, 0, 5, 0, 0, 1, 1, 0, 0, 6, 0, 0], // tail + walking legs
87
+ ];
88
+ const CF_COLS = 12;
89
+ const CF_ROWS = 8;
90
+ /** World width of crayfish in normalized coords (12 world-pixels / 64). */
91
+ export const CF_WORLD_W = 12 / 64;
92
+ // ===== Crayfish LOD 8×6 — compact for zoom < 1.3 =====
93
+ const CRAYFISH_LOD = [
94
+ [0, 7, 0, 0, 0, 0, 7, 0], // antennae (wider spread)
95
+ [0, 0, 1, 1, 1, 1, 0, 0], // head dome (4w — rounder)
96
+ [3, 1, 1, 1, 1, 1, 1, 4], // claws + body (6w — wider oval)
97
+ [0, 1, 2, 1, 1, 2, 1, 0], // eyes at col 2,5 (spaced for 3×3 rendering)
98
+ [0, 0, 1, 1, 1, 1, 0, 0], // lower body (4w)
99
+ [0, 5, 0, 1, 1, 0, 6, 0], // legs + tail
100
+ ];
101
+ const CF_LOD_COLS = 8;
102
+ const CF_LOD_ROWS = 6;
103
+ // ===== Jellyfish 10×8 — 6-lobe cloud (matches TUI/Android CloudCreature) =====
104
+ //
105
+ // Codex CLI creature: clover-shaped cloud with >_ terminal prompt marking.
106
+ // Cell types: 0=empty, 1=body, 2=marking(>_), 3=edge(breathe/contract)
107
+ //
108
+ export const JELLYFISH_GRID = [
109
+ [0, 0, 1, 1, 0, 0, 1, 1, 0, 0], // top lobes
110
+ [0, 1, 1, 1, 1, 1, 1, 1, 1, 0], // upper merge
111
+ [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], // widest — side lobes
112
+ [3, 1, 2, 2, 1, 1, 2, 1, 1, 3], // >_ marking + breathe edges
113
+ [3, 1, 1, 1, 1, 1, 1, 1, 1, 3], // center + breathe edges
114
+ [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], // widest
115
+ [0, 1, 1, 1, 1, 1, 1, 1, 1, 0], // taper
116
+ [0, 0, 1, 1, 0, 0, 1, 1, 0, 0], // bottom lobes
117
+ ];
118
+ const JF_COLS = 10;
119
+ const JF_ROWS = 8;
120
+ /** World width of jellyfish in normalized coords. */
121
+ export const JF_WORLD_W = 10 / 64;
122
+ // ===== Jellyfish LOD 7×5 — compact for zoom < 1.3 =====
123
+ const JELLYFISH_LOD = [
124
+ [0, 1, 1, 0, 1, 1, 0], // top lobes
125
+ [1, 1, 1, 1, 1, 1, 1], // widest
126
+ [1, 1, 2, 1, 2, 1, 1], // marking
127
+ [1, 1, 1, 1, 1, 1, 1], // widest
128
+ [0, 1, 1, 0, 1, 1, 0], // bottom lobes
129
+ ];
130
+ const JF_LOD_COLS = 7;
131
+ const JF_LOD_ROWS = 5;
132
+ // Cell type constants for jellyfish
133
+ const MARKING = 2;
134
+ const BREATHE_EDGE = 3;
135
+ // ===== OpenCode 10×9 — nested-square logo sprite (simulator SSOT) =====
136
+ const OPENCODE_FRAME = 8;
137
+ const OPENCODE_CORE = 9;
138
+ const OPENCODE_GRID = [
139
+ [8, 8, 8, 8, 8, 8, 8, 8, 8, 8],
140
+ [8, 0, 0, 0, 0, 0, 0, 0, 0, 8],
141
+ [8, 0, 9, 9, 9, 9, 9, 9, 0, 8],
142
+ [8, 0, 9, 0, 0, 0, 0, 9, 0, 8],
143
+ [8, 0, 9, 0, 0, 0, 0, 9, 0, 8],
144
+ [8, 0, 9, 0, 0, 0, 0, 9, 0, 8],
145
+ [8, 0, 9, 9, 9, 9, 9, 9, 0, 8],
146
+ [8, 0, 0, 0, 0, 0, 0, 0, 0, 8],
147
+ [8, 8, 8, 8, 8, 8, 8, 8, 8, 8],
148
+ ];
149
+ const OC_COLS = 10;
150
+ const OC_ROWS = 9;
151
+ const OPENCODE_LOD = [
152
+ [8, 8, 8, 8, 8, 8],
153
+ [8, 0, 0, 0, 0, 8],
154
+ [8, 0, 9, 9, 0, 8],
155
+ [8, 0, 9, 9, 0, 8],
156
+ [8, 0, 0, 0, 0, 8],
157
+ [8, 8, 8, 8, 8, 8],
158
+ ];
159
+ const OC_LOD_COLS = 6;
160
+ const OC_LOD_ROWS = 6;
161
+ export const COLORS = {
162
+ // Octopus (terracotta — Android match)
163
+ octopusBody: [0xC0, 0x70, 0x58],
164
+ octopusEye: [0x10, 0x08, 0x08], // near-black — simple dot eyes
165
+ octopusEyePupil: [0x10, 0x08, 0x08], // same as eye (unified)
166
+ octopusArm: [0xA0, 0x58, 0x40],
167
+ octopusLeg: [0xA0, 0x58, 0x40],
168
+ octopusSleeping: [0x80, 0x50, 0x40],
169
+ octopusStarburst: [0xD0, 0x88, 0x70],
170
+ octopusGlow: [0x60, 0x38, 0x2C], // warm terracotta glow
171
+ // Crayfish (red — Android match)
172
+ crayfishBody: [0xFF, 0x4D, 0x4D],
173
+ crayfishEye: [0x00, 0xE5, 0xCC], // teal — OpenClaw signature (1px center)
174
+ crayfishEyeRing: [0x10, 0x08, 0x08], // near-black surround (3×3 eye)
175
+ crayfishClaw: [0xCC, 0x44, 0x33], // brighter than body for visibility on dark water
176
+ crayfishLeg: [0xCC, 0x33, 0x33],
177
+ crayfishRouting: [0xFF, 0x6B, 0x6B],
178
+ crayfishAntenna: [0xDD, 0x55, 0x55],
179
+ crayfishGlow: [0x80, 0x20, 0x20], // warm red glow
180
+ crayfishSick: [0x88, 0x66, 0x66], // desaturated gray-red (gateway error)
181
+ // Jellyfish / Codex CLI (indigo — TUI/Android match)
182
+ jellyfishBody: [0x63, 0x66, 0xF1], // indigo #6366F1
183
+ jellyfishEdge: [0x4F, 0x46, 0xE5], // darker edge (breathe cells)
184
+ jellyfishMarking: [0xA5, 0xB4, 0xFC], // glow #A5B4FC (>_ prompt)
185
+ jellyfishGlow: [0x31, 0x33, 0x78], // dim indigo glow halo
186
+ jellyfishPulse: [0xA5, 0xB4, 0xFC], // bioluminescent pulse
187
+ jellyfishSleeping: [0x3A, 0x3C, 0x90], // dimmed indigo
188
+ // OpenCode (nested square)
189
+ opencodeOuter: [0xF1, 0xEC, 0xEC], // light frame
190
+ opencodeInner: [0x4B, 0x46, 0x46], // dark core
191
+ opencodePulse: [0xCF, 0xCE, 0xCD], // pulse state
192
+ opencodeSleeping: [0x8A, 0x84, 0x84], // sleep dim
193
+ // Tetra (neon)
194
+ tetraNeon: [0x00, 0xE5, 0xFF],
195
+ tetraBody: [0x1E, 0x40, 0xAF],
196
+ tetraFin: [0xFF, 0x6B, 0x6B],
197
+ // Environment — brightened for LED visibility (was Android-dark, too dim on Pixoo)
198
+ waterDeep: [0x14, 0x24, 0x3C],
199
+ waterMid: [0x1C, 0x38, 0x58],
200
+ waterLight: [0x24, 0x48, 0x6C],
201
+ waterSurface: [0x2C, 0x58, 0x80],
202
+ // Teal zone (50-70%) — brightened for LED
203
+ waterTealDeep: [0x10, 0x30, 0x3C],
204
+ waterTealMid: [0x18, 0x44, 0x50],
205
+ waterTealLight: [0x22, 0x58, 0x64],
206
+ waterTealSurface: [0x2C, 0x6C, 0x78],
207
+ // Amber zone (70-90%) — brightened for LED
208
+ waterAmberDeep: [0x34, 0x24, 0x14],
209
+ waterAmberMid: [0x4C, 0x36, 0x1C],
210
+ waterAmberLight: [0x60, 0x48, 0x24],
211
+ waterAmberSurface: [0x78, 0x5C, 0x2E],
212
+ // Red zone (90%+) — brightened for LED
213
+ waterRedDeep: [0x3C, 0x14, 0x14],
214
+ waterRedMid: [0x58, 0x1E, 0x1E],
215
+ waterRedLight: [0x70, 0x28, 0x28],
216
+ waterRedSurface: [0x88, 0x32, 0x32],
217
+ // Terrain — brightened for LED
218
+ sand: [0x38, 0x2C, 0x1E],
219
+ sandLight: [0x4C, 0x3C, 0x28],
220
+ sandDark: [0x28, 0x20, 0x14],
221
+ gravel: [0x44, 0x36, 0x24],
222
+ rock: [0x2C, 0x24, 0x1A], // dark earth (blends with sand)
223
+ rockLight: [0x38, 0x2E, 0x22], // warm earth (blends with sand)
224
+ // Seaweed (Android green)
225
+ seaweed: [0x22, 0xC5, 0x5E],
226
+ seaweedDark: [0x18, 0x90, 0x42],
227
+ seaweedLight: [0x30, 0xE0, 0x70],
228
+ // Effects
229
+ bubble: [0x40, 0x70, 0xA0],
230
+ bubbleBright: [0x60, 0x98, 0xCC],
231
+ lightRay: [0x20, 0x40, 0x60],
232
+ caustic: [0x1C, 0x36, 0x50],
233
+ dataParticle: [0x70, 0xB0, 0xFF],
234
+ dataParticleGreen: [0x50, 0xF0, 0x90],
235
+ // Tank walls (above water)
236
+ tankWall: [0x06, 0x0A, 0x10],
237
+ tankWallEdge: [0x10, 0x14, 0x1C],
238
+ // State shimmer
239
+ stateIdle: [0x22, 0xC5, 0x5E],
240
+ stateProcessing: [0x3B, 0x82, 0xF6],
241
+ stateAwaiting: [0xF5, 0x9E, 0x0B],
242
+ stateError: [0xEF, 0x44, 0x44],
243
+ white: [0xFF, 0xFF, 0xFF],
244
+ black: [0x00, 0x00, 0x00],
245
+ };
246
+ const SESSION_TONE_FACTORS = [1.08, 1.0, 0.9, 0.8, 0.72, 0.64];
247
+ function scaleColor(color, factor) {
248
+ return [
249
+ Math.max(0, Math.min(255, Math.round(color[0] * factor))),
250
+ Math.max(0, Math.min(255, Math.round(color[1] * factor))),
251
+ Math.max(0, Math.min(255, Math.round(color[2] * factor))),
252
+ ];
253
+ }
254
+ /**
255
+ * Multi-session Pixoo creatures can't show textual #1/#2 labels well.
256
+ * Use a stable brightness ramp instead: first session stays slightly brighter,
257
+ * later sessions get progressively darker while preserving the base hue.
258
+ */
259
+ export function getOctopusPaletteForSession(sessionIndex = 0) {
260
+ const tone = SESSION_TONE_FACTORS[Math.max(0, Math.min(SESSION_TONE_FACTORS.length - 1, sessionIndex))];
261
+ return {
262
+ body: scaleColor(COLORS.octopusBody, tone),
263
+ arm: scaleColor(COLORS.octopusArm, tone),
264
+ leg: scaleColor(COLORS.octopusLeg, tone),
265
+ sleeping: scaleColor(COLORS.octopusSleeping, tone),
266
+ starburst: scaleColor(COLORS.octopusStarburst, tone),
267
+ };
268
+ }
269
+ /**
270
+ * Multi-session jellyfish palette with brightness ramp (same as octopus).
271
+ */
272
+ export function getJellyfishPaletteForSession(sessionIndex = 0) {
273
+ const tone = SESSION_TONE_FACTORS[Math.max(0, Math.min(SESSION_TONE_FACTORS.length - 1, sessionIndex))];
274
+ return {
275
+ body: scaleColor(COLORS.jellyfishBody, tone),
276
+ edge: scaleColor(COLORS.jellyfishEdge, tone),
277
+ marking: scaleColor(COLORS.jellyfishMarking, tone),
278
+ sleeping: scaleColor(COLORS.jellyfishSleeping, tone),
279
+ pulse: scaleColor(COLORS.jellyfishPulse, tone),
280
+ };
281
+ }
282
+ export function getOpenCodePaletteForSession(sessionIndex = 0) {
283
+ const tone = SESSION_TONE_FACTORS[Math.max(0, Math.min(SESSION_TONE_FACTORS.length - 1, sessionIndex))];
284
+ return {
285
+ outer: scaleColor(COLORS.opencodeOuter, tone),
286
+ inner: scaleColor(COLORS.opencodeInner, tone),
287
+ sleeping: scaleColor(COLORS.opencodeSleeping, tone),
288
+ pulse: scaleColor(COLORS.opencodePulse, tone),
289
+ };
290
+ }
291
+ // ===== Pixel Operations =====
292
+ /** Set a pixel in the 64×64 RGB buffer. */
293
+ export function setPixel(buf, x, y, color) {
294
+ if (x < 0 || x >= 64 || y < 0 || y >= 64)
295
+ return;
296
+ const ix = Math.round(x);
297
+ const iy = Math.round(y);
298
+ if (ix < 0 || ix >= 64 || iy < 0 || iy >= 64)
299
+ return;
300
+ const idx = (iy * 64 + ix) * 3;
301
+ buf[idx] = color[0];
302
+ buf[idx + 1] = color[1];
303
+ buf[idx + 2] = color[2];
304
+ }
305
+ /** Alpha-blend a pixel onto existing buffer content. */
306
+ export function blendPixel(buf, x, y, color, alpha) {
307
+ const ix = Math.round(x);
308
+ const iy = Math.round(y);
309
+ if (ix < 0 || ix >= 64 || iy < 0 || iy >= 64 || alpha <= 0)
310
+ return;
311
+ const idx = (iy * 64 + ix) * 3;
312
+ const a = Math.min(1, alpha);
313
+ const inv = 1 - a;
314
+ buf[idx] = Math.min(255, Math.round(buf[idx] * inv + color[0] * a));
315
+ buf[idx + 1] = Math.min(255, Math.round(buf[idx + 1] * inv + color[1] * a));
316
+ buf[idx + 2] = Math.min(255, Math.round(buf[idx + 2] * inv + color[2] * a));
317
+ }
318
+ /** Additive-blend (glow) a pixel. */
319
+ export function glowPixel(buf, x, y, color, intensity) {
320
+ const ix = Math.round(x);
321
+ const iy = Math.round(y);
322
+ if (ix < 0 || ix >= 64 || iy < 0 || iy >= 64 || intensity <= 0)
323
+ return;
324
+ const idx = (iy * 64 + ix) * 3;
325
+ buf[idx] = Math.min(255, buf[idx] + Math.round(color[0] * intensity));
326
+ buf[idx + 1] = Math.min(255, buf[idx + 1] + Math.round(color[1] * intensity));
327
+ buf[idx + 2] = Math.min(255, buf[idx + 2] + Math.round(color[2] * intensity));
328
+ }
329
+ /** Fill a rectangle. */
330
+ export function fillRect(buf, x, y, w, h, color) {
331
+ for (let dy = 0; dy < h; dy++) {
332
+ for (let dx = 0; dx < w; dx++) {
333
+ setPixel(buf, x + dx, y + dy, color);
334
+ }
335
+ }
336
+ }
337
+ /** Fill a scaled cell (variable size rectangle) — edge rounding for correct PIXEL_ASPECT. */
338
+ function fillCell(buf, x, y, w, h, color) {
339
+ const ix = Math.floor(x);
340
+ const iy = Math.floor(y);
341
+ const iw = Math.max(1, Math.round(ix + w) - ix); // right edge - left edge
342
+ const ih = Math.max(1, Math.round(iy + h) - iy);
343
+ for (let dy = 0; dy < ih; dy++) {
344
+ for (let dx = 0; dx < iw; dx++) {
345
+ setPixel(buf, ix + dx, iy + dy, color);
346
+ }
347
+ }
348
+ }
349
+ /** Fill a scaled cell and track drawn pixels for outline generation. */
350
+ function fillCellTracked(buf, x, y, w, h, color, pixels) {
351
+ const ix = Math.floor(x);
352
+ const iy = Math.floor(y);
353
+ const iw = Math.max(1, Math.round(ix + w) - ix);
354
+ const ih = Math.max(1, Math.round(iy + h) - iy);
355
+ for (let dy = 0; dy < ih; dy++) {
356
+ for (let dx = 0; dx < iw; dx++) {
357
+ const px = ix + dx;
358
+ const py = iy + dy;
359
+ if (px >= 0 && px < 64 && py >= 0 && py < 64) {
360
+ const idx = (py * 64 + px) * 3;
361
+ buf[idx] = color[0];
362
+ buf[idx + 1] = color[1];
363
+ buf[idx + 2] = color[2];
364
+ pixels.add(py * 64 + px);
365
+ }
366
+ }
367
+ }
368
+ }
369
+ /** Glow-fill a scaled cell — edge rounding. */
370
+ function glowCell(buf, x, y, w, h, color, intensity) {
371
+ const ix = Math.floor(x);
372
+ const iy = Math.floor(y);
373
+ const iw = Math.max(1, Math.round(ix + w) - ix);
374
+ const ih = Math.max(1, Math.round(iy + h) - iy);
375
+ for (let dy = 0; dy < ih; dy++) {
376
+ for (let dx = 0; dx < iw; dx++) {
377
+ glowPixel(buf, ix + dx, iy + dy, color, intensity);
378
+ }
379
+ }
380
+ }
381
+ /** Linearly interpolate between two colors. */
382
+ export function lerpColor(a, b, t) {
383
+ const s = Math.max(0, Math.min(1, t));
384
+ return [
385
+ Math.round(a[0] + (b[0] - a[0]) * s),
386
+ Math.round(a[1] + (b[1] - a[1]) * s),
387
+ Math.round(a[2] + (b[2] - a[2]) * s),
388
+ ];
389
+ }
390
+ // ===== Creature Glow Halo =====
391
+ /** Draw elliptical glow halo behind a creature (additive blend). */
392
+ function drawCreatureGlow(buf, cx, cy, rx, ry, glowColor, intensity = 0.15, isLOD = false) {
393
+ // LOD: moderate glow — enough to hint presence without blurring silhouette
394
+ const actualIntensity = isLOD ? intensity * 1.5 : intensity;
395
+ const spread = isLOD ? 1.2 : 1.3;
396
+ const irx = Math.ceil(rx * spread);
397
+ const iry = Math.ceil(ry * spread);
398
+ for (let dy = -iry; dy <= iry; dy++) {
399
+ for (let dx = -irx; dx <= irx; dx++) {
400
+ const dist = Math.sqrt((dx / rx) ** 2 + (dy / ry) ** 2);
401
+ if (dist > spread)
402
+ continue;
403
+ const falloff = (1 - dist / spread) ** 2;
404
+ glowPixel(buf, cx + dx, cy + dy, glowColor, actualIntensity * falloff);
405
+ }
406
+ }
407
+ }
408
+ // ===== Creature Outline =====
409
+ /** 8-directional colored outline around tracked creature pixels. */
410
+ function drawCreatureOutline(buf, creaturePixels, bodyColor, alpha = 0.8) {
411
+ const outlineColor = [
412
+ Math.round(bodyColor[0] * 0.5),
413
+ Math.round(bodyColor[1] * 0.5),
414
+ Math.round(bodyColor[2] * 0.5),
415
+ ];
416
+ const neighbors = [-1, 0, 1];
417
+ for (const key of creaturePixels) {
418
+ const cx = key % 64;
419
+ const cy = Math.floor(key / 64);
420
+ for (const ndx of neighbors) {
421
+ for (const ndy of neighbors) {
422
+ if (ndx === 0 && ndy === 0)
423
+ continue;
424
+ const nx = cx + ndx;
425
+ const ny = cy + ndy;
426
+ if (nx < 0 || nx >= 64 || ny < 0 || ny >= 64)
427
+ continue;
428
+ if (!creaturePixels.has(ny * 64 + nx)) {
429
+ blendPixel(buf, nx, ny, outlineColor, alpha);
430
+ }
431
+ }
432
+ }
433
+ }
434
+ }
435
+ // ===== Octopus Cell Color =====
436
+ function getOctopusCellColor(cellType, state, blinkPhase, isLOD) {
437
+ if (cellType === EMPTY)
438
+ return null;
439
+ if (state === 'sleeping')
440
+ return COLORS.octopusSleeping;
441
+ switch (cellType) {
442
+ case BODY: return state === 'working' ? COLORS.octopusStarburst : COLORS.octopusBody;
443
+ case EYE:
444
+ return COLORS.octopusEye; // always-on black dot (no blink — consistent 2px vertical)
445
+ case LEFT_ARM:
446
+ case RIGHT_ARM: return COLORS.octopusArm;
447
+ case LEFT_LEG:
448
+ case RIGHT_LEG: return COLORS.octopusLeg;
449
+ default: return COLORS.octopusBody;
450
+ }
451
+ }
452
+ // ===== Crayfish Cell Color =====
453
+ function getCrayfishCellColor(cellType, routing, sick = false) {
454
+ if (cellType === EMPTY)
455
+ return null;
456
+ if (sick) {
457
+ // SICK: desaturated, muted colors — gateway error state
458
+ switch (cellType) {
459
+ case EYE: return COLORS.crayfishSick; // eyes drawn as overlay; grid cell = body
460
+ case LEFT_ARM:
461
+ case RIGHT_ARM: return [0x77, 0x55, 0x55];
462
+ case LEFT_LEG:
463
+ case RIGHT_LEG: return [0x77, 0x55, 0x55];
464
+ case ANTENNA: return [0x88, 0x66, 0x66];
465
+ default: return COLORS.crayfishSick;
466
+ }
467
+ }
468
+ const bodyColor = routing ? COLORS.crayfishRouting : COLORS.crayfishBody;
469
+ switch (cellType) {
470
+ case BODY: return bodyColor;
471
+ case EYE: return bodyColor; // eyes drawn as fixed 3×3 overlay after grid
472
+ case LEFT_ARM:
473
+ case RIGHT_ARM: return COLORS.crayfishClaw;
474
+ case LEFT_LEG:
475
+ case RIGHT_LEG: return COLORS.crayfishLeg;
476
+ case ANTENNA: return COLORS.crayfishAntenna;
477
+ default: return bodyColor;
478
+ }
479
+ }
480
+ // ===== Animation Helpers =====
481
+ /** Ensure animation offset has at least `minPx` magnitude when non-zero. */
482
+ function ensureMinAmplitude(value, minPx) {
483
+ if (Math.abs(value) < 0.01)
484
+ return 0; // truly zero → stay zero
485
+ const rounded = Math.round(value);
486
+ if (rounded === 0)
487
+ return value > 0 ? minPx : -minPx;
488
+ return rounded;
489
+ }
490
+ // ===== Scaled Creature Renderers (camera-aware) =====
491
+ /**
492
+ * Draw octopus — LED pixel art style (square pixels, no outline/glow).
493
+ *
494
+ * LED pixels glow naturally — no artificial outline or glow needed.
495
+ * Eyes rendered as negative space (black pixels within body).
496
+ * Body stays fixed; only arms and tentacles animate.
497
+ */
498
+ export function drawOctopus(buf, worldX, worldY, state, animFrame, cam, palette = getOctopusPaletteForSession(1)) {
499
+ if (!isVisible(worldX, worldY, cam, 0.15))
500
+ return;
501
+ const [scx, scy] = worldToScreen(worldX, worldY, cam);
502
+ // LOD selection
503
+ const useLOD = cam.zoom < 1.3;
504
+ const grid = useLOD ? OCTOPUS_LOD : OCTOPUS_GRID;
505
+ const cols = useLOD ? OCTO_LOD_COLS : OCTO_COLS;
506
+ const rows = useLOD ? OCTO_LOD_ROWS : OCTO_ROWS;
507
+ // Fixed 1px per cell — LED pixel art, no fractional sizes, no tearing
508
+ const cellSz = 1;
509
+ const spriteW = cols;
510
+ const spriteH = rows;
511
+ const baseX = Math.round(scx - spriteW / 2);
512
+ const baseY = Math.round(scy - spriteH / 2);
513
+ // Working: gentle vertical bob (integer pixels only)
514
+ const breathPx = state === 'working'
515
+ ? Math.round(Math.sin(animFrame * 0.3) * 1.5)
516
+ : 0;
517
+ // Body color by state
518
+ const bodyColor = state === 'working' ? palette.starburst
519
+ : state === 'sleeping' ? palette.sleeping
520
+ : palette.body;
521
+ // Draw all cells — solid fill, no outline
522
+ for (let row = 0; row < rows; row++) {
523
+ for (let col = 0; col < cols; col++) {
524
+ const cellType = grid[row][col];
525
+ if (cellType === EMPTY)
526
+ continue;
527
+ // Cell color — arms same as body (unified silhouette), eyes = negative space
528
+ let color;
529
+ if (cellType === EYE) {
530
+ color = COLORS.octopusEye; // black — negative space
531
+ }
532
+ else if (cellType === LEFT_ARM || cellType === RIGHT_ARM) {
533
+ color = state === 'working' ? palette.starburst : palette.arm;
534
+ }
535
+ else if (cellType === LEFT_LEG || cellType === RIGHT_LEG) {
536
+ color = palette.leg;
537
+ }
538
+ else {
539
+ color = bodyColor;
540
+ }
541
+ // Tentacle animation only — arms stay fixed (attached to body)
542
+ let dx = 0;
543
+ if (state !== 'sleeping' && (cellType === LEFT_LEG || cellType === RIGHT_LEG)) {
544
+ dx = Math.round(Math.sin(animFrame * 0.2 + col * 1.8) * 1.5);
545
+ }
546
+ fillCell(buf, baseX + col * cellSz + dx, baseY + row * cellSz + breathPx, cellSz, cellSz, color);
547
+ }
548
+ }
549
+ // "?" bubble when asking
550
+ if (state === 'asking') {
551
+ const bobY = Math.round(Math.sin(animFrame * 0.25));
552
+ const bx = scx;
553
+ const by = baseY - 3 + bobY;
554
+ const r = 3;
555
+ for (let dy = -r; dy <= r; dy++) {
556
+ for (let dx = -r; dx <= r; dx++) {
557
+ if (dx * dx + dy * dy <= r * r) {
558
+ blendPixel(buf, bx + dx, by + dy, COLORS.white, 0.7);
559
+ }
560
+ }
561
+ }
562
+ const qx = Math.round(bx);
563
+ const qy = Math.round(by);
564
+ setPixel(buf, qx + 1, qy - Math.round(r * 0.4), COLORS.stateAwaiting);
565
+ setPixel(buf, qx + 1, qy - Math.round(r * 0.2), COLORS.stateAwaiting);
566
+ setPixel(buf, qx, qy, COLORS.stateAwaiting);
567
+ setPixel(buf, qx, qy + Math.max(1, Math.round(r * 0.35)), COLORS.stateAwaiting);
568
+ }
569
+ // Starburst particles when working
570
+ if (state === 'working') {
571
+ const sparkPhase = animFrame * 0.35;
572
+ const dist = 5 + Math.sin(animFrame * 0.25) * 3;
573
+ for (let i = 0; i < 6; i++) {
574
+ const angle = sparkPhase + (i * Math.PI * 2 / 6);
575
+ const sx = scx + Math.cos(angle) * dist;
576
+ const sy = scy + breathPx + Math.sin(angle) * dist * 0.6;
577
+ setPixel(buf, Math.round(sx), Math.round(sy), palette.starburst);
578
+ }
579
+ }
580
+ }
581
+ /**
582
+ * Draw jellyfish — Codex CLI cloud creature (6-lobe clover, indigo).
583
+ *
584
+ * LED pixel art style matching octopus approach.
585
+ * Bell pulse: edge cells contract/expand rhythmically.
586
+ * >_ marking blinks subtly. Bioluminescent glow when processing.
587
+ */
588
+ export function drawJellyfish(buf, worldX, worldY, state, animFrame, cam, palette = getJellyfishPaletteForSession(1)) {
589
+ if (!isVisible(worldX, worldY, cam, 0.15))
590
+ return;
591
+ const [scx, scy] = worldToScreen(worldX, worldY, cam);
592
+ // LOD selection
593
+ const useLOD = cam.zoom < 1.3;
594
+ const grid = useLOD ? JELLYFISH_LOD : JELLYFISH_GRID;
595
+ const cols = useLOD ? JF_LOD_COLS : JF_COLS;
596
+ const rows = useLOD ? JF_LOD_ROWS : JF_ROWS;
597
+ // Fixed 1px per cell — LED pixel art
598
+ const cellSz = 1;
599
+ const spriteW = cols;
600
+ const spriteH = rows;
601
+ const baseX = Math.round(scx - spriteW / 2);
602
+ const baseY = Math.round(scy - spriteH / 2);
603
+ // Bell pulse: contraction/expansion
604
+ const pulseSpeed = state === 'working' ? 0.25 : 0.06;
605
+ const pulsePhase = Math.sin(animFrame * pulseSpeed);
606
+ const contracting = pulsePhase < 0;
607
+ // Vertical bob when working
608
+ const breathPx = state === 'working'
609
+ ? Math.round(Math.sin(animFrame * 0.3) * 1.5)
610
+ : 0;
611
+ // Bioluminescent body color pulse when working
612
+ const bodyColor = state === 'working'
613
+ ? lerpColor(palette.body, palette.pulse, (Math.sin(animFrame * 0.2) + 1) * 0.5)
614
+ : state === 'sleeping' ? palette.sleeping
615
+ : palette.body;
616
+ // >_ marking blink
617
+ const markingVisible = (animFrame % 60) > 5;
618
+ // Draw all cells
619
+ for (let row = 0; row < rows; row++) {
620
+ for (let col = 0; col < cols; col++) {
621
+ const cellType = grid[row][col];
622
+ if (cellType === EMPTY)
623
+ continue;
624
+ // Breathe edge: hide when contracting
625
+ if (cellType === BREATHE_EDGE) {
626
+ if (contracting)
627
+ continue;
628
+ fillCell(buf, baseX + col * cellSz, baseY + row * cellSz + breathPx, cellSz, cellSz, palette.edge);
629
+ continue;
630
+ }
631
+ // >_ marking: blink
632
+ if (cellType === MARKING) {
633
+ if (!markingVisible) {
634
+ // Show body color when marking hidden
635
+ fillCell(buf, baseX + col * cellSz, baseY + row * cellSz + breathPx, cellSz, cellSz, bodyColor);
636
+ continue;
637
+ }
638
+ fillCell(buf, baseX + col * cellSz, baseY + row * cellSz + breathPx, cellSz, cellSz, palette.marking);
639
+ continue;
640
+ }
641
+ // Body cell
642
+ fillCell(buf, baseX + col * cellSz, baseY + row * cellSz + breathPx, cellSz, cellSz, bodyColor);
643
+ }
644
+ }
645
+ // "?" bubble when asking
646
+ if (state === 'asking') {
647
+ const bobY = Math.round(Math.sin(animFrame * 0.25));
648
+ const bx = scx;
649
+ const by = baseY - 3 + bobY;
650
+ const r = 3;
651
+ for (let dy = -r; dy <= r; dy++) {
652
+ for (let dx = -r; dx <= r; dx++) {
653
+ if (dx * dx + dy * dy <= r * r) {
654
+ blendPixel(buf, bx + dx, by + dy, COLORS.white, 0.7);
655
+ }
656
+ }
657
+ }
658
+ const qx = Math.round(bx);
659
+ const qy = Math.round(by);
660
+ setPixel(buf, qx + 1, qy - Math.round(r * 0.4), COLORS.stateAwaiting);
661
+ setPixel(buf, qx + 1, qy - Math.round(r * 0.2), COLORS.stateAwaiting);
662
+ setPixel(buf, qx, qy, COLORS.stateAwaiting);
663
+ setPixel(buf, qx, qy + Math.max(1, Math.round(r * 0.35)), COLORS.stateAwaiting);
664
+ }
665
+ // Orbiting glow particles when working (bioluminescent)
666
+ if (state === 'working') {
667
+ const orbitPhase = animFrame * 0.2;
668
+ const dist = 5 + Math.sin(animFrame * 0.15) * 2;
669
+ for (let i = 0; i < 4; i++) {
670
+ const angle = orbitPhase + (i * Math.PI * 2 / 4);
671
+ const sx = scx + Math.cos(angle) * dist;
672
+ const sy = scy + breathPx + Math.sin(angle) * dist * 0.6;
673
+ setPixel(buf, Math.round(sx), Math.round(sy), palette.pulse);
674
+ }
675
+ }
676
+ }
677
+ // ===== OpenCode — nested-square logo (simulator SSOT) =====
678
+ export function drawOpenCode(buf, worldX, worldY, state, animFrame, camera, palette) {
679
+ const scx = Math.round(((worldX - camera.cx) * camera.zoom + 0.5) * 64);
680
+ const scy = Math.round(((worldY - camera.cy) * camera.zoom + 0.5) * 64);
681
+ const worldW = 10 / 64;
682
+ const pixW = Math.max(1, Math.round(worldW * camera.zoom * 64 / OC_COLS));
683
+ const pixH = pixW;
684
+ const breathPx = state === 'working'
685
+ ? Math.round(Math.sin(animFrame * 0.3) * 1.5)
686
+ : state === 'idle' ? Math.round(Math.sin(animFrame * 0.08) * 0.7) : 0;
687
+ const useLod = camera.zoom < 1.3;
688
+ const grid = useLod ? OPENCODE_LOD : OPENCODE_GRID;
689
+ const gc = useLod ? OC_LOD_COLS : OC_COLS;
690
+ const gr = useLod ? OC_LOD_ROWS : OC_ROWS;
691
+ const baseX = scx - Math.round((gc * pixW) / 2);
692
+ const baseY = scy + breathPx - Math.round((gr * pixH) / 2);
693
+ const outerColor = state === 'working'
694
+ ? lerpColor(palette.outer, palette.pulse, 0.5 + Math.sin(animFrame * 0.2) * 0.5)
695
+ : state === 'sleeping' ? palette.sleeping : palette.outer;
696
+ const innerColor = state === 'sleeping'
697
+ ? scaleColor(palette.inner, 0.6) : palette.inner;
698
+ for (let r = 0; r < gr; r++) {
699
+ for (let c = 0; c < gc; c++) {
700
+ const cell = grid[r][c];
701
+ if (cell === 0)
702
+ continue;
703
+ const color = (cell === OPENCODE_CORE) ? innerColor : outerColor;
704
+ for (let dy = 0; dy < pixH; dy++) {
705
+ for (let dx = 0; dx < pixW; dx++) {
706
+ setPixel(buf, baseX + c * pixW + dx, baseY + r * pixH + dy, color);
707
+ }
708
+ }
709
+ }
710
+ }
711
+ if (state === 'asking') {
712
+ // Amber three-dot asking indicator
713
+ const dotCx = scx + Math.round(gc / 2) + 3;
714
+ const dotCy = baseY - 2 + Math.round(Math.sin(animFrame * 0.26) * 1.4);
715
+ for (let i = -1; i <= 1; i++) {
716
+ blendPixel(buf, dotCx + i * 3, dotCy, COLORS.stateAwaiting, 0.75);
717
+ blendPixel(buf, dotCx + i * 3 + 1, dotCy, COLORS.stateAwaiting, 0.45);
718
+ }
719
+ }
720
+ }
721
+ /**
722
+ * Draw crayfish scaled by camera zoom.
723
+ * @param worldX Crayfish center in normalized world X (0~1)
724
+ * @param worldY Crayfish center in normalized world Y (0~1)
725
+ */
726
+ export function drawCrayfish(buf, worldX, worldY, routing, animFrame, cam, sick = false) {
727
+ if (!isVisible(worldX, worldY, cam, 0.15))
728
+ return;
729
+ const [scx, scy] = worldToScreen(worldX, worldY, cam);
730
+ // LOD selection — matches octopus threshold
731
+ const useLOD = cam.zoom < 1.3;
732
+ const grid = useLOD ? CRAYFISH_LOD : CRAYFISH_GRID;
733
+ const cols = useLOD ? CF_LOD_COLS : CF_COLS;
734
+ const rows = useLOD ? CF_LOD_ROWS : CF_ROWS;
735
+ // Cell size: detail cellW = Z, LOD cellW = 12*Z/8 = 1.5*Z (bigger cells for fewer cols)
736
+ const cellW = useLOD ? cam.zoom * 1.5 : cam.zoom;
737
+ const cellH = cellW * 1.5;
738
+ const spriteW = cols * cellW;
739
+ const spriteH = rows * cellH;
740
+ const baseX = scx - spriteW / 2;
741
+ const baseY = scy - spriteH / 2;
742
+ // SICK: suppress breathing, no heartbeat
743
+ // Composite breathing — asymmetric (slow inhale, quick exhale)
744
+ const breathRaw = sick ? 0 : Math.pow(Math.abs(Math.sin(animFrame * 0.15)), 0.7)
745
+ * Math.sign(Math.sin(animFrame * 0.15));
746
+ const breathPx = sick ? 0 : ensureMinAmplitude(breathRaw * cellH * 0.5, 1);
747
+ // Heartbeat glow (time-based for consistent 4s period regardless of frame step)
748
+ const heartPhase = (animFrame * 0.075) % (Math.PI * 2);
749
+ const beat1 = Math.max(0, Math.sin(heartPhase * 2) * 0.8);
750
+ const beat2 = Math.max(0, Math.sin(heartPhase * 2 + 1.2) * 0.5);
751
+ const heartGlow = sick ? 0 : Math.max(beat1, beat2);
752
+ // 1. Glow halo (before creature — stronger for LOD)
753
+ const glowRx = spriteW / 2 + 2;
754
+ const glowRy = spriteH / 2 + 2;
755
+ drawCreatureGlow(buf, scx, scy + breathPx, glowRx, glowRy, COLORS.crayfishGlow, 0.12, useLOD);
756
+ // 2. Draw cells with pixel tracking
757
+ const trackedPixels = new Set();
758
+ for (let row = 0; row < rows; row++) {
759
+ for (let col = 0; col < cols; col++) {
760
+ const cellType = grid[row][col];
761
+ const color = getCrayfishCellColor(cellType, routing, sick);
762
+ if (!color)
763
+ continue;
764
+ let dx = 0;
765
+ const dy = breathPx;
766
+ if (!sick) {
767
+ // Antenna wiggle — with occasional twitch
768
+ if (cellType === ANTENNA) {
769
+ const spd = routing ? 0.35 : 0.15;
770
+ const wiggle = Math.sin(animFrame * spd + col * 3) * cellW * 1.5;
771
+ // Random-ish twitch: sharp spike every ~30 frames
772
+ const twitch = ((animFrame + col * 17) % 60) < 4 ? cellW * 2 * (col < 6 ? -1 : 1) : 0;
773
+ dx = ensureMinAmplitude(wiggle + twitch, 1);
774
+ }
775
+ // Claw animation — routing: slower, wider clap; idle: gentle sway
776
+ if (cellType === LEFT_ARM || cellType === RIGHT_ARM) {
777
+ if (routing) {
778
+ const clap = Math.sin(animFrame * 0.3) * cellW * 3; // slower + wider
779
+ dx = ensureMinAmplitude(cellType === LEFT_ARM ? clap : -clap, 1);
780
+ }
781
+ else {
782
+ const gentle = Math.sin(animFrame * 0.125) * cellW;
783
+ dx = ensureMinAmplitude(cellType === LEFT_ARM ? gentle : -gentle, 1);
784
+ }
785
+ }
786
+ // Leg shift — larger amplitude for visibility
787
+ if (cellType === LEFT_LEG || cellType === RIGHT_LEG) {
788
+ dx = ensureMinAmplitude(Math.sin(animFrame * 0.1 + (cellType === LEFT_LEG ? 0 : Math.PI)) * cellW * 1.2, 1);
789
+ }
790
+ }
791
+ else {
792
+ // SICK: tilt — left side up, right side down (droopy posture)
793
+ const tiltPx = (col - cols / 2) * 0.15;
794
+ dx = col < cols / 2 ? 1 : -1; // slight inward collapse
795
+ fillCellTracked(buf, baseX + col * cellW + dx, baseY + row * cellH + dy + tiltPx, cellW, cellH, color, trackedPixels);
796
+ continue;
797
+ }
798
+ fillCellTracked(buf, baseX + col * cellW + dx, baseY + row * cellH + dy, cellW, cellH, color, trackedPixels);
799
+ }
800
+ }
801
+ // 3. Colored outline — moderate alpha for LOD
802
+ const bodyColor = routing ? COLORS.crayfishRouting : COLORS.crayfishBody;
803
+ const outlineAlpha = useLOD ? 0.6 : 0.8;
804
+ drawCreatureOutline(buf, trackedPixels, bodyColor, outlineAlpha);
805
+ // Fixed 3×3 eyes (teal center + black ring) — drawn at grid eye positions
806
+ const eyeRow = useLOD ? 2 : 3;
807
+ const eyeCols = useLOD ? [2, 5] : [4, 7];
808
+ const sickEyeCenter = [0x44, 0x66, 0x60]; // dim teal for sick
809
+ const eyeCenter = sick ? sickEyeCenter : COLORS.crayfishEye;
810
+ const eyeRing = sick ? [0x44, 0x33, 0x33] : COLORS.crayfishEyeRing;
811
+ for (const ec of eyeCols) {
812
+ const ex = Math.round(baseX + (ec + 0.5) * cellW);
813
+ const ey = Math.round(baseY + (eyeRow + 0.5) * cellH + breathPx);
814
+ // Black surround (8 neighbors)
815
+ for (let dy = -1; dy <= 1; dy++) {
816
+ for (let dx = -1; dx <= 1; dx++) {
817
+ if (dx === 0 && dy === 0)
818
+ continue;
819
+ setPixel(buf, ex + dx, ey + dy, eyeRing);
820
+ }
821
+ }
822
+ // Teal center
823
+ setPixel(buf, ex, ey, eyeCenter);
824
+ }
825
+ // Routing: signal wave particles (after outline)
826
+ if (routing) {
827
+ const wavePhase = animFrame * 0.3;
828
+ for (let i = 0; i < 4; i++) {
829
+ const angle = wavePhase + (i * Math.PI / 2);
830
+ const dist = (4 + Math.sin(animFrame * 0.2 + i)) * cellW;
831
+ const sx = scx + Math.cos(angle) * dist;
832
+ const sy = scy + breathPx + Math.sin(angle) * dist * 0.5;
833
+ glowCell(buf, sx, sy, cellW, cellW, COLORS.crayfishEye, 0.4);
834
+ }
835
+ // Body glow pulse
836
+ const bodyPulse = (Math.sin(animFrame * 0.25) + 1) * 0.15;
837
+ for (let row = 0; row < rows; row++) {
838
+ for (let col = 0; col < cols; col++) {
839
+ if (grid[row]?.[col] !== EMPTY) {
840
+ glowCell(buf, baseX + col * cellW, baseY + row * cellH + breathPx, cellW, cellH, COLORS.crayfishRouting, bodyPulse);
841
+ }
842
+ }
843
+ }
844
+ }
845
+ }
846
+ /**
847
+ * Draw a scaled neon tetra.
848
+ * At zoom 1.0: ~4 screen pixels. At zoom 2.0: ~8 screen pixels.
849
+ */
850
+ export function drawTetra(buf, worldX, worldY, heading, cam) {
851
+ if (!isVisible(worldX, worldY, cam, 0.08))
852
+ return;
853
+ const [sx, sy] = worldToScreen(worldX, worldY, cam);
854
+ const px = cam.zoom; // pixel scale
855
+ // Body (2px-equivalent)
856
+ const bw = Math.max(1, Math.round(px * 2));
857
+ const bh = Math.max(1, Math.round(px));
858
+ fillCell(buf, sx, sy, bw, bh, COLORS.tetraBody);
859
+ // Neon stripe (1px behind body)
860
+ const stripeX = heading > 0 ? sx - Math.round(px) : sx + Math.round(px * 2);
861
+ fillCell(buf, stripeX, sy, Math.max(1, Math.round(px)), bh, COLORS.tetraNeon);
862
+ // Tail fin
863
+ const finX = heading > 0 ? stripeX - Math.max(1, Math.round(px * 0.5)) : stripeX + Math.max(1, Math.round(px));
864
+ fillCell(buf, finX, sy, Math.max(1, Math.round(px * 0.5)), bh, COLORS.tetraFin);
865
+ }
866
+ // ===== Legacy World-Buffer Renderers (used for environment-embedded drawing) =====
867
+ /** Draw a tetra at pixel coordinates (for world buffer, zoom-unaware). */
868
+ export function drawTetraWorld(buf, x, y, heading) {
869
+ setPixel(buf, x, y, COLORS.tetraBody);
870
+ const tailX = heading > 0 ? x - 1 : x + 1;
871
+ setPixel(buf, tailX, y, COLORS.tetraNeon);
872
+ }
873
+ // ===== 3×5 Pixel Font + HUD Helpers =====
874
+ /**
875
+ * 3×5 bitmask font for HUD numerals.
876
+ * Each glyph is 5 rows of 3 bits (MSB = left pixel).
877
+ */
878
+ const PIXEL_FONT = {
879
+ '0': [0b111, 0b101, 0b101, 0b101, 0b111],
880
+ '1': [0b010, 0b110, 0b010, 0b010, 0b111],
881
+ '2': [0b111, 0b001, 0b111, 0b100, 0b111],
882
+ '3': [0b111, 0b001, 0b111, 0b001, 0b111],
883
+ '4': [0b101, 0b101, 0b111, 0b001, 0b001],
884
+ '5': [0b111, 0b100, 0b111, 0b001, 0b111],
885
+ '6': [0b111, 0b100, 0b111, 0b101, 0b111],
886
+ '7': [0b111, 0b001, 0b001, 0b001, 0b001],
887
+ '8': [0b111, 0b101, 0b111, 0b101, 0b111],
888
+ '9': [0b111, 0b101, 0b111, 0b001, 0b111],
889
+ '%': [0b101, 0b001, 0b010, 0b100, 0b101],
890
+ 'h': [0b100, 0b100, 0b111, 0b101, 0b101],
891
+ 'm': [0b101, 0b111, 0b101, 0b101, 0b101], // two pillars joined — distinct from 'n'
892
+ 'd': [0b001, 0b001, 0b011, 0b101, 0b011], // lowercase d — for day units (e.g. "6d22h")
893
+ ' ': [0b000, 0b000, 0b000, 0b000, 0b000],
894
+ };
895
+ /** Draw right-aligned text using 3×5 pixel font. */
896
+ export function drawText(buf, text, rightX, y, color) {
897
+ let cursorX = rightX;
898
+ // Render right-to-left
899
+ for (let ci = text.length - 1; ci >= 0; ci--) {
900
+ const glyph = PIXEL_FONT[text[ci]];
901
+ if (!glyph) {
902
+ cursorX -= 2;
903
+ continue;
904
+ } // unknown char = 1px gap
905
+ cursorX -= 3; // glyph width
906
+ for (let row = 0; row < 5; row++) {
907
+ const bits = glyph[row];
908
+ for (let col = 0; col < 3; col++) {
909
+ if (bits & (1 << (2 - col))) {
910
+ setPixel(buf, cursorX + col, y + row, color);
911
+ }
912
+ }
913
+ }
914
+ cursorX -= 1; // 1px kerning gap
915
+ }
916
+ }
917
+ /** Draw a horizontal gauge bar. */
918
+ export function drawGaugeBar(buf, x, y, width, height, percent, color, bgColor) {
919
+ const filled = Math.round(width * Math.max(0, Math.min(1, percent / 100)));
920
+ for (let dy = 0; dy < height; dy++) {
921
+ for (let dx = 0; dx < width; dx++) {
922
+ setPixel(buf, x + dx, y + dy, dx < filled ? color : bgColor);
923
+ }
924
+ }
925
+ }
926
+ //# sourceMappingURL=pixoo-sprites.js.map