claude-buddy-reroll 1.9.1 → 2.0.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/package.json +1 -1
- package/src/dex-ui.mjs +10 -7
- package/src/display.mjs +248 -90
- package/src/selector.mjs +216 -212
- package/src/ui.mjs +136 -106
package/package.json
CHANGED
package/src/dex-ui.mjs
CHANGED
|
@@ -41,28 +41,31 @@ function pushUniqueVariant(ordered, seen, variant) {
|
|
|
41
41
|
|
|
42
42
|
function makeBox(title, lines, width = 28) {
|
|
43
43
|
const innerWidth = width - 4;
|
|
44
|
+
const DIM = '\x1b[2m';
|
|
45
|
+
const BOLD = '\x1b[1m';
|
|
46
|
+
const GRAY = '\x1b[90m';
|
|
47
|
+
const R = '\x1b[0m';
|
|
44
48
|
const output = [
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
`+${'-'.repeat(width - 2)}+`,
|
|
49
|
+
`${GRAY}${BOLD}${title}${R}`,
|
|
50
|
+
`${GRAY}${'─'.repeat(width - 2)}${R}`,
|
|
48
51
|
];
|
|
49
52
|
|
|
50
53
|
for (const line of lines) {
|
|
51
|
-
output.push(
|
|
54
|
+
output.push(`${DIM} ${fitLine(line, innerWidth)}${R}`);
|
|
52
55
|
}
|
|
53
56
|
|
|
54
|
-
output.push(
|
|
57
|
+
output.push('');
|
|
55
58
|
return output;
|
|
56
59
|
}
|
|
57
60
|
|
|
58
|
-
function joinColumns(leftLines, rightLines, leftWidth = 28, gap = '
|
|
61
|
+
function joinColumns(leftLines, rightLines, leftWidth = 28, gap = ' ') {
|
|
59
62
|
const totalRows = Math.max(leftLines.length, rightLines.length);
|
|
60
63
|
const output = [];
|
|
61
64
|
|
|
62
65
|
for (let row = 0; row < totalRows; row++) {
|
|
63
66
|
const left = leftLines[row] || ' '.repeat(leftWidth);
|
|
64
67
|
const right = rightLines[row] || '';
|
|
65
|
-
output.push(
|
|
68
|
+
output.push(` ${left}${gap}${right}`);
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
return output;
|
package/src/display.mjs
CHANGED
|
@@ -1,151 +1,309 @@
|
|
|
1
|
-
// Display utilities for buddy cards —
|
|
1
|
+
// Display utilities for buddy cards — v3: rarity = structure
|
|
2
|
+
// Each rarity tier has a fundamentally different card layout.
|
|
2
3
|
|
|
3
4
|
import { RARITY_STARS, STAT_FLOORS, STATS } from './engine.mjs';
|
|
4
5
|
import { renderSprite } from './sprites.mjs';
|
|
5
6
|
import { formatEye, formatShinyTag, formatStars, isAsciiOnlyTerminal, toTerminalSafeText } from './terminal.mjs';
|
|
6
7
|
import { padAnsiEnd, visibleLength } from './ansi.mjs';
|
|
7
8
|
|
|
8
|
-
const
|
|
9
|
-
const DIM = '\x1b[2m';
|
|
9
|
+
const R = '\x1b[0m';
|
|
10
10
|
const BOLD = '\x1b[1m';
|
|
11
|
-
const
|
|
11
|
+
const DIM = '\x1b[2m';
|
|
12
12
|
const ITALIC = '\x1b[3m';
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
const RARITY_STYLE = {
|
|
16
|
-
common: { color: '\x1b[37m', label: '\x1b[37m', bg: '', badge: 'COMMON', accent: '\x1b[90m', tl: '┌', tr: '┐', bl: '└', br: '┘', h: '─', v: '│' },
|
|
17
|
-
uncommon: { color: '\x1b[32m', label: '\x1b[32m', bg: '', badge: 'UNCOMMON', accent: '\x1b[32m', tl: '╭', tr: '╮', bl: '╰', br: '╯', h: '─', v: '│' },
|
|
18
|
-
rare: { color: '\x1b[36m', label: '\x1b[36m', bg: '', badge: 'RARE', accent: '\x1b[36m', tl: '╔', tr: '╗', bl: '╚', br: '╝', h: '═', v: '║' },
|
|
19
|
-
epic: { color: '\x1b[35m', label: '\x1b[35m', bg: '\x1b[48;5;53m', badge: 'EPIC', accent: '\x1b[35;1m', tl: '╔', tr: '╗', bl: '╚', br: '╝', h: '═', v: '║' },
|
|
20
|
-
legendary: { color: '\x1b[33;1m', label: '\x1b[33;1m', bg: '\x1b[48;5;58m', badge: 'LEGENDARY', accent: '\x1b[33;1m', tl: '╔', tr: '╗', bl: '╚', br: '╝', h: '═', v: '║' },
|
|
21
|
-
};
|
|
13
|
+
const GRAY = '\x1b[90m';
|
|
22
14
|
|
|
23
|
-
const
|
|
24
|
-
common: {
|
|
25
|
-
uncommon: {
|
|
26
|
-
rare: {
|
|
27
|
-
epic: {
|
|
28
|
-
legendary: {
|
|
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' },
|
|
29
21
|
};
|
|
30
22
|
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const s = ascii ? { ...style, ...ASCII_STYLE[Object.keys(RARITY_STYLE).find(k => RARITY_STYLE[k] === style)] } : style;
|
|
36
|
-
return {
|
|
37
|
-
top: `${style.color}${s.tl}${s.h.repeat(CARD_WIDTH - 2)}${s.tr}${RESET}`,
|
|
38
|
-
bottom: `${style.color}${s.bl}${s.h.repeat(CARD_WIDTH - 2)}${s.br}${RESET}`,
|
|
39
|
-
mid: `${style.color}${s.v}${s.h.repeat(CARD_WIDTH - 2)}${s.v}${RESET}`,
|
|
40
|
-
row: (content) => {
|
|
41
|
-
const padded = padAnsiEnd(content, CARD_WIDTH - 4);
|
|
42
|
-
return `${style.color}${s.v}${RESET} ${padded} ${style.color}${s.v}${RESET}`;
|
|
43
|
-
},
|
|
44
|
-
empty: () => `${style.color}${s.v}${RESET}${' '.repeat(CARD_WIDTH - 2)}${style.color}${s.v}${RESET}`,
|
|
45
|
-
};
|
|
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}`;
|
|
46
27
|
}
|
|
47
28
|
|
|
48
|
-
function
|
|
29
|
+
function centerText(text, width) {
|
|
49
30
|
const vis = visibleLength(text);
|
|
50
31
|
if (vis >= width) return text;
|
|
51
32
|
const left = Math.floor((width - vis) / 2);
|
|
52
|
-
|
|
53
|
-
return ' '.repeat(left) + text + ' '.repeat(right);
|
|
33
|
+
return ' '.repeat(left) + text;
|
|
54
34
|
}
|
|
55
35
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
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');
|
|
61
54
|
}
|
|
62
55
|
|
|
63
|
-
|
|
56
|
+
// ─── UNCOMMON: light frame, adds stats ──────────────────
|
|
57
|
+
|
|
58
|
+
function renderUncommon(result, { index, frame, showSalt }) {
|
|
64
59
|
const { bones } = result;
|
|
65
|
-
const {
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
const stars = formatStars(RARITY_STARS
|
|
70
|
-
const shinyTag = shiny ? formatShinyTag(' ✨') : '';
|
|
71
|
-
const sprite = toTerminalSafeText(renderSprite(species, formatEye(eye), hat, frame));
|
|
72
|
-
const floor = STAT_FLOORS[rarity] || 5;
|
|
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);
|
|
73
65
|
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
const nameText = `${style.label}${BOLD}${header}${species.toUpperCase()}${RESET} ${style.color}${stars}${shinyTag}${RESET}`;
|
|
66
|
+
const spriteLines = renderSprite(species, formatEye(eye), hat, frame).split('\n');
|
|
67
|
+
const faceLine = spriteLines.find(l => l.includes(formatEye(eye))) || spriteLines[1] || '';
|
|
77
68
|
|
|
78
69
|
const lines = [];
|
|
79
70
|
lines.push('');
|
|
80
|
-
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}`);
|
|
81
75
|
|
|
82
|
-
//
|
|
83
|
-
|
|
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
84
|
|
|
85
|
-
|
|
86
|
-
|
|
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
|
+
}
|
|
87
92
|
|
|
88
|
-
|
|
89
|
-
lines.push(b.mid);
|
|
93
|
+
// ─── RARE: double frame, full sprite, stat bars ─────────
|
|
90
94
|
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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;
|
|
96
104
|
|
|
97
|
-
|
|
98
|
-
|
|
105
|
+
const v = (content) => {
|
|
106
|
+
const padded = padAnsiEnd(content, inner);
|
|
107
|
+
return ` ${c.color}║${R} ${padded}${c.color}║${R}`;
|
|
108
|
+
};
|
|
99
109
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
lines.push(
|
|
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}`);
|
|
104
116
|
|
|
105
|
-
//
|
|
106
|
-
|
|
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)}`));
|
|
107
125
|
|
|
108
|
-
//
|
|
109
|
-
if (stats
|
|
126
|
+
// Stat bars
|
|
127
|
+
if (stats) {
|
|
128
|
+
lines.push(v(''));
|
|
110
129
|
for (const name of STATS) {
|
|
111
130
|
if (stats[name] === undefined) continue;
|
|
112
131
|
const val = stats[name];
|
|
113
132
|
const isHigh = val >= floor + 40;
|
|
114
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('');
|
|
115
166
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const valStr = String(val).padStart(3);
|
|
119
|
-
const marker = isHigh ? `${style.accent}▲${RESET}` : isLow ? `\x1b[31m▼${RESET}` : ' ';
|
|
167
|
+
const decorChar = isAsciiOnlyTerminal() ? '*' : '✦';
|
|
168
|
+
const decor = ` ${c.color}╠${'─'.repeat(Math.floor(W / 2 - 2))}${decorChar}${'─'.repeat(Math.ceil(W / 2 - 1))}╣${R}`;
|
|
120
169
|
|
|
121
|
-
|
|
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}`));
|
|
122
201
|
}
|
|
123
202
|
}
|
|
124
203
|
|
|
125
|
-
|
|
204
|
+
lines.push(blank());
|
|
126
205
|
if (showSalt && result.salt) {
|
|
127
|
-
lines.push(
|
|
128
|
-
lines.push(b.row(`${DIM}${ITALIC}${result.salt}${RESET}`));
|
|
206
|
+
lines.push(v(`${DIM}${ITALIC}${result.salt}${R}`));
|
|
129
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 ─────────
|
|
130
214
|
|
|
131
|
-
|
|
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 = [];
|
|
132
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());
|
|
133
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('');
|
|
134
278
|
return lines.join('\n');
|
|
135
279
|
}
|
|
136
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
|
+
|
|
137
297
|
export function renderMiniCard(result, index) {
|
|
138
298
|
const { bones } = result;
|
|
139
299
|
const { rarity, species, eye, hat, shiny } = bones;
|
|
140
|
-
const
|
|
300
|
+
const c = RARITY[rarity] || RARITY.common;
|
|
141
301
|
const stars = formatStars(RARITY_STARS[rarity]);
|
|
142
302
|
const shinyTag = shiny
|
|
143
303
|
? (isAsciiOnlyTerminal() ? ' [S]' : ` ${formatShinyTag('✨')}`)
|
|
144
304
|
: '';
|
|
305
|
+
const hatTag = hat !== 'none' ? ` ${DIM}+${toTerminalSafeText(hat)}${R}` : '';
|
|
145
306
|
|
|
146
307
|
const idx = String(index + 1).padStart(2, ' ');
|
|
147
|
-
|
|
148
|
-
const hatTag = hat !== 'none' ? ` ${DIM}+${toTerminalSafeText(hat)}${RESET}` : '';
|
|
149
|
-
|
|
150
|
-
return `${style.color}${idx}. ${formatEye(eye)} ${BOLD}${species.padEnd(10)}${RESET}${style.color} ${stars.padEnd(5)} ${badge}${shinyTag}${hatTag}${RESET}`;
|
|
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}`;
|
|
151
309
|
}
|
package/src/selector.mjs
CHANGED
|
@@ -1,212 +1,216 @@
|
|
|
1
|
-
// Terminal UI selector —
|
|
2
|
-
import { emitKeypressEvents } from 'readline';
|
|
3
|
-
import { padAnsiEnd, stripAnsi } from './ansi.mjs';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
let
|
|
50
|
-
let
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (
|
|
156
|
-
process.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (custom.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
cursor = (cursor +
|
|
191
|
-
tick++;
|
|
192
|
-
render();
|
|
193
|
-
} else if (key.name === '
|
|
194
|
-
cursor = (cursor
|
|
195
|
-
tick++;
|
|
196
|
-
render();
|
|
197
|
-
} else if (key.name === '
|
|
198
|
-
cursor = (cursor +
|
|
199
|
-
tick++;
|
|
200
|
-
render();
|
|
201
|
-
} else if (key.name === '
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
1
|
+
// Terminal UI selector — v2: clean layout, subtle focus, smooth feel
|
|
2
|
+
import { emitKeypressEvents } from 'readline';
|
|
3
|
+
import { padAnsiEnd, stripAnsi, visibleLength } from './ansi.mjs';
|
|
4
|
+
|
|
5
|
+
const R = '\x1b[0m';
|
|
6
|
+
const BOLD = '\x1b[1m';
|
|
7
|
+
const DIM = '\x1b[2m';
|
|
8
|
+
const CYAN = '\x1b[36m';
|
|
9
|
+
const GRAY = '\x1b[90m';
|
|
10
|
+
const WHITE = '\x1b[37m';
|
|
11
|
+
const HIDE_CURSOR = '\x1b[?25l';
|
|
12
|
+
const SHOW_CURSOR = '\x1b[?25h';
|
|
13
|
+
const CLEAR = '\x1b[2J\x1b[H';
|
|
14
|
+
|
|
15
|
+
// Focus style: subtle arrow + bold text, no heavy BG inversion
|
|
16
|
+
const FOCUS_POINTER = `${CYAN}›${R}`;
|
|
17
|
+
const UNFOCUS_SPACE = ' ';
|
|
18
|
+
|
|
19
|
+
export function getSelectorRenderLineCount(bodyRows) {
|
|
20
|
+
return bodyRows + 5;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getFullscreenBodyRows({ gridRows, previewHeight = 0, previewLinesLength = 0, footerLinesLength = 1, terminalRows = 24 }) {
|
|
24
|
+
const desiredBodyRows = Math.max(gridRows, previewHeight, previewLinesLength);
|
|
25
|
+
const reservedRows = 2 + 1 + Math.max(1, footerLinesLength);
|
|
26
|
+
const availableBodyRows = Math.max(gridRows, terminalRows - reservedRows);
|
|
27
|
+
return Math.min(desiredBodyRows, availableBodyRows);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Show an interactive list selector with arrow key navigation.
|
|
32
|
+
* @param {Object} options
|
|
33
|
+
* @param {string} options.title
|
|
34
|
+
* @param {Array<{label: string, description?: string, value: any}>} options.items
|
|
35
|
+
* @param {number} [options.columns=2]
|
|
36
|
+
* @param {number} [options.selected=0]
|
|
37
|
+
* @param {(item: any, meta: { cursor: number, tick: number }) => string} [options.preview]
|
|
38
|
+
* @param {number} [options.previewHeight=0]
|
|
39
|
+
* @param {boolean} [options.fullscreen=false]
|
|
40
|
+
* @param {number} [options.animationIntervalMs=0]
|
|
41
|
+
* @param {(item: any, meta: { cursor: number, tick: number, state: any }) => string[]} [options.footer]
|
|
42
|
+
* @param {any} [options.state]
|
|
43
|
+
* @param {(str: string, key: any, meta: { cursor: number, item: any, items: any[], tick: number, state: any }) => {handled?: boolean, state?: any, patch?: object, tick?: number}|void} [options.onKey]
|
|
44
|
+
* @returns {Promise<{index: number, value: any} | null>}
|
|
45
|
+
*/
|
|
46
|
+
export async function select({ title, items, columns = 2, selected = 0, preview = null, previewHeight = 0, fullscreen = false, animationIntervalMs = 0, footer = null, state = {}, onKey = null }) {
|
|
47
|
+
if (!process.stdin.isTTY) return null;
|
|
48
|
+
|
|
49
|
+
let cursor = selected;
|
|
50
|
+
let tick = 0;
|
|
51
|
+
let selectorState = state;
|
|
52
|
+
const rows = Math.ceil(items.length / columns);
|
|
53
|
+
const colWidth = Math.min(24, Math.floor(((process.stdout.columns || 80) - 6) / columns));
|
|
54
|
+
let renderedBodyRows = rows;
|
|
55
|
+
let didFullscreenInit = false;
|
|
56
|
+
|
|
57
|
+
function render() {
|
|
58
|
+
const meta = { cursor, tick, state: selectorState };
|
|
59
|
+
const previewText = typeof preview === 'function' ? preview(items[cursor], meta) : '';
|
|
60
|
+
const previewLines = previewText ? String(previewText).split('\n') : [];
|
|
61
|
+
const footerLines = typeof footer === 'function' ? footer(items[cursor], meta) : null;
|
|
62
|
+
const resolvedFooter = Array.isArray(footerLines) && footerLines.length > 0
|
|
63
|
+
? footerLines
|
|
64
|
+
: [` ${FOCUS_POINTER} ${items[cursor].label}${items[cursor].description ? ` ${DIM}${items[cursor].description}${R}` : ''}`];
|
|
65
|
+
const bodyRows = fullscreen
|
|
66
|
+
? getFullscreenBodyRows({
|
|
67
|
+
gridRows: rows,
|
|
68
|
+
previewHeight,
|
|
69
|
+
previewLinesLength: previewLines.length,
|
|
70
|
+
footerLinesLength: resolvedFooter.length,
|
|
71
|
+
terminalRows: process.stdout.rows || 24,
|
|
72
|
+
})
|
|
73
|
+
: Math.max(rows, previewHeight, previewLines.length);
|
|
74
|
+
const totalLines = getSelectorRenderLineCount(Math.max(renderedBodyRows, bodyRows)) + (resolvedFooter.length - 1);
|
|
75
|
+
|
|
76
|
+
if (fullscreen) {
|
|
77
|
+
if (!didFullscreenInit) {
|
|
78
|
+
process.stdout.write(CLEAR);
|
|
79
|
+
for (let i = 0; i < totalLines; i++) process.stdout.write('\n');
|
|
80
|
+
didFullscreenInit = true;
|
|
81
|
+
}
|
|
82
|
+
process.stdout.write(`\x1b[${totalLines}A`);
|
|
83
|
+
} else {
|
|
84
|
+
process.stdout.write(`\x1b[${totalLines}A`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Title
|
|
88
|
+
process.stdout.write(`\x1b[2K ${BOLD}${title}${R}\n`);
|
|
89
|
+
process.stdout.write(`\x1b[2K\n`);
|
|
90
|
+
|
|
91
|
+
for (let row = 0; row < bodyRows; row++) {
|
|
92
|
+
let gridLine = ' ';
|
|
93
|
+
|
|
94
|
+
if (row < rows) {
|
|
95
|
+
for (let col = 0; col < columns; col++) {
|
|
96
|
+
const idx = row * columns + col;
|
|
97
|
+
if (idx >= items.length) {
|
|
98
|
+
gridLine += ' '.repeat(colWidth);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const item = items[idx];
|
|
102
|
+
const isFocused = idx === cursor;
|
|
103
|
+
|
|
104
|
+
if (isFocused) {
|
|
105
|
+
const text = `${FOCUS_POINTER} ${BOLD}${stripAnsi(item.label)}${R}`;
|
|
106
|
+
gridLine += padAnsiEnd(text, colWidth);
|
|
107
|
+
} else {
|
|
108
|
+
const text = `${UNFOCUS_SPACE} ${DIM}${stripAnsi(item.label)}${R}`;
|
|
109
|
+
gridLine += padAnsiEnd(text, colWidth);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
gridLine += ' '.repeat(columns * colWidth);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const previewLine = previewLines[row] ? ` ${previewLines[row]}` : '';
|
|
117
|
+
process.stdout.write(`\x1b[2K${gridLine}${previewLine}\n`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
process.stdout.write(`\x1b[2K\n`);
|
|
121
|
+
for (const line of resolvedFooter) {
|
|
122
|
+
process.stdout.write(`\x1b[2K ${line}\n`);
|
|
123
|
+
}
|
|
124
|
+
renderedBodyRows = bodyRows;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return new Promise((resolve) => {
|
|
128
|
+
process.stdout.write(HIDE_CURSOR);
|
|
129
|
+
|
|
130
|
+
let animationTimer = null;
|
|
131
|
+
|
|
132
|
+
if (!fullscreen) {
|
|
133
|
+
const initialPreviewText = typeof preview === 'function' ? preview(items[cursor], { cursor, tick, state: selectorState }) : '';
|
|
134
|
+
const initialPreviewLines = initialPreviewText ? String(initialPreviewText).split('\n') : [];
|
|
135
|
+
const initialFooter = typeof footer === 'function' ? footer(items[cursor], { cursor, tick, state: selectorState }) : null;
|
|
136
|
+
const resolvedFooter = Array.isArray(initialFooter) && initialFooter.length > 0 ? initialFooter : [''];
|
|
137
|
+
const totalLines = getSelectorRenderLineCount(Math.max(rows, previewHeight, initialPreviewLines.length)) + (resolvedFooter.length - 1);
|
|
138
|
+
for (let i = 0; i < totalLines; i++) process.stdout.write('\n');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
render();
|
|
142
|
+
|
|
143
|
+
if (animationIntervalMs > 0) {
|
|
144
|
+
animationTimer = setInterval(() => {
|
|
145
|
+
tick++;
|
|
146
|
+
render();
|
|
147
|
+
}, animationIntervalMs);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
emitKeypressEvents(process.stdin);
|
|
151
|
+
if (process.stdin.setRawMode) process.stdin.setRawMode(true);
|
|
152
|
+
process.stdin.resume();
|
|
153
|
+
|
|
154
|
+
function cleanup() {
|
|
155
|
+
if (process.stdin.setRawMode) process.stdin.setRawMode(false);
|
|
156
|
+
process.stdin.pause();
|
|
157
|
+
process.stdin.removeListener('keypress', handleKeypress);
|
|
158
|
+
if (animationTimer) clearInterval(animationTimer);
|
|
159
|
+
if (fullscreen) process.stdout.write(CLEAR);
|
|
160
|
+
process.stdout.write(SHOW_CURSOR);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function handleKeypress(str, key) {
|
|
164
|
+
if (!key) return;
|
|
165
|
+
|
|
166
|
+
if (typeof onKey === 'function') {
|
|
167
|
+
const custom = onKey(str, key, {
|
|
168
|
+
cursor,
|
|
169
|
+
item: items[cursor],
|
|
170
|
+
items,
|
|
171
|
+
tick,
|
|
172
|
+
state: selectorState,
|
|
173
|
+
});
|
|
174
|
+
if (custom) {
|
|
175
|
+
if (Object.prototype.hasOwnProperty.call(custom, 'state')) {
|
|
176
|
+
selectorState = custom.state;
|
|
177
|
+
} else if (custom.patch && selectorState && typeof selectorState === 'object') {
|
|
178
|
+
selectorState = { ...selectorState, ...custom.patch };
|
|
179
|
+
}
|
|
180
|
+
if (typeof custom.tick === 'number') tick = custom.tick;
|
|
181
|
+
if (custom.handled) {
|
|
182
|
+
tick++;
|
|
183
|
+
render();
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (key.name === 'up') {
|
|
190
|
+
cursor = (cursor - columns + items.length) % items.length;
|
|
191
|
+
tick++;
|
|
192
|
+
render();
|
|
193
|
+
} else if (key.name === 'down') {
|
|
194
|
+
cursor = (cursor + columns) % items.length;
|
|
195
|
+
tick++;
|
|
196
|
+
render();
|
|
197
|
+
} else if (key.name === 'left') {
|
|
198
|
+
cursor = (cursor - 1 + items.length) % items.length;
|
|
199
|
+
tick++;
|
|
200
|
+
render();
|
|
201
|
+
} else if (key.name === 'right') {
|
|
202
|
+
cursor = (cursor + 1) % items.length;
|
|
203
|
+
tick++;
|
|
204
|
+
render();
|
|
205
|
+
} else if (key.name === 'return') {
|
|
206
|
+
cleanup();
|
|
207
|
+
resolve({ index: cursor, value: items[cursor].value });
|
|
208
|
+
} else if (key.name === 'escape' || (key.ctrl && key.name === 'c')) {
|
|
209
|
+
cleanup();
|
|
210
|
+
resolve(null);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
process.stdin.on('keypress', handleKeypress);
|
|
215
|
+
});
|
|
216
|
+
}
|
package/src/ui.mjs
CHANGED
|
@@ -1,106 +1,136 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
export function
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
1
|
+
// UI layout primitives — v2: clean, consistent, Speaki-branded
|
|
2
|
+
import { RARITIES, RARITY_STARS, RARITY_WEIGHTS } from './engine.mjs';
|
|
3
|
+
import { padAnsiEnd, visibleLength } from './ansi.mjs';
|
|
4
|
+
import { formatStars } from './terminal.mjs';
|
|
5
|
+
|
|
6
|
+
const R = '\x1b[0m';
|
|
7
|
+
const BOLD = '\x1b[1m';
|
|
8
|
+
const DIM = '\x1b[2m';
|
|
9
|
+
const ITALIC = '\x1b[3m';
|
|
10
|
+
const CYAN = '\x1b[36m';
|
|
11
|
+
const GREEN = '\x1b[32m';
|
|
12
|
+
const MAGENTA = '\x1b[35m';
|
|
13
|
+
const YELLOW = '\x1b[33;1m';
|
|
14
|
+
const RED = '\x1b[31m';
|
|
15
|
+
const WHITE = '\x1b[37m';
|
|
16
|
+
const GRAY = '\x1b[90m';
|
|
17
|
+
|
|
18
|
+
const RARITY_TONE = {
|
|
19
|
+
common: GRAY,
|
|
20
|
+
uncommon: GREEN,
|
|
21
|
+
rare: '\x1b[34m',
|
|
22
|
+
epic: MAGENTA,
|
|
23
|
+
legendary: YELLOW,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// ─── Layout primitives ──────────────────────────────────
|
|
27
|
+
|
|
28
|
+
function hr(width = 48, char = '─', color = GRAY) {
|
|
29
|
+
return `${color}${char.repeat(width)}${R}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function heading(text, color = MAGENTA) {
|
|
33
|
+
return `${color}${BOLD}${text}${R}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function label(key, value) {
|
|
37
|
+
return ` ${GRAY}${key}${R} ${value}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function bullet(text, icon = '›', color = GREEN) {
|
|
41
|
+
return ` ${color}${icon}${R} ${text}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ─── Banner ─────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
export function renderBanner(title = 'BUDDY LAB', subtitle = 'collectible companion terminal') {
|
|
47
|
+
return [
|
|
48
|
+
'',
|
|
49
|
+
` ${MAGENTA}${BOLD}${title}${R}`,
|
|
50
|
+
` ${DIM}${subtitle}${R}`,
|
|
51
|
+
` ${hr(40)}`,
|
|
52
|
+
'',
|
|
53
|
+
].join('\n');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ─── Section panel ──────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
export function renderPanel(title, lines = [], color = GRAY) {
|
|
59
|
+
const out = [];
|
|
60
|
+
out.push(` ${color}${BOLD}${title}${R}`);
|
|
61
|
+
for (const line of lines) {
|
|
62
|
+
out.push(` ${line}`);
|
|
63
|
+
}
|
|
64
|
+
out.push('');
|
|
65
|
+
return out.join('\n');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ─── Quota summary ──────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
export function renderQuotaSummary({ used = 0, limit = 0, starred = false, eventRemaining = 0 } = {}) {
|
|
71
|
+
const remaining = Math.max(0, limit - used);
|
|
72
|
+
const bar = renderQuotaBar(used, limit);
|
|
73
|
+
return [
|
|
74
|
+
`${bar} ${DIM}${remaining}/${limit} left${starred ? ' ⭐' : ''}${R}`,
|
|
75
|
+
`${DIM}사용: ${used} 이벤트: ${eventRemaining}회 남음${R}`,
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function renderQuotaBar(used, limit) {
|
|
80
|
+
const w = 10;
|
|
81
|
+
const filled = Math.min(w, Math.round((used / Math.max(limit, 1)) * w));
|
|
82
|
+
const remaining = w - filled;
|
|
83
|
+
return `${GREEN}${'█'.repeat(remaining)}${GRAY}${'░'.repeat(filled)}${R}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ─── Rarity table ───────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
export function renderRarityOverview() {
|
|
89
|
+
return RARITIES.map((rarity) => {
|
|
90
|
+
const pct = `${RARITY_WEIGHTS[rarity]}%`.padStart(3);
|
|
91
|
+
const color = RARITY_TONE[rarity] || WHITE;
|
|
92
|
+
const stars = formatStars(RARITY_STARS[rarity]);
|
|
93
|
+
return `${color}${stars.padEnd(6)}${R} ${color}${rarity.padEnd(10)}${R} ${GRAY}${pct}${R}`;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ─── Home screen ────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
export function renderHomeScreen() {
|
|
100
|
+
return [
|
|
101
|
+
renderBanner(),
|
|
102
|
+
renderPanel('Commands', [
|
|
103
|
+
`${GREEN}bdy check${R} ${DIM}현재 버디 + 설치 상태${R}`,
|
|
104
|
+
`${GREEN}bdy gacha 10${R} ${DIM}10연차 가챠${R}`,
|
|
105
|
+
`${GREEN}bdy reroll${R} ${DIM}후보 뽑고 바로 적용${R}`,
|
|
106
|
+
`${GREEN}bdy dex${R} ${DIM}도감 탐색${R}`,
|
|
107
|
+
`${GREEN}bdy restore${R} ${DIM}원래 버디로 복원${R}`,
|
|
108
|
+
]),
|
|
109
|
+
renderPanel('Rarity', renderRarityOverview(), MAGENTA),
|
|
110
|
+
].join('\n');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ─── Check screen ───────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
export function renderCheckScreen({ accountLabel, installLabel, salt, patched, quotaLines, buddyCard, profileLine = '' }) {
|
|
116
|
+
return [
|
|
117
|
+
renderBanner('BUDDY STATUS', '현재 버디 + 설치 상태'),
|
|
118
|
+
renderPanel('Runtime', [
|
|
119
|
+
label('Account', accountLabel),
|
|
120
|
+
label('Install', installLabel),
|
|
121
|
+
label('Salt', `${salt}${patched ? ` ${YELLOW}(patched)${R}` : ` ${DIM}(original)${R}`}`),
|
|
122
|
+
...(profileLine ? [label('Profile', profileLine)] : []),
|
|
123
|
+
]),
|
|
124
|
+
renderPanel('Quota', quotaLines, GREEN),
|
|
125
|
+
buddyCard,
|
|
126
|
+
].join('\n');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ─── Search status ──────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
export function renderSearchStatus({ targetSpecies, criteria, attempts }) {
|
|
132
|
+
const rarityText = criteria?.rarity ? ` ${RARITY_TONE[criteria.rarity] || ''}${criteria.rarity}${R}` : '';
|
|
133
|
+
return ` ${CYAN}›${R} ${targetSpecies}${rarityText} ${GRAY}(${attempts} attempts)${R}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export { R as RESET, BOLD, DIM, CYAN, GREEN, MAGENTA, YELLOW, RED };
|