@ramarivera/coding-buddy 0.4.0-alpha.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/.claude-plugin/marketplace.json +40 -0
- package/.claude-plugin/plugin.json +28 -0
- package/LICENSE +21 -0
- package/README.md +451 -0
- package/cli/backup.ts +336 -0
- package/cli/disable.ts +94 -0
- package/cli/doctor.ts +220 -0
- package/cli/hunt.ts +167 -0
- package/cli/index.ts +115 -0
- package/cli/install.ts +335 -0
- package/cli/pick.ts +492 -0
- package/cli/settings.ts +68 -0
- package/cli/show.ts +31 -0
- package/cli/test-statusline.sh +41 -0
- package/cli/test-statusline.ts +122 -0
- package/cli/uninstall.ts +110 -0
- package/cli/verify.ts +19 -0
- package/hooks/buddy-comment.sh +65 -0
- package/hooks/hooks.json +35 -0
- package/hooks/name-react.sh +176 -0
- package/hooks/react.sh +204 -0
- package/package.json +60 -0
- package/server/achievements.ts +445 -0
- package/server/art.ts +376 -0
- package/server/engine.ts +448 -0
- package/server/index.ts +774 -0
- package/server/reactions.ts +187 -0
- package/server/state.ts +409 -0
- package/skills/buddy/SKILL.md +59 -0
- package/statusline/buddy-status.sh +389 -0
package/server/art.ts
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ASCII art for all 18 buddy species
|
|
3
|
+
*
|
|
4
|
+
* Each species has 3 animation frames (idle variations).
|
|
5
|
+
* Each frame is 5 lines, ~12 chars wide.
|
|
6
|
+
* {E} is replaced with the eye character at render time.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Species, Eye, Hat, Rarity, StatName, BuddyBones } from "./engine.ts";
|
|
10
|
+
|
|
11
|
+
// ─── Species art: 3 frames × 5 lines each ──────────────────────────────────
|
|
12
|
+
|
|
13
|
+
export const SPECIES_ART: Record<Species, string[][]> = {
|
|
14
|
+
duck: [
|
|
15
|
+
[" ", " __ ", " <({E} )___ ", " ( ._> ", " `--' "],
|
|
16
|
+
[" ", " __ ", " <({E} )___ ", " ( ._> ", " `--'~ "],
|
|
17
|
+
[" ", " __ ", " <({E} )___ ", " ( .__> ", " `--' "],
|
|
18
|
+
],
|
|
19
|
+
goose: [
|
|
20
|
+
[" ", " ({E}> ", " || ", " _(__)_ ", " ^^^^ "],
|
|
21
|
+
[" ", " ({E}> ", " || ", " _(__)_ ", " ^^^^ "],
|
|
22
|
+
[" ", " ({E}>> ", " || ", " _(__)_ ", " ^^^^ "],
|
|
23
|
+
],
|
|
24
|
+
blob: [
|
|
25
|
+
[" ", " .----. ", " ( {E} {E} ) ", " ( ) ", " `----' "],
|
|
26
|
+
[" ", " .------. ", " ( {E} {E} ) ", " ( ) ", " `------' "],
|
|
27
|
+
[" ", " .--. ", " ({E} {E}) ", " ( ) ", " `--' "],
|
|
28
|
+
],
|
|
29
|
+
cat: [
|
|
30
|
+
[" ", " /\\_/\\ ", " ( {E} {E}) ", " ( \u03c9 ) ", " (\")_(\") "],
|
|
31
|
+
[" ", " /\\_/\\ ", " ( {E} {E}) ", " ( \u03c9 ) ", " (\")_(\")~ "],
|
|
32
|
+
[" ", " /\\-/\\ ", " ( {E} {E}) ", " ( \u03c9 ) ", " (\")_(\") "],
|
|
33
|
+
],
|
|
34
|
+
dragon: [
|
|
35
|
+
[" ", " /^\\ /^\\ ", " < {E} {E} > ", " ( ~~ ) ", " `-vvvv-' "],
|
|
36
|
+
[" ", " /^\\ /^\\ ", " < {E} {E} > ", " ( ) ", " `-vvvv-' "],
|
|
37
|
+
[" ~ ~ ", " /^\\ /^\\ ", " < {E} {E} > ", " ( ~~ ) ", " `-vvvv-' "],
|
|
38
|
+
],
|
|
39
|
+
octopus: [
|
|
40
|
+
[" ", " .----. ", " ( {E} {E} ) ", " (______) ", " /\\/\\/\\/\\ "],
|
|
41
|
+
[" ", " .----. ", " ( {E} {E} ) ", " (______) ", " \\/\\/\\/\\/ "],
|
|
42
|
+
[" o ", " .----. ", " ( {E} {E} ) ", " (______) ", " /\\/\\/\\/\\ "],
|
|
43
|
+
],
|
|
44
|
+
owl: [
|
|
45
|
+
[" ", " /\\ /\\ ", " (({E})({E})) ", " ( >< ) ", " `----' "],
|
|
46
|
+
[" ", " /\\ /\\ ", " (({E})({E})) ", " ( >< ) ", " .----. "],
|
|
47
|
+
[" ", " /\\ /\\ ", " (({E})(-)) ", " ( >< ) ", " `----' "],
|
|
48
|
+
],
|
|
49
|
+
penguin: [
|
|
50
|
+
[" ", " .---. ", " ({E}>{E}) ", " /( )\\ ", " `---' "],
|
|
51
|
+
[" ", " .---. ", " ({E}>{E}) ", " |( )| ", " `---' "],
|
|
52
|
+
[" .---. ", " ({E}>{E}) ", " /( )\\ ", " `---' ", " ~ ~ "],
|
|
53
|
+
],
|
|
54
|
+
turtle: [
|
|
55
|
+
[" ", " _,--._ ", " ( {E} {E} ) ", " /[______]\\ ", " `` `` "],
|
|
56
|
+
[" ", " _,--._ ", " ( {E} {E} ) ", " /[______]\\ ", " `` `` "],
|
|
57
|
+
[" ", " _,--._ ", " ( {E} {E} ) ", " /[======]\\ ", " `` `` "],
|
|
58
|
+
],
|
|
59
|
+
snail: [
|
|
60
|
+
[" ", " {E} .--. ", " \\ ( @ ) ", " \\_`--' ", " ~~~~~~~ "],
|
|
61
|
+
[" ", " {E} .--. ", " | ( @ ) ", " \\_`--' ", " ~~~~~~~ "],
|
|
62
|
+
[" ", " {E} .--. ", " \\ ( @ ) ", " \\_`--' ", " ~~~~~~ "],
|
|
63
|
+
],
|
|
64
|
+
ghost: [
|
|
65
|
+
[" ", " .----. ", " / {E} {E} \\ ", " | | ", " ~`~``~`~ "],
|
|
66
|
+
[" ", " .----. ", " / {E} {E} \\ ", " | | ", " `~`~~`~` "],
|
|
67
|
+
[" ~ ~ ", " .----. ", " / {E} {E} \\ ", " | | ", " ~~`~~`~~ "],
|
|
68
|
+
],
|
|
69
|
+
axolotl: [
|
|
70
|
+
[" ", "}~(______)~{", "}~({E} .. {E})~{", " ( .--. ) ", " (_/ \\_) "],
|
|
71
|
+
[" ", "~}(______){~", "~}({E} .. {E}){~", " ( .--. ) ", " (_/ \\_) "],
|
|
72
|
+
[" ", "}~(______)~{", "}~({E} .. {E})~{", " ( -- ) ", " ~_/ \\_~ "],
|
|
73
|
+
],
|
|
74
|
+
capybara: [
|
|
75
|
+
[" ", " n______n ", " ( {E} {E} ) ", " ( oo ) ", " `------' "],
|
|
76
|
+
[" ", " n______n ", " ( {E} {E} ) ", " ( Oo ) ", " `------' "],
|
|
77
|
+
[" ~ ~ ", " u______n ", " ( {E} {E} ) ", " ( oo ) ", " `------' "],
|
|
78
|
+
],
|
|
79
|
+
cactus: [
|
|
80
|
+
[" ", " n ____ n ", " | |{E} {E}| | ", " |_| |_| ", " | | "],
|
|
81
|
+
[" ", " ____ ", " n |{E} {E}| n ", " |_| |_| ", " | | "],
|
|
82
|
+
[" n n ", " | ____ | ", " | |{E} {E}| | ", " |_| |_| ", " | | "],
|
|
83
|
+
],
|
|
84
|
+
robot: [
|
|
85
|
+
[" ", " .[||]. ", " [ {E} {E} ] ", " [ ==== ] ", " `------' "],
|
|
86
|
+
[" ", " .[||]. ", " [ {E} {E} ] ", " [ -==- ] ", " `------' "],
|
|
87
|
+
[" * ", " .[||]. ", " [ {E} {E} ] ", " [ ==== ] ", " `------' "],
|
|
88
|
+
],
|
|
89
|
+
rabbit: [
|
|
90
|
+
[" ", " (\\__/) ", " ( {E} {E} ) ", " =( .. )= ", " (\")__(\")" ],
|
|
91
|
+
[" ", " (|__/) ", " ( {E} {E} ) ", " =( .. )= ", " (\")__(\")" ],
|
|
92
|
+
[" ", " (\\__/) ", " ( {E} {E} ) ", " =( . . )= ", " (\")__(\")" ],
|
|
93
|
+
],
|
|
94
|
+
mushroom: [
|
|
95
|
+
[" ", " .-o-OO-o-. ", "(__________)"," |{E} {E}| ", " |____| "],
|
|
96
|
+
[" ", " .-O-oo-O-. ", "(__________)"," |{E} {E}| ", " |____| "],
|
|
97
|
+
[" . o . ", " .-o-OO-o-. ", "(__________)"," |{E} {E}| ", " |____| "],
|
|
98
|
+
],
|
|
99
|
+
chonk: [
|
|
100
|
+
[" ", " /\\ /\\ ", " ( {E} {E} ) ", " ( .. ) ", " `------' "],
|
|
101
|
+
[" ", " /\\ /| ", " ( {E} {E} ) ", " ( .. ) ", " `------' "],
|
|
102
|
+
[" ", " /\\ /\\ ", " ( {E} {E} ) ", " ( .. ) ", " `------'~ "],
|
|
103
|
+
],
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// ─── Hat art ────────────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
export const HAT_ART: Record<Hat, string> = {
|
|
109
|
+
none: "",
|
|
110
|
+
crown: " \\^^^/ ",
|
|
111
|
+
tophat: " [___] ",
|
|
112
|
+
propeller: " -+- ",
|
|
113
|
+
halo: " ( ) ",
|
|
114
|
+
wizard: " /^\\ ",
|
|
115
|
+
beanie: " (___) ",
|
|
116
|
+
tinyduck: " ,> ",
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// ─── Rarity ANSI colors ────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
const RARITY_COLOR: Record<Rarity, string> = {
|
|
122
|
+
common: "\x1b[38;2;153;153;153m", // inactive rgb(153,153,153)
|
|
123
|
+
uncommon: "\x1b[38;2;78;186;101m", // success rgb(78,186,101)
|
|
124
|
+
rare: "\x1b[38;2;177;185;249m", // permission rgb(177,185,249)
|
|
125
|
+
epic: "\x1b[38;2;175;135;255m", // autoAccept rgb(175,135,255)
|
|
126
|
+
legendary: "\x1b[38;2;255;193;7m", // warning rgb(255,193,7)
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const SHINY_COLOR = "\x1b[93m"; // bright yellow
|
|
130
|
+
const BOLD = "\x1b[1m";
|
|
131
|
+
const DIM = "\x1b[2m";
|
|
132
|
+
const NC = "\x1b[0m";
|
|
133
|
+
|
|
134
|
+
export const RARITY_STARS: Record<Rarity, string> = {
|
|
135
|
+
common: "\u2605",
|
|
136
|
+
uncommon: "\u2605\u2605",
|
|
137
|
+
rare: "\u2605\u2605\u2605",
|
|
138
|
+
epic: "\u2605\u2605\u2605\u2605",
|
|
139
|
+
legendary: "\u2605\u2605\u2605\u2605\u2605",
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// ─── Display width helpers ──────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
function stripAnsi(s: string): string { return s.replace(/\x1b\[[^m]*m/g, ""); }
|
|
145
|
+
|
|
146
|
+
function charWidth(cp: number): number {
|
|
147
|
+
if (cp >= 0xFE00 && cp <= 0xFE0F) return 0;
|
|
148
|
+
if (cp === 0x200D) return 0;
|
|
149
|
+
if (cp >= 0x1F000) return 2;
|
|
150
|
+
if (cp === 0x2728) return 2; // ✨
|
|
151
|
+
if (cp >= 0x2600 && cp <= 0x27BF) return 1;
|
|
152
|
+
if (cp >= 0x2500 && cp <= 0x259F) return 1;
|
|
153
|
+
if (cp >= 0x3000 && cp <= 0x9FFF) return 2;
|
|
154
|
+
if (cp >= 0xFF01 && cp <= 0xFF60) return 2;
|
|
155
|
+
return 1;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function displayWidth(s: string): number {
|
|
159
|
+
let w = 0;
|
|
160
|
+
for (const ch of stripAnsi(s)) w += charWidth(ch.codePointAt(0)!);
|
|
161
|
+
return w;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Pad string with spaces to reach target display width */
|
|
165
|
+
function dpad(s: string, targetW: number): string {
|
|
166
|
+
const w = displayWidth(s);
|
|
167
|
+
return w < targetW ? s + " ".repeat(targetW - w) : s;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ─── Render functions ───────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
export function getArtFrame(species: Species, eye: Eye, frame: number = 0): string[] {
|
|
173
|
+
const frames = SPECIES_ART[species];
|
|
174
|
+
const f = frames[frame % frames.length];
|
|
175
|
+
return f.map((line) => line.replace(/\{E\}/g, eye));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function renderCompanionCard(
|
|
179
|
+
bones: BuddyBones,
|
|
180
|
+
name: string,
|
|
181
|
+
personality: string,
|
|
182
|
+
reaction?: string,
|
|
183
|
+
frame: number = 0,
|
|
184
|
+
width: number = 40,
|
|
185
|
+
): string {
|
|
186
|
+
const color = RARITY_COLOR[bones.rarity];
|
|
187
|
+
const stars = RARITY_STARS[bones.rarity];
|
|
188
|
+
const shiny = bones.shiny ? `${SHINY_COLOR}\u2728 ${NC}` : "";
|
|
189
|
+
const art = getArtFrame(bones.species, bones.eye, frame);
|
|
190
|
+
|
|
191
|
+
// Hat: replace first empty art line
|
|
192
|
+
const hatLine = HAT_ART[bones.hat];
|
|
193
|
+
if (hatLine && !art[0].trim()) {
|
|
194
|
+
art[0] = hatLine;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Build the card
|
|
198
|
+
const W = Math.max(24, width);
|
|
199
|
+
const hr = "\u2500".repeat(W - 2);
|
|
200
|
+
const lines: string[] = [];
|
|
201
|
+
|
|
202
|
+
// Top border
|
|
203
|
+
lines.push(`${color}\u256d${hr}\u256e${NC}`);
|
|
204
|
+
|
|
205
|
+
// Inner width = W - 2 (borders), content area = W - 4 (borders + padding)
|
|
206
|
+
const innerW = W - 4;
|
|
207
|
+
|
|
208
|
+
// Species art (centered)
|
|
209
|
+
for (const artLine of art) {
|
|
210
|
+
if (!artLine.trim()) continue;
|
|
211
|
+
lines.push(`${color}\u2502${NC} ${dpad(artLine, innerW)}${color}\u2502${NC}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Separator
|
|
215
|
+
lines.push(`${color}\u251c${"╌".repeat(W - 2)}\u2524${NC}`);
|
|
216
|
+
|
|
217
|
+
// Name + rarity
|
|
218
|
+
const nameStarsRaw = `${BOLD}${name}${NC} ${color}${stars}${NC}`;
|
|
219
|
+
lines.push(`${color}\u2502${NC} ${nameStarsRaw}${" ".repeat(Math.max(0, innerW - displayWidth(name) - 2 - displayWidth(stars)))}${color}\u2502${NC}`);
|
|
220
|
+
|
|
221
|
+
const rarityRaw = `${shiny}${color}${BOLD}${bones.rarity.toUpperCase()}${NC} ${bones.species}`;
|
|
222
|
+
const rarityVis = (bones.shiny ? 3 : 0) + bones.rarity.length + 1 + bones.species.length;
|
|
223
|
+
lines.push(`${color}\u2502${NC} ${rarityRaw}${" ".repeat(Math.max(0, innerW - rarityVis))}${color}\u2502${NC}`);
|
|
224
|
+
|
|
225
|
+
// Eye + Hat info
|
|
226
|
+
const cosmeticLine = `eye: ${bones.eye} hat: ${bones.hat}`;
|
|
227
|
+
lines.push(`${color}\u2502${NC} ${DIM}${cosmeticLine}${NC}${" ".repeat(Math.max(0, innerW - displayWidth(cosmeticLine)))}${color}\u2502${NC}`);
|
|
228
|
+
|
|
229
|
+
// Separator
|
|
230
|
+
lines.push(`${color}\u251c${"╌".repeat(W - 2)}\u2524${NC}`);
|
|
231
|
+
|
|
232
|
+
// Stats
|
|
233
|
+
const STAT_NAMES: StatName[] = ["DEBUGGING", "PATIENCE", "CHAOS", "WISDOM", "SNARK"];
|
|
234
|
+
for (const stat of STAT_NAMES) {
|
|
235
|
+
const val = bones.stats[stat];
|
|
236
|
+
const filled = Math.round(val / 10);
|
|
237
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(10 - filled);
|
|
238
|
+
const label = stat.slice(0, 3).padEnd(3);
|
|
239
|
+
const marker = stat === bones.peak ? " \u25b2" : stat === bones.dump ? " \u25bc" : " ";
|
|
240
|
+
const valStr = String(val).padStart(3);
|
|
241
|
+
const statLine = `${DIM}${label}${NC} ${bar} ${valStr}${marker}`;
|
|
242
|
+
const statVis = 3 + 1 + 10 + 1 + 3 + 2; // label + spaces + bar + val + marker = 20
|
|
243
|
+
lines.push(`${color}\u2502${NC} ${statLine}${" ".repeat(Math.max(0, innerW - statVis))}${color}\u2502${NC}`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Speech bubble (if reaction)
|
|
247
|
+
if (reaction) {
|
|
248
|
+
lines.push(`${color}\u251c${"╌".repeat(W - 2)}\u2524${NC}`);
|
|
249
|
+
const maxMsg = innerW - 3; // "💬 " prefix (💬 = 2 cols + space)
|
|
250
|
+
const msg = displayWidth(reaction) > maxMsg ? reaction.slice(0, maxMsg - 1) + "\u2026" : reaction;
|
|
251
|
+
const msgPad = Math.max(0, innerW - displayWidth(msg) - 3);
|
|
252
|
+
lines.push(`${color}\u2502${NC} \ud83d\udcac ${msg}${" ".repeat(msgPad)}${color}\u2502${NC}`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Personality
|
|
256
|
+
if (personality) {
|
|
257
|
+
lines.push(`${color}\u251c${"╌".repeat(W - 2)}\u2524${NC}`);
|
|
258
|
+
const words = personality.split(" ");
|
|
259
|
+
let line = "";
|
|
260
|
+
for (const word of words) {
|
|
261
|
+
if (displayWidth(line) + displayWidth(word) + 1 > innerW) {
|
|
262
|
+
lines.push(`${color}\u2502${NC} ${DIM}${dpad(line, innerW)}${NC}${color}\u2502${NC}`);
|
|
263
|
+
line = word;
|
|
264
|
+
} else {
|
|
265
|
+
line = line ? `${line} ${word}` : word;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (line) {
|
|
269
|
+
lines.push(`${color}\u2502${NC} ${DIM}${dpad(line, innerW)}${NC}${color}\u2502${NC}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Bottom border
|
|
274
|
+
lines.push(`${color}\u2570${hr}\u256f${NC}`);
|
|
275
|
+
|
|
276
|
+
return lines.join("\n");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ─── Markdown-native render (for MCP tool responses) ───────────────────────
|
|
280
|
+
//
|
|
281
|
+
// Claude Code's UI doesn't render raw ANSI escape codes properly — it strips
|
|
282
|
+
// the ESC byte but leaves "[38;2;...m" as literal text, making the output
|
|
283
|
+
// unreadable. This renderer produces pure markdown with unicode rarity dots
|
|
284
|
+
// instead of ANSI colors, so it renders cleanly in any MCP client UI.
|
|
285
|
+
|
|
286
|
+
const RARITY_DOT: Record<Rarity, string> = {
|
|
287
|
+
common: "\u26AA", // ⚪ white circle
|
|
288
|
+
uncommon: "\uD83D\uDFE2", // 🟢 green circle
|
|
289
|
+
rare: "\uD83D\uDD35", // 🔵 blue circle
|
|
290
|
+
epic: "\uD83D\uDFE3", // 🟣 purple circle
|
|
291
|
+
legendary: "\uD83D\uDFE1", // 🟡 yellow circle
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
export function renderCompanionCardMarkdown(
|
|
295
|
+
bones: BuddyBones,
|
|
296
|
+
name: string,
|
|
297
|
+
personality: string,
|
|
298
|
+
reaction?: string,
|
|
299
|
+
frame: number = 0,
|
|
300
|
+
): string {
|
|
301
|
+
const dot = RARITY_DOT[bones.rarity];
|
|
302
|
+
const stars = RARITY_STARS[bones.rarity];
|
|
303
|
+
const shiny = bones.shiny ? " \u2728" : "";
|
|
304
|
+
const art = getArtFrame(bones.species, bones.eye, frame);
|
|
305
|
+
|
|
306
|
+
// Hat: replace first empty art line
|
|
307
|
+
const hatLine = HAT_ART[bones.hat];
|
|
308
|
+
if (hatLine && !art[0].trim()) {
|
|
309
|
+
art[0] = hatLine;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Strip empty lines from art for cleaner rendering
|
|
313
|
+
const artLines = art.filter((l) => l.trim().length > 0);
|
|
314
|
+
|
|
315
|
+
const STAT_NAMES: StatName[] = ["DEBUGGING", "PATIENCE", "CHAOS", "WISDOM", "SNARK"];
|
|
316
|
+
const statRows = STAT_NAMES.map((stat) => {
|
|
317
|
+
const val = bones.stats[stat];
|
|
318
|
+
const filled = Math.round(val / 10);
|
|
319
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(10 - filled);
|
|
320
|
+
const marker = stat === bones.peak ? " \u25B2" : stat === bones.dump ? " \u25BC" : "";
|
|
321
|
+
const label = `**${stat.slice(0, 3)}**${stat.slice(3)}`;
|
|
322
|
+
return `| ${label} | ${val}${marker} | \`${bar}\` |`;
|
|
323
|
+
}).join("\n");
|
|
324
|
+
|
|
325
|
+
const parts: string[] = [];
|
|
326
|
+
|
|
327
|
+
// Header: rarity dot, name, species+rarity, stars, shiny
|
|
328
|
+
parts.push(`### ${dot} ${name} · \`${bones.rarity.toUpperCase()} ${bones.species}\` · ${stars}${shiny}`);
|
|
329
|
+
parts.push("");
|
|
330
|
+
|
|
331
|
+
// ASCII art in a code block (preserves monospaced formatting)
|
|
332
|
+
parts.push("```");
|
|
333
|
+
parts.push(artLines.join("\n"));
|
|
334
|
+
parts.push("```");
|
|
335
|
+
parts.push("");
|
|
336
|
+
|
|
337
|
+
// Identity line
|
|
338
|
+
parts.push(`**Identity:** eye \`${bones.eye}\` · hat \`${bones.hat}\``);
|
|
339
|
+
parts.push("");
|
|
340
|
+
|
|
341
|
+
// Stats table
|
|
342
|
+
parts.push("| Stat | Value | Bar |");
|
|
343
|
+
parts.push("|---|---|---|");
|
|
344
|
+
parts.push(statRows);
|
|
345
|
+
parts.push("");
|
|
346
|
+
|
|
347
|
+
// Reaction (if any) — reactions often already contain asterisks
|
|
348
|
+
// for actions like "*blinks slowly*", so render them verbatim to avoid
|
|
349
|
+
// accidentally turning italics into bold.
|
|
350
|
+
if (reaction) {
|
|
351
|
+
parts.push(`\ud83d\udcac ${reaction}`);
|
|
352
|
+
parts.push("");
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Personality as blockquote
|
|
356
|
+
if (personality) {
|
|
357
|
+
parts.push(`> ${personality}`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return parts.join("\n");
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ─── Compact status line render ─────────────────────────────────────────────
|
|
364
|
+
|
|
365
|
+
export function renderStatusLine(
|
|
366
|
+
bones: BuddyBones,
|
|
367
|
+
name: string,
|
|
368
|
+
reaction?: string,
|
|
369
|
+
): string {
|
|
370
|
+
const face = SPECIES_ART[bones.species][0][2]?.replace(/\{E\}/g, bones.eye).trim() || "(?)";
|
|
371
|
+
const color = RARITY_COLOR[bones.rarity];
|
|
372
|
+
const stars = RARITY_STARS[bones.rarity];
|
|
373
|
+
const shiny = bones.shiny ? "\u2728" : "";
|
|
374
|
+
const msg = reaction ? ` \u2502 "${reaction}"` : "";
|
|
375
|
+
return `${color}${face}${NC} ${BOLD}${name}${NC} ${shiny}${color}${stars}${NC}${msg}`;
|
|
376
|
+
}
|