koishi-plugin-vsinger-gacha 1.2.1 → 1.3.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/README.md CHANGED
@@ -62,6 +62,20 @@ npm i @napi-rs/canvas
62
62
  - 想一次性缓存全部:`npm run fetch-assets -- --dir data/vsinger-gacha/cards`
63
63
  - 要动态卡 mp4(1.1GB):下载 Release 的 `assets-videos.tar` 解压到 `cards/`
64
64
 
65
+ ## 统一渲染系统
66
+
67
+ 所有图片(抽卡结果、图鉴、以后新增的图)都走同一套设计令牌与组件:
68
+
69
+ ```
70
+ lib/render/
71
+ ├── theme.js 设计令牌:颜色/字号/间距/圆角/档位配色
72
+ ├── kit.js 组件:背景、毛玻璃面板、卡牌、角标、标题、页脚、文本截断
73
+ ├── result.js 抽卡结果布局
74
+ └── handbook.js 图鉴布局(复用 kit)
75
+ ```
76
+
77
+ 新增图片时只写布局,样式自动统一。
78
+
65
79
  ## 兼容性
66
80
 
67
81
  | 项 | 说明 |
package/lib/handbook.js CHANGED
@@ -9,6 +9,7 @@ const fs = require('node:fs');
9
9
  const path = require('node:path');
10
10
 
11
11
  const FONT_FILE = require('node:path').join(__dirname, '..', 'assets', 'font.ttf');
12
+ const kit = require('./render/kit');
12
13
 
13
14
  let Canvas = null;
14
15
  /** 按需加载画布(没装也不影响抽卡/积分) */
@@ -249,67 +250,19 @@ async function render({ game, store, uid, displayName, outDir, tagsDir, thumbsDi
249
250
  const cx = x0 + (i % COLS_PER_COL) * (TW + GAP);
250
251
  const cy = y + POOL_TITLE_H + Math.floor(i / COLS_PER_COL) * ROWH;
251
252
  const n = owned[c.h] || 0;
252
- const tier = tierOf(c.v);
253
- const tcol = TIER_COLOR[tier];
254
-
255
- // 辉光(拥有)
256
- if (n) {
257
- g.save();
258
- g.shadowColor = tcol;
259
- g.shadowBlur = 22;
260
- g.fillStyle = 'rgba(0,0,0,0.6)';
261
- g.fillRect(cx, cy, TW, TH);
262
- g.restore();
263
- }
264
253
  const img = await load(path.join(thumbs, c.h + '.jpg'));
265
- if (img) {
266
- g.save();
267
- if (!n) g.globalAlpha = 0.62;
268
- g.drawImage(img, cx, cy, TW, TH);
269
- g.restore();
270
- } else {
271
- g.fillStyle = '#1b2233';
272
- g.fillRect(cx, cy, TW, TH);
273
- g.fillStyle = '#7d879c';
274
- g.font = '18px VSingerSans, sans-serif';
275
- g.fillText('缺图', cx + TW / 2 - 18, cy + TH / 2);
276
- }
277
- g.strokeStyle = n ? '#ffffff' : 'rgba(205,210,222,0.8)';
278
- g.lineWidth = 2;
279
- g.strokeRect(cx + 1, cy + 1, TW - 2, TH - 2);
280
-
281
- // 角标
282
- const tagFile = tier === 40 ? tagBig : (tier === 30 ? tagSmall : null);
283
- if (tagFile && fs.existsSync(tagFile)) {
284
- const ti = await load(tagFile);
285
- if (ti) {
286
- const w = TW * 0.46;
287
- g.drawImage(ti, cx + 3, cy + 3, w, (ti.height / ti.width) * w);
288
- }
289
- }
290
- if (laser[c.h] && fs.existsSync(tagHolo)) {
291
- const hi = await load(tagHolo);
292
- if (hi) {
293
- const w = TW * 0.36;
294
- const hh = (hi.height / hi.width) * w;
295
- g.drawImage(hi, cx + (TW - w) / 2, cy + TH - hh - 4, w, hh);
296
- }
297
- }
298
-
299
- // 文字
300
- g.fillStyle = n ? '#ffffff' : 'rgba(200,205,218,0.9)';
301
- g.font = '16px VSingerSans, sans-serif';
302
- g.fillText(fitText(g, c.v.card_name, TW), cx, cy + TH + 20);
303
- g.fillStyle = n ? tcol : 'rgba(225,230,240,0.85)';
304
- g.font = '13px VSingerSans, sans-serif';
305
- g.fillText(fitText(g, `${TIER_NAME[tier]} ${c.v.rate_pct}%`, TW), cx, cy + TH + 40);
306
- if (n > 1) {
307
- g.fillStyle = 'rgba(0,0,0,0.75)';
308
- g.fillRect(cx + TW - 38, cy + 4, 34, 20);
309
- g.fillStyle = '#8cffaa';
310
- g.font = 'bold 15px VSingerSans, sans-serif';
311
- g.fillText(`×${n}`, cx + TW - 35, cy + 19);
312
- }
254
+ await kit.card(g, Canvas, {
255
+ img, x: cx, y: cy, w: TW, h: TH,
256
+ name: c.v.card_name,
257
+ rate: c.v.rate_pct,
258
+ tier: tierOf(c.v),
259
+ laser: Boolean(laser[c.h]),
260
+ count: n,
261
+ owned: n > 0,
262
+ tagsDir,
263
+ nameSize: 15,
264
+ metaSize: 12,
265
+ });
313
266
  }
314
267
  y += blk.h;
315
268
  }
