claude-buddy-reroll 1.9.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/display.mjs +309 -91
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-buddy-reroll",
3
- "version": "1.9.0",
3
+ "version": "2.0.0",
4
4
  "description": "Speaki-styled npm terminal for buddy rerolls, dex replay, and SALT patching.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/display.mjs CHANGED
@@ -1,91 +1,309 @@
1
- // Display utilities for buddy cards
2
-
3
- import { RARITY_STARS, STAT_FLOORS } from './engine.mjs';
4
- import { renderSprite } from './sprites.mjs';
5
- import { formatEye, formatShinyTag, formatStars, isAsciiOnlyTerminal, toTerminalSafeText } from './terminal.mjs';
6
- import { renderStatBars } from './stat-bars.mjs';
7
-
8
- const RARITY_STYLE = {
9
- common: { border: '─', color: '\x1b[37m', label: '\x1b[37m', badge: 'COMMON' },
10
- uncommon: { border: '═', color: '\x1b[32m', label: '\x1b[32m', badge: 'UNCOMMON' },
11
- rare: { border: '━', color: '\x1b[36m', label: '\x1b[36m', badge: 'RARE' },
12
- epic: { border: '▓', color: '\x1b[35m', label: '\x1b[35m', badge: 'EPIC' },
13
- legendary: { border: '█', color: '\x1b[33m', label: '\x1b[33;1m', badge: 'LEGENDARY' },
14
- };
15
-
16
- const ASCII_BORDER = {
17
- common: '-',
18
- uncommon: '=',
19
- rare: '=',
20
- epic: '#',
21
- legendary: '#',
22
- };
23
-
24
- const RESET = '\x1b[0m';
25
- const DIM = '\x1b[2m';
26
- const BOLD = '\x1b[1m';
27
- const GRAY = '\x1b[90m';
28
-
29
- function statBar(value, max = 100, color = '') {
30
- const width = 14;
31
- const filled = Math.round((value / max) * width);
32
- return `${color}${'█'.repeat(filled)}${GRAY}${'░'.repeat(width - filled)}${RESET} ${String(value).padStart(3)}`;
33
- }
34
-
35
- export function renderCard(result, { showSalt = false, index = null, frame = 0 } = {}) {
36
- const { bones, inspirationSeed } = result;
37
- const { rarity, species, eye, hat, shiny, stats } = bones;
38
- const style = RARITY_STYLE[rarity];
39
- const borderChar = isAsciiOnlyTerminal() ? ASCII_BORDER[rarity] : style.border;
40
- const stars = formatStars(RARITY_STARS[rarity]);
41
- const shinyTag = shiny ? formatShinyTag(' ✨ SHINY!') : '';
42
- const sprite = toTerminalSafeText(renderSprite(species, formatEye(eye), hat, frame));
43
- const rarityBadge = `${style.label}[ ${style.badge} ]${RESET}`;
44
-
45
- const header = index !== null ? `#${index + 1} ` : '';
46
- const saltLine = showSalt && result.salt ? `${DIM}salt: ${result.salt}${RESET}` : '';
47
-
48
- const lines = [
49
- '',
50
- `${style.color}${borderChar.repeat(36)}${RESET}`,
51
- ` ${rarityBadge}`,
52
- `${style.color} ${header}${BOLD}${species.toUpperCase()}${RESET}${style.color} ${stars}${shinyTag}${RESET}`,
53
- `${style.color}${borderChar.repeat(36)}${RESET}`,
54
- '',
55
- ...sprite.split('\n').map((line) => ` ${line}`),
56
- '',
57
- ` ${DIM}Eye:${RESET} ${formatEye(eye)} ${DIM}Hat:${RESET} ${toTerminalSafeText(hat === 'none' ? '(none)' : hat)}`,
58
- '',
59
- ` ${DIM}Stats:${RESET}`,
60
- ];
61
-
62
- const statBarsBlock = renderStatBars(stats, rarity);
63
- if (statBarsBlock) {
64
- for (const line of statBarsBlock.split('\n')) {
65
- lines.push(toTerminalSafeText(line));
66
- }
67
- }
68
-
69
- lines.push('');
70
- if (saltLine) lines.push(` ${saltLine}`);
71
- lines.push(`${style.color}${borderChar.repeat(36)}${RESET}`);
72
- lines.push('');
73
-
74
- return lines.join('\n');
75
- }
76
-
77
- export function renderMiniCard(result, index) {
78
- const { bones } = result;
79
- const { rarity, species, eye, shiny } = bones;
80
- const style = RARITY_STYLE[rarity];
81
- const stars = formatStars(RARITY_STARS[rarity]);
82
- const badge = style.badge.padEnd(9);
83
- const shinyTag = shiny
84
- ? (isAsciiOnlyTerminal() ? '[SHINY]' : formatShinyTag('✨'))
85
- : '';
86
-
87
- const idx = String(index + 1).padStart(2, ' ');
88
- return `${style.color}${idx}. ${formatEye(eye)} ${species.padEnd(10)} ${stars.padEnd(5)} ${badge} ${shinyTag.padEnd(8)}${RESET}`;
89
- }
90
-
91
- // Dex rendering is handled directly in cli.mjs
1
+ // Display utilities for buddy cards — v3: rarity = structure
2
+ // Each rarity tier has a fundamentally different card layout.
3
+
4
+ import { RARITY_STARS, STAT_FLOORS, STATS } from './engine.mjs';
5
+ import { renderSprite } from './sprites.mjs';
6
+ import { formatEye, formatShinyTag, formatStars, isAsciiOnlyTerminal, toTerminalSafeText } from './terminal.mjs';
7
+ import { padAnsiEnd, visibleLength } from './ansi.mjs';
8
+
9
+ const R = '\x1b[0m';
10
+ const BOLD = '\x1b[1m';
11
+ const DIM = '\x1b[2m';
12
+ const ITALIC = '\x1b[3m';
13
+ const GRAY = '\x1b[90m';
14
+
15
+ const RARITY = {
16
+ common: { color: '\x1b[90m', accent: '\x1b[37m', bg: '' },
17
+ uncommon: { color: '\x1b[32m', accent: '\x1b[32;1m', bg: '' },
18
+ rare: { color: '\x1b[34m', accent: '\x1b[34;1m', bg: '' },
19
+ epic: { color: '\x1b[35m', accent: '\x1b[35;1m', bg: '\x1b[48;5;53m' },
20
+ legendary: { color: '\x1b[33;1m', accent: '\x1b[33;1m', bg: '\x1b[48;5;58m' },
21
+ };
22
+
23
+ function statBar(value, color) {
24
+ const w = 12;
25
+ const filled = Math.round((value / 100) * w);
26
+ return `${color}${'█'.repeat(filled)}${GRAY}${'░'.repeat(w - filled)}${R}`;
27
+ }
28
+
29
+ function centerText(text, width) {
30
+ const vis = visibleLength(text);
31
+ if (vis >= width) return text;
32
+ const left = Math.floor((width - vis) / 2);
33
+ return ' '.repeat(left) + text;
34
+ }
35
+
36
+ // ─── COMMON: compact, no frame, disposable feel ─────────
37
+
38
+ function renderCommon(result, { index, frame }) {
39
+ const { bones } = result;
40
+ const { species, eye } = bones;
41
+ const c = RARITY.common;
42
+ const idx = index !== null ? `${GRAY}#${index + 1}${R} ` : '';
43
+
44
+ // Extract just the "face" — second or third line of sprite
45
+ const spriteLines = renderSprite(species, formatEye(eye), 'none', frame).split('\n');
46
+ const faceLine = spriteLines.find(l => l.includes(formatEye(eye))) || spriteLines[1] || '';
47
+
48
+ return [
49
+ '',
50
+ ` ${idx}${c.color}${species}${R} ${GRAY}${formatEye(eye)}${R}`,
51
+ ` ${c.color}${toTerminalSafeText(faceLine.trim())}${R}`,
52
+ '',
53
+ ].join('\n');
54
+ }
55
+
56
+ // ─── UNCOMMON: light frame, adds stats ──────────────────
57
+
58
+ function renderUncommon(result, { index, frame, showSalt }) {
59
+ const { bones } = result;
60
+ const { species, eye, hat, stats } = bones;
61
+ const c = RARITY.uncommon;
62
+ const W = 32;
63
+ const idx = index !== null ? `#${index + 1} ` : '';
64
+ const stars = formatStars(RARITY_STARS.uncommon);
65
+
66
+ const spriteLines = renderSprite(species, formatEye(eye), hat, frame).split('\n');
67
+ const faceLine = spriteLines.find(l => l.includes(formatEye(eye))) || spriteLines[1] || '';
68
+
69
+ const lines = [];
70
+ lines.push('');
71
+ lines.push(` ${c.color}╭${'─'.repeat(W)}╮${R}`);
72
+ lines.push(` ${c.color}│${R} ${c.accent}${BOLD}${idx}${species.toUpperCase()}${R} ${c.color}${stars}${R}${' '.repeat(Math.max(0, W - visibleLength(`${idx}${species.toUpperCase()} ${stars}`) - 1))}${c.color}│${R}`);
73
+ lines.push(` ${c.color}│${R} ${toTerminalSafeText(padAnsiEnd(faceLine.trim(), W - 1))}${c.color}│${R}`);
74
+ lines.push(` ${c.color}│${R} ${DIM}Eye${R} ${formatEye(eye)} ${DIM}Hat${R} ${toTerminalSafeText(hat === 'none' ? '—' : hat)}${' '.repeat(Math.max(0, W - 20))}${c.color}│${R}`);
75
+
76
+ // Compact stats
77
+ if (stats) {
78
+ const top = STATS.filter(s => stats[s] !== undefined)
79
+ .sort((a, b) => (stats[b] ?? 0) - (stats[a] ?? 0))
80
+ .slice(0, 3);
81
+ const statStr = top.map(s => `${s.slice(0, 3)}:${stats[s]}`).join(' ');
82
+ lines.push(` ${c.color}│${R} ${GRAY}${statStr}${R}${' '.repeat(Math.max(0, W - visibleLength(statStr) - 1))}${c.color}│${R}`);
83
+ }
84
+
85
+ if (showSalt && result.salt) {
86
+ lines.push(` ${c.color}│${R} ${DIM}${ITALIC}${result.salt}${R}${' '.repeat(Math.max(0, W - result.salt.length - 1))}${c.color}│${R}`);
87
+ }
88
+ lines.push(` ${c.color}╰${'─'.repeat(W)}╯${R}`);
89
+ lines.push('');
90
+ return lines.join('\n');
91
+ }
92
+
93
+ // ─── RARE: double frame, full sprite, stat bars ─────────
94
+
95
+ function renderRare(result, { index, frame, showSalt }) {
96
+ const { bones } = result;
97
+ const { species, eye, hat, shiny, stats, rarity } = bones;
98
+ const c = RARITY.rare;
99
+ const W = 36;
100
+ const inner = W - 2;
101
+ const idx = index !== null ? `#${index + 1} ` : '';
102
+ const stars = formatStars(RARITY_STARS[rarity]);
103
+ const floor = STAT_FLOORS[rarity] || 25;
104
+
105
+ const v = (content) => {
106
+ const padded = padAnsiEnd(content, inner);
107
+ return ` ${c.color}║${R} ${padded}${c.color}║${R}`;
108
+ };
109
+
110
+ const lines = [];
111
+ lines.push('');
112
+ lines.push(` ${c.color}╔${'═'.repeat(W)}╗${R}`);
113
+ lines.push(v(`${c.accent}${BOLD}${rarity.toUpperCase()}${R} ${c.color}${stars}${R}`));
114
+ lines.push(v(`${c.accent}${BOLD}${idx}${species.toUpperCase()}${R}${shiny ? ` ${formatShinyTag('✨')}` : ''}`));
115
+ lines.push(` ${c.color}╠${'═'.repeat(W)}╣${R}`);
116
+
117
+ // Full sprite
118
+ const sprite = toTerminalSafeText(renderSprite(species, formatEye(eye), hat, frame));
119
+ for (const sl of sprite.split('\n')) {
120
+ lines.push(v(centerText(`${c.color}${sl}${R}`, inner)));
121
+ }
122
+
123
+ lines.push(` ${c.color}╠${'═'.repeat(W)}╣${R}`);
124
+ lines.push(v(`${DIM}Eye${R} ${formatEye(eye)} ${DIM}Hat${R} ${toTerminalSafeText(hat === 'none' ? '—' : hat)}`));
125
+
126
+ // Stat bars
127
+ if (stats) {
128
+ lines.push(v(''));
129
+ for (const name of STATS) {
130
+ if (stats[name] === undefined) continue;
131
+ const val = stats[name];
132
+ const isHigh = val >= floor + 40;
133
+ const isLow = val <= floor;
134
+ const bar = statBar(val, c.color);
135
+ const marker = isHigh ? `${c.accent}▲${R}` : isLow ? `\x1b[31m▼${R}` : ' ';
136
+ lines.push(v(`${GRAY}${name.slice(0, 5).padEnd(5)}${R} ${bar} ${String(val).padStart(3)} ${marker}`));
137
+ }
138
+ }
139
+
140
+ if (showSalt && result.salt) {
141
+ lines.push(` ${c.color}╠${'═'.repeat(W)}╣${R}`);
142
+ lines.push(v(`${DIM}${ITALIC}${result.salt}${R}`));
143
+ }
144
+ lines.push(` ${c.color}╚${'═'.repeat(W)}╝${R}`);
145
+ lines.push('');
146
+ return lines.join('\n');
147
+ }
148
+
149
+ // ─── EPIC: bg tint, wide padding, decorative ────────────
150
+
151
+ function renderEpic(result, { index, frame, showSalt }) {
152
+ const { bones } = result;
153
+ const { species, eye, hat, shiny, stats, rarity } = bones;
154
+ const c = RARITY.epic;
155
+ const W = 38;
156
+ const inner = W - 2;
157
+ const floor = STAT_FLOORS[rarity] || 35;
158
+ const idx = index !== null ? `#${index + 1} ` : '';
159
+ const stars = formatStars(RARITY_STARS[rarity]);
160
+
161
+ const v = (content) => {
162
+ const padded = padAnsiEnd(content, inner);
163
+ return ` ${c.color}║${R}${c.bg} ${padded}${R} ${c.color}║${R}`;
164
+ };
165
+ const blank = () => v('');
166
+
167
+ const decorChar = isAsciiOnlyTerminal() ? '*' : '✦';
168
+ const decor = ` ${c.color}╠${'─'.repeat(Math.floor(W / 2 - 2))}${decorChar}${'─'.repeat(Math.ceil(W / 2 - 1))}╣${R}`;
169
+
170
+ const lines = [];
171
+ lines.push('');
172
+ lines.push(` ${c.color}╔${'═'.repeat(W)}╗${R}`);
173
+ lines.push(blank());
174
+ lines.push(v(centerText(`${c.accent}${BOLD}${formatStars(RARITY_STARS[rarity])} ${rarity.toUpperCase()}${R}`, inner)));
175
+ lines.push(v(centerText(`${c.accent}${BOLD}${idx}${species.toUpperCase()}${R}${shiny ? ` ${formatShinyTag('✨ SHINY')}` : ''}`, inner)));
176
+ lines.push(blank());
177
+ lines.push(decor);
178
+ lines.push(blank());
179
+
180
+ // Full sprite (colored)
181
+ const sprite = toTerminalSafeText(renderSprite(species, formatEye(eye), hat, frame));
182
+ for (const sl of sprite.split('\n')) {
183
+ lines.push(v(centerText(`${c.accent}${sl}${R}`, inner)));
184
+ }
185
+
186
+ lines.push(blank());
187
+ lines.push(decor);
188
+ lines.push(v(`${DIM}Eye${R} ${formatEye(eye)} ${DIM}Hat${R} ${toTerminalSafeText(hat === 'none' ? '—' : hat)} ${DIM}Shiny${R} ${shiny ? `${c.accent}YES${R}` : `${GRAY}no${R}`}`));
189
+ lines.push(blank());
190
+
191
+ // Stat bars with wide format
192
+ if (stats) {
193
+ for (const name of STATS) {
194
+ if (stats[name] === undefined) continue;
195
+ const val = stats[name];
196
+ const isHigh = val >= floor + 40;
197
+ const isLow = val <= floor;
198
+ const bar = statBar(val, c.color);
199
+ const marker = isHigh ? `${c.accent}▲${R}` : isLow ? `\x1b[31m▼${R}` : ' ';
200
+ lines.push(v(` ${GRAY}${name.padEnd(9)}${R} ${bar} ${String(val).padStart(3)} ${marker}`));
201
+ }
202
+ }
203
+
204
+ lines.push(blank());
205
+ if (showSalt && result.salt) {
206
+ lines.push(v(`${DIM}${ITALIC}${result.salt}${R}`));
207
+ }
208
+ lines.push(` ${c.color}╚${'═'.repeat(W)}╝${R}`);
209
+ lines.push('');
210
+ return lines.join('\n');
211
+ }
212
+
213
+ // ─── LEGENDARY: sparkle frame, maximum presence ─────────
214
+
215
+ function renderLegendary(result, { index, frame, showSalt }) {
216
+ const { bones } = result;
217
+ const { species, eye, hat, shiny, stats, rarity } = bones;
218
+ const c = RARITY.legendary;
219
+ const W = 40;
220
+ const inner = W - 2;
221
+ const floor = STAT_FLOORS[rarity] || 50;
222
+ const idx = index !== null ? `#${index + 1} ` : '';
223
+
224
+ const v = (content) => {
225
+ const padded = padAnsiEnd(content, inner);
226
+ return ` ${c.color}║${R}${c.bg} ${padded}${R} ${c.color}║${R}`;
227
+ };
228
+ const blank = () => v('');
229
+
230
+ const sp = isAsciiOnlyTerminal() ? '*' : '✦';
231
+ const sparkleTop = ` ${c.color}${sp}${'═'.repeat(W)}${sp}${R}`;
232
+ const sparkleBot = ` ${c.color}${sp}${'═'.repeat(W)}${sp}${R}`;
233
+ const decor = ` ${c.color}║${'─'.repeat(Math.floor(W / 2 - 3))} ${sp} ${sp} ${sp} ${'─'.repeat(Math.ceil(W / 2 - 4))}║${R}`;
234
+
235
+ const lines = [];
236
+ lines.push('');
237
+ lines.push(sparkleTop);
238
+ lines.push(blank());
239
+ lines.push(v(centerText(`${c.accent}${BOLD}${formatStars('★ ★ ★ ★ ★')} L E G E N D A R Y${R}`, inner)));
240
+ lines.push(blank());
241
+ lines.push(v(centerText(`${c.accent}${BOLD}${sp} ${idx}${species.toUpperCase()} ${sp}${R}`, inner)));
242
+ lines.push(blank());
243
+ lines.push(decor);
244
+ lines.push(blank());
245
+
246
+ // Full sprite (golden)
247
+ const sprite = toTerminalSafeText(renderSprite(species, formatEye(eye), hat, frame));
248
+ for (const sl of sprite.split('\n')) {
249
+ lines.push(v(centerText(`${c.accent}${sl}${R}`, inner)));
250
+ }
251
+
252
+ lines.push(blank());
253
+ lines.push(decor);
254
+ lines.push(blank());
255
+ lines.push(v(` ${DIM}Eye${R} ${formatEye(eye)} ${DIM}Hat${R} ${toTerminalSafeText(hat === 'none' ? '—' : hat)} ${shiny ? `${c.accent}${BOLD}✨ SHINY!${R}` : ''}`));
256
+ lines.push(blank());
257
+
258
+ // Full stat bars
259
+ if (stats) {
260
+ for (const name of STATS) {
261
+ if (stats[name] === undefined) continue;
262
+ const val = stats[name];
263
+ const isHigh = val >= floor + 40;
264
+ const isLow = val <= floor;
265
+ const bar = statBar(val, c.color);
266
+ const marker = isHigh ? `${c.accent}▲${R}` : isLow ? `\x1b[31m▼${R}` : ' ';
267
+ lines.push(v(` ${c.accent}${name.padEnd(9)}${R} ${bar} ${String(val).padStart(3)} ${marker}`));
268
+ }
269
+ }
270
+
271
+ lines.push(blank());
272
+ if (showSalt && result.salt) {
273
+ lines.push(v(`${DIM}${ITALIC}${result.salt}${R}`));
274
+ lines.push(blank());
275
+ }
276
+ lines.push(sparkleBot);
277
+ lines.push('');
278
+ return lines.join('\n');
279
+ }
280
+
281
+ // ─── Public API ─────────────────────────────────────────
282
+
283
+ export function renderCard(result, { showSalt = false, index = null, frame = 0 } = {}) {
284
+ const { rarity } = result.bones;
285
+ const opts = { index, frame, showSalt };
286
+
287
+ switch (rarity) {
288
+ case 'common': return renderCommon(result, opts);
289
+ case 'uncommon': return renderUncommon(result, opts);
290
+ case 'rare': return renderRare(result, opts);
291
+ case 'epic': return renderEpic(result, opts);
292
+ case 'legendary': return renderLegendary(result, opts);
293
+ default: return renderCommon(result, opts);
294
+ }
295
+ }
296
+
297
+ export function renderMiniCard(result, index) {
298
+ const { bones } = result;
299
+ const { rarity, species, eye, hat, shiny } = bones;
300
+ const c = RARITY[rarity] || RARITY.common;
301
+ const stars = formatStars(RARITY_STARS[rarity]);
302
+ const shinyTag = shiny
303
+ ? (isAsciiOnlyTerminal() ? ' [S]' : ` ${formatShinyTag('✨')}`)
304
+ : '';
305
+ const hatTag = hat !== 'none' ? ` ${DIM}+${toTerminalSafeText(hat)}${R}` : '';
306
+
307
+ const idx = String(index + 1).padStart(2, ' ');
308
+ return `${c.color}${idx}. ${formatEye(eye)} ${BOLD}${species.padEnd(10)}${R}${c.color} ${stars.padEnd(5)} ${rarity.slice(0, 3).toUpperCase()}${shinyTag}${hatTag}${R}`;
309
+ }