opencode-buddy 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -156
- package/package.json +11 -20
- package/src/persistence.js +7 -7
- package/src/server-plugin.js +133 -0
- package/src/species.js +541 -0
- package/src/state.js +120 -0
- package/src/tui-plugin.jsx +156 -0
- package/bin/opencode-buddy.js +0 -45
- package/src/buddy/species.js +0 -399
- package/src/buddy/state.js +0 -168
- package/src/index.js +0 -332
- package/src/install.js +0 -157
- package/src/plugin.js +0 -178
- package/src/tmux.js +0 -90
- package/src/tui.js +0 -164
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
import type { TuiPlugin, TuiPluginApi, TuiPluginModule, TuiSlotPlugin } from "@opencode-ai/plugin/tui"
|
|
3
|
+
import { createSignal, createMemo, onCleanup, Show, For } from "solid-js"
|
|
4
|
+
import * as persistence from "./persistence.js"
|
|
5
|
+
import {
|
|
6
|
+
tick as tickState,
|
|
7
|
+
celebrate as celebrateState,
|
|
8
|
+
scared as scaredState,
|
|
9
|
+
deriveState,
|
|
10
|
+
} from "./state.js"
|
|
11
|
+
import { renderFrame, frameCount, frameIntervalMs } from "./species.js"
|
|
12
|
+
|
|
13
|
+
const ANIMATION_TICK_MS = frameIntervalMs()
|
|
14
|
+
const REFRESH_TICK_MS = 1500
|
|
15
|
+
|
|
16
|
+
function View(props: { api: TuiPluginApi; session_id: string }) {
|
|
17
|
+
const theme = () => props.api.theme.current
|
|
18
|
+
const [state, setState] = createSignal<any | null>(null)
|
|
19
|
+
const [frame, setFrame] = createSignal(0)
|
|
20
|
+
const [lastMtime, setLastMtime] = createSignal(0)
|
|
21
|
+
|
|
22
|
+
const refresh = async () => {
|
|
23
|
+
const mt = await persistence.mtime()
|
|
24
|
+
if (mt === 0) {
|
|
25
|
+
const fresh = {
|
|
26
|
+
species: "duck",
|
|
27
|
+
name: "Quack",
|
|
28
|
+
hunger: 80,
|
|
29
|
+
happiness: 80,
|
|
30
|
+
energy: 100,
|
|
31
|
+
xp: 0,
|
|
32
|
+
level: 1,
|
|
33
|
+
hatchedAt: Date.now(),
|
|
34
|
+
lastFed: Date.now(),
|
|
35
|
+
lastPlayed: Date.now(),
|
|
36
|
+
lastTick: Date.now(),
|
|
37
|
+
state: "idle",
|
|
38
|
+
stateUntil: 0,
|
|
39
|
+
stateReason: "",
|
|
40
|
+
}
|
|
41
|
+
await persistence.save(fresh)
|
|
42
|
+
setLastMtime(Date.now())
|
|
43
|
+
setState(fresh)
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
if (mt !== lastMtime()) {
|
|
47
|
+
setLastMtime(mt)
|
|
48
|
+
const onDisk = await persistence.load()
|
|
49
|
+
if (onDisk) {
|
|
50
|
+
const ticked = tickState(onDisk)
|
|
51
|
+
const derived = deriveState(ticked)
|
|
52
|
+
setState(derived)
|
|
53
|
+
// Persist derived state (so transient state changes survive)
|
|
54
|
+
persistence.save(derived).catch(() => {})
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
refresh()
|
|
60
|
+
const refreshTimer = setInterval(refresh, REFRESH_TICK_MS)
|
|
61
|
+
onCleanup(() => clearInterval(refreshTimer))
|
|
62
|
+
|
|
63
|
+
const animTimer = setInterval(() => {
|
|
64
|
+
setFrame((f) => f + 1)
|
|
65
|
+
}, ANIMATION_TICK_MS)
|
|
66
|
+
onCleanup(() => clearInterval(animTimer))
|
|
67
|
+
|
|
68
|
+
const s = () => state()
|
|
69
|
+
const species = () => s()?.species ?? "duck"
|
|
70
|
+
const buddyState = () => s()?.state ?? "idle"
|
|
71
|
+
const fc = createMemo(() => frameCount(species(), buddyState()))
|
|
72
|
+
const lines = createMemo(() => renderFrame(species(), buddyState(), frame() % Math.max(1, fc())))
|
|
73
|
+
|
|
74
|
+
const bar = (v: number, color: any) => {
|
|
75
|
+
const filled = Math.round((v / 100) * 10)
|
|
76
|
+
return (
|
|
77
|
+
<text>
|
|
78
|
+
<span style={{ fg: theme().success }}>{"█".repeat(filled)}</span>
|
|
79
|
+
<span style={{ fg: theme().borderSubtle }}>{"░".repeat(10 - filled)}</span>
|
|
80
|
+
</text>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<Show when={s()}>
|
|
86
|
+
<box flexDirection="column" paddingTop={1} paddingBottom={1}>
|
|
87
|
+
<text fg={theme().accent}>
|
|
88
|
+
<b> {s()!.name} the {species()}</b>
|
|
89
|
+
</text>
|
|
90
|
+
<For each={lines()}>
|
|
91
|
+
{(line) => (
|
|
92
|
+
<text>
|
|
93
|
+
{" "}
|
|
94
|
+
{line}
|
|
95
|
+
</text>
|
|
96
|
+
)}
|
|
97
|
+
</For>
|
|
98
|
+
<text fg={theme().textMuted}> {"─".repeat(20)}</text>
|
|
99
|
+
<text>
|
|
100
|
+
{" hunger "}
|
|
101
|
+
{bar(s()!.hunger, theme().success)}
|
|
102
|
+
{" "}
|
|
103
|
+
{Math.floor(s()!.hunger)}
|
|
104
|
+
</text>
|
|
105
|
+
<text>
|
|
106
|
+
{" happy "}
|
|
107
|
+
{bar(s()!.happiness, theme().accent)}
|
|
108
|
+
{" "}
|
|
109
|
+
{Math.floor(s()!.happiness)}
|
|
110
|
+
</text>
|
|
111
|
+
<text>
|
|
112
|
+
{" energy "}
|
|
113
|
+
{bar(s()!.energy, theme().info)}
|
|
114
|
+
{" "}
|
|
115
|
+
{Math.floor(s()!.energy)}
|
|
116
|
+
</text>
|
|
117
|
+
<text fg={theme().textMuted}>
|
|
118
|
+
{" "}
|
|
119
|
+
{buddyState()} · Lv {s()!.level} · xp {s()!.xp}/{s()!.level * 50}
|
|
120
|
+
</text>
|
|
121
|
+
</box>
|
|
122
|
+
</Show>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const tui: TuiPlugin = async (api) => {
|
|
127
|
+
// Register the slot. `api` is captured in the closure so the slot
|
|
128
|
+
// function can pass it to the Solid component.
|
|
129
|
+
api.slots.register({
|
|
130
|
+
order: 500,
|
|
131
|
+
slots: {
|
|
132
|
+
sidebar_content(_ctx, props) {
|
|
133
|
+
return <View api={api} session_id={props.session_id} />
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
} as TuiSlotPlugin)
|
|
137
|
+
|
|
138
|
+
// React to opencode session events
|
|
139
|
+
api.event.on("session.idle", async () => {
|
|
140
|
+
const s = await persistence.load()
|
|
141
|
+
if (!s) return
|
|
142
|
+
await persistence.save(celebrateState(s))
|
|
143
|
+
})
|
|
144
|
+
api.event.on("session.error", async () => {
|
|
145
|
+
const s = await persistence.load()
|
|
146
|
+
if (!s) return
|
|
147
|
+
await persistence.save(scaredState(s))
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const plugin: TuiPluginModule & { id: string } = {
|
|
152
|
+
id: "opencode-buddy",
|
|
153
|
+
tui,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export default plugin
|
package/bin/opencode-buddy.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// CLI entry point. Routes to either the headless command runner or the
|
|
3
|
-
// interactive TUI renderer.
|
|
4
|
-
|
|
5
|
-
import { runInteractive, runCommand } from "../src/index.js";
|
|
6
|
-
import { install, uninstall } from "../src/install.js";
|
|
7
|
-
|
|
8
|
-
const argv = process.argv.slice(2);
|
|
9
|
-
const [sub, ...rest] = argv;
|
|
10
|
-
|
|
11
|
-
async function main() {
|
|
12
|
-
if (!sub || sub === "help" || sub === "--help" || sub === "-h") {
|
|
13
|
-
await runCommand([]);
|
|
14
|
-
return;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
if (sub === "start") {
|
|
18
|
-
await runInteractive({ autoSplit: true });
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
if (sub === "render") {
|
|
23
|
-
// Re-entered inside the tmux side pane by `start`. Just render.
|
|
24
|
-
await runInteractive({ autoSplit: false });
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (sub === "install") {
|
|
29
|
-
await install();
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (sub === "uninstall") {
|
|
34
|
-
await uninstall();
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Everything else: headless command
|
|
39
|
-
await runCommand(argv);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
main().catch((err) => {
|
|
43
|
-
console.error(`opencode-buddy: ${err.message || err}`);
|
|
44
|
-
process.exit(1);
|
|
45
|
-
});
|
package/src/buddy/species.js
DELETED
|
@@ -1,399 +0,0 @@
|
|
|
1
|
-
// Species library: 6 ASCII animals × multiple states × animation frames.
|
|
2
|
-
//
|
|
3
|
-
// Frame format: array of strings, each string is one row. All frames in a
|
|
4
|
-
// state are the same width/height, so the renderer can swap them at a fixed
|
|
5
|
-
// cadence without resizing the canvas.
|
|
6
|
-
|
|
7
|
-
const RESET = "\x1b[0m";
|
|
8
|
-
|
|
9
|
-
const C = {
|
|
10
|
-
reset: RESET,
|
|
11
|
-
dim: "\x1b[2m",
|
|
12
|
-
bold: "\x1b[1m",
|
|
13
|
-
red: "\x1b[31m",
|
|
14
|
-
green: "\x1b[32m",
|
|
15
|
-
yellow: "\x1b[33m",
|
|
16
|
-
blue: "\x1b[34m",
|
|
17
|
-
magenta: "\x1b[35m",
|
|
18
|
-
cyan: "\x1b[36m",
|
|
19
|
-
gray: "\x1b[90m",
|
|
20
|
-
brightRed: "\x1b[91m",
|
|
21
|
-
brightGreen: "\x1b[92m",
|
|
22
|
-
brightYellow: "\x1b[93m",
|
|
23
|
-
brightBlue: "\x1b[94m",
|
|
24
|
-
brightMagenta: "\x1b[95m",
|
|
25
|
-
brightCyan: "\x1b[96m",
|
|
26
|
-
bgYellow: "\x1b[43m",
|
|
27
|
-
bgRed: "\x1b[41m",
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
// Each frame is a multiline string. Keep them all the same dimensions
|
|
31
|
-
// across states of the same species so the canvas doesn't jitter.
|
|
32
|
-
const RAW = {
|
|
33
|
-
duck: {
|
|
34
|
-
idle: [
|
|
35
|
-
` __ `,
|
|
36
|
-
` <(o )___ `,
|
|
37
|
-
` ( ._> / `,
|
|
38
|
-
` \`--' `,
|
|
39
|
-
` `,
|
|
40
|
-
` ~ idle ~ `,
|
|
41
|
-
],
|
|
42
|
-
working: [
|
|
43
|
-
` __ `,
|
|
44
|
-
` <(o )___ `,
|
|
45
|
-
` ( ._> / `,
|
|
46
|
-
` *\`--'* .. .. `,
|
|
47
|
-
` / / `,
|
|
48
|
-
` coding... `,
|
|
49
|
-
],
|
|
50
|
-
celebrating: [
|
|
51
|
-
` \\o/ `,
|
|
52
|
-
` <(o )___ ! `,
|
|
53
|
-
` ( ._> / `,
|
|
54
|
-
` \`--' \\o/ `,
|
|
55
|
-
` `,
|
|
56
|
-
` \\o/ nice! `,
|
|
57
|
-
],
|
|
58
|
-
scared: [
|
|
59
|
-
` __ `,
|
|
60
|
-
` <(O )___ `,
|
|
61
|
-
` ( ._> / `,
|
|
62
|
-
` \`--' !! `,
|
|
63
|
-
` `,
|
|
64
|
-
` !! oh no `,
|
|
65
|
-
],
|
|
66
|
-
sleeping: [
|
|
67
|
-
` __ `,
|
|
68
|
-
` <(o )___ `,
|
|
69
|
-
` ( -_> z `,
|
|
70
|
-
` \`--' z `,
|
|
71
|
-
` `,
|
|
72
|
-
` z z z z z `,
|
|
73
|
-
],
|
|
74
|
-
},
|
|
75
|
-
|
|
76
|
-
cat: {
|
|
77
|
-
idle: [
|
|
78
|
-
` /\\_/\\ `,
|
|
79
|
-
` ( o.o ) `,
|
|
80
|
-
` > ^ < `,
|
|
81
|
-
` /| |\\ `,
|
|
82
|
-
` (_| |_) `,
|
|
83
|
-
` meow `,
|
|
84
|
-
],
|
|
85
|
-
working: [
|
|
86
|
-
` /\\_/\\ ... `,
|
|
87
|
-
` ( o.o ) `,
|
|
88
|
-
` > ^ < `,
|
|
89
|
-
` /| |\\ .. `,
|
|
90
|
-
` (_| |_) `,
|
|
91
|
-
` typing... `,
|
|
92
|
-
],
|
|
93
|
-
celebrating: [
|
|
94
|
-
` /\\_/\\ `,
|
|
95
|
-
` ( ^.^ ) \\o/ `,
|
|
96
|
-
` > ~ < `,
|
|
97
|
-
` /| |\\ \\o/ `,
|
|
98
|
-
` (_| |_) `,
|
|
99
|
-
` purrrfect! `,
|
|
100
|
-
],
|
|
101
|
-
scared: [
|
|
102
|
-
` /\\_/\\ !! `,
|
|
103
|
-
` ( O.O ) `,
|
|
104
|
-
` > ^ < `,
|
|
105
|
-
` /| |\\ `,
|
|
106
|
-
` (_| |_) /\\ `,
|
|
107
|
-
` !! hiss `,
|
|
108
|
-
],
|
|
109
|
-
sleeping: [
|
|
110
|
-
` /\\_/\\ `,
|
|
111
|
-
` ( -.- ) z `,
|
|
112
|
-
` > ^ < `,
|
|
113
|
-
` /| |\\ z `,
|
|
114
|
-
` (_| |_) `,
|
|
115
|
-
` z z z z z `,
|
|
116
|
-
],
|
|
117
|
-
},
|
|
118
|
-
|
|
119
|
-
dragon: {
|
|
120
|
-
idle: [
|
|
121
|
-
` /\\_/\\ `,
|
|
122
|
-
` ( o o ) ~~ `,
|
|
123
|
-
` > ^ < / `,
|
|
124
|
-
` /| |\\ `,
|
|
125
|
-
` (_| |_) `,
|
|
126
|
-
` rawr `,
|
|
127
|
-
],
|
|
128
|
-
working: [
|
|
129
|
-
` /\\_/\\ ~~~ `,
|
|
130
|
-
` ( o o ) ~~ `,
|
|
131
|
-
` > ^ < `,
|
|
132
|
-
` /| |\\ ~~ `,
|
|
133
|
-
` (_| |_) `,
|
|
134
|
-
` forging... `,
|
|
135
|
-
],
|
|
136
|
-
celebrating: [
|
|
137
|
-
` /\\^/\\ \\o/ `,
|
|
138
|
-
` ( ^.^ ) `,
|
|
139
|
-
` > ~ < \\o/ `,
|
|
140
|
-
` /| |\\ `,
|
|
141
|
-
` (_| |_) `,
|
|
142
|
-
` +50 xp!! \\o/ `,
|
|
143
|
-
],
|
|
144
|
-
scared: [
|
|
145
|
-
` /\\_/\\ !! `,
|
|
146
|
-
` ( O O ) `,
|
|
147
|
-
` > ^ < `,
|
|
148
|
-
` /| |\\ !! `,
|
|
149
|
-
` (_| |_) `,
|
|
150
|
-
` !! yikes `,
|
|
151
|
-
],
|
|
152
|
-
sleeping: [
|
|
153
|
-
` /\\_/\\ `,
|
|
154
|
-
` ( - - ) z `,
|
|
155
|
-
` > ^ < `,
|
|
156
|
-
` /| |\\ z `,
|
|
157
|
-
` (_| |_) `,
|
|
158
|
-
` z z z z z `,
|
|
159
|
-
],
|
|
160
|
-
},
|
|
161
|
-
|
|
162
|
-
axolotl: {
|
|
163
|
-
idle: [
|
|
164
|
-
` ^___^ `,
|
|
165
|
-
` (o . o) `,
|
|
166
|
-
` \\|_|_|/ `,
|
|
167
|
-
` \\| |/ `,
|
|
168
|
-
` ) ( `,
|
|
169
|
-
` ~ ambien `,
|
|
170
|
-
],
|
|
171
|
-
working: [
|
|
172
|
-
` ^___^ .. `,
|
|
173
|
-
` (o . o) `,
|
|
174
|
-
` \\|_|_|/ .. `,
|
|
175
|
-
` \\| |/ `,
|
|
176
|
-
` ) ( `,
|
|
177
|
-
` swimming... `,
|
|
178
|
-
],
|
|
179
|
-
celebrating: [
|
|
180
|
-
` ^___^ \\o/ `,
|
|
181
|
-
` (o ^ o) `,
|
|
182
|
-
` \\|_|_|/ \\o/ `,
|
|
183
|
-
` \\| |/ `,
|
|
184
|
-
` ) ( `,
|
|
185
|
-
` \\o/ splish! `,
|
|
186
|
-
],
|
|
187
|
-
scared: [
|
|
188
|
-
` ^___^ !! `,
|
|
189
|
-
` (O . O) `,
|
|
190
|
-
` \\|_|_|/ `,
|
|
191
|
-
` \\| |/ !! `,
|
|
192
|
-
` ) ( `,
|
|
193
|
-
` !! gulp `,
|
|
194
|
-
],
|
|
195
|
-
sleeping: [
|
|
196
|
-
` ^___^ `,
|
|
197
|
-
` (o - o) z `,
|
|
198
|
-
` \\|_|_|/ `,
|
|
199
|
-
` \\| |/ z `,
|
|
200
|
-
` ) ( `,
|
|
201
|
-
` z z z z z `,
|
|
202
|
-
],
|
|
203
|
-
},
|
|
204
|
-
|
|
205
|
-
robot: {
|
|
206
|
-
idle: [
|
|
207
|
-
` [ O . O ] `,
|
|
208
|
-
` /|#####|\\ `,
|
|
209
|
-
` / |#####| \\ `,
|
|
210
|
-
` | | `,
|
|
211
|
-
` /| | | |\\ `,
|
|
212
|
-
` beep `,
|
|
213
|
-
],
|
|
214
|
-
working: [
|
|
215
|
-
` [ O . O ] ... `,
|
|
216
|
-
` /|#####|\\ `,
|
|
217
|
-
` / |#####| \\ .. `,
|
|
218
|
-
` | | `,
|
|
219
|
-
` /| | | |\\ `,
|
|
220
|
-
` compiling... `,
|
|
221
|
-
],
|
|
222
|
-
celebrating: [
|
|
223
|
-
` [ ^ . ^ ] \\o/ `,
|
|
224
|
-
` /|#####|\\ `,
|
|
225
|
-
` / |#####| \\ `,
|
|
226
|
-
` | | `,
|
|
227
|
-
` /| | | |\\ \\o/ `,
|
|
228
|
-
` pass tests! `,
|
|
229
|
-
],
|
|
230
|
-
scared: [
|
|
231
|
-
` [ O . O ] !! `,
|
|
232
|
-
` /|#####|\\ `,
|
|
233
|
-
` / |#####| \\ `,
|
|
234
|
-
` | | !! `,
|
|
235
|
-
` /| | | |\\ `,
|
|
236
|
-
` !! segfault `,
|
|
237
|
-
],
|
|
238
|
-
sleeping: [
|
|
239
|
-
` [ - . - ] z `,
|
|
240
|
-
` /|#####|\\ `,
|
|
241
|
-
` / |#####| \\ z `,
|
|
242
|
-
` | | `,
|
|
243
|
-
` /| | | |\\ `,
|
|
244
|
-
` z z z z z `,
|
|
245
|
-
],
|
|
246
|
-
},
|
|
247
|
-
|
|
248
|
-
ghost: {
|
|
249
|
-
idle: [
|
|
250
|
-
` .-\"\"-. `,
|
|
251
|
-
` ( o . o ) `,
|
|
252
|
-
` | ~ ~ | `,
|
|
253
|
-
` | | `,
|
|
254
|
-
` \\uuuuu/ `,
|
|
255
|
-
` boo `,
|
|
256
|
-
],
|
|
257
|
-
working: [
|
|
258
|
-
` .-\"\"-. ... `,
|
|
259
|
-
` ( o . o ) `,
|
|
260
|
-
` | ~ ~ | `,
|
|
261
|
-
` | | `,
|
|
262
|
-
` \\uuuuu/ `,
|
|
263
|
-
` haunting... `,
|
|
264
|
-
],
|
|
265
|
-
celebrating: [
|
|
266
|
-
` .-\"\"-. \\o/ `,
|
|
267
|
-
` ( ^ . ^ ) `,
|
|
268
|
-
` | ~ ~ | \\o/ `,
|
|
269
|
-
` | | `,
|
|
270
|
-
` \\uuuuu/ `,
|
|
271
|
-
` \\o/ spooky! `,
|
|
272
|
-
],
|
|
273
|
-
scared: [
|
|
274
|
-
` .-\"\"-. !! `,
|
|
275
|
-
` ( O . O ) `,
|
|
276
|
-
` | ~ ~ | `,
|
|
277
|
-
` | | !! `,
|
|
278
|
-
` \\uuuuu/ `,
|
|
279
|
-
` !! yeek `,
|
|
280
|
-
],
|
|
281
|
-
sleeping: [
|
|
282
|
-
` .-\"\"-. `,
|
|
283
|
-
` ( - . - ) z `,
|
|
284
|
-
` | ~ ~ | `,
|
|
285
|
-
` | | `,
|
|
286
|
-
` \\uuuuu/ z `,
|
|
287
|
-
` z z z z z `,
|
|
288
|
-
],
|
|
289
|
-
},
|
|
290
|
-
};
|
|
291
|
-
|
|
292
|
-
// Per-character color map. Each species has a function that takes a char
|
|
293
|
-
// from the raw frame and returns an ANSI-colored version. This way we
|
|
294
|
-
// can give ducks a yellow bill, axolotls pink gills, ghosts a translucent
|
|
295
|
-
// hue, etc.
|
|
296
|
-
const PALETTES = {
|
|
297
|
-
duck: (ch) => {
|
|
298
|
-
if (ch === "o" || ch === "O") return C.brightYellow + ch + RESET;
|
|
299
|
-
if (ch === "(" || ch === ")" || ch === "<" || ch === ">")
|
|
300
|
-
return C.yellow + ch + RESET;
|
|
301
|
-
if (ch === "_" || ch === "-") return C.yellow + ch + RESET;
|
|
302
|
-
if (ch === "." || ch === "*") return C.cyan + ch + RESET;
|
|
303
|
-
if (ch === "z") return C.gray + ch + RESET;
|
|
304
|
-
if (ch === "!") return C.brightYellow + ch + RESET;
|
|
305
|
-
if (ch === "~") return C.cyan + ch + RESET;
|
|
306
|
-
if (ch === " ") return ch;
|
|
307
|
-
return C.yellow + ch + RESET;
|
|
308
|
-
},
|
|
309
|
-
|
|
310
|
-
cat: (ch) => {
|
|
311
|
-
if (ch === "o" || ch === "O" || ch === "^") return C.brightGreen + ch + RESET;
|
|
312
|
-
if (ch === "!" || ch === "~") return C.brightMagenta + ch + RESET;
|
|
313
|
-
if (ch === "z") return C.gray + ch + RESET;
|
|
314
|
-
if (ch === ".") return C.gray + ch + RESET;
|
|
315
|
-
if (ch === " ") return ch;
|
|
316
|
-
return C.green + ch + RESET;
|
|
317
|
-
},
|
|
318
|
-
|
|
319
|
-
dragon: (ch) => {
|
|
320
|
-
if (ch === "o" || ch === "O" || ch === "^") return C.brightRed + ch + RESET;
|
|
321
|
-
if (ch === "~") return C.red + ch + RESET;
|
|
322
|
-
if (ch === "!" || ch === "o" || ch === "O") return C.brightYellow + ch + RESET;
|
|
323
|
-
if (ch === "z") return C.gray + ch + RESET;
|
|
324
|
-
if (ch === " ") return ch;
|
|
325
|
-
return C.green + ch + RESET;
|
|
326
|
-
},
|
|
327
|
-
|
|
328
|
-
axolotl: (ch) => {
|
|
329
|
-
if (ch === "o" || ch === "O") return C.brightMagenta + ch + RESET;
|
|
330
|
-
if (ch === "^" || ch === "_") return C.magenta + ch + RESET;
|
|
331
|
-
if (ch === "!" || ch === "o") return C.brightYellow + ch + RESET;
|
|
332
|
-
if (ch === "z") return C.gray + ch + RESET;
|
|
333
|
-
if (ch === ".") return C.cyan + ch + RESET;
|
|
334
|
-
if (ch === " ") return ch;
|
|
335
|
-
return C.magenta + ch + RESET;
|
|
336
|
-
},
|
|
337
|
-
|
|
338
|
-
robot: (ch) => {
|
|
339
|
-
if (ch === "O" || ch === "#") return C.brightCyan + ch + RESET;
|
|
340
|
-
if (ch === "o") return C.cyan + ch + RESET;
|
|
341
|
-
if (ch === ".") return C.gray + ch + RESET;
|
|
342
|
-
if (ch === "!" || ch === "o") return C.brightYellow + ch + RESET;
|
|
343
|
-
if (ch === "z") return C.gray + ch + RESET;
|
|
344
|
-
if (ch === " ") return ch;
|
|
345
|
-
return C.gray + ch + RESET;
|
|
346
|
-
},
|
|
347
|
-
|
|
348
|
-
ghost: (ch) => {
|
|
349
|
-
if (ch === "o" || ch === "O") return C.brightCyan + ch + RESET;
|
|
350
|
-
if (ch === "~") return C.cyan + ch + RESET;
|
|
351
|
-
if (ch === "!" || ch === "o") return C.brightMagenta + ch + RESET;
|
|
352
|
-
if (ch === "z") return C.gray + ch + RESET;
|
|
353
|
-
if (ch === ".") return C.gray + ch + RESET;
|
|
354
|
-
if (ch === " ") return ch;
|
|
355
|
-
return C.brightCyan + ch + RESET;
|
|
356
|
-
},
|
|
357
|
-
};
|
|
358
|
-
|
|
359
|
-
export const SPECIES = ["duck", "cat", "dragon", "axolotl", "robot", "ghost"];
|
|
360
|
-
|
|
361
|
-
export const STATES = ["idle", "working", "celebrating", "scared", "sleeping"];
|
|
362
|
-
|
|
363
|
-
// Random flavor line shown under the buddy in idle / sleeping.
|
|
364
|
-
export const FLAVOR = {
|
|
365
|
-
duck: ["quack.", "need code.", "swimming in semicolons"],
|
|
366
|
-
cat: ["purr.", "knock something off desk?", "yarn > threads"],
|
|
367
|
-
dragon: ["rawr.", "hoarding stack traces", "breathing fire on bugs"],
|
|
368
|
-
axolotl: ["ambien?", "regenerating tail...", "neotenic forever"],
|
|
369
|
-
robot: ["beep boop", "01010110", "needs oil"],
|
|
370
|
-
ghost: ["boo.", "haunting the stack", "passed on... to prod"],
|
|
371
|
-
};
|
|
372
|
-
|
|
373
|
-
function targetWidth() {
|
|
374
|
-
// All species share this width so the rendered canvas never resizes when
|
|
375
|
-
// a state changes. Each row is padded (or trimmed) to this length.
|
|
376
|
-
return 20;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
export function paint(species, state) {
|
|
380
|
-
const frame = RAW[species][state];
|
|
381
|
-
const palette = PALETTES[species];
|
|
382
|
-
const w = targetWidth();
|
|
383
|
-
return frame.map((row) => {
|
|
384
|
-
let out = "";
|
|
385
|
-
// Use [...row] to count grapheme-ish units. ASCII art is pure ASCII
|
|
386
|
-
// so spread over code units is fine and matches visual columns.
|
|
387
|
-
for (const ch of row) out += palette(ch);
|
|
388
|
-
if (out.length < w) out += " ".repeat(w - out.length);
|
|
389
|
-
return out;
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
export function frameHeight(species) {
|
|
394
|
-
return RAW[species].idle.length;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
export function frameWidth() {
|
|
398
|
-
return targetWidth();
|
|
399
|
-
}
|