@@ -331,141 +284,10 @@ async function render({ game, store, uid, displayName, outDir, tagsDir, thumbsDi
331
284
  }
332
285
 
333
286
 
334
- /**
335
- * 抽卡结果图(不依赖合并转发,任何平台都能发)
336
- * @param {object} opts
337
- * @param {Array} opts.results [{hash|h, card, tier, laser}]
338
- * @param {string} opts.title
339
- */
340
- async function renderResult({ results, title, outDir, tagsDir, thumbsDir }) {
287
+ async function renderResult(opts) {
341
288
  const Canvas = getCanvas();
342
- const fs = require('node:fs');
343
- const path = require('node:path');
344
-
345
- const TIER_NAME = { 40: '大隐藏', 30: '小隐藏', 20: '稀有', 10: '普通' };
346
- const TIER_COLOR = { 40: '#ff6d84', 30: '#ffb44d', 20: '#8fc0ff', 10: '#c3cad8' };
347
- const tierOf = (card) => {
348
- const sc = Number((card && card.card_scarcity) || 10);
349
- return sc === 40 ? 40 : sc === 30 ? 30 : sc === 20 ? 20 : 10;
350
- };
351
-
352
- const n = Math.max(1, results.length);
353
- const PER_ROW = n <= 3 ? n : Math.min(5, n);
354
- const rows = Math.ceil(n / PER_ROW);
355
- const CW = n === 1 ? 300 : 170; // 卡宽(1 张时放大)
356
- const CH = Math.round(CW * 1.43);
357
- const LABEL = 58;
358
- const GAP = 16;
359
- const PAD = 32;
360
- const HEAD = 108;
361
- const W = Math.max(520, PAD * 2 + PER_ROW * CW + (PER_ROW - 1) * GAP);
362
- const H = HEAD + rows * (CH + LABEL + GAP) + PAD;
363
-
364
- const canvas = Canvas.createCanvas(W, H);
365
- const g = canvas.getContext('2d');
366
- g.textBaseline = 'top';
367
-
368
- // 背景:深蓝渐变 + 顶部柔光
369
- const bg = g.createLinearGradient(0, 0, W * 0.6, H);
370
- bg.addColorStop(0, '#111c33');
371
- bg.addColorStop(0.55, '#16263f');
372
- bg.addColorStop(1, '#0d1524');
373
- g.fillStyle = bg;
374
- g.fillRect(0, 0, W, H);
375
- const glow = g.createRadialGradient(W * 0.25, 0, 10, W * 0.25, 0, Math.max(W, H) * 0.75);
376
- glow.addColorStop(0, 'rgba(120,180,255,0.28)');
377
- glow.addColorStop(1, 'rgba(120,180,255,0)');
378
- g.fillStyle = glow;
379
- g.fillRect(0, 0, W, H);
380
-
381
- // 标题
382
- g.fillStyle = '#ffffff';
383
- g.font = 'bold 32px VSingerSans, sans-serif';
384
- const t = String(title || '').replace(/\s+/g, ' ');
385
- const maxTitle = W - PAD * 2;
386
- let tt = t;
387
- while (tt.length > 2 && g.measureText(tt).width > maxTitle) tt = tt.slice(0, -1);
388
- if (tt !== t) tt = tt.slice(0, -1) + '…';
389
- g.fillText(tt, PAD, 30);
390
- g.fillStyle = 'rgba(200,215,240,0.65)';
391
- g.font = '18px VSingerSans, sans-serif';
392
- g.fillText(`共 ${n} 张 | 抽卡结果`, PAD, 72);
393
-
394
- const rowW = PER_ROW * CW + (PER_ROW - 1) * GAP;
395
- const x0 = Math.round((W - rowW) / 2);
396
-
397
- for (let i = 0; i < results.length; i++) {
398
- const r = results[i];
399
- const hash = r.hash || r.h;
400
- const col = i % PER_ROW;
401
- const row = Math.floor(i / PER_ROW);
402
- const x = x0 + col * (CW + GAP);
403
- const y = HEAD + row * (CH + LABEL + GAP);
404
- const tier = tierOf(r.card);
405
-
406
- // 卡片底
407
- g.save();
408
- g.beginPath();
409
- g.roundRect(x, y, CW, CH, 14);
410
- g.clip();
411
- g.fillStyle = '#1b2436';
412
- g.fillRect(x, y, CW, CH);
413
- const thumb = hash ? path.join(thumbsDir, hash + '.jpg') : '';
414
- if (thumb && fs.existsSync(thumb)) {
415
- try {
416
- const img = await Canvas.loadImage(thumb);
417
- g.drawImage(img, x, y, CW, CH);
418
- } catch { /* 忽略 */ }
419
- }
420
- g.restore();
421
-
422
- // 边框(按档位上色)
423
- g.save();
424
- g.beginPath();
425
- g.roundRect(x + 1, y + 1, CW - 2, CH - 2, 13);
426
- g.strokeStyle = TIER_COLOR[tier];
427
- g.lineWidth = 3;
428
- g.stroke();
429
- g.restore();
430
-
431
- // 档位角标
432
- const tag = tier === 40 ? path.join(tagsDir, 'tag_40.webp') : tier === 30 ? path.join(tagsDir, 'tag_30.webp') : null;
433
- if (tag && fs.existsSync(tag)) {
434
- try {
435
- const ti = await Canvas.loadImage(tag);
436
- const tw = CW * 0.46;
437
- g.drawImage(ti, x + 4, y + 4, tw, (ti.height / ti.width) * tw);
438
- } catch { /* 忽略 */ }
439
- }
440
- if (r.laser) {
441
- const holo = path.join(tagsDir, 'tag_holo.png');
442
- if (fs.existsSync(holo)) {
443
- try {
444
- const hi = await Canvas.loadImage(holo);
445
- const hw = CW * 0.36;
446
- const hh = (hi.height / hi.width) * hw;
447
- g.drawImage(hi, x + (CW - hw) / 2, y + CH - hh - 6, hw, hh);
448
- } catch { /* 忽略 */ }
449
- }
450
- }
451
-
452
- // 名称 + 档位/概率
453
- g.fillStyle = '#ffffff';
454
- g.font = 'bold 19px VSingerSans, sans-serif';
455
- const name = String((r.card && r.card.card_name) || '');
456
- let nn = name;
457
- while (nn.length > 1 && g.measureText(nn).width > CW) nn = nn.slice(0, -1);
458
- g.fillText(nn === name ? nn : nn.slice(0, -1) + '…', x, y + CH + 8);
459
- g.fillStyle = TIER_COLOR[tier];
460
- g.font = '15px VSingerSans, sans-serif';
461
- const rate = r.card && r.card.rate_pct != null ? ` ${r.card.rate_pct}%` : '';
462
- g.fillText(`${TIER_NAME[tier]}${rate}`, x, y + CH + 34);
463
- }
464
-
465
- fs.mkdirSync(outDir, { recursive: true });
466
- const out = path.join(outDir, `draw_${Date.now()}.jpg`);
467
- fs.writeFileSync(out, await canvas.encode('jpeg', 90));
468
- return out;
289
+ const { render } = require('./render/result');
290
+ return render({ Canvas, ...opts });
469
291
  }
470
292
 
471
293
  module.exports = { render, renderResult };
@@ -0,0 +1,166 @@
1
+ 'use strict';
2
+ /** 渲染基础组件:背景 / 面板 / 卡片 / 角标 / 标题 / 文本 */
3
+ const { FONT, COLOR, TIER, RADIUS, SPACE, FONT_SIZE, tierOf } = require('./theme');
4
+
5
+ function roundRect(g, x, y, w, h, r) {
6
+ g.beginPath();
7
+ if (g.roundRect) {
8
+ g.roundRect(x, y, w, h, r);
9
+ } else {
10
+ g.moveTo(x + r, y);
11
+ g.arcTo(x + w, y, x + w, y + h, r);
12
+ g.arcTo(x + w, y + h, x, y + h, r);
13
+ g.arcTo(x, y + h, x, y, r);
14
+ g.arcTo(x, y, x + w, y, r);
15
+ g.closePath();
16
+ }
17
+ return g;
18
+ }
19
+
20
+ /** 背景:深蓝渐变 + 顶部柔光 + 底部压暗 */
21
+ function background(g, W, H) {
22
+ const bg = g.createLinearGradient(0, 0, W * 0.6, H);
23
+ bg.addColorStop(0, COLOR.bg2);
24
+ bg.addColorStop(0.55, COLOR.bg1);
25
+ bg.addColorStop(1, COLOR.bg0);
26
+ g.fillStyle = bg;
27
+ g.fillRect(0, 0, W, H);
28
+
29
+ const glow = g.createRadialGradient(W * 0.25, 0, 10, W * 0.25, 0, Math.max(W, H) * 0.75);
30
+ glow.addColorStop(0, COLOR.glow);
31
+ glow.addColorStop(1, 'rgba(120,180,255,0)');
32
+ g.fillStyle = glow;
33
+ g.fillRect(0, 0, W, H);
34
+ }
35
+
36
+ /** 毛玻璃面板 */
37
+ function panel(g, x, y, w, h, opts = {}) {
38
+ const r = opts.radius == null ? RADIUS.lg : opts.radius;
39
+ g.save();
40
+ roundRect(g, x, y, w, h, r);
41
+ g.fillStyle = opts.fill || COLOR.panel;
42
+ g.fill();
43
+ g.strokeStyle = opts.stroke || COLOR.panelBorder;
44
+ g.lineWidth = opts.lineWidth || 1;
45
+ g.stroke();
46
+ g.restore();
47
+ }
48
+
49
+ /** 标题区:主标题 + 副标题 + 强调短线 */
50
+ function header(g, x, y, W, title, subtitle) {
51
+ g.textBaseline = 'top';
52
+ g.fillStyle = COLOR.accent;
53
+ roundRect(g, x, y + 4, 6, 34, 3);
54
+ g.fill();
55
+
56
+ g.fillStyle = COLOR.text;
57
+ g.font = `bold ${FONT_SIZE.h1}px ${FONT}`;
58
+ const maxW = W - 40;
59
+ let t = String(title || '');
60
+ while (t.length > 2 && g.measureText(t).width > maxW) t = t.slice(0, -1);
61
+ g.fillText(t.length < String(title).length ? t.slice(0, -1) + '…' : t, x + 16, y);
62
+
63
+ if (subtitle) {
64
+ g.fillStyle = COLOR.textSub;
65
+ g.font = `${FONT_SIZE.body}px ${FONT}`;
66
+ g.fillText(String(subtitle), x + 16, y + 42);
67
+ }
68
+ }
69
+
70
+ /** 文本截断 */
71
+ function fit(g, text, font, maxW) {
72
+ const s = String(text == null ? '' : text);
73
+ g.font = font;
74
+ if (g.measureText(s).width <= maxW) return s;
75
+ let t = s;
76
+ while (t.length > 1 && g.measureText(t + '…').width > maxW) t = t.slice(0, -1);
77
+ return t + '…';
78
+ }
79
+
80
+ /**
81
+ * 卡牌组件(图鉴 / 结果图共用)
82
+ * @param {object} o {img, name, rate, tier, laser, count, x, y, w, h, owned}
83
+ */
84
+ async function card(g, Canvas, o) {
85
+ const w = o.w || 170;
86
+ const h = o.h || Math.round(w * 1.43);
87
+ const r = RADIUS.md;
88
+ const tier = o.tier == null ? tierOf({ card_scarcity: o.scarcity }) : o.tier;
89
+ const tc = (TIER[tier] || TIER[10]).color;
90
+
91
+ // 底 + 图
92
+ g.save();
93
+ roundRect(g, o.x, o.y, w, h, r);
94
+ g.clip();
95
+ g.fillStyle = COLOR.cardBg;
96
+ g.fillRect(o.x, o.y, w, h);
97
+ if (o.img) {
98
+ try {
99
+ g.drawImage(o.img, o.x, o.y, w, h);
100
+ } catch { /* 忽略 */ }
101
+ }
102
+ if (o.owned === false) {
103
+ g.fillStyle = 'rgba(0,0,0,0.42)';
104
+ g.fillRect(o.x, o.y, w, h);
105
+ }
106
+ g.restore();
107
+
108
+ // 边框(档位色 + 拥有加粗)
109
+ g.save();
110
+ roundRect(g, o.x + 1, o.y + 1, w - 2, h - 2, r - 1);
111
+ g.strokeStyle = o.owned === false ? 'rgba(160,166,180,0.55)' : tc;
112
+ g.lineWidth = o.owned === false ? 1.5 : 2.5;
113
+ g.stroke();
114
+ g.restore();
115
+
116
+ // 档位角标
117
+ const tagFile = o.tagsDir ? require('node:path').join(o.tagsDir, (TIER[tier] || {}).tag || '') : '';
118
+ if ((TIER[tier] || {}).tag && o.tagsDir && require('node:fs').existsSync(tagFile)) {
119
+ try {
120
+ const ti = await Canvas.loadImage(tagFile);
121
+ const tw = w * 0.46;
122
+ g.drawImage(ti, o.x + 4, o.y + 4, tw, (ti.height / ti.width) * tw);
123
+ } catch { /* 忽略 */ }
124
+ }
125
+ // 镭射标
126
+ if (o.laser && o.tagsDir) {
127
+ const holo = require('node:path').join(o.tagsDir, 'tag_holo.png');
128
+ if (require('node:fs').existsSync(holo)) {
129
+ try {
130
+ const hi = await Canvas.loadImage(holo);
131
+ const hw = w * 0.36;
132
+ const hh = (hi.height / hi.width) * hw;
133
+ g.drawImage(hi, o.x + (w - hw) / 2, o.y + h - hh - 6, hw, hh);
134
+ } catch { /* 忽略 */ }
135
+ }
136
+ }
137
+ // 重复次数
138
+ if (o.count > 1) {
139
+ g.fillStyle = 'rgba(0,0,0,0.72)';
140
+ roundRect(g, o.x + w - 46, o.y + 6, 40, 22, 8);
141
+ g.fill();
142
+ g.fillStyle = '#8cffaa';
143
+ g.font = `bold 14px ${FONT}`;
144
+ g.fillText(`×${o.count}`, o.x + w - 40, o.y + 9);
145
+ }
146
+
147
+ // 名称 / 档位概率
148
+ g.fillStyle = o.owned === false ? COLOR.textDim : COLOR.text;
149
+ g.font = `bold ${o.nameSize || FONT_SIZE.h3}px ${FONT}`;
150
+ g.fillText(fit(g, o.name, `bold ${o.nameSize || FONT_SIZE.h3}px ${FONT}`, w), o.x, o.y + h + 8);
151
+ if (o.rate != null || o.tierName) {
152
+ g.fillStyle = tc;
153
+ g.font = `${o.metaSize || FONT_SIZE.meta}px ${FONT}`;
154
+ const meta = `${o.tierName || (TIER[tier] || TIER[10]).name}${o.rate != null ? ' ' + o.rate + '%' : ''}`;
155
+ g.fillText(fit(g, meta, `${o.metaSize || FONT_SIZE.meta}px ${FONT}`, w), o.x, o.y + h + 32);
156
+ }
157
+ }
158
+
159
+ /** 页脚 */
160
+ function footer(g, W, H, text) {
161
+ g.fillStyle = COLOR.textDim;
162
+ g.font = `${FONT_SIZE.tiny}px ${FONT}`;
163
+ g.fillText(String(text), SPACE.lg, H - 22);
164
+ }
165
+
166
+ module.exports = { roundRect, background, panel, header, card, footer, fit };
@@ -0,0 +1,77 @@
1
+ 'use strict';
2
+ /** 抽卡结果布局(统一渲染系统) */
3
+ const path = require('node:path');
4
+ const fs = require('node:fs');
5
+ const theme = require('./theme');
6
+ const kit = require('./kit');
7
+
8
+ /**
9
+ * @param {object} o
10
+ * @param {object} o.Canvas @napi-rs/canvas
11
+ * @param {Array} o.results [{hash|h, card, laser}]
12
+ * @param {string} o.title
13
+ * @param {string} o.outDir
14
+ * @param {string} o.tagsDir
15
+ * @param {string} o.thumbsDir
16
+ * @param {number} [o.thumbW]
17
+ */
18
+ async function render(o) {
19
+ const { Canvas, tagsDir, thumbsDir, outDir } = o;
20
+ const results = o.results || [];
21
+ const n = Math.max(1, results.length);
22
+ const perRow = n <= 3 ? n : Math.min(5, n);
23
+ const rows = Math.ceil(n / perRow);
24
+ const cw = n === 1 ? 300 : 170;
25
+ const ch = Math.round(cw * 1.43);
26
+ const label = theme.CARD.label;
27
+ const gap = theme.CARD.gap;
28
+ const pad = theme.SPACE.xl;
29
+ const head = 120;
30
+
31
+ const W = Math.max(560, pad * 2 + perRow * cw + (perRow - 1) * gap);
32
+ const H = head + rows * (ch + label + gap) + pad;
33
+
34
+ const canvas = Canvas.createCanvas(W, H);
35
+ const g = canvas.getContext('2d');
36
+ kit.background(g, W, H);
37
+ kit.header(g, pad - 16, 30, W, o.title, `共 ${n} 张 | 抽卡结果`);
38
+
39
+ const rowW = perRow * cw + (perRow - 1) * gap;
40
+ const x0 = Math.round((W - rowW) / 2);
41
+
42
+ for (let i = 0; i < results.length; i++) {
43
+ const r = results[i];
44
+ const hash = r.hash || r.h;
45
+ const col = i % perRow;
46
+ const row = Math.floor(i / perRow);
47
+ const x = x0 + col * (cw + gap);
48
+ const y = head + row * (ch + label + gap);
49
+
50
+ let img = null;
51
+ const f = hash ? path.join(thumbsDir, hash + '.jpg') : '';
52
+ if (f && fs.existsSync(f)) {
53
+ try { img = await Canvas.loadImage(f); } catch { img = null; }
54
+ }
55
+
56
+ await kit.card(g, Canvas, {
57
+ img, x, y, w: cw, h: ch,
58
+ name: (r.card && r.card.card_name) || '',
59
+ rate: r.card && r.card.rate_pct,
60
+ tier: theme.tierOf(r.card),
61
+ laser: Boolean(r.laser),
62
+ tagsDir,
63
+ owned: true,
64
+ nameSize: cw >= 260 ? 22 : 18,
65
+ metaSize: cw >= 260 ? 17 : 14,
66
+ });
67
+ }
68
+
69
+ kit.footer(g, W, H, 'VSinger 收藏集 · 抽卡结果');
70
+
71
+ fs.mkdirSync(outDir, { recursive: true });
72
+ const out = path.join(outDir, `draw_${Date.now()}.jpg`);
73
+ fs.writeFileSync(out, await canvas.encode('jpeg', 90));
74
+ return out;
75
+ }
76
+
77
+ module.exports = { render };
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+ /** 设计令牌:所有渲染图共用这一套 */
3
+ const FONT = 'VSingerSans, sans-serif';
4
+
5
+ const COLOR = {
6
+ bg0: '#0d1524',
7
+ bg1: '#16263f',
8
+ bg2: '#111c33',
9
+ glow: 'rgba(120,180,255,0.28)',
10
+ text: '#ffffff',
11
+ textSub: 'rgba(200,215,240,0.72)',
12
+ textDim: 'rgba(200,207,220,0.55)',
13
+ panel: 'rgba(16,24,40,0.72)',
14
+ panelBorder: 'rgba(255,255,255,0.08)',
15
+ accent: '#66ccff',
16
+ accentSoft: 'rgba(102,204,255,0.18)',
17
+ cardBg: '#1b2436',
18
+ cardBorder: 'rgba(255,255,255,0.10)',
19
+ };
20
+
21
+ const TIER = {
22
+ 40: { name: '大隐藏', color: '#ff6d84', tag: 'tag_40.webp' },
23
+ 30: { name: '小隐藏', color: '#ffb44d', tag: 'tag_30.webp' },
24
+ 20: { name: '稀有', color: '#8fc0ff', tag: null },
25
+ 10: { name: '普通', color: '#c3cad8', tag: null },
26
+ };
27
+
28
+ const RADIUS = { sm: 8, md: 12, lg: 16, xl: 22 };
29
+ const SPACE = { xs: 8, sm: 12, md: 16, lg: 24, xl: 32 };
30
+ const FONT_SIZE = { h1: 32, h2: 24, h3: 19, body: 16, meta: 14, tiny: 12 };
31
+
32
+ /** 卡片几何(图鉴与结果图共用) */
33
+ const CARD = { w: 170, h: 243, label: 58, gap: 16 };
34
+
35
+ function tierOf(card) {
36
+ const sc = Number((card && card.card_scarcity) || 10);
37
+ return sc === 40 ? 40 : sc === 30 ? 30 : sc === 20 ? 20 : 10;
38
+ }
39
+
40
+ module.exports = { FONT, COLOR, TIER, RADIUS, SPACE, FONT_SIZE, CARD, tierOf };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-vsinger-gacha",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "VSinger 收藏集抽卡 / 签到积分 / 图鉴(Koishi 插件,一次性安装)",
5
5
  "main": "lib/index.js",
6
6
  "files": [