koishi-plugin-vsinger-gacha 1.2.0 → 1.2.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/lib/handbook.js CHANGED
@@ -334,69 +334,107 @@ async function render({ game, store, uid, displayName, outDir, tagsDir, thumbsDi
334
334
  /**
335
335
  * 抽卡结果图(不依赖合并转发,任何平台都能发)
336
336
  * @param {object} opts
337
- * @param {Array} opts.results [{h, card, tier, laser}]
338
- * @param {string} opts.title 标题(如「zb 抽了 10 张(-100 分,剩 233)」)
337
+ * @param {Array} opts.results [{hash|h, card, tier, laser}]
338
+ * @param {string} opts.title
339
339
  */
340
340
  async function renderResult({ results, title, outDir, tagsDir, thumbsDir }) {
341
341
  const Canvas = getCanvas();
342
342
  const fs = require('node:fs');
343
343
  const path = require('node:path');
344
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;
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;
355
363
 
356
364
  const canvas = Canvas.createCanvas(W, H);
357
365
  const g = canvas.getContext('2d');
358
- const bg = g.createLinearGradient(0, 0, W, H);
359
- bg.addColorStop(0, '#101a2e');
360
- bg.addColorStop(1, '#16304f');
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');
361
373
  g.fillStyle = bg;
362
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);
363
380
 
381
+ // 标题
364
382
  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' };
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);
371
396
 
372
397
  for (let i = 0; i < results.length; i++) {
373
398
  const r = results[i];
399
+ const hash = r.hash || r.h;
374
400
  const col = i % PER_ROW;
375
401
  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)) {
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)) {
384
415
  try {
385
416
  const img = await Canvas.loadImage(thumb);
386
- g.drawImage(img, x, y, W_CARD, H_CARD);
417
+ g.drawImage(img, x, y, CW, CH);
387
418
  } catch { /* 忽略 */ }
388
419
  }
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
- // 角标
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
+ // 档位角标
394
432
  const tag = tier === 40 ? path.join(tagsDir, 'tag_40.webp') : tier === 30 ? path.join(tagsDir, 'tag_30.webp') : null;
395
433
  if (tag && fs.existsSync(tag)) {
396
434
  try {
397
435
  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);
436
+ const tw = CW * 0.46;
437
+ g.drawImage(ti, x + 4, y + 4, tw, (ti.height / ti.width) * tw);
400
438
  } catch { /* 忽略 */ }
401
439
  }
402
440
  if (r.laser) {
@@ -404,25 +442,29 @@ async function renderResult({ results, title, outDir, tagsDir, thumbsDir }) {
404
442
  if (fs.existsSync(holo)) {
405
443
  try {
406
444
  const hi = await Canvas.loadImage(holo);
407
- const hw = W_CARD * 0.36;
445
+ const hw = CW * 0.36;
408
446
  const hh = (hi.height / hi.width) * hw;
409
- g.drawImage(hi, x + (W_CARD - hw) / 2, y + H_CARD - hh - 4, hw, hh);
447
+ g.drawImage(hi, x + (CW - hw) / 2, y + CH - hh - 6, hw, hh);
410
448
  } catch { /* 忽略 */ }
411
449
  }
412
450
  }
413
451
 
452
+ // 名称 + 档位/概率
414
453
  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);
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);
418
459
  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);
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);
421
463
  }
422
464
 
423
465
  fs.mkdirSync(outDir, { recursive: true });
424
466
  const out = path.join(outDir, `draw_${Date.now()}.jpg`);
425
- fs.writeFileSync(out, await canvas.encode('jpeg', 88));
467
+ fs.writeFileSync(out, await canvas.encode('jpeg', 90));
426
468
  return out;
427
469
  }
428
470
 
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 {
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.2.1",
4
4
  "description": "VSinger 收藏集抽卡 / 签到积分 / 图鉴(Koishi 插件,一次性安装)",
5
5
  "main": "lib/index.js",
6
6
  "files": [