opencode-buddy 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/README.md +206 -0
- package/bin/opencode-buddy.js +45 -0
- package/package.json +61 -0
- package/src/buddy/species.js +399 -0
- package/src/buddy/state.js +161 -0
- package/src/index.js +302 -0
- package/src/install.js +157 -0
- package/src/persistence.js +28 -0
- package/src/plugin.js +178 -0
- package/src/tmux.js +90 -0
- package/src/tui.js +164 -0
|
@@ -0,0 +1,399 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// Buddy state machine + attribute bookkeeping.
|
|
2
|
+
//
|
|
3
|
+
// Attributes (all 0..100):
|
|
4
|
+
// hunger 100 = full, 0 = starving
|
|
5
|
+
// happiness 100 = delighted, 0 = grumpy
|
|
6
|
+
// energy 100 = wide awake, 0 = exhausted
|
|
7
|
+
//
|
|
8
|
+
// External events from opencode (via tmux pane polling or CLI notify):
|
|
9
|
+
// working -> state becomes 'working' (boosts happiness over time)
|
|
10
|
+
// done -> state becomes 'celebrating' for a few seconds
|
|
11
|
+
// error -> state becomes 'scared' for a few seconds
|
|
12
|
+
// idle -> state returns to 'idle' / 'sleeping' based on energy
|
|
13
|
+
//
|
|
14
|
+
// User actions:
|
|
15
|
+
// feed -> +25 hunger, -5 energy
|
|
16
|
+
// play -> +20 happiness, -10 energy, -5 hunger
|
|
17
|
+
// rest -> +30 energy
|
|
18
|
+
//
|
|
19
|
+
// Time decay: every minute, hunger -0.5, happiness -0.3, energy -0.2.
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_STATE = {
|
|
22
|
+
species: "duck",
|
|
23
|
+
name: "Quack",
|
|
24
|
+
hunger: 80,
|
|
25
|
+
happiness: 80,
|
|
26
|
+
energy: 100,
|
|
27
|
+
xp: 0,
|
|
28
|
+
level: 1,
|
|
29
|
+
hatchedAt: Date.now(),
|
|
30
|
+
lastFed: Date.now(),
|
|
31
|
+
lastPlayed: Date.now(),
|
|
32
|
+
lastTick: Date.now(),
|
|
33
|
+
// ephemeral, not persisted as state-of-truth
|
|
34
|
+
state: "idle",
|
|
35
|
+
stateUntil: 0,
|
|
36
|
+
stateReason: "",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const CLAMP = (n, lo = 0, hi = 100) => Math.max(lo, Math.min(hi, n));
|
|
40
|
+
|
|
41
|
+
export function tick(state, now = Date.now()) {
|
|
42
|
+
const minutes = (now - state.lastTick) / 60_000;
|
|
43
|
+
if (minutes <= 0) return state;
|
|
44
|
+
|
|
45
|
+
const next = {
|
|
46
|
+
...state,
|
|
47
|
+
hunger: CLAMP(state.hunger - 0.5 * minutes),
|
|
48
|
+
happiness: CLAMP(state.happiness - 0.3 * minutes),
|
|
49
|
+
energy: CLAMP(state.energy - 0.2 * minutes),
|
|
50
|
+
lastTick: now,
|
|
51
|
+
};
|
|
52
|
+
return next;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function feed(state) {
|
|
56
|
+
return {
|
|
57
|
+
...state,
|
|
58
|
+
hunger: CLAMP(state.hunger + 25),
|
|
59
|
+
energy: CLAMP(state.energy - 5),
|
|
60
|
+
lastFed: Date.now(),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function play(state) {
|
|
65
|
+
return {
|
|
66
|
+
...state,
|
|
67
|
+
happiness: CLAMP(state.happiness + 20),
|
|
68
|
+
energy: CLAMP(state.energy - 10),
|
|
69
|
+
hunger: CLAMP(state.hunger - 5),
|
|
70
|
+
lastPlayed: Date.now(),
|
|
71
|
+
xp: state.xp + 5,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function rest(state) {
|
|
76
|
+
return {
|
|
77
|
+
...state,
|
|
78
|
+
energy: CLAMP(state.energy + 30),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function rename(state, name) {
|
|
83
|
+
return { ...state, name: String(name).slice(0, 20) };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function switchSpecies(state, species) {
|
|
87
|
+
return { ...state, species };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function celebrate(state, durationMs = 4000) {
|
|
91
|
+
return {
|
|
92
|
+
...state,
|
|
93
|
+
happiness: CLAMP(state.happiness + 5),
|
|
94
|
+
state: "celebrating",
|
|
95
|
+
stateUntil: Date.now() + durationMs,
|
|
96
|
+
stateReason: "session idle",
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function scared(state, durationMs = 5000) {
|
|
101
|
+
return {
|
|
102
|
+
...state,
|
|
103
|
+
state: "scared",
|
|
104
|
+
stateUntil: Date.now() + durationMs,
|
|
105
|
+
stateReason: "session error",
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function working(state) {
|
|
110
|
+
// Don't overwrite a stronger transient state
|
|
111
|
+
if (state.state === "celebrating" && Date.now() < state.stateUntil)
|
|
112
|
+
return state;
|
|
113
|
+
if (state.state === "scared" && Date.now() < state.stateUntil) return state;
|
|
114
|
+
return { ...state, state: "working", stateUntil: 0, stateReason: "" };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Derive state purely from attributes when no external override is active.
|
|
118
|
+
export function deriveState(state, now = Date.now()) {
|
|
119
|
+
if (state.state === "celebrating" && now < state.stateUntil) return state;
|
|
120
|
+
if (state.state === "scared" && now < state.stateUntil) return state;
|
|
121
|
+
|
|
122
|
+
if (state.energy < 20) {
|
|
123
|
+
return { ...state, state: "sleeping", stateReason: "low energy" };
|
|
124
|
+
}
|
|
125
|
+
if (state.hunger < 25) {
|
|
126
|
+
return { ...state, state: "scared", stateReason: "hungry", stateUntil: now + 30_000 };
|
|
127
|
+
}
|
|
128
|
+
if (state.state === "working") return state;
|
|
129
|
+
return { ...state, state: "idle", stateReason: "" };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function maybeLevelUp(state) {
|
|
133
|
+
const xpNeeded = state.level * 50;
|
|
134
|
+
if (state.xp < xpNeeded) return state;
|
|
135
|
+
return {
|
|
136
|
+
...state,
|
|
137
|
+
level: state.level + 1,
|
|
138
|
+
xp: state.xp - xpNeeded,
|
|
139
|
+
happiness: CLAMP(state.happiness + 10),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function describe(state) {
|
|
144
|
+
const mood =
|
|
145
|
+
state.happiness > 80
|
|
146
|
+
? "happy"
|
|
147
|
+
: state.happiness > 50
|
|
148
|
+
? "okay"
|
|
149
|
+
: state.happiness > 25
|
|
150
|
+
? "grumpy"
|
|
151
|
+
: "depressed";
|
|
152
|
+
const tummy =
|
|
153
|
+
state.hunger > 70
|
|
154
|
+
? "full"
|
|
155
|
+
: state.hunger > 40
|
|
156
|
+
? "peckish"
|
|
157
|
+
: state.hunger > 20
|
|
158
|
+
? "hungry"
|
|
159
|
+
: "starving";
|
|
160
|
+
return `${state.name} the ${state.species} | Lv ${state.level} | ${mood} | ${tummy} | hunger ${Math.floor(state.hunger)} happiness ${Math.floor(state.happiness)} energy ${Math.floor(state.energy)} | xp ${state.xp}/${state.level * 50}`;
|
|
161
|
+
}
|