koishi-plugin-vsinger-gacha 1.2.0 → 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,99 +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 [{h, card, tier, laser}]
338
- * @param {string} opts.title 标题(如「zb 抽了 10 张(-100 分,剩 233)」)
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 W_CARD = TW;
346
- const H_CARD = TH;
347
- const LABEL = 44;
348
- const GAP = 12;
349
- const PAD = 24;
350
- const PER_ROW = Math.min(5, Math.max(1, results.length));
351
- const rows = Math.max(1, Math.ceil(results.length / PER_ROW));
352
- const W = PAD * 2 + PER_ROW * W_CARD + (PER_ROW - 1) * GAP;
353
- const HEAD = 92;
354
- const H = HEAD + rows * (H_CARD + LABEL + GAP) + PAD;
355
-
356
- const canvas = Canvas.createCanvas(W, H);
357
- const g = canvas.getContext('2d');
358
- const bg = g.createLinearGradient(0, 0, W, H);
359
- bg.addColorStop(0, '#101a2e');
360
- bg.addColorStop(1, '#16304f');
361
- g.fillStyle = bg;
362
- g.fillRect(0, 0, W, H);
363
-
364
- g.fillStyle = '#ffffff';
365
- g.font = 'bold 30px VSingerSans, sans-serif';
366
- g.textBaseline = 'top';
367
- g.fillText(String(title).slice(0, 40), PAD, 26);
368
-
369
- const TIER_NAME = { 40: '大隐藏', 30: '小隐藏', 20: '稀有', 10: '普通' };
370
- const TIER_COLOR = { 40: '#ff6078', 30: '#ffb050', 20: '#96beff', 10: '#bec4d2' };
371
-
372
- for (let i = 0; i < results.length; i++) {
373
- const r = results[i];
374
- const col = i % PER_ROW;
375
- const row = Math.floor(i / PER_ROW);
376
- const x = PAD + col * (W_CARD + GAP);
377
- const y = HEAD + row * (H_CARD + LABEL + GAP);
378
-
379
- g.fillStyle = 'rgba(0,0,0,0.45)';
380
- g.fillRect(x, y, W_CARD, H_CARD);
381
-
382
- const thumb = path.join(thumbsDir, r.h + '.jpg');
383
- if (fs.existsSync(thumb)) {
384
- try {
385
- const img = await Canvas.loadImage(thumb);
386
- g.drawImage(img, x, y, W_CARD, H_CARD);
387
- } catch { /* 忽略 */ }
388
- }
389
- const tier = Number(r.card && r.card.card_scarcity) === 40 ? 40
390
- : Number(r.card && r.card.card_scarcity) === 30 ? 30
391
- : Number(r.card && r.card.card_scarcity) === 20 ? 20 : 10;
392
-
393
- // 角标
394
- const tag = tier === 40 ? path.join(tagsDir, 'tag_40.webp') : tier === 30 ? path.join(tagsDir, 'tag_30.webp') : null;
395
- if (tag && fs.existsSync(tag)) {
396
- try {
397
- const ti = await Canvas.loadImage(tag);
398
- const tw = W_CARD * 0.46;
399
- g.drawImage(ti, x + 3, y + 3, tw, (ti.height / ti.width) * tw);
400
- } catch { /* 忽略 */ }
401
- }
402
- if (r.laser) {
403
- const holo = path.join(tagsDir, 'tag_holo.png');
404
- if (fs.existsSync(holo)) {
405
- try {
406
- const hi = await Canvas.loadImage(holo);
407
- const hw = W_CARD * 0.36;
408
- const hh = (hi.height / hi.width) * hw;
409
- g.drawImage(hi, x + (W_CARD - hw) / 2, y + H_CARD - hh - 4, hw, hh);
410
- } catch { /* 忽略 */ }
411
- }
412
- }
413
-
414
- g.fillStyle = '#ffffff';
415
- g.font = '16px VSingerSans, sans-serif';
416
- const name = String((r.card && r.card.card_name) || '').slice(0, 12);
417
- g.fillText(name, x, y + H_CARD + 6);
418
- g.fillStyle = TIER_COLOR[tier];
419
- g.font = '13px VSingerSans, sans-serif';
420
- g.fillText(`${TIER_NAME[tier]} ${r.card && r.card.rate_pct != null ? r.card.rate_pct + '%' : ''}`, x, y + H_CARD + 26);
421
- }
422
-
423
- fs.mkdirSync(outDir, { recursive: true });
424
- const out = path.join(outDir, `draw_${Date.now()}.jpg`);
425
- fs.writeFileSync(out, await canvas.encode('jpeg', 88));
426
- return out;
289
+ const { render } = require('./render/result');
290
+ return render({ Canvas, ...opts });
427
291
  }
428
292
 
429
293
  module.exports = { render, renderResult };
package/lib/index.js CHANGED
@@ -29,6 +29,8 @@ const Config = Schema.intersect([
29
29
  .description('普卡出镭射版的概率'),
30
30
  idMapFile: Schema.string().default('')
31
31
  .description('可选:openid ↔ QQ 映射表(如 /root/qqbot/data/id-map.json),用于统一用户标识'),
32
+ ignoreAtOthers: Schema.boolean().default(true)
33
+ .description('消息里 @ 的是别的机器人/别人时,本插件不响应(避免同群两个机器人重复回复)'),
32
34
  debug: Schema.boolean().default(false)
33
35
  .description('调试模式:把收到的每条消息打到日志里'),
34
36
  resultImage: Schema.union(['auto', 'always', 'off']).default('auto')
@@ -222,6 +224,20 @@ function apply(ctx, config) {
222
224
  logger.info('抽卡数据就绪:%d 张卡 / %d 个奖池(卡图齐全)', game.cards.length, game.pools.size);
223
225
  }
224
226
 
227
+ // ★ 同群多机器人时:消息 @ 的不是自己 → 不处理(避免重复回复)
228
+ if (config.ignoreAtOthers !== false) {
229
+ ctx.middleware((session, next) => {
230
+ try {
231
+ const els = (session.event && session.event.message && session.event.message.elements) || [];
232
+ const ats = els.filter((e) => e && e.type === 'at').map((e) => String(e.attrs && e.attrs.id || ''));
233
+ if (ats.length && !ats.includes('all') && !ats.includes(String(session.selfId || ''))) {
234
+ return; // 拦截:这条是给别的机器人的
235
+ }
236
+ } catch { /* 忽略 */ }
237
+ return next();
238
+ }, true);
239
+ }
240
+
225
241
  if (config.debug) {
226
242
  ctx.middleware((session, next) => {
227
243
  try {
@@ -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.0",
3
+ "version": "1.3.0",
4
4
  "description": "VSinger 收藏集抽卡 / 签到积分 / 图鉴(Koishi 插件,一次性安装)",
5
5
  "main": "lib/index.js",
6
6
  "files": [