@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.
- package/LICENSE +21 -0
- package/dist/adapters/claude-code.d.ts +11 -27
- package/dist/adapters/claude-code.d.ts.map +1 -1
- package/dist/adapters/claude-code.js +29 -123
- package/dist/adapters/claude-code.js.map +1 -1
- package/dist/adapters/codex-cli.d.ts +21 -0
- package/dist/adapters/codex-cli.d.ts.map +1 -0
- package/dist/adapters/codex-cli.js +53 -0
- package/dist/adapters/codex-cli.js.map +1 -0
- package/dist/adapters/index.d.ts +4 -0
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +13 -0
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/monitor.d.ts +34 -0
- package/dist/adapters/monitor.d.ts.map +1 -0
- package/dist/adapters/monitor.js +86 -0
- package/dist/adapters/monitor.js.map +1 -0
- package/dist/adapters/openclaw.d.ts +36 -2
- package/dist/adapters/openclaw.d.ts.map +1 -1
- package/dist/adapters/openclaw.js +329 -59
- package/dist/adapters/openclaw.js.map +1 -1
- package/dist/adapters/opencode-adapter.d.ts +48 -0
- package/dist/adapters/opencode-adapter.d.ts.map +1 -0
- package/dist/adapters/opencode-adapter.js +345 -0
- package/dist/adapters/opencode-adapter.js.map +1 -0
- package/dist/adapters/pty-adapter.d.ts +56 -0
- package/dist/adapters/pty-adapter.d.ts.map +1 -0
- package/dist/adapters/pty-adapter.js +143 -0
- package/dist/adapters/pty-adapter.js.map +1 -0
- package/dist/adb-reverse.d.ts +25 -0
- package/dist/adb-reverse.d.ts.map +1 -0
- package/dist/adb-reverse.js +129 -0
- package/dist/adb-reverse.js.map +1 -0
- package/dist/antigravity-local.d.ts +7 -0
- package/dist/antigravity-local.d.ts.map +1 -0
- package/dist/antigravity-local.js +153 -0
- package/dist/antigravity-local.js.map +1 -0
- package/dist/apme/adapters/claude-hook.d.ts +15 -0
- package/dist/apme/adapters/claude-hook.d.ts.map +1 -0
- package/dist/apme/adapters/claude-hook.js +85 -0
- package/dist/apme/adapters/claude-hook.js.map +1 -0
- package/dist/apme/adapters/claude-pty.d.ts +28 -0
- package/dist/apme/adapters/claude-pty.d.ts.map +1 -0
- package/dist/apme/adapters/claude-pty.js +88 -0
- package/dist/apme/adapters/claude-pty.js.map +1 -0
- package/dist/apme/adapters/timeline.d.ts +16 -0
- package/dist/apme/adapters/timeline.d.ts.map +1 -0
- package/dist/apme/adapters/timeline.js +66 -0
- package/dist/apme/adapters/timeline.js.map +1 -0
- package/dist/apme/classifier.d.ts +53 -0
- package/dist/apme/classifier.d.ts.map +1 -0
- package/dist/apme/classifier.js +266 -0
- package/dist/apme/classifier.js.map +1 -0
- package/dist/apme/claude-transcript-reader.d.ts +38 -0
- package/dist/apme/claude-transcript-reader.d.ts.map +1 -0
- package/dist/apme/claude-transcript-reader.js +127 -0
- package/dist/apme/claude-transcript-reader.js.map +1 -0
- package/dist/apme/collector.d.ts +108 -0
- package/dist/apme/collector.d.ts.map +1 -0
- package/dist/apme/collector.js +659 -0
- package/dist/apme/collector.js.map +1 -0
- package/dist/apme/dashboard-html.d.ts +9 -0
- package/dist/apme/dashboard-html.d.ts.map +1 -0
- package/dist/apme/dashboard-html.js +416 -0
- package/dist/apme/dashboard-html.js.map +1 -0
- package/dist/apme/http.d.ts +19 -0
- package/dist/apme/http.d.ts.map +1 -0
- package/dist/apme/http.js +210 -0
- package/dist/apme/http.js.map +1 -0
- package/dist/apme/hw-sampler.d.ts +30 -0
- package/dist/apme/hw-sampler.d.ts.map +1 -0
- package/dist/apme/hw-sampler.js +106 -0
- package/dist/apme/hw-sampler.js.map +1 -0
- package/dist/apme/index.d.ts +43 -0
- package/dist/apme/index.d.ts.map +1 -0
- package/dist/apme/index.js +75 -0
- package/dist/apme/index.js.map +1 -0
- package/dist/apme/outcome.d.ts +48 -0
- package/dist/apme/outcome.d.ts.map +1 -0
- package/dist/apme/outcome.js +232 -0
- package/dist/apme/outcome.js.map +1 -0
- package/dist/apme/recommend.d.ts +32 -0
- package/dist/apme/recommend.d.ts.map +1 -0
- package/dist/apme/recommend.js +41 -0
- package/dist/apme/recommend.js.map +1 -0
- package/dist/apme/runner.d.ts +94 -0
- package/dist/apme/runner.d.ts.map +1 -0
- package/dist/apme/runner.js +812 -0
- package/dist/apme/runner.js.map +1 -0
- package/dist/apme/settings.d.ts +65 -0
- package/dist/apme/settings.d.ts.map +1 -0
- package/dist/apme/settings.js +97 -0
- package/dist/apme/settings.js.map +1 -0
- package/dist/apme/store.d.ts +99 -0
- package/dist/apme/store.d.ts.map +1 -0
- package/dist/apme/store.js +911 -0
- package/dist/apme/store.js.map +1 -0
- package/dist/apme/tuner.d.ts +64 -0
- package/dist/apme/tuner.d.ts.map +1 -0
- package/dist/apme/tuner.js +297 -0
- package/dist/apme/tuner.js.map +1 -0
- package/dist/apme/types.d.ts +11 -0
- package/dist/apme/types.d.ts.map +1 -0
- package/dist/apme/types.js +10 -0
- package/dist/apme/types.js.map +1 -0
- package/dist/auth.d.ts +1 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +15 -5
- package/dist/auth.js.map +1 -1
- package/dist/bridge-core.d.ts +162 -0
- package/dist/bridge-core.d.ts.map +1 -0
- package/dist/bridge-core.js +627 -0
- package/dist/bridge-core.js.map +1 -0
- package/dist/check-deps.d.ts +3 -1
- package/dist/check-deps.d.ts.map +1 -1
- package/dist/check-deps.js +34 -13
- package/dist/check-deps.js.map +1 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1235 -0
- package/dist/cli.js.map +1 -0
- package/dist/codex-auth.d.ts +10 -0
- package/dist/codex-auth.d.ts.map +1 -0
- package/dist/codex-auth.js +63 -0
- package/dist/codex-auth.js.map +1 -0
- package/dist/codex-output-parser.d.ts +21 -0
- package/dist/codex-output-parser.d.ts.map +1 -0
- package/dist/codex-output-parser.js +253 -0
- package/dist/codex-output-parser.js.map +1 -0
- package/dist/d200h/hid-protocol.d.ts +62 -0
- package/dist/d200h/hid-protocol.d.ts.map +1 -0
- package/dist/d200h/hid-protocol.js +124 -0
- package/dist/d200h/hid-protocol.js.map +1 -0
- package/dist/d200h/image-renderer.d.ts +36 -0
- package/dist/d200h/image-renderer.d.ts.map +1 -0
- package/dist/d200h/image-renderer.js +487 -0
- package/dist/d200h/image-renderer.js.map +1 -0
- package/dist/daemon-server.d.ts +19 -0
- package/dist/daemon-server.d.ts.map +1 -0
- package/dist/daemon-server.js +1168 -0
- package/dist/daemon-server.js.map +1 -0
- package/dist/daemon-ws-client.d.ts +82 -0
- package/dist/daemon-ws-client.d.ts.map +1 -0
- package/dist/daemon-ws-client.js +179 -0
- package/dist/daemon-ws-client.js.map +1 -0
- package/dist/daemon.d.ts +10 -0
- package/dist/daemon.d.ts.map +1 -0
- package/dist/daemon.js +56 -0
- package/dist/daemon.js.map +1 -0
- package/dist/display-monitor.d.ts +25 -0
- package/dist/display-monitor.d.ts.map +1 -0
- package/dist/display-monitor.js +143 -0
- package/dist/display-monitor.js.map +1 -0
- package/dist/esp32-serial.d.ts +112 -0
- package/dist/esp32-serial.d.ts.map +1 -0
- package/dist/esp32-serial.js +502 -0
- package/dist/esp32-serial.js.map +1 -0
- package/dist/gateway-probe.d.ts +12 -0
- package/dist/gateway-probe.d.ts.map +1 -0
- package/dist/gateway-probe.js +39 -0
- package/dist/gateway-probe.js.map +1 -0
- package/dist/hook-server.d.ts +29 -0
- package/dist/hook-server.d.ts.map +1 -1
- package/dist/hook-server.js +366 -8
- package/dist/hook-server.js.map +1 -1
- package/dist/index.d.ts +20 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1305 -562
- package/dist/index.js.map +1 -1
- package/dist/log-stream.d.ts +26 -0
- package/dist/log-stream.d.ts.map +1 -0
- package/dist/log-stream.js +111 -0
- package/dist/log-stream.js.map +1 -0
- package/dist/logger.d.ts +9 -2
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +36 -5
- package/dist/logger.js.map +1 -1
- package/dist/mdns.d.ts +15 -1
- package/dist/mdns.d.ts.map +1 -1
- package/dist/mdns.js +143 -28
- package/dist/mdns.js.map +1 -1
- package/dist/mlx-probe.d.ts +14 -0
- package/dist/mlx-probe.d.ts.map +1 -0
- package/dist/mlx-probe.js +44 -0
- package/dist/mlx-probe.js.map +1 -0
- package/dist/model-catalog.d.ts +5 -3
- package/dist/model-catalog.d.ts.map +1 -1
- package/dist/model-catalog.js +45 -29
- package/dist/model-catalog.js.map +1 -1
- package/dist/modules/adb-module.d.ts +14 -0
- package/dist/modules/adb-module.d.ts.map +1 -0
- package/dist/modules/adb-module.js +36 -0
- package/dist/modules/adb-module.js.map +1 -0
- package/dist/modules/d200h-module.d.ts +43 -0
- package/dist/modules/d200h-module.d.ts.map +1 -0
- package/dist/modules/d200h-module.js +360 -0
- package/dist/modules/d200h-module.js.map +1 -0
- package/dist/modules/d200h-renderer.d.ts +7 -0
- package/dist/modules/d200h-renderer.d.ts.map +1 -0
- package/dist/modules/d200h-renderer.js +321 -0
- package/dist/modules/d200h-renderer.js.map +1 -0
- package/dist/modules/index.d.ts +23 -0
- package/dist/modules/index.d.ts.map +1 -0
- package/dist/modules/index.js +65 -0
- package/dist/modules/index.js.map +1 -0
- package/dist/modules/mdns-module.d.ts +12 -0
- package/dist/modules/mdns-module.d.ts.map +1 -0
- package/dist/modules/mdns-module.js +21 -0
- package/dist/modules/mdns-module.js.map +1 -0
- package/dist/modules/pixoo-module.d.ts +8 -0
- package/dist/modules/pixoo-module.d.ts.map +1 -0
- package/dist/modules/pixoo-module.js +24 -0
- package/dist/modules/pixoo-module.js.map +1 -0
- package/dist/modules/serial-module.d.ts +18 -0
- package/dist/modules/serial-module.d.ts.map +1 -0
- package/dist/modules/serial-module.js +47 -0
- package/dist/modules/serial-module.js.map +1 -0
- package/dist/modules/types.d.ts +39 -0
- package/dist/modules/types.d.ts.map +1 -0
- package/dist/modules/types.js +2 -0
- package/dist/modules/types.js.map +1 -0
- package/dist/ollama-probe.d.ts +13 -0
- package/dist/ollama-probe.d.ts.map +1 -0
- package/dist/ollama-probe.js +46 -0
- package/dist/ollama-probe.js.map +1 -0
- package/dist/opencode-client.d.ts +126 -0
- package/dist/opencode-client.d.ts.map +1 -0
- package/dist/opencode-client.js +181 -0
- package/dist/opencode-client.js.map +1 -0
- package/dist/output-parser.d.ts +3 -0
- package/dist/output-parser.d.ts.map +1 -1
- package/dist/output-parser.js +29 -3
- package/dist/output-parser.js.map +1 -1
- package/dist/passive-observer.d.ts +38 -0
- package/dist/passive-observer.d.ts.map +1 -0
- package/dist/passive-observer.js +632 -0
- package/dist/passive-observer.js.map +1 -0
- package/dist/pixoo/pixoo-bridge.d.ts +50 -0
- package/dist/pixoo/pixoo-bridge.d.ts.map +1 -0
- package/dist/pixoo/pixoo-bridge.js +280 -0
- package/dist/pixoo/pixoo-bridge.js.map +1 -0
- package/dist/pixoo/pixoo-camera.d.ts +80 -0
- package/dist/pixoo/pixoo-camera.d.ts.map +1 -0
- package/dist/pixoo/pixoo-camera.js +271 -0
- package/dist/pixoo/pixoo-camera.js.map +1 -0
- package/dist/pixoo/pixoo-client.d.ts +67 -0
- package/dist/pixoo/pixoo-client.d.ts.map +1 -0
- package/dist/pixoo/pixoo-client.js +335 -0
- package/dist/pixoo/pixoo-client.js.map +1 -0
- package/dist/pixoo/pixoo-font.d.ts +24 -0
- package/dist/pixoo/pixoo-font.d.ts.map +1 -0
- package/dist/pixoo/pixoo-font.js +121 -0
- package/dist/pixoo/pixoo-font.js.map +1 -0
- package/dist/pixoo/pixoo-renderer.d.ts +32 -0
- package/dist/pixoo/pixoo-renderer.d.ts.map +1 -0
- package/dist/pixoo/pixoo-renderer.js +696 -0
- package/dist/pixoo/pixoo-renderer.js.map +1 -0
- package/dist/pixoo/pixoo-settings.d.ts +10 -0
- package/dist/pixoo/pixoo-settings.d.ts.map +1 -0
- package/dist/pixoo/pixoo-settings.js +47 -0
- package/dist/pixoo/pixoo-settings.js.map +1 -0
- package/dist/pixoo/pixoo-sprites.d.ts +187 -0
- package/dist/pixoo/pixoo-sprites.d.ts.map +1 -0
- package/dist/pixoo/pixoo-sprites.js +926 -0
- package/dist/pixoo/pixoo-sprites.js.map +1 -0
- package/dist/pty-manager.d.ts +1 -1
- package/dist/pty-manager.d.ts.map +1 -1
- package/dist/pty-manager.js +36 -15
- package/dist/pty-manager.js.map +1 -1
- package/dist/session-aggregator.d.ts +41 -0
- package/dist/session-aggregator.d.ts.map +1 -0
- package/dist/session-aggregator.js +76 -0
- package/dist/session-aggregator.js.map +1 -0
- package/dist/session-focus-relay.d.ts +35 -0
- package/dist/session-focus-relay.d.ts.map +1 -0
- package/dist/session-focus-relay.js +127 -0
- package/dist/session-focus-relay.js.map +1 -0
- package/dist/session-registry.d.ts +88 -1
- package/dist/session-registry.d.ts.map +1 -1
- package/dist/session-registry.js +222 -15
- package/dist/session-registry.js.map +1 -1
- package/dist/session-timeline-relay.d.ts +27 -0
- package/dist/session-timeline-relay.d.ts.map +1 -0
- package/dist/session-timeline-relay.js +124 -0
- package/dist/session-timeline-relay.js.map +1 -0
- package/dist/state-machine.d.ts +1 -0
- package/dist/state-machine.d.ts.map +1 -1
- package/dist/state-machine.js +14 -0
- package/dist/state-machine.js.map +1 -1
- package/dist/terminal-status.d.ts +28 -0
- package/dist/terminal-status.d.ts.map +1 -0
- package/dist/terminal-status.js +152 -0
- package/dist/terminal-status.js.map +1 -0
- package/dist/timeline-store.d.ts +22 -0
- package/dist/timeline-store.d.ts.map +1 -0
- package/dist/timeline-store.js +79 -0
- package/dist/timeline-store.js.map +1 -0
- package/dist/timeline-summarizer.d.ts +14 -0
- package/dist/timeline-summarizer.d.ts.map +1 -0
- package/dist/timeline-summarizer.js +154 -0
- package/dist/timeline-summarizer.js.map +1 -0
- package/dist/tts.d.ts +25 -0
- package/dist/tts.d.ts.map +1 -0
- package/dist/tts.js +99 -0
- package/dist/tts.js.map +1 -0
- package/dist/tui/ansi.d.ts +92 -0
- package/dist/tui/ansi.d.ts.map +1 -0
- package/dist/tui/ansi.js +181 -0
- package/dist/tui/ansi.js.map +1 -0
- package/dist/tui/dashboard.d.ts +35 -0
- package/dist/tui/dashboard.d.ts.map +1 -0
- package/dist/tui/dashboard.js +436 -0
- package/dist/tui/dashboard.js.map +1 -0
- package/dist/tui/gauge.d.ts +15 -0
- package/dist/tui/gauge.d.ts.map +1 -0
- package/dist/tui/gauge.js +82 -0
- package/dist/tui/gauge.js.map +1 -0
- package/dist/tui/renderer.d.ts +15 -0
- package/dist/tui/renderer.d.ts.map +1 -0
- package/dist/tui/renderer.js +758 -0
- package/dist/tui/renderer.js.map +1 -0
- package/dist/tui/screen.d.ts +27 -0
- package/dist/tui/screen.d.ts.map +1 -0
- package/dist/tui/screen.js +109 -0
- package/dist/tui/screen.js.map +1 -0
- package/dist/tui/terrarium.d.ts +96 -0
- package/dist/tui/terrarium.d.ts.map +1 -0
- package/dist/tui/terrarium.js +912 -0
- package/dist/tui/terrarium.js.map +1 -0
- package/dist/types.d.ts +4 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +3 -2
- package/dist/types.js.map +1 -1
- package/dist/usage-api.d.ts +9 -2
- package/dist/usage-api.d.ts.map +1 -1
- package/dist/usage-api.js +224 -42
- package/dist/usage-api.js.map +1 -1
- package/dist/usage-event.d.ts +12 -0
- package/dist/usage-event.d.ts.map +1 -0
- package/dist/usage-event.js +78 -0
- package/dist/usage-event.js.map +1 -0
- package/dist/usage-tracker.d.ts +4 -0
- package/dist/usage-tracker.d.ts.map +1 -1
- package/dist/usage-tracker.js +17 -0
- package/dist/usage-tracker.js.map +1 -1
- package/dist/utility-proxy.d.ts +30 -0
- package/dist/utility-proxy.d.ts.map +1 -0
- package/dist/utility-proxy.js +217 -0
- package/dist/utility-proxy.js.map +1 -0
- package/dist/utils/project-name.d.ts +22 -0
- package/dist/utils/project-name.d.ts.map +1 -0
- package/dist/utils/project-name.js +72 -0
- package/dist/utils/project-name.js.map +1 -0
- package/dist/version-check.d.ts +26 -0
- package/dist/version-check.d.ts.map +1 -0
- package/dist/version-check.js +225 -0
- package/dist/version-check.js.map +1 -0
- package/dist/voice-assistant.d.ts +63 -0
- package/dist/voice-assistant.d.ts.map +1 -0
- package/dist/voice-assistant.js +406 -0
- package/dist/voice-assistant.js.map +1 -0
- package/dist/voice.d.ts +2 -0
- package/dist/voice.d.ts.map +1 -1
- package/dist/voice.js +26 -1
- package/dist/voice.js.map +1 -1
- package/dist/wake-word.d.ts +35 -0
- package/dist/wake-word.d.ts.map +1 -0
- package/dist/wake-word.js +225 -0
- package/dist/wake-word.js.map +1 -0
- package/dist/whisper-server-manager.d.ts +1 -1
- package/dist/whisper-server-manager.js +1 -1
- package/dist/wifi-config.d.ts +24 -0
- package/dist/wifi-config.d.ts.map +1 -0
- package/dist/wifi-config.js +84 -0
- package/dist/wifi-config.js.map +1 -0
- package/dist/ws-server.d.ts +12 -0
- package/dist/ws-server.d.ts.map +1 -1
- package/dist/ws-server.js +81 -3
- package/dist/ws-server.js.map +1 -1
- 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
|